Skip to content
flow.py 102 KiB
Newer Older
    flow_session.completion_time = completion_time

    # }}}
    flow_session.in_progress = False
    return grade_flow_session(fctx, flow_session, grading_rule,
def expire_flow_session(
        fctx,  # type: FlowContext
        flow_session,  # type: FlowSession
        grading_rule,  # type: FlowSessionGradingRule
        now_datetime,  # type: datetime.datetime
        past_due_only=False,  # type:bool
        ):
    # type: (...) -> bool

    # This function does not need to be transactionally atomic.
    # It only does one atomic 'thing' in each execution path.

    if not flow_session.in_progress:
        raise RuntimeError(_("Can't expire a session that's not in progress"))
    if flow_session.participation is None:
        raise RuntimeError(_("Can't expire an anonymous flow session"))
    assert isinstance(grading_rule, FlowSessionGradingRule)

    if past_due_only:
        if grading_rule.due is None:
            return False
        elif now_datetime < grading_rule.due:
            return False
    adjust_flow_session_page_data(fctx.repo, flow_session,
            flow_session.course.identifier, fctx.flow_desc,
            respect_preview=False)
Andreas Klöckner's avatar
Andreas Klöckner committed
    if flow_session.expiration_mode == flow_session_expiration_mode.roll_over:
        session_start_rule = get_session_start_rule(
                flow_session.course, flow_session.participation,
                flow_session.flow_id, fctx.flow_desc, now_datetime,
        if not session_start_rule.may_start_new_session:
            # No new session allowed: finish.
Dong Zhuang's avatar
Dong Zhuang committed
            finish_flow_session(fctx, flow_session, grading_rule,
                                now_datetime=now_datetime, respect_preview=False)
            return True
            flow_session.access_rules_tag = session_start_rule.tag_session
            # {{{ FIXME: This is weird and should probably not exist.
            access_rule = get_session_access_rule(
                    flow_session, fctx.flow_desc, now_datetime)
            if session_start_rule.default_expiration_mode is not None:
                flow_session.expiration_mode = \
                        session_start_rule.default_expiration_mode

            elif not is_expiration_mode_allowed(
                    flow_session.expiration_mode, access_rule.permissions):
                flow_session.expiration_mode = flow_session_expiration_mode.end
Andreas Klöckner's avatar
Andreas Klöckner committed
    elif flow_session.expiration_mode == flow_session_expiration_mode.end:
Dong Zhuang's avatar
Dong Zhuang committed
        finish_flow_session(fctx, flow_session, grading_rule,
                            now_datetime=now_datetime, respect_preview=False)
        return True
                _("invalid expiration mode '%(mode)s' on flow session ID "
                    "mode": flow_session.expiration_mode,
                    "session_id": flow_session.id})
Dong Zhuang's avatar
Dong Zhuang committed
def get_flow_session_attempt_id(flow_session):
    # type: (FlowSession) -> Text
    return "flow-session-%d" % flow_session.id


def grade_flow_session(
        fctx,  # type: FlowContext
        flow_session,  # type: FlowSession
        grading_rule,  # type: FlowSessionGradingRule
        answer_visits=None,  # type: Optional[List[Optional[FlowPageVisit]]]
        ):
    # type: (...) -> GradeInfo

    """Updates the grade on an existing flow session and logs a
    grade change with the grade records subsystem.
    """

    if answer_visits is None:
        answer_visits = assemble_answer_visits(flow_session)

    grade_info = gather_grade_info(fctx, flow_session, grading_rule, answer_visits)
    if (points is not None
            and grading_rule.credit_percent is not None
            and grading_rule.credit_percent != 100):
Dong Zhuang's avatar
Dong Zhuang committed
                # Translators: grade flow: calculating grade.
                _("Counted at %(percent).1f%% of %(point).1f points") % {
                    "percent": grading_rule.credit_percent,
                    "point": points})
        points = points * grading_rule.credit_percent / 100
    flow_session.points = points
    flow_session.max_points = grade_info.max_points
    flow_session.append_comment(comment)
    # Need to save grade record even if no grade is available yet, because
    # a grade record may *already* be saved, and that one might be mistaken
    # for the current one.
    if (grading_rule.grade_identifier
            and grading_rule.generates_grade
            and flow_session.participation is not None):
        from course.models import get_flow_grading_opportunity
        gopp = get_flow_grading_opportunity(
                flow_session.course, flow_session.flow_id, fctx.flow_desc,
Andreas Klöckner's avatar
Andreas Klöckner committed
                grading_rule.grade_identifier,
                grading_rule.grade_aggregation_strategy)

        from course.models import grade_state_change_types
        gchange = GradeChange()
        gchange.opportunity = gopp
        gchange.participation = flow_session.participation
        gchange.state = grade_state_change_types.graded
Dong Zhuang's avatar
Dong Zhuang committed
        gchange.attempt_id = get_flow_session_attempt_id(flow_session)
        gchange.points = points
        gchange.max_points = grade_info.max_points
        # creator left as NULL
        gchange.flow_session = flow_session
        gchange.comment = comment

        previous_grade_changes = list(GradeChange.objects
                .filter(
                    opportunity=gchange.opportunity,
                    participation=gchange.participation,
                    attempt_id=gchange.attempt_id,
                    flow_session=gchange.flow_session)
                .order_by("-grade_time")
                [:1])

        # only save if modified or no previous grades
        do_save = True
        if previous_grade_changes:
            previous_grade_change, = previous_grade_changes
            if (previous_grade_change.points == gchange.points
                    and previous_grade_change.max_points == gchange.max_points
Dong Zhuang's avatar
Dong Zhuang committed
                    and previous_grade_change.state == gchange.state
                    and previous_grade_change.comment == gchange.comment):
                do_save = False
        else:
            # no previous grade changes
            if points is None:
                do_save = False
def unsubmit_page(prev_answer_visit, now_datetime):
    # type: (FlowPageVisit, datetime.datetime) -> None

    prev_answer_visit.id = None
    prev_answer_visit.visit_time = now_datetime
    prev_answer_visit.remote_address = None
    prev_answer_visit.user = None
    prev_answer_visit.is_synthetic = True

    assert prev_answer_visit.is_submitted_answer
    prev_answer_visit.is_submitted_answer = False

    prev_answer_visit.save()


        now_datetime,  # type: datetime.datetime
        session,  # type: FlowSession
        force=False,  # type: bool
        suppress_log=False,  # type: bool
        unsubmit_pages=False,  # type: bool
        ):
    # type: (...) -> None
    with transaction.atomic():
        if session.in_progress:
            raise RuntimeError(
                    _("Cannot reopen a session that's already in progress"))
        if session.participation is None:
            raise RuntimeError(
                    _("Cannot reopen anonymous sessions"))

        session.in_progress = True
        session.points = None
        session.max_points = None

        if not suppress_log:
            session.append_comment(
                    _("Session reopened at %(now)s, previous completion time "
                    "was '%(complete_time)s'.") % {
                        "now": format_datetime_local(now_datetime),
                        "complete_time": format_datetime_local(
                            as_local_time(session.completion_time))
                        })
        session.completion_time = None
        session.save()
        if unsubmit_pages:
            answer_visits = assemble_answer_visits(session)
            for visit in answer_visits:
                if visit is not None:
                    unsubmit_page(visit, now_datetime)
def finish_flow_session_standalone(
        repo,  # type: Repo_ish
        course,  # type: Course
        session,  # type: FlowSession
        force_regrade=False,  # type: bool
        now_datetime=None,  # type: Optional[datetime.datetime]
        past_due_only=False,  # type: bool
        respect_preview=True,  # type:bool
    # Do not be tempted to call adjust_flow_session_page_data in here.
    # This function may be called from within a transaction.
    if now_datetime is None:
        from django.utils.timezone import now
        now_datetime_filled = now()
    else:
        now_datetime_filled = now_datetime
    fctx = FlowContext(repo, course, session.flow_id)
Andreas Klöckner's avatar
Andreas Klöckner committed
    grading_rule = get_session_grading_rule(session, fctx.flow_desc,
            now_datetime_filled)
Dong Zhuang's avatar
Dong Zhuang committed
    if past_due_only:
        if grading_rule.due is None:
            return False
        elif now_datetime_filled < grading_rule.due:
    finish_flow_session(fctx, session, grading_rule,
            now_datetime=now_datetime_filled,
            respect_preview=respect_preview)
def expire_flow_session_standalone(
        repo,  # type: Any
        course,  # type: Course
        session,  # type: FlowSession
        now_datetime,  # type: datetime.datetime
        past_due_only=False,  # type: bool
        ):
    # type: (...) -> bool
    assert session.participation is not None

    fctx = FlowContext(repo, course, session.flow_id)
    grading_rule = get_session_grading_rule(session, fctx.flow_desc, now_datetime)
    return expire_flow_session(fctx, session, grading_rule, now_datetime,
            past_due_only=past_due_only)
def regrade_session(
        repo,  # type: Repo_ish
        course,  # type: Course
        session,  # type: FlowSession
        ):
Andreas Klöckner's avatar
Andreas Klöckner committed
    # type: (...) -> None
    adjust_flow_session_page_data(repo, session, course.identifier,
            respect_preview=False)
    if session.in_progress:
Andreas Klöckner's avatar
Andreas Klöckner committed
            answer_visits = assemble_answer_visits(session)  # type: List[Optional[FlowPageVisit]]  # noqa
            for i in range(len(answer_visits)):
                answer_visit = answer_visits[i]
                if answer_visit is not None:
                    if answer_visit.get_most_recent_grade():
                        # Only make a new grade if there already is one.
                        grade_page_visit(answer_visit, respect_preview=False)
        prev_completion_time = session.completion_time

        with transaction.atomic():
            session.append_comment(
                    _("Session regraded at %(time)s.") % {
                        "time": format_datetime_local(now_datetime)
            reopen_session(now_datetime, session, force=True, suppress_log=True)
            finish_flow_session_standalone(
                    repo, course, session, force_regrade=True,
                    now_datetime=prev_completion_time,
                    respect_preview=False)

def recalculate_session_grade(repo, course, session):
Andreas Klöckner's avatar
Andreas Klöckner committed
    # type: (Repo_ish, Course, FlowSession) -> None

    """Only redoes the final grade determination without regrading
    individual pages.
    """

    if session.in_progress:
        raise RuntimeError(_("cannot recalculate grade on in-progress session"))

    prev_completion_time = session.completion_time

    adjust_flow_session_page_data(repo, session, course.identifier,
            respect_preview=False)
        session.append_comment(
                _("Session grade recomputed at %(time)s.") % {
                    "time": format_datetime_local(now_datetime)
        reopen_session(now_datetime, session, force=True, suppress_log=True)
        finish_flow_session_standalone(
                repo, course, session, force_regrade=False,
                now_datetime=prev_completion_time,
                respect_preview=False)
def lock_down_if_needed(
        request,  # type: http.HttpRequest
        permissions,  # type: FrozenSet[Text]
        flow_session,  # type: FlowSession
        ):
    # type: (...) -> None

    if flow_permission.lock_down_as_exam_session in permissions:
        request.session[
                "relate_session_locked_to_exam_flow_session_pk"] = \
                        flow_session.pk


def view_start_flow(pctx, flow_id):
    # type: (CoursePageContext, Text) -> http.HttpResponse
    fctx = FlowContext(pctx.repo, pctx.course, flow_id,
            participation=pctx.participation)

    if request.method == "POST":
        return post_start_flow(pctx, fctx, flow_id)
Dong Zhuang's avatar
Dong Zhuang committed
    login_exam_ticket = get_login_exam_ticket(pctx.request)
    now_datetime = get_now_or_fake_time(request)

    session_start_rule = get_session_start_rule(
            pctx.course, pctx.participation,
            flow_id, fctx.flow_desc, now_datetime,
            facilities=pctx.request.relate_facilities,
            login_exam_ticket=login_exam_ticket)

    if session_start_rule.may_list_existing_sessions:
        past_sessions = (FlowSession.objects
                .filter(
                    participation=pctx.participation,
                    flow_id=fctx.flow_id,
                    participation__isnull=False)
               .order_by("start_time"))

        from collections import namedtuple
        SessionProperties = namedtuple("SessionProperties",  # noqa
                ["may_view", "may_modify", "due", "grade_description",
                    "grade_shown"])

        past_sessions_and_properties = []
        for session in past_sessions:
            access_rule = get_session_access_rule(
                    session, fctx.flow_desc, now_datetime,
                    facilities=pctx.request.relate_facilities,
                    login_exam_ticket=login_exam_ticket)
            grading_rule = get_session_grading_rule(
                    session, fctx.flow_desc, now_datetime)

            session_properties = SessionProperties(
                    may_view=flow_permission.view in access_rule.permissions,
                    may_modify=(
                        flow_permission.submit_answer in access_rule.permissions
Andreas Klöckner's avatar
Andreas Klöckner committed
                        or flow_permission.end_session in access_rule.permissions
Dong Zhuang's avatar
Dong Zhuang committed
                        ),
                    due=grading_rule.due,
                    grade_description=grading_rule.description,
                    grade_shown=(
                        flow_permission.cannot_see_flow_result
                        not in access_rule.permissions))
            past_sessions_and_properties.append((session, session_properties))
    else:
        past_sessions_and_properties = []
Dong Zhuang's avatar
Dong Zhuang committed
    may_start = session_start_rule.may_start_new_session
    new_session_grading_rule = None
    start_may_decrease_grade = False
    grade_aggregation_strategy_descr = None
Dong Zhuang's avatar
Dong Zhuang committed
    if may_start:
        potential_session = FlowSession(
            course=pctx.course,
            participation=pctx.participation,
            flow_id=flow_id,
            in_progress=True,

            # default_expiration_mode ignored
            expiration_mode=flow_session_expiration_mode.end,

            access_rules_tag=session_start_rule.tag_session)

        new_session_grading_rule = get_session_grading_rule(
                potential_session, fctx.flow_desc, now_datetime)

        start_may_decrease_grade = (
                bool(past_sessions_and_properties)
Andreas Klöckner's avatar
Andreas Klöckner committed
                and new_session_grading_rule.grade_aggregation_strategy
                not in [
Andreas Klöckner's avatar
Andreas Klöckner committed
                    None,
                    grade_aggregation_strategy.max_grade,
                    grade_aggregation_strategy.use_earliest])

Dong Zhuang's avatar
Dong Zhuang committed
        grade_aggregation_strategy_descr = (
            dict(GRADE_AGGREGATION_STRATEGY_CHOICES).get(
                new_session_grading_rule.grade_aggregation_strategy))

    return render_course_page(pctx, "course/flow-start.html", {
        "flow_desc": fctx.flow_desc,
        "flow_identifier": flow_id,

        "now": now_datetime,
        "may_start": may_start,
        "new_session_grading_rule": new_session_grading_rule,
        "grade_aggregation_strategy_descr": grade_aggregation_strategy_descr,
        "start_may_decrease_grade": start_may_decrease_grade,
        "past_sessions_and_properties": past_sessions_and_properties,
        },
        allow_instant_flow_requests=False)

@retry_transaction_decorator(serializable=True)
def post_start_flow(pctx, fctx, flow_id):
    # type: (CoursePageContext, FlowContext, Text) -> http.HttpResponse

    now_datetime = get_now_or_fake_time(pctx.request)
    login_exam_ticket = get_login_exam_ticket(pctx.request)
    past_sessions = (FlowSession.objects
            .filter(
                participation=pctx.participation,
                flow_id=fctx.flow_id,
                participation__isnull=False)
           .order_by("start_time"))

    if past_sessions:
        latest_session = past_sessions.reverse()[0]

        cooldown_seconds = getattr(
            settings, "RELATE_SESSION_RESTART_COOLDOWN_SECONDS", 10)

        from datetime import timedelta
        if (
                timedelta(seconds=0)
                <= (now_datetime - latest_session.start_time)
Andreas Klöckner's avatar
Andreas Klöckner committed
                < timedelta(seconds=cooldown_seconds)):
            return redirect("relate-view_flow_page",
                pctx.course.identifier, latest_session.id, 0)

    session_start_rule = get_session_start_rule(
            pctx.course, pctx.participation,
            flow_id, fctx.flow_desc, now_datetime,
            facilities=pctx.request.relate_facilities,
            login_exam_ticket=login_exam_ticket)

    if not session_start_rule.may_start_new_session:
        raise PermissionDenied(_("new session not allowed"))

    flow_user = pctx.request.user
    if not flow_user.is_authenticated:
        flow_user = None

    session = start_flow(
            pctx.repo, pctx.course, pctx.participation,
            user=flow_user,
            flow_id=flow_id, flow_desc=fctx.flow_desc,
            session_start_rule=session_start_rule,
    access_rule = get_session_access_rule(
            session, fctx.flow_desc, now_datetime,
            facilities=pctx.request.relate_facilities,
            login_exam_ticket=login_exam_ticket)

    lock_down_if_needed(pctx.request, access_rule.permissions, session)

    return redirect("relate-view_flow_page",
            pctx.course.identifier, session.id, 0)

# {{{ view: resume flow

# The purpose of this interstitial redirection page is to set the exam
# lockdown flag upon resumption/review. Without this, the exam lockdown
# middleware will refuse access to flow pages in a locked-down facility.

@course_view
def view_resume_flow(pctx, flow_session_id):
    # type: (CoursePageContext, int) -> http.HttpResponse

    now_datetime = get_now_or_fake_time(pctx.request)

    flow_session = get_and_check_flow_session(pctx, int(flow_session_id))

    fctx = FlowContext(pctx.repo, pctx.course, flow_session.flow_id,
            participation=pctx.participation)

    login_exam_ticket = get_login_exam_ticket(pctx.request)

    access_rule = get_session_access_rule(
            flow_session, fctx.flow_desc, now_datetime,
            facilities=pctx.request.relate_facilities,
            login_exam_ticket=login_exam_ticket)

    lock_down_if_needed(pctx.request, access_rule.permissions,
            flow_session)

    return redirect("relate-view_flow_page",
            pctx.course.identifier, flow_session.id, 0)


# }}}


def get_and_check_flow_session(pctx, flow_session_id):
    # type: (CoursePageContext, int) -> FlowSession

        flow_session = (FlowSession.objects
                .select_related("participation")
                .get(id=flow_session_id))
    except ObjectDoesNotExist:
        raise http.Http404()
    if flow_session.course.pk != pctx.course.pk:
        raise http.Http404()
    my_session = (
            pctx.participation == flow_session.participation
Andreas Klöckner's avatar
Andreas Klöckner committed
            or (
                # anonymous by participation
                flow_session.participation is None
                and (
                    # We don't know whose (legacy)
                    # Truly anonymous sessions belong to everyone.
                    flow_session.user is None
                    or pctx.request.user == flow_session.user)))

    if not my_session:
Andreas Klöckner's avatar
Andreas Klöckner committed
        my_perms = pctx.permissions()

        from course.enrollment import get_participation_role_identifiers
        owner_roles = get_participation_role_identifiers(
                pctx.course, flow_session.participation)

        allowed = False
        for orole in owner_roles:
            for perm, arg in my_perms:
                if (
                        perm == pperm.view_flow_sessions_from_role
                        and arg == orole):
                    allowed = True
                    break
            if allowed:
                break

        if not allowed:
            raise PermissionDenied(_("may not view other people's sessions"))
def will_receive_feedback(permissions):
    # type: (FrozenSet[Text]) -> bool
    return (
            flow_permission.see_correctness in permissions
            or flow_permission.see_answer_after_submission in permissions)
Dong Zhuang's avatar
Dong Zhuang committed
def may_send_email_about_flow_page(flow_session, permissions):
    # type: (FlowSession, FrozenSet[Text]) -> bool
Dong Zhuang's avatar
Dong Zhuang committed
    return (
        flow_session.participation is not None
        and flow_session.user is not None
        and flow_permission.send_email_about_flow_page in permissions)
def get_page_behavior(
        page,  # type: PageBase
        permissions,  # type: FrozenSet[Text]
        session_in_progress,  # type: bool
        answer_was_graded,  # type: bool
        generates_grade,  # type: bool
        is_unenrolled_session,  # type: bool
        viewing_prior_version=False,  # type: bool
        ):
    # type: (...) -> PageBehavior
    show_correctness = False
    if page.expects_answer():
        if answer_was_graded:
            show_correctness = flow_permission.see_correctness in permissions
            show_answer = flow_permission.see_answer_after_submission in permissions
            if session_in_progress:
                # Don't reveal the answer if they can still change their mind
Andreas Klöckner's avatar
Andreas Klöckner committed
                show_answer = (show_answer
                        and flow_permission.change_answer not in permissions)
            show_answer = show_answer or (
                    flow_permission.see_answer_before_submission in permissions)
        else:
            # 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
Andreas Klöckner's avatar
Andreas Klöckner committed
                or flow_permission.see_answer_after_submission in permissions)
Andreas Klöckner's avatar
Andreas Klöckner committed
    may_change_answer = (
            not viewing_prior_version

            and (not answer_was_graded
Andreas Klöckner's avatar
Andreas Klöckner committed
                or (flow_permission.change_answer in permissions))

            # can happen if no answer was ever saved
            and session_in_progress

            and (flow_permission.submit_answer in permissions)

            and (generates_grade and not is_unenrolled_session
                or (not generates_grade))
            )

    from course.page.base import PageBehavior  # noqa
    return PageBehavior(
            show_correctness=show_correctness,
            show_answer=show_answer,
Andreas Klöckner's avatar
Andreas Klöckner committed
            may_change_answer=may_change_answer,
            )
def add_buttons_to_form(form, fpctx, flow_session, permissions):
    # type: (StyledForm, FlowPageContext, FlowSession, FrozenSet[Text]) -> StyledForm
    from crispy_forms.layout import Submit
    show_save_button = getattr(form, "show_save_button", True)
    if show_save_button:
        form.helper.add_input(
                Submit("save", _("Save answer"),
                    css_class="relate-save-button"))
    if will_receive_feedback(permissions):
        if flow_permission.change_answer in permissions:
            form.helper.add_input(
                    Submit(
                        "submit", _("Submit answer for feedback"),
                        accesskey="g",
                        css_class="relate-save-button relate-submit-button"))
                    Submit("submit", _("Submit final answer"),
                        css_class="relate-save-button relate-submit-button"))
    else:
        # Only offer 'save and move on' if student will receive no feedback
        if fpctx.page_data.page_ordinal + 1 < flow_session.page_count:
            form.helper.add_input(
                        mark_safe_lazy(
                            string_concat(
                                _("Save answer and move on"),
                                " &raquo;")),
Andreas Klöckner's avatar
Andreas Klöckner committed
                        css_class="relate-save-button"))
        else:
            form.helper.add_input(
                    Submit("save_and_finish",
                        mark_safe_lazy(
                            string_concat(
                                _("Save answer and finish"),
                                " &raquo;")),
Andreas Klöckner's avatar
Andreas Klöckner committed
                        css_class="relate-save-button"))
def create_flow_page_visit(request, flow_session, page_data):
    # type: (http.HttpRequest, FlowSession, FlowPageData) -> None

    if request.user.is_authenticated:
        # The access to 'is_authenticated' ought to wake up SimpleLazyObject.
        user = request.user
    else:
        user = None

Andreas Klöckner's avatar
Andreas Klöckner committed
    visit = FlowPageVisit(
        flow_session=flow_session,
        page_data=page_data,
        remote_address=request.META["REMOTE_ADDR"],

    if hasattr(request, "relate_impersonate_original_user"):
        visit.impersonated_by = request.relate_impersonate_original_user

    visit.save()
def view_flow_page(pctx, flow_session_id, page_ordinal):
    # type: (CoursePageContext, int, int) -> http.HttpResponse

    request = pctx.request
    login_exam_ticket = get_login_exam_ticket(request)
    page_ordinal = int(page_ordinal)
    flow_session_id = int(flow_session_id)
    flow_session = get_and_check_flow_session(pctx, flow_session_id)
Dong Zhuang's avatar
Dong Zhuang committed
    assert flow_session is not None
Dong Zhuang's avatar
Dong Zhuang committed
    flow_id = flow_session.flow_id
    adjust_flow_session_page_data(pctx.repo, flow_session, pctx.course.identifier,
            respect_preview=True)
        fpctx = FlowPageContext(pctx.repo, pctx.course, flow_id, page_ordinal,
                                participation=pctx.participation,
                                flow_session=flow_session,
                                request=pctx.request)
    except PageOrdinalOutOfRange:
        return redirect("relate-view_flow_page",
                pctx.course.identifier,
                flow_session.id,
                flow_session.page_count-1)
    if fpctx.page is None:
        raise http.Http404()

    assert fpctx.page_context is not None
    assert fpctx.page_data is not None

    access_rule = get_session_access_rule(
            flow_session, fpctx.flow_desc, now_datetime,
            facilities=pctx.request.relate_facilities,
            login_exam_ticket=login_exam_ticket)
            flow_session, fpctx.flow_desc, now_datetime)
    generates_grade = (
            grading_rule.grade_identifier is not None
Andreas Klöckner's avatar
Andreas Klöckner committed
            and grading_rule.generates_grade)
    permissions = fpctx.page.get_modified_permissions_for_page(
            access_rule.permissions)
    if access_rule.message:
        messages.add_message(request, messages.INFO, access_rule.message)

    lock_down_if_needed(pctx.request, permissions, flow_session)
    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"))
    answer_visit = None
    prev_visit_id = None
    viewing_prior_version = False
    if request.method == "POST":
        if "finish" in request.POST:
            return redirect("relate-finish_flow_session_view",
                    pctx.course.identifier, flow_session_id)
            post_result = post_flow_page(
                    flow_session, fpctx, request, permissions, generates_grade)
            if not isinstance(post_result, tuple):
                # ought to be an HTTP response
                return post_result
            (
                page_behavior,
                prev_answer_visits,
                form,
                feedback,
                answer_data,
                answer_was_graded) = post_result
            if prev_answer_visits:
                prev_visit_id = prev_answer_visits[0].id

            # continue at common flow page generation below
        create_flow_page_visit(request, flow_session, fpctx.page_data)
        prev_answer_visits = list(
                get_prev_answer_visits_qset(fpctx.page_data))

        # {{{ fish out previous answer_visit

        prev_visit_id = pctx.request.GET.get("visit_id")
        if prev_visit_id is not None:
            try:
                prev_visit_id = int(prev_visit_id)
            except ValueError:
                raise SuspiciousOperation("non-integer passed for 'visit_id'")

        if prev_answer_visits and prev_visit_id is not None:
            answer_visit = prev_answer_visits[0]

            for ivisit, pvisit in enumerate(prev_answer_visits):
                if pvisit.id == prev_visit_id:
                    answer_visit = pvisit
                    if ivisit > 0:
                        viewing_prior_version = True

                    break

            if viewing_prior_version:
                from django.template import defaultfilters
                messages.add_message(request, messages.INFO, (
                    _("Viewing prior submission dated %(date)s. ")
                    % {
                        "date": defaultfilters.date(
                            as_local_time(answer_visit.visit_time),
                            "DATETIME_FORMAT"),
Andreas Klöckner's avatar
Andreas Klöckner committed
                    + '<a class="btn btn-default btn-sm" href="?" '
                    'role="button">&laquo; %s</a>'
                    % _("Go back")))

            prev_visit_id = answer_visit.id

        elif prev_answer_visits:
            answer_visit = prev_answer_visits[0]
            prev_visit_id = answer_visit.id

        else:
            answer_visit = None

        # }}}

        if answer_visit is not None:
            answer_was_graded = answer_visit.is_submitted_answer
        page_behavior = get_page_behavior(
                page=fpctx.page,
                permissions=permissions,
                session_in_progress=flow_session.in_progress,
                generates_grade=generates_grade,
                is_unenrolled_session=flow_session.participation is None,
                viewing_prior_version=viewing_prior_version)
        if fpctx.page.expects_answer():
            if answer_visit is not None:
                answer_data = answer_visit.answer
                most_recent_grade = 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

            try:
                form = fpctx.page.make_form(
                        page_context, page_data.data,
                        answer_data, page_behavior)
            except InvalidPageData as e:
                messages.add_message(request, messages.ERROR,
                            "The page data stored in the database was found "
                            "to be invalid for the page as given in the "
                            "course content. Likely the course content was "
                            "changed in an incompatible way (say, by adding "
                            "an option to a choice question) without changing "
                            "the question ID. The precise error encountered "

                return render_course_page(pctx, "course/course-base.html", {})
            form = None

    # start common flow page generation

    # form, page_behavior, answer_was_graded, feedback
    # answer_data, grade_data
    if form is not None and page_behavior.may_change_answer:
        form = add_buttons_to_form(form, fpctx, flow_session,
    if (fpctx.page.expects_answer() and answer_was_graded
            and (
                page_behavior.show_correctness
                or page_behavior.show_answer)):
        shown_feedback = feedback

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

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

    if (generates_grade
            and flow_session.participation is None
            and flow_permission.submit_answer in permissions):
        messages.add_message(request, messages.INFO,
                _("Changes to this session are being prevented "
                    "because this session yields a permanent grade, but "
                    "you have not completed your enrollment process in "
                    "this course."))

Andreas Klöckner's avatar
Andreas Klöckner committed
    # {{{ render flow page