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