headless_lms_models/
lib.rs

1/*!
2Functions and structs for interacting with the database.
3
4Each submodule corresponds to a database table.
5*/
6// we always use --document-private-items, so this warning is moot
7#![allow(rustdoc::private_intra_doc_links)]
8pub mod application_task_default_language_models;
9pub mod certificate_configuration_to_requirements;
10pub mod certificate_configurations;
11pub mod certificate_fonts;
12pub mod chapters;
13pub mod chatbot_configurations;
14pub mod chatbot_configurations_models;
15pub mod chatbot_conversation_message_tool_calls;
16pub mod chatbot_conversation_message_tool_outputs;
17pub mod chatbot_conversation_messages;
18pub mod chatbot_conversation_messages_citations;
19pub mod chatbot_conversation_suggested_messages;
20pub mod chatbot_conversations;
21pub mod chatbot_page_sync_statuses;
22pub mod cms_ai;
23pub mod code_giveaway_codes;
24pub mod code_giveaways;
25pub mod course_background_question_answers;
26pub mod course_background_questions;
27pub mod course_custom_privacy_policy_checkbox_texts;
28pub mod course_exams;
29pub mod course_instance_enrollments;
30pub mod course_instances;
31pub mod course_language_groups;
32pub mod course_module_completion_registered_to_study_registries;
33pub mod course_module_completions;
34pub mod course_modules;
35pub mod courses;
36pub mod email_deliveries;
37pub mod email_templates;
38pub mod email_verification_tokens;
39pub mod ended_processed_exams;
40pub mod error;
41pub mod exams;
42pub mod exercise_language_groups;
43pub mod exercise_repositories;
44pub mod exercise_reset_logs;
45pub mod exercise_service_info;
46pub mod exercise_services;
47pub mod exercise_slide_submissions;
48pub mod exercise_slides;
49pub mod exercise_task_gradings;
50pub mod exercise_task_regrading_submissions;
51pub mod exercise_task_submissions;
52pub mod exercise_tasks;
53pub mod exercises;
54pub mod feedback;
55pub mod file_uploads;
56pub mod flagged_answers;
57pub mod generated_certificates;
58pub mod glossary;
59pub mod join_code_uses;
60pub mod library;
61pub mod marketing_consents;
62pub mod material_references;
63pub mod oauth_access_token;
64pub mod oauth_auth_code;
65pub mod oauth_client;
66pub mod oauth_dpop_proofs;
67pub mod oauth_refresh_tokens;
68pub mod oauth_user_client_scopes;
69pub mod offered_answers_to_peer_review_temporary;
70pub mod open_university_registration_links;
71pub mod organizations;
72pub mod other_domain_to_course_redirections;
73pub mod page_audio_files;
74pub mod page_history;
75pub mod page_language_groups;
76pub mod page_visit_datum;
77pub mod page_visit_datum_daily_visit_hashing_keys;
78pub mod page_visit_datum_summary_by_courses;
79pub mod page_visit_datum_summary_by_courses_countries;
80pub mod page_visit_datum_summary_by_courses_device_types;
81pub mod page_visit_datum_summary_by_pages;
82pub mod pages;
83pub mod partner_block;
84pub mod peer_or_self_review_configs;
85pub mod peer_or_self_review_question_submissions;
86pub mod peer_or_self_review_questions;
87pub mod peer_or_self_review_submissions;
88pub mod peer_review_queue_entries;
89pub mod pending_roles;
90pub mod playground_examples;
91pub mod privacy_link;
92pub mod proposed_block_edits;
93pub mod proposed_page_edits;
94pub mod regradings;
95pub mod rejected_exercise_slide_submissions;
96pub mod repository_exercises;
97pub mod research_forms;
98pub mod roles;
99pub mod student_countries;
100pub mod study_registry_registrars;
101pub mod suspected_cheaters;
102pub mod teacher_grading_decisions;
103pub mod url_redirections;
104pub mod user_chapter_locking_statuses;
105pub mod user_course_exercise_service_variables;
106pub mod user_course_settings;
107pub mod user_details;
108pub mod user_email_codes;
109pub mod user_exercise_slide_states;
110pub mod user_exercise_states;
111pub mod user_exercise_task_states;
112pub mod user_passwords;
113pub mod user_research_consents;
114pub mod users;
115
116pub mod prelude;
117#[cfg(test)]
118pub mod test_helper;
119
120use exercises::Exercise;
121use futures::future::BoxFuture;
122use url::Url;
123use user_exercise_states::UserExerciseState;
124use uuid::Uuid;
125
126pub use self::error::{HttpErrorType, ModelError, ModelErrorType, ModelResult};
127use crate::prelude::*;
128
129#[macro_use]
130extern crate tracing;
131
132/**
133Helper struct to use with functions that insert data into the database.
134
135## Examples
136
137### Usage when inserting to a database
138
139By calling `.into_uuid()` function implemented by `PKeyPolicy<Uuid>`, this enum can be used with
140SQLX queries while letting the caller dictate how the primary key should be decided.
141
142```no_check
143# use headless_lms_models::{ModelResult, PKeyPolicy};
144# use uuid::Uuid;
145# use sqlx::PgConnection;
146async fn insert(
147    conn: &mut PgConnection,
148    pkey_policy: PKeyPolicy<Uuid>,
149) -> ModelResult<Uuid> {
150    let res = sqlx::query!(
151        "INSERT INTO organizations (id) VALUES ($1) RETURNING id",
152        pkey_policy.into_uuid(),
153    )
154    .fetch_one(conn)
155    .await?;
156    Ok(res.id)
157}
158
159# async fn random_function(conn: &mut PgConnection) -> ModelResult<()> {
160// Insert using generated id.
161let foo_1_id = insert(conn, PKeyPolicy::Generate).await.unwrap();
162
163// Insert using fixed id.
164let uuid = Uuid::parse_str("8fce44cf-738e-4fc9-8d8e-47c350fd3a7f").unwrap();
165let foo_2_id = insert(conn, PKeyPolicy::Fixed(uuid)).await.unwrap();
166assert_eq!(foo_2_id, uuid);
167# Ok(())
168# }
169```
170
171### Usage in a higher-order function.
172
173When `PKeyPolicy` is used with a higher-order function, an arbitrary struct can be provided
174instead. The data can be mapped further by calling the `.map()` or `.map_ref()` methods.
175
176```no_run
177# use headless_lms_models::{ModelResult, PKeyPolicy};
178# use uuid::Uuid;
179# use sqlx::PgConnection;
180# mod foos {
181#   use headless_lms_models::{ModelResult, PKeyPolicy};
182#   use uuid::Uuid;
183#   use sqlx::PgConnection;
184#   pub async fn insert(conn: &mut PgConnection, pkey_policy: PKeyPolicy<Uuid>) -> ModelResult<()> {
185#       Ok(())
186#   }
187# }
188# mod bars {
189#   use headless_lms_models::{ModelResult, PKeyPolicy};
190#   use uuid::Uuid;
191#   use sqlx::PgConnection;
192#   pub async fn insert(conn: &mut PgConnection, pkey_policy: PKeyPolicy<Uuid>) -> ModelResult<()> {
193#       Ok(())
194#   }
195# }
196
197struct FooBar {
198    foo: Uuid,
199    bar: Uuid,
200}
201
202async fn multiple_inserts(
203    conn: &mut PgConnection,
204    pkey_policy: PKeyPolicy<FooBar>,
205) -> ModelResult<()> {
206    foos::insert(conn, pkey_policy.map_ref(|x| x.foo)).await?;
207    bars::insert(conn, pkey_policy.map_ref(|x| x.bar)).await?;
208    Ok(())
209}
210
211# async fn some_function(conn: &mut PgConnection) {
212// Insert using generated ids.
213assert!(multiple_inserts(conn, PKeyPolicy::Generate).await.is_ok());
214
215// Insert using fixed ids.
216let foobar = FooBar {
217    foo: Uuid::parse_str("52760668-cc9d-4144-9226-d2aacb83bea9").unwrap(),
218    bar: Uuid::parse_str("ce9bd0cd-0e66-4522-a1b4-52a9347a115c").unwrap(),
219};
220assert!(multiple_inserts(conn, PKeyPolicy::Fixed(foobar)).await.is_ok());
221# }
222```
223*/
224pub enum PKeyPolicy<T> {
225    /// Ids will be generated based on the associated data. Usually only used in
226    /// local test environments where reproducible database states are desired.
227    Fixed(T),
228    /// Ids will be generated on the database level. This should be the default
229    /// behavior.
230    Generate,
231}
232
233impl<T> PKeyPolicy<T> {
234    /// Gets reference to the fixed data, if there are any.
235    pub fn fixed(&self) -> Option<&T> {
236        match self {
237            PKeyPolicy::Fixed(t) => Some(t),
238            PKeyPolicy::Generate => None,
239        }
240    }
241
242    /// Maps `PKeyPolicy<T>` to `PKeyPolicy<U>` by applying a function to the contained value.
243    pub fn map<U, F>(self, f: F) -> PKeyPolicy<U>
244    where
245        F: FnOnce(T) -> U,
246    {
247        match self {
248            PKeyPolicy::Fixed(x) => PKeyPolicy::Fixed(f(x)),
249            PKeyPolicy::Generate => PKeyPolicy::Generate,
250        }
251    }
252
253    /// Maps a reference of contained data in `Fixed(T)` to `PKeyPolicy<U>` by applying a function
254    /// to the contained value. This is useful whenever a referenced value can be used instead of
255    /// having to consume the original value.
256    pub fn map_ref<U, F>(&self, f: F) -> PKeyPolicy<U>
257    where
258        F: FnOnce(&T) -> U,
259    {
260        match self {
261            PKeyPolicy::Fixed(x) => PKeyPolicy::Fixed(f(x)),
262            PKeyPolicy::Generate => PKeyPolicy::Generate,
263        }
264    }
265}
266
267impl PKeyPolicy<Uuid> {
268    /// Maps into the contained `Uuid` value or generates a new one.
269    pub fn into_uuid(self) -> Uuid {
270        match self {
271            PKeyPolicy::Fixed(uuid) => uuid,
272            PKeyPolicy::Generate => Uuid::new_v4(),
273        }
274    }
275}
276
277/// A "trait alias" so this `for<'a>` ... string doesn't need to be repeated everywhere
278/// Arguments:
279///   `Url`: The URL that the request is sent to (the exercise service's endpoint)
280///   `&str`: Exercise type/service slug
281///   `Option<Value>`: The Json for the request, for example the private spec in a public spec request
282pub trait SpecFetcher:
283    for<'a> Fn(
284    Url,
285    &'a str,
286    Option<&'a serde_json::Value>,
287) -> BoxFuture<'a, ModelResult<serde_json::Value>>
288{
289}
290
291impl<
292    T: for<'a> Fn(
293        Url,
294        &'a str,
295        Option<&'a serde_json::Value>,
296    ) -> BoxFuture<'a, ModelResult<serde_json::Value>>,
297> SpecFetcher for T
298{
299}
300
301/// Either a course or exam id.
302///
303/// Exercises can either be part of courses or exams. Many user-related actions need to differentiate
304/// between two, so `CourseOrExamId` helps when handling these separate scenarios.
305#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Hash)]
306pub enum CourseOrExamId {
307    Course(Uuid),
308    Exam(Uuid),
309}
310
311impl CourseOrExamId {
312    pub fn from_course_and_exam_ids(
313        course_id: Option<Uuid>,
314        exam_id: Option<Uuid>,
315    ) -> ModelResult<Self> {
316        match (course_id, exam_id) {
317            (None, None) => Err(ModelError::new(
318                ModelErrorType::Generic,
319                "Expected either course or exam id, but neither were provided.",
320                None,
321            )),
322            (Some(course_id), None) => Ok(Self::Course(course_id)),
323            (None, Some(exam_id)) => Ok(Self::Exam(exam_id)),
324            (Some(_), Some(_)) => Err(ModelError::new(
325                ModelErrorType::Generic,
326                "Expected either course or exam id, but both were provided.",
327                None,
328            )),
329        }
330    }
331
332    pub fn to_course_and_exam_ids(&self) -> (Option<Uuid>, Option<Uuid>) {
333        match self {
334            CourseOrExamId::Course(course_id) => (Some(*course_id), None),
335            CourseOrExamId::Exam(exam_id) => (None, Some(*exam_id)),
336        }
337    }
338}
339
340impl TryFrom<UserExerciseState> for CourseOrExamId {
341    type Error = ModelError;
342
343    fn try_from(user_exercise_state: UserExerciseState) -> Result<Self, Self::Error> {
344        Self::from_course_and_exam_ids(user_exercise_state.course_id, user_exercise_state.exam_id)
345    }
346}
347
348impl TryFrom<&UserExerciseState> for CourseOrExamId {
349    type Error = ModelError;
350
351    fn try_from(user_exercise_state: &UserExerciseState) -> Result<Self, Self::Error> {
352        Self::from_course_and_exam_ids(user_exercise_state.course_id, user_exercise_state.exam_id)
353    }
354}
355
356impl TryFrom<Exercise> for CourseOrExamId {
357    type Error = ModelError;
358
359    fn try_from(exercise: Exercise) -> Result<Self, Self::Error> {
360        Self::from_course_and_exam_ids(exercise.course_id, exercise.exam_id)
361    }
362}
363
364impl TryFrom<&Exercise> for CourseOrExamId {
365    type Error = ModelError;
366
367    fn try_from(exercise: &Exercise) -> Result<Self, Self::Error> {
368        Self::from_course_and_exam_ids(exercise.course_id, exercise.exam_id)
369    }
370}