def __init__(self, vctx, location, page_desc): super(FileUploadQuestion, self).__init__(vctx, location, page_desc) if not (set(page_desc.mime_types) <= set(self.ALLOWED_MIME_TYPES)): raise ValidationError( string_concat( location, ": ", _("unrecognized mime types"), " '%(presenttype)s'") % { 'presenttype': ", ".join( set(page_desc.mime_types) - set(self.ALLOWED_MIME_TYPES))}) if page_desc.maximum_megabytes <= 0: raise ValidationError( string_concat( location, ": ", _("'maximum_megabytes' expects a positive value, " "got %(value)s instead") % {'value': str(page_desc.maximum_megabytes)})) if vctx is not None: if not hasattr(page_desc, "value"): vctx.add_warning(location, _("upload question does not have " "assigned point value"))
def __init__(self, vctx, location, page_desc): super(ChoiceQuestion, self).__init__(vctx, location, page_desc) if self.correct_choice_count < 1: raise ValidationError( string_concat( "%(location)s: ", _("one or more correct answer(s) " "expected, %(n_correct)d found")) % { 'location': location, 'n_correct': self.correct_choice_count}) if self.disregard_choice_count: raise ValidationError( string_concat( "%(location)s: ", _("ChoiceQuestion does not allow any choices " "marked 'disregard'")) % {'location': location}) if self.always_correct_choice_count: raise ValidationError( string_concat( "%(location)s: ", _("ChoiceQuestion does not allow any choices " "marked 'always_correct'")) % {'location': location})
def get_auto_feedback(correctness): # type: (Optional[float]) -> Text correctness = validate_point_count(correctness) if correctness is None: return six.text_type( ugettext_noop("No information on correctness of answer.")) if correctness == 0: return six.text_type(ugettext_noop("Your answer is not correct.")) elif correctness == 1: return six.text_type(ugettext_noop("Your answer is correct.")) elif correctness > 1: return six.text_type( string_concat( ugettext_noop( "Your answer is correct and earned bonus points."), " (%.1f %%)") % (100*correctness)) elif correctness > 0.5: return six.text_type( string_concat( ugettext_noop("Your answer is mostly correct."), " (%.1f %%)") % (100*correctness)) else: return six.text_type( string_concat( ugettext_noop("Your answer is somewhat correct. "), "(%.1f%%)") % (100*correctness))
def __init__(self, vctx, location, page_desc): super(TextQuestion, self).__init__(vctx, location, page_desc) if len(page_desc.answers) == 0: raise ValidationError( string_concat( "%s: ", _("at least one answer must be provided")) % location) self.matchers = [ parse_matcher( vctx, "%s, answer %d" % (location, i+1), answer) for i, answer in enumerate(page_desc.answers)] if not any(matcher.correct_answer_text() is not None for matcher in self.matchers): raise ValidationError( string_concat( "%s: ", _("no matcher is able to provide a plain-text " "correct answer")) % location)
def get_matcher_class(location, matcher_type, pattern_type): for matcher_class in TEXT_ANSWER_MATCHER_CLASSES: if matcher_class.type == matcher_type: if matcher_class.pattern_type != pattern_type: raise ValidationError( string_concat( "%(location)s: ", # Translators: a "matcher" is used to determine # if the answer to text question (blank filling # question) is correct. _("%(matcherclassname)s only accepts " "'%(matchertype)s' patterns")) % { 'location': location, 'matcherclassname': matcher_class.__name__, 'matchertype': matcher_class.pattern_type}) return matcher_class raise ValidationError( string_concat( "%(location)s: ", _("unknown match type '%(matchertype)s'")) % { 'location': location, 'matchertype': matcher_type})
def __init__(self, vctx, location, page_desc): super(MultipleChoiceQuestion, self).__init__(vctx, location, page_desc) pd = self.page_desc if hasattr(pd, "credit_mode"): credit_mode = pd.credit_mode if ( hasattr(pd, "allow_partial_credit") or hasattr(pd, "allow_partial_credit_subset_only")): raise ValidationError( string_concat( "%(location)s: ", _("'allow_partial_credit' or " "'allow_partial_credit_subset_only' may not be specified" "at the same time as 'credit_mode'")) % {'location': location}) else: partial = getattr(pd, "allow_partial_credit", False) partial_subset = getattr(pd, "allow_partial_credit_subset_only", False) if not partial and not partial_subset: credit_mode = "exact" elif partial and not partial_subset: credit_mode = "proportional" elif not partial and partial_subset: credit_mode = "proportional_correct" else: assert partial and partial_subset raise ValidationError( string_concat( "%(location)s: ", _("'allow_partial_credit' and " "'allow_partial_credit_subset_only' are not allowed to " "coexist when both attribute are 'True'")) % {'location': location}) if credit_mode not in [ "exact", "proportional", "proportional_correct"]: raise ValidationError( string_concat( "%(location)s: ", _("unrecognized credit_mode '%(credit_mode)s'")) % {'location': location, "credit_mode": credit_mode}) if vctx is not None and not hasattr(pd, "credit_mode"): vctx.add_warning(location, _("'credit_mode' will be required on multi-select choice " "questions in a future version. set " "'credit_mode: {}' to match current behavior.") .format(credit_mode)) self.credit_mode = credit_mode
def __init__(self, vctx, location, page_desc): super(PythonCodeQuestionWithHumanTextFeedback, self).__init__( vctx, location, page_desc) if vctx is not None: if ( hasattr(self.page_desc, "human_feedback_value") and hasattr(self.page_desc, "human_feedback_percentage")): raise ValidationError( string_concat( "%(location)s: ", _("'human_feedback_value' and " "'human_feedback_percentage' are not " "allowed to coexist")) % {'location': location} ) if not (hasattr(self.page_desc, "human_feedback_value") or hasattr(self.page_desc, "human_feedback_percentage")): raise ValidationError( string_concat( "%(location)s: ", _("expecting either 'human_feedback_value' " "or 'human_feedback_percentage', found neither.")) % {'location': location} ) if hasattr(self.page_desc, "human_feedback_value"): vctx.add_warning( location, _("Used deprecated 'human_feedback_value' attribute--" "use 'human_feedback_percentage' instead.")) if self.page_desc.value == 0: raise ValidationError("".join([ "%s: ", _("'human_feedback_value' attribute is not allowed " "if value of question is 0, use " "'human_feedback_percentage' instead")]) % location) if self.page_desc.human_feedback_value > self.page_desc.value: raise ValidationError("".join([ "%s: ", _("human_feedback_value greater than overall " "value of question")]) % location) if hasattr(self.page_desc, "human_feedback_percentage"): if not ( 0 <= self.page_desc.human_feedback_percentage <= 100): raise ValidationError("".join([ "%s: ", _("the value of human_feedback_percentage " "must be between 0 and 100")]) % location) if hasattr(self.page_desc, "human_feedback_value"): self.human_feedback_percentage = ( self.page_desc.human_feedback_value * 100 / self.page_desc.value) else: self.human_feedback_percentage = ( self.page_desc.human_feedback_percentage)
def __init__(self, vctx, location, name, answers_desc): super(ChoicesAnswer, self).__init__( vctx, location, name, answers_desc) validate_struct( vctx, location, answers_desc, required_attrs=( ("type", str), ("choices", list) ), allowed_attrs=( ("weight", (int, float)), ("hint", str), ("hint_title", str), ("required", bool), ), ) self.weight = getattr(answers_desc, "weight", 0) correct_choice_count = 0 for choice_idx, choice in enumerate(answers_desc.choices): try: choice = str(choice) except Exception: raise ValidationError( string_concat( "%(location)s: '%(answer_name)s' ", _("choice %(idx)d: unable to convert to string") ) % {'location': location, 'answer_name': self.name, 'idx': choice_idx+1}) if choice.startswith(self.CORRECT_TAG): correct_choice_count += 1 if vctx is not None: validate_markup(vctx, location, remove_prefix(self.CORRECT_TAG, choice)) if correct_choice_count < 1: raise ValidationError( string_concat( "%(location)s: ", _("one or more correct answer(s) expected " " for question '%(question_name)s', " "%(n_correct)d found")) % { 'location': location, 'question_name': self.name, 'n_correct': correct_choice_count}) self.hint = getattr(self.answers_desc, "hint", "") self.width = 0
def csv_data_importable(file_contents, column_idx_list, header_count): import csv spamreader = csv.reader(file_contents) n_header_row = 0 try: if six.PY2: row0 = spamreader.next() else: row0 = spamreader.__next__() except Exception as e: err_msg = type(e).__name__ err_str = str(e) if err_msg == "Error": err_msg = "" else: err_msg += ": " err_msg += err_str if "line contains NULL byte" in err_str: err_msg = err_msg.rstrip(".") + ". " err_msg += _("Are you sure the file is a CSV file other " "than a Microsoft Excel file?") return False, ( string_concat( pgettext_lazy("Starting of Error message", "Error"), ": %s" % err_msg)) from itertools import chain for row in chain([row0], spamreader): n_header_row += 1 if n_header_row <= header_count: continue try: for column_idx in column_idx_list: if column_idx is not None: six.text_type(get_col_contents_or_empty(row, column_idx-1)) except UnicodeDecodeError: return False, ( _("Error: Columns to be imported contain " "non-ASCII characters. " "Please save your CSV file as utf-8 encoded " "and import again.") ) except Exception as e: return False, ( string_concat( pgettext_lazy("Starting of Error message", "Error"), ": %(err_type)s: %(err_str)s") % { "err_type": type(e).__name__, "err_str": str(e)} ) return True, ""
def parse_matcher_string(vctx, location, matcher_desc): match = MATCHER_RE.match(matcher_desc) if match is not None: matcher_type = match.group(1) pattern = match.group(2) else: match = MATCHER_RE_2.match(matcher_desc) if match is None: raise ValidationError( string_concat( "%s: ", _("does not specify match type")) % location) matcher_type = match.group(1) pattern = match.group(2) if vctx is not None: vctx.add_warning(location, _("uses deprecated 'matcher:answer' style")) return (get_matcher_class(location, matcher_type, "string") (vctx, location, pattern))
def make_time_histogram(pctx, flow_id): qset = FlowSession.objects.filter( course=pctx.course, flow_id=flow_id) from relate.utils import string_concat hist = Histogram( num_log_bins=True, num_bin_title_formatter=( lambda minutes: string_concat( "$>$ %.1f ", pgettext("Minute (time unit)", "min")) % minutes)) for session in qset: if session.in_progress: hist.add_data_point( "".join(["<", pgettext("Status of session", "in progress"), ">"])) else: delta = session.completion_time - session.start_time minutes = delta.total_seconds() / 60 hist.add_data_point(minutes) return hist
def __init__(self, vctx, location, page_desc): super(PythonCodeQuestion, self).__init__(vctx, location, page_desc) if vctx is not None and hasattr(page_desc, "data_files"): for data_file in page_desc.data_files: try: if not isinstance(data_file, str): raise ObjectDoesNotExist() from course.content import get_repo_blob get_repo_blob(vctx.repo, data_file, vctx.commit_sha) except ObjectDoesNotExist: raise ValidationError( string_concat( "%(location)s: ", _("data file '%(file)s' not found")) % {"location": location, "file": data_file}) if not getattr(page_desc, "single_submission", False) and vctx is not None: is_multi_submit = False if hasattr(page_desc, "access_rules"): if hasattr(page_desc.access_rules, "add_permissions"): if (flow_permission.change_answer in page_desc.access_rules.add_permissions): is_multi_submit = True if not is_multi_submit: vctx.add_warning(location, _("code question does not explicitly " "allow multiple submission. Either add " "access_rules/add_permssions/change_answer " "or add 'single_submission: True' to confirm that you intend " "for only a single submission to be allowed. " "While you're at it, consider adding " "access_rules/add_permssions/see_correctness."))
def __init__(self, vctx, location, page_desc): super(PageBaseWithTitle, self).__init__(vctx, location, page_desc) title = None try: title = self.page_desc.title except AttributeError: pass if title is None: try: md_body = self.markup_body_for_title() except NotImplementedError: from warnings import warn warn(_("PageBaseWithTitle subclass '%s' does not implement " "markdown_body_for_title()") % type(self).__name__) else: from course.content import extract_title_from_markup title = extract_title_from_markup(md_body) if title is None: raise ValidationError( string_concat( "%s: ", _("no title found in body or title attribute")) % (location)) self._title = title
def __init__(self, vctx, location, pattern): self.pattern = pattern try: self.pattern_sym = parse_sympy(pattern) except ImportError: tp, e, _ = sys.exc_info() if vctx is not None: vctx.add_warning( location, string_concat( "%(location)s: ", _("unable to check symbolic expression"), "(%(err_type)s: %(err_str)s)") % { 'location': location, "err_type": tp.__name__, "err_str": str(e) }) except Exception: tp, e, _ = sys.exc_info() raise ValidationError( "%(location)s: %(err_type)s: %(err_str)s" % { "location": location, "err_type": tp.__name__, "err_str": str(e) })
def __init__(self, vctx, location, page_desc): super(PageBaseWithValue, self).__init__(vctx, location, page_desc) if vctx is not None: if hasattr(page_desc, "value") and self.is_optional_page: raise ValidationError( string_concat( location, _("Attribute 'value' should be removed when " "'is_optional_page' is True."))) if hasattr(page_desc, "value") and page_desc.value < 0: raise ValidationError( string_concat( location, _("Attribute 'value' expects a non-negative value, " "got %s instead") % str(page_desc.value)))
def get_length_attr_em(location, width_attr): # type: (Text, Text) -> Optional[float] """ generate the length for input box, the unit is 'em' """ if width_attr is None: return None if isinstance(width_attr, (int, float)): return width_attr width_re_match = WIDTH_STR_RE.match(width_attr) if width_re_match: length_value = width_re_match.group(1) length_unit = width_re_match.group(2) else: raise ValidationError( string_concat( "%(location)s: ", _("unrecogonized width attribute string: " "'%(width_attr)s'")) % { "location": location, "width_attr": width_attr }) if length_unit not in ALLOWED_LENGTH_UNIT: raise ValidationError( string_concat( "%(location)s: ", _("unsupported length unit '%(length_unit)s', " "expected length unit can be " "%(allowed_length_unit)s", )) % { "location": location, "length_unit": length_unit, "allowed_length_unit": ", ".join( ["'" + item + "'" for item in ALLOWED_LENGTH_UNIT]) }) if length_unit == "%": return float(length_value)*DEFAULT_WIDTH/100.0 else: return float(length_value)/cast(float, EM_LEN_DICT[length_unit])
def parse_validator(vctx, location, validator_desc): if not isinstance(validator_desc, Struct): raise ValidationError( string_concat( "%s: ", _("must be struct or string")) % location) if not hasattr(validator_desc, "type"): raise ValidationError( string_concat( "%s: ", "matcher must supply 'type'") % location) return (get_validator_class(location, validator_desc.type) (vctx, location, validator_desc))
def correct_answer(self, page_context, page_data, answer_data, grade_data): corr_idx_list = self.unpermuted_correct_indices() always_correct_idx_list = self.unpermuted_always_correct_indices() result = (string_concat(_("The correct answer is"), ": %s") % self.get_answer_html(page_context, corr_idx_list)) if len(always_correct_idx_list) > 0: result = (string_concat(result, string_concat(_("Additional acceptable options are"), ": %s") % self.get_answer_html(page_context, always_correct_idx_list))) if hasattr(self.page_desc, "answer_explanation"): result += markup_to_html(page_context, self.page_desc.answer_explanation) return result
def __init__(self, vctx, location, page_desc): super(PageBaseWithValue, self).__init__(vctx, location, page_desc) if vctx is not None: if hasattr(page_desc, "value") and self.is_optional_page: raise ValidationError( string_concat( location, _("Attribute 'value' should be removed when " "'is_optional_page' is True.")))
def get_page_id(self, obj): if obj.page_data.page_ordinal is None: return string_concat("%s/%s (", _("not in use"), ")") % ( obj.page_data.group_id, obj.page_data.page_id) else: return "%s/%s (%s)" % ( obj.page_data.group_id, obj.page_data.page_id, obj.page_data.page_ordinal)
def parse_question(vctx, location, name, answers_desc): if isinstance(answers_desc, Struct): return (get_question_class(location, answers_desc.type, answers_desc) (vctx, location, name, answers_desc)) else: raise ValidationError( string_concat( "%s: ", _("Embedded question '%s' must be a struct" % name)) % location)
def get_default_option(): # type: () -> Tuple[Text, Text] # For the default language used, if USE_I18N is True, display # "Disabled". Otherwise display its lang info. if not settings.USE_I18N: formatted_descr = ( get_formatted_options(settings.LANGUAGE_CODE, None)[1]) else: formatted_descr = _("disabled (i.e., displayed language is " "determined by user's browser preference)") return "", string_concat("%s: " % _("Default"), formatted_descr)
def get_validator_class(location, validator_type): for validator_class in TEXT_ANSWER_VALIDATOR_CLASSES: if validator_class.type == validator_type: return validator_class raise ValidationError( string_concat( "%(location)s: ", _("unknown validator type"), "'%(type)s'") % {'location': location, 'type': validator_type})
def parse_matcher(vctx, location, matcher_desc): if isinstance(matcher_desc, six.string_types): return parse_matcher_string(vctx, location, matcher_desc) else: if not isinstance(matcher_desc, Struct): raise ValidationError( string_concat( "%s: ", _("must be struct or string")) % location) if not hasattr(matcher_desc, "type"): raise ValidationError( string_concat( "%s: ", _("matcher must supply 'type'")) % location) return (get_matcher_class(location, matcher_desc.type, "struct") (vctx, location, matcher_desc))
def correct_answer(self, page_context, page_data, answer_data, grade_data): corr_idx = self.unpermuted_correct_indices()[0] result = (string_concat(_("A correct answer is"), ": '%s'.") % self.process_choice_string( page_context, self.choices[corr_idx].text)) if hasattr(self.page_desc, "answer_explanation"): result += markup_to_html(page_context, self.page_desc.answer_explanation) return result
def __init__(self, vctx, location, page_desc): super(TextQuestion, self).__init__(vctx, location, page_desc) if len(page_desc.answers) == 0: raise ValidationError( string_concat("%s: ", _("at least one answer must be provided")) % location) self.matchers = [ parse_matcher(vctx, "%s, answer %d" % (location, i + 1), answer) for i, answer in enumerate(page_desc.answers) ] if not any(matcher.correct_answer_text() is not None for matcher in self.matchers): raise ValidationError( string_concat( "%s: ", _("no matcher is able to provide a plain-text " "correct answer")) % location)
def get_question_class(location, q_type, answers_desc): for question_class in ALLOWED_EMBEDDED_QUESTION_CLASSES: if question_class.type == q_type: return question_class else: raise ValidationError( string_concat( "%(location)s: ", _("unknown embedded question type '%(type)s'")) % { 'location': location, 'type': q_type})
def get_formatted_options(lang_code, lang_descr): # type: (Text, Optional[Text]) -> Tuple[Text, Text] if lang_descr is None: lang_descr = OrderedDict(settings.LANGUAGES).get(lang_code) if lang_descr is None: try: lang_info = translation.get_language_info(lang_code) lang_descr = lang_info["name_translated"] except KeyError: return (lang_code.strip(), lang_code) return (lang_code.strip(), string_concat(_(lang_descr), " (%s)" % lang_code))
def get_user(self, obj): from django.urls import reverse from django.conf import settings return string_concat( "<a href='%(link)s'>", "%(user_fullname)s", "</a>") % { "link": reverse("admin:%s_change" % settings.AUTH_USER_MODEL.replace(".", "_").lower(), args=(obj.user.id, )), "user_fullname": obj.user.get_full_name(force_verbose_blank=True), }
class ParticipationQueryForm(StyledForm): queries = forms.CharField( required=True, widget=forms.Textarea, help_text=string_concat( _("Enter queries, one per line."), " ", _("Allowed"), ": ", "<code>and</code>, " "<code>or</code>, " "<code>not</code>, " "<code>id:1234</code>, " "<code>email:[email protected]</code>, " "<code>email-contains:abc</code>, " "<code>username:abc</code>, " "<code>username-contains:abc</code>, " "<code>institutional-id:2015abcd</code>, " "<code>institutional-id-contains:2015</code>, " "<code>tagged:abc</code>, " "<code>role:instructor|teaching_assistant|" "student|observer|auditor</code>, " "<code>status:requested|active|dropped|denied</code>|" "<code>has-started:flow_id</code>|" "<code>has-submitted:flow_id</code>."), label=_("Queries")) op = forms.ChoiceField(choices=( ("apply_tag", _("Apply tag")), ("remove_tag", _("Remove tag")), ("drop", _("Drop")), ), label=_("Operation"), required=True) tag = forms.CharField(label=_("Tag"), help_text=_("Tag to apply or remove"), required=False) def __init__(self, *args, **kwargs): super(ParticipationQueryForm, self).__init__(*args, **kwargs) self.helper.add_input(Submit("list", _("List"))) self.helper.add_input(Submit("apply", _("Apply operation"))) def clean_tag(self): tag = self.cleaned_data.get("tag") if tag: import re name_valid_re = re.compile(NAME_VALID_REGEX) if name_valid_re.match(tag) is None: self.add_error("tag", _("Name contains invalid characters.")) return tag
def __init__(self, vctx, location, page_desc): super(TextQuestionBase, self).__init__(vctx, location, page_desc) widget = TextAnswerForm.get_text_widget(getattr( page_desc, "widget", None), check_only=True) if widget is None: raise ValidationError( string_concat("%(location)s: ", _("unrecognized widget type"), "'%(type)s'") % { 'location': location, 'type': getattr(page_desc, "widget") })
def get_auto_feedback(correctness): # type: (Optional[float]) -> Text correctness = validate_point_count(correctness) if correctness is None: return six.text_type(_("No information on correctness of answer.")) if correctness == 0: return six.text_type(_("Your answer is not correct.")) elif correctness == 1: return six.text_type(_("Your answer is correct.")) elif correctness > 1: return six.text_type( string_concat(_("Your answer is correct and earned bonus points."), " (%.1f %%)") % (100 * correctness)) elif correctness > 0.5: return six.text_type( string_concat(_("Your answer is mostly correct."), " (%.1f %%)") % (100 * correctness)) else: return six.text_type( string_concat(_("Your answer is somewhat correct. "), "(%.1f%%)") % (100 * correctness))
def __init__(self, vctx, location, pattern): try: self.pattern = re.compile(pattern, self.re_flags) except Exception: tp, e, __ = sys.exc_info() raise ValidationError( string_concat("%(location)s: ", _("regex '%(pattern)s' did not compile"), ": %(err_type)s: %(err_str)s") % { "location": location, "pattern": pattern, "err_type": tp.__name__, "err_str": str(e) })
def grade( self, page_context, # type: PageContext page_data, # type: Any answer_data, # type: Any grade_data, # type: Any ): # type: (...) -> Optional[AnswerFeedback] """This method is appropriate if the grade consists *only* of the feedback provided by humans. If more complicated/combined feedback is desired, a subclass would likely override this. """ if answer_data is None: return AnswerFeedback(correctness=0, feedback=ugettext_noop("No answer provided.")) if grade_data is None: return None if not grade_data["released"]: return None if (grade_data["grade_percent"] is not None or grade_data["feedback_text"]): if grade_data["grade_percent"] is not None: correctness = grade_data["grade_percent"]/100 feedback_text = "<p>%s</p>" % get_auto_feedback(correctness) else: correctness = None feedback_text = "" if grade_data["feedback_text"]: feedback_text += ( string_concat( "<p>", _("The following feedback was provided"), ":<p>") + markup_to_html( page_context, grade_data["feedback_text"], use_jinja=False)) return AnswerFeedback( correctness=correctness, feedback=feedback_text) else: return None
def __init__(self, vctx, location, page_desc): super(SurveyChoiceQuestion, self).__init__(vctx, location, page_desc) for choice_idx, choice in enumerate(page_desc.choices): try: choice = str(choice) except Exception: raise ValidationError( string_concat( "%(location)s, ", _("choice %(idx)d: unable to convert to string") ) % {"location": location, "idx": choice_idx+1}) if vctx is not None: validate_markup(vctx, location, choice)
def __init__(self, vctx, location, pattern): try: self.pattern = re.compile(pattern, self.re_flags) except Exception: tp, e, _ = sys.exc_info() raise ValidationError( string_concat( "%(location)s: ", _("regex '%(pattern)s' did not compile"), ": %(err_type)s: %(err_str)s") % { "location": location, "pattern": pattern, "err_type": tp.__name__, "err_str": str(e)})
def __init__(self, vctx, location, page_desc): super(TextQuestionBase, self).__init__(vctx, location, page_desc) widget = TextAnswerForm.get_text_widget( getattr(page_desc, "widget", None), check_only=True) if widget is None: raise ValidationError( string_concat( "%(location)s: ", _("unrecognized widget type"), "'%(type)s'") % { 'location': location, 'type': getattr(page_desc, "widget")})
def send_enrollment_decision(participation, approved, request=None): # type: (Participation, bool, http.HttpRequest) -> None with translation.override(settings.RELATE_ADMIN_EMAIL_LOCALE): course = participation.course if request: course_uri = request.build_absolute_uri( reverse("relate-course_page", args=(course.identifier,))) else: # This will happen when this method is triggered by # a model signal which doesn't contain a request object. from six.moves.urllib.parse import urljoin course_uri = urljoin(getattr(settings, "RELATE_BASE_URL"), course.get_absolute_url()) from relate.utils import render_email_template message = render_email_template("course/enrollment-decision-email.txt", { "user": participation.user, "approved": approved, "course": course, "course_uri": course_uri }) from django.core.mail import EmailMessage email_kwargs = {} if settings.RELATE_EMAIL_SMTP_ALLOW_NONAUTHORIZED_SENDER: from_email = course.get_from_email() else: from_email = getattr(settings, "ENROLLMENT_EMAIL_FROM", settings.ROBOT_EMAIL_FROM) from relate.utils import get_outbound_mail_connection email_kwargs.update( {"connection": ( get_outbound_mail_connection("enroll") if hasattr(settings, "ENROLLMENT_EMAIL_FROM") else get_outbound_mail_connection("robot"))}) msg = EmailMessage( string_concat("[%s] ", _("Your enrollment request")) % course.identifier, message, from_email, [participation.user.email], **email_kwargs) msg.bcc = [course.notify_email] msg.send()
def __init__(self, flow_ids, *args, **kwargs): super(FlowTestForm, self).__init__(*args, **kwargs) self.fields["flow_id"] = forms.ChoiceField(choices=[ (fid, fid) for fid in flow_ids ], required=True, label=_("Flow ID"), widget=Select2Widget()) self.helper.add_input( Submit( "test", mark_safe_lazy( string_concat(pgettext("Start an activity", "Go"), " »")), ))
def _create_recurring_events_backend(course, time, kind, starting_ordinal, interval, count, duration_in_minutes, all_day, shown_in_calendar): ordinal = starting_ordinal import datetime for i in range(count): evt = Event() evt.course = course evt.kind = kind evt.ordinal = ordinal evt.time = time evt.all_day = all_day evt.shown_in_calendar = shown_in_calendar if duration_in_minutes: evt.end_time = evt.time + datetime.timedelta( minutes=duration_in_minutes) try: evt.save() except IntegrityError: raise EventAlreadyExists( _("'%(event_kind)s %(event_ordinal)d' already exists") % {'event_kind': kind, 'event_ordinal': ordinal}) date = time.date() if interval == "weekly": date += datetime.timedelta(weeks=1) elif interval == "biweekly": date += datetime.timedelta(weeks=2) else: raise ValueError( string_concat( pgettext_lazy( "Unkown time interval", "unknown interval"), ": %s") % interval) time = time.tzinfo.localize( datetime.datetime(date.year, date.month, date.day, time.hour, time.minute, time.second)) del date ordinal += 1
def make_time_histogram(pctx, flow_id): qset = FlowSession.objects.filter(course=pctx.course, flow_id=flow_id) from relate.utils import string_concat hist = Histogram( num_log_bins=True, num_bin_title_formatter=(lambda minutes: string_concat( "$>$ %.1f ", pgettext("Minute (time unit)", "min")) % minutes)) for session in qset: if session.in_progress: hist.add_data_point("".join( ["<", pgettext("Status of session", "in progress"), ">"])) else: delta = session.completion_time - session.start_time minutes = delta.total_seconds() / 60 hist.add_data_point(minutes) return hist
def __init__(self, sessions, *args, **kwargs): # type: (List[FlowSession], *Any, **Any) -> None super(ExceptionStage2Form, self).__init__(*args, **kwargs) self.fields["session"] = forms.ChoiceField( choices=((session.id, strify_session_for_exception(session)) for session in sessions), help_text=_( "The rules that currently apply to selected " "session will provide the default values for the rules " "on the next page."), label=_("Session")) self.helper.add_input( Submit( "next", mark_safe_lazy( string_concat(pgettext("Next step", "Next"), " »"))))
def correct_answer(self, page_context, page_data, answer_data, grade_data): # FIXME: Could use 'best' match to answer CA_PATTERN = string_concat(_("A correct answer is"), ": '%s'.") # noqa for matcher in self.matchers: unspec_correct_answer_text = matcher.correct_answer_text() if unspec_correct_answer_text is not None: break assert unspec_correct_answer_text result = CA_PATTERN % unspec_correct_answer_text if hasattr(self.page_desc, "answer_explanation"): result += markup_to_html(page_context, self.page_desc.answer_explanation) return result
def __init__(self, vctx, location, page_desc, language_mode): super(CodeQuestion, self).__init__(vctx, location, page_desc) if vctx is not None and hasattr(page_desc, "data_files"): for data_file in page_desc.data_files: try: if not isinstance(data_file, str): raise ObjectDoesNotExist() from course.content import get_repo_blob get_repo_blob(vctx.repo, data_file, vctx.commit_sha) except ObjectDoesNotExist: raise ValidationError( string_concat("%(location)s: ", _("data file '%(file)s' not found")) % { "location": location, "file": data_file }) if hasattr(page_desc, "docker_image"): self.container_image = page_desc.docker_image if not getattr(page_desc, "single_submission", False) and vctx is not None: is_multi_submit = False if hasattr(page_desc, "access_rules"): if hasattr(page_desc.access_rules, "add_permissions"): if (flow_permission.change_answer in page_desc.access_rules.add_permissions): is_multi_submit = True if not is_multi_submit: vctx.add_warning( location, _("code question does not explicitly " "allow multiple submission. Either add " "access_rules/add_permssions/change_answer " "or add 'single_submission: True' to confirm that you intend " "for only a single submission to be allowed. " "While you're at it, consider adding " "access_rules/add_permssions/see_correctness."))
def correct_answer(self, page_context, page_data, answer_data, grade_data): # FIXME: Could use 'best' match to answer cor_answer_output = self.get_question(page_context, page_data) for idx, wrapped in enumerate(self.embedded_wrapped_name_list): correct_answer_i = self.answer_instance_list[idx] \ .get_correct_answer_text(page_context) cor_answer_output = cor_answer_output.replace( wrapped, "<strong>" + correct_answer_i + "</strong>") CA_PATTERN = string_concat(_("A correct answer is"), ": <br/> %s") # noqa result = CA_PATTERN % cor_answer_output if hasattr(self.page_desc, "answer_explanation"): result += markup_to_html(page_context, self.page_desc.answer_explanation) return result
def __init__(self, vctx, location, page_desc): super(FileUploadQuestion, self).__init__(vctx, location, page_desc) if not (set(page_desc.mime_types) <= set(self.ALLOWED_MIME_TYPES)): raise ValidationError( string_concat("%(location)s: ", _("unrecognized mime types"), " '%(presenttype)s'") % { 'location': location, 'presenttype': ", ".join( set(page_desc.mime_types) - set(self.ALLOWED_MIME_TYPES)) }) if vctx is not None: if not hasattr(page_desc, "value"): vctx.add_warning( location, _("upload question does not have " "assigned point value"))
def parse_matcher_string(vctx, location, matcher_desc): match = MATCHER_RE.match(matcher_desc) if match is not None: matcher_type = match.group(1) pattern = match.group(2) else: match = MATCHER_RE_2.match(matcher_desc) if match is None: raise ValidationError( string_concat("%s: ", _("does not specify match type")) % location) matcher_type = match.group(1) pattern = match.group(2) if vctx is not None: vctx.add_warning(location, _("uses deprecated 'matcher:answer' style")) return (get_matcher_class(location, matcher_type, "string")(vctx, location, pattern))
def __init__(self, course, flow_ids, *args, **kwargs): super(ExceptionStage1Form, self).__init__(*args, **kwargs) self.fields["participation"] = ParticipationChoiceField( queryset=(Participation.objects.filter( course=course, status=participation_status.active, ).order_by("user__last_name")), required=True, help_text=_("Select participant for whom exception is to " "be granted."), label=_("Participant"), widget=Select2Widget()) self.fields["flow_id"] = forms.ChoiceField(choices=[ (fid, fid) for fid in flow_ids ], required=True, label=_("Flow ID")) self.helper.add_input( Submit( "next", mark_safe_lazy( string_concat(pgettext("Next step", "Next"), " »"))))
def __init__(self, vctx, location, page_desc): super(PageBaseWithTitle, self).__init__(vctx, location, page_desc) title = None try: title = self.page_desc.title except AttributeError: pass if title is None: try: md_body = self.markup_body_for_title() except NotImplementedError: from warnings import warn warn(_("PageBaseWithTitle subclass '%s' does not implement " "markup_body_for_title()") % type(self).__name__) else: from course.content import extract_title_from_markup title = extract_title_from_markup(md_body) if title is None: raise ValidationError( string_concat( "%s: ", _("no title found in body or title attribute")) % (location)) from markdown import markdown from django.utils.html import strip_tags title = strip_tags(markdown(title)) if not title and vctx is not None: vctx.add_warning(location, _("the rendered title is an empty string")) self._title = title
def get_participant(self, obj): if obj.flow_session.participation: return obj.flow_session.participation.user else: return string_concat("(", _("anonymous"), ")")
def grant_exception_stage_2(pctx, participation_id, flow_id): # type: (CoursePageContext, Text, Text) -> http.HttpResponse if not pctx.has_permission(pperm.grant_exception): raise PermissionDenied(_("may not grant exceptions")) # {{{ get flow data participation = get_object_or_404(Participation, id=participation_id) form_text = (string_concat( "<div class='well'>", ugettext("Granting exception to '%(participation)s' for " "'%(flow_id)s'."), "</div>") % { 'participation': participation, 'flow_id': flow_id }) from course.content import get_flow_desc try: flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) except ObjectDoesNotExist: raise http.Http404() now_datetime = get_now_or_fake_time(pctx.request) if hasattr(flow_desc, "rules"): access_rules_tags = getattr(flow_desc.rules, "tags", []) else: access_rules_tags = [] NONE_SESSION_TAG = string_concat("<<<", _("NONE"), ">>>") # noqa session_tag_choices = [(tag, tag) for tag in access_rules_tags] + [ (NONE_SESSION_TAG, string_concat("(", _("NONE"), ")")) ] from course.utils import get_session_start_rule session_start_rule = get_session_start_rule(pctx.course, participation, flow_id, flow_desc, now_datetime) create_session_is_override = False if not session_start_rule.may_start_new_session: create_session_is_override = True form_text += ( "<div class='alert alert-info'>%s</div>" % (string_concat( "<i class='fa fa-info-circle'></i> ", _("Creating a new session is (technically) not allowed " "by course rules. Clicking 'Create Session' anyway will " "override this rule.")))) default_tag = session_start_rule.tag_session if default_tag is None: default_tag = NONE_SESSION_TAG # }}} def find_sessions(): # type: () -> List[FlowSession] return list( FlowSession.objects.filter(participation=participation, flow_id=flow_id).order_by("start_time")) exception_form = None request = pctx.request if request.method == "POST": exception_form = ExceptionStage2Form(find_sessions(), request.POST) create_session_form = CreateSessionForm(session_tag_choices, default_tag, create_session_is_override, request.POST) if "create_session" in request.POST or "next" in request.POST: pass else: raise SuspiciousOperation(_("invalid command")) if create_session_form.is_valid() and "create_session" in request.POST: from course.flow import start_flow access_rules_tag = ( create_session_form. cleaned_data["access_rules_tag_for_new_session"]) if access_rules_tag == NONE_SESSION_TAG: access_rules_tag = None start_flow(pctx.repo, pctx.course, participation, user=participation.user, flow_id=flow_id, flow_desc=flow_desc, session_start_rule=session_start_rule, now_datetime=now_datetime) exception_form = None elif exception_form.is_valid( ) and "next" in request.POST: # type: ignore return redirect( "relate-grant_exception_stage_3", pctx.course.identifier, participation.id, flow_id, exception_form.cleaned_data["session"]) # type: ignore else: create_session_form = CreateSessionForm(session_tag_choices, default_tag, create_session_is_override) if exception_form is None: exception_form = ExceptionStage2Form(find_sessions()) return render_course_page( pctx, "course/generic-course-form.html", { "forms": [exception_form, create_session_form], "form_text": form_text, "form_description": _("Grant Exception"), })
def grade(self, page_context, page_data, answer_data, grade_data): if answer_data is None: return AnswerFeedback(correctness=0, feedback=_("No answer provided.")) user_code = answer_data["answer"] # {{{ request run run_req = {"compile_only": False, "user_code": user_code} def transfer_attr(name): if hasattr(self.page_desc, name): run_req[name] = getattr(self.page_desc, name) transfer_attr("setup_code") transfer_attr("names_for_user") transfer_attr("names_from_user") run_req["test_code"] = self.get_test_code() if hasattr(self.page_desc, "data_files"): run_req["data_files"] = {} from course.content import get_repo_blob for data_file in self.page_desc.data_files: from base64 import b64encode run_req["data_files"][data_file] = \ b64encode( get_repo_blob( page_context.repo, data_file, page_context.commit_sha).data).decode() try: response_dict = request_run_with_retries( run_req, run_timeout=self.page_desc.timeout, image=self.container_image) except Exception: from traceback import format_exc response_dict = { "result": "uncaught_error", "message": "Error connecting to container", "traceback": "".join(format_exc()), } # }}} feedback_bits = [] correctness = None if "points" in response_dict: correctness = response_dict["points"] try: feedback_bits.append("<p><b>%s</b></p>" % _(get_auto_feedback(correctness))) except Exception as e: correctness = None response_dict["result"] = "setup_error" response_dict["message"] = ("%s: %s" % (type(e).__name__, str(e))) # {{{ send email if the grading code broke if response_dict["result"] in [ "uncaught_error", "setup_compile_error", "setup_error", "test_compile_error", "test_error" ]: error_msg_parts = ["RESULT: %s" % response_dict["result"]] for key, val in sorted(response_dict.items()): if (key not in ["result", "figures"] and val and isinstance(val, str)): error_msg_parts.append( "-------------------------------------") error_msg_parts.append(key) error_msg_parts.append( "-------------------------------------") error_msg_parts.append(val) error_msg_parts.append("-------------------------------------") error_msg_parts.append("user code") error_msg_parts.append("-------------------------------------") error_msg_parts.append(user_code) error_msg_parts.append("-------------------------------------") error_msg = "\n".join(error_msg_parts) from relate.utils import local_now, format_datetime_local from course.utils import LanguageOverride with LanguageOverride(page_context.course): from relate.utils import render_email_template message = render_email_template( "course/broken-code-question-email.txt", { "site": getattr(settings, "RELATE_BASE_URL"), "page_id": self.page_desc.id, "course": page_context.course, "error_message": error_msg, "review_uri": page_context.page_uri, "time": format_datetime_local(local_now()) }) if (not page_context.in_sandbox and not is_nuisance_failure(response_dict)): try: from django.core.mail import EmailMessage msg = EmailMessage( "".join([ "[%s:%s] ", _("code question execution failed") ]) % (page_context.course.identifier, page_context.flow_session.flow_id if page_context.flow_session is not None else _("<unknown flow>")), message, settings.ROBOT_EMAIL_FROM, [page_context.course.notify_email]) from relate.utils import get_outbound_mail_connection msg.connection = get_outbound_mail_connection("robot") msg.send() except Exception: from traceback import format_exc feedback_bits.append( str( string_concat( "<p>", _("Both the grading code and the attempt to " "notify course staff about the issue failed. " "Please contact the course or site staff and " "inform them of this issue, mentioning this " "entire error message:"), "</p>", "<p>", _("Sending an email to the course staff about the " "following failure failed with " "the following error message:"), "<pre>", "".join(format_exc()), "</pre>", _("The original failure message follows:"), "</p>"))) # }}} if hasattr(self.page_desc, "correct_code"): def normalize_code(s): return (s.replace(" ", "").replace("\r", "").replace( "\n", "").replace("\t", "")) if (normalize_code(user_code) == normalize_code( self.page_desc.correct_code)): feedback_bits.append( "<p><b>%s</b></p>" % _("It looks like you submitted code that is identical to " "the reference solution. This is not allowed.")) from relate.utils import dict_to_struct response = dict_to_struct(response_dict) bulk_feedback_bits = [] if response.result == "success": pass elif response.result in [ "uncaught_error", "setup_compile_error", "setup_error", "test_compile_error", "test_error" ]: feedback_bits.append("".join([ "<p>", _("The grading code failed. Sorry about that. " "The staff has been informed, and if this problem is " "due to an issue with the grading code, " "it will be fixed as soon as possible. " "In the meantime, you'll see a traceback " "below that may help you figure out what went wrong."), "</p>" ])) elif response.result == "timeout": feedback_bits.append("".join([ "<p>", _("Your code took too long to execute. The problem " "specifies that your code may take at most %s seconds " "to run. " "It took longer than that and was aborted."), "</p>" ]) % self.page_desc.timeout) correctness = 0 elif response.result == "user_compile_error": feedback_bits.append("".join([ "<p>", _("Your code failed to compile. An error message is " "below."), "</p>" ])) correctness = 0 elif response.result == "user_error": feedback_bits.append("".join([ "<p>", _("Your code failed with an exception. " "A traceback is below."), "</p>" ])) correctness = 0 else: raise RuntimeError("invalid run result: %s" % response.result) if hasattr(response, "feedback") and response.feedback: def sanitize(s): import bleach return bleach.clean(s, tags=["p", "pre"]) feedback_bits.append("".join([ "<p>", _("Here is some feedback on your code"), ":" "<ul>%s</ul></p>" ]) % "".join("<li>%s</li>" % sanitize(fb_item) for fb_item in response.feedback)) if hasattr(response, "traceback") and response.traceback: feedback_bits.append("".join([ "<p>", _("This is the exception traceback"), ":" "<pre>%s</pre></p>" ]) % escape(response.traceback)) if hasattr(response, "exec_host") and response.exec_host != "localhost": import socket try: exec_host_name, dummy, dummy = socket.gethostbyaddr( response.exec_host) except socket.error: exec_host_name = response.exec_host feedback_bits.append("".join( ["<p>", _("Your code ran on %s.") % exec_host_name, "</p>"])) if hasattr(response, "stdout") and response.stdout: bulk_feedback_bits.append("".join([ "<p>", _("Your code printed the following output"), ":" "<pre>%s</pre></p>" ]) % escape(response.stdout)) if hasattr(response, "stderr") and response.stderr: bulk_feedback_bits.append("".join([ "<p>", _("Your code printed the following error messages"), ":" "<pre>%s</pre></p>" ]) % escape(response.stderr)) if hasattr(response, "figures") and response.figures: fig_lines = [ "".join([ "<p>", _("Your code produced the following plots"), ":</p>" ]), '<dl class="result-figure-list">', ] for nr, mime_type, b64data in response.figures: if mime_type in ["image/jpeg", "image/png"]: fig_lines.extend([ "".join(["<dt>", _("Figure"), "%d<dt>"]) % nr, '<dd><img alt="Figure %d" src="data:%s;base64,%s"></dd>' % (nr, mime_type, b64data) ]) fig_lines.append("</dl>") bulk_feedback_bits.extend(fig_lines) # {{{ html output / santization if hasattr(response, "html") and response.html: def is_allowed_data_uri(allowed_mimetypes, uri): import re m = re.match(r"^data:([-a-z0-9]+/[-a-z0-9]+);base64,", uri) if not m: return False mimetype = m.group(1) return mimetype in allowed_mimetypes def sanitize(s): import bleach def filter_audio_attributes(tag, name, value): if name in ["controls"]: return True else: return False def filter_source_attributes(tag, name, value): if name in ["type"]: return True elif name == "src": if is_allowed_data_uri([ "audio/wav", ], value): return bleach.sanitizer.VALUE_SAFE else: return False else: return False def filter_img_attributes(tag, name, value): if name in ["alt", "title"]: return True elif name == "src": return is_allowed_data_uri([ "image/png", "image/jpeg", ], value) else: return False if not isinstance(s, str): return _("(Non-string in 'HTML' output filtered out)") return bleach.clean(s, tags=bleach.ALLOWED_TAGS + ["audio", "video", "source"], attributes={ "audio": filter_audio_attributes, "source": filter_source_attributes, "img": filter_img_attributes, }) bulk_feedback_bits.extend( sanitize(snippet) for snippet in response.html) # }}} return AnswerFeedback(correctness=correctness, feedback="\n".join(feedback_bits), bulk_feedback="\n".join(bulk_feedback_bits))
def update_grade_data_from_grading_form_v2(self, request, page_context, page_data, grade_data, grading_form, files_data): if grade_data is None: grade_data = {} for k in self.grade_data_attrs: if k == "grade_percent": grade_data[k] = grading_form.cleaned_percent() else: grade_data[k] = grading_form.cleaned_data[k] if grading_form.cleaned_data["notify"] and page_context.flow_session: with translation.override(settings.RELATE_ADMIN_EMAIL_LOCALE): from django.template.loader import render_to_string from course.utils import will_use_masked_profile_for_email staff_email = [ page_context.course.notify_email, request.user.email ] message = render_to_string( "course/grade-notify.txt", { "page_title": self.title(page_context, page_data), "course": page_context.course, "participation": page_context.flow_session.participation, "feedback_text": grade_data["feedback_text"], "flow_session": page_context.flow_session, "review_uri": page_context.page_uri, "use_masked_profile": will_use_masked_profile_for_email(staff_email) }) from django.core.mail import EmailMessage msg = EmailMessage( string_concat("[%(identifier)s:%(flow_id)s] ", _("New notification")) % { 'identifier': page_context.course.identifier, 'flow_id': page_context.flow_session.flow_id }, message, getattr(settings, "GRADER_FEEDBACK_EMAIL_FROM", page_context.course.get_from_email()), [page_context.flow_session.participation.user.email]) msg.bcc = [page_context.course.notify_email] if grading_form.cleaned_data["may_reply"]: msg.reply_to = [request.user.email] if hasattr(settings, "GRADER_FEEDBACK_EMAIL_FROM"): from relate.utils import get_outbound_mail_connection msg.connection = get_outbound_mail_connection( "grader_feedback") msg.send() if (grading_form.cleaned_data["notes"] and grading_form.cleaned_data["notify_instructor"] and page_context.flow_session): with translation.override(settings.RELATE_ADMIN_EMAIL_LOCALE): from django.template.loader import render_to_string from course.utils import will_use_masked_profile_for_email staff_email = [ page_context.course.notify_email, request.user.email ] message = render_to_string( "course/grade-internal-notes-notify.txt", { "page_title": self.title(page_context, page_data), "course": page_context.course, "participation": page_context.flow_session.participation, "notes_text": grade_data["notes"], "flow_session": page_context.flow_session, "review_uri": page_context.page_uri, "sender": request.user, "use_masked_profile": will_use_masked_profile_for_email(staff_email) }) from django.core.mail import EmailMessage msg = EmailMessage( string_concat("[%(identifier)s:%(flow_id)s] ", _("Grading notes from %(ta)s")) % { 'identifier': page_context.course.identifier, 'flow_id': page_context.flow_session.flow_id, 'ta': request.user.get_full_name() }, message, getattr(settings, "GRADER_FEEDBACK_EMAIL_FROM", page_context.course.get_from_email()), [page_context.course.notify_email]) msg.bcc = [request.user.email] msg.reply_to = [request.user.email] if hasattr(settings, "GRADER_FEEDBACK_EMAIL_FROM"): from relate.utils import get_outbound_mail_connection msg.connection = get_outbound_mail_connection( "grader_feedback") msg.send() return grade_data
from django.utils.translation import ( ugettext_lazy as _, ugettext) from course.validation import validate_struct, ValidationError import django.forms as forms from relate.utils import StyledForm, Struct, string_concat from course.page.base import ( AnswerFeedback, PageBaseWithTitle, PageBaseWithValue, markup_to_html, PageBaseWithHumanTextFeedback, PageBaseWithCorrectAnswer, get_editor_interaction_mode) import re import sys CORRECT_ANSWER_PATTERN = string_concat(_("A correct answer is"), ": '%s'.") # noqa class TextAnswerForm(StyledForm): # prevents form submission with codemirror's empty textarea use_required_attribute = False @staticmethod def get_text_widget(widget_type, read_only=False, check_only=False, interaction_mode=None): """Returns None if no widget found.""" if widget_type in [None, "text_input"]: if check_only: return True
def __init__(self, vctx, location, matcher_desc): self.matcher_desc = matcher_desc validate_struct( vctx, location, matcher_desc, required_attrs=( ("type", str), ("value", six.integer_types + (float, str)), ), allowed_attrs=( ("rtol", six.integer_types + (float, str)), ("atol", six.integer_types + (float, str)), ), ) try: self.matcher_desc.value = \ float_or_sympy_evalf(matcher_desc.value) except Exception: raise ValidationError( string_concat( "%s: 'value' ", _("does not provide a valid float literal")) % location) if hasattr(matcher_desc, "rtol"): try: self.matcher_desc.rtol = \ float_or_sympy_evalf(matcher_desc.rtol) except Exception: raise ValidationError( string_concat( "%s: 'rtol' ", _("does not provide a valid float literal")) % location) if matcher_desc.value == 0: raise ValidationError( string_concat( "%s: 'rtol' ", _("not allowed when 'value' is zero")) % location) if hasattr(matcher_desc, "atol"): try: self.matcher_desc.atol = \ float_or_sympy_evalf(matcher_desc.atol) except Exception: raise ValidationError( string_concat( "%s: 'atol' ", _("does not provide a valid float literal")) % location) else: if matcher_desc.value == 0: vctx.add_warning(location, _("Float match for 'value' zero should have atol--" "otherwise it will match any number")) if ( not matcher_desc.value == 0 and not hasattr(matcher_desc, "atol") and not hasattr(matcher_desc, "rtol") and vctx is not None): vctx.add_warning(location, _("Float match should have either rtol or atol--" "otherwise it will match any number"))
def __init__(self, vctx, location, page_desc): super(InlineMultiQuestion, self).__init__(vctx, location, page_desc) expanded_question = page_desc.question self.embedded_wrapped_name_list = WRAPPED_NAME_RE.findall( expanded_question) self.embedded_name_list = NAME_RE.findall(expanded_question) answer_instance_list = [] for idx, name in enumerate(self.embedded_name_list): answers_desc = getattr(self.page_desc.answers, name) parsed_answer = parse_question(vctx, location, name, answers_desc) answer_instance_list.append(parsed_answer) self.answer_instance_list = answer_instance_list from relate.utils import struct_to_dict answers_name_list = struct_to_dict(page_desc.answers).keys() invalid_answer_name = [] invalid_embedded_name = [] if not answer_instance_list: raise ValidationError( string_concat( "%(location)s: ", _("InlineMultiQuestion requires at least one " "answer field to be defined.")) % {'location': location}) for answers_name in answers_name_list: if NAME_VALIDATE_RE.match(answers_name) is None: invalid_answer_name.append(answers_name) if len(invalid_answer_name) > 0: raise ValidationError( string_concat( "%s: ", _("invalid answers name %s. "), _("A valid name should start with letters. " "Alphanumeric with underscores. " "Do not use spaces.")) % (location, ", ".join( ["'" + name + "'" for name in invalid_answer_name]))) for embedded_name in self.embedded_name_list: if NAME_VALIDATE_RE.match(embedded_name) is None: invalid_embedded_name.append(embedded_name) if len(invalid_embedded_name) > 0: raise ValidationError( string_concat( "%s: ", _("invalid embedded question name %s. "), _("A valid name should start with letters. " "Alphanumeric with underscores. " "Do not use spaces.")) % (location, ", ".join( ["'" + name + "'" for name in invalid_embedded_name]))) if len(set(self.embedded_name_list)) < len(self.embedded_name_list): duplicated = list( set([ x for x in self.embedded_name_list if self.embedded_name_list.count(x) > 1 ])) raise ValidationError( string_concat("%s: ", _("embedded question name %s not unique.")) % (location, ", ".join(duplicated))) no_answer_set = set(self.embedded_name_list) - set(answers_name_list) redundant_answer_list = list( set(answers_name_list) - set(self.embedded_name_list)) if no_answer_set: raise ValidationError( string_concat( "%s: ", _("correct answer(s) not provided for question %s.")) % (location, ", ".join( ["'" + item + "'" for item in list(no_answer_set)]))) if redundant_answer_list: if vctx is not None: vctx.add_warning( location, _("redundant answers %s provided for " "non-existing question(s).") % ", ".join( ["'" + item + "'" for item in redundant_answer_list])) if vctx is not None: validate_markup(vctx, location, page_desc.question) def reverse_func(*args, **kwargs): pass # FIXME This is a bit redundant since validate_markup already calls # markup_to_html. remainder_html = markup_to_html(vctx, page_desc.question, reverse_func=reverse_func) html_list = [] for wrapped_name in self.embedded_wrapped_name_list: [html, remainder_html] = remainder_html.split(wrapped_name) html_list.append(html) if remainder_html != "": html_list.append(remainder_html) # make sure all [[ and ]] are paired. embedded_removed = " ".join(html_list) for sep in ["[[", "]]"]: if sep in embedded_removed: raise ValidationError( string_concat("%s: ", _("have unpaired '%s'.")) % (location, sep)) for idx, name in enumerate(self.embedded_name_list): answers_desc = getattr(page_desc.answers, name) parse_question(vctx, location, name, answers_desc)
def enroll_view(request, course_identifier): # type: (http.HttpRequest, str) -> http.HttpResponse course = get_object_or_404(Course, identifier=course_identifier) participation = get_participation_for_request(request, course) if participation is not None: messages.add_message(request, messages.ERROR, _("Already enrolled. Cannot re-renroll.")) return redirect("relate-course_page", course_identifier) if not course.accepts_enrollment: messages.add_message(request, messages.ERROR, _("Course is not accepting enrollments.")) return redirect("relate-course_page", course_identifier) if request.method != "POST": # This can happen if someone tries to refresh the page, or switches to # desktop view on mobile. messages.add_message(request, messages.ERROR, _("Can only enroll using POST request")) return redirect("relate-course_page", course_identifier) user = request.user if (course.enrollment_required_email_suffix and user.status != user_status.active): messages.add_message( request, messages.ERROR, _("Your email address is not yet confirmed. " "Confirm your email to continue.")) return redirect("relate-course_page", course_identifier) preapproval = None if request.user.email: try: preapproval = ParticipationPreapproval.objects.get( course=course, email__iexact=request.user.email) except ParticipationPreapproval.DoesNotExist: if user.institutional_id: if not (course.preapproval_require_verified_inst_id and not user.institutional_id_verified): try: preapproval = ParticipationPreapproval.objects.get( course=course, institutional_id__iexact=user.institutional_id) except ParticipationPreapproval.DoesNotExist: pass pass if (preapproval is None and course.enrollment_required_email_suffix and not user.email.endswith(course.enrollment_required_email_suffix)): messages.add_message( request, messages.ERROR, _("Enrollment not allowed. Please use your '%s' email to " "enroll.") % course.enrollment_required_email_suffix) return redirect("relate-course_page", course_identifier) roles = ParticipationRole.objects.filter( course=course, is_default_for_new_participants=True) if preapproval is not None: roles = list(preapproval.roles.all()) try: if course.enrollment_approval_required and preapproval is None: participation = handle_enrollment_request( course, user, participation_status.requested, roles, request) with translation.override(settings.RELATE_ADMIN_EMAIL_LOCALE): from django.template.loader import render_to_string message = render_to_string( "course/enrollment-request-email.txt", { "user": user, "course": course, "admin_uri": mark_safe( request.build_absolute_uri( reverse("relate-edit_participation", args=(course.identifier, participation.id)))) }) from django.core.mail import EmailMessage msg = EmailMessage( string_concat("[%s] ", _("New enrollment request")) % course_identifier, message, settings.ROBOT_EMAIL_FROM, [course.notify_email]) from relate.utils import get_outbound_mail_connection msg.connection = get_outbound_mail_connection("robot") msg.send() messages.add_message( request, messages.INFO, _("Enrollment request sent. You will receive notifcation " "by email once your request has been acted upon.")) else: handle_enrollment_request(course, user, participation_status.active, roles, request) messages.add_message(request, messages.SUCCESS, _("Successfully enrolled.")) except IntegrityError: messages.add_message( request, messages.ERROR, _("A participation already exists. Enrollment attempt aborted.")) return redirect("relate-course_page", course_identifier)
def grant_exception_stage_3(pctx, participation_id, flow_id, session_id): # type: (CoursePageContext, int, Text, int) -> http.HttpResponse if not pctx.has_permission(pperm.grant_exception): raise PermissionDenied(_("may not grant exceptions")) participation = get_object_or_404(Participation, id=participation_id) from course.content import get_flow_desc try: flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) except ObjectDoesNotExist: raise http.Http404() session = FlowSession.objects.get(id=int(session_id)) now_datetime = get_now_or_fake_time(pctx.request) from course.utils import (get_session_access_rule, get_session_grading_rule) access_rule = get_session_access_rule(session, flow_desc, now_datetime) grading_rule = get_session_grading_rule(session, flow_desc, now_datetime) request = pctx.request if request.method == "POST": form = ExceptionStage3Form({}, flow_desc, session.access_rules_tag, request.POST) from course.constants import flow_rule_kind if form.is_valid(): permissions = [ key for key, _ in FLOW_PERMISSION_CHOICES if form.cleaned_data[key] ] from course.validation import (validate_session_access_rule, validate_session_grading_rule, ValidationContext) from relate.utils import dict_to_struct vctx = ValidationContext(repo=pctx.repo, commit_sha=pctx.course_commit_sha) flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) tags = [] # type: List[Text] if hasattr(flow_desc, "rules"): try: from typing import Text # noqa except ImportError: Text = None # noqa tags = cast(List[Text], getattr(flow_desc.rules, "tags", [])) # type: ignore # noqa # {{{ put together access rule if form.cleaned_data["create_access_exception"]: new_access_rule = {"permissions": permissions} if (form.cleaned_data.get("restrict_to_same_tag") and session.access_rules_tag is not None): new_access_rule["if_has_tag"] = session.access_rules_tag validate_session_access_rule( vctx, ugettext("newly created exception"), dict_to_struct(new_access_rule), tags) fre_access = FlowRuleException( flow_id=flow_id, participation=participation, expiration=form.cleaned_data["access_expires"], creator=pctx.request.user, comment=form.cleaned_data["comment"], kind=flow_rule_kind.access, rule=new_access_rule) fre_access.save() # }}} new_access_rules_tag = form.cleaned_data.get( "set_access_rules_tag") if new_access_rules_tag == NONE_SESSION_TAG: new_access_rules_tag = None if session.access_rules_tag != new_access_rules_tag: session.access_rules_tag = new_access_rules_tag session.save() # {{{ put together grading rule if form.cleaned_data["create_grading_exception"]: due = form.cleaned_data["due"] if form.cleaned_data["due_same_as_access_expiration"]: due = form.cleaned_data["access_expires"] descr = ugettext("Granted excecption") if form.cleaned_data["credit_percent"] is not None: descr += string_concat(" (%.1f%% ", ugettext('credit'), ")") \ % form.cleaned_data["credit_percent"] due_local_naive = due if due_local_naive is not None: from relate.utils import as_local_time due_local_naive = (as_local_time(due_local_naive).replace( tzinfo=None)) new_grading_rule = { "description": descr, } if due_local_naive is not None: new_grading_rule["due"] = due_local_naive new_grading_rule["if_completed_before"] = due_local_naive for attr_name in [ "credit_percent", "bonus_points", "max_points", "max_points_enforced_cap", "generates_grade" ]: if form.cleaned_data[attr_name] is not None: new_grading_rule[attr_name] = form.cleaned_data[ attr_name] if (form.cleaned_data.get("restrict_to_same_tag") and session.access_rules_tag is not None): new_grading_rule["if_has_tag"] = session.access_rules_tag validate_session_grading_rule( vctx, ugettext("newly created exception"), dict_to_struct(new_grading_rule), tags, grading_rule.grade_identifier) fre_grading = FlowRuleException( flow_id=flow_id, participation=participation, creator=pctx.request.user, comment=form.cleaned_data["comment"], kind=flow_rule_kind.grading, rule=new_grading_rule) fre_grading.save() # }}} messages.add_message( pctx.request, messages.SUCCESS, ugettext("Exception granted to '%(participation)s' " "for '%(flow_id)s'.") % { 'participation': participation, 'flow_id': flow_id }) return redirect("relate-grant_exception", pctx.course.identifier) else: data = { "restrict_to_same_tag": session.access_rules_tag is not None, #"due_same_as_access_expiration": True, "due": grading_rule.due, "generates_grade": grading_rule.generates_grade, "credit_percent": grading_rule.credit_percent, "bonus_points": grading_rule.bonus_points, "max_points": grading_rule.max_points, "max_points_enforced_cap": grading_rule.max_points_enforced_cap, } for perm in access_rule.permissions: data[perm] = True form = ExceptionStage3Form(data, flow_desc, session.access_rules_tag) return render_course_page( pctx, "course/generic-course-form.html", { "form": form, "form_description": ugettext("Grant Exception"), "form_text": string_concat( "<div class='well'>", ugettext("Granting exception to '%(participation)s' " "for '%(flow_id)s' (session %(session)s)."), "</div>") % { 'participation': participation, 'flow_id': flow_id, 'session': strify_session_for_exception(session) }, })