def clean_url(self): if self.instance and self.instance.visible_to_students: # URL must not be changed for visible course instances. return self.instance.url # ModelForm runs form validation before the model validation. # The URL validator is copied here from the model definition because # the cleaned data returned here is used in the rendering of the form # POST target page. Even though the model validation would stop invalid # data from being saved to the database, the next page rendering could # crash due to invalid data if this method returned an invalid value. # Raising a ValidationError here prevents the course instance from # having invalid values. generate_url_key_validator()(self.cleaned_data["url"]) return self.cleaned_data["url"]
def clean_url(self): if self.instance and self.instance.visible_to_students: # URL must not be changed for visible course instances. return self.instance.url # ModelForm runs form validation before the model validation. # The URL validator is copied here from the model definition because # the cleaned data returned here is used in the rendering of the form # POST target page. Even though the model validation would stop invalid # data from being saved to the database, the next page rendering could # crash due to invalid data if this method returned an invalid value. # Raising a ValidationError here prevents the course instance from # having invalid values. generate_url_key_validator()(self.cleaned_data["url"]) return self.cleaned_data["url"]
class CloneInstanceForm(forms.Form): url = forms.CharField( label=_("New URL identifier for the course instance:"), validators=[generate_url_key_validator()]) assistants = forms.BooleanField(label=_("Assistants"), required=False, initial=True) categories = forms.BooleanField(label=_("Exercise categories"), required=False, initial=True) modules = forms.BooleanField(label=_("Course modules"), required=False, initial=True) chapters = forms.BooleanField(label=_("Content chapters"), required=False, initial=True) exercises = forms.BooleanField(label=_("Exercises"), required=False, initial=True) menuitems = forms.BooleanField(label=_("Menu items"), required=False, initial=True) usertags = forms.BooleanField(label=_("Student tags"), required=False, initial=True) def __init__(self, *args, **kwargs): self.instance = kwargs.pop('instance') super().__init__(*args, **kwargs) def clean_url(self): url = self.cleaned_data['url'] if CourseInstance.objects.filter(course=self.instance.course, url=url).exists(): raise ValidationError(_("The URL is already taken.")) return url def clean(self): errors = {} if self.cleaned_data['chapters'] or self.cleaned_data['exercises']: if not self.cleaned_data['categories']: errors['categories'] = _( "Can't clone chapters and exercises without cloning exercise categories." ) if not self.cleaned_data['modules']: errors['modules'] = _( "Can't clone chapters and exercises without cloning course modules." ) if errors: raise ValidationError(errors)
class CloneInstanceForm(forms.Form): url = forms.CharField(label=_("New URL identifier for the course instance:"), validators=[generate_url_key_validator()]) def __init__(self, *args, **kwargs): self.instance = kwargs.pop('instance') super().__init__(*args, **kwargs) def clean_url(self): url = self.cleaned_data['url'] if CourseInstance.objects.filter( course=self.instance.course, url=url).exists(): raise ValidationError(_("The URL is already taken.")) return url
class Course(UrlMixin, models.Model): """ Course model represents a course in a university. A course has a name and an identification number. It also has a URL which is included in the addresses of pages under the course. """ name = models.CharField(max_length=255, verbose_name=_('name')) code = models.CharField(max_length=255, verbose_name=_('code')) url = models.CharField( unique=True, max_length=255, blank=False, validators=[generate_url_key_validator()], help_text=_("Input an URL identifier for this course."), verbose_name=_('Urlidentifier')) teachers = models.ManyToManyField(UserProfile, related_name="teaching_courses", blank=True, verbose_name=_('teachers')) def __str__(self): return "{} {}".format(self.code, self.name) class Meta: verbose_name = '课程' verbose_name_plural = verbose_name def clean(self): super().clean() RESERVED = ("admin", "accounts", "shibboleth", "api", "archive", "course", "exercise", "diploma") if self.url in RESERVED: raise ValidationError({ 'url': _("Taken words include: {}").format(", ".join(RESERVED)) }) def is_teacher(self, user): return (user and user.is_authenticated and (user.is_superuser or (isinstance(user, User) and self.teachers.filter(id=user.userprofile.id).exists()) or (isinstance(user, GraderUser) and user._course == self))) ABSOLUTE_URL_NAME = "course-instances" def get_url_kwargs(self): return dict(course_slug=self.url)
class Course(UrlMixin, models.Model): """ Course model represents a course in a university. A course has a name and an identification number. It also has a URL which is included in the addresses of pages under the course. """ name = models.CharField( verbose_name=_('LABEL_NAME'), max_length=255, ) code = models.CharField( verbose_name=_('LABEL_CODE'), max_length=255, ) url = models.CharField( verbose_name=_('LABEL_URL'), unique=True, max_length=255, blank=False, help_text=_('COURSE_URL_IDENTIFIER_HELPTEXT'), validators=[generate_url_key_validator()], ) objects = CourseManager() class Meta: verbose_name = _('MODEL_NAME_COURSE') verbose_name_plural = _('MODEL_NAME_COURSE_PLURAL') def __str__(self): return "{} {}".format(self.code, self.name) def clean(self): super().clean() RESERVED = ("admin", "accounts", "shibboleth", "api", "archive", "course", "exercise", "diploma") if self.url in RESERVED: raise ValidationError({ 'url': format_lazy(_('TAKEN_WORDS_INCLUDE -- {}'), ", ".join(RESERVED)) }) ABSOLUTE_URL_NAME = "course-instances" def get_url_kwargs(self): return dict(course_slug=self.url)
class LearningObject(UrlMixin, ModelWithInheritance): """ All learning objects inherit this model. """ STATUS = Enum([ ('READY', 'ready', _("Ready")), ('UNLISTED', 'unlisted', _("Unlisted in table of contents")), ('ENROLLMENT', 'enrollment', _("Enrollment questions")), ('ENROLLMENT_EXTERNAL', 'enrollment_ext', _("Enrollment questions for external students")), ('HIDDEN', 'hidden', _("Hidden from non course staff")), ('MAINTENANCE', 'maintenance', _("Maintenance")), ]) AUDIENCE = Enum([ ('COURSE_AUDIENCE', 0, _('Course audience')), ('INTERNAL_USERS', 1, _('Only internal users')), ('EXTERNAL_USERS', 2, _('Only external users')), ('REGISTERED_USERS', 3, _('Only registered users')), ]) status = models.CharField(max_length=32, choices=STATUS.choices, default=STATUS.READY) audience = models.IntegerField(choices=AUDIENCE.choices, default=AUDIENCE.COURSE_AUDIENCE) category = models.ForeignKey(LearningObjectCategory, on_delete=models.CASCADE, related_name="learning_objects") course_module = models.ForeignKey(CourseModule, on_delete=models.CASCADE, related_name="learning_objects") parent = models.ForeignKey('self', on_delete=models.SET_NULL, blank=True, null=True, related_name='children') order = models.IntegerField(default=1) url = models.CharField(max_length=512, validators=[generate_url_key_validator()], help_text=_("Input an URL identifier for this object.")) name = models.CharField(max_length=255) description = models.TextField(blank=True, help_text=_("Internal description is not presented on site.")) use_wide_column = models.BooleanField(default=False, help_text=_("Remove the third info column for more space.")) service_url = models.CharField(max_length=4096, blank=True) exercise_info = JSONField(blank=True) model_answers = models.TextField(blank=True, help_text=_("List model answer files as protected URL addresses.")) templates = models.TextField(blank=True, help_text=_("List template files as protected URL addresses.")) # Keep this to support ExerciseWithAttachment # Maybe this should inject extra content to any exercise content = models.TextField(blank=True) objects = LearningObjectManager() class Meta: app_label = "exercise" ordering = ['course_module', 'order', 'id'] unique_together = ['course_module', 'parent', 'url'] def clean(self): """ Validates the model before saving (standard method used in Django admin). """ super().clean() errors = {} RESERVED = ("submissions", "plain", "info") if self.url in RESERVED: errors['url'] = _("Taken words include: {}").format(", ".join(RESERVED)) if self.course_module.course_instance != self.category.course_instance: errors['category'] = _('Course_module and category must belong to the same course instance.') if self.parent: if self.parent.course_module != self.course_module: errors['parent'] = _('Cannot select parent from another course module.') if self.parent.id == self.id: errors['parent'] = _('Cannot select self as a parent.') if errors: raise ValidationError(errors) def save(self, *args, **kwargs): super().save(*args, **kwargs) # Trigger LearningObject post save signal for extending classes. cls = self.__class__ while cls.__bases__: cls = cls.__bases__[0] if cls.__name__ == 'LearningObject': signals.post_save.send(sender=cls, instance=self) def delete(self, *args, **kwargs): super().delete(*args, **kwargs) # Trigger LearningObject post delete signal for extending classes. cls = self.__class__ while cls.__bases__: cls = cls.__bases__[0] if cls.__name__ == 'LearningObject': signals.post_delete.send(sender=cls, instance=self) def __str__(self): if self.order >= 0: if self.course_instance.content_numbering == CourseInstance.CONTENT_NUMBERING.ARABIC: number = self.number() if self.course_instance.module_numbering in ( CourseInstance.CONTENT_NUMBERING.ARABIC, CourseInstance.CONTENT_NUMBERING.HIDDEN, ): return "{:d}.{} {}".format(self.course_module.order, number, self.name) return "{} {}".format(number, self.name) elif self.course_instance.content_numbering == CourseInstance.CONTENT_NUMBERING.ROMAN: return "{} {}".format(roman_numeral(self.order), self.name) return self.name def number(self): return ".".join([str(o.order) for o in self.parent_list()]) def parent_list(self): if not hasattr(self, '_parents'): def recursion(obj, parents): if not obj is None: return recursion(obj.parent, [obj] + parents) return parents self._parents = recursion(self.parent, [self]) return self._parents @property def course_instance(self): return self.course_module.course_instance @property def is_submittable(self): return False def is_empty(self): return not self.service_url and self.as_leaf_class()._is_empty() def _is_empty(self): return True def is_open(self, when=None): return self.course_module.exercises_open(when=when) def is_closed(self, when=None): return self.course_module.is_closed(when=when) @property def can_show_model_solutions(self): """Can model solutions be shown to students? This method checks only the module deadline and ignores personal deadline extensions. """ return self.is_closed() and not self.course_instance.is_on_lifesupport() and not self.course_instance.is_archived() def can_show_model_solutions_to_student(self, student): """Can model solutions be shown to the given student (User)? This method checks personal deadline extensions in addition to the common module deadline. """ # The old version of this method was defined in this LearningObject class # even though only exercises could be submitted to and have model solutions. # Class BaseExercise overrides this method since deadline deviations are # defined only for them, not learning objects. return student.is_authenticated and self.can_show_model_solutions def get_path(self): return "/".join([o.url for o in self.parent_list()]) ABSOLUTE_URL_NAME = "exercise" def get_url_kwargs(self): return dict(exercise_path=self.get_path(), **self.course_module.get_url_kwargs()) def get_display_url(self): if self.status == self.STATUS.UNLISTED and self.parent: return "{}#chapter-exercise-{:d}".format( self.parent_list()[-2].get_absolute_url(), self.order ) return self.get_absolute_url() def get_submission_list_url(self): return self.get_url("submission-list") def load(self, request, students, url_name="exercise"): """ Loads the learning object page. """ page = ExercisePage(self) if not self.service_url: return page language = get_language() cache = ExerciseCache(self, language, request, students, url_name) page.head = cache.head() page.content = cache.content() page.is_loaded = True return page def load_page(self, language, request, students, url_name, last_modified=None): return load_exercise_page( request, self.get_load_url(language, request, students, url_name), last_modified, self ) def get_service_url(self, language): return pick_localized(self.service_url, language) def get_load_url(self, language, request, students, url_name="exercise"): return update_url_params(self.get_service_url(language), { 'lang': language, }) def get_models(self): entries = pick_localized(self.model_answers, get_language()) return [(url,url.split('/')[-1]) for url in entries.split()] def get_templates(self): entries = pick_localized(self.templates, get_language()) return [(url,url.split('/')[-1]) for url in entries.split()] def get_form_spec_keys(self, include_static_fields=False): """Return the keys of the form fields of this exercise. This is based on the form_spec structure of the exercise_info, which is saved in the course JSON import. """ form_spec = ( self.exercise_info.get('form_spec', []) if isinstance(self.exercise_info, dict) else [] ) keys = set() for item in form_spec: key = item.get('key') typ = item.get('type') if not include_static_fields and typ == 'static': continue if key: # avoid empty or missing values keys.add(key) return keys
class CourseModule(UrlMixin, models.Model): """ CourseModule objects connect chapters and learning objects to logical sets of each other and course instances. They also contain information about the opening times and deadlines for exercises. """ STATUS = Enum([ ('READY', 'ready', _("Ready")), ('UNLISTED', 'unlisted', _("Unlisted in table of contents")), ('HIDDEN', 'hidden', _("Hidden")), ('MAINTENANCE', 'maintenance', _("Maintenance")), ]) status = models.CharField(max_length=32, choices=STATUS.choices, default=STATUS.READY) order = models.IntegerField(default=1) name = models.CharField(max_length=255) url = models.CharField( max_length=255, validators=[generate_url_key_validator()], help_text=_("Input an URL identifier for this module.")) points_to_pass = models.PositiveIntegerField(default=0) introduction = models.TextField(blank=True) course_instance = models.ForeignKey(CourseInstance, on_delete=models.CASCADE, related_name="course_modules") reading_opening_time = models.DateTimeField( verbose_name=_("Opening time for the reading material"), null=True, blank=True, help_text= _("Leave empty if the reading material should not open before the exercises." )) opening_time = models.DateTimeField(default=timezone.now) closing_time = models.DateTimeField(default=timezone.now) # early_submissions_allowed= models.BooleanField(default=False) # early_submissions_start = models.DateTimeField(default=timezone.now, blank=True, null=True) # early_submission_bonus = PercentField(default=0.1, # help_text=_("Multiplier of points to reward, as decimal. 0.1 = 10%")) late_submissions_allowed = models.BooleanField(default=False) late_submission_deadline = models.DateTimeField(default=timezone.now) late_submission_penalty = PercentField( default=0.5, help_text=_("Multiplier of points to reduce, as decimal. 0.1 = 10%")) objects = CourseModuleManager() class Meta: unique_together = ("course_instance", "url") ordering = ['order', 'closing_time', 'id'] def __str__(self): if self.order > 0: if self.course_instance.module_numbering == CourseInstance.CONTENT_NUMBERING.ARABIC: return "{:d}. {}".format(self.order, self.name) elif self.course_instance.module_numbering == CourseInstance.CONTENT_NUMBERING.ROMAN: return "{} {}".format(roman_numeral(self.order), self.name) return self.name def clean(self): super().clean() errors = {} RESERVED = ("toc", "teachers", "user", "exercises", "apps", "lti-login") if self.url in RESERVED: errors['url'] = _("Taken words include: {}").format( ", ".join(RESERVED)) if self.opening_time > self.closing_time: errors['opening_time'] = _( "Opening time must be earlier than the closing time.") if self.late_submissions_allowed and self.late_submission_deadline <= self.closing_time: errors['late_submission_deadline'] = _( "Late submission deadline must be later than the closing time." ) if self.reading_opening_time and self.reading_opening_time > self.opening_time: errors['reading_opening_time'] = _( "Opening time of reading material " "must be earlier than the opening time of the exercises.") if errors: raise ValidationError(errors) def is_open(self, when=None): when = when or timezone.now() if self.reading_opening_time: return self.reading_opening_time <= when <= self.closing_time return self.opening_time <= when <= self.closing_time def is_after_open(self, when=None): """ Checks if current time is past the round opening time. """ when = when or timezone.now() if self.reading_opening_time: return self.reading_opening_time <= when return self.opening_time <= when def have_exercises_been_opened(self, when=None): when = when or timezone.now() return self.opening_time <= when def exercises_open(self, when=None): when = when or timezone.now() return self.opening_time <= when <= self.closing_time def is_late_submission_open(self, when=None): when = when or timezone.now() return self.late_submissions_allowed \ and self.closing_time <= when <= self.late_submission_deadline def is_closed(self, when=None): when = when or timezone.now() if self.late_submissions_allowed and self.late_submission_penalty < 1: return when > self.late_submission_deadline return when > self.closing_time def are_requirements_passed(self, cached_points): for r in self.requirements.all(): if not r.is_passed(cached_points): return False return True def get_late_submission_point_worth(self): """ Returns the percentage (0-100) that late submission points are worth. """ point_worth = 0 if self.late_submissions_allowed: point_worth = int((1.0 - self.late_submission_penalty) * 100.0) return point_worth def number_of_submitters(self): return self.course_instance.students\ .filter(submissions__exercise__course_module=self).distinct().count() ABSOLUTE_URL_NAME = "module" def get_url_kwargs(self): return dict(module_slug=self.url, **self.course_instance.get_url_kwargs())
class CourseInstance(UrlMixin, models.Model): """ CourseInstance class represent an instance of a course. A single course may have several instances either at the same time or during different years. All instances have the same teacher, but teaching assistants and students are connected to individual instances. """ ENROLLMENT_AUDIENCE = Enum([ ('INTERNAL_USERS', 1, _('Internal users')), ('EXTERNAL_USERS', 2, _('External users')), ('ALL_USERS', 3, _('Internal and external users')), ]) VIEW_ACCESS = Enum([ ('ENROLLED', 1, _('Enrolled students')), ('ENROLLMENT_AUDIENCE', 2, _('Enrollment audience')), ('ALL_REGISTERED', 3, _('All registered users')), ('PUBLIC', 4, _('Public to internet')), ]) INDEX_TYPE = Enum([ ('RESULTS', 0, _('User results')), ('TOC', 1, _("Table of contents")), ('LAST', 2, _("Link to last visited content")), ('EXPERIMENT', 10, _("Experimental setup (hard-coded)")), ]) CONTENT_NUMBERING = Enum([ ('NONE', 0, _("No numbering")), ('ARABIC', 1, _("Arabic")), ('ROMAN', 2, _("Roman")), ('HIDDEN', 3, _("Hidden arabic")), ]) course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name="instances") instance_name = models.CharField(max_length=255) url = models.CharField( max_length=255, blank=False, validators=[generate_url_key_validator()], help_text=_("Input an URL identifier for this course instance.")) visible_to_students = models.BooleanField(default=True) enrollment_audience = models.IntegerField( choices=ENROLLMENT_AUDIENCE.choices, default=ENROLLMENT_AUDIENCE.INTERNAL_USERS) view_content_to = models.IntegerField(choices=VIEW_ACCESS.choices, default=VIEW_ACCESS.ENROLLED) starting_time = models.DateTimeField() ending_time = models.DateTimeField() lifesupport_time = models.DateTimeField(blank=True, null=True) archive_time = models.DateTimeField(blank=True, null=True) enrollment_starting_time = models.DateTimeField(blank=True, null=True) enrollment_ending_time = models.DateTimeField(blank=True, null=True) image = models.ImageField(blank=True, null=True, upload_to=build_upload_dir) language = models.CharField(max_length=255, blank=True, default="") description = models.TextField(blank=True) footer = models.TextField(blank=True) index_mode = models.IntegerField( choices=INDEX_TYPE.choices, default=INDEX_TYPE.RESULTS, help_text=_('Select content for the course index page.')) module_numbering = models.IntegerField(choices=CONTENT_NUMBERING.choices, default=CONTENT_NUMBERING.ARABIC) content_numbering = models.IntegerField(choices=CONTENT_NUMBERING.choices, default=CONTENT_NUMBERING.ARABIC) head_urls = models.TextField(blank=True, help_text=_( "External CSS and JS resources " "that are included on all course pages. " "Separate with white space.")) configure_url = models.URLField(blank=True) build_log_url = models.URLField(blank=True) last_modified = models.DateTimeField(auto_now=True, blank=True, null=True) technical_error_emails = models.CharField( max_length=255, blank=True, help_text=_( "By default exercise errors are reported to teacher " "email addresses. Set this field as comma separated emails to " "override the recipients.")) plugins = GenericRelation(BasePlugin, object_id_field="container_pk", content_type_field="container_type") tabs = GenericRelation(BaseTab, object_id_field="container_pk", content_type_field="container_type") assistants = models.ManyToManyField(UserProfile, related_name="assisting_courses", blank=True) students = models.ManyToManyField(UserProfile, related_name="enrolled", blank=True, through='Enrollment') # usertags from course.models.UserTag # taggings from course.models.UserTagging # categories from course.models.LearningObjectCategory # course_modules from course.models.CourseModule objects = CourseInstanceManager() class Meta: unique_together = ("course", "url") def __str__(self): return "{}: {}".format(str(self.course), self.instance_name) def clean(self): super().clean() errors = {} if self.ending_time <= self.starting_time: errors['ending_time'] = _( "Ending time must be later than starting time.") if self.lifesupport_time and self.lifesupport_time < self.ending_time: errors['lifesupport_time'] = _( "Lifesupport time must be later than ending time.") if (self.archive_time and not self.lifesupport_time) \ or (self.lifesupport_time and not self.archive_time): # Must not set only one of lifesupport and archive time since their # default values could change their order. Lifesupport time must not # be earlier than archive time. errors['archive_time'] = _( "Lifesupport time and archive time must be either both set or both unset." ) elif self.archive_time and self.archive_time < self.lifesupport_time: errors['archive_time'] = _( "Archive time must be later than lifesupport time.") if self.language.startswith("|"): langs = list(filter( None, self.language.split("|"))) # remove pipes & empty strings for lang in langs: if not self.is_valid_language(lang): if "language" in errors: errors['language'] += (", " + lang) else: errors['language'] = _( "Language code(s) missing from settings: ") + lang elif not self.is_valid_language(self.language): errors['language'] = _("Language code missing from settings.") if errors: raise ValidationError(errors) def is_valid_language(self, lang): return lang == "" or lang in [key for key, name in settings.LANGUAGES] def save(self, *args, **kwargs): """ Saves the model. """ super().save(*args, **kwargs) if self.image: resize_image(self.image.path, (800, 600)) def is_assistant(self, user): return (user and user.is_authenticated and isinstance(user, User) and self.assistants.filter(id=user.userprofile.id).exists()) def is_teacher(self, user): return self.course.is_teacher(user) def is_course_staff(self, user): return self.is_teacher(user) or self.is_assistant(user) def is_student(self, user): return (user and user.is_authenticated and isinstance(user, User) and self.students.filter(id=user.userprofile.id).exists()) def is_enrollable(self, user): if self.is_course_staff(user): # Allow course staff to enroll even if the course instance is hidden # or the user does not belong to the enrollment audience. return True if user and user.is_authenticated and self.visible_to_students: if self.enrollment_audience == self.ENROLLMENT_AUDIENCE.INTERNAL_USERS: return not user.userprofile.is_external if self.enrollment_audience == self.ENROLLMENT_AUDIENCE.EXTERNAL_USERS: return user.userprofile.is_external return True return False def enroll_student(self, user): if user and user.is_authenticated: _, created = Enrollment.objects.get_or_create( course_instance=self, user_profile=user.userprofile) return created return False def tag_user(self, user, tag): UserTagging.objects.create(tag=tag, user=user.userprofile, course_instance=self) def get_enrollment_for(self, user): return Enrollment.objects.filter( course_instance=self, user_profile=user.userprofile).first() def get_user_tags(self, user): return self.taggings.filter( user=user.uesrprofile).select_related('tag') def get_course_staff_profiles(self): return UserProfile.objects.filter(Q(teaching_courses=self.course) | Q(assisting_courses=self))\ .distinct() def get_student_profiles(self): return self.students.all() def get_submitted_profiles(self): return UserProfile.objects.filter(submissions__exercise__course_module__course_instance=self)\ .distinct()\ .exclude(assisting_courses=self)\ .exclude(teaching_courses=self.course) def is_open(self, when=None): when = when or timezone.now() return self.starting_time <= when <= self.ending_time def is_past(self, when=None): when = when or timezone.now() return self.ending_time < when def is_on_lifesupport(self, when=None): when = when or timezone.now() return self.lifesupport_start < when def is_archived(self, when=None): when = when or timezone.now() return self.archive_start < when @property def archive_start(self): if self.archive_time: # not null return self.archive_time return self.ending_time + datetime.timedelta(days=365) @property def lifesupport_start(self): if self.lifesupport_time: # not null return self.lifesupport_time return self.ending_time + datetime.timedelta(days=365) @property def enrollment_start(self): return self.enrollment_starting_time or self.starting_time @property def enrollment_end(self): return self.enrollment_ending_time or self.ending_time def is_enrollment_open(self): return self.enrollment_start <= timezone.now() <= self.enrollment_end def has_enrollment_closed(self): return timezone.now() > self.enrollment_end def is_visible_to(self, user=None): if self.visible_to_students: return True return user and self.is_course_staff(user) @property def head_css_urls(self): return [url for url in self.head_urls.split() if ".css" in url] @property def head_js_urls(self): return [url for url in self.head_urls.split() if ".js" in url] ABSOLUTE_URL_NAME = "course" EDIT_URL_NAME = "course-edit" def get_url_kwargs(self): # dict(foo=bar, **baz()) is not nice, but it's cleanest solution for this # specific problem. For more read out stackoverflow answer about merging # python dicts in single line: http://stackoverflow.com/a/26853961 return dict(instance_slug=self.url, **self.course.get_url_kwargs())
class CourseInstance(UrlMixin, models.Model): """ CourseInstance class represent an instance of a course. A single course may have several instances either at the same time or during different years. All instances have the same teacher, but teaching assistants and students are connected to individual instances. """ ENROLLMENT_AUDIENCE = Enum([ ('INTERNAL_USERS', 1, _('INTERNAL_USERS')), ('EXTERNAL_USERS', 2, _('EXTERNAL_USERS')), ('ALL_USERS', 3, _('ALL_USERS')), ]) VIEW_ACCESS = Enum([ ('ENROLLED', 1, _('ENROLLED_STUDENTS')), ('ENROLLMENT_AUDIENCE', 2, _('ENROLLMENT_AUDIENCE')), ('ALL_REGISTERED', 3, _('ALL_REGISTERED_USERS')), ('PUBLIC', 4, _('PUBLIC')), ]) INDEX_TYPE = Enum([ ('RESULTS', 0, _('USER_RESULTS')), ('TOC', 1, _('TABLE_OF_CONTENTS')), ('LAST', 2, _('LAST_VISITED_LINK')), ('EXPERIMENT', 10, _('EXPERIMENTAL_SETUP')), ]) CONTENT_NUMBERING = Enum([ ('NONE', 0, _('NUMBERING_NONE')), ('ARABIC', 1, _('NUMBERING_ARABIC')), ('ROMAN', 2, _('NUMBERING_ROMAN')), ('HIDDEN', 3, _('NUMBERING_HIDDEN_ARABIC')), ]) course = models.ForeignKey( Course, verbose_name=_('LABEL_COURSE'), on_delete=models.CASCADE, related_name="instances", ) instance_name = models.CharField( verbose_name=_('LABEL_INSTANCE_NAME'), max_length=255, ) url = models.CharField( verbose_name=_('LABEL_URL'), max_length=255, blank=False, help_text=_('COURSE_INSTANCE_URL_IDENTIFIER_HELPTEXT'), validators=[generate_url_key_validator()], ) visible_to_students = models.BooleanField( verbose_name=_('LABEL_VISIBLE_TO_STUDENTS'), default=True, ) enrollment_audience = models.IntegerField( verbose_name=_('LABEL_ENROLLMENT_AUDIENCE'), choices=ENROLLMENT_AUDIENCE.choices, default=ENROLLMENT_AUDIENCE.INTERNAL_USERS, ) view_content_to = models.IntegerField( verbose_name=_('LABEL_VIEW_CONTENT_TO'), choices=VIEW_ACCESS.choices, default=VIEW_ACCESS.ENROLLED, ) starting_time = models.DateTimeField( verbose_name=_('LABEL_STARTING_TIME'), ) ending_time = models.DateTimeField(verbose_name=_('LABEL_ENDING_TIME'), ) lifesupport_time = models.DateTimeField( verbose_name=_('LABEL_LIFESUPPORT_TIME'), blank=True, null=True, ) archive_time = models.DateTimeField( verbose_name=_('LABEL_ARCHIVE_TIME'), blank=True, null=True, ) enrollment_starting_time = models.DateTimeField( verbose_name=_('LABEL_ENROLLMENT_STARTING_TIME'), blank=True, null=True, ) enrollment_ending_time = models.DateTimeField( verbose_name=_('LABEL_ENROLLMENT_ENDING_TIME'), blank=True, null=True, ) image = models.ImageField( verbose_name=_('LABEL_IMAGE'), blank=True, null=True, upload_to=build_upload_dir, ) language = models.CharField( verbose_name=_('LABEL_LANGUAGE'), max_length=255, blank=True, default="", ) description = models.TextField( verbose_name=_('LABEL_DESCRIPTION'), blank=True, ) footer = models.TextField( verbose_name=_('LABEL_FOOTER'), blank=True, ) index_mode = models.IntegerField( verbose_name=_('LABEL_INDEX_MODE'), choices=INDEX_TYPE.choices, default=INDEX_TYPE.RESULTS, help_text=_('COURSE_INSTANCE_INDEX_CONTENT_SELECTION_HELPTEXT'), ) module_numbering = models.IntegerField( verbose_name=_('LABEL_MODULE_NUMBERING'), choices=CONTENT_NUMBERING.choices, default=CONTENT_NUMBERING.ARABIC, ) content_numbering = models.IntegerField( verbose_name=_('LABEL_CONTENT_NUMBERING'), choices=CONTENT_NUMBERING.choices, default=CONTENT_NUMBERING.ARABIC, ) head_urls = models.TextField( verbose_name=_('LABEL_HEAD_URLS'), blank=True, help_text=_( 'COURSE_INSTANCE_EXTERNAL_CSS_AND_JS_FOR_ALL_PAGES_HELPTEXT'), ) configure_url = models.URLField(blank=True) build_log_url = models.URLField(blank=True) last_modified = models.DateTimeField(auto_now=True, blank=True, null=True) technical_error_emails = models.CharField( verbose_name=_('LABEL_TECHNICAL_ERROR_EMAILS'), max_length=255, blank=True, help_text=_( 'COURSE_INSTANCE_EXERCISE_ERROR_EMAIL_RECIPIENT_OVERRIDE_HELPTEXT' ), ) plugins = GenericRelation(BasePlugin, object_id_field="container_pk", content_type_field="container_type") tabs = GenericRelation(BaseTab, object_id_field="container_pk", content_type_field="container_type") sis_id = models.CharField( verbose_name=_('LABEL_SIS_IDENTIFIER'), max_length=255, blank=True, default="", ) sis_enroll = models.BooleanField( verbose_name=_('LABEL_SIS_ENROLL'), default=False, help_text=_('COURSE_INSTANCE_SIS_ENROLL_HELP')) # usertags from course.models.UserTag # taggings from course.models.UserTagging # categories from course.models.LearningObjectCategory # course_modules from course.models.CourseModule objects = CourseInstanceManager() class Meta: verbose_name = _('MODEL_NAME_COURSE_INSTANCE') verbose_name_plural = _('MODEL_NAME_COURSE_INSTANCE_PLURAL') unique_together = ("course", "url") def __str__(self): return "{}: {}".format(str(self.course), self.instance_name) def clean(self): super().clean() errors = {} RESERVED = ("instances", ) if self.instance_name in RESERVED: errors['instance_name'] = format_lazy( _('COURSE_INSTANCE_ERROR_INSTANCE_NAME -- {}'), self.instance_name) if self.url in RESERVED: errors['url'] = format_lazy(_('COURSE_INSTANCE_ERROR_URL -- {}'), self.url) if self.ending_time <= self.starting_time: errors['ending_time'] = _( 'COURSE_INSTANCE_ERROR_ENDING_TIME_BEFORE_STARTING') if self.lifesupport_time and self.lifesupport_time < self.ending_time: errors['lifesupport_time'] = _( 'COURSE_INSTANCE_ERROR_LIFESUPPORT_TIME_BEFORE_ENDING') if (self.archive_time and not self.lifesupport_time) \ or (self.lifesupport_time and not self.archive_time): # Must not set only one of lifesupport and archive time since their # default values could change their order. Lifesupport time must not # be earlier than archive time. errors['archive_time'] = _( 'COURSE_INSTANCE_ERROR_ARCHIVE_TIME_AND_LIFESUPPORT_ONLY_ONE_SET' ) elif self.archive_time and self.archive_time < self.lifesupport_time: errors['archive_time'] = _( 'COURSE_INSTANCE_ERROR_ARCHIVE_TIME_BEFORE_LIFESUPPORT') if self.language.startswith("|"): langs = list(filter( None, self.language.split("|"))) # remove pipes & empty strings for lang in langs: if not self.is_valid_language(lang): if "language" in errors: errors['language'] += (", " + lang) else: errors['language'] = _( 'COURSE_INSTANCE_ERROR_LANGUAGE(S)_MISSING_FROM_SETTINGS' ) + lang elif not self.is_valid_language(self.language): errors['language'] = _( 'COURSE_INSTANCE_ERROR_LANGUAGE_MISSING_FROM_SETTINGS') if errors: raise ValidationError(errors) def is_valid_language(self, lang): return lang == "" or lang in [key for key, name in settings.LANGUAGES] @property def languages(self): return self.language.strip('|').split('|') @property def default_language(self): language = self.language language_code = language.lstrip('|').split('|', 1)[0] if language_code: return language_code return settings.LANGUAGE_CODE.split('-', 1)[0] @property def students(self): return UserProfile.objects.filter( enrollment__role=Enrollment.ENROLLMENT_ROLE.STUDENT, enrollment__status=Enrollment.ENROLLMENT_STATUS.ACTIVE, enrollment__course_instance=self) @property def all_students(self): return UserProfile.objects.filter( enrollment__role=Enrollment.ENROLLMENT_ROLE.STUDENT, enrollment__course_instance=self).annotate( enrollment_status=F('enrollment__status')) @property def assistants(self): return UserProfile.objects.filter( enrollment__role=Enrollment.ENROLLMENT_ROLE.ASSISTANT, enrollment__status=Enrollment.ENROLLMENT_STATUS.ACTIVE, enrollment__course_instance=self) @property def teachers(self): return UserProfile.objects.filter( enrollment__role=Enrollment.ENROLLMENT_ROLE.TEACHER, enrollment__status=Enrollment.ENROLLMENT_STATUS.ACTIVE, enrollment__course_instance=self) @property def course_staff(self): return UserProfile.objects.filter( Q(enrollment__role=Enrollment.ENROLLMENT_ROLE.TEACHER) | Q(enrollment__role=Enrollment.ENROLLMENT_ROLE.ASSISTANT), enrollment__status=Enrollment.ENROLLMENT_STATUS.ACTIVE, enrollment__course_instance=self) @property def course_staff_and_students(self): return UserProfile.objects.filter( enrollment__status=Enrollment.ENROLLMENT_STATUS.ACTIVE, enrollment__course_instance=self) def save(self, *args, **kwargs): """ Saves the model. """ super().save(*args, **kwargs) if self.image: resize_image(self.image.path, (800, 600)) def is_assistant(self, user): return (user and user.is_authenticated and isinstance(user, User) and self.assistants.filter(id=user.userprofile.id).exists()) def is_teacher(self, user): return ( user and user.is_authenticated and (user.is_superuser or (isinstance(user, User) and self.teachers.filter(id=user.userprofile.id).exists()) or (isinstance(user, GraderUser) and (Permission.WRITE, self.course) in user.permissions.courses))) def is_course_staff(self, user): return self.is_teacher(user) or self.is_assistant(user) def is_student(self, user): return (user and user.is_authenticated and isinstance(user, User) and self.students.filter(id=user.userprofile.id).exists()) def is_banned(self, user): return (user and user.is_authenticated and isinstance(user, User) and self.all_students.filter( enrollment__status=Enrollment.ENROLLMENT_STATUS.BANNED, id=user.userprofile.id, ).exists()) def is_enrollable(self, user): if self.is_course_staff(user): # Allow course staff to enroll even if the course instance is hidden # or the user does not belong to the enrollment audience. return True enrollment = self.get_enrollment_for(user) if enrollment and enrollment.status == Enrollment.ENROLLMENT_STATUS.BANNED: return False if user and user.is_authenticated and self.visible_to_students: if self.enrollment_audience == self.ENROLLMENT_AUDIENCE.INTERNAL_USERS: return not user.userprofile.is_external if self.enrollment_audience == self.ENROLLMENT_AUDIENCE.EXTERNAL_USERS: return user.userprofile.is_external return True return False def enroll_student(self, user, from_sis=False): # Return value False indicates whether that the user was already enrolled. if user and user.is_authenticated: try: enrollment = Enrollment.objects.get( course_instance=self, user_profile=user.userprofile, ) if (enrollment.role == Enrollment.ENROLLMENT_ROLE.STUDENT and enrollment.status == Enrollment.ENROLLMENT_STATUS.ACTIVE): if not enrollment.from_sis and from_sis: enrollment.from_sis = from_sis enrollment.save() return False enrollment.role = Enrollment.ENROLLMENT_ROLE.STUDENT enrollment.status = Enrollment.ENROLLMENT_STATUS.ACTIVE enrollment.from_sis = from_sis enrollment.save() return True except Enrollment.DoesNotExist: Enrollment.objects.create( course_instance=self, user_profile=user.userprofile, role=Enrollment.ENROLLMENT_ROLE.STUDENT, status=Enrollment.ENROLLMENT_STATUS.ACTIVE, from_sis=from_sis, ) return True return False def enroll_from_sis(self) -> int: """ Enroll students based on the participants information in Student Info System. If student has removed herself in SIS, she will also be marked as removed in A+. Returns ------- Number of students enrolled based on this call. -1 if there was problem accessing SIS. """ from .sis import get_sis_configuration, StudentInfoSystem from .cache.menu import invalidate_content sis: StudentInfoSystem = get_sis_configuration() if not sis: return -1 count = 0 try: participants = sis.get_participants(self.sis_id) except Exception as e: logger.exception(f"Error in getting participants from SIS.") return -1 for i in participants: try: profile = UserProfile.get_by_student_id(i) if self.enroll_student(profile.user, from_sis=True): count = count + 1 except UserProfile.DoesNotExist: # This is a common scenario, if the user has enrolled in SIS, but not # yet logged in to A+, then the user profile does not exist yet. pass # Remove SIS-enrolled students who are not anymore in SIS participants, # for example, because they have first enrolled in SIS, but then # unenrolled themselves. students = self.all_students.filter(enrollment__from_sis=True) to_remove = students.exclude(student_id__in=participants) qs = Enrollment.objects.filter(user_profile__in=to_remove, course_instance=self) qs.update(status=Enrollment.ENROLLMENT_STATUS.REMOVED) for e in qs: invalidate_content(Enrollment, e) return count def set_users_with_role(self, users, role, remove_others_with_role=False): # This method is used for adding or replacing (depending on the last # parameter) users with a specific role, e.g. teachers and assistants. # It is recommended to use the convenience methods (starting with # "add"/"clear"/"set") for common use cases. for user in users: Enrollment.objects.update_or_create( course_instance=self, user_profile=user, defaults={ 'role': role, 'status': Enrollment.ENROLLMENT_STATUS.ACTIVE, }, ) if remove_others_with_role: for enrollment in Enrollment.objects.filter( role=role, status=Enrollment.ENROLLMENT_STATUS.ACTIVE, course_instance=self): if enrollment.user_profile not in users: enrollment.status = Enrollment.ENROLLMENT_STATUS.REMOVED enrollment.save() def add_assistant(self, user): self.set_users_with_role([user], Enrollment.ENROLLMENT_ROLE.ASSISTANT) def clear_assistants(self): self.set_users_with_role([], Enrollment.ENROLLMENT_ROLE.ASSISTANT, remove_others_with_role=True) def set_assistants(self, users): self.set_users_with_role(users, Enrollment.ENROLLMENT_ROLE.ASSISTANT, remove_others_with_role=True) def add_teacher(self, user): self.set_users_with_role([user], Enrollment.ENROLLMENT_ROLE.TEACHER) def clear_teachers(self): self.set_users_with_role([], Enrollment.ENROLLMENT_ROLE.TEACHER, remove_others_with_role=True) def set_teachers(self, users): self.set_users_with_role(users, Enrollment.ENROLLMENT_ROLE.TEACHER, remove_others_with_role=True) def tag_user(self, user, tag): UserTagging.objects.create(tag=tag, user=user.userprofile, course_instance=self) def get_enrollment_for(self, user): try: return Enrollment.objects.get(course_instance=self, user_profile=user.userprofile) except Enrollment.DoesNotExist: return None def get_user_tags(self, user): return self.taggings.filter( user=user.uesrprofile).select_related('tag') def get_course_staff_profiles(self): return self.course_staff.all() def get_student_profiles(self): return self.students.all() def get_submitted_profiles(self): return UserProfile.objects\ .filter(submissions__exercise__course_module__course_instance=self)\ .distinct()\ .exclude( Q(enrollment__role=Enrollment.ENROLLMENT_ROLE.TEACHER) | Q(enrollment__role=Enrollment.ENROLLMENT_ROLE.ASSISTANT), enrollment__status=Enrollment.ENROLLMENT_STATUS.ACTIVE, enrollment__course_instance=self) def is_open(self, when=None): when = when or timezone.now() return self.starting_time <= when <= self.ending_time def is_past(self, when=None): when = when or timezone.now() return self.ending_time < when def is_on_lifesupport(self, when=None): when = when or timezone.now() return self.lifesupport_start < when def is_archived(self, when=None): when = when or timezone.now() return self.archive_start < when @property def archive_start(self): if self.archive_time: # not null return self.archive_time return self.ending_time + datetime.timedelta(days=365) @property def lifesupport_start(self): if self.lifesupport_time: # not null return self.lifesupport_time return self.ending_time + datetime.timedelta(days=365) @property def enrollment_start(self): return self.enrollment_starting_time or self.starting_time @property def enrollment_end(self): return self.enrollment_ending_time or self.ending_time def is_enrollment_open(self): return self.enrollment_start <= timezone.now() <= self.enrollment_end def has_enrollment_closed(self): return timezone.now() > self.enrollment_end def is_visible_to(self, user=None): if self.visible_to_students: return True return user and self.is_course_staff(user) @property def head_css_urls(self): return [url for url in self.head_urls.split() if ".css" in url] @property def head_js_urls(self): return [url for url in self.head_urls.split() if ".js" in url] ABSOLUTE_URL_NAME = "course" EDIT_URL_NAME = "course-edit" def get_url_kwargs(self): # dict(foo=bar, **baz()) is not nice, but it's cleanest solution for this # specific problem. For more read out stackoverflow answer about merging # python dicts in single line: http://stackoverflow.com/a/26853961 return dict(instance_slug=self.url, **self.course.get_url_kwargs())
class CourseModule(UrlMixin, models.Model): """ CourseModule objects connect chapters and learning objects to logical sets of each other and course instances. They also contain information about the opening times and deadlines for exercises. """ STATUS = Enum([ ('READY', 'ready', _('STATUS_READY')), ('UNLISTED', 'unlisted', _('STATUS_UNLISTED')), ('HIDDEN', 'hidden', _('STATUS_HIDDEN')), ('MAINTENANCE', 'maintenance', _('STATUS_MAINTENANCE')), ]) status = models.CharField( verbose_name=_('LABEL_STATUS'), max_length=32, choices=STATUS.choices, default=STATUS.READY, ) order = models.IntegerField( verbose_name=_('LABEL_ORDER'), default=1, ) name = models.CharField( verbose_name=_('LABEL_NAME'), max_length=255, ) url = models.CharField( verbose_name=_('LABEL_URL'), max_length=255, help_text=_('MODULE_URL_IDENTIFIER_HELPTEXT'), validators=[generate_url_key_validator()], ) points_to_pass = models.PositiveIntegerField( verbose_name=_('LABEL_POINTS_TO_PASS'), default=0) introduction = models.TextField( verbose_name=_('LABEL_INTRODUCTION'), blank=True, ) course_instance = models.ForeignKey( CourseInstance, verbose_name=_('LABEL_COURSE_INSTANCE'), on_delete=models.CASCADE, related_name="course_modules", ) reading_opening_time = models.DateTimeField( verbose_name=_('LABEL_READING_OPENING_TIME'), blank=True, null=True, help_text=_('MODULE_READING_OPENING_TIME_HELPTEXT'), ) opening_time = models.DateTimeField( verbose_name=_('LABEL_EXERCISE_OPENING_TIME'), default=timezone.now) closing_time = models.DateTimeField( verbose_name=_('LABEL_CLOSING_TIME'), default=timezone.now, help_text=_('MODULE_CLOSING_TIME_HELPTEXT'), ) # early_submissions_allowed= models.BooleanField(default=False) # early_submissions_start = models.DateTimeField(default=timezone.now, blank=True, null=True) # early_submission_bonus = PercentField(default=0.1, # help_text=_("Multiplier of points to reward, as decimal. 0.1 = 10%")) late_submissions_allowed = models.BooleanField( verbose_name=_('LABEL_LATE_SUBMISSIONS_ALLOWED'), default=False, ) late_submission_deadline = models.DateTimeField( verbose_name=_('LABEL_LATE_SUBMISSION_DEADLINE'), default=timezone.now, ) late_submission_penalty = PercentField( verbose_name=_('LABEL_LATE_SUBMISSION_PENALTY'), default=0.5, help_text=_('MODULE_LATE_SUBMISSION_PENALTY_HELPTEXT'), ) objects = CourseModuleManager() class Meta: verbose_name = _('MODEL_NAME_COURSE_MODULE') verbose_name_plural = _('MODEL_NAME_COURSE_MODULE_PLURAL') unique_together = ("course_instance", "url") ordering = ['order', 'closing_time', 'id'] def __str__(self): if self.order > 0: if self.course_instance.module_numbering == CourseInstance.CONTENT_NUMBERING.ARABIC: return "{:d}. {}".format(self.order, self.name) elif self.course_instance.module_numbering == CourseInstance.CONTENT_NUMBERING.ROMAN: return "{} {}".format(roman_numeral(self.order), self.name) return self.name def clean(self): super().clean() errors = {} RESERVED = ("toc", "teachers", "user", "exercises", "apps", "lti-login") if self.url in RESERVED: errors['url'] = format_lazy(_('TAKEN_WORDS_INCLUDE -- {}'), ", ".join(RESERVED)) if self.opening_time > self.closing_time: errors['opening_time'] = _( 'MODULE_ERROR_OPENING_TIME_AFTER_CLOSING_TIME') if self.late_submissions_allowed and self.late_submission_deadline <= self.closing_time: errors['late_submission_deadline'] = _( 'MODULE_ERROR_LATE_SUBMISSION_DL_BEFORE_CLOSING_TIME') if self.reading_opening_time and self.reading_opening_time > self.opening_time: errors['reading_opening_time'] = _( 'MODULE_ERROR_READING_OPENING_TIME_AFTER_EXERCISE_OPENING') if errors: raise ValidationError(errors) def is_open(self, when=None): when = when or timezone.now() if self.reading_opening_time: return self.reading_opening_time <= when <= self.closing_time return self.opening_time <= when <= self.closing_time def is_after_open(self, when=None): """ Checks if current time is past the round opening time. """ when = when or timezone.now() if self.reading_opening_time: return self.reading_opening_time <= when return self.opening_time <= when def have_exercises_been_opened(self, when=None): when = when or timezone.now() return self.opening_time <= when def exercises_open(self, when=None): when = when or timezone.now() return self.opening_time <= when <= self.closing_time def is_late_submission_open(self, when=None): when = when or timezone.now() return self.late_submissions_allowed \ and self.closing_time <= when <= self.late_submission_deadline def is_closed(self, when=None): when = when or timezone.now() if self.late_submissions_allowed and self.late_submission_penalty < 1: return when > self.late_submission_deadline return when > self.closing_time def are_requirements_passed(self, cached_points): for r in self.requirements.all(): if not r.is_passed(cached_points): return False return True def get_late_submission_point_worth(self): """ Returns the percentage (0-100) that late submission points are worth. """ point_worth = 0 if self.late_submissions_allowed: point_worth = int((1.0 - self.late_submission_penalty) * 100.0) return point_worth def number_of_submitters(self): return self.course_instance.students\ .filter(submissions__exercise__course_module=self).distinct().count() ABSOLUTE_URL_NAME = "module" def get_url_kwargs(self): return dict(module_slug=self.url, **self.course_instance.get_url_kwargs())
class CloneInstanceForm(forms.Form): url = forms.CharField( label=_('COURSE_NEW_URL_IDENTIFIER_COURSE_INSTANCE'), validators=[generate_url_key_validator()], ) teachers = forms.BooleanField( label=_('LABEL_TEACHERS'), required=False, initial=True, ) assistants = forms.BooleanField( label=_('LABEL_ASSISTANTS'), required=False, initial=True, ) categories = forms.BooleanField( label=_('LABEL_EXERCISE_CATEGORIES'), required=False, initial=True, ) modules = forms.BooleanField( label=_('LABEL_COURSE_MODULES'), required=False, initial=True, ) chapters = forms.BooleanField( label=_('LABEL_CHAPTERS'), required=False, initial=True, ) exercises = forms.BooleanField( label=_('LABEL_EXERCISES'), required=False, initial=True, ) menuitems = forms.BooleanField( label=_('LABEL_MENU_ITEMS'), required=False, initial=True, ) usertags = forms.BooleanField( label=_('LABEL_STUDENT_TAGS'), required=False, initial=True, ) def set_sis_selector(self) -> None: sis: StudentInfoSystem = get_sis_configuration() if not sis: # Student Info System not configured return try: instances = sis.get_instances(self.instance.course.code) # If there are no SIS instances by this course code, don't show menu if instances: options = [('none', '---------')] + instances self.fields['sis'] = forms.ChoiceField( choices=options, label=_('LABEL_SIS_INSTANCE'), ) except Exception as e: logger.exception("Error getting instances from SIS.") def __init__(self, *args, **kwargs): self.instance = kwargs.pop('instance') super().__init__(*args, **kwargs) self.set_sis_selector() def clean_url(self): url = self.cleaned_data['url'] if CourseInstance.objects.filter(course=self.instance.course, url=url).exists(): raise ValidationError(_('ERROR_URL_ALREADY_TAKEN')) return url def clean(self): errors = {} if self.cleaned_data['chapters'] or self.cleaned_data['exercises']: if not self.cleaned_data['categories']: errors['categories'] = _( 'ERROR_CATEGORIES_NEED_CLONING_TO_CLONE_CHAPTERS_AND_EXERCISES' ) if not self.cleaned_data['modules']: errors['modules'] = _( 'ERROR_MODULES_NEED_CLONING_TO_CLONE_CHAPTERS_AND_EXERCISES' ) if errors: raise ValidationError(errors)