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