feat: Implement video play count tracking, refactor user update API, add missing CMS delete functions, and update database transaction handling.
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
-- Migration: Add missing delete functions
|
||||
-- Adds fn_delete_module and fn_delete_lesson which were missing from the CRUD migration
|
||||
|
||||
CREATE OR REPLACE FUNCTION fn_delete_module(
|
||||
p_id UUID,
|
||||
p_organization_id UUID
|
||||
) RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
v_deleted_count INTEGER;
|
||||
BEGIN
|
||||
DELETE FROM modules
|
||||
WHERE id = p_id AND organization_id = p_organization_id;
|
||||
|
||||
GET DIAGNOSTICS v_deleted_count = ROW_COUNT;
|
||||
RETURN v_deleted_count > 0;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE FUNCTION fn_delete_lesson(
|
||||
p_id UUID,
|
||||
p_organization_id UUID
|
||||
) RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
v_deleted_count INTEGER;
|
||||
BEGIN
|
||||
DELETE FROM lessons
|
||||
WHERE id = p_id AND organization_id = p_organization_id;
|
||||
|
||||
GET DIAGNOSTICS v_deleted_count = ROW_COUNT;
|
||||
RETURN v_deleted_count > 0;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
@@ -1,8 +1,8 @@
|
||||
use sqlx::{Postgres, Transaction};
|
||||
use sqlx::{Postgres, PgConnection};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub async fn set_session_context(
|
||||
tx: &mut Transaction<'_, Postgres>,
|
||||
conn: &mut PgConnection,
|
||||
user_id: Option<Uuid>,
|
||||
org_id: Option<Uuid>,
|
||||
ip_address: Option<String>,
|
||||
@@ -12,31 +12,31 @@ pub async fn set_session_context(
|
||||
if let Some(uid) = user_id {
|
||||
let _ = sqlx::query("SELECT set_config('app.current_user_id', $1, true)")
|
||||
.bind(uid.to_string())
|
||||
.execute(&mut **tx)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
}
|
||||
if let Some(oid) = org_id {
|
||||
let _ = sqlx::query("SELECT set_config('app.current_org_id', $1, true)")
|
||||
.bind(oid.to_string())
|
||||
.execute(&mut **tx)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
}
|
||||
if let Some(ip) = ip_address {
|
||||
let _ = sqlx::query("SELECT set_config('app.client_ip', $1, true)")
|
||||
.bind(ip)
|
||||
.execute(&mut **tx)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
}
|
||||
if let Some(ua) = user_agent {
|
||||
let _ = sqlx::query("SELECT set_config('app.user_agent', $1, true)")
|
||||
.bind(ua)
|
||||
.execute(&mut **tx)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
}
|
||||
if let Some(et) = event_type {
|
||||
let _ = sqlx::query("SELECT set_config('app.event_type', $1, true)")
|
||||
.bind(et)
|
||||
.execute(&mut **tx)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -199,7 +199,7 @@ pub async fn create_course(
|
||||
.map(|s| s.to_string());
|
||||
|
||||
crate::db_util::set_session_context(
|
||||
&mut tx,
|
||||
&mut *tx,
|
||||
Some(instructor_id),
|
||||
Some(org_ctx.id),
|
||||
ip,
|
||||
@@ -400,7 +400,7 @@ pub async fn create_module(
|
||||
.map(|s| s.to_string());
|
||||
|
||||
crate::db_util::set_session_context(
|
||||
&mut tx,
|
||||
&mut *tx,
|
||||
Some(claims.sub),
|
||||
Some(org_ctx.id),
|
||||
ip,
|
||||
@@ -498,7 +498,7 @@ pub async fn create_lesson(
|
||||
.map(|s| s.to_string());
|
||||
|
||||
crate::db_util::set_session_context(
|
||||
&mut tx,
|
||||
&mut *tx,
|
||||
Some(claims.sub),
|
||||
Some(org_ctx.id),
|
||||
ip,
|
||||
@@ -1008,7 +1008,7 @@ pub async fn update_lesson(
|
||||
.map(|s| s.to_string());
|
||||
|
||||
crate::db_util::set_session_context(
|
||||
&mut tx,
|
||||
&mut *tx,
|
||||
Some(claims.sub),
|
||||
Some(org_ctx.id),
|
||||
ip,
|
||||
@@ -1231,7 +1231,7 @@ pub async fn reorder_modules(
|
||||
.map(|s| s.to_string());
|
||||
|
||||
crate::db_util::set_session_context(
|
||||
&mut tx,
|
||||
&mut *tx,
|
||||
Some(claims.sub),
|
||||
Some(org_ctx.id),
|
||||
ip,
|
||||
@@ -1282,7 +1282,7 @@ pub async fn reorder_lessons(
|
||||
.map(|s| s.to_string());
|
||||
|
||||
crate::db_util::set_session_context(
|
||||
&mut tx,
|
||||
&mut *tx,
|
||||
Some(claims.sub),
|
||||
Some(org_ctx.id),
|
||||
ip,
|
||||
@@ -1759,7 +1759,7 @@ pub async fn update_module(
|
||||
.map(|s| s.to_string());
|
||||
|
||||
crate::db_util::set_session_context(
|
||||
&mut tx,
|
||||
&mut *tx,
|
||||
Some(claims.sub),
|
||||
Some(org_ctx.id),
|
||||
ip,
|
||||
@@ -1801,8 +1801,8 @@ pub async fn delete_module(
|
||||
headers: axum::http::HeaderMap,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
let mut tx = pool
|
||||
.begin()
|
||||
let mut conn = pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
@@ -1818,7 +1818,7 @@ pub async fn delete_module(
|
||||
.map(|s| s.to_string());
|
||||
|
||||
crate::db_util::set_session_context(
|
||||
&mut tx,
|
||||
&mut conn,
|
||||
Some(claims.sub),
|
||||
Some(org_ctx.id),
|
||||
ip,
|
||||
@@ -1831,7 +1831,7 @@ pub async fn delete_module(
|
||||
let success = sqlx::query_scalar::<_, bool>("SELECT fn_delete_module($1, $2)")
|
||||
.bind(id)
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&mut *tx)
|
||||
.fetch_one(&mut *conn)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Delete module failed: {}", e);
|
||||
@@ -1842,10 +1842,6 @@ pub async fn delete_module(
|
||||
return Err(StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
tx.commit()
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
@@ -1860,8 +1856,8 @@ pub async fn delete_lesson(
|
||||
return Err(StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
let mut tx = pool
|
||||
.begin()
|
||||
let mut conn = pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
@@ -1877,7 +1873,7 @@ pub async fn delete_lesson(
|
||||
.map(|s| s.to_string());
|
||||
|
||||
crate::db_util::set_session_context(
|
||||
&mut tx,
|
||||
&mut conn,
|
||||
Some(claims.sub),
|
||||
Some(org_ctx.id),
|
||||
ip,
|
||||
@@ -1890,7 +1886,7 @@ pub async fn delete_lesson(
|
||||
let success = sqlx::query_scalar::<_, bool>("SELECT fn_delete_lesson($1, $2)")
|
||||
.bind(id)
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&mut *tx)
|
||||
.fetch_one(&mut *conn)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Delete lesson failed: {}", e);
|
||||
@@ -1901,10 +1897,6 @@ pub async fn delete_lesson(
|
||||
return Err(StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
tx.commit()
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
-- Migration: Fix Grading Logic for Multi-tenancy
|
||||
-- Corrects fn_upsert_user_grade to properly handle organization_id in user_badges
|
||||
|
||||
CREATE OR REPLACE FUNCTION fn_upsert_user_grade(
|
||||
p_organization_id UUID,
|
||||
p_user_id UUID,
|
||||
p_course_id UUID,
|
||||
p_lesson_id UUID,
|
||||
p_score FLOAT4,
|
||||
p_metadata JSONB DEFAULT NULL
|
||||
) RETURNS SETOF user_grades AS $$
|
||||
DECLARE
|
||||
v_grade user_grades;
|
||||
v_xp_amount INTEGER := 20; -- Default XP for completion
|
||||
v_badge_id UUID;
|
||||
BEGIN
|
||||
-- 1. Upsert grade
|
||||
INSERT INTO user_grades (organization_id, user_id, course_id, lesson_id, score, metadata, attempts_count)
|
||||
VALUES (p_organization_id, p_user_id, p_course_id, p_lesson_id, p_score, p_metadata, 1)
|
||||
ON CONFLICT (user_id, lesson_id) DO UPDATE SET
|
||||
score = EXCLUDED.score,
|
||||
metadata = EXCLUDED.metadata,
|
||||
attempts_count = user_grades.attempts_count + 1,
|
||||
updated_at = NOW()
|
||||
RETURNING * INTO v_grade;
|
||||
|
||||
-- 2. Award XP automatically
|
||||
PERFORM fn_award_xp(p_user_id, p_organization_id, v_xp_amount, 'lesson_completion', 'lesson', p_lesson_id);
|
||||
|
||||
-- 3. Check for new badges
|
||||
FOR v_badge_id IN
|
||||
SELECT id FROM badges
|
||||
WHERE organization_id = p_organization_id
|
||||
AND requirement_type = 'points'
|
||||
AND requirement_value <= (SELECT xp FROM users WHERE id = p_user_id)
|
||||
AND id NOT IN (SELECT badge_id FROM user_badges WHERE user_id = p_user_id AND organization_id = p_organization_id)
|
||||
LOOP
|
||||
INSERT INTO user_badges (user_id, badge_id, organization_id)
|
||||
VALUES (p_user_id, v_badge_id, p_organization_id)
|
||||
ON CONFLICT (user_id, badge_id) DO NOTHING;
|
||||
END LOOP;
|
||||
|
||||
RETURN NEXT v_grade;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
Reference in New Issue
Block a user