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