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::Edit,
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 res = models::exercises::get_all_exercise_statuses_by_user_id_and_course_id(
549 &mut conn,
550 course_instance.course_id,
551 user_id,
552 )
553 .await?;
554
555 token.authorized_ok(web::Json(res))
556}
557
558#[instrument(skip(pool))]
562#[utoipa::path(
563 get,
564 path = "/{course_instance_id}/course-module-completions/{user_id}",
565 operation_id = "getCourseInstanceCourseModuleCompletionsForUser",
566 tag = "course-instances",
567 params(
568 ("course_instance_id" = Uuid, Path, description = "Course instance id"),
569 ("user_id" = Uuid, Path, description = "User id")
570 ),
571 responses(
572 (status = 200, description = "Course module completions for user", body = serde_json::Value)
573 )
574)]
575async fn get_all_get_all_course_module_completions_for_user_by_course_instance_id(
576 params: web::Path<(Uuid, Uuid)>,
577 pool: web::Data<PgPool>,
578 user: AuthUser,
579) -> ControllerResult<web::Json<Vec<CourseModuleCompletion>>> {
580 let (course_instance_id, user_id) = params.into_inner();
581 let mut conn = pool.acquire().await?;
582 let token = authorize(
583 &mut conn,
584 Act::ViewUserProgressOrDetails,
585 Some(user.id),
586 Res::CourseInstance(course_instance_id),
587 )
588 .await?;
589
590 let course_instance =
591 models::course_instances::get_course_instance(&mut conn, course_instance_id).await?;
592
593 let res = models::course_module_completions::get_all_by_course_id_and_user_id(
594 &mut conn,
595 course_instance.course_id,
596 user_id,
597 )
598 .await?;
599
600 token.authorized_ok(web::Json(res))
601}
602
603#[instrument(skip(pool))]
607#[utoipa::path(
608 get,
609 path = "/{course_instance_id}/progress/{user_id}",
610 operation_id = "getCourseInstanceUserProgress",
611 tag = "course-instances",
612 params(
613 ("course_instance_id" = Uuid, Path, description = "Course instance id"),
614 ("user_id" = Uuid, Path, description = "User id")
615 ),
616 responses(
617 (status = 200, description = "User progress for course instance", body = serde_json::Value)
618 )
619)]
620async fn get_user_progress_for_course_instance(
621 user: AuthUser,
622 params: web::Path<(Uuid, Uuid)>,
623 pool: web::Data<PgPool>,
624) -> ControllerResult<web::Json<Vec<UserCourseProgress>>> {
625 let (course_instance_id, user_id) = params.into_inner();
626 let mut conn = pool.acquire().await?;
627 let token = authorize(
628 &mut conn,
629 Act::ViewUserProgressOrDetails,
630 Some(user.id),
631 Res::CourseInstance(course_instance_id),
632 )
633 .await?;
634
635 let course_instance =
636 models::course_instances::get_course_instance(&mut conn, course_instance_id).await?;
637
638 let user_course_progress = models::user_exercise_states::get_user_course_progress(
639 &mut conn,
640 course_instance.course_id,
641 user_id,
642 false,
643 )
644 .await?;
645 token.authorized_ok(web::Json(user_course_progress))
646}
647
648pub fn _add_routes(cfg: &mut ServiceConfig) {
656 cfg.route("/{course_instance_id}", web::get().to(get_course_instance))
657 .route(
658 "/{course_instance_id}/email-templates",
659 web::post().to(post_new_email_template),
660 )
661 .route(
662 "/{course_instance_id}/email-templates",
663 web::get().to(get_email_templates_by_course_instance_id),
664 )
665 .route(
666 "/{course_instance_id}/export-points",
667 web::get().to(point_export),
668 )
669 .route("/{course_instance_id}/edit", web::post().to(edit))
670 .route("/{course_instance_id}/delete", web::post().to(delete))
671 .route(
672 "/{course_instance_id}/completions",
673 web::get().to(completions),
674 )
675 .route(
676 "/{course_instance_id}/export-completions",
677 web::get().to(completions_export),
678 )
679 .route(
680 "/{course_instance_id}/completions",
681 web::post().to(post_completions),
682 )
683 .route(
684 "/{course_instance_id}/completions/preview",
685 web::post().to(preview_post_completions),
686 )
687 .route("/{course_instance_id}/points", web::get().to(points))
688 .route(
689 "/{course_instance_id}/status-for-all-exercises/{user_id}",
690 web::get().to(get_all_exercise_statuses_by_course_instance_id),
691 )
692 .route(
693 "/{course_instance_id}/course-module-completions/{user_id}",
694 web::get().to(get_all_get_all_course_module_completions_for_user_by_course_instance_id),
695 )
696 .route(
697 "/{course_instance_id}/progress/{user_id}",
698 web::get().to(get_user_progress_for_course_instance),
699 )
700 .route(
701 "/{course_instance_id}/default-certificate-configurations",
702 web::get().to(certificate_configurations),
703 );
704}