headless_lms_models/
course_modules.rs

1use std::collections::HashMap;
2
3use crate::{chapters, prelude::*};
4
5/// Matches the columns in the database.
6struct CourseModulesSchema {
7    id: Uuid,
8    created_at: DateTime<Utc>,
9    updated_at: DateTime<Utc>,
10    deleted_at: Option<DateTime<Utc>>,
11    name: Option<String>,
12    course_id: Uuid,
13    order_number: i32,
14    copied_from: Option<Uuid>,
15    uh_course_code: Option<String>,
16    automatic_completion: bool,
17    automatic_completion_number_of_exercises_attempted_treshold: Option<i32>,
18    automatic_completion_number_of_points_treshold: Option<i32>,
19    automatic_completion_requires_exam: bool,
20    completion_registration_link_override: Option<String>,
21    ects_credits: Option<f32>,
22    enable_registering_completion_to_uh_open_university: bool,
23    certification_enabled: bool,
24}
25/**
26 * Based on [CourseModulesSchema] but completion_policy parsed and addded (and some not needeed fields removed).
27 */
28#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
29#[cfg_attr(feature = "ts_rs", derive(TS))]
30pub struct CourseModule {
31    pub id: Uuid,
32    pub created_at: DateTime<Utc>,
33    pub updated_at: DateTime<Utc>,
34    pub deleted_at: Option<DateTime<Utc>>,
35    pub name: Option<String>,
36    pub course_id: Uuid,
37    pub order_number: i32,
38    pub copied_from: Option<Uuid>,
39    pub uh_course_code: Option<String>,
40    pub completion_policy: CompletionPolicy,
41    /// If set, use this link rather than the default one when registering course completions.
42    pub completion_registration_link_override: Option<String>,
43    pub ects_credits: Option<f32>,
44    pub enable_registering_completion_to_uh_open_university: bool,
45    pub certification_enabled: bool,
46}
47
48impl CourseModule {
49    pub fn new(id: Uuid, course_id: Uuid) -> Self {
50        Self {
51            id,
52            created_at: Utc::now(),
53            updated_at: Utc::now(),
54            deleted_at: None,
55            name: None,
56            course_id,
57            order_number: 0,
58            copied_from: None,
59            uh_course_code: None,
60            completion_policy: CompletionPolicy::Manual,
61            completion_registration_link_override: None,
62            ects_credits: None,
63            enable_registering_completion_to_uh_open_university: false,
64            certification_enabled: false,
65        }
66    }
67    pub fn set_timestamps(
68        mut self,
69        created_at: DateTime<Utc>,
70        updated_at: DateTime<Utc>,
71        deleted_at: Option<DateTime<Utc>>,
72    ) -> Self {
73        self.created_at = created_at;
74        self.updated_at = updated_at;
75        self.deleted_at = deleted_at;
76        self
77    }
78
79    /// order_number == 0 in and only if name == None
80    pub fn set_name_and_order_number(mut self, name: Option<String>, order_number: i32) -> Self {
81        self.name = name;
82        self.order_number = order_number;
83        self
84    }
85
86    pub fn set_completion_policy(mut self, completion_policy: CompletionPolicy) -> Self {
87        self.completion_policy = completion_policy;
88        self
89    }
90
91    pub fn set_registration_info(
92        mut self,
93        uh_course_code: Option<String>,
94        ects_credits: Option<f32>,
95        completion_registration_link_override: Option<String>,
96        enable_registering_completion_to_uh_open_university: bool,
97    ) -> Self {
98        self.uh_course_code = uh_course_code;
99        self.ects_credits = ects_credits;
100        self.completion_registration_link_override = completion_registration_link_override;
101        self.enable_registering_completion_to_uh_open_university =
102            enable_registering_completion_to_uh_open_university;
103        self
104    }
105
106    pub fn set_certification_enabled(mut self, certification_enabled: bool) -> Self {
107        self.certification_enabled = certification_enabled;
108        self
109    }
110
111    pub fn is_default_module(&self) -> bool {
112        self.name.is_none()
113    }
114}
115
116impl From<CourseModulesSchema> for CourseModule {
117    fn from(schema: CourseModulesSchema) -> Self {
118        let completion_policy = if schema.automatic_completion {
119            CompletionPolicy::Automatic(AutomaticCompletionRequirements {
120                course_module_id: schema.id,
121                number_of_exercises_attempted_treshold: schema
122                    .automatic_completion_number_of_exercises_attempted_treshold,
123                number_of_points_treshold: schema.automatic_completion_number_of_points_treshold,
124                requires_exam: schema.automatic_completion_requires_exam,
125            })
126        } else {
127            CompletionPolicy::Manual
128        };
129        Self {
130            id: schema.id,
131            created_at: schema.created_at,
132            updated_at: schema.updated_at,
133            deleted_at: schema.deleted_at,
134            name: schema.name,
135            course_id: schema.course_id,
136            order_number: schema.order_number,
137            copied_from: schema.copied_from,
138            uh_course_code: schema.uh_course_code,
139            completion_policy,
140            completion_registration_link_override: schema.completion_registration_link_override,
141            ects_credits: schema.ects_credits,
142            enable_registering_completion_to_uh_open_university: schema
143                .enable_registering_completion_to_uh_open_university,
144            certification_enabled: schema.certification_enabled,
145        }
146    }
147}
148
149#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
150#[cfg_attr(feature = "ts_rs", derive(TS))]
151pub struct NewCourseModule {
152    completion_policy: CompletionPolicy,
153    completion_registration_link_override: Option<String>,
154    course_id: Uuid,
155    ects_credits: Option<f32>,
156    name: Option<String>,
157    order_number: i32,
158    uh_course_code: Option<String>,
159    enable_registering_completion_to_uh_open_university: bool,
160}
161
162impl NewCourseModule {
163    pub fn new(course_id: Uuid, name: Option<String>, order_number: i32) -> Self {
164        Self {
165            completion_policy: CompletionPolicy::Manual,
166            completion_registration_link_override: None,
167            course_id,
168            ects_credits: None,
169            name,
170            order_number,
171            uh_course_code: None,
172            enable_registering_completion_to_uh_open_university: false,
173        }
174    }
175
176    pub fn new_course_default(course_id: Uuid) -> Self {
177        Self::new(course_id, None, 0)
178    }
179
180    pub fn set_uh_course_code(mut self, uh_course_code: Option<String>) -> Self {
181        self.uh_course_code = uh_course_code;
182        self
183    }
184
185    pub fn set_completion_policy(mut self, completion_policy: CompletionPolicy) -> Self {
186        self.completion_policy = completion_policy;
187        self
188    }
189
190    pub fn set_completion_registration_link_override(
191        mut self,
192        completion_registration_link_override: Option<String>,
193    ) -> Self {
194        self.completion_registration_link_override = completion_registration_link_override;
195        self
196    }
197
198    pub fn set_ects_credits(mut self, ects_credits: Option<f32>) -> Self {
199        self.ects_credits = ects_credits;
200        self
201    }
202
203    pub fn set_enable_registering_completion_to_uh_open_university(
204        mut self,
205        enable_registering_completion_to_uh_open_university: bool,
206    ) -> Self {
207        self.enable_registering_completion_to_uh_open_university =
208            enable_registering_completion_to_uh_open_university;
209        self
210    }
211}
212
213pub async fn insert(
214    conn: &mut PgConnection,
215    pkey_policy: PKeyPolicy<Uuid>,
216    new_course_module: &NewCourseModule,
217) -> ModelResult<CourseModule> {
218    let (automatic_completion, exercises_treshold, points_treshold, requires_exam) =
219        new_course_module.completion_policy.to_database_fields();
220    let res = sqlx::query_as!(
221        CourseModulesSchema,
222        "
223INSERT INTO course_modules (
224    id,
225    course_id,
226    name,
227    order_number,
228    automatic_completion,
229    automatic_completion_number_of_exercises_attempted_treshold,
230    automatic_completion_number_of_points_treshold,
231    automatic_completion_requires_exam,
232    ects_credits,
233    enable_registering_completion_to_uh_open_university
234  )
235VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
236RETURNING *
237        ",
238        pkey_policy.into_uuid(),
239        new_course_module.course_id,
240        new_course_module.name,
241        new_course_module.order_number,
242        automatic_completion,
243        exercises_treshold,
244        points_treshold,
245        requires_exam,
246        new_course_module.ects_credits,
247        new_course_module.enable_registering_completion_to_uh_open_university
248    )
249    .fetch_one(conn)
250    .await?;
251    Ok(res.into())
252}
253
254pub async fn rename(conn: &mut PgConnection, id: Uuid, name: &str) -> ModelResult<()> {
255    sqlx::query!(
256        "
257UPDATE course_modules
258SET name = $1
259WHERE id = $2
260",
261        name,
262        id
263    )
264    .execute(conn)
265    .await?;
266    Ok(())
267}
268
269pub async fn delete(conn: &mut PgConnection, id: Uuid) -> ModelResult<()> {
270    let associated_chapters = chapters::get_for_module(conn, id).await?;
271    if !associated_chapters.is_empty() {
272        return Err(ModelError::new(
273            ModelErrorType::InvalidRequest,
274            format!(
275                "Cannot remove module {id} because it has {} chapters associated with it",
276                associated_chapters.len()
277            ),
278            None,
279        ));
280    }
281    sqlx::query!(
282        "
283UPDATE course_modules
284SET deleted_at = now()
285WHERE id = $1
286AND deleted_at IS NULL
287",
288        id
289    )
290    .execute(conn)
291    .await?;
292    Ok(())
293}
294
295pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<CourseModule> {
296    let res = sqlx::query_as!(
297        CourseModulesSchema,
298        "
299SELECT *
300FROM course_modules
301WHERE id = $1
302  AND deleted_at IS NULL
303        ",
304        id,
305    )
306    .fetch_one(conn)
307    .await?;
308    Ok(res.into())
309}
310
311pub async fn get_by_course_id(
312    conn: &mut PgConnection,
313    course_id: Uuid,
314) -> ModelResult<Vec<CourseModule>> {
315    let res = sqlx::query_as!(
316        CourseModulesSchema,
317        "
318SELECT *
319FROM course_modules
320WHERE course_id = $1
321AND deleted_at IS NULL
322",
323        course_id
324    )
325    .map(|x| x.into())
326    .fetch_all(conn)
327    .await?;
328    Ok(res)
329}
330
331/// Gets course module where the given exercise belongs to. This will result in an error in the case
332/// of an exam exercise.
333pub async fn get_by_exercise_id(
334    conn: &mut PgConnection,
335    exercise_id: Uuid,
336) -> ModelResult<CourseModule> {
337    let res = sqlx::query_as!(
338        CourseModulesSchema,
339        "
340SELECT course_modules.*
341FROM exercises
342  LEFT JOIN chapters ON (exercises.chapter_id = chapters.id)
343  LEFT JOIN course_modules ON (chapters.course_module_id = course_modules.id)
344WHERE exercises.id = $1
345AND chapters.deleted_at IS NULL
346AND course_modules.deleted_at IS NULL
347        ",
348        exercise_id,
349    )
350    .fetch_one(conn)
351    .await?;
352    Ok(res.into())
353}
354
355pub async fn get_course_module_id_by_chapter(
356    conn: &mut PgConnection,
357    chapter_id: Uuid,
358) -> ModelResult<Uuid> {
359    let res: Uuid = sqlx::query!(
360        r#"
361SELECT c.course_module_id
362from chapters c
363where c.id = $1
364  AND deleted_at IS NULL
365  "#,
366        chapter_id
367    )
368    .map(|record| record.course_module_id)
369    .fetch_one(conn)
370    .await?;
371    Ok(res)
372}
373
374pub async fn get_default_by_course_id(
375    conn: &mut PgConnection,
376    course_id: Uuid,
377) -> ModelResult<CourseModule> {
378    let res = sqlx::query_as!(
379        CourseModulesSchema,
380        "
381SELECT *
382FROM course_modules
383WHERE course_id = $1
384  AND name IS NULL
385  AND deleted_at IS NULL
386        ",
387        course_id,
388    )
389    .fetch_one(conn)
390    .await?;
391    Ok(res.into())
392}
393
394/// Gets all course modules with a matching `uh_course_code` or course `slug`.
395///
396/// In the latter case only one record at most is returned, but there is no way to distinguish between
397/// these two scenarios in advance.
398pub async fn get_ids_by_course_slug_or_uh_course_code(
399    conn: &mut PgConnection,
400    course_slug_or_code: &str,
401) -> ModelResult<Vec<Uuid>> {
402    let res = sqlx::query!(
403        "
404SELECT course_modules.id
405FROM course_modules
406  LEFT JOIN courses ON (course_modules.course_id = courses.id)
407WHERE (
408    course_modules.uh_course_code = $1
409    OR courses.slug = $1
410  )
411  AND course_modules.deleted_at IS NULL
412        ",
413        course_slug_or_code,
414    )
415    .map(|record| record.id)
416    .fetch_all(conn)
417    .await?;
418    Ok(res)
419}
420
421/// Gets course modules for the given course as a map, indexed by the `id` field.
422pub async fn get_by_course_id_as_map(
423    conn: &mut PgConnection,
424    course_id: Uuid,
425) -> ModelResult<HashMap<Uuid, CourseModule>> {
426    let res = get_by_course_id(conn, course_id)
427        .await?
428        .into_iter()
429        .map(|course_module| (course_module.id, course_module))
430        .collect();
431    Ok(res)
432}
433
434pub async fn get_all_uh_course_codes_for_open_university(
435    conn: &mut PgConnection,
436) -> ModelResult<Vec<String>> {
437    let res = sqlx::query!(
438        "
439SELECT DISTINCT uh_course_code
440FROM course_modules
441WHERE uh_course_code IS NOT NULL
442  AND enable_registering_completion_to_uh_open_university = true
443  AND deleted_at IS NULL
444"
445    )
446    .fetch_all(conn)
447    .await?
448    .into_iter()
449    .filter_map(|x| x.uh_course_code)
450    .collect();
451    Ok(res)
452}
453
454#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
455#[cfg_attr(feature = "ts_rs", derive(TS))]
456pub struct AutomaticCompletionRequirements {
457    /// Course module associated with these requirements.
458    pub course_module_id: Uuid,
459    pub number_of_exercises_attempted_treshold: Option<i32>,
460    pub number_of_points_treshold: Option<i32>,
461    pub requires_exam: bool,
462}
463
464impl AutomaticCompletionRequirements {
465    /// Shorthand for checking whether the given exercise related values pass their respective
466    /// tresholds.
467    pub fn passes_exercise_tresholds(
468        &self,
469        exercises_attempted: i32,
470        exercise_points: i32,
471    ) -> bool {
472        self.passes_number_of_exercises_attempted_treshold(exercises_attempted)
473            && self.passes_number_of_exercise_points_treshold(exercise_points)
474    }
475
476    /// Whether the given number is higher than the exercises attempted treshold. Always returns
477    /// true if there is no treshold.
478    pub fn passes_number_of_exercises_attempted_treshold(&self, exercises_attempted: i32) -> bool {
479        self.number_of_exercises_attempted_treshold
480            .map(|x| x <= exercises_attempted)
481            .unwrap_or(true)
482    }
483
484    /// Whether the given number is higher than the exercise points treshold. Always returns true
485    /// if there is no treshold.
486    pub fn passes_number_of_exercise_points_treshold(&self, exercise_points: i32) -> bool {
487        self.number_of_points_treshold
488            .map(|x| x <= exercise_points)
489            .unwrap_or(true)
490    }
491}
492
493#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
494#[serde(tag = "policy", rename_all = "kebab-case")]
495#[cfg_attr(feature = "ts_rs", derive(TS))]
496pub enum CompletionPolicy {
497    Automatic(AutomaticCompletionRequirements),
498    Manual,
499}
500
501impl CompletionPolicy {
502    /// Returns associated data for `Automatic` variant, if matches.
503    pub fn automatic(&self) -> Option<&AutomaticCompletionRequirements> {
504        match self {
505            CompletionPolicy::Automatic(requirements) => Some(requirements),
506            CompletionPolicy::Manual => None,
507        }
508    }
509
510    fn to_database_fields(&self) -> (bool, Option<i32>, Option<i32>, bool) {
511        match self {
512            CompletionPolicy::Automatic(requirements) => (
513                true,
514                requirements.number_of_exercises_attempted_treshold,
515                requirements.number_of_points_treshold,
516                requirements.requires_exam,
517            ),
518            CompletionPolicy::Manual => (false, None, None, false),
519        }
520    }
521}
522
523pub async fn update_automatic_completion_status(
524    conn: &mut PgConnection,
525    id: Uuid,
526    automatic_completion_policy: &CompletionPolicy,
527) -> ModelResult<CourseModule> {
528    let (automatic_completion, exercises_treshold, points_treshold, requires_exam) =
529        automatic_completion_policy.to_database_fields();
530    let res = sqlx::query_as!(
531        CourseModulesSchema,
532        "
533UPDATE course_modules
534SET automatic_completion = $1,
535  automatic_completion_number_of_exercises_attempted_treshold = $2,
536  automatic_completion_number_of_points_treshold = $3,
537  automatic_completion_requires_exam = $4
538WHERE id = $5
539  AND deleted_at IS NULL
540RETURNING *
541        ",
542        automatic_completion,
543        exercises_treshold,
544        points_treshold,
545        requires_exam,
546        id,
547    )
548    .fetch_one(conn)
549    .await?;
550    Ok(res.into())
551}
552
553pub async fn update_uh_course_code(
554    conn: &mut PgConnection,
555    id: Uuid,
556    uh_course_code: Option<String>,
557) -> ModelResult<CourseModule> {
558    let res = sqlx::query_as!(
559        CourseModulesSchema,
560        "
561UPDATE course_modules
562SET uh_course_code = $1
563WHERE id = $2
564  AND deleted_at IS NULL
565RETURNING *
566        ",
567        uh_course_code,
568        id,
569    )
570    .fetch_one(conn)
571    .await?;
572    Ok(res.into())
573}
574
575pub async fn update_enable_registering_completion_to_uh_open_university(
576    conn: &mut PgConnection,
577    id: Uuid,
578    enable_registering_completion_to_uh_open_university: bool,
579) -> ModelResult<CourseModule> {
580    let res = sqlx::query_as!(
581        CourseModulesSchema,
582        "
583UPDATE course_modules
584SET enable_registering_completion_to_uh_open_university = $1
585WHERE id = $2
586  AND deleted_at IS NULL
587RETURNING *
588        ",
589        enable_registering_completion_to_uh_open_university,
590        id,
591    )
592    .fetch_one(conn)
593    .await?;
594    Ok(res.into())
595}
596
597#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
598#[cfg_attr(feature = "ts_rs", derive(TS))]
599pub struct NewModule {
600    name: String,
601    order_number: i32,
602    chapters: Vec<Uuid>,
603    uh_course_code: Option<String>,
604    ects_credits: Option<f32>,
605    completion_policy: CompletionPolicy,
606    completion_registration_link_override: Option<String>,
607    enable_registering_completion_to_uh_open_university: bool,
608}
609
610#[derive(Debug, Deserialize)]
611#[cfg_attr(feature = "ts_rs", derive(TS))]
612pub struct ModifiedModule {
613    id: Uuid,
614    name: Option<String>,
615    order_number: i32,
616    uh_course_code: Option<String>,
617    ects_credits: Option<f32>,
618    completion_policy: CompletionPolicy,
619    completion_registration_link_override: Option<String>,
620    enable_registering_completion_to_uh_open_university: bool,
621}
622
623#[derive(Debug, Deserialize)]
624#[cfg_attr(feature = "ts_rs", derive(TS))]
625pub struct ModuleUpdates {
626    new_modules: Vec<NewModule>,
627    deleted_modules: Vec<Uuid>,
628    modified_modules: Vec<ModifiedModule>,
629    moved_chapters: Vec<(Uuid, Uuid)>,
630}
631
632pub async fn update_with_order_number(
633    conn: &mut PgConnection,
634    id: Uuid,
635    name: Option<&str>,
636    order_number: i32,
637) -> ModelResult<()> {
638    sqlx::query!(
639        "
640UPDATE course_modules
641SET name = COALESCE($1, name),
642  order_number = $2
643WHERE id = $3
644",
645        name,
646        order_number,
647        id,
648    )
649    .execute(conn)
650    .await?;
651    Ok(())
652}
653
654pub async fn update(
655    conn: &mut PgConnection,
656    id: Uuid,
657    updated_course_module: &NewCourseModule,
658) -> ModelResult<()> {
659    // destructure so new fields cause a compilation error here
660    let NewCourseModule {
661        completion_policy: _,
662        course_id: _,
663        ects_credits,
664        order_number,
665        name,
666        uh_course_code,
667        completion_registration_link_override,
668        enable_registering_completion_to_uh_open_university,
669    } = updated_course_module;
670    let (automatic_completion, exercises_treshold, points_treshold, requires_exam) =
671        updated_course_module.completion_policy.to_database_fields();
672    sqlx::query!(
673        "
674UPDATE course_modules
675SET name = COALESCE($2, name),
676  order_number = $3,
677  uh_course_code = $4,
678  ects_credits = $5,
679  automatic_completion = $6,
680  automatic_completion_number_of_exercises_attempted_treshold = $7,
681  automatic_completion_number_of_points_treshold = $8,
682  automatic_completion_requires_exam = $9,
683  completion_registration_link_override = $10,
684  enable_registering_completion_to_uh_open_university = $11
685WHERE id = $1
686        ",
687        id,
688        name.as_ref(),
689        order_number,
690        uh_course_code.as_ref(),
691        ects_credits.as_ref(),
692        automatic_completion,
693        exercises_treshold,
694        points_treshold,
695        requires_exam,
696        completion_registration_link_override.as_ref(),
697        enable_registering_completion_to_uh_open_university
698    )
699    .execute(conn)
700    .await?;
701    Ok(())
702}
703
704pub async fn update_modules(
705    conn: &mut PgConnection,
706    course_id: Uuid,
707    updates: ModuleUpdates,
708) -> ModelResult<()> {
709    let mut tx = conn.begin().await?;
710
711    // scramble order of modified and deleted modules
712    for module_id in updates
713        .modified_modules
714        .iter()
715        // do not scramble the default module, it should always be first
716        .filter(|m| m.order_number != 0)
717        .map(|m| m.id)
718        .chain(updates.deleted_modules.iter().copied())
719    {
720        update_with_order_number(&mut tx, module_id, None, rand::random()).await?;
721    }
722    let mut modified_and_new_modules = updates.modified_modules;
723    for new in updates.new_modules {
724        // destructure so new fields cause a compilation error here
725        let NewModule {
726            name,
727            order_number,
728            chapters,
729            uh_course_code,
730            ects_credits,
731            completion_policy,
732            completion_registration_link_override,
733            enable_registering_completion_to_uh_open_university,
734        } = new;
735        // insert with a random order number to avoid conflicts
736        let new_course_module = NewCourseModule::new(course_id, Some(name.clone()), rand::random())
737            .set_completion_policy(completion_policy.clone())
738            .set_completion_registration_link_override(completion_registration_link_override)
739            .set_ects_credits(ects_credits)
740            .set_uh_course_code(uh_course_code)
741            .set_enable_registering_completion_to_uh_open_university(
742                enable_registering_completion_to_uh_open_university,
743            );
744        let module = insert(&mut tx, PKeyPolicy::Generate, &new_course_module).await?;
745        for chapter in chapters {
746            chapters::set_module(&mut tx, chapter, module.id).await?;
747        }
748        //modify the order number with the rest
749        modified_and_new_modules.push(ModifiedModule {
750            id: module.id,
751            name: None,
752            order_number,
753            uh_course_code: module.uh_course_code,
754            ects_credits,
755            completion_policy,
756            completion_registration_link_override: module.completion_registration_link_override,
757            enable_registering_completion_to_uh_open_university: module
758                .enable_registering_completion_to_uh_open_university,
759        })
760    }
761    // update modified and new modules
762    for module in modified_and_new_modules {
763        // destructure so new fields cause a compilation error here
764        let ModifiedModule {
765            id,
766            name,
767            order_number,
768            uh_course_code,
769            ects_credits,
770            completion_policy,
771            completion_registration_link_override,
772            enable_registering_completion_to_uh_open_university,
773        } = module;
774        update(
775            &mut tx,
776            id,
777            &NewCourseModule::new(course_id, name.clone(), order_number)
778                .set_completion_policy(completion_policy)
779                .set_completion_registration_link_override(completion_registration_link_override)
780                .set_ects_credits(ects_credits)
781                .set_uh_course_code(uh_course_code)
782                .set_enable_registering_completion_to_uh_open_university(
783                    enable_registering_completion_to_uh_open_university,
784                ),
785        )
786        .await?;
787    }
788    for (chapter, module) in updates.moved_chapters {
789        chapters::set_module(&mut tx, chapter, module).await?;
790    }
791    for deleted in updates.deleted_modules {
792        delete(&mut tx, deleted).await?;
793    }
794
795    tx.commit().await?;
796    Ok(())
797}
798
799pub async fn update_certification_enabled(
800    conn: &mut PgConnection,
801    id: Uuid,
802    enabled: bool,
803) -> ModelResult<()> {
804    sqlx::query!(
805        "
806UPDATE course_modules
807SET certification_enabled = $1
808WHERE id = $2
809",
810        enabled,
811        id
812    )
813    .execute(conn)
814    .await?;
815    Ok(())
816}
817
818#[cfg(test)]
819mod tests {
820
821    mod automatic_completion_requirements {
822        use uuid::Uuid;
823
824        use super::super::AutomaticCompletionRequirements;
825
826        #[test]
827        fn passes_exercise_tresholds() {
828            let requirements1 = AutomaticCompletionRequirements {
829                course_module_id: Uuid::parse_str("66d98fc6-784a-4b39-a494-24ae9b1c9b14").unwrap(),
830                number_of_exercises_attempted_treshold: Some(10),
831                number_of_points_treshold: Some(50),
832                requires_exam: false,
833            };
834            let requirements2 = AutomaticCompletionRequirements {
835                course_module_id: Uuid::parse_str("66d98fc6-784a-4b39-a494-24ae9b1c9b14").unwrap(),
836                number_of_exercises_attempted_treshold: Some(50),
837                number_of_points_treshold: Some(10),
838                requires_exam: false,
839            };
840
841            let requirements3 = AutomaticCompletionRequirements {
842                course_module_id: Uuid::parse_str("66d98fc6-784a-4b39-a494-24ae9b1c9b14").unwrap(),
843                number_of_exercises_attempted_treshold: Some(0),
844                number_of_points_treshold: Some(0),
845                requires_exam: false,
846            };
847
848            let requirements4 = AutomaticCompletionRequirements {
849                course_module_id: Uuid::parse_str("66d98fc6-784a-4b39-a494-24ae9b1c9b14").unwrap(),
850                number_of_exercises_attempted_treshold: Some(10),
851                number_of_points_treshold: None,
852                requires_exam: false,
853            };
854
855            let requirements5 = AutomaticCompletionRequirements {
856                course_module_id: Uuid::parse_str("66d98fc6-784a-4b39-a494-24ae9b1c9b14").unwrap(),
857                number_of_exercises_attempted_treshold: None,
858                number_of_points_treshold: Some(10),
859                requires_exam: false,
860            };
861            assert!(requirements1.passes_exercise_tresholds(10, 50));
862            assert!(requirements2.passes_exercise_tresholds(50, 10));
863
864            assert!(!requirements1.passes_exercise_tresholds(50, 10));
865            assert!(!requirements2.passes_exercise_tresholds(10, 50));
866
867            assert!(!requirements1.passes_exercise_tresholds(100, 0));
868            assert!(!requirements2.passes_exercise_tresholds(100, 0));
869
870            assert!(requirements3.passes_exercise_tresholds(1, 1));
871            assert!(requirements3.passes_exercise_tresholds(0, 0));
872
873            assert!(requirements4.passes_exercise_tresholds(10, 1));
874            assert!(!requirements4.passes_exercise_tresholds(1, 10));
875
876            assert!(requirements5.passes_exercise_tresholds(0, 10));
877            assert!(!requirements5.passes_exercise_tresholds(10, 0));
878        }
879    }
880}