class Event(models.Model): """ Represents an event that we want to confirm attendance. """ sheet = models.ForeignKey(AttendanceSheet, related_name='events') date = models.DateField() created = models.DateTimeField() expires = models.DateTimeField() passphrase = models.CharField( _('Passphrase'), max_length=200, help_text=_( 'The passphrase is case-insensitive. We tolerate small typing ' 'errors.' ), ) def update(self, commit=True): """ Regenerate passphrase and increases expiration time. """ new = self.passphrase while new == self.passphrase: new = phrase() self.passphrase = new self.expires += self.sheet.expiration_interval if commit: self.save()
class Sprint(models.Model): """ A sprint """ project = models.ForeignKey(ScrumProject, related_name='sprints') description = models.RichTextField(blank=True) start_date = models.DateTimeField() due_date = models.DateTimeField() duration_weeks = models.PositiveIntegerField(default=1, validators=[non_null]) def next_start_date(self, date=None): """ Return the next valid date that the sprint could start after the given. If no arguments are given, consider the current time. """ date = date or now() return date def attach(self, project, commit=True): """ Associate sprint to project, updating required values. """ date = project.finish_date() self.project = project self.start_date = self.next_start_date(date) self.due_date = self.start_date + one_week * self.duration_weeks if commit: self.save()
class Event(models.Model): """ Represents an event that we want to confirm attendance. """ sheet = models.ForeignKey(AttendanceSheet, related_name='events') date = models.DateField() created = models.DateTimeField() expires = models.DateTimeField() passphrase = models.CharField(max_length=100) def update(self): """ Regenerate passphrase and increases expiration time. """ self.passphrase = new_random_passphrase() self.expires += self.sheet.expiration_interval self.save()
class CodeCarouselItem(models.Orderable): """ A simple state of the code in a SyncCodeActivity. """ activity = models.ParentalKey('cs_core.CodeCarouselActivity', related_name='items') text = models.TextField() timestamp = models.DateTimeField(auto_now=True) # Wagtail admin panels = [ panels.FieldPanel('text', widget=blocks.AceWidget()), ]
class SyncCodeEditItem(models.Model): """ A simple state of the code in a SyncCodeActivity. """ activity = models.ForeignKey(SyncCodeActivity, related_name='data') text = models.TextField() next = models.OneToOneField('self', blank=True, null=True, related_name='previous') timestamp = models.DateTimeField(auto_now=True) @property def prev(self): try: return self.previous except ObjectDoesNotExist: return None
class TestState(models.Model): """ Register iospec expansions for a given question. """ class Meta: unique_together = [('question', 'hash')] question = models.ForeignKey('CodingIoQuestion') hash = models.models.CharField(max_length=32) uuid = models.UUIDField(default=uuid.uuid4, editable=False) created = models.DateTimeField(auto_now_add=True) pre_tests_source = models.TextField(blank=True) post_tests_source = models.TextField(blank=True) pre_tests_source_expansion = models.TextField(blank=True) post_tests_source_expansion = models.TextField(blank=True) @property def is_current(self): return self.hash == self.question.test_state_hash def __str__(self): status = 'current' if self.is_current else 'outdated' return 'TestState for %s (%s)' % (self.question, status)
class Deadline(models.Model): """ Describes a deadline of an activity. Users may define soft/hard deadlines. """ name = models.CharField( _('name'), max_length=140, blank=True, help_text=_( 'A unique string identifier. Useful for creating human-friendly ' 'references to the deadline object.')) start = models.DateField( _('start'), blank=True, null=True, ) deadline = models.DateTimeField( _('deadline'), blank=True, null=True, ) hard_deadline = models.DateTimeField( _('hard deadline'), blank=True, null=True, help_text=_( 'If set, responses submitted after the deadline will be accepted ' 'with a penalty.')) penalty = models.DecimalField( _('delay penalty'), default=25, decimal_places=2, max_digits=6, help_text=_( 'Sets the percentage of the total grade that will be lost due to ' 'delayed responses.'), ) def get_status(self): """ Return one of the strings depending on how the current time relates to the deadline: closed: Activity has not opened yet. valid: Current time is within the deadline. expired: Hard deadline has expired. Users cannot submit to the activity. penalty: Official deadline has expired, but users can still submit with a penalty. """ now = timezone.now() if self.start is not None and now < self.start: return 'closed' elif ((self.hard_deadline is not None and now > self.hard_deadline) or (self.hard_deadline is None and self.deadline is not None and now > self.deadline)): return 'expired' elif (self.hard_deadline is not None and now < self.hard_deadline and self.deadline is not None and now > self.deadline): return 'penalty' else: return 'valid' def get_penalty(self): """ Return the penalty value """ status = self.get_status() if status == 'expired': return Decimal(100) elif status == 'penalty': return self.penalty elif status == 'valid': return Decimal(0) else: raise RuntimeError('cannot get penalty of closed activity') def revise_grade(self, grade): """ Return the update grade considering any possible delay penalty. """ return (100 - self.get_penalty()) * Decimal(grade) / 100
class Progress(CommitMixin, models.CopyMixin, models.StatusModel, models.TimeStampedModel, models.PolymorphicModel): """ When an user starts an activity it opens a Progress object which control all submissions to the given activity. The Progress object also manages individual submissions that may span several http requests. """ class Meta: unique_together = [('user', 'activity_page')] verbose_name = _('student progress') verbose_name_plural = _('student progress list') STATUS_OPENED = 'opened' STATUS_CLOSED = 'closed' STATUS_INCOMPLETE = 'incomplete' STATUS_WAITING = 'waiting' STATUS_INVALID = 'invalid' STATUS_DONE = 'done' STATUS = models.Choices( (STATUS_OPENED, _('opened')), (STATUS_CLOSED, _('closed')), ) user = models.ForeignKey(models.User, on_delete=models.CASCADE) activity_page = models.ForeignKey(models.Page, on_delete=models.CASCADE) final_grade_pc = models.DecimalField( _('final score'), max_digits=6, decimal_places=3, default=Decimal, help_text=_( 'Final grade given to considering all submissions, penalties, etc.' ), ) given_grade_pc = models.DecimalField( _('grade'), max_digits=6, decimal_places=3, default=Decimal, help_text=_('Final grade before applying any modifier.'), ) finished = models.DateTimeField(blank=True, null=True) best_submission = models.ForeignKey('Submission', blank=True, null=True, related_name='+') points = models.IntegerField(default=0) score = models.IntegerField(default=0) stars = models.FloatField(default=0.0) is_correct = models.BooleanField(default=bool) has_submissions = models.BooleanField(default=bool) has_feedback = models.BooleanField(default=bool) has_post_tests = models.BooleanField(default=bool) objects = ProgressManager() #: The number of submissions num_submissions = property(lambda x: x.submissions.count()) #: Specific activity reference activity = property(lambda x: x.activity_page.specific) activity_id = property(lambda x: x.activity_page_id) #: Has progress mixin interface username = property(lambda x: x.user.username) def __repr__(self): return '<%s: %s>' % (self.__class__.__name__, self) def __str__(self): tries = self.num_submissions user = self.user activity = self.activity grade = '%s pts' % (self.final_grade_pc or 0) fmt = '%s by %s (%s, %s tries)' return fmt % (activity, user, grade, tries) def submit(self, request, payload, recycle=True, commit=True): """ Creates new submission. Args: recycle: If True, recycle submission objects with the same content as the current submission. If a submission exists with the same content as the current submission, it simply returns the previous submission. If recycled, sets the submission.recycled to True. """ submission_class = self.activity.submission_class submission = submission_class(progress=self, **payload) submission.ip_address = get_ip(request) submission.hash = submission.compute_hash() submission.full_clean() # Then check if any submission is equal to some past submission and # then recycle it recyclable = submission_class.objects.recyclable(submission) recyclable = recyclable if recycle else () for possibly_equal in recyclable: if submission.is_equal(possibly_equal): possibly_equal.recycled = True possibly_equal.bump_recycles() return possibly_equal else: return submission.commit(commit) def register_feedback(self, feedback, commit=True): """ This method is called after a submission is graded and produces a feedback. """ submission = feedback.submission # Check if it is the best submission grade = feedback.given_grade_pc if (self.best_submission is None or self.best_submission.feedback.given_grade_pc < grade): self.best_submission = submission # Update grades for activity considering past submissions self.update_grades_from_feedback(feedback) self.commit(commit) def update_grades_from_feedback(self, feedback): """ Update grades from the current progress object from the given feedback. """ # Update grades, keeping always the best grade if self.given_grade_pc < (feedback.given_grade_pc or 0): self.given_grade_pc = feedback.given_grade_pc if self.final_grade_pc < feedback.final_grade_pc: self.final_grade_pc = feedback.final_grade_pc # Update the is_correct field self.is_correct = self.is_correct or feedback.is_correct
class User(AbstractBaseUser, PermissionsMixin): """ Base user model. """ REQUIRED_FIELDS = ['alias', 'name', 'school_id', 'role'] USERNAME_FIELD = 'email' ROLE_STUDENT, ROLE_TEACHER, ROLE_STAFF, ROLE_ADMIN = range(4) ROLE_CHOICES = [ (ROLE_STUDENT, _('Student')), (ROLE_TEACHER, _('Teacher')), (ROLE_STAFF, _('School staff')), (ROLE_ADMIN, _('Administrator')), ] email = models.EmailField( _('E-mail'), db_index=True, unique=True, help_text=_( 'Users can register additional e-mail addresses. This is the ' 'main e-mail address which is used for login.')) name = models.CharField(_('Name'), max_length=140, help_text=_('Full name of the user.')) alias = models.CharField( _('Alias'), max_length=20, help_text=_('Public alias used to identify the user.')) school_id = models.CharField( _('School id'), max_length=20, blank=False, unique=True, validators=[], # TODO: validate school id number help_text=_('Identification number in your school issued id card.')) role = models.IntegerField( _('Main'), choices=ROLE_CHOICES, default=ROLE_STUDENT, help_text=_('User main role in the codeschool platform.')) is_staff = models.BooleanField( _('staff status'), default=False, help_text=_( 'Designates whether the user can log into this admin site.'), ) is_active = models.BooleanField( _('active'), default=True, help_text=_( 'Designates whether this user should be treated as active. ' 'Unselect this instead of deleting accounts.'), ) date_joined = models.DateTimeField(_('date joined'), default=timezone.now) objects = UserManager() # Temporary properties defined for compatibility username = property(lambda x: x.email) @property def profile(self): if self.id is None: return self._lazy_profile try: return self.profile_ref except AttributeError: self.profile_ref = Profile(user=self) return self.profile_ref @profile.setter def profile(self, value): if self.id is None: self._lazy_profile = value else: self.profile_ref = value @lazy def _lazy_profile(self): return Profile(user=self) def save(self, *args, **kwargs): new = self.id is None if new: with transaction.atomic(): super().save(*args, **kwargs) self.profile.save() else: super().save(*args, **kwargs) def get_full_name(self): return self.name.strip() def get_short_name(self): return self.alias def get_absolute_url(self): return reverse('users:profile-detail', args=(self.id, ))
class Progress(models.CopyMixin, models.StatusModel, models.TimeStampedModel, models.PolymorphicModel): """ When an user starts an activity it opens a Progress object which control all submissions to the given activity. The Progress object also manages individual submissions that may span several http requests. """ class Meta: unique_together = [('user', 'activity_page')] verbose_name = _('student progress') verbose_name_plural = _('student progress list') STATUS_OPENED = 'opened' STATUS_CLOSED = 'closed' STATUS_INCOMPLETE = 'incomplete' STATUS_WAITING = 'waiting' STATUS_INVALID = 'invalid' STATUS_DONE = 'done' STATUS = models.Choices( (STATUS_OPENED, _('opened')), (STATUS_CLOSED, _('closed')), ) user = models.ForeignKey(models.User, on_delete=models.CASCADE) activity_page = models.ForeignKey(models.Page, on_delete=models.CASCADE) final_grade_pc = models.DecimalField( _('final score'), max_digits=6, decimal_places=3, default=Decimal, help_text=_( 'Final grade given to considering all submissions, penalties, etc.' ), ) given_grade_pc = models.DecimalField( _('grade'), max_digits=6, decimal_places=3, default=Decimal, help_text=_('Final grade before applying any modifier.'), ) finished = models.DateTimeField(blank=True, null=True) best_submission = models.ForeignKey('Submission', blank=True, null=True, related_name='+') points = models.IntegerField(default=0) score = models.IntegerField(default=0) stars = models.FloatField(default=0.0) is_correct = models.BooleanField(default=bool) has_submissions = models.BooleanField(default=bool) has_feedback = models.BooleanField(default=bool) has_post_tests = models.BooleanField(default=bool) objects = ProgressManager() #: The number of submissions num_submissions = property(lambda x: x.submissions.count()) #: Specific activity reference activity = property(lambda x: x.activity_page.specific) activity_id = property(lambda x: x.activity_page_id) #: Has progress mixin interface username = property(lambda x: x.user.username) def __repr__(self): return '<%s: %s>' % (self.__class__.__name__, self) def __str__(self): tries = self.num_submissions user = self.user activity = self.activity grade = '%s pts' % (self.final_grade_pc or 0) fmt = '%s by %s (%s, %s tries)' return fmt % (activity, user, grade, tries) def __hash__(self): return hash(self.id) def __eq__(self, other): if isinstance(other, Progress): if self.pk is None: return False else: return self.pk == other.pk return NotImplemented def submit(self, request, recycle=True, **kwargs): """ Creates new submission. """ submission_class = self.activity.submission_class submission = submission_class(progress=self, **kwargs) submission.ip_address = get_ip(request) if not recycle: submission.save() return submission # Collect all submissions with the same hash as current one recyclable = submission_class.objects\ .filter(progress=self, hash=submission.compute_hash()) \ .order_by('created') # Then check if any submission is actually equal to the current amongst # all candidates for possibly_equal in recyclable: if submission.is_equal(possibly_equal): possibly_equal.recycled = True possibly_equal.bump_recycles() return possibly_equal else: submission.save() return submission def register_feedback(self, feedback): """ This method is called after a submission is graded and produces a feedback. """ submission = feedback.submission self.update_grades_from_feedback(feedback) if not self.activity.has_submissions: print('first submission') if feedback.is_correct: print('first correct submission') def update_grades_from_feedback(self, feedback): """ Update grades from the current progress object from the given feedback. """ # Update grades if self.given_grade_pc < (feedback.given_grade_pc or 0): self.given_grade_pc = feedback.given_grade_pc # TODO: decide better update strategy if self.final_grade_pc < feedback.final_grade_pc: self.final_grade_pc = feedback.final_grade_pc # # Register points and stars associated with submission. # score_kwargs = {} # final_points = feedback.final_points() # final_stars = feedback.final_stars() # if final_points > self.points: # score_kwargs['points'] = final_points - self.points # self.points = final_points # if final_stars > self.stars: # score_kwargs['stars'] = final_stars - self.stars # self.stars = final_stars # # # If some score has changed, we save the update fields and update the # # corresponding UserScore object # if score_kwargs: # from codeschool.gamification.models import UserScore # self.save(update_fields=score_kwargs.keys()) # score_kwargs['diff'] = True # UserScore.update(self.user, self.activity_page, **score_kwargs) # Update the is_correct field self.is_correct = self.is_correct or feedback.is_correct self.save() def update_from_submissions(self, grades=True, score=True, commit=True, refresh=False): """ Update grades and gamification scores for all submissions. Args: grades, score (bool): choose to update final grades and/or final scores. commit: if True (default), save changes to database. refresh: if True (default), recompute grade from scratch. """ submissions = self.submissions.all() if refresh and submissions.count(): first = submissions.first() if grades: self.final_grade_pc = first.given_grade_pc self.given_grade_pc = first.given_grade_pc if score: self.points = first.points self.stars = first.stars self.score = first.score for submission in submissions: if grades: submission.update_response_grades(commit=False) if score: submission.update_response_score(commit=False) if commit: self.save() def regrade(self, method=None, force_update=False): """ Return the final grade for the user using the given method. If not method is given, it uses the default grading method for the activity. """ activity = self.activity # Choose grading method if method is None and self.final_grade_pc is not None: return self.final_grade_pc elif method is None: grading_method = activity.grading_method else: grading_method = GradingMethod.from_name(activity.owner, method) # Grade response. We save the result to the final_grade_pc attribute if # no explicit grading method is given. grade = grading_method.grade(self) if method is None and (force_update or self.final_grade_pc is None): self.final_grade_pc = grade return grade
class ResponseContext(models.PolymorphicModel): """ Define a different context for a response object. The context group responses into explicit groups and may also be used to define additional constraints on the correct answers. """ class Meta: unique_together = [('activity', 'name')] # Basic activity = models.ParentalKey( 'wagtailcore.Page', related_name='contexts', ) name = models.CharField(_('name'), max_length=140, blank=True, help_text=_('A unique identifier.')) description = models.RichTextField( _('description'), blank=True, ) # Grading and submissions grading_method = models.ForeignKey( 'cs_core.GradingMethod', on_delete=models.SET_DEFAULT, default=grading_method_best, blank=True, help_text=_('Choose the strategy for grading this activity.')) single_submission = models.BooleanField( _('single submission'), default=False, help_text=_( 'If set, students will be allowed to send only a single response.', ), ) # Feedback delayed_feedback = models.BooleanField( _('delayed feedback'), default=False, help_text=_( 'If set, students will be only be able to see the feedback after ' 'the activity expires its deadline.')) # Deadlines deadline = models.DateTimeField( _('deadline'), blank=True, null=True, ) hard_deadline = models.DateTimeField( _('hard deadline'), blank=True, null=True, help_text=_( 'If set, responses submitted after the deadline will be accepted ' 'with a penalty.')) delay_penalty = models.DecimalField( _('delay penalty'), default=25, decimal_places=2, max_digits=6, help_text=_( 'Sets the percentage of the total grade that will be lost due to ' 'delayed responses.'), ) # Programming languages/formats format = models.ForeignKey( 'cs_core.FileFormat', blank=True, null=True, help_text=_( 'Defines the required file format or programming language for ' 'student responses, when applicable.')) # Extra constraints and resources constraints = models.StreamField([], default=[]) resources = models.StreamField([], default=[]) def clean(self): if not isinstance(self.activity, Activity): return ValidationError({ 'parent': _('Parent is not an Activity subclass'), }) super().clean()
class Response(models.CopyMixin, models.StatusModel, models.TimeStampedModel, models.PolymorphicModel, models.ClusterableModel): """ When an user starts an activity it opens a Session object that controls how responses to the given activity will be submitted. The session object manages individual response submissions that may span several http requests. """ class Meta: unique_together = [('user', 'activity_page')] verbose_name = _('final response') verbose_name_plural = _('final responses') STATUS_OPENED = 'opened' STATUS_CLOSED = 'closed' STATUS_INCOMPLETE = 'incomplete' STATUS_WAITING = 'waiting' STATUS_INVALID = 'invalid' STATUS_DONE = 'done' STATUS = models.Choices( (STATUS_OPENED, _('opened')), (STATUS_CLOSED, _('closed')), ) user = models.ForeignKey( models.User, related_name='responses', on_delete=models.CASCADE, ) activity_page = models.ForeignKey( models.Page, related_name='responses', on_delete=models.CASCADE, ) grade = models.DecimalField( _('given grade'), max_digits=6, decimal_places=3, blank=True, null=True, default=0, help_text=_( 'Grade given to response considering all submissions, penalties, ' 'etc.'), ) finish_time = models.DateTimeField( blank=True, null=True, ) points = models.IntegerField(default=0) score = models.IntegerField(default=0) stars = models.FloatField(default=0.0) is_finished = models.BooleanField(default=bool) is_correct = models.BooleanField(default=bool) objects = ResponseManager() #: The number of submissions in the current session. num_submissions = property(lambda x: x.submissions.count()) #: Specific activity reference activity = property(lambda x: x.activity_page.specific) activity_id = property(lambda x: x.activity_page_id) @activity.setter def activity(self, value): self.activity_page = value.page_ptr @classmethod def _get_response(cls, user, activity): """ Return the response object associated with the given user/activity. Create a new response object if it does not exist. """ if user is None or activity is None: raise TypeError( 'Response objects must be bound to an user or activity.') response, create = Response.objects.get_or_create(user=user, activity=activity) return response def __repr__(self): tries = self.num_submissions user = self.user activity = self.activity class_name = self.__class__.__name__ grade = '%s pts' % (self.grade or 0) fmt = '<%s: %s by %s (%s, %s tries)>' return fmt % (class_name, activity, user, grade, tries) def __str__(self): return repr(self) def __hash__(self): return hash(self.id) def __eq__(self, other): if isinstance(other, Response): if self.pk is None: return False else: return self.pk == other.pk return NotImplemented def register_submission(self, submission): """ This method is called when a submission is graded. """ assert submission.response_id == self.id # Register points and stars associated with submission. score_kwargs = {} final_points = submission.final_points() final_stars = submission.final_stars() if final_points > self.points: score_kwargs['points'] = final_points - self.points self.points = final_points if final_stars > self.stars: score_kwargs['stars'] = final_stars - self.stars self.stars = final_stars # If some score has changed, we save the update fields and update the # corresponding UserScore object if score_kwargs: from codeschool.lms.gamification.models import UserScore self.save(update_fields=score_kwargs.keys()) score_kwargs['diff'] = True UserScore.update(self.user, self.activity_page, **score_kwargs) def regrade(self, method=None, force_update=False): """ Return the final grade for the user using the given method. If not method is given, it uses the default grading method for the activity. """ activity = self.activity # Choose grading method if method is None and self.final_grade is not None: return self.final_grade elif method is None: grading_method = activity.grading_method else: grading_method = GradingMethod.from_name(activity.owner, method) # Grade response. We save the result to the final_grade attribute if # no explicit grading method is given. grade = grading_method.grade(self) if method is None and (force_update or self.final_grade is None): self.final_grade = grade return grade