headless_lms_server/controllers/
langs.rs1use crate::controllers::helpers::file_uploading;
6use crate::domain::langs::{convert::Convert, 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_utils::file_store::file_utils;
13use models::CourseOrExamId;
14use models::chapters::DatabaseChapter;
15use models::library::grading::{StudentExerciseSlideSubmission, StudentExerciseTaskSubmission};
16use mooc_langs_api as api;
17use std::collections::HashSet;
18
19#[instrument(skip(pool))]
25async fn get_course_instances(
26 pool: web::Data<PgPool>,
27 user: AuthToken,
28) -> ControllerResult<web::Json<Vec<api::CourseInstance>>> {
29 let mut conn = pool.acquire().await?;
30
31 let course_instances =
32 models::course_instances::get_enrolled_course_instances_for_user_with_exercise_type(
33 &mut conn, user.id, "tmc",
34 )
35 .await?
36 .convert();
37
38 let token = skip_authorize();
40 token.authorized_ok(web::Json(course_instances))
41}
42
43#[instrument(skip(pool))]
49async fn get_course_instance(
50 pool: web::Data<PgPool>,
51 user: AuthToken,
52 course_instance: web::Path<Uuid>,
53) -> ControllerResult<web::Json<api::CourseInstance>> {
54 let mut conn = pool.acquire().await?;
55 let token = authorize(
56 &mut conn,
57 Act::View,
58 Some(user.id),
59 Res::CourseInstance(*course_instance),
60 )
61 .await?;
62
63 let course_instance =
64 models::course_instances::get_course_instance_with_info(&mut conn, *course_instance)
65 .await?
66 .convert();
67
68 token.authorized_ok(web::Json(course_instance))
69}
70
71#[instrument(skip(pool))]
80async fn get_course_instance_exercises(
81 pool: web::Data<PgPool>,
82 user: AuthToken,
83 course_instance: web::Path<Uuid>,
84) -> ControllerResult<web::Json<Vec<api::ExerciseSlide>>> {
85 let mut conn = pool.acquire().await?;
86 let token = authorize(
87 &mut conn,
88 Act::View,
89 Some(user.id),
90 Res::CourseInstance(*course_instance),
91 )
92 .await?;
93
94 let mut slides = Vec::new();
95 let open_chapter_ids = models::chapters::course_instance_chapters(&mut conn, *course_instance)
97 .await?
98 .into_iter()
99 .filter(DatabaseChapter::has_opened)
100 .map(|c| c.id)
101 .collect::<HashSet<_>>();
102
103 let course_instance =
104 models::course_instances::get_course_instance(&mut conn, *course_instance).await?;
105 let open_chapter_exercises =
106 models::exercises::get_exercises_by_course_id(&mut conn, course_instance.course_id)
107 .await?
108 .into_iter()
109 .filter(|e| {
110 e.chapter_id
111 .map(|ci| open_chapter_ids.contains(&ci))
112 .unwrap_or_default()
113 });
114 for open_exercise in open_chapter_exercises {
115 let (slide, _) = models::exercises::get_or_select_exercise_slide(
116 &mut conn,
117 Some(user.id),
118 &open_exercise,
119 models_requests::fetch_service_info,
120 )
121 .await?;
122 let tasks: Vec<api::ExerciseTask> = slide
123 .exercise_tasks
124 .into_iter()
125 .filter(|et| et.exercise_service_slug == "tmc")
127 .map(|mut et| {
129 et.model_solution_spec = None;
130 et
131 })
132 .map(Convert::convert)
133 .collect();
134 if !tasks.is_empty() {
136 slides.push(api::ExerciseSlide {
137 slide_id: slide.id,
138 exercise_id: open_exercise.id,
139 exercise_name: open_exercise.name,
140 exercise_order_number: open_exercise.order_number,
141 deadline: open_exercise.deadline,
142 tasks,
143 });
144 }
145 }
146
147 token.authorized_ok(web::Json(slides))
148}
149
150#[instrument(skip(pool))]
158async fn get_exercise(
159 pool: web::Data<PgPool>,
160 user: AuthToken,
161 exercise_id: web::Path<Uuid>,
162) -> ControllerResult<web::Json<api::ExerciseSlide>> {
163 let mut conn = pool.acquire().await?;
164 let token = authorize(
165 &mut conn,
166 Act::View,
167 Some(user.id),
168 Res::Exercise(*exercise_id),
169 )
170 .await?;
171
172 let exercise = models::exercises::get_by_id(&mut conn, *exercise_id).await?;
173 let (exercise_slide, instance_or_exam_id) = models::exercises::get_or_select_exercise_slide(
174 &mut conn,
175 Some(user.id),
176 &exercise,
177 models_requests::fetch_service_info,
178 )
179 .await?;
180 match instance_or_exam_id {
181 Some(CourseOrExamId::Course(_id)) => {}
182 _ => {
183 return Err(ControllerError::new(
184 ControllerErrorType::BadRequest,
185 "User is not enrolled to this exercise's course".to_string(),
186 None,
187 ));
188 }
189 }
190
191 token.authorized_ok(web::Json(api::ExerciseSlide {
192 slide_id: exercise_slide.id,
193 exercise_id: exercise.id,
194 exercise_name: exercise.name,
195 exercise_order_number: exercise.order_number,
196 deadline: exercise.deadline,
197 tasks: exercise_slide
198 .exercise_tasks
199 .into_iter()
200 .map(Convert::convert)
201 .collect(),
202 }))
203}
204
205#[derive(MultipartForm)]
206struct SubmissionForm {
207 submission: MultipartJson<api::ExerciseSlideSubmission>,
208 file: TempFile,
209}
210
211async fn submit_exercise(
217 pool: web::Data<PgPool>,
218 file_store: web::Data<dyn FileStore>,
219 jwt_key: web::Data<JwtKey>,
220 exercise_id: web::Path<Uuid>,
221 submission: MultipartForm<SubmissionForm>,
222 user: AuthToken,
223 app_conf: web::Data<ApplicationConfiguration>,
224) -> ControllerResult<web::Json<api::ExerciseTaskSubmissionResult>> {
225 let mut conn = pool.acquire().await?;
226 let token = authorize(
227 &mut conn,
228 Act::View,
229 Some(user.id),
230 Res::Exercise(*exercise_id),
231 )
232 .await?;
233
234 let submission_form = submission.into_inner();
236 let submission = submission_form.submission.into_inner();
237 let temp_file = submission_form.file;
238 let exercise = models::exercises::get_by_id(&mut conn, *exercise_id).await?;
239 let course_id = exercise
240 .course_id
241 .ok_or_else(|| anyhow::anyhow!("Cannot answer non-course exercises"))?;
242 let exercise_slide =
243 models::exercise_slides::get_exercise_slide(&mut conn, submission.exercise_slide_id)
244 .await?;
245 let exercise_task =
246 models::exercise_tasks::get_exercise_task_by_id(&mut conn, submission.exercise_task_id)
247 .await?;
248
249 let file = temp_file.file.into_file();
251 let mime = temp_file
252 .content_type
253 .ok_or_else(|| anyhow::anyhow!("Missing content-type header"))?;
254 let contents = file_utils::file_to_payload(file);
255 let (_upload_id, upload_path) = file_uploading::upload_exercise_archive(
256 &mut conn,
257 contents,
258 file_store.as_ref(),
259 file_uploading::ExerciseTaskInfo {
260 course_id,
261 exercise: &exercise,
262 exercise_slide: &exercise_slide,
263 exercise_task: &exercise_task,
264 },
265 mime,
266 user.id,
267 )
268 .await?;
269
270 let download_url = file_store.get_download_url(&upload_path, app_conf.as_ref());
272 let data_json = serde_json::json!({
274 "type": "editor",
275 "archive_download_url": download_url
276 });
277 let result = domain::exercises::process_submission(
278 &mut conn,
279 user.id,
280 exercise,
281 &StudentExerciseSlideSubmission {
282 exercise_slide_id: submission.exercise_slide_id,
283 exercise_task_submissions: vec![StudentExerciseTaskSubmission {
284 exercise_task_id: submission.exercise_task_id,
285 data_json,
286 }],
287 },
288 jwt_key.into_inner(),
289 )
290 .await?;
291
292 let submission = result
294 .exercise_task_submission_results
295 .into_iter()
296 .next()
297 .ok_or_else(|| {
298 ControllerError::new(
299 ControllerErrorType::InternalServerError,
300 "Failed to find exercise task submission id".to_string(),
301 None,
302 )
303 })?;
304 let result = api::ExerciseTaskSubmissionResult {
305 submission_id: submission.submission.id,
306 };
307 token.authorized_ok(web::Json(result))
308}
309
310async fn get_submission_grading(
311 pool: web::Data<PgPool>,
312 submission_id: web::Path<Uuid>,
313 user: AuthToken,
314) -> ControllerResult<web::Json<api::ExerciseTaskSubmissionStatus>> {
315 let mut conn = pool.acquire().await?;
316 let token = authorize(
317 &mut conn,
318 Act::View,
319 Some(user.id),
320 Res::ExerciseTaskSubmission(*submission_id),
321 )
322 .await?;
323
324 let grading = models::exercise_task_gradings::get_by_exercise_task_submission_id(
325 &mut conn,
326 *submission_id,
327 )
328 .await?;
329 let status = match grading {
330 Some(grading) => api::ExerciseTaskSubmissionStatus::Grading {
331 grading_progress: grading.grading_progress.convert(),
332 score_given: grading.score_given,
333 grading_started_at: grading.grading_started_at,
334 grading_completed_at: grading.grading_completed_at,
335 feedback_json: grading.feedback_json,
336 feedback_text: grading.feedback_text,
337 },
338 None => api::ExerciseTaskSubmissionStatus::NoGradingYet,
339 };
340 token.authorized_ok(web::Json(status))
341}
342
343pub fn _add_routes(cfg: &mut ServiceConfig) {
344 cfg.route("/course-instances", web::get().to(get_course_instances))
345 .route("/course-instance/{id}", web::get().to(get_course_instance))
346 .route(
347 "/course-instances/{id}/exercises",
348 web::get().to(get_course_instance_exercises),
349 )
350 .route("/exercises/{id}", web::get().to(get_exercise))
351 .route("/exercises/{id}/submit", web::post().to(submit_exercise))
352 .route(
353 "/submissions/{id}/grading",
354 web::get().to(get_submission_grading),
355 );
356}