Example #1
0
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")
    })
Example #2
0
    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)
Example #3
0
    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)
Example #4
0
 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
Example #5
0
 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
Example #6
0
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")
        })
Example #7
0
    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))
Example #8
0
    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)
Example #9
0
    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))
Example #10
0
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",
    })
Example #11
0
    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()
Example #12
0
    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))
Example #13
0
    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()
Example #14
0
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)
Example #15
0
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,
        })
Example #16
0
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
Example #17
0
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,
        })
Example #18
0
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
            ],
        })
Example #19
0
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),
    })
Example #20
0
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
Example #21
0
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),
    })
Example #22
0
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()
Example #23
0
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,
        })
Example #24
0
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")
        })
Example #25
0
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)
            },
        })
Example #26
0
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"),
        })
Example #27
0
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")
        })
Example #28
0
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",
    })
Example #29
0
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
Example #30
0
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
Example #31
0
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,
        })
Example #32
0
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)},
    })
Example #33
0
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)
Example #34
0
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,
        })
Example #35
0
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"),
    })