Skip to main content

headless_lms_models/
exercise_slides.rs

1use std::collections::HashMap;
2
3use futures::future::BoxFuture;
4use url::Url;
5use utoipa::ToSchema;
6
7use crate::{
8    exercise_service_info::ExerciseServiceInfoApi,
9    exercise_tasks::{self, CourseMaterialExerciseTask},
10    prelude::*,
11};
12
13pub struct NewExerciseSlide {
14    exercise_id: Uuid,
15    order_number: i32,
16}
17
18#[derive(Debug, Serialize, Deserialize, FromRow, PartialEq, Clone)]
19
20pub struct ExerciseSlide {
21    pub id: Uuid,
22    pub created_at: DateTime<Utc>,
23    pub updated_at: DateTime<Utc>,
24    pub deleted_at: Option<DateTime<Utc>>,
25    pub exercise_id: Uuid,
26    pub order_number: i32,
27}
28
29#[derive(Debug, Serialize, Deserialize, ToSchema)]
30
31pub struct CourseMaterialExerciseSlide {
32    pub id: Uuid,
33    pub exercise_tasks: Vec<CourseMaterialExerciseTask>,
34}
35
36pub async fn insert(
37    conn: &mut PgConnection,
38    pkey_policy: PKeyPolicy<Uuid>,
39    exercise_id: Uuid,
40    order_number: i32,
41) -> ModelResult<Uuid> {
42    let res = sqlx::query!(
43        "
44INSERT INTO exercise_slides (id, exercise_id, order_number)
45VALUES ($1, $2, $3)
46RETURNING id
47        ",
48        pkey_policy.into_uuid(),
49        exercise_id,
50        order_number
51    )
52    .fetch_one(conn)
53    .await?;
54    Ok(res.id)
55}
56
57pub async fn insert_exercise_slide(
58    conn: &mut PgConnection,
59    new_slide: NewExerciseSlide,
60) -> ModelResult<ExerciseSlide> {
61    let res = sqlx::query_as!(
62        ExerciseSlide,
63        "
64INSERT INTO exercise_slides (exercise_id, order_number)
65VALUES ($1, $2)
66RETURNING *;
67    ",
68        new_slide.exercise_id,
69        new_slide.order_number
70    )
71    .fetch_one(conn)
72    .await?;
73    Ok(res)
74}
75
76pub async fn upsert(
77    conn: &mut PgConnection,
78    id: Uuid,
79    exercise_id: Uuid,
80    order_number: i32,
81) -> ModelResult<Uuid> {
82    let res = sqlx::query!(
83        "
84INSERT INTO exercise_slides (id, exercise_id, order_number)
85VALUES ($1, $2, $3) ON CONFLICT (id) DO
86UPDATE
87SET exercise_id = $2,
88    order_number = $3,
89    deleted_at = NULL
90RETURNING id;
91    ",
92        id,
93        exercise_id,
94        order_number
95    )
96    .fetch_one(conn)
97    .await?;
98    Ok(res.id)
99}
100
101pub async fn get_exercise_slides(conn: &mut PgConnection) -> ModelResult<Vec<ExerciseSlide>> {
102    let res = sqlx::query_as!(
103        ExerciseSlide,
104        "
105SELECT *
106FROM exercise_slides
107WHERE deleted_at IS NULL;
108    "
109    )
110    .fetch_all(conn)
111    .await?;
112    Ok(res)
113}
114
115pub async fn get_exercise_slides_by_exercise_ids(
116    conn: &mut PgConnection,
117    exercise_ids: &[Uuid],
118) -> ModelResult<Vec<ExerciseSlide>> {
119    let res = sqlx::query_as!(
120        ExerciseSlide,
121        "
122SELECT *
123FROM exercise_slides
124WHERE exercise_id = ANY($1)
125  AND deleted_at IS NULL;
126        ",
127        exercise_ids,
128    )
129    .fetch_all(conn)
130    .await?;
131    Ok(res)
132}
133
134pub async fn get_exercise_slide(conn: &mut PgConnection, id: Uuid) -> ModelResult<ExerciseSlide> {
135    let res = sqlx::query_as!(
136        ExerciseSlide,
137        "
138SELECT *
139FROM exercise_slides
140WHERE id = $1
141  AND deleted_at IS NULL;
142    ",
143        id
144    )
145    .fetch_one(conn)
146    .await?;
147    Ok(res)
148}
149
150pub async fn get_random_exercise_slide_for_exercise(
151    conn: &mut PgConnection,
152    exercise_id: Uuid,
153) -> ModelResult<ExerciseSlide> {
154    let res = sqlx::query_as!(
155        ExerciseSlide,
156        "
157SELECT *
158FROM exercise_slides
159WHERE exercise_id = $1
160  AND deleted_at IS NULL
161ORDER BY random()
162LIMIT 1;
163        ",
164        exercise_id
165    )
166    .fetch_one(conn)
167    .await?;
168    Ok(res)
169}
170
171/// The slide of each exercise that the course site shows the user: the one already selected for
172/// them in `course_id`, otherwise a random one, as for anonymous visitors. Read-only: unlike
173/// [`crate::exercises::get_or_select_exercise_slide`], a random pick isn't stored, so it can differ
174/// between calls. Exercises without slides are left out.
175pub async fn get_shown_slide_ids_for_exercises(
176    conn: &mut PgConnection,
177    user_id: Option<Uuid>,
178    course_id: Uuid,
179    exercise_ids: &[Uuid],
180) -> ModelResult<HashMap<Uuid, Uuid>> {
181    let rows = sqlx::query!(
182        "
183SELECT DISTINCT ON (s.exercise_id) s.exercise_id,
184  s.id
185FROM exercise_slides s
186  LEFT JOIN user_exercise_states ues ON ues.exercise_id = s.exercise_id
187  AND ues.user_id = $1
188  AND ues.course_id = $2
189  AND ues.deleted_at IS NULL
190WHERE s.exercise_id = ANY($3)
191  AND s.deleted_at IS NULL
192ORDER BY s.exercise_id,
193  s.id = ues.selected_exercise_slide_id DESC NULLS LAST,
194  random()
195        ",
196        user_id,
197        course_id,
198        exercise_ids,
199    )
200    .fetch_all(conn)
201    .await?;
202    Ok(rows
203        .into_iter()
204        .map(|row| (row.exercise_id, row.id))
205        .collect())
206}
207
208pub async fn get_exercise_slide_by_exercise_task_id(
209    conn: &mut PgConnection,
210    exercise_task_id: Uuid,
211) -> ModelResult<Option<ExerciseSlide>> {
212    let res = sqlx::query_as!(
213        ExerciseSlide,
214        "
215SELECT s.*
216FROM exercise_slides s
217  JOIN exercise_tasks t ON (s.id = t.exercise_slide_id)
218WHERE t.id = $1
219  AND t.deleted_at IS NULL
220  AND s.deleted_at IS NULL;
221    ",
222        exercise_task_id,
223    )
224    .fetch_optional(conn)
225    .await?;
226    Ok(res)
227}
228
229pub async fn get_exercise_slides_by_exercise_id(
230    conn: &mut PgConnection,
231    exercise_id: Uuid,
232) -> ModelResult<Vec<ExerciseSlide>> {
233    let res = sqlx::query_as!(
234        ExerciseSlide,
235        "
236SELECT *
237FROM exercise_slides
238WHERE exercise_id = $1
239  AND deleted_at IS NULL;
240    ",
241        exercise_id
242    )
243    .fetch_all(conn)
244    .await?;
245    Ok(res)
246}
247
248pub async fn get_course_material_exercise_slide_by_id(
249    conn: &mut PgConnection,
250    id: Uuid,
251    user_id: Option<Uuid>,
252    fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
253    file_store: &dyn FileStore,
254    app_conf: &ApplicationConfiguration,
255) -> ModelResult<CourseMaterialExerciseSlide> {
256    let exercise_tasks = exercise_tasks::get_course_material_exercise_tasks(
257        conn,
258        id,
259        user_id,
260        fetch_service_info,
261        file_store,
262        app_conf,
263    )
264    .await?;
265    Ok(CourseMaterialExerciseSlide { id, exercise_tasks })
266}
267
268pub async fn delete_exercise_slides_by_exercise_ids(
269    conn: &mut PgConnection,
270    exercise_ids: &[Uuid],
271) -> ModelResult<Vec<Uuid>> {
272    let deleted_ids = sqlx::query!(
273        "
274UPDATE exercise_slides
275SET deleted_at = now()
276WHERE exercise_id = ANY($1)
277AND deleted_at IS NULL
278RETURNING id;
279        ",
280        exercise_ids,
281    )
282    .fetch_all(conn)
283    .await?
284    .into_iter()
285    .map(|x| x.id)
286    .collect();
287    Ok(deleted_ids)
288}