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