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