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