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']
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")
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())
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())
class CourseModule(UrlMixin, models.Model): """ CourseModule objects connect chapters and learning objects to logical sets of each other and course instances. They also contain information about the opening times and deadlines for exercises. """ STATUS = Enum([ ('READY', 'ready', _('STATUS_READY')), ('UNLISTED', 'unlisted', _('STATUS_UNLISTED')), ('HIDDEN', 'hidden', _('STATUS_HIDDEN')), ('MAINTENANCE', 'maintenance', _('STATUS_MAINTENANCE')), ]) status = models.CharField( verbose_name=_('LABEL_STATUS'), max_length=32, choices=STATUS.choices, default=STATUS.READY, ) order = models.IntegerField( verbose_name=_('LABEL_ORDER'), default=1, ) name = models.CharField( verbose_name=_('LABEL_NAME'), max_length=255, ) url = models.CharField( verbose_name=_('LABEL_URL'), max_length=255, help_text=_('MODULE_URL_IDENTIFIER_HELPTEXT'), validators=[generate_url_key_validator()], ) points_to_pass = models.PositiveIntegerField( verbose_name=_('LABEL_POINTS_TO_PASS'), default=0) introduction = models.TextField( verbose_name=_('LABEL_INTRODUCTION'), blank=True, ) course_instance = models.ForeignKey( CourseInstance, verbose_name=_('LABEL_COURSE_INSTANCE'), on_delete=models.CASCADE, related_name="course_modules", ) reading_opening_time = models.DateTimeField( verbose_name=_('LABEL_READING_OPENING_TIME'), blank=True, null=True, help_text=_('MODULE_READING_OPENING_TIME_HELPTEXT'), ) opening_time = models.DateTimeField( verbose_name=_('LABEL_EXERCISE_OPENING_TIME'), default=timezone.now) closing_time = models.DateTimeField( verbose_name=_('LABEL_CLOSING_TIME'), default=timezone.now, help_text=_('MODULE_CLOSING_TIME_HELPTEXT'), ) # early_submissions_allowed= models.BooleanField(default=False) # early_submissions_start = models.DateTimeField(default=timezone.now, blank=True, null=True) # early_submission_bonus = PercentField(default=0.1, # help_text=_("Multiplier of points to reward, as decimal. 0.1 = 10%")) late_submissions_allowed = models.BooleanField( verbose_name=_('LABEL_LATE_SUBMISSIONS_ALLOWED'), default=False, ) late_submission_deadline = models.DateTimeField( verbose_name=_('LABEL_LATE_SUBMISSION_DEADLINE'), default=timezone.now, ) late_submission_penalty = PercentField( verbose_name=_('LABEL_LATE_SUBMISSION_PENALTY'), default=0.5, help_text=_('MODULE_LATE_SUBMISSION_PENALTY_HELPTEXT'), ) objects = CourseModuleManager() class Meta: verbose_name = _('MODEL_NAME_COURSE_MODULE') verbose_name_plural = _('MODEL_NAME_COURSE_MODULE_PLURAL') unique_together = ("course_instance", "url") ordering = ['order', 'closing_time', 'id'] def __str__(self): if self.order > 0: if self.course_instance.module_numbering == CourseInstance.CONTENT_NUMBERING.ARABIC: return "{:d}. {}".format(self.order, self.name) elif self.course_instance.module_numbering == CourseInstance.CONTENT_NUMBERING.ROMAN: return "{} {}".format(roman_numeral(self.order), self.name) return self.name def clean(self): super().clean() errors = {} RESERVED = ("toc", "teachers", "user", "exercises", "apps", "lti-login") if self.url in RESERVED: errors['url'] = format_lazy(_('TAKEN_WORDS_INCLUDE -- {}'), ", ".join(RESERVED)) if self.opening_time > self.closing_time: errors['opening_time'] = _( 'MODULE_ERROR_OPENING_TIME_AFTER_CLOSING_TIME') if self.late_submissions_allowed and self.late_submission_deadline <= self.closing_time: errors['late_submission_deadline'] = _( 'MODULE_ERROR_LATE_SUBMISSION_DL_BEFORE_CLOSING_TIME') if self.reading_opening_time and self.reading_opening_time > self.opening_time: errors['reading_opening_time'] = _( 'MODULE_ERROR_READING_OPENING_TIME_AFTER_EXERCISE_OPENING') if errors: raise ValidationError(errors) def is_open(self, when=None): when = when or timezone.now() if self.reading_opening_time: return self.reading_opening_time <= when <= self.closing_time return self.opening_time <= when <= self.closing_time def is_after_open(self, when=None): """ Checks if current time is past the round opening time. """ when = when or timezone.now() if self.reading_opening_time: return self.reading_opening_time <= when return self.opening_time <= when def have_exercises_been_opened(self, when=None): when = when or timezone.now() return self.opening_time <= when def exercises_open(self, when=None): when = when or timezone.now() return self.opening_time <= when <= self.closing_time def is_late_submission_open(self, when=None): when = when or timezone.now() return self.late_submissions_allowed \ and self.closing_time <= when <= self.late_submission_deadline def is_closed(self, when=None): when = when or timezone.now() if self.late_submissions_allowed and self.late_submission_penalty < 1: return when > self.late_submission_deadline return when > self.closing_time def are_requirements_passed(self, cached_points): for r in self.requirements.all(): if not r.is_passed(cached_points): return False return True def get_late_submission_point_worth(self): """ Returns the percentage (0-100) that late submission points are worth. """ point_worth = 0 if self.late_submissions_allowed: point_worth = int((1.0 - self.late_submission_penalty) * 100.0) return point_worth def number_of_submitters(self): return self.course_instance.students\ .filter(submissions__exercise__course_module=self).distinct().count() ABSOLUTE_URL_NAME = "module" def get_url_kwargs(self): return dict(module_slug=self.url, **self.course_instance.get_url_kwargs())
class 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")
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, })
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")