Skip to content

Commit 75bdc9f

Browse files
authored
Merge pull request #190 from layer5io/issue/887-canonical-quiz-json
fix: emit canonical academy quiz JSON
2 parents 8308294 + 0b1664a commit 75bdc9f

10 files changed

Lines changed: 223 additions & 78 deletions

File tree

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,22 @@ the build can emit warnings or break shared assumptions for other content.
6969
Shared theme assets, icons, and reusable partials should stay in
7070
academy-theme itself rather than in a consuming org repository.
7171

72+
## Quiz JSON contract
73+
74+
Quiz JSON emitted by the `test` layouts is normalized toward the
75+
`meshery/schemas` academy Quiz contract. The generated JSON now uses
76+
canonical quiz/question field names, numeric `timeLimit`, hyphenated question
77+
types, and UUIDs for page, parent, question, and option IDs.
78+
79+
Authors can still keep front matter ergonomic:
80+
81+
- page, question, and option `id` values may stay short slugs;
82+
- legacy snake_case quiz keys such as `pass_percentage`, `time_limit`,
83+
`max_attempts`, `correct_answer`, and `is_correct` are still accepted in
84+
front matter during the transition;
85+
- emitted UUIDs are derived deterministically from the content file path plus
86+
the authored ID, so repeated builds produce stable values.
87+
7288
## ID Validation
7389

7490
The theme checks publishable root Academy content during Hugo builds and emits

archetypes/final-test.md

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,70 @@
11
---
22
title: '{{ replace .File.ContentBaseName `-` ` ` | title }}'
3-
pass_percentage: 70 # Minimum percentage required to pass the test
4-
time_limit: 15 # Duration of the test in minutes
3+
passPercentage: 70 # Minimum percentage required to pass the test
4+
timeLimit: 15 # Duration of the test in minutes
55
level: "beginner" # Difficulty level of the test
66
category: "Programming Languages" # Category of the test
77
tags: ["golang", "basics", "syntax", "fundamentals"] # Tags for the test, useful for search and categorization
88
type: "test" # Type of the content, in this case, a test ( required for the test to be recognized by the system )
99
final: true # Indicates that this test is the final exam and must be completed to complete the course , module or section
1010

1111
questions:
12+
# IDs can stay short slugs in front matter; academy-theme derives stable UUIDs in emitted quiz JSON.
1213
# Multiple Choice Question (Single Answer)
1314
# NOTE: The 'marks' field must be a positive number (greater than 0). Negative or zero values will cause a build error.
1415
- id: "q1"
1516
text: "What keyword is used to define a function in Go?"
16-
type: "multiple_answers"
17+
type: "single-answer"
1718
marks: 2
1819
explanation: "The 'func' keyword is used to declare functions in Go, similar to how 'function' is used in JavaScript."
1920
options:
2021
- id: "a"
2122
text: "function"
22-
is_correct: false
23+
isCorrect: false
2324
- id: "b"
2425
text: "def"
25-
is_correct: false
26+
isCorrect: false
2627
- id: "c"
2728
text: "func"
28-
is_correct: true
29+
isCorrect: true
2930
- id: "d"
3031
text: "fn"
31-
is_correct: false
32+
isCorrect: false
3233

3334
# Short Answer Question
3435
- id: "q2"
3536
text: "Go is a statically typed language. (true/false)"
36-
type: "short_answer"
37+
type: "short-answer"
3738
marks: 2
38-
correct_answer: "true"
39+
correctAnswer: "true"
3940
case_sensitive: false
4041
explanation: "Go is indeed a statically typed language, meaning variable types are determined at compile time."
4142

4243
# Short Answer Question (Numeric)
4344
- id: "q3"
4445
text: "What is the zero value of an uninitialized int in Go?"
45-
type: "short_answer"
46+
type: "short-answer"
4647
marks: 2
47-
correct_answer: "0"
48+
correctAnswer: "0"
4849
explanation: "In Go, the zero value for numeric types like int is 0."
4950

5051
# Multiple Choice Question (Multiple Answers)
5152
- id: "q4"
5253
text: "What are the purposes of the 'defer' keyword in Go? (Select all that apply)"
53-
type: "multiple_answers"
54+
type: "multiple-answers"
5455
marks: 2
5556
explanation: "The defer keyword is commonly used to delay function execution until the surrounding function returns, often used for cleanup tasks like closing files."
5657
options:
5758
- id: "a"
5859
text: "To delay the execution of a function until the surrounding function returns"
59-
is_correct: true
60+
isCorrect: true
6061
- id: "b"
6162
text: "To define a constant value"
62-
is_correct: false
63+
isCorrect: false
6364
- id: "c"
6465
text: "To close resources like files or network connections"
65-
is_correct: true
66+
isCorrect: true
6667
- id: "d"
6768
text: "To handle errors in a function"
68-
is_correct: false
69+
isCorrect: false
6970
---

archetypes/optional-test.md

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,70 @@
11
---
22
title: '{{ replace .File.ContentBaseName `-` ` ` | title }}'
3-
pass_percentage: 70 # Minimum percentage required to pass the test
4-
time_limit: 15 # Duration of the test in minutes
3+
passPercentage: 70 # Minimum percentage required to pass the test
4+
timeLimit: 15 # Duration of the test in minutes
55
level: "beginner" # Difficulty level of the test
66
category: "Programming Languages" # Category of the test
77
tags: ["golang", "basics", "syntax", "fundamentals"] # Tags for the test, useful for search and categorization
88
type: "test" # Type of the content, in this case, a test ( required for the test to be recognized by the system )
99
is_optional: true # Indicates that this test is optional and does not need to be completed to take the final exam
1010

1111
questions:
12+
# IDs can stay short slugs in front matter; academy-theme derives stable UUIDs in emitted quiz JSON.
1213
# Multiple Choice Question (Single Answer)
1314
# NOTE: The 'marks' field must be a positive number (greater than 0). Negative or zero values will cause a build error.
1415
- id: "q1"
1516
text: "What keyword is used to define a function in Go?"
16-
type: "multiple_answers"
17+
type: "single-answer"
1718
marks: 2
1819
explanation: "The 'func' keyword is used to declare functions in Go, similar to how 'function' is used in JavaScript."
1920
options:
2021
- id: "a"
2122
text: "function"
22-
is_correct: false
23+
isCorrect: false
2324
- id: "b"
2425
text: "def"
25-
is_correct: false
26+
isCorrect: false
2627
- id: "c"
2728
text: "func"
28-
is_correct: true
29+
isCorrect: true
2930
- id: "d"
3031
text: "fn"
31-
is_correct: false
32+
isCorrect: false
3233

3334
# Short Answer Question
3435
- id: "q2"
3536
text: "Go is a statically typed language. (true/false)"
36-
type: "short_answer"
37+
type: "short-answer"
3738
marks: 2
38-
correct_answer: "true"
39+
correctAnswer: "true"
3940
case_sensitive: false
4041
explanation: "Go is indeed a statically typed language, meaning variable types are determined at compile time."
4142

4243
# Short Answer Question (Numeric)
4344
- id: "q3"
4445
text: "What is the zero value of an uninitialized int in Go?"
45-
type: "short_answer"
46+
type: "short-answer"
4647
marks: 2
47-
correct_answer: "0"
48+
correctAnswer: "0"
4849
explanation: "In Go, the zero value for numeric types like int is 0."
4950

5051
# Multiple Choice Question (Multiple Answers)
5152
- id: "q4"
5253
text: "What are the purposes of the 'defer' keyword in Go? (Select all that apply)"
53-
type: "multiple_answers"
54+
type: "multiple-answers"
5455
marks: 2
5556
explanation: "The defer keyword is commonly used to delay function execution until the surrounding function returns, often used for cleanup tasks like closing files."
5657
options:
5758
- id: "a"
5859
text: "To delay the execution of a function until the surrounding function returns"
59-
is_correct: true
60+
isCorrect: true
6061
- id: "b"
6162
text: "To define a constant value"
62-
is_correct: false
63+
isCorrect: false
6364
- id: "c"
6465
text: "To close resources like files or network connections"
65-
is_correct: true
66+
isCorrect: true
6667
- id: "d"
6768
text: "To handle errors in a function"
68-
is_correct: false
69+
isCorrect: false
6970
---

archetypes/test.md

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,50 @@
11
---
22
title: 'test'
3-
pass_percentage: 70 # Minimum percentage required to pass the test
4-
time_limit: 15 # Duration of the test in minutes
5-
max_attempts: 3 # Maximum number of attempts allowed
3+
passPercentage: 70 # Minimum percentage required to pass the test
4+
timeLimit: 15 # Duration of the test in minutes
5+
maxAttempts: 3 # Maximum number of attempts allowed
66
level: "beginner" # Difficulty level of the test
77
category: "Programming Languages" # Category of the test
88
tags: ["golang", "basics", "syntax", "fundamentals"] # Tags for the test, useful for search and categorization
99
type: "test" # Type of the content, in this case, a test ( required for the test to be recognized by the system )
1010

1111
questions:
12+
# IDs can stay short slugs in front matter; academy-theme derives stable UUIDs in emitted quiz JSON.
1213
# Multiple Choice Question (Single Answer)
1314
- id: "q1"
1415
text: "What keyword is used to define a function in Go?"
15-
type: "multiple_answers"
16+
type: "single-answer"
1617
marks: 2
1718
explanation: "The 'func' keyword is used to declare functions in Go, similar to how 'function' is used in JavaScript."
1819
options:
1920
- id: "a"
2021
text: "function"
21-
is_correct: false
22+
isCorrect: false
2223
- id: "b"
2324
text: "def"
24-
is_correct: false
25+
isCorrect: false
2526
- id: "c"
2627
text: "func"
27-
is_correct: true
28+
isCorrect: true
2829
- id: "d"
2930
text: "fn"
30-
is_correct: false
31+
isCorrect: false
3132

3233
# Short Answer Question
3334
- id: "q2"
3435
text: "Go is a statically typed language. (true/false)"
35-
type: "short_answer"
36+
type: "short-answer"
3637
marks: 2
37-
correct_answer: "true"
38+
correctAnswer: "true"
3839
case_sensitive: false
3940
explanation: "Go is indeed a statically typed language, meaning variable types are determined at compile time."
4041

4142
# Short Answer Question (Numeric)
4243
- id: "q3"
4344
text: "What is the zero value of an uninitialized int in Go?"
44-
type: "short_answer"
45+
type: "short-answer"
4546
marks: 2
46-
correct_answer: "0"
47+
correctAnswer: "0"
4748
explanation: "In Go, the zero value for numeric types like int is 0."
4849

4950
# Multiple Choice Question (Multiple Answers)
@@ -55,14 +56,14 @@ questions:
5556
options:
5657
- id: "a"
5758
text: "To delay the execution of a function until the surrounding function returns"
58-
is_correct: true
59+
isCorrect: true
5960
- id: "b"
6061
text: "To define a constant value"
61-
is_correct: false
62+
isCorrect: false
6263
- id: "c"
6364
text: "To close resources like files or network connections"
64-
is_correct: true
65+
isCorrect: true
6566
- id: "d"
6667
text: "To handle errors in a function"
67-
is_correct: false
68+
isCorrect: false
6869
---
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{{- $scope := trim (printf "%v" .scope) " \n\r\t" -}}
2+
{{- $name := trim (printf "%v" .name) " \n\r\t" -}}
3+
{{- $digest := lower (sha1 (printf "academy-theme:%s:%s" $scope $name)) -}}
4+
{{- $variantSource := substr $digest 16 1 -}}
5+
{{- $variant := "b" -}}
6+
{{- if in "0123" $variantSource -}}
7+
{{- $variant = "8" -}}
8+
{{- else if in "4567" $variantSource -}}
9+
{{- $variant = "9" -}}
10+
{{- else if in "89ab" $variantSource -}}
11+
{{- $variant = "a" -}}
12+
{{- end -}}
13+
{{- printf "%s-%s-5%s-%s%s-%s" (substr $digest 0 8) (substr $digest 8 4) (substr $digest 13 3) $variant (substr $digest 17 3) (substr $digest 20 12) -}}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{{- $value := trim (printf "%v" .value) " \n\r\t" -}}
2+
{{- $scope := trim (printf "%v" .scope) " \n\r\t" -}}
3+
{{- if eq $value "" -}}
4+
{{- $value = $scope -}}
5+
{{- end -}}
6+
{{- if gt (len (findRE `(?i)^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$` $value)) 0 -}}
7+
{{- lower $value -}}
8+
{{- else -}}
9+
{{- partial "academy/derive-uuid.html" (dict "scope" $scope "name" $value) -}}
10+
{{- end -}}

layouts/partials/id.html

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
{{- $id := .Params.id -}}
2-
{{- if $id -}}
3-
{{- $id -}}
4-
{{- else -}}
5-
{{- .RelPermalink | md5 -}}
6-
{{- end -}}
1+
{{- $id := .Params.id | default .RelPermalink -}}
2+
{{- $scope := or .File.Path .RelPermalink -}}
3+
{{- partial "academy/normalize-id.html" (dict "value" $id "scope" $scope) -}}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{{- $option := .option -}}
2+
{{- $index := .index -}}
3+
{{- $questionScope := printf "%v" .questionScope -}}
4+
{{- $optionKey := trim (printf "%v" (or $option.id (printf "option-%d" (add $index 1)))) " \n\r\t" -}}
5+
6+
{{- return (dict
7+
"id" (partial "academy/normalize-id.html" (dict "value" $optionKey "scope" (printf "%s#option:%s" $questionScope $optionKey)))
8+
"text" (printf "%v" (or $option.text ""))
9+
"isCorrect" (or $option.isCorrect $option.is_correct false)
10+
) -}}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{{- $question := .question -}}
2+
{{- $index := .index -}}
3+
{{- $quizFilePath := printf "%v" .filePath -}}
4+
{{- $questionKey := trim (printf "%v" (or $question.id (printf "question-%d" (add $index 1)))) " \n\r\t" -}}
5+
{{- $questionScope := printf "%s#question:%s" $quizFilePath $questionKey -}}
6+
{{- $questionType := lower (replace (printf "%v" (or $question.type "single-answer")) "_" "-") -}}
7+
{{- $options := slice -}}
8+
{{- $correctOptionIds := slice -}}
9+
{{- $optionIdMap := dict -}}
10+
11+
{{- range $optionIndex, $option := ($question.options | default (slice)) -}}
12+
{{- $optionKey := trim (printf "%v" (or $option.id (printf "option-%d" (add $optionIndex 1)))) " \n\r\t" -}}
13+
{{- $canonicalOption := partial "test/normalize-question-option.html" (dict
14+
"index" $optionIndex
15+
"option" $option
16+
"questionScope" $questionScope
17+
) -}}
18+
{{- $options = $options | append $canonicalOption -}}
19+
{{- $optionIdMap = merge $optionIdMap (dict $optionKey $canonicalOption.id) -}}
20+
{{- if $canonicalOption.isCorrect -}}
21+
{{- $correctOptionIds = $correctOptionIds | append $canonicalOption.id -}}
22+
{{- end -}}
23+
{{- end -}}
24+
25+
{{- $correctAnswer := trim (printf "%v" (or $question.correctAnswer $question.correct_answer "")) " \n\r\t" -}}
26+
{{- if eq $correctAnswer "" -}}
27+
{{- if eq $questionType "multiple-answers" -}}
28+
{{- $correctAnswer = delimit $correctOptionIds "," -}}
29+
{{- else if gt (len $correctOptionIds) 0 -}}
30+
{{- $correctAnswer = index $correctOptionIds 0 -}}
31+
{{- end -}}
32+
{{- else if or (eq $questionType "multiple-answers") (eq $questionType "single-answer") -}}
33+
{{- $normalizedAnswers := slice -}}
34+
{{- range (split $correctAnswer ",") -}}
35+
{{- $answer := trim . " \n\r\t" -}}
36+
{{- if ne $answer "" -}}
37+
{{- if isset $optionIdMap $answer -}}
38+
{{- $normalizedAnswers = $normalizedAnswers | append (index $optionIdMap $answer) -}}
39+
{{- else -}}
40+
{{- $normalizedAnswers = $normalizedAnswers | append $answer -}}
41+
{{- end -}}
42+
{{- end -}}
43+
{{- end -}}
44+
{{- if eq $questionType "multiple-answers" -}}
45+
{{- $correctAnswer = delimit $normalizedAnswers "," -}}
46+
{{- else if gt (len $normalizedAnswers) 0 -}}
47+
{{- $correctAnswer = index $normalizedAnswers 0 -}}
48+
{{- else -}}
49+
{{- $correctAnswer = "" -}}
50+
{{- end -}}
51+
{{- end -}}
52+
53+
{{- $canonicalQuestion := dict
54+
"id" (partial "academy/normalize-id.html" (dict "value" $questionKey "scope" $questionScope))
55+
"text" (printf "%v" (or $question.text ""))
56+
"type" $questionType
57+
"marks" (int (or $question.marks 0))
58+
"explanation" (printf "%v" (or $question.explanation ""))
59+
"caseSensitive" (or $question.caseSensitive $question.case_sensitive false)
60+
"options" $options
61+
"correctAnswer" $correctAnswer
62+
-}}
63+
64+
{{- if or (eq $questionType "multiple-answers") (eq $questionType "single-answer") $question.multipleAnswers $question.multiple_answers -}}
65+
{{- $canonicalQuestion = merge $canonicalQuestion (dict
66+
"multipleAnswers" (or $question.multipleAnswers $question.multiple_answers (eq $questionType "multiple-answers"))
67+
) -}}
68+
{{- end -}}
69+
70+
{{- return $canonicalQuestion -}}

0 commit comments

Comments
 (0)