Exemple #1
0
    def __init__(self, vctx, location, page_desc):
        super(FileUploadQuestion, self).__init__(vctx, location, page_desc)

        if not (set(page_desc.mime_types) <= set(self.ALLOWED_MIME_TYPES)):
            raise ValidationError(
                string_concat(
                    location, ": ",
                    _("unrecognized mime types"),
                    " '%(presenttype)s'")
                % {
                    'presenttype': ", ".join(
                        set(page_desc.mime_types)
                        - set(self.ALLOWED_MIME_TYPES))})

        if page_desc.maximum_megabytes <= 0:
            raise ValidationError(
                string_concat(
                    location, ": ",
                    _("'maximum_megabytes' expects a positive value, "
                      "got %(value)s instead")
                    % {'value': str(page_desc.maximum_megabytes)}))

        if vctx is not None:
            if not hasattr(page_desc, "value"):
                vctx.add_warning(location, _("upload question does not have "
                        "assigned point value"))
Exemple #2
0
    def __init__(self, vctx, location, page_desc):
        super(ChoiceQuestion, self).__init__(vctx, location, page_desc)

        if self.correct_choice_count < 1:
            raise ValidationError(
                    string_concat(
                        "%(location)s: ",
                        _("one or more correct answer(s) "
                        "expected, %(n_correct)d found"))
                    % {
                        'location': location,
                        'n_correct': self.correct_choice_count})

        if self.disregard_choice_count:
            raise ValidationError(
                    string_concat(
                        "%(location)s: ",
                        _("ChoiceQuestion does not allow any choices "
                        "marked 'disregard'"))
                    % {'location': location})

        if self.always_correct_choice_count:
            raise ValidationError(
                    string_concat(
                        "%(location)s: ",
                        _("ChoiceQuestion does not allow any choices "
                        "marked 'always_correct'"))
                    % {'location': location})
Exemple #3
0
def get_auto_feedback(correctness):
    # type: (Optional[float]) -> Text

    correctness = validate_point_count(correctness)

    if correctness is None:
        return six.text_type(
            ugettext_noop("No information on correctness of answer."))

    if correctness == 0:
        return six.text_type(ugettext_noop("Your answer is not correct."))
    elif correctness == 1:
        return six.text_type(ugettext_noop("Your answer is correct."))
    elif correctness > 1:
        return six.text_type(
                string_concat(
                    ugettext_noop(
                        "Your answer is correct and earned bonus points."),
                    " (%.1f %%)")
                % (100*correctness))
    elif correctness > 0.5:
        return six.text_type(
                string_concat(
                    ugettext_noop("Your answer is mostly correct."),
                    " (%.1f %%)")
                % (100*correctness))
    else:
        return six.text_type(
                string_concat(
                    ugettext_noop("Your answer is somewhat correct. "),
                    "(%.1f%%)")
                % (100*correctness))
Exemple #4
0
    def __init__(self, vctx, location, page_desc):
        super(TextQuestion, self).__init__(vctx, location, page_desc)

        if len(page_desc.answers) == 0:
            raise ValidationError(
                    string_concat(
                        "%s: ",
                        _("at least one answer must be provided"))
                    % location)

        self.matchers = [
                parse_matcher(
                    vctx,
                    "%s, answer %d" % (location, i+1),
                    answer)
                for i, answer in enumerate(page_desc.answers)]

        if not any(matcher.correct_answer_text() is not None
                for matcher in self.matchers):
            raise ValidationError(
                    string_concat(
                        "%s: ",
                        _("no matcher is able to provide a plain-text "
                        "correct answer"))
                    % location)
Exemple #5
0
def get_matcher_class(location, matcher_type, pattern_type):
    for matcher_class in TEXT_ANSWER_MATCHER_CLASSES:
        if matcher_class.type == matcher_type:

            if matcher_class.pattern_type != pattern_type:
                raise ValidationError(
                    string_concat(
                        "%(location)s: ",
                        # Translators: a "matcher" is used to determine
                        # if the answer to text question (blank filling
                        # question) is correct.
                        _("%(matcherclassname)s only accepts "
                            "'%(matchertype)s' patterns"))
                        % {
                            'location': location,
                            'matcherclassname': matcher_class.__name__,
                            'matchertype': matcher_class.pattern_type})

            return matcher_class

    raise ValidationError(
            string_concat(
                "%(location)s: ",
                _("unknown match type '%(matchertype)s'"))
            % {
                'location': location,
                'matchertype': matcher_type})
Exemple #6
0
    def __init__(self, vctx, location, page_desc):
        super(MultipleChoiceQuestion, self).__init__(vctx, location, page_desc)

        pd = self.page_desc

        if hasattr(pd, "credit_mode"):
            credit_mode = pd.credit_mode

            if (
                    hasattr(pd, "allow_partial_credit")
                    or
                    hasattr(pd, "allow_partial_credit_subset_only")):
                raise ValidationError(
                        string_concat(
                            "%(location)s: ",
                            _("'allow_partial_credit' or "
                            "'allow_partial_credit_subset_only' may not be specified"
                            "at the same time as 'credit_mode'"))
                        % {'location': location})

        else:

            partial = getattr(pd, "allow_partial_credit", False)
            partial_subset = getattr(pd, "allow_partial_credit_subset_only", False)

            if not partial and not partial_subset:
                credit_mode = "exact"
            elif partial and not partial_subset:
                credit_mode = "proportional"
            elif not partial and partial_subset:
                credit_mode = "proportional_correct"
            else:
                assert partial and partial_subset
                raise ValidationError(
                        string_concat(
                            "%(location)s: ",
                            _("'allow_partial_credit' and "
                            "'allow_partial_credit_subset_only' are not allowed to "
                            "coexist when both attribute are 'True'"))
                        % {'location': location})

        if credit_mode not in [
                "exact",
                "proportional",
                "proportional_correct"]:
            raise ValidationError(
                    string_concat(
                        "%(location)s: ",
                        _("unrecognized credit_mode '%(credit_mode)s'"))
                    % {'location': location, "credit_mode": credit_mode})

        if vctx is not None and not hasattr(pd, "credit_mode"):
            vctx.add_warning(location,
                    _("'credit_mode' will be required on multi-select choice "
                        "questions in a future version. set "
                        "'credit_mode: {}' to match current behavior.")
                    .format(credit_mode))

        self.credit_mode = credit_mode
Exemple #7
0
    def __init__(self, vctx, location, page_desc):
        super(PythonCodeQuestionWithHumanTextFeedback, self).__init__(
                vctx, location, page_desc)

        if vctx is not None:
            if (
                    hasattr(self.page_desc, "human_feedback_value")
                    and hasattr(self.page_desc, "human_feedback_percentage")):
                raise ValidationError(
                    string_concat(
                        "%(location)s: ",
                        _("'human_feedback_value' and "
                          "'human_feedback_percentage' are not "
                          "allowed to coexist"))
                    % {'location': location}
                )
            if not (hasattr(self.page_desc, "human_feedback_value")
                    or hasattr(self.page_desc, "human_feedback_percentage")):
                raise ValidationError(
                    string_concat(
                        "%(location)s: ",
                        _("expecting either 'human_feedback_value' "
                          "or 'human_feedback_percentage', found neither."))
                    % {'location': location}
                )
            if hasattr(self.page_desc, "human_feedback_value"):
                vctx.add_warning(
                    location,
                    _("Used deprecated 'human_feedback_value' attribute--"
                      "use 'human_feedback_percentage' instead."))
                if self.page_desc.value == 0:
                    raise ValidationError("".join([
                        "%s: ",
                        _("'human_feedback_value' attribute is not allowed "
                          "if value of question is 0, use "
                          "'human_feedback_percentage' instead")])
                        % location)
                if self.page_desc.human_feedback_value > self.page_desc.value:
                    raise ValidationError("".join([
                        "%s: ",
                        _("human_feedback_value greater than overall "
                            "value of question")])
                        % location)
            if hasattr(self.page_desc, "human_feedback_percentage"):
                if not (
                        0 <= self.page_desc.human_feedback_percentage <= 100):
                    raise ValidationError("".join([
                        "%s: ",
                        _("the value of human_feedback_percentage "
                          "must be between 0 and 100")])
                        % location)

        if hasattr(self.page_desc, "human_feedback_value"):
            self.human_feedback_percentage = (
                self.page_desc.human_feedback_value * 100 / self.page_desc.value)
        else:
            self.human_feedback_percentage = (
                self.page_desc.human_feedback_percentage)
Exemple #8
0
    def __init__(self, vctx, location, name, answers_desc):
        super(ChoicesAnswer, self).__init__(
            vctx, location, name, answers_desc)

        validate_struct(
            vctx,
            location,
            answers_desc,
            required_attrs=(
                ("type", str),
                ("choices", list)
                ),
            allowed_attrs=(
                ("weight", (int, float)),
                ("hint", str),
                ("hint_title", str),
                ("required", bool),
                ),
            )

        self.weight = getattr(answers_desc, "weight", 0)

        correct_choice_count = 0
        for choice_idx, choice in enumerate(answers_desc.choices):
            try:
                choice = str(choice)
            except Exception:
                raise ValidationError(
                        string_concat(
                            "%(location)s: '%(answer_name)s' ",
                            _("choice %(idx)d: unable to convert to string")
                            )
                        % {'location': location,
                            'answer_name': self.name,
                            'idx': choice_idx+1})

            if choice.startswith(self.CORRECT_TAG):
                correct_choice_count += 1

            if vctx is not None:
                validate_markup(vctx, location,
                        remove_prefix(self.CORRECT_TAG, choice))

        if correct_choice_count < 1:
            raise ValidationError(
                    string_concat(
                        "%(location)s: ",
                        _("one or more correct answer(s) expected "
                        " for question '%(question_name)s', "
                        "%(n_correct)d found"))
                    % {
                        'location': location,
                        'question_name': self.name,
                        'n_correct': correct_choice_count})

        self.hint = getattr(self.answers_desc, "hint", "")
        self.width = 0
Exemple #9
0
def csv_data_importable(file_contents, column_idx_list, header_count):
    import csv
    spamreader = csv.reader(file_contents)
    n_header_row = 0
    try:
        if six.PY2:
            row0 = spamreader.next()
        else:
            row0 = spamreader.__next__()
    except Exception as e:
        err_msg = type(e).__name__
        err_str = str(e)
        if err_msg == "Error":
            err_msg = ""
        else:
            err_msg += ": "
        err_msg += err_str

        if "line contains NULL byte" in err_str:
            err_msg = err_msg.rstrip(".") + ". "
            err_msg += _("Are you sure the file is a CSV file other "
                         "than a Microsoft Excel file?")

        return False, (
            string_concat(
                pgettext_lazy("Starting of Error message", "Error"),
                ": %s" % err_msg))

    from itertools import chain

    for row in chain([row0], spamreader):
        n_header_row += 1
        if n_header_row <= header_count:
            continue
        try:
            for column_idx in column_idx_list:
                if column_idx is not None:
                    six.text_type(get_col_contents_or_empty(row, column_idx-1))
        except UnicodeDecodeError:
            return False, (
                    _("Error: Columns to be imported contain "
                        "non-ASCII characters. "
                        "Please save your CSV file as utf-8 encoded "
                        "and import again.")
            )
        except Exception as e:
            return False, (
                    string_concat(
                        pgettext_lazy("Starting of Error message",
                            "Error"),
                        ": %(err_type)s: %(err_str)s")
                    % {
                        "err_type": type(e).__name__,
                        "err_str": str(e)}
                    )

    return True, ""
Exemple #10
0
def parse_matcher_string(vctx, location, matcher_desc):
    match = MATCHER_RE.match(matcher_desc)

    if match is not None:
        matcher_type = match.group(1)
        pattern = match.group(2)
    else:
        match = MATCHER_RE_2.match(matcher_desc)

        if match is None:
            raise ValidationError(
                    string_concat(
                        "%s: ",
                        _("does not specify match type"))
                    % location)

        matcher_type = match.group(1)
        pattern = match.group(2)

        if vctx is not None:
            vctx.add_warning(location,
                    _("uses deprecated 'matcher:answer' style"))

    return (get_matcher_class(location, matcher_type, "string")
            (vctx, location, pattern))
Exemple #11
0
def make_time_histogram(pctx, flow_id):
    qset = FlowSession.objects.filter(
            course=pctx.course,
            flow_id=flow_id)

    from relate.utils import string_concat
    hist = Histogram(
            num_log_bins=True,
            num_bin_title_formatter=(
                lambda minutes: string_concat(
                    "$>$ %.1f ",
                    pgettext("Minute (time unit)", "min"))
                % minutes))
    for session in qset:
        if session.in_progress:
            hist.add_data_point(
                    "".join(["<",
                        pgettext("Status of session", "in progress"),
                        ">"]))
        else:
            delta = session.completion_time - session.start_time
            minutes = delta.total_seconds() / 60
            hist.add_data_point(minutes)

    return hist
Exemple #12
0
    def __init__(self, vctx, location, page_desc):
        super(PythonCodeQuestion, self).__init__(vctx, location, page_desc)

        if vctx is not None and hasattr(page_desc, "data_files"):
            for data_file in page_desc.data_files:
                try:
                    if not isinstance(data_file, str):
                        raise ObjectDoesNotExist()

                    from course.content import get_repo_blob
                    get_repo_blob(vctx.repo, data_file, vctx.commit_sha)
                except ObjectDoesNotExist:
                    raise ValidationError(
                        string_concat(
                            "%(location)s: ",
                            _("data file '%(file)s' not found"))
                        % {"location": location, "file": data_file})

        if not getattr(page_desc, "single_submission", False) and vctx is not None:
            is_multi_submit = False

            if hasattr(page_desc, "access_rules"):
                if hasattr(page_desc.access_rules, "add_permissions"):
                    if (flow_permission.change_answer
                            in page_desc.access_rules.add_permissions):
                        is_multi_submit = True

            if not is_multi_submit:
                vctx.add_warning(location, _("code question does not explicitly "
                    "allow multiple submission. Either add "
                    "access_rules/add_permssions/change_answer "
                    "or add 'single_submission: True' to confirm that you intend "
                    "for only a single submission to be allowed. "
                    "While you're at it, consider adding "
                    "access_rules/add_permssions/see_correctness."))
Exemple #13
0
    def __init__(self, vctx, location, page_desc):
        super(PageBaseWithTitle, self).__init__(vctx, location, page_desc)

        title = None
        try:
            title = self.page_desc.title
        except AttributeError:
            pass

        if title is None:
            try:
                md_body = self.markup_body_for_title()
            except NotImplementedError:
                from warnings import warn
                warn(_("PageBaseWithTitle subclass '%s' does not implement "
                        "markdown_body_for_title()")
                        % type(self).__name__)
            else:
                from course.content import extract_title_from_markup
                title = extract_title_from_markup(md_body)

        if title is None:
            raise ValidationError(
                    string_concat(
                        "%s: ",
                        _("no title found in body or title attribute"))
                    % (location))

        self._title = title
Exemple #14
0
    def __init__(self, vctx, location, pattern):
        self.pattern = pattern

        try:
            self.pattern_sym = parse_sympy(pattern)
        except ImportError:
            tp, e, _ = sys.exc_info()
            if vctx is not None:
                vctx.add_warning(
                        location,
                        string_concat(
                            "%(location)s: ",
                            _("unable to check symbolic expression"),
                            "(%(err_type)s: %(err_str)s)")
                        % {
                            'location': location,
                            "err_type": tp.__name__,
                            "err_str": str(e)
                            })

        except Exception:
            tp, e, _ = sys.exc_info()
            raise ValidationError(
                    "%(location)s: %(err_type)s: %(err_str)s"
                    % {
                        "location": location,
                        "err_type": tp.__name__,
                        "err_str": str(e)
                        })
Exemple #15
0
    def __init__(self, vctx, location, page_desc):
        super(PageBaseWithValue, self).__init__(vctx, location, page_desc)

        if vctx is not None:
            if hasattr(page_desc, "value") and self.is_optional_page:
                raise ValidationError(
                    string_concat(
                        location,
                        _("Attribute 'value' should be removed when "
                          "'is_optional_page' is True.")))

            if hasattr(page_desc, "value") and page_desc.value < 0:
                raise ValidationError(
                    string_concat(
                        location,
                        _("Attribute 'value' expects a non-negative value, "
                          "got %s instead") % str(page_desc.value)))
Exemple #16
0
    def get_length_attr_em(location, width_attr):
        # type: (Text, Text) -> Optional[float]
        """
        generate the length for input box, the unit is 'em'
        """

        if width_attr is None:
            return None

        if isinstance(width_attr, (int, float)):
            return width_attr

        width_re_match = WIDTH_STR_RE.match(width_attr)
        if width_re_match:
            length_value = width_re_match.group(1)
            length_unit = width_re_match.group(2)
        else:
            raise ValidationError(
                    string_concat(
                        "%(location)s: ",
                        _("unrecogonized width attribute string: "
                        "'%(width_attr)s'"))
                    % {
                        "location": location,
                        "width_attr": width_attr
                        })

        if length_unit not in ALLOWED_LENGTH_UNIT:
            raise ValidationError(
                    string_concat(
                        "%(location)s: ",
                        _("unsupported length unit '%(length_unit)s', "
                          "expected length unit can be "
                          "%(allowed_length_unit)s", ))
                        % {
                            "location": location,
                            "length_unit": length_unit,
                            "allowed_length_unit": ", ".join(
                                ["'" + item + "'"
                                    for item in ALLOWED_LENGTH_UNIT])
                                })

        if length_unit == "%":
            return float(length_value)*DEFAULT_WIDTH/100.0
        else:
            return float(length_value)/cast(float, EM_LEN_DICT[length_unit])
Exemple #17
0
def parse_validator(vctx, location, validator_desc):
    if not isinstance(validator_desc, Struct):
        raise ValidationError(
                string_concat(
                    "%s: ",
                    _("must be struct or string"))
                % location)

    if not hasattr(validator_desc, "type"):
        raise ValidationError(
                string_concat(
                    "%s: ",
                    "matcher must supply 'type'")
                % location)

    return (get_validator_class(location, validator_desc.type)
        (vctx, location, validator_desc))
Exemple #18
0
    def correct_answer(self, page_context, page_data, answer_data, grade_data):
        corr_idx_list = self.unpermuted_correct_indices()
        always_correct_idx_list = self.unpermuted_always_correct_indices()

        result = (string_concat(_("The correct answer is"), ": %s")
                    % self.get_answer_html(page_context, corr_idx_list))

        if len(always_correct_idx_list) > 0:
            result = (string_concat(result,
                        string_concat(_("Additional acceptable options are"),
                            ": %s")
                        % self.get_answer_html(page_context,
                            always_correct_idx_list)))

        if hasattr(self.page_desc, "answer_explanation"):
            result += markup_to_html(page_context, self.page_desc.answer_explanation)

        return result
Exemple #19
0
    def __init__(self, vctx, location, page_desc):
        super(PageBaseWithValue, self).__init__(vctx, location, page_desc)

        if vctx is not None:
            if hasattr(page_desc, "value") and self.is_optional_page:
                raise ValidationError(
                    string_concat(
                        location,
                        _("Attribute 'value' should be removed when "
                          "'is_optional_page' is True.")))
Exemple #20
0
 def get_page_id(self, obj):
     if obj.page_data.page_ordinal is None:
         return string_concat("%s/%s (", _("not in use"), ")") % (
                 obj.page_data.group_id,
                 obj.page_data.page_id)
     else:
         return "%s/%s (%s)" % (
                 obj.page_data.group_id,
                 obj.page_data.page_id,
                 obj.page_data.page_ordinal)
Exemple #21
0
def parse_question(vctx, location, name, answers_desc):
    if isinstance(answers_desc, Struct):
        return (get_question_class(location, answers_desc.type, answers_desc)
            (vctx, location, name, answers_desc))
    else:
        raise ValidationError(
                string_concat(
                    "%s: ",
                    _("Embedded question '%s' must be a struct" % name))
                % location)
Exemple #22
0
 def get_default_option():
     # type: () -> Tuple[Text, Text]
     # For the default language used, if USE_I18N is True, display
     # "Disabled". Otherwise display its lang info.
     if not settings.USE_I18N:
         formatted_descr = (
             get_formatted_options(settings.LANGUAGE_CODE, None)[1])
     else:
         formatted_descr = _("disabled (i.e., displayed language is "
                             "determined by user's browser preference)")
     return "", string_concat("%s: " % _("Default"), formatted_descr)
Exemple #23
0
def get_validator_class(location, validator_type):
    for validator_class in TEXT_ANSWER_VALIDATOR_CLASSES:
        if validator_class.type == validator_type:
            return validator_class

    raise ValidationError(
            string_concat(
                "%(location)s: ",
                _("unknown validator type"),
                "'%(type)s'")
            % {'location': location, 'type': validator_type})
Exemple #24
0
def parse_matcher(vctx, location, matcher_desc):
    if isinstance(matcher_desc, six.string_types):
        return parse_matcher_string(vctx, location, matcher_desc)
    else:
        if not isinstance(matcher_desc, Struct):
            raise ValidationError(
                    string_concat(
                        "%s: ",
                        _("must be struct or string"))
                    % location)

        if not hasattr(matcher_desc, "type"):
            raise ValidationError(
                    string_concat(
                        "%s: ",
                        _("matcher must supply 'type'"))
                    % location)

        return (get_matcher_class(location, matcher_desc.type, "struct")
            (vctx, location, matcher_desc))
Exemple #25
0
    def correct_answer(self, page_context, page_data, answer_data, grade_data):
        corr_idx = self.unpermuted_correct_indices()[0]
        result = (string_concat(_("A correct answer is"), ": '%s'.")
                % self.process_choice_string(
                    page_context,
                    self.choices[corr_idx].text))

        if hasattr(self.page_desc, "answer_explanation"):
            result += markup_to_html(page_context, self.page_desc.answer_explanation)

        return result
Exemple #26
0
    def __init__(self, vctx, location, page_desc):
        super(TextQuestion, self).__init__(vctx, location, page_desc)

        if len(page_desc.answers) == 0:
            raise ValidationError(
                string_concat("%s: ",
                              _("at least one answer must be provided")) %
                location)

        self.matchers = [
            parse_matcher(vctx, "%s, answer %d" % (location, i + 1), answer)
            for i, answer in enumerate(page_desc.answers)
        ]

        if not any(matcher.correct_answer_text() is not None
                   for matcher in self.matchers):
            raise ValidationError(
                string_concat(
                    "%s: ",
                    _("no matcher is able to provide a plain-text "
                      "correct answer")) % location)
Exemple #27
0
def get_question_class(location, q_type, answers_desc):
    for question_class in ALLOWED_EMBEDDED_QUESTION_CLASSES:
        if question_class.type == q_type:
            return question_class
    else:
        raise ValidationError(
            string_concat(
                "%(location)s: ",
                _("unknown embedded question type '%(type)s'"))
            % {
                'location': location,
                'type': q_type})
Exemple #28
0
    def get_formatted_options(lang_code, lang_descr):
        # type: (Text, Optional[Text]) -> Tuple[Text, Text]
        if lang_descr is None:
            lang_descr = OrderedDict(settings.LANGUAGES).get(lang_code)
            if lang_descr is None:
                try:
                    lang_info = translation.get_language_info(lang_code)
                    lang_descr = lang_info["name_translated"]
                except KeyError:
                    return (lang_code.strip(), lang_code)

        return (lang_code.strip(),
                string_concat(_(lang_descr), " (%s)" % lang_code))
Exemple #29
0
    def get_user(self, obj):
        from django.urls import reverse
        from django.conf import settings

        return string_concat(
            "<a href='%(link)s'>", "%(user_fullname)s", "</a>") % {
                "link":
                reverse("admin:%s_change" %
                        settings.AUTH_USER_MODEL.replace(".", "_").lower(),
                        args=(obj.user.id, )),
                "user_fullname":
                obj.user.get_full_name(force_verbose_blank=True),
            }
Exemple #30
0
    def get_formatted_options(lang_code, lang_descr):
        # type: (Text, Optional[Text]) -> Tuple[Text, Text]
        if lang_descr is None:
            lang_descr = OrderedDict(settings.LANGUAGES).get(lang_code)
            if lang_descr is None:
                try:
                    lang_info = translation.get_language_info(lang_code)
                    lang_descr = lang_info["name_translated"]
                except KeyError:
                    return (lang_code.strip(), lang_code)

        return (lang_code.strip(),
                string_concat(_(lang_descr), " (%s)" % lang_code))
Exemple #31
0
class ParticipationQueryForm(StyledForm):
    queries = forms.CharField(
        required=True,
        widget=forms.Textarea,
        help_text=string_concat(
            _("Enter queries, one per line."), " ", _("Allowed"), ": ",
            "<code>and</code>, "
            "<code>or</code>, "
            "<code>not</code>, "
            "<code>id:1234</code>, "
            "<code>email:[email protected]</code>, "
            "<code>email-contains:abc</code>, "
            "<code>username:abc</code>, "
            "<code>username-contains:abc</code>, "
            "<code>institutional-id:2015abcd</code>, "
            "<code>institutional-id-contains:2015</code>, "
            "<code>tagged:abc</code>, "
            "<code>role:instructor|teaching_assistant|"
            "student|observer|auditor</code>, "
            "<code>status:requested|active|dropped|denied</code>|"
            "<code>has-started:flow_id</code>|"
            "<code>has-submitted:flow_id</code>."),
        label=_("Queries"))
    op = forms.ChoiceField(choices=(
        ("apply_tag", _("Apply tag")),
        ("remove_tag", _("Remove tag")),
        ("drop", _("Drop")),
    ),
                           label=_("Operation"),
                           required=True)
    tag = forms.CharField(label=_("Tag"),
                          help_text=_("Tag to apply or remove"),
                          required=False)

    def __init__(self, *args, **kwargs):
        super(ParticipationQueryForm, self).__init__(*args, **kwargs)

        self.helper.add_input(Submit("list", _("List")))
        self.helper.add_input(Submit("apply", _("Apply operation")))

    def clean_tag(self):
        tag = self.cleaned_data.get("tag")

        if tag:
            import re
            name_valid_re = re.compile(NAME_VALID_REGEX)

            if name_valid_re.match(tag) is None:
                self.add_error("tag", _("Name contains invalid characters."))
        return tag
Exemple #32
0
    def __init__(self, vctx, location, page_desc):
        super(TextQuestionBase, self).__init__(vctx, location, page_desc)

        widget = TextAnswerForm.get_text_widget(getattr(
            page_desc, "widget", None),
                                                check_only=True)

        if widget is None:
            raise ValidationError(
                string_concat("%(location)s: ", _("unrecognized widget type"),
                              "'%(type)s'") % {
                                  'location': location,
                                  'type': getattr(page_desc, "widget")
                              })
Exemple #33
0
def get_auto_feedback(correctness):
    # type: (Optional[float]) -> Text

    correctness = validate_point_count(correctness)

    if correctness is None:
        return six.text_type(_("No information on correctness of answer."))

    if correctness == 0:
        return six.text_type(_("Your answer is not correct."))
    elif correctness == 1:
        return six.text_type(_("Your answer is correct."))
    elif correctness > 1:
        return six.text_type(
            string_concat(_("Your answer is correct and earned bonus points."),
                          " (%.1f %%)") % (100 * correctness))
    elif correctness > 0.5:
        return six.text_type(
            string_concat(_("Your answer is mostly correct."), " (%.1f %%)") %
            (100 * correctness))
    else:
        return six.text_type(
            string_concat(_("Your answer is somewhat correct. "), "(%.1f%%)") %
            (100 * correctness))
Exemple #34
0
    def __init__(self, vctx, location, pattern):
        try:
            self.pattern = re.compile(pattern, self.re_flags)
        except Exception:
            tp, e, __ = sys.exc_info()

            raise ValidationError(
                string_concat("%(location)s: ",
                              _("regex '%(pattern)s' did not compile"),
                              ": %(err_type)s: %(err_str)s") % {
                                  "location": location,
                                  "pattern": pattern,
                                  "err_type": tp.__name__,
                                  "err_str": str(e)
                              })
Exemple #35
0
    def grade(
            self,
            page_context,  # type: PageContext
            page_data,  # type: Any
            answer_data,  # type: Any
            grade_data,  # type: Any
            ):
        # type: (...) -> Optional[AnswerFeedback]
        """This method is appropriate if the grade consists *only* of the
        feedback provided by humans. If more complicated/combined feedback
        is desired, a subclass would likely override this.
        """

        if answer_data is None:
            return AnswerFeedback(correctness=0,
                    feedback=ugettext_noop("No answer provided."))

        if grade_data is None:
            return None

        if not grade_data["released"]:
            return None

        if (grade_data["grade_percent"] is not None
                or grade_data["feedback_text"]):
            if grade_data["grade_percent"] is not None:
                correctness = grade_data["grade_percent"]/100
                feedback_text = "<p>%s</p>" % get_auto_feedback(correctness)

            else:
                correctness = None
                feedback_text = ""

            if grade_data["feedback_text"]:
                feedback_text += (
                        string_concat(
                            "<p>",
                            _("The following feedback was provided"),
                            ":<p>")
                        + markup_to_html(
                            page_context, grade_data["feedback_text"],
                            use_jinja=False))

            return AnswerFeedback(
                    correctness=correctness,
                    feedback=feedback_text)
        else:
            return None
Exemple #36
0
    def __init__(self, vctx, location, page_desc):
        super(SurveyChoiceQuestion, self).__init__(vctx, location, page_desc)

        for choice_idx, choice in enumerate(page_desc.choices):
            try:
                choice = str(choice)
            except Exception:
                raise ValidationError(
                    string_concat(
                        "%(location)s, ",
                        _("choice %(idx)d: unable to convert to string")
                        )
                    % {"location": location, "idx": choice_idx+1})

            if vctx is not None:
                validate_markup(vctx, location, choice)
Exemple #37
0
    def __init__(self, vctx, location, pattern):
        try:
            self.pattern = re.compile(pattern, self.re_flags)
        except Exception:
            tp, e, _ = sys.exc_info()

            raise ValidationError(
                    string_concat(
                        "%(location)s: ",
                        _("regex '%(pattern)s' did not compile"),
                        ": %(err_type)s: %(err_str)s")
                    % {
                        "location": location,
                        "pattern": pattern,
                        "err_type": tp.__name__,
                        "err_str": str(e)})
Exemple #38
0
    def __init__(self, vctx, location, page_desc):
        super(TextQuestionBase, self).__init__(vctx, location, page_desc)

        widget = TextAnswerForm.get_text_widget(
                getattr(page_desc, "widget", None),
                check_only=True)

        if widget is None:
            raise ValidationError(
                    string_concat(
                        "%(location)s: ",
                        _("unrecognized widget type"),
                        "'%(type)s'")
                    % {
                        'location': location,
                        'type': getattr(page_desc, "widget")})
Exemple #39
0
def send_enrollment_decision(participation, approved, request=None):
    # type: (Participation, bool, http.HttpRequest) -> None

    with translation.override(settings.RELATE_ADMIN_EMAIL_LOCALE):
        course = participation.course
        if request:
            course_uri = request.build_absolute_uri(
                    reverse("relate-course_page",
                        args=(course.identifier,)))
        else:
            # This will happen when this method is triggered by
            # a model signal which doesn't contain a request object.
            from six.moves.urllib.parse import urljoin
            course_uri = urljoin(getattr(settings, "RELATE_BASE_URL"),
                                 course.get_absolute_url())

        from relate.utils import render_email_template
        message = render_email_template("course/enrollment-decision-email.txt", {
            "user": participation.user,
            "approved": approved,
            "course": course,
            "course_uri": course_uri
            })

        from django.core.mail import EmailMessage
        email_kwargs = {}
        if settings.RELATE_EMAIL_SMTP_ALLOW_NONAUTHORIZED_SENDER:
            from_email = course.get_from_email()
        else:
            from_email = getattr(settings, "ENROLLMENT_EMAIL_FROM",
                                 settings.ROBOT_EMAIL_FROM)
            from relate.utils import get_outbound_mail_connection
            email_kwargs.update(
                {"connection": (
                    get_outbound_mail_connection("enroll")
                    if hasattr(settings, "ENROLLMENT_EMAIL_FROM")
                    else get_outbound_mail_connection("robot"))})

        msg = EmailMessage(
                string_concat("[%s] ", _("Your enrollment request"))
                % course.identifier,
                message,
                from_email,
                [participation.user.email],
                **email_kwargs)
        msg.bcc = [course.notify_email]
        msg.send()
Exemple #40
0
    def __init__(self, flow_ids, *args, **kwargs):
        super(FlowTestForm, self).__init__(*args, **kwargs)

        self.fields["flow_id"] = forms.ChoiceField(choices=[
            (fid, fid) for fid in flow_ids
        ],
                                                   required=True,
                                                   label=_("Flow ID"),
                                                   widget=Select2Widget())

        self.helper.add_input(
            Submit(
                "test",
                mark_safe_lazy(
                    string_concat(pgettext("Start an activity", "Go"),
                                  " &raquo;")),
            ))
Exemple #41
0
def _create_recurring_events_backend(course, time, kind, starting_ordinal, interval,
        count, duration_in_minutes, all_day, shown_in_calendar):
    ordinal = starting_ordinal

    import datetime

    for i in range(count):
        evt = Event()
        evt.course = course
        evt.kind = kind
        evt.ordinal = ordinal
        evt.time = time
        evt.all_day = all_day
        evt.shown_in_calendar = shown_in_calendar

        if duration_in_minutes:
            evt.end_time = evt.time + datetime.timedelta(
                    minutes=duration_in_minutes)
        try:
            evt.save()
        except IntegrityError:
            raise EventAlreadyExists(
                _("'%(event_kind)s %(event_ordinal)d' already exists") %
                {'event_kind': kind, 'event_ordinal': ordinal})

        date = time.date()
        if interval == "weekly":
            date += datetime.timedelta(weeks=1)
        elif interval == "biweekly":
            date += datetime.timedelta(weeks=2)
        else:
            raise ValueError(
                    string_concat(
                        pgettext_lazy(
                            "Unkown time interval",
                            "unknown interval"),
                        ": %s")
                    % interval)

        time = time.tzinfo.localize(
                datetime.datetime(date.year, date.month, date.day,
                    time.hour, time.minute, time.second))
        del date

        ordinal += 1
Exemple #42
0
def make_time_histogram(pctx, flow_id):
    qset = FlowSession.objects.filter(course=pctx.course, flow_id=flow_id)

    from relate.utils import string_concat
    hist = Histogram(
        num_log_bins=True,
        num_bin_title_formatter=(lambda minutes: string_concat(
            "$>$ %.1f ", pgettext("Minute (time unit)", "min")) % minutes))
    for session in qset:
        if session.in_progress:
            hist.add_data_point("".join(
                ["<", pgettext("Status of session", "in progress"), ">"]))
        else:
            delta = session.completion_time - session.start_time
            minutes = delta.total_seconds() / 60
            hist.add_data_point(minutes)

    return hist
Exemple #43
0
    def __init__(self, sessions, *args, **kwargs):
        # type: (List[FlowSession], *Any, **Any) -> None

        super(ExceptionStage2Form, self).__init__(*args, **kwargs)

        self.fields["session"] = forms.ChoiceField(
            choices=((session.id, strify_session_for_exception(session))
                     for session in sessions),
            help_text=_(
                "The rules that currently apply to selected "
                "session will provide the default values for the rules "
                "on the next page."),
            label=_("Session"))

        self.helper.add_input(
            Submit(
                "next",
                mark_safe_lazy(
                    string_concat(pgettext("Next step", "Next"), " &raquo;"))))
Exemple #44
0
    def correct_answer(self, page_context, page_data, answer_data, grade_data):
        # FIXME: Could use 'best' match to answer

        CA_PATTERN = string_concat(_("A correct answer is"), ": '%s'.")  # noqa

        for matcher in self.matchers:
            unspec_correct_answer_text = matcher.correct_answer_text()
            if unspec_correct_answer_text is not None:
                break

        assert unspec_correct_answer_text

        result = CA_PATTERN % unspec_correct_answer_text

        if hasattr(self.page_desc, "answer_explanation"):
            result += markup_to_html(page_context,
                                     self.page_desc.answer_explanation)

        return result
Exemple #45
0
    def __init__(self, vctx, location, page_desc, language_mode):
        super(CodeQuestion, self).__init__(vctx, location, page_desc)

        if vctx is not None and hasattr(page_desc, "data_files"):
            for data_file in page_desc.data_files:
                try:
                    if not isinstance(data_file, str):
                        raise ObjectDoesNotExist()

                    from course.content import get_repo_blob
                    get_repo_blob(vctx.repo, data_file, vctx.commit_sha)
                except ObjectDoesNotExist:
                    raise ValidationError(
                        string_concat("%(location)s: ",
                                      _("data file '%(file)s' not found")) % {
                                          "location": location,
                                          "file": data_file
                                      })

        if hasattr(page_desc, "docker_image"):
            self.container_image = page_desc.docker_image

        if not getattr(page_desc, "single_submission",
                       False) and vctx is not None:
            is_multi_submit = False

            if hasattr(page_desc, "access_rules"):
                if hasattr(page_desc.access_rules, "add_permissions"):
                    if (flow_permission.change_answer
                            in page_desc.access_rules.add_permissions):
                        is_multi_submit = True

            if not is_multi_submit:
                vctx.add_warning(
                    location,
                    _("code question does not explicitly "
                      "allow multiple submission. Either add "
                      "access_rules/add_permssions/change_answer "
                      "or add 'single_submission: True' to confirm that you intend "
                      "for only a single submission to be allowed. "
                      "While you're at it, consider adding "
                      "access_rules/add_permssions/see_correctness."))
Exemple #46
0
    def correct_answer(self, page_context, page_data, answer_data, grade_data):
        # FIXME: Could use 'best' match to answer

        cor_answer_output = self.get_question(page_context, page_data)

        for idx, wrapped in enumerate(self.embedded_wrapped_name_list):
            correct_answer_i = self.answer_instance_list[idx] \
                    .get_correct_answer_text(page_context)
            cor_answer_output = cor_answer_output.replace(
                wrapped,
                "<strong>" + correct_answer_i + "</strong>")

        CA_PATTERN = string_concat(_("A correct answer is"), ": <br/> %s")  # noqa

        result = CA_PATTERN % cor_answer_output

        if hasattr(self.page_desc, "answer_explanation"):
            result += markup_to_html(page_context, self.page_desc.answer_explanation)

        return result
Exemple #47
0
    def __init__(self, vctx, location, page_desc):
        super(FileUploadQuestion, self).__init__(vctx, location, page_desc)

        if not (set(page_desc.mime_types) <= set(self.ALLOWED_MIME_TYPES)):
            raise ValidationError(
                string_concat("%(location)s: ", _("unrecognized mime types"),
                              " '%(presenttype)s'") % {
                                  'location':
                                  location,
                                  'presenttype':
                                  ", ".join(
                                      set(page_desc.mime_types) -
                                      set(self.ALLOWED_MIME_TYPES))
                              })

        if vctx is not None:
            if not hasattr(page_desc, "value"):
                vctx.add_warning(
                    location,
                    _("upload question does not have "
                      "assigned point value"))
Exemple #48
0
def parse_matcher_string(vctx, location, matcher_desc):
    match = MATCHER_RE.match(matcher_desc)

    if match is not None:
        matcher_type = match.group(1)
        pattern = match.group(2)
    else:
        match = MATCHER_RE_2.match(matcher_desc)

        if match is None:
            raise ValidationError(
                string_concat("%s: ", _("does not specify match type")) %
                location)

        matcher_type = match.group(1)
        pattern = match.group(2)

        if vctx is not None:
            vctx.add_warning(location,
                             _("uses deprecated 'matcher:answer' style"))

    return (get_matcher_class(location, matcher_type, "string")(vctx, location,
                                                                pattern))
Exemple #49
0
    def __init__(self, course, flow_ids, *args, **kwargs):
        super(ExceptionStage1Form, self).__init__(*args, **kwargs)

        self.fields["participation"] = ParticipationChoiceField(
            queryset=(Participation.objects.filter(
                course=course,
                status=participation_status.active,
            ).order_by("user__last_name")),
            required=True,
            help_text=_("Select participant for whom exception is to "
                        "be granted."),
            label=_("Participant"),
            widget=Select2Widget())
        self.fields["flow_id"] = forms.ChoiceField(choices=[
            (fid, fid) for fid in flow_ids
        ],
                                                   required=True,
                                                   label=_("Flow ID"))

        self.helper.add_input(
            Submit(
                "next",
                mark_safe_lazy(
                    string_concat(pgettext("Next step", "Next"), " &raquo;"))))
Exemple #50
0
    def __init__(self, vctx, location, page_desc):
        super(PageBaseWithTitle, self).__init__(vctx, location, page_desc)

        title = None
        try:
            title = self.page_desc.title
        except AttributeError:
            pass

        if title is None:
            try:
                md_body = self.markup_body_for_title()
            except NotImplementedError:
                from warnings import warn
                warn(_("PageBaseWithTitle subclass '%s' does not implement "
                        "markup_body_for_title()")
                        % type(self).__name__)
            else:
                from course.content import extract_title_from_markup
                title = extract_title_from_markup(md_body)

        if title is None:
            raise ValidationError(
                    string_concat(
                        "%s: ",
                        _("no title found in body or title attribute"))
                    % (location))

        from markdown import markdown
        from django.utils.html import strip_tags
        title = strip_tags(markdown(title))

        if not title and vctx is not None:
            vctx.add_warning(location, _("the rendered title is an empty string"))

        self._title = title
Exemple #51
0
 def get_participant(self, obj):
     if obj.flow_session.participation:
         return obj.flow_session.participation.user
     else:
         return string_concat("(", _("anonymous"), ")")
Exemple #52
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"),
        })
Exemple #53
0
    def grade(self, page_context, page_data, answer_data, grade_data):
        if answer_data is None:
            return AnswerFeedback(correctness=0,
                                  feedback=_("No answer provided."))

        user_code = answer_data["answer"]

        # {{{ request run

        run_req = {"compile_only": False, "user_code": user_code}

        def transfer_attr(name):
            if hasattr(self.page_desc, name):
                run_req[name] = getattr(self.page_desc, name)

        transfer_attr("setup_code")
        transfer_attr("names_for_user")
        transfer_attr("names_from_user")

        run_req["test_code"] = self.get_test_code()

        if hasattr(self.page_desc, "data_files"):
            run_req["data_files"] = {}

            from course.content import get_repo_blob

            for data_file in self.page_desc.data_files:
                from base64 import b64encode
                run_req["data_files"][data_file] = \
                        b64encode(
                                get_repo_blob(
                                    page_context.repo, data_file,
                                    page_context.commit_sha).data).decode()

        try:
            response_dict = request_run_with_retries(
                run_req,
                run_timeout=self.page_desc.timeout,
                image=self.container_image)
        except Exception:
            from traceback import format_exc
            response_dict = {
                "result": "uncaught_error",
                "message": "Error connecting to container",
                "traceback": "".join(format_exc()),
            }

        # }}}

        feedback_bits = []

        correctness = None

        if "points" in response_dict:
            correctness = response_dict["points"]
            try:
                feedback_bits.append("<p><b>%s</b></p>" %
                                     _(get_auto_feedback(correctness)))
            except Exception as e:
                correctness = None
                response_dict["result"] = "setup_error"
                response_dict["message"] = ("%s: %s" %
                                            (type(e).__name__, str(e)))

        # {{{ send email if the grading code broke

        if response_dict["result"] in [
                "uncaught_error", "setup_compile_error", "setup_error",
                "test_compile_error", "test_error"
        ]:
            error_msg_parts = ["RESULT: %s" % response_dict["result"]]
            for key, val in sorted(response_dict.items()):
                if (key not in ["result", "figures"] and val
                        and isinstance(val, str)):
                    error_msg_parts.append(
                        "-------------------------------------")
                    error_msg_parts.append(key)
                    error_msg_parts.append(
                        "-------------------------------------")
                    error_msg_parts.append(val)
            error_msg_parts.append("-------------------------------------")
            error_msg_parts.append("user code")
            error_msg_parts.append("-------------------------------------")
            error_msg_parts.append(user_code)
            error_msg_parts.append("-------------------------------------")

            error_msg = "\n".join(error_msg_parts)

            from relate.utils import local_now, format_datetime_local
            from course.utils import LanguageOverride
            with LanguageOverride(page_context.course):
                from relate.utils import render_email_template
                message = render_email_template(
                    "course/broken-code-question-email.txt", {
                        "site": getattr(settings, "RELATE_BASE_URL"),
                        "page_id": self.page_desc.id,
                        "course": page_context.course,
                        "error_message": error_msg,
                        "review_uri": page_context.page_uri,
                        "time": format_datetime_local(local_now())
                    })

                if (not page_context.in_sandbox
                        and not is_nuisance_failure(response_dict)):
                    try:
                        from django.core.mail import EmailMessage
                        msg = EmailMessage(
                            "".join([
                                "[%s:%s] ",
                                _("code question execution failed")
                            ]) % (page_context.course.identifier,
                                  page_context.flow_session.flow_id
                                  if page_context.flow_session is not None else
                                  _("<unknown flow>")), message,
                            settings.ROBOT_EMAIL_FROM,
                            [page_context.course.notify_email])

                        from relate.utils import get_outbound_mail_connection
                        msg.connection = get_outbound_mail_connection("robot")
                        msg.send()

                    except Exception:
                        from traceback import format_exc
                        feedback_bits.append(
                            str(
                                string_concat(
                                    "<p>",
                                    _("Both the grading code and the attempt to "
                                      "notify course staff about the issue failed. "
                                      "Please contact the course or site staff and "
                                      "inform them of this issue, mentioning this "
                                      "entire error message:"), "</p>", "<p>",
                                    _("Sending an email to the course staff about the "
                                      "following failure failed with "
                                      "the following error message:"), "<pre>",
                                    "".join(format_exc()), "</pre>",
                                    _("The original failure message follows:"),
                                    "</p>")))

        # }}}

        if hasattr(self.page_desc, "correct_code"):

            def normalize_code(s):
                return (s.replace(" ", "").replace("\r", "").replace(
                    "\n", "").replace("\t", ""))

            if (normalize_code(user_code) == normalize_code(
                    self.page_desc.correct_code)):
                feedback_bits.append(
                    "<p><b>%s</b></p>" %
                    _("It looks like you submitted code that is identical to "
                      "the reference solution. This is not allowed."))

        from relate.utils import dict_to_struct
        response = dict_to_struct(response_dict)

        bulk_feedback_bits = []

        if response.result == "success":
            pass
        elif response.result in [
                "uncaught_error", "setup_compile_error", "setup_error",
                "test_compile_error", "test_error"
        ]:
            feedback_bits.append("".join([
                "<p>",
                _("The grading code failed. Sorry about that. "
                  "The staff has been informed, and if this problem is "
                  "due to an issue with the grading code, "
                  "it will be fixed as soon as possible. "
                  "In the meantime, you'll see a traceback "
                  "below that may help you figure out what went wrong."),
                "</p>"
            ]))
        elif response.result == "timeout":
            feedback_bits.append("".join([
                "<p>",
                _("Your code took too long to execute. The problem "
                  "specifies that your code may take at most %s seconds "
                  "to run. "
                  "It took longer than that and was aborted."), "</p>"
            ]) % self.page_desc.timeout)

            correctness = 0
        elif response.result == "user_compile_error":
            feedback_bits.append("".join([
                "<p>",
                _("Your code failed to compile. An error message is "
                  "below."), "</p>"
            ]))

            correctness = 0
        elif response.result == "user_error":
            feedback_bits.append("".join([
                "<p>",
                _("Your code failed with an exception. "
                  "A traceback is below."), "</p>"
            ]))

            correctness = 0
        else:
            raise RuntimeError("invalid run result: %s" % response.result)

        if hasattr(response, "feedback") and response.feedback:

            def sanitize(s):
                import bleach
                return bleach.clean(s, tags=["p", "pre"])

            feedback_bits.append("".join([
                "<p>",
                _("Here is some feedback on your code"), ":"
                "<ul>%s</ul></p>"
            ]) % "".join("<li>%s</li>" % sanitize(fb_item)
                         for fb_item in response.feedback))
        if hasattr(response, "traceback") and response.traceback:
            feedback_bits.append("".join([
                "<p>",
                _("This is the exception traceback"), ":"
                "<pre>%s</pre></p>"
            ]) % escape(response.traceback))
        if hasattr(response,
                   "exec_host") and response.exec_host != "localhost":
            import socket
            try:
                exec_host_name, dummy, dummy = socket.gethostbyaddr(
                    response.exec_host)
            except socket.error:
                exec_host_name = response.exec_host

            feedback_bits.append("".join(
                ["<p>",
                 _("Your code ran on %s.") % exec_host_name, "</p>"]))

        if hasattr(response, "stdout") and response.stdout:
            bulk_feedback_bits.append("".join([
                "<p>",
                _("Your code printed the following output"), ":"
                "<pre>%s</pre></p>"
            ]) % escape(response.stdout))
        if hasattr(response, "stderr") and response.stderr:
            bulk_feedback_bits.append("".join([
                "<p>",
                _("Your code printed the following error messages"), ":"
                "<pre>%s</pre></p>"
            ]) % escape(response.stderr))
        if hasattr(response, "figures") and response.figures:
            fig_lines = [
                "".join([
                    "<p>",
                    _("Your code produced the following plots"), ":</p>"
                ]),
                '<dl class="result-figure-list">',
            ]

            for nr, mime_type, b64data in response.figures:
                if mime_type in ["image/jpeg", "image/png"]:
                    fig_lines.extend([
                        "".join(["<dt>", _("Figure"), "%d<dt>"]) % nr,
                        '<dd><img alt="Figure %d" src="data:%s;base64,%s"></dd>'
                        % (nr, mime_type, b64data)
                    ])

            fig_lines.append("</dl>")
            bulk_feedback_bits.extend(fig_lines)

        # {{{ html output / santization

        if hasattr(response, "html") and response.html:

            def is_allowed_data_uri(allowed_mimetypes, uri):
                import re
                m = re.match(r"^data:([-a-z0-9]+/[-a-z0-9]+);base64,", uri)
                if not m:
                    return False

                mimetype = m.group(1)
                return mimetype in allowed_mimetypes

            def sanitize(s):
                import bleach

                def filter_audio_attributes(tag, name, value):
                    if name in ["controls"]:
                        return True
                    else:
                        return False

                def filter_source_attributes(tag, name, value):
                    if name in ["type"]:
                        return True
                    elif name == "src":
                        if is_allowed_data_uri([
                                "audio/wav",
                        ], value):
                            return bleach.sanitizer.VALUE_SAFE
                        else:
                            return False
                    else:
                        return False

                def filter_img_attributes(tag, name, value):
                    if name in ["alt", "title"]:
                        return True
                    elif name == "src":
                        return is_allowed_data_uri([
                            "image/png",
                            "image/jpeg",
                        ], value)
                    else:
                        return False

                if not isinstance(s, str):
                    return _("(Non-string in 'HTML' output filtered out)")

                return bleach.clean(s,
                                    tags=bleach.ALLOWED_TAGS +
                                    ["audio", "video", "source"],
                                    attributes={
                                        "audio": filter_audio_attributes,
                                        "source": filter_source_attributes,
                                        "img": filter_img_attributes,
                                    })

            bulk_feedback_bits.extend(
                sanitize(snippet) for snippet in response.html)

        # }}}

        return AnswerFeedback(correctness=correctness,
                              feedback="\n".join(feedback_bits),
                              bulk_feedback="\n".join(bulk_feedback_bits))
Exemple #54
0
    def update_grade_data_from_grading_form_v2(self, request, page_context,
                                               page_data, grade_data,
                                               grading_form, files_data):

        if grade_data is None:
            grade_data = {}
        for k in self.grade_data_attrs:
            if k == "grade_percent":
                grade_data[k] = grading_form.cleaned_percent()
            else:
                grade_data[k] = grading_form.cleaned_data[k]

        if grading_form.cleaned_data["notify"] and page_context.flow_session:
            with translation.override(settings.RELATE_ADMIN_EMAIL_LOCALE):
                from django.template.loader import render_to_string
                from course.utils import will_use_masked_profile_for_email
                staff_email = [
                    page_context.course.notify_email, request.user.email
                ]
                message = render_to_string(
                    "course/grade-notify.txt", {
                        "page_title":
                        self.title(page_context, page_data),
                        "course":
                        page_context.course,
                        "participation":
                        page_context.flow_session.participation,
                        "feedback_text":
                        grade_data["feedback_text"],
                        "flow_session":
                        page_context.flow_session,
                        "review_uri":
                        page_context.page_uri,
                        "use_masked_profile":
                        will_use_masked_profile_for_email(staff_email)
                    })

                from django.core.mail import EmailMessage
                msg = EmailMessage(
                    string_concat("[%(identifier)s:%(flow_id)s] ",
                                  _("New notification")) %
                    {
                        'identifier': page_context.course.identifier,
                        'flow_id': page_context.flow_session.flow_id
                    }, message,
                    getattr(settings, "GRADER_FEEDBACK_EMAIL_FROM",
                            page_context.course.get_from_email()),
                    [page_context.flow_session.participation.user.email])
                msg.bcc = [page_context.course.notify_email]

                if grading_form.cleaned_data["may_reply"]:
                    msg.reply_to = [request.user.email]

                if hasattr(settings, "GRADER_FEEDBACK_EMAIL_FROM"):
                    from relate.utils import get_outbound_mail_connection
                    msg.connection = get_outbound_mail_connection(
                        "grader_feedback")
                msg.send()

        if (grading_form.cleaned_data["notes"]
                and grading_form.cleaned_data["notify_instructor"]
                and page_context.flow_session):
            with translation.override(settings.RELATE_ADMIN_EMAIL_LOCALE):
                from django.template.loader import render_to_string
                from course.utils import will_use_masked_profile_for_email
                staff_email = [
                    page_context.course.notify_email, request.user.email
                ]
                message = render_to_string(
                    "course/grade-internal-notes-notify.txt", {
                        "page_title":
                        self.title(page_context, page_data),
                        "course":
                        page_context.course,
                        "participation":
                        page_context.flow_session.participation,
                        "notes_text":
                        grade_data["notes"],
                        "flow_session":
                        page_context.flow_session,
                        "review_uri":
                        page_context.page_uri,
                        "sender":
                        request.user,
                        "use_masked_profile":
                        will_use_masked_profile_for_email(staff_email)
                    })

                from django.core.mail import EmailMessage
                msg = EmailMessage(
                    string_concat("[%(identifier)s:%(flow_id)s] ",
                                  _("Grading notes from %(ta)s")) %
                    {
                        'identifier': page_context.course.identifier,
                        'flow_id': page_context.flow_session.flow_id,
                        'ta': request.user.get_full_name()
                    }, message,
                    getattr(settings, "GRADER_FEEDBACK_EMAIL_FROM",
                            page_context.course.get_from_email()),
                    [page_context.course.notify_email])
                msg.bcc = [request.user.email]
                msg.reply_to = [request.user.email]

                if hasattr(settings, "GRADER_FEEDBACK_EMAIL_FROM"):
                    from relate.utils import get_outbound_mail_connection
                    msg.connection = get_outbound_mail_connection(
                        "grader_feedback")
                msg.send()

        return grade_data
Exemple #55
0
from django.utils.translation import (
        ugettext_lazy as _, ugettext)
from course.validation import validate_struct, ValidationError
import django.forms as forms

from relate.utils import StyledForm, Struct, string_concat
from course.page.base import (
        AnswerFeedback, PageBaseWithTitle, PageBaseWithValue, markup_to_html,
        PageBaseWithHumanTextFeedback, PageBaseWithCorrectAnswer,

        get_editor_interaction_mode)

import re
import sys

CORRECT_ANSWER_PATTERN = string_concat(_("A correct answer is"), ": '%s'.")  # noqa


class TextAnswerForm(StyledForm):
    # prevents form submission with codemirror's empty textarea
    use_required_attribute = False

    @staticmethod
    def get_text_widget(widget_type, read_only=False, check_only=False,
            interaction_mode=None):
        """Returns None if no widget found."""

        if widget_type in [None, "text_input"]:
            if check_only:
                return True
Exemple #56
0
    def __init__(self, vctx, location, matcher_desc):
        self.matcher_desc = matcher_desc

        validate_struct(
                vctx,
                location,
                matcher_desc,
                required_attrs=(
                    ("type", str),
                    ("value", six.integer_types + (float, str)),
                    ),
                allowed_attrs=(
                    ("rtol", six.integer_types + (float, str)),
                    ("atol", six.integer_types + (float, str)),
                    ),
                )

        try:
            self.matcher_desc.value = \
                    float_or_sympy_evalf(matcher_desc.value)
        except Exception:
            raise ValidationError(
                    string_concat(
                        "%s: 'value' ",
                        _("does not provide a valid float literal"))
                    % location)

        if hasattr(matcher_desc, "rtol"):
            try:
                self.matcher_desc.rtol = \
                        float_or_sympy_evalf(matcher_desc.rtol)
            except Exception:
                raise ValidationError(
                        string_concat(
                            "%s: 'rtol' ",
                            _("does not provide a valid float literal"))
                        % location)

            if matcher_desc.value == 0:
                raise ValidationError(
                        string_concat(
                            "%s: 'rtol' ",
                            _("not allowed when 'value' is zero"))
                        % location)

        if hasattr(matcher_desc, "atol"):
            try:
                self.matcher_desc.atol = \
                        float_or_sympy_evalf(matcher_desc.atol)
            except Exception:
                raise ValidationError(
                        string_concat(
                            "%s: 'atol' ",
                            _("does not provide a valid float literal"))
                        % location)
        else:
            if matcher_desc.value == 0:
                vctx.add_warning(location,
                         _("Float match for 'value' zero should have atol--"
                           "otherwise it will match any number"))

        if (
                not matcher_desc.value == 0
                and
                not hasattr(matcher_desc, "atol")
                and
                not hasattr(matcher_desc, "rtol")
                and
                vctx is not None):
            vctx.add_warning(location,
                    _("Float match should have either rtol or atol--"
                        "otherwise it will match any number"))
Exemple #57
0
    def __init__(self, vctx, location, page_desc):
        super(InlineMultiQuestion, self).__init__(vctx, location, page_desc)

        expanded_question = page_desc.question

        self.embedded_wrapped_name_list = WRAPPED_NAME_RE.findall(
            expanded_question)
        self.embedded_name_list = NAME_RE.findall(expanded_question)

        answer_instance_list = []

        for idx, name in enumerate(self.embedded_name_list):
            answers_desc = getattr(self.page_desc.answers, name)

            parsed_answer = parse_question(vctx, location, name, answers_desc)
            answer_instance_list.append(parsed_answer)

        self.answer_instance_list = answer_instance_list

        from relate.utils import struct_to_dict
        answers_name_list = struct_to_dict(page_desc.answers).keys()

        invalid_answer_name = []
        invalid_embedded_name = []

        if not answer_instance_list:
            raise ValidationError(
                string_concat(
                    "%(location)s: ",
                    _("InlineMultiQuestion requires at least one "
                      "answer field to be defined.")) % {'location': location})

        for answers_name in answers_name_list:
            if NAME_VALIDATE_RE.match(answers_name) is None:
                invalid_answer_name.append(answers_name)
        if len(invalid_answer_name) > 0:
            raise ValidationError(
                string_concat(
                    "%s: ", _("invalid answers name %s. "),
                    _("A valid name should start with letters. "
                      "Alphanumeric with underscores. "
                      "Do not use spaces.")) % (location, ", ".join(
                          ["'" + name + "'" for name in invalid_answer_name])))

        for embedded_name in self.embedded_name_list:
            if NAME_VALIDATE_RE.match(embedded_name) is None:
                invalid_embedded_name.append(embedded_name)
        if len(invalid_embedded_name) > 0:
            raise ValidationError(
                string_concat(
                    "%s: ", _("invalid embedded question name %s. "),
                    _("A valid name should start with letters. "
                      "Alphanumeric with underscores. "
                      "Do not use spaces.")) % (location, ", ".join(
                          ["'" + name + "'"
                           for name in invalid_embedded_name])))

        if len(set(self.embedded_name_list)) < len(self.embedded_name_list):
            duplicated = list(
                set([
                    x for x in self.embedded_name_list
                    if self.embedded_name_list.count(x) > 1
                ]))
            raise ValidationError(
                string_concat("%s: ",
                              _("embedded question name %s not unique.")) %
                (location, ", ".join(duplicated)))

        no_answer_set = set(self.embedded_name_list) - set(answers_name_list)
        redundant_answer_list = list(
            set(answers_name_list) - set(self.embedded_name_list))

        if no_answer_set:
            raise ValidationError(
                string_concat(
                    "%s: ",
                    _("correct answer(s) not provided for question %s.")) %
                (location, ", ".join(
                    ["'" + item + "'" for item in list(no_answer_set)])))

        if redundant_answer_list:
            if vctx is not None:
                vctx.add_warning(
                    location,
                    _("redundant answers %s provided for "
                      "non-existing question(s).") % ", ".join(
                          ["'" + item + "'"
                           for item in redundant_answer_list]))

        if vctx is not None:
            validate_markup(vctx, location, page_desc.question)

            def reverse_func(*args, **kwargs):
                pass

            # FIXME This is a bit redundant since validate_markup already calls
            # markup_to_html.
            remainder_html = markup_to_html(vctx,
                                            page_desc.question,
                                            reverse_func=reverse_func)

            html_list = []
            for wrapped_name in self.embedded_wrapped_name_list:
                [html, remainder_html] = remainder_html.split(wrapped_name)
                html_list.append(html)

            if remainder_html != "":
                html_list.append(remainder_html)

            # make sure all [[ and ]] are paired.
            embedded_removed = " ".join(html_list)

            for sep in ["[[", "]]"]:
                if sep in embedded_removed:
                    raise ValidationError(
                        string_concat("%s: ", _("have unpaired '%s'.")) %
                        (location, sep))

            for idx, name in enumerate(self.embedded_name_list):
                answers_desc = getattr(page_desc.answers, name)

                parse_question(vctx, location, name, answers_desc)
Exemple #58
0
def enroll_view(request, course_identifier):
    # type: (http.HttpRequest, str) -> http.HttpResponse

    course = get_object_or_404(Course, identifier=course_identifier)
    participation = get_participation_for_request(request, course)

    if participation is not None:
        messages.add_message(request, messages.ERROR,
                             _("Already enrolled. Cannot re-renroll."))
        return redirect("relate-course_page", course_identifier)

    if not course.accepts_enrollment:
        messages.add_message(request, messages.ERROR,
                             _("Course is not accepting enrollments."))
        return redirect("relate-course_page", course_identifier)

    if request.method != "POST":
        # This can happen if someone tries to refresh the page, or switches to
        # desktop view on mobile.
        messages.add_message(request, messages.ERROR,
                             _("Can only enroll using POST request"))
        return redirect("relate-course_page", course_identifier)

    user = request.user
    if (course.enrollment_required_email_suffix
            and user.status != user_status.active):
        messages.add_message(
            request, messages.ERROR,
            _("Your email address is not yet confirmed. "
              "Confirm your email to continue."))
        return redirect("relate-course_page", course_identifier)

    preapproval = None
    if request.user.email:
        try:
            preapproval = ParticipationPreapproval.objects.get(
                course=course, email__iexact=request.user.email)
        except ParticipationPreapproval.DoesNotExist:
            if user.institutional_id:
                if not (course.preapproval_require_verified_inst_id
                        and not user.institutional_id_verified):
                    try:
                        preapproval = ParticipationPreapproval.objects.get(
                            course=course,
                            institutional_id__iexact=user.institutional_id)
                    except ParticipationPreapproval.DoesNotExist:
                        pass
            pass

    if (preapproval is None and course.enrollment_required_email_suffix and
            not user.email.endswith(course.enrollment_required_email_suffix)):

        messages.add_message(
            request, messages.ERROR,
            _("Enrollment not allowed. Please use your '%s' email to "
              "enroll.") % course.enrollment_required_email_suffix)
        return redirect("relate-course_page", course_identifier)

    roles = ParticipationRole.objects.filter(
        course=course, is_default_for_new_participants=True)

    if preapproval is not None:
        roles = list(preapproval.roles.all())

    try:
        if course.enrollment_approval_required and preapproval is None:
            participation = handle_enrollment_request(
                course, user, participation_status.requested, roles, request)

            with translation.override(settings.RELATE_ADMIN_EMAIL_LOCALE):
                from django.template.loader import render_to_string
                message = render_to_string(
                    "course/enrollment-request-email.txt", {
                        "user":
                        user,
                        "course":
                        course,
                        "admin_uri":
                        mark_safe(
                            request.build_absolute_uri(
                                reverse("relate-edit_participation",
                                        args=(course.identifier,
                                              participation.id))))
                    })

                from django.core.mail import EmailMessage
                msg = EmailMessage(
                    string_concat("[%s] ", _("New enrollment request")) %
                    course_identifier, message, settings.ROBOT_EMAIL_FROM,
                    [course.notify_email])

                from relate.utils import get_outbound_mail_connection
                msg.connection = get_outbound_mail_connection("robot")
                msg.send()

            messages.add_message(
                request, messages.INFO,
                _("Enrollment request sent. You will receive notifcation "
                  "by email once your request has been acted upon."))
        else:
            handle_enrollment_request(course, user,
                                      participation_status.active, roles,
                                      request)

            messages.add_message(request, messages.SUCCESS,
                                 _("Successfully enrolled."))

    except IntegrityError:
        messages.add_message(
            request, messages.ERROR,
            _("A participation already exists. Enrollment attempt aborted."))

    return redirect("relate-course_page", course_identifier)
Exemple #59
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)
            },
        })