Newer
Older
def required_attrs(self):
return (*super().required_attrs(), ("answers", list))
def allowed_attrs(self):
return (*super().allowed_attrs(), ("answer_explanation", "markup"))
def get_validators(self):
return self.matchers
def grade(self, page_context, page_data, answer_data, grade_data):
if answer_data is None:
return AnswerFeedback(correctness=0,
feedback=gettext("No answer provided."))
answer = answer_data["answer"]
# Must start with 'None' to allow matcher to set feedback for zero
# correctness.
afb = None
Andreas Klöckner
committed
for matcher in self.matchers:
try:
matcher.validate(answer)
except forms.ValidationError:
continue
Andreas Klöckner
committed
matcher_afb = matcher.grade(answer)
if matcher_afb.correctness is not None:
if afb is None:
afb = matcher_afb
elif matcher_afb.correctness > afb.correctness:
afb = matcher_afb
if afb is None:
afb = AnswerFeedback(0)
Andreas Klöckner
committed
return afb
def correct_answer(self, page_context, page_data, answer_data, grade_data):
# FIXME: Could use 'best' match to answer
for matcher in self.matchers: # pragma: no branch
unspec_correct_answer_text = matcher.correct_answer_text()
if unspec_correct_answer_text is not None:
break
assert unspec_correct_answer_text
result = CORRECT_ANSWER_PATTERN % unspec_correct_answer_text
if hasattr(self.page_desc, "answer_explanation"):
result += markup_to_html(page_context, self.page_desc.answer_explanation)
return result
Andreas Klöckner
committed
def _is_case_sensitive(self):
return any(matcher.is_case_sensitive for matcher in self.matchers)
# }}}
# {{{ human-graded text question
class HumanGradedTextQuestion(TextQuestionBase, PageBaseWithValue,
PageBaseWithHumanTextFeedback, PageBaseWithCorrectAnswer):
"""
A page asking for a textual answer, with human-graded feedback.
Supports automatic computation of point values from textual feedback.
See :ref:`points-from-feedback`.
.. attribute:: id
|id-page-attr|
.. attribute:: type
``HumanGradedTextQuestion``
.. attribute:: is_optional_page
|is-optional-page-attr|
.. attribute:: access_rules
|access-rules-page-attr|
.. attribute:: title
|title-page-attr|
.. attribute:: value
|value-page-attr|
.. attribute:: prompt
The page's prompt, written in :ref:`markup`.
.. attribute:: widget
.. attribute:: initial_text
Text with which to prepopulate the input widget.
.. attribute:: validators
Optional.
TODO
.. attribute:: correct_answer
Optional.
Content that is revealed when answers are visible
(see :ref:`flow-permissions`). Written in :ref:`markup`.
.. attribute:: rubric
Required.
The grading guideline for this question, in :ref:`markup`.
"""
def __init__(self, vctx, location, page_desc):
super().__init__(vctx, location, page_desc)
self.validators = [
parse_validator(
vctx,
"%s, validator %d" % (location, i+1),
for i, answer in enumerate(
getattr(page_desc, "validators", []))]
def allowed_attrs(self):
return (*super().allowed_attrs(), ("validators", list))
def human_feedback_point_value(self, page_context, page_data):
return self.max_points(page_data)
def get_validators(self):
return self.validators
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
# {{{ rich text
class RichTextAnswerForm(StyledForm):
# FIXME: ugh, this should be a PageBase thing
show_save_button = False
def __init__(self, read_only: bool, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
from course.utils import ProseMirrorTextarea
self.fields["answer"] = forms.JSONField(
required=True,
widget=ProseMirrorTextarea(attrs={"readonly": read_only}),
help_text=ProseMirrorTextarea.math_help_text,
label=_("Answer"))
class HumanGradedRichTextQuestion(PageBaseWithValue, PageBaseWithTitle,
PageBaseWithHumanTextFeedback, PageBaseWithCorrectAnswer):
"""
A page asking for a textual answer, with human-graded feedback.
Supports automatic computation of point values from textual feedback.
See :ref:`points-from-feedback`.
.. attribute:: id
|id-page-attr|
.. attribute:: type
``HumanGradedRichTextQuestion``
.. attribute:: is_optional_page
|is-optional-page-attr|
.. attribute:: access_rules
|access-rules-page-attr|
.. attribute:: title
|title-page-attr|
.. attribute:: value
|value-page-attr|
.. attribute:: prompt
The page's prompt, written in :ref:`markup`.
.. attribute:: correct_answer
Optional.
Content that is revealed when answers are visible
(see :ref:`flow-permissions`). Written in :ref:`markup`.
.. attribute:: rubric
Required.
The grading guideline for this question, in :ref:`markup`.
"""
def required_attrs(self) -> AttrSpec:
return (*super().required_attrs(), ("prompt", "markup"))
def body(self, page_context: PageContext, page_data: Any) -> str:
return markup_to_html(page_context, self.page_desc.prompt)
def markup_body_for_title(self) -> str:
return self.page_desc.prompt
def human_feedback_point_value(self,
page_context: PageContext,
page_data: Any
) -> float:
return self.max_points(page_data)
def make_form(
self,
page_context: PageContext,
page_data: Any,
answer_data: Any,
page_behavior: Any,
) -> StyledForm:
kwargs = {}
if answer_data is not None:
from json import dumps
kwargs.update({"data": {"answer": dumps(answer_data["answer"])}})
return RichTextAnswerForm(
read_only=not page_behavior.may_change_answer,
**kwargs)
def process_form_post(
self,
page_context: PageContext,
page_data: Any,
post_data: Any,
files_data: Any,
page_behavior: PageBehavior,
) -> StyledForm:
return RichTextAnswerForm(
not page_behavior.may_change_answer,
post_data, files_data,
)
def answer_data(self, page_context, page_data, form, files_data):
data = form.cleaned_data["answer"]
assert isinstance(data, dict)
return {"answer": data}
def normalized_answer(
self,
page_context: PageContext,
page_data: Any,
answer_data: Any
) -> str | None:
if answer_data is None:
return None
from json import dumps
from django.utils.html import escape
return escape(dumps(answer_data["answer"]))
def normalized_bytes_answer(
self,
page_context: PageContext,
page_data: Any,
answer_data: Any,
) -> tuple[str, bytes] | None:
return None
# }}}