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
286",
287        id
288    )
289    .execute(conn)
290    .await?;
291    Ok(())
292}
293
294pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<CourseModule> {
295    let res = sqlx::query_as!(
296        CourseModulesSchema,
297        "
298SELECT *
299FROM course_modules
300WHERE id = $1
301  AND deleted_at IS NULL
302        ",
303        id,
304    )
305    .fetch_one(conn)
306    .await?;
307    Ok(res.into())
308}
309
310pub async fn get_by_course_id(
311    conn: &mut PgConnection,
312    course_id: Uuid,
313) -> ModelResult<Vec<CourseModule>> {
314    let res = sqlx::query_as!(
315        CourseModulesSchema,
316        "
317SELECT *
318FROM course_modules
319WHERE course_id = $1
320AND deleted_at IS NULL
321",
322        course_id
323    )
324    .map(|x| x.into())
325    .fetch_all(conn)
326    .await?;
327    Ok(res)
328}
329
330/// Gets course module where the given exercise belongs to. This will result in an error in the case
331/// of an exam exercise.
332pub async fn get_by_exercise_id(
333    conn: &mut PgConnection,
334    exercise_id: Uuid,
335) -> ModelResult<CourseModule> {
336    let res = sqlx::query_as!(
337        CourseModulesSchema,
338        "
339SELECT course_modules.*
340FROM exercises
341  LEFT JOIN chapters ON (exercises.chapter_id = chapters.id)
342  LEFT JOIN course_modules ON (chapters.course_module_id = course_modules.id)
343WHERE exercises.id = $1
344AND chapters.deleted_at IS NULL
345AND course_modules.deleted_at IS NULL
346        ",
347        exercise_id,
348    )
349    .fetch_one(conn)
350    .await?;
351    Ok(res.into())
352}
353
354pub async fn get_course_module_id_by_chapter(
355    conn: &mut PgConnection,
356    chapter_id: Uuid,
357) -> ModelResult<Uuid> {
358    let res: Uuid = sqlx::query!(
359        r#"
360SELECT c.course_module_id
361from chapters c
362where c.id = $1
363  AND deleted_at IS NULL
364  "#,
365        chapter_id
366    )
367    .map(|record| record.course_module_id)
368    .fetch_one(conn)
369    .await?;
370    Ok(res)
371}
372
373pub async fn get_default_by_course_id(
374    conn: &mut PgConnection,
375    course_id: Uuid,
376) -> ModelResult<CourseModule> {
377    let res = sqlx::query_as!(
378        CourseModulesSchema,
379        "
380SELECT *
381FROM course_modules
382WHERE course_id = $1
383  AND name IS NULL
384  AND deleted_at IS NULL
385        ",
386        course_id,
387    )
388    .fetch_one(conn)
389    .await?;
390    Ok(res.into())
391}
392
393/// Gets all course modules with a matching `uh_course_code` or course `slug`.
394///
395/// In the latter case only one record at most is returned, but there is no way to distinguish between
396/// these two scenarios in advance.
397pub async fn get_ids_by_course_slug_or_uh_course_code(
398    conn: &mut PgConnection,
399    course_slug_or_code: &str,
400) -> ModelResult<Vec<Uuid>> {
401    let res = sqlx::query!(
402        "
403SELECT course_modules.id
404FROM course_modules
405  LEFT JOIN courses ON (course_modules.course_id = courses.id)
406WHERE (
407    course_modules.uh_course_code = $1
408    OR courses.slug = $1
409  )
410  AND course_modules.deleted_at IS NULL
411        ",
412        course_slug_or_code,
413    )
414    .map(|record| record.id)
415    .fetch_all(conn)
416    .await?;
417    Ok(res)
418}
419
420/// Gets course modules for the given course as a map, indexed by the `id` field.
421pub async fn get_by_course_id_as_map(
422    conn: &mut PgConnection,
423    course_id: Uuid,
424) -> ModelResult<HashMap<Uuid, CourseModule>> {
425    let res = get_by_course_id(conn, course_id)
426        .await?
427        .into_iter()
428        .map(|course_module| (course_module.id, course_module))
429        .collect();
430    Ok(res)
431}
432
433pub async fn get_all_uh_course_codes_for_open_university(
434    conn: &mut PgConnection,
435) -> ModelResult<Vec<String>> {
436    let res = sqlx::query!(
437        "
438SELECT DISTINCT uh_course_code
439FROM course_modules
440WHERE uh_course_code IS NOT NULL
441  AND enable_registering_completion_to_uh_open_university = true
442  AND deleted_at IS NULL
443"
444    )
445    .fetch_all(conn)
446    .await?
447    .into_iter()
448    .filter_map(|x| x.uh_course_code)
449    .collect();
450    Ok(res)
451}
452
453#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
454#[cfg_attr(feature = "ts_rs", derive(TS))]
455pub struct AutomaticCompletionRequirements {
456    /// Course module associated with these requirements.
457    pub course_module_id: Uuid,
458    pub number_of_exercises_attempted_treshold: Option<i32>,
459    pub number_of_points_treshold: Option<i32>,
460    pub requires_exam: bool,
461}
462
463impl AutomaticCompletionRequirements {
464    /// Shorthand for checking whether the given exercise related values pass their respective
465    /// tresholds.
466    pub fn passes_exercise_tresholds(
467        &self,
468        exercises_attempted: i32,
469        exercise_points: i32,
470    ) -> bool {
471        self.passes_number_of_exercises_attempted_treshold(exercises_attempted)
472            && self.passes_number_of_exercise_points_treshold(exercise_points)
473    }
474
475    /// Whether the given number is higher than the exercises attempted treshold. Always returns
476    /// true if there is no treshold.
477    pub fn passes_number_of_exercises_attempted_treshold(&self, exercises_attempted: i32) -> bool {
478        self.number_of_exercises_attempted_treshold
479            .map(|x| x <= exercises_attempted)
480            .unwrap_or(true)
481    }
482
483    /// Whether the given number is higher than the exercise points treshold. Always returns true
484    /// if there is no treshold.
485    pub fn passes_number_of_exercise_points_treshold(&self, exercise_points: i32) -> bool {
486        self.number_of_points_treshold
487            .map(|x| x <= exercise_points)
488            .unwrap_or(true)
489    }
490}
491
492#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
493#[serde(tag = "policy", rename_all = "kebab-case")]
494#[cfg_attr(feature = "ts_rs", derive(TS))]
495pub enum CompletionPolicy {
496    Automatic(AutomaticCompletionRequirements),
497    Manual,
498}
499
500impl CompletionPolicy {
501    /// Returns associated data for `Automatic` variant, if matches.
502    pub fn automatic(&self) -> Option<&AutomaticCompletionRequirements> {
503        match self {
504            CompletionPolicy::Automatic(requirements) => Some(requirements),
505            CompletionPolicy::Manual => None,
506        }
507    }
508
509    fn to_database_fields(&self) -> (bool, Option<i32>, Option<i32>, bool) {
510        match self {
511            CompletionPolicy::Automatic(requirements) => (
512                true,
513                requirements.number_of_exercises_attempted_treshold,
514                requirements.number_of_points_treshold,
515                requirements.requires_exam,
516            ),
517            CompletionPolicy::Manual => (false, None, None, false),
518        }
519    }
520}
521
522pub async fn update_automatic_completion_status(
523    conn: &mut PgConnection,
524    id: Uuid,
525    automatic_completion_policy: &CompletionPolicy,
526) -> ModelResult<CourseModule> {
527    let (automatic_completion, exercises_treshold, points_treshold, requires_exam) =
528        automatic_completion_policy.to_database_fields();
529    let res = sqlx::query_as!(
530        CourseModulesSchema,
531        "
532UPDATE course_modules
533SET automatic_completion = $1,
534  automatic_completion_number_of_exercises_attempted_treshold = $2,
535  automatic_completion_number_of_points_treshold = $3,
536  automatic_completion_requires_exam = $4
537WHERE id = $5
538  AND deleted_at IS NULL
539RETURNING *
540        ",
541        automatic_completion,
542        exercises_treshold,
543        points_treshold,
544        requires_exam,
545        id,
546    )
547    .fetch_one(conn)
548    .await?;
549    Ok(res.into())
550}
551
552pub async fn update_uh_course_code(
553    conn: &mut PgConnection,
554    id: Uuid,
555    uh_course_code: Option<String>,
556) -> ModelResult<CourseModule> {
557    let res = sqlx::query_as!(
558        CourseModulesSchema,
559        "
560UPDATE course_modules
561SET uh_course_code = $1
562WHERE id = $2
563  AND deleted_at IS NULL
564RETURNING *
565        ",
566        uh_course_code,
567        id,
568    )
569    .fetch_one(conn)
570    .await?;
571    Ok(res.into())
572}
573
574pub async fn update_enable_registering_completion_to_uh_open_university(
575    conn: &mut PgConnection,
576    id: Uuid,
577    enable_registering_completion_to_uh_open_university: bool,
578) -> ModelResult<CourseModule> {
579    let res = sqlx::query_as!(
580        CourseModulesSchema,
581        "
582UPDATE course_modules
583SET enable_registering_completion_to_uh_open_university = $1
584WHERE id = $2
585  AND deleted_at IS NULL
586RETURNING *
587        ",
588        enable_registering_completion_to_uh_open_university,
589        id,
590    )
591    .fetch_one(conn)
592    .await?;
593    Ok(res.into())
594}
595
596#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
597#[cfg_attr(feature = "ts_rs", derive(TS))]
598pub struct NewModule {
599    name: String,
600    order_number: i32,
601    chapters: Vec<Uuid>,
602    uh_course_code: Option<String>,
603    ects_credits: Option<f32>,
604    completion_policy: CompletionPolicy,
605    completion_registration_link_override: Option<String>,
606    enable_registering_completion_to_uh_open_university: bool,
607}
608
609#[derive(Debug, Deserialize)]
610#[cfg_attr(feature = "ts_rs", derive(TS))]
611pub struct ModifiedModule {
612    id: Uuid,
613    name: Option<String>,
614    order_number: i32,
615    uh_course_code: Option<String>,
616    ects_credits: Option<f32>,
617    completion_policy: CompletionPolicy,
618    completion_registration_link_override: Option<String>,
619    enable_registering_completion_to_uh_open_university: bool,
620}
621
622#[derive(Debug, Deserialize)]
623#[cfg_attr(feature = "ts_rs", derive(TS))]
624pub struct ModuleUpdates {
625    new_modules: Vec<NewModule>,
626    deleted_modules: Vec<Uuid>,
627    modified_modules: Vec<ModifiedModule>,
628    moved_chapters: Vec<(Uuid, Uuid)>,
629}
630
631pub async fn update_with_order_number(
632    conn: &mut PgConnection,
633    id: Uuid,
634    name: Option<&str>,
635    order_number: i32,
636) -> ModelResult<()> {
637    sqlx::query!(
638        "
639UPDATE course_modules
640SET name = COALESCE($1, name),
641  order_number = $2
642WHERE id = $3
643",
644        name,
645        order_number,
646        id,
647    )
648    .execute(conn)
649    .await?;
650    Ok(())
651}
652
653pub async fn update(
654    conn: &mut PgConnection,
655    id: Uuid,
656    updated_course_module: &NewCourseModule,
657) -> ModelResult<()> {
658    // destructure so new fields cause a compilation error here
659    let NewCourseModule {
660        completion_policy: _,
661        course_id: _,
662        ects_credits,
663        order_number,
664        name,
665        uh_course_code,
666        completion_registration_link_override,
667        enable_registering_completion_to_uh_open_university,
668    } = updated_course_module;
669    let (automatic_completion, exercises_treshold, points_treshold, requires_exam) =
670        updated_course_module.completion_policy.to_database_fields();
671    sqlx::query!(
672        "
673UPDATE course_modules
674SET name = COALESCE($2, name),
675  order_number = $3,
676  uh_course_code = $4,
677  ects_credits = $5,
678  automatic_completion = $6,
679  automatic_completion_number_of_exercises_attempted_treshold = $7,
680  automatic_completion_number_of_points_treshold = $8,
681  automatic_completion_requires_exam = $9,
682  completion_registration_link_override = $10,
683  enable_registering_completion_to_uh_open_university = $11
684WHERE id = $1
685        ",
686        id,
687        name.as_ref(),
688        order_number,
689        uh_course_code.as_ref(),
690        ects_credits.as_ref(),
691        automatic_completion,
692        exercises_treshold,
693        points_treshold,
694        requires_exam,
695        completion_registration_link_override.as_ref(),
696        enable_registering_completion_to_uh_open_university
697    )
698    .execute(conn)
699    .await?;
700    Ok(())
701}
702
703pub async fn update_modules(
704    conn: &mut PgConnection,
705    course_id: Uuid,
706    updates: ModuleUpdates,
707) -> ModelResult<()> {
708    let mut tx = conn.begin().await?;
709
710    // scramble order of modified and deleted modules
711    for module_id in updates
712        .modified_modules
713        .iter()
714        // do not scramble the default module, it should always be first
715        .filter(|m| m.order_number != 0)
716        .map(|m| m.id)
717        .chain(updates.deleted_modules.iter().copied())
718    {
719        update_with_order_number(&mut tx, module_id, None, rand::random()).await?;
720    }
721    let mut modified_and_new_modules = updates.modified_modules;
722    for new in updates.new_modules {
723        // destructure so new fields cause a compilation error here
724        let NewModule {
725            name,
726            order_number,
727            chapters,
728            uh_course_code,
729            ects_credits,
730            completion_policy,
731            completion_registration_link_override,
732            enable_registering_completion_to_uh_open_university,
733        } = new;
734        // insert with a random order number to avoid conflicts
735        let new_course_module = NewCourseModule::new(course_id, Some(name.clone()), rand::random())
736            .set_completion_policy(completion_policy.clone())
737            .set_completion_registration_link_override(completion_registration_link_override)
738            .set_ects_credits(ects_credits)
739            .set_uh_course_code(uh_course_code)
740            .set_enable_registering_completion_to_uh_open_university(
741                enable_registering_completion_to_uh_open_university,
742            );
743        let module = insert(&mut tx, PKeyPolicy::Generate, &new_course_module).await?;
744        for chapter in chapters {
745            chapters::set_module(&mut tx, chapter, module.id).await?;
746        }
747        //modify the order number with the rest
748        modified_and_new_modules.push(ModifiedModule {
749            id: module.id,
750            name: None,
751            order_number,
752            uh_course_code: module.uh_course_code,
753            ects_credits,
754            completion_policy,
755            completion_registration_link_override: module.completion_registration_link_override,
756            enable_registering_completion_to_uh_open_university: module
757                .enable_registering_completion_to_uh_open_university,
758        })
759    }
760    // update modified and new modules
761    for module in modified_and_new_modules {
762        // destructure so new fields cause a compilation error here
763        let ModifiedModule {
764            id,
765            name,
766            order_number,
767            uh_course_code,
768            ects_credits,
769            completion_policy,
770            completion_registration_link_override,
771            enable_registering_completion_to_uh_open_university,
772        } = module;
773        update(
774            &mut tx,
775            id,
776            &NewCourseModule::new(course_id, name.clone(), order_number)
777                .set_completion_policy(completion_policy)
778                .set_completion_registration_link_override(completion_registration_link_override)
779                .set_ects_credits(ects_credits)
780                .set_uh_course_code(uh_course_code)
781                .set_enable_registering_completion_to_uh_open_university(
782                    enable_registering_completion_to_uh_open_university,
783                ),
784        )
785        .await?;
786    }
787    for (chapter, module) in updates.moved_chapters {
788        chapters::set_module(&mut tx, chapter, module).await?;
789    }
790    for deleted in updates.deleted_modules {
791        delete(&mut tx, deleted).await?;
792    }
793
794    tx.commit().await?;
795    Ok(())
796}
797
798pub async fn update_certification_enabled(
799    conn: &mut PgConnection,
800    id: Uuid,
801    enabled: bool,
802) -> ModelResult<()> {
803    sqlx::query!(
804        "
805UPDATE course_modules
806SET certification_enabled = $1
807WHERE id = $2
808",
809        enabled,
810        id
811    )
812    .execute(conn)
813    .await?;
814    Ok(())
815}
816
817#[cfg(test)]
818mod tests {
819
820    mod automatic_completion_requirements {
821        use uuid::Uuid;
822
823        use super::super::AutomaticCompletionRequirements;
824
825        #[test]
826        fn passes_exercise_tresholds() {
827            let requirements1 = AutomaticCompletionRequirements {
828                course_module_id: Uuid::parse_str("66d98fc6-784a-4b39-a494-24ae9b1c9b14").unwrap(),
829                number_of_exercises_attempted_treshold: Some(10),
830                number_of_points_treshold: Some(50),
831                requires_exam: false,
832            };
833            let requirements2 = AutomaticCompletionRequirements {
834                course_module_id: Uuid::parse_str("66d98fc6-784a-4b39-a494-24ae9b1c9b14").unwrap(),
835                number_of_exercises_attempted_treshold: Some(50),
836                number_of_points_treshold: Some(10),
837                requires_exam: false,
838            };
839
840            let requirements3 = AutomaticCompletionRequirements {
841                course_module_id: Uuid::parse_str("66d98fc6-784a-4b39-a494-24ae9b1c9b14").unwrap(),
842                number_of_exercises_attempted_treshold: Some(0),
843                number_of_points_treshold: Some(0),
844                requires_exam: false,
845            };
846
847            let requirements4 = AutomaticCompletionRequirements {
848                course_module_id: Uuid::parse_str("66d98fc6-784a-4b39-a494-24ae9b1c9b14").unwrap(),
849                number_of_exercises_attempted_treshold: Some(10),
850                number_of_points_treshold: None,
851                requires_exam: false,
852            };
853
854            let requirements5 = AutomaticCompletionRequirements {
855                course_module_id: Uuid::parse_str("66d98fc6-784a-4b39-a494-24ae9b1c9b14").unwrap(),
856                number_of_exercises_attempted_treshold: None,
857                number_of_points_treshold: Some(10),
858                requires_exam: false,
859            };
860            assert!(requirements1.passes_exercise_tresholds(10, 50));
861            assert!(requirements2.passes_exercise_tresholds(50, 10));
862
863            assert!(!requirements1.passes_exercise_tresholds(50, 10));
864            assert!(!requirements2.passes_exercise_tresholds(10, 50));
865
866            assert!(!requirements1.passes_exercise_tresholds(100, 0));
867            assert!(!requirements2.passes_exercise_tresholds(100, 0));
868
869            assert!(requirements3.passes_exercise_tresholds(1, 1));
870            assert!(requirements3.passes_exercise_tresholds(0, 0));
871
872            assert!(requirements4.passes_exercise_tresholds(10, 1));
873            assert!(!requirements4.passes_exercise_tresholds(1, 10));
874
875            assert!(requirements5.passes_exercise_tresholds(0, 10));
876            assert!(!requirements5.passes_exercise_tresholds(10, 0));
877        }
878    }
879}