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 pub internal_url: Option<String>,
21 pub max_reprocessing_submissions_at_once: i32,
22}
23
24#[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 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 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 url.set_path("");
170 Ok(url)
171}
172
173pub 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
185pub 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}