in2lambda 0.3.3__tar.gz → 1.0.1__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.
- {in2lambda-0.3.3 → in2lambda-1.0.1}/PKG-INFO +8 -3
- in2lambda-1.0.1/in2lambda/api/part.py +30 -0
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/api/question.py +48 -31
- in2lambda-1.0.1/in2lambda/api/response_area.py +317 -0
- in2lambda-1.0.1/in2lambda/api/set.py +201 -0
- in2lambda-1.0.1/in2lambda/api/visibility_status.py +77 -0
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/filters/PartPartSolSol/filter.py +7 -7
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/filters/PartSolPartSol/filter.py +8 -10
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/filters/PartsOneSol/filter.py +6 -6
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/filters/PartsSepSol/filter.py +8 -8
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/filters/markdown.py +9 -9
- in2lambda-1.0.1/in2lambda/json_convert/__init__.py +1 -0
- in2lambda-1.0.1/in2lambda/json_convert/json_convert.py +259 -0
- in2lambda-0.3.3/in2lambda/json_convert/minimal_template.json → in2lambda-1.0.1/in2lambda/json_convert/minimal_template_question.json +9 -10
- in2lambda-1.0.1/in2lambda/json_convert/minimal_template_set.json +9 -0
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/main.py +9 -9
- {in2lambda-0.3.3 → in2lambda-1.0.1}/pyproject.toml +12 -3
- in2lambda-0.3.3/in2lambda/api/module.py +0 -125
- in2lambda-0.3.3/in2lambda/api/part.py +0 -11
- in2lambda-0.3.3/in2lambda/json_convert/__init__.py +0 -1
- in2lambda-0.3.3/in2lambda/json_convert/json_convert.py +0 -99
- {in2lambda-0.3.3 → in2lambda-1.0.1}/LICENSE +0 -0
- {in2lambda-0.3.3 → in2lambda-1.0.1}/README.md +0 -0
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/__init__.py +0 -0
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/api/__init__.py +0 -0
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/example_questions.tex +0 -0
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/example_solutions.tex +0 -0
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/figures/O2_potential.png +0 -0
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/figures/activated_complex.png +0 -0
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/figures/rocket-momentum.png +0 -0
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/filters/PartPartSolSol/__init__.py +0 -0
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/filters/PartPartSolSol/example.tex +0 -0
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/filters/PartSolPartSol/__init__.py +0 -0
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/filters/PartSolPartSol/example.tex +0 -0
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/filters/PartsOneSol/__init__.py +0 -0
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/filters/PartsOneSol/example.tex +0 -0
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/filters/PartsSepSol/__init__.py +0 -0
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/filters/PartsSepSol/example.tex +0 -0
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/filters/__init__.py +0 -0
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/katex_convert/__init__.py +0 -0
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/katex_convert/delete_list.txt +0 -0
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/katex_convert/katex_convert.py +0 -0
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/katex_convert/replace_list.txt +0 -0
- {in2lambda-0.3.3 → in2lambda-1.0.1}/in2lambda/py.typed +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: in2lambda
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Converts content ready for import into Lambda Feedback
|
|
5
|
-
Home-page: https://lambda-feedback.github.io/in2lambda
|
|
6
5
|
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
7
|
Requires-Python: >=3.10,<4.0
|
|
8
8
|
Classifier: Development Status :: 3 - Alpha
|
|
9
9
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -13,13 +13,18 @@ Classifier: Programming Language :: Python :: 3
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.10
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
16
19
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
20
|
Classifier: Typing :: Typed
|
|
18
21
|
Requires-Dist: beartype (>=0.17.2,<0.18.0)
|
|
19
22
|
Requires-Dist: panflute (>=2.3.1,<3.0.0)
|
|
23
|
+
Requires-Dist: requests (<2.34.0)
|
|
20
24
|
Requires-Dist: rich-click (>=1.7.4,<2.0.0)
|
|
21
25
|
Project-URL: Bug Tracker, https://github.com/lambda-feedback/in2lambda/issues
|
|
22
26
|
Project-URL: Documentation, https://lambda-feedback.github.io/in2lambda
|
|
27
|
+
Project-URL: Homepage, https://lambda-feedback.github.io/in2lambda
|
|
23
28
|
Project-URL: Repository, https://github.com/lambda-feedback/in2lambda
|
|
24
29
|
Description-Content-Type: text/markdown
|
|
25
30
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""A part of a question."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from in2lambda.api.response_area import ResponseArea
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Branch:
|
|
10
|
+
"""A (titled) branch of a structured tutorial or worked solution, which may branch further."""
|
|
11
|
+
|
|
12
|
+
title: str
|
|
13
|
+
content: str
|
|
14
|
+
branches: list["Branch"] = field(default_factory=list)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Part:
|
|
19
|
+
"""A part of a question as represented on Lambda Feedback.
|
|
20
|
+
|
|
21
|
+
Markdown content for the question and supporting materials. Note that in ``tutorial`` and ``worked_solution`` fields, steps can by separated by a line with only ``---`` (or ``***``).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
content: str = ""
|
|
25
|
+
response_areas: list[ResponseArea] = field(default_factory=list)
|
|
26
|
+
tutorial: str = ""
|
|
27
|
+
tutorial_branches: list[Branch] = field(default_factory=list)
|
|
28
|
+
answer: str = ""
|
|
29
|
+
worked_solution: str = ""
|
|
30
|
+
worked_solution_branches: list[Branch] = field(default_factory=list)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
"""A full question with optional parts that's contained in a
|
|
1
|
+
"""A full question with optional parts that's contained in a set."""
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
|
-
from typing import Union
|
|
4
|
+
from typing import Optional, Union
|
|
5
5
|
|
|
6
6
|
import panflute as pf
|
|
7
7
|
|
|
@@ -14,10 +14,12 @@ class Question:
|
|
|
14
14
|
|
|
15
15
|
Each question has a title and is composed of a list of parts.
|
|
16
16
|
|
|
17
|
+
It also carries the settings Lambda Feedback keeps per question.
|
|
18
|
+
|
|
17
19
|
Examples:
|
|
18
20
|
>>> from in2lambda.api.question import Question
|
|
19
|
-
>>> Question(title="Some title",
|
|
20
|
-
Question(title='Some title', parts=[], images=[],
|
|
21
|
+
>>> Question(title="Some title", master_content="Some text")
|
|
22
|
+
Question(title='Some title', parts=[], images=[], master_content='Some text')
|
|
21
23
|
"""
|
|
22
24
|
|
|
23
25
|
title: str = ""
|
|
@@ -25,10 +27,10 @@ class Question:
|
|
|
25
27
|
parts: list[Part] = field(default_factory=list)
|
|
26
28
|
images: list[str] = field(default_factory=list)
|
|
27
29
|
|
|
28
|
-
# Use getter/setter methods for
|
|
30
|
+
# Use getter/setter methods for master_content without exposing private attribute.
|
|
29
31
|
# See https://stackoverflow.com/a/61480946
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
master_content: str = ""
|
|
33
|
+
_master_content: str = field(init=False, repr=False, default="")
|
|
32
34
|
|
|
33
35
|
_last_part: dict[str, int] = field(
|
|
34
36
|
default_factory=lambda: {"solution": 0, "text": 0}, repr=False
|
|
@@ -36,8 +38,23 @@ class Question:
|
|
|
36
38
|
"""Keeps track of the last question part that contains a solution /
|
|
37
39
|
text."""
|
|
38
40
|
|
|
41
|
+
# The settings below are left out of the repr, so that printing a question shows its
|
|
42
|
+
# content rather than nine lines of configuration.
|
|
43
|
+
|
|
44
|
+
# The skill level may be an int as well as a float, because Lambda Feedback writes
|
|
45
|
+
# its exports in JavaScript, which writes the levels 0.0 and 1.0 as 0 and 1.
|
|
46
|
+
skill: Optional[Union[int, float]] = field(default=None, repr=False)
|
|
47
|
+
guidance: Optional[str] = field(default=None, repr=False)
|
|
48
|
+
duration_lower_bound: Optional[int] = field(default=None, repr=False)
|
|
49
|
+
duration_upper_bound: Optional[int] = field(default=None, repr=False)
|
|
50
|
+
publish: bool = field(default=False, repr=False)
|
|
51
|
+
display_final_answer: bool = field(default=True, repr=False)
|
|
52
|
+
display_worked_solution: bool = field(default=True, repr=False)
|
|
53
|
+
display_structured_tutorial: bool = field(default=True, repr=False)
|
|
54
|
+
display_chatbot: bool = field(default=False, repr=False)
|
|
55
|
+
|
|
39
56
|
@property
|
|
40
|
-
def
|
|
57
|
+
def master_content(self) -> str:
|
|
41
58
|
r"""Main top-level question text.
|
|
42
59
|
|
|
43
60
|
Setting the attribute multiple times appends to the current value with a newline.
|
|
@@ -46,21 +63,21 @@ class Question:
|
|
|
46
63
|
Examples:
|
|
47
64
|
>>> from in2lambda.api.question import Question
|
|
48
65
|
>>> question = Question()
|
|
49
|
-
>>> question.
|
|
50
|
-
>>> question.
|
|
51
|
-
>>> question.
|
|
66
|
+
>>> question.master_content = "hello"
|
|
67
|
+
>>> question.master_content = "there"
|
|
68
|
+
>>> question.master_content
|
|
52
69
|
'hello\nthere'
|
|
53
70
|
"""
|
|
54
|
-
return self.
|
|
71
|
+
return self._master_content
|
|
55
72
|
|
|
56
|
-
@
|
|
57
|
-
def
|
|
73
|
+
@master_content.setter
|
|
74
|
+
def master_content(self, value: Union[pf.Element, str, property]) -> None:
|
|
58
75
|
r"""Appends to the top-level main text, which starts off as an empty string.
|
|
59
76
|
|
|
60
77
|
Args:
|
|
61
78
|
value: A panflute element or string denoting what to append to the main text.
|
|
62
79
|
|
|
63
|
-
See example in
|
|
80
|
+
See example in master_content property.
|
|
64
81
|
"""
|
|
65
82
|
# Converts the inputted value into a string, stored in text_value
|
|
66
83
|
match value:
|
|
@@ -69,13 +86,13 @@ class Question:
|
|
|
69
86
|
case property():
|
|
70
87
|
# Use default value when no value set at initialisation.
|
|
71
88
|
# See https://stackoverflow.com/a/61480946
|
|
72
|
-
text_value = self.
|
|
89
|
+
text_value = self.master_content
|
|
73
90
|
case pf.Element():
|
|
74
91
|
text_value = pf.stringify(value, False)
|
|
75
92
|
|
|
76
|
-
if self.
|
|
77
|
-
self.
|
|
78
|
-
self.
|
|
93
|
+
if self._master_content:
|
|
94
|
+
self._master_content += "\n"
|
|
95
|
+
self._master_content += text_value
|
|
79
96
|
|
|
80
97
|
def add_solution(self, elem: Union[pf.Element, str]) -> None:
|
|
81
98
|
"""Adds a worked solution to all question parts without one, or inserts a new empty part with the solution if all parts already have a solution.
|
|
@@ -89,20 +106,20 @@ class Question:
|
|
|
89
106
|
>>> question.add_part_text("part a")
|
|
90
107
|
>>> question.add_solution("part a solution")
|
|
91
108
|
>>> question
|
|
92
|
-
Question(title='', parts=[Part(
|
|
109
|
+
Question(title='', parts=[Part(content='part a', response_areas=[], tutorial='', tutorial_branches=[], answer='', worked_solution='part a solution', worked_solution_branches=[])], images=[], master_content='')
|
|
93
110
|
>>> question.add_part_text("part b")
|
|
94
111
|
>>> question.add_part_text("part c")
|
|
95
112
|
>>> question.add_solution("Solution for b")
|
|
96
113
|
>>> # Note that since c doesn't have a solution, it's set to b's solution
|
|
97
114
|
>>> question
|
|
98
|
-
Question(title='', parts=[Part(
|
|
99
|
-
Part(
|
|
100
|
-
Part(
|
|
115
|
+
Question(title='', parts=[Part(content='part a', response_areas=[], tutorial='', tutorial_branches=[], answer='', worked_solution='part a solution', worked_solution_branches=[]), \
|
|
116
|
+
Part(content='part b', response_areas=[], tutorial='', tutorial_branches=[], answer='', worked_solution='Solution for b', worked_solution_branches=[]), \
|
|
117
|
+
Part(content='part c', response_areas=[], tutorial='', tutorial_branches=[], answer='', worked_solution='Solution for b', worked_solution_branches=[])], images=[], master_content='')
|
|
101
118
|
>>> question.add_solution("We now have a solution for c!")
|
|
102
119
|
>>> question
|
|
103
|
-
Question(title='', parts=[Part(
|
|
104
|
-
Part(
|
|
105
|
-
Part(
|
|
120
|
+
Question(title='', parts=[Part(content='part a', response_areas=[], tutorial='', tutorial_branches=[], answer='', worked_solution='part a solution', worked_solution_branches=[]), \
|
|
121
|
+
Part(content='part b', response_areas=[], tutorial='', tutorial_branches=[], answer='', worked_solution='Solution for b', worked_solution_branches=[]), \
|
|
122
|
+
Part(content='part c', response_areas=[], tutorial='', tutorial_branches=[], answer='', worked_solution='We now have a solution for c!', worked_solution_branches=[])], images=[], master_content='')
|
|
106
123
|
"""
|
|
107
124
|
elem_text = elem if isinstance(elem, str) else pf.stringify(elem)
|
|
108
125
|
|
|
@@ -131,19 +148,19 @@ Part(text='part c', worked_solution='We now have a solution for c!')], images=[]
|
|
|
131
148
|
>>> question.add_part_text("part a")
|
|
132
149
|
>>> question.add_solution("part a solution")
|
|
133
150
|
>>> question
|
|
134
|
-
Question(title='', parts=[Part(
|
|
151
|
+
Question(title='', parts=[Part(content='part a', response_areas=[], tutorial='', tutorial_branches=[], answer='', worked_solution='part a solution', worked_solution_branches=[])], images=[], master_content='')
|
|
135
152
|
>>> # Supports adding the answer first.
|
|
136
153
|
>>> question.add_solution("part b solution")
|
|
137
154
|
>>> question.add_part_text("part b")
|
|
138
155
|
>>> question
|
|
139
|
-
Question(title='', parts=[Part(
|
|
140
|
-
Part(
|
|
156
|
+
Question(title='', parts=[Part(content='part a', response_areas=[], tutorial='', tutorial_branches=[], answer='', worked_solution='part a solution', worked_solution_branches=[]), \
|
|
157
|
+
Part(content='part b', response_areas=[], tutorial='', tutorial_branches=[], answer='', worked_solution='part b solution', worked_solution_branches=[])], images=[], master_content='')
|
|
141
158
|
"""
|
|
142
159
|
elem_text = elem if isinstance(elem, str) else pf.stringify(elem)
|
|
143
160
|
|
|
144
161
|
if len(self.parts) == self._last_part["text"]:
|
|
145
|
-
self.parts.append(Part(
|
|
162
|
+
self.parts.append(Part(content=elem_text))
|
|
146
163
|
else:
|
|
147
|
-
self.parts[self._last_part["text"]].
|
|
164
|
+
self.parts[self._last_part["text"]].content = elem_text
|
|
148
165
|
|
|
149
166
|
self._last_part["text"] += 1
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""An area for student responses that will receive automated feedback."""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, ClassVar, Optional, Union
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _new_id() -> str:
|
|
10
|
+
return str(uuid.uuid4())
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class InputSymbol:
|
|
15
|
+
"""Defines symbols and how to enter them into a response area.
|
|
16
|
+
|
|
17
|
+
Fields per symbol:
|
|
18
|
+
- ``symbol`` for display to users (markdown, typically dollar-delimited math latex)
|
|
19
|
+
- ``code`` for users to input (plan text, fed to the algorithm and also be rendered to users within backticks as `code`)
|
|
20
|
+
- ``aliases`` list of alternative code inputs, to be substituted by the `code` field when evaluating
|
|
21
|
+
- ``is_visible`` Boolean, whether to display input symbol info to users (in addition to always sending it to the evaluator to help parsing)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
symbol: str
|
|
25
|
+
code: str
|
|
26
|
+
aliases: list[str] = field(default_factory=list)
|
|
27
|
+
is_visible: bool = True
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class Test:
|
|
32
|
+
"""Teacher author's assertions of how responses should be evaluated."""
|
|
33
|
+
|
|
34
|
+
# Its name would otherwise make pytest try to collect it wherever it is imported.
|
|
35
|
+
__test__ = False
|
|
36
|
+
|
|
37
|
+
payload: str
|
|
38
|
+
is_correct: bool
|
|
39
|
+
id: str = field(default_factory=_new_id)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Color(Enum):
|
|
43
|
+
"""Acceptable colours for a feedback case."""
|
|
44
|
+
|
|
45
|
+
RED = "#ff5630"
|
|
46
|
+
AMBER = "#FFAB00"
|
|
47
|
+
GREEN = "#22C55E"
|
|
48
|
+
BLUE = "#0099c4"
|
|
49
|
+
GREY = "#C4CDD5"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class Case:
|
|
54
|
+
"""A declared case for which bespoke feedback is prescribed."""
|
|
55
|
+
|
|
56
|
+
answer: str
|
|
57
|
+
feedback: str
|
|
58
|
+
is_correct: bool
|
|
59
|
+
params: Any = None
|
|
60
|
+
color: Optional[Color] = None
|
|
61
|
+
id: str = field(default_factory=_new_id)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(kw_only=True)
|
|
65
|
+
class ResponseArea:
|
|
66
|
+
"""Specifies the interaction when a student provides a response.
|
|
67
|
+
|
|
68
|
+
Multiple response areas per part are allowed; if so, order is implicit.
|
|
69
|
+
Use a subclass for each response type. ``grade_params`` are passed to the
|
|
70
|
+
evaluation function unedited.
|
|
71
|
+
|
|
72
|
+
Examples:
|
|
73
|
+
>>> from in2lambda.api.response_area import NumericUnits, Test
|
|
74
|
+
>>> area = NumericUnits(
|
|
75
|
+
... answer="30 N",
|
|
76
|
+
... grade_params={"rtol": 0.05},
|
|
77
|
+
... pre_text="$F=$",
|
|
78
|
+
... tests=[Test("30 N", True)],
|
|
79
|
+
... )
|
|
80
|
+
>>> area.response_type, area.evaluation_function
|
|
81
|
+
('NUMERIC_UNITS', 'compareExpressions')
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
response_type: ClassVar[str]
|
|
85
|
+
answer: Any = None
|
|
86
|
+
evaluation_function: str
|
|
87
|
+
grade_params: Optional[dict[str, Any]] = None
|
|
88
|
+
pre_text: str = ""
|
|
89
|
+
post_text: str = ""
|
|
90
|
+
content_after: str = ""
|
|
91
|
+
"""Markdown shown after the box, before the next one."""
|
|
92
|
+
check_button_text: str = ""
|
|
93
|
+
"""Replaces the label of the check button; left out of the JSON when empty."""
|
|
94
|
+
input_symbols: list[InputSymbol] = field(default_factory=list)
|
|
95
|
+
display_input_symbols: bool = False
|
|
96
|
+
live_preview: bool = False
|
|
97
|
+
include_in_pdf: bool = False
|
|
98
|
+
save_allowed: bool = False
|
|
99
|
+
separate_feedback: bool = True
|
|
100
|
+
common_feedback_color: str = "#C4CDD5"
|
|
101
|
+
correct_feedback_color: str = "#22C55E"
|
|
102
|
+
correct_feedback_prefix: str = "Correct"
|
|
103
|
+
incorrect_feedback_color: str = "#ff5630"
|
|
104
|
+
incorrect_feedback_prefix: str = "Incorrect"
|
|
105
|
+
tests: list[Test] = field(default_factory=list)
|
|
106
|
+
cases: list[Case] = field(default_factory=list)
|
|
107
|
+
|
|
108
|
+
def _config(self) -> Optional[dict[str, Any]]:
|
|
109
|
+
"""The response type's settings, as Lambda Feedback writes them."""
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
def _grade_params(self) -> Optional[dict[str, Any]]:
|
|
113
|
+
"""The parameters sent to the evaluation function."""
|
|
114
|
+
return self.grade_params
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass(kw_only=True)
|
|
118
|
+
class _Maths(ResponseArea):
|
|
119
|
+
answer: str = ""
|
|
120
|
+
allow_handwrite: bool = True
|
|
121
|
+
allow_photo: bool = True
|
|
122
|
+
enable_refinement: bool = True
|
|
123
|
+
|
|
124
|
+
def _config(self) -> Optional[dict[str, Any]]:
|
|
125
|
+
return {
|
|
126
|
+
"allowHandwrite": self.allow_handwrite,
|
|
127
|
+
"allowPhoto": self.allow_photo,
|
|
128
|
+
"enableRefinement": self.enable_refinement,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass(kw_only=True)
|
|
133
|
+
class MathSingleLine(_Maths):
|
|
134
|
+
"""A single symbolic expression."""
|
|
135
|
+
|
|
136
|
+
response_type: ClassVar[str] = "MATH_SINGLE_LINE"
|
|
137
|
+
evaluation_function: str = "compareExpressions"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass(kw_only=True)
|
|
141
|
+
class MathMultiLines(_Maths):
|
|
142
|
+
"""Multiple lines of mathematical work."""
|
|
143
|
+
|
|
144
|
+
response_type: ClassVar[str] = "MATH_MULTI_LINES"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass(kw_only=True)
|
|
148
|
+
class Number(ResponseArea):
|
|
149
|
+
"""A number."""
|
|
150
|
+
|
|
151
|
+
response_type: ClassVar[str] = "NUMBER"
|
|
152
|
+
answer: Optional[Union[int, float]] = None
|
|
153
|
+
evaluation_function: str = "isSimilar"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass(kw_only=True)
|
|
157
|
+
class NumericUnits(ResponseArea):
|
|
158
|
+
"""A number with units, e.g. ``30 N``."""
|
|
159
|
+
|
|
160
|
+
response_type: ClassVar[str] = "NUMERIC_UNITS"
|
|
161
|
+
answer: str = ""
|
|
162
|
+
evaluation_function: str = "compareExpressions"
|
|
163
|
+
|
|
164
|
+
def _grade_params(self) -> Optional[dict[str, Any]]:
|
|
165
|
+
# compareExpressions compares units only when told to.
|
|
166
|
+
return {"physical_quantity": True, **(self.grade_params or {})}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@dataclass(kw_only=True)
|
|
170
|
+
class Text(ResponseArea):
|
|
171
|
+
"""A short piece of text."""
|
|
172
|
+
|
|
173
|
+
response_type: ClassVar[str] = "TEXT"
|
|
174
|
+
answer: str = ""
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass(kw_only=True)
|
|
178
|
+
class Boolean(ResponseArea):
|
|
179
|
+
"""True or false."""
|
|
180
|
+
|
|
181
|
+
response_type: ClassVar[str] = "BOOLEAN"
|
|
182
|
+
answer: bool = False
|
|
183
|
+
evaluation_function: str = "isExactEqual"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@dataclass(kw_only=True)
|
|
187
|
+
class MultipleChoice(ResponseArea):
|
|
188
|
+
"""A choice among ``options``; ``answer`` marks each option correct or not."""
|
|
189
|
+
|
|
190
|
+
response_type: ClassVar[str] = "MULTIPLE_CHOICE"
|
|
191
|
+
answer: list[bool] = field(default_factory=list)
|
|
192
|
+
evaluation_function: str = "arrayEqual"
|
|
193
|
+
options: list[str] = field(default_factory=list)
|
|
194
|
+
single: bool = True
|
|
195
|
+
randomise: bool = False
|
|
196
|
+
|
|
197
|
+
def _config(self) -> Optional[dict[str, Any]]:
|
|
198
|
+
return {
|
|
199
|
+
"options": self.options,
|
|
200
|
+
"randomise": self.randomise,
|
|
201
|
+
"single": self.single,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@dataclass(kw_only=True)
|
|
206
|
+
class Table(ResponseArea):
|
|
207
|
+
"""A grid of cells, one row per row name and one column per column name."""
|
|
208
|
+
|
|
209
|
+
response_type: ClassVar[str] = "TABLE"
|
|
210
|
+
answer: list[list[str]] = field(default_factory=list)
|
|
211
|
+
evaluation_function: str = "arraySymbolicEqual"
|
|
212
|
+
row_names: list[str] = field(default_factory=list)
|
|
213
|
+
col_names: list[str] = field(default_factory=list)
|
|
214
|
+
|
|
215
|
+
def _config(self) -> Optional[dict[str, Any]]:
|
|
216
|
+
return {
|
|
217
|
+
"rows": len(self.row_names),
|
|
218
|
+
"cols": len(self.col_names),
|
|
219
|
+
"rowNames": self.row_names,
|
|
220
|
+
"colNames": self.col_names,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@dataclass(kw_only=True)
|
|
225
|
+
class Matrix(ResponseArea):
|
|
226
|
+
"""A table, with parentheses displayed around it, with the shape of ``answer``."""
|
|
227
|
+
|
|
228
|
+
response_type: ClassVar[str] = "MATRIX"
|
|
229
|
+
answer: list[list[str]] = field(default_factory=list)
|
|
230
|
+
evaluation_function: str = "arraySymbolicEqual"
|
|
231
|
+
|
|
232
|
+
def _config(self) -> Optional[dict[str, Any]]:
|
|
233
|
+
return {
|
|
234
|
+
"rows": len(self.answer),
|
|
235
|
+
"cols": len(self.answer[0]) if self.answer else 0,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@dataclass(kw_only=True)
|
|
240
|
+
class Code(ResponseArea):
|
|
241
|
+
"""A monaco code editor."""
|
|
242
|
+
|
|
243
|
+
response_type: ClassVar[str] = "CODE"
|
|
244
|
+
answer: str = ""
|
|
245
|
+
evaluation_function: str = "evaluatePython"
|
|
246
|
+
language: str = "python"
|
|
247
|
+
|
|
248
|
+
def _config(self) -> Optional[dict[str, Any]]:
|
|
249
|
+
return {"language": self.language}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@dataclass(kw_only=True)
|
|
253
|
+
class Essay(ResponseArea):
|
|
254
|
+
"""Large text box."""
|
|
255
|
+
|
|
256
|
+
response_type: ClassVar[str] = "ESSAY"
|
|
257
|
+
answer: Union[str, dict[str, str]] = ""
|
|
258
|
+
allow_draw: bool = False
|
|
259
|
+
allow_scan: bool = False
|
|
260
|
+
repeat_for_subjects: bool = False
|
|
261
|
+
allow_empty_submission: bool = False
|
|
262
|
+
|
|
263
|
+
def _config(self) -> Optional[dict[str, Any]]:
|
|
264
|
+
return {
|
|
265
|
+
"allowDraw": self.allow_draw,
|
|
266
|
+
"allowScan": self.allow_scan,
|
|
267
|
+
"repeatForSubjects": self.repeat_for_subjects,
|
|
268
|
+
"allowEmptySubmission": self.allow_empty_submission,
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@dataclass(kw_only=True)
|
|
273
|
+
class Likert(ResponseArea):
|
|
274
|
+
"""A multiple choice with Likert scales."""
|
|
275
|
+
|
|
276
|
+
response_type: ClassVar[str] = "LIKERT"
|
|
277
|
+
answer: Union[
|
|
278
|
+
list[Optional[Union[int, float]]],
|
|
279
|
+
dict[str, list[Optional[Union[int, float]]]],
|
|
280
|
+
None,
|
|
281
|
+
] = None
|
|
282
|
+
statements: list[str] = field(default_factory=list)
|
|
283
|
+
options: Optional[list[str]] = None
|
|
284
|
+
repeat_for_subjects: bool = False
|
|
285
|
+
allow_empty_submission: bool = False
|
|
286
|
+
|
|
287
|
+
def _config(self) -> Optional[dict[str, Any]]:
|
|
288
|
+
config: dict[str, Any] = {
|
|
289
|
+
"statements": self.statements,
|
|
290
|
+
"repeatForSubjects": self.repeat_for_subjects,
|
|
291
|
+
"allowEmptySubmission": self.allow_empty_submission,
|
|
292
|
+
}
|
|
293
|
+
if self.options is not None:
|
|
294
|
+
config["options"] = self.options
|
|
295
|
+
return config
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@dataclass(kw_only=True)
|
|
299
|
+
class Images(ResponseArea):
|
|
300
|
+
"""Upload images."""
|
|
301
|
+
|
|
302
|
+
response_type: ClassVar[str] = "IMAGES"
|
|
303
|
+
max_images: int = 3
|
|
304
|
+
allowed_types: list[str] = field(
|
|
305
|
+
default_factory=lambda: ["image/jpg", "image/jpeg", "image/png"]
|
|
306
|
+
)
|
|
307
|
+
max_size_mb: int = 10
|
|
308
|
+
resize_max_side: int = 0
|
|
309
|
+
"""The longest side images are resized to; 0 leaves them as they are."""
|
|
310
|
+
|
|
311
|
+
def _config(self) -> Optional[dict[str, Any]]:
|
|
312
|
+
return {
|
|
313
|
+
"maxImages": self.max_images,
|
|
314
|
+
"allowedTypes": self.allowed_types,
|
|
315
|
+
"maxSizeMb": self.max_size_mb,
|
|
316
|
+
"resizeMaxSide": self.resize_max_side,
|
|
317
|
+
}
|