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)
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
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)
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)
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)})
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)
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
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
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))
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")
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])
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))
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))
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])
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)
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)
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"))
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))
def clean(self): super(FlowRuleException, self).clean() if (self.kind == flow_rule_kind.grading and self.expiration is not None): raise ValidationError(_("grading rules may not expire")) from course.validation import ( ValidationError as ContentValidationError, validate_session_start_rule, validate_session_access_rule, validate_session_grading_rule, ValidationContext) from course.content import (get_course_repo, get_course_commit_sha, get_flow_desc) from relate.utils import dict_to_struct rule = dict_to_struct(self.rule) repo = get_course_repo(self.participation.course) commit_sha = get_course_commit_sha( self.participation.course, self.participation) ctx = ValidationContext( repo=repo, commit_sha=commit_sha) flow_desc = get_flow_desc(repo, self.participation.course, self.flow_id, commit_sha) tags = None if hasattr(flow_desc, "rules"): tags = getattr(flow_desc.rules, "tags", None) try: if self.kind == flow_rule_kind.start: validate_session_start_rule(ctx, unicode(self), rule, tags) elif self.kind == flow_rule_kind.access: validate_session_access_rule(ctx, unicode(self), rule, tags) elif self.kind == flow_rule_kind.grading: validate_session_grading_rule(ctx, unicode(self), rule, tags) else: # the rule refers to FlowRuleException rule raise ValidationError(_("invalid rule kind: ")+self.kind) except ContentValidationError as e: # the rule refers to FlowRuleException rule raise ValidationError(_("invalid existing_session_rules: ")+str(e))
def 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
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)
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)
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)
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)
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, }, )
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)
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) }, })
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(
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)
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))
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))
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"))
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())
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:]))
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))
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"))
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"
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, })
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())
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())
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)
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))
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))
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, })