def list_available_exams(request): now_datetime = get_now_or_fake_time(request) if request.user.is_authenticated(): participations = ( Participation.objects.filter( user=request.user, status=participation_status.active)) else: participations = [] from django.db.models import Q exams = ( Exam.objects .filter( course__in=[p.course for p in participations], active=True, no_exams_before__lt=now_datetime) .filter( Q(no_exams_after__isnull=True) | Q(no_exams_after__gt=now_datetime)) .order_by("no_exams_before", "course__number")) return render(request, "course/list-exams.html", { "exams": exams })
def render_course_page(pctx, template_name, args, allow_instant_flow_requests=True): # type: (CoursePageContext, Text, Dict[Text, Any], bool) -> http.HttpResponse args = args.copy() from course.views import get_now_or_fake_time now_datetime = get_now_or_fake_time(pctx.request) if allow_instant_flow_requests: from course.models import InstantFlowRequest instant_flow_requests = list((InstantFlowRequest.objects .filter( course=pctx.course, start_time__lte=now_datetime, end_time__gte=now_datetime, cancelled=False) .order_by("start_time"))) else: instant_flow_requests = [] args.update({ "course": pctx.course, "pperm": ParticipationPermissionWrapper(pctx), "participation": pctx.participation, "num_instant_flow_requests": len(instant_flow_requests), "instant_flow_requests": [(i+1, r) for i, r in enumerate(instant_flow_requests)], }) return render(pctx.request, template_name, args)
def render_course_page(pctx, template_name, args, allow_instant_flow_requests=True): args = args.copy() from course.views import get_now_or_fake_time now_datetime = get_now_or_fake_time(pctx.request) if allow_instant_flow_requests: from course.models import InstantFlowRequest instant_flow_requests = list((InstantFlowRequest.objects .filter( course=pctx.course, start_time__lte=now_datetime, end_time__gte=now_datetime, cancelled=False) .order_by("start_time"))) else: instant_flow_requests = [] args.update({ "course": pctx.course, "participation": pctx.participation, "role": pctx.role, "participation_role": participation_role, "num_instant_flow_requests": len(instant_flow_requests), "instant_flow_requests": [(i+1, r) for i, r in enumerate(instant_flow_requests)], }) return render(pctx.request, template_name, args)
def update_expiration_mode(pctx, flow_session_id): if pctx.request.method != "POST": raise SuspiciousOperation(_("only POST allowed")) flow_session = get_object_or_404(FlowSession, id=flow_session_id) if flow_session.participation != pctx.participation: raise PermissionDenied(_("may only change your own flow sessions")) if not flow_session.in_progress: raise PermissionDenied(_("may only change in-progress flow sessions")) expmode = pctx.request.POST.get("expiration_mode") if not any(expmode == em_key for em_key, _ in FLOW_SESSION_EXPIRATION_MODE_CHOICES): raise SuspiciousOperation(_("invalid expiration mode")) fctx = FlowContext( pctx.repo, pctx.course, flow_session.flow_id, participation=pctx.participation, flow_session=flow_session ) access_rule = get_session_access_rule( flow_session, pctx.role, fctx.flow_desc, get_now_or_fake_time(pctx.request), pctx.remote_address ) if is_expiration_mode_allowed(expmode, access_rule.permissions): flow_session.expiration_mode = expmode flow_session.save() return http.HttpResponse("OK") else: raise PermissionDenied()
def issue_exam_ticket(request): now_datetime = get_now_or_fake_time(request) if request.method == "POST": form = IssueTicketForm(now_datetime, request.POST) if form.is_valid(): exam = form.cleaned_data["exam"] try: participation = Participation.objects.get( course=exam.course, user=form.cleaned_data["user"], status=participation_status.active, ) except ObjectDoesNotExist: messages.add_message(request, messages.ERROR, _("User is not enrolled in course.")) participation = None if participation is not None: if form.cleaned_data["revoke_prior"]: ExamTicket.objects.filter( exam=exam, participation=participation, state__in=( exam_ticket_states.valid, exam_ticket_states.used, ) ).update(state=exam_ticket_states.revoked) ticket = ExamTicket() ticket.exam = exam ticket.participation = participation ticket.creator = request.user ticket.state = exam_ticket_states.valid ticket.code = gen_ticket_code() ticket.save() messages.add_message(request, messages.SUCCESS, _( "Ticket issued for <b>%(participation)s</b>. " "The ticket code is <b>%(ticket_code)s</b>." ) % {"participation": participation, "ticket_code": ticket.code}) form = IssueTicketForm(now_datetime, initial_exam=exam) else: form = IssueTicketForm(now_datetime) return render(request, "generic-form.html", { "form_description": _("Issue Exam Ticket"), "form": form, })
def check_in_for_exam(request): now_datetime = get_now_or_fake_time(request) if request.method == "POST": form = ExamCheckInForm(request.POST) if form.is_valid(): username = form.cleaned_data["username"] code = form.cleaned_data["code"] pretend_facilities = request.session.get( "relate_pretend_facilities", None) is_valid, msg = check_exam_ticket(username, code, now_datetime) if not is_valid: messages.add_message(request, messages.ERROR, msg) else: from django.contrib.auth import authenticate, login user = authenticate(username=username, code=code, now_datetime=now_datetime) assert user is not None login(request, user) ticket = ExamTicket.objects.get( participation__user=user, code=code, state__in=( exam_ticket_states.valid, exam_ticket_states.used, ) ) if ticket.state == exam_ticket_states.valid: ticket.state = exam_ticket_states.used ticket.usage_time = now_datetime ticket.save() if pretend_facilities: # Make pretend-facilities survive exam login. request.session["relate_pretend_facilities"] = pretend_facilities request.session["relate_exam_ticket_pk_used_for_login"] = ticket.pk return redirect("relate-view_start_flow", ticket.exam.course.identifier, ticket.exam.flow_id) else: form = ExamCheckInForm() return render(request, "course/exam-check-in.html", { "form_description": _("Check in for Exam"), "form": form })
def __init__(self, request, course_identifier, flow_identifier, flow_session=None): CoursePageContext.__init__(self, request, course_identifier) self.flow_session = flow_session self.flow_identifier = flow_identifier from course.content import get_flow_commit_sha from django.core.exceptions import ObjectDoesNotExist # Fetch 'current' version of the flow to compute permissions # and versioning rules. # Fall back to 'old' version if current git version does not # contain this flow any more. try: current_flow_desc_sha = self.course_commit_sha current_flow_desc = get_flow_desc(self.repo, self.course, flow_identifier, current_flow_desc_sha) except ObjectDoesNotExist: if self.flow_session is None: raise http.Http404() current_flow_desc_sha = self.flow_session.active_git_commit_sha.encode() current_flow_desc = get_flow_desc(self.repo, self.course, flow_identifier, current_flow_desc_sha) self.flow_commit_sha = get_flow_commit_sha( self.course, self.participation, current_flow_desc, self.flow_session) if self.flow_commit_sha == current_flow_desc_sha: self.flow_desc = current_flow_desc else: self.flow_desc = get_flow_desc(self.repo, self.course, flow_identifier, self.flow_commit_sha) # {{{ figure out permissions from course.views import get_now_or_fake_time self.permissions, self.stipulations = get_flow_permissions( self.course, self.participation, self.role, flow_identifier, current_flow_desc, get_now_or_fake_time(request))
def get_facilities_config(request=None): from django.conf import settings # This is called during offline validation, where Django isn't really set up. # The getattr makes this usable. facilities = getattr(settings, "RELATE_FACILITIES", None) if facilities is None: # Only happens during offline validation. Suppresses errors there. return None if callable(facilities): from course.views import get_now_or_fake_time now_datetime = get_now_or_fake_time(request) result = facilities(now_datetime) if not isinstance(result, dict): raise RuntimeError("RELATE_FACILITIES must return a dictionary") return result else: return facilities
def view_calendar(pctx): if not pctx.has_permission(pperm.view_calendar): raise PermissionDenied(_("may not view calendar")) from course.views import get_now_or_fake_time now = get_now_or_fake_time(pctx.request) events_json = [] from course.content import (get_raw_yaml_from_repo, markup_to_html, parse_date_spec) 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"].rstrip("{nr}").strip() 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_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_id from course_flowsession " "where course_id = %s and flow_id = %s " "order by access_rules_id", (pctx.course.id, opportunity.flow_id)) rule_ids = [mangle_rule_id(row[0]) for row in cursor.fetchall()] request = pctx.request if request.method == "POST": batch_session_ops_form = ModifySessionsForm( rule_ids, 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" else: raise SuspiciousOperation("invalid operation") if batch_session_ops_form.is_valid(): rule_id = batch_session_ops_form.cleaned_data["rule_id"] past_end_only = batch_session_ops_form.cleaned_data["past_end_only"] if rule_id == RULE_ID_NONE_STRING: rule_id = None try: if op == "expire": count = expire_in_progress_sessions( pctx.repo, pctx.course, opportunity.flow_id, rule_id, now_datetime, past_end_only=past_end_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_id, now_datetime, past_end_only=past_end_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_id) messages.add_message(pctx.request, messages.SUCCESS, "%d session(s) regraded." % 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))) else: batch_session_ops_form = ModifySessionsForm(rule_ids) # }}} # 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") .prefetch_related("user")) grade_changes = list(GradeChange.objects .filter(opportunity=opportunity) .order_by( "participation__id", "grade_time") .prefetch_related("participation") .prefetch_related("participation__user") .prefetch_related("opportunity")) idx = 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")) 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, })
def grade_flow_page(pctx, flow_session_id, page_ordinal): page_ordinal = int(page_ordinal) if pctx.role not in [ participation_role.instructor, participation_role.teaching_assistant]: raise PermissionDenied( _("must be instructor or TA to view grades")) 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")) fpctx = FlowPageContext(pctx.repo, pctx.course, flow_session.flow_id, page_ordinal, participation=flow_session.participation, flow_session=flow_session) if fpctx.page_desc is None: raise http.Http404() # {{{ 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( "participation__user__last_name", "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 # }}} # {{{ reproduce student view form = None feedback = None answer_data = None grade_data = None most_recent_grade = None if fpctx.page.expects_answer(): if fpctx.prev_answer_visit is not None: answer_data = fpctx.prev_answer_visit.answer most_recent_grade = fpctx.prev_answer_visit.get_most_recent_grade() if most_recent_grade is not None: feedback = get_feedback_for_grade(most_recent_grade) grade_data = most_recent_grade.grade_data else: feedback = None grade_data = None else: feedback = None from course.page.base import PageBehavior page_behavior = PageBehavior( show_correctness=True, show_answer=False, may_change_answer=False) form = fpctx.page.make_form( fpctx.page_context, fpctx.page_data.data, answer_data, page_behavior) 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 (fpctx.page.expects_answer() and fpctx.page.is_answer_gradable() and fpctx.prev_answer_visit is not None and not flow_session.in_progress): request = pctx.request if pctx.request.method == "POST": 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( fpctx.page_context, fpctx.page_data, grade_data, grading_form, request.FILES) feedback = fpctx.page.grade( fpctx.page_context, fpctx.page_data, answer_data, grade_data) if feedback is not None: correctness = feedback.correctness else: correctness = None 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) most_recent_grade.save() update_bulk_feedback( fpctx.prev_answer_visit.page_data, most_recent_grade, bulk_feedback_json) grading_rule = get_session_grading_rule( flow_session, flow_session.participation.role, fpctx.flow_desc, get_now_or_fake_time(request)) from course.flow import grade_flow_session grade_flow_session(fpctx, flow_session, grading_rule) else: grading_form = fpctx.page.make_grading_form( fpctx.page_context, fpctx.page_data, grade_data) else: grading_form = None if grading_form is not None: from crispy_forms.layout import Submit grading_form.helper.add_input( Submit( "submit", _("Submit"), accesskey="s", css_class="col-lg-offset-2 relate-grading-save-button")) grading_form_html = fpctx.page.grading_form_to_html( pctx.request, fpctx.page_context, grading_form, grade_data) else: grading_form_html = None # }}} # {{{ compute points_awarded max_points = None points_awarded = None if (fpctx.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, flow_session.participation.role, 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) 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, "ordinal": fpctx.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, "most_recent_grade": most_recent_grade, "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, })
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 = 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 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"] 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 course.views import get_now_or_fake_time default_date = get_now_or_fake_time(pctx.request).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 finish_flow_session_view(pctx, flow_session_id): now_datetime = get_now_or_fake_time(pctx.request) request = pctx.request flow_session_id = int(flow_session_id) flow_session = get_and_check_flow_session(pctx, flow_session_id) flow_id = flow_session.flow_id fctx = FlowContext(pctx.repo, pctx.course, flow_id, participation=pctx.participation, flow_session=flow_session) access_rule = get_session_access_rule(flow_session, pctx.role, fctx.flow_desc, now_datetime, pctx.remote_address) answer_visits = assemble_answer_visits(flow_session) from course.content import markup_to_html completion_text = markup_to_html(fctx.course, fctx.repo, pctx.course_commit_sha, fctx.flow_desc.completion_text) (answered_count, unanswered_count) = count_answered_gradable(fctx, flow_session, answer_visits) is_graded_flow = bool(answered_count + unanswered_count) if flow_permission.view not in access_rule.permissions: raise PermissionDenied() def render_finish_response(template, **kwargs): render_args = {"flow_identifier": fctx.flow_id, "flow_desc": fctx.flow_desc} render_args.update(kwargs) return render_course_page(pctx, template, render_args, allow_instant_flow_requests=False) if request.method == "POST": if "submit" not in request.POST: raise SuspiciousOperation(_("odd POST parameters")) if not flow_session.in_progress: raise PermissionDenied(_("Can't end a session that's already ended")) if flow_permission.end_session not in access_rule.permissions: raise PermissionDenied(_("not permitted to end session")) grading_rule = get_session_grading_rule(flow_session, pctx.role, fctx.flow_desc, now_datetime) grade_info = finish_flow_session(fctx, flow_session, grading_rule, now_datetime=now_datetime) if is_graded_flow: return render_finish_response( "course/flow-completion-grade.html", completion_text=completion_text, grade_info=grade_info ) else: return render_finish_response( "course/flow-completion.html", last_page_nr=None, flow_session=flow_session, completion_text=completion_text, ) if not is_graded_flow or (flow_session.in_progress and flow_permission.end_session not in access_rule.permissions): # No ability to end--just show completion page. return render_finish_response( "course/flow-completion.html", last_page_nr=flow_session.page_count - 1, flow_session=flow_session, completion_text=completion_text, ) elif not flow_session.in_progress: # Just reviewing: re-show grades. grade_info = gather_grade_info(fctx, flow_session, answer_visits) return render_finish_response( "course/flow-completion-grade.html", completion_text=completion_text, grade_info=grade_info ) else: # confirm ending flow return render_finish_response( "course/flow-confirm-completion.html", last_page_nr=flow_session.page_count - 1, flow_session=flow_session, answered_count=answered_count, unanswered_count=unanswered_count, total_count=answered_count + unanswered_count, )
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 = [] 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 = 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() 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 start_flow(pctx, flow_identifier): request = pctx.request now_datetime = get_now_or_fake_time(request) fctx = FlowContext(pctx.repo, pctx.course, flow_identifier, participation=pctx.participation) current_access_rule = fctx.get_current_access_rule( None, pctx.role, pctx.participation, now_datetime) may_view = flow_permission.view in current_access_rule.permissions have_in_progress_session = (FlowSession.objects .filter( participation=pctx.participation, flow_id=fctx.flow_identifier, in_progress=True, participation__isnull=False, )).count() > 0 past_sessions = (FlowSession.objects .filter( participation=pctx.participation, flow_id=fctx.flow_identifier, participation__isnull=False) .order_by("start_time")) past_session_count = past_sessions.count() if current_access_rule.allowed_session_count is not None: allowed_another_session = ( past_session_count < current_access_rule.allowed_session_count) else: allowed_another_session = True if request.method == "POST": from course.content import set_up_flow_session_page_data resume_match = None for post_key in request.POST: resume_match = RESUME_RE.match(post_key) if resume_match is not None: break if resume_match is not None: resume_session_id = int(resume_match.group(1)) resume_session = get_object_or_404(FlowSession, pk=resume_session_id) if resume_session.participation != pctx.participation: raise PermissionDenied("not your session") if not may_view: raise PermissionDenied("may not resume session without " "'view' permission") if resume_session.participation is None: raise PermissionDenied("can't resume anonymous session") if resume_session.flow_id != fctx.flow_identifier: raise SuspiciousOperation("flow id mismatch on resume") if not (flow_permission.view_past in current_access_rule.permissions or resume_session.in_progress): raise PermissionDenied("not allowed to resume session") request.session["flow_session_id"] = resume_session_id return redirect("course.flow.view_flow_page", pctx.course.identifier, flow_identifier, 0) elif ("start_no_credit" in request.POST or "start_credit" in request.POST): if not may_view: raise PermissionDenied("may not start session without " "'view' permission") if not allowed_another_session: raise PermissionDenied("new session would exceed " "allowed session count limit exceed") if have_in_progress_session: raise PermissionDenied("cannot start flow when other flow " "session is already in progress") session = FlowSession() session.course = fctx.course session.participation = pctx.participation session.active_git_commit_sha = fctx.flow_commit_sha.decode() session.flow_id = flow_identifier session.in_progress = True session.for_credit = "start_credit" in request.POST session.access_rules_id = current_access_rule.id session.save() request.session["flow_session_id"] = session.id page_count = set_up_flow_session_page_data(fctx.repo, session, pctx.course.identifier, fctx.flow_desc, fctx.flow_commit_sha) session.page_count = page_count session.save() return redirect("course.flow.view_flow_page", pctx.course.identifier, flow_identifier, 0) else: raise SuspiciousOperation("unrecognized POST action") else: may_start_credit = ( may_view and not have_in_progress_session and allowed_another_session and flow_permission.start_credit in current_access_rule.permissions) may_start_no_credit = ( may_view and not have_in_progress_session and allowed_another_session and (flow_permission.start_no_credit in current_access_rule.permissions)) may_review = ( may_view and flow_permission.view_past in current_access_rule.permissions) if hasattr(fctx.flow_desc, "grade_aggregation_strategy"): from course.models import GRADE_AGGREGATION_STRATEGY_CHOICES grade_aggregation_strategy_text = ( dict(GRADE_AGGREGATION_STRATEGY_CHOICES) [fctx.flow_desc.grade_aggregation_strategy]) else: grade_aggregation_strategy_text = None # {{{ fish out relevant rules from course.utils import ( get_flow_access_rules, get_relevant_rules) rules = get_flow_access_rules(fctx.course, pctx.participation, flow_identifier, fctx.flow_desc) # }}} return render_course_page(pctx, "course/flow-start.html", { "flow_desc": fctx.flow_desc, "grade_aggregation_strategy": grade_aggregation_strategy_text, "flow_identifier": flow_identifier, "rules": get_relevant_rules(rules, pctx.role, now_datetime), "now": now_datetime, "may_start_credit": may_start_credit, "may_start_no_credit": may_start_no_credit, "may_review": may_review, "past_sessions": past_sessions, }, allow_instant_flow_requests=False)
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_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 from course.tasks import ( expire_in_progress_sessions, finish_in_progress_sessions, regrade_flow_sessions, recalculate_ended_sessions) if op == "expire": async_res = expire_in_progress_sessions.delay( pctx.course.id, opportunity.flow_id, rule_tag, now_datetime, past_due_only=past_due_only) return redirect("relate-monitor_task", async_res.id) elif op == "end": async_res = finish_in_progress_sessions.delay( pctx.course.id, opportunity.flow_id, rule_tag, now_datetime, past_due_only=past_due_only) return redirect("relate-monitor_task", async_res.id) elif op == "regrade": async_res = regrade_flow_sessions.delay( pctx.course.id, opportunity.flow_id, rule_tag, inprog_value=False) return redirect("relate-monitor_task", async_res.id) elif op == "recalculate": async_res = recalculate_ended_sessions.delay( pctx.course.id, opportunity.flow_id, rule_tag) return redirect("relate-monitor_task", async_res.id) else: raise SuspiciousOperation("invalid operation") 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)) 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-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 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 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_flow_page(pctx, flow_session_id, ordinal): request = pctx.request ordinal = int(ordinal) flow_session_id = int(flow_session_id) flow_session = get_and_check_flow_session(pctx, flow_session_id) flow_id = flow_session.flow_id if flow_session is None: messages.add_message( request, messages.WARNING, _("No in-progress session record found for this flow. " "Redirected to flow start page."), ) return redirect("relate-view_start_flow", pctx.course.identifier, flow_id) try: fpctx = FlowPageContext( pctx.repo, pctx.course, flow_id, ordinal, participation=pctx.participation, flow_session=flow_session ) except PageOrdinalOutOfRange: return redirect("relate-view_flow_page", pctx.course.identifier, flow_session.id, flow_session.page_count - 1) access_rule = get_session_access_rule( flow_session, pctx.role, fpctx.flow_desc, get_now_or_fake_time(request), pctx.remote_address ) permissions = fpctx.page.get_modified_permissions_for_page(access_rule.permissions) if access_rule.message: messages.add_message(request, messages.INFO, access_rule.message) page_context = fpctx.page_context page_data = fpctx.page_data answer_data = None grade_data = None if flow_permission.view not in permissions: raise PermissionDenied(_("not allowed to view flow")) if request.method == "POST": if "finish" in request.POST: return redirect("relate-finish_flow_session_view", pctx.course.identifier, flow_session_id) else: submission_allowed = True # reject answer update if permission not present if flow_permission.submit_answer not in permissions: messages.add_message(request, messages.ERROR, _("Answer submission not allowed.")) submission_allowed = False # reject if previous answer was final if ( fpctx.prev_answer_visit is not None and fpctx.prev_answer_visit.is_submitted_answer and flow_permission.change_answer not in permissions ): messages.add_message(request, messages.ERROR, _("Already have final answer.")) submission_allowed = False form = fpctx.page.post_form( fpctx.page_context, fpctx.page_data.data, post_data=request.POST, files_data=request.FILES ) pressed_button = get_pressed_button(form) if submission_allowed and form.is_valid(): # {{{ form validated, process answer messages.add_message(request, messages.INFO, _("Answer saved.")) page_visit = FlowPageVisit() page_visit.flow_session = flow_session page_visit.page_data = fpctx.page_data page_visit.remote_address = request.META["REMOTE_ADDR"] answer_data = page_visit.answer = fpctx.page.answer_data( fpctx.page_context, fpctx.page_data.data, form, request.FILES ) page_visit.is_submitted_answer = pressed_button == "submit" page_visit.save() answer_was_graded = page_visit.is_submitted_answer may_change_answer = not answer_was_graded or flow_permission.change_answer in permissions if fpctx.page.is_answer_gradable(): feedback = fpctx.page.grade(page_context, page_data.data, page_visit.answer, grade_data=None) if page_visit.is_submitted_answer: grade = FlowPageVisitGrade() grade.visit = page_visit grade.max_points = fpctx.page.max_points(page_data.data) grade.graded_at_git_commit_sha = pctx.course_commit_sha bulk_feedback_json = None if feedback is not None: grade.correctness = feedback.correctness grade.feedback, bulk_feedback_json = feedback.as_json() grade.save() update_bulk_feedback(page_data, grade, bulk_feedback_json) del grade else: feedback = None if pressed_button == "save_and_next" and not will_receive_feedback(permissions): return redirect("relate-view_flow_page", pctx.course.identifier, flow_session_id, fpctx.ordinal + 1) elif pressed_button == "save_and_finish" and not will_receive_feedback(permissions): return redirect("relate-finish_flow_session_view", pctx.course.identifier, flow_session_id) else: form = fpctx.page.make_form(page_context, page_data.data, page_visit.answer, not may_change_answer) # continue at common flow page generation below # }}} del page_visit else: # form did not validate create_flow_page_visit(request, flow_session, fpctx.page_data) answer_was_graded = False may_change_answer = True # because we were allowed this far in by the check above feedback = None # continue at common flow page generation below else: create_flow_page_visit(request, flow_session, fpctx.page_data) if fpctx.prev_answer_visit is not None: answer_was_graded = fpctx.prev_answer_visit.is_submitted_answer else: answer_was_graded = False may_change_answer = ( (not answer_was_graded or (flow_permission.change_answer in permissions)) # can happen if no answer was ever saved and flow_session.in_progress and (flow_permission.submit_answer in permissions) ) if fpctx.page.expects_answer(): if fpctx.prev_answer_visit is not None: answer_data = fpctx.prev_answer_visit.answer most_recent_grade = fpctx.prev_answer_visit.get_most_recent_grade() if most_recent_grade is not None: feedback = get_feedback_for_grade(most_recent_grade) grade_data = most_recent_grade.grade_data else: feedback = None grade_data = None else: feedback = None form = fpctx.page.make_form(page_context, page_data.data, answer_data, not may_change_answer) else: form = None feedback = None # start common flow page generation # defined at this point: # form, form_html, may_change_answer, answer_was_graded, feedback if form is not None and may_change_answer: form = add_buttons_to_form(form, fpctx, flow_session, permissions) show_correctness = None show_answer = None shown_feedback = None if fpctx.page.expects_answer() and answer_was_graded: show_correctness = flow_permission.see_correctness in permissions show_answer = flow_permission.see_answer_after_submission in permissions if show_correctness or show_answer: shown_feedback = feedback elif fpctx.page.expects_answer() and not answer_was_graded: # Don't show answer yet show_answer = flow_permission.see_answer_before_submission in permissions else: show_answer = ( flow_permission.see_answer_before_submission in permissions or flow_permission.see_answer_after_submission in permissions ) title = fpctx.page.title(page_context, page_data.data) body = fpctx.page.body(page_context, page_data.data) if show_answer: correct_answer = fpctx.page.correct_answer(page_context, page_data.data, answer_data, grade_data) else: correct_answer = None # {{{ render flow page if form is not None: form_html = fpctx.page.form_to_html(pctx.request, page_context, form, answer_data) else: form_html = None expiration_mode_choices = [] for key, descr in FLOW_SESSION_EXPIRATION_MODE_CHOICES: if is_expiration_mode_allowed(key, permissions): expiration_mode_choices.append((key, descr)) args = { "flow_identifier": fpctx.flow_id, "flow_desc": fpctx.flow_desc, "ordinal": fpctx.ordinal, "page_data": fpctx.page_data, "percentage": int(100 * (fpctx.ordinal + 1) / flow_session.page_count), "flow_session": flow_session, "page_numbers": zip(range(flow_session.page_count), range(1, flow_session.page_count + 1)), "title": title, "body": body, "form": form, "form_html": form_html, "feedback": shown_feedback, "correct_answer": correct_answer, "show_correctness": show_correctness, "may_change_answer": may_change_answer, "may_change_graded_answer": ((flow_permission.change_answer in permissions) and flow_session.in_progress), "will_receive_feedback": will_receive_feedback(permissions), "show_answer": show_answer, "expiration_mode_choices": expiration_mode_choices, "expiration_mode_choice_count": len(expiration_mode_choices), "expiration_mode": flow_session.expiration_mode, } if fpctx.page.expects_answer() and fpctx.page.is_answer_gradable(): args["max_points"] = fpctx.page.max_points(fpctx.page_data) return render_course_page(pctx, "course/flow-page.html", args, allow_instant_flow_requests=False)
def view_start_flow(pctx, flow_id): request = pctx.request now_datetime = get_now_or_fake_time(request) fctx = FlowContext(pctx.repo, pctx.course, flow_id, participation=pctx.participation) session_start_rule = get_session_start_rule( pctx.course, pctx.participation, pctx.role, flow_id, fctx.flow_desc, now_datetime, remote_address=pctx.remote_address, ) if request.method == "POST": if "start" in request.POST: if not session_start_rule.may_start_new_session: raise PermissionDenied(_("new session not allowed")) session = start_flow( pctx.repo, pctx.course, pctx.participation, flow_id, fctx.flow_desc, access_rules_tag=session_start_rule.tag_session, now_datetime=now_datetime, ) return redirect("relate-view_flow_page", pctx.course.identifier, session.id, 0) else: raise SuspiciousOperation(_("unrecognized POST action")) else: if session_start_rule.may_list_existing_sessions: past_sessions = FlowSession.objects.filter( participation=pctx.participation, flow_id=fctx.flow_id, participation__isnull=False ).order_by("start_time") from collections import namedtuple SessionProperties = namedtuple( "SessionProperties", ["may_view", "may_modify", "due", "grade_description"] # noqa ) past_sessions_and_properties = [] for session in past_sessions: access_rule = get_session_access_rule( session, pctx.role, fctx.flow_desc, now_datetime, remote_address=pctx.remote_address ) grading_rule = get_session_grading_rule(session, pctx.role, fctx.flow_desc, now_datetime) session_properties = SessionProperties( may_view=flow_permission.view in access_rule.permissions, may_modify=( flow_permission.submit_answer in access_rule.permissions or flow_permission.end_session in access_rule.permissions ), due=grading_rule.due, grade_description=grading_rule.description, ) past_sessions_and_properties.append((session, session_properties)) else: past_sessions_and_properties = [] may_start = session_start_rule.may_start_new_session potential_session = FlowSession( course=pctx.course, participation=pctx.participation, flow_id=flow_id, in_progress=True, expiration_mode=flow_session_expiration_mode.end, access_rules_tag=session_start_rule.tag_session, ) new_session_grading_rule = get_session_grading_rule(potential_session, pctx.role, fctx.flow_desc, now_datetime) start_may_decrease_grade = bool( past_sessions_and_properties ) and new_session_grading_rule.grade_aggregation_strategy not in [ None, grade_aggregation_strategy.max_grade, grade_aggregation_strategy.use_earliest, ] return render_course_page( pctx, "course/flow-start.html", { "flow_desc": fctx.flow_desc, "flow_identifier": flow_id, "now": now_datetime, "may_start": may_start, "new_session_grading_rule": new_session_grading_rule, "grade_aggregation_strategy_descr": ( dict(GRADE_AGGREGATION_STRATEGY_CHOICES).get(new_session_grading_rule.grade_aggregation_strategy) ), "start_may_decrease_grade": start_may_decrease_grade, "past_sessions_and_properties": past_sessions_and_properties, }, allow_instant_flow_requests=False, )
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("^(expire|end|reopen|regrade)_([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, 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 == "regrade": regrade_session( pctx.repo, pctx.course, session) messages.add_message(pctx.request, messages.SUCCESS, "Session regraded.") elif op == "reopen": reopen_session(session) messages.add_message(pctx.request, messages.SUCCESS, "Session reopened.") 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") .prefetch_related("participation") .prefetch_related("participation__user") .prefetch_related("creator") .prefetch_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")) # {{{ fish out grade rules from course.content import get_flow_desc flow_desc = get_flow_desc( pctx.repo, pctx.course, opportunity.flow_id, pctx.course_commit_sha) from course.utils import ( get_flow_access_rules, get_relevant_rules) all_flow_rules = get_flow_access_rules(pctx.course, participation, opportunity.flow_id, flow_desc) relevant_flow_rules = get_relevant_rules( all_flow_rules, pctx.participation.role, now()) if hasattr(flow_desc, "grade_aggregation_strategy"): from course.models import GRADE_AGGREGATION_STRATEGY_CHOICES flow_grade_aggregation_strategy_text = ( dict(GRADE_AGGREGATION_STRATEGY_CHOICES) [flow_desc.grade_aggregation_strategy]) # }}} else: flow_sessions = None relevant_flow_rules = 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": flow_sessions, "allow_session_actions": allow_session_actions, "show_page_grades": pctx.role in [ participation_role.instructor, participation_role.teaching_assistant ], "flow_rules": relevant_flow_rules, "flow_grade_aggregation_strategy": flow_grade_aggregation_strategy_text, })
def view_flow_page(pctx, flow_identifier, ordinal): request = pctx.request flow_session = find_current_flow_session( request, pctx.course, flow_identifier) if flow_session is None: messages.add_message(request, messages.WARNING, "No in-progress session record found for this flow. " "Redirected to flow start page.") return redirect("course.flow.start_flow", pctx.course.identifier, flow_identifier) fpctx = FlowPageContext(pctx.repo, pctx.course, flow_identifier, ordinal, participation=pctx.participation, flow_session=flow_session) if fpctx.page_desc is None: messages.add_message(request, messages.ERROR, "Your session does not match the course content and needs " "to be reset. Course staff have been notified about this issue. " "Please get in touch with them to get help.") from django.template.loader import render_to_string message = render_to_string("course/session-mismatch.txt", { "page_data": fpctx.page_data, "course": pctx.course, "user": pctx.request.user, }) from django.core.mail import send_mail from django.conf import settings send_mail("[%s] session mismatch with course content" % pctx.course.identifier, message, settings.ROBOT_EMAIL_FROM, recipient_list=[pctx.course.email]) return redirect("course.flow.start_flow", pctx.course.identifier, flow_identifier) current_access_rule = fpctx.get_current_access_rule( flow_session, pctx.role, pctx.participation, get_now_or_fake_time(request)) permissions = fpctx.page.get_modified_permissions_for_page( current_access_rule.permissions) page_context = fpctx.page_context page_data = fpctx.page_data answer_data = None grade_data = None if flow_permission.view not in permissions: raise PermissionDenied("not allowed to view flow") if request.method == "POST": if "finish" in request.POST: return redirect("course.flow.finish_flow_session_view", pctx.course.identifier, flow_identifier) else: # reject answer update if flow is not in-progress if not flow_session.in_progress: raise PermissionDenied("session is not in progress") # reject if previous answer was final if (fpctx.prev_answer_visit is not None and fpctx.prev_answer_visit.is_graded_answer and flow_permission.change_answer not in permissions): raise PermissionDenied("already have final answer") form = fpctx.page.post_form( fpctx.page_context, fpctx.page_data.data, post_data=request.POST, files_data=request.FILES) pressed_button = get_pressed_button(form) if form.is_valid(): # {{{ form validated, process answer messages.add_message(request, messages.INFO, "Answer saved.") page_visit = FlowPageVisit() page_visit.flow_session = flow_session page_visit.page_data = fpctx.page_data page_visit.remote_address = request.META['REMOTE_ADDR'] answer_data = page_visit.answer = fpctx.page.answer_data( fpctx.page_context, fpctx.page_data.data, form, request.FILES) page_visit.is_graded_answer = pressed_button == "submit" page_visit.save() answer_was_graded = page_visit.is_graded_answer may_change_answer = ( not answer_was_graded or flow_permission.change_answer in permissions) feedback = fpctx.page.grade( page_context, page_data.data, page_visit.answer, grade_data=None) if page_visit.is_graded_answer: grade = FlowPageVisitGrade() grade.visit = page_visit grade.max_points = fpctx.page.max_points(page_data.data) grade.graded_at_git_commit_sha = fpctx.flow_commit_sha if feedback is not None: grade.correctness = feedback.correctness grade.feedback = feedback.as_json() grade.save() del grade if (pressed_button == "save_and_next" and not will_receive_feedback(permissions)): return redirect("course.flow.view_flow_page", pctx.course.identifier, flow_identifier, fpctx.ordinal + 1) elif (pressed_button == "save_and_finish" and not will_receive_feedback(permissions)): return redirect("course.flow.finish_flow_session_view", pctx.course.identifier, flow_identifier) else: form = fpctx.page.make_form( page_context, page_data.data, page_visit.answer, not may_change_answer) # continue at common flow page generation below # }}} del page_visit else: # form did not validate create_flow_page_visit(request, flow_session, fpctx.page_data) answer_was_graded = False may_change_answer = True # because we were allowed this far in by the check above feedback = None # continue at common flow page generation below else: create_flow_page_visit(request, flow_session, fpctx.page_data) if fpctx.prev_answer_visit is not None: answer_was_graded = fpctx.prev_answer_visit.is_graded_answer else: answer_was_graded = False may_change_answer = ( (not answer_was_graded or (flow_permission.change_answer in permissions)) # can happen if no answer was ever saved and flow_session.in_progress) if fpctx.page.expects_answer(): if fpctx.prev_answer_visit is not None: answer_data = fpctx.prev_answer_visit.answer most_recent_grade = fpctx.prev_answer_visit.get_most_recent_grade() if most_recent_grade is not None: if most_recent_grade.feedback is not None: from course.page import AnswerFeedback feedback = AnswerFeedback.from_json( most_recent_grade.feedback) else: feedback = None grade_data = most_recent_grade.grade_data else: feedback = None grade_data = None else: feedback = None form = fpctx.page.make_form( page_context, page_data.data, answer_data, not may_change_answer) else: form = None feedback = None # start common flow page generation # defined at this point: # form, form_html, may_change_answer, answer_was_graded, feedback if form is not None and may_change_answer: form = add_buttons_to_form(form, fpctx, flow_session, permissions) show_correctness = None show_answer = None shown_feedback = None if fpctx.page.expects_answer() and answer_was_graded: show_correctness = ( flow_permission.see_correctness in permissions or ( (flow_permission.see_correctness_after_completion in permissions) and not flow_session.in_progress)) show_answer = flow_permission.see_answer in permissions if show_correctness or show_answer: shown_feedback = feedback elif fpctx.page.expects_answer() and not answer_was_graded: # Don't show answer yet pass else: show_answer = flow_permission.see_answer in permissions title = fpctx.page.title(page_context, page_data.data) body = fpctx.page.body(page_context, page_data.data) if show_answer: correct_answer = fpctx.page.correct_answer( page_context, page_data.data, answer_data, grade_data) else: correct_answer = None # {{{ render flow page if form is not None: form_html = fpctx.page.form_to_html( pctx.request, page_context, form, answer_data) else: form_html = None args = { "flow_identifier": fpctx.flow_identifier, "flow_desc": fpctx.flow_desc, "ordinal": fpctx.ordinal, "page_data": fpctx.page_data, "percentage": int(100*(fpctx.ordinal+1) / flow_session.page_count), "flow_session": flow_session, "title": title, "body": body, "form": form, "form_html": form_html, "feedback": shown_feedback, "correct_answer": correct_answer, "show_correctness": show_correctness, "may_change_answer": may_change_answer, "may_change_graded_answer": ( (flow_permission.change_answer in permissions) and flow_session.in_progress), "will_receive_feedback": will_receive_feedback(permissions), "show_answer": show_answer, } if fpctx.page.expects_answer(): args["max_points"] = fpctx.page.max_points(fpctx.page_data) return render_course_page( pctx, "course/flow-page.html", args, allow_instant_flow_requests=False)
def issue_exam_ticket(request): # must import locally for mock to work from course.views import get_now_or_fake_time now_datetime = get_now_or_fake_time(request) if request.method == "POST": form = IssueTicketForm(now_datetime, request.POST) if form.is_valid(): exam = form.cleaned_data["exam"] try: participation = Participation.objects.get( course=exam.course, user=form.cleaned_data["user"], status=participation_status.active, ) except ObjectDoesNotExist: messages.add_message(request, messages.ERROR, _("User is not enrolled in course.")) participation = None if participation is not None: if form.cleaned_data["revoke_prior"]: ExamTicket.objects.filter( exam=exam, participation=participation, state__in=( exam_ticket_states.valid, exam_ticket_states.used, )).update(state=exam_ticket_states.revoked) 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() messages.add_message( request, messages.SUCCESS, _("Ticket issued for <b>%(participation)s</b>. " "The ticket code is <b>%(ticket_code)s</b>.") % { "participation": participation, "ticket_code": ticket.code }) form = IssueTicketForm(now_datetime, initial_exam=exam) else: form = IssueTicketForm(now_datetime) return render(request, "generic-form.html", { "form_description": _("Issue Exam Ticket"), "form": form, })
def grade_flow_page(pctx, flow_session_id, page_ordinal): now_datetime = get_now_or_fake_time(pctx.request) page_ordinal = int(page_ordinal) if pctx.role not in [ participation_role.instructor, participation_role.teaching_assistant]: raise PermissionDenied( _("must be instructor or TA to view grades")) 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")) 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() from course.flow import adjust_flow_session_page_data adjust_flow_session_page_data(pctx.repo, flow_session, pctx.course.identifier, fpctx.flow_desc) # {{{ 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 # }}} # {{{ reproduce student view form = None feedback = None answer_data = None grade_data = None most_recent_grade = None if fpctx.page.expects_answer(): if fpctx.prev_answer_visit is not None: answer_data = fpctx.prev_answer_visit.answer most_recent_grade = fpctx.prev_answer_visit.get_most_recent_grade() if most_recent_grade is not None: feedback = get_feedback_for_grade(most_recent_grade) grade_data = most_recent_grade.grade_data else: feedback = None grade_data = None else: feedback = None from course.page.base import PageBehavior page_behavior = PageBehavior( show_correctness=True, show_answer=False, may_change_answer=False) form = fpctx.page.make_form( fpctx.page_context, fpctx.page_data.data, answer_data, page_behavior) 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 (fpctx.page.expects_answer() and fpctx.page.is_answer_gradable() and fpctx.prev_answer_visit is not None and not flow_session.in_progress): request = pctx.request if pctx.request.method == "POST": 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( fpctx.page_context, fpctx.page_data, grade_data, grading_form, request.FILES) with translation.override(settings.RELATE_ADMIN_EMAIL_LOCALE): feedback = fpctx.page.grade( fpctx.page_context, fpctx.page_data, answer_data, grade_data) if feedback is not None: correctness = feedback.correctness else: correctness = None 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) _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 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) else: grading_form_html = None # }}} # {{{ compute points_awarded max_points = None points_awarded = None if (fpctx.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, flow_session.participation.role, 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) 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, "ordinal": fpctx.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, "most_recent_grade": most_recent_grade, "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), })
def finish_flow_session_view(pctx, flow_identifier): request = pctx.request flow_session = find_current_flow_session( request, pctx.course, flow_identifier) if flow_session is None: messages.add_message(request, messages.WARNING, "No session record found for this flow. " "Redirected to flow start page.") return redirect("course.flow.start_flow", pctx.course.identifier, flow_identifier) fctx = FlowContext(pctx.repo, pctx.course, flow_identifier, participation=pctx.participation, flow_session=flow_session) current_access_rule = fctx.get_current_access_rule( flow_session, pctx.role, pctx.participation, get_now_or_fake_time(request)) answer_visits = assemble_answer_visits(flow_session) from course.content import markup_to_html completion_text = markup_to_html( fctx.course, fctx.repo, fctx.flow_commit_sha, fctx.flow_desc.completion_text) (answered_count, unanswered_count) = count_answered( fctx, flow_session, answer_visits) is_graded_flow = bool(answered_count + unanswered_count) if flow_permission.view not in current_access_rule.permissions: raise PermissionDenied() 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) if request.method == "POST": if "submit" not in request.POST: raise SuspiciousOperation("odd POST parameters") if not flow_session.in_progress: raise PermissionDenied("Can't end a session that's already ended") # Actually end the flow session request.session["flow_session_id"] = None grade_info = finish_flow_session(fctx, flow_session, current_access_rule) if is_graded_flow: return render_finish_response( "course/flow-completion-grade.html", completion_text=completion_text, grade_info=grade_info) else: return render_finish_response( "course/flow-completion.html", last_page_nr=None, completion_text=completion_text) # }}} if (not is_graded_flow and fctx.flow_commit_sha == fctx.course_commit_sha): # Not serious--no questions in flow, and no new version available. # No need to end the flow visit. return render_finish_response( "course/flow-completion.html", last_page_nr=flow_session.page_count-1, completion_text=completion_text) elif not flow_session.in_progress: # Just reviewing: re-show grades. grade_info = gather_grade_info(flow_session, answer_visits) return render_finish_response( "course/flow-completion-grade.html", completion_text=completion_text, grade_info=grade_info) else: # confirm ending flow return render_finish_response( "course/flow-confirm-completion.html", last_page_nr=flow_session.page_count-1, answered_count=answered_count, unanswered_count=unanswered_count, total_count=answered_count+unanswered_count)
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, string_concat( pgettext_lazy("Starting of Error message", "Error"), ": %(err_type)s %(err_str)s") % { "err_type": type(e).__name__, "err_str": 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 view_single_grade(pctx, participation_id, opportunity_id): 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 ( 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 == "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, string_concat( pgettext_lazy("Starting of Error message", "Error"), ": %(err_type)s %(err_str)s") % { "err_type": type(e).__name__, "err_str": 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) 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( # noqa "SessionProperties", ["due", "grade_description"]) from course.utils import get_session_grading_rule from course.content import get_flow_desc try: flow_desc = get_flow_desc(pctx.repo, pctx.course, opportunity.flow_id, pctx.course_commit_sha) except ObjectDoesNotExist: flow_sessions_and_session_properties = None else: 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 avg_grade_percentage, avg_grade_population = average_grade(opportunity) return render_course_page(pctx, "course/gradebook-single.html", { "opportunity": opportunity, "avg_grade_percentage": avg_grade_percentage, "avg_grade_population": avg_grade_population, "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 ], })
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 = 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 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"] 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 course.views import get_now_or_fake_time default_date = get_now_or_fake_time(pctx.request).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(), })