Source code for codeschool.lms.activities.models.activity

import decimal
import logging

from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.utils.translation import ugettext_lazy as _

from codeschool import models
from codeschool.types.rules import Rules
from codeschool.mixins import CommitMixin
from .utils import AuxiliaryClassIntrospection
from ..managers.activity import ActivityManager
from ..meta import ActivityMeta

logger = logging.getLogger('codeschool.lms.activities')
ZERO = decimal.Decimal(0)


def bool_to_true():
    return True


class Activity(CommitMixin,
               models.RoutableViewsPage,
               models.DecoupledAdminPage,
               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.'
        ),
    )
    # Do we need this? Can we use wagtail's live attribute?
    visible = models.BooleanField(
        _('Invisible'),
        default=bool_to_true,
        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()

    # 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)