Skip to content
flow.py 100 KiB
Newer Older
        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:
        if not flow_session.in_progress:
            end_time = as_local_time(flow_session.completion_time)
        else:
            end_time = now_datetime
        session_minutes = (
                end_time - 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)

    from django.db import connection
    with connection.cursor() as c:
        c.execute(
                "SELECT DISTINCT course_flowpagedata.page_ordinal "
                "FROM course_flowpagevisit "
                "INNER JOIN course_flowpagedata "
                "ON course_flowpagedata.id = course_flowpagevisit.page_data_id "
                "WHERE course_flowpagedata.flow_session_id = %s "
                "AND course_flowpagevisit.answer IS NOT NULL "
                "ORDER BY course_flowpagedata.page_ordinal",
        flow_page_ordinals_with_answers = {row[0] for row in c.fetchall()}
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,
        "page_ordinal": fpctx.page_ordinal,
Andreas Klöckner's avatar
Andreas Klöckner committed
        "page_data": fpctx.page_data,
        "percentage": int(100 * (fpctx.page_ordinal+1) / flow_session.page_count),
        "flow_session": flow_session,
        "all_page_data": all_page_data,
        "flow_page_ordinals_with_answers": flow_page_ordinals_with_answers,
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,
Andreas Klöckner's avatar
Andreas Klöckner committed
            and (flow_permission.change_answer in permissions)),
        "will_receive_feedback": will_receive_feedback(permissions),
        "show_answer": page_behavior.show_answer,
        "may_send_email_about_flow_page":
Dong Zhuang's avatar
Dong Zhuang committed
            may_send_email_about_flow_page(flow_session, permissions),
        "hide_point_count": flow_permission.hide_point_count in permissions,
        "expects_answer": fpctx.page.expects_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),
        "viewing_prior_version": viewing_prior_version,
        "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)
        args["page_expect_answer_and_gradable"] = True
    if fpctx.page.is_optional_page:
        assert not getattr(args, "max_points", None)
        args["is_optional_page"] = True

    return render_course_page(
            pctx, "course/flow-page.html", args,
            allow_instant_flow_requests=False)
def get_prev_answer_visits_dropdown_content(
            pctx, flow_session_id, page_ordinal, prev_visit_id):
    """
    :return: serialized prev_answer_visits items for past-submission-dropdown
    """
    request = pctx.request
Andreas Klöckner's avatar
Andreas Klöckner committed
    if request.method != "GET":
    page_ordinal = int(page_ordinal)
    flow_session_id = int(flow_session_id)
    flow_session = get_and_check_flow_session(pctx, flow_session_id)

    page_data = get_object_or_404(
        FlowPageData, flow_session=flow_session, page_ordinal=page_ordinal)
    prev_answer_visits = get_prev_answer_visits_qset(page_data)

    return render(request, "course/flow-page-prev-visits.html", {
        "prev_answer_visits": prev_answer_visits,
        "prev_visit_id": (
            None
            if prev_visit_id == "None" else
            int(prev_visit_id)),
    })
def get_pressed_button(form: StyledForm) -> str:
    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()
Andreas Klöckner's avatar
Andreas Klöckner committed
def post_flow_page(
        flow_session: FlowSession,
        fpctx: FlowPageContext,
        request: http.HttpRequest,
        permissions: frozenset[str],
        ) -> tuple[PageBehavior, list[FlowPageVisit],
                forms.Form, AnswerFeedback | None, Any, bool]:
    page_context = fpctx.page_context
    page_data = fpctx.page_data

Andreas Klöckner's avatar
Andreas Klöckner committed
    assert page_context is not None

    submission_allowed = True

Andreas Klöckner's avatar
Andreas Klöckner committed
    assert fpctx.page is not None

    # 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

Dong Zhuang's avatar
Dong Zhuang committed
    prev_answer_visits = list(
            get_prev_answer_visits_qset(fpctx.page_data))

    # 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(
Andreas Klöckner's avatar
Andreas Klöckner committed
            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(
Andreas Klöckner's avatar
Andreas Klöckner committed
                page_context, fpctx.page_data.data,
                form, request.FILES)
        answer_visit.is_submitted_answer = pressed_button == "submit"
        if hasattr(request, "relate_impersonate_original_user"):
            answer_visit.impersonated_by = \
                request.relate_impersonate_original_user
        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 LanguageOverride(course=fpctx.course):
                feedback: AnswerFeedback | None = fpctx.page.grade(
                        page_context, page_data.data, answer_visit.answer,

            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.page_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)

# {{{ view: send interaction email to course staffs in flow pages

@course_view
def send_email_about_flow_page(pctx, flow_session_id, page_ordinal):

    # {{{ check if interaction email is allowed for this page.

    page_ordinal = int(page_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

Dong Zhuang's avatar
Dong Zhuang committed
    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)

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

    request = pctx.request
    now_datetime = get_now_or_fake_time(request)
    login_exam_ticket = get_login_exam_ticket(request)
    access_rule = get_session_access_rule(
            flow_session, fpctx.flow_desc, now_datetime,
            facilities=pctx.request.relate_facilities,
            login_exam_ticket=login_exam_ticket)

    permissions = fpctx.page.get_modified_permissions_for_page(
            access_rule.permissions)

Dong Zhuang's avatar
Dong Zhuang committed
    if not may_send_email_about_flow_page(flow_session, permissions):
    review_url = reverse(
        "relate-view_flow_page",
        kwargs={"course_identifier": pctx.course.identifier,
                "flow_session_id": flow_session_id,
                "page_ordinal": page_ordinal
    from urllib.parse import urljoin
    review_uri = urljoin(settings.RELATE_BASE_URL, review_url)

    if request.method == "POST":
        form = FlowPageInteractionEmailForm(review_uri, request.POST)

            from_email = getattr(
                    settings,
                    "STUDENT_INTERACT_EMAIL_FROM",
                    settings.ROBOT_EMAIL_FROM)
            student_email = flow_session.participation.user.email

Dong Zhuang's avatar
Dong Zhuang committed
            from course.constants import participation_status

            ta_email_list = Participation.objects.filter(
                    course=pctx.course,
                    roles__permissions__permission=pperm.assign_grade,
                    roles__identifier="ta",
Dong Zhuang's avatar
Dong Zhuang committed
                    status=participation_status.active
            ).values_list("user__email", flat=True)

Dong Zhuang's avatar
Dong Zhuang committed
            recipient_list = ta_email_list
            if not recipient_list:

                # instructors to receive the email
                recipient_list = Participation.objects.filter(
                    course=pctx.course,
                    roles__permissions__permission=pperm.assign_grade,
                    roles__identifier="instructor"
Dong Zhuang's avatar
Dong Zhuang committed
                ).values_list("user__email", flat=True)
            with LanguageOverride(course=pctx.course):
                from course.utils import will_use_masked_profile_for_email
Dong Zhuang's avatar
Dong Zhuang committed

                if will_use_masked_profile_for_email(recipient_list):
                    username = pctx.participation.user.get_masked_profile()
                else:
                    username = pctx.participation.user.get_full_name()
Dong Zhuang's avatar
Dong Zhuang committed

                page_id = FlowPageData.objects.get(
                    flow_session=flow_session_id, page_ordinal=page_ordinal).page_id

                from relate.utils import render_email_template

                message = render_email_template(
                    "course/flow-page-interaction-email.txt", {
                        "page_id": page_id,
                        "flow_session_id": flow_session_id,
                        "course": pctx.course,
                        "question_text": form.cleaned_data["message"],
                        "review_uri": review_uri,
                    })

                from django.core.mail import EmailMessage
                msg = EmailMessage(
                    subject=string_concat(
Andreas Klöckner's avatar
Andreas Klöckner committed
                        "[%(identifier)s:%(flow_id)s--%(page_id)s] ",
                        _("Interaction request from %(username)s"))
                    % {
                            "identifier": pctx.course_identifier,
                            "flow_id": flow_session_id,
                            "page_id": page_id,
                            "username": username
                    body=message,
                    from_email=from_email,
                    to=recipient_list,
                )
                # TODO: add instructors to msg.bcc according to
                # settings in Course model.
                msg.bcc = [student_email]
                msg.reply_to = [student_email]

                from relate.utils import get_outbound_mail_connection
                msg.connection = get_outbound_mail_connection("student_interact")
                msg.send()

                messages.add_message(
                    request, messages.SUCCESS,
                    _("Email sent, and notice that you will "
                      "also receive a copy of the email."))

            return redirect("relate-view_flow_page",
                            pctx.course.identifier, flow_session_id, page_ordinal)
        form = FlowPageInteractionEmailForm(review_uri)
Andreas Klöckner's avatar
Andreas Klöckner committed
    return render_course_page(
            pctx, "course/generic-course-form.html", {
                "form": form,
                "form_description": _("Send interaction email"),
                })


class FlowPageInteractionEmailForm(StyledForm):
    def __init__(self, review_uri, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["message"] = forms.CharField(
                required=True,
                widget=forms.Textarea,
Andreas Klöckner's avatar
Andreas Klöckner committed
                help_text=string_concat(
                    _("Your questions about page %s . ") % review_uri,
                    _("Notice that <strong>only</strong> questions "
                      "for that page will be answered."),
                ),
                label=_("Message"))
        self.helper.add_input(
            Submit(
                "submit", _("Send Email"),
                css_class="relate-submit-button"))

    def clean_message(self):
        message = cleaned_data.get("message")
        if len(message) < 20:
            raise forms.ValidationError(
                _("At least 20 characters are required for submission."))
        return message

# }}}


# {{{ view: update page bookmark state

@course_view
def update_page_bookmark_state(pctx, flow_session_id, page_ordinal):
    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"))

    bookmark_state = pctx.request.POST.get("bookmark_state")
    if bookmark_state not in ["0", "1"]:
        raise SuspiciousOperation(_("invalid bookmark state"))

    bookmark_state = bookmark_state == "1"

    fpd = get_object_or_404(FlowPageData.objects,
                            flow_session=flow_session,
                            page_ordinal=page_ordinal)

    fpd.bookmarked = bookmark_state
    fpd.save()

    return http.HttpResponse("OK")

# }}}


# {{{ view: update expiration mode
def update_expiration_mode(
        pctx: CoursePageContext, flow_session_id: int) -> http.HttpResponse:
    if pctx.request.method != "POST":
        raise SuspiciousOperation(_("only POST allowed"))
    login_exam_ticket = get_login_exam_ticket(pctx.request)

    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(
            get_now_or_fake_time(pctx.request),
            facilities=pctx.request.relate_facilities,
            login_exam_ticket=login_exam_ticket)
    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: CoursePageContext, flow_session_id: int) -> http.HttpResponse:
    # 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)
    login_exam_ticket = get_login_exam_ticket(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, fctx.flow_desc, now_datetime,
            facilities=pctx.request.relate_facilities,
            login_exam_ticket=login_exam_ticket)
    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,
    answer_visits: list[FlowPageVisit | None] = assemble_answer_visits(flow_session)
    (answered_page_data_list, unanswered_page_data_list, is_interactive_flow) =\
            fctx, flow_session, answer_visits)
    if flow_permission.view not in access_rule.permissions:
        raise PermissionDenied()

    def render_finish_response(template, **kwargs) -> http.HttpResponse:
            "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)
    grading_rule = get_session_grading_rule(
            flow_session, fctx.flow_desc, now_datetime)
    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"))
                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):
Andreas Klöckner's avatar
Andreas Klöckner committed
                [*fctx.flow_desc.notify_on_submit, fctx.course.notify_email])
Dong Zhuang's avatar
Dong Zhuang committed

            from course.utils import will_use_masked_profile_for_email
            use_masked_profile = will_use_masked_profile_for_email(staff_email)

Dong Zhuang's avatar
Dong Zhuang committed
            if flow_session.participation is None or flow_session.user is None:
Dong Zhuang's avatar
Dong Zhuang committed
                # because Anonymous doesn't have get_masked_profile() method
Dong Zhuang's avatar
Dong Zhuang committed
                use_masked_profile = False

Andreas Klöckner's avatar
Andreas Klöckner committed
            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,
Andreas Klöckner's avatar
Andreas Klöckner committed
                                grading_rule.grade_identifier,
                                grading_rule.grade_aggregation_strategy).id))
Andreas Klöckner's avatar
Andreas Klöckner committed
            else:
                review_uri = reverse("relate-view_flow_page",
                        args=(
                            pctx.course.identifier,
                            flow_session.id,
                            0))

            with LanguageOverride(course=pctx.course):
                from relate.utils import render_email_template
                participation = flow_session.participation
                message = render_email_template("course/submit-notify.txt", {
Andreas Klöckner's avatar
Andreas Klöckner committed
                    "course": fctx.course,
                    "flow_session": flow_session,
                    "use_masked_profile": use_masked_profile,
Andreas Klöckner's avatar
Andreas Klöckner committed
                    "review_uri": pctx.request.build_absolute_uri(review_uri)
                    })

                participation_desc = repr(participation)
                if use_masked_profile:
                    participation_desc = _(
                        "%(user)s in %(course)s as %(role)s") % {
                        "user": participation.user.get_masked_profile(),
                        "course": flow_session.course,
                        "role": "/".join(
                            role.identifier
                            for role in participation.roles.all())
                    }

Andreas Klöckner's avatar
Andreas Klöckner committed
                from django.core.mail import EmailMessage
                msg = EmailMessage(
                        string_concat("[%(identifier)s:%(flow_id)s] ",
                            _("Submission by %(participation_desc)s"))
                        % {"participation_desc": participation_desc,
                            "identifier": fctx.course.identifier,
                            "flow_id": flow_session.flow_id},
Andreas Klöckner's avatar
Andreas Klöckner committed
                        message,
                        getattr(settings, "NOTIFICATION_EMAIL_FROM",
                            settings.ROBOT_EMAIL_FROM),
Andreas Klöckner's avatar
Andreas Klöckner committed
                        fctx.flow_desc.notify_on_submit)
                msg.bcc = [fctx.course.notify_email]
                from relate.utils import get_outbound_mail_connection
                msg.connection = (
                    get_outbound_mail_connection("notification")
                    if hasattr(settings, "NOTIFICATION_EMAIL_FROM")
                    else get_outbound_mail_connection("robot"))
            if flow_permission.cannot_see_flow_result in access_rule.permissions:
                grade_info = None

            return render_finish_response(
                    "course/flow-completion-grade.html",
                    completion_text=completion_text,
                    grade_info=grade_info)

        else:
            return render_finish_response(
                    "course/flow-completion.html",
                    last_page_nr=None,
                    flow_session=flow_session,
                    completion_text=completion_text)

    if (not is_interactive_flow
Andreas Klöckner's avatar
Andreas Klöckner committed
            or (flow_session.in_progress
                and flow_permission.end_session not in access_rule.permissions)):
        # No ability to end--just show completion page.

        return render_finish_response(
                "course/flow-completion.html",
                last_page_nr=flow_session.page_count-1,
                flow_session=flow_session,
                completion_text=completion_text)

    elif not flow_session.in_progress:
        # Just reviewing: re-show grades.
        grade_info = gather_grade_info(
                fctx, flow_session, grading_rule, answer_visits)
        if flow_permission.cannot_see_flow_result in access_rule.permissions:
            grade_info = None

        return render_finish_response(
                "course/flow-completion-grade.html",
                completion_text=completion_text,
                grade_info=grade_info)

    else:
        # confirm ending flow
        answered_count = len(answered_page_data_list)
        unanswered_count = len(unanswered_page_data_list)
        required_count = answered_count + unanswered_count
        session_may_generate_grade = (
            grading_rule.generates_grade and required_count)
        return render_finish_response(
                "course/flow-confirm-completion.html",
                last_page_nr=flow_session.page_count-1,
                flow_session=flow_session,
                answered_count=answered_count,
                unanswered_count=unanswered_count,
                unanswered_page_data_list=unanswered_page_data_list,
                required_count=required_count,
                session_may_generate_grade=session_may_generate_grade)

# {{{ view: regrade flow

class RegradeFlowForm(StyledForm):
    def __init__(self, flow_ids: list[str], *args: Any, **kwargs: Any) -> None:
        super().__init__(*args, **kwargs)

        self.fields["flow_id"] = forms.ChoiceField(
                choices=[(fid, fid) for fid in flow_ids],
ifaint's avatar
ifaint committed
                required=True,
                label=_("Flow ID"),
                widget=Select2Widget())
        self.fields["access_rules_tag"] = forms.CharField(
                required=False,
                help_text=_("If non-empty, limit the regrading to sessions "
                "started under this access rules tag."),
ifaint's avatar
ifaint committed
                label=_("Access rules tag"))
        self.fields["regraded_session_in_progress"] = forms.ChoiceField(
                choices=(
                        _("Regrade in-progress and not-in-progress sessions")),
                        _("Regrade in-progress sessions only")),
                        _("Regrade not-in-progress sessions only")),
ifaint's avatar
ifaint committed
                    ),
                label=_("Regraded session in progress"))

        self.helper.add_input(
                Submit("regrade", _("Regrade")))
def regrade_flows_view(pctx: CoursePageContext) -> http.HttpResponse:
    if not pctx.has_permission(pperm.batch_regrade_flow_session):
        raise PermissionDenied(_("may not batch-regrade flows"))

    from course.content import list_flow_ids
    flow_ids = list_flow_ids(pctx.repo, pctx.course_commit_sha)

    request = pctx.request
    if request.method == "POST":
        form = RegradeFlowForm(flow_ids, request.POST, request.FILES)
        if form.is_valid():
            inprog_value = {
                    "any": None,
                    "yes": True,
                    "no": False,
                    }[form.cleaned_data["regraded_session_in_progress"]]

            from course.tasks import regrade_flow_sessions
            async_res = regrade_flow_sessions.delay(
                    pctx.course.id,
                    form.cleaned_data["flow_id"],
                    form.cleaned_data["access_rules_tag"],
                    inprog_value)
            return redirect("relate-monitor_task", async_res.id)
    else:
        form = RegradeFlowForm(flow_ids)

    return render_course_page(pctx, "course/generic-course-form.html", {
        "form": form,
        "form_text": string_concat(
            "<p>",
            _("This regrading process is only intended for flows that do"
            "not show up in the grade book. If you would like to regrade"
            "for-credit flows, use the corresponding functionality in "
            "the grade book."),
ifaint's avatar
ifaint committed
        "form_description": _("Regrade not-for-credit Flow Sessions"),

# {{{ view: unsubmit flow page

class UnsubmitFlowPageForm(forms.Form):
    def __init__(self, *args: Any, **kwargs: Any) -> None:
        super().__init__(*args, **kwargs)

        self.helper.add_input(Submit("submit", _("Re-allow changes")))
        self.helper.add_input(Submit("cancel", _("Cancel")))


@course_view
def view_unsubmit_flow_page(
        pctx: CoursePageContext, flow_session_id: int, page_ordinal: int
        ) -> http.HttpResponse:
Dong Zhuang's avatar
Dong Zhuang committed
    if pctx.participation is None:
        raise PermissionDenied()

    if not pctx.has_permission(pperm.reopen_flow_session):
        raise PermissionDenied()

    request = pctx.request
    now_datetime = get_now_or_fake_time(request)

    page_ordinal = int(page_ordinal)
    flow_session_id = int(flow_session_id)

Dong Zhuang's avatar
Dong Zhuang committed
    flow_session = get_and_check_flow_session(pctx, flow_session_id)

    adjust_flow_session_page_data(pctx.repo, flow_session, pctx.course.identifier,
            respect_preview=True)

    page_data = get_object_or_404(
            FlowPageData, flow_session=flow_session, page_ordinal=page_ordinal)

    visit = get_first_from_qset(
            get_prev_answer_visits_qset(page_data)
            .filter(is_submitted_answer=True))

    if visit is None:
        messages.add_message(request, messages.INFO,
                _("No prior answers found that could be un-submitted."))
        return redirect("relate-view_flow_page",
                        pctx.course.identifier, flow_session_id, page_ordinal)
    if request.method == "POST":
        form = UnsubmitFlowPageForm(request.POST)
        if form.is_valid():
            if "submit" in request.POST:
                unsubmit_page(visit, now_datetime)
                messages.add_message(request, messages.INFO,
                        _("Flow page changes reallowed. "))

            return redirect("relate-view_flow_page",
                            pctx.course.identifier, flow_session_id, page_ordinal)
    else:
        form = UnsubmitFlowPageForm()

    return render_course_page(pctx, "course/generic-course-form.html", {
        "form_description": _("Re-allow Changes to Flow Page"),
        "form": form
        })

# }}}

def get_pv_purgeable_courses_for_user_qs(user: User) -> query.QuerySet:
    course_qs = Course.objects.all()
    if user.is_superuser:
        # do not filter queryset
        pass
    else:
        course_qs = course_qs.filter(
                participations__user=user,
                participations__roles__permissions__permission=(
                    pperm.use_admin_interface))

    return course_qs


class PurgePageViewData(StyledForm):
    def __init__(self, user: User, *args: Any, **kwargs: Any) -> None:
        self.helper = FormHelper()
        super().__init__(*args, **kwargs)

        self.fields["course"] = forms.ModelChoiceField(
                queryset=get_pv_purgeable_courses_for_user_qs(user),
                required=True)

        self.helper.add_input(
                Submit("submit", _("Purge Page View Data"),
                    css_class="btn btn-danger"))


def purge_page_view_data(request):
    purgeable_courses = get_pv_purgeable_courses_for_user_qs(request.user)
    if not purgeable_courses.count():
        raise PermissionDenied()
    if request.method == "POST":
        form = PurgePageViewData(request.user, request.POST)
        if form.is_valid():
            if "submit" in request.POST:
                course = form.cleaned_data["course"]

                from course.tasks import purge_page_view_data
                async_res = purge_page_view_data.delay(course.id)

                return redirect("relate-monitor_task", async_res.id)
    else:
        form = PurgePageViewData(request.user)

    return render(request, "generic-form.html", {
        "form_description": _("Purge Page View Data"),
        "form": form
        })

# }}}

# vim: foldmethod=marker