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