Skip to content
base.py 45.1 KiB
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,
                help_text=_("Grade assigned, in percent"),
                required=False,

                # avoid unfortunate scroll wheel accidents reported by graders
Andreas Klöckner's avatar
Andreas Klöckner committed
                widget=TextInputWithButtons(
                    [0, 10, 20, 25, 30, 40, 50, 60, 70, 75, 80, 90, 100]),
ifaint's avatar
ifaint committed
                label=_("Grade percent"))
        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,
ifaint's avatar
ifaint committed
                    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
Andreas Klöckner's avatar
Andreas Klöckner committed
                    widget=TextInputWithButtons(
                        create_default_point_scale(point_value)),
ifaint's avatar
ifaint committed
                    label=_("Grade points"))
        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(
                widget=cm_widget,
                required=False,
                help_text=mark_safe(
                    _("Feedback to be shown to student, using "
                    "<a href='http://documen.tician.de/"
                    "relate/content.html#relate-markup'>"
                    "RELATE-flavored Markdown</a>. "
                    "See RELATE documentation for automatic computation of point "
                    "count from <tt>[pts:N/N]</tt> and <tt>[pts:N]</tt>. "
                    "Use Ctrl-Semicolon/Ctrl-Shift-Semicolon "
                    "to move between <tt>[pts:]</tt> fields. ")
                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,
ifaint's avatar
ifaint committed
                help_text=_("Checking this box and submitting the form "
                "will notify the participant "
ifaint's avatar
ifaint committed
                "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.)"),
                label=_("Released"))
        self.fields["notes"] = forms.CharField(
                widget=forms.Textarea(),
                help_text=_("Internal notes, not shown to student"),
ifaint's avatar
ifaint committed
                required=False,
                label=_("Notes"))
        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"))

    def clean(self):
        grade_percent = self.cleaned_data.get("grade_percent")
        grade_points = self.cleaned_data.get("grade_points")
        if (self.point_value is not None
                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) "
ifaint's avatar
ifaint committed
                        "disagree"))

        super(StyledForm, self).clean()

    def cleaned_percent(self):
        if self.point_value is None:
            return self.cleaned_data["grade_percent"]
Dong Zhuang's avatar
Dong Zhuang committed
        else:
            candidate_percentages = []
Dong Zhuang's avatar
Dong Zhuang committed
            if self.cleaned_data["grade_percent"] is not None:
                candidate_percentages.append(self.cleaned_data["grade_percent"])
Dong Zhuang's avatar
Dong Zhuang committed
            if self.cleaned_data.get("grade_points") is not None:
                candidate_percentages.append(
                    100 * self.cleaned_data["grade_points"] / self.point_value)
Dong Zhuang's avatar
Dong Zhuang committed
            if not candidate_percentages:
Andreas Klöckner's avatar
Andreas Klöckner committed
                return None
Dong Zhuang's avatar
Dong Zhuang committed

            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:
Andreas Klöckner's avatar
Andreas Klöckner committed
        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)
        if grade_data is not None:
            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)
        return HumanTextFeedbackForm(
                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
Andreas Klöckner's avatar
Andreas Klöckner committed
                from relate.utils import render_email_template

                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"]
Andreas Klöckner's avatar
Andreas Klöckner committed
                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
Andreas Klöckner's avatar
Andreas Klöckner committed
                from relate.utils import render_email_template

                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(
            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.
        """

        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>"

            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:
Andreas Klöckner's avatar
Andreas Klöckner committed
        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):
Andreas Klöckner's avatar
Andreas Klöckner committed
        return page_context.flow_session.participation.user.editor_mode
# vim: foldmethod=marker