def flow_analytics(pctx, flow_id): if pctx.role not in [ participation_role.teaching_assistant, participation_role.instructor, participation_role.observer, ]: raise PermissionDenied(_("must be at least TA to view analytics")) restrict_to_first_attempt = int( bool(pctx.request.GET.get("restrict_to_first_attempt") == "1")) try: stats_list = make_page_answer_stats_list(pctx, flow_id, restrict_to_first_attempt) except ObjectDoesNotExist: messages.add_message(pctx.request, messages.ERROR, _("Flow '%s' was not found in the repository, but it exists in " "the database--maybe it was deleted?") % flow_id) raise http.Http404() return render_course_page(pctx, "course/analytics-flow.html", { "flow_identifier": flow_id, "grade_histogram": make_grade_histogram(pctx, flow_id), "page_answer_stats_list": stats_list, "time_histogram": make_time_histogram(pctx, flow_id), "participant_count": count_participants(pctx, flow_id), "restrict_to_first_attempt": restrict_to_first_attempt, })
def test_flow(pctx): if not pctx.has_permission(pperm.test_flow): raise PermissionDenied() 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 = FlowTestForm(flow_ids, request.POST, request.FILES) if "test" not in request.POST: raise SuspiciousOperation(_("invalid operation")) if form.is_valid(): return redirect("relate-view_start_flow", pctx.course.identifier, form.cleaned_data["flow_id"]) else: form = FlowTestForm(flow_ids) return render_course_page(pctx, "course/generic-course-form.html", { "form": form, "form_description": _("Test Flow"), })
def grant_exception(pctx): if pctx.role not in [ participation_role.instructor, participation_role.teaching_assistant]: raise PermissionDenied("must be instructor or TA to grant exceptions") 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 = ExceptionStage1Form(pctx.course, flow_ids, request.POST) if form.is_valid(): return redirect("course.views.grant_exception_stage_2", pctx.course.identifier, form.cleaned_data["participation"].id, form.cleaned_data["flow_id"]) else: form = ExceptionStage1Form(pctx.course, flow_ids) return render_course_page(pctx, "course/generic-course-form.html", { "form": form, "form_description": "Grant Exception", })
def course_page(pctx): from course.content import get_processed_course_chunks chunks = get_processed_course_chunks( pctx.course, pctx.repo, pctx.course_commit_sha, pctx.course_desc, pctx.role, get_now_or_fake_time(pctx.request), facilities=pctx.request.relate_facilities) show_enroll_button = ( pctx.course.accepts_enrollment and pctx.role == participation_role.unenrolled) if pctx.request.user.is_authenticated() and Participation.objects.filter( user=pctx.request.user, course=pctx.course, status=participation_status.requested).count(): show_enroll_button = False messages.add_message(pctx.request, messages.INFO, _("Your enrollment request is pending. You will be " "notified once it has been acted upon.")) return render_course_page(pctx, "course/course-page.html", { "chunks": chunks, "show_enroll_button": show_enroll_button, })
def course_page(pctx): from course.content import get_processed_course_chunks chunks = get_processed_course_chunks( pctx.course, pctx.repo, pctx.course_commit_sha, pctx.course_desc, pctx.role, get_now_or_fake_time(pctx.request)) return render_course_page(pctx, "course/course-page.html", { "chunks": chunks, })
def create_recurring_events(pctx): if not pctx.has_permission(pperm.edit_events): raise PermissionDenied(_("may not edit events")) request = pctx.request if request.method == "POST": form = RecurringEventForm(request.POST, request.FILES) if form.is_valid(): if form.cleaned_data["starting_ordinal"] is not None: starting_ordinal = form.cleaned_data["starting_ordinal"] starting_ordinal_specified = True else: starting_ordinal = 1 starting_ordinal_specified = False while True: try: _create_recurring_events_backend( course=pctx.course, time=form.cleaned_data["time"], kind=form.cleaned_data["kind"], starting_ordinal=starting_ordinal, interval=form.cleaned_data["interval"], count=form.cleaned_data["count"], duration_in_minutes=(form.cleaned_data["duration_in_minutes"]), ) except EventAlreadyExists as e: if starting_ordinal_specified: messages.add_message( request, messages.ERROR, string_concat("%(err_type)s: %(err_str)s. ", _("No events created.")) % {"err_type": type(e).__name__, "err_str": str(e)}, ) else: starting_ordinal += 10 continue except Exception as e: messages.add_message( request, messages.ERROR, string_concat("%(err_type)s: %(err_str)s. ", _("No events created.")) % {"err_type": type(e).__name__, "err_str": str(e)}, ) else: messages.add_message(request, messages.SUCCESS, _("Events created.")) break else: form = RecurringEventForm() return render_course_page( pctx, "course/generic-course-form.html", {"form": form, "form_description": _("Create recurring events")} )
def render_finish_response(template, **kwargs): render_args = { "flow_identifier": fctx.flow_identifier, "flow_desc": fctx.flow_desc, } render_args.update(kwargs) return render_course_page( pctx, template, render_args, allow_instant_flow_requests=False)
def course_page(pctx): # type: (CoursePageContext) -> http.HttpResponse from course.content import get_processed_page_chunks, get_course_desc page_desc = get_course_desc(pctx.repo, pctx.course, pctx.course_commit_sha) chunks = get_processed_page_chunks( pctx.course, pctx.repo, pctx.course_commit_sha, page_desc, pctx.role_identifiers(), get_now_or_fake_time(pctx.request), facilities=pctx.request.relate_facilities) show_enroll_button = ( pctx.course.accepts_enrollment and pctx.participation is None) if pctx.request.user.is_authenticated and Participation.objects.filter( user=pctx.request.user, course=pctx.course, status=participation_status.requested).count(): show_enroll_button = False messages.add_message(pctx.request, messages.INFO, _("Your enrollment request is pending. You will be " "notified once it has been acted upon.")) from course.models import ParticipationPreapproval if ParticipationPreapproval.objects.filter( course=pctx.course).exclude(institutional_id=None).count(): if not pctx.request.user.institutional_id: from django.urls import reverse messages.add_message(pctx.request, messages.WARNING, _("This course uses institutional ID for " "enrollment preapproval, please <a href='%s' " "role='button' class='btn btn-md btn-primary'>" "fill in your institutional ID »" "</a> in your profile.") % ( reverse("relate-user_profile") + "?referer=" + pctx.request.path + "&set_inst_id=1" ) ) else: if pctx.course.preapproval_require_verified_inst_id: messages.add_message(pctx.request, messages.WARNING, _("Your institutional ID is not verified or " "preapproved. Please contact your course " "staff.") ) return render_course_page(pctx, "course/course-page.html", { "chunks": chunks, "show_enroll_button": show_enroll_button, })
def flow_analytics(pctx, flow_identifier): if pctx.role not in [ participation_role.teaching_assistant, participation_role.instructor]: raise PermissionDenied("must be at least TA to view analytics") return render_course_page(pctx, "course/analytics-flow.html", { "flow_identifier": flow_identifier, "grade_histogram": make_grade_histogram(pctx, flow_identifier), "page_answer_stats_list": make_page_answer_stats_list(pctx, flow_identifier), "time_histogram": make_time_histogram(pctx, flow_identifier), })
def create_preapprovals(pctx): if pctx.role != participation_role.instructor: raise PermissionDenied(_("only instructors may do that")) request = pctx.request if request.method == "POST": form = BulkPreapprovalsForm(request.POST) if form.is_valid(): created_count = 0 exist_count = 0 role = form.cleaned_data["role"] for l in form.cleaned_data["emails"].split("\n"): l = l.strip() if not l: continue try: preapproval = ParticipationPreapproval.objects.get( email__iexact=l, course=pctx.course) except ParticipationPreapproval.DoesNotExist: pass else: exist_count += 1 continue preapproval = ParticipationPreapproval() preapproval.email = l preapproval.course = pctx.course preapproval.role = role preapproval.creator = request.user preapproval.save() created_count += 1 messages.add_message(request, messages.INFO, _("%(n_created)d preapprovals created, " "%(n_exist)d already existed.") % { 'n_created': created_count, 'n_exist': exist_count}) return redirect("relate-home") else: form = BulkPreapprovalsForm() return render_course_page(pctx, "course/generic-course-form.html", { "form": form, "form_description": _("Create Participation Preapprovals"), })
def manage_instant_flow_requests(pctx): if pctx.role != participation_role.instructor: raise PermissionDenied( _("must be instructor to manage instant flow requests")) 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 = InstantFlowRequestForm(flow_ids, request.POST, request.FILES) if "add" in request.POST: op = "add" elif "cancel" in request.POST: op = "cancel" else: raise SuspiciousOperation(_("invalid operation")) now_datetime = get_now_or_fake_time(pctx.request) if form.is_valid(): if op == "add": from datetime import timedelta ifr = InstantFlowRequest() ifr.course = pctx.course ifr.flow_id = form.cleaned_data["flow_id"] ifr.start_time = now_datetime ifr.end_time = ( now_datetime + timedelta( minutes=form.cleaned_data["duration_in_minutes"])) ifr.save() elif op == "cancel": (InstantFlowRequest.objects .filter( course=pctx.course, start_time__lte=now_datetime, end_time__gte=now_datetime, cancelled=False) .order_by("start_time") .update(cancelled=True)) else: raise SuspiciousOperation(_("invalid operation")) else: form = InstantFlowRequestForm(flow_ids) return render_course_page(pctx, "course/generic-course-form.html", { "form": form, "form_description": _("Manage Instant Flow Requests"), })
def flow_list(pctx): if not pctx.has_permission(pperm.view_analytics): raise PermissionDenied(_("may not view analytics")) cursor = connection.cursor() cursor.execute("select distinct flow_id from course_flowsession " "where course_id=%s order by flow_id", [pctx.course.id]) flow_ids = [row[0] for row in cursor.fetchall()] return render_course_page(pctx, "course/analytics-flows.html", { "flow_ids": flow_ids, })
def view_markup_sandbox(pctx): if pctx.role not in [ participation_role.instructor, participation_role.teaching_assistant]: raise PermissionDenied( ugettext("must be instructor or TA to access sandbox")) request = pctx.request preview_text = "" from course.models import get_user_status ustatus = get_user_status(request.user) def make_form(data=None): help_text = (ugettext("Enter <a href=\"http://documen.tician.de/" "relate/content.html#relate-markup\">" "RELATE markup</a>.")) return SandboxForm( None, "markdown", ustatus.editor_mode, help_text, data) if request.method == "POST": form = make_form(request.POST) if form.is_valid(): from course.content import markup_to_html try: preview_text = markup_to_html( pctx.course, pctx.repo, pctx.course_commit_sha, form.cleaned_data["content"]) except: import sys tp, e, _ = sys.exc_info() messages.add_message(pctx.request, messages.ERROR, ugettext("Markup failed to render") + ": " + "%(err_type)s: %(err_str)s" % { "err_type": tp.__name__, "err_str": e}) form = make_form(request.POST) else: form = make_form() return render_course_page(pctx, "course/sandbox-markup.html", { "form": form, "preview_text": preview_text, })
def create_recurring_events(pctx): if pctx.role != participation_role.instructor: raise PermissionDenied("only instructors may do that") request = pctx.request if request.method == "POST": form = RecurringEventForm(request.POST, request.FILES) if form.is_valid(): time = form.cleaned_data["time"] ordinal = form.cleaned_data["starting_ordinal"] interval = form.cleaned_data["interval"] import datetime for i in xrange(form.cleaned_data["count"]): evt = Event() evt.course = pctx.course evt.kind = form.cleaned_data["kind"] evt.ordinal = ordinal evt.time = time if form.cleaned_data["duration_in_minutes"]: evt.end_time = evt.time + datetime.timedelta( minutes=form.cleaned_data["duration_in_minutes"]) evt.save() if interval == "weekly": date = time.date() date += datetime.timedelta(weeks=1) time = time.tzinfo.localize( datetime.datetime(date.year, date.month, date.day, time.hour, time.minute, time.second)) del date else: raise ValueError("unknown interval: %s" % interval) ordinal += 1 messages.add_message(request, messages.SUCCESS, "Events created.") else: form = RecurringEventForm() return render_course_page(pctx, "course/generic-course-form.html", { "form": form, "form_description": "Create recurring events", })
def import_grades(pctx): if pctx.role != participation_role.instructor: raise PermissionDenied() form_text = "" request = pctx.request if request.method == "POST": form = ImportGradesForm( pctx.course, request.POST, request.FILES) is_import = "import" in request.POST if form.is_valid(): try: grade_changes = csv_to_grade_changes( course=pctx.course, grading_opportunity=form.cleaned_data["grading_opportunity"], attempt_id=form.cleaned_data["attempt_id"], file_contents=request.FILES["file"], id_column=form.cleaned_data["id_column"], points_column=form.cleaned_data["points_column"], feedback_column=form.cleaned_data["feedback_column"], max_points=form.cleaned_data["max_points"], creator=request.user, grade_time=now(), has_header=form.cleaned_data["format"] == "csvhead") except Exception as e: messages.add_message(pctx.request, messages.ERROR, "Error: %s %s" % (type(e).__name__, str(e))) else: if is_import: GradeChange.objects.bulk_create(grade_changes) messages.add_message(pctx.request, messages.SUCCESS, "%d grades imported." % len(grade_changes)) else: from django.template.loader import render_to_string form_text = render_to_string( "course/grade-import-preview.html", { "grade_changes": grade_changes, }) else: form = ImportGradesForm(pctx.course) return render_course_page(pctx, "course/generic-course-form.html", { "form_description": "Import Grade Data", "form": form, "form_text": form_text, })
def renumber_events(pctx): if pctx.role not in [ participation_role.instructor, participation_role.teaching_assistant]: raise PermissionDenied(_("only instructors and TAs may do that")) request = pctx.request if request.method == "POST": form = RenumberEventsForm(request.POST, request.FILES) if form.is_valid(): events = list(Event.objects .filter(course=pctx.course, kind=form.cleaned_data["kind"]) .order_by('time')) if events: queryset = (Event.objects .filter(course=pctx.course, kind=form.cleaned_data["kind"])) queryset.delete() ordinal = form.cleaned_data["starting_ordinal"] for event in events: new_event = Event() new_event.course = pctx.course new_event.kind = form.cleaned_data["kind"] new_event.ordinal = ordinal new_event.time = event.time new_event.end_time = event.end_time new_event.all_day = event.all_day new_event.shown_in_calendar = event.shown_in_calendar new_event.save() ordinal += 1 messages.add_message(request, messages.SUCCESS, _("Events renumbered.")) else: messages.add_message(request, messages.ERROR, _("No events found.")) else: form = RenumberEventsForm() return render_course_page(pctx, "course/generic-course-form.html", { "form": form, "form_description": _("Renumber events"), })
def view_grading_opportunity_list(pctx): if pctx.role not in [ participation_role.instructor, participation_role.teaching_assistant]: raise PermissionDenied(_("must be instructor or TA to view grades")) grading_opps = list((GradingOpportunity.objects .filter( course=pctx.course, shown_in_grade_book=True, ) .order_by("identifier"))) return render_course_page(pctx, "course/gradebook-opp-list.html", { "grading_opps": grading_opps, })
def flow_list(pctx): if pctx.role not in [ participation_role.teaching_assistant, participation_role.instructor]: raise PermissionDenied("must be at least TA to view analytics") cursor = connection.cursor() cursor.execute("select distinct flow_id from course_flowsession " "where course_id=%s order by flow_id", [pctx.course.id]) flow_ids = [row[0] for row in cursor.fetchall()] return render_course_page(pctx, "course/analytics-flows.html", { "flow_ids": flow_ids, })
def view_participant_list(pctx): if pctx.role not in [ participation_role.instructor, participation_role.teaching_assistant]: raise PermissionDenied(_("must be instructor or TA to view grades")) participations = list(Participation.objects .filter( course=pctx.course, status=participation_status.active) .order_by("id") .select_related("user")) return render_course_page(pctx, "course/gradebook-participant-list.html", { "participations": participations, })
def static_page(pctx, page_path): from course.content import get_staticpage_desc, get_processed_page_chunks try: page_desc = get_staticpage_desc(pctx.repo, pctx.course, pctx.course_commit_sha, "staticpages/"+page_path+".yml") except ObjectDoesNotExist: raise http.Http404() chunks = get_processed_page_chunks( pctx.course, pctx.repo, pctx.course_commit_sha, page_desc, pctx.role, get_now_or_fake_time(pctx.request), facilities=pctx.request.relate_facilities) return render_course_page(pctx, "course/static-page.html", { "chunks": chunks, "show_enroll_button": False, })
def regrade_not_for_credit_flows_view(pctx): 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(): sessions = FlowSession.objects.filter(course=pctx.course, flow_id=form.cleaned_data["flow_id"]) if form.cleaned_data["access_rules_tag"]: sessions = sessions.filter(access_rules_tag=form.cleaned_data["access_rules_tag"]) inprog_value = {"any": None, "yes": True, "no": False}[form.cleaned_data["regraded_session_in_progress"]] if inprog_value is not None: sessions = sessions.filter(in_progress=inprog_value) count = _regrade_sessions(pctx.repo, pctx.course, sessions) messages.add_message(request, messages.SUCCESS, _("%d sessions regraded.") % count) 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." ), "</p>", ), "form_description": _("Regrade not-for-credit Flow Sessions"), }, )
def edit_course(pctx): if not pctx.has_permission(pperm.edit_course): raise PermissionDenied() request = pctx.request if request.method == 'POST': form = EditCourseForm(request.POST, instance=pctx.course) if form.is_valid(): form.save() else: form = EditCourseForm(instance=pctx.course) return render_course_page(pctx, "course/generic-course-form.html", { "form_description": _("Edit Course"), "form": form })
def view_markup_sandbox(pctx): if not pctx.has_permission(pperm.use_markup_sandbox): raise PermissionDenied() request = pctx.request preview_text = "" def make_form(data=None): help_text = (ugettext("Enter <a href=\"http://documen.tician.de/" "relate/content.html#relate-markup\">" "RELATE markup</a>.")) return SandboxForm( None, "markdown", request.user.editor_mode, help_text, data) if request.method == "POST" and "preview" in request.POST: form = make_form(request.POST) if form.is_valid(): from course.content import markup_to_html try: preview_text = markup_to_html( pctx.course, pctx.repo, pctx.course_commit_sha, form.cleaned_data["content"]) except: import sys tp, e, _ = sys.exc_info() messages.add_message(pctx.request, messages.ERROR, ugettext("Markup failed to render") + ": " + "%(err_type)s: %(err_str)s" % { "err_type": tp.__name__, "err_str": e}) form = make_form(request.POST) else: form = make_form() return render_course_page(pctx, "course/sandbox-markup.html", { "form": form, "preview_text": preview_text, })
def view_markup_sandbox(pctx): request = pctx.request preview_text = "" def make_form(data=None): help_text = ("Enter <a href=\"http://documen.tician.de/" "courseflow/content.html#courseflow-markup\">" "CourseFlow markup</a>.") return SandboxForm( None, "markdown", vim_mode, help_text, data) vim_mode = pctx.request.session.get(CF_SANDBOX_VIM_MODE, False) if request.method == "POST": form = make_form(request.POST) if form.is_valid(): pctx.request.session[CF_SANDBOX_VIM_MODE] = \ vim_mode = form.cleaned_data["vim_mode"] from course.content import markup_to_html try: preview_text = markup_to_html( pctx.course, pctx.repo, pctx.course_commit_sha, form.cleaned_data["content"]) except: import sys tp, e, _ = sys.exc_info() messages.add_message(pctx.request, messages.ERROR, "Markup failed to render: " "%s: %s" % (tp.__name__, e)) form = make_form(request.POST) else: form = make_form() return render_course_page(pctx, "course/sandbox-markup.html", { "form": form, "preview_text": preview_text, })
def renumber_events(pctx): if pctx.role != participation_role.instructor: raise PermissionDenied("only instructors may do that") request = pctx.request if request.method == "POST": form = RenumberEventsForm(request.POST, request.FILES) if form.is_valid(): labels = list(Event.objects .filter(course=pctx.course, kind=form.cleaned_data["kind"]) .order_by('time')) if labels: queryset = (Event.objects .filter(course=pctx.course, kind=form.cleaned_data["kind"])) queryset.delete() ordinal = form.cleaned_data["starting_ordinal"] for label in labels: new_label = Event() new_label.course = pctx.course new_label.kind = form.cleaned_data["kind"] new_label.ordinal = ordinal new_label.time = label.time new_label.save() ordinal += 1 messages.add_message(request, messages.SUCCESS, "Events renumbered.") else: messages.add_message(request, messages.ERROR, "No events found.") else: form = RenumberEventsForm() return render_course_page(pctx, "course/generic-course-form.html", { "form": form, "form_description": "Renumber events", })
def grant_exception_stage_2(pctx, participation_id, flow_id): if pctx.role not in [ participation_role.instructor, participation_role.teaching_assistant]: raise PermissionDenied("must be instructor or TA to grant exceptions") participation = get_object_or_404(Participation, id=participation_id) from course.content import get_flow_desc try: flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) except ObjectDoesNotExist: raise http.Http404() if not hasattr(flow_desc, "access_rules"): messages.add_message(pctx.request, messages.ERROR, "Flow '%s' does not declare access rules." % flow_id) return redirect("course.views.grant_exception", pctx.course.identifier) base_ruleset_choices = [rule.id for rule in flow_desc.access_rules] request = pctx.request if request.method == "POST": form = ExceptionStage2Form(base_ruleset_choices, request.POST) if form.is_valid(): return redirect( "course.views.grant_exception_stage_3", pctx.course.identifier, participation.id, flow_id, form.cleaned_data["base_ruleset"]) else: form = ExceptionStage2Form(base_ruleset_choices) return render_course_page(pctx, "course/generic-course-form.html", { "form": form, "form_description": "Grant Exception", })
def view_gradebook(pctx): if pctx.role not in [ participation_role.instructor, participation_role.teaching_assistant]: raise PermissionDenied(_("must be instructor or TA to view grades")) participations, grading_opps, grade_table = get_grade_table(pctx.course) grade_table = sorted(zip(participations, grade_table), key=lambda (participation, grades): (participation.user.last_name.lower(), participation.user.first_name.lower())) return render_course_page(pctx, "course/gradebook.html", { "grade_table": grade_table, "grading_opportunities": grading_opps, "participations": participations, "grade_state_change_types": grade_state_change_types, })
def flow_analytics(pctx, flow_id): if pctx.role not in [ participation_role.teaching_assistant, participation_role.instructor, participation_role.observer, ]: raise PermissionDenied(_("must be at least TA to view analytics")) restrict_to_first_attempt = int( bool(pctx.request.GET.get("restrict_to_first_attempt") == "1")) return render_course_page(pctx, "course/analytics-flow.html", { "flow_identifier": flow_id, "grade_histogram": make_grade_histogram(pctx, flow_id), "page_answer_stats_list": make_page_answer_stats_list(pctx, flow_id, restrict_to_first_attempt), "time_histogram": make_time_histogram(pctx, flow_id), "participant_count": count_participants(pctx, flow_id), "restrict_to_first_attempt": restrict_to_first_attempt, })
def renumber_events(pctx): if not pctx.has_permission(pperm.edit_events): raise PermissionDenied(_("may not edit events")) request = pctx.request if request.method == "POST": form = RenumberEventsForm(request.POST, request.FILES) if form.is_valid(): events = list(Event.objects.filter(course=pctx.course, kind=form.cleaned_data["kind"]).order_by("time")) if events: queryset = Event.objects.filter(course=pctx.course, kind=form.cleaned_data["kind"]) queryset.delete() ordinal = form.cleaned_data["starting_ordinal"] for event in events: new_event = Event() new_event.course = pctx.course new_event.kind = form.cleaned_data["kind"] new_event.ordinal = ordinal new_event.time = event.time new_event.end_time = event.end_time new_event.all_day = event.all_day new_event.shown_in_calendar = event.shown_in_calendar new_event.save() ordinal += 1 messages.add_message(request, messages.SUCCESS, _("Events renumbered.")) else: messages.add_message(request, messages.ERROR, _("No events found.")) else: form = RenumberEventsForm() return render_course_page( pctx, "course/generic-course-form.html", {"form": form, "form_description": _("Renumber events")} )
def check_events(pctx): if pctx.role != participation_role.instructor: raise PermissionDenied("only instructors may do that") invalid_datespecs = {} from course.content import InvalidDatespec, parse_date_spec def datespec_callback(location, datespec): try: parse_date_spec(pctx.course, datespec, return_now_on_error=False) except InvalidDatespec as e: invalid_datespecs.setdefault(e.datespec, []).append(location) from course.validation import validate_course_content validate_course_content( pctx.repo, pctx.course.course_file, pctx.course.events_file, pctx.course_commit_sha, datespec_callback=datespec_callback) return render_course_page(pctx, "course/invalid-datespec-list.html", { "invalid_datespecs": sorted(invalid_datespecs.iteritems()), })
def static_page(pctx, page_path): # type: (CoursePageContext, Text) -> http.HttpResponse from course.content import get_staticpage_desc, get_processed_page_chunks try: page_desc = get_staticpage_desc(pctx.repo, pctx.course, pctx.course_commit_sha, "staticpages/" + page_path + ".yml") except ObjectDoesNotExist: raise http.Http404() chunks = get_processed_page_chunks( pctx.course, pctx.repo, pctx.course_commit_sha, page_desc, pctx.role_identifiers(), get_now_or_fake_time(pctx.request), facilities=pctx.request.relate_facilities) return render_course_page(pctx, "course/static-page.html", { "chunks": chunks, "show_enroll_button": False })
def view_gradebook(pctx): if pctx.role not in [ participation_role.instructor, participation_role.teaching_assistant ]: raise PermissionDenied(_("must be instructor or TA to view grades")) participations, grading_opps, grade_table = get_grade_table(pctx.course) def grade_key(entry): (participation, grades) = entry return (participation.user.last_name.lower(), participation.user.first_name.lower()) grade_table = sorted(zip(participations, grade_table), key=grade_key) return render_course_page( pctx, "course/gradebook.html", { "grade_table": grade_table, "grading_opportunities": grading_opps, "participations": participations, "grade_state_change_types": grade_state_change_types, })
def grant_exception(pctx): if not pctx.has_permission(pperm.grant_exception): raise PermissionDenied(_("may not grant exceptions")) 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 = ExceptionStage1Form(pctx.course, flow_ids, request.POST) if form.is_valid(): return redirect("relate-grant_exception_stage_2", pctx.course.identifier, form.cleaned_data["participation"].id, form.cleaned_data["flow_id"]) else: form = ExceptionStage1Form(pctx.course, flow_ids) return render_course_page(pctx, "course/generic-course-form.html", { "form": form, "form_description": _("Grant Exception"), })
def update_course(pctx): if not (pctx.has_permission(pperm.update_content) or pctx.has_permission(pperm.preview_content)): raise PermissionDenied() course = pctx.course request = pctx.request content_repo = pctx.repo from course.content import SubdirRepoWrapper if isinstance(content_repo, SubdirRepoWrapper): repo = content_repo.repo else: repo = content_repo participation = pctx.participation previewing = bool(participation is not None and participation.preview_git_commit_sha) may_update = pctx.has_permission(pperm.update_content) response_form = None form = None if request.method == "POST": form = GitUpdateForm(may_update, previewing, repo, request.POST, request.FILES) command = None for cmd in ALLOWED_COURSE_REVISIOIN_COMMANDS: if cmd in form.data: command = cmd break if command is None: raise SuspiciousOperation(_("invalid command")) if form.is_valid(): new_sha = form.cleaned_data["new_sha"].encode() try: run_course_update_command( request, repo, content_repo, pctx, command, new_sha, may_update, prevent_discarding_revisions=form. cleaned_data["prevent_discarding_revisions"]) except Exception as e: import traceback traceback.print_exc() messages.add_message( pctx.request, messages.ERROR, string_concat( pgettext("Starting of Error message", "Error"), ": %(err_type)s %(err_str)s") % { "err_type": type(e).__name__, "err_str": str(e) }) else: response_form = form if response_form is None: previewing = bool(participation is not None and participation.preview_git_commit_sha) form = GitUpdateForm( may_update, previewing, repo, { "new_sha": repo.head().decode(), "prevent_discarding_revisions": True, }) from django.template.loader import render_to_string form_text = render_to_string( "course/git-sha-table.html", { "participation": participation, "is_previewing": previewing, "course": course, "repo": repo, "current_git_head": repo.head().decode(), "git_url": request.build_absolute_uri( reverse("relate-git_endpoint", args=(course.identifier, ""))), "token_url": reverse("relate-manage_authentication_tokens", args=(course.identifier, )), }) assert form is not None return render_course_page( pctx, "course/generic-course-form.html", { "form": form, "form_text": form_text, "form_description": gettext("Update Course Revision"), })
def grade_flow_page(pctx, flow_session_id, page_ordinal): # type: (CoursePageContext, int, int) -> http.HttpResponse now_datetime = get_now_or_fake_time(pctx.request) page_ordinal = int(page_ordinal) viewing_prev_grade = False prev_grade_id = pctx.request.GET.get("grade_id") if prev_grade_id is not None: try: prev_grade_id = int(prev_grade_id) viewing_prev_grade = True except ValueError: raise SuspiciousOperation("non-integer passed for 'grade_id'") if not pctx.has_permission(pperm.view_gradebook): raise PermissionDenied(_("may not view grade book")) flow_session = get_object_or_404(FlowSession, id=int(flow_session_id)) if flow_session.course.pk != pctx.course.pk: raise SuspiciousOperation( _("Flow session not part of specified course")) if flow_session.participation is None: raise SuspiciousOperation(_("Cannot grade anonymous session")) from course.flow import adjust_flow_session_page_data adjust_flow_session_page_data(pctx.repo, flow_session, pctx.course.identifier, respect_preview=False) fpctx = FlowPageContext(pctx.repo, pctx.course, flow_session.flow_id, page_ordinal, participation=flow_session.participation, flow_session=flow_session, request=pctx.request) if fpctx.page_desc is None: raise http.Http404() assert fpctx.page is not None assert fpctx.page_context is not None # {{{ enable flow session zapping all_flow_sessions = list( FlowSession.objects.filter(course=pctx.course, flow_id=flow_session.flow_id, participation__isnull=False, in_progress=flow_session.in_progress). order_by( # Datatables will default to sorting the user list # by the first column, which happens to be the username. # Match that sorting. "participation__user__username", "start_time")) next_flow_session_id = None prev_flow_session_id = None for i, other_flow_session in enumerate(all_flow_sessions): if other_flow_session.pk == flow_session.pk: if i > 0: prev_flow_session_id = all_flow_sessions[i - 1].id if i + 1 < len(all_flow_sessions): next_flow_session_id = all_flow_sessions[i + 1].id # }}} prev_grades = get_prev_visit_grades(pctx.course_identifier, flow_session_id, page_ordinal) # {{{ reproduce student view form = None feedback = None answer_data = None grade_data = None shown_grade = None page_expects_answer = fpctx.page.expects_answer() if page_expects_answer: if fpctx.prev_answer_visit is not None and prev_grade_id is None: answer_data = fpctx.prev_answer_visit.answer shown_grade = fpctx.prev_answer_visit.get_most_recent_grade() if shown_grade is not None: feedback = get_feedback_for_grade(shown_grade) grade_data = shown_grade.grade_data else: feedback = None grade_data = None if shown_grade is not None: prev_grade_id = shown_grade.id elif prev_grade_id is not None: try: shown_grade = prev_grades.filter(id=prev_grade_id).get() except ObjectDoesNotExist: raise http.Http404() feedback = get_feedback_for_grade(shown_grade) grade_data = shown_grade.grade_data answer_data = shown_grade.visit.answer else: feedback = None from course.page.base import PageBehavior page_behavior = PageBehavior(show_correctness=True, show_answer=False, may_change_answer=False) try: form = fpctx.page.make_form(fpctx.page_context, fpctx.page_data.data, answer_data, page_behavior) except InvalidPageData as e: messages.add_message( pctx.request, messages.ERROR, _("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", {}) if form is not None: form_html = fpctx.page.form_to_html(pctx.request, fpctx.page_context, form, answer_data) else: form_html = None # }}} # {{{ grading form if (page_expects_answer and fpctx.page.is_answer_gradable() and fpctx.prev_answer_visit is not None and not flow_session.in_progress and not viewing_prev_grade): request = pctx.request if pctx.request.method == "POST": if not pctx.has_permission(pperm.assign_grade): raise PermissionDenied(_("may not assign grades")) grading_form = fpctx.page.post_grading_form( fpctx.page_context, fpctx.page_data, grade_data, request.POST, request.FILES) if grading_form.is_valid(): grade_data = fpctx.page.update_grade_data_from_grading_form_v2( request, fpctx.page_context, fpctx.page_data, grade_data, grading_form, request.FILES) from course.utils import LanguageOverride with LanguageOverride(pctx.course): feedback = fpctx.page.grade(fpctx.page_context, fpctx.page_data.data, answer_data, grade_data) if feedback is not None: correctness = feedback.correctness else: correctness = None feedback_json = None # type: Optional[Dict[Text, Any]] bulk_feedback_json = None # type: Optional[Dict[Text, Any]] if feedback is not None: feedback_json, bulk_feedback_json = feedback.as_json() else: feedback_json = bulk_feedback_json = None most_recent_grade = FlowPageVisitGrade( visit=fpctx.prev_answer_visit, grader=pctx.request.user, graded_at_git_commit_sha=pctx.course_commit_sha, grade_data=grade_data, max_points=fpctx.page.max_points(fpctx.page_data), correctness=correctness, feedback=feedback_json) prev_grade_id = _save_grade(fpctx, flow_session, most_recent_grade, bulk_feedback_json, now_datetime) else: grading_form = fpctx.page.make_grading_form( fpctx.page_context, fpctx.page_data, grade_data) else: grading_form = None grading_form_html = None # type: Optional[Text] if grading_form is not None: from crispy_forms.layout import Submit grading_form.helper.form_class += " relate-grading-form" grading_form.helper.add_input( Submit("submit", _("Submit"), accesskey="s", css_class="relate-grading-save-button")) grading_form_html = fpctx.page.grading_form_to_html( pctx.request, fpctx.page_context, grading_form, grade_data) # }}} # {{{ compute points_awarded max_points = None points_awarded = None if (page_expects_answer and fpctx.page.is_answer_gradable()): max_points = fpctx.page.max_points(fpctx.page_data) if feedback is not None and feedback.correctness is not None: points_awarded = max_points * feedback.correctness # }}} grading_rule = get_session_grading_rule(flow_session, fpctx.flow_desc, get_now_or_fake_time(pctx.request)) if grading_rule.grade_identifier is not None: grading_opportunity = get_flow_grading_opportunity( pctx.course, flow_session.flow_id, fpctx.flow_desc, grading_rule.grade_identifier, grading_rule. grade_aggregation_strategy) # type: Optional[GradingOpportunity] else: grading_opportunity = None return render_course_page( pctx, "course/grade-flow-page.html", { "flow_identifier": fpctx.flow_id, "flow_session": flow_session, "flow_desc": fpctx.flow_desc, "page_ordinal": fpctx.page_ordinal, "page_data": fpctx.page_data, "body": fpctx.page.body(fpctx.page_context, fpctx.page_data.data), "form": form, "form_html": form_html, "feedback": feedback, "max_points": max_points, "points_awarded": points_awarded, "shown_grade": shown_grade, "prev_grade_id": prev_grade_id, "expects_answer": page_expects_answer, "grading_opportunity": grading_opportunity, "prev_flow_session_id": prev_flow_session_id, "next_flow_session_id": next_flow_session_id, "grading_form": grading_form, "grading_form_html": grading_form_html, "correct_answer": fpctx.page.correct_answer(fpctx.page_context, fpctx.page_data.data, answer_data, grade_data), # Wrappers used by JavaScript template (tmpl) so as not to # conflict with Django template's tag wrapper "JQ_OPEN": '{%', 'JQ_CLOSE': '%}', })
def query_participations(pctx): if (not pctx.has_permission(pperm.query_participation) or pctx.has_permission(pperm.view_participant_masked_profile)): raise PermissionDenied(_("may not query participations")) request = pctx.request result = None if request.method == "POST": form = ParticipationQueryForm(request.POST) if form.is_valid(): parsed_query = None try: for lineno, q in enumerate( form.cleaned_data["queries"].split("\n")): q = q.strip() if not q: continue parsed_subquery = parse_query(pctx.course, q) if parsed_query is None: parsed_query = parsed_subquery else: parsed_query = parsed_query | parsed_subquery except Exception as e: messages.add_message( request, messages.ERROR, _("Error in line %(lineno)d: %(error_type)s: %(error)s") % { "lineno": lineno + 1, "error_type": type(e).__name__, "error": str(e), }) parsed_query = None if parsed_query is not None: result = list( Participation.objects.filter(course=pctx.course).filter( parsed_query).order_by("user__username"). select_related("user").prefetch_related("tags")) if "apply" in request.POST: if form.cleaned_data["op"] == "apply_tag": ptag, __ = ParticipationTag.objects.get_or_create( course=pctx.course, name=form.cleaned_data["tag"]) for p in result: p.tags.add(ptag) elif form.cleaned_data["op"] == "remove_tag": ptag, __ = ParticipationTag.objects.get_or_create( course=pctx.course, name=form.cleaned_data["tag"]) for p in result: p.tags.remove(ptag) elif form.cleaned_data["op"] == "drop": for p in result: p.status = participation_status.dropped p.save() else: raise RuntimeError("unexpected operation") messages.add_message( request, messages.INFO, "Operation successful on %d participations." % len(result)) else: form = ParticipationQueryForm() return render_course_page(pctx, "course/query-participations.html", { "form": form, "result": result, })
def send_instant_message(pctx): if not pctx.has_permission(pperm.send_instant_message): raise PermissionDenied(_("may not batch-download submissions")) request = pctx.request course = pctx.course if not course.course_xmpp_id: messages.add_message( request, messages.ERROR, _("Instant messaging is not enabled for this course.")) return redirect("relate-course_page", pctx.course_identifier) xmpp = get_xmpp_connection(pctx.course) if xmpp.is_recipient_online(): form_text = _("Recipient is <span class='label label-success'>" "Online</span>.") else: form_text = _("Recipient is <span class='label label-danger'>" "Offline</span>.") form_text = "<div class='well'>%s</div>" % form_text if request.method == "POST": form = InstantMessageForm(request.POST, request.FILES) if form.is_valid(): msg = InstantMessage() msg.participation = pctx.participation msg.text = form.cleaned_data["message"] msg.save() try: if not course.recipient_xmpp_id: raise RuntimeError(_("no recipient XMPP ID")) if not course.course_xmpp_password: raise RuntimeError(_("no XMPP password")) xmpp.send_message(mto=course.recipient_xmpp_id, mbody=form.cleaned_data["message"], mtype='chat') except Exception: from traceback import print_exc print_exc() messages.add_message( request, messages.ERROR, _("An error occurred while sending the message. " "Sorry.")) else: messages.add_message(request, messages.SUCCESS, _("Message sent.")) form = InstantMessageForm() else: form = InstantMessageForm() return render_course_page( pctx, "course/generic-course-form.html", { "form": form, "form_text": form_text, "form_description": _("Send instant message"), })
def show_grader_statistics(pctx, flow_id): if not pctx.has_permission(pperm.view_grader_stats): raise PermissionDenied(_("may not view grader stats")) grades = ( FlowPageVisitGrade.objects.filter( visit__flow_session__course=pctx.course, visit__flow_session__flow_id=flow_id, # There are just way too many autograder grades, which makes this # report super slow. grader__isnull=False).order_by( "visit__id", "grade_time").select_related("visit").select_related( "grader").select_related("visit__page_data")) graders = set() # tuples: (page_ordinal, id) pages = set() counts = {} grader_counts = {} page_counts = {} def commit_grade_info(grade): grader = grade.grader page = (grade.visit.page_data.page_ordinal, grade.visit.page_data.group_id + "/" + grade.visit.page_data.page_id) graders.add(grader) pages.add(page) key = (page, grade.grader) counts[key] = counts.get(key, 0) + 1 grader_counts[grader] = grader_counts.get(grader, 0) + 1 page_counts[page] = page_counts.get(page, 0) + 1 last_grade = None for grade in grades.iterator(): if last_grade is not None and last_grade.visit != grade.visit: commit_grade_info(last_grade) last_grade = grade if last_grade is not None: commit_grade_info(last_grade) graders = sorted(graders, key=lambda grader: grader.last_name if grader is not None else None) pages = sorted(pages) stats_table = [[counts.get((page, grader), 0) for grader in graders] for page in pages] page_counts = [page_counts.get(page, 0) for page in pages] grader_counts = [grader_counts.get(grader, 0) for grader in graders] return render_course_page( pctx, "course/grading-statistics.html", { "flow_id": flow_id, "pages": pages, "graders": graders, "pages_stats_counts": list(zip(pages, stats_table, page_counts)), "grader_counts": grader_counts, })
def page_analytics(pctx, flow_id, group_id, page_id): if pctx.role not in [ participation_role.teaching_assistant, participation_role.instructor, participation_role.observer, ]: raise PermissionDenied(_("must be at least TA to view analytics")) flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) restrict_to_first_attempt = int( bool(pctx.request.GET.get("restrict_to_first_attempt") == "1")) is_multiple_submit = is_flow_multiple_submit(flow_desc) page_cache = PageInstanceCache(pctx.repo, pctx.course, flow_id) visits = (FlowPageVisit.objects .filter( flow_session__course=pctx.course, flow_session__flow_id=flow_id, page_data__group_id=group_id, page_data__page_id=page_id, is_submitted_answer=True, )) if connection.features.can_distinct_on_fields: if restrict_to_first_attempt: visits = (visits .distinct("flow_session__participation__id") .order_by("flow_session__participation__id", "visit_time")) elif is_multiple_submit: visits = (visits .distinct("page_data__id") .order_by("page_data__id", "-visit_time")) visits = (visits .select_related("flow_session") .select_related("page_data")) normalized_answer_and_correctness_to_count = {} title = None body = None total_count = 0 graded_count = 0 for visit in visits: page = page_cache.get_page(group_id, page_id, pctx.course_commit_sha) from course.page import PageContext grading_page_context = PageContext( course=pctx.course, repo=pctx.repo, commit_sha=pctx.course_commit_sha, flow_session=visit.flow_session) title = page.title(grading_page_context, visit.page_data.data) body = page.body(grading_page_context, visit.page_data.data) normalized_answer = page.normalized_answer( grading_page_context, visit.page_data.data, visit.answer) answer_feedback = visit.get_most_recent_feedback() if answer_feedback is not None: key = (normalized_answer, answer_feedback.correctness) normalized_answer_and_correctness_to_count[key] = \ normalized_answer_and_correctness_to_count.get(key, 0) + 1 graded_count += 1 else: key = (normalized_answer, None) normalized_answer_and_correctness_to_count[key] = \ normalized_answer_and_correctness_to_count.get(key, 0) + 1 total_count += 1 answer_stats = [] for (normalized_answer, correctness), count in \ normalized_answer_and_correctness_to_count.iteritems(): answer_stats.append( AnswerStats( normalized_answer=normalized_answer, correctness=correctness, count=count, percentage=safe_div(100 * count, total_count))) answer_stats = sorted( answer_stats, key=lambda astats: astats.percentage, reverse=True) return render_course_page(pctx, "course/analytics-page.html", { "flow_identifier": flow_id, "group_id": group_id, "page_id": page_id, "title": title, "body": body, "answer_stats_list": answer_stats, "restrict_to_first_attempt": restrict_to_first_attempt, })
def send_instant_message(pctx): if pctx.role not in [ participation_role.student, participation_role.teaching_assistant, participation_role.instructor ]: raise PermissionDenied("only enrolled folks may do that") request = pctx.request course = pctx.course if not course.course_xmpp_id: messages.add_message( request, messages.ERROR, "Instant messaging is not enabled for this course.") return redirect("course.views.course_page", pctx.course_identifier) xmpp = get_xmpp_connection(pctx.course) if xmpp.is_recipient_online(): form_text = "Recipient is <span class=\"label label-success\">Online</span>." else: form_text = "Recipient is <span class=\"label label-danger\">Offline</span>." form_text = "<div class=\"well\">%s</div>" % form_text if request.method == "POST": form = InstantMessageForm(request.POST, request.FILES) if form.is_valid(): msg = InstantMessage() msg.participation = pctx.participation msg.text = form.cleaned_data["message"] msg.save() try: if not course.recipient_xmpp_id: raise RuntimeError("no recipient XMPP ID") if not course.course_xmpp_password: raise RuntimeError("no XMPP password") xmpp.send_message(mto=course.recipient_xmpp_id, mbody=form.cleaned_data["message"], mtype='chat') except Exception: from traceback import print_exc print_exc() messages.add_message( request, messages.ERROR, "An error occurred while sending the message. Sorry.") else: messages.add_message(request, messages.SUCCESS, "Message sent.") form = InstantMessageForm() else: form = InstantMessageForm() return render_course_page( pctx, "course/generic-course-form.html", { "form": form, "form_text": form_text, "form_description": "Send instant message", })
def view_grades_by_opportunity(pctx, opp_id): from course.views import get_now_or_fake_time now_datetime = get_now_or_fake_time(pctx.request) if pctx.role not in [ participation_role.instructor, participation_role.teaching_assistant]: raise PermissionDenied("must be instructor or TA to view grades") opportunity = get_object_or_404(GradingOpportunity, id=int(opp_id)) if pctx.course != opportunity.course: raise SuspiciousOperation("opportunity from wrong course") # {{{ batch sessions form batch_session_ops_form = None if pctx.role == participation_role.instructor and opportunity.flow_id: cursor = connection.cursor() cursor.execute("select distinct access_rules_tag from course_flowsession " "where course_id = %s and flow_id = %s " "order by access_rules_tag", (pctx.course.id, opportunity.flow_id)) session_rule_tags = [ mangle_session_access_rule_tag(row[0]) for row in cursor.fetchall()] request = pctx.request if request.method == "POST": batch_session_ops_form = ModifySessionsForm( session_rule_tags, request.POST, request.FILES) if "expire" in request.POST: op = "expire" elif "end" in request.POST: op = "end" elif "regrade" in request.POST: op = "regrade" elif "recalculate" in request.POST: op = "recalculate" else: raise SuspiciousOperation("invalid operation") if batch_session_ops_form.is_valid(): rule_tag = batch_session_ops_form.cleaned_data["rule_tag"] past_due_only = batch_session_ops_form.cleaned_data["past_due_only"] if rule_tag == RULE_TAG_NONE_STRING: rule_tag = None try: if op == "expire": count = expire_in_progress_sessions( pctx.repo, pctx.course, opportunity.flow_id, rule_tag, now_datetime, past_due_only=past_due_only) messages.add_message(pctx.request, messages.SUCCESS, "%d session(s) expired." % count) elif op == "end": count = finish_in_progress_sessions( pctx.repo, pctx.course, opportunity.flow_id, rule_tag, now_datetime, past_due_only=past_due_only) messages.add_message(pctx.request, messages.SUCCESS, "%d session(s) ended." % count) elif op == "regrade": count = regrade_ended_sessions( pctx.repo, pctx.course, opportunity.flow_id, rule_tag) messages.add_message(pctx.request, messages.SUCCESS, "%d session(s) regraded." % count) elif op == "recalculate": count = recalculate_ended_sessions( pctx.repo, pctx.course, opportunity.flow_id, rule_tag) messages.add_message(pctx.request, messages.SUCCESS, "Grade recalculated for %d session(s)." % count) else: raise SuspiciousOperation("invalid operation") except Exception as e: messages.add_message(pctx.request, messages.ERROR, "Error: %s %s" % (type(e).__name__, str(e))) raise else: batch_session_ops_form = ModifySessionsForm(session_rule_tags) # }}} # NOTE: It's important that these queries are sorted consistently, # also consistently with the code below. participations = list(Participation.objects .filter( course=pctx.course, status=participation_status.active) .order_by("id") .select_related("user")) grade_changes = list(GradeChange.objects .filter(opportunity=opportunity) .order_by( "participation__id", "grade_time") .select_related("participation") .select_related("participation__user") .select_related("opportunity")) idx = 0 finished_sessions = 0 total_sessions = 0 grade_table = [] for participation in participations: while ( idx < len(grade_changes) and grade_changes[idx].participation.id < participation.id): idx += 1 my_grade_changes = [] while ( idx < len(grade_changes) and grade_changes[idx].participation.pk == participation.pk): my_grade_changes.append(grade_changes[idx]) idx += 1 state_machine = GradeStateMachine() state_machine.consume(my_grade_changes) if opportunity.flow_id: flow_sessions = (FlowSession.objects .filter( participation=participation, flow_id=opportunity.flow_id, ) .order_by("start_time")) for fsession in flow_sessions: total_sessions += 1 if not fsession.in_progress: finished_sessions += 1 else: flow_sessions = None grade_table.append( OpportunityGradeInfo( grade_state_machine=state_machine, flow_sessions=flow_sessions)) grade_table = sorted(zip(participations, grade_table), key=lambda (participation, grades): (participation.user.last_name.lower(), participation.user.first_name.lower())) return render_course_page(pctx, "course/gradebook-by-opp.html", { "opportunity": opportunity, "participations": participations, "grade_state_change_types": grade_state_change_types, "grade_table": grade_table, "batch_session_ops_form": batch_session_ops_form, "total_sessions": total_sessions, "finished_sessions": finished_sessions, })
def renumber_events(pctx): if not pctx.has_permission(pperm.edit_events): raise PermissionDenied(_("may not edit events")) request = pctx.request message = None message_level = None if request.method == "POST": form = RenumberEventsForm( pctx.course.identifier, request.POST, request.FILES) if form.is_valid(): kind = form.cleaned_data["kind"] order_field = "time" if form.cleaned_data["preserve_ordinal_order"]: order_field = "ordinal" events = list( Event.objects.filter( course=pctx.course, kind=kind, # there might be event with the same kind but no ordinal, # we don't renumber that ordinal__isnull=False) .order_by(order_field)) assert events queryset = (Event.objects.filter( course=pctx.course, kind=kind, # there might be event with the same kind but no ordinal, # we don't renumber that ordinal__isnull=False)) queryset.delete() ordinal = form.cleaned_data["starting_ordinal"] for event in events: new_event = Event() new_event.course = pctx.course new_event.kind = kind new_event.ordinal = ordinal new_event.time = event.time new_event.end_time = event.end_time new_event.all_day = event.all_day new_event.shown_in_calendar = event.shown_in_calendar new_event.save() ordinal += 1 message = _("Events renumbered.") message_level = messages.SUCCESS else: form = RenumberEventsForm(pctx.course.identifier) if messages and message_level: messages.add_message(request, message_level, message) return render_course_page(pctx, "course/generic-course-form.html", { "form": form, "form_description": _("Renumber events"), })
def create_recurring_events(pctx): if not pctx.has_permission(pperm.edit_events): raise PermissionDenied(_("may not edit events")) request = pctx.request message = None message_level = None if request.method == "POST": form = RecurringEventForm( pctx.course.identifier, request.POST, request.FILES) if form.is_valid(): if form.cleaned_data["starting_ordinal"] is not None: starting_ordinal = form.cleaned_data["starting_ordinal"] starting_ordinal_specified = True else: starting_ordinal = 1 starting_ordinal_specified = False while True: try: _create_recurring_events_backend( course=pctx.course, time=form.cleaned_data["time"], kind=form.cleaned_data["kind"], starting_ordinal=starting_ordinal, interval=form.cleaned_data["interval"], count=form.cleaned_data["count"], duration_in_minutes=( form.cleaned_data["duration_in_minutes"]), all_day=form.cleaned_data["all_day"], shown_in_calendar=( form.cleaned_data["shown_in_calendar"]) ) message = _("Events created.") message_level = messages.SUCCESS except EventAlreadyExists as e: if starting_ordinal_specified: message = ( string_concat( "%(err_type)s: %(err_str)s. ", _("No events created.")) % { "err_type": type(e).__name__, "err_str": str(e)}) message_level = messages.ERROR else: starting_ordinal += 10 continue except Exception as e: if isinstance(e, ValidationError): for field, error in e.error_dict.items(): try: form.add_error(field, error) except ValueError: # This happens when ValidationError were # raised for fields which don't exist in # RecurringEventForm form.add_error( "__all__", "'%s': %s" % (field, error)) else: message = ( string_concat( "%(err_type)s: %(err_str)s. ", _("No events created.")) % { "err_type": type(e).__name__, "err_str": str(e)}) message_level = messages.ERROR break else: form = RecurringEventForm(pctx.course.identifier) if message and message_level: messages.add_message(request, message_level, message) return render_course_page(pctx, "course/generic-course-form.html", { "form": form, "form_description": _("Create recurring events"), })
def batch_issue_exam_tickets(pctx): if not pctx.has_permission(pperm.batch_issue_exam_ticket): raise PermissionDenied(_("may not batch-issue tickets")) form_text = "" request = pctx.request if request.method == "POST": form = BatchIssueTicketsForm(pctx.course, request.user.editor_mode, request.POST) if form.is_valid(): exam = form.cleaned_data["exam"] from jinja2 import TemplateSyntaxError from course.content import markup_to_html try: with transaction.atomic(): if form.cleaned_data["revoke_prior"]: ExamTicket.objects.filter( exam=exam, state__in=( exam_ticket_states.valid, exam_ticket_states.used, )).update(state=exam_ticket_states.revoked) tickets = [] for participation in (Participation.objects.filter( course=pctx.course, status=participation_status.active).order_by( "user__last_name")): ticket = ExamTicket() ticket.exam = exam ticket.participation = participation ticket.creator = request.user ticket.state = exam_ticket_states.valid ticket.code = gen_ticket_code() ticket.valid_start_time = \ form.cleaned_data["valid_start_time"] ticket.valid_end_time = form.cleaned_data[ "valid_end_time"] ticket.restrict_to_facility = \ form.cleaned_data["restrict_to_facility"] ticket.save() tickets.append(ticket) checkin_uri = pctx.request.build_absolute_uri( reverse("relate-check_in_for_exam")) form_text = markup_to_html(pctx.course, pctx.repo, pctx.course_commit_sha, form.cleaned_data["format"], jinja_env={ "tickets": tickets, "checkin_uri": checkin_uri, }) except TemplateSyntaxError as e: messages.add_message( request, messages.ERROR, string_concat(_("Template rendering failed"), ": line %(lineno)d: %(err_str)s") % { "lineno": e.lineno, "err_str": e.message.decode("utf-8") }) except Exception as e: messages.add_message( request, messages.ERROR, string_concat(_("Template rendering failed"), ": %(err_type)s: %(err_str)s") % { "err_type": type(e).__name__, "err_str": str(e) }) else: messages.add_message(request, messages.SUCCESS, _("%d tickets issued.") % len(tickets)) else: form = BatchIssueTicketsForm(pctx.course, request.user.editor_mode) return render_course_page( pctx, "course/batch-exam-tickets-form.html", { "form": form, "form_text": form_text, "form_description": gettext("Batch-Issue Exam Tickets") })
def view_page_sandbox(pctx): from relate.utils import dict_to_struct import yaml PAGE_SESSION_KEY = "cf_validated_sandbox_page:" + pctx.course.identifier ANSWER_DATA_SESSION_KEY = "cf_page_sandbox_answer_data:" + pctx.course.identifier request = pctx.request page_source = pctx.request.session.get(PAGE_SESSION_KEY) page_errors = None is_preview_post = (request.method == "POST" and "preview" in request.POST) def make_form(data=None): return SandboxForm( page_source, "yaml", vim_mode, "Enter YAML markup for a flow page.", data) vim_mode = pctx.request.session.get(CF_SANDBOX_VIM_MODE, False) if is_preview_post: edit_form = make_form(pctx.request.POST) if edit_form.is_valid(): pctx.request.session[CF_SANDBOX_VIM_MODE] = \ vim_mode = edit_form.cleaned_data["vim_mode"] try: new_page_source = edit_form.cleaned_data["content"] page_desc = dict_to_struct(yaml.load(new_page_source)) from course.validation import validate_flow_page, ValidationContext vctx = ValidationContext( repo=pctx.repo, commit_sha=pctx.course_commit_sha) validate_flow_page(vctx, "sandbox", page_desc) except: import sys tp, e, _ = sys.exc_info() page_errors = ( "Page failed to load/validate: " "%s: %s" % (tp.__name__, e)) else: # Yay, it did validate. request.session[PAGE_SESSION_KEY] = page_source = new_page_source del new_page_source edit_form = make_form(pctx.request.POST) else: edit_form = make_form() have_valid_page = page_source is not None if have_valid_page: page_desc = dict_to_struct(yaml.load(page_source)) from course.content import instantiate_flow_page page = instantiate_flow_page("sandbox", pctx.repo, page_desc, pctx.course_commit_sha) page_data = page.make_page_data() from course.page import PageContext page_context = PageContext( course=pctx.course, repo=pctx.repo, commit_sha=pctx.course_commit_sha, flow_session=None) title = page.title(page_context, page_data) body = page.body(page_context, page_data) # {{{ try to recover answer_data answer_data = None stored_answer_data_tuple = \ pctx.request.session.get(ANSWER_DATA_SESSION_KEY) # Session storage uses JSON and may turn tuples into lists. if (isinstance(stored_answer_data_tuple, (list, tuple)) and len(stored_answer_data_tuple) == 2): stored_answer_data_page_id, stored_answer_data = \ stored_answer_data_tuple if stored_answer_data_page_id == page_desc.id: answer_data = stored_answer_data # }}} feedback = None page_form_html = None if page.expects_answer(): if request.method == "POST" and not is_preview_post: page_form = page.post_form(page_context, page_data, request.POST, request.FILES) if page_form.is_valid(): answer_data = page.answer_data(page_context, page_data, page_form, request.FILES) feedback = page.grade(page_context, page_data, answer_data, grade_data=None) pctx.request.session[ANSWER_DATA_SESSION_KEY] = ( page_desc.id, answer_data) else: page_form = page.make_form(page_context, page_data, answer_data, answer_is_final=False) if page_form is not None: page_form.helper.add_input( Submit("submit", "Submit answer", accesskey="g", css_class="col-lg-offset-2")) page_form_html = page.form_to_html( pctx.request, page_context, page_form, answer_data) correct_answer = page.correct_answer( page_context, page_data, answer_data, grade_data=None) return render_course_page(pctx, "course/sandbox-page.html", { "edit_form": edit_form, "page_errors": page_errors, "form": edit_form, # to placate form.media "have_valid_page": True, "title": title, "body": body, "page_form_html": page_form_html, "feedback": feedback, "correct_answer": correct_answer, }) else: return render_course_page(pctx, "course/sandbox-page.html", { "edit_form": edit_form, "form": edit_form, # to placate form.media "have_valid_page": False, "page_errors": page_errors, })
def grant_exception_stage_2(pctx, participation_id, flow_id): # type: (CoursePageContext, Text, Text) -> http.HttpResponse if not pctx.has_permission(pperm.grant_exception): raise PermissionDenied(_("may not grant exceptions")) # {{{ get flow data participation = get_object_or_404(Participation, id=participation_id) form_text = (string_concat( "<div class='well'>", ugettext("Granting exception to '%(participation)s' for " "'%(flow_id)s'."), "</div>") % { 'participation': participation, 'flow_id': flow_id }) from course.content import get_flow_desc try: flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) except ObjectDoesNotExist: raise http.Http404() now_datetime = get_now_or_fake_time(pctx.request) if hasattr(flow_desc, "rules"): access_rules_tags = getattr(flow_desc.rules, "tags", []) else: access_rules_tags = [] NONE_SESSION_TAG = string_concat("<<<", _("NONE"), ">>>") # noqa session_tag_choices = [(tag, tag) for tag in access_rules_tags] + [ (NONE_SESSION_TAG, string_concat("(", _("NONE"), ")")) ] from course.utils import get_session_start_rule session_start_rule = get_session_start_rule(pctx.course, participation, flow_id, flow_desc, now_datetime) create_session_is_override = False if not session_start_rule.may_start_new_session: create_session_is_override = True form_text += ( "<div class='alert alert-info'>%s</div>" % (string_concat( "<i class='fa fa-info-circle'></i> ", _("Creating a new session is (technically) not allowed " "by course rules. Clicking 'Create Session' anyway will " "override this rule.")))) default_tag = session_start_rule.tag_session if default_tag is None: default_tag = NONE_SESSION_TAG # }}} def find_sessions(): # type: () -> List[FlowSession] return list( FlowSession.objects.filter(participation=participation, flow_id=flow_id).order_by("start_time")) exception_form = None request = pctx.request if request.method == "POST": exception_form = ExceptionStage2Form(find_sessions(), request.POST) create_session_form = CreateSessionForm(session_tag_choices, default_tag, create_session_is_override, request.POST) if "create_session" in request.POST or "next" in request.POST: pass else: raise SuspiciousOperation(_("invalid command")) if create_session_form.is_valid() and "create_session" in request.POST: from course.flow import start_flow access_rules_tag = ( create_session_form. cleaned_data["access_rules_tag_for_new_session"]) if access_rules_tag == NONE_SESSION_TAG: access_rules_tag = None start_flow(pctx.repo, pctx.course, participation, user=participation.user, flow_id=flow_id, flow_desc=flow_desc, session_start_rule=session_start_rule, now_datetime=now_datetime) exception_form = None elif exception_form.is_valid( ) and "next" in request.POST: # type: ignore return redirect( "relate-grant_exception_stage_3", pctx.course.identifier, participation.id, flow_id, exception_form.cleaned_data["session"]) # type: ignore else: create_session_form = CreateSessionForm(session_tag_choices, default_tag, create_session_is_override) if exception_form is None: exception_form = ExceptionStage2Form(find_sessions()) return render_course_page( pctx, "course/generic-course-form.html", { "forms": [exception_form, create_session_form], "form_text": form_text, "form_description": _("Grant Exception"), })
def create_recurring_events(pctx): if not pctx.has_permission(pperm.edit_events): raise PermissionDenied(_("may not edit events")) request = pctx.request if request.method == "POST": form = RecurringEventForm(request.POST, request.FILES) if form.is_valid(): if form.cleaned_data["starting_ordinal"] is not None: starting_ordinal = form.cleaned_data["starting_ordinal"] starting_ordinal_specified = True else: starting_ordinal = 1 starting_ordinal_specified = False while True: try: _create_recurring_events_backend( course=pctx.course, time=form.cleaned_data["time"], kind=form.cleaned_data["kind"], starting_ordinal=starting_ordinal, interval=form.cleaned_data["interval"], count=form.cleaned_data["count"], duration_in_minutes=( form.cleaned_data["duration_in_minutes"]), all_day=form.cleaned_data["all_day"], shown_in_calendar=( form.cleaned_data["shown_in_calendar"]) ) except EventAlreadyExists as e: if starting_ordinal_specified: messages.add_message(request, messages.ERROR, string_concat( "%(err_type)s: %(err_str)s. ", _("No events created.")) % { "err_type": type(e).__name__, "err_str": str(e)}) else: starting_ordinal += 10 continue except Exception as e: messages.add_message(request, messages.ERROR, string_concat( "%(err_type)s: %(err_str)s. ", _("No events created.")) % { "err_type": type(e).__name__, "err_str": str(e)}) else: messages.add_message(request, messages.SUCCESS, _("Events created.")) break else: form = RecurringEventForm() return render_course_page(pctx, "course/generic-course-form.html", { "form": form, "form_description": _("Create recurring events"), })
def manage_authentication_tokens(pctx): # type: (http.HttpRequest) -> http.HttpResponse request = pctx.request if not request.user.is_authenticated: raise PermissionDenied() from course.views import get_now_or_fake_time now_datetime = get_now_or_fake_time(request) if request.method == 'POST': form = AuthenticationTokenForm(pctx.participation, request.POST) revoke_prefix = "revoke_" revoke_post_args = [ key for key in request.POST if key.startswith("revoke_") ] if revoke_post_args: token_id = int(revoke_post_args[0][len(revoke_prefix):]) auth_token = get_object_or_404(AuthenticationToken, id=token_id, user=request.user) auth_token.revocation_time = now_datetime auth_token.save() form = AuthenticationTokenForm(pctx.participation) elif "create" in request.POST: if form.is_valid(): token = make_sign_in_key(request.user) from django.contrib.auth.hashers import make_password auth_token = AuthenticationToken( user=pctx.request.user, participation=pctx.participation, restrict_to_participation_role=form. cleaned_data["restrict_to_participation_role"], description=form.cleaned_data["description"], valid_until=form.cleaned_data["valid_until"], token_hash=make_password(token)) auth_token.save() user_token = "%d_%s" % (auth_token.id, token) messages.add_message( request, messages.SUCCESS, _("A new authentication token has been created: %s. " "Please save this token, as you will not be able " "to retrieve it later.") % user_token) else: messages.add_message(request, messages.ERROR, _("Could not find which button was pressed.")) else: form = AuthenticationTokenForm(pctx.participation) from django.db.models import Q from datetime import timedelta tokens = AuthenticationToken.objects.filter( user=request.user, participation__course=pctx.course, ).filter( Q(revocation_time=None) | Q(revocation_time__gt=now_datetime - timedelta(weeks=1))) return render_course_page(pctx, "course/manage-auth-tokens.html", { "form": form, "new_token_message": "", "tokens": tokens, })
def view_calendar(pctx): from course.content import markup_to_html, parse_date_spec from course.views import get_now_or_fake_time now = get_now_or_fake_time(pctx.request) if not pctx.has_permission(pperm.view_calendar): raise PermissionDenied(_("may not view calendar")) events_json = [] from course.content import get_raw_yaml_from_repo try: event_descr = get_raw_yaml_from_repo(pctx.repo, pctx.course.events_file, pctx.course_commit_sha) except ObjectDoesNotExist: event_descr = {} event_kinds_desc = event_descr.get("event_kinds", {}) event_info_desc = event_descr.get("events", {}) event_info_list = [] events = sorted( Event.objects .filter( course=pctx.course, shown_in_calendar=True), key=lambda evt: ( -evt.time.year, -evt.time.month, -evt.time.day, evt.time.hour, evt.time.minute, evt.time.second)) for event in events: kind_desc = event_kinds_desc.get(event.kind) human_title = six.text_type(event) event_json = { "id": event.id, "start": event.time.isoformat(), "allDay": event.all_day, } if event.end_time is not None: event_json["end"] = event.end_time.isoformat() if kind_desc is not None: if "color" in kind_desc: event_json["color"] = kind_desc["color"] if "title" in kind_desc: if event.ordinal is not None: human_title = kind_desc["title"].format(nr=event.ordinal) else: human_title = kind_desc["title"] description = None show_description = True event_desc = event_info_desc.get(six.text_type(event)) if event_desc is not None: if "description" in event_desc: description = markup_to_html( pctx.course, pctx.repo, pctx.course_commit_sha, event_desc["description"]) if "title" in event_desc: human_title = event_desc["title"] if "color" in event_desc: event_json["color"] = event_desc["color"] if "show_description_from" in event_desc: ds = parse_date_spec( pctx.course, event_desc["show_description_from"]) if now < ds: show_description = False if "show_description_until" in event_desc: ds = parse_date_spec( pctx.course, event_desc["show_description_until"]) if now > ds: show_description = False event_json["title"] = human_title if show_description and description: event_json["url"] = "#event-%d" % event.id start_time = event.time end_time = event.end_time if event.all_day: start_time = start_time.date() if end_time is not None: local_end_time = as_local_time(end_time) end_midnight = datetime.time(tzinfo=local_end_time.tzinfo) if local_end_time.time() == end_midnight: end_time = (end_time - datetime.timedelta(days=1)).date() else: end_time = end_time.date() event_info_list.append( EventInfo( id=event.id, human_title=human_title, start_time=start_time, end_time=end_time, description=description )) events_json.append(event_json) default_date = now.date() if pctx.course.end_date is not None and default_date > pctx.course.end_date: default_date = pctx.course.end_date from json import dumps return render_course_page(pctx, "course/calendar.html", { "events_json": dumps(events_json), "event_info_list": event_info_list, "default_date": default_date.isoformat(), })
def view_single_grade(pctx, participation_id, opportunity_id): from course.views import get_now_or_fake_time now_datetime = get_now_or_fake_time(pctx.request) participation = get_object_or_404(Participation, id=int(participation_id)) if participation.course != pctx.course: raise SuspiciousOperation("participation does not match course") opportunity = get_object_or_404(GradingOpportunity, id=int(opportunity_id)) if pctx.role in [ participation_role.instructor, participation_role.teaching_assistant]: if not opportunity.shown_in_grade_book: messages.add_message(pctx.request, messages.INFO, "This grade is not shown in the grade book.") if not opportunity.shown_in_student_grade_book: messages.add_message(pctx.request, messages.INFO, "This grade is not shown in the student grade book.") elif pctx.role == participation_role.student: if participation != pctx.participation: raise PermissionDenied("may not view other people's grades") if not (opportunity.shown_in_grade_book and opportunity.shown_in_student_grade_book): raise PermissionDenied("grade has not been released") else: raise PermissionDenied() # {{{ modify sessions buttons if pctx.role in [ participation_role.instructor, participation_role.teaching_assistant]: allow_session_actions = True request = pctx.request if pctx.request.method == "POST": action_re = re.compile("^([a-z]+)_([0-9]+)$") for key in request.POST.keys(): action_match = action_re.match(key) if action_match: break if not action_match: raise SuspiciousOperation("unknown action") session = FlowSession.objects.get(id=int(action_match.group(2))) op = action_match.group(1) from course.flow import ( reopen_session, regrade_session, recalculate_session_grade, expire_flow_session_standalone, finish_flow_session_standalone) try: if op == "expire": expire_flow_session_standalone( pctx.repo, pctx.course, session, now_datetime) messages.add_message(pctx.request, messages.SUCCESS, "Session expired.") elif op == "end": finish_flow_session_standalone( pctx.repo, pctx.course, session, now_datetime=now_datetime) messages.add_message(pctx.request, messages.SUCCESS, "Session ended.") elif op == "reopen": reopen_session(session) messages.add_message(pctx.request, messages.SUCCESS, "Session reopened.") elif op == "regrade": regrade_session( pctx.repo, pctx.course, session) messages.add_message(pctx.request, messages.SUCCESS, "Session regraded.") elif op == "recalculate": recalculate_session_grade( pctx.repo, pctx.course, session) messages.add_message(pctx.request, messages.SUCCESS, "Session grade recalculated.") else: raise SuspiciousOperation("invalid session operation") except Exception as e: messages.add_message(pctx.request, messages.ERROR, "Error: %s %s" % (type(e).__name__, str(e))) else: allow_session_actions = False # }}} grade_changes = list(GradeChange.objects .filter( opportunity=opportunity, participation=participation) .order_by("grade_time") .select_related("participation") .select_related("participation__user") .select_related("creator") .select_related("opportunity")) state_machine = GradeStateMachine() state_machine.consume(grade_changes, set_is_superseded=True) flow_grade_aggregation_strategy_text = None if opportunity.flow_id: flow_sessions = list(FlowSession.objects .filter( participation=participation, flow_id=opportunity.flow_id, ) .order_by("start_time")) from collections import namedtuple SessionProperties = namedtuple("SessionProperties", ["due", "grade_description"]) from course.utils import get_session_grading_rule from course.content import get_flow_desc flow_desc = get_flow_desc(pctx.repo, pctx.course, opportunity.flow_id, pctx.course_commit_sha) flow_sessions_and_session_properties = [] for session in flow_sessions: grading_rule = get_session_grading_rule( session, pctx.role, flow_desc, now_datetime) session_properties = SessionProperties( due=grading_rule.due, grade_description=grading_rule.description) flow_sessions_and_session_properties.append( (session, session_properties)) else: flow_sessions_and_session_properties = None return render_course_page(pctx, "course/gradebook-single.html", { "opportunity": opportunity, "grade_participation": participation, "grade_state_change_types": grade_state_change_types, "grade_changes": grade_changes, "state_machine": state_machine, "flow_sessions_and_session_properties": flow_sessions_and_session_properties, "allow_session_actions": allow_session_actions, "show_privileged_info": pctx.role in [ participation_role.instructor, participation_role.teaching_assistant ], "flow_grade_aggregation_strategy": flow_grade_aggregation_strategy_text, })
def view_page_sandbox(pctx): # type: (CoursePageContext) -> http.HttpResponse if not pctx.has_permission(pperm.use_page_sandbox): raise PermissionDenied() from course.validation import ValidationError from relate.utils import dict_to_struct, Struct import yaml PAGE_SESSION_KEY = make_sandbox_session_key( # noqa PAGE_SESSION_KEY_PREFIX, pctx.course.identifier) ANSWER_DATA_SESSION_KEY = make_sandbox_session_key( # noqa ANSWER_DATA_SESSION_KEY_PREFIX, pctx.course.identifier) PAGE_DATA_SESSION_KEY = make_sandbox_session_key( # noqa PAGE_DATA_SESSION_KEY_PREFIX, pctx.course.identifier) request = pctx.request page_source = pctx.request.session.get(PAGE_SESSION_KEY) page_errors = None page_warnings = None is_clear_post = (request.method == "POST" and "clear" in request.POST) is_clear_response_post = (request.method == "POST" and "clear_response" in request.POST) is_preview_post = (request.method == "POST" and "preview" in request.POST) def make_form(data=None): # type: (Optional[Text]) -> PageSandboxForm return PageSandboxForm(page_source, "yaml", request.user.editor_mode, ugettext("Enter YAML markup for a flow page."), data) if is_preview_post: edit_form = make_form(pctx.request.POST) new_page_source = None if edit_form.is_valid(): try: from pytools.py_codegen import remove_common_indentation new_page_source = remove_common_indentation( edit_form.cleaned_data["content"], require_leading_newline=False) from course.content import expand_yaml_macros new_page_source = expand_yaml_macros(pctx.repo, pctx.course_commit_sha, new_page_source) yaml_data = yaml.load(new_page_source) # type: ignore page_desc = dict_to_struct(yaml_data) if not isinstance(page_desc, Struct): raise ValidationError( "Provided page source code is not " "a dictionary. Do you need to remove a leading " "list marker ('-') or some stray indentation?") from course.validation import validate_flow_page, ValidationContext vctx = ValidationContext(repo=pctx.repo, commit_sha=pctx.course_commit_sha) validate_flow_page(vctx, "sandbox", page_desc) page_warnings = vctx.warnings except Exception: import sys tp, e, _ = sys.exc_info() page_errors = (ugettext("Page failed to load/validate") + ": " + "%(err_type)s: %(err_str)s" % { "err_type": tp.__name__, "err_str": e }) # type: ignore else: # Yay, it did validate. request.session[ PAGE_SESSION_KEY] = page_source = new_page_source del new_page_source edit_form = make_form(pctx.request.POST) elif is_clear_post: page_source = None pctx.request.session[PAGE_DATA_SESSION_KEY] = None pctx.request.session[ANSWER_DATA_SESSION_KEY] = None del pctx.request.session[PAGE_DATA_SESSION_KEY] del pctx.request.session[ANSWER_DATA_SESSION_KEY] edit_form = make_form() elif is_clear_response_post: page_source = None pctx.request.session[PAGE_DATA_SESSION_KEY] = None pctx.request.session[ANSWER_DATA_SESSION_KEY] = None del pctx.request.session[PAGE_DATA_SESSION_KEY] del pctx.request.session[ANSWER_DATA_SESSION_KEY] edit_form = make_form(pctx.request.POST) else: edit_form = make_form() have_valid_page = page_source is not None if have_valid_page: yaml_data = yaml.load(page_source) # type: ignore page_desc = cast(FlowPageDesc, dict_to_struct(yaml_data)) from course.content import instantiate_flow_page try: page = instantiate_flow_page("sandbox", pctx.repo, page_desc, pctx.course_commit_sha) except Exception: import sys tp, e, _ = sys.exc_info() page_errors = (ugettext("Page failed to load/validate") + ": " + "%(err_type)s: %(err_str)s" % { "err_type": tp.__name__, "err_str": e }) # type: ignore have_valid_page = False if have_valid_page: page_desc = cast(FlowPageDesc, page_desc) # Try to recover page_data, answer_data page_data = get_sandbox_data_for_page(pctx, page_desc, PAGE_DATA_SESSION_KEY) answer_data = get_sandbox_data_for_page(pctx, page_desc, ANSWER_DATA_SESSION_KEY) from course.models import FlowSession from course.page import PageContext page_context = PageContext( course=pctx.course, repo=pctx.repo, commit_sha=pctx.course_commit_sha, # This helps code pages retrieve the editor pref. flow_session=FlowSession(course=pctx.course, participation=pctx.participation), in_sandbox=True) if page_data is None: page_data = page.initialize_page_data(page_context) pctx.request.session[PAGE_DATA_SESSION_KEY] = (page_desc.type, page_desc.id, page_data) title = page.title(page_context, page_data) body = page.body(page_context, page_data) feedback = None page_form_html = None if page.expects_answer(): from course.page.base import PageBehavior page_behavior = PageBehavior(show_correctness=True, show_answer=True, may_change_answer=True) if request.method == "POST" and not is_preview_post: page_form = page.process_form_post(page_context, page_data, request.POST, request.FILES, page_behavior) if page_form.is_valid(): answer_data = page.answer_data(page_context, page_data, page_form, request.FILES) feedback = page.grade(page_context, page_data, answer_data, grade_data=None) pctx.request.session[ANSWER_DATA_SESSION_KEY] = ( page_desc.type, page_desc.id, answer_data) else: try: page_form = page.make_form(page_context, page_data, answer_data, page_behavior) except Exception: import sys tp, e, _ = sys.exc_info() page_errors = ( ugettext("Page failed to load/validate " "(change page ID to clear faults)") + ": " + "%(err_type)s: %(err_str)s" % { "err_type": tp.__name__, "err_str": e }) # type: ignore # noqa: E501 have_valid_page = False page_form = None if page_form is not None: page_form.helper.add_input( Submit("submit", ugettext("Submit answer"), accesskey="g")) page_form_html = page.form_to_html(pctx.request, page_context, page_form, answer_data) correct_answer = page.correct_answer(page_context, page_data, answer_data, grade_data=None) return render_course_page( pctx, "course/sandbox-page.html", { "edit_form": edit_form, "page_errors": page_errors, "page_warnings": page_warnings, "form": edit_form, # to placate form.media "have_valid_page": True, "title": title, "body": body, "page_form_html": page_form_html, "feedback": feedback, "correct_answer": correct_answer, }) else: return render_course_page( pctx, "course/sandbox-page.html", { "edit_form": edit_form, "form": edit_form, # to placate form.media "have_valid_page": False, "page_errors": page_errors, "page_warnings": page_warnings, })
def create_preapprovals(pctx): if pctx.role != participation_role.instructor: raise PermissionDenied(_("only instructors may do that")) request = pctx.request if request.method == "POST": form = BulkPreapprovalsForm(request.POST) if form.is_valid(): created_count = 0 exist_count = 0 pending_approved_count = 0 role = form.cleaned_data["role"] for l in form.cleaned_data["emails"].split("\n"): l = l.strip() if not l: continue try: preapproval = ParticipationPreapproval.objects.get( email__iexact=l, course=pctx.course) except ParticipationPreapproval.DoesNotExist: # approve if l is requesting enrollment try: pending_participation = Participation.objects.get( course=pctx.course, status=participation_status.requested, user__email__iexact=l) except Participation.DoesNotExist: pass else: pending_participation.status = participation_status.active pending_participation.save() send_enrollment_decision(pending_participation, True, request) pending_approved_count += 1 else: exist_count += 1 continue preapproval = ParticipationPreapproval() preapproval.email = l preapproval.course = pctx.course preapproval.role = role preapproval.creator = request.user preapproval.save() created_count += 1 messages.add_message( request, messages.INFO, _("%(n_created)d preapprovals created, " "%(n_exist)d already existed, " "%(n_requested_approved)d pending requests approved.") % { 'n_created': created_count, 'n_exist': exist_count, 'n_requested_approved': pending_approved_count }) return redirect("relate-home") else: form = BulkPreapprovalsForm() return render_course_page( pctx, "course/generic-course-form.html", { "form": form, "form_description": _("Create Participation Preapprovals"), })
def update_course(pctx): if not (pctx.has_permission(pperm.update_content) or pctx.has_permission(pperm.preview_content)): raise PermissionDenied() course = pctx.course request = pctx.request content_repo = pctx.repo from course.content import SubdirRepoWrapper if isinstance(content_repo, SubdirRepoWrapper): repo = content_repo.repo else: repo = content_repo participation = pctx.participation previewing = bool(participation is not None and participation.preview_git_commit_sha) may_update = pctx.has_permission(pperm.update_content) response_form = None if request.method == "POST": form = GitUpdateForm(may_update, previewing, repo, request.POST, request.FILES) commands = [ "fetch", "fetch_update", "update", "fetch_preview", "preview", "end_preview" ] command = None for cmd in commands: if cmd in form.data: command = cmd break if command is None: raise SuspiciousOperation(_("invalid command")) if form.is_valid(): new_sha = form.cleaned_data["new_sha"].encode() try: run_course_update_command( request, repo, content_repo, pctx, command, new_sha, may_update, prevent_discarding_revisions=form. cleaned_data["prevent_discarding_revisions"]) except Exception as e: import traceback traceback.print_exc() messages.add_message( pctx.request, messages.ERROR, string_concat( pgettext("Starting of Error message", "Error"), ": %(err_type)s %(err_str)s") % { "err_type": type(e).__name__, "err_str": str(e) }) else: response_form = form if response_form is None: previewing = bool(participation is not None and participation.preview_git_commit_sha) form = GitUpdateForm(may_update, previewing, repo, { "new_sha": repo.head(), "prevent_discarding_revisions": True, }) text_lines = [ "<table class='table'>", string_concat("<tr><th>", ugettext("Git Source URL"), "</th><td><tt>%(git_source)s</tt></td></tr>") % { 'git_source': pctx.course.git_source }, string_concat("<tr><th>", ugettext("Public active git SHA"), "</th><td> %(commit)s (%(message)s)</td></tr>") % { 'commit': course.active_git_commit_sha, 'message': _get_commit_message_as_html(repo, course.active_git_commit_sha) }, string_concat("<tr><th>", ugettext("Current git HEAD"), "</th><td>%(commit)s (%(message)s)</td></tr>") % { 'commit': repo.head().decode(), 'message': _get_commit_message_as_html(repo, repo.head()) }, ] if participation is not None and participation.preview_git_commit_sha: text_lines.append( string_concat("<tr><th>", ugettext("Current preview git SHA"), "</th><td>%(commit)s (%(message)s)</td></tr>") % { 'commit': participation.preview_git_commit_sha, 'message': _get_commit_message_as_html( repo, participation.preview_git_commit_sha), }) else: text_lines.append("".join([ "<tr><th>", ugettext("Current preview git SHA"), "</th><td>", ugettext("None"), "</td></tr>", ])) text_lines.append("</table>") return render_course_page( pctx, "course/generic-course-form.html", { "form": form, "form_text": "".join("<p>%s</p>" % line for line in text_lines), "form_description": ugettext("Update Course Revision"), })
def view_calendar(pctx): from course.content import markup_to_html events_json = [] from course.content import get_raw_yaml_from_repo try: event_descr = get_raw_yaml_from_repo(pctx.repo, pctx.course.events_file, pctx.course_commit_sha) except ObjectDoesNotExist: event_descr = {} event_kinds_desc = event_descr.get("event_kinds", {}) event_info_desc = event_descr.get("events", {}) event_info_list = [] for event in (Event.objects.filter( course=pctx.course, shown_in_calendar=True).order_by("time")): kind_desc = event_kinds_desc.get(event.kind) human_title = unicode(event) event_json = { "id": event.id, "start": event.time.isoformat(), "allDay": event.all_day, } if event.end_time is not None: event_json["end"] = event.end_time.isoformat() if kind_desc is not None: if "color" in kind_desc: event_json["color"] = kind_desc["color"] if "title" in kind_desc: if event.ordinal is not None: human_title = kind_desc["title"].format(nr=event.ordinal) else: human_title = kind_desc["title"] description = None event_desc = event_info_desc.get(unicode(event)) if event_desc is not None: if "description" in event_desc: description = markup_to_html(pctx.course, pctx.repo, pctx.course_commit_sha, event_desc["description"]) if "title" in event_desc: human_title = event_desc["title"] if "color" in event_desc: human_title = event_desc["color"] event_json["title"] = human_title if description: event_json["url"] = "#event-%d" % event.id start_time = event.time end_time = event.end_time if event.all_day: start_time = start_time.date() end_time = end_time.date() event_info_list.append( EventInfo(id=event.id, human_title=human_title, start_time=start_time, end_time=end_time, description=description)) events_json.append(event_json) from json import dumps return render_course_page(pctx, "course/calendar.html", { "events_json": dumps(events_json), "event_info_list": event_info_list, })
def view_participant_grades(pctx, participation_id=None): if pctx.participation is None: raise PermissionDenied("must be enrolled to view grades") if participation_id is not None: grade_participation = Participation.objects.get(id=int(participation_id)) else: grade_participation = pctx.participation if pctx.role in [ participation_role.instructor, participation_role.teaching_assistant]: is_student_viewing = False elif pctx.role == participation_role.student: if grade_participation != pctx.participation: raise PermissionDenied("may not view other people's grades") is_student_viewing = True else: raise PermissionDenied() # NOTE: It's important that these two queries are sorted consistently, # also consistently with the code below. grading_opps = list((GradingOpportunity.objects .filter( course=pctx.course, shown_in_grade_book=True, ) .order_by("identifier"))) grade_changes = list(GradeChange.objects .filter( participation=grade_participation, opportunity__course=pctx.course, opportunity__shown_in_grade_book=True) .order_by( "participation__id", "opportunity__identifier", "grade_time") .select_related("participation") .select_related("participation__user") .select_related("opportunity")) idx = 0 grade_table = [] for opp in grading_opps: if is_student_viewing: if not (opp.shown_in_grade_book and opp.shown_in_student_grade_book): continue else: if not opp.shown_in_grade_book: continue while ( idx < len(grade_changes) and grade_changes[idx].opportunity.identifier < opp.identifier ): idx += 1 my_grade_changes = [] while ( idx < len(grade_changes) and grade_changes[idx].opportunity.pk == opp.pk): my_grade_changes.append(grade_changes[idx]) idx += 1 state_machine = GradeStateMachine() state_machine.consume(my_grade_changes) grade_table.append( GradeInfo( opportunity=opp, grade_state_machine=state_machine)) return render_course_page(pctx, "course/gradebook-participant.html", { "grade_table": grade_table, "grade_participation": grade_participation, "grading_opportunities": grading_opps, "grade_state_change_types": grade_state_change_types, })
def create_preapprovals(pctx): if not pctx.has_permission(pperm.preapprove_participation): raise PermissionDenied(_("may not preapprove participation")) request = pctx.request if request.method == "POST": form = BulkPreapprovalsForm(pctx.course, request.POST) if form.is_valid(): created_count = 0 exist_count = 0 pending_approved_count = 0 roles = form.cleaned_data["roles"] for l in form.cleaned_data["preapproval_data"].split("\n"): l = l.strip() preapp_type = form.cleaned_data["preapproval_type"] if not l: continue if preapp_type == "email": try: preapproval = ParticipationPreapproval.objects.get( email__iexact=l, course=pctx.course) except ParticipationPreapproval.DoesNotExist: # approve if l is requesting enrollment try: pending = Participation.objects.get( course=pctx.course, status=participation_status.requested, user__email__iexact=l) except Participation.DoesNotExist: pass else: pending.status = \ participation_status.active pending.save() send_enrollment_decision(pending, True, request) pending_approved_count += 1 else: exist_count += 1 continue preapproval = ParticipationPreapproval() preapproval.email = l preapproval.course = pctx.course preapproval.creator = request.user preapproval.save() preapproval.roles.set(roles) created_count += 1 elif preapp_type == "institutional_id": try: preapproval = ParticipationPreapproval.objects.get( course=pctx.course, institutional_id__iexact=l) except ParticipationPreapproval.DoesNotExist: # approve if l is requesting enrollment try: pending = Participation.objects.get( course=pctx.course, status=participation_status.requested, user__institutional_id__iexact=l) if (pctx.course. preapproval_require_verified_inst_id and not pending.user.institutional_id_verified ): raise Participation.DoesNotExist except Participation.DoesNotExist: pass else: pending.status = participation_status.active pending.save() send_enrollment_decision(pending, True, request) pending_approved_count += 1 else: exist_count += 1 continue preapproval = ParticipationPreapproval() preapproval.institutional_id = l preapproval.course = pctx.course preapproval.creator = request.user preapproval.save() preapproval.roles.set(roles) created_count += 1 messages.add_message( request, messages.INFO, _("%(n_created)d preapprovals created, " "%(n_exist)d already existed, " "%(n_requested_approved)d pending requests approved.") % { 'n_created': created_count, 'n_exist': exist_count, 'n_requested_approved': pending_approved_count }) return redirect("relate-course_page", pctx.course.identifier) else: form = BulkPreapprovalsForm(pctx.course) return render_course_page( pctx, "course/generic-course-form.html", { "form": form, "form_description": _("Create Participation Preapprovals"), })
def grant_exception_stage_3(pctx, participation_id, flow_id, session_id): # type: (CoursePageContext, int, Text, int) -> http.HttpResponse if not pctx.has_permission(pperm.grant_exception): raise PermissionDenied(_("may not grant exceptions")) participation = get_object_or_404(Participation, id=participation_id) from course.content import get_flow_desc try: flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) except ObjectDoesNotExist: raise http.Http404() session = FlowSession.objects.get(id=int(session_id)) now_datetime = get_now_or_fake_time(pctx.request) from course.utils import (get_session_access_rule, get_session_grading_rule) access_rule = get_session_access_rule(session, flow_desc, now_datetime) grading_rule = get_session_grading_rule(session, flow_desc, now_datetime) request = pctx.request if request.method == "POST": form = ExceptionStage3Form({}, flow_desc, session.access_rules_tag, request.POST) from course.constants import flow_rule_kind if form.is_valid(): permissions = [ key for key, _ in FLOW_PERMISSION_CHOICES if form.cleaned_data[key] ] from course.validation import (validate_session_access_rule, validate_session_grading_rule, ValidationContext) from relate.utils import dict_to_struct vctx = ValidationContext(repo=pctx.repo, commit_sha=pctx.course_commit_sha) flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) tags = [] # type: List[Text] if hasattr(flow_desc, "rules"): try: from typing import Text # noqa except ImportError: Text = None # noqa tags = cast(List[Text], getattr(flow_desc.rules, "tags", [])) # type: ignore # noqa # {{{ put together access rule if form.cleaned_data["create_access_exception"]: new_access_rule = {"permissions": permissions} if (form.cleaned_data.get("restrict_to_same_tag") and session.access_rules_tag is not None): new_access_rule["if_has_tag"] = session.access_rules_tag validate_session_access_rule( vctx, ugettext("newly created exception"), dict_to_struct(new_access_rule), tags) fre_access = FlowRuleException( flow_id=flow_id, participation=participation, expiration=form.cleaned_data["access_expires"], creator=pctx.request.user, comment=form.cleaned_data["comment"], kind=flow_rule_kind.access, rule=new_access_rule) fre_access.save() # }}} new_access_rules_tag = form.cleaned_data.get( "set_access_rules_tag") if new_access_rules_tag == NONE_SESSION_TAG: new_access_rules_tag = None if session.access_rules_tag != new_access_rules_tag: session.access_rules_tag = new_access_rules_tag session.save() # {{{ put together grading rule if form.cleaned_data["create_grading_exception"]: due = form.cleaned_data["due"] if form.cleaned_data["due_same_as_access_expiration"]: due = form.cleaned_data["access_expires"] descr = ugettext("Granted excecption") if form.cleaned_data["credit_percent"] is not None: descr += string_concat(" (%.1f%% ", ugettext('credit'), ")") \ % form.cleaned_data["credit_percent"] due_local_naive = due if due_local_naive is not None: from relate.utils import as_local_time due_local_naive = (as_local_time(due_local_naive).replace( tzinfo=None)) new_grading_rule = { "description": descr, } if due_local_naive is not None: new_grading_rule["due"] = due_local_naive new_grading_rule["if_completed_before"] = due_local_naive for attr_name in [ "credit_percent", "bonus_points", "max_points", "max_points_enforced_cap", "generates_grade" ]: if form.cleaned_data[attr_name] is not None: new_grading_rule[attr_name] = form.cleaned_data[ attr_name] if (form.cleaned_data.get("restrict_to_same_tag") and session.access_rules_tag is not None): new_grading_rule["if_has_tag"] = session.access_rules_tag validate_session_grading_rule( vctx, ugettext("newly created exception"), dict_to_struct(new_grading_rule), tags, grading_rule.grade_identifier) fre_grading = FlowRuleException( flow_id=flow_id, participation=participation, creator=pctx.request.user, comment=form.cleaned_data["comment"], kind=flow_rule_kind.grading, rule=new_grading_rule) fre_grading.save() # }}} messages.add_message( pctx.request, messages.SUCCESS, ugettext("Exception granted to '%(participation)s' " "for '%(flow_id)s'.") % { 'participation': participation, 'flow_id': flow_id }) return redirect("relate-grant_exception", pctx.course.identifier) else: data = { "restrict_to_same_tag": session.access_rules_tag is not None, #"due_same_as_access_expiration": True, "due": grading_rule.due, "generates_grade": grading_rule.generates_grade, "credit_percent": grading_rule.credit_percent, "bonus_points": grading_rule.bonus_points, "max_points": grading_rule.max_points, "max_points_enforced_cap": grading_rule.max_points_enforced_cap, } for perm in access_rule.permissions: data[perm] = True form = ExceptionStage3Form(data, flow_desc, session.access_rules_tag) return render_course_page( pctx, "course/generic-course-form.html", { "form": form, "form_description": ugettext("Grant Exception"), "form_text": string_concat( "<div class='well'>", ugettext("Granting exception to '%(participation)s' " "for '%(flow_id)s' (session %(session)s)."), "</div>") % { 'participation': participation, 'flow_id': flow_id, 'session': strify_session_for_exception(session) }, })
def edit_participation(pctx, participation_id): # type: (CoursePageContext, int) -> http.HttpResponse if not pctx.has_permission(pperm.edit_participation): raise PermissionDenied() request = pctx.request num_participation_id = int(participation_id) if num_participation_id == -1: participation = Participation(course=pctx.course, status=participation_status.active) add_new = True else: participation = get_object_or_404(Participation, id=num_participation_id) add_new = False if participation.course.id != pctx.course.id: raise SuspiciousOperation( "may not edit participation in different course") if request.method == 'POST': form = EditParticipationForm(add_new, pctx, request.POST, instance=participation) reset_form = False try: if form.is_valid(): if "submit" in request.POST: form.save() messages.add_message(request, messages.SUCCESS, _("Changes saved.")) elif "approve" in request.POST: send_enrollment_decision(participation, True, pctx.request) # FIXME: Double-saving participation = form.save() participation.status = participation_status.active participation.save() reset_form = True messages.add_message(request, messages.SUCCESS, _("Successfully enrolled.")) elif "deny" in request.POST: send_enrollment_decision(participation, False, pctx.request) # FIXME: Double-saving participation = form.save() participation.status = participation_status.denied participation.save() reset_form = True messages.add_message(request, messages.SUCCESS, _("Successfully denied.")) elif "drop" in request.POST: # FIXME: Double-saving participation = form.save() participation.status = participation_status.dropped participation.save() reset_form = True messages.add_message(request, messages.SUCCESS, _("Successfully dropped.")) except IntegrityError as e: messages.add_message( request, messages.ERROR, _("A data integrity issue was detected when saving " "this participation. Maybe a participation for " "this user already exists? (%s)") % str(e)) if reset_form: form = EditParticipationForm(add_new, pctx, instance=participation) else: form = EditParticipationForm(add_new, pctx, instance=participation) return render_course_page(pctx, "course/generic-course-form.html", { "form_description": _("Edit Participation"), "form": form })
def import_grades(pctx): if pctx.role != participation_role.instructor: raise PermissionDenied() form_text = "" log_lines = [] request = pctx.request if request.method == "POST": form = ImportGradesForm( pctx.course, request.POST, request.FILES) is_import = "import" in request.POST if form.is_valid(): try: total_count, grade_changes = csv_to_grade_changes( log_lines=log_lines, course=pctx.course, grading_opportunity=form.cleaned_data["grading_opportunity"], attempt_id=form.cleaned_data["attempt_id"], file_contents=request.FILES["file"], id_column=form.cleaned_data["id_column"], points_column=form.cleaned_data["points_column"], feedback_column=form.cleaned_data["feedback_column"], max_points=form.cleaned_data["max_points"], creator=request.user, grade_time=now(), has_header=form.cleaned_data["format"] == "csvhead") except Exception as e: messages.add_message(pctx.request, messages.ERROR, "Error: %s %s" % (type(e).__name__, str(e))) else: if total_count != len(grade_changes): messages.add_message(pctx.request, messages.INFO, "%d grades found, %d unchanged." % (total_count, total_count - len(grade_changes))) from django.template.loader import render_to_string if is_import: GradeChange.objects.bulk_create(grade_changes) form_text = render_to_string( "course/grade-import-preview.html", { "show_grade_changes": False, "log_lines": log_lines, }) messages.add_message(pctx.request, messages.SUCCESS, "%d grades imported." % len(grade_changes)) else: form_text = render_to_string( "course/grade-import-preview.html", { "show_grade_changes": True, "grade_changes": grade_changes, "log_lines": log_lines, }) else: form = ImportGradesForm(pctx.course) return render_course_page(pctx, "course/generic-course-form.html", { "form_description": "Import Grade Data", "form": form, "form_text": form_text, })