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