1use std::{collections::HashMap, net::IpAddr, path::Path};
4
5use actix_http::header::{self, X_FORWARDED_FOR};
6use actix_web::web::Json;
7use chrono::Utc;
8use futures::{FutureExt, future::OptionFuture};
9use headless_lms_models::courses::{CourseLanguageVersionNavigationInfo, CourseMaterialCourse};
10use headless_lms_models::{
11 course_custom_privacy_policy_checkbox_texts::CourseCustomPrivacyPolicyCheckboxText,
12 marketing_consents::UserMarketingConsent,
13};
14use headless_lms_models::{partner_block::PartnersBlock, privacy_link::PrivacyLink};
15use headless_lms_utils::ip_to_country::IpToCountryMapper;
16use isbot::Bots;
17use models::{
18 chapters::ChapterWithStatus,
19 course_instances::CourseInstance,
20 course_modules::CourseModule,
21 courses::{self, get_nondeleted_course_id_by_slug},
22 feedback,
23 feedback::NewFeedback,
24 glossary::Term,
25 material_references::MaterialReference,
26 page_visit_datum::NewPageVisitDatum,
27 page_visit_datum_daily_visit_hashing_keys::{
28 GenerateAnonymousIdentifierInput, generate_anonymous_identifier,
29 },
30 pages::{CoursePageWithUserData, Page, PageSearchResult, PageVisibility, SearchRequest},
31 proposed_page_edits::{self, NewProposedPageEdits},
32 research_forms::{
33 NewResearchFormQuestionAnswer, ResearchForm, ResearchFormQuestion,
34 ResearchFormQuestionAnswer,
35 },
36 student_countries::StudentCountry,
37 user_course_settings::UserCourseSettings,
38};
39use utoipa::{OpenApi, ToSchema};
40
41use crate::{
42 domain::authorization::{
43 Action, Resource, authorize_access_to_course_material,
44 authorize_with_fetched_list_of_roles, can_user_view_chapter, skip_authorize,
45 },
46 prelude::*,
47};
48
49#[derive(OpenApi)]
50#[openapi(paths(
51 get_course,
52 get_course_page_by_path,
53 get_current_course_instance,
54 get_course_instances,
55 get_public_course_pages,
56 get_chapters,
57 get_user_course_settings,
58 search_pages_with_phrase,
59 search_pages_with_words,
60 feedback,
61 propose_edit,
62 glossary,
63 get_material_references_by_course_id,
64 get_public_top_level_pages,
65 get_all_course_language_versions_navigation_info_from_page,
66 get_page_by_course_id_and_language_group,
67 student_country,
68 get_student_countries,
69 get_student_country,
70 get_research_form_with_course_id,
71 get_research_form_questions_with_course_id,
72 upsert_course_research_form_answer,
73 get_research_form_answers_with_user_id,
74 update_marketing_consent,
75 fetch_user_marketing_consent,
76 get_ai_usage_notice_acknowledgement,
77 acknowledge_ai_usage_notice,
78 get_partners_block,
79 get_privacy_link,
80 get_custom_privacy_policy_checkbox_texts,
81 get_user_chapter_locks
82))]
83pub(crate) struct CourseMaterialCoursesApiDoc;
84
85#[utoipa::path(
89 get,
90 path = "/{course_id}",
91 operation_id = "getCourseMaterialCourse",
92 tag = "course-material-courses",
93 params(
94 ("course_id" = Uuid, Path, description = "Course id")
95 ),
96 responses(
97 (status = 200, description = "Course", body = CourseMaterialCourse)
98 )
99)]
100#[instrument(skip(pool))]
101async fn get_course(
102 course_id: web::Path<Uuid>,
103 pool: web::Data<PgPool>,
104 auth: Option<AuthUser>,
105) -> ControllerResult<web::Json<CourseMaterialCourse>> {
106 let mut conn = pool.acquire().await?;
107 let token =
108 authorize_access_to_course_material(&mut conn, auth.map(|u| u.id), *course_id).await?;
109 let course = models::courses::get_course(&mut conn, *course_id).await?;
110 token.authorized_ok(web::Json(course.into()))
111}
112
113#[utoipa::path(
123 get,
124 path = "/{course_slug}/page-by-path/{url_path}",
125 operation_id = "getCourseMaterialCoursePageByPath",
126 tag = "course-material-courses",
127 params(
128 ("course_slug" = String, Path, description = "Course slug"),
129 ("url_path" = String, Path, description = "Page path within the course")
130 ),
131 responses(
132 (status = 200, description = "Course page with user data", body = CoursePageWithUserData)
133 )
134)]
135#[instrument(skip(pool, ip_to_country_mapper, req, file_store, app_conf))]
136async fn get_course_page_by_path(
137 params: web::Path<(String, String)>,
138 pool: web::Data<PgPool>,
139 user: Option<AuthUser>,
140 ip_to_country_mapper: web::Data<IpToCountryMapper>,
141 req: HttpRequest,
142 file_store: web::Data<dyn FileStore>,
143 app_conf: web::Data<ApplicationConfiguration>,
144) -> ControllerResult<web::Json<CoursePageWithUserData>> {
145 let mut conn = pool.acquire().await?;
146
147 let (course_slug, raw_page_path) = params.into_inner();
148 let path = if raw_page_path.starts_with('/') {
149 raw_page_path
150 } else {
151 format!("/{}", raw_page_path)
152 };
153 let user_id = user.map(|u| u.id);
154 let course_data = get_nondeleted_course_id_by_slug(&mut conn, &course_slug).await?;
155 let page_with_user_data = models::pages::get_page_with_user_data_by_path(
156 &mut conn,
157 user_id,
158 &course_data,
159 &path,
160 file_store.as_ref(),
161 &app_conf,
162 )
163 .await?;
164
165 if !can_user_view_chapter(
167 &mut conn,
168 user_id,
169 page_with_user_data.page.course_id,
170 page_with_user_data.page.chapter_id,
171 )
172 .await?
173 {
174 return Err(ControllerError::new(
175 ControllerErrorType::UnauthorizedWithReason(
176 crate::domain::error::UnauthorizedReason::ChapterNotOpenYet,
177 ),
178 "Chapter is not open yet.".to_string(),
179 None,
180 ));
181 }
182
183 let token = authorize_access_to_course_material(
184 &mut conn,
185 user_id,
186 page_with_user_data.page.course_id.ok_or_else(|| {
187 ControllerError::new(
188 ControllerErrorType::NotFound,
189 "Course not found".to_string(),
190 None,
191 )
192 })?,
193 )
194 .await?;
195
196 let temp_request_information =
197 derive_information_from_requester(req, ip_to_country_mapper).await?;
198
199 let RequestInformation {
200 ip,
201 referrer,
202 utm_source,
203 utm_medium,
204 utm_campaign,
205 utm_term,
206 utm_content,
207 country,
208 user_agent,
209 has_bot_user_agent,
210 browser_admits_its_a_bot,
211 browser,
212 browser_version,
213 operating_system,
214 operating_system_version,
215 device_type,
216 } = temp_request_information.data;
217
218 let course_or_exam_id = page_with_user_data
219 .page
220 .course_id
221 .unwrap_or_else(|| page_with_user_data.page.exam_id.unwrap_or_else(Uuid::nil));
222 let anonymous_identifier = generate_anonymous_identifier(
223 &mut conn,
224 GenerateAnonymousIdentifierInput {
225 user_agent,
226 ip_address: ip.map(|ip| ip.to_string()).unwrap_or_default(),
227 course_id: course_or_exam_id,
228 },
229 )
230 .await?;
231
232 models::page_visit_datum::insert(
233 &mut conn,
234 NewPageVisitDatum {
235 course_id: page_with_user_data.page.course_id,
236 page_id: page_with_user_data.page.id,
237 country,
238 browser,
239 browser_version,
240 operating_system,
241 operating_system_version,
242 device_type,
243 referrer,
244 is_bot: has_bot_user_agent || browser_admits_its_a_bot,
245 utm_source,
246 utm_medium,
247 utm_campaign,
248 utm_term,
249 utm_content,
250 anonymous_identifier,
251 exam_id: page_with_user_data.page.exam_id,
252 },
253 )
254 .await?;
255
256 token.authorized_ok(web::Json(page_with_user_data))
257}
258
259struct RequestInformation {
260 ip: Option<IpAddr>,
261 user_agent: String,
262 referrer: Option<String>,
263 utm_source: Option<String>,
264 utm_medium: Option<String>,
265 utm_campaign: Option<String>,
266 utm_term: Option<String>,
267 utm_content: Option<String>,
268 country: Option<String>,
269 has_bot_user_agent: bool,
270 browser_admits_its_a_bot: bool,
271 browser: Option<String>,
272 browser_version: Option<String>,
273 operating_system: Option<String>,
274 operating_system_version: Option<String>,
275 device_type: Option<String>,
276}
277
278async fn derive_information_from_requester(
280 req: HttpRequest,
281 ip_to_country_mapper: web::Data<IpToCountryMapper>,
282) -> ControllerResult<RequestInformation> {
283 let mut headers = req.headers().clone();
284 let x_real_ip = headers.get("X-Real-IP");
285 let x_forwarded_for = headers.get(X_FORWARDED_FOR);
286 let connection_info = req.connection_info();
287 let peer_address = connection_info.peer_addr();
288 let headers_clone = headers.clone();
289 let user_agent = headers_clone.get(header::USER_AGENT);
290 let bots = Bots::default();
291 let has_bot_user_agent = user_agent
292 .and_then(|ua| ua.to_str().ok())
293 .map(|ua| bots.is_bot(ua))
294 .unwrap_or(true);
295 let header_totally_not_a_bot = headers.get("totally-not-a-bot");
297 let browser_admits_its_a_bot = header_totally_not_a_bot.is_none();
298 if has_bot_user_agent || browser_admits_its_a_bot {
299 warn!(
300 ?has_bot_user_agent,
301 ?browser_admits_its_a_bot,
302 ?user_agent,
303 ?header_totally_not_a_bot,
304 "The requester is a bot"
305 )
306 }
307
308 let user_agent_parser = woothee::parser::Parser::new();
309 let parsed_user_agent = user_agent
310 .and_then(|ua| ua.to_str().ok())
311 .and_then(|ua| user_agent_parser.parse(ua));
312
313 let ip: Option<IpAddr> = connection_info
314 .realip_remote_addr()
315 .and_then(|ip| ip.parse::<IpAddr>().ok());
316
317 info!(
318 "Ip {:?}, x_real_ip {:?}, x_forwarded_for {:?}, peer_address {:?}",
319 ip, x_real_ip, x_forwarded_for, peer_address
320 );
321
322 let country = ip
323 .and_then(|ip| ip_to_country_mapper.map_ip_to_country(&ip))
324 .map(|c| c.to_string());
325
326 let utm_tags = headers
327 .remove("utm-tags")
328 .next()
329 .and_then(|utms| String::from_utf8(utms.as_bytes().to_vec()).ok())
330 .and_then(|utms| serde_json::from_str::<serde_json::Value>(&utms).ok())
331 .and_then(|o| o.as_object().cloned());
332
333 let utm_source = utm_tags
334 .clone()
335 .and_then(|mut tags| tags.remove("utm_source"))
336 .and_then(|v| v.as_str().map(|s| s.to_string()));
337
338 let utm_medium = utm_tags
339 .clone()
340 .and_then(|mut tags| tags.remove("utm_medium"))
341 .and_then(|v| v.as_str().map(|s| s.to_string()));
342
343 let utm_campaign = utm_tags
344 .clone()
345 .and_then(|mut tags| tags.remove("utm_campaign"))
346 .and_then(|v| v.as_str().map(|s| s.to_string()));
347
348 let utm_term = utm_tags
349 .clone()
350 .and_then(|mut tags| tags.remove("utm_term"))
351 .and_then(|v| v.as_str().map(|s| s.to_string()));
352
353 let utm_content = utm_tags
354 .and_then(|mut tags| tags.remove("utm_content"))
355 .and_then(|v| v.as_str().map(|s| s.to_string()));
356
357 let referrer = headers
358 .get("Orignal-Referrer")
359 .and_then(|r| r.to_str().ok())
360 .map(|r| r.to_string());
361
362 let browser = parsed_user_agent.as_ref().map(|ua| ua.name.to_string());
363 let browser_version = parsed_user_agent.as_ref().map(|ua| ua.version.to_string());
364 let operating_system = parsed_user_agent.as_ref().map(|ua| ua.os.to_string());
365 let operating_system_version = parsed_user_agent
366 .as_ref()
367 .map(|ua| ua.os_version.to_string());
368 let device_type = parsed_user_agent.as_ref().map(|ua| ua.category.to_string());
369 let token = skip_authorize();
370 token.authorized_ok(RequestInformation {
371 ip,
372 user_agent: user_agent
373 .and_then(|ua| ua.to_str().ok())
374 .unwrap_or_default()
375 .to_string(),
376 referrer,
377 utm_source,
378 utm_medium,
379 utm_campaign,
380 utm_term,
381 utm_content,
382 country,
383 has_bot_user_agent,
384 browser_admits_its_a_bot,
385 browser,
386 browser_version,
387 operating_system,
388 operating_system_version,
389 device_type,
390 })
391}
392
393#[utoipa::path(
397 get,
398 path = "/{course_id}/current-instance",
399 operation_id = "getCurrentCourseMaterialCourseInstance",
400 tag = "course-material-courses",
401 params(
402 ("course_id" = Uuid, Path, description = "Course id")
403 ),
404 responses(
405 (status = 200, description = "Current course instance", body = Option<CourseInstance>)
406 )
407)]
408#[instrument(skip(pool))]
409async fn get_current_course_instance(
410 pool: web::Data<PgPool>,
411 course_id: web::Path<Uuid>,
412 user: Option<AuthUser>,
413) -> ControllerResult<web::Json<Option<CourseInstance>>> {
414 let mut conn = pool.acquire().await?;
415 if let Some(user) = user {
416 let instance = models::course_instances::current_course_instance_of_user(
417 &mut conn, user.id, *course_id,
418 )
419 .await?;
420 let token = skip_authorize();
421 token.authorized_ok(web::Json(instance))
422 } else {
423 Err(ControllerError::new(
424 ControllerErrorType::NotFound,
425 "User not found".to_string(),
426 None,
427 ))
428 }
429}
430
431#[utoipa::path(
435 get,
436 path = "/{course_id}/course-instances",
437 operation_id = "getCourseMaterialCourseInstances",
438 tag = "course-material-courses",
439 params(
440 ("course_id" = Uuid, Path, description = "Course id")
441 ),
442 responses(
443 (status = 200, description = "Course instances", body = Vec<CourseInstance>)
444 )
445)]
446async fn get_course_instances(
447 pool: web::Data<PgPool>,
448 course_id: web::Path<Uuid>,
449) -> ControllerResult<web::Json<Vec<CourseInstance>>> {
450 let mut conn = pool.acquire().await?;
451 let instances =
452 models::course_instances::get_course_instances_for_course(&mut conn, *course_id).await?;
453 let token = skip_authorize();
454 token.authorized_ok(web::Json(instances))
455}
456
457#[utoipa::path(
463 get,
464 path = "/{course_id}/pages",
465 operation_id = "getCourseMaterialCoursePages",
466 tag = "course-material-courses",
467 params(
468 ("course_id" = Uuid, Path, description = "Course id")
469 ),
470 responses(
471 (status = 200, description = "Public course pages", body = Vec<Page>)
472 )
473)]
474#[instrument(skip(pool))]
475async fn get_public_course_pages(
476 course_id: web::Path<Uuid>,
477 pool: web::Data<PgPool>,
478 auth: Option<AuthUser>,
479) -> ControllerResult<web::Json<Vec<Page>>> {
480 let mut conn = pool.acquire().await?;
481 let user_id = auth.map(|u| u.id);
482 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
483 let pages: Vec<Page> = models::pages::get_all_by_course_id_and_visibility(
484 &mut conn,
485 *course_id,
486 PageVisibility::Public,
487 )
488 .await?;
489 let pages = models::pages::filter_course_material_pages(&mut conn, user_id, pages).await?;
490 token.authorized_ok(web::Json(pages))
491}
492
493#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
494
495pub struct ChaptersWithStatus {
496 pub is_previewable: bool,
497 pub modules: Vec<CourseMaterialCourseModule>,
498}
499
500#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
501
502pub struct CourseMaterialCourseModule {
503 pub chapters: Vec<ChapterWithStatus>,
504 pub id: Uuid,
505 pub is_default: bool,
506 pub name: Option<String>,
507 pub order_number: i32,
508}
509
510#[utoipa::path(
515 get,
516 path = "/{course_id}/chapters",
517 operation_id = "getCourseMaterialChapters",
518 tag = "course-material-courses",
519 params(
520 ("course_id" = Uuid, Path, description = "Course id")
521 ),
522 responses(
523 (status = 200, description = "Course chapters grouped by module", body = ChaptersWithStatus)
524 )
525)]
526#[instrument(skip(pool, file_store, app_conf))]
527async fn get_chapters(
528 course_id: web::Path<Uuid>,
529 user: Option<AuthUser>,
530 pool: web::Data<PgPool>,
531 file_store: web::Data<dyn FileStore>,
532 app_conf: web::Data<ApplicationConfiguration>,
533) -> ControllerResult<web::Json<ChaptersWithStatus>> {
534 let mut conn = pool.acquire().await?;
535 let user_id = user.as_ref().map(|u| u.id);
536 let is_previewable = OptionFuture::from(user.map(|u| {
537 authorize(&mut conn, Act::Teach, Some(u.id), Res::Course(*course_id)).map(|r| r.ok())
538 }))
539 .await
540 .is_some();
541 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
542 let course_modules = models::course_modules::get_by_course_id(&mut conn, *course_id).await?;
543 let exercise_deadline_overrides =
544 models::chapters::exercise_deadline_overrides_by_chapter_for_course(&mut conn, *course_id)
545 .await?;
546 let chapters = models::chapters::course_chapters(&mut conn, *course_id)
547 .await?
548 .into_iter()
549 .map(|chapter| {
550 let chapter_image_url = chapter
551 .chapter_image_path
552 .as_ref()
553 .map(|path| file_store.get_download_url(Path::new(&path), &app_conf));
554 let exercise_deadline_overrides = exercise_deadline_overrides.get(&chapter.id).copied();
555 ChapterWithStatus::from_database_chapter_timestamp_and_image_url(
556 chapter,
557 Utc::now(),
558 chapter_image_url,
559 exercise_deadline_overrides,
560 )
561 })
562 .collect();
563 let modules = collect_course_modules(course_modules, chapters)?.data;
564 token.authorized_ok(web::Json(ChaptersWithStatus {
565 is_previewable,
566 modules,
567 }))
568}
569
570fn collect_course_modules(
572 course_modules: Vec<CourseModule>,
573 chapters: Vec<ChapterWithStatus>,
574) -> ControllerResult<Vec<CourseMaterialCourseModule>> {
575 let mut course_modules: HashMap<Uuid, CourseMaterialCourseModule> = course_modules
576 .into_iter()
577 .map(|course_module| {
578 (
579 course_module.id,
580 CourseMaterialCourseModule {
581 chapters: vec![],
582 id: course_module.id,
583 is_default: course_module.name.is_none(),
584 name: course_module.name,
585 order_number: course_module.order_number,
586 },
587 )
588 })
589 .collect();
590 for chapter in chapters {
591 course_modules
592 .get_mut(&chapter.course_module_id)
593 .ok_or_else(|| {
594 ControllerError::new(
595 ControllerErrorType::InternalServerError,
596 "Module data mismatch.".to_string(),
597 None,
598 )
599 })?
600 .chapters
601 .push(chapter);
602 }
603 let token = skip_authorize();
604 token.authorized_ok(course_modules.into_values().collect())
605}
606
607#[utoipa::path(
611 get,
612 path = "/{course_id}/user-settings",
613 operation_id = "getCourseMaterialUserCourseSettings",
614 tag = "course-material-courses",
615 params(
616 ("course_id" = Uuid, Path, description = "Course id")
617 ),
618 responses(
619 (status = 200, description = "User course settings", body = Option<UserCourseSettings>)
620 )
621)]
622#[instrument(skip(pool))]
623async fn get_user_course_settings(
624 pool: web::Data<PgPool>,
625 course_id: web::Path<Uuid>,
626 user: Option<AuthUser>,
627) -> ControllerResult<web::Json<Option<UserCourseSettings>>> {
628 let mut conn = pool.acquire().await?;
629 if let Some(user) = user {
630 let settings = models::user_course_settings::get_user_course_settings_by_course_id(
631 &mut conn, user.id, *course_id,
632 )
633 .await?;
634 let token = skip_authorize();
635 token.authorized_ok(web::Json(settings))
636 } else {
637 Err(ControllerError::new(
638 ControllerErrorType::NotFound,
639 "User not found".to_string(),
640 None,
641 ))
642 }
643}
644
645#[utoipa::path(
664 post,
665 path = "/{course_id}/search-pages-with-phrase",
666 operation_id = "searchPagesWithPhrase",
667 tag = "course-material-courses",
668 params(
669 ("course_id" = Uuid, Path, description = "Course id")
670 ),
671 request_body = SearchRequest,
672 responses(
673 (status = 200, description = "Matching pages", body = Vec<PageSearchResult>)
674 )
675)]
676#[instrument(skip(pool))]
677async fn search_pages_with_phrase(
678 course_id: web::Path<Uuid>,
679 payload: web::Json<SearchRequest>,
680 pool: web::Data<PgPool>,
681 auth: Option<AuthUser>,
682) -> ControllerResult<web::Json<Vec<PageSearchResult>>> {
683 let mut conn = pool.acquire().await?;
684 let token =
685 authorize_access_to_course_material(&mut conn, auth.map(|u| u.id), *course_id).await?;
686 let res =
687 models::pages::get_page_search_results_for_phrase(&mut conn, *course_id, &payload).await?;
688 token.authorized_ok(web::Json(res))
689}
690
691#[utoipa::path(
710 post,
711 path = "/{course_id}/search-pages-with-words",
712 operation_id = "searchPagesWithWords",
713 tag = "course-material-courses",
714 params(
715 ("course_id" = Uuid, Path, description = "Course id")
716 ),
717 request_body = SearchRequest,
718 responses(
719 (status = 200, description = "Matching pages", body = Vec<PageSearchResult>)
720 )
721)]
722#[instrument(skip(pool))]
723async fn search_pages_with_words(
724 course_id: web::Path<Uuid>,
725 payload: web::Json<SearchRequest>,
726 pool: web::Data<PgPool>,
727 auth: Option<AuthUser>,
728) -> ControllerResult<web::Json<Vec<PageSearchResult>>> {
729 let mut conn = pool.acquire().await?;
730 let token =
731 authorize_access_to_course_material(&mut conn, auth.map(|u| u.id), *course_id).await?;
732 let res =
733 models::pages::get_page_search_results_for_words(&mut conn, *course_id, &payload).await?;
734 token.authorized_ok(web::Json(res))
735}
736
737#[utoipa::path(
741 post,
742 path = "/{course_id}/feedback",
743 operation_id = "postFeedback",
744 tag = "course-material-courses",
745 params(
746 ("course_id" = Uuid, Path, description = "Course id")
747 ),
748 request_body = Vec<NewFeedback>,
749 responses(
750 (status = 200, description = "Created feedback ids", body = Vec<Uuid>)
751 )
752)]
753pub async fn feedback(
754 course_id: web::Path<Uuid>,
755 new_feedback: web::Json<Vec<NewFeedback>>,
756 pool: web::Data<PgPool>,
757 user: Option<AuthUser>,
758) -> ControllerResult<web::Json<Vec<Uuid>>> {
759 let mut conn = pool.acquire().await?;
760 let fs = new_feedback.into_inner();
761 let user_id = user.as_ref().map(|u| u.id);
762
763 for f in &fs {
765 if f.feedback_given.len() > 1000 {
766 return Err(ControllerError::new(
767 ControllerErrorType::BadRequest,
768 "Feedback given too long: max 1000".to_string(),
769 None,
770 ));
771 }
772 if f.related_blocks.len() > 100 {
773 return Err(ControllerError::new(
774 ControllerErrorType::BadRequest,
775 "Too many related blocks: max 100".to_string(),
776 None,
777 ));
778 }
779 for block in &f.related_blocks {
780 if block.text.as_ref().map(|t| t.len()).unwrap_or_default() > 10000 {
781 return Err(ControllerError::new(
782 ControllerErrorType::BadRequest,
783 "Block text too long: max 10000".to_string(),
784 None,
785 ));
786 }
787 }
788 }
789
790 let mut tx = conn.begin().await?;
791 let mut ids = vec![];
792 for f in fs {
793 let id = feedback::insert(&mut tx, PKeyPolicy::Generate, user_id, *course_id, f).await?;
794 ids.push(id);
795 }
796 tx.commit().await?;
797 let token = skip_authorize();
798 token.authorized_ok(web::Json(ids))
799}
800
801#[utoipa::path(
805 post,
806 path = "/{course_slug}/propose-edit",
807 operation_id = "postCourseMaterialCourseEditProposal",
808 tag = "course-material-courses",
809 params(
810 ("course_slug" = String, Path, description = "Course slug")
811 ),
812 request_body = NewProposedPageEdits,
813 responses(
814 (status = 200, description = "Created edit proposal id", body = Uuid)
815 )
816)]
817async fn propose_edit(
818 course_slug: web::Path<String>,
819 edits: web::Json<NewProposedPageEdits>,
820 pool: web::Data<PgPool>,
821 user: Option<AuthUser>,
822) -> ControllerResult<web::Json<Uuid>> {
823 let mut conn = pool.acquire().await?;
824 let course = courses::get_course_by_slug(&mut conn, course_slug.as_str()).await?;
825 let edits = edits.into_inner();
826 let token =
827 authorize_access_to_course_material(&mut conn, user.as_ref().map(|u| u.id), course.id)
828 .await?;
829 let (id, _) = proposed_page_edits::create_for_page_id_and_course_id(
830 &mut conn,
831 PKeyPolicy::Generate,
832 course.id,
833 user.map(|u| u.id),
834 &edits,
835 )
836 .await?;
837 token.authorized_ok(web::Json(id))
838}
839
840#[utoipa::path(
841 get,
842 path = "/{course_id}/glossary",
843 operation_id = "getCourseMaterialGlossary",
844 tag = "course-material-courses",
845 params(
846 ("course_id" = Uuid, Path, description = "Course id")
847 ),
848 responses(
849 (status = 200, description = "Course glossary", body = Vec<Term>)
850 )
851)]
852#[instrument(skip(pool))]
853async fn glossary(
854 pool: web::Data<PgPool>,
855 course_id: web::Path<Uuid>,
856 auth: Option<AuthUser>,
857) -> ControllerResult<web::Json<Vec<Term>>> {
858 let mut conn = pool.acquire().await?;
859 let token =
860 authorize_access_to_course_material(&mut conn, auth.map(|u| u.id), *course_id).await?;
861 let glossary = models::glossary::fetch_for_course(&mut conn, *course_id).await?;
862 token.authorized_ok(web::Json(glossary))
863}
864
865#[utoipa::path(
866 get,
867 path = "/{course_id}/references",
868 operation_id = "getCourseMaterialReferences",
869 tag = "course-material-courses",
870 params(
871 ("course_id" = Uuid, Path, description = "Course id")
872 ),
873 responses(
874 (status = 200, description = "Course references", body = Vec<MaterialReference>)
875 )
876)]
877#[instrument(skip(pool))]
878async fn get_material_references_by_course_id(
879 course_id: web::Path<Uuid>,
880 pool: web::Data<PgPool>,
881 user: Option<AuthUser>,
882) -> ControllerResult<web::Json<Vec<MaterialReference>>> {
883 let mut conn = pool.acquire().await?;
884 let token =
885 authorize_access_to_course_material(&mut conn, user.map(|u| u.id), *course_id).await?;
886 let res =
887 models::material_references::get_references_by_course_id(&mut conn, *course_id).await?;
888
889 token.authorized_ok(web::Json(res))
890}
891
892#[utoipa::path(
896 get,
897 path = "/{course_id}/top-level-pages",
898 operation_id = "getCourseMaterialTopLevelPages",
899 tag = "course-material-courses",
900 params(
901 ("course_id" = Uuid, Path, description = "Course id")
902 ),
903 responses(
904 (status = 200, description = "Top-level pages", body = Vec<Page>)
905 )
906)]
907#[instrument(skip(pool))]
908async fn get_public_top_level_pages(
909 course_id: web::Path<Uuid>,
910 pool: web::Data<PgPool>,
911 auth: Option<AuthUser>,
912) -> ControllerResult<web::Json<Vec<Page>>> {
913 let mut conn = pool.acquire().await?;
914 let user_id = auth.map(|u| u.id);
915 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
916 let page = models::pages::get_course_top_level_pages_by_course_id_and_visibility(
917 &mut conn,
918 *course_id,
919 PageVisibility::Public,
920 )
921 .await?;
922 let page = models::pages::filter_course_material_pages(&mut conn, user_id, page).await?;
923 token.authorized_ok(web::Json(page))
924}
925
926#[utoipa::path(
930 get,
931 path = "/{course_id}/language-versions-navigation-info/from-page/{page_id}",
932 operation_id = "getCourseMaterialLanguageVersionNavigationInfos",
933 tag = "course-material-courses",
934 params(
935 ("course_id" = Uuid, Path, description = "Course id"),
936 ("page_id" = Uuid, Path, description = "Page id")
937 ),
938 responses(
939 (status = 200, description = "Language version navigation info", body = Vec<CourseLanguageVersionNavigationInfo>)
940 )
941)]
942#[instrument(skip(pool))]
943async fn get_all_course_language_versions_navigation_info_from_page(
944 pool: web::Data<PgPool>,
945 path: web::Path<(Uuid, Uuid)>,
946 user: Option<AuthUser>,
947) -> ControllerResult<web::Json<Vec<CourseLanguageVersionNavigationInfo>>> {
948 let mut conn = pool.acquire().await?;
949 let (course_id, page_id) = path.into_inner();
950 let token = skip_authorize();
951 let course = models::courses::get_course(&mut conn, course_id).await?;
952
953 let unfiltered_language_versions =
954 models::courses::get_all_language_versions_of_course(&mut conn, &course).await?;
955
956 let all_pages_in_same_page_language_group =
957 models::page_language_groups::get_all_pages_in_page_language_group_mapping(
958 &mut conn, page_id,
959 )
960 .await?;
961
962 let mut accessible_courses = unfiltered_language_versions
963 .clone()
964 .into_iter()
965 .filter(|c| !c.is_draft)
966 .collect::<Vec<_>>();
967
968 if let Some(user_id) = user.map(|u| u.id) {
970 let user_roles = models::roles::get_roles(&mut conn, user_id).await?;
971
972 for course_version in unfiltered_language_versions.iter().filter(|c| c.is_draft) {
973 if authorize_with_fetched_list_of_roles(
974 &mut conn,
975 Action::ViewMaterial,
976 Some(user_id),
977 Resource::Course(course_version.id),
978 &user_roles,
979 )
980 .await
981 .is_ok()
982 {
983 accessible_courses.push(course_version.clone());
984 }
985 }
986 }
987
988 token.authorized_ok(web::Json(
989 accessible_courses
990 .into_iter()
991 .map(|c| {
992 let page_language_group_navigation_info =
993 all_pages_in_same_page_language_group.get(&CourseOrExamId::Course(c.id));
994 CourseLanguageVersionNavigationInfo::from_course_and_page_info(
995 &c,
996 page_language_group_navigation_info,
997 )
998 })
999 .collect(),
1000 ))
1001}
1002
1003#[utoipa::path(
1007 get,
1008 path = "/{course_id}/pages/by-language-group-id/{page_language_group_id}",
1009 operation_id = "getCourseMaterialPageByCourseIdAndLanguageGroupId",
1010 tag = "course-material-courses",
1011 params(
1012 ("course_id" = Uuid, Path, description = "Course id"),
1013 ("page_language_group_id" = Uuid, Path, description = "Page language group id")
1014 ),
1015 responses(
1016 (status = 200, description = "Page in requested language group", body = Page)
1017 )
1018)]
1019#[instrument(skip(pool))]
1020async fn get_page_by_course_id_and_language_group(
1021 info: web::Path<(Uuid, Uuid)>,
1022 pool: web::Data<PgPool>,
1023 auth: Option<AuthUser>,
1024) -> ControllerResult<web::Json<Page>> {
1025 let mut conn = pool.acquire().await?;
1026 let (course_id, page_language_group_id) = info.into_inner();
1027 let user_id = auth.map(|u| u.id);
1028 let token = authorize_access_to_course_material(&mut conn, user_id, course_id).await?;
1029
1030 let page: Page = models::pages::get_page_by_course_id_and_language_group(
1031 &mut conn,
1032 course_id,
1033 page_language_group_id,
1034 )
1035 .await?;
1036 let page = models::pages::filter_course_material_page(&mut conn, user_id, page).await?;
1037 token.authorized_ok(web::Json(page))
1038}
1039
1040#[utoipa::path(
1044 post,
1045 path = "/{course_id}/course-instances/{course_instance_id}/student-countries/{country_code}",
1046 operation_id = "postCourseMaterialStudentCountry",
1047 tag = "course-material-courses",
1048 params(
1049 ("course_id" = Uuid, Path, description = "Course id"),
1050 ("course_instance_id" = Uuid, Path, description = "Course instance id"),
1051 ("country_code" = String, Path, description = "Country code")
1052 ),
1053 responses(
1054 (status = 200, description = "Student country recorded", body = bool)
1055 )
1056)]
1057#[instrument(skip(pool))]
1058async fn student_country(
1059 query: web::Path<(Uuid, Uuid, String)>,
1060 pool: web::Data<PgPool>,
1061 user: AuthUser,
1062) -> ControllerResult<Json<bool>> {
1063 let mut conn = pool.acquire().await?;
1064 let (course_id, course_instance_id, country_code) = query.into_inner();
1065
1066 models::student_countries::insert(
1067 &mut conn,
1068 user.id,
1069 course_id,
1070 course_instance_id,
1071 &country_code,
1072 )
1073 .await?;
1074 let token = skip_authorize();
1075
1076 token.authorized_ok(Json(true))
1077}
1078
1079#[utoipa::path(
1083 get,
1084 path = "/{course_id}/course-instances/{course_instance_id}/student-countries",
1085 operation_id = "getCourseMaterialStudentCountries",
1086 tag = "course-material-courses",
1087 params(
1088 ("course_id" = Uuid, Path, description = "Course id"),
1089 ("course_instance_id" = Uuid, Path, description = "Course instance id")
1090 ),
1091 responses(
1092 (status = 200, description = "Student country counts", body = HashMap<String, u32>)
1093 )
1094)]
1095#[instrument(skip(pool))]
1096async fn get_student_countries(
1097 query: web::Path<(Uuid, Uuid)>,
1098 pool: web::Data<PgPool>,
1099 user: AuthUser,
1100) -> ControllerResult<web::Json<HashMap<String, u32>>> {
1101 let mut conn = pool.acquire().await?;
1102 let token = skip_authorize();
1103 let (course_id, course_instance_id) = query.into_inner();
1104
1105 let country_codes: Vec<String> =
1106 models::student_countries::get_countries(&mut conn, course_id, course_instance_id)
1107 .await?
1108 .into_iter()
1109 .map(|c| c.country_code)
1110 .collect();
1111
1112 let mut frequency: HashMap<String, u32> = HashMap::new();
1113 for code in country_codes {
1114 *frequency.entry(code).or_insert(0) += 1
1115 }
1116
1117 token.authorized_ok(web::Json(frequency))
1118}
1119
1120#[utoipa::path(
1124 get,
1125 path = "/{course_instance_id}/student-country",
1126 operation_id = "getCourseMaterialStudentCountry",
1127 tag = "course-material-courses",
1128 params(
1129 ("course_instance_id" = Uuid, Path, description = "Course instance id")
1130 ),
1131 responses(
1132 (status = 200, description = "Selected student country", body = StudentCountry)
1133 )
1134)]
1135#[instrument(skip(pool))]
1136async fn get_student_country(
1137 course_instance_id: web::Path<Uuid>,
1138 pool: web::Data<PgPool>,
1139 user: AuthUser,
1140) -> ControllerResult<web::Json<StudentCountry>> {
1141 let mut conn = pool.acquire().await?;
1142 let token = skip_authorize();
1143 let res = models::student_countries::get_selected_country_by_user_id(
1144 &mut conn,
1145 user.id,
1146 *course_instance_id,
1147 )
1148 .await?;
1149
1150 token.authorized_ok(web::Json(res))
1151}
1152
1153#[utoipa::path(
1157 get,
1158 path = "/{course_id}/research-consent-form",
1159 operation_id = "getCourseMaterialResearchConsentForm",
1160 tag = "course-material-courses",
1161 params(
1162 ("course_id" = Uuid, Path, description = "Course id")
1163 ),
1164 responses(
1165 (status = 200, description = "Research consent form", body = Option<ResearchForm>)
1166 )
1167)]
1168#[instrument(skip(pool))]
1169async fn get_research_form_with_course_id(
1170 course_id: web::Path<Uuid>,
1171 user: AuthUser,
1172 pool: web::Data<PgPool>,
1173) -> ControllerResult<web::Json<Option<ResearchForm>>> {
1174 let mut conn = pool.acquire().await?;
1175 let user_id = Some(user.id);
1176
1177 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
1178
1179 let res = models::research_forms::get_research_form_with_course_id(&mut conn, *course_id)
1180 .await
1181 .optional()?;
1182
1183 token.authorized_ok(web::Json(res))
1184}
1185
1186#[utoipa::path(
1190 get,
1191 path = "/{course_id}/research-consent-form-questions",
1192 operation_id = "getCourseMaterialResearchConsentFormQuestions",
1193 tag = "course-material-courses",
1194 params(
1195 ("course_id" = Uuid, Path, description = "Course id")
1196 ),
1197 responses(
1198 (status = 200, description = "Research consent form questions", body = Vec<ResearchFormQuestion>)
1199 )
1200)]
1201#[instrument(skip(pool))]
1202async fn get_research_form_questions_with_course_id(
1203 course_id: web::Path<Uuid>,
1204 user: AuthUser,
1205 pool: web::Data<PgPool>,
1206) -> ControllerResult<web::Json<Vec<ResearchFormQuestion>>> {
1207 let mut conn = pool.acquire().await?;
1208 let user_id = Some(user.id);
1209
1210 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
1211 let res =
1212 models::research_forms::get_research_form_questions_with_course_id(&mut conn, *course_id)
1213 .await?;
1214
1215 token.authorized_ok(web::Json(res))
1216}
1217
1218#[utoipa::path(
1223 post,
1224 path = "/{course_id}/research-consent-form-questions-answer",
1225 operation_id = "postCourseMaterialResearchConsentFormAnswer",
1226 tag = "course-material-courses",
1227 params(
1228 ("course_id" = Uuid, Path, description = "Course id")
1229 ),
1230 request_body = NewResearchFormQuestionAnswer,
1231 responses(
1232 (status = 200, description = "Research consent answer id", body = Uuid)
1233 )
1234)]
1235#[instrument(skip(pool, payload))]
1236async fn upsert_course_research_form_answer(
1237 payload: web::Json<NewResearchFormQuestionAnswer>,
1238 pool: web::Data<PgPool>,
1239 course_id: web::Path<Uuid>,
1240 user: AuthUser,
1241) -> ControllerResult<web::Json<Uuid>> {
1242 let mut conn = pool.acquire().await?;
1243 let user_id = Some(user.id);
1244
1245 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
1246 let answer = payload.into_inner();
1247 let res = models::research_forms::upsert_answer_for_user_id_and_question_id(
1248 &mut conn,
1249 user.id,
1250 *course_id,
1251 answer.research_form_question_id,
1252 answer.research_consent,
1253 )
1254 .await?;
1255
1256 token.authorized_ok(web::Json(res))
1257}
1258
1259#[utoipa::path(
1263 get,
1264 path = "/{course_id}/research-consent-form-user-answers",
1265 operation_id = "getCourseMaterialResearchConsentFormAnswers",
1266 tag = "course-material-courses",
1267 params(
1268 ("course_id" = Uuid, Path, description = "Course id")
1269 ),
1270 responses(
1271 (status = 200, description = "Research consent answers", body = Vec<ResearchFormQuestionAnswer>)
1272 )
1273)]
1274#[instrument(skip(pool))]
1275async fn get_research_form_answers_with_user_id(
1276 course_id: web::Path<Uuid>,
1277 user: AuthUser,
1278 pool: web::Data<PgPool>,
1279) -> ControllerResult<web::Json<Vec<ResearchFormQuestionAnswer>>> {
1280 let mut conn = pool.acquire().await?;
1281 let user_id = Some(user.id);
1282
1283 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
1284
1285 let res = models::research_forms::get_research_form_answers_with_user_id(
1286 &mut conn, *course_id, user.id,
1287 )
1288 .await?;
1289
1290 token.authorized_ok(web::Json(res))
1291}
1292
1293#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
1294
1295pub struct UserMarketingConsentPayload {
1296 pub course_language_groups_id: Uuid,
1297 pub email_subscription: bool,
1298 pub marketing_consent: bool,
1299}
1300
1301#[utoipa::path(
1305 post,
1306 path = "/{course_id}/user-marketing-consent",
1307 operation_id = "updateMarketingConsent",
1308 tag = "course-material-courses",
1309 params(
1310 ("course_id" = Uuid, Path, description = "Course id")
1311 ),
1312 request_body = UserMarketingConsentPayload,
1313 responses(
1314 (status = 200, description = "Marketing consent id", body = Uuid)
1315 )
1316)]
1317#[instrument(skip(pool, payload))]
1318async fn update_marketing_consent(
1319 payload: web::Json<UserMarketingConsentPayload>,
1320 pool: web::Data<PgPool>,
1321 course_id: web::Path<Uuid>,
1322 user: AuthUser,
1323) -> ControllerResult<web::Json<Uuid>> {
1324 let mut conn = pool.acquire().await?;
1325 let user_id = Some(user.id);
1326
1327 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
1328
1329 let email_subscription = if payload.email_subscription {
1330 "subscribed"
1331 } else {
1332 "unsubscribed"
1333 };
1334
1335 let result = models::marketing_consents::upsert_marketing_consent(
1336 &mut conn,
1337 *course_id,
1338 payload.course_language_groups_id,
1339 &user.id,
1340 email_subscription,
1341 payload.marketing_consent,
1342 )
1343 .await?;
1344
1345 token.authorized_ok(web::Json(result))
1346}
1347
1348#[utoipa::path(
1353 get,
1354 path = "/{course_id}/ai-usage-notice-acknowledgement",
1355 operation_id = "getAiUsageNoticeAcknowledgement",
1356 tag = "course-material-courses",
1357 params(
1358 ("course_id" = Uuid, Path, description = "Course id")
1359 ),
1360 responses(
1361 (status = 200, description = "Whether the user has acknowledged the notice", body = bool)
1362 )
1363)]
1364#[instrument(skip(pool))]
1365async fn get_ai_usage_notice_acknowledgement(
1366 pool: web::Data<PgPool>,
1367 course_id: web::Path<Uuid>,
1368 user: Option<AuthUser>,
1369) -> ControllerResult<web::Json<bool>> {
1370 let mut conn = pool.acquire().await?;
1371 let token = skip_authorize();
1372 let acknowledged = match user {
1373 Some(user) => {
1374 models::user_ai_usage_notice_acknowledgements::has_acknowledged(
1375 &mut conn, user.id, *course_id,
1376 )
1377 .await?
1378 }
1379 None => false,
1380 };
1381 token.authorized_ok(web::Json(acknowledged))
1382}
1383
1384#[utoipa::path(
1389 post,
1390 path = "/{course_id}/ai-usage-notice-acknowledgement",
1391 operation_id = "acknowledgeAiUsageNotice",
1392 tag = "course-material-courses",
1393 params(
1394 ("course_id" = Uuid, Path, description = "Course id")
1395 ),
1396 responses(
1397 (status = 200, description = "Acknowledgement recorded", body = bool)
1398 )
1399)]
1400#[instrument(skip(pool))]
1401async fn acknowledge_ai_usage_notice(
1402 pool: web::Data<PgPool>,
1403 course_id: web::Path<Uuid>,
1404 user: AuthUser,
1405) -> ControllerResult<web::Json<bool>> {
1406 let mut conn = pool.acquire().await?;
1407 let token = authorize_access_to_course_material(&mut conn, Some(user.id), *course_id).await?;
1408 models::user_ai_usage_notice_acknowledgements::acknowledge(&mut conn, user.id, *course_id)
1409 .await?;
1410 token.authorized_ok(web::Json(true))
1411}
1412
1413#[utoipa::path(
1417 get,
1418 path = "/{course_id}/fetch-user-marketing-consent",
1419 operation_id = "getCourseMaterialUserMarketingConsent",
1420 tag = "course-material-courses",
1421 params(
1422 ("course_id" = Uuid, Path, description = "Course id")
1423 ),
1424 responses(
1425 (status = 200, description = "User marketing consent", body = Option<UserMarketingConsent>)
1426 )
1427)]
1428#[instrument(skip(pool))]
1429async fn fetch_user_marketing_consent(
1430 pool: web::Data<PgPool>,
1431 course_id: web::Path<Uuid>,
1432 user: AuthUser,
1433) -> ControllerResult<web::Json<Option<UserMarketingConsent>>> {
1434 let mut conn = pool.acquire().await?;
1435 let user_id = Some(user.id);
1436
1437 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
1438
1439 let result =
1440 models::marketing_consents::fetch_user_marketing_consent(&mut conn, *course_id, &user.id)
1441 .await
1442 .ok();
1443
1444 token.authorized_ok(web::Json(result))
1445}
1446
1447#[utoipa::path(
1451 get,
1452 path = "/{course_id}/partners-block",
1453 operation_id = "getCourseMaterialPartnersBlock",
1454 tag = "course-material-courses",
1455 params(
1456 ("course_id" = Uuid, Path, description = "Course id")
1457 ),
1458 responses(
1459 (status = 200, description = "Partners block", body = Option<PartnersBlock>)
1460 )
1461)]
1462#[instrument(skip(pool))]
1463async fn get_partners_block(
1464 path: web::Path<Uuid>,
1465 pool: web::Data<PgPool>,
1466) -> ControllerResult<web::Json<Option<PartnersBlock>>> {
1467 let course_id = path.into_inner();
1468 let mut conn = pool.acquire().await?;
1469 let partner_block = models::partner_block::get_partner_block(&mut conn, course_id)
1470 .await
1471 .optional()?;
1472 let token = skip_authorize();
1473 token.authorized_ok(web::Json(partner_block))
1474}
1475
1476#[utoipa::path(
1480 get,
1481 path = "/{course_id}/privacy-link",
1482 operation_id = "getCourseMaterialPrivacyLink",
1483 tag = "course-material-courses",
1484 params(
1485 ("course_id" = Uuid, Path, description = "Course id")
1486 ),
1487 responses(
1488 (status = 200, description = "Privacy links", body = Vec<PrivacyLink>)
1489 )
1490)]
1491#[instrument(skip(pool))]
1492async fn get_privacy_link(
1493 course_id: web::Path<Uuid>,
1494 pool: web::Data<PgPool>,
1495) -> ControllerResult<web::Json<Vec<PrivacyLink>>> {
1496 let mut conn = pool.acquire().await?;
1497 let privacy_link = models::privacy_link::get_privacy_link(&mut conn, *course_id).await?;
1498 let token = skip_authorize();
1499 token.authorized_ok(web::Json(privacy_link))
1500}
1501
1502#[utoipa::path(
1506 get,
1507 path = "/{course_id}/custom-privacy-policy-checkbox-texts",
1508 operation_id = "getCourseMaterialCustomPrivacyPolicyCheckboxTexts",
1509 tag = "course-material-courses",
1510 params(
1511 ("course_id" = Uuid, Path, description = "Course id")
1512 ),
1513 responses(
1514 (status = 200, description = "Custom privacy policy checkbox texts", body = Vec<CourseCustomPrivacyPolicyCheckboxText>)
1515 )
1516)]
1517#[instrument(skip(pool))]
1518async fn get_custom_privacy_policy_checkbox_texts(
1519 course_id: web::Path<Uuid>,
1520 pool: web::Data<PgPool>,
1521 user: AuthUser, ) -> ControllerResult<web::Json<Vec<CourseCustomPrivacyPolicyCheckboxText>>> {
1523 let mut conn = pool.acquire().await?;
1524
1525 let token = authorize_access_to_course_material(&mut conn, Some(user.id), *course_id).await?;
1526
1527 let texts = models::course_custom_privacy_policy_checkbox_texts::get_all_by_course_id(
1528 &mut conn, *course_id,
1529 )
1530 .await?;
1531
1532 token.authorized_ok(web::Json(texts))
1533}
1534
1535#[utoipa::path(
1541 get,
1542 path = "/{course_id}/user-chapter-locks",
1543 operation_id = "getCourseMaterialUserChapterLocks",
1544 tag = "course-material-courses",
1545 params(
1546 ("course_id" = Uuid, Path, description = "Course id")
1547 ),
1548 responses(
1549 (status = 200, description = "User chapter locking statuses", body = Vec<models::user_chapter_locking_statuses::UserChapterLockingStatus>)
1550 )
1551)]
1552#[instrument(skip(pool))]
1553async fn get_user_chapter_locks(
1554 course_id: web::Path<Uuid>,
1555 pool: web::Data<PgPool>,
1556 user: AuthUser,
1557) -> ControllerResult<web::Json<Vec<models::user_chapter_locking_statuses::UserChapterLockingStatus>>>
1558{
1559 use models::user_chapter_locking_statuses;
1560 let mut conn = pool.acquire().await?;
1561 let token = authorize_access_to_course_material(&mut conn, Some(user.id), *course_id).await?;
1562
1563 let statuses =
1564 user_chapter_locking_statuses::get_or_init_all_for_course(&mut conn, user.id, *course_id)
1565 .await?;
1566
1567 token.authorized_ok(web::Json(statuses))
1568}
1569
1570pub fn _add_routes(cfg: &mut ServiceConfig) {
1578 cfg.route("/{course_id}", web::get().to(get_course))
1579 .route("/{course_id}/chapters", web::get().to(get_chapters))
1580 .route(
1581 "/{course_id}/course-instances",
1582 web::get().to(get_course_instances),
1583 )
1584 .route(
1585 "/{course_id}/current-instance",
1586 web::get().to(get_current_course_instance),
1587 )
1588 .route("/{course_id}/feedback", web::post().to(feedback))
1589 .route(
1590 "/{course_id}/page-by-path/{url_path:.*}",
1591 web::get().to(get_course_page_by_path),
1592 )
1593 .route(
1594 "/{course_id}/search-pages-with-phrase",
1595 web::post().to(search_pages_with_phrase),
1596 )
1597 .route(
1598 "/{course_id}/language-versions-navigation-info/from-page/{page_id}",
1599 web::get().to(get_all_course_language_versions_navigation_info_from_page),
1600 )
1601 .route(
1602 "/{course_id}/search-pages-with-words",
1603 web::post().to(search_pages_with_words),
1604 )
1605 .route(
1606 "/{course_id}/user-settings",
1607 web::get().to(get_user_course_settings),
1608 )
1609 .route(
1610 "/{course_id}/top-level-pages",
1611 web::get().to(get_public_top_level_pages),
1612 )
1613 .route("/{course_id}/propose-edit", web::post().to(propose_edit))
1614 .route("/{course_id}/glossary", web::get().to(glossary))
1615 .route(
1616 "/{course_id}/references",
1617 web::get().to(get_material_references_by_course_id),
1618 )
1619 .route(
1620 "/{course_id}/pages/by-language-group-id/{page_language_group_id}",
1621 web::get().to(get_page_by_course_id_and_language_group),
1622 )
1623 .route("/{course_id}/pages", web::get().to(get_public_course_pages))
1624 .route(
1625 "/{course_id}/course-instances/{course_instance_id}/student-countries/{country_code}",
1626 web::post().to(student_country),
1627 )
1628 .route(
1629 "/{course_instance_id}/student-country",
1630 web::get().to(get_student_country),
1631 )
1632 .route(
1633 "/{course_id}/course-instances/{course_instance_id}/student-countries",
1634 web::get().to(get_student_countries),
1635 )
1636 .route(
1637 "/{course_id}/research-consent-form-questions-answer",
1638 web::post().to(upsert_course_research_form_answer),
1639 )
1640 .route(
1641 "/{courseId}/research-consent-form-user-answers",
1642 web::get().to(get_research_form_answers_with_user_id),
1643 )
1644 .route(
1645 "/{course_id}/research-consent-form",
1646 web::get().to(get_research_form_with_course_id),
1647 )
1648 .route(
1649 "/{course_id}/partners-block",
1650 web::get().to(get_partners_block),
1651 )
1652 .route("/{course_id}/privacy-link", web::get().to(get_privacy_link))
1653 .route(
1654 "/{course_id}/research-consent-form-questions",
1655 web::get().to(get_research_form_questions_with_course_id),
1656 )
1657 .route(
1658 "/{course_id}/user-marketing-consent",
1659 web::post().to(update_marketing_consent),
1660 )
1661 .route(
1662 "/{course_id}/fetch-user-marketing-consent",
1663 web::get().to(fetch_user_marketing_consent),
1664 )
1665 .route(
1666 "/{course_id}/ai-usage-notice-acknowledgement",
1667 web::get().to(get_ai_usage_notice_acknowledgement),
1668 )
1669 .route(
1670 "/{course_id}/ai-usage-notice-acknowledgement",
1671 web::post().to(acknowledge_ai_usage_notice),
1672 )
1673 .route(
1674 "/{course_id}/custom-privacy-policy-checkbox-texts",
1675 web::get().to(get_custom_privacy_policy_checkbox_texts),
1676 )
1677 .route(
1678 "/{course_id}/user-chapter-locks",
1679 web::get().to(get_user_chapter_locks),
1680 );
1681}