1use chrono::Utc;
4use models::{
5 certificate_configurations::CertificateConfigurationAndRequirements,
6 course_instances::{self, CourseInstance, CourseInstanceForm, Points},
7 course_module_completions::CourseModuleCompletion,
8 courses,
9 email_templates::{EmailTemplate, EmailTemplateNew},
10 exercises::ExerciseStatusSummaryForUser,
11 library::{
12 self,
13 progressing::{
14 CourseInstanceCompletionSummary, ManualCompletionPreview,
15 TeacherManualCompletionRequest,
16 },
17 },
18 user_exercise_states::UserCourseProgress,
19};
20use utoipa::OpenApi;
21
22use crate::{
23 domain::csv_export::{
24 course_instance_export::CompletionsExportOperation, general_export,
25 points::PointExportOperation,
26 },
27 prelude::*,
28};
29
30#[derive(OpenApi)]
31#[openapi(paths(
32 get_course_instance,
33 post_new_email_template,
34 get_email_templates_by_course_instance_id,
35 point_export,
36 points,
37 completions,
38 post_completions,
39 preview_post_completions,
40 edit,
41 delete,
42 completions_export,
43 certificate_configurations,
44 get_all_exercise_statuses_by_course_instance_id,
45 get_all_get_all_course_module_completions_for_user_by_course_instance_id,
46 get_user_progress_for_course_instance
47))]
48pub(crate) struct MainFrontendCourseInstancesApiDoc;
49
50#[instrument(skip(pool))]
54#[utoipa::path(
55 get,
56 path = "/{course_instance_id}",
57 operation_id = "getCourseInstance",
58 tag = "course-instances",
59 params(
60 ("course_instance_id" = Uuid, Path, description = "Course instance id")
61 ),
62 responses(
63 (status = 200, description = "Course instance", body = CourseInstance)
64 )
65)]
66async fn get_course_instance(
67 course_instance_id: web::Path<Uuid>,
68 user: AuthUser,
69 pool: web::Data<PgPool>,
70) -> ControllerResult<web::Json<CourseInstance>> {
71 let mut conn = pool.acquire().await?;
72 let token = authorize(
73 &mut conn,
74 Act::ViewUserProgressOrDetails,
75 Some(user.id),
76 Res::CourseInstance(*course_instance_id),
77 )
78 .await?;
79 let course_instance =
80 models::course_instances::get_course_instance(&mut conn, *course_instance_id).await?;
81 token.authorized_ok(web::Json(course_instance))
82}
83
84#[instrument(skip(payload, pool))]
85#[utoipa::path(
86 post,
87 path = "/{course_instance_id}/email-templates",
88 operation_id = "createCourseInstanceEmailTemplate",
89 tag = "course-instances",
90 params(
91 ("course_instance_id" = Uuid, Path, description = "Course instance id")
92 ),
93 request_body = EmailTemplateNew,
94 responses(
95 (status = 200, description = "Created email template", body = EmailTemplate)
96 )
97)]
98async fn post_new_email_template(
99 course_instance_id: web::Path<Uuid>,
100 payload: web::Json<EmailTemplateNew>,
101 pool: web::Data<PgPool>,
102 user: AuthUser,
103) -> ControllerResult<web::Json<EmailTemplate>> {
104 let mut conn = pool.acquire().await?;
105 let token = authorize(
106 &mut conn,
107 Act::Edit,
108 Some(user.id),
109 Res::CourseInstance(*course_instance_id),
110 )
111 .await?;
112 let course_instance =
113 models::course_instances::get_course_instance(&mut conn, *course_instance_id).await?;
114 let new_email_template = payload.0;
115 let email_template = models::email_templates::insert_email_template(
116 &mut conn,
117 Some(course_instance.course_id),
118 new_email_template,
119 None,
120 )
121 .await?;
122 token.authorized_ok(web::Json(email_template))
123}
124
125#[instrument(skip(pool))]
126#[utoipa::path(
127 get,
128 path = "/{course_instance_id}/email-templates",
129 operation_id = "getCourseInstanceEmailTemplates",
130 tag = "course-instances",
131 params(
132 ("course_instance_id" = Uuid, Path, description = "Course instance id")
133 ),
134 responses(
135 (status = 200, description = "Course instance email templates", body = [EmailTemplate])
136 )
137)]
138async fn get_email_templates_by_course_instance_id(
139 course_instance_id: web::Path<Uuid>,
140 pool: web::Data<PgPool>,
141 user: AuthUser,
142) -> ControllerResult<web::Json<Vec<EmailTemplate>>> {
143 let mut conn = pool.acquire().await?;
144 let token = authorize(
145 &mut conn,
146 Act::Edit,
147 Some(user.id),
148 Res::CourseInstance(*course_instance_id),
149 )
150 .await?;
151
152 let course_instance =
153 models::course_instances::get_course_instance(&mut conn, *course_instance_id).await?;
154 let email_templates =
155 models::email_templates::get_email_templates(&mut conn, course_instance.course_id).await?;
156 token.authorized_ok(web::Json(email_templates))
157}
158
159#[instrument(skip(pool))]
163#[utoipa::path(
164 get,
165 path = "/{course_instance_id}/export-points",
166 operation_id = "exportCourseInstancePointsCsv",
167 tag = "course-instances",
168 params(
169 ("course_instance_id" = Uuid, Path, description = "Course instance id")
170 ),
171 responses(
172 (status = 200, description = "Course instance points CSV", body = String, content_type = "text/csv")
173 )
174)]
175pub async fn point_export(
176 course_instance_id: web::Path<Uuid>,
177 pool: web::Data<PgPool>,
178 user: AuthUser,
179) -> ControllerResult<HttpResponse> {
180 let mut conn = pool.acquire().await?;
181 let token = authorize(
182 &mut conn,
183 Act::Edit,
184 Some(user.id),
185 Res::CourseInstance(*course_instance_id),
186 )
187 .await?;
188
189 let course_instance =
190 course_instances::get_course_instance(&mut conn, *course_instance_id).await?;
191 let course = courses::get_course(&mut conn, course_instance.course_id).await?;
192
193 general_export(
194 pool,
195 &format!(
196 "attachment; filename=\"{} - {} - Point export {}.csv\"",
197 course.name,
198 course_instance.name.as_deref().unwrap_or("unnamed"),
199 Utc::now().format("%Y-%m-%d")
200 ),
201 PointExportOperation {
202 course_instance_id: *course_instance_id,
203 },
204 token,
205 )
206 .await
207}
208
209#[instrument(skip(pool))]
210#[utoipa::path(
211 get,
212 path = "/{course_instance_id}/points",
213 operation_id = "getCourseInstancePoints",
214 tag = "course-instances",
215 params(
216 ("course_instance_id" = Uuid, Path, description = "Course instance id"),
217 ("page" = Option<i64>, Query, description = "Page number"),
218 ("limit" = Option<i64>, Query, description = "Page size")
219 ),
220 responses(
221 (status = 200, description = "Course instance points", body = Points)
222 )
223)]
224async fn points(
225 course_instance_id: web::Path<Uuid>,
226 pagination: web::Query<Pagination>,
227 pool: web::Data<PgPool>,
228 user: AuthUser,
229) -> ControllerResult<web::Json<Points>> {
230 let mut conn = pool.acquire().await?;
231 let token = authorize(
232 &mut conn,
233 Act::ViewUserProgressOrDetails,
234 Some(user.id),
235 Res::CourseInstance(*course_instance_id),
236 )
237 .await?;
238 let points = course_instances::get_points(&mut conn, *course_instance_id, *pagination).await?;
239 token.authorized_ok(web::Json(points))
240}
241
242#[instrument(skip(pool))]
246#[utoipa::path(
247 get,
248 path = "/{course_instance_id}/completions",
249 operation_id = "getCourseInstanceCompletions",
250 tag = "course-instances",
251 params(
252 ("course_instance_id" = Uuid, Path, description = "Course instance id")
253 ),
254 responses(
255 (status = 200, description = "Course instance completion summary", body = CourseInstanceCompletionSummary)
256 )
257)]
258async fn completions(
259 course_instance_id: web::Path<Uuid>,
260 pool: web::Data<PgPool>,
261 user: AuthUser,
262) -> ControllerResult<web::Json<CourseInstanceCompletionSummary>> {
263 let mut conn = pool.acquire().await?;
264 let token = authorize(
265 &mut conn,
266 Act::ViewUserProgressOrDetails,
267 Some(user.id),
268 Res::CourseInstance(*course_instance_id),
269 )
270 .await?;
271 let course_instance =
272 course_instances::get_course_instance(&mut conn, *course_instance_id).await?;
273 let completions =
274 library::progressing::get_course_instance_completion_summary(&mut conn, &course_instance)
275 .await?;
276 token.authorized_ok(web::Json(completions))
277}
278
279#[instrument(skip(pool, payload))]
283#[utoipa::path(
284 post,
285 path = "/{course_instance_id}/completions",
286 operation_id = "createCourseInstanceCompletions",
287 tag = "course-instances",
288 params(
289 ("course_instance_id" = Uuid, Path, description = "Course instance id")
290 ),
291 request_body = TeacherManualCompletionRequest,
292 responses(
293 (status = 200, description = "Manual completions added")
294 )
295)]
296async fn post_completions(
297 course_instance_id: web::Path<Uuid>,
298 pool: web::Data<PgPool>,
299 user: AuthUser,
300 payload: web::Json<TeacherManualCompletionRequest>,
301) -> ControllerResult<web::Json<()>> {
302 let mut conn = pool.acquire().await?;
303 let token = authorize(
304 &mut conn,
305 Act::Edit,
306 Some(user.id),
307 Res::CourseInstance(*course_instance_id),
308 )
309 .await?;
310 let data = payload.0;
311 let course_instance =
312 course_instances::get_course_instance(&mut conn, *course_instance_id).await?;
313 library::progressing::add_manual_completions(&mut conn, user.id, &course_instance, &data)
314 .await?;
315 token.authorized_ok(web::Json(()))
316}
317
318#[instrument(skip(pool, payload))]
319#[utoipa::path(
320 post,
321 path = "/{course_instance_id}/completions/preview",
322 operation_id = "previewCourseInstanceCompletions",
323 tag = "course-instances",
324 params(
325 ("course_instance_id" = Uuid, Path, description = "Course instance id")
326 ),
327 request_body = TeacherManualCompletionRequest,
328 responses(
329 (status = 200, description = "Manual completion preview", body = ManualCompletionPreview)
330 )
331)]
332async fn preview_post_completions(
333 course_instance_id: web::Path<Uuid>,
334 pool: web::Data<PgPool>,
335 user: AuthUser,
336 payload: web::Json<TeacherManualCompletionRequest>,
337) -> ControllerResult<web::Json<ManualCompletionPreview>> {
338 let mut conn = pool.acquire().await?;
339 let token = authorize(
340 &mut conn,
341 Act::Edit,
342 Some(user.id),
343 Res::CourseInstance(*course_instance_id),
344 )
345 .await?;
346 let data = payload.0;
347 let instance = course_instances::get_course_instance(&mut conn, *course_instance_id).await?;
348 let preview =
349 library::progressing::get_manual_completion_result_preview(&mut conn, &instance, &data)
350 .await?;
351 token.authorized_ok(web::Json(preview))
352}
353
354#[instrument(skip(pool))]
358#[utoipa::path(
359 post,
360 path = "/{course_instance_id}/edit",
361 operation_id = "editCourseInstance",
362 tag = "course-instances",
363 params(
364 ("course_instance_id" = Uuid, Path, description = "Course instance id")
365 ),
366 request_body = CourseInstanceForm,
367 responses(
368 (status = 200, description = "Course instance updated")
369 )
370)]
371pub async fn edit(
372 update: web::Json<CourseInstanceForm>,
373 course_instance_id: web::Path<Uuid>,
374 pool: web::Data<PgPool>,
375 user: AuthUser,
376) -> ControllerResult<HttpResponse> {
377 let mut conn = pool.acquire().await?;
378 let token = authorize(
379 &mut conn,
380 Act::Edit,
381 Some(user.id),
382 Res::CourseInstance(*course_instance_id),
383 )
384 .await?;
385 course_instances::edit(&mut conn, *course_instance_id, update.into_inner()).await?;
386 token.authorized_ok(HttpResponse::Ok().finish())
387}
388
389#[instrument(skip(pool))]
393#[utoipa::path(
394 post,
395 path = "/{course_instance_id}/delete",
396 operation_id = "deleteCourseInstance",
397 tag = "course-instances",
398 params(
399 ("course_instance_id" = Uuid, Path, description = "Course instance id")
400 ),
401 responses(
402 (status = 200, description = "Course instance deleted")
403 )
404)]
405async fn delete(
406 id: web::Path<Uuid>,
407 pool: web::Data<PgPool>,
408 user: AuthUser,
409) -> ControllerResult<HttpResponse> {
410 let mut conn = pool.acquire().await?;
411 let token = authorize(
412 &mut conn,
413 Act::Edit,
414 Some(user.id),
415 Res::CourseInstance(*id),
416 )
417 .await?;
418 models::course_instances::delete(&mut conn, *id).await?;
419 token.authorized_ok(HttpResponse::Ok().finish())
420}
421
422#[instrument(skip(pool))]
426#[utoipa::path(
427 get,
428 path = "/{course_instance_id}/export-completions",
429 operation_id = "exportCourseInstanceCompletionsCsv",
430 tag = "course-instances",
431 params(
432 ("course_instance_id" = Uuid, Path, description = "Course instance id")
433 ),
434 responses(
435 (status = 200, description = "Course instance completions CSV", body = String, content_type = "text/csv")
436 )
437)]
438pub async fn completions_export(
439 course_instance_id: web::Path<Uuid>,
440 pool: web::Data<PgPool>,
441 user: AuthUser,
442) -> ControllerResult<HttpResponse> {
443 let mut conn = pool.acquire().await?;
444 let token = authorize(
445 &mut conn,
446 Act::Edit,
447 Some(user.id),
448 Res::CourseInstance(*course_instance_id),
449 )
450 .await?;
451
452 let course_instance =
453 course_instances::get_course_instance(&mut conn, *course_instance_id).await?;
454 let course = courses::get_course(&mut conn, course_instance.course_id).await?;
455
456 general_export(
457 pool,
458 &format!(
459 "attachment; filename=\"{} - {} - Completions export {}.csv\"",
460 course.name,
461 course_instance.name.as_deref().unwrap_or("unnamed"),
462 Utc::now().format("%Y-%m-%d")
463 ),
464 CompletionsExportOperation {
465 course_instance_id: *course_instance_id,
466 },
467 token,
468 )
469 .await
470}
471#[instrument(skip(pool))]
475#[utoipa::path(
476 get,
477 path = "/{course_instance_id}/default-certificate-configurations",
478 operation_id = "getCourseInstanceDefaultCertificateConfigurations",
479 tag = "course-instances",
480 params(
481 ("course_instance_id" = Uuid, Path, description = "Course instance id")
482 ),
483 responses(
484 (status = 200, description = "Default certificate configurations", body = [CertificateConfigurationAndRequirements])
485 )
486)]
487pub async fn certificate_configurations(
488 course_instance_id: web::Path<Uuid>,
489 pool: web::Data<PgPool>,
490 user: AuthUser,
491) -> ControllerResult<web::Json<Vec<CertificateConfigurationAndRequirements>>> {
492 let mut conn = pool.acquire().await?;
493 let token = authorize(
494 &mut conn,
495 Act::Teach,
496 Some(user.id),
497 Res::CourseInstance(*course_instance_id),
498 )
499 .await?;
500
501 let course_instance =
502 models::course_instances::get_course_instance(&mut conn, *course_instance_id).await?;
503
504 let certificate_configurations =
505 models::certificate_configurations::get_default_certificate_configurations_and_requirements_by_course(
506 &mut conn,
507 course_instance.course_id,
508 )
509 .await?;
510 token.authorized_ok(web::Json(certificate_configurations))
511}
512
513#[instrument(skip(pool))]
517#[utoipa::path(
518 get,
519 path = "/{course_instance_id}/status-for-all-exercises/{user_id}",
520 operation_id = "getCourseInstanceExerciseStatusesForUser",
521 tag = "course-instances",
522 params(
523 ("course_instance_id" = Uuid, Path, description = "Course instance id"),
524 ("user_id" = Uuid, Path, description = "User id")
525 ),
526 responses(
527 (status = 200, description = "Exercise statuses for user", body = serde_json::Value)
528 )
529)]
530async fn get_all_exercise_statuses_by_course_instance_id(
531 params: web::Path<(Uuid, Uuid)>,
532 pool: web::Data<PgPool>,
533 user: AuthUser,
534) -> ControllerResult<web::Json<Vec<ExerciseStatusSummaryForUser>>> {
535 let (course_instance_id, user_id) = params.into_inner();
536 let mut conn = pool.acquire().await?;
537 let token = authorize(
538 &mut conn,
539 Act::ViewUserProgressOrDetails,
540 Some(user.id),
541 Res::CourseInstance(course_instance_id),
542 )
543 .await?;
544
545 let course_instance =
546 models::course_instances::get_course_instance(&mut conn, course_instance_id).await?;
547
548 let _enrollment = models::course_instance_enrollments::get_by_user_and_course_instance_id(
549 &mut conn,
550 user_id,
551 course_instance_id,
552 )
553 .await
554 .map_err(|err| match err.error_type() {
555 ModelErrorType::RecordNotFound | ModelErrorType::NotFound => controller_err!(
556 Forbidden,
557 "User is not enrolled in the requested course instance".to_string()
558 ),
559 _ => err.into(),
560 })?;
561
562 let res = models::exercises::get_all_exercise_statuses_by_user_id_and_course_id(
563 &mut conn,
564 course_instance.course_id,
565 user_id,
566 )
567 .await?;
568
569 token.authorized_ok(web::Json(res))
570}
571
572#[instrument(skip(pool))]
576#[utoipa::path(
577 get,
578 path = "/{course_instance_id}/course-module-completions/{user_id}",
579 operation_id = "getCourseInstanceCourseModuleCompletionsForUser",
580 tag = "course-instances",
581 params(
582 ("course_instance_id" = Uuid, Path, description = "Course instance id"),
583 ("user_id" = Uuid, Path, description = "User id")
584 ),
585 responses(
586 (status = 200, description = "Course module completions for user", body = serde_json::Value)
587 )
588)]
589async fn get_all_get_all_course_module_completions_for_user_by_course_instance_id(
590 params: web::Path<(Uuid, Uuid)>,
591 pool: web::Data<PgPool>,
592 user: AuthUser,
593) -> ControllerResult<web::Json<Vec<CourseModuleCompletion>>> {
594 let (course_instance_id, user_id) = params.into_inner();
595 let mut conn = pool.acquire().await?;
596 let token = authorize(
597 &mut conn,
598 Act::ViewUserProgressOrDetails,
599 Some(user.id),
600 Res::CourseInstance(course_instance_id),
601 )
602 .await?;
603
604 let course_instance =
605 models::course_instances::get_course_instance(&mut conn, course_instance_id).await?;
606
607 let _enrollment = models::course_instance_enrollments::get_by_user_and_course_instance_id(
608 &mut conn,
609 user_id,
610 course_instance_id,
611 )
612 .await
613 .map_err(|err| match err.error_type() {
614 ModelErrorType::RecordNotFound | ModelErrorType::NotFound => controller_err!(
615 Forbidden,
616 "User is not enrolled in the requested course instance".to_string()
617 ),
618 _ => err.into(),
619 })?;
620
621 let res = models::course_module_completions::get_all_by_course_id_and_user_id(
622 &mut conn,
623 course_instance.course_id,
624 user_id,
625 )
626 .await?;
627
628 token.authorized_ok(web::Json(res))
629}
630
631#[instrument(skip(pool))]
635#[utoipa::path(
636 get,
637 path = "/{course_instance_id}/progress/{user_id}",
638 operation_id = "getCourseInstanceUserProgress",
639 tag = "course-instances",
640 params(
641 ("course_instance_id" = Uuid, Path, description = "Course instance id"),
642 ("user_id" = Uuid, Path, description = "User id")
643 ),
644 responses(
645 (status = 200, description = "User progress for course instance", body = serde_json::Value)
646 )
647)]
648async fn get_user_progress_for_course_instance(
649 user: AuthUser,
650 params: web::Path<(Uuid, Uuid)>,
651 pool: web::Data<PgPool>,
652) -> ControllerResult<web::Json<Vec<UserCourseProgress>>> {
653 let (course_instance_id, user_id) = params.into_inner();
654 let mut conn = pool.acquire().await?;
655 let token = authorize(
656 &mut conn,
657 Act::ViewUserProgressOrDetails,
658 Some(user.id),
659 Res::CourseInstance(course_instance_id),
660 )
661 .await?;
662
663 let course_instance =
664 models::course_instances::get_course_instance(&mut conn, course_instance_id).await?;
665
666 let _enrollment = models::course_instance_enrollments::get_by_user_and_course_instance_id(
667 &mut conn,
668 user_id,
669 course_instance_id,
670 )
671 .await
672 .map_err(|err| match err.error_type() {
673 ModelErrorType::RecordNotFound | ModelErrorType::NotFound => controller_err!(
674 Forbidden,
675 "User is not enrolled in the requested course instance".to_string()
676 ),
677 _ => err.into(),
678 })?;
679
680 let user_course_progress = models::user_exercise_states::get_user_course_progress(
681 &mut conn,
682 course_instance.course_id,
683 user_id,
684 false,
685 )
686 .await?;
687 token.authorized_ok(web::Json(user_course_progress))
688}
689
690pub fn _add_routes(cfg: &mut ServiceConfig) {
698 cfg.route("/{course_instance_id}", web::get().to(get_course_instance))
699 .route(
700 "/{course_instance_id}/email-templates",
701 web::post().to(post_new_email_template),
702 )
703 .route(
704 "/{course_instance_id}/email-templates",
705 web::get().to(get_email_templates_by_course_instance_id),
706 )
707 .route(
708 "/{course_instance_id}/export-points",
709 web::get().to(point_export),
710 )
711 .route("/{course_instance_id}/edit", web::post().to(edit))
712 .route("/{course_instance_id}/delete", web::post().to(delete))
713 .route(
714 "/{course_instance_id}/completions",
715 web::get().to(completions),
716 )
717 .route(
718 "/{course_instance_id}/export-completions",
719 web::get().to(completions_export),
720 )
721 .route(
722 "/{course_instance_id}/completions",
723 web::post().to(post_completions),
724 )
725 .route(
726 "/{course_instance_id}/completions/preview",
727 web::post().to(preview_post_completions),
728 )
729 .route("/{course_instance_id}/points", web::get().to(points))
730 .route(
731 "/{course_instance_id}/status-for-all-exercises/{user_id}",
732 web::get().to(get_all_exercise_statuses_by_course_instance_id),
733 )
734 .route(
735 "/{course_instance_id}/course-module-completions/{user_id}",
736 web::get().to(get_all_get_all_course_module_completions_for_user_by_course_instance_id),
737 )
738 .route(
739 "/{course_instance_id}/progress/{user_id}",
740 web::get().to(get_user_progress_for_course_instance),
741 )
742 .route(
743 "/{course_instance_id}/default-certificate-configurations",
744 web::get().to(certificate_configurations),
745 );
746}