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};
20
21use crate::{
22    domain::csv_export::{
23        course_instance_export::CompletionsExportOperation, general_export,
24        points::PointExportOperation,
25    },
26    prelude::*,
27};
28
29/**
30GET /course-instances/:id
31*/
32#[instrument(skip(pool))]
33async fn get_course_instance(
34    course_instance_id: web::Path<Uuid>,
35    user: AuthUser,
36    pool: web::Data<PgPool>,
37) -> ControllerResult<web::Json<CourseInstance>> {
38    let mut conn = pool.acquire().await?;
39    let token = authorize(
40        &mut conn,
41        Act::Edit,
42        Some(user.id),
43        Res::CourseInstance(*course_instance_id),
44    )
45    .await?;
46    let course_instance =
47        models::course_instances::get_course_instance(&mut conn, *course_instance_id).await?;
48    token.authorized_ok(web::Json(course_instance))
49}
50
51#[instrument(skip(payload, pool))]
52async fn post_new_email_template(
53    course_instance_id: web::Path<Uuid>,
54    payload: web::Json<EmailTemplateNew>,
55    pool: web::Data<PgPool>,
56    user: AuthUser,
57) -> ControllerResult<web::Json<EmailTemplate>> {
58    let mut conn = pool.acquire().await?;
59    let token = authorize(
60        &mut conn,
61        Act::Edit,
62        Some(user.id),
63        Res::CourseInstance(*course_instance_id),
64    )
65    .await?;
66    let course_instance =
67        models::course_instances::get_course_instance(&mut conn, *course_instance_id).await?;
68    let new_email_template = payload.0;
69    let email_template = models::email_templates::insert_email_template(
70        &mut conn,
71        Some(course_instance.course_id),
72        new_email_template,
73        None,
74    )
75    .await?;
76    token.authorized_ok(web::Json(email_template))
77}
78
79#[instrument(skip(pool))]
80async fn get_email_templates_by_course_instance_id(
81    course_instance_id: web::Path<Uuid>,
82    pool: web::Data<PgPool>,
83    user: AuthUser,
84) -> ControllerResult<web::Json<Vec<EmailTemplate>>> {
85    let mut conn = pool.acquire().await?;
86    let token = authorize(
87        &mut conn,
88        Act::Edit,
89        Some(user.id),
90        Res::CourseInstance(*course_instance_id),
91    )
92    .await?;
93
94    let course_instance =
95        models::course_instances::get_course_instance(&mut conn, *course_instance_id).await?;
96    let email_templates =
97        models::email_templates::get_email_templates(&mut conn, course_instance.course_id).await?;
98    token.authorized_ok(web::Json(email_templates))
99}
100
101/**
102GET `/api/v0/main-frontend/course-instances/${courseInstanceId}/export-points` - gets CSV of course instance points based on course_instance ID.
103*/
104#[instrument(skip(pool))]
105pub async fn point_export(
106    course_instance_id: web::Path<Uuid>,
107    pool: web::Data<PgPool>,
108    user: AuthUser,
109) -> ControllerResult<HttpResponse> {
110    let mut conn = pool.acquire().await?;
111    let token = authorize(
112        &mut conn,
113        Act::Edit,
114        Some(user.id),
115        Res::CourseInstance(*course_instance_id),
116    )
117    .await?;
118
119    let course_instance =
120        course_instances::get_course_instance(&mut conn, *course_instance_id).await?;
121    let course = courses::get_course(&mut conn, course_instance.course_id).await?;
122
123    general_export(
124        pool,
125        &format!(
126            "attachment; filename=\"{} - {} - Point export {}.csv\"",
127            course.name,
128            course_instance.name.as_deref().unwrap_or("unnamed"),
129            Utc::now().format("%Y-%m-%d")
130        ),
131        PointExportOperation {
132            course_instance_id: *course_instance_id,
133        },
134        token,
135    )
136    .await
137}
138
139#[instrument(skip(pool))]
140async fn points(
141    course_instance_id: web::Path<Uuid>,
142    pagination: web::Query<Pagination>,
143    pool: web::Data<PgPool>,
144    user: AuthUser,
145) -> ControllerResult<web::Json<Points>> {
146    let mut conn = pool.acquire().await?;
147    let token = authorize(
148        &mut conn,
149        Act::ViewUserProgressOrDetails,
150        Some(user.id),
151        Res::CourseInstance(*course_instance_id),
152    )
153    .await?;
154    let points = course_instances::get_points(&mut conn, *course_instance_id, *pagination).await?;
155    token.authorized_ok(web::Json(points))
156}
157
158/**
159GET `/api/v0/main-frontend/course-instances/{course_instance_id}/completions`
160*/
161#[instrument(skip(pool))]
162async fn completions(
163    course_instance_id: web::Path<Uuid>,
164    pool: web::Data<PgPool>,
165    user: AuthUser,
166) -> ControllerResult<web::Json<CourseInstanceCompletionSummary>> {
167    let mut conn = pool.acquire().await?;
168    let token = authorize(
169        &mut conn,
170        Act::ViewUserProgressOrDetails,
171        Some(user.id),
172        Res::CourseInstance(*course_instance_id),
173    )
174    .await?;
175    let course_instance =
176        course_instances::get_course_instance(&mut conn, *course_instance_id).await?;
177    let completions =
178        library::progressing::get_course_instance_completion_summary(&mut conn, &course_instance)
179            .await?;
180    token.authorized_ok(web::Json(completions))
181}
182
183/**
184POST `/api/v0/main-frontend/course-instances/{course_instance_id}/completions`
185*/
186#[instrument(skip(pool, payload))]
187async fn post_completions(
188    course_instance_id: web::Path<Uuid>,
189    pool: web::Data<PgPool>,
190    user: AuthUser,
191    payload: web::Json<TeacherManualCompletionRequest>,
192) -> ControllerResult<web::Json<()>> {
193    let mut conn = pool.acquire().await?;
194    let token = authorize(
195        &mut conn,
196        Act::Edit,
197        Some(user.id),
198        Res::CourseInstance(*course_instance_id),
199    )
200    .await?;
201    let data = payload.0;
202    let course_instance =
203        course_instances::get_course_instance(&mut conn, *course_instance_id).await?;
204    library::progressing::add_manual_completions(&mut conn, user.id, &course_instance, &data)
205        .await?;
206    token.authorized_ok(web::Json(()))
207}
208
209#[instrument(skip(pool, payload))]
210async fn preview_post_completions(
211    course_instance_id: web::Path<Uuid>,
212    pool: web::Data<PgPool>,
213    user: AuthUser,
214    payload: web::Json<TeacherManualCompletionRequest>,
215) -> ControllerResult<web::Json<ManualCompletionPreview>> {
216    let mut conn = pool.acquire().await?;
217    let token = authorize(
218        &mut conn,
219        Act::Edit,
220        Some(user.id),
221        Res::CourseInstance(*course_instance_id),
222    )
223    .await?;
224    let data = payload.0;
225    let instance = course_instances::get_course_instance(&mut conn, *course_instance_id).await?;
226    let preview =
227        library::progressing::get_manual_completion_result_preview(&mut conn, &instance, &data)
228            .await?;
229    token.authorized_ok(web::Json(preview))
230}
231
232/**
233POST /course-instances/:id/edit
234*/
235#[instrument(skip(pool))]
236pub async fn edit(
237    update: web::Json<CourseInstanceForm>,
238    course_instance_id: web::Path<Uuid>,
239    pool: web::Data<PgPool>,
240    user: AuthUser,
241) -> ControllerResult<HttpResponse> {
242    let mut conn = pool.acquire().await?;
243    let token = authorize(
244        &mut conn,
245        Act::Edit,
246        Some(user.id),
247        Res::CourseInstance(*course_instance_id),
248    )
249    .await?;
250    course_instances::edit(&mut conn, *course_instance_id, update.into_inner()).await?;
251    token.authorized_ok(HttpResponse::Ok().finish())
252}
253
254/**
255POST /course-instances/:id/delete
256*/
257#[instrument(skip(pool))]
258async fn delete(
259    id: web::Path<Uuid>,
260    pool: web::Data<PgPool>,
261    user: AuthUser,
262) -> ControllerResult<HttpResponse> {
263    let mut conn = pool.acquire().await?;
264    let token = authorize(
265        &mut conn,
266        Act::Edit,
267        Some(user.id),
268        Res::CourseInstance(*id),
269    )
270    .await?;
271    models::course_instances::delete(&mut conn, *id).await?;
272    token.authorized_ok(HttpResponse::Ok().finish())
273}
274
275/**
276GET /course-instances/:id/export-completions - gets CSV of course completion based on course_instance ID.
277*/
278#[instrument(skip(pool))]
279pub async fn completions_export(
280    course_instance_id: web::Path<Uuid>,
281    pool: web::Data<PgPool>,
282    user: AuthUser,
283) -> ControllerResult<HttpResponse> {
284    let mut conn = pool.acquire().await?;
285    let token = authorize(
286        &mut conn,
287        Act::Edit,
288        Some(user.id),
289        Res::CourseInstance(*course_instance_id),
290    )
291    .await?;
292
293    let course_instance =
294        course_instances::get_course_instance(&mut conn, *course_instance_id).await?;
295    let course = courses::get_course(&mut conn, course_instance.course_id).await?;
296
297    general_export(
298        pool,
299        &format!(
300            "attachment; filename=\"{} - {} - Completions export {}.csv\"",
301            course.name,
302            course_instance.name.as_deref().unwrap_or("unnamed"),
303            Utc::now().format("%Y-%m-%d")
304        ),
305        CompletionsExportOperation {
306            course_instance_id: *course_instance_id,
307        },
308        token,
309    )
310    .await
311}
312/**
313GET /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.
314*/
315#[instrument(skip(pool))]
316pub async fn certificate_configurations(
317    course_instance_id: web::Path<Uuid>,
318    pool: web::Data<PgPool>,
319    user: AuthUser,
320) -> ControllerResult<web::Json<Vec<CertificateConfigurationAndRequirements>>> {
321    let mut conn = pool.acquire().await?;
322    let token = authorize(
323        &mut conn,
324        Act::Teach,
325        Some(user.id),
326        Res::CourseInstance(*course_instance_id),
327    )
328    .await?;
329
330    let course_instance =
331        models::course_instances::get_course_instance(&mut conn, *course_instance_id).await?;
332
333    let certificate_configurations =
334        models::certificate_configurations::get_default_certificate_configurations_and_requirements_by_course(
335            &mut conn,
336            course_instance.course_id,
337        )
338        .await?;
339    token.authorized_ok(web::Json(certificate_configurations))
340}
341
342/**
343GET /course-instances/:id/status-for-all-exercises/:user_id - Returns a status for all exercises in a course instance for a given user.
344*/
345#[instrument(skip(pool))]
346
347async fn get_all_exercise_statuses_by_course_instance_id(
348    params: web::Path<(Uuid, Uuid)>,
349    pool: web::Data<PgPool>,
350    user: AuthUser,
351) -> ControllerResult<web::Json<Vec<ExerciseStatusSummaryForUser>>> {
352    let (course_instance_id, user_id) = params.into_inner();
353    let mut conn = pool.acquire().await?;
354    let token = authorize(
355        &mut conn,
356        Act::ViewUserProgressOrDetails,
357        Some(user.id),
358        Res::CourseInstance(course_instance_id),
359    )
360    .await?;
361
362    let course_instance =
363        models::course_instances::get_course_instance(&mut conn, course_instance_id).await?;
364
365    let res = models::exercises::get_all_exercise_statuses_by_user_id_and_course_id(
366        &mut conn,
367        course_instance.course_id,
368        user_id,
369    )
370    .await?;
371
372    token.authorized_ok(web::Json(res))
373}
374
375/**
376GET /course-instances/:id/course-module-completions/:user_id - Returns a list of all course module completions for a given user for this course instance.
377*/
378#[instrument(skip(pool))]
379
380async fn get_all_get_all_course_module_completions_for_user_by_course_instance_id(
381    params: web::Path<(Uuid, Uuid)>,
382    pool: web::Data<PgPool>,
383    user: AuthUser,
384) -> ControllerResult<web::Json<Vec<CourseModuleCompletion>>> {
385    let (course_instance_id, user_id) = params.into_inner();
386    let mut conn = pool.acquire().await?;
387    let token = authorize(
388        &mut conn,
389        Act::ViewUserProgressOrDetails,
390        Some(user.id),
391        Res::CourseInstance(course_instance_id),
392    )
393    .await?;
394
395    let course_instance =
396        models::course_instances::get_course_instance(&mut conn, course_instance_id).await?;
397
398    let res = models::course_module_completions::get_all_by_course_id_and_user_id(
399        &mut conn,
400        course_instance.course_id,
401        user_id,
402    )
403    .await?;
404
405    token.authorized_ok(web::Json(res))
406}
407
408/**
409 GET /api/v0/main-frontend/course-instance/:course_instance_id/progress/:user_id - returns user progress information.
410*/
411#[instrument(skip(pool))]
412async fn get_user_progress_for_course_instance(
413    user: AuthUser,
414    params: web::Path<(Uuid, Uuid)>,
415    pool: web::Data<PgPool>,
416) -> ControllerResult<web::Json<Vec<UserCourseProgress>>> {
417    let (course_instance_id, user_id) = params.into_inner();
418    let mut conn = pool.acquire().await?;
419    let token = authorize(
420        &mut conn,
421        Act::ViewUserProgressOrDetails,
422        Some(user.id),
423        Res::CourseInstance(course_instance_id),
424    )
425    .await?;
426
427    let course_instance =
428        models::course_instances::get_course_instance(&mut conn, course_instance_id).await?;
429
430    let user_course_progress = models::user_exercise_states::get_user_course_progress(
431        &mut conn,
432        course_instance.course_id,
433        user_id,
434        false,
435    )
436    .await?;
437    token.authorized_ok(web::Json(user_course_progress))
438}
439
440/**
441Add a route for each controller in this module.
442
443The name starts with an underline in order to appear before other functions in the module documentation.
444
445We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
446*/
447pub fn _add_routes(cfg: &mut ServiceConfig) {
448    cfg.route("/{course_instance_id}", web::get().to(get_course_instance))
449        .route(
450            "/{course_instance_id}/email-templates",
451            web::post().to(post_new_email_template),
452        )
453        .route(
454            "/{course_instance_id}/email-templates",
455            web::get().to(get_email_templates_by_course_instance_id),
456        )
457        .route(
458            "/{course_instance_id}/export-points",
459            web::get().to(point_export),
460        )
461        .route("/{course_instance_id}/edit", web::post().to(edit))
462        .route("/{course_instance_id}/delete", web::post().to(delete))
463        .route(
464            "/{course_instance_id}/completions",
465            web::get().to(completions),
466        )
467        .route(
468            "/{course_instance_id}/export-completions",
469            web::get().to(completions_export),
470        )
471        .route(
472            "/{course_instance_id}/completions",
473            web::post().to(post_completions),
474        )
475        .route(
476            "/{course_instance_id}/completions/preview",
477            web::post().to(preview_post_completions),
478        )
479        .route("/{course_instance_id}/points", web::get().to(points))
480        .route(
481            "/{course_instance_id}/status-for-all-exercises/{user_id}",
482            web::get().to(get_all_exercise_statuses_by_course_instance_id),
483        )
484        .route(
485            "/{course_instance_id}/course-module-completions/{user_id}",
486            web::get().to(get_all_get_all_course_module_completions_for_user_by_course_instance_id),
487        )
488        .route(
489            "/{course_instance_id}/progress/{user_id}",
490            web::get().to(get_user_progress_for_course_instance),
491        )
492        .route(
493            "/{course_instance_id}/default-certificate-configurations",
494            web::get().to(certificate_configurations),
495        );
496}