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::chapters::DatabaseChapter;
14use models::library::grading::{StudentExerciseSlideSubmission, StudentExerciseTaskSubmission};
15use models::user_exercise_states::CourseInstanceOrExamId;
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))]
52async fn get_course_instance_exercises(
53 pool: web::Data<PgPool>,
54 user: AuthToken,
55 course_instance: web::Path<Uuid>,
56) -> ControllerResult<web::Json<Vec<api::ExerciseSlide>>> {
57 let mut conn = pool.acquire().await?;
58 let token = authorize(
59 &mut conn,
60 Act::View,
61 Some(user.id),
62 Res::CourseInstance(*course_instance),
63 )
64 .await?;
65
66 let mut slides = Vec::new();
67 let open_chapter_ids = models::chapters::course_instance_chapters(&mut conn, *course_instance)
69 .await?
70 .into_iter()
71 .filter(DatabaseChapter::has_opened)
72 .map(|c| c.id)
73 .collect::<HashSet<_>>();
74 let open_chapter_exercises =
75 models::exercises::get_exercises_by_course_instance_id(&mut conn, *course_instance)
76 .await?
77 .into_iter()
78 .filter(|e| {
79 e.chapter_id
80 .map(|ci| open_chapter_ids.contains(&ci))
81 .unwrap_or_default()
82 });
83 for open_exercise in open_chapter_exercises {
84 let (slide, _) = models::exercises::get_or_select_exercise_slide(
85 &mut conn,
86 Some(user.id),
87 &open_exercise,
88 models_requests::fetch_service_info,
89 )
90 .await?;
91 let tasks: Vec<api::ExerciseTask> = slide
92 .exercise_tasks
93 .into_iter()
94 .filter(|et| et.exercise_service_slug == "tmc")
96 .map(|mut et| {
98 et.model_solution_spec = None;
99 et
100 })
101 .map(Convert::convert)
102 .collect();
103 if !tasks.is_empty() {
105 slides.push(api::ExerciseSlide {
106 slide_id: slide.id,
107 exercise_id: open_exercise.id,
108 exercise_name: open_exercise.name,
109 exercise_order_number: open_exercise.order_number,
110 deadline: open_exercise.deadline,
111 tasks,
112 });
113 }
114 }
115
116 token.authorized_ok(web::Json(slides))
117}
118
119#[instrument(skip(pool))]
127async fn get_exercise(
128 pool: web::Data<PgPool>,
129 user: AuthToken,
130 exercise_id: web::Path<Uuid>,
131) -> ControllerResult<web::Json<api::ExerciseSlide>> {
132 let mut conn = pool.acquire().await?;
133 let token = authorize(
134 &mut conn,
135 Act::View,
136 Some(user.id),
137 Res::Exercise(*exercise_id),
138 )
139 .await?;
140
141 let exercise = models::exercises::get_by_id(&mut conn, *exercise_id).await?;
142 let (exercise_slide, instance_or_exam_id) = models::exercises::get_or_select_exercise_slide(
143 &mut conn,
144 Some(user.id),
145 &exercise,
146 models_requests::fetch_service_info,
147 )
148 .await?;
149 match instance_or_exam_id {
150 Some(CourseInstanceOrExamId::Instance(_id)) => {}
151 _ => {
152 return Err(ControllerError::new(
153 ControllerErrorType::BadRequest,
154 "User is not enrolled to this exercise's course".to_string(),
155 None,
156 ));
157 }
158 }
159
160 token.authorized_ok(web::Json(api::ExerciseSlide {
161 slide_id: exercise_slide.id,
162 exercise_id: exercise.id,
163 exercise_name: exercise.name,
164 exercise_order_number: exercise.order_number,
165 deadline: exercise.deadline,
166 tasks: exercise_slide
167 .exercise_tasks
168 .into_iter()
169 .map(Convert::convert)
170 .collect(),
171 }))
172}
173
174#[derive(MultipartForm)]
175struct SubmissionForm {
176 submission: MultipartJson<api::ExerciseSlideSubmission>,
177 file: TempFile,
178}
179
180async fn submit_exercise(
186 pool: web::Data<PgPool>,
187 file_store: web::Data<dyn FileStore>,
188 jwt_key: web::Data<JwtKey>,
189 exercise_id: web::Path<Uuid>,
190 submission: MultipartForm<SubmissionForm>,
191 user: AuthToken,
192 app_conf: web::Data<ApplicationConfiguration>,
193) -> ControllerResult<web::Json<api::ExerciseTaskSubmissionResult>> {
194 let mut conn = pool.acquire().await?;
195 let token = authorize(
196 &mut conn,
197 Act::View,
198 Some(user.id),
199 Res::Exercise(*exercise_id),
200 )
201 .await?;
202
203 let submission_form = submission.into_inner();
205 let submission = submission_form.submission.into_inner();
206 let temp_file = submission_form.file;
207 let exercise = models::exercises::get_by_id(&mut conn, *exercise_id).await?;
208 let course_id = exercise
209 .course_id
210 .ok_or_else(|| anyhow::anyhow!("Cannot answer non-course exercises"))?;
211 let exercise_slide =
212 models::exercise_slides::get_exercise_slide(&mut conn, submission.exercise_slide_id)
213 .await?;
214 let exercise_task =
215 models::exercise_tasks::get_exercise_task_by_id(&mut conn, submission.exercise_task_id)
216 .await?;
217
218 let file = temp_file.file.into_file();
220 let mime = temp_file
221 .content_type
222 .ok_or_else(|| anyhow::anyhow!("Missing content-type header"))?;
223 let contents = file_utils::file_to_payload(file);
224 let (_upload_id, upload_path) = file_uploading::upload_exercise_archive(
225 &mut conn,
226 contents,
227 file_store.as_ref(),
228 file_uploading::ExerciseTaskInfo {
229 course_id,
230 exercise: &exercise,
231 exercise_slide: &exercise_slide,
232 exercise_task: &exercise_task,
233 },
234 mime,
235 user.id,
236 )
237 .await?;
238
239 let download_url = file_store.get_download_url(&upload_path, app_conf.as_ref());
241 let data_json = serde_json::json!({
243 "type": "editor",
244 "archive_download_url": download_url
245 });
246 let result = domain::exercises::process_submission(
247 &mut conn,
248 user.id,
249 exercise,
250 &StudentExerciseSlideSubmission {
251 exercise_slide_id: submission.exercise_slide_id,
252 exercise_task_submissions: vec![StudentExerciseTaskSubmission {
253 exercise_task_id: submission.exercise_task_id,
254 data_json,
255 }],
256 },
257 jwt_key.into_inner(),
258 )
259 .await?;
260
261 let submission = result
263 .exercise_task_submission_results
264 .into_iter()
265 .next()
266 .ok_or_else(|| {
267 ControllerError::new(
268 ControllerErrorType::InternalServerError,
269 "Failed to find exercise task submission id".to_string(),
270 None,
271 )
272 })?;
273 let result = api::ExerciseTaskSubmissionResult {
274 submission_id: submission.submission.id,
275 };
276 token.authorized_ok(web::Json(result))
277}
278
279async fn get_submission_grading(
280 pool: web::Data<PgPool>,
281 submission_id: web::Path<Uuid>,
282 user: AuthToken,
283) -> ControllerResult<web::Json<api::ExerciseTaskSubmissionStatus>> {
284 let mut conn = pool.acquire().await?;
285 let token = authorize(
286 &mut conn,
287 Act::View,
288 Some(user.id),
289 Res::ExerciseTaskSubmission(*submission_id),
290 )
291 .await?;
292
293 let grading = models::exercise_task_gradings::get_by_exercise_task_submission_id(
294 &mut conn,
295 *submission_id,
296 )
297 .await?;
298 let status = match grading {
299 Some(grading) => api::ExerciseTaskSubmissionStatus::Grading {
300 grading_progress: grading.grading_progress.convert(),
301 score_given: grading.score_given,
302 grading_started_at: grading.grading_started_at,
303 grading_completed_at: grading.grading_completed_at,
304 feedback_json: grading.feedback_json,
305 feedback_text: grading.feedback_text,
306 },
307 None => api::ExerciseTaskSubmissionStatus::NoGradingYet,
308 };
309 token.authorized_ok(web::Json(status))
310}
311
312pub fn _add_routes(cfg: &mut ServiceConfig) {
313 cfg.route("/course-instances", web::get().to(get_course_instances))
314 .route(
315 "/course-instances/{id}/exercises",
316 web::get().to(get_course_instance_exercises),
317 )
318 .route("/exercises/{id}", web::get().to(get_exercise))
319 .route("/exercises/{id}/submit", web::post().to(submit_exercise))
320 .route(
321 "/submissions/{id}/grading",
322 web::get().to(get_submission_grading),
323 );
324}