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