Newer
Older
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
_("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(
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."))
Dong Zhuang
committed
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(
Dong Zhuang
committed
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 translation.override(settings.RELATE_ADMIN_EMAIL_LOCALE):
feedback = fpctx.page.grade(
page_context, page_data.data, answer_visit.answer,
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
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
Dong Zhuang
committed
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)
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
# {{{ view: send interaction email to course staffs in flow pages
@course_view
def send_email_about_flow_page(pctx, flow_session_id, ordinal):
# {{{ check if interaction email is allowed for this page.
ordinal = int(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
fpctx = FlowPageContext(pctx.repo, pctx.course, flow_id, 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)
if not may_send_email_about_flow_page(permissions):
raise http.Http404()
# }}}
from django.conf import settings
from course.models import FlowSession
flow_session = get_object_or_404(
FlowSession, id=int(flow_session_id))
from course.models import FlowPageData
page_id = FlowPageData.objects.get(
flow_session=flow_session_id, ordinal=ordinal).page_id
from django.core.urlresolvers import reverse
review_url = reverse(
"relate-view_flow_page",
kwargs={'course_identifier': pctx.course.identifier,
'flow_session_id': flow_session_id,
'ordinal': ordinal
}
)
from six.moves.urllib.parse import urljoin
review_uri = urljoin(getattr(settings, "RELATE_BASE_URL"),
review_url)
if request.method == "POST":
form = FlowPageInteractionEmailForm(review_uri, request.POST)
if form.is_valid():
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
from_email = getattr(
settings,
"STUDENT_INTERACT_EMAIL_FROM",
settings.ROBOT_EMAIL_FROM)
student_email = flow_session.participation.user.email
from course.constants import (
participation_permission as pperm)
ta_email_list = Participation.objects.filter(
course=pctx.course,
roles__permissions__permission=pperm.assign_grade,
roles__identifier="ta",
).values_list("user__email", flat=True)
instructor_email_list = Participation.objects.filter(
course=pctx.course,
roles__permissions__permission=pperm.assign_grade,
roles__identifier="instructor"
).values_list("user__email", flat=True)
recipient_list = ta_email_list
if not recipient_list:
recipient_list = instructor_email_list
from django.utils import translation
with translation.override(settings.RELATE_ADMIN_EMAIL_LOCALE):
from django.template.loader import render_to_string
message = render_to_string(
"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,
"username": pctx.participation.user.get_full_name()
})
from django.core.mail import EmailMessage
msg = EmailMessage(
subject=string_concat(
"[%(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': pctx.participation.user.get_full_name()
},
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, ordinal)
form = FlowPageInteractionEmailForm(review_uri)
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(FlowPageInteractionEmailForm, self).__init__(*args, **kwargs)
self.fields["message"] = forms.CharField(
required=True,
widget=forms.Textarea,
_("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):
cleaned_data = super(FlowPageInteractionEmailForm, self).clean()
message = cleaned_data.get("message")
if len(message) < 20:
raise forms.ValidationError(
_("At least 20 characters are required for submission."))
return message
# }}}
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
# {{{ view: update page bookmark state
@course_view
def update_page_bookmark_state(pctx, flow_session_id, 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,
ordinal=ordinal)
fpd.bookmarked = bookmark_state
fpd.save()
return http.HttpResponse("OK")
# }}}
# {{{ view: update expiration mode
@course_view
def update_expiration_mode(pctx, flow_session_id):
Andreas Klöckner
committed
# type: (CoursePageContext, int) -> http.HttpRequest
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,
Andreas Klöckner
committed
participation=pctx.participation)
access_rule = get_session_access_rule(
flow_session, fctx.flow_desc,
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()
# {{{ view: finish flow
def finish_flow_session_view(pctx, flow_session_id):
Andreas Klöckner
committed
# type: (CoursePageContext, int) -> http.HttpResponse
Andreas Klöckner
committed
# Does not need to be atomic: All writing to the db
# is done in 'finish_flow_session' below.
Andreas Klöckner
committed
now_datetime = get_now_or_fake_time(pctx.request)
login_exam_ticket = get_login_exam_ticket(pctx.request)
Andreas Klöckner
committed
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,
Andreas Klöckner
committed
participation=pctx.participation)
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", ""))
Andreas Klöckner
committed
adjust_flow_session_page_data(pctx.repo, flow_session, pctx.course.identifier,
fctx.flow_desc, respect_preview=True)
Andreas Klöckner
committed
answer_visits = assemble_answer_visits(flow_session) # type: List[Optional[FlowPageVisit]] # noqa
(answered_page_data_list, unanswered_page_data_list) =\
get_session_answered_page_data(
fctx, flow_session, answer_visits)
answered_count = len(answered_page_data_list)
unanswered_count = len(unanswered_page_data_list)
is_interactive_flow = bool(answered_count + unanswered_count) # type: bool
if flow_permission.view not in access_rule.permissions:
raise PermissionDenied()
def render_finish_response(template, **kwargs):
Andreas Klöckner
committed
# type: (...) -> http.HttpResponse
"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"))
Andreas Klöckner
committed
grade_info = finish_flow_session(
fctx, flow_session, grading_rule,
Andreas Klöckner
committed
now_datetime=now_datetime)
# {{{ 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.grade_identifier,
grading_rule.grade_aggregation_strategy).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 %(participation)s"))
% {'participation': flow_session.participation,
'identifier': fctx.course.identifier,
'flow_id': flow_session.flow_id},
message,
getattr(settings, "NOTIFICATION_EMAIL_FROM",
settings.ROBOT_EMAIL_FROM),
fctx.flow_desc.notify_on_submit)
msg.bcc = [fctx.course.notify_email]
from relate.utils import get_outbound_mail_connection
get_outbound_mail_connection("notification")
if hasattr(settings, "NOTIFICATION_EMAIL_FROM")
else get_outbound_mail_connection("robot"))
if is_interactive_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)
if (not is_interactive_flow
(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, 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
return render_finish_response(
"course/flow-confirm-completion.html",
flow_session=flow_session,
answered_count=answered_count,
unanswered_count=unanswered_count,
unanswered_page_data_list=unanswered_page_data_list,
total_count=answered_count+unanswered_count)
# {{{ view: regrade flow
class RegradeFlowForm(StyledForm):
def __init__(self, flow_ids, *args, **kwargs):
Andreas Klöckner
committed
# type: (List[Text], *Any, **Any) -> None
super(RegradeFlowForm, self).__init__(*args, **kwargs)
self.fields["flow_id"] = forms.ChoiceField(
choices=[(fid, fid) for fid in flow_ids],
label=_("Flow ID"),
widget=Select2Widget())
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"))
Andreas Klöckner
committed
# type: (CoursePageContext) -> http.HttpResponse
if not pctx.has_permission(pperm.batch_regrade_flow_session):
Andreas Klöckner
committed
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 "
"form_description": _("Regrade not-for-credit Flow Sessions"),