class CourseModule(models.Model):
    """ 
    CourseModule objects connect learning objects to logical sets of each other and 
    course instances. They also contain information about the opening times and 
    deadlines for exercises. 
    """
    name = models.CharField(max_length=30)
    points_to_pass = models.PositiveIntegerField(default=0)

    # A textual introduction to this exercise round
    introduction = models.TextField(blank=True)

    # Relations
    course_instance = models.ForeignKey(CourseInstance,
                                        related_name=u"course_modules")

    # Fields related to the opening of the rounds
    opening_time = models.DateTimeField(default=datetime.now)
    closing_time = models.DateTimeField(default=datetime.now)

    def get_exercises(self):
        return BaseExercise.objects.filter(course_module=self)

    """
    Functionality related to early bonuses has been disabled. The following lines
    are commented out so that they can be restored later if necessary.
    
    # Settings related to early submission bonuses
    early_submissions_allowed= models.BooleanField(default=False)
    early_submissions_start = models.DateTimeField(default=datetime.now, blank=True, null=True)
    early_submission_bonus  = PercentField(default=0.1, 
        help_text=_("Multiplier of points to reward, as decimal. 0.1 = 10%"))
    """
    # Settings that can be used to allow late submissions to exercises
    late_submissions_allowed = models.BooleanField(default=False)
    late_submission_deadline = models.DateTimeField(default=datetime.now)
    late_submission_penalty = PercentField(
        default=0.5,
        help_text=_("Multiplier of points to reduce, as decimal. 0.1 = 10%"))

    def is_late_submission_open(self):
        return self.late_submissions_allowed and \
            self.closing_time <= datetime.now() <= self.late_submission_deadline

    def is_open(self):
        return self.opening_time <= datetime.now() <= self.closing_time

    def __unicode__(self):
        return self.name

    def get_breadcrumb(self):
        """ 
        Returns a list of tuples containing the names and URL 
        addresses of parent objects and self. 
        """
        return self.course_instance.get_breadcrumb()

    class Meta:
        app_label = 'exercise'
        ordering = ['closing_time', 'id']
示例#2
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")
示例#3
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=[RegexValidator(regex="^[\w\-\.]*$")],
        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,
                                        related_name="course_modules")
    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):
        """
        Validates the model before saving (standard method used in Django admin).
        """
        RESERVED = ("toc", "teachers", "user", "exercises", "apps",
                    "lti-login")
        if self.url in RESERVED:
            raise ValidationError({
                'url':
                _("Taken words include: {}").format(", ".join(RESERVED))
            })

    def is_open(self, when=None):
        when = when or timezone.now()
        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()
        return self.opening_time <= when

    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

    ABSOLUTE_URL_NAME = "module"

    def get_url_kwargs(self):
        return dict(module_slug=self.url,
                    **self.course_instance.get_url_kwargs())
示例#4
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,
                              verbose_name='当前状态')
    order = models.IntegerField(default=1, verbose_name='序号')
    name = models.CharField(max_length=255, verbose_name='名称')
    url = models.CharField(
        max_length=255,
        validators=[generate_url_key_validator()],
        help_text=_("Input an URL identifier for this module."),
        verbose_name='utl标识符')
    points_to_pass = models.PositiveIntegerField(default=0,
                                                 verbose_name='通过分数')
    introduction = models.TextField(blank=True, verbose_name='描述')
    course_instance = models.ForeignKey(CourseInstance,
                                        on_delete=models.CASCADE,
                                        related_name="course_modules",
                                        verbose_name='课程实例')
    opening_time = models.DateTimeField(default=timezone.now,
                                        verbose_name='课程开放时间')
    closing_time = models.DateTimeField(default=timezone.now,
                                        verbose_name='课程结束时间')

    # 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,
                                                   verbose_name='是否允许逾期提交')
    late_submission_deadline = models.DateTimeField(default=timezone.now,
                                                    verbose_name='逾期提交截止时间')
    late_submission_penalty = PercentField(
        default=0.5,
        help_text=_("Multiplier of points to reduce, as decimal. 0.1 = 10%"),
        verbose_name='逾期提交惩罚')

    objects = CourseModuleManager()

    class Meta:
        unique_together = ("course_instance", "url")
        ordering = ['order', 'closing_time', 'id']
        verbose_name = '课程模型'
        verbose_name_plural = verbose_name

    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 errors:
            raise ValidationError(errors)

    def is_open(self, when=None):
        when = when or timezone.now()
        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()
        return self.opening_time <= when

    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())
示例#5
0
文件: models.py 项目: apluslms/a-plus
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())
示例#6
0
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")
示例#7
0
文件: models.py 项目: ihantola/a-plus
class CourseModule(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_READY = 'ready'
    STATUS_HIDDEN = 'hidden'
    STATUS_MAINTENANCE = 'maintenance'
    STATUS_CHOICES = (
        (STATUS_READY, _("Ready")),
        (STATUS_HIDDEN, _("Hidden")),
        (STATUS_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=[RegexValidator(regex="^[\w\-\.]*$")],
        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,
                                        related_name="course_modules")
    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 == 1:
                return "{:d}. {}".format(self.order, self.name)
            elif self.course_instance.module_numbering == 2:
                return "{} {}".format(roman_numeral(self.order), self.name)
        return self.name

    def clean(self):
        """
        Validates the model before saving (standard method used in Django admin).
        """
        RESERVED = ("teachers", "user", "exercises", "apps", "lti-login")
        if self.url in RESERVED:
            raise ValidationError({
                'url':
                _("Taken words include: {}").format(", ".join(RESERVED))
            })

    def is_open(self, when=None):
        when = when or timezone.now()
        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()
        return self.opening_time <= when

    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 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 next_module(self):
        return self.course_instance.course_modules\
            .exclude(status='hidden').filter(order__gt=self.order).first()

    def previous_module(self):
        return self.course_instance.course_modules\
            .exclude(status='hidden').filter(order__lt=self.order).last()

    def _children(self):
        if not hasattr(self, '_module_children'):
            self._module_children = ModuleTree(self)
        return self._module_children

    def next(self):
        return self._children().first() or self.next_module()

    def previous(self):
        module = self.previous_module()
        return module._children().last() if module else None

    def flat_learning_objects(self, with_sub_markers=True):
        return self._children().flat(None, with_sub_markers)

    def flat_admin_learning_objects(self, with_sub_markers=True):
        return self._children().flat(None, with_sub_markers, True)

    def get_absolute_url(self):
        instance = self.course_instance
        return reverse('module',
                       kwargs={
                           'course': instance.course.url,
                           'instance': instance.url,
                           'module': self.url,
                       })
示例#8
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")