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