1use anyhow::Result;
2use chrono::{DateTime, Utc};
3use headless_lms_utils::document_schema_processor::GutenbergBlock;
4use serde_json::Value;
5use uuid::Uuid;
6
7use headless_lms_models::{
8 pages::{CmsPageExercise, CmsPageExerciseSlide, CmsPageExerciseTask},
9 peer_or_self_review_configs::CmsPeerOrSelfReviewConfig,
10 peer_or_self_review_questions::CmsPeerOrSelfReviewQuestion,
11};
12
13use crate::programs::seed::{
14 builder::{context::SeedContext, json_source::JsonSource},
15 seed_helpers::example_exercise_flexible,
16};
17use headless_lms_utils::attributes;
18
19#[derive(Debug, Clone)]
21pub struct ExerciseIds {
22 pub exercise_id: Uuid,
23 pub slide_id: Uuid,
24 pub task_id: Uuid,
25 pub block_id: Uuid,
26}
27
28#[derive(Debug, Clone)]
30pub enum ExerciseBuilder {
31 ExampleExercise {
33 ids: ExerciseIds,
34 assignment_blocks: Vec<GutenbergBlock>,
35 name: String,
36 options: Value,
37 needs_peer_review: bool,
38 peer_or_self_review_config: Option<CmsPeerOrSelfReviewConfig>,
39 peer_or_self_review_questions: Option<Vec<CmsPeerOrSelfReviewQuestion>>,
40 },
41 Quizzes {
43 ids: ExerciseIds,
44 name: String,
45 needs_peer_review: bool,
46 deadline: Option<DateTime<Utc>>,
47 spec: JsonSource,
48 assignment_blocks: Vec<GutenbergBlock>,
49 },
50 Tmc {
52 ids: ExerciseIds,
53 name: String,
54 deadline: Option<DateTime<Utc>>,
55 spec: JsonSource,
56 assignment_blocks: Vec<GutenbergBlock>,
57 },
58}
59
60impl ExerciseBuilder {
61 pub fn quizzes(
62 name: impl Into<String>,
63 ids: ExerciseIds,
64 needs_pr: bool,
65 deadline: Option<DateTime<Utc>>,
66 spec: JsonSource,
67 assignment_blocks: Vec<GutenbergBlock>,
68 ) -> Self {
69 Self::Quizzes {
70 ids,
71 name: name.into(),
72 needs_peer_review: needs_pr,
73 deadline,
74 spec,
75 assignment_blocks,
76 }
77 }
78
79 pub fn tmc(
80 name: impl Into<String>,
81 ids: ExerciseIds,
82 deadline: Option<DateTime<Utc>>,
83 spec: JsonSource,
84 assignment_blocks: Vec<GutenbergBlock>,
85 ) -> Self {
86 Self::Tmc {
87 ids,
88 name: name.into(),
89 deadline,
90 spec,
91 assignment_blocks,
92 }
93 }
94
95 pub fn example_exercise(
96 name: impl Into<String>,
97 ids: ExerciseIds,
98 assignment_blocks: Vec<GutenbergBlock>,
99 options: Value,
100 needs_peer_review: bool,
101 peer_or_self_review_config: Option<CmsPeerOrSelfReviewConfig>,
102 peer_or_self_review_questions: Option<Vec<CmsPeerOrSelfReviewQuestion>>,
103 ) -> Self {
104 Self::ExampleExercise {
105 ids,
106 assignment_blocks,
107 name: name.into(),
108 options,
109 needs_peer_review,
110 peer_or_self_review_config,
111 peer_or_self_review_questions,
112 }
113 }
114
115 pub(crate) fn to_cms(
116 &self,
117 _cx: &SeedContext,
118 ) -> Result<(
119 GutenbergBlock,
120 CmsPageExercise,
121 CmsPageExerciseSlide,
122 CmsPageExerciseTask,
123 )> {
124 Ok(match self {
125 ExerciseBuilder::Quizzes {
126 ids,
127 name,
128 needs_peer_review: _,
129 deadline: _,
130 spec,
131 assignment_blocks,
132 } => {
133 let spec_v = spec.load()?;
134 let assignment_json = serde_json::to_value(assignment_blocks)?;
135 let (block, exercise, mut slides, mut tasks) = example_exercise_flexible(
136 ids.exercise_id,
137 name.clone(),
138 vec![(
139 ids.slide_id,
140 vec![(ids.task_id, "quizzes".to_string(), assignment_json, spec_v)],
141 )],
142 ids.block_id,
143 None,
144 None,
145 None,
146 );
147 let slide = slides.swap_remove(0);
148 let task = tasks.swap_remove(0);
149 (block, exercise, slide, task)
150 }
151
152 ExerciseBuilder::Tmc {
153 ids,
154 name,
155 deadline: _,
156 spec,
157 assignment_blocks,
158 } => {
159 let spec_v = spec.load()?;
160 let assignment_json = serde_json::to_value(assignment_blocks)?;
161 let (block, exercise, mut slides, mut tasks) = example_exercise_flexible(
162 ids.exercise_id,
163 name.clone(),
164 vec![(
165 ids.slide_id,
166 vec![(ids.task_id, "tmc".to_string(), assignment_json, spec_v)],
167 )],
168 ids.block_id,
169 Some(false),
170 None,
171 None,
172 );
173 let slide = slides.swap_remove(0);
174 let task = tasks.swap_remove(0);
175 (block, exercise, slide, task)
176 }
177
178 ExerciseBuilder::ExampleExercise {
179 ids,
180 assignment_blocks,
181 name,
182 options,
183 needs_peer_review,
184 peer_or_self_review_config,
185 peer_or_self_review_questions,
186 } => {
187 let assignment_json = serde_json::to_value(assignment_blocks)?;
188 let (_block, exercise, slides, tasks) = example_exercise_flexible(
189 ids.exercise_id,
190 name.clone(),
191 vec![(
192 ids.slide_id,
193 vec![(
194 ids.task_id,
195 "example-exercise".to_string(),
196 assignment_json,
197 options.clone(),
198 )],
199 )],
200 ids.block_id,
201 Some(*needs_peer_review),
202 peer_or_self_review_config.clone(),
203 peer_or_self_review_questions.clone(),
204 );
205
206 let slide = slides
207 .into_iter()
208 .next()
209 .expect("example exercise must produce one slide");
210 let task = tasks
211 .into_iter()
212 .next()
213 .expect("example exercise must produce one task");
214
215 let b = GutenbergBlock {
216 client_id: ids.block_id,
217 name: "moocfi/exercise".to_string(),
218 is_valid: true,
219 attributes: attributes! {
220 "id": ids.exercise_id,
221 "name": exercise.name,
222 "dropCap": false,
223 },
224 inner_blocks: vec![],
225 };
226
227 (b, exercise, slide, task)
228 }
229 })
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use crate::programs::seed::seed_helpers::paragraph;
237 use chrono::Utc;
238 use serde_json::json;
239 use uuid::Uuid;
240
241 fn create_test_ids() -> ExerciseIds {
242 ExerciseIds {
243 exercise_id: Uuid::new_v4(),
244 slide_id: Uuid::new_v4(),
245 task_id: Uuid::new_v4(),
246 block_id: Uuid::new_v4(),
247 }
248 }
249
250 #[test]
251 fn test_quizzes_builder_creation() {
252 let ids = create_test_ids();
253 let assignment_blocks = vec![paragraph("Answer this question.", Uuid::new_v4())];
254 let spec = JsonSource::Inline(json!({"type": "multiple_choice"}));
255 let deadline = Some(Utc::now());
256
257 let builder = ExerciseBuilder::quizzes(
258 "Test Quiz",
259 ids.clone(),
260 true,
261 deadline,
262 spec,
263 assignment_blocks.clone(),
264 );
265
266 match builder {
267 ExerciseBuilder::Quizzes {
268 ids: builder_ids,
269 name,
270 needs_peer_review,
271 deadline: builder_deadline,
272 spec: _,
273 assignment_blocks: builder_blocks,
274 } => {
275 assert_eq!(name, "Test Quiz");
276 assert!(needs_peer_review);
277 assert_eq!(builder_deadline, deadline);
278 assert_eq!(builder_ids.exercise_id, ids.exercise_id);
279 assert_eq!(builder_blocks.len(), 1);
280 assert_eq!(builder_blocks[0].name, "core/paragraph");
281 }
282 _ => panic!("Expected Quizzes variant"),
283 }
284 }
285
286 #[test]
287 fn test_tmc_builder_creation() {
288 let ids = create_test_ids();
289 let assignment_blocks = vec![paragraph("Write an `add` function.", Uuid::new_v4())];
290 let spec = JsonSource::Inline(json!({"type": "tmc"}));
291 let deadline = Some(Utc::now());
292
293 let builder = ExerciseBuilder::tmc(
294 "Test TMC",
295 ids.clone(),
296 deadline,
297 spec,
298 assignment_blocks.clone(),
299 );
300
301 match builder {
302 ExerciseBuilder::Tmc {
303 ids: builder_ids,
304 name,
305 deadline: builder_deadline,
306 spec: _,
307 assignment_blocks: builder_blocks,
308 } => {
309 assert_eq!(name, "Test TMC");
310 assert_eq!(builder_deadline, deadline);
311 assert_eq!(builder_ids.exercise_id, ids.exercise_id);
312 assert_eq!(builder_blocks.len(), 1);
313 assert_eq!(builder_blocks[0].name, "core/paragraph");
314 }
315 _ => panic!("Expected Tmc variant"),
316 }
317 }
318
319 #[test]
320 fn test_example_exercise_builder_creation() {
321 let ids = create_test_ids();
322 let assignment_blocks = vec![paragraph("Answer this question.", Uuid::new_v4())];
323 let options = json!({
324 "options": [
325 {"text": "Option 1", "correct": true},
326 {"text": "Option 2", "correct": false}
327 ]
328 });
329
330 let builder = ExerciseBuilder::example_exercise(
331 "Test Example Exercise",
332 ids.clone(),
333 assignment_blocks.clone(),
334 options.clone(),
335 false,
336 None,
337 None,
338 );
339
340 match builder {
341 ExerciseBuilder::ExampleExercise {
342 ids: builder_ids,
343 assignment_blocks: builder_blocks,
344 name,
345 options: builder_options,
346 needs_peer_review: _,
347 peer_or_self_review_config: _,
348 peer_or_self_review_questions: _,
349 } => {
350 assert_eq!(name, "Test Example Exercise");
351 assert_eq!(builder_ids.exercise_id, ids.exercise_id);
352 assert_eq!(builder_blocks.len(), 1);
353 assert_eq!(builder_blocks[0].name, "core/paragraph");
354 assert_eq!(builder_options, options);
355 }
356 _ => panic!("Expected ExampleExercise variant"),
357 }
358 }
359
360 #[test]
361 fn test_exercise_ids_structure() {
362 let ids = create_test_ids();
363
364 assert_ne!(ids.exercise_id, ids.slide_id);
365 assert_ne!(ids.exercise_id, ids.task_id);
366 assert_ne!(ids.exercise_id, ids.block_id);
367 assert_ne!(ids.slide_id, ids.task_id);
368 assert_ne!(ids.slide_id, ids.block_id);
369 assert_ne!(ids.task_id, ids.block_id);
370 }
371
372 #[test]
373 fn test_builder_without_deadline() {
374 let ids = create_test_ids();
375 let assignment_blocks = vec![paragraph("Answer this question.", Uuid::new_v4())];
376 let spec = JsonSource::Inline(json!({"type": "multiple_choice"}));
377
378 let builder = ExerciseBuilder::quizzes(
379 "No Deadline Quiz",
380 ids,
381 false,
382 None,
383 spec,
384 assignment_blocks,
385 );
386
387 match builder {
388 ExerciseBuilder::Quizzes {
389 deadline,
390 needs_peer_review,
391 ..
392 } => {
393 assert_eq!(deadline, None);
394 assert!(!needs_peer_review);
395 }
396 _ => panic!("Expected Quizzes variant"),
397 }
398 }
399
400 #[test]
401 fn test_string_conversion_for_names() {
402 let ids = create_test_ids();
403 let assignment_blocks = vec![paragraph("Answer this question.", Uuid::new_v4())];
404 let spec = JsonSource::Inline(json!({"type": "multiple_choice"}));
405
406 let builder1 = ExerciseBuilder::quizzes(
407 "String name".to_string(),
408 ids.clone(),
409 false,
410 None,
411 spec.clone(),
412 assignment_blocks.clone(),
413 );
414
415 let builder2 =
416 ExerciseBuilder::quizzes("&str name", ids, false, None, spec, assignment_blocks);
417
418 match (builder1, builder2) {
419 (
420 ExerciseBuilder::Quizzes { name: name1, .. },
421 ExerciseBuilder::Quizzes { name: name2, .. },
422 ) => {
423 assert_eq!(name1, "String name");
424 assert_eq!(name2, "&str name");
425 }
426 _ => panic!("Expected Quizzes variants"),
427 }
428 }
429}