in2lambda 0.3.2__tar.gz → 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {in2lambda-0.3.2 → in2lambda-1.0.0}/PKG-INFO +5 -3
  2. {in2lambda-0.3.2 → in2lambda-1.0.0}/in2lambda/api/question.py +1 -1
  3. in2lambda-1.0.0/in2lambda/api/set.py +185 -0
  4. in2lambda-1.0.0/in2lambda/api/visibility_status.py +76 -0
  5. in2lambda-1.0.0/in2lambda/figures/O2_potential.png +0 -0
  6. in2lambda-1.0.0/in2lambda/figures/activated_complex.png +0 -0
  7. in2lambda-1.0.0/in2lambda/figures/rocket-momentum.png +0 -0
  8. {in2lambda-0.3.2 → in2lambda-1.0.0}/in2lambda/filters/PartPartSolSol/filter.py +7 -7
  9. {in2lambda-0.3.2 → in2lambda-1.0.0}/in2lambda/filters/PartSolPartSol/filter.py +8 -10
  10. {in2lambda-0.3.2 → in2lambda-1.0.0}/in2lambda/filters/PartsOneSol/filter.py +6 -6
  11. {in2lambda-0.3.2 → in2lambda-1.0.0}/in2lambda/filters/PartsSepSol/filter.py +8 -8
  12. {in2lambda-0.3.2 → in2lambda-1.0.0}/in2lambda/filters/markdown.py +10 -10
  13. in2lambda-1.0.0/in2lambda/json_convert/__init__.py +1 -0
  14. in2lambda-1.0.0/in2lambda/json_convert/json_convert.py +144 -0
  15. in2lambda-1.0.0/in2lambda/json_convert/minimal_template_question.json +23 -0
  16. in2lambda-1.0.0/in2lambda/json_convert/minimal_template_set.json +9 -0
  17. {in2lambda-0.3.2 → in2lambda-1.0.0}/in2lambda/main.py +50 -18
  18. {in2lambda-0.3.2 → in2lambda-1.0.0}/pyproject.toml +5 -1
  19. in2lambda-0.3.2/in2lambda/api/module.py +0 -122
  20. in2lambda-0.3.2/in2lambda/json_convert/__init__.py +0 -1
  21. in2lambda-0.3.2/in2lambda/json_convert/json_convert.py +0 -98
  22. in2lambda-0.3.2/in2lambda/json_convert/minimal_template.json +0 -26
  23. {in2lambda-0.3.2 → in2lambda-1.0.0}/LICENSE +0 -0
  24. {in2lambda-0.3.2 → in2lambda-1.0.0}/README.md +0 -0
  25. {in2lambda-0.3.2 → in2lambda-1.0.0}/in2lambda/__init__.py +0 -0
  26. {in2lambda-0.3.2 → in2lambda-1.0.0}/in2lambda/api/__init__.py +0 -0
  27. {in2lambda-0.3.2 → in2lambda-1.0.0}/in2lambda/api/part.py +0 -0
  28. {in2lambda-0.3.2 → in2lambda-1.0.0}/in2lambda/example_questions.tex +0 -0
  29. {in2lambda-0.3.2 → in2lambda-1.0.0}/in2lambda/example_solutions.tex +0 -0
  30. {in2lambda-0.3.2 → in2lambda-1.0.0}/in2lambda/filters/PartPartSolSol/__init__.py +0 -0
  31. {in2lambda-0.3.2 → in2lambda-1.0.0}/in2lambda/filters/PartPartSolSol/example.tex +0 -0
  32. {in2lambda-0.3.2 → in2lambda-1.0.0}/in2lambda/filters/PartSolPartSol/__init__.py +0 -0
  33. {in2lambda-0.3.2 → in2lambda-1.0.0}/in2lambda/filters/PartSolPartSol/example.tex +0 -0
  34. {in2lambda-0.3.2 → in2lambda-1.0.0}/in2lambda/filters/PartsOneSol/__init__.py +0 -0
  35. {in2lambda-0.3.2 → in2lambda-1.0.0}/in2lambda/filters/PartsOneSol/example.tex +0 -0
  36. {in2lambda-0.3.2 → in2lambda-1.0.0}/in2lambda/filters/PartsSepSol/__init__.py +0 -0
  37. {in2lambda-0.3.2 → in2lambda-1.0.0}/in2lambda/filters/PartsSepSol/example.tex +0 -0
  38. {in2lambda-0.3.2 → in2lambda-1.0.0}/in2lambda/filters/__init__.py +0 -0
  39. {in2lambda-0.3.2 → in2lambda-1.0.0}/in2lambda/katex_convert/__init__.py +0 -0
  40. {in2lambda-0.3.2 → in2lambda-1.0.0}/in2lambda/katex_convert/delete_list.txt +0 -0
  41. {in2lambda-0.3.2 → in2lambda-1.0.0}/in2lambda/katex_convert/katex_convert.py +0 -0
  42. {in2lambda-0.3.2 → in2lambda-1.0.0}/in2lambda/katex_convert/replace_list.txt +0 -0
  43. {in2lambda-0.3.2 → in2lambda-1.0.0}/in2lambda/py.typed +0 -0
@@ -1,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: in2lambda
3
- Version: 0.3.2
3
+ Version: 1.0.0
4
4
  Summary: Converts content ready for import into Lambda Feedback
5
- Home-page: https://lambda-feedback.github.io/in2lambda
6
5
  License: MIT
7
6
  Requires-Python: >=3.10,<4.0
8
7
  Classifier: Development Status :: 3 - Alpha
@@ -13,13 +12,16 @@ Classifier: Programming Language :: Python :: 3
13
12
  Classifier: Programming Language :: Python :: 3.10
14
13
  Classifier: Programming Language :: Python :: 3.11
15
14
  Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
16
  Classifier: Programming Language :: Python :: 3 :: Only
17
17
  Classifier: Typing :: Typed
18
18
  Requires-Dist: beartype (>=0.17.2,<0.18.0)
19
19
  Requires-Dist: panflute (>=2.3.1,<3.0.0)
20
+ Requires-Dist: requests (<2.32.0)
20
21
  Requires-Dist: rich-click (>=1.7.4,<2.0.0)
21
22
  Project-URL: Bug Tracker, https://github.com/lambda-feedback/in2lambda/issues
22
23
  Project-URL: Documentation, https://lambda-feedback.github.io/in2lambda
24
+ Project-URL: Homepage, https://lambda-feedback.github.io/in2lambda
23
25
  Project-URL: Repository, https://github.com/lambda-feedback/in2lambda
24
26
  Description-Content-Type: text/markdown
25
27
 
@@ -1,4 +1,4 @@
1
- """A full question with optional parts that's contained in a module."""
1
+ """A full question with optional parts that's contained in a set."""
2
2
 
3
3
  from dataclasses import dataclass, field
4
4
  from typing import Union
@@ -0,0 +1,185 @@
1
+ """Represents a list of questions."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Union
5
+
6
+ import panflute as pf
7
+
8
+ from in2lambda.api.question import Question
9
+ from in2lambda.api.visibility_status import VisibilityController, VisibilityStatus
10
+
11
+
12
+ @dataclass
13
+ class Set:
14
+ """Represents a list of questions."""
15
+
16
+ _name: str = field(default="set")
17
+ _description: str = field(default="")
18
+ _finalAnswerVisibility: VisibilityController = field(
19
+ default_factory=lambda: VisibilityController(
20
+ VisibilityStatus.OPEN_WITH_WARNINGS
21
+ )
22
+ )
23
+ _workedSolutionVisibility: VisibilityController = field(
24
+ default_factory=lambda: VisibilityController(
25
+ VisibilityStatus.OPEN_WITH_WARNINGS
26
+ )
27
+ )
28
+ _structuredTutorialVisibility: VisibilityController = field(
29
+ default_factory=lambda: VisibilityController(VisibilityStatus.OPEN)
30
+ )
31
+
32
+ questions: list[Question] = field(default_factory=list)
33
+ _current_question_index = -1
34
+
35
+ @property
36
+ def current_question(self) -> Question:
37
+ """The current question being modified, or Question("INVALID") if there are no questions.
38
+
39
+ The reasoning behind returning Question("INVALID") is in case filter logic is being applied
40
+ on text before the first question (e.g. intro paragraphs). In that case, there is no effect.
41
+
42
+ Returns:
43
+ The current question or Question("INVALID") if there are no questions.
44
+
45
+ Examples:
46
+ >>> from in2lambda.api.set import Set
47
+ >>> Set().current_question
48
+ Question(title='INVALID', parts=[], images=[], main_text='')
49
+ >>> s = Set()
50
+ >>> s.add_question()
51
+ >>> s.current_question
52
+ Question(title='', parts=[], images=[], main_text='')
53
+ """
54
+ return (
55
+ self.questions[self._current_question_index]
56
+ if self.questions
57
+ else Question("INVALID")
58
+ )
59
+
60
+ def add_question(
61
+ self, title: str = "", main_text: Union[pf.Element, str] = pf.Str("")
62
+ ) -> None:
63
+ """Inserts a new question into the set.
64
+
65
+ Args:
66
+ title: An optional string for the title of the question. If no title
67
+ is provided, the question title auto-increments i.e. Question 1, 2, etc.
68
+ main_text: An optional string or panflute element for the main question text.
69
+
70
+ Examples:
71
+ >>> from in2lambda.api.set import Set
72
+ >>> import panflute as pf
73
+ >>> s = Set()
74
+ >>> s.add_question("Some title", pf.Para(pf.Str("hello"), pf.Space, pf.Str("there")))
75
+ >>> s.questions
76
+ [Question(title='Some title', parts=[], images=[], main_text='hello there')]
77
+ >>> s.add_question(main_text="Normal string text")
78
+ >>> s.questions[1].main_text
79
+ 'Normal string text'
80
+ """
81
+ question = Question(title=title)
82
+ question.main_text = main_text
83
+ self.questions.append(question)
84
+
85
+ def increment_current_question(self) -> None:
86
+ """Manually overrides the current question being modified.
87
+
88
+ The default (-1) indicates the last question added. Incrementing for the
89
+ first time sets to 0 i.e. the first question.
90
+
91
+ The is useful if adding question text first and answers later.
92
+
93
+ Examples:
94
+ >>> from in2lambda.api.set import Set
95
+ >>> s = Set()
96
+ >>> # Imagine adding the questions from a question file first...
97
+ >>> s.add_question("Question 1")
98
+ >>> s.add_question("Question 2")
99
+ >>> # ...and then adding solutions from an answer file later
100
+ >>> s.increment_current_question() # Loop back to question 1
101
+ >>> s.current_question.add_solution("Question 1 answer")
102
+ >>> s.increment_current_question()
103
+ >>> s.current_question.add_solution("Question 2 answer")
104
+ >>> s.questions
105
+ [Question(title='Question 1', parts=[Part(text='', worked_solution='Question 1 answer')], images=[], main_text=''),\
106
+ Question(title='Question 2', parts=[Part(text='', worked_solution='Question 2 answer')], images=[], main_text='')]
107
+ """
108
+ self._current_question_index += 1
109
+
110
+ def to_json(self, output_dir: str) -> None:
111
+ """Turns this set into Lambda Feedback JSON/ZIP files.
112
+
113
+ WARNING: This will overwrite any existing files in the directory.
114
+
115
+ Args:
116
+ output_dir: Where to output the final Lambda Feedback JSON/ZIP files.
117
+
118
+ Examples:
119
+ >>> import tempfile
120
+ >>> import os
121
+ >>> import json
122
+ >>> # Create a set with two questions
123
+ >>> s = Set()
124
+ >>> s.add_question("Question 1")
125
+ >>> s.add_question("Question 2")
126
+ >>> with tempfile.TemporaryDirectory() as temp_dir:
127
+ ... # Write the JSON files to the temporary directory
128
+ ... s.to_json(temp_dir)
129
+ ... # Check the contents of the directory
130
+ ... sorted(os.listdir(temp_dir))
131
+ ... # Check the contents of the set directory
132
+ ... sorted(os.listdir(f"{temp_dir}/set"))
133
+ ... # Check the title of the first question
134
+ ... with open(f"{temp_dir}/set/question_000_Question_1.json") as file:
135
+ ... print(f"Question 1's title: {json.load(file)['title']}")
136
+ ['set', 'set.zip']
137
+ ['question_000_Question_1.json', 'question_001_Question_2.json', 'set_set.json']
138
+ Question 1's title: Question 1
139
+ """
140
+ from in2lambda.json_convert import json_convert
141
+
142
+ json_convert.main(self, output_dir)
143
+
144
+ def set_name(self, name: str) -> None:
145
+ """Sets the name of the set.
146
+
147
+ Args:
148
+ name: The name to set for the set.
149
+
150
+ Examples:
151
+ >>> from in2lambda.api.set import Set
152
+ >>> s = Set()
153
+ >>> s.set_name("My Question Set")
154
+ >>> s._name
155
+ 'My Question Set'
156
+ """
157
+ self._name = name
158
+
159
+ def set_description(self, description: str) -> None:
160
+ """Sets the description of the set.
161
+
162
+ Args:
163
+ description: The description to set for the set.
164
+
165
+ Examples:
166
+ >>> from in2lambda.api.set import Set
167
+ >>> s = Set()
168
+ >>> s.set_description("This is my question set.")
169
+ >>> s._description
170
+ 'This is my question set.'
171
+ """
172
+ self._description = description
173
+
174
+ def __repr__(self):
175
+ """Custom representation showing visibility status values instead of memory addresses."""
176
+ return (
177
+ f"Set("
178
+ f"_name={self._name!r}, "
179
+ f"_description={self._description!r}, "
180
+ f"_finalAnswerVisibility={str(self._finalAnswerVisibility)!r}, "
181
+ f"_workedSolutionVisibility={str(self._workedSolutionVisibility)!r}, "
182
+ f"_structuredTutorialVisibility={str(self._structuredTutorialVisibility)!r}, "
183
+ f"questions={self.questions!r}"
184
+ f")"
185
+ )
@@ -0,0 +1,76 @@
1
+ """Module for managing visibility status of questions and sets."""
2
+
3
+ from enum import Enum
4
+
5
+ class VisibilityStatus(Enum):
6
+ """Enum representing the visibility status of a question or set."""
7
+
8
+ OPEN = "OPEN"
9
+ HIDE = "HIDE"
10
+ OPEN_WITH_WARNINGS = "OPEN_WITH_WARNINGS"
11
+
12
+ def __str__(self):
13
+ """Return the string representation of the visibility status."""
14
+ return self.value
15
+
16
+ def __repr__(self) -> str:
17
+ """Return a string representation for debugging."""
18
+ return str(self)
19
+
20
+
21
+ class VisibilityController:
22
+ """Controller for managing visibility status with easy-to-use methods."""
23
+
24
+ def __init__(self, initial_status: VisibilityStatus = VisibilityStatus.OPEN):
25
+ """Initialize the VisibilityController with a specific status."""
26
+ self._status = initial_status
27
+
28
+ @property
29
+ def status(self) -> VisibilityStatus:
30
+ """Return the current visibility status."""
31
+ return self._status
32
+
33
+ def to_open(self):
34
+ """Change status to OPEN.
35
+
36
+ Example:
37
+ >>> vc = VisibilityController()
38
+ >>> vc.to_open()
39
+ >>> vc.status
40
+ OPEN
41
+ """
42
+ self._status = VisibilityStatus.OPEN
43
+
44
+ def to_hide(self):
45
+ """Change status to HIDE.
46
+
47
+ Example:
48
+ >>> vc = VisibilityController()
49
+ >>> vc.to_hide()
50
+ >>> vc.status
51
+ HIDE
52
+ """
53
+ self._status = VisibilityStatus.HIDE
54
+
55
+ def to_open_with_warnings(self):
56
+ """Change status to OPEN_WITH_WARNINGS.
57
+
58
+ Example:
59
+ >>> vc = VisibilityController()
60
+ >>> vc.to_open_with_warnings()
61
+ >>> vc.status
62
+ OPEN_WITH_WARNINGS
63
+ """
64
+ self._status = VisibilityStatus.OPEN_WITH_WARNINGS
65
+
66
+ def __str__(self):
67
+ """Return the string representation of the visibility status."""
68
+ return str(self._status)
69
+
70
+ def __repr__(self) -> str:
71
+ """Return a string representation for debugging."""
72
+ return str(self)
73
+
74
+ def to_dict(self):
75
+ """Convert VisibilityController to dictionary for JSON serialization."""
76
+ return {"status": str(self._status)}
@@ -6,7 +6,7 @@ from typing import Optional
6
6
 
7
7
  import panflute as pf
8
8
 
9
- from in2lambda.api.module import Module
9
+ from in2lambda.api.set import Set
10
10
  from in2lambda.filters.markdown import filter
11
11
 
12
12
 
@@ -14,7 +14,7 @@ from in2lambda.filters.markdown import filter
14
14
  def pandoc_filter(
15
15
  elem: pf.Element,
16
16
  doc: pf.elements.Doc,
17
- module: Module,
17
+ set: Set,
18
18
  parsing_answers: bool,
19
19
  ) -> Optional[pf.Str]:
20
20
  """A Pandoc filter that parses and translates various TeX elements.
@@ -23,7 +23,7 @@ def pandoc_filter(
23
23
  elem: The current TeX element being processed. This could be a paragraph,
24
24
  ordered list, etc.
25
25
  doc: A Pandoc document container - essentially the Pandoc AST.
26
- module: The Python API that is used to store the result after processing
26
+ set: The Python API that is used to store the result after processing
27
27
  the TeX file.
28
28
  parsing_answers: Whether an answers-only document is currently being parsed.
29
29
 
@@ -46,10 +46,10 @@ def pandoc_filter(
46
46
 
47
47
  # Solutions are in a Div
48
48
  if isinstance(elem, pf.Div):
49
- module.add_question(main_text="\n".join(pandoc_filter.question))
49
+ set.add_question(main_text="\n".join(pandoc_filter.question))
50
50
  if hasattr(pandoc_filter, "parts"):
51
51
  for part in pandoc_filter.parts:
52
- module.current_question.add_part_text(part)
52
+ set.current_question.add_part_text(part)
53
53
  pandoc_filter.question = []
54
54
  pandoc_filter.parts = []
55
55
 
@@ -58,8 +58,8 @@ def pandoc_filter(
58
58
  match type(first_answer_part := elem.content[0]):
59
59
  case pf.OrderedList:
60
60
  for item in first_answer_part.content:
61
- module.current_question.add_solution(pf.stringify(item))
61
+ set.current_question.add_solution(pf.stringify(item))
62
62
  case pf.Para:
63
- module.current_question.add_solution(pf.stringify(elem))
63
+ set.current_question.add_solution(pf.stringify(elem))
64
64
 
65
65
  return None
@@ -7,7 +7,7 @@ from typing import Optional
7
7
 
8
8
  import panflute as pf
9
9
 
10
- from in2lambda.api.module import Module
10
+ from in2lambda.api.set import Set
11
11
  from in2lambda.filters.markdown import filter
12
12
 
13
13
 
@@ -15,7 +15,7 @@ from in2lambda.filters.markdown import filter
15
15
  def pandoc_filter(
16
16
  elem: pf.Element,
17
17
  doc: pf.elements.Doc,
18
- module: Module,
18
+ set: Set,
19
19
  parsing_answers: bool,
20
20
  ) -> Optional[pf.Str]:
21
21
  """A Pandoc filter that parses and translates various TeX elements.
@@ -24,7 +24,7 @@ def pandoc_filter(
24
24
  elem: The current TeX element being processed. This could be a paragraph,
25
25
  ordered list, etc.
26
26
  doc: A Pandoc document container - essentially the Pandoc AST.
27
- module: The Python API that is used to store the result after processing
27
+ set: The Python API that is used to store the result after processing
28
28
  the TeX file.
29
29
  parsing_answers: Whether an answers-only document is currently being parsed.
30
30
 
@@ -48,16 +48,14 @@ def pandoc_filter(
48
48
  for item in listItem.content
49
49
  if not isinstance(item, pf.Div)
50
50
  ]
51
- module.current_question.add_part_text("\n".join(part))
52
- module.current_question.add_solution(
53
- pandoc_filter.solutions.popleft()
54
- )
51
+ set.current_question.add_part_text("\n".join(part))
52
+ set.current_question.add_solution(pandoc_filter.solutions.popleft())
55
53
 
56
54
  if isinstance(elem, pf.Div):
57
55
  pandoc_filter.solutions.append(pf.stringify(elem))
58
56
  if pandoc_filter.question:
59
- module.add_question(main_text="\n".join(pandoc_filter.question))
60
- module.current_question.add_solution(pf.stringify(elem))
61
- module.current_question._last_part["solution"] -= 1
57
+ set.add_question(main_text="\n".join(pandoc_filter.question))
58
+ set.current_question.add_solution(pf.stringify(elem))
59
+ set.current_question._last_part["solution"] -= 1
62
60
  pandoc_filter.question = []
63
61
  return None
@@ -9,7 +9,7 @@ from typing import Optional
9
9
 
10
10
  import panflute as pf
11
11
 
12
- from in2lambda.api.module import Module
12
+ from in2lambda.api.set import Set
13
13
  from in2lambda.filters.markdown import filter
14
14
 
15
15
 
@@ -17,7 +17,7 @@ from in2lambda.filters.markdown import filter
17
17
  def pandoc_filter(
18
18
  elem: pf.Element,
19
19
  doc: pf.elements.Doc,
20
- module: Module,
20
+ set: Set,
21
21
  parsing_answers: bool,
22
22
  ) -> Optional[pf.Str]:
23
23
  """A Pandoc filter that parses and translates various TeX elements.
@@ -26,7 +26,7 @@ def pandoc_filter(
26
26
  elem: The current TeX element being processed. This could be a paragraph,
27
27
  ordered list, etc.
28
28
  doc: A Pandoc document container - essentially the Pandoc AST.
29
- module: The Python API that is used to store the result after processing
29
+ set: The Python API that is used to store the result after processing
30
30
  the TeX file.
31
31
  parsing_answers: Whether an answers-only document is currently being parsed.
32
32
 
@@ -42,17 +42,17 @@ def pandoc_filter(
42
42
  isinstance(elem.prev, pf.Header)
43
43
  and pf.stringify(elem.prev) != "Solution"
44
44
  ):
45
- module.add_question(main_text=elem)
45
+ set.add_question(main_text=elem)
46
46
 
47
47
  # Parts are denoted via ordered lists
48
48
  case pf.OrderedList:
49
49
  for item in elem.content:
50
- module.current_question.add_part_text(item)
50
+ set.current_question.add_part_text(item)
51
51
 
52
52
  # Solution is in a Div with nested content being "Solution"
53
53
  case pf.Div:
54
54
  if pf.stringify(elem.content[0].content) == "Solution":
55
- module.current_question.add_solution(
55
+ set.current_question.add_solution(
56
56
  pf.stringify(elem) # [len("Solution") :] - For Jon Rackham
57
57
  )
58
58
 
@@ -9,7 +9,7 @@ from typing import Optional
9
9
 
10
10
  import panflute as pf
11
11
 
12
- from in2lambda.api.module import Module
12
+ from in2lambda.api.set import Set
13
13
  from in2lambda.filters.markdown import filter
14
14
 
15
15
 
@@ -17,7 +17,7 @@ from in2lambda.filters.markdown import filter
17
17
  def pandoc_filter(
18
18
  elem: pf.Element,
19
19
  doc: pf.elements.Doc,
20
- module: Module,
20
+ set: Set,
21
21
  parsing_answers: bool,
22
22
  ) -> Optional[pf.Str]:
23
23
  """A Pandoc filter that parses and translates various TeX elements.
@@ -26,7 +26,7 @@ def pandoc_filter(
26
26
  elem: The current TeX element being processed. This could be a paragraph,
27
27
  ordered list, etc.
28
28
  doc: A Pandoc document container - essentially the Pandoc AST.
29
- module: The Python API that is used to store the result after processing
29
+ set: The Python API that is used to store the result after processing
30
30
  the TeX file.
31
31
  parsing_answers: Whether an answers-only document is currently being parsed.
32
32
 
@@ -39,7 +39,7 @@ def pandoc_filter(
39
39
  for numbered_part in elem.content:
40
40
  if parsing_answers:
41
41
  # Denotes that we've reached the answer for a new question
42
- module.increment_current_question()
42
+ set.increment_current_question()
43
43
  # For each numbered question, extract blurb and parts
44
44
  blurb: list[str] = []
45
45
  lettered_parts: list[str] = []
@@ -64,17 +64,17 @@ def pandoc_filter(
64
64
 
65
65
  # Answers for questions with no parts
66
66
  if parsing_answers and not lettered_parts:
67
- module.current_question.add_solution(spaced_blurb)
67
+ set.current_question.add_solution(spaced_blurb)
68
68
  # Add the main question text as the Lambda Feedback blurb
69
69
  elif not parsing_answers:
70
- module.add_question(main_text=spaced_blurb)
70
+ set.add_question(main_text=spaced_blurb)
71
71
 
72
72
  # Add each part solution/text
73
73
  # For the solution, prepend any top level answer text to each part answer
74
74
  for part in lettered_parts:
75
75
  (
76
- module.current_question.add_solution(spaced_blurb + part)
76
+ set.current_question.add_solution(spaced_blurb + part)
77
77
  if parsing_answers
78
- else module.current_question.add_part_text(part)
78
+ else set.current_question.add_part_text(part)
79
79
  )
80
80
  return None
@@ -9,7 +9,7 @@ import panflute as pf
9
9
  from beartype.typing import Callable, Optional
10
10
  from rich_click import echo
11
11
 
12
- from in2lambda.api.module import Module
12
+ from in2lambda.api.set import Set
13
13
  from in2lambda.katex_convert.katex_convert import latex_to_katex
14
14
 
15
15
 
@@ -32,12 +32,12 @@ def image_directories(tex_file: str) -> list[str]:
32
32
  >>> temp_dir = tempfile.mkdtemp()
33
33
  >>> tex_file = os.path.join(temp_dir, 'test.tex')
34
34
  >>> with open(tex_file, 'w') as f:
35
- ... f.write("\graphicspath{{subdir1/}{subdir2/}{subdir3/}}")
35
+ ... f.write("\\graphicspath{{subdir1/}{subdir2/}{subdir3/}}")
36
36
  45
37
37
  >>> image_directories(tex_file)
38
38
  ['subdir1/', 'subdir2/', 'subdir3/']
39
39
  >>> with open(tex_file, 'w') as f:
40
- ... f.write("\graphicspath{ { subdir1/ }, { subdir2/ }, { subdir3/ } }")
40
+ ... f.write("\\graphicspath{ { subdir1/ }, { subdir2/ }, { subdir3/ } }")
41
41
  57
42
42
  >>> image_directories.cache_clear()
43
43
  >>> image_directories(tex_file)
@@ -71,14 +71,14 @@ def image_path(image_name: str, tex_file: str) -> Optional[str]:
71
71
  The absolute path to the image if it can be found. If not, it returns None.
72
72
 
73
73
  Examples:
74
- >>> from in2lambda.filters.markdown import image_path
74
+ set.current_question.images.append(path)
75
75
  >>> import tempfile
76
76
  >>> import os
77
77
  >>> # Example TeX file with a subdirectory
78
78
  >>> temp_dir = tempfile.mkdtemp()
79
79
  >>> tex_file = os.path.join(temp_dir, 'test.tex')
80
80
  >>> with open(tex_file, 'w') as f:
81
- ... f.write("\graphicspath{{./subdir1/}{./subdir2/}{./subdir3/}}")
81
+ ... f.write("\\graphicspath{{./subdir1/}{./subdir2/}{./subdir3/}}")
82
82
  51
83
83
  >>> # Example image in a relative subdirectory
84
84
  >>> sub_dir = os.path.join(temp_dir, 'subdir3')
@@ -117,11 +117,11 @@ def image_path(image_name: str, tex_file: str) -> Optional[str]:
117
117
 
118
118
  def filter(
119
119
  func: Callable[
120
- [pf.Element, pf.elements.Doc, Module, bool],
120
+ [pf.Element, pf.elements.Doc, Set, bool],
121
121
  Optional[pf.Str],
122
122
  ]
123
123
  ) -> Callable[
124
- [pf.Element, pf.elements.Doc, Module, str, bool],
124
+ [pf.Element, pf.elements.Doc, Set, str, bool],
125
125
  Optional[pf.Str],
126
126
  ]:
127
127
  """Python decorator to make generic LaTeX elements markdown readable.
@@ -136,7 +136,7 @@ def filter(
136
136
  def markdown_converter(
137
137
  elem: pf.Element,
138
138
  doc: pf.elements.Doc,
139
- module: Module,
139
+ set: Set,
140
140
  tex_file: str,
141
141
  parsing_answers: bool,
142
142
  ) -> Optional[pf.Str]:
@@ -175,7 +175,7 @@ def filter(
175
175
  if path is None:
176
176
  echo(f"Warning: Couldn't find {elem.url}")
177
177
  else:
178
- module.current_question.images.append(path)
178
+ set.current_question.images.append(path)
179
179
  return pf.Str(f"![pictureTag]({elem.url})")
180
180
 
181
181
  case pf.Strong:
@@ -189,6 +189,6 @@ def filter(
189
189
  case pf.Str:
190
190
  return pf.Str(elem.text.replace("\u00a0", "\u202f"))
191
191
 
192
- return func(elem, doc, module, parsing_answers)
192
+ return func(elem, doc, set, parsing_answers)
193
193
 
194
194
  return markdown_converter
@@ -0,0 +1 @@
1
+ """Files relating to translating the questions of a Python set object into JSON."""
@@ -0,0 +1,144 @@
1
+ """Converts questions from a Python set object into Lambda Feedback JSON."""
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ import shutil
7
+ import zipfile
8
+ from copy import deepcopy
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from in2lambda.api.set import Set
13
+
14
+ MINIMAL_QUESTION_TEMPLATE = "minimal_template_question.json"
15
+ MINIMAL_SET_TEMPLATE = "minimal_template_set.json"
16
+
17
+
18
+ def _zip_sorted_folder(folder_path, zip_path):
19
+ """Zips the contents of a folder, preserving the directory structure.
20
+
21
+ Args:
22
+ folder_path: The path to the folder to zip.
23
+ zip_path: The path where the zip file will be created.
24
+ """
25
+ with zipfile.ZipFile(zip_path, "w") as zf:
26
+ for root, dirs, files in os.walk(folder_path):
27
+ # Sort files for deterministic, alphabetical order
28
+ for file in sorted(files):
29
+ abs_path = os.path.join(root, file)
30
+ rel_path = os.path.relpath(abs_path, folder_path)
31
+ zf.write(abs_path, arcname=rel_path)
32
+
33
+
34
+ def converter(
35
+ question_template: dict[str, Any],
36
+ set_template: dict[str, Any],
37
+ SetQuestions: Set,
38
+ output_dir: str,
39
+ ) -> None:
40
+ """Turns a set of question objects into Lambda Feedback JSON.
41
+
42
+ Args:
43
+ question_template: The loaded JSON from the minimal question template (it needs to be in sync).
44
+ set_template: The loaded JSON from the minimal set template (it needs to be in sync).
45
+ SetQuestions: A Set object containing questions.
46
+ output_dir: The absolute path for where to produced the final JSON/zip files.
47
+ """
48
+ ListQuestions = SetQuestions.questions
49
+ set_name = SetQuestions._name
50
+ set_description = SetQuestions._description
51
+
52
+ # create directory to put the questions
53
+ os.makedirs(output_dir, exist_ok=True)
54
+ output_question = os.path.join(output_dir, set_name)
55
+ os.makedirs(output_question, exist_ok=True)
56
+
57
+ set_template["name"] = set_name
58
+ set_template["description"] = set_description
59
+ set_template["finalAnswerVisibility"] = str(
60
+ SetQuestions._finalAnswerVisibility.status
61
+ )
62
+ set_template["workedSolutionVisibility"] = str(
63
+ SetQuestions._workedSolutionVisibility.status
64
+ )
65
+ set_template["structuredTutorialVisibility"] = str(
66
+ SetQuestions._structuredTutorialVisibility.status
67
+ )
68
+ # create the set file
69
+ with open(f"{output_question}/set_{set_name}.json", "w") as file:
70
+ json.dump(set_template, file)
71
+
72
+ for i in range(len(ListQuestions)):
73
+ output = deepcopy(question_template)
74
+
75
+ output["orderNumber"] = i # order number starts at 0
76
+ # add title to the question file
77
+ if ListQuestions[i].title != "":
78
+ output["title"] = ListQuestions[i].title
79
+ else:
80
+ output["title"] = "Question " + str(i + 1)
81
+
82
+ # add main text to the question file
83
+ output["masterContent"] = ListQuestions[i].main_text
84
+
85
+ # add parts to the question file
86
+ if ListQuestions[i].parts:
87
+ output["parts"][0]["content"] = ListQuestions[i].parts[0].text
88
+ output["parts"][0]["workedSolution"]["content"] = (
89
+ ListQuestions[i].parts[0].worked_solution
90
+ )
91
+ for j in range(1, len(ListQuestions[i].parts)):
92
+ output["parts"].append(deepcopy(question_template["parts"][0]))
93
+ output["parts"][j]["content"] = ListQuestions[i].parts[j].text
94
+ output["parts"][j]["orderNumber"] = j
95
+ output["parts"][j]["workedSolution"]["content"] = (
96
+ ListQuestions[i].parts[j].worked_solution
97
+ )
98
+
99
+ # Output file
100
+ filename = (
101
+ "question_" + str(i).zfill(3) + "_" + re.sub(r'[^\w\-_.]', '_', output['title'].strip())
102
+ )
103
+
104
+ # write questions into directory
105
+ with open(f"{output_question}/{filename}.json", "w") as file:
106
+ json.dump(output, file)
107
+
108
+ # write image into directory
109
+ for k in range(len(ListQuestions[i].images)):
110
+ image_path = os.path.abspath(
111
+ ListQuestions[i].images[k]
112
+ ) # converts computer path into python path
113
+ # If images exist, create a media directory
114
+ output_image = os.path.join(output_question, "media")
115
+ os.makedirs(output_image, exist_ok=True)
116
+ shutil.copy(image_path, output_image) # copies image into the directory
117
+
118
+ # output zip file in destination folder
119
+ _zip_sorted_folder(output_question, output_question + ".zip")
120
+
121
+
122
+ def main(set_questions: Set, output_dir: str) -> None:
123
+ """Preliminary defensive programming before calling the main converter function.
124
+
125
+ This ultimately then produces the Lambda Feedback JSON/ZIP files.
126
+
127
+ Args:
128
+ set_questions: A Set object containing questions.
129
+ output_dir: Where to output the final Lambda Feedback JSON/ZIP files.
130
+ """
131
+ # Use path so minimal template can be found regardless of where the user is running python from.
132
+ with open(Path(__file__).with_name(MINIMAL_QUESTION_TEMPLATE), "r") as file:
133
+ question_template = json.load(file)
134
+
135
+ with open(Path(__file__).with_name(MINIMAL_SET_TEMPLATE), "r") as file:
136
+ set_template = json.load(file)
137
+
138
+ # check if directory exists in file
139
+ if os.path.isdir(output_dir):
140
+ try:
141
+ shutil.rmtree(output_dir)
142
+ except OSError as e:
143
+ print("Error: %s : %s" % (output_dir, e.strerror))
144
+ converter(question_template, set_template, set_questions, output_dir)
@@ -0,0 +1,23 @@
1
+ {
2
+ "orderNumber": 0,
3
+ "title": "Question title here",
4
+ "masterContent": "Top level question here",
5
+ "publish": false,
6
+ "displayFinalAnswer": true,
7
+ "displayStructuredTutorial": true,
8
+ "displayWorkedSolution": true,
9
+ "displayChatbot": false,
10
+ "parts": [
11
+ {
12
+ "orderNumber": 0,
13
+ "content": "Part text here",
14
+ "answerContent": "",
15
+ "responseAreas": [],
16
+ "workedSolution": {
17
+ "title": "",
18
+ "content": "Part worked solution here",
19
+ "children": []
20
+ }
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "a simple example",
3
+ "description": "description here",
4
+ "manuallyHidden": true,
5
+ "finalAnswerVisibility": "OPEN_WITH_WARNINGS",
6
+ "workedSolutionVisibility": "OPEN_WITH_WARNINGS",
7
+ "structuredTutorialVisibility": "OPEN",
8
+ "chatbotVisibility": "HIDE"
9
+ }
@@ -1,14 +1,34 @@
1
1
  """The main input for in2lambda, defining both the CLT and main library function."""
2
2
 
3
+ # This commented block makes it run the local files rather than the pip library (I think, I don't understand it. Kevin wrote it.)
4
+ #
5
+ # import sys
6
+ # import os
7
+ # sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
8
+
3
9
  import importlib
4
10
  import pkgutil
11
+ import subprocess
5
12
  from typing import Optional
6
13
 
7
14
  import panflute as pf
8
15
  import rich_click as click
9
16
 
10
17
  import in2lambda.filters
11
- from in2lambda.api.module import Module
18
+ from in2lambda.api.set import Set
19
+
20
+
21
+ def docx_to_md(docx_file: str) -> str:
22
+ """Converts .docx files to markdown.
23
+
24
+ Args:
25
+ docx_file: A file path with the file extension included.
26
+
27
+ Returns:
28
+ the contents of the .docx file in markdown formatting
29
+ """
30
+ md_output = subprocess.check_output(["pandoc", docx_file, "-t", "markdown"])
31
+ return md_output.decode("utf-8")
12
32
 
13
33
 
14
34
  def file_type(file: str) -> str:
@@ -52,7 +72,7 @@ def file_type(file: str) -> str:
52
72
  ):
53
73
  return "markdown"
54
74
  case "docx":
55
- return "docx" # Pandoc doesn't seem to support doc
75
+ return "docx" # Pandoc doesn't seem to support .doc, and panflute doesn't like .docx.
56
76
  raise RuntimeError(f"Unsupported file extension: .{extension}")
57
77
 
58
78
 
@@ -61,7 +81,7 @@ def runner(
61
81
  chosen_filter: str,
62
82
  output_dir: Optional[str] = None,
63
83
  answer_file: Optional[str] = None,
64
- ) -> Module:
84
+ ) -> Set:
65
85
  r"""Takes in a TeX file for a given subject and outputs how it's broken down within Lambda Feedback.
66
86
 
67
87
  Args:
@@ -80,50 +100,62 @@ def runner(
80
100
  >>> from in2lambda.main import runner
81
101
  >>> # Retrieve an example TeX file and run the given filter.
82
102
  >>> runner(f"{os.path.dirname(in2lambda.__file__)}/filters/PartsSepSol/example.tex", "PartsSepSol") # doctest: +ELLIPSIS
83
- Module(questions=[Question(title='', parts=[Part(text=..., worked_solution=''), ...], images=[], main_text='This is a sample question\n\n'), ...])
103
+ Set(_name='set', _description='', _finalAnswerVisibility='OPEN_WITH_WARNINGS', _workedSolutionVisibility='OPEN_WITH_WARNINGS', _structuredTutorialVisibility='OPEN', questions=[Question(title='', parts=[Part(text=..., worked_solution=''), ...], images=[], main_text='This is a sample question\n\n'), ...])
84
104
  >>> runner(f"{os.path.dirname(in2lambda.__file__)}/filters/PartsOneSol/example.tex", "PartsOneSol") # doctest: +ELLIPSIS
85
- Module(questions=[Question(title='', parts=[Part(text='This is part (a)\n\n', worked_solution=''), ...], images=[], main_text='Here is some preliminary question information that might be useful.'), ...)
105
+ Set(_name='set', _description='', _finalAnswerVisibility='OPEN_WITH_WARNINGS', _workedSolutionVisibility='OPEN_WITH_WARNINGS', _structuredTutorialVisibility='OPEN', questions=[Question(title='', parts=[Part(text=..., worked_solution=''), ...], images=[], main_text='Here is some preliminary question information that might be useful.'), ...])
86
106
  """
87
107
  # The list of questions for Lambda Feedback as a Python API.
88
- module = Module()
108
+ set_obj = Set()
89
109
 
90
110
  # Dynamically import the correct pandoc filter depending on the subject.
91
111
  filter_module = importlib.import_module(f"in2lambda.filters.{chosen_filter}.filter")
92
112
 
93
- with open(question_file, "r", encoding="utf-8") as file:
94
- text = file.read()
113
+ if file_type(question_file) == "docx":
114
+ # Convert .docx to md using Pandoc and proceed
115
+ text = docx_to_md(question_file)
116
+ input_format = "markdown"
117
+ else:
118
+ with open(question_file, "r", encoding="utf-8") as file:
119
+ text = file.read()
120
+
121
+ input_format = file_type(question_file)
95
122
 
96
123
  # Parse the Pandoc AST using the relevant panflute filter.
97
124
  pf.run_filter(
98
125
  filter_module.pandoc_filter,
99
- doc=pf.convert_text(
100
- text, input_format=file_type(question_file), standalone=True
101
- ),
102
- module=module,
126
+ doc=pf.convert_text(text, input_format=input_format, standalone=True),
127
+ set=set_obj,
103
128
  tex_file=question_file,
104
129
  parsing_answers=False,
105
130
  )
106
131
 
107
132
  # If separate answer TeX file provided, parse that as well.
108
133
  if answer_file:
109
- with open(answer_file, "r", encoding="utf-8") as file:
110
- answer_text = file.read()
134
+
135
+ if file_type(answer_file) == "docx":
136
+
137
+ answer_text = docx_to_md(answer_file)
138
+ answer_format = "markdown"
139
+ else:
140
+ with open(answer_file, "r", encoding="utf-8") as file:
141
+ answer_text = file.read()
142
+ answer_format = file_type(answer_file)
111
143
 
112
144
  pf.run_filter(
113
145
  filter_module.pandoc_filter,
114
146
  doc=pf.convert_text(
115
- answer_text, input_format=file_type(answer_file), standalone=True
147
+ answer_text, input_format=answer_format, standalone=True
116
148
  ),
117
- module=module,
149
+ set=set_obj,
118
150
  tex_file=answer_file,
119
151
  parsing_answers=True,
120
152
  )
121
153
 
122
154
  # Read the Python API format and convert to JSON.
123
155
  if output_dir is not None:
124
- module.to_json(output_dir)
156
+ set_obj.to_json(output_dir)
125
157
 
126
- return module
158
+ return set_obj
127
159
 
128
160
 
129
161
  @click.command(
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "in2lambda"
3
- version = "0.3.2"
3
+ version = "1.0.0"
4
4
  description = "Converts content ready for import into Lambda Feedback"
5
5
  authors = []
6
6
  license = "MIT"
@@ -26,6 +26,7 @@ python = "^3.10"
26
26
  panflute = "^2.3.1"
27
27
  rich-click = "^1.7.4"
28
28
  beartype = "^0.17.2"
29
+ requests = "<2.32.0"
29
30
 
30
31
  [tool.poetry.scripts]
31
32
  in2lambda = "in2lambda.main:cli"
@@ -56,6 +57,9 @@ sphinx-togglebutton = "^0.3.2"
56
57
  # panflute is missing type hints
57
58
  ignore_missing_imports = true
58
59
 
60
+ [tool.isort]
61
+ profile = "black"
62
+
59
63
  [build-system]
60
64
  requires = ["poetry-core"]
61
65
  build-backend = "poetry.core.masonry.api"
@@ -1,122 +0,0 @@
1
- """Represents a list of questions."""
2
-
3
- from dataclasses import dataclass, field
4
- from typing import Union
5
-
6
- import panflute as pf
7
-
8
- from in2lambda.api.question import Question
9
- from in2lambda.json_convert import json_convert
10
-
11
-
12
- @dataclass
13
- class Module:
14
- """Represents a list of questions."""
15
-
16
- questions: list[Question] = field(default_factory=list)
17
- _current_question_index = -1
18
-
19
- @property
20
- def current_question(self) -> Question:
21
- """The current question being modified, or Question("INVALID") if there are no questions.
22
-
23
- The reasoning behind returning Question("INVALID") is in case filter logic is being applied
24
- on text before the first question (e.g. intro paragraphs). In that case, there is no effect.
25
-
26
- Returns:
27
- The current question or Question("INVALID") if there are no questions.
28
-
29
- Examples:
30
- >>> from in2lambda.api.module import Module
31
- >>> Module().current_question
32
- Question(title='INVALID', parts=[], images=[], main_text='')
33
- >>> module = Module()
34
- >>> module.add_question()
35
- >>> module.current_question
36
- Question(title='', parts=[], images=[], main_text='')
37
- """
38
- return (
39
- self.questions[self._current_question_index]
40
- if self.questions
41
- else Question("INVALID")
42
- )
43
-
44
- def add_question(
45
- self, title: str = "", main_text: Union[pf.Element, str] = pf.Str("")
46
- ) -> None:
47
- """Inserts a new question into the module.
48
-
49
- Args:
50
- title: An optional string for the title of the question. If no title
51
- is provided, the question title auto-increments i.e. Question 1, 2, etc.
52
- main_text: An optional string or panflute element for the main question text.
53
-
54
- Examples:
55
- >>> from in2lambda.api.module import Module
56
- >>> import panflute as pf
57
- >>> module = Module()
58
- >>> module.add_question("Some title", pf.Para(pf.Str("hello"), pf.Space, pf.Str("there")))
59
- >>> module
60
- Module(questions=[Question(title='Some title', parts=[], images=[], main_text='hello there')])
61
- >>> module.add_question(main_text="Normal string text")
62
- >>> module.questions[1].main_text
63
- 'Normal string text'
64
- """
65
- question = Question(title=title)
66
- question.main_text = main_text
67
- self.questions.append(question)
68
-
69
- def increment_current_question(self) -> None:
70
- """Manually overrides the current question being modified.
71
-
72
- The default (-1) indicates the last question added. Incrementing for the
73
- first time sets to 0 i.e. the first question.
74
-
75
- The is useful if adding question text first and answers later.
76
-
77
- Examples:
78
- >>> from in2lambda.api.module import Module
79
- >>> module = Module()
80
- >>> # Imagine adding the questions from a question file first...
81
- >>> module.add_question("Question 1")
82
- >>> module.add_question("Question 2")
83
- >>> # ...and then adding solutions from an answer file later
84
- >>> module.increment_current_question() # Loop back to question 1
85
- >>> module.current_question.add_solution("Question 1 answer")
86
- >>> module.increment_current_question()
87
- >>> module.current_question.add_solution("Question 2 answer")
88
- >>> module.questions
89
- [Question(title='Question 1', parts=[Part(text='', worked_solution='Question 1 answer')], images=[], main_text=''),\
90
- Question(title='Question 2', parts=[Part(text='', worked_solution='Question 2 answer')], images=[], main_text='')]
91
- """
92
- self._current_question_index += 1
93
-
94
- def to_json(self, output_dir: str) -> None:
95
- """Turns this module into Lambda Feedback JSON/ZIP files.
96
-
97
- WARNING: This will overwrite any existing files in the directory.
98
-
99
- Args:
100
- output_dir: Where to output the final Lambda Feedback JSON/ZIP files.
101
-
102
- Examples:
103
- >>> import tempfile
104
- >>> import os
105
- >>> import json
106
- >>> # Create a module with two questions
107
- >>> module = Module()
108
- >>> module.add_question("Question 1")
109
- >>> module.add_question("Question 2")
110
- >>> with tempfile.TemporaryDirectory() as temp_dir:
111
- ... # Write the JSON files to the temporary directory
112
- ... module.to_json(temp_dir)
113
- ... # Check the contents of the directory
114
- ... sorted(os.listdir(temp_dir))
115
- ... # Check the title of the first question
116
- ... with open(f"{temp_dir}/question_1/question_1.json") as file:
117
- ... print(f"Question 1's title: {json.load(file)['title']}")
118
- ['question_1', 'question_1.zip', 'question_2', 'question_2.zip']
119
- Question 1's title: Question 1
120
-
121
- """
122
- json_convert.main(self.questions, output_dir)
@@ -1 +0,0 @@
1
- """Files relating to translating the questions of a Python module object into JSON."""
@@ -1,98 +0,0 @@
1
- """Converts questions from a Python module object into Lambda Feedback JSON."""
2
-
3
- import json
4
- import os
5
- import shutil
6
- from copy import deepcopy
7
- from pathlib import Path
8
- from typing import Any
9
-
10
- from in2lambda.api.question import Question
11
-
12
- MINIMAL_TEMPLATE = "minimal_template.json"
13
-
14
-
15
- def converter(
16
- template: dict[str, Any], ListQuestions: list[Question], output_dir: str
17
- ) -> None:
18
- """Turns a list of question objects into Lambda Feedback JSON.
19
-
20
- Args:
21
- template: The loaded JSON from the minimal template (it needs to be in sync).
22
- ListQuestions: A list of question objects.
23
- output_dir: The absolute path for where to produced the final JSON/zip files.
24
- """
25
- # Create output by copying template
26
-
27
- for i in range(len(ListQuestions)):
28
- output = deepcopy(template)
29
-
30
- # add title to the question file
31
- if ListQuestions[i].title != "":
32
- output["title"] = ListQuestions[i].title
33
- else:
34
- output["title"] = "Question " + str(i + 1)
35
-
36
- # add main text to the question file
37
- output["masterContent"] = ListQuestions[i].main_text
38
-
39
- # add parts to the question file
40
- if ListQuestions[i].parts:
41
- output["parts"][0]["content"] = ListQuestions[i].parts[0].text
42
- output["parts"][0]["workedSolution"][0]["content"] = (
43
- ListQuestions[i].parts[0].worked_solution
44
- )
45
- for j in range(1, len(ListQuestions[i].parts)):
46
- output["parts"].append(deepcopy(template["parts"][0]))
47
- output["parts"][j]["content"] = ListQuestions[i].parts[j].text
48
- output["parts"][j]["workedSolution"][0]["content"] = (
49
- ListQuestions[i].parts[j].worked_solution
50
- )
51
-
52
- # Output file
53
- filename = "question_" + str(i + 1)
54
-
55
- # create directory to put the questions
56
- os.makedirs(output_dir, exist_ok=True)
57
- output_question = os.path.join(output_dir, filename)
58
- os.makedirs(output_question, exist_ok=True)
59
-
60
- # create directory to put image
61
- output_image = os.path.join(output_question, "media")
62
- os.makedirs(output_image, exist_ok=True)
63
-
64
- # write questions into directory
65
- with open(f"{output_question}/{filename}.json", "w") as file:
66
- json.dump(output, file)
67
-
68
- # write image into directory
69
- for k in range(len(ListQuestions[i].images)):
70
- image_path = os.path.abspath(
71
- ListQuestions[i].images[k]
72
- ) # converts computer path into python path
73
- shutil.copy(image_path, output_image) # copies image into the directory
74
-
75
- # output zip file in destination folder
76
- shutil.make_archive(output_question, "zip", output_question)
77
-
78
-
79
- def main(questions: list[Question], output_dir: str) -> None:
80
- """Preliminary defensive programming before calling the main converter function.
81
-
82
- This ultimately then produces the Lambda Feedback JSON/ZIP files.
83
-
84
- Args:
85
- questions: A list of question objects.
86
- output_dir: Where to output the final Lambda Feedback JSON/ZIP files.
87
- """
88
- # Use path so minimal template can be found regardless of where the user is running python from.
89
- with open(Path(__file__).with_name(MINIMAL_TEMPLATE), "r") as file:
90
- template = json.load(file)
91
-
92
- # check if directory exists in file
93
- if os.path.isdir(output_dir):
94
- try:
95
- shutil.rmtree(output_dir)
96
- except OSError as e:
97
- print("Error: %s : %s" % (output_dir, e.strerror))
98
- converter(template, questions, output_dir)
@@ -1,26 +0,0 @@
1
- {
2
- "masterContent": "Top level question here",
3
- "parts": [
4
- {
5
- "answer": "",
6
- "content": "Part text here",
7
- "responseArea": [],
8
- "tutorial": [],
9
- "universalPartId": "N/A",
10
- "workedSolution": [
11
- {
12
- "content": "Part worked solution here",
13
- "id": "N/A",
14
- "title": ""
15
- }
16
- ]
17
- }
18
- ],
19
- "publish": false,
20
- "questionSettings": {
21
- "displayFinalAnswer": true,
22
- "displayStructuredTutorial": true,
23
- "displayWorkedSolution": true
24
- },
25
- "title": "Question title here"
26
- }
File without changes
File without changes
File without changes