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