Example #1
0
def convert_bulk_feedback(stdout, stderr):
    from course.models import BULK_FEEDBACK_FILENAME_KEY, update_bulk_feedback
    fbf_pk_qset = (FlowPageBulkFeedback.objects.annotate(
        bf_len=Length("bulk_feedback")).filter(
            ~Q(bulk_feedback__contains=BULK_FEEDBACK_FILENAME_KEY)
            & Q(bf_len__gte=256)).values("pk"))

    fbf_pk_qset_iterator = iter(fbf_pk_qset)

    quit = False
    total_count = 0
    while not quit:
        with transaction.atomic():
            for i in range(200):
                try:
                    fbf_pk = next(fbf_pk_qset_iterator)
                except StopIteration:
                    quit = True
                    break
                fbf = (FlowPageBulkFeedback.objects.select_related(
                    "page_data", "page_data__flow_session",
                    "page_data__flow_session__participation",
                    "page_data__flow_session__participation__user").get(
                        pk=fbf_pk["pk"]))

                update_bulk_feedback(fbf.page_data, fbf.grade,
                                     fbf.bulk_feedback)
                total_count += 1

        stdout.write("converted %d items of bulk feedback..." % total_count)

    stdout.write("done with bulk feedback!")
Example #2
0
def _save_grade(fpctx, flow_session, most_recent_grade, bulk_feedback_json,
        now_datetime):
    most_recent_grade.save()

    update_bulk_feedback(
            fpctx.prev_answer_visit.page_data,
            most_recent_grade,
            bulk_feedback_json)

    grading_rule = get_session_grading_rule(
            flow_session, flow_session.participation.role,
            fpctx.flow_desc, now_datetime)

    from course.flow import grade_flow_session
    grade_flow_session(fpctx, flow_session, grading_rule)
Example #3
0
def _save_grade(
        fpctx,  # type: FlowPageContext
        flow_session,  # type: FlowSession
        most_recent_grade,  # type: FlowPageVisitGrade
        bulk_feedback_json,  # type: Any
        now_datetime,  # type: datetime.datetime
):
    # type: (...) -> None
    most_recent_grade.save()

    update_bulk_feedback(fpctx.prev_answer_visit.page_data, most_recent_grade,
                         bulk_feedback_json)

    grading_rule = get_session_grading_rule(flow_session, fpctx.flow_desc,
                                            now_datetime)

    from course.flow import grade_flow_session
    grade_flow_session(fpctx, flow_session, grading_rule)
Example #4
0
def grade_flow_page(pctx, flow_session_id, page_ordinal):
    page_ordinal = int(page_ordinal)

    if pctx.role not in [
            participation_role.instructor,
            participation_role.teaching_assistant]:
        raise PermissionDenied(
                _("must be instructor or TA to view grades"))

    flow_session = get_object_or_404(FlowSession, id=int(flow_session_id))

    if flow_session.course.pk != pctx.course.pk:
        raise SuspiciousOperation(
                _("Flow session not part of specified course"))
    if flow_session.participation is None:
        raise SuspiciousOperation(
                _("Cannot grade anonymous session"))

    fpctx = FlowPageContext(pctx.repo, pctx.course, flow_session.flow_id,
            page_ordinal, participation=flow_session.participation,
            flow_session=flow_session, request=pctx.request)

    if fpctx.page_desc is None:
        raise http.Http404()

    # {{{ enable flow session zapping

    all_flow_sessions = list(FlowSession.objects
            .filter(
                course=pctx.course,
                flow_id=flow_session.flow_id,
                participation__isnull=False,
                in_progress=flow_session.in_progress)
            .order_by(
                "participation__user__last_name",
                "start_time"))

    next_flow_session_id = None
    prev_flow_session_id = None
    for i, other_flow_session in enumerate(all_flow_sessions):
        if other_flow_session.pk == flow_session.pk:
            if i > 0:
                prev_flow_session_id = all_flow_sessions[i-1].id
            if i + 1 < len(all_flow_sessions):
                next_flow_session_id = all_flow_sessions[i+1].id

    # }}}

    # {{{ reproduce student view

    form = None
    feedback = None
    answer_data = None
    grade_data = None
    most_recent_grade = None

    if fpctx.page.expects_answer():
        if fpctx.prev_answer_visit is not None:
            answer_data = fpctx.prev_answer_visit.answer

            most_recent_grade = fpctx.prev_answer_visit.get_most_recent_grade()
            if most_recent_grade is not None:
                feedback = get_feedback_for_grade(most_recent_grade)
                grade_data = most_recent_grade.grade_data
            else:
                feedback = None
                grade_data = None

        else:
            feedback = None

        from course.page.base import PageBehavior
        page_behavior = PageBehavior(
                show_correctness=True,
                show_answer=False,
                may_change_answer=False)

        form = fpctx.page.make_form(
                fpctx.page_context, fpctx.page_data.data,
                answer_data, page_behavior)

    if form is not None:
        form_html = fpctx.page.form_to_html(
                pctx.request, fpctx.page_context, form, answer_data)
    else:
        form_html = None

    # }}}

    # {{{ grading form

    if (fpctx.page.expects_answer()
            and fpctx.page.is_answer_gradable()
            and fpctx.prev_answer_visit is not None
            and not flow_session.in_progress):
        request = pctx.request
        if pctx.request.method == "POST":
            grading_form = fpctx.page.post_grading_form(
                    fpctx.page_context, fpctx.page_data, grade_data,
                    request.POST, request.FILES)
            if grading_form.is_valid():
                grade_data = fpctx.page.update_grade_data_from_grading_form(
                        fpctx.page_context, fpctx.page_data, grade_data,
                        grading_form, request.FILES)

                with translation.override(settings.RELATE_ADMIN_EMAIL_LOCALE):
                    feedback = fpctx.page.grade(
                            fpctx.page_context, fpctx.page_data,
                            answer_data, grade_data)

                if feedback is not None:
                    correctness = feedback.correctness
                else:
                    correctness = None

                if feedback is not None:
                    feedback_json, bulk_feedback_json = feedback.as_json()
                else:
                    feedback_json = bulk_feedback_json = None

                most_recent_grade = FlowPageVisitGrade(
                        visit=fpctx.prev_answer_visit,
                        grader=pctx.request.user,
                        graded_at_git_commit_sha=pctx.course_commit_sha,

                        grade_data=grade_data,

                        max_points=fpctx.page.max_points(fpctx.page_data),
                        correctness=correctness,
                        feedback=feedback_json)

                most_recent_grade.save()

                update_bulk_feedback(
                        fpctx.prev_answer_visit.page_data,
                        most_recent_grade,
                        bulk_feedback_json)

                grading_rule = get_session_grading_rule(
                        flow_session, flow_session.participation.role,
                        fpctx.flow_desc, get_now_or_fake_time(request))

                from course.flow import grade_flow_session
                grade_flow_session(fpctx, flow_session, grading_rule)

        else:
            grading_form = fpctx.page.make_grading_form(
                    fpctx.page_context, fpctx.page_data, grade_data)

    else:
        grading_form = None

    if grading_form is not None:
        from crispy_forms.layout import Submit
        grading_form.helper.form_class += " relate-grading-form"
        grading_form.helper.add_input(
                Submit(
                    "submit", _("Submit"),
                    accesskey="s",
                    css_class="relate-grading-save-button"))

        grading_form_html = fpctx.page.grading_form_to_html(
                pctx.request, fpctx.page_context, grading_form, grade_data)

    else:
        grading_form_html = None

    # }}}

    # {{{ compute points_awarded

    max_points = None
    points_awarded = None
    if (fpctx.page.expects_answer()
            and fpctx.page.is_answer_gradable()):
        max_points = fpctx.page.max_points(fpctx.page_data)
        if feedback is not None and feedback.correctness is not None:
            points_awarded = max_points * feedback.correctness

    # }}}

    grading_rule = get_session_grading_rule(
            flow_session, flow_session.participation.role,
            fpctx.flow_desc, get_now_or_fake_time(pctx.request))

    if grading_rule.grade_identifier is not None:
        grading_opportunity = get_flow_grading_opportunity(
                pctx.course, flow_session.flow_id, fpctx.flow_desc,
                grading_rule)
    else:
        grading_opportunity = None

    return render_course_page(
            pctx,
            "course/grade-flow-page.html",
            {
                "flow_identifier": fpctx.flow_id,
                "flow_session": flow_session,
                "flow_desc": fpctx.flow_desc,
                "ordinal": fpctx.ordinal,
                "page_data": fpctx.page_data,

                "body": fpctx.page.body(
                    fpctx.page_context, fpctx.page_data.data),
                "form": form,
                "form_html": form_html,
                "feedback": feedback,
                "max_points": max_points,
                "points_awarded": points_awarded,
                "most_recent_grade": most_recent_grade,

                "grading_opportunity": grading_opportunity,

                "prev_flow_session_id": prev_flow_session_id,
                "next_flow_session_id": next_flow_session_id,

                "grading_form": grading_form,
                "grading_form_html": grading_form_html,
            })
Example #5
0
def view_flow_page(pctx, flow_session_id, ordinal):
    request = pctx.request

    ordinal = int(ordinal)

    flow_session_id = int(flow_session_id)
    flow_session = get_and_check_flow_session(pctx, flow_session_id)
    flow_id = flow_session.flow_id

    if flow_session is None:
        messages.add_message(
            request,
            messages.WARNING,
            _("No in-progress session record found for this flow. " "Redirected to flow start page."),
        )

        return redirect("relate-view_start_flow", pctx.course.identifier, flow_id)

    try:
        fpctx = FlowPageContext(
            pctx.repo, pctx.course, flow_id, ordinal, participation=pctx.participation, flow_session=flow_session
        )
    except PageOrdinalOutOfRange:
        return redirect("relate-view_flow_page", pctx.course.identifier, flow_session.id, flow_session.page_count - 1)

    access_rule = get_session_access_rule(
        flow_session, pctx.role, fpctx.flow_desc, get_now_or_fake_time(request), pctx.remote_address
    )
    permissions = fpctx.page.get_modified_permissions_for_page(access_rule.permissions)

    if access_rule.message:
        messages.add_message(request, messages.INFO, access_rule.message)

    page_context = fpctx.page_context
    page_data = fpctx.page_data
    answer_data = None
    grade_data = None

    if flow_permission.view not in permissions:
        raise PermissionDenied(_("not allowed to view flow"))

    if request.method == "POST":
        if "finish" in request.POST:
            return redirect("relate-finish_flow_session_view", pctx.course.identifier, flow_session_id)
        else:
            submission_allowed = True

            # reject answer update if permission not present
            if flow_permission.submit_answer not in permissions:
                messages.add_message(request, messages.ERROR, _("Answer submission not allowed."))
                submission_allowed = False

            # reject if previous answer was final
            if (
                fpctx.prev_answer_visit is not None
                and fpctx.prev_answer_visit.is_submitted_answer
                and flow_permission.change_answer not in permissions
            ):
                messages.add_message(request, messages.ERROR, _("Already have final answer."))
                submission_allowed = False

            form = fpctx.page.post_form(
                fpctx.page_context, fpctx.page_data.data, post_data=request.POST, files_data=request.FILES
            )

            pressed_button = get_pressed_button(form)

            if submission_allowed and form.is_valid():
                # {{{ form validated, process answer

                messages.add_message(request, messages.INFO, _("Answer saved."))

                page_visit = FlowPageVisit()
                page_visit.flow_session = flow_session
                page_visit.page_data = fpctx.page_data
                page_visit.remote_address = request.META["REMOTE_ADDR"]

                answer_data = page_visit.answer = fpctx.page.answer_data(
                    fpctx.page_context, fpctx.page_data.data, form, request.FILES
                )
                page_visit.is_submitted_answer = pressed_button == "submit"
                page_visit.save()

                answer_was_graded = page_visit.is_submitted_answer
                may_change_answer = not answer_was_graded or flow_permission.change_answer in permissions

                if fpctx.page.is_answer_gradable():
                    feedback = fpctx.page.grade(page_context, page_data.data, page_visit.answer, grade_data=None)

                    if page_visit.is_submitted_answer:
                        grade = FlowPageVisitGrade()
                        grade.visit = page_visit
                        grade.max_points = fpctx.page.max_points(page_data.data)
                        grade.graded_at_git_commit_sha = pctx.course_commit_sha

                        bulk_feedback_json = None
                        if feedback is not None:
                            grade.correctness = feedback.correctness
                            grade.feedback, bulk_feedback_json = feedback.as_json()

                        grade.save()

                        update_bulk_feedback(page_data, grade, bulk_feedback_json)

                        del grade
                else:
                    feedback = None

                if pressed_button == "save_and_next" and not will_receive_feedback(permissions):
                    return redirect("relate-view_flow_page", pctx.course.identifier, flow_session_id, fpctx.ordinal + 1)
                elif pressed_button == "save_and_finish" and not will_receive_feedback(permissions):
                    return redirect("relate-finish_flow_session_view", pctx.course.identifier, flow_session_id)
                else:
                    form = fpctx.page.make_form(page_context, page_data.data, page_visit.answer, not may_change_answer)

                    # continue at common flow page generation below

                # }}}

                del page_visit

            else:
                # form did not validate
                create_flow_page_visit(request, flow_session, fpctx.page_data)

                answer_was_graded = False
                may_change_answer = True
                # because we were allowed this far in by the check above

                feedback = None

                # continue at common flow page generation below

    else:
        create_flow_page_visit(request, flow_session, fpctx.page_data)

        if fpctx.prev_answer_visit is not None:
            answer_was_graded = fpctx.prev_answer_visit.is_submitted_answer
        else:
            answer_was_graded = False

        may_change_answer = (
            (not answer_was_graded or (flow_permission.change_answer in permissions))
            # can happen if no answer was ever saved
            and flow_session.in_progress
            and (flow_permission.submit_answer in permissions)
        )

        if fpctx.page.expects_answer():
            if fpctx.prev_answer_visit is not None:
                answer_data = fpctx.prev_answer_visit.answer

                most_recent_grade = fpctx.prev_answer_visit.get_most_recent_grade()
                if most_recent_grade is not None:
                    feedback = get_feedback_for_grade(most_recent_grade)
                    grade_data = most_recent_grade.grade_data
                else:
                    feedback = None
                    grade_data = None

            else:
                feedback = None

            form = fpctx.page.make_form(page_context, page_data.data, answer_data, not may_change_answer)
        else:
            form = None
            feedback = None

    # start common flow page generation

    # defined at this point:
    # form, form_html, may_change_answer, answer_was_graded, feedback

    if form is not None and may_change_answer:
        form = add_buttons_to_form(form, fpctx, flow_session, permissions)

    show_correctness = None
    show_answer = None

    shown_feedback = None

    if fpctx.page.expects_answer() and answer_was_graded:
        show_correctness = flow_permission.see_correctness in permissions

        show_answer = flow_permission.see_answer_after_submission in permissions

        if show_correctness or show_answer:
            shown_feedback = feedback

    elif fpctx.page.expects_answer() and not answer_was_graded:
        # Don't show answer yet
        show_answer = flow_permission.see_answer_before_submission in permissions
    else:
        show_answer = (
            flow_permission.see_answer_before_submission in permissions
            or flow_permission.see_answer_after_submission in permissions
        )

    title = fpctx.page.title(page_context, page_data.data)
    body = fpctx.page.body(page_context, page_data.data)

    if show_answer:
        correct_answer = fpctx.page.correct_answer(page_context, page_data.data, answer_data, grade_data)
    else:
        correct_answer = None

    # {{{ render flow page

    if form is not None:
        form_html = fpctx.page.form_to_html(pctx.request, page_context, form, answer_data)
    else:
        form_html = None

    expiration_mode_choices = []

    for key, descr in FLOW_SESSION_EXPIRATION_MODE_CHOICES:
        if is_expiration_mode_allowed(key, permissions):
            expiration_mode_choices.append((key, descr))

    args = {
        "flow_identifier": fpctx.flow_id,
        "flow_desc": fpctx.flow_desc,
        "ordinal": fpctx.ordinal,
        "page_data": fpctx.page_data,
        "percentage": int(100 * (fpctx.ordinal + 1) / flow_session.page_count),
        "flow_session": flow_session,
        "page_numbers": zip(range(flow_session.page_count), range(1, flow_session.page_count + 1)),
        "title": title,
        "body": body,
        "form": form,
        "form_html": form_html,
        "feedback": shown_feedback,
        "correct_answer": correct_answer,
        "show_correctness": show_correctness,
        "may_change_answer": may_change_answer,
        "may_change_graded_answer": ((flow_permission.change_answer in permissions) and flow_session.in_progress),
        "will_receive_feedback": will_receive_feedback(permissions),
        "show_answer": show_answer,
        "expiration_mode_choices": expiration_mode_choices,
        "expiration_mode_choice_count": len(expiration_mode_choices),
        "expiration_mode": flow_session.expiration_mode,
    }

    if fpctx.page.expects_answer() and fpctx.page.is_answer_gradable():
        args["max_points"] = fpctx.page.max_points(fpctx.page_data)

    return render_course_page(pctx, "course/flow-page.html", args, allow_instant_flow_requests=False)
Example #6
0
def grade_page_visit(visit, visit_grade_model=FlowPageVisitGrade, grade_data=None, graded_at_git_commit_sha=None):
    if not visit.is_submitted_answer:
        raise RuntimeError(_("cannot grade ungraded answer"))

    flow_session = visit.flow_session
    course = flow_session.course
    page_data = visit.page_data

    most_recent_grade = visit.get_most_recent_grade()
    if most_recent_grade is not None and grade_data is None:
        grade_data = most_recent_grade.grade_data

    from course.content import (
        get_course_repo,
        get_course_commit_sha,
        get_flow_desc,
        get_flow_page_desc,
        instantiate_flow_page,
    )

    repo = get_course_repo(course)

    course_commit_sha = get_course_commit_sha(course, flow_session.participation)

    flow_desc = get_flow_desc(repo, course, flow_session.flow_id, course_commit_sha)

    page_desc = get_flow_page_desc(flow_session.flow_id, flow_desc, page_data.group_id, page_data.page_id)

    page = instantiate_flow_page(
        location="flow '%s', group, '%s', page '%s'" % (flow_session.flow_id, page_data.group_id, page_data.page_id),
        repo=repo,
        page_desc=page_desc,
        commit_sha=course_commit_sha,
    )

    assert page.expects_answer()
    if not page.is_answer_gradable():
        return

    from course.page import PageContext

    grading_page_context = PageContext(
        course=course, repo=repo, commit_sha=course_commit_sha, flow_session=flow_session
    )

    answer_feedback = page.grade(grading_page_context, visit.page_data.data, visit.answer, grade_data=grade_data)

    grade = visit_grade_model()
    grade.visit = visit
    grade.grade_data = grade_data
    grade.max_points = page.max_points(visit.page_data)
    grade.graded_at_git_commit_sha = graded_at_git_commit_sha

    bulk_feedback_json = None
    if answer_feedback is not None:
        grade.correctness = answer_feedback.correctness
        grade.feedback, bulk_feedback_json = answer_feedback.as_json()

    grade.save()

    update_bulk_feedback(page_data, grade, bulk_feedback_json)
Example #7
0
def grade_flow_page(pctx, flow_session_id, page_ordinal):
    page_ordinal = int(page_ordinal)

    if pctx.role not in [
            participation_role.instructor,
            participation_role.teaching_assistant]:
        raise PermissionDenied(
                _("must be instructor or TA to view grades"))

    flow_session = get_object_or_404(FlowSession, id=int(flow_session_id))

    if flow_session.course.pk != pctx.course.pk:
        raise SuspiciousOperation(
                _("Flow session not part of specified course"))
    if flow_session.participation is None:
        raise SuspiciousOperation(
                _("Cannot grade anonymous session"))

    fpctx = FlowPageContext(pctx.repo, pctx.course, flow_session.flow_id,
            page_ordinal, participation=flow_session.participation,
            flow_session=flow_session)

    if fpctx.page_desc is None:
        raise http.Http404()

    # {{{ enable flow session zapping

    all_flow_sessions = list(FlowSession.objects
            .filter(
                course=pctx.course,
                flow_id=flow_session.flow_id,
                participation__isnull=False,
                in_progress=flow_session.in_progress)
            .order_by(
                "participation__user__last_name",
                "start_time"))

    next_flow_session_id = None
    prev_flow_session_id = None
    for i, other_flow_session in enumerate(all_flow_sessions):
        if other_flow_session.pk == flow_session.pk:
            if i > 0:
                prev_flow_session_id = all_flow_sessions[i-1].id
            if i + 1 < len(all_flow_sessions):
                next_flow_session_id = all_flow_sessions[i+1].id

    # }}}

    # {{{ reproduce student view

    form = None
    feedback = None
    answer_data = None
    grade_data = None
    most_recent_grade = None

    if fpctx.page.expects_answer():
        if fpctx.prev_answer_visit is not None:
            answer_data = fpctx.prev_answer_visit.answer

            most_recent_grade = fpctx.prev_answer_visit.get_most_recent_grade()
            if most_recent_grade is not None:
                feedback = get_feedback_for_grade(most_recent_grade)
                grade_data = most_recent_grade.grade_data
            else:
                feedback = None
                grade_data = None

        else:
            feedback = None

        from course.page.base import PageBehavior
        page_behavior = PageBehavior(
                show_correctness=True,
                show_answer=False,
                may_change_answer=False)

        form = fpctx.page.make_form(
                fpctx.page_context, fpctx.page_data.data,
                answer_data, page_behavior)

    if form is not None:
        form_html = fpctx.page.form_to_html(
                pctx.request, fpctx.page_context, form, answer_data)
    else:
        form_html = None

    # }}}

    # {{{ grading form

    if (fpctx.page.expects_answer()
            and fpctx.page.is_answer_gradable()
            and fpctx.prev_answer_visit is not None
            and not flow_session.in_progress):
        request = pctx.request
        if pctx.request.method == "POST":
            grading_form = fpctx.page.post_grading_form(
                    fpctx.page_context, fpctx.page_data, grade_data,
                    request.POST, request.FILES)
            if grading_form.is_valid():
                grade_data = fpctx.page.update_grade_data_from_grading_form(
                        fpctx.page_context, fpctx.page_data, grade_data,
                        grading_form, request.FILES)

                feedback = fpctx.page.grade(
                        fpctx.page_context, fpctx.page_data,
                        answer_data, grade_data)

                if feedback is not None:
                    correctness = feedback.correctness
                else:
                    correctness = None

                if feedback is not None:
                    feedback_json, bulk_feedback_json = feedback.as_json()
                else:
                    feedback_json = bulk_feedback_json = None

                most_recent_grade = FlowPageVisitGrade(
                        visit=fpctx.prev_answer_visit,
                        grader=pctx.request.user,
                        graded_at_git_commit_sha=pctx.course_commit_sha,

                        grade_data=grade_data,

                        max_points=fpctx.page.max_points(fpctx.page_data),
                        correctness=correctness,
                        feedback=feedback_json)

                most_recent_grade.save()

                update_bulk_feedback(
                        fpctx.prev_answer_visit.page_data,
                        most_recent_grade,
                        bulk_feedback_json)

                grading_rule = get_session_grading_rule(
                        flow_session, flow_session.participation.role,
                        fpctx.flow_desc, get_now_or_fake_time(request))

                from course.flow import grade_flow_session
                grade_flow_session(fpctx, flow_session, grading_rule)

        else:
            grading_form = fpctx.page.make_grading_form(
                    fpctx.page_context, fpctx.page_data, grade_data)

    else:
        grading_form = None

    if grading_form is not None:
        from crispy_forms.layout import Submit
        grading_form.helper.add_input(
                Submit(
                    "submit", _("Submit"),
                    accesskey="s",
                    css_class="col-lg-offset-2 relate-grading-save-button"))

        grading_form_html = fpctx.page.grading_form_to_html(
                pctx.request, fpctx.page_context, grading_form, grade_data)

    else:
        grading_form_html = None

    # }}}

    # {{{ compute points_awarded

    max_points = None
    points_awarded = None
    if (fpctx.page.expects_answer()
            and fpctx.page.is_answer_gradable()):
        max_points = fpctx.page.max_points(fpctx.page_data)
        if feedback is not None and feedback.correctness is not None:
            points_awarded = max_points * feedback.correctness

    # }}}

    grading_rule = get_session_grading_rule(
            flow_session, flow_session.participation.role,
            fpctx.flow_desc, get_now_or_fake_time(pctx.request))

    if grading_rule.grade_identifier is not None:
        grading_opportunity = get_flow_grading_opportunity(
                pctx.course, flow_session.flow_id, fpctx.flow_desc,
                grading_rule)
    else:
        grading_opportunity = None

    return render_course_page(
            pctx,
            "course/grade-flow-page.html",
            {
                "flow_identifier": fpctx.flow_id,
                "flow_session": flow_session,
                "flow_desc": fpctx.flow_desc,
                "ordinal": fpctx.ordinal,
                "page_data": fpctx.page_data,

                "body": fpctx.page.body(
                    fpctx.page_context, fpctx.page_data.data),
                "form": form,
                "form_html": form_html,
                "feedback": feedback,
                "max_points": max_points,
                "points_awarded": points_awarded,
                "most_recent_grade": most_recent_grade,

                "grading_opportunity": grading_opportunity,

                "prev_flow_session_id": prev_flow_session_id,
                "next_flow_session_id": next_flow_session_id,

                "grading_form": grading_form,
                "grading_form_html": grading_form_html,
            })