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