Skip to main content

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