1use std::collections::HashMap;
2
3use futures::future::BoxFuture;
4use url::Url;
5
6use crate::{
7 exercise_services::{
8 ExerciseService, get_exercise_service_by_exercise_type, get_exercise_services,
9 },
10 prelude::*,
11};
12
13#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
14pub struct ExerciseServiceInfo {
15 pub exercise_service_id: Uuid,
16 pub created_at: DateTime<Utc>,
17 pub updated_at: DateTime<Utc>,
18 pub user_interface_iframe_path: String,
19 pub grade_endpoint_path: String,
20 pub public_spec_endpoint_path: String,
21 pub model_solution_spec_endpoint_path: String,
22 pub has_custom_view: bool,
24 pub csv_export_definitions_endpoint_path: Option<String>,
25 pub csv_export_answers_endpoint_path: Option<String>,
26}
27
28#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
29pub struct PathInfo {
30 pub exercise_service_id: Uuid,
31 pub user_interface_iframe_path: String,
32 pub grade_endpoint_path: String,
33 pub public_spec_endpoint_path: String,
34 pub model_solution_spec_endpoint_path: String,
35 pub has_custom_view: bool,
37}
38
39#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
40#[cfg_attr(feature = "ts_rs", derive(TS))]
41pub struct CourseMaterialExerciseServiceInfo {
42 pub exercise_iframe_url: String,
43}
44
45#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
46#[cfg_attr(feature = "ts_rs", derive(TS))]
47pub struct ExerciseServiceInfoApi {
48 pub service_name: String,
49 pub user_interface_iframe_path: String,
50 pub grade_endpoint_path: String,
51 pub public_spec_endpoint_path: String,
52 pub model_solution_spec_endpoint_path: String,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub has_custom_view: Option<bool>,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub csv_export_definitions_endpoint_path: Option<String>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub csv_export_answers_endpoint_path: Option<String>,
59}
60
61pub async fn insert(
62 conn: &mut PgConnection,
63 exercise_service_info: &PathInfo,
64) -> ModelResult<ExerciseServiceInfo> {
65 let res = sqlx::query_as!(
66 ExerciseServiceInfo,
67 "
68INSERT INTO exercise_service_info (
69 exercise_service_id,
70 user_interface_iframe_path,
71 grade_endpoint_path,
72 public_spec_endpoint_path,
73 model_solution_spec_endpoint_path,
74 has_custom_view
75 )
76VALUES ($1, $2, $3, $4, $5, $6)
77RETURNING *
78",
79 exercise_service_info.exercise_service_id,
80 exercise_service_info.user_interface_iframe_path,
81 exercise_service_info.grade_endpoint_path,
82 exercise_service_info.public_spec_endpoint_path,
83 exercise_service_info.model_solution_spec_endpoint_path,
84 exercise_service_info.has_custom_view
85 )
86 .fetch_one(conn)
87 .await?;
88 Ok(res)
89}
90
91pub async fn fetch_and_upsert_service_info(
92 conn: &mut PgConnection,
93 exercise_service: &ExerciseService,
94 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
95) -> ModelResult<ExerciseServiceInfo> {
96 let url = match exercise_service
97 .internal_url
98 .clone()
99 .map(|url| Url::parse(&url))
100 {
101 Some(Ok(url)) => url.to_string(),
102
103 Some(Err(e)) => {
104 warn!(
105 "Internal_url provided for {} is not a valid url. Using public_url instead. Error: {}",
106 exercise_service.name,
107 e.to_string()
108 );
109 exercise_service.public_url.clone()
110 }
111 None => exercise_service.public_url.clone(),
112 };
113 let fetched_info = fetch_service_info(url.parse()?).await?;
114 let res = upsert_service_info(conn, exercise_service.id, &fetched_info).await?;
115 Ok(res)
116}
117
118pub async fn upsert_service_info(
119 conn: &mut PgConnection,
120 exercise_service_id: Uuid,
121 update: &ExerciseServiceInfoApi,
122) -> ModelResult<ExerciseServiceInfo> {
123 let res = sqlx::query_as!(
124 ExerciseServiceInfo,
125 r#"
126INSERT INTO exercise_service_info(
127 exercise_service_id,
128 user_interface_iframe_path,
129 grade_endpoint_path,
130 public_spec_endpoint_path,
131 model_solution_spec_endpoint_path,
132 has_custom_view,
133 csv_export_definitions_endpoint_path,
134 csv_export_answers_endpoint_path
135 )
136VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
137ON CONFLICT(exercise_service_id) DO UPDATE
138SET user_interface_iframe_path = $2,
139 grade_endpoint_path = $3,
140 public_spec_endpoint_path = $4,
141 model_solution_spec_endpoint_path = $5,
142 has_custom_view = $6,
143 csv_export_definitions_endpoint_path = $7,
144 csv_export_answers_endpoint_path = $8
145RETURNING *
146 "#,
147 exercise_service_id,
148 update.user_interface_iframe_path,
149 update.grade_endpoint_path,
150 update.public_spec_endpoint_path,
151 update.model_solution_spec_endpoint_path,
152 update.has_custom_view.unwrap_or_else(|| false),
153 update.csv_export_definitions_endpoint_path.as_deref(),
154 update.csv_export_answers_endpoint_path.as_deref()
155 )
156 .fetch_one(conn)
157 .await?;
158 Ok(res)
159}
160
161pub async fn get_service_info(
162 conn: &mut PgConnection,
163 exercise_service_id: Uuid,
164) -> ModelResult<ExerciseServiceInfo> {
165 let res = sqlx::query_as!(
166 ExerciseServiceInfo,
167 r#"
168SELECT *
169FROM exercise_service_info
170WHERE exercise_service_id = $1
171 "#,
172 exercise_service_id
173 )
174 .fetch_one(conn)
175 .await?;
176 Ok(res)
177}
178
179pub async fn get_service_info_by_exercise_type(
180 conn: &mut PgConnection,
181 exercise_type: &str,
182 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
183) -> ModelResult<ExerciseServiceInfo> {
184 let exercise_service = get_exercise_service_by_exercise_type(conn, exercise_type).await?;
185 let service_info =
186 get_upsert_service_info_by_exercise_service(conn, &exercise_service, fetch_service_info)
187 .await?;
188 Ok(service_info)
189}
190
191pub async fn get_all_exercise_services_by_type(
192 conn: &mut PgConnection,
193) -> ModelResult<HashMap<String, (ExerciseService, ExerciseServiceInfo)>> {
194 let mut exercise_services_by_type = HashMap::new();
195 for exercise_service in get_exercise_services(conn).await? {
196 match get_service_info_by_exercise_service(conn, &exercise_service).await {
197 Ok(Some(info)) => {
198 exercise_services_by_type
199 .insert(exercise_service.slug.clone(), (exercise_service, info));
200 }
201 _ => {
202 tracing::error!(
203 "No corresponding service info found for {} ({})",
204 exercise_service.name,
205 exercise_service.id
206 );
207 }
208 }
209 }
210 Ok(exercise_services_by_type)
211}
212
213pub async fn get_upsert_all_exercise_services_by_type(
214 conn: &mut PgConnection,
215 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
216) -> ModelResult<HashMap<String, (ExerciseService, ExerciseServiceInfo)>> {
217 let mut exercise_services_by_type = HashMap::new();
218 for exercise_service in get_exercise_services(conn).await? {
219 match get_upsert_service_info_by_exercise_service(
220 conn,
221 &exercise_service,
222 &fetch_service_info,
223 )
224 .await
225 {
226 Ok(info) => {
227 exercise_services_by_type
228 .insert(exercise_service.slug.clone(), (exercise_service, info));
229 }
230 _ => {
231 tracing::error!(
232 "No corresponding service info found for {} ({})",
233 exercise_service.name,
234 exercise_service.id
235 );
236 }
237 }
238 }
239 Ok(exercise_services_by_type)
240}
241
242pub async fn get_selected_exercise_services_by_type(
243 conn: &mut PgConnection,
244 slugs: &[String],
245 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
246) -> ModelResult<HashMap<String, (ExerciseService, ExerciseServiceInfo)>> {
247 let selected_services = sqlx::query_as!(
248 ExerciseService,
249 "
250SELECT *
251FROM exercise_services
252WHERE slug = ANY($1);",
253 slugs,
254 )
255 .fetch_all(&mut *conn)
256 .await?;
257 let mut exercise_services_by_type = HashMap::new();
258 for exercise_service in selected_services {
259 let info = get_upsert_service_info_by_exercise_service(
260 conn,
261 &exercise_service,
262 &fetch_service_info,
263 )
264 .await?;
265 exercise_services_by_type.insert(exercise_service.slug.clone(), (exercise_service, info));
266 }
267 Ok(exercise_services_by_type)
268}
269
270pub async fn get_upsert_service_info_by_exercise_service(
271 conn: &mut PgConnection,
272 exercise_service: &ExerciseService,
273 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
274) -> ModelResult<ExerciseServiceInfo> {
275 let res = get_service_info(conn, exercise_service.id).await;
276 let service_info = match res {
277 Ok(exercise_service_info) => exercise_service_info,
278 _ => {
279 warn!(
280 "Could not find service info for {} ({}). This is rare and only should happen when a background worker has not had the opportunity to complete their fetching task yet. Trying the fetching here in this worker so that we can continue.",
281 exercise_service.name, exercise_service.slug
282 );
283
284 fetch_and_upsert_service_info(conn, exercise_service, fetch_service_info).await?
285 }
286 };
287 Ok(service_info)
288}
289
290pub async fn get_service_info_by_exercise_service(
291 conn: &mut PgConnection,
292 exercise_service: &ExerciseService,
293) -> ModelResult<Option<ExerciseServiceInfo>> {
294 let res = get_service_info(conn, exercise_service.id).await;
295 let service_info = match res {
296 Ok(exercise_service_info) => exercise_service_info,
297 _ => {
298 warn!(
299 "Could not find service info for {} ({}). This is rare and only should happen when a background worker has not had the opportunity to complete their fetching task yet.",
300 exercise_service.name, exercise_service.slug
301 );
302 return Ok(None);
303 }
304 };
305 Ok(Some(service_info))
306}
307
308pub async fn get_course_material_service_info_by_exercise_type(
313 conn: &mut PgConnection,
314 exercise_type: &str,
315 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
316) -> ModelResult<Option<CourseMaterialExerciseServiceInfo>> {
317 match get_exercise_service_by_exercise_type(conn, exercise_type).await {
318 Ok(exercise_service) => {
319 let full_service_info = get_upsert_service_info_by_exercise_service(
320 conn,
321 &exercise_service,
322 fetch_service_info,
323 )
324 .await;
325 let service_info_option = match full_service_info {
326 Ok(o) => {
327 let mut url =
331 Url::parse(&exercise_service.public_url).map_err(|original_err| {
332 ModelError::new(
333 ModelErrorType::Generic,
334 original_err.to_string(),
335 Some(original_err.into()),
336 )
337 })?;
338 url.set_path(&o.user_interface_iframe_path);
339 url.set_query(None);
340 url.set_fragment(None);
341
342 Some(CourseMaterialExerciseServiceInfo {
343 exercise_iframe_url: url.to_string(),
344 })
345 }
346 _ => None,
347 };
348
349 Ok(service_info_option)
350 }
351 _ => Ok(None),
352 }
353}