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
171pub 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}