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