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