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,
45 authorize_with_fetched_list_of_roles, can_user_view_chapter, 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 authorize_with_fetched_list_of_roles(
985 &mut conn,
986 Action::ViewMaterial,
987 Some(user_id),
988 Resource::Course(course_version.id),
989 &user_roles,
990 )
991 .await
992 .is_ok()
993 {
994 accessible_courses.push(course_version.clone());
995 }
996 }
997 }
998
999 token.authorized_ok(web::Json(
1000 accessible_courses
1001 .into_iter()
1002 .map(|c| {
1003 let page_language_group_navigation_info =
1004 all_pages_in_same_page_language_group.get(&CourseOrExamId::Course(c.id));
1005 CourseLanguageVersionNavigationInfo::from_course_and_page_info(
1006 &c,
1007 page_language_group_navigation_info,
1008 )
1009 })
1010 .collect(),
1011 ))
1012}
1013
1014#[utoipa::path(
1018 get,
1019 path = "/{course_id}/pages/by-language-group-id/{page_language_group_id}",
1020 operation_id = "getCourseMaterialPageByCourseIdAndLanguageGroupId",
1021 tag = "course-material-courses",
1022 params(
1023 ("course_id" = Uuid, Path, description = "Course id"),
1024 ("page_language_group_id" = Uuid, Path, description = "Page language group id")
1025 ),
1026 responses(
1027 (status = 200, description = "Page in requested language group", body = Page)
1028 )
1029)]
1030#[instrument(skip(pool))]
1031async fn get_page_by_course_id_and_language_group(
1032 info: web::Path<(Uuid, Uuid)>,
1033 pool: web::Data<PgPool>,
1034 auth: Option<AuthUser>,
1035) -> ControllerResult<web::Json<Page>> {
1036 let mut conn = pool.acquire().await?;
1037 let (course_id, page_language_group_id) = info.into_inner();
1038 let user_id = auth.map(|u| u.id);
1039 let token = authorize_access_to_course_material(&mut conn, user_id, course_id).await?;
1040
1041 let page: Page = models::pages::get_page_by_course_id_and_language_group(
1042 &mut conn,
1043 course_id,
1044 page_language_group_id,
1045 )
1046 .await?;
1047 let page = models::pages::filter_course_material_page(&mut conn, user_id, page).await?;
1048 token.authorized_ok(web::Json(page))
1049}
1050
1051#[utoipa::path(
1055 post,
1056 path = "/{course_id}/course-instances/{course_instance_id}/student-countries/{country_code}",
1057 operation_id = "postCourseMaterialStudentCountry",
1058 tag = "course-material-courses",
1059 params(
1060 ("course_id" = Uuid, Path, description = "Course id"),
1061 ("course_instance_id" = Uuid, Path, description = "Course instance id"),
1062 ("country_code" = String, Path, description = "Country code")
1063 ),
1064 responses(
1065 (status = 200, description = "Student country recorded", body = bool)
1066 )
1067)]
1068#[instrument(skip(pool))]
1069async fn student_country(
1070 query: web::Path<(Uuid, Uuid, String)>,
1071 pool: web::Data<PgPool>,
1072 user: AuthUser,
1073) -> ControllerResult<Json<bool>> {
1074 let mut conn = pool.acquire().await?;
1075 let (course_id, course_instance_id, country_code) = query.into_inner();
1076
1077 models::student_countries::insert(
1078 &mut conn,
1079 user.id,
1080 course_id,
1081 course_instance_id,
1082 &country_code,
1083 )
1084 .await?;
1085 let token = skip_authorize();
1086
1087 token.authorized_ok(Json(true))
1088}
1089
1090#[utoipa::path(
1094 get,
1095 path = "/{course_id}/course-instances/{course_instance_id}/student-countries",
1096 operation_id = "getCourseMaterialStudentCountries",
1097 tag = "course-material-courses",
1098 params(
1099 ("course_id" = Uuid, Path, description = "Course id"),
1100 ("course_instance_id" = Uuid, Path, description = "Course instance id")
1101 ),
1102 responses(
1103 (status = 200, description = "Student country counts", body = HashMap<String, u32>)
1104 )
1105)]
1106#[instrument(skip(pool))]
1107async fn get_student_countries(
1108 query: web::Path<(Uuid, Uuid)>,
1109 pool: web::Data<PgPool>,
1110 user: AuthUser,
1111) -> ControllerResult<web::Json<HashMap<String, u32>>> {
1112 let mut conn = pool.acquire().await?;
1113 let token = skip_authorize();
1114 let (course_id, course_instance_id) = query.into_inner();
1115
1116 let country_codes: Vec<String> =
1117 models::student_countries::get_countries(&mut conn, course_id, course_instance_id)
1118 .await?
1119 .into_iter()
1120 .map(|c| c.country_code)
1121 .collect();
1122
1123 let mut frequency: HashMap<String, u32> = HashMap::new();
1124 for code in country_codes {
1125 *frequency.entry(code).or_insert(0) += 1
1126 }
1127
1128 token.authorized_ok(web::Json(frequency))
1129}
1130
1131#[utoipa::path(
1135 get,
1136 path = "/{course_instance_id}/student-country",
1137 operation_id = "getCourseMaterialStudentCountry",
1138 tag = "course-material-courses",
1139 params(
1140 ("course_instance_id" = Uuid, Path, description = "Course instance id")
1141 ),
1142 responses(
1143 (status = 200, description = "Selected student country", body = StudentCountry)
1144 )
1145)]
1146#[instrument(skip(pool))]
1147async fn get_student_country(
1148 course_instance_id: web::Path<Uuid>,
1149 pool: web::Data<PgPool>,
1150 user: AuthUser,
1151) -> ControllerResult<web::Json<StudentCountry>> {
1152 let mut conn = pool.acquire().await?;
1153 let token = skip_authorize();
1154 let res = models::student_countries::get_selected_country_by_user_id(
1155 &mut conn,
1156 user.id,
1157 *course_instance_id,
1158 )
1159 .await?;
1160
1161 token.authorized_ok(web::Json(res))
1162}
1163
1164#[utoipa::path(
1168 get,
1169 path = "/{course_id}/research-consent-form",
1170 operation_id = "getCourseMaterialResearchConsentForm",
1171 tag = "course-material-courses",
1172 params(
1173 ("course_id" = Uuid, Path, description = "Course id")
1174 ),
1175 responses(
1176 (status = 200, description = "Research consent form", body = Option<ResearchForm>)
1177 )
1178)]
1179#[instrument(skip(pool))]
1180async fn get_research_form_with_course_id(
1181 course_id: web::Path<Uuid>,
1182 user: AuthUser,
1183 pool: web::Data<PgPool>,
1184) -> ControllerResult<web::Json<Option<ResearchForm>>> {
1185 let mut conn = pool.acquire().await?;
1186 let user_id = Some(user.id);
1187
1188 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
1189
1190 let res = models::research_forms::get_research_form_with_course_id(&mut conn, *course_id)
1191 .await
1192 .optional()?;
1193
1194 token.authorized_ok(web::Json(res))
1195}
1196
1197#[utoipa::path(
1201 get,
1202 path = "/{course_id}/research-consent-form-questions",
1203 operation_id = "getCourseMaterialResearchConsentFormQuestions",
1204 tag = "course-material-courses",
1205 params(
1206 ("course_id" = Uuid, Path, description = "Course id")
1207 ),
1208 responses(
1209 (status = 200, description = "Research consent form questions", body = Vec<ResearchFormQuestion>)
1210 )
1211)]
1212#[instrument(skip(pool))]
1213async fn get_research_form_questions_with_course_id(
1214 course_id: web::Path<Uuid>,
1215 user: AuthUser,
1216 pool: web::Data<PgPool>,
1217) -> ControllerResult<web::Json<Vec<ResearchFormQuestion>>> {
1218 let mut conn = pool.acquire().await?;
1219 let user_id = Some(user.id);
1220
1221 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
1222 let res =
1223 models::research_forms::get_research_form_questions_with_course_id(&mut conn, *course_id)
1224 .await?;
1225
1226 token.authorized_ok(web::Json(res))
1227}
1228
1229#[utoipa::path(
1234 post,
1235 path = "/{course_id}/research-consent-form-questions-answer",
1236 operation_id = "postCourseMaterialResearchConsentFormAnswer",
1237 tag = "course-material-courses",
1238 params(
1239 ("course_id" = Uuid, Path, description = "Course id")
1240 ),
1241 request_body = NewResearchFormQuestionAnswer,
1242 responses(
1243 (status = 200, description = "Research consent answer id", body = Uuid)
1244 )
1245)]
1246#[instrument(skip(pool, payload))]
1247async fn upsert_course_research_form_answer(
1248 payload: web::Json<NewResearchFormQuestionAnswer>,
1249 pool: web::Data<PgPool>,
1250 course_id: web::Path<Uuid>,
1251 user: AuthUser,
1252) -> ControllerResult<web::Json<Uuid>> {
1253 let mut conn = pool.acquire().await?;
1254 let user_id = Some(user.id);
1255
1256 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
1257 let answer = payload.into_inner();
1258 let res = models::research_forms::upsert_answer_for_user_id_and_question_id(
1259 &mut conn,
1260 user.id,
1261 *course_id,
1262 answer.research_form_question_id,
1263 answer.research_consent,
1264 )
1265 .await?;
1266
1267 token.authorized_ok(web::Json(res))
1268}
1269
1270#[utoipa::path(
1274 get,
1275 path = "/{course_id}/research-consent-form-user-answers",
1276 operation_id = "getCourseMaterialResearchConsentFormAnswers",
1277 tag = "course-material-courses",
1278 params(
1279 ("course_id" = Uuid, Path, description = "Course id")
1280 ),
1281 responses(
1282 (status = 200, description = "Research consent answers", body = Vec<ResearchFormQuestionAnswer>)
1283 )
1284)]
1285#[instrument(skip(pool))]
1286async fn get_research_form_answers_with_user_id(
1287 course_id: web::Path<Uuid>,
1288 user: AuthUser,
1289 pool: web::Data<PgPool>,
1290) -> ControllerResult<web::Json<Vec<ResearchFormQuestionAnswer>>> {
1291 let mut conn = pool.acquire().await?;
1292 let user_id = Some(user.id);
1293
1294 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
1295
1296 let res = models::research_forms::get_research_form_answers_with_user_id(
1297 &mut conn, *course_id, user.id,
1298 )
1299 .await?;
1300
1301 token.authorized_ok(web::Json(res))
1302}
1303
1304#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
1305
1306pub struct UserMarketingConsentPayload {
1307 pub course_language_groups_id: Uuid,
1308 pub email_subscription: bool,
1309 pub marketing_consent: bool,
1310}
1311
1312#[utoipa::path(
1316 post,
1317 path = "/{course_id}/user-marketing-consent",
1318 operation_id = "updateMarketingConsent",
1319 tag = "course-material-courses",
1320 params(
1321 ("course_id" = Uuid, Path, description = "Course id")
1322 ),
1323 request_body = UserMarketingConsentPayload,
1324 responses(
1325 (status = 200, description = "Marketing consent id", body = Uuid)
1326 )
1327)]
1328#[instrument(skip(pool, payload))]
1329async fn update_marketing_consent(
1330 payload: web::Json<UserMarketingConsentPayload>,
1331 pool: web::Data<PgPool>,
1332 course_id: web::Path<Uuid>,
1333 user: AuthUser,
1334) -> ControllerResult<web::Json<Uuid>> {
1335 let mut conn = pool.acquire().await?;
1336 let user_id = Some(user.id);
1337
1338 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
1339
1340 let email_subscription = if payload.email_subscription {
1341 "subscribed"
1342 } else {
1343 "unsubscribed"
1344 };
1345
1346 let result = models::marketing_consents::upsert_marketing_consent(
1347 &mut conn,
1348 *course_id,
1349 payload.course_language_groups_id,
1350 &user.id,
1351 email_subscription,
1352 payload.marketing_consent,
1353 )
1354 .await?;
1355
1356 token.authorized_ok(web::Json(result))
1357}
1358
1359#[utoipa::path(
1364 get,
1365 path = "/{course_id}/ai-usage-notice-acknowledgement",
1366 operation_id = "getAiUsageNoticeAcknowledgement",
1367 tag = "course-material-courses",
1368 params(
1369 ("course_id" = Uuid, Path, description = "Course id")
1370 ),
1371 responses(
1372 (status = 200, description = "Whether the user has acknowledged the notice", body = bool)
1373 )
1374)]
1375#[instrument(skip(pool))]
1376async fn get_ai_usage_notice_acknowledgement(
1377 pool: web::Data<PgPool>,
1378 course_id: web::Path<Uuid>,
1379 user: Option<AuthUser>,
1380) -> ControllerResult<web::Json<bool>> {
1381 let mut conn = pool.acquire().await?;
1382 let token = skip_authorize();
1383 let acknowledged = match user {
1384 Some(user) => {
1385 models::user_ai_usage_notice_acknowledgements::has_acknowledged(
1386 &mut conn, user.id, *course_id,
1387 )
1388 .await?
1389 }
1390 None => false,
1391 };
1392 token.authorized_ok(web::Json(acknowledged))
1393}
1394
1395#[utoipa::path(
1400 post,
1401 path = "/{course_id}/ai-usage-notice-acknowledgement",
1402 operation_id = "acknowledgeAiUsageNotice",
1403 tag = "course-material-courses",
1404 params(
1405 ("course_id" = Uuid, Path, description = "Course id")
1406 ),
1407 responses(
1408 (status = 200, description = "Acknowledgement recorded", body = bool)
1409 )
1410)]
1411#[instrument(skip(pool))]
1412async fn acknowledge_ai_usage_notice(
1413 pool: web::Data<PgPool>,
1414 course_id: web::Path<Uuid>,
1415 user: AuthUser,
1416) -> ControllerResult<web::Json<bool>> {
1417 let mut conn = pool.acquire().await?;
1418 let token = authorize_access_to_course_material(&mut conn, Some(user.id), *course_id).await?;
1419 models::user_ai_usage_notice_acknowledgements::acknowledge(&mut conn, user.id, *course_id)
1420 .await?;
1421 token.authorized_ok(web::Json(true))
1422}
1423
1424#[utoipa::path(
1428 get,
1429 path = "/{course_id}/fetch-user-marketing-consent",
1430 operation_id = "getCourseMaterialUserMarketingConsent",
1431 tag = "course-material-courses",
1432 params(
1433 ("course_id" = Uuid, Path, description = "Course id")
1434 ),
1435 responses(
1436 (status = 200, description = "User marketing consent", body = Option<UserMarketingConsent>)
1437 )
1438)]
1439#[instrument(skip(pool))]
1440async fn fetch_user_marketing_consent(
1441 pool: web::Data<PgPool>,
1442 course_id: web::Path<Uuid>,
1443 user: AuthUser,
1444) -> ControllerResult<web::Json<Option<UserMarketingConsent>>> {
1445 let mut conn = pool.acquire().await?;
1446 let user_id = Some(user.id);
1447
1448 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
1449
1450 let result =
1451 models::marketing_consents::fetch_user_marketing_consent(&mut conn, *course_id, &user.id)
1452 .await
1453 .ok();
1454
1455 token.authorized_ok(web::Json(result))
1456}
1457
1458#[utoipa::path(
1462 get,
1463 path = "/{course_id}/partners-block",
1464 operation_id = "getCourseMaterialPartnersBlock",
1465 tag = "course-material-courses",
1466 params(
1467 ("course_id" = Uuid, Path, description = "Course id")
1468 ),
1469 responses(
1470 (status = 200, description = "Partners block", body = Option<PartnersBlock>)
1471 )
1472)]
1473#[instrument(skip(pool))]
1474async fn get_partners_block(
1475 path: web::Path<Uuid>,
1476 pool: web::Data<PgPool>,
1477) -> ControllerResult<web::Json<Option<PartnersBlock>>> {
1478 let course_id = path.into_inner();
1479 let mut conn = pool.acquire().await?;
1480 let partner_block = models::partner_block::get_partner_block(&mut conn, course_id)
1481 .await
1482 .optional()?;
1483 let token = skip_authorize();
1484 token.authorized_ok(web::Json(partner_block))
1485}
1486
1487#[utoipa::path(
1491 get,
1492 path = "/{course_id}/privacy-link",
1493 operation_id = "getCourseMaterialPrivacyLink",
1494 tag = "course-material-courses",
1495 params(
1496 ("course_id" = Uuid, Path, description = "Course id")
1497 ),
1498 responses(
1499 (status = 200, description = "Privacy links", body = Vec<PrivacyLink>)
1500 )
1501)]
1502#[instrument(skip(pool))]
1503async fn get_privacy_link(
1504 course_id: web::Path<Uuid>,
1505 pool: web::Data<PgPool>,
1506) -> ControllerResult<web::Json<Vec<PrivacyLink>>> {
1507 let mut conn = pool.acquire().await?;
1508 let privacy_link = models::privacy_link::get_privacy_link(&mut conn, *course_id).await?;
1509 let token = skip_authorize();
1510 token.authorized_ok(web::Json(privacy_link))
1511}
1512
1513#[utoipa::path(
1517 get,
1518 path = "/{course_id}/custom-privacy-policy-checkbox-texts",
1519 operation_id = "getCourseMaterialCustomPrivacyPolicyCheckboxTexts",
1520 tag = "course-material-courses",
1521 params(
1522 ("course_id" = Uuid, Path, description = "Course id")
1523 ),
1524 responses(
1525 (status = 200, description = "Custom privacy policy checkbox texts", body = Vec<CourseCustomPrivacyPolicyCheckboxText>)
1526 )
1527)]
1528#[instrument(skip(pool))]
1529async fn get_custom_privacy_policy_checkbox_texts(
1530 course_id: web::Path<Uuid>,
1531 pool: web::Data<PgPool>,
1532 user: AuthUser, ) -> ControllerResult<web::Json<Vec<CourseCustomPrivacyPolicyCheckboxText>>> {
1534 let mut conn = pool.acquire().await?;
1535
1536 let token = authorize_access_to_course_material(&mut conn, Some(user.id), *course_id).await?;
1537
1538 let texts = models::course_custom_privacy_policy_checkbox_texts::get_all_by_course_id(
1539 &mut conn, *course_id,
1540 )
1541 .await?;
1542
1543 token.authorized_ok(web::Json(texts))
1544}
1545
1546#[utoipa::path(
1552 get,
1553 path = "/{course_id}/user-chapter-locks",
1554 operation_id = "getCourseMaterialUserChapterLocks",
1555 tag = "course-material-courses",
1556 params(
1557 ("course_id" = Uuid, Path, description = "Course id")
1558 ),
1559 responses(
1560 (status = 200, description = "User chapter locking statuses", body = Vec<models::user_chapter_locking_statuses::UserChapterLockingStatus>)
1561 )
1562)]
1563#[instrument(skip(pool))]
1564async fn get_user_chapter_locks(
1565 course_id: web::Path<Uuid>,
1566 pool: web::Data<PgPool>,
1567 user: AuthUser,
1568) -> ControllerResult<web::Json<Vec<models::user_chapter_locking_statuses::UserChapterLockingStatus>>>
1569{
1570 use models::user_chapter_locking_statuses;
1571 let mut conn = pool.acquire().await?;
1572 let token = authorize_access_to_course_material(&mut conn, Some(user.id), *course_id).await?;
1573
1574 let statuses =
1575 user_chapter_locking_statuses::get_or_init_all_for_course(&mut conn, user.id, *course_id)
1576 .await?;
1577
1578 token.authorized_ok(web::Json(statuses))
1579}
1580
1581pub fn _add_routes(cfg: &mut ServiceConfig) {
1589 cfg.route("/{course_id}", web::get().to(get_course))
1590 .route("/{course_id}/chapters", web::get().to(get_chapters))
1591 .route(
1592 "/{course_id}/course-instances",
1593 web::get().to(get_course_instances),
1594 )
1595 .route(
1596 "/{course_id}/current-instance",
1597 web::get().to(get_current_course_instance),
1598 )
1599 .route("/{course_id}/feedback", web::post().to(feedback))
1600 .route(
1601 "/{course_id}/page-by-path/{url_path:.*}",
1602 web::get().to(get_course_page_by_path),
1603 )
1604 .route(
1605 "/{course_id}/search-pages-with-phrase",
1606 web::post().to(search_pages_with_phrase),
1607 )
1608 .route(
1609 "/{course_id}/language-versions-navigation-info/from-page/{page_id}",
1610 web::get().to(get_all_course_language_versions_navigation_info_from_page),
1611 )
1612 .route(
1613 "/{course_id}/search-pages-with-words",
1614 web::post().to(search_pages_with_words),
1615 )
1616 .route(
1617 "/{course_id}/user-settings",
1618 web::get().to(get_user_course_settings),
1619 )
1620 .route(
1621 "/{course_id}/top-level-pages",
1622 web::get().to(get_public_top_level_pages),
1623 )
1624 .route("/{course_id}/propose-edit", web::post().to(propose_edit))
1625 .route("/{course_id}/glossary", web::get().to(glossary))
1626 .route(
1627 "/{course_id}/references",
1628 web::get().to(get_material_references_by_course_id),
1629 )
1630 .route(
1631 "/{course_id}/pages/by-language-group-id/{page_language_group_id}",
1632 web::get().to(get_page_by_course_id_and_language_group),
1633 )
1634 .route("/{course_id}/pages", web::get().to(get_public_course_pages))
1635 .route(
1636 "/{course_id}/course-instances/{course_instance_id}/student-countries/{country_code}",
1637 web::post().to(student_country),
1638 )
1639 .route(
1640 "/{course_instance_id}/student-country",
1641 web::get().to(get_student_country),
1642 )
1643 .route(
1644 "/{course_id}/course-instances/{course_instance_id}/student-countries",
1645 web::get().to(get_student_countries),
1646 )
1647 .route(
1648 "/{course_id}/research-consent-form-questions-answer",
1649 web::post().to(upsert_course_research_form_answer),
1650 )
1651 .route(
1652 "/{courseId}/research-consent-form-user-answers",
1653 web::get().to(get_research_form_answers_with_user_id),
1654 )
1655 .route(
1656 "/{course_id}/research-consent-form",
1657 web::get().to(get_research_form_with_course_id),
1658 )
1659 .route(
1660 "/{course_id}/partners-block",
1661 web::get().to(get_partners_block),
1662 )
1663 .route("/{course_id}/privacy-link", web::get().to(get_privacy_link))
1664 .route(
1665 "/{course_id}/research-consent-form-questions",
1666 web::get().to(get_research_form_questions_with_course_id),
1667 )
1668 .route(
1669 "/{course_id}/user-marketing-consent",
1670 web::post().to(update_marketing_consent),
1671 )
1672 .route(
1673 "/{course_id}/fetch-user-marketing-consent",
1674 web::get().to(fetch_user_marketing_consent),
1675 )
1676 .route(
1677 "/{course_id}/ai-usage-notice-acknowledgement",
1678 web::get().to(get_ai_usage_notice_acknowledgement),
1679 )
1680 .route(
1681 "/{course_id}/ai-usage-notice-acknowledgement",
1682 web::post().to(acknowledge_ai_usage_notice),
1683 )
1684 .route(
1685 "/{course_id}/custom-privacy-policy-checkbox-texts",
1686 web::get().to(get_custom_privacy_policy_checkbox_texts),
1687 )
1688 .route(
1689 "/{course_id}/user-chapter-locks",
1690 web::get().to(get_user_chapter_locks),
1691 );
1692}