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