Exemple #1
0
class CourseDiplomaDesign(models.Model):
    USERGROUP = CourseInstance.ENROLLMENT_AUDIENCE
    course = models.OneToOneField(CourseInstance, on_delete=models.SET_NULL, null=True)
    availability = models.IntegerField(choices=USERGROUP.choices, default=USERGROUP.EXTERNAL_USERS)
    logo = models.ImageField(blank=True, null=True, upload_to=build_upload_dir)
    title = models.TextField(blank=True)
    body = models.TextField(blank=True)
    date = models.CharField(max_length=256)
    signature_name = models.CharField(max_length=256, blank=True)
    signature_title = models.CharField(max_length=256, blank=True)
    small_print = models.TextField(blank=True)
    point_limits = JSONField(blank=True, help_text=(
        "A list of length 5 where each element is the required points for n:th grade."
        "The element can be a list of 2-tuples [[difficulty_level_a, points],[difficulty_level_b, points]]."
    ))
    pad_points = models.BooleanField(default=False, help_text=(
        "If difficulty levels are used the lower level can be padded with higher level points."
    ))
    exercises_to_pass = models.ManyToManyField(BaseExercise, blank=True)
    modules_to_pass = models.ManyToManyField(CourseModule, blank=True)

    class Meta:
        verbose_name = '设计课程证书'
        verbose_name_plural = verbose_name

    def __str__(self):
        return "CourseDiplomaDesign {} for {}".format(self.pk, str(self.course))
Exemple #2
0
class Roll(models.Model):
    results = JSONField()
    timestamp = models.DateTimeField()

    @staticmethod
    def get_cache_key(id):
        return 'roll-%s' % id
Exemple #3
0
class Submission(UrlMixin, models.Model):
    """
    A submission to some course exercise from one or more submitters.
    """
    STATUS = Enum([
        ('INITIALIZED', 'initialized', _("Initialized")),
        ('WAITING', 'waiting', _("In grading")),
        ('READY', 'ready', _("Ready")), # graded normally
        ('ERROR', 'error', _("Error")),
        ('REJECTED', 'rejected', _("Rejected")), # missing fields etc
        ('UNOFFICIAL', 'unofficial', _("No effect on grading")),
        # unofficial: graded after the deadline or after exceeding the submission limit
    ])
    submission_time = models.DateTimeField(auto_now_add=True)
    hash = models.CharField(max_length=32, default=get_random_string)

    # Relations
    exercise = models.ForeignKey(exercise_models.BaseExercise,
        on_delete=models.CASCADE,
        related_name="submissions")
    submitters = models.ManyToManyField(UserProfile,
        related_name="submissions")
    grader = models.ForeignKey(UserProfile, on_delete=models.SET_NULL,
        related_name="graded_submissions", blank=True, null=True)

    # Grading and feedback
    feedback = models.TextField(blank=True)
    assistant_feedback = models.TextField(blank=True)
    status = models.CharField(max_length=32,
        choices=STATUS.choices, default=STATUS.INITIALIZED)
    grade = models.IntegerField(default=0)
    grading_time = models.DateTimeField(blank=True, null=True)
    late_penalty_applied = PercentField(blank=True, null=True)

    # Points received from assessment, before scaled to grade
    service_points = models.IntegerField(default=0)
    service_max_points = models.IntegerField(default=0)

    # Additional data
    submission_data = JSONField(blank=True)
    grading_data = JSONField(blank=True)
    meta_data = JSONField(blank=True)

    objects = SubmissionManager()

    class Meta:
        app_label = 'exercise'
        ordering = ['-id']

    def __str__(self):
        return str(self.id)

    def ordinal_number(self):
        return self.submitters.first().submissions.exclude_errors().filter(
            exercise=self.exercise,
            submission_time__lt=self.submission_time
        ).count() + 1

    def is_submitter(self, user):
        return user and user.is_authenticated and \
            self.submitters.filter(id=user.userprofile.id).exists()

    def add_files(self, files):
        """
        Adds the given files to this submission as SubmittedFile objects.

        @param files: a QueryDict containing files from a POST request
        """
        for key in files:
            for uploaded_file in files.getlist(key):
                self.files.create(
                    file_object=uploaded_file,
                    param_name=key,
                )

    def get_post_parameters(self, request, url):
        """
        Produces submission data for POST as (data_dict, files_dict).
        """
        self._data = {}
        for (key, value) in self.submission_data or {}:
            if key in self._data:
                self._data[key].append(value)
            else:
                self._data[key] = [ value ]

        self._files = {}
        for file in self.files.all().order_by("id"):
            # Requests supports only one file per name in a multipart post.
            self._files[file.param_name] = (
                file.filename,
                open(file.file_object.path, "rb")
            )

        students = list(self.submitters.all())
        if self.is_submitter(request.user):
            user = request.user
        else:
            user = students[0].user if students else None
        self.exercise.as_leaf_class().modify_post_parameters(
            self._data, self._files, user, students, request, url)
        return (self._data, self._files)

    def clean_post_parameters(self):
        for key in self._files.keys():
            self._files[key][1].close()
        del self._files
        del self._data

    def set_points(self, points, max_points, no_penalties=False):
        """
        Sets the points and maximum points for this submissions. If the given
        maximum points are different than the ones for the exercise this
        submission is for, the points will be scaled.

        The method also checks if the submission is late and if it is, by
        default applies the late_submission_penalty set for the
        exercise.course_module. If no_penalties is True, the penalty is not
        applied.
        """
        exercise = self.exercise

        # Evade bad max points in remote service.
        if max_points == 0 and points > 0:
            max_points = exercise.max_points

        # The given points must be between zero and max points
        assert 0 <= points <= max_points

        # If service max points is zero, then exercise max points must be zero
        # too because otherwise adjusted_grade would be ambiguous.
        # Disabled: Teacher is always responsible the exercise can be passed.
        #assert not (max_points == 0 and self.exercise.max_points != 0)

        self.service_points = points
        self.service_max_points = max_points
        self.late_penalty_applied = None

        # Scale the given points to the maximum points for the exercise
        if max_points > 0:
            adjusted_grade = (1.0 * exercise.max_points * points / max_points)
        else:
            adjusted_grade = 0.0

        if not no_penalties:
            timing,_ = exercise.get_timing(self.submitters.all(), self.submission_time)
            if timing in (exercise.TIMING.LATE, exercise.TIMING.CLOSED_AFTER):
                self.late_penalty_applied = (
                    exercise.course_module.late_submission_penalty if
                    exercise.course_module.late_submissions_allowed else 0
                )
                adjusted_grade -= (adjusted_grade * self.late_penalty_applied)
            elif timing == exercise.TIMING.UNOFFICIAL:
                self.status = self.STATUS.UNOFFICIAL
            if self.exercise.no_submissions_left(self.submitters.all()):
                self.status = self.STATUS.UNOFFICIAL

        self.grade = round(adjusted_grade)

        # Finally check that the grade is in bounds after all the math.
        assert 0 <= self.grade <= self.exercise.max_points

    def scale_grade_to(self, percentage):
        percentage = float(percentage)/100
        self.grade = round(max(self.grade*percentage,0))
        self.grade = min(self.grade,self.exercise.max_points)

    def set_waiting(self):
        self.status = self.STATUS.WAITING

    def set_ready(self):
        self.grading_time = timezone.now()
        if self.status != self.STATUS.UNOFFICIAL:
            self.status = self.STATUS.READY

        # Fire set hooks.
        for hook in self.exercise.course_module.course_instance \
                .course_hooks.filter(hook_type="post-grading"):
            hook.trigger({
                "submission_id": self.id,
                "exercise_id": self.exercise.id,
                "course_id": self.exercise.course_module.course_instance.id,
                "site": settings.BASE_URL,
            })

    def set_rejected(self):
        self.status = self.STATUS.REJECTED

    def set_error(self):
        self.status = self.STATUS.ERROR

    @property
    def is_graded(self):
        return self.status in (self.STATUS.READY, self.STATUS.UNOFFICIAL)

    @property
    def lang(self):
        try:
            return self.meta_data.get('lang', None)
        except AttributeError:
            # Handle cases where database includes null or non dictionary json
            return None

    ABSOLUTE_URL_NAME = "submission"

    def get_url_kwargs(self):
        return dict(submission_id=self.id, **self.exercise.get_url_kwargs())

    def get_inspect_url(self):
        return self.get_url("submission-inspect")
Exemple #4
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
Exemple #5
0
class CourseDiplomaDesign(models.Model):
    USERGROUP = CourseInstance.ENROLLMENT_AUDIENCE
    course = models.OneToOneField(
        CourseInstance,
        verbose_name=_('LABEL_COURSE'),
        on_delete=models.SET_NULL,
        null=True,
    )
    availability = models.IntegerField(
        verbose_name=_('LABEL_AVAILABILTY'),
        choices=USERGROUP.choices,
        default=USERGROUP.EXTERNAL_USERS,
    )
    logo = models.ImageField(
        verbose_name=_('LABEL_LOGO'),
        blank=True,
        null=True,
        upload_to=build_upload_dir,
    )
    title = models.TextField(
        verbose_name=_('LABEL_TITLE'),
        blank=True,
    )
    body = models.TextField(
        verbose_name=_('LABEL_BODY'),
        blank=True,
    )
    date = models.CharField(
        verbose_name=_('LABEL_DATE'),
        max_length=256,
    )
    signature_name = models.CharField(
        verbose_name=_('LABEL_SIGNATURE_NAME'),
        max_length=256,
        blank=True,
    )
    signature_title = models.CharField(
        verbose_name=_('LABEL_SIGNATURE_TITLE'),
        max_length=256,
        blank=True,
    )
    small_print = models.TextField(
        verbose_name=_('LABEL_SMALL_PRINT'),
        blank=True,
    )
    point_limits = JSONField(
        verbose_name=_('LABEL_POINT_LIMITS'),
        blank=True,
        help_text=_('DIPLOMA_POINT_LIMITS_HELPTEXT'),
    )
    pad_points = models.BooleanField(
        verbose_name=_('LABEL_PAD_POINTS'),
        default=False,
        help_text=_('DIPLOMA_PAD_POINTS_HELPTEXT'),
    )
    exercises_to_pass = models.ManyToManyField(
        BaseExercise,
        verbose_name=_('LABEL_EXERCISES_TO_PASS'),
        blank=True,
    )
    modules_to_pass = models.ManyToManyField(
        CourseModule,
        verbose_name=_('LABEL_MODULES_TO_PASS'),
        blank=True,
    )

    class Meta:
        verbose_name = _('MODEL_NAME_COURSE_DIPLOMA_DESIGN')
        verbose_name_plural = _('MODEL_NAME_COURSE_DIPLOMA_DESIGN_PLURAL')

    def __str__(self):
        return "CourseDiplomaDesign {} for {}".format(self.pk,
                                                      str(self.course))
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,
                                 related_name="learning_objects")
    course_module = models.ForeignKey(CourseModule,
                                      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=255,
        validators=[RegexValidator(regex="^[\w\-\.]*$")],
        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.URLField(blank=True)
    exercise_info = JSONField(blank=True)
    model_answers = models.TextField(
        blank=True,
        help_text=_("List model answer 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).
        """
        course_instance_error = ValidationError({
            'category':
            _('Course_module and category must belong to the same course instance.'
              )
        })
        try:
            if (self.course_module.course_instance !=
                    self.category.course_instance):
                raise course_instance_error
        except (LearningObjectCategory.DoesNotExist,
                CourseModule.DoesNotExist):
            raise course_instance_error
        if self.parent and (self.parent.course_module != self.course_module
                            or self.parent.id == self.id):
            raise ValidationError({
                'parent':
                _('Cannot select parent from another course module.')
            })
        RESERVED = ("submissions", "plain", "info")
        if self.url in RESERVED:
            raise ValidationError({
                'url':
                _("Taken words include: {}").format(", ".join(RESERVED))
            })

    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 __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.is_open(when=when)

    def is_after_open(self, when=None):
        return self.course_module.is_after_open(when=when)

    def is_closed(self, when=None):
        return self.course_module.is_closed(when=when)

    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_load_url(self, language, request, students, url_name="exercise"):
        return update_url_params(self.service_url, {
            'lang': language,
        })

    def get_models(self):
        return [(url, url.split('/')[-1])
                for url in self.model_answers.split()]
class Submission(models.Model):
    """
    A submission to some course exercise from one or more submitters.
    """
    STATUS_INITIALIZED = "initialized"
    STATUS_WAITING = "waiting"
    STATUS_READY = "ready"
    STATUS_ERROR = "error"
    STATUS_CHOICES = (
        (STATUS_INITIALIZED, _("Initialized")),
        (STATUS_WAITING, _("Waiting")),
        (STATUS_READY, _("Ready")),
        (STATUS_ERROR, _("Error")),
    )

    submission_time = models.DateTimeField(auto_now_add=True)
    hash = models.CharField(max_length=32, default=get_random_string)

    # Relations
    exercise = models.ForeignKey(exercise_models.BaseExercise,
                                 related_name="submissions")
    submitters = models.ManyToManyField(UserProfile,
                                        related_name="submissions")
    grader = models.ForeignKey(UserProfile,
                               related_name="graded_submissions",
                               blank=True,
                               null=True)

    # Grading and feedback
    feedback = models.TextField(blank=True)
    assistant_feedback = models.TextField(blank=True)
    status = models.CharField(max_length=32,
                              choices=STATUS_CHOICES,
                              default=STATUS_INITIALIZED)
    grade = models.IntegerField(default=0)
    grading_time = models.DateTimeField(blank=True, null=True)
    late_penalty_applied = PercentField(blank=True, null=True)

    # Points received from assessment, before scaled to grade
    service_points = models.IntegerField(default=0)
    service_max_points = models.IntegerField(default=0)

    # Additional data
    submission_data = JSONField(blank=True)
    grading_data = JSONField(blank=True)

    objects = SubmissionManager()

    class Meta:
        app_label = 'exercise'
        ordering = ['-submission_time']

    def __str__(self):
        return str(self.id)

    def is_submitter(self, user):
        return user and user.is_authenticated() and \
            self.submitters.filter(id=user.userprofile.id).exists()

    def add_files(self, files):
        """
        Adds the given files to this submission as SubmittedFile objects.

        @param files: a QueryDict containing files from a POST request
        """
        for key in files:
            for uploaded_file in files.getlist(key):
                userfile = SubmittedFile()
                userfile.file_object = uploaded_file
                userfile.param_name = key
                self.files.add(userfile)

    def get_post_parameters(self, request, url):
        """
        Produces submission data for POST as (data_dict, files_dict).
        """
        self._data = {}
        for (key, value) in self.submission_data or {}:
            if key in self._data:
                self._data[key].append(value)
            else:
                self._data[key] = [value]

        self._files = {}
        for file in self.files.all().order_by("id"):
            # Requests supports only one file per name in a multipart post.
            self._files[file.param_name] = (file.filename,
                                            open(file.file_object.path, "rb"))

        if self.is_submitter(request.user):
            user = request.user
        else:
            user = self.submitters.first().user
        self.exercise.as_leaf_class().modify_post_parameters(
            self._data, self._files, user, request.get_host(), url)
        return (self._data, self._files)

    def clean_post_parameters(self):
        for key in self._files.keys():
            self._files[key][1].close()
        del self._files
        del self._data

    def set_points(self, points, max_points, no_penalties=False):
        """
        Sets the points and maximum points for this submissions. If the given
        maximum points are different than the ones for the exercise this
        submission is for, the points will be scaled.

        The method also checks if the submission is late and if it is, by
        default applies the late_submission_penalty set for the
        exercise.course_module. If no_penalties is True, the penalty is not
        applied.
        """

        # The given points must be between zero and max points
        assert 0 <= points <= max_points

        # If service max points is zero, then exercise max points must be zero
        # too because otherwise adjusted_grade would be ambiguous.
        # Disabled: Teacher is always responsible the exercise can be passed.
        #assert not (max_points == 0 and self.exercise.max_points != 0)

        self.service_points = points
        self.service_max_points = max_points

        # Scale the given points to the maximum points for the exercise
        if max_points > 0:
            adjusted_grade = (1.0 * self.exercise.max_points * points /
                              max_points)
        else:
            adjusted_grade = 0.0

        # Check if this submission was done late. If it was, reduce the points
        # with late submission penalty. No less than 0 points are given. This
        # is not done if no_penalties is True.
        if not no_penalties and self.is_late():
            self.late_penalty_applied = \
                self.exercise.course_module.late_submission_penalty \
                if self.exercise.course_module.late_submissions_allowed else 0
            adjusted_grade -= (adjusted_grade * self.late_penalty_applied)
        else:
            self.late_penalty_applied = None

        self.grade = round(adjusted_grade)

        # Finally check that the grade is in bounds after all the math.
        assert 0 <= self.grade <= self.exercise.max_points

    def set_waiting(self):
        self.status = self.STATUS_WAITING

    def set_ready(self):
        self.grading_time = timezone.now()
        self.status = self.STATUS_READY

        # Fire set hooks.
        for hook in self.exercise.course_module.course_instance \
                .course_hooks.filter(hook_type="post-grading"):
            hook.trigger({"submission_id": self.id})

    def set_error(self):
        self.status = self.STATUS_ERROR

    def is_late(self):
        if self.submission_time <= self.exercise.course_module.closing_time:
            return False
        deviation = self.exercise.one_has_deadline_deviation(
            self.submitters.all())
        if deviation and deviation.without_late_penalty\
                and self.submission_time <= deviation.get_new_deadline():
            return False
        return True

    def is_graded(self):
        return self.status == self.STATUS_READY

    def head(self):
        return self.exercise.content_head

    def get_url(self, name):
        exercise = self.exercise
        instance = exercise.course_instance
        return reverse(name,
                       kwargs={
                           "course": instance.course.url,
                           "instance": instance.url,
                           "module": exercise.course_module.url,
                           "exercise_path": exercise.get_path(),
                           "submission_id": self.id,
                       })

    def get_absolute_url(self):
        return self.get_url("submission")

    def get_inspect_url(self):
        return self.get_url("submission-inspect")
Exemple #8
0
class SubmissionDraft(models.Model):
    """
    An incomplete submission that is saved automatically before the user
    submits it. A user can have exactly one draft per exercise instead of
    multiple. The one draft is continuously updated as the user types.
    """
    timestamp = models.DateTimeField(
        verbose_name=_('LABEL_TIMESTAMP'),
        auto_now=True,
    )
    exercise = models.ForeignKey(exercise_models.BaseExercise,
                                 verbose_name=_('LABEL_EXERCISE'),
                                 on_delete=models.CASCADE,
                                 related_name='submission_drafts')
    submitter = models.ForeignKey(UserProfile,
                                  verbose_name=_('LABEL_SUBMITTER'),
                                  on_delete=models.CASCADE,
                                  related_name='submission_drafts')
    submission_data = JSONField(
        verbose_name=_('LABEL_SUBMISSION_DATA'),
        blank=True,
    )
    # This flag is set to False when the student makes an actual submission.
    # This way the draft doesn't have to be deleted and recreated every time
    # the student makes a submission and then starts a new draft.
    active = models.BooleanField(
        verbose_name=_('LABEL_ACTIVE'),
        default=True,
    )

    if TYPE_CHECKING:
        objects: models.Manager['SubmissionDraft']
        id: models.AutoField

    class Meta:
        verbose_name = _('MODEL_NAME_SUBMISSION_DRAFT')
        verbose_name_plural = _('MODEL_NAME_SUBMISSION_DRAFT_PLURAL')
        app_label = 'exercise'
        unique_together = ('exercise', 'submitter')

    def load(self, request: HttpRequest) -> ExercisePage:
        """
        Loads the draft page, i.e. the exercise form with the user's
        incomplete answers filled in.
        """
        enrollment = self.exercise.course_instance.get_enrollment_for(
            request.user)
        if enrollment and enrollment.selected_group:
            students = list(enrollment.selected_group.members.all())
        else:
            students = [request.user.userprofile]

        page = self.exercise.as_leaf_class().load(
            request,
            students,
            url_name='exercise',
        )
        if self.submission_data:
            data = pairs_to_dict(self.submission_data)
            # Format the timestamp so that it can be used in Javascript's Date constructor
            timestamp = str(int(self.timestamp.timestamp() * 1000))
            page.populate_form(field_values=data,
                               data_values={'draft-timestamp': timestamp},
                               allow_submit=True)

        return page
Exemple #9
0
class Submission(UrlMixin, models.Model):
    """
    A submission to some course exercise from one or more submitters.
    """
    STATUS = Enum([
        ('INITIALIZED', 'initialized', _('STATUS_INITIALIZED')),
        ('WAITING', 'waiting', _('STATUS_WAITING')),
        ('READY', 'ready', _('STATUS_READY')),  # graded normally
        ('ERROR', 'error', _('STATUS_ERROR')),
        ('REJECTED', 'rejected', _('STATUS_REJECTED')),  # missing fields etc
        ('UNOFFICIAL', 'unofficial', _('STATUS_UNOFFICIAL')),
        # unofficial: graded after the deadline or after exceeding the submission limit
    ])
    submission_time = models.DateTimeField(
        verbose_name=_('LABEL_SUBMISSION_TIME'),
        auto_now_add=True,
    )
    hash = models.CharField(
        verbose_name=_('LABEL_HASH'),
        max_length=32,
        default=get_random_string,
    )

    # Relations
    exercise = models.ForeignKey(exercise_models.BaseExercise,
                                 verbose_name=_('LABEL_EXERCISE'),
                                 on_delete=models.CASCADE,
                                 related_name="submissions")
    submitters = models.ManyToManyField(UserProfile,
                                        verbose_name=_('LABEL_SUBMITTERS'),
                                        related_name="submissions")
    grader = models.ForeignKey(
        UserProfile,
        verbose_name=_('LABEL_GRADER'),
        on_delete=models.SET_NULL,
        related_name="graded_submissions",
        blank=True,
        null=True,
    )

    # Grading and feedback
    feedback = models.TextField(
        verbose_name=_('LABEL_FEEDBACK'),
        blank=True,
    )
    assistant_feedback = models.TextField(
        verbose_name=_('LABEL_STAFF_FEEDBACK'),
        blank=True,
    )
    status = models.CharField(
        verbose_name=_('LABEL_STATUS'),
        max_length=32,
        choices=STATUS.choices,
        default=STATUS.INITIALIZED,
    )
    grade = models.IntegerField(
        verbose_name=_('LABEL_GRADE'),
        default=0,
    )
    grading_time = models.DateTimeField(
        verbose_name=_('LABEL_GRADING_TIME'),
        blank=True,
        null=True,
    )
    late_penalty_applied = PercentField(
        verbose_name=_('LABEL_LATE_PENALTY_APPLIED'),
        blank=True,
        null=True,
    )
    force_exercise_points = models.BooleanField(
        verbose_name=_('LABEL_FORCE_EXERCISE_POINTS'),
        default=False,
    )

    # Points received from assessment, before scaled to grade
    service_points = models.IntegerField(
        verbose_name=_('LABEL_SERVICE_POINTS'),
        default=0,
    )
    service_max_points = models.IntegerField(
        verbose_name=_('LABEL_SERVICE_MAX_POINTS'),
        default=0,
    )

    # Additional data
    submission_data = JSONField(
        verbose_name=_('LABEL_SUBMISSION_DATA'),
        blank=True,
    )
    grading_data = JSONField(
        verbose_name=_('LABEL_GRADING_DATA'),
        blank=True,
    )
    meta_data = JSONField(
        verbose_name=_('LABEL_META_DATA'),
        blank=True,
    )

    objects = SubmissionManager()

    class Meta:
        verbose_name = _('MODEL_NAME_SUBMISSION')
        verbose_name_plural = _('MODEL_NAME_SUBMISSION_PLURAL')
        app_label = 'exercise'
        ordering = ['-id']

    def __str__(self):
        return str(self.id)

    def ordinal_number(self):
        return self.submitters.first().submissions.exclude_errors().filter(
            exercise=self.exercise,
            submission_time__lt=self.submission_time).count() + 1

    def is_submitter(self, user):
        return user and user.is_authenticated and \
            self.submitters.filter(id=user.userprofile.id).exists()

    def add_files(self, files):
        """
        Adds the given files to this submission as SubmittedFile objects.

        @param files: a QueryDict containing files from a POST request
        """
        for key in files:
            for uploaded_file in files.getlist(key):
                self.files.create(
                    file_object=uploaded_file,
                    param_name=key,
                )

    def load(self,
             request: HttpRequest,
             allow_submit: bool = True) -> ExercisePage:
        """
        Loads the submission page, i.e. the exercise form with the submitted
        answers filled in. Not the same as the graded form, which is stored in
        `feedback`.

        The `allow_submit` argument determines if the submit button will be
        shown on the page.
        """
        # Load the exercise page and parse its contents
        submitters = list(self.submitters.all())
        page = self.exercise.as_leaf_class().load(
            request,
            submitters,
            url_name='exercise',
            ordinal=self.ordinal_number(),
        )
        if self.submission_data:
            data = pairs_to_dict(self.submission_data)
            page.populate_form(field_values=data, allow_submit=allow_submit)

        return page

    def get_post_parameters(
            self, request: HttpRequest, url: str
    ) -> Tuple[Dict[str, List[str]], Dict[str, Tuple[str, IO]]]:
        """
        Produces submission data for POST as (data_dict, files_dict).
        """
        if self.submission_data:
            self._data = pairs_to_dict(self.submission_data)
        else:
            self._data = {}

        self._files = {}
        for file in self.files.all().order_by("id"):
            # Requests supports only one file per name in a multipart post.
            self._files[file.param_name] = (file.filename,
                                            open(file.file_object.path, "rb"))

        students = list(self.submitters.all())
        if self.is_submitter(request.user):
            user = request.user
        else:
            user = students[0].user if students else None
        self.exercise.as_leaf_class().modify_post_parameters(
            self._data, self._files, user, students, request, url)
        return (self._data, self._files)

    def clean_post_parameters(self):
        for key in self._files.keys():
            self._files[key][1].close()
        del self._files
        del self._data

    def set_points(self, points, max_points, no_penalties=False):
        """
        Sets the points and maximum points for this submissions. If the given
        maximum points are different than the ones for the exercise this
        submission is for, the points will be scaled.

        The method also checks if the submission is late and if it is, by
        default applies the late_submission_penalty set for the
        exercise.course_module. If no_penalties is True, the penalty is not
        applied.
        """
        exercise = self.exercise

        # Evade bad max points in remote service.
        if max_points == 0 and points > 0:
            max_points = exercise.max_points

        # The given points must be between zero and max points
        assert 0 <= points <= max_points

        # If service max points is zero, then exercise max points must be zero
        # too because otherwise adjusted_grade would be ambiguous.
        # Disabled: Teacher is always responsible the exercise can be passed.
        #assert not (max_points == 0 and self.exercise.max_points != 0)

        self.service_points = points
        self.service_max_points = max_points
        self.late_penalty_applied = None

        # Scale the given points to the maximum points for the exercise
        if max_points > 0:
            adjusted_grade = (1.0 * exercise.max_points * points / max_points)
        else:
            adjusted_grade = 0.0

        if not no_penalties:
            timing, _ = exercise.get_timing(self.submitters.all(),
                                            self.submission_time)
            if timing in (exercise.TIMING.LATE, exercise.TIMING.CLOSED_AFTER):
                self.late_penalty_applied = (
                    exercise.course_module.late_submission_penalty
                    if exercise.course_module.late_submissions_allowed else 0)
                adjusted_grade -= (adjusted_grade * self.late_penalty_applied)
            elif timing == exercise.TIMING.UNOFFICIAL:
                self.status = self.STATUS.UNOFFICIAL
            if self.exercise.no_submissions_left(self.submitters.all()):
                self.status = self.STATUS.UNOFFICIAL

        self.grade = round(adjusted_grade)

        # Finally check that the grade is in bounds after all the math.
        assert 0 <= self.grade <= self.exercise.max_points

    def scale_grade_to(self, percentage):
        percentage = float(percentage) / 100
        self.grade = round(max(self.grade * percentage, 0))
        self.grade = min(self.grade, self.exercise.max_points)

    def set_waiting(self):
        self.status = self.STATUS.WAITING

    def set_ready(self):
        self.grading_time = timezone.now()
        if self.status != self.STATUS.UNOFFICIAL or self.force_exercise_points:
            self.status = self.STATUS.READY

        # Fire set hooks.
        for hook in self.exercise.course_module.course_instance \
                .course_hooks.filter(hook_type="post-grading"):
            hook.trigger({
                "submission_id":
                self.id,
                "exercise_id":
                self.exercise.id,
                "course_id":
                self.exercise.course_module.course_instance.id,
                "site":
                settings.BASE_URL,
            })

    def set_rejected(self):
        self.status = self.STATUS.REJECTED

    def set_error(self):
        self.status = self.STATUS.ERROR

    @property
    def is_graded(self):
        return self.status in (self.STATUS.READY, self.STATUS.UNOFFICIAL)

    @property
    def lang(self):
        try:
            return self.meta_data.get('lang', None)
        except AttributeError:
            # Handle cases where database includes null or non dictionary json
            return None

    ABSOLUTE_URL_NAME = "submission"

    def get_url_kwargs(self):
        return dict(submission_id=self.id, **self.exercise.get_url_kwargs())

    def get_inspect_url(self):
        return self.get_url("submission-inspect")