Exemplo n.º 1
0
class ExpectedUsername(models.Model):
    """A string of an allowed value for e.g., white listing user names that
    can enroll in a specific course/activity/event etc.

    This class is used to create white lists of users that might not exist yet
    in the database. If you are sure that your users exist, maybe it is more
    convenient to create a regular Group."""

    username = models.CharField(max_length=100, )
    listener_id = models.IntegerField(
        null=True,
        blank=True,
    )
    listener_type = models.ForeignKey(
        models.ContentType,
        null=True,
        blank=True,
    )
    listener_action = models.CharField(
        max_length=30,
        blank=True,
    )

    @property
    def exists(self):
        return models.User.objects.filter(username=self.username).size() == 1

    @property
    def is_active(self):
        try:
            return models.User.objects.get(username=self.username).is_active
        except models.User.DoesNotExist:
            return False

    @property
    def listener(self):
        ctype = models.ContentType.objects.get(pk=self.listener_type)
        cls = ctype.model_class()
        try:
            return cls.objects.get(pk=self.listener_id)
        except cls.DoesNotExist:
            return None

    @property
    def user(self):
        return models.User.objects.get(username=self.username)

    def notify(self, user=None):
        """
        Notify that user with the given username was created.
        """

        if self.action:
            listener = self.listener
            if listener is not None:
                callback = getattr(listener, action)
                callback(user or self.user)

    def __str__(self):
        return self.username
Exemplo n.º 2
0
class FreeFormQuestion(Question):
    """
    A free form question is *not* automatically graded.

    The student can submit a resource that can be a text, code, file, image,
    etc and a human has to analyse and grade it manually.
    """

    Type = Type
    type = models.IntegerField(
        _('Text type'),
        choices=[
            (Type.CODE.value, _('Code')),
            (Type.RICHTEXT.value, _('Rich text')),
            (Type.FILE.value, _('File')),
            (Type.PHYSICAL.value, _('Physical delivery')),
        ],
        default=Type.CODE,
    )
    filter = models.CharField(
        _('filter'),
        max_length=30,
        blank=True,
        help_text=_(
            'Filters the response by some criteria.'
        ),
    )

    class Meta:
        autograde = False
Exemplo n.º 3
0
class TimeSlot(models.Model):
    """
    Represents the weekly time slot that can be assigned to lessons for a
    given course.
    """

    class Meta:
        ordering = ('weekday', 'start')

    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY = range(7)
    WEEKDAY_CHOICES = [
        (MONDAY, _('Monday')),
        (TUESDAY, _('Tuesday')),
        (WEDNESDAY, _('Wednesday')),
        (THURSDAY, _('Thursday')),
        (FRIDAY, _('Friday')),
        (SATURDAY, _('Saturday')),
        (SUNDAY, _('Sunday'))
    ]
    course = models.ParentalKey(
        'Classroom',
        related_name='time_slots'
    )
    weekday = models.IntegerField(
        _('weekday'),
        choices=WEEKDAY_CHOICES,
        help_text=_('Day of the week in which this class takes place.')
    )
    start = models.TimeField(
        _('start'),
        blank=True,
        null=True,
        help_text=_('The time in which the class starts.'),
    )
    end = models.TimeField(
        _('ends'),
        blank=True,
        null=True,
        help_text=_('The time in which the class ends.'),
    )
    room = models.CharField(
        _('classroom'),
        max_length=100,
        blank=True,
        help_text=_('Name for the room in which this class takes place.'),
    )

    # Wagtail admin
    panels = [
        panels.FieldRowPanel([
            panels.FieldPanel('weekday', classname='col6'),
            panels.FieldPanel('room', classname='col6'),
        ]),
        panels.FieldRowPanel([
            panels.FieldPanel('start', classname='col6'),
            panels.FieldPanel('end', classname='col6'),
        ]),
    ]
Exemplo n.º 4
0
class GivenPoints(models.TimeStampedModel):
    """
    Handles users experience points for any given event.

    Points are associated to a unique (user, token, index) tuple. The token +
    index pair is used to identify resources in codeschool that may emmit
    points. This resources can be model instances or any arbitrary combination
    of string and ints.
    """

    user = models.ForeignKey(models.User)
    points = models.IntegerField(default=0)
    token = models.CharField(max_length=100)
    index = models.IntegerField(blank=True, null=True)

    objects = GivenPointsQuerySet.as_manager()

    class Meta:
        unique_together = [('user', 'token', 'index')]
Exemplo n.º 5
0
class GivenXp(models.Model):
    """
    Handles users experience points.
    """
    class Meta:
        unique_together = [('user', 'token', 'index')]

    user = models.ForeignKey(models.User)
    points = models.IntegerField(default=0)
    token = models.CharField(max_length=100)
    index = models.IntegerField(blank=True, null=True)
    objects = GivenXpManager()
    _leaderboard_expire_time = time() - 1  # begin at expired state

    @classmethod
    def total_score(cls, user):
        """
        The total Xp points associated to the given user.
        """

        points = cls.objects.filter(user=user).values_list('points', flat=True)
        return sum(points)

    @classmethod
    def leaderboard(cls, force_refresh=False):
        """
        Construct the leaderboard from all GivenXp entries.

        The leaderboard is cached and refreshed at most every 5min.
        """

        if force_refresh or time() <= cls._leaderboard_expire_time:
            cls._leaderboard_cache = counter = Counter()
            values = cls.objects\
                .select_related('user')\
                .values_list('user', 'points')

            for user, points in values:
                counter[user] += points
            cls._leaderboard_expire_time = time() + 5 * 60

        return cls._leaderboard_cache
Exemplo n.º 6
0
class ExhibitEntry(models.ClusterableModel):
    """
    Each user submission
    """
    class Meta:
        unique_together = [('user', 'exhibit')]

    exhibit = models.ParentalKey(CodeExhibit, related_name='entries')
    user = models.ForeignKey(models.User,
                             related_name='+',
                             on_delete=models.CASCADE)
    name = models.CharField(max_length=200)
    source = models.TextField()
    image = models.ImageField(upload_to='images/code_exhibit/')
    # image = models.FileField(upload_to='images/code_exhibit/')
    votes_from = models.ManyToManyField(models.User, related_name='+')
    num_votes = models.IntegerField(default=int)
    objects = ExhibitEntryQuerySet.as_manager()

    def vote(self, user):
        """
        Register a vote from user.
        """

        if not self.votes_from.filter(id=user.id).count():
            self.votes_from.add(user)
            self.num_votes += 1
            self.save(update_fields=['num_votes'])

    def unvote(self, user):
        """
        Remove a vote from user.
        """

        if self.votes_from.filter(id=user.id).count():
            self.votes_from.remove(user)
            self.num_votes -= 1
            self.save(update_fields=['num_votes'])

    def icon_for_user(self, user):
        if user in self.votes_from.all():
            return 'star'
        return 'start_border'

    # Wagtail admin
    panels = [
        panels.FieldPanel('user'),
        panels.FieldPanel('source'),
        panels.FieldPanel('image'),
        panels.FieldPanel('num_votes'),
    ]
Exemplo n.º 7
0
class TimeSlot(models.Model):
    """Represents the weekly time slot that can be assigned to classes for a
    given course."""

    class Meta:
        unique_together = ('course', 'weekday')

    weekday = models.IntegerField(
        choices=[(0, 'Monday'), (1, 'Tuesday'), (2, 'Wednesday'),
                 (3, 'Thursday'), (4, 'Friday'), (5, 'Saturday'),
                 (6, 'Sunday')]
    )
    start = models.TimeField()
    end = models.TimeField()
    course = models.ForeignKey(Course)
    room = models.CharField(max_length=100, blank=True)
Exemplo n.º 8
0
class ScrumProject(models.RoutablePageMixin, models.Page):
    """
    A simple scrum project.
    """

    description = models.RichTextField()
    members = models.ManyToManyField(models.User)
    workday_duration = models.IntegerField(default=2)

    @property
    def backlog_tasks(self):
        return self.tasks.filter(status=Task.STATUS_BACKLOG)

    # Public functions
    def finish_date(self):
        """
        Return the finish date for the last sprint.
        """
        try:
            return self.sprints.order_by('due_date').last().due_date

        except Sprint.DoesNotExist:
            return now()

    # Serving pages
    @models.route(r'^sprints/new/$')
    def serve_new_sprint(self, request):
        return serve_new_sprint(request, self)

    @models.route(r'^sprints/(?P<id>[0-9]+)/$')
    def serve_view_sprint(self, request, id=None, *args, **kwargs):
        print(args)
        print(kwargs)
        sprint = get_object_or_404(Sprint, id=id)
        return serve_view_sprint(request, self, sprint)

    @models.route(r'^sprints/$')
    def serve_list_sprint(self, request, *args, **kwargs):
        return serve_list_sprints(request, self)

    # Wagtail specific
    template = 'scrum/project.jinja2'

    content_panels = models.Page.content_panels + [
        panels.FieldPanel('description'),
        panels.FieldPanel('workday_duration'),
    ]
Exemplo n.º 9
0
class FreeTextQuestion(Question):
    TYPE_CODE = 0
    TYPE_RICHTEXT = 1

    text_type = models.IntegerField(
        _('Text type'),
        choices=[
            (TYPE_CODE, _('Code')),
            (TYPE_RICHTEXT, _('Rich text')),
        ],
        default=TYPE_CODE,
    )
    syntax_highlight = models.CharField(
        choices=[x.split(':') for x in formats],
        default='python',
        help_text=_('Syntax highlight for code based questions.'),
    )
Exemplo n.º 10
0
class TeamABC(models.TimeStampedModel):
    """
    Common functionality between Pair and Team.
    """

    name = models.CharField(default=phrases.phrase)
    activity_ctype = models.ForeignKey(models.ContentType,
                                       blank=True,
                                       null=True)
    activity_id = models.IntegerField(blank=True, null=True)

    class Meta:
        abstract = True

    @property
    def activity(self):
        "Instantiated activity"
        return self.activity_ctype.get_object_for_this_type(
            id=self.activity_id)
Exemplo n.º 11
0
class Post(models.TimeStampedModel, models.PolymorphicModel):
    """
    Represents a post in the user time-line.
    """

    VISIBILITY_PUBLIC = 1
    VISIBILITY_FRIENDS = 0
    VISIBILITY_OPTIONS = [
        (VISIBILITY_FRIENDS, _('Friends only')),
        (VISIBILITY_PUBLIC, _('Pubic')),
    ]

    user = models.ForeignKey(models.User)
    text = models.RichTextField()
    visibility = models.IntegerField(choices=VISIBILITY_OPTIONS,
                                     default=VISIBILITY_FRIENDS)

    def __str__(self):
        return 'Post by %s at %s' % (self.user, self.created)
Exemplo n.º 12
0
class Task(models.Model):
    """
    A task that can be on the backlog or on a sprint.
    """

    STATUS_BACKLOG = 0
    STATUS_TODO = 1
    STATUS_DOING = 2
    STATUS_DONE = 3
    STATUS = models.Choices(
        (STATUS_BACKLOG, 'backlog'),
        (STATUS_TODO, 'todo'),
        (STATUS_DOING, 'doing'),
        (STATUS_DONE, 'done'),
    )
    sprint = models.ForeignKey(Sprint, related_name='tasks')
    project = models.ForeignKey(ScrumProject, related_name='tasks')
    status = models.StatusField()
    created_by = models.ForeignKey(models.User, related_name='+')
    assigned_to = models.ManyToManyField(models.User, related_name='+')
    description = models.RichTextField()
    duration_hours = models.IntegerField()
    objects = TaskQuerySet.as_manager()
Exemplo n.º 13
0
class Activity(CommitMixin, metaclass=ActivityMeta):
    """
    Represents a gradable activity inside a course. Activities may not have an
    explicit grade, but yet may provide points to the students via the
    gamefication features of Codeschool.

    Activities can be scheduled to be done in the class or as a homework
    assignment.

    Each concrete activity is represented by a different subclass.
    """

    VISIBILITY_PRIVATE, VISIBILITY_STAFF, VISIBILITY_PUBLIC = range(3)
    VISIBILITY_CHOICES = [(VISIBILITY_PRIVATE, _('Private')),
                          (VISIBILITY_STAFF, _('STAFF')),
                          (VISIBILITY_PUBLIC, _('Public'))]

    owner = models.ForeignKey(
        models.User,
        verbose_name=_('Owner'),
        help_text=_('The activity\'s owner.'),
    )
    visibility = models.PositiveSmallIntegerField(
        _('Visibility'),
        choices=VISIBILITY_CHOICES,
        help_text=_('Makes activity invisible to users.'),
    )
    closed = models.BooleanField(
        _('Closed to submissions'),
        default=bool,
        help_text=_(
            'A closed activity does not accept new submissions, but users can '
            'see that they still exist.'))
    group_submission = models.BooleanField(
        _('Group submissions'),
        default=bool,
        help_text=_(
            'If enabled, submissions are registered to groups instead of '
            'individual students.'))
    max_group_size = models.IntegerField(
        _('Maximum group size'),
        default=6,
        help_text=_(
            'If group submission is enabled, define the maximum size of a '
            'group.'),
    )
    disabled = models.BooleanField(
        _('Disabled'),
        default=bool,
        help_text=_(
            'Activities can be automatically disabled when Codeshool '
            'encounters an error. This usually produces a message saved on '
            'the .disabled_message attribute. '
            'This field is not controlled directly by users.'))
    disabled_message = models.TextField(
        _('Disabled message'),
        blank=True,
        help_text=_('Messsage explaining why the activity was disabled.'))
    has_submissions = models.BooleanField(default=bool)
    has_correct_submissions = models.BooleanField(default=bool)
    section_title = property(lambda self: _(self._meta.verbose_name))

    objects = ActivityManager()
    rules = Rules()

    class Meta:
        abstract = True
        verbose_name = _('activity')
        verbose_name_plural = _('activities')
        permissions = [
            ('interact', 'Interact'),
            ('view_submissions', 'View submissions'),
        ]

    # These properties dynamically define the progress/submission/feedback
    # classes associated with the current class.
    progress_class = AuxiliaryClassIntrospection('progress')
    submission_class = AuxiliaryClassIntrospection('submission')
    feedback_class = AuxiliaryClassIntrospection('feedback')

    @property
    def submissions(self):
        return self.submission_class.objects.filter(
            progress__activity_page_id=self.id)

    def clean(self):
        super().clean()

        if not self.author_name and self.owner:
            name = self.owner.get_full_name()
            email = self.owner.email
            self.author_name = '%s <%s>' % (name, email)

        if self.disabled:
            raise ValidationError(self.disabled_message)

    def disable(self, error_message=_('Internal error'), commit=True):
        """
        Disable activity.

        Args:
            message:
                An error message explaining why activity was disabled.
        """

        self.disabled = True
        self.disabled_message = error_message
        self.commit(commit, update_fields=['disabled', 'disabled_message'])

    def submit(self, request, _commit=True, **kwargs):
        """
        Create a new Submission object for the given question and saves it on
        the database.

        Args:
            request:
                The request object for the current submission. The user is
                obtained from the request object.

        This code loads the :cls:`Progress` object for the given user and
        calls it :meth:`Progress.submit`` passing all named arguments to it.

        Subclasses should personalize the submit() method of the Progress object
        instead of the one in this class.
        """

        assert hasattr(request, 'user'), 'request do not have a user attr'

        # Test if activity is active
        if self.closed or self.disabled:
            raise RuntimeError('activity is closed to new submissions')

        # Fetch submission class
        submission_class = self.submission_class
        if submission_class is None:
            raise ImproperlyConfigured(
                '%s must define a submission_class attribute with the '
                'appropriate submission class.' % self.__class__.__name__)

        # Dispatch to the progress object
        user = request.user
        logger.info('%r, submission from user %r' %
                    (self.title, user.username))
        progress = self.progress_set.for_user(user)
        return progress.submit(request, kwargs, commit=_commit)

    def filter_user_submission_payload(self, request, payload):
        """
        Filter a dictionary of arguments supplied by an user and return a
        dictionary with only those arguments that should be passed to the
        .submit() function.
        """

        data_fields = self.submission_class.data_fields()
        return {k: v for (k, v) in payload.items() if k in data_fields}

    def submit_with_user_payload(self, request, payload):
        """
        Return a submission from a dictionary of user provided kwargs.

        It first process the keyword arguments and pass them to the .submit()
        method.
        """

        payload = self.filter_user_submission_payload(request, payload)
        return self.submit(request, **payload)
Exemplo n.º 14
0
class Submission(HasProgressMixin, models.CopyMixin, models.TimeStampedModel,
                 models.PolymorphicModel):
    """
    Represents a student's simple submission in response to some activity.
    """
    class Meta:
        verbose_name = _('submission')
        verbose_name_plural = _('submissions')

    progress = models.ForeignKey('Progress', related_name='submissions')
    hash = models.CharField(max_length=32, blank=True)
    ip_address = models.CharField(max_length=20, blank=True)
    num_recycles = models.IntegerField(default=0)
    recycled = False
    has_feedback = property(lambda self: hasattr(self, 'feedback'))
    objects = SubmissionManager()

    # Delegated properties
    @property
    def final_grade_pc(self):
        if self.has_feedback:
            return None
        return self.feedback.final_grade_pc

    @property
    def feedback_class(self):
        name = self.__class__.__name__.replace('Submission', 'Feedback')
        return apps.get_model(self._meta.app_label, name)

    def __repr__(self):
        return '<%s: %s>' % (self.__class__.__name__, self)

    def __str__(self):
        base = '%s by %s' % (self.activity_title, self.sender_username)
        # if self.feedback_set.last():
        #     points = self.final_feedback_pc.given_grade
        #     base += ' (%s%%)' % points
        return base

    def save(self, *args, **kwargs):
        if not self.hash:
            self.hash = self.compute_hash()
        super().save(*args, **kwargs)

    def compute_hash(self):
        """
        Computes a hash of data to deduplicate submissions.
        """

        raise ImproperlyConfigured(
            'Submission subclass must implement the compute_hash() method.')

    def auto_feedback(self, silent=False):
        """
        Performs automatic grading and return the feedback object.

        Args:
            silent:
                Prevents the submission_graded_signal from triggering in the
                end of a successful grading.
        """

        feedback = self.feedback_class(submission=self, manual_grading=False)
        feedback.update_autograde()
        feedback.update_final_grade()
        feedback.save()
        self.progress.register_feedback(feedback)
        self.register_feedback(feedback)

        # Send signal
        if not silent:
            submission_graded_signal.send(Submission,
                                          submission=self,
                                          feedback=feedback,
                                          automatic=True)
        return feedback

    def register_feedback(self, feedback, commit=True):
        """
        Update itself when a new feedback becomes available.

        This method should not update the progress instance.
        """

        self.final_feedback = feedback
        if commit:
            self.save()

    def bump_recycles(self):
        """
        Increase the recycle count by one.
        """

        self.num_recycles += 1
        self.save(update_fields=['num_recycles'])

    def is_equal(self, other):
        """
        Check both submissions are equal/equivalent to each other.
        """

        if self.hash == other.hash and self.hash is not None:
            return True

        return self.submission_data() == other.submission_data()

    def submission_data(self):
        """
        Return a dictionary with data specific for submission.

        It ignores metadata such as creation and modification times, number of
        recycles, etc. This method should only return data relevant to grading
        the submission.
        """

        blacklist = {
            'id',
            'num_recycles',
            'ip_address',
            'created',
            'modified',
            'hash',
            'final_feedback_id',
            'submission_ptr_id',
            'polymorphic_ctype_id',
        }

        def forbidden_attr(k):
            return k.startswith('_') or k in blacklist

        return {
            k: v
            for k, v in self.__dict__.items() if not forbidden_attr(k)
        }

    def autograde_value(self, *args, **kwargs):
        """
        This method should be implemented in subclasses.
        """

        raise ImproperlyConfigured(
            'Progress subclass %r must implement the autograde_value().'
            'This method should perform the automatic grading and return the '
            'resulting grade. Any additional relevant feedback data might be '
            'saved to the `feedback_data` attribute, which is then is pickled '
            'and saved into the database.' % type(self).__name__)

    def manual_grade(self, grade, commit=True, raises=False, silent=False):
        """
        Saves result of manual grading.

        Args:
            grade (number):
                Given grade, as a percentage value.
            commit:
                If false, prevents saving the object when grading is complete.
                The user must save the object manually after calling this
                method.
            raises:
                If submission has already been graded, raises a GradingError.
            silent:
                Prevents the submission_graded_signal from triggering in the
                end of a successful grading.
        """

        if self.status != self.STATUS_PENDING and raises:
            raise GradingError('Submission has already been graded!')

        raise NotImplementedError('TODO')

    def update_progress(self, commit=True):
        """
        Update all parameters for the progress object.

        Return True if update was required or False otherwise.
        """

        update = False
        progress = self.progress

        if self.is_correct and not progress.is_correct:
            update = True
            progress.is_correct = True

        if self.given_grade_pc > progress.best_given_grade_pc:
            update = True
            fmt = self.description, progress.best_given_grade_pc, self.given_grade_pc
            progress.best_given_grade_pc = self.given_grade_pc
            logger.info('(%s) grade: %s -> %s' % fmt)

        if progress.best_given_grade_pc > progress.grade:
            old = progress.grade
            new = progress.grade = progress.best_given_grade_pc
            logger.info('(%s) grade: %s -> %s' %
                        (progress.description, old, new))

        if commit and update:
            progress.save()

        return update

    def regrade(self, method, commit=True):
        """
        Recompute the grade for the given submission.

        If status != 'done', it simply calls the .autograde() method. Otherwise,
        it accept different strategies for updating to the new grades:
            'update':
                Recompute the grades and replace the old values with the new
                ones. Only saves the submission if the feedback_data or the
                given_grade_pc attributes change.
            'best':
                Only update if the if the grade increase.
            'worst':
                Only update if the grades decrease.
            'best-feedback':
                Like 'best', but updates feedback_data even if the grades
                change.
            'worst-feedback':
                Like 'worst', but updates feedback_data even if the grades
                change.

        Return a boolean telling if the regrading was necessary.
        """
        if self.status != self.STATUS_DONE:
            return self.auto_feedback()

        # We keep a copy of the state, if necessary. We only have to take some
        # action if the state changes.
        def rollback():
            self.__dict__.clear()
            self.__dict__.update(state)

        state = self.__dict__.copy()
        self.auto_feedback(force=True, commit=False)

        # Each method deals with the new state in a different manner
        if method == 'update':
            if state != self.__dict__:
                if commit:
                    self.save()
                return False
            return True
        elif method in ('best', 'best-feedback'):
            if self.given_grade_pc <= state.get('given_grade_pc', 0):
                new_feedback_data = self.feedback_data
                rollback()
                if new_feedback_data != self.feedback_data:
                    self.feedback_data = new_feedback_data
                    if commit:
                        self.save()
                    return True
                return False
            elif commit:
                self.save()
            return True

        elif method in ('worst', 'worst-feedback'):
            if self.given_grade_pc >= state.get('given_grade_pc', 0):
                new_feedback_data = self.feedback_data
                rollback()
                if new_feedback_data != self.feedback_data:
                    self.feedback_data = new_feedback_data
                    if commit:
                        self.save()
                    return True
                return False
            elif commit:
                self.save()
            return True
        else:
            rollback()
            raise ValueError('invalid method: %s' % method)

    def get_feedback_title(self):
        """
        Return the title for the feedback message.
        """

        try:
            feedback = self.feedback
        except AttributeError:
            return _('Not graded')
        else:
            return feedback.get_feedback_title()
Exemplo n.º 15
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
Exemplo n.º 16
0
class AttendanceSheet(models.Model):
    """
    Controls student attendance by generating a new public passphrase under 
    teacher request. Students confirm attendance by typing the secret phrase
    in a small interval. 
    """

    max_attempts = models.SmallIntegerField(default=3)
    expiration_minutes = models.SmallIntegerField(default=5)
    owner = models.ForeignKey(models.User)
    last_event = models.ForeignKey('Event', blank=True, null=True)
    max_string_distance = models.SmallIntegerField(default=0)
    max_number_of_absence = models.IntegerField(blank=True, null=True)

    @property
    def expiration_interval(self):
        return datetime.timedelta(minutes=self.expiration_minutes)

    @property
    def attendance_checks(self):
        return AttendanceCheck.objects.filter(event__sheet=self)

    def new_event(self):
        """
        Create a new event in attendance sheet.
        """

        current_time = now()
        new = self.events.create(passphrase=new_random_passphrase(),
                                 date=current_time.date(),
                                 created=current_time,
                                 expires=current_time +
                                 self.expiration_interval)
        self.last_event = new
        self.save(update_fields=['last_event'])
        return new

    def get_today_event(self):
        """
        Return the last event created for today
        """

        if self.last_event.date() == now().date():
            return self.last_event
        else:
            return self.new_event()

    def number_of_absences(self, user):
        """
        Return the total number of absence for user.
        """

        return self.attendance_checks.filter(user=user,
                                             has_attended=False).count()

    def absence_table(self, users=None, method='fraction'):
        """
        Return a mapping between users and their respective absence rate. 

        Args:
            users:
                A queryset of users.
            method:
                One of 'fraction' (default), 'number', 'attendance' or 
                'attendance-fraction'
        """

        try:
            get_value_from_absence = {
                'fraction': lambda x: x / num_events,
                'number': lambda x: x,
                'attendance': lambda x: num_events - x,
                'attendance-fraction': lambda x: (num_events - x) / num_events
            }[method]
        except KeyError:
            raise ValueError('invalid method: %r' % method)

        num_events = self.events.count()
        if users is None:
            users = models.User.objects.all()

        result = collections.OrderedDict()
        for user in users:
            absence = self.user_absence(user)
            result[user] = get_value_from_absence(absence)
        return result

    def render_dialog(self, request):
        """
        Renders attendance dialog based on request.
        """

        context = {
            'passphrase': self.passphrase,
            'is_expired': self.is_expired(),
            'minutes_left': self.minutes_left(raises=False)
        }
        user = request.user
        if user == self.owner:
            template = 'attendance/edit.jinja2'
        else:
            template = 'attendance/view.jinja2'
            context['attempts'] = self.user_attempts(user)
        return render_to_string(template, request=request, context=context)

    def user_attempts(self, user):
        """
        Return the number of user attempts in the last attendance event.
        """

        if self.last_event is None:
            return 0

        qs = self.attendance_checks.filter(user=user, event=self.last_event)
        return qs.count()

    def minutes_left(self, raises=True):
        """
        Return how many minutes left for expiration.
        """

        if self.last_event:
            time = now()
            if self.last_event.expires < time:
                return 0.0
            else:
                dt = self.last_event.expires - time
                return dt.minutes
        if raises:
            raise ValueError('last event is not defined')
        else:
            return None

    def is_expired(self):
        """
        Return True if last_event has already expired.
        """

        if not self.last_event:
            return False
        return self.last_event.expires < now()
Exemplo n.º 17
0
class Submission(ResponseDataMixin, FeedbackDataMixin, models.CopyMixin,
                 models.StatusModel, models.TimeStampedModel,
                 models.PolymorphicModel):
    """
    Represents a student's simple submission in response to some activity.

    Submissions can be in 4 different states:

    pending:
        The response has been sent, but was not graded. Grading can be manual or
        automatic, depending on the activity.
    waiting:
        Waiting for manual feedback.
    incomplete:
        For long-term activities, this tells that the student started a response
        and is completing it gradually, but the final response was not achieved
        yet.
    invalid:
        The response has been sent, but contains malformed data.
    done:
        The response was graded and evaluated and it initialized a feedback
        object.

    A response always starts at pending status. We can request it to be graded
    by calling the :func:`Response.autograde` method. This method must raise
    an InvalidResponseError if the response is invalid or ManualGradingError if
    the response subclass does not implement automatic grading.
    """
    class Meta:
        verbose_name = _('submission')
        verbose_name_plural = _('submissions')

    # Feedback messages
    MESSAGE_OK = _('*Congratulations!* Your response is correct!')
    MESSAGE_OK_WITH_PENALTIES = _(
        'Your response is correct, but you did not achieved the maximum grade.'
    )
    MESSAGE_WRONG = _('I\'m sorry, your response is wrong.')
    MESSAGE_PARTIAL = _(
        'Your answer is partially correct: you achieved only %(grade)d%% of '
        'the total grade.')
    MESSAGE_NOT_GRADED = _('Your response has not been graded yet!')

    # Status
    STATUS_PENDING = 'pending'
    STATUS_INCOMPLETE = 'incomplete'
    STATUS_WAITING = 'waiting'
    STATUS_INVALID = 'invalid'
    STATUS_DONE = 'done'

    # Fields
    STATUS = models.Choices(
        (STATUS_PENDING, _('pending')),
        (STATUS_INCOMPLETE, _('incomplete')),
        (STATUS_WAITING, _('waiting')),
        (STATUS_INVALID, _('invalid')),
        (STATUS_DONE, _('done')),
    )

    response = models.ParentalKey(
        'Response',
        related_name='submissions',
    )
    given_grade = models.DecimalField(
        _('percentage of maximum grade'),
        help_text=_(
            'This grade is given by the auto-grader and represents the grade '
            'for the response before accounting for any bonuses or penalties.'
        ),
        max_digits=6,
        decimal_places=3,
        blank=True,
        null=True,
    )
    final_grade = models.DecimalField(
        _('final grade'),
        help_text=_(
            'Similar to given_grade, but can account for additional factors '
            'such as delay penalties or for any other reason the teacher may '
            'want to override the student\'s grade.'),
        max_digits=6,
        decimal_places=3,
        blank=True,
        null=True,
    )
    manual_override = models.BooleanField(default=False)
    points = models.IntegerField(default=0)
    score = models.IntegerField(default=0)
    stars = models.FloatField(default=0)
    objects = SubmissionManager()

    # Status properties
    is_done = property(lambda x: x.status == x.STATUS_DONE)
    is_pending = property(lambda x: x.status == x.STATUS_PENDING)
    is_waiting = property(lambda x: x.status == x.STATUS_WAITING)
    is_invalid = property(lambda x: x.status == x.STATUS_INVALID)

    @property
    def is_correct(self):
        if self.given_grade is None:
            raise AttributeError('accessing attribute of non-graded response.')
        else:
            return self.given_grade == 100

    # Delegate properties
    activity = delegate_to('response')
    activity_id = delegate_to('response')
    activity_page = delegate_to('response')
    activity_page_id = delegate_to('response')
    user = delegate_to('response')
    user_id = delegate_to('response')
    stars_total = delegate_to('activity')
    points_total = delegate_to('activity')

    @classmethod
    def response_data_hash(cls, response_data):
        """
        Computes a hash for the response_data attribute.

        Data must be given as a JSON-like structure or as a string of JSON data.
        """

        if response_data:
            if isinstance(response_data, str):
                data = response_data
            else:
                data = json.dumps(response_data, default=json_default)
            return md5hash(data)
        return ''

    def __init__(self, *args, **kwargs):
        # Django is loading object from the database -- we step out the way
        if args and not kwargs:
            super().__init__(*args, **kwargs)
            return

        # We create the response_data and feedback_data manually always using
        # copies of passed dicts. We save these variables here, init object and
        # then copy this data to the initialized dictionaries
        response_data = kwargs.pop('response_data', None) or {}
        feedback_data = kwargs.pop('feedback_data', None) or {}

        # This part makes a Submission instance initialize from a user +
        # activity instead of requiring a response object. The response is
        # automatically created on demand.
        user = kwargs.pop('user', None)
        if 'response' in kwargs and user and user != kwargs['response'].user:
            response_user = kwargs['response'].user
            raise ValueError('Inconsistent user definition: %s vs. %s' %
                             (user, response_user))
        elif 'response' not in kwargs and user:
            try:
                activity = kwargs.pop('activity')
            except KeyError:
                raise TypeError(
                    '%s objects bound to a user must also provide an '
                    'activity parameter.' % type(self).__name__)
            else:
                # User-bound constructor tries to obtain the response object by
                # searching for an specific (user, activity) tuple.
                response, created = Response.objects.get_or_create(
                    user=user, activity=activity)
                kwargs['response'] = response

        if 'context' in kwargs or 'activity' in kwargs:
            raise TypeError(
                'Must provide an user to instantiate a bound submission.')
        super().__init__(*args, **kwargs)

        # Now that we have initialized the submission, we fill the data
        # passed in the response_data and feedback_data dictionaries.
        self.response_data = dict(self.response_data or {}, **response_data)
        self.feedback_data = dict(self.response_data or {}, **feedback_data)

    def __str__(self):
        if self.given_grade is None:
            grade = self.status
        else:
            grade = '%s pts' % self.final_grade
        user = self.user
        activity = self.activity
        name = self.__class__.__name__
        return '<%s: %s by %s (%s)>' % (name, activity, user, grade)

    def __html__(self):
        """
        A string of html source representing the feedback.
        """

        if self.is_done:
            data = {'grade': (self.final_grade or 0)}

            if self.final_grade == 100:
                return markdown(self.MESSAGE_OK)
            elif self.given_grade == 100:
                return markdown(self.ok_with_penalties_message)
            elif not self.given_grade:
                return markdown(self.MESSAGE_WRONG)
            else:
                return markdown(self.MESSAGE_PARTIAL % data)
        else:
            return markdown(self.MESSAGE_NOT_GRADED)

    def save(self, *args, **kwargs):
        if not self.response_hash:
            self.response_hash = self.response_hash_from_data(
                self.response_hash)
        super().save(*args, **kwargs)

    def final_points(self):
        """
        Return the amount of points awarded to the submission after
        considering all penalties and bonuses.
        """

        return self.points

    def final_stars(self):
        """
        Return the amount of stars awarded to the submission after
        considering all penalties and bonuses.
        """

        return self.stars

    def given_stars(self):
        """
        Compute the number of stars that should be awarded to the submission
        without taking into account bonuses and penalties.
        """

        return self.stars_total * (self.given_grade / 100)

    def given_points(self):
        """
        Compute the number of points that should be awarded to the submission
        without taking into account bonuses and penalties.
        """

        return int(self.points_total * (self.given_grade / 100))

    def feedback(self, commit=True, force=False, silent=False):
        """
        Return the feedback object associated to the given response.

        This method may trigger the autograde() method, if grading was not
        performed yet. If you want to defer database access, call it with
        commit=False to prevent saving any modifications to the response object
        to the database.

        The commit, force and silent arguments have the same meaning as in
        the :func:`Submission.autograde` method.
        """

        if self.status == self.STATUS_PENDING:
            self.autograde(commit=commit, force=force, silent=silent)
        elif self.status == self.STATUS_INVALID:
            raise self.feedback_data
        elif self.status == self.STATUS_WAITING:
            return None
        return self.feedback_data

    def autograde(self, commit=True, force=False, silent=False):
        """
        Performs automatic grading.

        Response subclasses must implement the autograde_compute() method in
        order to make automatic grading work. This method may write any
        relevant information to the `feedback_data` attribute and must return
        a numeric value from 0 to 100 with the given automatic grade.

        Args:
            commit:
                If false, prevents saving the object when grading is complete.
                The user must save the object manually after calling this
                method.
            force:
                If true, force regrading the item even if it has already been
                graded. The default behavior is to ignore autograde from a
                graded submission.
            silent:
                Prevents the submission_graded_signal from triggering in the
                end of a successful grading.
        """

        if self.status == self.STATUS_PENDING or force:
            # Evaluate grade using the autograde_value() method of subclass.
            try:
                value = self.autograde_value()
            except self.InvalidSubmissionError as ex:
                self.status = self.STATUS_INVALID
                self.feedback_data = ex
                self.given_grade = self.final_grade = decimal.Decimal(0)
                if commit:
                    self.save()
                raise

            # If no value is returned, change to STATUS_WAITING. This probably
            # means that response is partial and we need other submissions to
            # complete the final response
            if value is None:
                self.status = self.STATUS_WAITING

            # A regular submission has a decimal grade value. We save it and
            # change state to STATUS_DONE
            else:
                self.given_grade = decimal.Decimal(value)
                if self.final_grade is None:
                    self.final_grade = self.given_grade
                self.status = self.STATUS_DONE

            # Commit results
            if commit and self.pk:
                self.save(update_fields=[
                    'status', 'feedback_data', 'given_grade', 'final_grade'
                ])
            elif commit:
                self.save()

            # If STATUS_DONE, we submit the submission_graded signal.
            if self.status == self.STATUS_DONE:
                self.stars = self.given_stars()
                self.points = self.given_points()
                self.response.register_submission(self)
                if not silent:
                    submission_graded_signal.send(
                        Submission,
                        submission=self,
                        given_grade=self.given_grade,
                        automatic=True,
                    )

        elif self.status == self.STATUS_INVALID:
            raise self.feedback_data

    def manual_grade(self, grade, commit=True, raises=False, silent=False):
        """
        Saves result of manual grading.

        Args:
            grade (number):
                Given grade, as a percentage value.
            commit:
                If false, prevents saving the object when grading is complete.
                The user must save the object manually after calling this
                method.
            raises:
                If submission has already been graded, raises a GradingError.
            silent:
                Prevents the submission_graded_signal from triggering in the
                end of a successful grading.
        """

        if self.status != self.STATUS_PENDING and raises:
            raise GradingError('Submission has already been graded!')

        raise NotImplementedError('TODO')

    def autograde_value(self):
        """
        This method should be implemented in subclasses.
        """

        raise ImproperlyConfigured(
            'Response subclass %r must implement the autograde_value().'
            'This method should perform the automatic grading and return the '
            'resulting grade. Any additional relevant feedback data might be '
            'saved to the `feedback_data` attribute, which is then is pickled '
            'and saved into the database.' % type(self).__name__)

    def regrade(self, method, commit=True):
        """
        Recompute the grade for the given submission.

        If status != 'done', it simply calls the .autograde() method. Otherwise,
        it accept different strategies for updating to the new grades:
            'update':
                Recompute the grades and replace the old values with the new
                ones. Only saves the submission if the feedback_data or the
                given_grade attributes change.
            'best':
                Only update if the if the grade increase.
            'worst':
                Only update if the grades decrease.
            'best-feedback':
                Like 'best', but updates feedback_data even if the grades
                change.
            'worst-feedback':
                Like 'worst', but updates feedback_data even if the grades
                change.

        Return a boolean telling if the regrading was necessary.
        """
        if self.status != self.STATUS_DONE:
            return self.autograde()

        # We keep a copy of the state, if necessary. We only have to take some
        # action if the state changes.
        def rollback():
            self.__dict__.clear()
            self.__dict__.update(state)

        state = self.__dict__.copy()
        self.autograde(force=True, commit=False)

        # Each method deals with the new state in a different manner
        if method == 'update':
            if state != self.__dict__:
                if commit:
                    self.save()
                return False
            return True
        elif method in ('best', 'best-feedback'):
            if self.given_grade <= state.get('given_grade', 0):
                new_feedback_data = self.feedback_data
                rollback()
                if new_feedback_data != self.feedback_data:
                    self.feedback_data = new_feedback_data
                    if commit:
                        self.save()
                    return True
                return False
            elif commit:
                self.save()
            return True

        elif method in ('worst', 'worst-feedback'):
            if self.given_grade >= state.get('given_grade', 0):
                new_feedback_data = self.feedback_data
                rollback()
                if new_feedback_data != self.feedback_data:
                    self.feedback_data = new_feedback_data
                    if commit:
                        self.save()
                    return True
                return False
            elif commit:
                self.save()
            return True
        else:
            rollback()
            raise ValueError('invalid method: %s' % method)
Exemplo n.º 18
0
class QuizActivity(Activity):
    """
    Represent a quiz.
    """
    class Meta:
        verbose_name = _('quiz activity')
        verbose_name_plural = _('quiz activities')

    GRADING_METHOD_MAX = 0
    GRADING_METHOD_MIN = 1
    GRADING_METHOD_AVERAGE = 2
    GRADING_METHOD_CHOICES = (
        (GRADING_METHOD_MAX, _('largest grade of all responses')),
        (GRADING_METHOD_MIN, _('smallest grade of all responses')),
        (GRADING_METHOD_AVERAGE, _('mean grade')),
    )
    quiz_grading_method = models.IntegerField(choices=GRADING_METHOD_CHOICES)
    language = models.ForeignKey(ProgrammingLanguage, blank=True, null=True)

    # Derived attributes
    items = QuizActivityItem.as_items()
    questions = property(lambda x: list(x))
    num_questions = property(lambda x: len(x.items))

    def __iter__(self):
        return (x.question.as_subclass() for x in self.items)

    def __len__(self):
        return len(self.items)

    def __getitem__(self, idx):
        return self.items[idx].question

    def __delitem__(self, idx):
        del self.items[idx]

    def add_question(self, question):
        """Add a question to the quiz."""

        item = QuizActivityItem(quiz=self, question=question)
        item.save()
        self.items.append(item)

    def get_user_response(self, user):
        """Return a response object for the given user.

        For now, users can only have one response ."""

        try:
            response = self.responses.filter(user=user,
                                             parent__isnull=True).first()
            if response:
                return response.quizresponse
            else:
                raise Response.DoesNotExist
        except Response.DoesNotExist:
            new = QuizResponse.objects.create(activity=self, user=user)
            return new

    def register_response(self, user, response, commit=True):
        """Register a question response to the given user."""

        self.get_user_response(user).register_response(response, commit)

    def iter_tagged_questions(self, tag='answered', user=None):
        """Iterate over tuples of (question, question) where tag is some property
        associated with each question.

        Args:
            tag:
                Some property pertaining to the question. It accept the
                following string values:

                answered:
                    A boolean value telling if the question has been answered
                    by the user.
            user:
                The user associated with the tag. This may be necessary to
                some tags.
        """

        if tag == 'answered':
            response = self.get_user_response(user)
            for question in self.questions:
                yield (question, response.is_answered(question))
        else:
            return NotImplemented

    def get_final_grade(self, user):
        """Return the final grade for the given user."""

        response = self.get_user_response(user)
        return response.get_final_grade()
Exemplo n.º 19
0
class KeyValuePair(models.Model):
    """
    Represents a (key, value) pair datum.
    """

    class Meta:
        abstract = True

    name = models.CharField(max_length=30, unique=True)
    value = models.CharField(max_length=100)
    type = models.IntegerField(choices=[
        (0, 'str'),
        (1, 'int'),
        (2, 'float'),
        (3, 'bool'),
    ])

    @property
    def data(self):
        raw_data = self.value
        if self.type == 0:
            return raw_data
        elif self.type == 1:
            return int(raw_data)
        elif self.type == 2:
            return float(raw_data)
        elif self.type == 3:
            return bool(int(raw_data))
        else:
            raise ValueError(self.type)

    @classmethod
    def serialize(cls, value):
        """
        Return string representation of value.
        """

        try:
            return {
                int: str,
                float: str,
                str: lambda x: x,
                bool: lambda x: str(int(x))
            }[type(value)](value)
        except KeyError:
            type_name = value.__class__.__name__
            raise TypeError('invalid config value type: %r' % type_name)

    @classmethod
    def data_type(cls, value):
        """
        Return data type for value.
        """

        try:
            return {str: 0, int: 1, float: 2, bool: 3}[type(value)]
        except KeyError:
            type_name = value.__class__.__name__
            raise TypeError('invalid config value type: %r' % type_name)

    def __str__(self):
        return self.name
Exemplo n.º 20
0
class Submission(CommitMixin, FromProgressAttributesMixin, models.CopyMixin,
                 models.TimeStampedModel, models.PolymorphicModel):
    """
    Represents a student's simple submission in response to some activity.
    """

    progress = models.ForeignKey('Progress', related_name='submissions')
    hash = models.CharField(max_length=32)
    ip_address = models.CharField(max_length=20, blank=True)
    num_recycles = models.IntegerField(default=0)
    recycled = False

    class Meta:
        verbose_name = _('submission')
        verbose_name_plural = _('submissions')

    # Properties
    has_feedback = property(lambda self: hasattr(self, 'feedback'))
    objects = SubmissionManager()

    # Delegated properties
    @property
    def final_grade_pc(self):
        if self.has_feedback:
            return None
        return self.feedback.final_grade_pc

    @property
    def feedback_class(self):
        name = self.__class__.__name__.replace('Submission', 'Feedback')
        return apps.get_model(self._meta.app_label, name)

    @classmethod
    def data_fields(cls):
        """
        Return a list of attributes that store submission data.

        It ignores metadata such as creation and modification times, number of
        recycles, etc. This method should only return fields relevant to grading
        the submission.
        """

        blacklist = {
            'id',
            'num_recycles',
            'ip_address',
            'created',
            'modified',
            'hash',
            'final_feedback_id',
            'submission_ptr_id',
            'polymorphic_ctype_id',
            'progress_id',
        }

        fields = [field.attname for field in cls._meta.fields]
        return [field for field in fields if field not in blacklist]

    def __repr__(self):
        return '<%s: %s>' % (self.__class__.__name__, self)

    def __str__(self):
        username = self.user.username
        base = '%s by %s' % (self.activity_title, username)
        return base

    def clean(self):
        if not self.hash:
            self.hash = self.compute_hash()
        super().clean()

    def compute_hash(self):
        """
        Computes a hash of data to deduplicate submissions.
        """

        fields = get_default_fields(type(self))
        return md5hash(';'.join(map(lambda f: str(getattr(self, f)), fields)))

    def auto_feedback(self, silent=False, commit=True):
        """
        Performs automatic grading and return the feedback object.

        Args:
            silent:
                Prevents the submission_graded_signal from triggering in the
                end of a successful grading.
        """

        # Create feedback object
        feedback = self.feedback_class(submission=self, manual_grading=False)
        feedback.given_grade_pc, state = feedback.get_autograde_value()
        feedback.is_correct = feedback.given_grade_pc == 100
        update_state(feedback, state)
        feedback.final_grade_pc = feedback.get_final_grade_value()
        feedback.commit(commit)

        # Register graded feedback
        self.register_feedback(feedback)

        # Send signal
        if not silent:
            submission_graded_signal.send(Submission,
                                          submission=self,
                                          feedback=feedback,
                                          automatic=True)
        return feedback

    def is_equal(self, other):
        """
        Check both submissions are equal/equivalent to each other.
        """

        if self.hash != other.hash and self.hash and other.hash:
            return False

        fields = get_default_fields(type(self))
        return all(getattr(self, f) == getattr(other, f) for f in fields)

    def bump_recycles(self):
        """
        Increase the recycle count by one.
        """

        self.num_recycles += 1
        self.save(update_fields=['num_recycles'])

    def register_feedback(self, feedback, commit=True):
        """
        Update itself when a new feedback becomes available.

        This method should not update the progress instance.
        """

        # Call the register feedback of the progress object
        self.progress.register_feedback(feedback)

    def manual_grade(self, grade, commit=True, raises=False, silent=False):
        """
        Saves result of manual grading.

        Args:
            grade (number):
                Given grade, as a percentage value.
            commit:
                If false, prevents saving the object when grading is complete.
                The user must save the object manually after calling this
                method.
            raises:
                If submission has already been graded, raises a GradingError.
            silent:
                Prevents the submission_graded_signal from triggering in the
                end of a successful grading.
        """

        if self.status != self.STATUS_PENDING and raises:
            raise GradingError('Submission has already been graded!')

        raise NotImplementedError('TODO')

    def get_feedback_title(self):
        """
        Return the title for the feedback message.
        """

        try:
            feedback = self.feedback
        except AttributeError:
            return _('Not graded')
        else:
            return feedback.get_feedback_title()
Exemplo n.º 21
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
Exemplo n.º 22
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, ))
Exemplo n.º 23
0
class Profile(models.TimeStampedModel):
    """
    Social information about users.
    """

    GENDER_MALE, GENDER_FEMALE, GENDER_OTHER = 0, 1, 2
    GENDER_CHOICES = [
        (GENDER_MALE, _('Male')),
        (GENDER_FEMALE, _('Female')),
        (GENDER_OTHER, _('Other')),
    ]

    VISIBILITY_PUBLIC, VISIBILITY_FRIENDS, VISIBILITY_HIDDEN = range(3)
    VISIBILITY_CHOICES = enumerate(
        [_('Any Codeschool user'),
         _('Only friends'),
         _('Private')])

    visibility = models.IntegerField(
        _('Visibility'),
        choices=VISIBILITY_CHOICES,
        default=VISIBILITY_FRIENDS,
        help_text=_('Who do you want to share information in your profile?'))
    user = models.OneToOneField(
        User,
        verbose_name=_('user'),
        related_name='profile_ref',
    )
    phone = models.CharField(
        _('Phone'),
        max_length=20,
        blank=True,
        null=True,
    )
    gender = models.SmallIntegerField(
        _('gender'),
        choices=GENDER_CHOICES,
        blank=True,
        null=True,
    )
    date_of_birth = models.DateField(
        _('date of birth'),
        blank=True,
        null=True,
    )
    website = models.URLField(
        _('Website'),
        blank=True,
        null=True,
        help_text=_('A website that is shown publicly in your profile.'))
    about_me = models.RichTextField(
        _('About me'),
        blank=True,
        help_text=_('A small description about yourself.'))

    # Delegates and properties
    username = delegate_to('user', True)
    name = delegate_to('user')
    email = delegate_to('user')

    class Meta:
        permissions = (
            ('student', _('Can access/modify data visible to student\'s')),
            ('teacher',
             _('Can access/modify data visible only to Teacher\'s')),
        )

    @property
    def age(self):
        if self.date_of_birth is None:
            return None
        today = timezone.now().date()
        birthday = self.date_of_birth
        years = today.year - birthday.year
        birthday = datetime.date(today.year, birthday.month, birthday.day)
        if birthday > today:
            return years - 1
        else:
            return years

    def __str__(self):
        if self.user is None:
            return __('Unbound profile')
        full_name = self.user.get_full_name() or self.user.username
        return __('%(name)s\'s profile') % {'name': full_name}

    def get_absolute_url(self):
        self.user.get_absolute_url()
Exemplo n.º 24
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
Exemplo n.º 25
0
class ScoreHandler(models.TimeStampedModel):
    """
    Common implementations for TotalScores and UserScores.
    """
    class Meta:
        abstract = True

    page = models.ForeignKey(models.Page, related_name='+')
    points = models.IntegerField(default=0)
    stars = models.DecimalField(default=Decimal(0),
                                decimal_places=1,
                                max_digits=5)

    @lazy_classattribute
    def _wagtail_root(cls):
        return models.Page.objects.get(path='0001')

    @lazy
    def specific(self):
        return self.page.specific

    def get_parent(self):
        """
        Return parent resource handler.
        """

        raise NotImplementedError('must be implemented in subclasses')

    def get_children(self):
        """
        Return a queryset with all children resource handlers.
        """

        raise NotImplementedError('must be implemented in subclasses')

    def set_diff(self,
                 points=0,
                 stars=0,
                 propagate=True,
                 commit=True,
                 optimistic=False):
        """
        Change the given resources by the given amounts and propagate to all
        the parents.
        """

        # Update fields
        kwargs = {}
        if points and (points > 0 or not optimistic):
            self.points += points
            kwargs['points'] = points
        if stars and (stars > 0 or not optimistic):
            self.stars += stars
            kwargs['stars'] = stars

        if kwargs and commit:
            self.save(update_fields=kwargs.keys())

        # Propagate to all parent resources
        if propagate and kwargs and commit:
            parent = self.get_parent()
            kwargs['commit'] = True
            kwargs['propagate'] = True
            if parent is not None:
                parent.set_diff(optimistic=False, **kwargs)

    def set_values(self,
                   points=0,
                   stars=0,
                   propagate=True,
                   optimistic=False,
                   commit=True):
        """
        Register a new value for the resource.

        If new value is greater than the current value, update the resource
        and propagate.

        Args:
            points, score, stars, (number):
                New value assigned to each specified resource.
            propagate (bool):
                If True (default), increment all parent nodes.
            optimistic (bool):
                If True, only update if give value is greater than the
                registered value.
            commit (bool):
                If True (default), commit results to the database.
        """

        d_points = points - self.points
        d_stars = Decimal(stars) - self.stars

        self.set_diff(points=d_points,
                      stars=d_stars,
                      propagate=propagate,
                      commit=commit,
                      optimistic=optimistic)
Exemplo n.º 26
0
class Activity(models.RoutablePageExt, metaclass=ActivityMeta):
    """
    Represents a gradable activity inside a course. Activities may not have an
    explicit grade, but yet may provide points to the students via the
    gamefication features of Codeschool.

    Activities can be scheduled to be done in the class or as a homework
    assignment.

    Each concrete activity is represented by a different subclass.
    """

    class Meta:
        abstract = True
        verbose_name = _('activity')
        verbose_name_plural = _('activities')
        permissions = [
            ('interact', 'Interact'),
            ('view_submissions', 'View submissions'),
        ]

    author_name = models.CharField(
        _('Author\'s name'),
        max_length=100,
        blank=True,
        help_text=_(
            'The author\'s name, if not the same user as the question owner.'
        ),
    )
    visible = models.BooleanField(
        _('Invisible'),
        default=bool,
        help_text=_(
            'Makes activity invisible to users.'
        ),
    )
    closed = models.BooleanField(
        _('Closed to submissions'),
        default=bool,
        help_text=_(
            'A closed activity does not accept new submissions, but users can '
            'see that they still exist.'
        )
    )
    group_submission = models.BooleanField(
        _('Group submissions'),
        default=bool,
        help_text=_(
            'If enabled, submissions are registered to groups instead of '
            'individual students.'
        )
    )
    max_group_size = models.IntegerField(
        _('Maximum group size'),
        default=6,
        help_text=_(
            'If group submission is enabled, define the maximum size of a '
            'group.'
        ),
    )
    disabled = models.BooleanField(
        _('Disabled'),
        default=bool,
        help_text=_(
            'Activities can be automatically disabled when Codeshool '
            'encounters an error. This usually produces a message saved on '
            'the .disabled_message attribute.'
        )
    )
    disabled_message = models.TextField(
        _('Disabled message'),
        blank=True,
        help_text=_(
            'Messsage explaining why the activity was disabled.'
        )
    )
    has_submissions = models.BooleanField(default=bool)
    has_correct_submissions = models.BooleanField(default=bool)
    section_title = property(lambda self: _(self._meta.verbose_name))

    objects = ActivityManager()
    rules = Rules()

    # These properties dynamically define the progress/submission/feedback
    # classes associated with the current class.
    progress_class = AuxiliaryClassIntrospection('progress')
    submission_class = AuxiliaryClassIntrospection('submission')
    feedback_class = AuxiliaryClassIntrospection('feedback')

    @property
    def submissions(self):
        return self.submission_class.objects.filter(
            progress__activity_page_id=self.id
        )

    def clean(self):
        super().clean()

        if not self.author_name and self.owner:
            name = self.owner.get_full_name()
            email = self.owner.email
            self.author_name = '%s <%s>' % (name, email)

        if self.disabled:
            raise ValidationError(self.disabled_message)

    def submit(self, request, user=None, **kwargs):
        """
        Create a new Submission object for the given question and saves it on
        the database.

        Args:
            request:
                The request object for the current submission.
            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.
            user:
                The user who submitted the response. If not given, uses the user
                in the request object.
        """

        if hasattr(request, 'username'):
            raise ValueError

        # Test if activity is active
        if self.closed:
            raise ValueError('activity is closed to new submissions')

        # Fetch submission class
        submission_class = self.submission_class
        if submission_class is None:
            raise ImproperlyConfigured(
                '%s must define a submission_class attribute with the '
                'appropriate submission class.' % self.__class__.__name__
            )

        # Add progress information to the given submission kwargs
        if user is None:
            user = request.user
        logger.info('%r, submission from user %r' %
                    (self.title, user.username))
        progress = self.progress_set.for_user(user)
        return progress.submit(request, **kwargs)
Exemplo n.º 27
0
class AttendanceSheet(models.Model):
    """
    Controls student attendance by generating a new public pass-phrase under
    teacher request. Students confirm attendance by typing the secret phrase
    within a small interval after the teacher starts checking the attendance.
    """

    max_attempts = models.SmallIntegerField(
        _('Maximum number of attempts'),
        default=3,
        help_text=_(
            'How many times a student can attempt to prove attendance. A '
            'maximum is necessary to avoid a brute force attack.'
        ),
    )
    expiration_minutes = models.SmallIntegerField(
        _('Expiration time'),
        default=5,
        help_text=_(
            'Time (in minutes) before attendance session expires.'
        )
    )
    owner = models.ForeignKey(models.User)
    last_event = models.ForeignKey('Event', blank=True, null=True)
    max_string_distance = models.SmallIntegerField(
        _('Fuzzyness'),
        default=1,
        help_text=_(
            'Maximum number of wrong characters that is considered acceptable '
            'when comparing the expected passphrase with the one given by the'
            'student.'
        ),
    )
    max_number_of_absence = models.IntegerField(blank=True, null=True)

    # Properties
    expiration_interval = property(
        lambda self: datetime.timedelta(minutes=self.expiration_minutes))
    attendance_checks = property(
        lambda self: AttendanceCheck.objects.filter(event__sheet=self)
    )

    def __str__(self):
        try:
            return self.attendancepage_set.first().title
        except models.ObjectDoesNotExist:
            user = self.owner.get_full_name() or self.owner.username
            return _('Attendance sheet (%s)' % user)

    def new_event(self, commit=True):
        """
        Create a new event in attendance sheet.
        """

        current_time = now()
        event = Event(
            passphrase=phrase(),
            date=current_time.date(),
            created=current_time,
            expires=current_time + self.expiration_interval,
            sheet=self,
        )
        self.last_event = event
        if commit:
            event.save()
            self.save(update_fields=['last_event'])
        return event

    def current_passphrase(self):
        """
        Return the current passphrase.
        """
        return self.current_event().passphrase

    def current_event(self):
        """
        Return the last event created for today.

        If no event is found, create a new one.
        """

        if self.last_event and self.last_event.date == now().date():
            return self.last_event
        else:
            return self.new_event()

    def number_of_absences(self, user):
        """
        Return the total number of absence for user.
        """

        return self.attendance_checks.filter(user=user,
                                             has_attended=False).count()

    def absence_table(self, users=None, method='fraction'):
        """
        Return a mapping between users and their respective absence rate. 

        Args:
            users:
                A queryset of users.
            method:
                One of 'fraction' (default), 'number', 'attendance' or 
                'attendance-fraction'
        """

        try:
            get_value_from_absence = {
                'fraction': lambda x: x / num_events,
                'number': lambda x: x,
                'attendance': lambda x: num_events - x,
                'attendance-fraction': lambda x: (num_events - x) / num_events
            }[method]
        except KeyError:
            raise ValueError('invalid method: %r' % method)

        num_events = self.events.count()
        if users is None:
            users = models.User.objects.all()

        result = collections.OrderedDict()
        for user in users:
            absence = self.user_absence(user)
            result[user] = get_value_from_absence(absence)
        return result

    def user_attempts(self, user):
        """
        Return the number of user attempts in the last attendance event.
        """

        if self.last_event is None:
            return 0

        qs = self.attendance_checks.filter(user=user, event=self.last_event)
        return qs.count()

    def minutes_left(self, raises=True):
        """
        Return how many minutes left for expiration.
        """

        if self.last_event:
            time = now()
            if self.last_event.expires < time:
                return 0.0
            else:
                dt = self.last_event.expires - time
                return dt.total_seconds() / 60.
        if raises:
            raise ValueError('last event is not defined')
        else:
            return None

    def is_expired(self):
        """
        Return True if last_event has already expired.
        """

        if not self.last_event:
            return False
        return self.last_event.expires < now()

    def is_valid(self, passphrase):
        """
        Check if passphrase is valid.
        """
        if self.is_expired():
            return False
        distance = string_distance(passphrase, self.current_passphrase())
        return distance <= self.max_string_distance
Exemplo n.º 28
0
class HasScorePage(models.Page):
    """
    Mixin abstract page class for Page elements that implement the Score API.

    Subclasses define points_value, stars_value, and difficulty fields that
    define how activities contribute to Codeschool score system.
    """
    class Meta:
        abstract = True

    DIFFICULTY_TRIVIAL = 0
    DIFFICULTY_VERY_EASY = 1
    DIFFICULTY_EASY = 2
    DIFFICULTY_REGULAR = 3
    DIFFICULTY_HARD = 4
    DIFFICULTY_VERY_HARD = 5
    DIFFICULTY_CHALLENGE = 6
    DIFFICULTY_CHOICES = [
        (DIFFICULTY_TRIVIAL, _('Trivial')),
        (DIFFICULTY_VERY_EASY, _('Very Easy')),
        (DIFFICULTY_EASY, _('Easy')),
        (DIFFICULTY_REGULAR, _('Regular')),
        (DIFFICULTY_HARD, _('Hard')),
        (DIFFICULTY_VERY_HARD, _('Very Hard')),
        (DIFFICULTY_CHALLENGE, _('Challenge!')),
    ]
    SCORE_FROM_DIFFICULTY = {
        DIFFICULTY_TRIVIAL: 10,
        DIFFICULTY_VERY_EASY: 30,
        DIFFICULTY_EASY: 60,
        DIFFICULTY_REGULAR: 100,
        DIFFICULTY_HARD: 150,
        DIFFICULTY_VERY_HARD: 250,
        DIFFICULTY_CHALLENGE: 500,
    }
    DEFAULT_DIFFICULTY = DIFFICULTY_REGULAR

    points_total = models.IntegerField(
        _('value'),
        blank=True,
        help_text=_(
            'Points may be awarded in specific contexts (e.g., associated with '
            'a quiz or in a list of activities) and in Codeschool\'s generic '
            'ranking system.'))
    stars_total = models.DecimalField(
        _('stars'),
        decimal_places=1,
        max_digits=5,
        blank=True,
        help_text=_(
            'Number of stars the activity is worth (fractional stars are '
            'accepted). Stars are optional bonus points for special '
            'accomplishments that can be used to trade "special powers" in '
            'codeschool.'),
        default=0.0)
    difficulty = models.IntegerField(
        blank=True,
        choices=DIFFICULTY_CHOICES,
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if not kwargs:
            self._score_memo = self.points_total, self.stars_total

    def clean(self):
        # Fill default difficulty
        if self.difficulty is None:
            self.difficulty = self.DEFAULT_DIFFICULTY

        # Fill default points value from difficulty
        if self.points_total is None:
            self.points_total = self.SCORE_FROM_DIFFICULTY[self.difficulty]

        super().clean()

    def save(self, *args, **kwargs):
        scores = getattr(self, '_score_memo', (0, 0))
        super().save(*args, **kwargs)

        # Update the ScoreTotals table, if necessary.
        if scores != (self.points_total, self.stars_total):
            points = self.points_total
            stars = self.stars_total
            TotalScore.update(self, points=points, stars=stars)

    def get_score_contributions(self):
        """
        Return a dictionary with the score value associated with
        points, score, and stars.
        """

        return {
            'points': self.points_total,
            'stars': self.stars_total,
        }