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