def view_reopen_session(pctx, flow_session_id, opportunity_id): if pctx.role not in [ participation_role.instructor, participation_role.teaching_assistant ]: raise PermissionDenied() request = pctx.request session = get_object_or_404(FlowSession, id=int(flow_session_id)) from course.content import get_flow_desc try: flow_desc = get_flow_desc(pctx.repo, pctx.course, session.flow_id, pctx.course_commit_sha) except ObjectDoesNotExist: raise http.Http404() if request.method == "POST": form = ReopenSessionForm(flow_desc, session.access_rules_tag, request.POST, request.FILES) if form.is_valid(): new_access_rules_tag = form.cleaned_data["set_access_rules_tag"] if new_access_rules_tag == NONE_SESSION_TAG: new_access_rules_tag = None session.access_rules_tag = new_access_rules_tag from relate.utils import (local_now, as_local_time, format_datetime_local) session.append_comment( ugettext("Session reopened at %(now)s by %(user)s, " "previous completion time was '%(completion_time)s': " "%(comment)s.") % { "now": format_datetime_local(local_now()), "user": pctx.request.user, "completion_time": format_datetime_local( as_local_time(session.completion_time)), "comment": form.cleaned_data["comment"] }) session.save() from course.flow import reopen_session reopen_session(session, suppress_log=True) return redirect("relate-view_single_grade", pctx.course.identifier, session.participation.id, opportunity_id) else: form = ReopenSessionForm(flow_desc, session.access_rules_tag) return render(request, "generic-form.html", { "form": form, "form_description": _("Reopen session") })
def __init__(self, repo, course, flow_id, participation=None, flow_session=None): """*participation* and *flow_session* are not stored and only used to figure out versioning of the flow content. """ self.repo = repo self.course = course self.flow_id = flow_id from django.core.exceptions import ObjectDoesNotExist self.course_commit_sha = get_course_commit_sha(self.course, participation) try: self.flow_desc = get_flow_desc(self.repo, self.course, flow_id, self.course_commit_sha) except ObjectDoesNotExist: raise http.Http404() if flow_session is not None: from course.content import adjust_flow_session_page_data adjust_flow_session_page_data(repo, flow_session, course.identifier, self.flow_desc)
def __init__(self, repo, course, flow_id, participation=None, flow_session=None): """*participation* and *flow_session* are not stored and only used to figure out versioning of the flow content. """ self.repo = repo self.course = course self.flow_id = flow_id from django.core.exceptions import ObjectDoesNotExist self.course_commit_sha = get_course_commit_sha( self.course, participation) try: self.flow_desc = get_flow_desc(self.repo, self.course, flow_id, self.course_commit_sha) except ObjectDoesNotExist: raise http.Http404() if flow_session is not None: from course.content import adjust_flow_session_page_data adjust_flow_session_page_data(repo, flow_session, course.identifier, self.flow_desc)
def get_flow_desc_from_cache(self, commit_sha): try: return self.flow_desc_cache[commit_sha] except KeyError: flow_desc = get_flow_desc(self.repo, self.course, self.flow_id, commit_sha) self.flow_desc_cache[commit_sha] = flow_desc return flow_desc
def view_reopen_session(pctx, flow_session_id, opportunity_id): if pctx.role not in [ participation_role.instructor, participation_role.teaching_assistant]: raise PermissionDenied() request = pctx.request session = get_object_or_404(FlowSession, id=int(flow_session_id)) from course.content import get_flow_desc try: flow_desc = get_flow_desc(pctx.repo, pctx.course, session.flow_id, pctx.course_commit_sha) except ObjectDoesNotExist: raise http.Http404() if request.method == "POST": form = ReopenSessionForm(flow_desc, session.access_rules_tag, request.POST, request.FILES) if form.is_valid(): new_access_rules_tag = form.cleaned_data["set_access_rules_tag"] if new_access_rules_tag == NONE_SESSION_TAG: new_access_rules_tag = None session.access_rules_tag = new_access_rules_tag from relate.utils import ( local_now, as_local_time, format_datetime_local) session.append_comment( ugettext("Session reopened at %(now)s by %(user)s, " "previous completion time was '%(completion_time)s': " "%(comment)s.") % { "now": format_datetime_local(local_now()), "user": pctx.request.user, "completion_time": format_datetime_local( as_local_time(session.completion_time)), "comment": form.cleaned_data["comment"] }) session.save() from course.flow import reopen_session reopen_session(session, suppress_log=True) return redirect("relate-view_single_grade", pctx.course.identifier, session.participation.id, opportunity_id) else: form = ReopenSessionForm(flow_desc, session.access_rules_tag) return render(request, "generic-form.html", { "form": form, "form_description": _("Reopen session") })
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 __init__(self, repo, course, flow_identifier, participation=None, flow_session=None): """*participation* and *flow_session* are not stored and only used to figure out versioning of the flow content. """ self.repo = repo self.course = course 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. self.course_commit_sha = get_course_commit_sha( self.course, participation) try: current_flow_desc_sha = self.course_commit_sha self.current_flow_desc = get_flow_desc(self.repo, self.course, flow_identifier, current_flow_desc_sha) except ObjectDoesNotExist: if flow_session is None: raise http.Http404() current_flow_desc_sha = flow_session.active_git_commit_sha.encode() self.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, participation, self.current_flow_desc, flow_session) if self.flow_commit_sha == current_flow_desc_sha: self.flow_desc = self.current_flow_desc else: self.flow_desc = get_flow_desc(self.repo, self.course, flow_identifier, self.flow_commit_sha)
def clean(self): super(FlowRuleException, self).clean() if (self.kind == flow_rule_kind.grading and self.expiration is not None): raise ValidationError(_("grading rules may not expire")) from course.validation import ( ValidationError as ContentValidationError, validate_session_start_rule, validate_session_access_rule, validate_session_grading_rule, ValidationContext) from course.content import (get_course_repo, get_course_commit_sha, get_flow_desc) from relate.utils import dict_to_struct rule = dict_to_struct(self.rule) repo = get_course_repo(self.participation.course) commit_sha = get_course_commit_sha( self.participation.course, self.participation) ctx = ValidationContext( repo=repo, commit_sha=commit_sha) flow_desc = get_flow_desc(repo, self.participation.course, self.flow_id, commit_sha) tags = None if hasattr(flow_desc, "rules"): tags = getattr(flow_desc.rules, "tags", None) try: if self.kind == flow_rule_kind.start: validate_session_start_rule(ctx, unicode(self), rule, tags) elif self.kind == flow_rule_kind.access: validate_session_access_rule(ctx, unicode(self), rule, tags) elif self.kind == flow_rule_kind.grading: validate_session_grading_rule(ctx, unicode(self), rule, tags) else: # the rule refers to FlowRuleException rule raise ValidationError(_("invalid rule kind: ")+self.kind) except ContentValidationError as e: # the rule refers to FlowRuleException rule raise ValidationError(_("invalid existing_session_rules: ")+str(e))
def grant_exception_stage_2(pctx, participation_id, flow_id): if pctx.role not in [ participation_role.instructor, participation_role.teaching_assistant]: raise PermissionDenied("must be instructor or TA to grant exceptions") participation = get_object_or_404(Participation, id=participation_id) from course.content import get_flow_desc try: flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) except ObjectDoesNotExist: raise http.Http404() if not hasattr(flow_desc, "access_rules"): messages.add_message(pctx.request, messages.ERROR, "Flow '%s' does not declare access rules." % flow_id) return redirect("course.views.grant_exception", pctx.course.identifier) base_ruleset_choices = [rule.id for rule in flow_desc.access_rules] request = pctx.request if request.method == "POST": form = ExceptionStage2Form(base_ruleset_choices, request.POST) if form.is_valid(): return redirect( "course.views.grant_exception_stage_3", pctx.course.identifier, participation.id, flow_id, form.cleaned_data["base_ruleset"]) else: form = ExceptionStage2Form(base_ruleset_choices) return render_course_page(pctx, "course/generic-course-form.html", { "form": form, "form_description": "Grant Exception", })
def __init__(self, repo, course, flow_id, participation=None): # type: (Repo_ish, Course, Text, Optional[Participation]) -> None """*participation* and *flow_session* are not stored and only used to figure out versioning of the flow content. """ self.repo = repo self.course = course self.flow_id = flow_id from django.core.exceptions import ObjectDoesNotExist self.course_commit_sha = get_course_commit_sha(self.course, participation) try: self.flow_desc = get_flow_desc(self.repo, self.course, flow_id, self.course_commit_sha) except ObjectDoesNotExist: raise http.Http404()
def clean(self): if (self.kind == flow_rule_kind.grading and self.expiration is not None): raise ValidationError("grading rules may not expire") from course.validation import (ValidationError as ContentValidationError, validate_session_start_rule, validate_session_access_rule, validate_session_grading_rule, ValidationContext) from course.content import (get_course_repo, get_course_commit_sha, get_flow_desc) from relate.utils import dict_to_struct rule = dict_to_struct(self.rule) repo = get_course_repo(self.participation.course) commit_sha = get_course_commit_sha(self.participation.course, self.participation) ctx = ValidationContext(repo=repo, commit_sha=commit_sha) flow_desc = get_flow_desc(repo, self.participation.course, self.flow_id, commit_sha) tags = None if hasattr(flow_desc, "rules"): tags = getattr(flow_desc.rules, "tags", None) try: if self.kind == flow_rule_kind.start: validate_session_start_rule(ctx, unicode(self), rule, tags) elif self.kind == flow_rule_kind.access: validate_session_access_rule(ctx, unicode(self), rule, tags) elif self.kind == flow_rule_kind.grading: validate_session_grading_rule(ctx, unicode(self), rule, tags) else: raise ValidationError("invalid rule kind: " + self.kind) except ContentValidationError as e: raise ValidationError("invalid existing_session_rules: " + str(e))
def __init__(self, repo, course, flow_id, participation=None): # type: (Repo_ish, Course, Text, Optional[Participation]) -> None """*participation* and *flow_session* are not stored and only used to figure out versioning of the flow content. """ self.repo = repo self.course = course self.flow_id = flow_id from django.core.exceptions import ObjectDoesNotExist self.course_commit_sha = get_course_commit_sha( self.course, participation) try: self.flow_desc = get_flow_desc(self.repo, self.course, flow_id, self.course_commit_sha) except ObjectDoesNotExist: raise http.Http404()
def _get_current_access_rule(participation, flow_id): course = participation.course from course.content import ( get_course_repo, get_course_commit_sha, get_flow_desc) repo = get_course_repo(course) course_commit_sha = get_course_commit_sha(course, participation) try: flow_desc = get_flow_desc(repo, course, flow_id.encode(), course_commit_sha) except ObjectDoesNotExist: return None else: from course.utils import get_current_flow_access_rule from django.utils.timezone import now return get_current_flow_access_rule(course, participation, participation.role, flow_id, flow_desc, now(), rule_id=None, use_exceptions=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 make_page_answer_stats_list(pctx, flow_identifier): flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_identifier, pctx.course_commit_sha) is_multiple_submit = is_flow_multiple_submit(flow_desc) page_cache = PageInstanceCache(pctx.repo, pctx.course, flow_identifier) page_info_list = [] for group_desc in flow_desc.groups: for page_desc in group_desc.pages: points = 0 count = 0 visits = (FlowPageVisit.objects .filter( flow_session__flow_id=flow_identifier, page_data__group_id=group_desc.id, page_data__page_id=page_desc.id, is_graded_answer=True, )) if is_multiple_submit and connection.features.can_distinct_on_fields: visits = (visits .distinct("page_data") .order_by("page_data", "-visit_time")) visits = (visits .prefetch_related("flow_session") .prefetch_related("page_data")) title = None for visit in visits: flow_commit_sha = get_flow_commit_sha( pctx.course, pctx.participation, flow_desc, visit.flow_session) page = page_cache.get_page(group_desc.id, page_desc.id, flow_commit_sha) from course.page import PageContext grading_page_context = PageContext( course=pctx.course, repo=pctx.repo, commit_sha=flow_commit_sha) title = page.title(grading_page_context, visit.page_data.data) answer_feedback = visit.get_most_recent_feedback() if (answer_feedback is not None and answer_feedback.correctness is not None): count += 1 points += answer_feedback.correctness page_info_list.append( PageAnswerStats( group_id=group_desc.id, page_id=page_desc.id, title=title, average_correctness=safe_div(points, count), answer_count=count, url=reverse( "course.analytics.page_analytics", args=( pctx.course_identifier, flow_identifier, group_desc.id, page_desc.id, )))) return page_info_list
def page_analytics(pctx, flow_id, group_id, page_id): if pctx.role not in [ participation_role.teaching_assistant, participation_role.instructor, participation_role.observer, ]: raise PermissionDenied(_("must be at least TA to view analytics")) flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) restrict_to_first_attempt = int( bool(pctx.request.GET.get("restrict_to_first_attempt") == "1")) is_multiple_submit = is_flow_multiple_submit(flow_desc) page_cache = PageInstanceCache(pctx.repo, pctx.course, flow_id) visits = (FlowPageVisit.objects .filter( flow_session__course=pctx.course, flow_session__flow_id=flow_id, page_data__group_id=group_id, page_data__page_id=page_id, is_submitted_answer=True, )) if connection.features.can_distinct_on_fields: if restrict_to_first_attempt: visits = (visits .distinct("flow_session__participation__id") .order_by("flow_session__participation__id", "visit_time")) elif is_multiple_submit: visits = (visits .distinct("page_data__id") .order_by("page_data__id", "-visit_time")) visits = (visits .select_related("flow_session") .select_related("page_data")) normalized_answer_and_correctness_to_count = {} title = None body = None total_count = 0 graded_count = 0 for visit in visits: page = page_cache.get_page(group_id, page_id, pctx.course_commit_sha) from course.page import PageContext grading_page_context = PageContext( course=pctx.course, repo=pctx.repo, commit_sha=pctx.course_commit_sha, flow_session=visit.flow_session) title = page.title(grading_page_context, visit.page_data.data) body = page.body(grading_page_context, visit.page_data.data) normalized_answer = page.normalized_answer( grading_page_context, visit.page_data.data, visit.answer) answer_feedback = visit.get_most_recent_feedback() if answer_feedback is not None: key = (normalized_answer, answer_feedback.correctness) normalized_answer_and_correctness_to_count[key] = \ normalized_answer_and_correctness_to_count.get(key, 0) + 1 graded_count += 1 else: key = (normalized_answer, None) normalized_answer_and_correctness_to_count[key] = \ normalized_answer_and_correctness_to_count.get(key, 0) + 1 total_count += 1 answer_stats = [] for (normalized_answer, correctness), count in \ normalized_answer_and_correctness_to_count.iteritems(): answer_stats.append( AnswerStats( normalized_answer=normalized_answer, correctness=correctness, count=count, percentage=safe_div(100 * count, total_count))) answer_stats = sorted( answer_stats, key=lambda astats: astats.percentage, reverse=True) return render_course_page(pctx, "course/analytics-page.html", { "flow_identifier": flow_id, "group_id": group_id, "page_id": page_id, "title": title, "body": body, "answer_stats_list": answer_stats, "restrict_to_first_attempt": restrict_to_first_attempt, })
def 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 grant_exception_stage_3(pctx, participation_id, flow_id, session_id): if pctx.role not in [ participation_role.instructor, participation_role.teaching_assistant]: raise PermissionDenied("must be instructor or TA to grant exceptions") participation = get_object_or_404(Participation, id=participation_id) from course.content import get_flow_desc try: flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) except ObjectDoesNotExist: raise http.Http404() session = FlowSession.objects.get(id=int(session_id)) now_datetime = get_now_or_fake_time(pctx.request) from course.utils import ( get_session_access_rule, get_session_grading_rule) access_rule = get_session_access_rule( session, pctx.role, flow_desc, now_datetime) grading_rule = get_session_grading_rule( session, pctx.role, flow_desc, now_datetime) request = pctx.request if request.method == "POST": form = ExceptionStage3Form({}, session.access_rules_tag, request.POST) from course.constants import flow_rule_kind if form.is_valid(): permissions = [ key for key, _ in FLOW_PERMISSION_CHOICES if form.cleaned_data[key]] from course.validation import ( validate_session_access_rule, validate_session_grading_rule, ValidationContext) from relate.utils import dict_to_struct vctx = ValidationContext( repo=pctx.repo, commit_sha=pctx.course_commit_sha) from course.content import get_flow_desc flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) tags = None if hasattr(flow_desc, "rules"): tags = getattr(flow_desc.rules, "tags", None) # {{{ put together access rule new_access_rule = {"permissions": permissions} if (form.cleaned_data["restrict_to_same_tag"] and session.access_rules_tag is not None): new_access_rule["if_has_tag"] = session.access_rules_tag validate_session_access_rule(vctx, "newly created exception", dict_to_struct(new_access_rule), tags) fre_access = FlowRuleException( flow_id=flow_id, participation=participation, expiration=form.cleaned_data["access_expires"], creator=pctx.request.user, comment=form.cleaned_data["comment"], kind=flow_rule_kind.access, rule=new_access_rule) fre_access.save() # }}} # {{{ put together grading rule due = form.cleaned_data["due"] if form.cleaned_data["due_same_as_access_expiration"]: due = form.cleaned_data["access_expires"] descr = "Granted excecption" if form.cleaned_data["credit_percent"] is not None: descr += " (%.1f%% credit)" % form.cleaned_data["credit_percent"] from relate.utils import as_local_time new_grading_rule = { "credit_percent": form.cleaned_data["credit_percent"], "due": as_local_time(due).replace(tzinfo=None), "if_completed_before": as_local_time(due).replace(tzinfo=None), "description": descr, } if (form.cleaned_data["restrict_to_same_tag"] and session.access_rules_tag is not None): new_grading_rule["if_has_tag"] = session.access_rules_tag if hasattr(grading_rule, "grade_identifier"): new_grading_rule["grade_identifier"] = \ grading_rule.grade_identifier if hasattr(grading_rule, "grade_aggregation_strategy"): new_grading_rule["grade_aggregation_strategy"] = \ grading_rule.grade_aggregation_strategy validate_session_grading_rule(vctx, "newly created exception", dict_to_struct(new_grading_rule), tags) fre_grading = FlowRuleException( flow_id=flow_id, participation=participation, creator=pctx.request.user, comment=form.cleaned_data["comment"], kind=flow_rule_kind.grading, rule=new_grading_rule) fre_grading.save() # }}} messages.add_message(pctx.request, messages.SUCCESS, "Exception granted to '%s' for '%s'." % (participation, flow_id)) return redirect( "course.views.grant_exception", pctx.course.identifier) else: data = { "restrict_to_same_tag": session.access_rules_tag is not None, "credit_percent": grading_rule.credit_percent, #"due_same_as_access_expiration": True, "due": grading_rule.due, } for perm in access_rule.permissions: data[perm] = True form = ExceptionStage3Form(data, session.access_rules_tag) return render_course_page(pctx, "course/generic-course-form.html", { "form": form, "form_description": "Grant Exception", "form_text": "<div class='well'>Granting exception to '%s' for '%s'.</div>" % (participation, flow_id), })
def make_page_answer_stats_list(pctx, flow_id, restrict_to_first_attempt): flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) page_cache = PageInstanceCache(pctx.repo, pctx.course, flow_id) page_info_list = [] for group_desc in flow_desc.groups: for page_desc in group_desc.pages: points = 0 graded_count = 0 empty_count = 0 answer_count = 0 total_count = 0 visits = (FlowPageVisit.objects .filter( flow_session__course=pctx.course, flow_session__flow_id=flow_id, page_data__group_id=group_desc.id, page_data__page_id=page_desc.id, is_submitted_answer=True, )) if connection.features.can_distinct_on_fields: if restrict_to_first_attempt: visits = (visits .distinct("flow_session__participation__id") .order_by("flow_session__participation__id", "visit_time")) elif is_page_multiple_submit(flow_desc, page_desc): visits = (visits .distinct("page_data__id") .order_by("page_data__id", "-visit_time")) visits = (visits .select_related("flow_session") .select_related("page_data")) answer_expected = False title = None for visit in visits: page = page_cache.get_page(group_desc.id, page_desc.id, pctx.course_commit_sha) answer_expected = answer_expected or page.expects_answer() from course.page import PageContext grading_page_context = PageContext( course=pctx.course, repo=pctx.repo, commit_sha=pctx.course_commit_sha, flow_session=visit.flow_session) title = page.title(grading_page_context, visit.page_data.data) answer_feedback = visit.get_most_recent_feedback() if visit.answer is not None: answer_count += 1 else: empty_count += 1 total_count += 1 if (answer_feedback is not None and answer_feedback.correctness is not None): if visit.answer is None: assert answer_feedback.correctness == 0 else: points += answer_feedback.correctness graded_count += 1 if not answer_expected: continue page_info_list.append( PageAnswerStats( group_id=group_desc.id, page_id=page_desc.id, title=title, average_correctness=safe_div(points, graded_count), average_emptiness=safe_div( empty_count, graded_count), answer_count=answer_count, total_count=total_count, url=reverse( "relate-page_analytics", args=( pctx.course_identifier, flow_id, group_desc.id, page_desc.id, )))) return page_info_list
def grant_exception_stage_3(pctx, participation_id, flow_id, base_ruleset): if pctx.role not in [ participation_role.instructor, participation_role.teaching_assistant]: raise PermissionDenied("must be instructor or TA to grant exceptions") participation = get_object_or_404(Participation, id=participation_id) from course.content import get_flow_desc try: flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) except ObjectDoesNotExist: raise http.Http404() if not hasattr(flow_desc, "access_rules"): messages.add_message(pctx.request, messages.ERROR, "Flow '%s' does not declare access rules." % flow_id) return redirect("course.views.grant_exception", pctx.course.identifier) ruleset = None for def_ruleset in flow_desc.access_rules: if def_ruleset.id == base_ruleset: ruleset = def_ruleset if ruleset is None: raise http.Http404() STIPULATION_KEYS = ["allowed_session_count", "credit_percent"] request = pctx.request if request.method == "POST": form = ExceptionStage3Form(request.POST) if form.is_valid(): fae = FlowAccessException() fae.participation = participation fae.flow_id = flow_id fae.expiration = form.cleaned_data["expiration"] fae.stipulations = {} for stip_key in STIPULATION_KEYS: if form.cleaned_data[stip_key] is not None: fae.stipulations[stip_key] = form.cleaned_data[stip_key] fae.creator = pctx.request.user fae.is_sticky = form.cleaned_data["sticky"] fae.comment = form.cleaned_data["comment"] fae.save() for key, _ in FLOW_PERMISSION_CHOICES: if form.cleaned_data[key]: faee = FlowAccessExceptionEntry() faee.exception = fae faee.permission = key faee.save() if form.cleaned_data["update_session"]: sessions = FlowSession.objects.filter( participation=participation, flow_id=flow_id, in_progress=True) assert sessions.count() <= 1 for session in sessions: session.access_rules_id = "exception" session.save() messages.add_message(pctx.request, messages.SUCCESS, "Exception granted.") return redirect( "course.views.grant_exception", pctx.course.identifier) else: data = { "update_session": True, "sticky": getattr(ruleset, "sticky", False), } for perm in ruleset.permissions: data[perm] = True for stip_key in STIPULATION_KEYS: if hasattr(ruleset, stip_key): data[stip_key] = getattr(ruleset, stip_key) form = ExceptionStage3Form(data) return render_course_page(pctx, "course/generic-course-form.html", { "form": form, "form_description": "Grant Exception", "form_text": "<div class='well'>Granting exception to '%s' for '%s'.</div>" % (participation, flow_id), })
def grade_page_visit(visit, visit_grade_model=FlowPageVisitGrade, grade_data=None, graded_at_git_commit_sha=None): if not visit.is_graded_answer: raise RuntimeError("cannot grade ungraded answer") flow_session = visit.flow_session course = flow_session.course page_data = visit.page_data from course.content import ( get_course_repo, get_course_commit_sha, get_flow_commit_sha, get_flow_desc, get_flow_page_desc, instantiate_flow_page) repo = get_course_repo(course) course_commit_sha = get_course_commit_sha( course, flow_session.participation) flow_desc_pre = get_flow_desc(repo, course, flow_session.flow_id, course_commit_sha) flow_commit_sha = get_flow_commit_sha( course, flow_session.participation, flow_desc_pre, visit.flow_session) flow_desc = get_flow_desc(repo, course, flow_session.flow_id, flow_commit_sha) page_desc = get_flow_page_desc( flow_session.flow_id, flow_desc, page_data.group_id, page_data.page_id) page = instantiate_flow_page( location="flow '%s', group, '%s', page '%s'" % (flow_session.flow_id, page_data.group_id, page_data.page_id), repo=repo, page_desc=page_desc, commit_sha=flow_commit_sha) from course.page import PageContext grading_page_context = PageContext( course=course, repo=repo, commit_sha=flow_commit_sha) answer_feedback = page.grade( grading_page_context, visit.page_data.data, visit.answer, grade_data=grade_data) grade = visit_grade_model() grade.visit = visit grade.grade_data = grade_data grade.max_points = page.max_points(visit.page_data) grade.graded_at_git_commit_sha = graded_at_git_commit_sha if answer_feedback is not None: grade.correctness = answer_feedback.correctness grade.feedback = answer_feedback.as_json() grade.save()
def page_analytics(pctx, flow_identifier, group_id, page_id): if pctx.role not in [ participation_role.teaching_assistant, participation_role.instructor]: raise PermissionDenied("must be at least TA to view analytics") flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_identifier, pctx.course_commit_sha) page_cache = PageInstanceCache(pctx.repo, pctx.course, flow_identifier) visits = (FlowPageVisit.objects .filter( flow_session__flow_id=flow_identifier, page_data__group_id=group_id, page_data__page_id=page_id, is_graded_answer=True, ) .prefetch_related("flow_session") .prefetch_related("page_data")) normalized_answer_and_correctness_to_count = {} title = None body = None total_count = 0 for visit in visits: flow_commit_sha = get_flow_commit_sha( pctx.course, pctx.participation, flow_desc, visit.flow_session) page = page_cache.get_page(group_id, page_id, flow_commit_sha) from course.page import PageContext grading_page_context = PageContext( course=pctx.course, repo=pctx.repo, commit_sha=flow_commit_sha) title = page.title(grading_page_context, visit.page_data.data) body = page.body(grading_page_context, visit.page_data.data) answer_feedback = visit.get_most_recent_feedback() if answer_feedback is not None: key = (answer_feedback.normalized_answer, answer_feedback.correctness) normalized_answer_and_correctness_to_count[key] = \ normalized_answer_and_correctness_to_count.get(key, 0) + 1 total_count += 1 answer_stats = [] for (normalized_answer, correctness), count in \ normalized_answer_and_correctness_to_count.iteritems(): answer_stats.append( AnswerStats( normalized_answer=normalized_answer, correctness=correctness, count=count, percentage=safe_div(100 * count, total_count))) answer_stats = sorted( answer_stats, key=lambda astats: astats.percentage, reverse=True) return render_course_page(pctx, "course/analytics-page.html", { "flow_identifier": flow_identifier, "group_id": group_id, "page_id": page_id, "title": title, "body": body, "answer_stats_list": answer_stats, })
def download_all_submissions(pctx, flow_id): if pctx.role not in [ participation_role.teaching_assistant, participation_role.instructor, participation_role.observer, ]: raise PermissionDenied(_("must be at least TA to download submissions")) from course.content import get_flow_desc flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) # {{{ find access rules tag if hasattr(flow_desc, "rules"): access_rules_tags = getattr(flow_desc.rules, "tags", []) else: access_rules_tags = [] ALL_SESSION_TAG = string_concat("<<<", _("ALL"), ">>>") # noqa session_tag_choices = [ (tag, tag) for tag in access_rules_tags] + [(ALL_SESSION_TAG, string_concat("(", _("ALL"), ")"))] # }}} page_ids = [ "%s/%s" % (group_desc.id, page_desc.id) for group_desc in flow_desc.groups for page_desc in group_desc.pages] request = pctx.request if request.method == "POST": form = DownloadAllSubmissionsForm(page_ids, session_tag_choices, request.POST) if form.is_valid(): which_attempt = form.cleaned_data["which_attempt"] slash_index = form.cleaned_data["page_id"].index("/") group_id = form.cleaned_data["page_id"][:slash_index] page_id = form.cleaned_data["page_id"][slash_index+1:] from course.utils import PageInstanceCache page_cache = PageInstanceCache(pctx.repo, pctx.course, flow_id) visits = (FlowPageVisit.objects .filter( flow_session__course=pctx.course, flow_session__flow_id=flow_id, page_data__group_id=group_id, page_data__page_id=page_id, is_submitted_answer=True, ) .select_related("flow_session") .select_related("flow_session__participation__user") .select_related("page_data") # We overwrite earlier submissions with later ones # in a dictionary below. .order_by("visit_time")) if form.cleaned_data["non_in_progress_only"]: visits = visits.filter(flow_session__in_progress=False) if form.cleaned_data["restrict_to_rules_tag"] != ALL_SESSION_TAG: visits = visits.filter( flow_session__access_rules_tag=( form.cleaned_data["restrict_to_rules_tag"])) submissions = {} for visit in visits: page = page_cache.get_page(group_id, page_id, pctx.course_commit_sha) from course.page import PageContext grading_page_context = PageContext( course=pctx.course, repo=pctx.repo, commit_sha=pctx.course_commit_sha, flow_session=visit.flow_session) bytes_answer = page.normalized_bytes_answer( grading_page_context, visit.page_data.data, visit.answer) if which_attempt in ["first", "last"]: key = (visit.flow_session.participation.user.username,) elif which_attempt == "all": key = (visit.flow_session.participation.user.username, str(visit.flow_session.id)) else: raise NotImplementedError() if bytes_answer is not None: if (which_attempt == "first" and key in submissions): # Already there, disregard further ones continue submissions[key] = bytes_answer from six import BytesIO from zipfile import ZipFile bio = BytesIO() with ZipFile(bio, "w") as subm_zip: for key, (extension, bytes_answer) in \ six.iteritems(submissions): subm_zip.writestr( "-".join(key) + extension, bytes_answer) extra_file = request.FILES.get("extra_file") if extra_file is not None: subm_zip.writestr(extra_file.name, extra_file.read()) response = http.HttpResponse( bio.getvalue(), content_type="application/zip") response['Content-Disposition'] = ( 'attachment; filename="submissions_%s_%s_%s_%s_%s.zip"' % (pctx.course.identifier, flow_id, group_id, page_id, now().date().strftime("%Y-%m-%d"))) return response else: form = DownloadAllSubmissionsForm(page_ids, session_tag_choices) return render_course_page(pctx, "course/generic-course-form.html", { "form": form, "form_description": _("Download All Submissions in Zip file") })
def grant_exception_stage_3(pctx, participation_id, flow_id, session_id): # type: (CoursePageContext, int, Text, int) -> http.HttpResponse if not pctx.has_permission(pperm.grant_exception): raise PermissionDenied(_("may not grant exceptions")) participation = get_object_or_404(Participation, id=participation_id) from course.content import get_flow_desc try: flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) except ObjectDoesNotExist: raise http.Http404() session = FlowSession.objects.get(id=int(session_id)) now_datetime = get_now_or_fake_time(pctx.request) from course.utils import (get_session_access_rule, get_session_grading_rule) access_rule = get_session_access_rule(session, flow_desc, now_datetime) grading_rule = get_session_grading_rule(session, flow_desc, now_datetime) request = pctx.request if request.method == "POST": form = ExceptionStage3Form({}, flow_desc, session.access_rules_tag, request.POST) from course.constants import flow_rule_kind if form.is_valid(): permissions = [ key for key, _ in FLOW_PERMISSION_CHOICES if form.cleaned_data[key] ] from course.validation import (validate_session_access_rule, validate_session_grading_rule, ValidationContext) from relate.utils import dict_to_struct vctx = ValidationContext(repo=pctx.repo, commit_sha=pctx.course_commit_sha) flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) tags = [] # type: List[Text] if hasattr(flow_desc, "rules"): try: from typing import Text # noqa except ImportError: Text = None # noqa tags = cast(List[Text], getattr(flow_desc.rules, "tags", [])) # type: ignore # noqa # {{{ put together access rule if form.cleaned_data["create_access_exception"]: new_access_rule = {"permissions": permissions} if (form.cleaned_data.get("restrict_to_same_tag") and session.access_rules_tag is not None): new_access_rule["if_has_tag"] = session.access_rules_tag validate_session_access_rule( vctx, ugettext("newly created exception"), dict_to_struct(new_access_rule), tags) fre_access = FlowRuleException( flow_id=flow_id, participation=participation, expiration=form.cleaned_data["access_expires"], creator=pctx.request.user, comment=form.cleaned_data["comment"], kind=flow_rule_kind.access, rule=new_access_rule) fre_access.save() # }}} new_access_rules_tag = form.cleaned_data.get( "set_access_rules_tag") if new_access_rules_tag == NONE_SESSION_TAG: new_access_rules_tag = None if session.access_rules_tag != new_access_rules_tag: session.access_rules_tag = new_access_rules_tag session.save() # {{{ put together grading rule if form.cleaned_data["create_grading_exception"]: due = form.cleaned_data["due"] if form.cleaned_data["due_same_as_access_expiration"]: due = form.cleaned_data["access_expires"] descr = ugettext("Granted excecption") if form.cleaned_data["credit_percent"] is not None: descr += string_concat(" (%.1f%% ", ugettext('credit'), ")") \ % form.cleaned_data["credit_percent"] due_local_naive = due if due_local_naive is not None: from relate.utils import as_local_time due_local_naive = (as_local_time(due_local_naive).replace( tzinfo=None)) new_grading_rule = { "description": descr, } if due_local_naive is not None: new_grading_rule["due"] = due_local_naive new_grading_rule["if_completed_before"] = due_local_naive for attr_name in [ "credit_percent", "bonus_points", "max_points", "max_points_enforced_cap", "generates_grade" ]: if form.cleaned_data[attr_name] is not None: new_grading_rule[attr_name] = form.cleaned_data[ attr_name] if (form.cleaned_data.get("restrict_to_same_tag") and session.access_rules_tag is not None): new_grading_rule["if_has_tag"] = session.access_rules_tag validate_session_grading_rule( vctx, ugettext("newly created exception"), dict_to_struct(new_grading_rule), tags, grading_rule.grade_identifier) fre_grading = FlowRuleException( flow_id=flow_id, participation=participation, creator=pctx.request.user, comment=form.cleaned_data["comment"], kind=flow_rule_kind.grading, rule=new_grading_rule) fre_grading.save() # }}} messages.add_message( pctx.request, messages.SUCCESS, ugettext("Exception granted to '%(participation)s' " "for '%(flow_id)s'.") % { 'participation': participation, 'flow_id': flow_id }) return redirect("relate-grant_exception", pctx.course.identifier) else: data = { "restrict_to_same_tag": session.access_rules_tag is not None, #"due_same_as_access_expiration": True, "due": grading_rule.due, "generates_grade": grading_rule.generates_grade, "credit_percent": grading_rule.credit_percent, "bonus_points": grading_rule.bonus_points, "max_points": grading_rule.max_points, "max_points_enforced_cap": grading_rule.max_points_enforced_cap, } for perm in access_rule.permissions: data[perm] = True form = ExceptionStage3Form(data, flow_desc, session.access_rules_tag) return render_course_page( pctx, "course/generic-course-form.html", { "form": form, "form_description": ugettext("Grant Exception"), "form_text": string_concat( "<div class='well'>", ugettext("Granting exception to '%(participation)s' " "for '%(flow_id)s' (session %(session)s)."), "</div>") % { 'participation': participation, 'flow_id': flow_id, 'session': strify_session_for_exception(session) }, })
def grant_exception_stage_2(pctx, participation_id, flow_id): # type: (CoursePageContext, Text, Text) -> http.HttpResponse if not pctx.has_permission(pperm.grant_exception): raise PermissionDenied(_("may not grant exceptions")) # {{{ get flow data participation = get_object_or_404(Participation, id=participation_id) form_text = (string_concat( "<div class='well'>", ugettext("Granting exception to '%(participation)s' for " "'%(flow_id)s'."), "</div>") % { 'participation': participation, 'flow_id': flow_id }) from course.content import get_flow_desc try: flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) except ObjectDoesNotExist: raise http.Http404() now_datetime = get_now_or_fake_time(pctx.request) if hasattr(flow_desc, "rules"): access_rules_tags = getattr(flow_desc.rules, "tags", []) else: access_rules_tags = [] NONE_SESSION_TAG = string_concat("<<<", _("NONE"), ">>>") # noqa session_tag_choices = [(tag, tag) for tag in access_rules_tags] + [ (NONE_SESSION_TAG, string_concat("(", _("NONE"), ")")) ] from course.utils import get_session_start_rule session_start_rule = get_session_start_rule(pctx.course, participation, flow_id, flow_desc, now_datetime) create_session_is_override = False if not session_start_rule.may_start_new_session: create_session_is_override = True form_text += ( "<div class='alert alert-info'>%s</div>" % (string_concat( "<i class='fa fa-info-circle'></i> ", _("Creating a new session is (technically) not allowed " "by course rules. Clicking 'Create Session' anyway will " "override this rule.")))) default_tag = session_start_rule.tag_session if default_tag is None: default_tag = NONE_SESSION_TAG # }}} def find_sessions(): # type: () -> List[FlowSession] return list( FlowSession.objects.filter(participation=participation, flow_id=flow_id).order_by("start_time")) exception_form = None request = pctx.request if request.method == "POST": exception_form = ExceptionStage2Form(find_sessions(), request.POST) create_session_form = CreateSessionForm(session_tag_choices, default_tag, create_session_is_override, request.POST) if "create_session" in request.POST or "next" in request.POST: pass else: raise SuspiciousOperation(_("invalid command")) if create_session_form.is_valid() and "create_session" in request.POST: from course.flow import start_flow access_rules_tag = ( create_session_form. cleaned_data["access_rules_tag_for_new_session"]) if access_rules_tag == NONE_SESSION_TAG: access_rules_tag = None start_flow(pctx.repo, pctx.course, participation, user=participation.user, flow_id=flow_id, flow_desc=flow_desc, session_start_rule=session_start_rule, now_datetime=now_datetime) exception_form = None elif exception_form.is_valid( ) and "next" in request.POST: # type: ignore return redirect( "relate-grant_exception_stage_3", pctx.course.identifier, participation.id, flow_id, exception_form.cleaned_data["session"]) # type: ignore else: create_session_form = CreateSessionForm(session_tag_choices, default_tag, create_session_is_override) if exception_form is None: exception_form = ExceptionStage2Form(find_sessions()) return render_course_page( pctx, "course/generic-course-form.html", { "forms": [exception_form, create_session_form], "form_text": form_text, "form_description": _("Grant Exception"), })
def download_all_submissions(pctx, flow_id): if pctx.role not in [ participation_role.teaching_assistant, participation_role.instructor, participation_role.observer, ]: raise PermissionDenied( _("must be at least TA to download submissions")) from course.content import get_flow_desc flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) # {{{ find access rules tag if hasattr(flow_desc, "rules"): access_rules_tags = getattr(flow_desc.rules, "tags", []) else: access_rules_tags = [] ALL_SESSION_TAG = string_concat("<<<", _("ALL"), ">>>") # noqa session_tag_choices = [(tag, tag) for tag in access_rules_tags] + [ (ALL_SESSION_TAG, string_concat("(", _("ALL"), ")")) ] # }}} page_ids = [ "%s/%s" % (group_desc.id, page_desc.id) for group_desc in flow_desc.groups for page_desc in group_desc.pages ] request = pctx.request if request.method == "POST": form = DownloadAllSubmissionsForm(page_ids, session_tag_choices, request.POST) if form.is_valid(): which_attempt = form.cleaned_data["which_attempt"] slash_index = form.cleaned_data["page_id"].index("/") group_id = form.cleaned_data["page_id"][:slash_index] page_id = form.cleaned_data["page_id"][slash_index + 1:] from course.utils import PageInstanceCache page_cache = PageInstanceCache(pctx.repo, pctx.course, flow_id) visits = ( FlowPageVisit.objects.filter( flow_session__course=pctx.course, flow_session__flow_id=flow_id, page_data__group_id=group_id, page_data__page_id=page_id, is_submitted_answer=True, ).select_related("flow_session").select_related( "flow_session__participation__user").select_related( "page_data") # We overwrite earlier submissions with later ones # in a dictionary below. .order_by("visit_time")) if form.cleaned_data["non_in_progress_only"]: visits = visits.filter(flow_session__in_progress=False) if form.cleaned_data["restrict_to_rules_tag"] != ALL_SESSION_TAG: visits = visits.filter(flow_session__access_rules_tag=( form.cleaned_data["restrict_to_rules_tag"])) submissions = {} for visit in visits: page = page_cache.get_page(group_id, page_id, pctx.course_commit_sha) from course.page import PageContext grading_page_context = PageContext( course=pctx.course, repo=pctx.repo, commit_sha=pctx.course_commit_sha, flow_session=visit.flow_session) bytes_answer = page.normalized_bytes_answer( grading_page_context, visit.page_data.data, visit.answer) if which_attempt in ["first", "last"]: key = (visit.flow_session.participation.user.username, ) elif which_attempt == "all": key = (visit.flow_session.participation.user.username, str(visit.flow_session.id)) else: raise NotImplementedError() if bytes_answer is not None: if (which_attempt == "first" and key in submissions): # Already there, disregard further ones continue submissions[key] = bytes_answer from six import BytesIO from zipfile import ZipFile bio = BytesIO() with ZipFile(bio, "w") as subm_zip: for key, (extension, bytes_answer) in \ six.iteritems(submissions): subm_zip.writestr("-".join(key) + extension, bytes_answer) extra_file = request.FILES.get("extra_file") if extra_file is not None: subm_zip.writestr(extra_file.name, extra_file.read()) response = http.HttpResponse(bio.getvalue(), content_type="application/zip") response['Content-Disposition'] = ( 'attachment; filename="submissions_%s_%s_%s_%s_%s.zip"' % (pctx.course.identifier, flow_id, group_id, page_id, now().date().strftime("%Y-%m-%d"))) return response else: form = DownloadAllSubmissionsForm(page_ids, session_tag_choices) return render_course_page( pctx, "course/generic-course-form.html", { "form": form, "form_description": _("Download All Submissions in Zip file") })
def grant_exception_stage_2(pctx, participation_id, flow_id): if pctx.role not in [ participation_role.instructor, participation_role.teaching_assistant]: raise PermissionDenied("must be instructor or TA to grant exceptions") # {{{ get flow data participation = get_object_or_404(Participation, id=participation_id) form_text = ("<div class='well'>Granting exception to '%s' for '%s'.</div>" % (participation, flow_id)) from course.content import get_flow_desc try: flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) except ObjectDoesNotExist: raise http.Http404() now_datetime = get_now_or_fake_time(pctx.request) if hasattr(flow_desc, "rules"): access_rules_tags = getattr(flow_desc.rules, "tags", []) else: access_rules_tags = [] NONE_SESSION_TAG = "<<<NONE>>>" session_tag_choices = [ (tag, tag) for tag in access_rules_tags] + [(NONE_SESSION_TAG, "(None)")] from course.utils import get_session_start_rule session_start_rule = get_session_start_rule(pctx.course, participation, participation.role, flow_id, flow_desc, now_datetime) create_session_is_override = False if not session_start_rule.may_start_new_session: create_session_is_override = True form_text += ("<div class='alert alert-info'>%s</div>" % "Creating a new session is (technically) not allowed by course " "rules. Clicking 'Create Session' anyway will override this rule.") default_tag = session_start_rule.tag_session if default_tag is None: default_tag = NONE_SESSION_TAG # }}} def find_sessions(): return (FlowSession.objects .filter( participation=participation, flow_id=flow_id) .order_by("start_time")) form = None request = pctx.request if request.method == "POST": form = ExceptionStage2Form( session_tag_choices, default_tag, create_session_is_override, find_sessions(), request.POST) if "create_session" in request.POST or "next" in request.POST: pass else: raise SuspiciousOperation("invalid command") if form.is_valid() and "create_session" in request.POST: from course.flow import start_flow access_rules_tag = form.cleaned_data["access_rules_tag_for_new_session"] if access_rules_tag == NONE_SESSION_TAG: access_rules_tag = None start_flow(pctx.repo, pctx.course, participation, flow_id, flow_desc=flow_desc, access_rules_tag=access_rules_tag, now_datetime=now_datetime) form = None elif form.is_valid() and "next" in request.POST: return redirect( "course.views.grant_exception_stage_3", pctx.course.identifier, participation.id, flow_id, form.cleaned_data["session"]) if form is None: form = ExceptionStage2Form(session_tag_choices, default_tag, create_session_is_override, find_sessions()) return render_course_page(pctx, "course/generic-course-form.html", { "form": form, "form_text": form_text, "form_description": "Grant Exception", })
def convert_flow_page_visit(stderr, fpv): course = fpv.flow_session.course from course.content import (get_course_repo, get_flow_desc, get_flow_page_desc, instantiate_flow_page) repo = get_course_repo(course) flow_id = fpv.flow_session.flow_id commit_sha = course.active_git_commit_sha.encode() try: flow_desc = get_flow_desc(repo, course, flow_id, commit_sha, tolerate_tabs=True) except ObjectDoesNotExist: stderr.write("warning: no flow yaml file found for '%s' in '%s'" % (flow_id, course.identifier)) return try: page_desc = get_flow_page_desc(fpv.flow_session.flow_id, flow_desc, fpv.page_data.group_id, fpv.page_data.page_id) except ObjectDoesNotExist: stderr.write( f"warning: flow page visit {fpv.id}: no page yaml desc " "found for " f"'{flow_id}:{fpv.page_data.group_id}/{fpv.page_data.page_id}' " f"in '{course.identifier}'") return page = instantiate_flow_page( location="flow '%s', group, '%s', page '%s'" % (flow_id, fpv.page_data.group_id, fpv.page_data.page_id), repo=repo, page_desc=page_desc, commit_sha=commit_sha) from course.page.base import PageContext pctx = PageContext(course=course, repo=repo, commit_sha=commit_sha, flow_session=fpv.flow_session, page_uri=None) from course.page.upload import FileUploadQuestion from course.page.code import CodeQuestion if isinstance(page, FileUploadQuestion): content, mime_type = page.get_content_from_answer_data(fpv.answer) from django.core.files.base import ContentFile answer_data = page.file_to_answer_data(pctx, ContentFile(content), mime_type) fpv.answer = answer_data fpv.save() return True elif isinstance(page, CodeQuestion): code = page.get_code_from_answer_data(fpv.answer) answer_data = page.code_to_answer_data(pctx, code) fpv.answer = answer_data fpv.save() return True else: return False assert False
def make_page_answer_stats_list(pctx, flow_id, restrict_to_first_attempt): flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) is_multiple_submit = is_flow_multiple_submit(flow_desc) page_cache = PageInstanceCache(pctx.repo, pctx.course, flow_id) page_info_list = [] for group_desc in flow_desc.groups: for page_desc in group_desc.pages: points = 0 graded_count = 0 answer_count = 0 total_count = 0 visits = (FlowPageVisit.objects .filter( flow_session__course=pctx.course, flow_session__flow_id=flow_id, page_data__group_id=group_desc.id, page_data__page_id=page_desc.id, is_submitted_answer=True, )) if connection.features.can_distinct_on_fields: if restrict_to_first_attempt: visits = (visits .distinct("flow_session__participation__id") .order_by("flow_session__participation__id", "visit_time")) elif is_multiple_submit: visits = (visits .distinct("page_data__id") .order_by("page_data__id", "-visit_time")) visits = (visits .select_related("flow_session") .select_related("page_data")) answer_expected = False title = None for visit in visits: page = page_cache.get_page(group_desc.id, page_desc.id, pctx.course_commit_sha) answer_expected = answer_expected or page.expects_answer() from course.page import PageContext grading_page_context = PageContext( course=pctx.course, repo=pctx.repo, commit_sha=pctx.course_commit_sha, flow_session=visit.flow_session) title = page.title(grading_page_context, visit.page_data.data) answer_feedback = visit.get_most_recent_feedback() if visit.answer is not None: answer_count += 1 total_count += 1 if (answer_feedback is not None and answer_feedback.correctness is not None): if visit.answer is None: assert answer_feedback.correctness == 0 else: points += answer_feedback.correctness graded_count += 1 if not answer_expected: continue page_info_list.append( PageAnswerStats( group_id=group_desc.id, page_id=page_desc.id, title=title, average_correctness=safe_div(points, graded_count), average_emptiness=safe_div( graded_count - answer_count, graded_count), answer_count=answer_count, total_count=total_count, url=reverse( "relate-page_analytics", args=( pctx.course_identifier, flow_id, group_desc.id, page_desc.id, )))) return page_info_list
def view_single_grade(pctx, participation_id, opportunity_id): from course.views import get_now_or_fake_time now_datetime = get_now_or_fake_time(pctx.request) participation = get_object_or_404(Participation, id=int(participation_id)) if participation.course != pctx.course: raise SuspiciousOperation("participation does not match course") opportunity = get_object_or_404(GradingOpportunity, id=int(opportunity_id)) if pctx.role in [ participation_role.instructor, participation_role.teaching_assistant]: if not opportunity.shown_in_grade_book: messages.add_message(pctx.request, messages.INFO, "This grade is not shown in the grade book.") if not opportunity.shown_in_student_grade_book: messages.add_message(pctx.request, messages.INFO, "This grade is not shown in the student grade book.") elif pctx.role == participation_role.student: if participation != pctx.participation: raise PermissionDenied("may not view other people's grades") if not (opportunity.shown_in_grade_book and opportunity.shown_in_student_grade_book): raise PermissionDenied("grade has not been released") else: raise PermissionDenied() # {{{ modify sessions buttons if pctx.role in [ participation_role.instructor, participation_role.teaching_assistant]: allow_session_actions = True request = pctx.request if pctx.request.method == "POST": action_re = re.compile("^([a-z]+)_([0-9]+)$") for key in request.POST.keys(): action_match = action_re.match(key) if action_match: break if not action_match: raise SuspiciousOperation("unknown action") session = FlowSession.objects.get(id=int(action_match.group(2))) op = action_match.group(1) from course.flow import ( reopen_session, regrade_session, recalculate_session_grade, expire_flow_session_standalone, finish_flow_session_standalone) try: if op == "expire": expire_flow_session_standalone( pctx.repo, pctx.course, session, now_datetime) messages.add_message(pctx.request, messages.SUCCESS, "Session expired.") elif op == "end": finish_flow_session_standalone( pctx.repo, pctx.course, session, now_datetime=now_datetime) messages.add_message(pctx.request, messages.SUCCESS, "Session ended.") elif op == "reopen": reopen_session(session) messages.add_message(pctx.request, messages.SUCCESS, "Session reopened.") elif op == "regrade": regrade_session( pctx.repo, pctx.course, session) messages.add_message(pctx.request, messages.SUCCESS, "Session regraded.") elif op == "recalculate": recalculate_session_grade( pctx.repo, pctx.course, session) messages.add_message(pctx.request, messages.SUCCESS, "Session grade recalculated.") else: raise SuspiciousOperation("invalid session operation") except Exception as e: messages.add_message(pctx.request, messages.ERROR, "Error: %s %s" % (type(e).__name__, str(e))) else: allow_session_actions = False # }}} grade_changes = list(GradeChange.objects .filter( opportunity=opportunity, participation=participation) .order_by("grade_time") .select_related("participation") .select_related("participation__user") .select_related("creator") .select_related("opportunity")) state_machine = GradeStateMachine() state_machine.consume(grade_changes, set_is_superseded=True) flow_grade_aggregation_strategy_text = None if opportunity.flow_id: flow_sessions = list(FlowSession.objects .filter( participation=participation, flow_id=opportunity.flow_id, ) .order_by("start_time")) from collections import namedtuple SessionProperties = namedtuple("SessionProperties", ["due", "grade_description"]) from course.utils import get_session_grading_rule from course.content import get_flow_desc flow_desc = get_flow_desc(pctx.repo, pctx.course, opportunity.flow_id, pctx.course_commit_sha) flow_sessions_and_session_properties = [] for session in flow_sessions: grading_rule = get_session_grading_rule( session, pctx.role, flow_desc, now_datetime) session_properties = SessionProperties( due=grading_rule.due, grade_description=grading_rule.description) flow_sessions_and_session_properties.append( (session, session_properties)) else: flow_sessions_and_session_properties = None return render_course_page(pctx, "course/gradebook-single.html", { "opportunity": opportunity, "grade_participation": participation, "grade_state_change_types": grade_state_change_types, "grade_changes": grade_changes, "state_machine": state_machine, "flow_sessions_and_session_properties": flow_sessions_and_session_properties, "allow_session_actions": allow_session_actions, "show_privileged_info": pctx.role in [ participation_role.instructor, participation_role.teaching_assistant ], "flow_grade_aggregation_strategy": flow_grade_aggregation_strategy_text, })
def grant_exception_stage_3(pctx, participation_id, flow_id, session_id): # type: (CoursePageContext, int, Text, int) -> http.HttpResponse if not pctx.has_permission(pperm.grant_exception): raise PermissionDenied(_("may not grant exceptions")) participation = get_object_or_404(Participation, id=participation_id) from course.content import get_flow_desc try: flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) except ObjectDoesNotExist: raise http.Http404() session = FlowSession.objects.get(id=int(session_id)) now_datetime = get_now_or_fake_time(pctx.request) from course.utils import ( get_session_access_rule, get_session_grading_rule) access_rule = get_session_access_rule(session, flow_desc, now_datetime) grading_rule = get_session_grading_rule(session, flow_desc, now_datetime) request = pctx.request if request.method == "POST": form = ExceptionStage3Form( {}, flow_desc, session.access_rules_tag, request.POST) from course.constants import flow_rule_kind if form.is_valid(): permissions = [ key for key, _ in FLOW_PERMISSION_CHOICES if form.cleaned_data[key]] from course.validation import ( validate_session_access_rule, validate_session_grading_rule, ValidationContext) from relate.utils import dict_to_struct vctx = ValidationContext( repo=pctx.repo, commit_sha=pctx.course_commit_sha) flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) tags = None if hasattr(flow_desc, "rules"): tags = getattr(flow_desc.rules, "tags", None) # {{{ put together access rule if form.cleaned_data["create_access_exception"]: new_access_rule = {"permissions": permissions} if (form.cleaned_data.get("restrict_to_same_tag") and session.access_rules_tag is not None): new_access_rule["if_has_tag"] = session.access_rules_tag validate_session_access_rule( vctx, ugettext("newly created exception"), dict_to_struct(new_access_rule), tags) fre_access = FlowRuleException( flow_id=flow_id, participation=participation, expiration=form.cleaned_data["access_expires"], creator=pctx.request.user, comment=form.cleaned_data["comment"], kind=flow_rule_kind.access, rule=new_access_rule) fre_access.save() # }}} new_access_rules_tag = form.cleaned_data.get("set_access_rules_tag") if new_access_rules_tag == NONE_SESSION_TAG: new_access_rules_tag = None if session.access_rules_tag != new_access_rules_tag: session.access_rules_tag = new_access_rules_tag session.save() # {{{ put together grading rule if form.cleaned_data["create_grading_exception"]: due = form.cleaned_data["due"] if form.cleaned_data["due_same_as_access_expiration"]: due = form.cleaned_data["access_expires"] descr = ugettext("Granted excecption") if form.cleaned_data["credit_percent"] is not None: descr += string_concat(" (%.1f%% ", ugettext('credit'), ")") \ % form.cleaned_data["credit_percent"] due_local_naive = due if due_local_naive is not None: from relate.utils import as_local_time due_local_naive = ( as_local_time(due_local_naive) .replace(tzinfo=None)) new_grading_rule = { "description": descr, } if due_local_naive is not None: new_grading_rule["due"] = due_local_naive new_grading_rule["if_completed_before"] = due_local_naive for attr_name in ["credit_percent", "bonus_points", "max_points", "max_points_enforced_cap"]: if form.cleaned_data[attr_name] is not None: new_grading_rule[attr_name] = form.cleaned_data[attr_name] if (form.cleaned_data.get("restrict_to_same_tag") and session.access_rules_tag is not None): new_grading_rule["if_has_tag"] = session.access_rules_tag if hasattr(grading_rule, "generates_grade"): new_grading_rule["generates_grade"] = \ grading_rule.generates_grade validate_session_grading_rule( vctx, ugettext("newly created exception"), dict_to_struct(new_grading_rule), tags, grading_rule.grade_identifier) fre_grading = FlowRuleException( flow_id=flow_id, participation=participation, creator=pctx.request.user, comment=form.cleaned_data["comment"], kind=flow_rule_kind.grading, rule=new_grading_rule) fre_grading.save() # }}} messages.add_message(pctx.request, messages.SUCCESS, ugettext( "Exception granted to '%(participation)s' " "for '%(flow_id)s'.") % { 'participation': participation, 'flow_id': flow_id}) return redirect( "relate-grant_exception", pctx.course.identifier) else: data = { "restrict_to_same_tag": session.access_rules_tag is not None, "credit_percent": grading_rule.credit_percent, #"due_same_as_access_expiration": True, "due": grading_rule.due, } for perm in access_rule.permissions: data[perm] = True form = ExceptionStage3Form(data, flow_desc, session.access_rules_tag) return render_course_page(pctx, "course/generic-course-form.html", { "form": form, "form_description": ugettext("Grant Exception"), "form_text": string_concat( "<div class='well'>", ugettext("Granting exception to '%(participation)s' " "for '%(flow_id)s' (session %(session)s)."), "</div>") % { 'participation': participation, 'flow_id': flow_id, 'session': strify_session_for_exception(session)}, })
def grade_page_visit(visit, visit_grade_model=FlowPageVisitGrade, grade_data=None, graded_at_git_commit_sha=None): if not visit.is_submitted_answer: raise RuntimeError(_("cannot grade ungraded answer")) flow_session = visit.flow_session course = flow_session.course page_data = visit.page_data most_recent_grade = visit.get_most_recent_grade() if most_recent_grade is not None and grade_data is None: grade_data = most_recent_grade.grade_data from course.content import ( get_course_repo, get_course_commit_sha, get_flow_desc, get_flow_page_desc, instantiate_flow_page, ) repo = get_course_repo(course) course_commit_sha = get_course_commit_sha(course, flow_session.participation) flow_desc = get_flow_desc(repo, course, flow_session.flow_id, course_commit_sha) page_desc = get_flow_page_desc(flow_session.flow_id, flow_desc, page_data.group_id, page_data.page_id) page = instantiate_flow_page( location="flow '%s', group, '%s', page '%s'" % (flow_session.flow_id, page_data.group_id, page_data.page_id), repo=repo, page_desc=page_desc, commit_sha=course_commit_sha, ) assert page.expects_answer() if not page.is_answer_gradable(): return from course.page import PageContext grading_page_context = PageContext( course=course, repo=repo, commit_sha=course_commit_sha, flow_session=flow_session ) answer_feedback = page.grade(grading_page_context, visit.page_data.data, visit.answer, grade_data=grade_data) grade = visit_grade_model() grade.visit = visit grade.grade_data = grade_data grade.max_points = page.max_points(visit.page_data) grade.graded_at_git_commit_sha = graded_at_git_commit_sha bulk_feedback_json = None if answer_feedback is not None: grade.correctness = answer_feedback.correctness grade.feedback, bulk_feedback_json = answer_feedback.as_json() grade.save() update_bulk_feedback(page_data, grade, bulk_feedback_json)
def page_analytics(pctx, flow_id, group_id, page_id): if pctx.role not in [ participation_role.teaching_assistant, participation_role.instructor, participation_role.observer, ]: raise PermissionDenied(_("must be at least TA to view analytics")) flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) restrict_to_first_attempt = int( bool(pctx.request.GET.get("restrict_to_first_attempt") == "1")) is_multiple_submit = is_flow_multiple_submit(flow_desc) page_cache = PageInstanceCache(pctx.repo, pctx.course, flow_id) visits = (FlowPageVisit.objects .filter( flow_session__course=pctx.course, flow_session__flow_id=flow_id, page_data__group_id=group_id, page_data__page_id=page_id, is_submitted_answer=True, )) if connection.features.can_distinct_on_fields: if restrict_to_first_attempt: visits = (visits .distinct("flow_session__participation__id") .order_by("flow_session__participation__id", "visit_time")) elif is_multiple_submit: visits = (visits .distinct("page_data__id") .order_by("page_data__id", "-visit_time")) visits = (visits .select_related("flow_session") .select_related("page_data")) normalized_answer_and_correctness_to_count = {} title = None body = None total_count = 0 graded_count = 0 for visit in visits: page = page_cache.get_page(group_id, page_id, pctx.course_commit_sha) from course.page import PageContext grading_page_context = PageContext( course=pctx.course, repo=pctx.repo, commit_sha=pctx.course_commit_sha, flow_session=visit.flow_session) title = page.title(grading_page_context, visit.page_data.data) body = page.body(grading_page_context, visit.page_data.data) normalized_answer = page.normalized_answer( grading_page_context, visit.page_data.data, visit.answer) answer_feedback = visit.get_most_recent_feedback() if answer_feedback is not None: key = (normalized_answer, answer_feedback.correctness) normalized_answer_and_correctness_to_count[key] = \ normalized_answer_and_correctness_to_count.get(key, 0) + 1 graded_count += 1 else: key = (normalized_answer, None) normalized_answer_and_correctness_to_count[key] = \ normalized_answer_and_correctness_to_count.get(key, 0) + 1 total_count += 1 answer_stats = [] for (normalized_answer, correctness), count in \ six.iteritems(normalized_answer_and_correctness_to_count): answer_stats.append( AnswerStats( normalized_answer=normalized_answer, correctness=correctness, count=count, percentage=safe_div(100 * count, total_count))) answer_stats = sorted( answer_stats, key=lambda astats: astats.percentage, reverse=True) return render_course_page(pctx, "course/analytics-page.html", { "flow_identifier": flow_id, "group_id": group_id, "page_id": page_id, "title": title, "body": body, "answer_stats_list": answer_stats, "restrict_to_first_attempt": restrict_to_first_attempt, })
def grant_exception_stage_2(pctx, participation_id, flow_id): # type: (CoursePageContext, Text, Text) -> http.HttpResponse if not pctx.has_permission(pperm.grant_exception): raise PermissionDenied(_("may not grant exceptions")) # {{{ get flow data participation = get_object_or_404(Participation, id=participation_id) form_text = ( string_concat( "<div class='well'>", ugettext("Granting exception to '%(participation)s' for " "'%(flow_id)s'."), "</div>") % { 'participation': participation, 'flow_id': flow_id}) from course.content import get_flow_desc try: flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) except ObjectDoesNotExist: raise http.Http404() now_datetime = get_now_or_fake_time(pctx.request) if hasattr(flow_desc, "rules"): access_rules_tags = getattr(flow_desc.rules, "tags", []) else: access_rules_tags = [] NONE_SESSION_TAG = string_concat("<<<", _("NONE"), ">>>") # noqa session_tag_choices = [ (tag, tag) for tag in access_rules_tags] + [(NONE_SESSION_TAG, string_concat("(", _("NONE"), ")"))] from course.utils import get_session_start_rule session_start_rule = get_session_start_rule(pctx.course, participation, flow_id, flow_desc, now_datetime) create_session_is_override = False if not session_start_rule.may_start_new_session: create_session_is_override = True form_text += ("<div class='alert alert-info'>%s</div>" % ( string_concat( "<i class='fa fa-info-circle'></i> ", _("Creating a new session is (technically) not allowed " "by course rules. Clicking 'Create Session' anyway will " "override this rule.")))) default_tag = session_start_rule.tag_session if default_tag is None: default_tag = NONE_SESSION_TAG # }}} def find_sessions(): # type: () -> List[FlowSession] return list(FlowSession.objects .filter( participation=participation, flow_id=flow_id) .order_by("start_time")) exception_form = None request = pctx.request if request.method == "POST": exception_form = ExceptionStage2Form(find_sessions(), request.POST) create_session_form = CreateSessionForm( session_tag_choices, default_tag, create_session_is_override, request.POST) if "create_session" in request.POST or "next" in request.POST: pass else: raise SuspiciousOperation(_("invalid command")) if create_session_form.is_valid() and "create_session" in request.POST: from course.flow import start_flow access_rules_tag = ( create_session_form.cleaned_data[ "access_rules_tag_for_new_session"]) if access_rules_tag == NONE_SESSION_TAG: access_rules_tag = None start_flow(pctx.repo, pctx.course, participation, user=participation.user, flow_id=flow_id, flow_desc=flow_desc, session_start_rule=session_start_rule, now_datetime=now_datetime) exception_form = None elif exception_form.is_valid() and "next" in request.POST: # type: ignore return redirect( "relate-grant_exception_stage_3", pctx.course.identifier, participation.id, flow_id, exception_form.cleaned_data["session"]) # type: ignore else: create_session_form = CreateSessionForm( session_tag_choices, default_tag, create_session_is_override) if exception_form is None: exception_form = ExceptionStage2Form(find_sessions()) return render_course_page(pctx, "course/generic-course-form.html", { "forms": [exception_form, create_session_form], "form_text": form_text, "form_description": _("Grant Exception"), })