Skip to main content

headless_lms_models/
page_language_groups.rs

1use std::collections::HashMap;
2
3use crate::prelude::*;
4
5#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
6pub struct PageLanguageGroup {
7    pub id: Uuid,
8    pub created_at: DateTime<Utc>,
9    pub updated_at: DateTime<Utc>,
10    pub deleted_at: Option<DateTime<Utc>>,
11    pub course_language_group_id: Uuid,
12}
13
14pub async fn insert(
15    conn: &mut PgConnection,
16    pkey_policy: PKeyPolicy<Uuid>,
17    course_language_group_id: Uuid,
18) -> ModelResult<Uuid> {
19    let res = sqlx::query!(
20        "
21INSERT INTO page_language_groups (
22  id,
23  course_language_group_id
24  )
25VALUES ($1, $2)
26RETURNING *
27        ",
28        pkey_policy.into_uuid(),
29        course_language_group_id
30    )
31    .fetch_one(conn)
32    .await?;
33    Ok(res.id)
34}
35
36/// Why a page can't be paired with another course's page through its page language group.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum PairingRefusal {
39    /// No page with the id, or it is deleted.
40    NotPublished,
41    /// The page isn't in a course of the course language group, or has no page language group.
42    NotInCourseLanguageGroup,
43}
44
45/// The page language group a page of a course in `course_language_group_id` joins to pair with
46/// `source_page_id`, a published page of another course in that course language group.
47///
48/// Doesn't check that the source is in another course; a course's own page passes.
49pub async fn resolve_pairing(
50    conn: &mut PgConnection,
51    source_page_id: Uuid,
52    course_language_group_id: Uuid,
53) -> ModelResult<Result<Uuid, PairingRefusal>> {
54    let source = sqlx::query!(
55        r#"
56SELECT p.page_language_group_id,
57  c.course_language_group_id AS "course_language_group_id?"
58FROM pages p
59  LEFT JOIN courses c ON c.id = p.course_id
60  AND c.deleted_at IS NULL
61WHERE p.id = $1
62  AND p.deleted_at IS NULL
63        "#,
64        source_page_id
65    )
66    .fetch_optional(conn)
67    .await?;
68    let Some(source) = source else {
69        return Ok(Err(PairingRefusal::NotPublished));
70    };
71    Ok(source
72        .page_language_group_id
73        .filter(|_| source.course_language_group_id == Some(course_language_group_id))
74        .ok_or(PairingRefusal::NotInCourseLanguageGroup))
75}
76
77/// Whether the course has a page other than `page_id` in page language group `id`.
78///
79/// Locks the group until the transaction ends, so concurrent publishes pairing pages with it see
80/// each other's pages; call it after inserting `page_id`.
81pub async fn course_has_other_page(
82    conn: &mut PgConnection,
83    id: Uuid,
84    course_id: Uuid,
85    page_id: Uuid,
86) -> ModelResult<bool> {
87    // Not FOR UPDATE: callers' inserted pages hold KEY SHARE on the group through their foreign
88    // key, which FOR UPDATE waits for, deadlocking two publishes into the same group.
89    sqlx::query!(
90        "SELECT id FROM page_language_groups WHERE id = $1 FOR NO KEY UPDATE",
91        id
92    )
93    .fetch_optional(&mut *conn)
94    .await?;
95    let has_other = sqlx::query_scalar!(
96        r#"
97SELECT EXISTS (
98    SELECT 1
99    FROM pages
100    WHERE page_language_group_id = $1
101      AND course_id = $2
102      AND id <> $3
103      AND deleted_at IS NULL
104  ) AS "has_other!"
105        "#,
106        id,
107        course_id,
108        page_id
109    )
110    .fetch_one(conn)
111    .await?;
112    Ok(has_other)
113}
114
115pub struct PageLanguageGroupNavigationInfo {
116    pub page_language_group_id: Uuid,
117    pub page_id: Uuid,
118    pub course_id: Option<Uuid>,
119    pub exam_id: Option<Uuid>,
120    pub page_path: String,
121}
122
123/** Can be used to find the same page in all the different language versions of the course. Returns a mappgig from course_id or exam_id to the page language group navigation info. */
124pub async fn get_all_pages_in_page_language_group_mapping(
125    conn: &mut PgConnection,
126    page_id: Uuid,
127) -> ModelResult<HashMap<CourseOrExamId, PageLanguageGroupNavigationInfo>> {
128    let res = sqlx::query_as!(
129        PageLanguageGroupNavigationInfo,
130        "
131SELECT plg.id AS page_language_group_id,
132  p.id AS page_id,
133  p.url_path AS page_path,
134  p.course_id AS course_id,
135  p.exam_id AS exam_id
136FROM page_language_groups plg
137  JOIN pages p ON plg.id = p.page_language_group_id
138WHERE plg.id = (SELECT page_language_group_id FROM pages WHERE id = $1)
139  AND plg.deleted_at IS NULL
140  AND p.deleted_at IS NULL
141        ",
142        page_id,
143    )
144    .fetch_all(conn)
145    .await?;
146    let mut result = HashMap::new();
147    for x in res {
148        let key = CourseOrExamId::from_course_and_exam_ids(x.course_id, x.exam_id)?;
149        result.insert(key, x);
150    }
151    Ok(result)
152}
153
154/// The page language group of each of the course's pages that has one, by page id.
155pub async fn get_by_course_id(
156    conn: &mut PgConnection,
157    course_id: Uuid,
158) -> ModelResult<HashMap<Uuid, Uuid>> {
159    let pages = crate::pages::get_all_by_course_id_and_visibility(
160        conn,
161        course_id,
162        crate::pages::PageVisibility::Any,
163    )
164    .await?;
165    Ok(pages
166        .into_iter()
167        .filter_map(|page| Some((page.id, page.page_language_group_id?)))
168        .collect())
169}