Newer
Older
now_datetime=now_datetime, respect_preview=False)
raise ValueError(
_("invalid expiration mode '%(mode)s' on flow session ID "
"%(session_id)d") % {
'mode': flow_session.expiration_mode,
'session_id': flow_session.id})
Andreas Klöckner
committed
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)
assert grade_info is not None
comment = None
points = grade_info.points
if (points is not None
and grading_rule.credit_percent is not None
and grading_rule.credit_percent != 100):
comment = (
_("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)
flow_session.save()
# 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,
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
gchange.attempt_id = "flow-session-%d" % flow_session.id
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,
state=gchange.state,
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
and previous_grade_change.comment == gchange.comment):
do_save = False
Andreas Klöckner
committed
else:
# no previous grade changes
if points is None:
do_save = False
if do_save:
gchange.save()
return grade_info
Andreas Klöckner
committed
def reopen_session(
session, force=False, suppress_log=False):
# type: (FlowSession, bool, bool) -> None
raise RuntimeError(
_("Cannot reopen a session that's already in progress"))
raise RuntimeError(
_("Cannot reopen anonymous sessions"))
session.in_progress = True
session.points = None
session.max_points = None
Andreas Klöckner
committed
if not suppress_log:
session.append_comment(
_("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
Andreas Klöckner
committed
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
Andreas Klöckner
committed
):
# type: (...) -> bool
Andreas Klöckner
committed
# Do not be tempted to call adjust_flow_session_page_data in here.
# This function may be called from within a transaction.
Andreas Klöckner
committed
assert session.participation is not None
Andreas Klöckner
committed
if now_datetime is None:
from django.utils.timezone import now
Andreas Klöckner
committed
now_datetime_filled = now()
else:
now_datetime_filled = now_datetime
Andreas Klöckner
committed
fctx = FlowContext(repo, course, session.flow_id)
grading_rule = get_session_grading_rule(session, fctx.flow_desc,
now_datetime_filled)
Andreas Klöckner
committed
if grading_rule.due is not None:
if (
past_due_only
and now_datetime_filled < grading_rule.due):
return False
finish_flow_session(fctx, session, grading_rule,
Andreas Klöckner
committed
force_regrade=force_regrade,
now_datetime=now_datetime_filled,
respect_preview=respect_preview)
Andreas Klöckner
committed
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
Andreas Klöckner
committed
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)
Andreas Klöckner
committed
def regrade_session(
repo, # type: Repo_ish
course, # type: Course
session, # type: FlowSession
):
adjust_flow_session_page_data(repo, session, course.identifier,
respect_preview=False)
Andreas Klöckner
committed
Andreas Klöckner
committed
with transaction.atomic():
answer_visits = assemble_answer_visits(session) # type: List[Optional[FlowPageVisit]] # noqa
Andreas Klöckner
committed
for i in range(len(answer_visits)):
answer_visit = answer_visits[i]
Andreas Klöckner
committed
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)
Andreas Klöckner
committed
prev_completion_time = session.completion_time
Andreas Klöckner
committed
with transaction.atomic():
session.append_comment(
_("Session regraded at %(time)s.") % {
'time': format_datetime_local(local_now())
})
session.save()
Andreas Klöckner
committed
Andreas Klöckner
committed
reopen_session(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):
# 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)
Andreas Klöckner
committed
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,
respect_preview=False)
# }}}
Andreas Klöckner
committed
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
# {{{ view: start flow
Andreas Klöckner
committed
# type: (CoursePageContext, Text) -> http.HttpResponse
request = pctx.request
login_exam_ticket = get_login_exam_ticket(pctx.request)
now_datetime = get_now_or_fake_time(request)
fctx = FlowContext(pctx.repo, pctx.course, flow_id,
participation=pctx.participation)
return post_start_flow(pctx, fctx, flow_id)
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
or
flow_permission.end_session in access_rule.permissions
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 = []
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,
# 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)
and
new_session_grading_rule.grade_aggregation_strategy not in
grade_aggregation_strategy.max_grade,
grade_aggregation_strategy.use_earliest])
return render_course_page(pctx, "course/flow-start.html", {
"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):
Andreas Klöckner
committed
# 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)
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,
now_datetime=now_datetime)
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):
Andreas Klöckner
committed
# 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)
# }}}
# {{{ view: flow page
def get_and_check_flow_session(pctx, flow_session_id):
Andreas Klöckner
committed
# 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()
Andreas Klöckner
committed
my_session = (
pctx.participation == flow_session.participation
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:
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"))
return flow_session
def will_receive_feedback(permissions):
Andreas Klöckner
committed
# type: (frozenset[Text]) -> bool
return (
flow_permission.see_correctness in permissions
or flow_permission.see_answer_after_submission in permissions)
def may_send_email_about_flow_page(permissions):
# type: (frozenset[Text]) -> bool
return flow_permission.send_email_about_flow_page in permissions
Andreas Klöckner
committed
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() 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)
not viewing_prior_version
and (not answer_was_graded
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))
)
Andreas Klöckner
committed
from course.page.base import PageBehavior # noqa
return PageBehavior(
show_correctness=show_correctness,
show_answer=show_answer,
def add_buttons_to_form(form, fpctx, flow_session, permissions):
Andreas Klöckner
committed
# 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:
"submit", _("Submit answer for feedback"),
accesskey="g",
css_class="relate-save-button relate-submit-button"))
form.helper.add_input(
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:
Submit("save_and_next",
mark_safe_lazy(
string_concat(
_("Save answer and move on"),
" »")),
else:
form.helper.add_input(
Submit("save_and_finish",
mark_safe_lazy(
string_concat(
_("Save answer and finish"),
" »")),
Dong Zhuang
committed
def create_flow_page_visit(request, flow_session, page_data):
Andreas Klöckner
committed
# 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
flow_session=flow_session,
page_data=page_data,
remote_address=request.META['REMOTE_ADDR'],
Dong Zhuang
committed
is_submitted_answer=None)
if hasattr(request, "relate_impersonate_original_user"):
visit.impersonated_by = request.relate_impersonate_original_user
visit.save()
@course_view
def view_flow_page(pctx, flow_session_id, ordinal):
Andreas Klöckner
committed
# type: (CoursePageContext, int, int) -> http.HttpResponse
request = pctx.request
login_exam_ticket = get_login_exam_ticket(request)
ordinal = int(ordinal)
flow_session_id = int(flow_session_id)
flow_session = get_and_check_flow_session(pctx, flow_session_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,
adjust_flow_session_page_data(pctx.repo, flow_session, pctx.course.identifier,
respect_preview=True)
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)
Andreas Klöckner
committed
if fpctx.page is None:
raise http.Http404()
assert fpctx.page_context is not None
assert fpctx.page_data is not None
Andreas Klöckner
committed
now_datetime = get_now_or_fake_time(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)
Andreas Klöckner
committed
grading_rule = get_session_grading_rule(
flow_session, fpctx.flow_desc, now_datetime)
generates_grade = (
grading_rule.grade_identifier is not None
and
grading_rule.generates_grade)
Andreas Klöckner
committed
del grading_rule
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
Dong Zhuang
committed
create_flow_page_visit(request, flow_session, fpctx.page_data)
prev_answer_visits = list(
get_prev_answer_visits_qset(fpctx.page_data))
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
# {{{ 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
else:
answer_was_graded = False
page_behavior = get_page_behavior(
page=fpctx.page,
permissions=permissions,
session_in_progress=flow_session.in_progress,
Andreas Klöckner
committed
answer_was_graded=answer_was_graded,
generates_grade=generates_grade,
is_unenrolled_session=flow_session.participation is None,
viewing_prior_version=viewing_prior_version)
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
else:
feedback = 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", {})
feedback = None
# start common flow page generation
# defined at this point:
# 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,
shown_feedback = None
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
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."))
if form is not None:
form_html = fpctx.page.form_to_html(
pctx.request, page_context, form, answer_data)
else:
form_html = None
expiration_mode_choices = []
for key, descr in FLOW_SESSION_EXPIRATION_MODE_CHOICES:
if is_expiration_mode_allowed(key, permissions):
expiration_mode_choices.append((key, descr))
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)
from django.db import connection
with connection.cursor() as c:
c.execute(
"SELECT DISTINCT course_flowpagedata.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.ordinal",
[flow_session.id])
flow_page_ordinals_with_answers = set(row[0] for row in c.fetchall())
"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,
"flow_page_ordinals_with_answers": flow_page_ordinals_with_answers,
"feedback": shown_feedback,
"correct_answer": correct_answer,
"show_correctness": page_behavior.show_correctness,
"may_change_answer": page_behavior.may_change_answer,
"may_change_graded_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,
"may_send_email_about_flow_page":
may_send_email_about_flow_page(permissions),
"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,
"flow_session_interaction_kind": flow_session_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():
args["max_points"] = fpctx.page.max_points(fpctx.page_data)
args["page_expect_answer_and_gradable"] = True
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, # type: FlowSession
fpctx, # type: FlowPageContext
request, # type: http.HttpRequest
permissions, # type: frozenset[Text]
generates_grade, # type: bool
):
# type: (...) -> Tuple[PageBehavior, List[FlowPageVisit], forms.Form, Optional[AnswerFeedback], Any, bool] # noqa
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