in2lambda 0.3.3__py3-none-any.whl → 1.0.0__py3-none-any.whl

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.
in2lambda/api/question.py CHANGED
@@ -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
in2lambda/api/set.py ADDED
@@ -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,7 +32,7 @@ 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/']
@@ -71,7 +71,7 @@ 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
@@ -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
@@ -1 +1 @@
1
- """Files relating to translating the questions of a Python module object into JSON."""
1
+ """Files relating to translating the questions of a Python set object into JSON."""
@@ -1,41 +1,78 @@
1
- """Converts questions from a Python module object into Lambda Feedback JSON."""
1
+ """Converts questions from a Python set object into Lambda Feedback JSON."""
2
2
 
3
3
  import json
4
4
  import os
5
+ import re
5
6
  import shutil
7
+ import zipfile
6
8
  from copy import deepcopy
7
9
  from pathlib import Path
8
10
  from typing import Any
9
11
 
10
- from in2lambda.api.question import Question
12
+ from in2lambda.api.set import Set
11
13
 
12
- MINIMAL_TEMPLATE = "minimal_template.json"
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)
13
32
 
14
33
 
15
34
  def converter(
16
- template: dict[str, Any], ListQuestions: list[Question], output_dir: str
35
+ question_template: dict[str, Any],
36
+ set_template: dict[str, Any],
37
+ SetQuestions: Set,
38
+ output_dir: str,
17
39
  ) -> None:
18
- """Turns a list of question objects into Lambda Feedback JSON.
40
+ """Turns a set of question objects into Lambda Feedback JSON.
19
41
 
20
42
  Args:
21
- template: The loaded JSON from the minimal template (it needs to be in sync).
22
- ListQuestions: A list of question objects.
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.
23
46
  output_dir: The absolute path for where to produced the final JSON/zip files.
24
47
  """
25
- # Create output by copying template
48
+ ListQuestions = SetQuestions.questions
49
+ set_name = SetQuestions._name
50
+ set_description = SetQuestions._description
26
51
 
27
52
  # create directory to put the questions
28
53
  os.makedirs(output_dir, exist_ok=True)
29
- output_question = os.path.join(output_dir, "set")
54
+ output_question = os.path.join(output_dir, set_name)
30
55
  os.makedirs(output_question, exist_ok=True)
31
56
 
32
- # create directory to put images - should be in set
33
- output_image = os.path.join(output_question, "media")
34
- os.makedirs(output_image, exist_ok=True)
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)
35
71
 
36
72
  for i in range(len(ListQuestions)):
37
- output = deepcopy(template)
73
+ output = deepcopy(question_template)
38
74
 
75
+ output["orderNumber"] = i # order number starts at 0
39
76
  # add title to the question file
40
77
  if ListQuestions[i].title != "":
41
78
  output["title"] = ListQuestions[i].title
@@ -52,7 +89,7 @@ def converter(
52
89
  ListQuestions[i].parts[0].worked_solution
53
90
  )
54
91
  for j in range(1, len(ListQuestions[i].parts)):
55
- output["parts"].append(deepcopy(template["parts"][0]))
92
+ output["parts"].append(deepcopy(question_template["parts"][0]))
56
93
  output["parts"][j]["content"] = ListQuestions[i].parts[j].text
57
94
  output["parts"][j]["orderNumber"] = j
58
95
  output["parts"][j]["workedSolution"]["content"] = (
@@ -60,7 +97,9 @@ def converter(
60
97
  )
61
98
 
62
99
  # Output file
63
- filename = "question_" + str(i + 1)
100
+ filename = (
101
+ "question_" + str(i).zfill(3) + "_" + re.sub(r'[^\w\-_.]', '_', output['title'].strip())
102
+ )
64
103
 
65
104
  # write questions into directory
66
105
  with open(f"{output_question}/{filename}.json", "w") as file:
@@ -71,24 +110,30 @@ def converter(
71
110
  image_path = os.path.abspath(
72
111
  ListQuestions[i].images[k]
73
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)
74
116
  shutil.copy(image_path, output_image) # copies image into the directory
75
117
 
76
- # output zip file in destination folder
77
- shutil.make_archive(output_question, "zip", output_question)
118
+ # output zip file in destination folder
119
+ _zip_sorted_folder(output_question, output_question + ".zip")
78
120
 
79
121
 
80
- def main(questions: list[Question], output_dir: str) -> None:
122
+ def main(set_questions: Set, output_dir: str) -> None:
81
123
  """Preliminary defensive programming before calling the main converter function.
82
124
 
83
125
  This ultimately then produces the Lambda Feedback JSON/ZIP files.
84
126
 
85
127
  Args:
86
- questions: A list of question objects.
128
+ set_questions: A Set object containing questions.
87
129
  output_dir: Where to output the final Lambda Feedback JSON/ZIP files.
88
130
  """
89
131
  # Use path so minimal template can be found regardless of where the user is running python from.
90
- with open(Path(__file__).with_name(MINIMAL_TEMPLATE), "r") as file:
91
- template = json.load(file)
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)
92
137
 
93
138
  # check if directory exists in file
94
139
  if os.path.isdir(output_dir):
@@ -96,4 +141,4 @@ def main(questions: list[Question], output_dir: str) -> None:
96
141
  shutil.rmtree(output_dir)
97
142
  except OSError as e:
98
143
  print("Error: %s : %s" % (output_dir, e.strerror))
99
- converter(template, questions, output_dir)
144
+ converter(question_template, set_template, set_questions, output_dir)
@@ -1,23 +1,23 @@
1
1
  {
2
+ "orderNumber": 0,
3
+ "title": "Question title here",
4
+ "masterContent": "Top level question here",
5
+ "publish": false,
2
6
  "displayFinalAnswer": true,
3
7
  "displayStructuredTutorial": true,
4
8
  "displayWorkedSolution": true,
5
- "masterContent": "Top level question here",
9
+ "displayChatbot": false,
6
10
  "parts": [
7
11
  {
8
- "answer": "",
9
- "content": "Part text here",
10
12
  "orderNumber": 0,
13
+ "content": "Part text here",
14
+ "answerContent": "",
11
15
  "responseAreas": [],
12
- "tutorial": [],
13
- "universalPartId": "N/A",
14
16
  "workedSolution": {
17
+ "title": "",
15
18
  "content": "Part worked solution here",
16
- "id": "N/A",
17
- "title": ""
19
+ "children": []
18
20
  }
19
21
  }
20
- ],
21
- "publish": false,
22
- "title": "Question title here"
22
+ ]
23
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
+ }
in2lambda/main.py CHANGED
@@ -15,7 +15,7 @@ import panflute as pf
15
15
  import rich_click as click
16
16
 
17
17
  import in2lambda.filters
18
- from in2lambda.api.module import Module
18
+ from in2lambda.api.set import Set
19
19
 
20
20
 
21
21
  def docx_to_md(docx_file: str) -> str:
@@ -81,7 +81,7 @@ def runner(
81
81
  chosen_filter: str,
82
82
  output_dir: Optional[str] = None,
83
83
  answer_file: Optional[str] = None,
84
- ) -> Module:
84
+ ) -> Set:
85
85
  r"""Takes in a TeX file for a given subject and outputs how it's broken down within Lambda Feedback.
86
86
 
87
87
  Args:
@@ -100,12 +100,12 @@ def runner(
100
100
  >>> from in2lambda.main import runner
101
101
  >>> # Retrieve an example TeX file and run the given filter.
102
102
  >>> runner(f"{os.path.dirname(in2lambda.__file__)}/filters/PartsSepSol/example.tex", "PartsSepSol") # doctest: +ELLIPSIS
103
- 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'), ...])
104
104
  >>> runner(f"{os.path.dirname(in2lambda.__file__)}/filters/PartsOneSol/example.tex", "PartsOneSol") # doctest: +ELLIPSIS
105
- 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.'), ...])
106
106
  """
107
107
  # The list of questions for Lambda Feedback as a Python API.
108
- module = Module()
108
+ set_obj = Set()
109
109
 
110
110
  # Dynamically import the correct pandoc filter depending on the subject.
111
111
  filter_module = importlib.import_module(f"in2lambda.filters.{chosen_filter}.filter")
@@ -124,7 +124,7 @@ def runner(
124
124
  pf.run_filter(
125
125
  filter_module.pandoc_filter,
126
126
  doc=pf.convert_text(text, input_format=input_format, standalone=True),
127
- module=module,
127
+ set=set_obj,
128
128
  tex_file=question_file,
129
129
  parsing_answers=False,
130
130
  )
@@ -146,16 +146,16 @@ def runner(
146
146
  doc=pf.convert_text(
147
147
  answer_text, input_format=answer_format, standalone=True
148
148
  ),
149
- module=module,
149
+ set=set_obj,
150
150
  tex_file=answer_file,
151
151
  parsing_answers=True,
152
152
  )
153
153
 
154
154
  # Read the Python API format and convert to JSON.
155
155
  if output_dir is not None:
156
- module.to_json(output_dir)
156
+ set_obj.to_json(output_dir)
157
157
 
158
- return module
158
+ return set_obj
159
159
 
160
160
 
161
161
  @click.command(
@@ -1,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: in2lambda
3
- Version: 0.3.3
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,8 +1,9 @@
1
1
  in2lambda/__init__.py,sha256=VvbVGkrakfqTIqnYUuv95_LFxitkMODRYr161R9128U,492
2
2
  in2lambda/api/__init__.py,sha256=uh1WA1JIIYmjaWXms3JR5LOlnizv-ozW4ijBHQxi9QA,103
3
- in2lambda/api/module.py,sha256=frAU0m6dJceXiVlGKiny_1UJGowwO0Q6p6RAlKjKqME,5173
4
3
  in2lambda/api/part.py,sha256=Y-W0sd5mJQifA8V8nZNTYk_HPhxhRQq5mUCdlYKioXw,204
5
- in2lambda/api/question.py,sha256=is-Bmx85GFAV7Jf8mblP2ZtkDgfbRcVbMhh9Tmy2hmk,6173
4
+ in2lambda/api/question.py,sha256=B1U8FMKs89UhV51s8ePTVvoSVNnisMrQyPz3kPdZmcM,6170
5
+ in2lambda/api/set.py,sha256=k7bY5iMuIbmDS_HxflY0J7RtRpW6DwsKMONYwqaDBAk,7135
6
+ in2lambda/api/visibility_status.py,sha256=UU-6SOtf5VYoLAZ5NCPy7QXxe6FyfnEmnZJK8Fm17mc,2170
6
7
  in2lambda/example_questions.tex,sha256=kvcuUUoQXTJYqrAlUMfibzGsL9qFgqbKS57JIg1YhkA,9491
7
8
  in2lambda/example_solutions.tex,sha256=0idJQV9Q8VhMO_dB5RYzYkZzyEMSA594sqnPdl4wlLA,22164
8
9
  in2lambda/figures/O2_potential.png,sha256=AxDizIID24J20wqVUAneGIN_WWr6Vfra3xoBdkgJbK0,64855
@@ -10,29 +11,30 @@ in2lambda/figures/activated_complex.png,sha256=6AQuS6lftVMX0GkAR5WpOQnSgzvhNA3Pb
10
11
  in2lambda/figures/rocket-momentum.png,sha256=EXsR7k_WehhSBfnsh4fssCTEKHpu6q3lCa4csHAFGHI,397693
11
12
  in2lambda/filters/PartPartSolSol/__init__.py,sha256=YJCnCoGJfXt7jrMdgcsEfkI7Su9WK-VOvUwwefQ8zog,124
12
13
  in2lambda/filters/PartPartSolSol/example.tex,sha256=TK4JyjYj_wFfi9bkV8uKfXdFIBYPsvgMng5x1XdE_oU,667
13
- in2lambda/filters/PartPartSolSol/filter.py,sha256=vttP0_RNjLuLu2GSUudzv2qk-AQHuRb172JAwf_0mmA,2383
14
+ in2lambda/filters/PartPartSolSol/filter.py,sha256=Dj7mc5jBExvHU5Dqfb8W0Df30gWqprHuVvCTYcsk0wQ,2356
14
15
  in2lambda/filters/PartSolPartSol/__init__.py,sha256=Rf1LgSoGNkkgVN3yt6dbT_UhZs5ngMAPLpOFx2NwCBo,124
15
16
  in2lambda/filters/PartSolPartSol/example.tex,sha256=teJGf6ClSgEYj7QuitPiTlfG8V2U2JEQUUzZFhQ35kk,607
16
- in2lambda/filters/PartSolPartSol/filter.py,sha256=bBUREAVw9FqrpQgTx9fAIivONjRefshCWSbIkZDs0Fc,2294
17
+ in2lambda/filters/PartSolPartSol/filter.py,sha256=XlgHaLzDvemgJSK23mg2LP5KX-zwxYMWgWmggnBo0W0,2218
17
18
  in2lambda/filters/PartsOneSol/__init__.py,sha256=2aOKWhpyDwlXGNylO1VNROux1HCg2jL9XWaY_4SyWXQ,121
18
19
  in2lambda/filters/PartsOneSol/example.tex,sha256=d45J2goEL_X-otuwtwJwXId1KHTW2sGGkv6rYfYZ_M4,756
19
- in2lambda/filters/PartsOneSol/filter.py,sha256=DLj7DcDt1Aru2B4Tb5rljOFKZ4sXB8UUc4Yd3knnJow,2017
20
+ in2lambda/filters/PartsOneSol/filter.py,sha256=dYol405f1wJfGEKSMYmVnzcJ_OvRCF8spSMZgTO9aos,1993
20
21
  in2lambda/filters/PartsSepSol/__init__.py,sha256=yAIUC3oJVob1R7Rrvn0sXGpK_-pXfF46DeJ9vaXSNfQ,121
21
22
  in2lambda/filters/PartsSepSol/example.tex,sha256=w7mJIcWCqKzJE_sJG4bg0J-V9kXSfe6doKWXgTd1nF8,687
22
- in2lambda/filters/PartsSepSol/filter.py,sha256=9cV6HhSqOQGTk5bIS36pgBbB-5UWO3mPI2s8l9ZsNeE,3206
23
+ in2lambda/filters/PartsSepSol/filter.py,sha256=izPDylUkuRdHDJlLsOx5nArs1owdXRmdSS6eaqEaBgE,3176
23
24
  in2lambda/filters/__init__.py,sha256=tla_1d85db2MAT-fgc5sNoScuYZJeTgKedzFn3NXT2c,69
24
- in2lambda/filters/markdown.py,sha256=6c0Q89CCtVhJ5F7of-eueUs9WqXXhMZNFq0uZr-ZxMQ,7156
25
- in2lambda/json_convert/__init__.py,sha256=2X3SLn6H449zXFprtZZU7M20elsOJ09mQr7EDnv04ow,87
26
- in2lambda/json_convert/json_convert.py,sha256=3C8Aa0RU1zRUXDvpvL6wV16jcGTuwVI0A1NfDcQg8fg,3662
27
- in2lambda/json_convert/minimal_template.json,sha256=o83cZbhabXqeJiTCkLqdhijrJiiAfv1_I6HLT_QunZ8,516
25
+ in2lambda/filters/markdown.py,sha256=OMnCt18asL7o-gbfggtQnTr5x5I_TpHppLOdfpUi308,7130
26
+ in2lambda/json_convert/__init__.py,sha256=cZI8iK_yGzVPy54_e6SPLk3yS4Nh1Rw0XirZIVt3CAo,84
27
+ in2lambda/json_convert/json_convert.py,sha256=LZZOwdac9h6UD5dIOSqa7XmLGCe_q_eMWSuVhQb1w1o,5520
28
+ in2lambda/json_convert/minimal_template_question.json,sha256=mJgp6vRbeWopaXVrpzbReHXAZx0No-9ICpUoRZTdH3c,519
29
+ in2lambda/json_convert/minimal_template_set.json,sha256=Uj4I_0rhaXdhXAIupgQMOddVmhm92e3XP3iIwh1esQc,283
28
30
  in2lambda/katex_convert/__init__.py,sha256=D0X21-NQXNrWwu_klZeCaQaqtV9vHfFv1PlrtuVAHlI,96
29
31
  in2lambda/katex_convert/delete_list.txt,sha256=Ywx8ltUbrvSxGaQVoTSXfXAe4FfPSCuRchkSCICYjHU,1753
30
32
  in2lambda/katex_convert/katex_convert.py,sha256=3UUw84432lOBnPLJwAw68MMyFPrD6ijxyYR-qVhJhnw,4909
31
33
  in2lambda/katex_convert/replace_list.txt,sha256=y5ciG4orj64eqEKsOVaQ95Zx7Xx0y4Awa4JIS27HVSw,611
32
- in2lambda/main.py,sha256=IifSQfMn1ARYlrvowYYcYLGo6b5sMOxxz-nnELk56_k,7068
34
+ in2lambda/main.py,sha256=zIat7kBuk25b0JUkcPdhHYQSy2H5PL5HAIYWMe-ch8E,7353
33
35
  in2lambda/py.typed,sha256=gN7aphE6U0Jj-OffDD7hw1OkRs9gg6OPOrVpN5eAnCA,56
34
- in2lambda-0.3.3.dist-info/LICENSE,sha256=vEyoprnh_ic_ZQ-woDohxEIE8GJOyXqbYZ-5cH42FG4,1072
35
- in2lambda-0.3.3.dist-info/METADATA,sha256=ROh8P1bkW2CWV73U1g6zrvEhvtz5Pc_iceFsqp2e5wI,1833
36
- in2lambda-0.3.3.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
37
- in2lambda-0.3.3.dist-info/entry_points.txt,sha256=xhkZ_lmsx7SgdhjJC-DvkFFqu-Ff-wD4Poir55bdUSM,48
38
- in2lambda-0.3.3.dist-info/RECORD,,
36
+ in2lambda-1.0.0.dist-info/LICENSE,sha256=vEyoprnh_ic_ZQ-woDohxEIE8GJOyXqbYZ-5cH42FG4,1072
37
+ in2lambda-1.0.0.dist-info/METADATA,sha256=RnRazCJTS1F-fzbqmRxHvuv2NNBFcOUuzyMhXQb7cbc,1930
38
+ in2lambda-1.0.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
39
+ in2lambda-1.0.0.dist-info/entry_points.txt,sha256=xhkZ_lmsx7SgdhjJC-DvkFFqu-Ff-wD4Poir55bdUSM,48
40
+ in2lambda-1.0.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.8.1
2
+ Generator: poetry-core 2.1.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
in2lambda/api/module.py DELETED
@@ -1,125 +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 contents of the set directory
116
- ... sorted(os.listdir(f"{temp_dir}/set"))
117
- ... # Check the title of the first question
118
- ... with open(f"{temp_dir}/set/question_1.json") as file:
119
- ... print(f"Question 1's title: {json.load(file)['title']}")
120
- ['set', 'set.zip']
121
- ['media', 'question_1.json', 'question_2.json']
122
- Question 1's title: Question 1
123
-
124
- """
125
- json_convert.main(self.questions, output_dir)