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