Newer
Older
super().__init__(*args)
self.point_value = point_value
self.fields["grade_percent"] = forms.FloatField(
min_value=0,
max_value=100 * MAX_EXTRA_CREDIT_FACTOR,
required=False,
# avoid unfortunate scroll wheel accidents reported by graders
widget=TextInputWithButtons(
[0, 10, 20, 25, 30, 40, 50, 60, 70, 75, 80, 90, 100]),
if point_value is not None and point_value != 0:
self.fields["grade_points"] = forms.FloatField(
min_value=0,
max_value=MAX_EXTRA_CREDIT_FACTOR*point_value,
help_text=_("Grade assigned, as points out of %.1f. "
"Fill out either this or 'grade percent'.")
% point_value,
required=False,
# avoid unfortunate scroll wheel accidents reported by graders
widget=TextInputWithButtons(
create_default_point_scale(point_value)),
from course.utils import JsLiteral, get_codemirror_widget
cm_widget, cm_help_text = get_codemirror_widget(
language_mode="markdown",
interaction_mode=editor_interaction_mode,
additional_keys={
"Ctrl-;":
JsLiteral("rlUtils.goToNextPointsField"),
"Shift-Ctrl-;":
JsLiteral("rlUtils.goToPreviousPointsField"),
self.fields["feedback_text"] = forms.CharField(
_("Feedback to be shown to student, using "
"<a href='http://documen.tician.de/"
"RELATE-flavored Markdown</a>. "
"See RELATE documentation for automatic computation of point "
"count from <tt>[pts:N/N]</tt> and <tt>[pts:N]</tt>. "
"to move between <tt>[pts:]</tt> fields. ")
+ cm_help_text),
label=_("Feedback text (Ctrl+Shift+F)"))
self.fields["rubric_text"] = forms.CharField(
widget=forms.HiddenInput(attrs={"value": rubric}),
initial=rubric,
required=False)
self.fields["notify"] = forms.BooleanField(
initial=False, required=False,
help_text=_("Checking this box and submitting the form "
"with a generic message containing the feedback text"),
label=_("Notify"))
self.fields["may_reply"] = forms.BooleanField(
initial=False, required=False,
help_text=_("Allow recipient to reply to this email?"),
label=_("May reply email to me"))
self.fields["released"] = forms.BooleanField(
initial=True, required=False,
help_text=_("Whether the grade and feedback are to "
"be shown to student. (If you would like to release "
"all grades at once, do not use this. Instead, use "
"the 'shown to students' checkbox for this 'grading "
"opportunity' in the grade book admin.)"),
self.fields["notes"] = forms.CharField(
widget=forms.Textarea(),
help_text=_("Internal notes, not shown to student"),
self.fields["notify_instructor"] = forms.BooleanField(
initial=False, required=False,
help_text=_("Checking this box and submitting the form "
"will notify the instructor "
"with a generic message containing the notes"),
label=_("Notify instructor"))
grade_percent = self.cleaned_data.get("grade_percent")
grade_points = self.cleaned_data.get("grade_points")
and grade_percent is not None
and grade_points is not None):
points_percent = 100*grade_points/self.point_value
direct_percent = grade_percent
if abs(points_percent - direct_percent) > 0.1:
raise FormValidationError(
_("Grade (percent) and Grade (points) "
super(StyledForm, self).clean()
def cleaned_percent(self):
if self.point_value is None:
return self.cleaned_data["grade_percent"]
if self.cleaned_data["grade_percent"] is not None:
candidate_percentages.append(self.cleaned_data["grade_percent"])
if self.cleaned_data.get("grade_points") is not None:
candidate_percentages.append(
100 * self.cleaned_data["grade_points"] / self.point_value)
if len(candidate_percentages) == 2:
if abs(candidate_percentages[1] - candidate_percentages[0]) > 0.1:
raise RuntimeError(_("Grade (percent) and Grade (points) "
"disagree"))
return max(candidate_percentages)
class PageBaseWithHumanTextFeedback(PageBase):
"""
.. automethod:: human_feedback_point_value
Supports automatic computation of point values from textual feedback.
See :ref:`points-from-feedback`.
grade_data_attrs = ["released", "grade_percent", "feedback_text", "notes"]
def required_attrs(self) -> AttrSpec:
return (*super().required_attrs(), ("rubric", "markup"))
def human_feedback_point_value(self,
page_context: PageContext,
page_data: Any
) -> float | None:
"""Subclasses can override this to make the point value of the human
feedback known, which will enable grade entry in points.
def make_grading_form(
self,
page_context: PageContext,
page_data: Any,
grade_data: Any,
) -> StyledForm | None:
human_feedback_point_value = self.human_feedback_point_value(
page_context, page_data)
editor_interaction_mode = get_editor_interaction_mode(page_context)
for k in self.grade_data_attrs:
form_data[k] = grade_data[k]
return HumanTextFeedbackForm(human_feedback_point_value, form_data,
editor_interaction_mode=editor_interaction_mode,
rubric=self.page_desc.rubric)
else:
return HumanTextFeedbackForm(human_feedback_point_value,
editor_interaction_mode=editor_interaction_mode,
rubric=self.page_desc.rubric)
def post_grading_form(
self,
page_context: PageContext,
page_data: Any,
grade_data: Any,
post_data: Any,
files_data: Any,
) -> StyledForm:
human_feedback_point_value = self.human_feedback_point_value(
page_context, page_data)
editor_interaction_mode = get_editor_interaction_mode(page_context)
human_feedback_point_value, post_data, files_data,
editor_interaction_mode=editor_interaction_mode,
rubric=self.page_desc.rubric)
def update_grade_data_from_grading_form_v2(
self,
request: django.http.HttpRequest,
page_context: PageContext,
page_data: Any,
grade_data: Any,
grading_form: Any,
files_data: Any
):
if grade_data is None:
grade_data = {}
for k in self.grade_data_attrs:
if k == "grade_percent":
grade_data[k] = grading_form.cleaned_percent()
else:
grade_data[k] = grading_form.cleaned_data[k]
if grading_form.cleaned_data["notify"] and page_context.flow_session:
from course.utils import LanguageOverride
with LanguageOverride(page_context.course):
from course.utils import will_use_masked_profile_for_email
assert request.user.is_authenticated
assert page_context.flow_session.participation is not None
staff_email = [page_context.course.notify_email, request.user.email]
message = render_email_template("course/grade-notify.txt", {
"page_title": self.title(page_context, page_data),
"course": page_context.course,
"participation": page_context.flow_session.participation,
"feedback_text": grade_data["feedback_text"],
"flow_session": page_context.flow_session,
"review_uri": page_context.page_uri,
"use_masked_profile":
will_use_masked_profile_for_email(staff_email)
from django.core.mail import EmailMessage
msg = EmailMessage(
string_concat("[%(identifier)s:%(flow_id)s] ",
_("New notification"))
% {"identifier": page_context.course.identifier,
"flow_id": page_context.flow_session.flow_id},
getattr(settings, "GRADER_FEEDBACK_EMAIL_FROM",
page_context.course.get_from_email()),
[page_context.flow_session.participation.user.email])
msg.bcc = [page_context.course.notify_email]
if grading_form.cleaned_data["may_reply"]:
msg.reply_to = [request.user.email]
if hasattr(settings, "GRADER_FEEDBACK_EMAIL_FROM"):
from relate.utils import get_outbound_mail_connection
msg.connection = get_outbound_mail_connection("grader_feedback")
msg.send()
if (grading_form.cleaned_data["notes"]
and grading_form.cleaned_data["notify_instructor"]
and page_context.flow_session):
from course.utils import LanguageOverride
with LanguageOverride(page_context.course):
from course.utils import will_use_masked_profile_for_email
assert request.user.is_authenticated
assert page_context.flow_session.user is not None
staff_email = [page_context.course.notify_email, request.user.email]
use_masked_profile = will_use_masked_profile_for_email(staff_email)
if use_masked_profile:
username = (
page_context.flow_session.user.get_masked_profile())
else:
username = (
page_context.flow_session.user.get_email_appellation())
message = render_email_template(
"course/grade-internal-notes-notify.txt",
{
"page_title": self.title(page_context, page_data),
"username": username,
"course": page_context.course,
"participation": page_context.flow_session.participation,
"notes_text": grade_data["notes"],
"flow_session": page_context.flow_session,
"review_uri": page_context.page_uri,
"sender": request.user,
})
from django.core.mail import EmailMessage
msg = EmailMessage(
string_concat("[%(identifier)s:%(flow_id)s] ",
_("Grading notes from %(ta)s"))
% {"identifier": page_context.course.identifier,
"flow_id": page_context.flow_session.flow_id,
"ta": request.user.get_full_name()
},
message,
getattr(settings, "GRADER_FEEDBACK_EMAIL_FROM",
page_context.course.get_from_email()),
[page_context.course.notify_email])
msg.bcc = [request.user.email]
msg.reply_to = [request.user.email]
if hasattr(settings, "GRADER_FEEDBACK_EMAIL_FROM"):
from relate.utils import get_outbound_mail_connection
msg.connection = get_outbound_mail_connection("grader_feedback")
return grade_data
def grading_form_to_html(self, request, page_context, grading_form, grade_data):
ctx = {
"form": grading_form,
"rubric": markup_to_html(page_context, self.page_desc.rubric)
}
from django.template.loader import render_to_string
return render_to_string(
Dong Zhuang
committed
"course/human-feedback-form.html", ctx, request)
page_context: PageContext,
page_data: Any,
answer_data: Any,
grade_data: Any,
) -> AnswerFeedback | None:
"""This method is appropriate if the grade consists *only* of the
feedback provided by humans. If more complicated/combined feedback
is desired, a subclass would likely override this.
"""
Andreas Klöckner
committed
if answer_data is None and grade_data is None:
return AnswerFeedback(correctness=0,
feedback=gettext_noop("No answer provided."))
if grade_data is None:
return None
if not grade_data["released"]:
return None
if (grade_data["grade_percent"] is not None
or grade_data["feedback_text"]):
if grade_data["grade_percent"] is not None:
correctness = grade_data["grade_percent"]/100
feedback_text = f"<p>{get_auto_feedback(correctness)}</p>"
else:
correctness = None
feedback_text = ""
if grade_data["feedback_text"]:
feedback_text += (
string_concat(
"<p>",
_("The following feedback was provided"),
":<p>")
+ markup_to_html(
page_context, grade_data["feedback_text"],
use_jinja=False))
return AnswerFeedback(
correctness=correctness,
feedback=feedback_text)
else:
return None
class PageBaseWithCorrectAnswer(PageBase):
def allowed_attrs(self) -> AttrSpec:
return (*super().allowed_attrs(), ("correct_answer", "markup"))
def correct_answer(
self,
page_context: PageContext,
page_data: Any,
answer_data: Any,
grade_data: Any,
) -> str | None:
if hasattr(self.page_desc, "correct_answer"):
return markup_to_html(page_context, self.page_desc.correct_answer)
else:
return None
# }}}
def get_editor_interaction_mode(page_context: PageContext) -> str:
if (page_context.request is not None
and not page_context.request.user.is_anonymous):
return page_context.request.user.editor_mode
elif (page_context.flow_session is not None
and page_context.flow_session.participation is not None):
return page_context.flow_session.participation.user.editor_mode
else:
return "default"