Skip to main content

headless_lms_models/
exercise_services.rs

1use url::Url;
2use utoipa::ToSchema;
3
4use crate::{
5    exercise_service_info::{ExerciseServiceInfo, get_all_exercise_services_by_type},
6    prelude::*,
7};
8
9#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
10
11pub struct ExerciseService {
12    pub id: Uuid,
13    pub created_at: DateTime<Utc>,
14    pub updated_at: DateTime<Utc>,
15    pub deleted_at: Option<DateTime<Utc>>,
16    pub name: String,
17    pub slug: String,
18    pub public_url: String,
19    /// This is needed because connecting to services directly inside the cluster with a special url is much for efficient than connecting to the same service with a url that would get routed though the internet. If not defined, use we can reach the service with the public url.
20    pub internal_url: Option<String>,
21    pub max_reprocessing_submissions_at_once: i32,
22}
23
24/// Exercise service definition that the CMS can use to render the editor view.
25#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
26
27pub struct ExerciseServiceIframeRenderingInfo {
28    pub id: Uuid,
29    pub name: String,
30    pub slug: String,
31    pub public_iframe_url: String,
32    // #[serde(skip_serializing_if = "Option::is_none")]
33    pub has_custom_view: bool,
34}
35
36#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
37
38pub struct ExerciseServiceNewOrUpdate {
39    pub name: String,
40    pub slug: String,
41    pub public_url: String,
42    pub internal_url: Option<String>,
43    pub max_reprocessing_submissions_at_once: i32,
44}
45
46pub async fn get_exercise_service(
47    conn: &mut PgConnection,
48    id: Uuid,
49) -> ModelResult<ExerciseService> {
50    let res = sqlx::query_as!(
51        ExerciseService,
52        r#"
53SELECT *
54FROM exercise_services
55WHERE id = $1
56  "#,
57        id
58    )
59    .fetch_one(conn)
60    .await?;
61    Ok(res)
62}
63
64pub async fn update_exercise_service(
65    conn: &mut PgConnection,
66    id: Uuid,
67    exercise_service_update: &ExerciseServiceNewOrUpdate,
68) -> ModelResult<ExerciseService> {
69    let res = sqlx::query_as!(
70        ExerciseService,
71        r#"
72UPDATE exercise_services
73    SET name=$1, slug=$2, public_url=$3, internal_url=$4, max_reprocessing_submissions_at_once=$5
74WHERE id=$6
75    RETURNING *
76        "#,
77        exercise_service_update.name,
78        exercise_service_update.slug,
79        exercise_service_update.public_url,
80        exercise_service_update.internal_url,
81        exercise_service_update.max_reprocessing_submissions_at_once,
82        id
83    )
84    .fetch_one(conn)
85    .await?;
86    Ok(res)
87}
88
89pub async fn delete_exercise_service(
90    conn: &mut PgConnection,
91    id: Uuid,
92) -> ModelResult<ExerciseService> {
93    let deleted = sqlx::query_as!(
94        ExerciseService,
95        r#"
96UPDATE exercise_services
97    SET deleted_at = now()
98WHERE id = $1
99AND deleted_at IS NULL
100    RETURNING *
101        "#,
102        id
103    )
104    .fetch_one(conn)
105    .await?;
106    Ok(deleted)
107}
108
109pub async fn get_exercise_service_by_exercise_type(
110    conn: &mut PgConnection,
111    exercise_type: &str,
112) -> ModelResult<ExerciseService> {
113    let res = sqlx::query_as!(
114        ExerciseService,
115        r#"
116SELECT *
117FROM exercise_services
118WHERE slug = $1
119AND deleted_at IS NULL
120  "#,
121        exercise_type
122    )
123    .fetch_one(conn)
124    .await?;
125    Ok(res)
126}
127
128pub async fn get_exercise_service_internally_preferred_baseurl_by_exercise_type(
129    conn: &mut PgConnection,
130    exercise_type: &str,
131) -> ModelResult<Url> {
132    let exercise_service = get_exercise_service_by_exercise_type(conn, exercise_type).await?;
133    get_exercise_service_internally_preferred_baseurl(&exercise_service)
134}
135
136pub fn get_exercise_service_internally_preferred_baseurl(
137    exercise_service: &ExerciseService,
138) -> ModelResult<Url> {
139    let stored_url_str = exercise_service
140        .internal_url
141        .as_ref()
142        .unwrap_or(&exercise_service.public_url);
143    let mut url = Url::parse(stored_url_str).map_err(|original_error| {
144        ModelError::new(
145            ModelErrorType::Generic,
146            original_error.to_string(),
147            Some(original_error.into()),
148        )
149    })?;
150    // remove the path because all relative urls in service info assume
151    // that the base url prefix has no path
152    url.set_path("");
153    Ok(url)
154}
155
156pub fn get_exercise_service_externally_preferred_baseurl(
157    exercise_service: &ExerciseService,
158) -> ModelResult<Url> {
159    let stored_url_str = &exercise_service.public_url;
160    let mut url = Url::parse(stored_url_str).map_err(|original_error| {
161        ModelError::new(
162            ModelErrorType::Generic,
163            original_error.to_string(),
164            Some(original_error.into()),
165        )
166    })?;
167    // remove the path because all relative urls in service info assume
168    // that the base url prefix has no path
169    url.set_path("");
170    Ok(url)
171}
172
173/**
174Returns a url that can be used to grade a submission for this exercise service.
175*/
176pub async fn get_internal_grade_url(
177    exercise_service: &ExerciseService,
178    exercise_service_info: &ExerciseServiceInfo,
179) -> ModelResult<Url> {
180    let mut url = get_exercise_service_internally_preferred_baseurl(exercise_service)?;
181    url.set_path(&exercise_service_info.grade_endpoint_path);
182    Ok(url)
183}
184
185/**
186Returns a url that can be used to generate a public version of a private spec.
187*/
188pub fn get_internal_public_spec_url(
189    exercise_service: &ExerciseService,
190    exercise_service_info: &ExerciseServiceInfo,
191) -> ModelResult<Url> {
192    let mut url = get_exercise_service_internally_preferred_baseurl(exercise_service)?;
193    url.set_path(&exercise_service_info.public_spec_endpoint_path);
194    Ok(url)
195}
196
197pub fn get_model_solution_url(
198    exercise_service: &ExerciseService,
199    exercise_service_info: &ExerciseServiceInfo,
200) -> ModelResult<Url> {
201    let mut url = get_exercise_service_internally_preferred_baseurl(exercise_service)?;
202    url.set_path(&exercise_service_info.model_solution_spec_endpoint_path);
203    Ok(url)
204}
205
206pub async fn get_exercise_services(conn: &mut PgConnection) -> ModelResult<Vec<ExerciseService>> {
207    let res = sqlx::query_as!(
208        ExerciseService,
209        r#"
210SELECT *
211FROM exercise_services
212WHERE deleted_at IS NULL
213"#
214    )
215    .fetch_all(conn)
216    .await?;
217    Ok(res)
218}
219
220pub async fn get_all_exercise_services_iframe_rendering_infos(
221    conn: &mut PgConnection,
222) -> ModelResult<Vec<ExerciseServiceIframeRenderingInfo>> {
223    let services = get_exercise_services(conn).await?;
224    let service_infos = get_all_exercise_services_by_type(conn).await?;
225    let res = services
226        .into_iter()
227        .filter_map(|exercise_service| {
228            if let Some((_, service_info)) = service_infos.get(&exercise_service.slug) {
229                match get_exercise_service_externally_preferred_baseurl(&exercise_service) { Ok(mut url) => {
230                    url.set_path(&service_info.user_interface_iframe_path);
231                    Some(ExerciseServiceIframeRenderingInfo {
232                        id: exercise_service.id,
233                        name: exercise_service.name,
234                        slug: exercise_service.slug,
235                        public_iframe_url: url.to_string(),
236                        has_custom_view: service_info.has_custom_view,
237                    })
238                } _ => {
239                    warn!(exercise_service_id = ?exercise_service.id, "Skipping exercise service from the list because it has an invalid base url");
240                    None
241                }}
242
243            } else {
244                warn!(exercise_service_id = ?exercise_service.id, "Skipping exercise service from the list because it doesn't have a service info");
245                None
246            }
247        })
248        .collect::<Vec<_>>();
249    Ok(res)
250}
251
252pub async fn insert_exercise_service(
253    conn: &mut PgConnection,
254    exercise_service_update: &ExerciseServiceNewOrUpdate,
255) -> ModelResult<ExerciseService> {
256    let res = sqlx::query_as!(
257        ExerciseService,
258        r#"
259INSERT INTO exercise_services (
260    name,
261    slug,
262    public_url,
263    internal_url,
264    max_reprocessing_submissions_at_once
265  )
266VALUES ($1, $2, $3, $4, $5)
267RETURNING *
268  "#,
269        exercise_service_update.name,
270        exercise_service_update.slug,
271        exercise_service_update.public_url,
272        exercise_service_update.internal_url,
273        exercise_service_update.max_reprocessing_submissions_at_once
274    )
275    .fetch_one(conn)
276    .await?;
277    Ok(res)
278}