Newer
Older
from crispy_forms.layout import Submit
form.helper.add_input(
if will_receive_feedback(permissions):
if flow_permission.change_answer in permissions:
"submit", _("Submit answer for grading"),
form.helper.add_input(
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"),
" »")),
return form
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"))
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):
request = pctx.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,
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
now_datetime = get_now_or_fake_time(request)
access_rule = get_session_access_rule(
Andreas Klöckner
committed
flow_session, pctx.role, fpctx.flow_desc, now_datetime,
facilities=pctx.request.relate_facilities)
Andreas Klöckner
committed
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)
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)
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"))
prev_answer_visits = list(
get_prev_answer_visits_qset(fpctx.page_data))
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)
submission_allowed = True
# reject answer update if permission not present
if flow_permission.submit_answer not in permissions:
messages.add_message(request, messages.ERROR,
# reject if previous answer was final
if (prev_answer_visits
and prev_answer_visits[0].is_submitted_answer
and flow_permission.change_answer
messages.add_message(request, messages.ERROR,
page_behavior = get_page_behavior(
page=fpctx.page,
permissions=permissions,
session_in_progress=flow_session.in_progress,
Andreas Klöckner
committed
answer_was_graded=False,
generates_grade=generates_grade,
Andreas Klöckner
committed
is_unenrolled_session=flow_session.participation is None)
form = fpctx.page.process_form_post(
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
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,
Andreas Klöckner
committed
answer_was_graded=answer_was_graded,
generates_grade=generates_grade,
Andreas Klöckner
committed
is_unenrolled_session=flow_session.participation is None)
if fpctx.page.is_answer_gradable():
feedback = fpctx.page.grade(
page_context, page_data.data, answer_visit.answer,
if answer_visit.is_submitted_answer:
grade.max_points = fpctx.page.max_points(page_data.data)
grade.graded_at_git_commit_sha = pctx.course_commit_sha
bulk_feedback_json = None
if feedback is not None:
grade.correctness = feedback.correctness
grade.feedback, bulk_feedback_json = feedback.as_json()
update_bulk_feedback(page_data, grade, bulk_feedback_json)
del grade
else:
feedback = None
if (pressed_button == "save_and_next"
and not will_receive_feedback(permissions)):
return redirect("relate-view_flow_page",
pctx.course.identifier,
fpctx.ordinal + 1)
elif (pressed_button == "save_and_finish"
and not will_receive_feedback(permissions)):
return redirect("relate-finish_flow_session_view",
pctx.course.identifier, flow_session_id)
# 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(
answer_data, page_behavior)
# continue at common flow page generation below
# }}}
else:
# form did not validate
create_flow_page_visit(request, flow_session, fpctx.page_data)
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."))
# continue at common flow page generation below
else:
create_flow_page_visit(request, flow_session, fpctx.page_data)
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
# {{{ 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
form = fpctx.page.make_form(
page_context, page_data.data,
answer_data, page_behavior)
feedback = None
# start common flow page generation
# defined at this point:
# form, form_html, may_change_answer, answer_was_graded, feedback
if form is not None and 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."))
Andreas Klöckner
committed
# {{{ FIXME: This warning should be deleted after October 2015
flow_session.participation is None
and
fpctx.page.expects_answer()
and
page_behavior.may_change_answer
messages.add_message(request, messages.WARNING,
_("<p><b>WARNING!</b> What you enter on this page will not be "
"associated with your user account, likely because "
"you have not completed your enrollment in this course. "
"Any data you enter here will not be retrievable later "
"and will not be graded. If this is not what you intended, "
"save your work on this session now (outside of RELATE), "
"complete your enrollment in this course in RELATE, "
"and restart your work on this flow.</p>"
"<p> To confirm that you've "
"completed your enrollment, make sure there is no 'Sign in' "
"or 'Enroll' button at the top of the main course page.<p>"
"<p><b>In addition, you should immediately bookmark this page "
"to ensure you'll be able to return to your work.</b>"))
Andreas Klöckner
committed
# }}}
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
"flow_desc": fpctx.flow_desc,
"ordinal": fpctx.ordinal,
"page_data": fpctx.page_data,
"percentage": int(100*(fpctx.ordinal+1) / flow_session.page_count),
"flow_session": flow_session,
"page_numbers": zip(
range(flow_session.page_count),
range(1, flow_session.page_count+1)),
"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,
"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,
"interaction_kind": get_interaction_kind(
fpctx, flow_session, generates_grade),
"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)
return render_course_page(
pctx, "course/flow-page.html", args,
allow_instant_flow_requests=False)
@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,
participation=pctx.participation,
flow_session=flow_session)
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()
# {{{ view: finish flow
def finish_flow_session_view(pctx, flow_session_id):
Andreas Klöckner
committed
now_datetime = get_now_or_fake_time(pctx.request)
request = pctx.request
flow_session_id = int(flow_session_id)
flow_session = get_and_check_flow_session(
pctx, flow_session_id)
fctx = FlowContext(pctx.repo, pctx.course, flow_id,
participation=pctx.participation,
flow_session=flow_session)
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", ""))
(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_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)
Andreas Klöckner
committed
grade_info = finish_flow_session(
fctx, flow_session, grading_rule,
Andreas Klöckner
committed
now_datetime=now_datetime)
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
# {{{ 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))
else:
review_uri = reverse("relate-view_flow_page",
args=(
pctx.course.identifier,
flow_session.id,
0))
with translation.override(settings.RELATE_ADMIN_EMAIL_LOCALE):
from django.template.loader import render_to_string
message = render_to_string("course/submit-notify.txt", {
"course": fctx.course,
"flow_session": flow_session,
"review_uri": pctx.request.build_absolute_uri(review_uri)
})
from django.core.mail import EmailMessage
msg = EmailMessage(
string_concat("[%(identifier)s:%(flow_id)s] ",
_("Submission by %s") % flow_session.participation)
% {'identifier': fctx.course.identifier,
'flow_id': flow_session.flow_id},
message,
fctx.course.from_email,
fctx.flow_desc.notify_on_submit)
msg.bcc = [fctx.course.notify_email]
msg.send()
# }}}
if is_graded_flow:
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)
(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",
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, 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
return render_finish_response(
"course/flow-confirm-completion.html",
flow_session=flow_session,
answered_count=answered_count,
unanswered_count=unanswered_count,
total_count=answered_count+unanswered_count)
# {{{ view: regrade flow
class RegradeFlowForm(StyledForm):
def __init__(self, flow_ids, *args, **kwargs):
super(RegradeFlowForm, self).__init__(*args, **kwargs)
self.fields["flow_id"] = forms.ChoiceField(
choices=[(fid, fid) for fid in flow_ids],
initial=participation_role.student,
self.fields["access_rules_tag"] = forms.CharField(
help_text=_("If non-empty, limit the regrading to sessions "
"started under this 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")),
),
label=_("Regraded session in progress"))
if pctx.role != participation_role.instructor:
raise PermissionDenied(_("must be instructor to 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 "
"form_description": _("Regrade not-for-credit Flow Sessions"),