Example #1
0
 def test_flow_desc_access_rule_has_change_answer_perm(self):
     flow_desc_dict = self.get_hacked_flow_desc(as_dict=True)
     rules = flow_desc_dict["rules"]
     rules.access = [dict_to_struct(
         {"permissions": ["submit_answer", "change_answer"]})]
     flow_desc = dict_to_struct(flow_desc_dict)
     self.assertTrue(analytics.is_flow_multiple_submit(flow_desc))
def get_yaml_from_repo_side_effect(repo, full_name, commit_sha, cached=True):
    if full_name == events_file:
        return dict_to_struct(
            {"event_kinds": dict_to_struct({
                "lecture": dict_to_struct({
                    "title": "Lecture {nr}",
                    "color": "blue"
                })}),
                "events": dict_to_struct({
                    "lecture 1": dict_to_struct({
                        "title": "l1"})
                })})
    else:
        return get_yaml_from_repo(repo, full_name, commit_sha, cached)
Example #3
0
def get_flow_rules(flow_desc, kind, participation, flow_id, now_datetime,
        consider_exceptions=True, default_rules_desc=[]):
    if (not hasattr(flow_desc, "rules")
            or not hasattr(flow_desc.rules, kind)):
        rules = default_rules_desc[:]
    else:
        rules = getattr(flow_desc.rules, kind)[:]

    from course.models import FlowRuleException
    if consider_exceptions:
        for exc in (
                FlowRuleException.objects
                .filter(
                    participation=participation,
                    active=True,
                    kind=kind,
                    flow_id=flow_id)
                # rules created first will get inserted first, and show up last
                .order_by("creation_time")):

            if exc.expiration is not None and now_datetime > exc.expiration:
                continue

            from relate.utils import dict_to_struct
            rules.insert(0, dict_to_struct(exc.rule))

    return rules
Example #4
0
    def test_choice_not_stringifiable(self):
        expected_page_error = (
            "choice 10: unable to convert to string")

        class BadChoice(object):
            def __str__(self):
                raise Exception

        from relate.utils import dict_to_struct
        fake_page_desc = dict_to_struct(
            {'type': 'SurveyChoiceQuestion', 'id': 'age_group_with_comment',
             'answer_comment': 'this is a survey question',
             'prompt': '\n# Age\n\nHow old are you?\n',
             'choices': [
                 '0-10 years', '11-20 years', '21-30 years', '31-40 years',
                 '41-50 years', '51-60 years', '61-70 years', '71-80 years',
                 '81-90 years', BadChoice()],
             '_field_names': ['type', 'id', 'answer_comment',
                              'prompt', 'choices']}
        )

        with mock.patch("relate.utils.dict_to_struct") as mock_dict_to_struct:
            mock_dict_to_struct.return_value = fake_page_desc

            markdown = SURVEY_CHOICE_QUESTION_MARKDOWN

            resp = (
                self.get_page_sandbox_preview_response(markdown))
            self.assertEqual(resp.status_code, 200)
            self.assertSandboxNotHasValidPage(resp)
            self.assertResponseContextContains(resp, PAGE_ERRORS,
                                               expected_page_error)
Example #5
0
def check_attributes_yml(vctx, repo, path, tree):
    try:
        _, attr_blob_sha = tree[".attributes.yml"]
    except KeyError:
        # no .attributes.yml here
        pass
    else:
        from relate.utils import dict_to_struct
        from yaml import load as load_yaml

        att_yml = dict_to_struct(load_yaml(repo[attr_blob_sha].data))

        loc = path + "/" + ".attributes.yml"
        validate_struct(
                vctx, loc, att_yml,
                required_attrs=[],
                allowed_attrs=[
                    ("public", list),
                ])

        if hasattr(att_yml, "public"):
            for i, l in enumerate(att_yml.public):
                if not isinstance(l, (str, unicode)):
                    raise ValidationError(
                            "%s: entry %d in 'public' is not a string"
                            % (loc, i+1))

    import stat
    for entry in tree.items():
        if stat.S_ISDIR(entry.mode):
            _, blob_sha = tree[entry.path]
            subtree = repo[blob_sha]
            check_attributes_yml(vctx, repo, path+"/"+entry.path, subtree)
Example #6
0
    def validate(self, new_page_source):
        from relate.utils import dict_to_struct
        import yaml

        try:
            page_desc = dict_to_struct(yaml.safe_load(new_page_source))

            from course.validation import (
                    validate_flow_page, ValidationContext)
            vctx = ValidationContext(
                    # FIXME
                    repo=None,
                    commit_sha=None)

            validate_flow_page(vctx, "submitted page", page_desc)

            if page_desc.type != self.validator_desc.page_type:
                raise ValidationError(ugettext("page must be of type '%s'")
                        % self.validator_desc.page_type)

        except Exception:
            tp, e, _ = sys.exc_info()

            raise forms.ValidationError("%(err_type)s: %(err_str)s"
                    % {"err_type": tp.__name__, "err_str": str(e)})
Example #7
0
def check_attributes_yml(vctx, repo, path, tree):
    try:
        _, attr_blob_sha = tree[".attributes.yml"]
    except KeyError:
        # no .attributes.yml here
        pass
    else:
        from relate.utils import dict_to_struct
        from yaml import load as load_yaml

        att_yml = dict_to_struct(load_yaml(repo[attr_blob_sha].data))

        loc = path + "/" + ".attributes.yml"
        validate_struct(vctx, loc, att_yml, required_attrs=[], allowed_attrs=[("public", list), ("in_exam", list)])

        for access_kind in ["public", "in_exam"]:
            if hasattr(att_yml, access_kind):
                for i, l in enumerate(att_yml.public):
                    if not isinstance(l, six.string_types):
                        raise ValidationError("%s: entry %d in '%s' is not a string" % (loc, i + 1, access_kind))

    import stat

    for entry in tree.items():
        if stat.S_ISDIR(entry.mode):
            _, blob_sha = tree[entry.path]
            subtree = repo[blob_sha]
            check_attributes_yml(vctx, repo, path + "/" + entry.path.decode("utf-8"), subtree)
Example #8
0
def get_yaml_from_repo(repo, full_name, commit_sha, cached=True):
    """Return decoded, struct-ified YAML data structure from
    the given file in *repo* at *commit_sha*.

    See :class:`relate.utils.Struct` for more on
    struct-ification.
    """

    if cached:
        cache_key = "%%%2".join((repo.controldir(), full_name, commit_sha))

        import django.core.cache as cache

        def_cache = cache.caches["default"]
        result = def_cache.get(cache_key)
        if result is not None:
            return result

    result = dict_to_struct(
        load_yaml(expand_yaml_macros(repo, commit_sha, get_repo_blob(repo, full_name, commit_sha).data))
    )

    if cached:
        def_cache.add(cache_key, result, None)

    return result
Example #9
0
def get_yaml_from_repo(repo, full_name, commit_sha, cached=True):
    """Return decoded, struct-ified YAML data structure from
    the given file in *repo* at *commit_sha*.

    See :class:`relate.utils.Struct` for more on
    struct-ification.
    """

    if cached:
        from six.moves.urllib.parse import quote_plus
        cache_key = "%%%2".join(
                (quote_plus(repo.controldir()), quote_plus(full_name),
                    commit_sha.decode()))

        import django.core.cache as cache
        def_cache = cache.caches["default"]
        result = None
        # Memcache is apparently limited to 250 characters.
        if len(cache_key) < 240:
            result = def_cache.get(cache_key)
        if result is not None:
            return result

    expanded = expand_yaml_macros(
            repo, commit_sha,
            get_repo_blob(repo, full_name, commit_sha).data)

    result = dict_to_struct(load_yaml(expanded))

    if cached:
        def_cache.add(cache_key, result, None)

    return result
Example #10
0
 def test_parse_matcher_instance_is_struct_no_type_error(self):
     s = dict_to_struct(
         {"value": "20.1"})
     with self.assertRaises(ValidationError) as cm:
         parse_matcher(None, "some where", s)
     self.assertIn("some where: matcher must supply 'type'",
                   str(cm.exception))
Example #11
0
 def test_parse_matcher_instance_is_struct(self):
     s = dict_to_struct(
         {"type": "float",
          "value": "20.1",
          })
     result = parse_matcher(None, "", s)
     self.assertTrue(isinstance(result, FloatMatcher))
     self.assertEqual(result.correct_answer_text(), "20.1")
Example #12
0
 def test_float_matcher_neither_atol_nor_rtol_present_warning(self):
     mock_vctx = mock.MagicMock()
     expected_warning = ("Float match should have either rtol or atol--"
                         "otherwise it will match any number")
     FloatMatcher(mock_vctx, "some where",
                  dict_to_struct(
                      {"type": "float",
                       "value": "1"}))
     self.assertIn(expected_warning, mock_vctx.add_warning.call_args[0])
Example #13
0
 def test_float_matcher_atol_error(self):
     expected_error_msg = "'atol' does not provide a valid float literal"
     with self.assertRaises(ValidationError) as cm:
         FloatMatcher(None, "",
                      dict_to_struct(
                          {"type": "float",
                           "value": "1",
                           "atol": "abcd"}))
     self.assertIn(expected_error_msg, str(cm.exception))
Example #14
0
 def test_float_matcher_value_zero_rtol_zero_error(self):
     expected_error_msg = "'rtol' not allowed when 'value' is zero"
     with self.assertRaises(ValidationError) as cm:
         FloatMatcher(None, "",
                      dict_to_struct(
                          {"type": "float",
                           "value": "0",
                           "rtol": "0"}))
     self.assertIn(expected_error_msg, str(cm.exception))
Example #15
0
    def test_float_matcher_value_zero_atol_not_present_warning(self):
        mock_vctx = mock.MagicMock()
        expected_warning = ("Float match for 'value' zero should have "
                            "atol--otherwise it will match any number")
        FloatMatcher(mock_vctx, "some where",
                     dict_to_struct(
                         {"type": "float",
                          "value": "0"}))

        self.assertIn(expected_warning, mock_vctx.add_warning.call_args[0])
Example #16
0
    def test_float_matcher_grade_neither_rtol_nor_atol(self):
        matcher = FloatMatcher(None, "",
                               dict_to_struct(
                                   {"type": "float",
                                    "value": "20.1",
                                    }))
        self.assertEqual(matcher.grade(""), 0)
        self.assertEqual(matcher.grade("abcd"), 0)

        self.assertEqual(matcher.grade(20000), 1)
        self.assertEqual(matcher.grade(-2), 1)
Example #17
0
    def test_float_matcher_grade_inf(self):
        matcher = FloatMatcher(None, "",
                               dict_to_struct(
                                   {"type": "float",
                                    "value": "inf",
                                    "rtol": 0.01
                                    }))

        self.assertEqual(matcher.grade(float("nan")), 0)
        self.assertEqual(matcher.grade(float("inf")), 1)
        self.assertEqual(matcher.grade(float("20.5")), 0)
Example #18
0
def get_session_grading_rule(session, role, flow_desc, now_datetime):
    flow_desc_rules = getattr(flow_desc, "rules", None)

    from relate.utils import dict_to_struct
    rules = get_flow_rules(flow_desc, flow_rule_kind.grading,
            session.participation, session.flow_id, now_datetime,
            default_rules_desc=[
                dict_to_struct(dict(
                    generates_grade=False,
                    ))])

    for rule in rules:
        if hasattr(rule, "if_has_role"):
            if role not in rule.if_has_role:
                continue

        if hasattr(rule, "if_has_tag"):
            if session.access_rules_tag != rule.if_has_tag:
                continue

        if hasattr(rule, "if_completed_before"):
            ds = parse_date_spec(session.course, rule.if_completed_before)
            if session.in_progress and now_datetime > ds:
                continue
            if not session.in_progress and session.completion_time > ds:
                continue

        due = parse_date_spec(session.course, getattr(rule, "due", None))
        if due is not None:
            assert due.tzinfo is not None

        generates_grade = getattr(rule, "generates_grade", True)

        grade_identifier = None
        grade_aggregation_strategy = None
        if flow_desc_rules is not None:
            grade_identifier = flow_desc_rules.grade_identifier
            grade_aggregation_strategy = getattr(
                    flow_desc_rules, "grade_aggregation_strategy", None)

        return FlowSessionGradingRule(
                grade_identifier=grade_identifier,
                grade_aggregation_strategy=grade_aggregation_strategy,
                due=due,
                generates_grade=generates_grade,
                description=getattr(rule, "description", None),
                credit_percent=getattr(rule, "credit_percent", 100),
                use_last_activity_as_completion_time=getattr(
                    rule, "use_last_activity_as_completion_time", False),
                )

    raise RuntimeError(_("grading rule determination was unable to find "
            "a grading rule"))
Example #19
0
    def test_float_matcher_validate(self):
        matcher = FloatMatcher(None, "",
                               dict_to_struct(
                                   {"type": "float",
                                    "value": "1",
                                    "atol": 0.01
                                    }))
        matcher.validate(1.1)

        expected_error_msg = "TypeError: can\'t convert expression to float"
        with self.assertRaises(forms.ValidationError) as cm:
            matcher.validate("abcd")
        self.assertIn(expected_error_msg, str(cm.exception))
Example #20
0
    def clean(self):
        super(FlowRuleException, self).clean()

        if (self.kind == flow_rule_kind.grading
                and self.expiration is not None):
            raise ValidationError(_("grading rules may not expire"))

        from course.validation import (
                ValidationError as ContentValidationError,
                validate_session_start_rule,
                validate_session_access_rule,
                validate_session_grading_rule,
                ValidationContext)
        from course.content import (get_course_repo,
                get_course_commit_sha,
                get_flow_desc)

        from relate.utils import dict_to_struct
        rule = dict_to_struct(self.rule)

        repo = get_course_repo(self.participation.course)
        commit_sha = get_course_commit_sha(
                self.participation.course, self.participation)
        ctx = ValidationContext(
                repo=repo,
                commit_sha=commit_sha)

        flow_desc = get_flow_desc(repo,
                self.participation.course,
                self.flow_id, commit_sha)

        tags = None
        if hasattr(flow_desc, "rules"):
            tags = getattr(flow_desc.rules, "tags", None)

        try:
            if self.kind == flow_rule_kind.start:
                validate_session_start_rule(ctx, unicode(self), rule, tags)
            elif self.kind == flow_rule_kind.access:
                validate_session_access_rule(ctx, unicode(self), rule, tags)
            elif self.kind == flow_rule_kind.grading:
                validate_session_grading_rule(ctx, unicode(self), rule, tags)
            else:
                # the rule refers to FlowRuleException rule
                raise ValidationError(_("invalid rule kind: ")+self.kind)

        except ContentValidationError as e:
            # the rule refers to FlowRuleException rule
            raise ValidationError(_("invalid existing_session_rules: ")+str(e))
Example #21
0
def get_yaml_from_repo(repo, full_name, commit_sha, cached=True):
    # type: (Repo_ish, Text, bytes, bool) -> Any

    """Return decoded, struct-ified YAML data structure from
    the given file in *repo* at *commit_sha*.

    See :class:`relate.utils.Struct` for more on
    struct-ification.
    """

    if cached:
        try:
            import django.core.cache as cache
        except ImproperlyConfigured:
            cached = False
        else:
            from six.moves.urllib.parse import quote_plus
            cache_key = "%%%2".join(
                    (CACHE_KEY_ROOT,
                        quote_plus(repo.controldir()), quote_plus(full_name),
                        commit_sha.decode()))

            def_cache = cache.caches["default"]
            result = None
            # Memcache is apparently limited to 250 characters.
            if len(cache_key) < 240:
                result = def_cache.get(cache_key)
            if result is not None:
                return result

    yaml_bytestream = get_repo_blob(
            repo, full_name, commit_sha, allow_tree=False).data
    yaml_text = yaml_bytestream.decode("utf-8")

    if LINE_HAS_INDENTING_TABS_RE.search(yaml_text):
        raise ValueError("File uses tabs in indentation. "
                "This is not allowed.")

    expanded = expand_yaml_macros(
            repo, commit_sha, yaml_bytestream)

    yaml_data = load_yaml(expanded)  # type:ignore
    result = dict_to_struct(yaml_data)

    if cached:
        def_cache.add(cache_key, result, None)

    return result
Example #22
0
    def test_float_matcher_grade_atol(self):
        matcher = FloatMatcher(None, "",
                               dict_to_struct(
                                   {"type": "float",
                                    "value": "1",
                                    "atol": 0.01
                                    }))
        self.assertEqual(matcher.grade(""), 0)
        self.assertEqual(matcher.grade(0), 0)
        self.assertEqual(matcher.grade("abcd"), 0)

        self.assertEqual(matcher.grade(1), 1)
        self.assertEqual(matcher.grade(1.005), 1)
        self.assertEqual(matcher.grade(1.02), 0)

        self.assertEqual(matcher.grade(float("nan")), 0)
        self.assertEqual(matcher.grade(float("inf")), 0)
Example #23
0
    def test_float_matcher_grade_rtol(self):
        matcher = FloatMatcher(None, "",
                               dict_to_struct(
                                   {"type": "float",
                                    "value": "100.1",
                                    "rtol": 0.01
                                    }))
        self.assertEqual(matcher.grade(""), 0)
        self.assertEqual(matcher.grade(0), 0)
        self.assertEqual(matcher.grade("abcd"), 0)

        self.assertEqual(matcher.grade(100), 1)
        self.assertEqual(matcher.grade(100.9), 1)
        self.assertEqual(matcher.grade(101.11), 0)
        self.assertEqual(matcher.correct_answer_text(), str(100.1))

        self.assertEqual(matcher.grade(float("nan")), 0)
        self.assertEqual(matcher.grade(float("inf")), 0)
Example #24
0
    def test_embedded_choice_not_stringifiable(self):
        expected_page_error = (
            "'choice' choice 2: unable to convert to string")

        class BadChoice(object):
            def __str__(self):
                raise Exception

        from relate.utils import dict_to_struct
        fake_page_desc = dict_to_struct(
            {'type': 'InlineMultiQuestion', 'id': 'inlinemulti',
             'prompt':
                 '\n# An InlineMultiQuestion example\n\nComplete the '
                 'following paragraph.\n',
             'question': '\nFoo and [[choice]] are often used in code '
                         'examples.\n',
             '_field_names': [
                 'type', 'id', 'prompt', 'question', 'answers', 'value'],
             'answers': {'_field_names': ['choice'],
                         'choice': {
                             '_field_names': ['type',
                                              'choices'],
                             'type': 'ChoicesAnswer',
                             'choices': [0.2,
                                         BadChoice(),
                                         '~CORRECT~ 0.25']}},
             'value': 10}
        )

        with mock.patch("relate.utils.dict_to_struct") as mock_dict_to_struct:
            mock_dict_to_struct.return_value = fake_page_desc

            markdown = INLINE_MULTI_MARKDOWN_EMBEDDED_CHOICE_QUESTION

            resp = (
                self.get_page_sandbox_preview_response(markdown))
            self.assertEqual(resp.status_code, 200)
            self.assertSandboxNotHasValidPage(resp)
            self.assertResponseContextContains(resp, PAGE_ERRORS,
                                               expected_page_error)
Example #25
0
    def test_choice_not_stringifiable(self):
        expected_page_error = (
            "choice 2: unable to convert to string")

        class BadChoice(object):
            def __str__(self):
                raise Exception

        from relate.utils import dict_to_struct
        fake_page_desc = dict_to_struct(
            {'type': 'MultipleChoiceQuestion', 'id': 'ice_cream_toppings',
             'value': 1, 'shuffle': False,
             'prompt': '# Ice Cream Toppings\nWhich of the following are '
                       'ice cream toppings?\n',
             'choices': ['~CORRECT~ Sprinkles',
                         BadChoice(),
                         'Vacuum cleaner dust', 'Spider webs',
                         '~CORRECT~ Almond bits'],
             'allow_partial_credit': True,
             '_field_names': [
                 'type', 'id', 'value', 'shuffle',
                 'prompt', 'choices',
                 'allow_partial_credit']}
        )

        with mock.patch("relate.utils.dict_to_struct") as mock_dict_to_struct:
            mock_dict_to_struct.return_value = fake_page_desc

            markdown = (MULTIPLE_CHOICES_MARKDWON_NORMAL_PATTERN
                         % {"shuffle": "False",
                            "credit_mode_str": "",
                            "extra_attr": "allow_partial_credit: True"})

            resp = (
                self.get_page_sandbox_preview_response(markdown))
            self.assertEqual(resp.status_code, 200)
            self.assertSandboxNotHasValidPage(resp)
            self.assertResponseContextContains(resp, PAGE_ERRORS,
                                               expected_page_error)
Example #26
0
def view_page_sandbox(pctx):
    if pctx.role not in [participation_role.instructor, participation_role.teaching_assistant]:
        raise PermissionDenied(ugettext("must be instructor or TA to access sandbox"))

    from course.validation import ValidationError
    from relate.utils import dict_to_struct, Struct
    import yaml

    PAGE_SESSION_KEY = "cf_validated_sandbox_page:" + pctx.course.identifier  # noqa
    ANSWER_DATA_SESSION_KEY = "cf_page_sandbox_answer_data:" + pctx.course.identifier  # noqa

    request = pctx.request
    page_source = pctx.request.session.get(PAGE_SESSION_KEY)

    page_errors = None
    page_warnings = None

    is_preview_post = request.method == "POST" and "preview" in request.POST

    from course.models import get_user_status

    ustatus = get_user_status(request.user)

    def make_form(data=None):
        return SandboxForm(
            page_source, "yaml", ustatus.editor_mode, ugettext("Enter YAML markup for a flow page."), data
        )

    if is_preview_post:
        edit_form = make_form(pctx.request.POST)

        if edit_form.is_valid():
            try:
                new_page_source = edit_form.cleaned_data["content"]
                page_desc = dict_to_struct(yaml.load(new_page_source))

                if not isinstance(page_desc, Struct):
                    raise ValidationError(
                        "Provided page source code is not "
                        "a dictionary. Do you need to remove a leading "
                        "list marker ('-') or some stray indentation?"
                    )

                from course.validation import validate_flow_page, ValidationContext

                vctx = ValidationContext(repo=pctx.repo, commit_sha=pctx.course_commit_sha)

                validate_flow_page(vctx, "sandbox", page_desc)

                page_warnings = vctx.warnings

            except:
                import sys

                tp, e, _ = sys.exc_info()

                page_errors = (
                    ugettext("Page failed to load/validate")
                    + ": "
                    + "%(err_type)s: %(err_str)s" % {"err_type": tp.__name__, "err_str": e}
                )

            else:
                # Yay, it did validate.
                request.session[PAGE_SESSION_KEY] = page_source = new_page_source

            del new_page_source

        edit_form = make_form(pctx.request.POST)

    else:
        edit_form = make_form()

    have_valid_page = page_source is not None
    if have_valid_page:
        page_desc = dict_to_struct(yaml.load(page_source))

        from course.content import instantiate_flow_page

        try:
            page = instantiate_flow_page("sandbox", pctx.repo, page_desc, pctx.course_commit_sha)
        except:
            import sys

            tp, e, _ = sys.exc_info()

            page_errors = (
                ugettext("Page failed to load/validate")
                + ": "
                + "%(err_type)s: %(err_str)s" % {"err_type": tp.__name__, "err_str": e}
            )
            have_valid_page = False

    if have_valid_page:
        page_data = page.make_page_data()

        from course.models import FlowSession
        from course.page import PageContext

        page_context = PageContext(
            course=pctx.course,
            repo=pctx.repo,
            commit_sha=pctx.course_commit_sha,
            # This helps code pages retrieve the editor pref.
            flow_session=FlowSession(course=pctx.course, participation=pctx.participation),
            in_sandbox=True,
        )

        title = page.title(page_context, page_data)
        body = page.body(page_context, page_data)

        # {{{ try to recover answer_data

        answer_data = None

        stored_answer_data_tuple = pctx.request.session.get(ANSWER_DATA_SESSION_KEY)

        # Session storage uses JSON and may turn tuples into lists.
        if isinstance(stored_answer_data_tuple, (list, tuple)) and len(stored_answer_data_tuple) == 3:
            stored_answer_data_page_type, stored_answer_data_page_id, stored_answer_data = stored_answer_data_tuple

            if stored_answer_data_page_type == page_desc.type and stored_answer_data_page_id == page_desc.id:
                answer_data = stored_answer_data

        # }}}

        feedback = None
        page_form_html = None

        if page.expects_answer():
            from course.page.base import PageBehavior

            page_behavior = PageBehavior(show_correctness=True, show_answer=True, may_change_answer=True)

            if request.method == "POST" and not is_preview_post:
                page_form = page.process_form_post(page_context, page_data, request.POST, request.FILES, page_behavior)

                if page_form.is_valid():

                    answer_data = page.answer_data(page_context, page_data, page_form, request.FILES)

                    feedback = page.grade(page_context, page_data, answer_data, grade_data=None)

                    pctx.request.session[ANSWER_DATA_SESSION_KEY] = (page_desc.type, page_desc.id, answer_data)

            else:
                page_form = page.make_form(page_context, page_data, answer_data, page_behavior)

            if page_form is not None:
                page_form.helper.add_input(Submit("submit", ugettext("Submit answer"), accesskey="g"))
                page_form_html = page.form_to_html(pctx.request, page_context, page_form, answer_data)

        correct_answer = page.correct_answer(page_context, page_data, answer_data, grade_data=None)

        return render_course_page(
            pctx,
            "course/sandbox-page.html",
            {
                "edit_form": edit_form,
                "page_errors": page_errors,
                "page_warnings": page_warnings,
                "form": edit_form,  # to placate form.media
                "have_valid_page": True,
                "title": title,
                "body": body,
                "page_form_html": page_form_html,
                "feedback": feedback,
                "correct_answer": correct_answer,
            },
        )

    else:

        return render_course_page(
            pctx,
            "course/sandbox-page.html",
            {
                "edit_form": edit_form,
                "form": edit_form,  # to placate form.media
                "have_valid_page": False,
                "page_errors": page_errors,
                "page_warnings": page_warnings,
            },
        )
Example #27
0
def get_session_start_rule(
        course,  # type: Course
        participation,  # type: Optional[Participation]
        flow_id,  # type: Text
        flow_desc,  # type: FlowDesc
        now_datetime,  # type: datetime.datetime
        facilities=None,  # type: Optional[FrozenSet[Text]]
        for_rollover=False,  # type: bool
        login_exam_ticket=None,  # type: Optional[ExamTicket]
        ):
    # type: (...) -> FlowSessionStartRule

    """Return a :class:`FlowSessionStartRule` if a new session is
    permitted or *None* if no new session is allowed.
    """

    if facilities is None:
        facilities = frozenset()

    from relate.utils import dict_to_struct
    rules = get_flow_rules(flow_desc, flow_rule_kind.start,
            participation, flow_id, now_datetime,
            default_rules_desc=[
                dict_to_struct(dict(
                    may_start_new_session=True,
                    may_list_existing_sessions=False))])

    from course.models import FlowSession  # noqa
    for rule in rules:
        if not _eval_generic_conditions(rule, course, participation,
                now_datetime, flow_id=flow_id,
                login_exam_ticket=login_exam_ticket):
            continue

        if not _eval_participation_tags_conditions(rule, participation):
            continue

        if not for_rollover and hasattr(rule, "if_in_facility"):
            if rule.if_in_facility not in facilities:
                continue

        if not for_rollover and hasattr(rule, "if_has_in_progress_session"):
            session_count = FlowSession.objects.filter(
                    participation=participation,
                    course=course,
                    flow_id=flow_id,
                    in_progress=True).count()

            if bool(session_count) != rule.if_has_in_progress_session:
                continue

        if not for_rollover and hasattr(rule, "if_has_session_tagged"):
            tagged_session_count = FlowSession.objects.filter(
                    participation=participation,
                    course=course,
                    access_rules_tag=rule.if_has_session_tagged,
                    flow_id=flow_id).count()

            if not tagged_session_count:
                continue

        if not for_rollover and hasattr(rule, "if_has_fewer_sessions_than"):
            session_count = FlowSession.objects.filter(
                    participation=participation,
                    course=course,
                    flow_id=flow_id).count()

            if session_count >= rule.if_has_fewer_sessions_than:
                continue

        if not for_rollover and hasattr(rule, "if_has_fewer_tagged_sessions_than"):
            tagged_session_count = FlowSession.objects.filter(
                    participation=participation,
                    course=course,
                    access_rules_tag__isnull=False,
                    flow_id=flow_id).count()

            if tagged_session_count >= rule.if_has_fewer_tagged_sessions_than:
                continue

        return FlowSessionStartRule(
                tag_session=getattr(rule, "tag_session", None),
                may_start_new_session=getattr(
                    rule, "may_start_new_session", True),
                may_list_existing_sessions=getattr(
                    rule, "may_list_existing_sessions", True),
                default_expiration_mode=getattr(
                    rule, "default_expiration_mode", None),
                )

    return FlowSessionStartRule(
            may_list_existing_sessions=False,
            may_start_new_session=False)
Example #28
0
def grant_exception_stage_3(pctx, participation_id, flow_id, session_id):
    if pctx.role not in [
            participation_role.instructor,
            participation_role.teaching_assistant
    ]:
        raise PermissionDenied(
            ugettext("must be instructor or TA to grant exceptions"))

    participation = get_object_or_404(Participation, id=participation_id)

    from course.content import get_flow_desc
    try:
        flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id,
                                  pctx.course_commit_sha)
    except ObjectDoesNotExist:
        raise http.Http404()

    session = FlowSession.objects.get(id=int(session_id))

    now_datetime = get_now_or_fake_time(pctx.request)
    from course.utils import (get_session_access_rule,
                              get_session_grading_rule)
    access_rule = get_session_access_rule(session, participation.role,
                                          flow_desc, now_datetime)
    grading_rule = get_session_grading_rule(session, participation.role,
                                            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)

            from course.content import get_flow_desc
            flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id,
                                      pctx.course_commit_sha)
            tags = None
            if hasattr(flow_desc, "rules"):
                tags = getattr(flow_desc.rules, "tags", None)

            # {{{ put together access rule

            new_access_rule = {"permissions": permissions}

            if (form.cleaned_data.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

            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

            if form.cleaned_data["credit_percent"] is not None:
                new_grading_rule["credit_percent"] = \
                        form.cleaned_data["credit_percent"]

            if (form.cleaned_data.get("restrict_to_same_tag")
                    and session.access_rules_tag is not None):
                new_grading_rule["if_has_tag"] = session.access_rules_tag

            if (hasattr(grading_rule, "grade_identifier")
                    and grading_rule.grade_identifier is not None):
                new_grading_rule["grade_identifier"] = \
                        grading_rule.grade_identifier
            else:
                new_grading_rule["grade_identifier"] = None

            if (hasattr(grading_rule, "grade_aggregation_strategy")
                    and grading_rule.grade_aggregation_strategy is not None):
                new_grading_rule["grade_aggregation_strategy"] = \
                        grading_rule.grade_aggregation_strategy

            validate_session_grading_rule(vctx,
                                          ugettext("newly created exception"),
                                          dict_to_struct(new_grading_rule),
                                          tags)

            fre_grading = FlowRuleException(
                flow_id=flow_id,
                participation=participation,
                creator=pctx.request.user,
                comment=form.cleaned_data["comment"],
                kind=flow_rule_kind.grading,
                rule=new_grading_rule)
            fre_grading.save()

            # }}}

            messages.add_message(
                pctx.request, messages.SUCCESS,
                ugettext("Exception granted to '%(participation)s' "
                         "for '%(flow_id)s'.") % {
                             'participation': participation,
                             'flow_id': flow_id
                         })
            return redirect("relate-grant_exception", pctx.course.identifier)

    else:
        data = {
            "restrict_to_same_tag": session.access_rules_tag is not None,
            "credit_percent": grading_rule.credit_percent,
            #"due_same_as_access_expiration": True,
            "due": grading_rule.due,
        }
        for perm in access_rule.permissions:
            data[perm] = True

        form = ExceptionStage3Form(data, flow_desc, session.access_rules_tag)

    return render_course_page(
        pctx, "course/generic-course-form.html", {
            "form": form,
            "form_description": ugettext("Grant Exception"),
            "form_text": string_concat(
                "<div class='well'>",
                ugettext("Granting exception to '%(participation)s' "
                         "for '%(flow_id)s' (session %(session)s)."), "</div>")
            % {
                'participation': participation,
                'flow_id': flow_id,
                'session': strify_session_for_exception(session)
            },
        })
Example #29
0
 def test_parse_matcher_instance_is_struct_no_type_error(self):
     s = dict_to_struct({"value": "20.1"})
     with self.assertRaises(ValidationError) as cm:
         parse_matcher(None, "some where", s)
     self.assertIn("some where: matcher must supply 'type'",
                   str(cm.exception))
validate_sha = "test_validate_sha"

staticpage1_path = "staticpages/spage1.yml"
staticpage1_location = "spage1.yml"
staticpage1_id = "spage1"
staticpage1_desc = mock.MagicMock()

staticpage2_path = "staticpages/spage2.yml"
staticpage2_location = "spage2.yml"
staticpage2_id = "spage2"
staticpage2_desc = mock.MagicMock()

flow1_path = "flows/flow1.yml"
flow1_location = "flow1.yml"
flow1_id = "flow1"
flow1_no_rule_desc = dict_to_struct(load_yaml(FLOW_WITHOUT_RULE_YAML))
flow1_with_access_rule_desc = dict_to_struct(load_yaml(FLOW_WITH_ACCESS_RULE_YAML))

flow2_path = "flows/flow2.yml"
flow2_location = "flow2.yml"
flow2_id = "flow2"
flow2_grade_identifier = "la_quiz"
flow2_default_desc = dict_to_struct(load_yaml(
    FLOW_WITH_GRADING_RULE_YAML_PATTERN % {
        "grade_identifier": flow2_grade_identifier}))

flow3_path = "flows/flow3.yml"
flow3_location = "flow3.yml"
flow3_id = "flow3"
flow3_grade_identifier = "la_quiz2"
flow3_default_desc = dict_to_struct(load_yaml(
Example #31
0
def get_session_start_rule(course, participation, role, flow_id, flow_desc,
        now_datetime, facilities=None, for_rollover=False,
        login_exam_ticket=None):
    """Return a :class:`FlowSessionStartRule` if a new session is
    permitted or *None* if no new session is allowed.
    """

    if facilities is None:
        facilities = frozenset()

    from relate.utils import dict_to_struct
    rules = get_flow_rules(flow_desc, flow_rule_kind.start,
            participation, flow_id, now_datetime,
            default_rules_desc=[
                dict_to_struct(dict(
                    may_start_new_session=True,
                    may_list_existing_sessions=False))])

    from course.models import FlowSession
    for rule in rules:
        if not _eval_generic_conditions(rule, course, role, now_datetime,
                flow_id=flow_id,
                login_exam_ticket=login_exam_ticket):
            continue

        if not for_rollover and hasattr(rule, "if_in_facility"):
            if rule.if_in_facility not in facilities:
                continue

        if not for_rollover and hasattr(rule, "if_has_in_progress_session"):
            session_count = FlowSession.objects.filter(
                    participation=participation,
                    course=course,
                    flow_id=flow_id,
                    in_progress=True).count()

            if bool(session_count) != rule.if_has_in_progress_session:
                continue

        if not for_rollover and hasattr(rule, "if_has_session_tagged"):
            tagged_session_count = FlowSession.objects.filter(
                    participation=participation,
                    course=course,
                    access_rules_tag=rule.if_has_session_tagged,
                    flow_id=flow_id).count()

            if not tagged_session_count:
                continue

        if not for_rollover and hasattr(rule, "if_has_fewer_sessions_than"):
            session_count = FlowSession.objects.filter(
                    participation=participation,
                    course=course,
                    flow_id=flow_id).count()

            if session_count >= rule.if_has_fewer_sessions_than:
                continue

        if not for_rollover and hasattr(rule, "if_has_fewer_tagged_sessions_than"):
            tagged_session_count = FlowSession.objects.filter(
                    participation=participation,
                    course=course,
                    access_rules_tag__isnull=False,
                    flow_id=flow_id).count()

            if tagged_session_count >= rule.if_has_fewer_tagged_sessions_than:
                continue

        return FlowSessionStartRule(
                tag_session=getattr(rule, "tag_session", None),
                may_start_new_session=getattr(
                    rule, "may_start_new_session", True),
                may_list_existing_sessions=getattr(
                    rule, "may_list_existing_sessions", True),
                )

    return FlowSessionStartRule(
            may_list_existing_sessions=False,
            may_start_new_session=False)
Example #32
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_python_run_with_retries(
                run_req, run_timeout=self.page_desc.timeout)
        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, six.string_types)):
                    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(
                            six.text_type(
                                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 runpy 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, six.text_type):
                    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))
Example #33
0
 def test_flow_desc_access_rule_has_no_change_answer_perm(self):
     flow_desc_dict = self.get_hacked_flow_desc(as_dict=True)
     rules = flow_desc_dict["rules"]
     rules.access = [dict_to_struct({"permissions": ["submit_answer"]})]
     flow_desc = dict_to_struct(flow_desc_dict)
     self.assertFalse(analytics.is_flow_multiple_submit(flow_desc))
Example #34
0
def get_session_grading_rule(
        session,  # type: FlowSession
        flow_desc,  # type: FlowDesc
        now_datetime  # type: datetime.datetime
):
    # type: (...) -> FlowSessionGradingRule

    flow_desc_rules = getattr(flow_desc, "rules", None)

    from relate.utils import dict_to_struct
    rules = get_flow_rules(
        flow_desc,
        flow_rule_kind.grading,
        session.participation,
        session.flow_id,
        now_datetime,
        default_rules_desc=[dict_to_struct(dict(generates_grade=False, ))])

    from course.enrollment import get_participation_role_identifiers
    roles = get_participation_role_identifiers(session.course,
                                               session.participation)

    for rule in rules:
        if hasattr(rule, "if_has_role"):
            if all(role not in rule.if_has_role for role in roles):
                continue

        if not _eval_generic_session_conditions(rule, session, now_datetime):
            continue

        if not _eval_participation_tags_conditions(rule,
                                                   session.participation):
            continue

        if hasattr(rule, "if_completed_before"):
            ds = parse_date_spec(session.course, rule.if_completed_before)
            if session.in_progress and now_datetime > ds:
                continue
            if not session.in_progress and session.completion_time > ds:
                continue

        due = parse_date_spec(session.course, getattr(rule, "due", None))
        if due is not None:
            assert due.tzinfo is not None

        generates_grade = getattr(rule, "generates_grade", True)

        grade_identifier = None
        grade_aggregation_strategy = None
        if flow_desc_rules is not None:
            grade_identifier = flow_desc_rules.grade_identifier
            grade_aggregation_strategy = getattr(flow_desc_rules,
                                                 "grade_aggregation_strategy",
                                                 None)

        bonus_points = getattr_with_fallback((rule, flow_desc), "bonus_points",
                                             0)
        max_points = getattr_with_fallback((rule, flow_desc), "max_points",
                                           None)
        max_points_enforced_cap = getattr_with_fallback(
            (rule, flow_desc), "max_points_enforced_cap", None)

        return FlowSessionGradingRule(
            grade_identifier=grade_identifier,
            grade_aggregation_strategy=grade_aggregation_strategy,
            due=due,
            generates_grade=generates_grade,
            description=getattr(rule, "description", None),
            credit_percent=getattr(rule, "credit_percent", 100),
            use_last_activity_as_completion_time=getattr(
                rule, "use_last_activity_as_completion_time", False),
            bonus_points=bonus_points,
            max_points=max_points,
            max_points_enforced_cap=max_points_enforced_cap,
        )

    raise RuntimeError(
        _("grading rule determination was unable to find "
          "a grading rule"))
Example #35
0
def get_session_access_rule(
        session,  # type: FlowSession
        flow_desc,  # type: FlowDesc
        now_datetime,  # type: datetime.datetime
        facilities=None,  # type: Optional[frozenset[Text]]
        login_exam_ticket=None,  # type: Optional[ExamTicket]
):
    # type: (...) -> FlowSessionAccessRule
    """Return a :class:`ExistingFlowSessionRule`` to describe
    how a flow may be accessed.
    """

    if facilities is None:
        facilities = frozenset()

    from relate.utils import dict_to_struct
    rules = get_flow_rules(flow_desc,
                           flow_rule_kind.access,
                           session.participation,
                           session.flow_id,
                           now_datetime,
                           default_rules_desc=[
                               dict_to_struct(
                                   dict(permissions=[flow_permission.view], ))
                           ])  # type: List[FlowSessionAccessRuleDesc]

    for rule in rules:
        if not _eval_generic_conditions(rule,
                                        session.course,
                                        session.participation,
                                        now_datetime,
                                        flow_id=session.flow_id,
                                        login_exam_ticket=login_exam_ticket):
            continue

        if not _eval_participation_tags_conditions(rule,
                                                   session.participation):
            continue

        if not _eval_generic_session_conditions(rule, session, now_datetime):
            continue

        if hasattr(rule, "if_in_facility"):
            if rule.if_in_facility not in facilities:
                continue

        if hasattr(rule, "if_in_progress"):
            if session.in_progress != rule.if_in_progress:
                continue

        if hasattr(rule, "if_expiration_mode"):
            if session.expiration_mode != rule.if_expiration_mode:
                continue

        if hasattr(rule, "if_session_duration_shorter_than_minutes"):
            duration_min = (now_datetime -
                            session.start_time).total_seconds() / 60

            if session.participation is not None:
                duration_min /= float(session.participation.time_factor)

            if duration_min > rule.if_session_duration_shorter_than_minutes:
                continue

        permissions = set(rule.permissions)

        # {{{ deal with deprecated permissions

        if "modify" in permissions:
            permissions.remove("modify")
            permissions.update([
                flow_permission.submit_answer,
                flow_permission.end_session,
            ])

        if "see_answer" in permissions:
            permissions.remove("see_answer")
            permissions.add(flow_permission.see_answer_after_submission)

        # }}}

        # Remove 'modify' permission from not-in-progress sessions
        if not session.in_progress:
            for perm in [
                    flow_permission.submit_answer,
                    flow_permission.end_session,
            ]:
                if perm in permissions:
                    permissions.remove(perm)

        return FlowSessionAccessRule(permissions=frozenset(permissions),
                                     message=getattr(rule, "message", None))

    return FlowSessionAccessRule(permissions=frozenset())
Example #36
0
    def test_get_modified_permissions_for_page(self):
        access_rule_permissions_list = [
            "view", "submit_answer", "end_session", "see_session_time",
            "lock_down_as_exam_session"
        ]
        access_rule_permissions = frozenset(access_rule_permissions_list)

        with self.subTest(access_rules="Not present"):
            page_desc = dict_to_struct({
                "id": "abcd",
                "type": "SomePageType",
            })
            page = PageBase(None, "", page_desc)
            self.assertSetEqual(
                page.get_modified_permissions_for_page(
                    access_rule_permissions), access_rule_permissions)

        with self.subTest(access_rules={}):
            page_desc = dict_to_struct({
                "id": "abcd",
                "type": "SomePageType",
                "access_rules": {}
            })
            page = PageBase(None, "", page_desc)
            self.assertSetEqual(
                page.get_modified_permissions_for_page(
                    access_rule_permissions), access_rule_permissions)

        with self.subTest(access_rules={
                "add_permissions": [],
                "remove_permissions": []
        }):
            page_desc = dict_to_struct({
                "id": "abcd",
                "type": "SomePageType",
                "access_rules": {
                    "add_permissions": [],
                    "remove_permissions": []
                }
            })
            page = PageBase(None, "", page_desc)
            self.assertSetEqual(
                page.get_modified_permissions_for_page(
                    access_rule_permissions), access_rule_permissions)

        with self.subTest(access_rules={
                "add_permissions": ["some_perm"],
                "remove_permissions": []
        }):
            page_desc = dict_to_struct({
                "id": "abcd",
                "type": "SomePageType",
                "access_rules": {
                    "add_permissions": ["some_perm"],
                    "remove_permissions": []
                }
            })
            page = PageBase(None, "", page_desc)
            self.assertSetEqual(
                page.get_modified_permissions_for_page(
                    access_rule_permissions),
                frozenset(access_rule_permissions_list + ["some_perm"]))

        with self.subTest(
                access_rules={"remove_permissions": ["none_exist_perm"]}):
            page_desc = dict_to_struct({
                "id": "abcd",
                "type": "SomePageType",
                "access_rules": {
                    "remove_permissions": ["none_exist_perm"]
                }
            })
            page = PageBase(None, "", page_desc)

            self.assertSetEqual(
                page.get_modified_permissions_for_page(
                    access_rule_permissions), access_rule_permissions)

        with self.subTest(access_rules={
                "remove_permissions": [access_rule_permissions_list[0]]
        }):
            page_desc = dict_to_struct({
                "id": "abcd",
                "type": "SomePageType",
                "access_rules": {
                    "remove_permissions": [access_rule_permissions_list[0]]
                }
            })
            page = PageBase(None, "", page_desc)

            self.assertSetEqual(
                page.get_modified_permissions_for_page(
                    access_rule_permissions),
                frozenset(access_rule_permissions_list[1:]))
Example #37
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")

        if hasattr(self.page_desc, "test_code"):
            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_python_run_with_retries(
                run_req, run_timeout=self.page_desc.timeout)
        except:
            from traceback import format_exc
            response_dict = {
                "result": "uncaught_error",
                "message": "Error connecting to container",
                "traceback": "".join(format_exc()),
            }

        # }}}

        feedback_bits = []

        # {{{ 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, six.string_types)):
                    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)

            with translation.override(settings.RELATE_ADMIN_EMAIL_LOCALE):
                from django.template.loader import render_to_string
                message = render_to_string(
                    "course/broken-code-question-email.txt", {
                        "page_id": self.page_desc.id,
                        "course": page_context.course,
                        "error_message": error_msg,
                    })

                if (not page_context.in_sandbox
                        and not is_nuisance_failure(response_dict)):
                    try:
                        from django.core.mail import send_mail
                        send_mail(
                            "".join(
                                ["[%s] ",
                                 _("code question execution failed")]) %
                            page_context.course.identifier,
                            message,
                            settings.ROBOT_EMAIL_FROM,
                            recipient_list=[page_context.course.notify_email])

                    except Exception:
                        from traceback import format_exc
                        feedback_bits.append(
                            six.text_type(
                                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>")))

        # }}}

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

        bulk_feedback_bits = []
        if hasattr(response, "points"):
            correctness = response.points
            feedback_bits.append("<p><b>%s</b></p>" %
                                 get_auto_feedback(correctness))
        else:
            correctness = None

        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 runpy result: %s" % response.result)

        if hasattr(response, "feedback") and response.feedback:
            feedback_bits.append("".join([
                "<p>",
                _("Here is some feedback on your code"), ":"
                "<ul>%s</ul></p>"
            ]) % "".join("<li>%s</li>" % escape(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:
                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)

        return AnswerFeedback(correctness=correctness,
                              feedback="\n".join(feedback_bits),
                              bulk_feedback="\n".join(bulk_feedback_bits))
Example #38
0
def get_session_grading_rule(
        session,  # type: FlowSession
        flow_desc,  # type: FlowDesc
        now_datetime  # type: datetime.datetime
        ):
    # type: (...) -> FlowSessionGradingRule

    flow_desc_rules = getattr(flow_desc, "rules", None)

    from relate.utils import dict_to_struct
    rules = get_flow_rules(flow_desc, flow_rule_kind.grading,
            session.participation, session.flow_id, now_datetime,
            default_rules_desc=[
                dict_to_struct(dict(
                    generates_grade=False,
                    ))])

    from course.enrollment import get_participation_role_identifiers
    roles = get_participation_role_identifiers(session.course, session.participation)

    for rule in rules:
        if hasattr(rule, "if_has_role"):
            if all(role not in rule.if_has_role for role in roles):
                continue

        if not _eval_generic_session_conditions(rule, session, now_datetime):
            continue

        if not _eval_participation_tags_conditions(rule, session.participation):
            continue

        if hasattr(rule, "if_completed_before"):
            ds = parse_date_spec(session.course, rule.if_completed_before)

            use_last_activity_as_completion_time = False
            if hasattr(rule, "use_last_activity_as_completion_time"):
                use_last_activity_as_completion_time = \
                        rule.use_last_activity_as_completion_time

            if use_last_activity_as_completion_time:
                last_activity = session.last_activity()
                if last_activity is not None:
                    completion_time = last_activity
                else:
                    completion_time = now_datetime
            else:
                if session.in_progress:
                    completion_time = now_datetime
                else:
                    completion_time = session.completion_time

            if completion_time > ds:
                continue

        due = parse_date_spec(session.course, getattr(rule, "due", None))
        if due is not None:
            assert due.tzinfo is not None

        generates_grade = getattr(rule, "generates_grade", True)

        grade_identifier = None
        grade_aggregation_strategy = None
        if flow_desc_rules is not None:
            grade_identifier = flow_desc_rules.grade_identifier
            grade_aggregation_strategy = getattr(
                    flow_desc_rules, "grade_aggregation_strategy", None)

        bonus_points = getattr_with_fallback((rule, flow_desc), "bonus_points", 0)
        max_points = getattr_with_fallback((rule, flow_desc), "max_points", None)
        max_points_enforced_cap = getattr_with_fallback(
                (rule, flow_desc), "max_points_enforced_cap", None)

        grade_aggregation_strategy = cast(Text, grade_aggregation_strategy)

        return FlowSessionGradingRule(
                grade_identifier=grade_identifier,
                grade_aggregation_strategy=grade_aggregation_strategy,
                due=due,
                generates_grade=generates_grade,
                description=getattr(rule, "description", None),
                credit_percent=getattr(rule, "credit_percent", 100),
                use_last_activity_as_completion_time=getattr(
                    rule, "use_last_activity_as_completion_time", False),

                bonus_points=bonus_points,
                max_points=max_points,
                max_points_enforced_cap=max_points_enforced_cap,
                )

    raise RuntimeError(_("grading rule determination was unable to find "
            "a grading rule"))
Example #39
0
validate_sha = "test_validate_sha"

staticpage1_path = "staticpages/spage1.yml"
staticpage1_location = "spage1.yml"
staticpage1_id = "spage1"
staticpage1_desc = mock.MagicMock()

staticpage2_path = "staticpages/spage2.yml"
staticpage2_location = "spage2.yml"
staticpage2_id = "spage2"
staticpage2_desc = mock.MagicMock()

flow1_path = "flows/flow1.yml"
flow1_location = "flow1.yml"
flow1_id = "flow1"
flow1_no_rule_desc = dict_to_struct(load_yaml(FLOW_WITHOUT_RULE_YAML))
flow1_with_access_rule_desc = dict_to_struct(
    load_yaml(FLOW_WITH_ACCESS_RULE_YAML))

flow2_path = "flows/flow2.yml"
flow2_location = "flow2.yml"
flow2_id = "flow2"
flow2_grade_identifier = "la_quiz"
flow2_default_desc = dict_to_struct(
    load_yaml(FLOW_WITH_GRADING_RULE_YAML_PATTERN %
              {"grade_identifier": flow2_grade_identifier}))

flow3_path = "flows/flow3.yml"
flow3_location = "flow3.yml"
flow3_id = "flow3"
flow3_grade_identifier = "la_quiz2"
Example #40
0
def view_page_sandbox(pctx):
    if pctx.role not in [
            participation_role.instructor,
            participation_role.teaching_assistant]:
        raise PermissionDenied(
                ugettext("must be instructor or TA to access sandbox"))

    from course.validation import ValidationError
    from relate.utils import dict_to_struct, Struct
    import yaml

    PAGE_SESSION_KEY = (  # noqa
            "cf_validated_sandbox_page:" + pctx.course.identifier)
    ANSWER_DATA_SESSION_KEY = (  # noqa
        "cf_page_sandbox_answer_data:" + pctx.course.identifier)

    request = pctx.request
    page_source = pctx.request.session.get(PAGE_SESSION_KEY)

    page_errors = None
    page_warnings = None

    is_preview_post = (request.method == "POST" and "preview" in request.POST)

    from course.models import get_user_status
    ustatus = get_user_status(request.user)

    def make_form(data=None):
        return SandboxForm(
                page_source, "yaml", ustatus.editor_mode,
                ugettext("Enter YAML markup for a flow page."),
                data)

    if is_preview_post:
        edit_form = make_form(pctx.request.POST)

        if edit_form.is_valid():
            try:
                new_page_source = edit_form.cleaned_data["content"]
                page_desc = dict_to_struct(yaml.load(new_page_source))

                if not isinstance(page_desc, Struct):
                    raise ValidationError("Provided page source code is not "
                            "a dictionary. Do you need to remove a leading "
                            "list marker ('-') or some stray indentation?")

                from course.validation import validate_flow_page, ValidationContext
                vctx = ValidationContext(
                        repo=pctx.repo,
                        commit_sha=pctx.course_commit_sha)

                validate_flow_page(vctx, "sandbox", page_desc)

                page_warnings = vctx.warnings

            except:
                import sys
                tp, e, _ = sys.exc_info()

                page_errors = (
                        ugettext("Page failed to load/validate")
                        + ": "
                        + "%(err_type)s: %(err_str)s" % {
                            "err_type": tp.__name__, "err_str": e})

            else:
                # Yay, it did validate.
                request.session[PAGE_SESSION_KEY] = page_source = new_page_source

            del new_page_source

        edit_form = make_form(pctx.request.POST)

    else:
        edit_form = make_form()

    have_valid_page = page_source is not None
    if have_valid_page:
        page_desc = dict_to_struct(yaml.load(page_source))

        from course.content import instantiate_flow_page
        try:
            page = instantiate_flow_page("sandbox", pctx.repo, page_desc,
                    pctx.course_commit_sha)
        except:
            import sys
            tp, e, _ = sys.exc_info()

            page_errors = (
                    ugettext("Page failed to load/validate")
                    + ": "
                    + "%(err_type)s: %(err_str)s" % {
                        "err_type": tp.__name__, "err_str": e})
            have_valid_page = False

    if have_valid_page:
        page_data = page.make_page_data()

        from course.models import FlowSession
        from course.page import PageContext
        page_context = PageContext(
                course=pctx.course,
                repo=pctx.repo,
                commit_sha=pctx.course_commit_sha,

                # This helps code pages retrieve the editor pref.
                flow_session=FlowSession(
                    course=pctx.course,
                    participation=pctx.participation),

                in_sandbox=True)

        title = page.title(page_context, page_data)
        body = page.body(page_context, page_data)

        # {{{ try to recover answer_data

        answer_data = None

        stored_answer_data_tuple = \
                pctx.request.session.get(ANSWER_DATA_SESSION_KEY)

        # Session storage uses JSON and may turn tuples into lists.
        if (isinstance(stored_answer_data_tuple, (list, tuple))
                and len(stored_answer_data_tuple) == 3):
            stored_answer_data_page_type, stored_answer_data_page_id, stored_answer_data = \
                    stored_answer_data_tuple

            if (
                    stored_answer_data_page_type == page_desc.type
                    and
                    stored_answer_data_page_id == page_desc.id):
                answer_data = stored_answer_data

        # }}}

        feedback = None
        page_form_html = None

        if page.expects_answer():
            from course.page.base import PageBehavior
            page_behavior = PageBehavior(
                    show_correctness=True,
                    show_answer=True,
                    may_change_answer=True)

            if request.method == "POST" and not is_preview_post:
                page_form = page.process_form_post(page_context, page_data,
                        request.POST, request.FILES,
                        page_behavior)

                if page_form.is_valid():

                    answer_data = page.answer_data(page_context, page_data,
                            page_form, request.FILES)

                    feedback = page.grade(page_context, page_data, answer_data,
                            grade_data=None)

                    pctx.request.session[ANSWER_DATA_SESSION_KEY] = (
                            page_desc.type, page_desc.id, answer_data)

            else:
                page_form = page.make_form(page_context, page_data,
                        answer_data, page_behavior)

            if page_form is not None:
                page_form.helper.add_input(
                        Submit("submit",
                            ugettext("Submit answer"),
                            accesskey="g"))
                page_form_html = page.form_to_html(
                        pctx.request, page_context, page_form, answer_data)

        correct_answer = page.correct_answer(
                page_context, page_data, answer_data,
                grade_data=None)

        return render_course_page(pctx, "course/sandbox-page.html", {
            "edit_form": edit_form,
            "page_errors": page_errors,
            "page_warnings": page_warnings,
            "form": edit_form,  # to placate form.media
            "have_valid_page": True,
            "title": title,
            "body": body,
            "page_form_html": page_form_html,
            "feedback": feedback,
            "correct_answer": correct_answer,
        })

    else:

        return render_course_page(pctx, "course/sandbox-page.html", {
            "edit_form": edit_form,
            "form": edit_form,  # to placate form.media
            "have_valid_page": False,
            "page_errors": page_errors,
            "page_warnings": page_warnings,
        })
Example #41
0
def get_session_access_rule(session, role, flow_desc, now_datetime,
        facilities=None, login_exam_ticket=None):
    """Return a :class:`ExistingFlowSessionRule`` to describe
    how a flow may be accessed.
    """

    if facilities is None:
        facilities = frozenset()

    from relate.utils import dict_to_struct
    rules = get_flow_rules(flow_desc, flow_rule_kind.access,
            session.participation, session.flow_id, now_datetime,
            default_rules_desc=[
                dict_to_struct(dict(
                    permissions=[flow_permission.view],
                    ))])

    for rule in rules:
        if not _eval_generic_conditions(rule, session.course, role, now_datetime,
                flow_id=session.flow_id,
                login_exam_ticket=login_exam_ticket):
            continue

        if not _eval_generic_session_conditions(rule, session, role, now_datetime):
            continue

        if hasattr(rule, "if_in_facility"):
            if rule.if_in_facility not in facilities:
                continue

        if hasattr(rule, "if_in_progress"):
            if session.in_progress != rule.if_in_progress:
                continue

        if hasattr(rule, "if_expiration_mode"):
            if session.expiration_mode != rule.if_expiration_mode:
                continue

        if hasattr(rule, "if_session_duration_shorter_than_minutes"):
            duration_min = (now_datetime - session.start_time).total_seconds() / 60

            if session.participation is not None:
                duration_min /= float(session.participation.time_factor)

            if duration_min > rule.if_session_duration_shorter_than_minutes:
                continue

        permissions = set(rule.permissions)

        # {{{ deal with deprecated permissions

        if "modify" in permissions:
            permissions.remove("modify")
            permissions.update([
                flow_permission.submit_answer,
                flow_permission.end_session,
                ])

        if "see_answer" in permissions:
            permissions.remove("see_answer")
            permissions.add(flow_permission.see_answer_after_submission)

        # }}}

        # Remove 'modify' permission from not-in-progress sessions
        if not session.in_progress:
            for perm in [
                    flow_permission.submit_answer,
                    flow_permission.end_session,
                    ]:
                if perm in permissions:
                    permissions.remove(perm)

        return FlowSessionAccessRule(
                permissions=frozenset(permissions),
                message=getattr(rule, "message", None)
                )

    return FlowSessionAccessRule(permissions=frozenset())
Example #42
0
def get_session_access_rule(session,
                            role,
                            flow_desc,
                            now_datetime,
                            remote_address=None):
    """Return a :class:`ExistingFlowSessionRule`` to describe
    how a flow may be accessed.
    """

    from relate.utils import dict_to_struct
    rules = get_flow_rules(flow_desc,
                           flow_rule_kind.access,
                           session.participation,
                           session.flow_id,
                           now_datetime,
                           default_rules_desc=[
                               dict_to_struct(
                                   dict(permissions=[flow_permission.view], ))
                           ])

    for rule in rules:
        if not _eval_generic_conditions(rule, session.course, role,
                                        now_datetime):
            continue

        if hasattr(rule, "if_in_facility"):
            if not is_address_in_facility(remote_address, rule.if_in_facility):
                continue

        if hasattr(rule, "if_has_tag"):
            if session.access_rules_tag != rule.if_has_tag:
                continue

        if hasattr(rule, "if_in_progress"):
            if session.in_progress != rule.if_in_progress:
                continue

        if hasattr(rule, "if_expiration_mode"):
            if session.expiration_mode != rule.if_expiration_mode:
                continue

        if hasattr(rule, "if_in_facility"):
            if not is_address_in_facility(remote_address, rule.if_in_facility):
                continue

        permissions = set(rule.permissions)

        # {{{ deal with deprecated permissions

        if "modify" in permissions:
            permissions.remove("modify")
            permissions.update([
                flow_permission.submit_answer,
                flow_permission.end_session,
            ])

        if "see_answer" in permissions:
            permissions.remove("see_answer")
            permissions.add(flow_permission.see_answer_after_submission)

        # }}}

        # Remove 'modify' permission from not-in-progress sessions
        if not session.in_progress:
            for perm in [
                    flow_permission.submit_answer,
                    flow_permission.end_session,
            ]:
                if perm in permissions:
                    permissions.remove(perm)

        return FlowSessionAccessRule(permissions=frozenset(permissions),
                                     message=getattr(rule, "message", None))

    return FlowSessionAccessRule(permissions=frozenset())
Example #43
0
def get_session_start_rule(course,
                           participation,
                           role,
                           flow_id,
                           flow_desc,
                           now_datetime,
                           facilities=None,
                           for_rollover=False):
    """Return a :class:`FlowSessionStartRule` if a new session is
    permitted or *None* if no new session is allowed.
    """

    if facilities is None:
        facilities = frozenset()

    from relate.utils import dict_to_struct
    rules = get_flow_rules(flow_desc,
                           flow_rule_kind.start,
                           participation,
                           flow_id,
                           now_datetime,
                           default_rules_desc=[
                               dict_to_struct(
                                   dict(may_start_new_session=True,
                                        may_list_existing_sessions=False))
                           ])

    for rule in rules:
        if not _eval_generic_conditions(rule, course, role, now_datetime):
            continue

        if not for_rollover and hasattr(rule, "if_in_facility"):
            if rule.if_in_facility not in facilities:
                continue

        if not for_rollover and hasattr(rule, "if_has_in_progress_session"):
            session_count = FlowSession.objects.filter(
                participation=participation,
                course=course,
                flow_id=flow_id,
                in_progress=True).count()

            if bool(session_count) != rule.if_has_in_progress_session:
                continue

        if not for_rollover and hasattr(rule, "if_has_session_tagged"):
            tagged_session_count = FlowSession.objects.filter(
                participation=participation,
                course=course,
                access_rules_tag=rule.if_has_session_tagged,
                flow_id=flow_id).count()

            if not tagged_session_count:
                continue

        if not for_rollover and hasattr(rule, "if_has_fewer_sessions_than"):
            session_count = FlowSession.objects.filter(
                participation=participation, course=course,
                flow_id=flow_id).count()

            if session_count >= rule.if_has_fewer_sessions_than:
                continue

        if not for_rollover and hasattr(rule,
                                        "if_has_fewer_tagged_sessions_than"):
            tagged_session_count = FlowSession.objects.filter(
                participation=participation,
                course=course,
                access_rules_tag__isnull=False,
                flow_id=flow_id).count()

            if tagged_session_count >= rule.if_has_fewer_tagged_sessions_than:
                continue

        return FlowSessionStartRule(
            tag_session=getattr(rule, "tag_session", None),
            may_start_new_session=getattr(rule, "may_start_new_session", True),
            may_list_existing_sessions=getattr(rule,
                                               "may_list_existing_sessions",
                                               True),
        )

    return FlowSessionStartRule(may_list_existing_sessions=False,
                                may_start_new_session=False)
Example #44
0
 def test_float_matcher_value_error(self):
     expected_error_msg = "'value' does not provide a valid float literal"
     with self.assertRaises(ValidationError) as cm:
         FloatMatcher(None, "",
                      dict_to_struct({"type": "float", "value": "abcd"}))
     self.assertIn(expected_error_msg, str(cm.exception))
Example #45
0
 def test_parse_validator_no_type(self):
     with self.assertRaises(ValidationError) as cm:
         parse_validator(None, "", dict_to_struct({"id": "abcd"}))
     self.assertIn("matcher must supply 'type'", str(cm.exception))
Example #46
0
def view_page_sandbox(pctx):
    # type: (CoursePageContext) -> http.HttpResponse

    if not pctx.has_permission(pperm.use_page_sandbox):
        raise PermissionDenied()

    from course.validation import ValidationError
    from relate.utils import dict_to_struct, Struct
    import yaml

    page_session_key = make_sandbox_session_key(
        PAGE_SESSION_KEY_PREFIX, pctx.course.identifier)
    answer_data_session_key = make_sandbox_session_key(
        ANSWER_DATA_SESSION_KEY_PREFIX, pctx.course.identifier)
    page_data_session_key = make_sandbox_session_key(
        PAGE_DATA_SESSION_KEY_PREFIX, pctx.course.identifier)

    request = pctx.request
    page_source = pctx.request.session.get(page_session_key)

    page_errors = None
    page_warnings = None

    is_clear_post = (request.method == "POST" and "clear" in request.POST)
    is_clear_response_post = (request.method == "POST"
            and "clear_response" in request.POST)
    is_preview_post = (request.method == "POST" and "preview" in request.POST)

    def make_form(data=None):
        # type: (Optional[Text]) -> PageSandboxForm
        return PageSandboxForm(
                page_source, "yaml", request.user.editor_mode,
                gettext("Enter YAML markup for a flow page."),
                data)

    if is_preview_post:
        edit_form = make_form(pctx.request.POST)
        new_page_source = None

        if edit_form.is_valid():
            form_content = edit_form.cleaned_data["content"]
            try:
                from pytools.py_codegen import remove_common_indentation
                new_page_source = remove_common_indentation(
                        form_content, require_leading_newline=False)
                from course.content import expand_yaml_macros
                new_page_source = expand_yaml_macros(
                        pctx.repo, pctx.course_commit_sha, new_page_source)

                yaml_data = yaml.safe_load(new_page_source)  # type: ignore
                page_desc = dict_to_struct(yaml_data)

                if not isinstance(page_desc, Struct):
                    raise ValidationError("Provided page source code is not "
                            "a dictionary. Do you need to remove a leading "
                            "list marker ('-') or some stray indentation?")

                from course.validation import validate_flow_page, ValidationContext
                vctx = ValidationContext(
                        repo=pctx.repo,
                        commit_sha=pctx.course_commit_sha)

                validate_flow_page(vctx, "sandbox", page_desc)

                page_warnings = vctx.warnings

            except Exception:
                import sys
                tp, e, _ = sys.exc_info()

                page_errors = (
                        gettext("Page failed to load/validate")
                        + ": "
                        + "%(err_type)s: %(err_str)s" % {
                            "err_type": tp.__name__, "err_str": e})  # type: ignore

            else:
                # Yay, it did validate.
                request.session[page_session_key] = page_source = form_content

            del new_page_source
            del form_content

        edit_form = make_form(pctx.request.POST)

    elif is_clear_post:
        page_source = None
        pctx.request.session[page_data_session_key] = None
        pctx.request.session[answer_data_session_key] = None
        del pctx.request.session[page_data_session_key]
        del pctx.request.session[answer_data_session_key]
        edit_form = make_form()

    elif is_clear_response_post:
        page_source = None
        pctx.request.session[page_data_session_key] = None
        pctx.request.session[answer_data_session_key] = None
        del pctx.request.session[page_data_session_key]
        del pctx.request.session[answer_data_session_key]
        edit_form = make_form(pctx.request.POST)

    else:
        edit_form = make_form()

    have_valid_page = page_source is not None
    if have_valid_page:
        yaml_data = yaml.safe_load(page_source)  # type: ignore
        page_desc = cast(FlowPageDesc, dict_to_struct(yaml_data))

        from course.content import instantiate_flow_page
        try:
            page = instantiate_flow_page("sandbox", pctx.repo, page_desc,
                    pctx.course_commit_sha)
        except Exception:
            import sys
            tp, e, _ = sys.exc_info()

            page_errors = (
                    gettext("Page failed to load/validate")
                    + ": "
                    + "%(err_type)s: %(err_str)s" % {
                        "err_type": tp.__name__, "err_str": e})  # type: ignore
            have_valid_page = False

    if have_valid_page:
        page_desc = cast(FlowPageDesc, page_desc)

        # Try to recover page_data, answer_data
        page_data = get_sandbox_data_for_page(
                pctx, page_desc, page_data_session_key)

        answer_data = get_sandbox_data_for_page(
                pctx, page_desc, answer_data_session_key)

        from course.models import FlowSession
        from course.page import PageContext
        page_context = PageContext(
                course=pctx.course,
                repo=pctx.repo,
                commit_sha=pctx.course_commit_sha,

                # This helps code pages retrieve the editor pref.
                flow_session=FlowSession(
                    course=pctx.course,
                    participation=pctx.participation),

                in_sandbox=True)

        if page_data is None:
            page_data = page.initialize_page_data(page_context)
            pctx.request.session[page_data_session_key] = (
                    page_desc.type, page_desc.id, page_data)

        title = page.title(page_context, page_data)
        body = page.body(page_context, page_data)

        feedback = None
        page_form_html = None

        if page.expects_answer():
            from course.page.base import PageBehavior
            page_behavior = PageBehavior(
                    show_correctness=True,
                    show_answer=True,
                    may_change_answer=True)

            if request.method == "POST" and not is_preview_post:
                page_form = page.process_form_post(page_context, page_data,
                        request.POST, request.FILES,
                        page_behavior)

                if page_form.is_valid():

                    answer_data = page.answer_data(page_context, page_data,
                            page_form, request.FILES)

                    feedback = page.grade(page_context, page_data, answer_data,
                            grade_data=None)

                    pctx.request.session[answer_data_session_key] = (
                            page_desc.type, page_desc.id, answer_data)

            else:
                try:
                    page_form = page.make_form(page_context, page_data,
                            answer_data, page_behavior)

                except Exception:
                    import sys
                    tp, e, _ = sys.exc_info()

                    page_errors = (
                            gettext("Page failed to load/validate "
                                "(change page ID to clear faults)")
                            + ": "
                            + "%(err_type)s: %(err_str)s" % {
                                "err_type": tp.__name__, "err_str": e})  # type: ignore  # noqa: E501

                    page_form = None

            if page_form is not None:
                page_form.helper.add_input(
                        Submit("submit",
                            gettext("Submit answer"),
                            accesskey="g"))
                page_form_html = page.form_to_html(
                        pctx.request, page_context, page_form, answer_data)

        correct_answer = page.correct_answer(
                page_context, page_data, answer_data,
                grade_data=None)

        have_valid_page = have_valid_page and not page_errors

        return render_course_page(pctx, "course/sandbox-page.html", {
            "edit_form": edit_form,
            "page_errors": page_errors,
            "page_warnings": page_warnings,
            "form": edit_form,  # to placate form.media
            "have_valid_page": have_valid_page,
            "title": title,
            "body": body,
            "page_form_html": page_form_html,
            "feedback": feedback,
            "correct_answer": correct_answer,
        })

    else:

        return render_course_page(pctx, "course/sandbox-page.html", {
            "edit_form": edit_form,
            "form": edit_form,  # to placate form.media
            "have_valid_page": have_valid_page,
            "page_errors": page_errors,
            "page_warnings": page_warnings,
        })