headless_lms_server/programs/seed/seed_courses/seed_lock_chapter_course.rs
1use crate::programs::seed::builder::chapter::ChapterBuilder;
2use crate::programs::seed::builder::context::SeedContext;
3use crate::programs::seed::builder::course::{CourseBuilder, CourseInstanceConfig};
4use crate::programs::seed::builder::exercise::{ExerciseBuilder, ExerciseIds};
5use crate::programs::seed::builder::json_source::JsonSource;
6use crate::programs::seed::builder::module::ModuleBuilder;
7use crate::programs::seed::builder::page::PageBuilder;
8use crate::programs::seed::seed_courses::CommonCourseData;
9use anyhow::Result;
10use headless_lms_base::config::ApplicationConfiguration;
11use headless_lms_models::roles::UserRole;
12use headless_lms_utils::{attributes, document_schema_processor::GutenbergBlock};
13use serde_json::json;
14use tracing::info;
15use uuid::Uuid;
16
17pub async fn seed_lock_chapter_course(
18 app_config: &ApplicationConfiguration,
19 course_id: Uuid,
20 course_name: &str,
21 course_slug: &str,
22 common_course_data: CommonCourseData,
23) -> Result<Uuid> {
24 let CommonCourseData {
25 db_pool,
26 organization_id: org,
27 teacher_user_id,
28 student_user_id: _student,
29 langs_user_id: _langs_user_id,
30 example_normal_user_ids: _users,
31 jwt_key: _jwt_key,
32 base_url: _base_url,
33 } = common_course_data;
34
35 let mut conn = db_pool.acquire().await?;
36 let cx = SeedContext {
37 teacher: teacher_user_id,
38 org,
39 base_course_ns: course_id,
40 };
41
42 info!("inserting lock chapter course {}", course_name);
43
44 let course = CourseBuilder::new(course_name, course_slug)
45 .desc("Course for testing chapter locking feature.")
46 .course_id(course_id)
47 .chapter_locking_enabled(true)
48 .instance(CourseInstanceConfig {
49 name: None,
50 description: None,
51 support_email: None,
52 teacher_in_charge_name: "admin".to_string(),
53 teacher_in_charge_email: "admin@example.com".to_string(),
54 opening_time: None,
55 closing_time: None,
56 instance_id: Some(cx.v5(b"1799a6d1-c1b1-4f51-a92f-e832554fc924")),
57 })
58 .role(teacher_user_id, UserRole::Teacher)
59 .module(
60 ModuleBuilder::new()
61 .order(0)
62 .chapter(
63 ChapterBuilder::new(1, "Chapter 1 - Lockable")
64 .fixed_ids(
65 cx.v5(b"b2c3d4e5-f6a7-8901-bcde-f12345678901"),
66 cx.v5(b"c3d4e5f6-a7b8-9012-cdef-123456789012"),
67 )
68 .page(
69 PageBuilder::new("/chapter-1/lock-page", "Lock Chapter Page")
70 .block(
71 GutenbergBlock::block_with_name_attributes_and_inner_blocks(
72 "moocfi/lock-chapter",
73 attributes! {},
74 vec![
75 GutenbergBlock::block_with_name_and_attributes(
76 "core/heading",
77 attributes! {
78 "content": "Model Solution",
79 "level": 2,
80 "anchor": "model-solution-title"
81 },
82 )
83 .with_id(cx.v5(b"883c8ed0-08db-4cd1-a0a4-5cc79c69bdfe")),
84 GutenbergBlock::paragraph("Congratulations on completing Chapter 1! Here's a model solution for the Customer Behavior Analysis Project.")
85 .with_id(cx.v5(b"f2a3b4c5-d6e7-8901-bcde-f1234567891")),
86 GutenbergBlock::paragraph("The project involved analyzing customer purchase data to identify patterns and segment customers. The first step was data cleaning, which included handling missing values using appropriate imputation methods and removing outliers using the IQR method.")
87 .with_id(cx.v5(b"f3a4b5c6-d7e8-9012-cdef-1234567892")),
88 GutenbergBlock::block_with_name_and_attributes(
89 "core/heading",
90 attributes! {
91 "content": "Data Cleaning",
92 "level": 3,
93 "anchor": "data-cleaning"
94 },
95 )
96 .with_id(cx.v5(b"f3b4c5d6-e7f8-9012-def3-3456789012")),
97 GutenbergBlock::block_with_name_and_attributes(
98 "core/code",
99 attributes! {
100 "content": "import pandas as pd
101import numpy as np
102from sklearn.cluster import KMeans
103
104# Load and clean data
105df = pd.read_csv('customer_data.csv')
106df['age'].fillna(df['age'].median(), inplace=True)
107df['income'].fillna(df['income'].mean(), inplace=True)
108
109# Remove outliers
110Q1 = df['purchase_amount'].quantile(0.25)
111Q3 = df['purchase_amount'].quantile(0.75)
112IQR = Q3 - Q1
113df = df[(df['purchase_amount'] >= Q1 - 1.5*IQR) &
114 (df['purchase_amount'] <= Q3 + 1.5*IQR)]"
115 },
116 )
117 .with_id(cx.v5(b"f4a5b6c7-d8e9-0123-def4-2345678903")),
118 GutenbergBlock::block_with_name_and_attributes(
119 "core/heading",
120 attributes! {
121 "content": "Key Findings",
122 "level": 3,
123 "anchor": "key-findings"
124 },
125 )
126 .with_id(cx.v5(b"f4b5c6d7-e8f9-0123-ef45-4567890123")),
127 GutenbergBlock::block_with_name_attributes_and_inner_blocks(
128 "core/list",
129 attributes! {
130 "ordered": false
131 },
132 vec![
133 GutenbergBlock::block_with_name_and_attributes(
134 "core/list-item",
135 attributes! {
136 "content": "Strong positive correlation (r=0.72) between income and purchase amount"
137 },
138 )
139 .with_id(cx.v5(b"f5a6b7c8-d9e0-1234-ef45-3456789014")),
140 GutenbergBlock::block_with_name_and_attributes(
141 "core/list-item",
142 attributes! {
143 "content": "Electronics category generates 35% more revenue than other categories"
144 },
145 )
146 .with_id(cx.v5(b"f6a7b8c9-d0e1-2345-f456-4567890125")),
147 GutenbergBlock::block_with_name_and_attributes(
148 "core/list-item",
149 attributes! {
150 "content": "High-value customers represent 18% of customers but 42% of total revenue"
151 },
152 )
153 .with_id(cx.v5(b"f7a8b9c0-d1e2-3456-5678-5678901236")),
154 ],
155 )
156 .with_id(cx.v5(b"f8a9b0c1-d2e3-4567-6789-6789012347")),
157 GutenbergBlock::block_with_name_and_attributes(
158 "core/quote",
159 attributes! {
160 "value": "The key insight from this analysis is that customer segmentation reveals distinct purchasing behaviors that can inform targeted marketing strategies."
161 },
162 )
163 .with_id(cx.v5(b"f0a1b2c3-d4e5-6789-8901-8901234569")),
164 GutenbergBlock::block_with_name_and_attributes(
165 "core/heading",
166 attributes! {
167 "content": "Recommendations",
168 "level": 3,
169 "anchor": "recommendations"
170 },
171 )
172 .with_id(cx.v5(b"f1b2c3d4-e5f6-7890-f012-0123456789")),
173 GutenbergBlock::paragraph("Based on the analysis, recommendations include developing a VIP program for high-value customers and increasing marketing focus on the Electronics category during peak seasons. The methodology demonstrated here provides a solid framework for data-driven decision making.")
174 .with_id(cx.v5(b"f1a2b3c4-d5e6-7890-9012-9012345670")),
175 ],
176 )
177 .with_id(cx.v5(b"d4e5f6a7-b8c9-0123-def4-234567890123")),
178 )
179 .block(
180 GutenbergBlock::paragraph("This is Chapter 1. You can lock this chapter when you're done.")
181 .with_id(cx.v5(b"e5f6a7b8-c9d0-1234-ef45-345678901234")),
182 ),
183 )
184 .page(
185 PageBuilder::new("/chapter-1/exercise-page", "Exercise in Chapter 1")
186 .exercise(ExerciseBuilder::quizzes(
187 "Chapter 1 Exercise",
188 ExerciseIds {
189 exercise_id: cx.v5(b"f6a7b8c9-d0e1-2345-f456-456789012345"),
190 slide_id: cx.v5(b"a7b8c9d0-e1f2-3456-5678-567890123456"),
191 task_id: cx.v5(b"b8c9d0e1-f2a3-4567-6789-678901234567"),
192 block_id: cx.v5(b"c9d0e1f2-a3b4-5678-7890-789012345678"),
193 },
194 false,
195 None,
196 JsonSource::Inline(json!({
197 "version": "2",
198 "title": "Chapter 1 Exercise",
199 "body": "Select the correct answer.",
200 "awardPointsEvenIfWrong": false,
201 "grantPointsPolicy": "grant_whenever_possible",
202 "quizItemDisplayDirection": "vertical",
203 "submitMessage": "",
204 "items": [
205 {
206 "type": "multiple-choice",
207 "id": "d0e1f2a3-b4c5-6789-8901-890123456789",
208 "failureMessage": "",
209 "options": [
210 {
211 "order": 1,
212 "additionalCorrectnessExplanationOnModelSolution": "",
213 "body": "",
214 "correct": true,
215 "id": "e1f2a3b4-c5d6-7890-9012-901234567890",
216 "messageAfterSubmissionWhenSelected": "",
217 "title": "Correct answer"
218 },
219 {
220 "order": 2,
221 "additionalCorrectnessExplanationOnModelSolution": "",
222 "body": "",
223 "correct": false,
224 "id": "f2a3b4c5-d6e7-8901-0123-012345678901",
225 "messageAfterSubmissionWhenSelected": "",
226 "title": "Wrong answer"
227 }
228 ],
229 "order": 0,
230 "successMessage": "",
231 "title": "What is the correct answer?",
232 "body": ""
233 }
234 ]
235 })),
236 vec![],
237 true,
238 )),
239 )
240 .page(
241 PageBuilder::new(
242 "/chapter-1/auto-graded-exercise-page",
243 "Auto-graded Exercise in Chapter 1",
244 )
245 .exercise(ExerciseBuilder::quizzes(
246 "Chapter 1 Auto-graded Exercise",
247 ExerciseIds {
248 exercise_id: cx.v5(b"a1b2c3d4-e5f6-7890-abcd-ef1234567891"),
249 slide_id: cx.v5(b"b2c3d4e5-f6a7-8901-bcde-f123456789012"),
250 task_id: cx.v5(b"c3d4e5f6-a7b8-9012-cdef-123456789013"),
251 block_id: cx.v5(b"d4e5f6a7-b8c9-0123-def4-234567890124"),
252 },
253 false,
254 None,
255 JsonSource::Inline(json!({
256 "version": "2",
257 "title": "Chapter 1 Auto-graded Exercise",
258 "body": "Select the correct answer.",
259 "awardPointsEvenIfWrong": false,
260 "grantPointsPolicy": "grant_whenever_possible",
261 "quizItemDisplayDirection": "vertical",
262 "submitMessage": "",
263 "items": [
264 {
265 "type": "multiple-choice",
266 "id": "e4f5a6b7-c8d9-0123-ef45-345678901235",
267 "failureMessage": "",
268 "options": [
269 {
270 "order": 1,
271 "additionalCorrectnessExplanationOnModelSolution": "",
272 "body": "",
273 "correct": true,
274 "id": "f5a6b7c8-d9e0-1234-f456-456789012346",
275 "messageAfterSubmissionWhenSelected": "",
276 "title": "Correct answer"
277 },
278 {
279 "order": 2,
280 "additionalCorrectnessExplanationOnModelSolution": "",
281 "body": "",
282 "correct": false,
283 "id": "a6b7c8d9-e0f1-2345-5678-567890123457",
284 "messageAfterSubmissionWhenSelected": "",
285 "title": "Wrong answer"
286 }
287 ],
288 "order": 0,
289 "successMessage": "",
290 "title": "What is the correct answer?",
291 "body": ""
292 }
293 ]
294 })),
295 vec![],
296 false,
297 )),
298 ),
299 )
300 .chapter(
301 ChapterBuilder::new(2, "Chapter 2 - Add Lock Later")
302 .fixed_ids(
303 cx.v5(b"z1y2x3w4-v5u6-7890-tsrq-pq9876543210"),
304 cx.v5(b"y2x3w4v5-u6t7-8901-srqp-op8765432109"),
305 )
306 .page(
307 PageBuilder::new("/chapter-2/lock-page", "Lock Chapter Page")
308 .block(
309 GutenbergBlock::block_with_name_attributes_and_inner_blocks(
310 "moocfi/lock-chapter",
311 attributes! {},
312 vec![
313 GutenbergBlock::block_with_name_and_attributes(
314 "core/heading",
315 attributes! {
316 "content": "Model Solution",
317 "level": 2,
318 "anchor": "model-solution-title"
319 },
320 )
321 .with_id(cx.v5(b"g1h2i3j4-k5l6-7890-mnop-qr1234567890")),
322 GutenbergBlock::paragraph("Congratulations on completing Chapter 2! Here's a model solution for the exercises.")
323 .with_id(cx.v5(b"g2h3i4j5-k6l7-8901-nopq-rs2345678901")),
324 ],
325 )
326 .with_id(cx.v5(b"g3h4i5j6-k7l8-9012-opqr-st3456789012")),
327 )
328 .block(
329 GutenbergBlock::paragraph("This is Chapter 2. You can lock this chapter when you're done.")
330 .with_id(cx.v5(b"g4h5i6j7-k8l9-0123-pqrs-tu4567890123")),
331 ),
332 )
333 .page(
334 PageBuilder::new("/chapter-2/exercise-page", "Exercise in Chapter 2")
335 .block(
336 GutenbergBlock::paragraph("This is Chapter 2.")
337 .with_id(cx.v5(b"x3w4v5u6-t7s8-9012-rqpo-no7654321098")),
338 )
339 .exercise(ExerciseBuilder::quizzes(
340 "Chapter 2 Exercise",
341 ExerciseIds {
342 exercise_id: cx.v5(b"w4v5u6t7-s8r9-0123-qpon-mn6543210987"),
343 slide_id: cx.v5(b"v5u6t7s8-r9q0-1234-ponm-lm5432109876"),
344 task_id: cx.v5(b"u6t7s8r9-q0p1-2345-onml-kl4321098765"),
345 block_id: cx.v5(b"t7s8r9q0-p1o2-3456-nmlk-jk3210987654"),
346 },
347 false,
348 None,
349 JsonSource::Inline(json!({
350 "version": "2",
351 "title": "Chapter 2 Exercise",
352 "body": "Select the correct answer.",
353 "awardPointsEvenIfWrong": false,
354 "grantPointsPolicy": "grant_whenever_possible",
355 "quizItemDisplayDirection": "vertical",
356 "submitMessage": "",
357 "items": [
358 {
359 "type": "multiple-choice",
360 "id": "b8c9d0e1-f2a3-4567-6789-678901234567",
361 "failureMessage": "",
362 "options": [
363 {
364 "order": 1,
365 "additionalCorrectnessExplanationOnModelSolution": "",
366 "body": "",
367 "correct": true,
368 "id": "c9d0e1f2-a3b4-5678-7890-789012345678",
369 "messageAfterSubmissionWhenSelected": "",
370 "title": "Correct answer"
371 },
372 {
373 "order": 2,
374 "additionalCorrectnessExplanationOnModelSolution": "",
375 "body": "",
376 "correct": false,
377 "id": "d0e1f2a3-b4c5-6789-8901-890123456789",
378 "messageAfterSubmissionWhenSelected": "",
379 "title": "Wrong answer"
380 }
381 ],
382 "order": 0,
383 "successMessage": "",
384 "title": "What is the correct answer?",
385 "body": ""
386 }
387 ]
388 })),
389 vec![],
390 true,
391 )),
392 ),
393 ),
394 );
395
396 let (course, _default_instance, _last_module) = course.seed(&mut conn, app_config, &cx).await?;
397
398 Ok(course.id)
399}