1use std::collections::HashMap;
2
3use crate::{
4 chapters,
5 chapters::DatabaseChapter,
6 exercises,
7 prelude::*,
8 user_details::UserDetail,
9 users::{self, User},
10};
11
12#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
13#[cfg_attr(feature = "ts_rs", derive(TS))]
14pub struct CourseInstance {
15 pub id: Uuid,
16 pub created_at: DateTime<Utc>,
17 pub updated_at: DateTime<Utc>,
18 pub deleted_at: Option<DateTime<Utc>>,
19 pub course_id: Uuid,
20 pub starts_at: Option<DateTime<Utc>>,
21 pub ends_at: Option<DateTime<Utc>>,
22 pub name: Option<String>,
23 pub description: Option<String>,
24 pub teacher_in_charge_name: String,
25 pub teacher_in_charge_email: String,
26 pub support_email: Option<String>,
27}
28
29impl CourseInstance {
30 pub fn is_open(&self) -> bool {
31 self.starts_at.map(|sa| sa < Utc::now()).unwrap_or_default()
32 }
33}
34
35#[derive(Debug, Deserialize)]
36#[cfg_attr(feature = "ts_rs", derive(TS))]
37pub struct CourseInstanceForm {
38 pub name: Option<String>,
39 pub description: Option<String>,
40 pub teacher_in_charge_name: String,
41 pub teacher_in_charge_email: String,
42 pub support_email: Option<String>,
43 pub opening_time: Option<DateTime<Utc>>,
44 pub closing_time: Option<DateTime<Utc>>,
45}
46
47#[derive(Debug, Clone, Copy)]
48pub struct NewCourseInstance<'a> {
49 pub course_id: Uuid,
50 pub name: Option<&'a str>,
51 pub description: Option<&'a str>,
52 pub teacher_in_charge_name: &'a str,
53 pub teacher_in_charge_email: &'a str,
54 pub support_email: Option<&'a str>,
55 pub opening_time: Option<DateTime<Utc>>,
56 pub closing_time: Option<DateTime<Utc>>,
57}
58
59pub async fn insert(
60 conn: &mut PgConnection,
61 pkey_policy: PKeyPolicy<Uuid>,
62 new_course_instance: NewCourseInstance<'_>,
63) -> ModelResult<CourseInstance> {
64 let course_instance = sqlx::query_as!(
65 CourseInstance,
66 r#"
67INSERT INTO course_instances (
68 id,
69 course_id,
70 name,
71 description,
72 teacher_in_charge_name,
73 teacher_in_charge_email,
74 support_email
75 )
76VALUES ($1, $2, $3, $4, $5, $6, $7)
77RETURNING id,
78 created_at,
79 updated_at,
80 deleted_at,
81 course_id,
82 starts_at,
83 ends_at,
84 name,
85 description,
86 teacher_in_charge_name,
87 teacher_in_charge_email,
88 support_email
89"#,
90 pkey_policy.into_uuid(),
91 new_course_instance.course_id,
92 new_course_instance.name,
93 new_course_instance.description,
94 new_course_instance.teacher_in_charge_name,
95 new_course_instance.teacher_in_charge_email,
96 new_course_instance.support_email,
97 )
98 .fetch_one(conn)
99 .await?;
100 Ok(course_instance)
101}
102
103pub async fn get_course_instance(
104 conn: &mut PgConnection,
105 course_instance_id: Uuid,
106) -> ModelResult<CourseInstance> {
107 let course_instance = sqlx::query_as!(
108 CourseInstance,
109 r#"
110SELECT id,
111 created_at,
112 updated_at,
113 deleted_at,
114 course_id,
115 starts_at,
116 ends_at,
117 name,
118 description,
119 teacher_in_charge_name,
120 teacher_in_charge_email,
121 support_email
122FROM course_instances
123WHERE id = $1
124 AND deleted_at IS NULL;
125 "#,
126 course_instance_id,
127 )
128 .fetch_one(conn)
129 .await?;
130 Ok(course_instance)
131}
132
133pub async fn get_course_instance_with_info(
134 conn: &mut PgConnection,
135 course_instance_id: Uuid,
136) -> ModelResult<CourseInstanceWithCourseInfo> {
137 let course_instance = sqlx::query_as!(
138 CourseInstanceWithCourseInfo,
139 r#"
140SELECT
141 c.id AS course_id,
142 c.slug AS course_slug,
143 c.name AS course_name,
144 c.description AS course_description,
145 ci.id AS course_instance_id,
146 ci.name AS course_instance_name,
147 ci.description AS course_instance_description,
148 o.name AS organization_name
149FROM course_instances AS ci
150 LEFT JOIN courses AS c ON ci.course_id = c.id
151 LEFT JOIN organizations AS o ON o.id = c.organization_id
152WHERE ci.id = $1
153 AND ci.deleted_at IS NULL
154 AND c.deleted_at IS NULL
155 AND o.deleted_at IS NULL
156 "#,
157 course_instance_id,
158 )
159 .fetch_one(conn)
160 .await?;
161 Ok(course_instance)
162}
163
164pub async fn get_default_by_course_id(
165 conn: &mut PgConnection,
166 course_id: Uuid,
167) -> ModelResult<CourseInstance> {
168 let res = sqlx::query_as!(
169 CourseInstance,
170 "
171SELECT *
172FROM course_instances
173WHERE course_id = $1
174 AND name IS NULL
175 AND deleted_at IS NULL
176 ",
177 course_id
178 )
179 .fetch_one(conn)
180 .await?;
181 Ok(res)
182}
183
184pub async fn get_organization_id(
185 conn: &mut PgConnection,
186 course_instance_id: Uuid,
187) -> ModelResult<Uuid> {
188 let res = sqlx::query!(
189 "
190SELECT courses.organization_id
191FROM course_instances
192 JOIN courses ON courses.id = course_instances.course_id
193WHERE course_instances.id = $1
194 AND course_instances.deleted_at IS NULL
195 AND courses.deleted_at IS NULL
196",
197 course_instance_id
198 )
199 .fetch_one(conn)
200 .await?;
201 Ok(res.organization_id)
202}
203
204pub async fn current_course_instance_of_user(
205 conn: &mut PgConnection,
206 user_id: Uuid,
207 course_id: Uuid,
208) -> ModelResult<Option<CourseInstance>> {
209 let course_instance_enrollment = sqlx::query_as!(
210 CourseInstance,
211 r#"
212SELECT i.id,
213 i.created_at,
214 i.updated_at,
215 i.deleted_at,
216 i.course_id,
217 i.starts_at,
218 i.ends_at,
219 i.name,
220 i.description,
221 i.teacher_in_charge_name,
222 i.teacher_in_charge_email,
223 i.support_email
224FROM user_course_settings ucs
225 JOIN course_instances i ON (ucs.current_course_instance_id = i.id)
226WHERE ucs.user_id = $1
227 AND ucs.current_course_id = $2
228 AND ucs.deleted_at IS NULL
229 AND i.deleted_at IS NULL;
230 "#,
231 user_id,
232 course_id,
233 )
234 .fetch_optional(conn)
235 .await?;
236 Ok(course_instance_enrollment)
237}
238
239pub async fn course_instance_by_users_latest_enrollment(
240 conn: &mut PgConnection,
241 user_id: Uuid,
242 course_id: Uuid,
243) -> ModelResult<Option<CourseInstance>> {
244 let course_instance = sqlx::query_as!(
245 CourseInstance,
246 r#"
247SELECT i.id,
248 i.created_at,
249 i.updated_at,
250 i.deleted_at,
251 i.course_id,
252 i.starts_at,
253 i.ends_at,
254 i.name,
255 i.description,
256 i.teacher_in_charge_name,
257 i.teacher_in_charge_email,
258 i.support_email
259FROM course_instances i
260 JOIN course_instance_enrollments ie ON (i.id = ie.course_id)
261WHERE i.course_id = $1
262 AND i.deleted_at IS NULL
263 AND ie.user_id = $2
264 AND ie.deleted_at IS NULL
265ORDER BY ie.created_at DESC;
266 "#,
267 course_id,
268 user_id,
269 )
270 .fetch_optional(conn)
271 .await?;
272 Ok(course_instance)
273}
274
275pub async fn get_all_course_instances(conn: &mut PgConnection) -> ModelResult<Vec<CourseInstance>> {
276 let course_instances = sqlx::query_as!(
277 CourseInstance,
278 r#"
279SELECT id,
280 created_at,
281 updated_at,
282 deleted_at,
283 course_id,
284 starts_at,
285 ends_at,
286 name,
287 description,
288 teacher_in_charge_name,
289 teacher_in_charge_email,
290 support_email
291FROM course_instances
292WHERE deleted_at IS NULL
293"#
294 )
295 .fetch_all(conn)
296 .await?;
297 Ok(course_instances)
298}
299
300pub async fn get_course_instances_for_course(
301 conn: &mut PgConnection,
302 course_id: Uuid,
303) -> ModelResult<Vec<CourseInstance>> {
304 let course_instances = sqlx::query_as!(
305 CourseInstance,
306 r#"
307SELECT id,
308 created_at,
309 updated_at,
310 deleted_at,
311 course_id,
312 starts_at,
313 ends_at,
314 name,
315 description,
316 teacher_in_charge_name,
317 teacher_in_charge_email,
318 support_email
319FROM course_instances
320WHERE course_id = $1
321 AND deleted_at IS NULL;
322 "#,
323 course_id,
324 )
325 .fetch_all(conn)
326 .await?;
327 Ok(course_instances)
328}
329
330pub async fn get_course_instance_ids_with_course_id(
331 conn: &mut PgConnection,
332 course_id: Uuid,
333) -> ModelResult<Vec<Uuid>> {
334 let res = sqlx::query!(
335 r#"
336SELECT id
337FROM course_instances
338WHERE course_id = $1
339 AND deleted_at IS NULL;
340 "#,
341 course_id,
342 )
343 .map(|r| r.id)
344 .fetch_all(conn)
345 .await?;
346 Ok(res)
347}
348
349#[derive(Debug, Serialize)]
350#[cfg_attr(feature = "ts_rs", derive(TS))]
351pub struct ChapterScore {
352 #[serde(flatten)]
353 pub chapter: DatabaseChapter,
354 pub score_given: f32,
355 pub score_total: i32,
356}
357
358#[derive(Debug, Default, Serialize)]
359#[cfg_attr(feature = "ts_rs", derive(TS))]
360pub struct PointMap(pub HashMap<Uuid, f32>);
361
362#[derive(Debug, Serialize)]
363#[cfg_attr(feature = "ts_rs", derive(TS))]
364pub struct Points {
365 pub chapter_points: Vec<ChapterScore>,
366 pub users: Vec<UserDetail>,
367 pub user_chapter_points: HashMap<Uuid, PointMap>,
369}
370
371pub async fn get_points(
372 conn: &mut PgConnection,
373 instance_id: Uuid,
374 _pagination: Pagination, ) -> ModelResult<Points> {
376 let mut chapter_point_totals = HashMap::<Uuid, i32>::new();
377 let mut exercise_to_chapter_id = HashMap::new();
378 let course_instance = crate::course_instances::get_course_instance(conn, instance_id).await?;
379 let exercises =
380 exercises::get_exercises_by_course_id(&mut *conn, course_instance.course_id).await?;
381 for exercise in exercises {
382 if let Some(chapter_id) = exercise.chapter_id {
383 let total = chapter_point_totals.entry(chapter_id).or_default();
385 *total += exercise.score_maximum;
386 exercise_to_chapter_id.insert(exercise.id, chapter_id);
387 }
388 }
389
390 let users: HashMap<Uuid, User> =
391 users::get_users_by_course_instance_enrollment(conn, instance_id)
392 .await?
393 .into_iter()
394 .map(|u| (u.id, u))
395 .collect();
396 let mut chapter_points_given = HashMap::<Uuid, f32>::new();
397 let states = sqlx::query!(
398 "
399SELECT user_id,
400 exercise_id,
401 score_given
402FROM user_exercise_states
403WHERE course_id = $1
404 AND deleted_at IS NULL
405ORDER BY user_id ASC
406",
407 course_instance.course_id,
408 )
409 .fetch_all(&mut *conn)
410 .await?;
411 let mut user_chapter_points = HashMap::<Uuid, PointMap>::new();
412 for state in states {
413 let user = match users.get(&state.user_id) {
414 Some(user) => user,
415 None => {
416 tracing::warn!(
417 "user {} has an exercise state but no enrollment",
418 state.user_id
419 );
420 continue;
421 }
422 };
423 if let Some(chapter_id) = exercise_to_chapter_id.get(&state.exercise_id).copied() {
424 let chapter_points = user_chapter_points.entry(user.id).or_default();
425 let user_given = chapter_points.0.entry(chapter_id).or_default();
426 let chapter_given = chapter_points_given.entry(chapter_id).or_default();
427 let score_given = state.score_given.unwrap_or_default();
428 *user_given += score_given;
429 *chapter_given += score_given;
430 }
431 }
432
433 let chapters = chapters::course_instance_chapters(&mut *conn, instance_id).await?;
434 let mut chapter_points: Vec<ChapterScore> = chapters
435 .into_iter()
436 .map(|c| ChapterScore {
437 score_given: chapter_points_given.get(&c.id).copied().unwrap_or_default(),
438 score_total: chapter_point_totals.get(&c.id).copied().unwrap_or_default(),
439 chapter: c,
440 })
441 .collect();
442 chapter_points.sort_by_key(|c| c.chapter.chapter_number);
443
444 let list_of_users = users.into_values().collect::<Vec<_>>();
445 let user_id_to_details =
446 crate::user_details::get_users_details_by_user_id_map(&mut *conn, &list_of_users).await?;
447
448 Ok(Points {
449 chapter_points,
450 users: list_of_users
451 .into_iter()
452 .filter_map(|user| user_id_to_details.get(&user.id).cloned())
453 .collect::<Vec<_>>(),
454 user_chapter_points,
455 })
456}
457
458pub async fn edit(
459 conn: &mut PgConnection,
460 instance_id: Uuid,
461 update: CourseInstanceForm,
462) -> ModelResult<()> {
463 sqlx::query!(
464 "
465UPDATE course_instances
466SET name = $1,
467 description = $2,
468 teacher_in_charge_name = $3,
469 teacher_in_charge_email = $4,
470 support_email = $5,
471 starts_at = $6,
472 ends_at = $7
473WHERE id = $8
474 AND deleted_at IS NULL
475",
476 update.name,
477 update.description,
478 update.teacher_in_charge_name,
479 update.teacher_in_charge_email,
480 update.support_email,
481 update.opening_time,
482 update.closing_time,
483 instance_id
484 )
485 .execute(conn)
486 .await?;
487 Ok(())
488}
489
490pub async fn delete(conn: &mut PgConnection, id: Uuid) -> ModelResult<()> {
491 sqlx::query!(
492 "
493UPDATE course_instances
494SET deleted_at = now()
495WHERE id = $1
496AND deleted_at IS NULL
497",
498 id
499 )
500 .execute(conn)
501 .await?;
502 Ok(())
503}
504
505pub async fn get_course_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<Uuid> {
506 let res = sqlx::query!(
507 "
508SELECT course_id
509FROM course_instances
510WHERE id = $1
511 AND deleted_at IS NULL
512",
513 id
514 )
515 .fetch_one(conn)
516 .await?;
517 Ok(res.course_id)
518}
519
520pub async fn is_open(conn: &mut PgConnection, id: Uuid) -> ModelResult<bool> {
521 let res = sqlx::query!(
522 "
523SELECT starts_at,
524 ends_at
525FROM course_instances
526WHERE id = $1
527 AND deleted_at IS NULL
528",
529 id
530 )
531 .fetch_one(conn)
532 .await?;
533 let has_started = match res.starts_at {
534 Some(starts_at) => starts_at <= Utc::now(),
535 None => true,
536 };
537 let has_ended = match res.ends_at {
538 Some(ends_at) => ends_at <= Utc::now(),
539 None => false,
540 };
541 let is_open = has_started && !has_ended;
542 Ok(is_open)
543}
544
545pub async fn get_by_ids(
546 conn: &mut PgConnection,
547 course_instance_ids: &[Uuid],
548) -> ModelResult<Vec<CourseInstance>> {
549 let course_instances = sqlx::query_as!(
550 CourseInstance,
551 r#"
552SELECT *
553FROM course_instances
554WHERE id IN (SELECT * FROM UNNEST($1::uuid[]))
555 AND deleted_at IS NULL
556 "#,
557 course_instance_ids
558 )
559 .fetch_all(conn)
560 .await?;
561 Ok(course_instances)
562}
563
564pub struct CourseInstanceWithCourseInfo {
565 pub course_id: Uuid,
566 pub course_slug: String,
567 pub course_name: String,
568 pub course_description: Option<String>,
569 pub course_instance_id: Uuid,
570 pub course_instance_name: Option<String>,
571 pub course_instance_description: Option<String>,
572 pub organization_name: String,
573}
574
575pub async fn get_enrolled_course_instances_for_user(
576 conn: &mut PgConnection,
577 user_id: Uuid,
578) -> ModelResult<Vec<CourseInstanceWithCourseInfo>> {
579 let course_instances = sqlx::query_as!(
580 CourseInstanceWithCourseInfo,
581 r#"
582SELECT
583 c.id AS course_id,
584 c.slug AS course_slug,
585 c.name AS course_name,
586 c.description AS course_description,
587 ci.id AS course_instance_id,
588 ci.name AS course_instance_name,
589 ci.description AS course_instance_description,
590 o.name AS organization_name
591FROM course_instances AS ci
592 JOIN course_instance_enrollments AS cie ON ci.id = cie.course_instance_id
593 LEFT JOIN courses AS c ON ci.course_id = c.id
594 LEFT JOIN organizations AS o ON o.id = c.organization_id
595WHERE cie.user_id = $1
596 AND ci.deleted_at IS NULL
597 AND cie.deleted_at IS NULL
598 AND c.deleted_at IS NULL
599 AND o.deleted_at IS NULL
600"#,
601 user_id
602 )
603 .fetch_all(conn)
604 .await?;
605 Ok(course_instances)
606}
607
608pub async fn get_enrolled_course_instances_for_user_with_exercise_type(
609 conn: &mut PgConnection,
610 user_id: Uuid,
611 exercise_type: &str,
612) -> ModelResult<Vec<CourseInstanceWithCourseInfo>> {
613 let course_instances = sqlx::query_as!(
614 CourseInstanceWithCourseInfo,
615 r#"
616SELECT DISTINCT ON (ci.id)
617 c.id AS course_id,
618 c.slug AS course_slug,
619 c.name AS course_name,
620 c.description AS course_description,
621 ci.id AS course_instance_id,
622 ci.name AS course_instance_name,
623 ci.description AS course_instance_description,
624 o.name AS organization_name
625FROM course_instances AS ci
626 JOIN course_instance_enrollments AS cie ON ci.id = cie.course_instance_id
627 LEFT JOIN courses AS c ON ci.course_id = c.id
628 LEFT JOIN exercises AS e ON e.course_id = c.id
629 LEFT JOIN exercise_slides AS es ON es.exercise_id = e.id
630 LEFT JOIN exercise_tasks AS et ON et.exercise_slide_id = es.id
631 LEFT JOIN organizations AS o ON o.id = c.organization_id
632WHERE cie.user_id = $1
633 AND et.exercise_type = $2
634 AND ci.deleted_at IS NULL
635 AND cie.deleted_at IS NULL
636 AND c.deleted_at IS NULL
637 AND e.deleted_at IS NULL
638 AND es.deleted_at IS NULL
639 AND et.deleted_at IS NULL
640"#,
641 user_id,
642 exercise_type,
643 )
644 .fetch_all(conn)
645 .await?;
646 Ok(course_instances)
647}
648
649pub async fn reset_progress_on_course_instance_for_user(
651 conn: &mut PgConnection,
652 user_id: Uuid,
653 course_id: Uuid,
654) -> ModelResult<()> {
655 let mut tx = conn.begin().await?;
656 sqlx::query!(
657 "
658UPDATE exercise_slide_submissions
659SET deleted_at = now()
660WHERE user_id = $1
661 AND course_id = $2
662 AND deleted_at IS NULL
663 ",
664 user_id,
665 course_id
666 )
667 .execute(&mut *tx)
668 .await?;
669 sqlx::query!(
670 "
671UPDATE exercise_task_submissions
672SET deleted_at = now()
673WHERE exercise_slide_submission_id IN (
674 SELECT id
675 FROM exercise_slide_submissions
676 WHERE user_id = $1
677 AND course_id = $2
678 )
679 AND deleted_at IS NULL
680",
681 user_id,
682 course_id
683 )
684 .execute(&mut *tx)
685 .await?;
686 sqlx::query!(
687 "
688UPDATE peer_review_queue_entries
689SET deleted_at = now()
690WHERE user_id = $1
691 AND course_id = $2
692 AND deleted_at IS NULL
693",
694 user_id,
695 course_id
696 )
697 .execute(&mut *tx)
698 .await?;
699 sqlx::query!(
700 "
701UPDATE peer_or_self_review_submissions
702SET deleted_at = now()
703WHERE user_id = $1
704 AND course_id = $2
705 AND deleted_at IS NULL
706",
707 user_id,
708 course_id
709 )
710 .execute(&mut *tx)
711 .await?;
712 sqlx::query!(
713 "
714UPDATE peer_or_self_review_question_submissions
715SET deleted_at = now()
716WHERE peer_or_self_review_submission_id IN (
717 SELECT id
718 FROM peer_or_self_review_submissions
719 WHERE user_id = $1
720 AND course_id = $2
721 )
722 AND deleted_at IS NULL
723",
724 user_id,
725 course_id
726 )
727 .execute(&mut *tx)
728 .await?;
729 sqlx::query!(
730 "
731UPDATE exercise_task_gradings
732SET deleted_at = now()
733WHERE exercise_task_submission_id IN (
734 SELECT id
735 FROM exercise_task_submissions
736 WHERE exercise_slide_submission_id IN (
737 SELECT id
738 FROM exercise_slide_submissions
739 WHERE user_id = $1
740 AND course_id = $2
741 )
742 )
743 AND deleted_at IS NULL
744",
745 user_id,
746 course_id
747 )
748 .execute(&mut *tx)
749 .await?;
750
751 sqlx::query!(
752 "
753UPDATE user_exercise_states
754SET deleted_at = now()
755WHERE user_id = $1
756 AND course_id = $2
757 AND deleted_at IS NULL
758",
759 user_id,
760 course_id
761 )
762 .execute(&mut *tx)
763 .await?;
764 sqlx::query!(
765 "
766UPDATE user_exercise_task_states
767SET deleted_at = now()
768WHERE user_exercise_slide_state_id IN (
769 SELECT id
770 FROM user_exercise_slide_states
771 WHERE user_exercise_state_id IN (
772 SELECT id
773 FROM user_exercise_states
774 WHERE user_id = $1
775 AND course_id = $2
776 )
777 )
778 AND deleted_at IS NULL
779",
780 user_id,
781 course_id
782 )
783 .execute(&mut *tx)
784 .await?;
785 sqlx::query!(
786 "
787UPDATE user_exercise_slide_states
788SET deleted_at = now()
789WHERE user_exercise_state_id IN (
790 SELECT id
791 FROM user_exercise_states
792 WHERE user_id = $1
793 AND course_id = $2
794 )
795 AND deleted_at IS NULL
796",
797 user_id,
798 course_id
799 )
800 .execute(&mut *tx)
801 .await?;
802 sqlx::query!(
803 "
804UPDATE teacher_grading_decisions
805SET deleted_at = now()
806WHERE user_exercise_state_id IN (
807 SELECT id
808 FROM user_exercise_states
809 WHERE user_id = $1
810 AND course_id = $2
811 )
812 AND deleted_at IS NULL
813",
814 user_id,
815 course_id
816 )
817 .execute(&mut *tx)
818 .await?;
819 sqlx::query!(
820 "
821UPDATE course_module_completions
822SET deleted_at = now()
823WHERE user_id = $1
824AND course_id = $2
825AND deleted_at IS NULL
826",
827 user_id,
828 course_id
829 )
830 .execute(&mut *tx)
831 .await?;
832 sqlx::query!(
833 "
834UPDATE generated_certificates
835SET deleted_at = NOW()
836WHERE user_id = $1
837 AND certificate_configuration_id IN (
838 SELECT certificate_configuration_id
839 FROM certificate_configuration_to_requirements
840 WHERE course_module_id IN (
841 SELECT id
842 FROM course_modules
843 WHERE course_id = $2
844 )
845 AND deleted_at IS NULL
846 )
847 AND deleted_at IS NULL
848",
849 user_id,
850 course_id
851 )
852 .execute(&mut *tx)
853 .await?;
854 sqlx::query!(
855 "
856UPDATE user_chapter_locking_statuses
857SET deleted_at = now()
858WHERE user_id = $1
859 AND course_id = $2
860 AND deleted_at IS NULL
861",
862 user_id,
863 course_id
864 )
865 .execute(&mut *tx)
866 .await?;
867
868 tx.commit().await?;
869 Ok(())
870}
871
872pub async fn get_course_average_duration(
873 conn: &mut PgConnection,
874 course_id: Uuid,
875) -> ModelResult<Option<i64>> {
876 let res = sqlx::query!(
877 "
878SELECT AVG(
879 EXTRACT(
880 EPOCH
881 FROM cmc.completion_date - ce.created_at
882 )
883 )::int8 AS average_duration_seconds
884FROM course_instance_enrollments ce
885 JOIN course_module_completions cmc ON (
886 cmc.course_id = ce.course_id
887 AND cmc.user_id = ce.user_id
888 )
889WHERE ce.course_id = $1
890 AND ce.deleted_at IS NULL
891 AND cmc.deleted_at IS NULL;
892 ",
893 course_id
894 )
895 .fetch_optional(conn)
896 .await?;
897
898 Ok(res.map(|r| r.average_duration_seconds).unwrap_or_default())
899}
900
901pub async fn get_student_duration(
902 conn: &mut PgConnection,
903 user_id: Uuid,
904 course_id: Uuid,
905) -> ModelResult<Option<i64>> {
906 let res = sqlx::query!(
907 "
908SELECT COALESCE(
909 EXTRACT(
910 EPOCH
911 FROM cmc.completion_date - ce.created_at
912 )::int8,
913 0
914 ) AS student_duration_seconds
915FROM course_instance_enrollments ce
916 JOIN course_module_completions cmc ON (
917 cmc.course_id = ce.course_id
918 AND cmc.user_id = ce.user_id
919 )
920WHERE ce.course_id = $1
921 AND ce.user_id = $2
922 AND ce.deleted_at IS NULL
923 AND cmc.deleted_at IS NULL;
924 ",
925 course_id,
926 user_id
927 )
928 .fetch_optional(conn)
929 .await?;
930
931 Ok(res.map(|r| r.student_duration_seconds).unwrap_or_default())
932}
933
934#[cfg(test)]
935mod test {
936 use super::*;
937 use crate::{
938 course_instance_enrollments::NewCourseInstanceEnrollment, exercise_tasks::NewExerciseTask,
939 test_helper::*,
940 };
941
942 #[tokio::test]
943 async fn allows_only_one_instance_per_course_without_name() {
944 insert_data!(:tx, :user, :org, course: course_id);
945
946 let mut tx1 = tx.begin().await;
947 let mut instance = NewCourseInstance {
949 course_id,
950 name: None,
951 description: None,
952 teacher_in_charge_name: "teacher",
953 teacher_in_charge_email: "teacher@example.com",
954 support_email: None,
955 opening_time: None,
956 closing_time: None,
957 };
958 insert(tx1.as_mut(), PKeyPolicy::Generate, instance)
959 .await
960 .unwrap_err();
961 tx1.rollback().await;
962
963 let mut tx2 = tx.begin().await;
964 instance.name = Some("name");
966 insert(tx2.as_mut(), PKeyPolicy::Generate, instance)
967 .await
968 .unwrap();
969 }
970
971 #[tokio::test]
972 async fn gets_enrolled_course_instances_for_user_with_exercise_type() {
973 insert_data!(:tx, user:user_id, :org, course:course_id, :instance, course_module:_course_module_id, chapter:chapter_id, page:page_id, :exercise, slide:exercise_slide_id);
974
975 crate::course_instance_enrollments::insert_enrollment_and_set_as_current(
977 tx.as_mut(),
978 NewCourseInstanceEnrollment {
979 course_id,
980 user_id,
981 course_instance_id: instance.id,
982 },
983 )
984 .await
985 .unwrap();
986 let course_instances =
987 get_enrolled_course_instances_for_user_with_exercise_type(tx.as_mut(), user_id, "tmc")
988 .await
989 .unwrap();
990 assert!(
991 course_instances.is_empty(),
992 "user should not be enrolled on any course with tmc exercises"
993 );
994
995 crate::exercise_tasks::insert(
997 tx.as_mut(),
998 PKeyPolicy::Generate,
999 NewExerciseTask {
1000 assignment: Vec::new(),
1001 exercise_slide_id,
1002 exercise_type: "tmc".to_string(),
1003 model_solution_spec: None,
1004 private_spec: None,
1005 public_spec: None,
1006 order_number: 1,
1007 },
1008 )
1009 .await
1010 .unwrap();
1011 let course_instances =
1012 get_enrolled_course_instances_for_user_with_exercise_type(tx.as_mut(), user_id, "tmc")
1013 .await
1014 .unwrap();
1015 assert_eq!(
1016 course_instances.len(),
1017 1,
1018 "user should be enrolled on one course with tmc exercises"
1019 );
1020 tx.rollback().await;
1021 }
1022
1023 #[tokio::test]
1024 async fn gets_course_average_duration_with_empty_database() {
1025 insert_data!(:tx, :user, :org, :course);
1026 let duration = get_course_average_duration(tx.as_mut(), course)
1027 .await
1028 .unwrap();
1029 assert!(duration.is_none())
1030 }
1031}