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