headless_lms_server/controllers/
langs.rs1use crate::controllers::helpers::file_uploading;
6use crate::domain::langs::token::AuthToken;
7use crate::domain::models_requests::{self, JwtKey};
8use crate::prelude::*;
9use actix_multipart::form::MultipartForm;
10use actix_multipart::form::json::Json as MultipartJson;
11use actix_multipart::form::tempfile::TempFile;
12use headless_lms_models::exercises::GradingProgress;
13use headless_lms_utils::file_store::file_utils;
14use models::CourseOrExamId;
15use models::chapters::DatabaseChapter;
16use models::library::grading::{StudentExerciseSlideSubmission, StudentExerciseTaskSubmission};
17use mooc_langs_api as api;
18use std::collections::HashSet;
19
20#[instrument(skip(pool))]
26async fn get_courses(
27 pool: web::Data<PgPool>,
28 user: AuthToken,
29) -> ControllerResult<web::Json<Vec<api::Course>>> {
30 let mut conn = pool.acquire().await?;
31
32 let courses =
33 models::course_instances::get_enrolled_course_instances_for_user_with_exercise_type(
34 &mut conn, user.id, "tmc",
35 )
36 .await?
37 .into_iter()
38 .map(|ci| api::Course {
39 id: ci.course_id,
40 slug: ci.course_slug,
41 name: ci.course_name,
42 description: ci.course_description,
43 organization_name: ci.organization_name,
44 })
45 .collect();
46
47 let token = skip_authorize();
49 token.authorized_ok(web::Json(courses))
50}
51
52#[instrument(skip(pool))]
58async fn get_course(
59 pool: web::Data<PgPool>,
60 user: AuthToken,
61 course: web::Path<Uuid>,
62) -> ControllerResult<web::Json<api::Course>> {
63 let mut conn = pool.acquire().await?;
64 let token = authorize(&mut conn, Act::View, Some(user.id), Res::Course(*course)).await?;
65
66 let course = models::courses::get_course(&mut conn, *course).await?;
67 let org = models::organizations::get_organization(&mut conn, course.organization_id).await?;
68 let course = api::Course {
69 id: course.id,
70 slug: course.slug,
71 name: course.name,
72 description: course.description,
73 organization_name: org.name,
74 };
75
76 token.authorized_ok(web::Json(course))
77}
78
79#[instrument(skip(pool))]
88async fn get_course_exercises(
89 pool: web::Data<PgPool>,
90 user: AuthToken,
91 course: web::Path<Uuid>,
92) -> ControllerResult<web::Json<Vec<api::ExerciseSlide>>> {
93 let mut conn = pool.acquire().await?;
94 let token = authorize(&mut conn, Act::View, Some(user.id), Res::Course(*course)).await?;
95
96 let mut slides = Vec::new();
97 let open_chapter_ids = models::chapters::course_chapters(&mut conn, *course)
99 .await?
100 .into_iter()
101 .filter(DatabaseChapter::has_opened)
102 .map(|c| c.id)
103 .collect::<HashSet<_>>();
104
105 let course = models::courses::get_course(&mut conn, *course).await?;
106 let open_chapter_exercises =
107 models::exercises::get_exercises_by_course_id(&mut conn, course.id)
108 .await?
109 .into_iter()
110 .filter(|e| {
111 e.chapter_id
112 .map(|ci| open_chapter_ids.contains(&ci))
113 .unwrap_or_default()
114 });
115 for open_exercise in open_chapter_exercises {
116 let (slide, _) = models::exercises::get_or_select_exercise_slide(
117 &mut conn,
118 Some(user.id),
119 &open_exercise,
120 models_requests::fetch_service_info,
121 )
122 .await?;
123 let tasks: Vec<api::ExerciseTask> = slide
124 .exercise_tasks
125 .into_iter()
126 .filter(|et| et.exercise_service_slug == "tmc")
128 .map(|mut et| {
130 et.model_solution_spec = None;
131 et
132 })
133 .map(|et| api::ExerciseTask {
134 task_id: et.id,
135 order_number: et.order_number,
136 assignment: et.assignment,
137 public_spec: et.public_spec,
138 model_solution_spec: et.model_solution_spec,
139 exercise_service_slug: et.exercise_service_slug,
140 })
141 .collect();
142 if !tasks.is_empty() {
144 slides.push(api::ExerciseSlide {
145 slide_id: slide.id,
146 exercise_id: open_exercise.id,
147 exercise_name: open_exercise.name,
148 exercise_order_number: open_exercise.order_number,
149 deadline: open_exercise.deadline,
150 tasks,
151 });
152 }
153 }
154
155 token.authorized_ok(web::Json(slides))
156}
157
158#[instrument(skip(pool))]
166async fn get_exercise(
167 pool: web::Data<PgPool>,
168 user: AuthToken,
169 exercise_id: web::Path<Uuid>,
170) -> ControllerResult<web::Json<api::ExerciseSlide>> {
171 let mut conn = pool.acquire().await?;
172 let token = authorize(
173 &mut conn,
174 Act::View,
175 Some(user.id),
176 Res::Exercise(*exercise_id),
177 )
178 .await?;
179
180 let exercise = models::exercises::get_by_id(&mut conn, *exercise_id).await?;
181 let (exercise_slide, course_or_exam_id) = models::exercises::get_or_select_exercise_slide(
182 &mut conn,
183 Some(user.id),
184 &exercise,
185 models_requests::fetch_service_info,
186 )
187 .await?;
188 match course_or_exam_id {
189 Some(CourseOrExamId::Course(_id)) => {}
190 _ => {
191 return Err(ControllerError::new(
192 ControllerErrorType::BadRequest,
193 "User is not enrolled to this exercise's course".to_string(),
194 None,
195 ));
196 }
197 }
198
199 token.authorized_ok(web::Json(api::ExerciseSlide {
200 slide_id: exercise_slide.id,
201 exercise_id: exercise.id,
202 exercise_name: exercise.name,
203 exercise_order_number: exercise.order_number,
204 deadline: exercise.deadline,
205 tasks: exercise_slide
206 .exercise_tasks
207 .into_iter()
208 .map(|et| api::ExerciseTask {
209 task_id: et.id,
210 order_number: et.order_number,
211 assignment: et.assignment,
212 public_spec: et.public_spec,
213 model_solution_spec: et.model_solution_spec,
214 exercise_service_slug: et.exercise_service_slug,
215 })
216 .collect(),
217 }))
218}
219
220#[derive(MultipartForm)]
221struct SubmissionForm {
222 submission: MultipartJson<api::ExerciseSlideSubmission>,
223 file: TempFile,
224}
225
226async fn submit_exercise(
232 pool: web::Data<PgPool>,
233 file_store: web::Data<dyn FileStore>,
234 jwt_key: web::Data<JwtKey>,
235 exercise_id: web::Path<Uuid>,
236 submission: MultipartForm<SubmissionForm>,
237 user: AuthToken,
238 app_conf: web::Data<ApplicationConfiguration>,
239) -> ControllerResult<web::Json<api::ExerciseTaskSubmissionResult>> {
240 let mut conn = pool.acquire().await?;
241 let token = authorize(
242 &mut conn,
243 Act::View,
244 Some(user.id),
245 Res::Exercise(*exercise_id),
246 )
247 .await?;
248
249 let submission_form = submission.into_inner();
251 let submission = submission_form.submission.into_inner();
252 let temp_file = submission_form.file;
253 let exercise = models::exercises::get_by_id(&mut conn, *exercise_id).await?;
254 let course_id = exercise
255 .course_id
256 .ok_or_else(|| anyhow::anyhow!("Cannot answer non-course exercises"))?;
257 let exercise_slide =
258 models::exercise_slides::get_exercise_slide(&mut conn, submission.exercise_slide_id)
259 .await?;
260 let exercise_task =
261 models::exercise_tasks::get_exercise_task_by_id(&mut conn, submission.exercise_task_id)
262 .await?;
263
264 let file = temp_file.file.into_file();
266 let mime = temp_file
267 .content_type
268 .ok_or_else(|| anyhow::anyhow!("Missing content-type header"))?;
269 let contents = file_utils::file_to_payload(file);
270 let (_upload_id, upload_path) = file_uploading::upload_exercise_archive(
271 &mut conn,
272 contents,
273 file_store.as_ref(),
274 file_uploading::ExerciseTaskInfo {
275 course_id,
276 exercise: &exercise,
277 exercise_slide: &exercise_slide,
278 exercise_task: &exercise_task,
279 },
280 mime,
281 user.id,
282 )
283 .await?;
284
285 let download_url = file_store.get_download_url(&upload_path, app_conf.as_ref());
287 let data_json = serde_json::json!({
289 "type": "editor",
290 "archive_download_url": download_url
291 });
292 let result = domain::exercises::process_submission(
293 &mut conn,
294 user.id,
295 exercise,
296 &StudentExerciseSlideSubmission {
297 exercise_slide_id: submission.exercise_slide_id,
298 exercise_task_submissions: vec![StudentExerciseTaskSubmission {
299 exercise_task_id: submission.exercise_task_id,
300 data_json,
301 }],
302 },
303 jwt_key.into_inner(),
304 )
305 .await?;
306
307 let submission = result
309 .exercise_task_submission_results
310 .into_iter()
311 .next()
312 .ok_or_else(|| {
313 ControllerError::new(
314 ControllerErrorType::InternalServerError,
315 "Failed to find exercise task submission id".to_string(),
316 None,
317 )
318 })?;
319 let result = api::ExerciseTaskSubmissionResult {
320 submission_id: submission.submission.id,
321 };
322 token.authorized_ok(web::Json(result))
323}
324
325async fn get_submission_grading(
326 pool: web::Data<PgPool>,
327 submission_id: web::Path<Uuid>,
328 user: AuthToken,
329) -> ControllerResult<web::Json<api::ExerciseTaskSubmissionStatus>> {
330 let mut conn = pool.acquire().await?;
331 let token = authorize(
332 &mut conn,
333 Act::View,
334 Some(user.id),
335 Res::ExerciseTaskSubmission(*submission_id),
336 )
337 .await?;
338
339 let grading = models::exercise_task_gradings::get_by_exercise_task_submission_id(
340 &mut conn,
341 *submission_id,
342 )
343 .await?;
344 let status = match grading {
345 Some(grading) => api::ExerciseTaskSubmissionStatus::Grading {
346 grading_progress: match grading.grading_progress {
347 GradingProgress::Failed => api::GradingProgress::Failed,
348 GradingProgress::NotReady => api::GradingProgress::NotReady,
349 GradingProgress::PendingManual => api::GradingProgress::PendingManual,
350 GradingProgress::Pending => api::GradingProgress::Pending,
351 GradingProgress::FullyGraded => api::GradingProgress::FullyGraded,
352 },
353 score_given: grading.score_given,
354 grading_started_at: grading.grading_started_at,
355 grading_completed_at: grading.grading_completed_at,
356 feedback_json: grading.feedback_json,
357 feedback_text: grading.feedback_text,
358 },
359 None => api::ExerciseTaskSubmissionStatus::NoGradingYet,
360 };
361 token.authorized_ok(web::Json(status))
362}
363
364pub fn _add_routes(cfg: &mut ServiceConfig) {
365 cfg.route("/courses", web::get().to(get_courses))
366 .route("/courses/{id}", web::get().to(get_course))
367 .route(
368 "/courses/{id}/exercises",
369 web::get().to(get_course_exercises),
370 )
371 .route("/exercises/{id}", web::get().to(get_exercise))
372 .route("/exercises/{id}/submit", web::post().to(submit_exercise))
373 .route(
374 "/submissions/{id}/grading",
375 web::get().to(get_submission_grading),
376 );
377}