Exemplo n.º 1
0
 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"]
Exemplo n.º 2
0
 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"]
Exemplo n.º 3
0
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)
Exemplo n.º 4
0
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
Exemplo n.º 5
0
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)
Exemplo n.º 6
0
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)
Exemplo n.º 7
0
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
Exemplo n.º 8
0
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())
Exemplo n.º 9
0
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())
Exemplo n.º 10
0
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())
Exemplo n.º 11
0
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())
Exemplo n.º 12
0
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)