headless_lms_server/controllers/main_frontend/
course_instances.rs

1//! Controllers for requests starting with `/api/v0/main-frontend/course-instances`.
2
3use 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/**
51GET /course-instances/:id
52*/
53#[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/**
160GET `/api/v0/main-frontend/course-instances/${courseInstanceId}/export-points` - gets CSV of course instance points based on course_instance ID.
161*/
162#[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/**
243GET `/api/v0/main-frontend/course-instances/{course_instance_id}/completions`
244*/
245#[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/**
280POST `/api/v0/main-frontend/course-instances/{course_instance_id}/completions`
281*/
282#[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/**
355POST /course-instances/:id/edit
356*/
357#[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/**
390POST /course-instances/:id/delete
391*/
392#[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/**
423GET /course-instances/:id/export-completions - gets CSV of course completion based on course_instance ID.
424*/
425#[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/**
472GET /course-instances/:id/default-certificate-configurations - gets default certificate configurations of the given course instance. A default certificate configuration requires only one course module to be completed.
473*/
474#[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/**
514GET /course-instances/:id/status-for-all-exercises/:user_id - Returns a status for all exercises in a course instance for a given user.
515*/
516#[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/**
559GET /course-instances/:id/course-module-completions/:user_id - Returns a list of all course module completions for a given user for this course instance.
560*/
561#[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/**
604 GET /api/v0/main-frontend/course-instance/:course_instance_id/progress/:user_id - returns user progress information.
605*/
606#[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
648/**
649Add a route for each controller in this module.
650
651The name starts with an underline in order to appear before other functions in the module documentation.
652
653We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
654*/
655pub 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}