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 +1 -1
- in2lambda/api/set.py +185 -0
- in2lambda/api/visibility_status.py +76 -0
- in2lambda/filters/PartPartSolSol/filter.py +7 -7
- in2lambda/filters/PartSolPartSol/filter.py +8 -10
- in2lambda/filters/PartsOneSol/filter.py +6 -6
- in2lambda/filters/PartsSepSol/filter.py +8 -8
- in2lambda/filters/markdown.py +8 -8
- in2lambda/json_convert/__init__.py +1 -1
- in2lambda/json_convert/json_convert.py +67 -22
- in2lambda/json_convert/{minimal_template.json → minimal_template_question.json} +10 -10
- in2lambda/json_convert/minimal_template_set.json +9 -0
- in2lambda/main.py +9 -9
- {in2lambda-0.3.3.dist-info → in2lambda-1.0.0.dist-info}/METADATA +5 -3
- {in2lambda-0.3.3.dist-info → in2lambda-1.0.0.dist-info}/RECORD +18 -16
- {in2lambda-0.3.3.dist-info → in2lambda-1.0.0.dist-info}/WHEEL +1 -1
- in2lambda/api/module.py +0 -125
- {in2lambda-0.3.3.dist-info → in2lambda-1.0.0.dist-info}/LICENSE +0 -0
- {in2lambda-0.3.3.dist-info → in2lambda-1.0.0.dist-info}/entry_points.txt +0 -0
in2lambda/api/question.py
CHANGED
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61
|
+
set.current_question.add_solution(pf.stringify(item))
|
|
62
62
|
case pf.Para:
|
|
63
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
+
set.current_question.add_solution(spaced_blurb + part)
|
|
77
77
|
if parsing_answers
|
|
78
|
-
else
|
|
78
|
+
else set.current_question.add_part_text(part)
|
|
79
79
|
)
|
|
80
80
|
return None
|
in2lambda/filters/markdown.py
CHANGED
|
@@ -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.
|
|
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
|
-
...
|
|
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
|
-
|
|
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,
|
|
120
|
+
[pf.Element, pf.elements.Doc, Set, bool],
|
|
121
121
|
Optional[pf.Str],
|
|
122
122
|
]
|
|
123
123
|
) -> Callable[
|
|
124
|
-
[pf.Element, pf.elements.Doc,
|
|
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
|
-
|
|
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
|
-
|
|
178
|
+
set.current_question.images.append(path)
|
|
179
179
|
return pf.Str(f"")
|
|
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,
|
|
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
|
|
1
|
+
"""Files relating to translating the questions of a Python set object into JSON."""
|
|
@@ -1,41 +1,78 @@
|
|
|
1
|
-
"""Converts questions from a Python
|
|
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.
|
|
12
|
+
from in2lambda.api.set import Set
|
|
11
13
|
|
|
12
|
-
|
|
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
|
-
|
|
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
|
|
40
|
+
"""Turns a set of question objects into Lambda Feedback JSON.
|
|
19
41
|
|
|
20
42
|
Args:
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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,
|
|
54
|
+
output_question = os.path.join(output_dir, set_name)
|
|
30
55
|
os.makedirs(output_question, exist_ok=True)
|
|
31
56
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
77
|
-
|
|
118
|
+
# output zip file in destination folder
|
|
119
|
+
_zip_sorted_folder(output_question, output_question + ".zip")
|
|
78
120
|
|
|
79
121
|
|
|
80
|
-
def main(
|
|
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
|
-
|
|
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(
|
|
91
|
-
|
|
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(
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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.
|
|
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
|
-
) ->
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
156
|
+
set_obj.to_json(output_dir)
|
|
157
157
|
|
|
158
|
-
return
|
|
158
|
+
return set_obj
|
|
159
159
|
|
|
160
160
|
|
|
161
161
|
@click.command(
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: in2lambda
|
|
3
|
-
Version: 0.
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
25
|
-
in2lambda/json_convert/__init__.py,sha256=
|
|
26
|
-
in2lambda/json_convert/json_convert.py,sha256=
|
|
27
|
-
in2lambda/json_convert/
|
|
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=
|
|
34
|
+
in2lambda/main.py,sha256=zIat7kBuk25b0JUkcPdhHYQSy2H5PL5HAIYWMe-ch8E,7353
|
|
33
35
|
in2lambda/py.typed,sha256=gN7aphE6U0Jj-OffDD7hw1OkRs9gg6OPOrVpN5eAnCA,56
|
|
34
|
-
in2lambda-0.
|
|
35
|
-
in2lambda-0.
|
|
36
|
-
in2lambda-0.
|
|
37
|
-
in2lambda-0.
|
|
38
|
-
in2lambda-0.
|
|
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,,
|
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)
|
|
File without changes
|
|
File without changes
|