Skip to content
flow.py 73.8 KiB
Newer Older
                _("Cannot reopen anonymous sessions"))

    session.in_progress = True
    session.points = None
    session.max_points = None
                _("Session reopened at %(now)s, previous completion time "
                "was '%(complete_time)s'.") % {
                    'now': format_datetime_local(local_now()),
                    'complete_time': format_datetime_local(
                        as_local_time(session.completion_time))
                    })
    session.completion_time = None
def finish_flow_session_standalone(repo, course, session, force_regrade=False,
        now_datetime=None, past_due_only=False):
    # 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 = now()
    fctx = FlowContext(repo, course, session.flow_id)
    grading_rule = get_session_grading_rule(
            session, session.participation.role, fctx.flow_desc, now_datetime)
    if (past_due_only
            and grading_rule.due is not None
            and now_datetime < grading_rule.due):
    finish_flow_session(fctx, session, grading_rule,
            force_regrade=force_regrade, now_datetime=now_datetime)
def expire_flow_session_standalone(repo, course, session, now_datetime,
        past_due_only=False):
    assert session.participation is not None

    fctx = FlowContext(repo, course, session.flow_id)
    grading_rule = get_session_grading_rule(
            session, session.participation.role, 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, course, session):
    adjust_flow_session_page_data(repo, session, course.identifier)

    if session.in_progress:
        fctx = FlowContext(repo, course, session.flow_id,
                participation=session.participation)
        with transaction.atomic():
            answer_visits = assemble_answer_visits(session)
            for i in range(len(answer_visits)):
                answer_visit = answer_visits[i]
                if answer_visit is not None and answer_visit.get_most_recent_grade():
                    # Only make a new grade if there already is one.
                    grade_page_visit(answer_visit,
                            graded_at_git_commit_sha=fctx.course_commit_sha)
        prev_completion_time = session.completion_time

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

def recalculate_session_grade(repo, course, session):
    """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)
    with transaction.atomic():
        session.append_comment(
                _("Session grade recomputed at %(time)s.") % {
                    'time': format_datetime_local(local_now())
                    })
        session.save()

        reopen_session(session, force=True, suppress_log=True)
        finish_flow_session_standalone(
                repo, course, session, force_regrade=False,
                now_datetime=prev_completion_time)
def lock_down_if_needed(request, permissions, flow_session):
    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):
    request = pctx.request

    now_datetime = get_now_or_fake_time(request)
    fctx = FlowContext(pctx.repo, pctx.course, flow_id,
            participation=pctx.participation)

    if request.method == "POST":
        return post_start_flow(pctx, fctx, flow_id)
        session_start_rule = get_session_start_rule(pctx.course, pctx.participation,
                pctx.role, flow_id, fctx.flow_desc, now_datetime,
                facilities=pctx.request.relate_facilities)

        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"))
Andreas Klöckner's avatar
Andreas Klöckner committed
            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, pctx.role, fctx.flow_desc, now_datetime,
                        facilities=pctx.request.relate_facilities)
                grading_rule = get_session_grading_rule(
                        session, pctx.role, 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
                            or
                            flow_permission.end_session in access_rule.permissions
                        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 = []
        may_start = session_start_rule.may_start_new_session
        potential_session = FlowSession(
            course=pctx.course,
            participation=pctx.participation,
            flow_id=flow_id,
            in_progress=True,
            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, pctx.role, fctx.flow_desc, now_datetime)

        start_may_decrease_grade = (
                bool(past_sessions_and_properties)
                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])

        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": (
                dict(GRADE_AGGREGATION_STRATEGY_CHOICES).get(
                    new_session_grading_rule.grade_aggregation_strategy)),
            "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):
    now_datetime = get_now_or_fake_time(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,
            pctx.role, flow_id, fctx.flow_desc, now_datetime,
            facilities=pctx.request.relate_facilities)

    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,
            access_rules_tag=session_start_rule.tag_session,
            now_datetime=now_datetime)

    access_rule = get_session_access_rule(
            session, pctx.role, fctx.flow_desc, now_datetime,
            facilities=pctx.request.relate_facilities)

    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):
    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)

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

    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):
    try:
        flow_session = FlowSession.objects.get(id=flow_session_id)
    except ObjectDoesNotExist:
        raise http.Http404()
    if pctx.role in [
            participation_role.instructor,
            participation_role.teaching_assistant]:
        pass
    elif pctx.role in [
            participation_role.student,
Andreas Klöckner's avatar
Andreas Klöckner committed
            participation_role.observer,
            participation_role.auditor,
            participation_role.unenrolled]:
        if (pctx.participation != flow_session.participation
                and flow_session.participation is not None):
            raise PermissionDenied(_("may not view other people's sessions"))

        if (flow_session.user is not None
                and pctx.request.user != flow_session.user):
            raise PermissionDenied(_("may not view other people's sessions"))
    else:
        raise PermissionDenied()
    if flow_session.course.pk != pctx.course.pk:
        raise SuspiciousOperation()
def will_receive_feedback(permissions):
    return (
            flow_permission.see_correctness in permissions
            or flow_permission.see_answer_after_submission in permissions)
def get_page_behavior(page, permissions, session_in_progress, answer_was_graded,
        generates_grade, is_unenrolled_session, viewing_prior_version=False):
    show_correctness = None
    show_answer = None

    if 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

    elif 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)

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
    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):
    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.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):
    FlowPageVisit(
        flow_session=flow_session,
        page_data=page_data,
        remote_address=request.META['REMOTE_ADDR'],
        is_submitted_answer=None).save()
def view_flow_page(pctx, flow_session_id, 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,
                request=pctx.request)
    except PageOrdinalOutOfRange:
        return redirect("relate-view_flow_page",
                pctx.course.identifier,
                flow_session.id,
                flow_session.page_count-1)
    adjust_flow_session_page_data(pctx.repo, flow_session,
            pctx.course.identifier, fpctx.flow_desc)

    access_rule = get_session_access_rule(
            flow_session, pctx.role, fpctx.flow_desc, now_datetime,
            facilities=pctx.request.relate_facilities)

    grading_rule = get_session_grading_rule(
            flow_session, pctx.role, fpctx.flow_desc, now_datetime)
    generates_grade = (
            grading_rule.grade_identifier is not None
            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

    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
            # 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'")

        viewing_prior_version = False
        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
                from relate.utils import as_local_time
                messages.add_message(request, messages.INFO,
                    _("Viewing prior submission dated %(date)s.")
                    % {
                        "date": defaultfilters.date(
                            as_local_time(pvisit.visit_time),
                            "DATETIME_FORMAT"),
                        })

            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,
                        ugettext(
                            "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 "
                            "was the following: "+str(e)))

                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

        form_html = fpctx.page.form_to_html(
                pctx.request, page_context, form, answer_data)
    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))

    session_minutes = None
    time_factor = 1
    if flow_permission.see_session_time in permissions:
        session_minutes = (
                now_datetime - flow_session.start_time).total_seconds() / 60
        if flow_session.participation is not None:
            time_factor = flow_session.participation.time_factor

    all_page_data = get_all_page_data(flow_session)

Andreas Klöckner's avatar
Andreas Klöckner committed
    args = {
        "flow_identifier": fpctx.flow_id,
Andreas Klöckner's avatar
Andreas Klöckner committed
        "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,
        "all_page_data": all_page_data,
Andreas Klöckner's avatar
Andreas Klöckner committed
        "title": title, "body": body,
        "form": form,
Andreas Klöckner's avatar
Andreas Klöckner committed
        "form_html": form_html,
        "correct_answer": correct_answer,

        "show_correctness": page_behavior.show_correctness,
        "may_change_answer": page_behavior.may_change_answer,
            page_behavior.may_change_answer
            and
            (flow_permission.change_answer in permissions)),
        "will_receive_feedback": will_receive_feedback(permissions),
        "show_answer": page_behavior.show_answer,
        "session_minutes": session_minutes,
        "time_factor": time_factor,

        "expiration_mode_choices": expiration_mode_choices,
        "expiration_mode_choice_count": len(expiration_mode_choices),
        "expiration_mode": flow_session.expiration_mode,
Andreas Klöckner's avatar
Andreas Klöckner committed

        "flow_session_interaction_kind": flow_session_interaction_kind,
Andreas Klöckner's avatar
Andreas Klöckner committed
        "interaction_kind": get_interaction_kind(
            fpctx, flow_session, generates_grade, all_page_data),

        "prev_answer_visits": prev_answer_visits,
        "prev_visit_id": prev_visit_id,
    if fpctx.page.expects_answer() and fpctx.page.is_answer_gradable():
Andreas Klöckner's avatar
Andreas Klöckner committed
        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)
def get_pressed_button(form):
    buttons = ["save", "save_and_next", "save_and_finish", "submit"]
    for button in buttons:
        if button in form.data:
            return button

    raise SuspiciousOperation(_("could not find which button was pressed"))


@retry_transaction_decorator()
def post_flow_page(flow_session, fpctx, request, permissions, generates_grade):
    page_context = fpctx.page_context
    page_data = fpctx.page_data

    prev_answer_visits = list(
            get_prev_answer_visits_qset(fpctx.page_data))

    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 (prev_answer_visits
            and prev_answer_visits[0].is_submitted_answer
            and flow_permission.change_answer
                not in permissions):
        messages.add_message(request, messages.ERROR,
                _("Already have final answer."))
        submission_allowed = False

    page_behavior = get_page_behavior(
            page=fpctx.page,
            permissions=permissions,
            session_in_progress=flow_session.in_progress,
            answer_was_graded=False,
            generates_grade=generates_grade,
            is_unenrolled_session=flow_session.participation is None)

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

    pressed_button = get_pressed_button(form)

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

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

        answer_visit = FlowPageVisit()
        answer_visit.flow_session = flow_session
        answer_visit.page_data = fpctx.page_data
        answer_visit.remote_address = request.META['REMOTE_ADDR']

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

        prev_answer_visits.insert(0, answer_visit)

        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,
                answer_was_graded=answer_was_graded,
                generates_grade=generates_grade,
                is_unenrolled_session=flow_session.participation is None)

        if fpctx.page.is_answer_gradable():
            with translation.override(settings.RELATE_ADMIN_EMAIL_LOCALE):
                feedback = fpctx.page.grade(
                        page_context, page_data.data, answer_visit.answer,
                        grade_data=None)

            if answer_visit.is_submitted_answer:
                grade = FlowPageVisitGrade()
                grade.visit = answer_visit
                grade.max_points = fpctx.page.max_points(page_data.data)
                grade.graded_at_git_commit_sha = fpctx.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",
                    fpctx.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",
                    fpctx.course.identifier, flow_session.id)
        else:
            # The form needs to be recreated here, although there
            # already is a form from the process_form_post above.  This
            # is because the value of 'answer_was_graded' may have
            # changed between then and now (and page_behavior with
            # it)--and that value depends on form validity, which we
            # can only decide once we have a form.

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

        # }}}

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

        answer_data = None
        answer_was_graded = False

        if prev_answer_visits:
            answer_data = prev_answer_visits[0].answer

        feedback = None
        messages.add_message(request, messages.ERROR,
                _("Failed to submit answer."))

    return (
            page_behavior,
            prev_answer_visits,
            form,
            feedback,
            answer_data,
            answer_was_graded)


@course_view
def update_expiration_mode(pctx, flow_session_id):
    if pctx.request.method != "POST":
        raise SuspiciousOperation(_("only POST allowed"))

    flow_session = get_object_or_404(FlowSession, id=flow_session_id)

    if flow_session.participation != pctx.participation:
        raise PermissionDenied(
                _("may only change your own flow sessions"))
    if not flow_session.in_progress:
        raise PermissionDenied(
                _("may only change in-progress flow sessions"))

    expmode = pctx.request.POST.get("expiration_mode")
    if not any(expmode == em_key
            for em_key, _ in FLOW_SESSION_EXPIRATION_MODE_CHOICES):
        raise SuspiciousOperation(_("invalid expiration mode"))

    fctx = FlowContext(pctx.repo, pctx.course, flow_session.flow_id,
    access_rule = get_session_access_rule(
            flow_session, pctx.role, fctx.flow_desc,
            get_now_or_fake_time(pctx.request),
            facilities=pctx.request.relate_facilities)
    if is_expiration_mode_allowed(expmode, access_rule.permissions):
        flow_session.expiration_mode = expmode
        flow_session.save()

        return http.HttpResponse("OK")
    else:
        raise PermissionDenied()

def finish_flow_session_view(pctx, flow_session_id):
    # Does not need to be atomic: All writing to the db
    # is done in 'finish_flow_session' below.

    now_datetime = get_now_or_fake_time(pctx.request)

    flow_session_id = int(flow_session_id)
    flow_session = get_and_check_flow_session(
            pctx, flow_session_id)
    flow_id = flow_session.flow_id
    fctx = FlowContext(pctx.repo, pctx.course, flow_id,
    access_rule = get_session_access_rule(
            flow_session, pctx.role, fctx.flow_desc, now_datetime,
            facilities=pctx.request.relate_facilities)
    answer_visits = assemble_answer_visits(flow_session)

    from course.content import markup_to_html
    completion_text = markup_to_html(
            fctx.course, fctx.repo, pctx.course_commit_sha,
            getattr(fctx.flow_desc, "completion_text", ""))
    adjust_flow_session_page_data(pctx.repo, flow_session, pctx.course.identifier,
            fctx.flow_desc)

    (answered_count, unanswered_count) = count_answered_gradable(
            fctx, flow_session, answer_visits)
    is_graded_flow = bool(answered_count + unanswered_count)
    if flow_permission.view not in access_rule.permissions:
        raise PermissionDenied()

    def render_finish_response(template, **kwargs):
        render_args = {
            "flow_identifier": fctx.flow_id,
            "flow_desc": fctx.flow_desc,
        }
        render_args.update(kwargs)
        return render_course_page(
                pctx, template, render_args,
                allow_instant_flow_requests=False)

    if request.method == "POST":
        if "submit" not in request.POST:
            raise SuspiciousOperation(_("odd POST parameters"))

        if not flow_session.in_progress:
            messages.add_message(request, messages.ERROR,
                    _("Cannot end a session that's already ended"))
        if flow_permission.end_session not in access_rule.permissions:
            raise PermissionDenied(
                    _("not permitted to end session"))

        grading_rule = get_session_grading_rule(
                flow_session, pctx.role, fctx.flow_desc, now_datetime)
                fctx, flow_session, grading_rule,
Andreas Klöckner's avatar
Andreas Klöckner committed
        # {{{ send notify email if requested

        if (hasattr(fctx.flow_desc, "notify_on_submit")
                and fctx.flow_desc.notify_on_submit):
            if (grading_rule.grade_identifier
                    and flow_session.participation is not None):
                from course.models import get_flow_grading_opportunity
                review_uri = reverse("relate-view_single_grade",
                        args=(
                            pctx.course.identifier,
                            flow_session.participation.id,
                            get_flow_grading_opportunity(
                                pctx.course, flow_session.flow_id, fctx.flow_desc,
                                grading_rule).id))