Esempio n. 1
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('Course', 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'),
        ]),
    ]
Esempio n. 2
0
class QuizItem(models.Orderable):
    """
    A question in a quiz.
    """

    quiz = models.ParentalKey(
        'cs_questions.Quiz',
        related_name='quiz_items',
    )
    question = models.ForeignKey(
        'wagtailcore.Page',
        related_name='+',
    )
    weight = models.FloatField(
        _('value'),
        default=1.0,
        help_text=_(
            'The non-normalized weight of this item in the total quiz grade.'),
    )

    # Wagtail admin
    panels = [
        panels.PageChooserPanel('question', [
            'cs_questions.CodingIoQuestion',
            'cs_questions.FormQuestion',
        ]),
        panels.FieldPanel('weight'),
    ]
Esempio n. 3
0
class Faculty(models.DescribablePage):
    """
    Describes a faculty/department or any institution that is responsible for
    managing disciplines.
    """

    location_coords = models.CharField(
        _('coordinates'),
        max_length=255,
        blank=True,
        null=True,
        help_text=_(
            'Latitude and longitude coordinates for the faculty building. The '
            'coordinates are selected from a Google Maps widget.'),
    )

    @property
    def courses(self):
        return apps.get_model(
            'cs_core', 'Course').objects.filter(path__startswith=self.path)

    # Wagtail admin
    parent_page_types = ['wagtailcore.Page']
    subpage_types = None
    subpage_types = ['Discipline']
    content_panels = models.DescribablePage.content_panels + [
        panels.MultiFieldPanel([
            panels.FieldPanel('location_coords', classname="gmap"),
        ],
                               heading=_('Location')),
    ]
Esempio n. 4
0
class LessonInfo(models.Orderable):
    """
    Intermediate model between a LessonPage and a Calendar to make it
    orderable.
    """
    class Meta:
        verbose_name = _('Lesson')
        verbose_name_plural = _('Lessons')

    calendar = models.ParentalKey(
        Calendar,
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name='info',
    )
    page = models.OneToOneField(
        Lesson,
        null=True,
        blank=True,
        related_name='info',
    )
    title = models.TextField(
        _('title'),
        help_text=_('A brief description for the lesson.'),
    )
    date = models.DateField(
        _('date'),
        null=True,
        blank=True,
        help_text=_('Date scheduled for this lesson.'),
    )

    def save(self, *args, **kwargs):
        if self.pk is None and self.page is None:
            self.page = lesson_page = Lesson(
                title=self.title,
                slug=slugify(self.title),
            )
            lesson_page._created_for_lesson = self
            self.calendar.add_child(instance=lesson_page)
        super().save(*args, **kwargs)

    panels = [
        panels.FieldPanel('title'),
        panels.FieldPanel('date'),
    ]
Esempio n. 5
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'),
    ]
Esempio n. 6
0
class QuestionAdmin(WagtailAdmin):
    class Meta:
        model = models.Question
        abstract = True

    subpage_types = []

    content_panels = \
        ShortDescriptionPage.content_panels[:-1] + [
            panels.MultiFieldPanel([
                panels.FieldPanel('import_file'),
                panels.FieldPanel('short_description'),
            ], heading=_('Options')),
            panels.StreamFieldPanel('body'),
            panels.MultiFieldPanel([
                panels.FieldPanel('author_name'),
                panels.FieldPanel('comments'),
            ], heading=_('Optional information'),
                classname='collapsible collapsed'),
        ]
Esempio n. 7
0
class LoginSettings(BaseSetting):
    username_as_school_id = models.BooleanField(
        default=False,
        help_text=_(
            'If true, force the username be equal to the school id for all '
            'student accounts.'))
    school_id_regex = models.TextField(
        default='',
        blank=True,
        help_text=_(
            'A regular expression for matching valid school ids. If blank, no'
            'check will be performed on the validity of the given school ids'),
    )
    panels = [
        panels.MultiFieldPanel([
            panels.FieldPanel('username_as_school_id'),
            panels.FieldPanel('school_id_regex'),
        ],
                               heading=_('School id configuration'))
    ]
Esempio n. 8
0
class QuestionAdmin(ShortDecriptionAdmin):
    class Meta:
        model = models.Question
        abstract = True

    subpage_types = []

    content_panels = [
        ...,

        # Main description
        panels.StreamFieldPanel('body'),

        # Options
        panels.MultiFieldPanel([
            panels.FieldPanel('author_name'),
            panels.FieldPanel('comments'),
        ],
                               heading=_('Optional information'),
                               classname='collapsible collapsed'),
    ]
Esempio n. 9
0
class CodingIoQuestionAdmin(QuestionAdmin):
    class Meta:
        model = models.CodingIoQuestion

    content_panels = QuestionAdmin.content_panels[:]
    content_panels.insert(
        -1,
        panels.MultiFieldPanel([
            panels.FieldPanel('num_pre_tests'),
            panels.FieldPanel('pre_tests_source'),
        ],
                               heading=_('Pre-tests definitions')))
    content_panels.insert(
        -1,
        panels.MultiFieldPanel([
            panels.FieldPanel('num_post_tests'),
            panels.FieldPanel('post_tests_source'),
        ],
                               heading=_('Post-tests definitions')))
    content_panels.insert(
        -1, panels.InlinePanel('answers', label=_('Answer keys')))

    settings_panels = QuestionAdmin.settings_panels + [
        panels.MultiFieldPanel([
            panels.FieldPanel('language'),
            panels.FieldPanel('timeout'),
        ],
                               heading=_('Options'))
    ]
Esempio n. 10
0
class Choice(models.Orderable):
    question = models.ParentalKey(MultipleChoiceQuestion,
                                  related_name='choices')
    text = models.RichTextField(_('Choice description'))
    uuid = models.UUIDField(default=uuid.uuid4)
    value = models.DecimalField(
        _('Value'), decimal_places=1, max_digits=4,
        validators=[validators.MinValueValidator(0),
                    validators.MaxValueValidator(100)],
        help_text=_(
            'Grade given for users that choose these option (value=0, for an '
            'incorrect choice and value=100 for a correct one).'
        ),
        default=0,
    )

    def __repr__(self):
        return 'Choice(value=%s, ...)' % self.value

    panels = [
        panels.FieldPanel('text'),
        panels.FieldPanel('value'),
    ]
Esempio n. 11
0
class CodeCarouselItem(models.Orderable):
    """
    A simple state of the code in a SyncCodeActivity.
    """

    activity = models.ParentalKey('cs_core.CodeCarouselActivity',
                                  related_name='items')
    text = models.TextField()
    timestamp = models.DateTimeField(auto_now=True)

    # Wagtail admin
    panels = [
        panels.FieldPanel('text', widget=blocks.AceWidget()),
    ]
Esempio n. 12
0
class CodeCarouselActivity(Activity):
    """
    In this activity, the students follow a piece of code that someone
    edit and is automatically updated in all of student machines. It keeps
    track of all modifications that were saved by the teacher.
    """
    class Meta:
        verbose_name = _('synchronized code activity')
        verbose_name_plural = _('synchronized code activities')

    default_material_icon = 'code'
    language = models.ForeignKey(
        'ProgrammingLanguage',
        on_delete=models.PROTECT,
        related_name='sync_code_activities',
        help_text=_('Chooses the programming language for the activity'),
    )

    @property
    def last(self):
        try:
            return self.items.order_by('timestamp').last()
        except CodeCarouselItem.DoesNotExist:
            return None

    @property
    def first(self):
        try:
            return self.items.order_by('timestamp').first()
        except CodeCarouselItem.DoesNotExist:
            return None

    # Wagtail admin
    content_panels = models.CodeschoolPage.content_panels + [
        panels.MultiFieldPanel([
            panels.RichTextFieldPanel('short_description'),
            panels.FieldPanel('language'),
        ],
                               heading=_('Options')),
        panels.InlinePanel('items', label='Items'),
    ]
Esempio n. 13
0
class Question(models.RoutablePageMixin, Activity):
    """
    Base abstract class for all question types.
    """

    class Meta:
        abstract = True
        permissions = (("download_question", "Can download question files"),)

    stem = models.StreamField(
        QUESTION_STEM_BLOCKS,
        blank=True,
        null=True,
        verbose_name=_('Question description'),
        help_text=_(
            'Describe what the question is asking and how should the students '
            'answer it as clearly as possible. Good questions should not be '
            'ambiguous.'
        ),
    )
    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.'
        ),
    )
    comments = models.RichTextField(
        _('Comments'),
        blank=True,
        help_text=_('(Optional) Any private information that you want to '
                    'associate to the question page.')
    )

    @property
    def long_description(self):
        return str(self.stem)

    # Permission control
    def can_edit(self, user):
        """Only the owner of the question can edit it"""
        if user is None or self.owner is None:
            return False
        return self.owner.pk == user.pk

    def can_create(self, user):
        """You have to be the teacher of a course in order to create new
        questions."""

        return not user.courses_as_teacher.empty()

    # Serving pages and routing
    @srvice.route(r'^submit-response/$')
    def respond_route(self, client, **kwargs):
        """
        Handles student responses via AJAX and a srvice program.
        """

        raise NotImplementedError

    @models.route(r'^stats/$')
    def stats_route(self, request, **kwargs):
        """
        Shows the stats for each question.
        """

        data = """<dl>
            <dt>Name<dt><dd>{name}<dd>
            <dt>Best grade<dt><dd>{best}<dd>
            <dt>Responses<dt><dd>{n_responses}<dd>
            <dt>Response items<dt><dd>{n_response_items}<dd>
            <dt>Correct responses<dt><dd>{n_correct}<dd>
            <dt>Mean grade responses<dt><dd>{mean}<dd>
            <dt>Context id</dt><dd>{context_id}</dd>
        </dl>
        """.format(
            context_id=self.default_context.id,
            name=self.title,
            best=self.best_final_grade(),
            mean=self.mean_final_grade(),
            n_correct=self.correct_responses().count(),
            n_response_items=self.response_items().count(),
            n_responses=self.responses.count(),
        )

        # Renders content
        context = {'content_body': data,
                   'content_text': 'Stats'}
        return render(request, 'base.jinja2', context)

    @models.route(r'^responses/')
    def response_list_route(self, request):
        """
        Renders a list of responses
        """

        user = request.user
        context = self.get_context(request)
        items = self.response_items(user=user, context='any')
        items = (x.get_real_instance() for x in items)
        context.update(
            question=self,
            object_list=items,
        )
        return render(request, 'cs_questions/response-list.jinja2', context)

    # Wagtail admin
    parent_page_types = [
        'cs_questions.QuestionList',
        'cs_core.Discipline',
        'cs_core.Faculty'
    ]
    content_panels = Activity.content_panels + [
        panels.StreamFieldPanel('stem'),
        panels.MultiFieldPanel([
            panels.FieldPanel('author_name'),
            panels.FieldPanel('comments'),
        ], heading=_('Optional information'),
           classname='collapsible collapsed'),
    ]
Esempio n. 14
0
class Profile(UserenaBaseProfile, models.CodeschoolPage):
    """
    Social information about users.
    """
    class Meta:
        permissions = (
            ('student', _('Can access/modify data visible to student\'s')),
            ('teacher',
             _('Can access/modify data visible only to Teacher\'s')),
        )

    username = delegate_to('user', True)
    first_name = delegate_to('user')
    last_name = delegate_to('user')
    email = delegate_to('user')

    @property
    def short_description(self):
        return '%s (id: %s)' % (self.get_full_name_or_username(),
                                self.school_id)

    @property
    def age(self):
        if self.date_of_birth is None:
            return None
        today = timezone.now().date()
        return int(round((today - self.date_of_birth).years))

    user = models.OneToOneField(
        models.User,
        unique=True,
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
        verbose_name=_('user'),
        related_name='_profile',
    )
    school_id = models.CharField(
        _('school id'),
        help_text=_('Identification number in your school issued id card.'),
        max_length=50,
        blank=True,
        null=True)
    nickname = models.CharField(max_length=50, blank=True, null=True)
    phone = models.CharField(max_length=20, blank=True, null=True)
    gender = models.SmallIntegerField(_('gender'),
                                      choices=[(0, _('male')),
                                               (1, _('female'))],
                                      blank=True,
                                      null=True)
    date_of_birth = models.DateField(_('date of birth'), blank=True, null=True)
    website = models.URLField(blank=True, null=True)
    about_me = models.RichTextField(blank=True, null=True)
    objects = ProfileManager.from_queryset(models.PageManager)()

    def __init__(self, *args, **kwargs):
        if 'user' in kwargs and 'id' not in kwargs:
            kwargs.setdefault('parent_page', profile_root())
        super().__init__(*args, **kwargs)

        if self.pk is None and self.user is not None:
            user = self.user
            self.title = self.title or __("%(name)s's profile") % {
                'name': user.get_full_name() or user.username
            }
            self.slug = self.slug or user.username.replace('.', '-')

    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_full_name_or_username(self):
        name = self.user.get_full_name()
        if name:
            return name
        else:
            return self.user.username

    # Wagtail admin
    parent_page_types = ['cs_core.ProfileRoot']
    content_panels = models.CodeschoolPage.content_panels + [
        panels.MultiFieldPanel([
            panels.FieldPanel('school_id'),
        ],
                               heading='Required information'),
        panels.MultiFieldPanel([
            panels.FieldPanel('nickname'),
            panels.FieldPanel('phone'),
            panels.FieldPanel('gender'),
            panels.FieldPanel('date_of_birth'),
        ],
                               heading=_('Personal Info')),
        panels.MultiFieldPanel([
            panels.FieldPanel('website'),
        ],
                               heading=_('Web presence')),
        panels.RichTextFieldPanel('about_me'),
    ]
Esempio n. 15
0
class ActivitySection(ScoreBoardMixin,
                      mixins.ShortDescriptionPage,
                      models.Page):
    """
    List of activities.
    """

    class Meta:
        verbose_name = _('section')
        verbose_name_plural = _('sections')

    material_icon = models.CharField(
        _('Optional icon'),
        max_length=20,
        default='help',
    )
    objects = ActivityListManager()

    @property
    def activities(self):
        return [x.specific for x in self.get_children()]

    # Special template constructors
    @classmethod
    def create_subpage(cls, parent=None, **kwargs):
        """
        Create a new ActivitySection using the given keyword arguments under the
        given parent page. If no parent is chosen, uses the "main-question-list"
        reference.
        """

        parent = parent or model_reference.load('main-question-list')
        new = cls(**kwargs)
        parent.add_child(instance=new)
        new.save()
        return new

    @classmethod
    def from_template(cls, template, parent=None):
        """
        Creates a new instance from the given template.

        Valid templates are:
            basic
                Very basic beginner IO based problems. First contact with
                programming.
            conditionals
                Simple problems based on if/else flow control.
            loops
                Problems that uses for/while loops.
            functions
                Problems that uses functions.
            files
                Reading and writing files.
            lists
                Linear data structures such as lists and arrays.
        """

        try:
            factory = getattr(cls, '_template_%s' % template)
            return factory(parent)
        except AttributeError:
            raise ValueError('invalid template name: %r' % template)

    @classmethod
    def _template_basic(cls, parent):
        return cls.create_subpage(
            parent,
            title=_('Basic'),
            short_description=_('Elementary programming problems.'),
            slug='basic',
            material_icon='insert_emoticon',
        )

    @classmethod
    def _template_conditionals(cls, parent):
        return cls.create_subpage(
            parent,
            title=_('Conditionals'),
            short_description=_('Conditional flow control (if/else).'),
            slug='conditionals',
            material_icon='code',
        )

    @classmethod
    def _template_loops(cls, parent):
        return cls.create_subpage(
            parent,
            title=_('Loops'),
            short_description=_('Iterations with for/while commands.'),
            slug='loops',
            material_icon='loop',
        )

    @classmethod
    def _template_functions(cls, parent):
        return cls.create_subpage(
            parent,
            title=_('Functions'),
            short_description=_('Organize code using functions.'),
            slug='functions',
            material_icon='functions',
        )

    @classmethod
    def _template_files(cls, parent):
        return cls.create_subpage(
            parent,
            title=_('Files'),
            short_description=_('Open, process and write files.'),
            slug='files',
            material_icon='insert_drive_file',
        )

    @classmethod
    def _template_lists(cls, parent):
        return cls.create_subpage(
            parent,
            title=_('Lists'),
            short_description=_('Linear data structures.'),
            slug='lists',
            material_icon='list',
        )

    def save(self, *args, **kwargs):
        if self.title is None:
            self.title = _('List of activities')
        if self.slug is None:
            self.slug = 'activities'
        super().save(*args, **kwargs)

    def score_board_total(self):
        """
        Return a score board mapping with the total score for each user.
        """

        board = self.score_board()
        scores = ScoreMap(self.title)
        for k, L in board.items():
            scores[k] = sum(L)
        return scores

    # Serving pages
    template = 'lms/activities/section.jinja2'

    def get_context(self, request, *args, **kwargs):
        return dict(
            super().get_context(request, *args, **kwargs),
            object_list=[obj.specific for obj in self.get_children()]
        )

    # Wagtail Admin
    parent_page_types = [ActivityList]
    content_panels = mixins.ShortDescriptionPage.content_panels + [
        panels.FieldPanel('material_icon')
    ]
Esempio n. 16
0
class Question(models.RoutablePageMixin,
               models.ShortDescriptionPageMixin,
               Activity,
               metaclass=QuestionMeta):
    """
    Base abstract class for all question types.
    """
    class Meta:
        abstract = True
        permissions = (("download_question", "Can download question files"), )

    EXT_TO_METHOD_CONVERSIONS = {'yml': 'yaml'}
    OPTIONAL_IMPORT_FIELDS = [
        'author_name', 'comments', 'score_value', 'star_value'
    ]
    base_form_class = QuestionAdminModelForm

    body = models.StreamField(
        QUESTION_BODY_BLOCKS,
        blank=True,
        null=True,
        verbose_name=_('Question description'),
        help_text=_(
            'Describe what the question is asking and how should the students '
            'answer it as clearly as possible. Good questions should not be '
            'ambiguous.'),
    )
    comments = models.RichTextField(
        _('Comments'),
        blank=True,
        help_text=_('(Optional) Any private information that you want to '
                    'associate to the question page.'))
    import_file = models.FileField(
        _('import question'),
        null=True,
        blank=True,
        upload_to='question-imports',
        help_text=_(
            'Fill missing fields from question file. You can safely leave this '
            'blank and manually insert all question fields.'))
    __imported_data = None

    def load_from_file_data(self, file_data):
        """
        Import content from raw file data.
        """

        fmt = self.loader_format_from_filename(file_data.name)
        self.load_from(file_data, format=fmt)
        self.__imported_data = dict(self.__dict__)

        logger.info('Imported question "%s" from file "%s"' %
                    (self.title, self.import_file.name))

        # We fake POST data after loading data from file in order to make the
        # required fields to validate. This part constructs a dictionary that
        # will be used to feed a fake POST data in the QuestionAdminModelForm
        # instance
        fake_post_data = {
            'title': self.title or _('Untitled'),
            'short_description': self.short_description or _('untitled'),
        }

        for field in self.OPTIONAL_IMPORT_FIELDS:
            if getattr(self, field, None):
                fake_post_data[field] = getattr(self, field)

        base_slug = slugify(fake_post_data['title'])
        auto_generated_slug = self._get_autogenerated_slug(base_slug)
        fake_post_data['slug'] = auto_generated_slug
        return fake_post_data

    def loader_format_from_filename(self, name):
        """
        Returns a string with the loader method from the file extension
        """

        _, ext = os.path.splitext(name)
        ext = ext.lstrip('.')
        return self.EXT_TO_METHOD_CONVERSIONS.get(ext, ext)

    def load_from(self, data, format='yaml'):
        """
        Load data from the given file or string object using the specified
        method.
        """

        try:
            loader = getattr(self, 'load_from_%s' % format)
        except AttributeError:
            raise ValueError('format %r is not implemented' % format)
        return loader(data)

    def full_clean(self, *args, **kwargs):
        if self.__imported_data is not None:
            blacklist = {
                # References
                'id',
                'owner_id',
                'page_ptr_id',
                'content_type_id',

                # Saved fields
                'title',
                'short_description',
                'seo_title',
                'author_name',
                'slug',
                'comments',
                'score_value',
                'stars_value',
                'difficulty',

                # Forbidden fields
                'import_file',

                # Wagtail fields
                'path',
                'depth',
                'url_path',
                'numchild',
                'go_live_at',
                'expire_at',
                'show_in_menus',
                'has_unpublished_changes',
                'latest_revision_created_at',
                'first_published_at',
                'live',
                'expired',
                'locked',
                'search_description',
            }

            data = {
                k: v
                for k, v in self.__imported_data.items()
                if (not k.startswith('_')) and k not in blacklist and v not in
                (None, '')
            }

            for k, v in data.items():
                setattr(self, k, v)

        super().full_clean(*args, **kwargs)

    # Serve pages
    def get_context(self, request, *args, **kwargs):
        return dict(
            super().get_context(request, *args, **kwargs),
            response=self.responses.response_for_request(request),
            question=self,
            form_name='response-form',
        )

    @srvice.route(r'^submit-response/$')
    def route_submit(self, client, **kwargs):
        """
        Handles student responses via AJAX and a srvice program.
        """

        response = self.submit(user=client.user, **kwargs)
        response.autograde()
        data = render_html(response)
        client.dialog(html=data)

    @models.route(r'^submissions/$')
    def route_submissions(self, request, *args, **kwargs):
        submissions = self.submissions.user(request.user).order_by('-created')
        context = self.get_context(request, *args, **kwargs)
        context['submissions'] = submissions

        # Fetch template name from explicit configuration or compute the default
        # value from the class name
        try:
            template = getattr(self, 'template_submissions')
            return render(request, template, context)
        except AttributeError:
            name = self.__class__.__name__.lower()
            if name.endswith('question'):
                name = name[:-8]
            template = 'questions/%s/submissions.jinja2' % name

            try:
                return render(request, template, context)
            except TemplateDoesNotExist:
                raise ImproperlyConfigured(
                    'Model %s must define a template_submissions attribute. '
                    'You  may want to extend this template from '
                    '"questions/submissions.jinja2"' % self.__class__.__name__)

    @models.route(r'^leaderboard/$')
    @models.route(r'^statistics/$')
    @models.route(r'^submissions/$')
    @models.route(r'^social/$')
    def route_page_does_not_exist(self, request):
        return render(
            request, 'base.jinja2', {
                'content_body':
                'The page you are trying to see is not implemented '
                'yet.',
                'content_title':
                'Not implemented',
                'title':
                'Not Implemented'
            })

    # Wagtail admin
    subpage_types = []
    content_panels = models.ShortDescriptionPageMixin.content_panels[:-1] + [
        panels.MultiFieldPanel([
            panels.FieldPanel('import_file'),
            panels.FieldPanel('short_description'),
        ],
                               heading=_('Options')),
        panels.StreamFieldPanel('body'),
        panels.MultiFieldPanel([
            panels.FieldPanel('author_name'),
            panels.FieldPanel('comments'),
        ],
                               heading=_('Optional information'),
                               classname='collapsible collapsed'),
    ]
Esempio n. 17
0
from django.utils.translation import ugettext_lazy as _
from codeschool.fixes.wagtailadmin import WagtailAdmin
from codeschool.models import Page
from codeschool import panels
from . import models


class AttendancePageAdmin(WagtailAdmin):

    class Meta:
        model = models.AttendancePage
        abstract = True

    subpage_types = []

    content_panels = \
        Page.content_panels + [
            panels.InlinePanel('attendance_sheet_single_list',
                               max_num=1, min_num=1),
        ]


models.AttendanceSheet.panels = [
    panels.FieldPanel('max_attempts'),
    panels.FieldPanel('expiration_minutes'),
    panels.FieldPanel('max_string_distance'),
]
Esempio n. 18
0
class Activity(models.CopyMixin,
               models.ShortDescribablePage):
    """
    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')

    icon_src = models.CharField(
        _('activity icon'),
        max_length=50,
        blank=True,
        help_text=_(
            'Optional icon name that can be used to personalize the activity. '
            'Material icons are available by using the "material:" namespace '
            'as in "material:menu".'),
    )
    resources = models.StreamField(RESOURCE_BLOCKS)
    objects = models.PageManager.from_queryset(ActivityQueryset)()

    # References
    @property
    def course(self):
        """
        Points to the course this activity belongs to.
        """

        return getattr(self.get_parent(), 'course_instance', None)

    @course.setter
    def course(self, value):
        self.set_parent(value)

    @property
    def default_context(self):
        """
        Return the default context.
        """

        cls = apps.get_model('cs_core', 'ResponseContext')
        return cls.objects.get_or_create(activity_id=self.id,
                                         name='default')[0]

    #: Define the default material icon used in conjunction with instances of
    #: the activity class.
    default_material_icon = 'help'

    #: The response class associated with the given activity.
    response_class = None

    @property
    def material_icon(self):
        """
        The material icon used in conjunction with the activity.
        """

        if self.icon_src.startswith('material:'):
            return self.icon_src[9:]
        return self.default_material_icon

    @property
    def icon_html(self):
        """
        A string of HTML source that points to the icon element fo the activity.
        """

        return '<i class="material-icon">%s</i>' % self.material_icon

    # We define the optional user and context objects to bind responses to the
    # question object. These are not saved into the database, but are rather
    # used as default values to fill-in in the response objects. These objects
    # can be bound at init time or using the bind() method.
    @bound_property
    def user(self):
        return getattr(self, '_user', None)

    @user.setter
    def user(self, value):
        if isinstance(value, int):
            value = models.User.objects.get(pk=value)
        elif isinstance(value, str):
            value = models.User.objects.get(username=value)
        if not isinstance(value, (models.User, type(None))):
            raise TypeError('invalid user: %r' % value)
        self._user = value

    @bound_property
    def response_context(self):
        try:
            return self._context
        except AttributeError:
            return self.default_context

    @response_context.setter
    def context(self, value):
        if isinstance(value, int):
            value = ResponseContext.objects.get(pk=int)
        if not isinstance(value, (models.ResponseContext, type(None))):
            raise TypeError('invalid context: %r' % value)
        self._context = value

    @property
    def is_user_bound(self):
        return self.user is not None

    @property
    def is_context_bound(self):
        return self.context is not None

    def __init__(self, *args, **kwargs):
        # Get parent page from initialization
        course = kwargs.pop('course', None)
        discipline = kwargs.pop('discipline', None)
        user = kwargs.pop('user', None)
        if sum(1 for x in [course, discipline, user] if x is not None) >= 2:
            raise TypeError(
                'Can only use one of course, discipline or user arguments.'
            )
        super().__init__(*args, **kwargs)
        parent = course or discipline or user
        if parent is not None:
            self.set_parent(parent)

    def bind(self, *args, **kwargs):
        """
        Temporary binds objects to activity.

        This is useful to bind a question instance to some specific user or
        response context. These changes are not persisted on the database and
        are just a convenience for using other methods.

        This method accept a single positional argument for passing a request
        object. Any number of keyword arguments might be given for each
        registered binding properties for the object. For convenience, invalid
        arguments are just ignored.
        """

        if args:
            request = args[0]
            kwargs.setdefault('user', request.user)

        # We check in the class if each item is a bound_property. If so, we
        # save its value with the given data.
        cls = self.__class__
        for k, v in kwargs.items():
            if isinstance(getattr(cls, k, None), bound_property):
                setattr(self, k, v)

        # Return self so this method can be chained.
        return self

    #
    # Response control
    #
    def get_response(self, user=None, context=None):
        """
        Get the response associated with given user and context.

        If no user and context is given, use the bound values.
        """

        user = user or self.user
        context = context or self.context
        Response = apps.get_model('cs_core', 'Response')
        return Response.get_response(user=user, context=context, activity=self)

    def register_response_item(self, *,
                               response_data=None,
                               user=None,
                               context=None,
                               autograde=False,
                               recycle=False,
                               _kwargs=None):
        """
        Create a new response item object for the given question and saves it on
        the database.

        Args:
            user:
                The user who submitted the response.
            context:
                The context object associated with the response. Uses the
                default context, if not given.
            autograde:
                If true, calls the autograde() method in the response to
                give the automatic gradings.
            recycle:
                If true, recycle response items with the same content as the
                submission. It checks if a different submission exists with the
                same response_data attribute. If so, it returns this submission
                instead of saving a new one in the database.
            _kwargs:
                Additional arguments that should be passed to the
                response item constructor. This should only be used by
                subclasses to pass extra arguments to the super method.
        """

        response_item_class = self.response_item_class

        # Check if the user and context are given
        user = user or self.user
        context = context or self.context
        if user is None:
            raise TypeError('a valid user is required')

        # We compute the hash and compare it with values on the database
        # if recycle is enabled
        response_hash = response_item_class.get_response_hash(response_data)
        response = None
        recycled = False
        if recycle:
            recyclable = response_item_class.objects.filter(
                activity=self,
                context=context,
                response_hash=response_hash,
            )
            for pk, value in recyclable.values_list('id', 'response_data'):
                if value == response_data:
                    response = recyclable.get(pk=pk)
                    recycled = True

        # Proceed if no response was created
        if response is None:
            response = self.response_item_class(
                user=user,
                context=context,
                activity=self,
                response_hash=response_hash,
                response_data=response_data,
                **(_kwargs or {})
            )

        # If the context owner is not the current activity, we have to take
        # additional steps to finalize the response_item to a proper state.
        if context.activity_id != self.id:
            context.activity.process_response_item(response, recycled)

        # Finalize response item
        if autograde:
            response.autograde()
        else:
            response.save()
        return response

    def process_response_item(self, response, recycled=False):
        """
        Process this response item generated by other activities using a context
        that you own.

        This might happen in compound activities like quizzes, in which the
        response to a question uses a context own by a quiz object. This
        function allows the container object to take any additional action
        after the response is created.
        """

    def has_response(self, user=None, context=None):
        """
        Return True if the user has responded to the activity.
        """

        response = self.get_response(user, context)
        return response.response_items.count() >= 1

    def correct_responses(self, context=None):
        """
        Return a queryset with all correct responses for the given context.
        """

        done = apps.get_model('cs_core', 'ResponseItem').STATUS_DONE
        items = self.response_items(context, status=done)
        return items.filter(given_grade=100)

    # def get_user_response(self, user, method='first'):
    #     """
    #     Return some response given by the user or None if the user has not
    #     responded.
    #
    #     Allowed methods:
    #         unique:
    #             Expects that response is unique and return it (or None).
    #         any:
    #             Return a random user response.
    #         first:
    #             Return the first response given by the user.
    #         last:
    #             Return the last response given by the user.
    #         best:
    #             Return the response with the best final grade.
    #         worst:
    #             Return the response with the worst final grade.
    #         best-given:
    #             Return the response with the best given grade.
    #         worst-given:
    #             Return the response with the worst given grade.
    #
    #     """
    #
    #     responses = self.responses.filter(user=user)
    #     first = lambda x: x.select_subclasses().first()
    #
    #     if method == 'unique':
    #         N = self.responses.count()
    #         if N == 0:
    #             return None
    #         elif N == 1:
    #             return response.select_subclasses().first()
    #         else:
    #             raise ValueError(
    #                 'more than one response found for user %r' % user.username
    #             )
    #     elif method == 'any':
    #         return first(responses)
    #     elif method == 'first':
    #         return first(responses.order_by('created'))
    #     elif method == 'last':
    #         return first(responses.order_by('-created'))
    #     elif method in ['best', 'worst', 'best-given', 'worst-given']:
    #         raise NotImplementedError(
    #             'method = %r is not implemented yet' % method
    #         )
    #     else:
    #         raise ValueError('invalid method: %r' % method)
    #
    # def autograde_all(self, force=False, context=None):
    #     """
    #     Grade all responses that had not been graded yet.
    #
    #     This function may take a while to run, locking the server. Maybe it is
    #     a good idea to run it as a task or in a separate thread.
    #
    #     Args:
    #         force (boolean):
    #             If True, forces the response to be re-graded.
    #     """
    #
    #     # Run autograde on each responses
    #     for response in responses:
    #         response.autograde(force=force)
    #
    # def select_users(self):
    #     """
    #     Return a queryset with all users that responded to the activity.
    #     """
    #
    #     user_ids = self.responses.values_list('user', flat=True).distinct()
    #     users = models.User.objects.filter(id__in=user_ids)
    #     return users
    #
    # def get_grades(self, users=None):
    #     """
    #     Return a dictionary mapping each user to their respective grade in the
    #     activity.
    #
    #     If a list of users is given, include only the users in this list.
    #     """
    #
    #     if users is None:
    #         users = self.select_users()
    #
    #     grades = {}
    #     for user in users:
    #         grade = self.get_user_grade(user)
    #         grades[user] = grade
    #     return grades

    #
    # Statistics
    #
    def response_items(self, context=None, status=None, user=None):
        """
        Return a queryset with all response items associated with the given
        activity.

        Can filter by context, status and user
        """

        items = self.response_item_class.objects
        queryset = items.filter(response__activity_id=self.id)

        # Filter context
        if context != 'any':
            context = context or self.context
            queryset = queryset.filter(response__context_id=context.id)

        # Filter user
        user = user or self.user
        if user:
            queryset = queryset.filter(response__user_id=user.id)

        # Filter by status
        if status:
            queryset = queryset.filter(status=status)

        return queryset

    def _stats(self, attr, context, by_item=False):
        if by_item:
            items = self.response_items(context, self.STATUS_DONE)
            values_list = items.values_list(attr, flat=True)
            return Statistics(attr, values_list)
        else:
            if context == 'any':
                items = self.responses.all()
            else:
                context = context or self.context
                items = self.responses.all().filter(context=context)
            return Statistics(attr, items.values_list(attr, flat=True))

    def best_final_grade(self, context=None):
        """
        Return the best final grade given for this activity.
        """

        return self._stats('final_grade', context).max()

    def best_given_grade(self, context=None):
        """
        Return the best grade given for this activity before applying any
        penalties and bonuses.
        """

        return self._stats('given_grade', context).min()

    def mean_final_grade(self, context=None, by_item=False):
        """
        Return the average value for the final grade for this activity.

        If by_item is True, compute the average over all response items instead
        of using the responses for each student.
        """

        return self._stats('final_grade', context, by_item).mean()

    def mean_given_grade(self, by_item=False):
        """
        Return the average value for the given grade for this activity.
        """

        return self._stats('given_grade', context, by_item).mean()

    #
    # Permission control
    #
    def can_edit(self, user):
        """
        Return True if user has permissions to edit activity.
        """

        return user == self.owner or self.course.can_edit(user)

    def can_view(self, user):
        """
        Return True if user has permission to view activity.
        """

        course = self.course
        return (
            self.can_edit(user) or
            user in course.students.all() or
            user in self.staff.all()
        )

    # Wagtail admin
    subpage_types = []
    parent_page_types = [
        'cs_core.Course',
        'cs_core.LessonPage'
    ]
    content_panels = models.CodeschoolPage.content_panels + [
        panels.MultiFieldPanel([
            panels.RichTextFieldPanel('short_description'),
        ], heading=_('Options')),
    ]
    promote_panels = models.CodeschoolPage.promote_panels + [
        panels.FieldPanel('icon_src')
    ]
    settings_panels = models.CodeschoolPage.settings_panels + [
        panels.StreamFieldPanel('resources'),
    ]
Esempio n. 19
0
class Quiz(Activity):
    """
    A quiz that may contain several different questions.
    """
    class Meta:
        verbose_name = _('quiz activity')
        verbose_name_plural = _('quiz activities')

    body = models.StreamField(
        QUESTION_STEM_BLOCKS,
        blank=True,
        null=True,
        verbose_name=_('Quiz description'),
        help_text=_(
            'This field should contain a text with any instructions, tips, or '
            'information that is relevant to the current quiz. Remember to '
            'explain clearly the rules and what is expected from each student.'
        ),
    )
    language = models.ForeignKey(
        ProgrammingLanguage,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
        related_name='quizzes',
        verbose_name=_('Programming language'),
        help_text=_(
            'Forces an specific programming language for all programing '
            'related questions. If not given, will accept responses in any '
            'programming language. This has no effect in non-programming '
            'activities.'),
    )

    # Derived attributes
    questions = property(lambda x: [i.question for i in x.quiz_items.all()])
    num_questions = property(lambda x: x.quiz_items.count())

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

        self.quiz_items.create(question=question, weight=weight)
        item = QuizItem(quiz=self, question=question)
        item.save()
        self.items.append(item)

    def register_response_item(self, *, user=None, context=None, **kwargs):
        """
        Return a response object for the given user.

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

        # Silently ignore autograde
        kwargs.pop('autograde', None)

        # Quiz responses do not accept any extra parameters in the constructor
        if kwargs:
            param = kwargs.popitem()[0]
            raise TypeError('invalid parameter: %r' % param)

        # We force that quiz responses have a single response_item which is
        # only updated by the process_response_item() method.
        response = self.get_response(user, context)
        if response.items.count() != 0:
            return response.items.first()

        return super().register_response_item(user=user, context=context)

    def process_response_item(self, response, recycled=False):
        """
        Process a question response and adds a reference to it in the related
        QuizResponseItem.
        """

        # We do not register recycled responses
        if not recycled:
            user = response.user
            context = response.context
            quiz_response_item = self.register_response_item(user=user,
                                                             context=context)
            quiz_response_item.register_response(response)

    # Wagtail admin
    parent_page_types = ['cs_questions.QuizList']
    content_panels = Activity.content_panels + [
        panels.StreamFieldPanel('body'),
        panels.InlinePanel('quiz_items', label=_('Questions')),
    ]
    settings_panels = Activity.settings_panels + [
        panels.FieldPanel('language'),
    ]
Esempio n. 20
0
class AnswerKey(models.Model):
    """
    Represents an answer to some question given in some specific computer
    language plus the placeholder text that should be displayed.
    """
    class ValidationError(Exception):
        pass

    class Meta:
        verbose_name = _('answer key')
        verbose_name_plural = _('answer keys')
        unique_together = [('question', 'language')]

    question = models.ParentalKey(CodingIoQuestion, related_name='answers')
    language = models.ForeignKey(
        ProgrammingLanguage,
        related_name='+',
    )
    source = models.TextField(
        _('answer source code'),
        blank=True,
        help_text=_(
            'Source code for the correct answer in the given programming '
            'language.'),
    )
    placeholder = models.TextField(
        _('placeholder source code'),
        blank=True,
        help_text=_(
            'This optional field controls which code should be placed in '
            'the source code editor when a question is opened. This is '
            'useful to put boilerplate or even a full program that the '
            'student should modify. It is possible to configure a global '
            'per-language boilerplate and leave this field blank.'),
    )
    source_hash = models.CharField(
        max_length=32,
        blank=True,
        help_text=_('Hash computed from the reference source'),
    )
    iospec_hash = models.CharField(
        max_length=32,
        blank=True,
        help_text=_('Hash computed from reference source and iospec_size.'),
    )
    iospec_source = models.TextField(
        _('expanded source'),
        blank=True,
        help_text=_(
            'Iospec source for the expanded testcase. This data is computed '
            'from the reference iospec source and the given reference program '
            'to expand the outputs from the given inputs.'))

    objects = AnswerKeyQueryset.as_manager()
    iospec_size = property(lambda x: x.question.iospec_size)

    @lazy
    def iospec(self):
        return parse_iospec(self.iospec_source)

    def __repr__(self):
        return '<AnswerKeyItem: %s (%s)>' % (self.question, self.language)

    def __str__(self):
        return '%s (%s)>' % (self.question, self.language)

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

        if self.question is None:
            return

        # We only have to update if the parent's hash is incompatible with the
        # current hash and the source field is defined. We make this test to
        # perform the expensive code re-evaluation only when strictly necessary
        parent_hash = self.parent_hash()
        source_hash = md5hash(self.source)

        if parent_hash != self.iospec_hash or source_hash != self.source_hash:
            try:
                iospec = self.question.iospec
            except Exception:
                raise ValidationError(
                    _('cannot register answer key for question with invalid '
                      'iospec.'))
            result = self._update_state(iospec, self.source, self.language)
            self.iospec_source = result.source()
            self.source_hash = source_hash
            self.iospec_hash = parent_hash

    def update(self, commit=True):
        """
        Update the internal iospec source and hash keys to match the given
        parent iospec value.

        It raises a ValidationError if the source code is invalid.
        """

        iospec = self.question.iospec
        result = self._update_state(iospec, self.source, self.language)
        self.iospec_source = result.source()
        self.source_hash = md5hash(self.source)
        self.iospec_hash = self.parent_hash()
        if commit:
            self.save()

    def _update_state(self, iospec, source, language):
        """
        Worker function for the .update() and .clean() methods.

        Update the hashes and the expanded iospec_source for the answer key.
        """

        # We expand inputs and compute the result for the given source code
        # string
        language = language.ejudge_ref()
        if len(iospec) <= self.iospec_size:
            iospec.expand_inputs(self.iospec_size)
        result = run_code(source, iospec, language)

        # Check if the result has runtime or build errors
        if result.has_errors:
            for testcase in iospec:
                result = run_code(source, testcase, language)
                if result.has_errors:
                    error_dic = {
                        'error': escape(result.get_error_message()),
                        'iospec': escape(testcase.source())
                    }
                    raise ValidationError(
                        {'source': mark_safe(ERROR_TEMPLATE % error_dic)})

        # The source may run fine, but still give results that are inconsistent
        # with the given testcases. This will only be noticed if the user
        # provides at least one simple IO test case.
        for (expected, value) in zip(iospec, result):
            expected_source = expected.source().rstrip()
            value_source = value.source().rstrip()
            if expected.is_simple and expected_source != value_source:
                msg = _(
                    '<div class="error-message">'
                    'Your program produced invalid results in this tescase:\n'
                    '<br>\n'
                    '<pre>%(diff)s</pre>\n'
                    '</div>')
                error = {
                    'diff':
                    '\n'.join(
                        differ.compare(expected.source().rstrip().splitlines(),
                                       value.source().rstrip().splitlines()))
                }
                msg = mark_safe(msg % error)
                raise ValidationError({'source': msg})

        # Now we save the result because it has all the computed expansions
        return result

    def save(self, *args, **kwds):
        if 'iospec' in self.__dict__:
            self.iospec_source = self.iospec.source()
        super().save(*args, **kwds)

    def run(self, source=None, iospec=None):
        """
        Runs the given source against the given iospec.

        If no source is given, use the reference implementation.

        If no iospec is given, user the default. The user may also pass a list
        of input strings.
        """

        source = source or self.source
        iospec = iospec or self.iospec
        if not source:
            raise ValueError('a source code string must be provided.')

        return run_code(source, iospec, self.language.ejudge_ref())

    def parent_hash(self):
        """
        Return the iospec hash from the question current iospec/iospec_size.
        """

        parent = self.question
        return md5hash(parent.iospec_source + str(parent.iospec_size))

    # Wagtail admin
    panels = [
        panels.FieldPanel('language'),
        panels.FieldPanel('source'),
        panels.FieldPanel('placeholder'),
    ]
Esempio n. 21
0
class Course(models.RoutablePageMixin, models.TimeStampedModel, models.Page):
    """
    One specific occurrence of a course for a given teacher in a given period.
    """

    discipline = models.ForeignKey('Discipline',
                                   blank=True,
                                   null=True,
                                   on_delete=models.DO_NOTHING)
    teacher = models.ForeignKey(models.User,
                                related_name='courses_as_teacher',
                                on_delete=models.DO_NOTHING)
    students = models.ManyToManyField(
        models.User,
        related_name='courses_as_student',
        blank=True,
    )
    staff = models.ManyToManyField(
        models.User,
        related_name='courses_as_staff',
        blank=True,
    )
    weekly_lessons = models.BooleanField(
        _('weekly lessons'),
        default=False,
        help_text=_(
            'If true, the lesson spans a whole week. Othewise, each lesson '
            'would correspond to a single day/time slot.'),
    )
    accept_subscriptions = models.BooleanField(
        _('accept subscriptions'),
        default=True,
        help_text=_('Set it to false to prevent new student subscriptions.'),
    )
    is_public = models.BooleanField(
        _('is it public?'),
        default=True,
        help_text=_(
            'If true, all students will be able to see the contents of the '
            'course. Most activities will not be available to non-subscribed '
            'students.'),
    )
    subscription_passphrase = models.CharField(
        _('subscription passphrase'),
        default=random_subscription_passphase,
        max_length=140,
        help_text=_(
            'A passphrase/word that students must enter to subscribe in the '
            'course. Leave empty if no passphrase should be necessary.'),
        blank=True,
    )
    short_description = models.CharField(max_length=140, blank=True)
    description = models.RichTextField(blank=True)
    activities_template = models.CharField(
        max_length=20,
        choices=[
            ('programming-beginner', _('A beginner programming course')),
            ('programming-intermediate',
             _('An intermediate programming course')),
            ('programming-marathon', _('A marathon-level programming course')),
        ],
        blank=True)

    @lazy
    def academic_description(self):
        return getattr(self.discipline, 'description', '')

    @lazy
    def syllabus(self):
        return getattr(self.discipline, 'syllabus', '')

    objects = CourseManager()

    @property
    def calendar_page(self):
        content_type = models.ContentType.objects.get(app_label='cs_core',
                                                      model='calendarpage')
        return apps.get_model('cs_core', 'CalendarPage').objects.get(
            depth=self.depth + 1,
            path__startswith=self.path,
            content_type_id=content_type,
        )

    @property
    def activities_page(self):
        content_type = models.ContentType.objects.get(app_label='cs_questions',
                                                      model='questionlist')
        return apps.get_model('cs_questions', 'QuestionList').objects.get(
            depth=self.depth + 1,
            path__startswith=self.path,
            content_type_id=content_type,
        )

    def save(self, *args, **kwargs):
        with transaction.atomic():
            created = self.id is None

            if not self.path:
                created = False
                root = model_reference.load('course-list')
                root.add_child(instance=self)
            else:
                super().save(*args, **kwargs)

            if created:
                self.create_calendar_page()
                self.create_activities_page()

    def create_calendar_page(self):
        """
        Creates a new calendar page if it does not exist.
        """

        model = apps.get_model('courses', 'calendar')
        calendar = model()
        self.add_child(instance=calendar)

    def create_activities_page(self):
        """
        Creates a new activities page if it does not exist.
        """

        model = apps.get_model('activities', 'activitylist')
        activities = model(
            title=_('Activities'),
            slug='activities',
            short_description=ACTIVITY_DESCRIPTION % {'name': self.title},
        )
        self.add_child(instance=activities)

    def enroll_student(self, student):
        """
        Register a new student in the course.
        """

        self.students.add(student)
        self.update_friendship_status(student)

    def is_registered(self, user):
        """
        Check if user is associated with the course in any way.
        """

        if self.teacher == user:
            return True
        elif user in self.students.all():
            return True
        elif user in self.staff.all():
            return True
        else:
            return False

    def update_friendship_status(self, student=None):
        """
        Recompute the friendship status for a single student by marking it as
        a colleague of all participants in the course.

        If no student is given, update the status of all enrolled students.
        """

        update = self._update_friendship_status
        if student is None:
            for student in self.students.all():
                update(student)
        else:
            update(student)

    def _update_friendship_status(self, student):
        for colleague in self.students.all():
            if colleague != student:
                FriendshipStatus.objects.get_or_create(
                    owner=student,
                    other=colleague,
                    status=FriendshipStatus.STATUS_COLLEAGUE)

    def get_user_role(self, user):
        """Return a string describing the most privileged role the user has
        as in the course. The possible values are:

        teacher:
            Owns the course and can do any kind of administrative tasks in
            the course.
        staff:
            Teacher assistants. May have some privileges granted by the teacher.
        student:
            Enrolled students.
        visitor:
            Have no relation to the course. If course is marked as public,
            visitors can access the course contents.
        """

        if user == self.teacher:
            return 'teacher'
        if user in self.staff.all():
            return 'staff'
        if user in self.students.all():
            return 'student'
        return 'visitor'

    def info_dict(self):
        """
        Return an ordered dictionary with relevant internationalized
        information about the course.
        """
        def yn(x):
            return _('Yes' if x else 'No')

        data = [
            ('Teacher', hyperlink(self.teacher)),
            ('Created', self.created),
            ('Accepting new subscriptions?', yn(self.accept_subscriptions)),
            ('Private?', yn(not self.is_public)),
        ]
        if self.academic_description:
            data.append(('Description', self.academic_description))
        if self.syllabus:
            data.append(('Description', self.academic_description))

        return OrderedDict([(_(k), v) for k, v in data])

    # Serving pages
    template = 'courses/detail.jinja2'

    def get_context(self, request, *args, **kwargs):
        return dict(
            super().get_context(request, *args, **kwargs),
            course=self,
        )

    def serve(self, request, *args, **kwargs):
        if self.is_registered(request.user):
            return super().serve(request, *args, **kwargs)
        return self.serve_registration(request, *args, **kwargs)

    def serve_registration(self, request, *args, **kwargs):
        context = self.get_context(request)
        if request.method == 'POST':
            form = PassPhraseForm(request.POST)
            if form.is_valid():
                self.enroll_student(request.user)
                return super().serve(request, *args, **kwargs)
        else:
            form = PassPhraseForm()

        context['form'] = form
        return render(request, 'courses/course-enroll.jinja2', context)

    # Wagtail admin
    parent_page_types = ['courses.CourseList']
    subpage_types = []
    content_panels = models.Page.content_panels + [
        panels.MultiFieldPanel([
            panels.FieldPanel('short_description'),
            panels.FieldPanel('description'),
            panels.FieldPanel('teacher')
        ],
                               heading=_('Options')),
        panels.InlinePanel(
            'time_slots',
            label=_('Time slots'),
            help_text=_('Define when the weekly classes take place.'),
        ),
    ]
    settings_panels = models.Page.settings_panels + [
        panels.MultiFieldPanel([
            panels.FieldPanel('weekly_lessons'),
        ],
                               heading=_('Options')),
        panels.MultiFieldPanel([
            panels.FieldPanel('accept_subscriptions'),
            panels.FieldPanel('is_public'),
            panels.FieldPanel('subscription_passphrase'),
        ],
                               heading=_('Subscription')),
    ]
Esempio n. 22
0
class CodeExhibit(Activity):
    """
    Students can publish code + an associated image (think about turtle art or
    Processing) and then vote in the best submissions.
    """
    class Meta:
        verbose_name = _('Code exhibit')
        verbose_name_plural = _('Code exhibits')

    description = models.RichTextField()
    language = models.ForeignKey(
        'core.ProgrammingLanguage',
        on_delete=models.PROTECT,
    )

    def get_submit_form(self, *args, **kwargs):
        class ExhibitEntryForm(forms.ModelForm):
            class Meta:
                model = ExhibitEntry
                fields = ['name', 'image', 'source']

        return ExhibitEntryForm(*args, **kwargs)

    # Page rendering and views
    template = 'code_exhibit/code_exhibit.jinja2'

    def get_context(self, request, *args, **kwargs):
        context = super().get_context(request, *args, **kwargs)
        context['entries'] = self.entries.all()
        return context

    @bricks.route(r'^get-form/$')
    def route_submit_form(self, client):
        form = self.get_submit_form()
        context = {'form': form, 'language': self.language}
        html = render_to_string('code_exhibit/submit.jinja2',
                                context,
                                request=client.request)
        client.dialog(html=html)

    @models.route(r'^submit-entry/$')
    def route_submit(self, request):
        if request.method == 'POST':
            print(request.POST)
            form = self.get_submit_form(request.POST, request.FILES)
            if form.is_valid():
                instance = form.save(commit=False)
                print('saving model', instance)
                instance.user = request.user
                instance.exhibit = self
                instance.save()
            else:
                print(form.errors)
        return redirect(self.get_absolute_url())

    # Wagtail Admin
    content_panels = Activity.content_panels + [
        panels.FieldPanel('description'),
        panels.FieldPanel('language'),
        panels.InlinePanel('entries'),
    ]
Esempio n. 23
0
class Course(models.RoutablePageMixin, models.CodeschoolPage):
    """
    One specific occurrence of a course for a given teacher in a given period.
    """
    class Meta:
        parent_init_attribute = 'discipline'

    teachers = models.ManyToManyField(
        models.User,
        related_name='courses_as_teacher',
        blank=True,
    )
    students = models.ManyToManyField(
        models.User,
        related_name='courses_as_student',
        blank=True,
    )
    staff = models.ManyToManyField(
        models.User,
        related_name='courses_as_staff_p',
        blank=True,
    )
    weekly_lessons = models.BooleanField(
        _('weekly lessons'),
        default=False,
        help_text=_(
            'If true, the lesson spans a whole week. Othewise, each lesson '
            'would correspond to a single day/time slot.'),
    )
    accept_subscriptions = models.BooleanField(
        _('accept subscriptions'),
        default=True,
        help_text=_('Set it to false to prevent new student subscriptions.'),
    )
    is_public = models.BooleanField(
        _('is it public?'),
        default=False,
        help_text=_(
            'If true, all students will be able to see the contents of the '
            'course. Most activities will not be available to non-subscribed '
            'students.'),
    )
    subscription_passphrase = models.CharField(
        _('subscription passphrase'),
        max_length=140,
        help_text=_(
            'A passphrase/word that students must enter to subscribe in the '
            'course. Leave empty if no passphrase should be necessary.'),
        blank=True,
    )
    objects = PageManager.from_queryset(CourseQueryset)()

    short_description = delegate_to('discipline', True)
    long_description = delegate_to('discipline', True)
    short_description_html = delegate_to('discipline', True)
    long_description_html = delegate_to('discipline', True)
    lessons = property(lambda x: x.calendar_page.lessons)

    @property
    def calendar_page(self):
        content_type = models.ContentType.objects.get(app_label='cs_core',
                                                      model='calendarpage')
        return apps.get_model('cs_core', 'CalendarPage').objects.get(
            depth=self.depth + 1,
            path__startswith=self.path,
            content_type_id=content_type,
        )

    @property
    def questions_page(self):
        content_type = models.ContentType.objects.get(app_label='cs_questions',
                                                      model='questionlist')
        return apps.get_model('cs_questions', 'QuestionList').objects.get(
            depth=self.depth + 1,
            path__startswith=self.path,
            content_type_id=content_type,
        )

    @property
    def gradables_page(self):
        content_type = models.ContentType.objects.get(app_label='cs_core',
                                                      model='gradablespage')
        return apps.get_model('cs_core', 'GradablesPage').objects.get(
            depth=self.depth + 1,
            path__startswith=self.path,
            content_type_id=content_type,
        )

    @property
    def discipline(self):
        return self.get_parent().specific

    @discipline.setter
    def discipline(self, value):
        self.set_parent(value)

    @property
    def questions(self):
        return self.questions_page.questions

    def add_question(self, question, copy=True):
        """
        Register a new question to the course.

        If `copy=True` (default), register a copy.
        """

        self.questions.add_question(question, copy)

    def new_question(self, cls, *args, **kwargs):
        """
        Create a new question instance by calling the cls with the given
        arguments and add it to the course.
        """

        self.questions.new_question(cls, *args, **kwargs)

    def add_lesson(self, lesson, copy=True):
        """
        Register a new lesson in the course.

        If `copy=True` (default), register a copy.
        """

        self.lessons.add_lesson(lesson, copy)

    def new_lesson(self, *args, **kwargs):
        """
        Create a new lesson instance by calling the Lesson constructor with the
        given arguments and add it to the course.
        """

        self.lessons.new_lesson(*args, **kwargs)

    def register_student(self, student):
        """
        Register a new student in the course.
        """

        self.students.add(student)
        self.update_friendship_status(student)

    def update_friendship_status(self, student=None):
        """
        Recompute the friendship status for a single student by marking it as
        a colleague of all participants in the course..

        If no student is given, update the status of all enrolled students.
        """

        update = self._update_friendship_status
        if student is None:
            for student in self.students.all():
                update(student)
        else:
            update(student)

    def _update_friendship_status(self, student):
        # Worker function for update_friendship_status
        colleague_status = FriendshipStatus.STATUS_COLLEAGUE
        for colleague in self.students.all():
            if colleague != student:
                try:
                    FriendshipStatus.objects.get(owner=student,
                                                 other=colleague)
                except FriendshipStatus.DoesNotExist:
                    FriendshipStatus.objects.create(owner=student,
                                                    other=colleague,
                                                    status=colleague_status)

    def get_absolute_url(self):
        return url_reverse('course-detail', args=(self.pk, ))

    def get_user_role(self, user):
        """Return a string describing the most privileged role the user
        as in the course. The possible values are:

        teacher:
            Owns the course and can do any kind of administrative tasks in
            the course.
        staff:
            Teacher assistants. May have some privileges granted by the teacher.
        student:
            Enrolled students.
        visitor:
            Have no relation to the course. If course is marked as public,
            visitors can access the course contents.
        """

        if user == self.teacher:
            return 'teacher'
        if user in self.staff.all():
            return 'staff'
        if user in self.students.all():
            return 'student'
        return 'visitor'

    def get_user_activities(self, user):
        """
        Return a sequence of all activities that are still open for the user.
        """

        activities = self.activities.filter(status=Activity.STATUS_OPEN)
        return activities.select_subclasses()

    def activity_duration(self):
        """
        Return the default duration (in minutes) for an activity starting from
        now.
        """

        return 120

    def next_time_slot(self):
        """Return the start and end times for the next class in the course.

        If a time slot is currently open, return it."""

        now = timezone.now()
        return now, now + timezone.timedelta(self.activity_duration())

    def next_date(self, date=None):
        """Return the date of the next available time slot."""

    def can_view(self, user):
        return user != annonymous_user()

    def can_edit(self, user):
        return user in self.teachers.all() or user == self.owner

    def get_context(self, request, *args, **kwargs):
        context = super().get_context(request, *args, **kwargs)
        context['activities'] = self.questions
        return context

    # Wagtail admin
    parent_page_types = ['cs_core.Discipline']
    subpage_types = []
    content_panels = Page.content_panels + [
        panels.InlinePanel(
            'time_slots',
            label=_('Time slots'),
            help_text=_('Define when the weekly classes take place.'),
        ),
    ]
    settings_panels = Page.settings_panels + [
        panels.MultiFieldPanel([
            panels.FieldPanel('weekly_lessons'),
            panels.FieldPanel('is_public'),
        ],
                               heading=_('Options')),
        panels.MultiFieldPanel([
            panels.FieldPanel('accept_subscriptions'),
            panels.FieldPanel('subscription_passphrase'),
        ],
                               heading=_('Subscription')),
    ]
Esempio n. 24
0
class CodingIoQuestion(Question):
    """
    CodeIo questions evaluate source code and judge them by checking if the
    inputs and corresponding outputs match an expected pattern.
    """
    class Meta:
        verbose_name = _('input/output question')
        verbose_name_plural = _('input/output questions')

    iospec_size = models.PositiveIntegerField(
        _('number of iospec template expansions'),
        default=10,
        help_text=_(
            'The desired number of test cases that will be computed after '
            'comparing the iospec template with the answer key. This is only a '
            'suggested value and will only be applied if the response template '
            'uses input commands to generate random input.'),
    )
    iospec_source = models.TextField(
        _('response template'),
        help_text=_(
            'Template used to grade I/O responses. See '
            'http://pythonhosted.org/iospec for a complete reference on the '
            'template format.'),
    )
    iospec_hash = models.CharField(
        max_length=32,
        blank=True,
        help_text=_('A hash to keep track of iospec updates.'),
    )
    timeout = models.FloatField(
        _('timeout in seconds'),
        blank=True,
        default=1.0,
        help_text=_(
            'Defines the maximum runtime the grader will spend evaluating '
            'each test case.'),
    )
    is_usable = models.BooleanField(
        _('is usable'),
        help_text=_(
            'Tells if the question has at least one usable iospec entry. A '
            'complete iospec may be given from a single iospec source or by a '
            'combination of a valid source and a reference computer program.'))
    is_consistent = models.BooleanField(
        _('is consistent'),
        help_text=_(
            'Checks if all given answer keys are consistent with each other. '
            'The question might become inconsistent by the addition of an '
            'reference program that yields different results from the '
            'equivalent program in a different language.'))

    @lazy
    def iospec(self):
        """
        The IoSpec structure corresponding to the iospec_source.
        """

        return parse_iospec(self.iospec_source)

    @property
    def is_answer_key_complete(self):
        """
        Return True if an answer key item exists for all programming languages.
        """

        refs = self.is_answer_keys.values('language__ref', flatten=True)
        all_refs = ProgrammingLanguage.objects.values('ref', flatten=True)
        return set(all_refs) == set(refs)

    @bound_property
    def language(self):
        """
        Instances can be bound to a programming language.
        """

        return getattr(self, '_language_bind', None)

    @language.setter
    def language(self, value):
        self._language_bind = programming_language(value, raises=False)

    @property
    def is_language_bound(self):
        return self.language is not None

    @property
    def default_language(self):
        """
        The main language associated with this question if a single answer key
        is defined.
        """

        return self.answer_key_items.get().language

    def _language(self, language=None, raises=True):
        # Shortcut used internally to normalize the given language
        if language is None:
            return self.language or self.default_language
        return programming_language(language, raises)

    def __init__(self, *args, **kwargs):
        # Supports automatic conversion between iospec data and iospec_source
        iospec = kwargs.pop('iospec', None)
        if iospec:
            kwargs['iospec_source'] = iospec.source()
            self.iospec = iospec
        super().__init__(*args, **kwargs)

    def clean(self):
        """
        Validate the iospec_source field.
        """

        super().clean()

        # We first should check if the iospec_source has been changed and thus
        # requires a possibly expensive validation.
        source = self.iospec_source
        iospec_hash = md5hash(source)
        if self.iospec_hash != iospec_hash:
            try:
                self.iospec = iospec.parse_string(self.iospec_source)
            except Exception:
                raise ValidationError(
                    {'iospec_source': _('invalid iospec syntax')})
            else:
                self.iospec_hash = iospec_hash
                if self.pk is None:
                    self.is_usable = self.iospec.is_simple
                    self.is_consistent = True
                else:
                    self.is_usable = self._is_usable(self.iospec)
                    self.is_consistent = self._is_consistent(self.iospec)

    def _is_usable(self, iospec):
        """
        This function is triggered during the clean() validation when a new
        iospec data is inserted into the database.
        """

        # Simple iospecs are always valid since they can be compared with
        # arbitrary programs.
        if iospec.is_simple_io:
            return True

        # For now we reject all complex iospec structures
        return False

    def _is_consistent(self, iospec):
        """
        This function is triggered during the clean() validation when a new
        iospec data is inserted into the database.
        """

        # Simple iospecs always produce consistent answer keys since we prevent
        # invalid reference programs of being inserted into the database
        # during AnswerKeyItem validation.
        if iospec.is_simple_io:
            return True

        # For now we reject all complex iospec structures
        return False

    # Serialization methods: support markio and sets it as the default
    # serialization method for CodingIoQuestion's
    @classmethod
    def load_markio(cls, source):
        """
        Creates a CodingIoQuestion object from a Markio object or source
        string and saves the resulting question in the database.

        This function can run without touching the database if the markio file
        does not define any information that should be saved in an answer key.

        Args:
            source:
                A string with the Markio source code.

        Returns:
            question:
                A question object.
        """

        import markio

        if isinstance(source, markio.Markio):
            data = source
        else:
            data = markio.parse_string(source)

        # Create question object from parsed markio data
        question = CodingIoQuestion.objects.create(
            title=data.title,
            author_name=data.author,
            timeout=data.timeout,
            short_description=data.short_description,
            long_description=data.description,
            iospec_source=data.tests,
        )

        # Add answer keys
        answer_keys = {}
        for (lang, answer_key) in data.answer_key.items():
            language = programming_language(lang)
            key = question.answer_keys.create(language=language,
                                              source=answer_key)
            answer_keys[lang] = key
        for (lang, placeholder) in data.placeholder.items():
            if placeholder is None:
                continue
            try:
                answer_keys[lang].placeholder = placeholder
                answer_keys[lang].save(update_fields=['placeholder'])
            except KeyError:
                language = ProgrammingLanguage.objects.get(lang)
                question.answer_keys.create(language=language,
                                            placeholder=placeholder)
        return question

    @classmethod
    def load(cls, format='markio', **kwargs):
        return super().load(format=format, **kwargs)

    def dump_markio(self):
        """
        Serializes question into a string of Markio source.
        """

        import markio

        tree = markio.Markio(
            title=self.name,
            author=self.author_name,
            timeout=self.timeout,
            short_description=self.short_description,
            description=self.long_description,
            tests=self.iospec_source,
        )

        for key in self.answer_keys.all():
            tree.add_answer_key(key.source, key.language.ref)
            tree.add_placeholder(key.placeholder, key.language.ref)

        return tree.source()

    def answer_key_item(self, language=None):
        """
        Return the AnswerKeyItem instance for the requested language or None if
        no object is found.
        """

        language = self._language(language)
        try:
            return self.answer_key_items.get(language=language)
        except AnswerKeyItem.DoesNotExist:
            return None

    def answer_key(self, language=None):
        """
        Return the answer key IoSpec object associated with the given language.
        """

        key = self.answer_key_item(language)
        if key is None or key.iospec_source is None:
            new_key = self.answer_key_item()
            if key == new_key:
                if self.iospec.is_simple:
                    raise ValueError('no valid iospec is defined for the '
                                     'question')
                return iospec.expand_inputs(self.iospec_size)
            key = new_key

        # We check if the answer key item is synchronized with the parent hash
        if key.iospec_hash != key.parent_hash():
            try:
                key.update(self.iospec)
            except ValidationError:
                return self.iospec
        return key.iospec

    def placeholder(self, language=None):
        """
        Return the placeholder text for the given language.
        """

        if key is None:
            return ''
        return key.placeholder

    def reference_source(self, language=None):
        """
        Return the reference source code for the given language or None, if no
        reference is found.
        """

        key = self.answer_key_item(language)
        if key is None:
            return ''
        return key.source

    def run_code(self, source=None, iospec=None, language=None):
        """
        Run the given source code string for the programming language using the
        default IoSpec.

        If no code string is given, runs the reference source code, it it
        exists.
        """

        if language is None:
            language = self.answer_key_items.get().language
        key = self.answer_key_item(language)
        return key.run(source, iospec)

    def update_iospec_source(self):
        """
        Updates the iospec_source attribute with the current iospec object.

        Any modifications made to `self.iospec` must be saved explicitly to
        persist on the database.
        """

        if 'iospec' in self.__dict__:
            self.iospec_source = self.iospec.source()

    def register_response_item(self, source, language=None, **kwargs):
        response_data = {
            'language': self._language(language).ref,
            'source': source,
        }
        kwargs.update(response_data=response_data)
        return super().register_response_item(**kwargs)

    # Serving pages and routing
    @srvice.route(r'^submit-response/$')
    def respond_route(self, client, source=None, language=None, **kwargs):
        """
        Handles student responses via AJAX and a srvice program.
        """

        if not language:
            client.dialog('<p>Please select the correct language</p>')
            return

        # Bug with <ace-editor>?
        if not source or source == '\x01\x01':
            client.dialog('<p>Internal error: please send it again!</p>')
            return

        language = programming_language(language)
        self.bind(client.request, language=language, **kwargs)
        response = self.register_response_item(source, autograde=True)
        html = render_html(response.feedback)
        client.dialog(html)

    @srvice.route(r'^placeholder/$')
    def get_placeholder_route(self, request, language):
        """
        Return the placeholder code for some language.
        """

        return self.get_placehoder(language)

    def get_context(self, request, *args, **kwargs):
        context = super().get_context(request, *args, **kwargs)
        context['form'] = ResponseForm(request.POST)
        return context

    # Wagtail admin
    content_panels = Question.content_panels[:]
    content_panels.insert(
        -1,
        panels.MultiFieldPanel([
            panels.FieldPanel('iospec_size'),
            panels.FieldPanel('iospec_source'),
        ],
                               heading=_('IoSpec definitions')))
    content_panels.insert(
        -1, panels.InlinePanel('answer_key_items', label=_('Answer keys')))
Esempio n. 25
0
class CodingIoQuestion(Question):
    """
    CodeIo questions evaluate source code and judge them by checking if the
    inputs and corresponding outputs match an expected pattern.
    """
    class Meta:
        verbose_name = _('Programming question (IO-based)')
        verbose_name_plural = _('Programming questions (IO-based)')

    EXT_TO_METHOD_CONVERSIONS = dict(
        Question.EXT_TO_METHOD_CONVERSIONS,
        md='markio',
    )

    iospec_size = models.PositiveIntegerField(
        _('number of iospec template expansions'),
        default=10,
        help_text=_(
            'The desired number of test cases that will be computed after '
            'comparing the iospec template with the answer key. This is only a '
            'suggested value and will only be applied if the response template '
            'uses input commands to generate random input.'),
    )
    iospec_source = models.TextField(
        _('response template'),
        help_text=_(
            'Template used to grade I/O responses. See '
            'http://pythonhosted.org/iospec for a complete reference on the '
            'template format.'),
    )
    iospec_hash = models.CharField(
        max_length=32,
        blank=True,
        help_text=_('A hash to keep track of iospec updates.'),
    )
    timeout = models.FloatField(
        _('timeout in seconds'),
        blank=True,
        default=1.0,
        help_text=_(
            'Defines the maximum runtime the grader will spend evaluating '
            'each test case.'),
    )
    language = models.ForeignKey(
        ProgrammingLanguage,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
        help_text=_(
            'Programming language associated with question. Leave it blank in '
            'order to accept submissions in any programming language. This '
            'option should be set only for questions that tests specific '
            'programming languages constructs or require techniques that only '
            'make sense in specific programming languages.'),
    )

    __iospec_updated = False
    __answers = ()

    @lazy
    def iospec(self):
        """
        The IoSpec structure corresponding to the iospec_source.
        """

        return parse_iospec(self.iospec_source)

    def __init__(self, *args, **kwargs):
        # Supports automatic conversion between iospec data and iospec_source
        iospec = kwargs.pop('iospec', None)
        if iospec:
            kwargs['iospec_source'] = iospec.source()
            self.iospec = iospec
        super().__init__(*args, **kwargs)

    def load_from_file_data(self, file_data):
        fake_post = super().load_from_file_data(file_data)
        fake_post['iospec_source'] = self.iospec_source
        return fake_post

    def clean(self):
        """
        Validate the iospec_source field.
        """

        super().clean()

        # We first should check if the iospec_source has been changed and would
        # require a possibly expensive validation.
        source = self.iospec_source
        iospec_hash = md5hash(source)
        if self.iospec_hash != iospec_hash:
            try:
                self.iospec = iospec = parse_iospec(self.iospec_source)
            except Exception as ex:
                raise ValidationError(
                    {'iospec_source': _('invalid iospec syntax: %s' % ex)})

            # Now we check if the new iospec requires an answer key code and
            # if it has some answer key defined
            self.__iospec_updated = True
            return
            if (not iospec.is_expanded) and not self.answers.has_program():
                raise ValidationError({
                    'iospec_source':
                    _('You iospec definition uses a command or an @input block '
                      'and thus requires an example grading code. Please define '
                      'an "Answer Key" item with source code for at least one '
                      'programming language.')
                })

    def load_from_markio(self, file_data):
        """
        Load question parameters from Markio file.
        """

        data = markio.parse(file_data)

        # Load simple data from markio
        self.title = data.title or self.title
        self.short_description = (data.short_description
                                  or self.short_description)
        self.timeout = data.timeout or self.timeout
        self.author_name = data.author or self.author_name
        self.iospec_source = data.tests or self.iospec_source

        # Load main description
        # noinspection PyUnresolvedReferences
        self.body = markdown_to_blocks(data.description)

        # Add answer keys
        answer_keys = OrderedDict()
        for (lang, answer_key) in data.answer_key.items():
            language = programming_language(lang)
            key = self.answers.create(question=self,
                                      language=language,
                                      source=answer_key)
            answer_keys[lang] = key
        for (lang, placeholder) in data.placeholder.items():
            if placeholder is None:
                continue
            try:
                answer_keys[lang].placeholder = placeholder
            except KeyError:
                language = ProgrammingLanguage.objects.get(lang)
                self.answer_keys.create(question=self,
                                        language=language,
                                        placeholder=placeholder)
        self.__answers = list(answer_keys.values())

    # Serialization methods: support markio and sets it as the default
    # serialization method for CodingIoQuestion's
    @classmethod
    def load_markio(cls, source):
        """
        Creates a CodingIoQuestion object from a Markio object or source
        string and saves the resulting question in the database.

        This function can run without touching the database if the markio file
        does not define any information that should be saved in an answer key.

        Args:
            source:
                A string with the Markio source code.

        Returns:
            question:
                A question object.
        """

        raise NotImplementedError

    def dump_markio(self):
        """
        Serializes question into a string of Markio source.
        """

        tree = markio.Markio(
            title=self.name,
            author=self.author_name,
            timeout=self.timeout,
            short_description=self.short_description,
            description=self.long_description,
            tests=self.iospec_source,
        )

        for key in self.answer_keys.all():
            tree.add_answer_key(key.source, key.language.ref)
            tree.add_placeholder(key.placeholder, key.language.ref)

        return tree.source()

    def full_clean(self, *args, **kwargs):
        if self.__answers:
            self.answers = self.__answers
        super().full_clean(*args, **kwargs)

    def placeholder(self, language=None):
        """
        Return the placeholder text for the given language.
        """

        key = self.answers[language or self.language]
        if key is None:
            return ''
        return key.placeholder

    def reference_source(self, language=None):
        """
        Return the reference source code for the given language or None, if no
        reference is found.
        """

        key = self.answers[language or self.language]
        if key is None:
            return ''
        return key.source

    def run_code(self, source, language=None, iospec=None):
        """
        Run the given source code string of the given programming language
        using the default or the given IoSpec.

        If no code string is given, runs the reference source code, if it
        exists.
        """

        key = self.answers[language or self.language]
        return key.run(source, iospec)

    def update_iospec_source(self):
        """
        Updates the iospec_source attribute with the current iospec object.

        Any modifications made to `self.iospec` must be saved explicitly to
        persist in the database.
        """

        if 'iospec' in self.__dict__:
            self.iospec_source = self.iospec.source()

    def submit(self, user, source=None, language=None, **kwargs):
        # Fetch info from response_data
        response_data = kwargs.get('response_data', {})
        if source is None and 'source' in response_data:
            source = response_data.pop('source')
        if language is None and 'language' in response_data:
            language = response_data.pop('language')

        # Assure language is valid
        language = language or self.language
        if not language:
            raise ValueError(
                'could not determine the programming language for '
                'the submission')

        # Assure response data is empty
        if response_data:
            key = next(iter(response_data))
            raise TypeError('invalid or duplicate parameter passed to '
                            'response_data: %r' % key)

        # Construct response data and pass it to super
        response_data = {
            'language': language.ref,
            'source': source,
        }

        return super().submit(user, response_data=response_data, **kwargs)

    # Serving pages and routing
    template = 'questions/coding_io/detail.jinja2'
    template_submissions = 'questions/coding_io/submissions.jinja2'

    def get_context(self, request, *args, **kwargs):
        context = dict(super().get_context(request, *args, **kwargs),
                       form=True)

        # Select default mode for the ace editor
        if self.language:
            context['default_mode'] = self.language.ace_mode()
        else:
            context['default_mode'] = get_config('CODESCHOOL_DEFAULT_ACE_MODE',
                                                 'python')

        # Enable language selection
        if self.language is None:
            context['select_language'] = True
            context['languages'] = ProgrammingLanguage.supported.all()
        else:
            context['select_language'] = False

        return context

    @srvice.route(r'^submit-response/$')
    def route_submit(self, client, source=None, language=None, **kwargs):
        """
        Handles student responses via AJAX and a srvice program.
        """

        # User must choose language
        if not language:
            if self.language is None:
                client.dialog('<p class="dialog-text">%s</p>' %
                              _('Please select the correct language'))
                return
            language = self.language
        else:
            language = programming_language(language)

        # Bug with <ace-editor>?
        if not source or source == '\x01\x01':
            client.dialog('<p class="dialog-text">%s</p>' %
                          _('Internal error: please send it again!'))
            return

        super().route_submit(
            client=client,
            language=language,
            source=source,
        )

    @srvice.route(r'^placeholder/$')
    def route_placeholder(self, request, language):
        """
        Return the placeholder code for some language.
        """

        return self.get_placehoder(language)

    # Wagtail admin
    content_panels = Question.content_panels[:]
    content_panels.insert(
        -1,
        panels.MultiFieldPanel([
            panels.FieldPanel('iospec_size'),
            panels.FieldPanel('iospec_source'),
        ],
                               heading=_('IoSpec definitions')))
    content_panels.insert(
        -1, panels.InlinePanel('answers', label=_('Answer keys')))
    settings_panels = Question.settings_panels + [
        panels.MultiFieldPanel([
            panels.FieldPanel('language'),
            panels.FieldPanel('timeout'),
        ],
                               heading=_('Options'))
    ]
Esempio n. 26
0
class Profile(UserenaBaseProfile):
    """
    Social information about users.
    """
    class Meta:
        permissions = (
            ('student', _('Can access/modify data visible to student\'s')),
            ('teacher',
             _('Can access/modify data visible only to Teacher\'s')),
        )

    GENDER_MALE, GENDER_FEMALE = 0, 1

    user = models.OneToOneField(
        User,
        verbose_name=_('user'),
        unique=True,
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
        related_name='profile',
    )
    school_id = models.CharField(
        _('school id'),
        max_length=50,
        blank=True,
        null=True,
        help_text=_('Identification number in your school issued id card.'),
    )
    is_teacher = models.BooleanField(default=False)
    nickname = models.CharField(max_length=50, blank=True, null=True)
    phone = models.CharField(max_length=20, blank=True, null=True)
    gender = models.SmallIntegerField(_('gender'),
                                      choices=[(GENDER_MALE, _('Male')),
                                               (GENDER_FEMALE, _('Female'))],
                                      blank=True,
                                      null=True)
    date_of_birth = models.DateField(_('date of birth'), blank=True, null=True)
    website = models.URLField(blank=True, null=True)
    about_me = models.RichTextField(blank=True, null=True)

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

    @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_full_name_or_username(self):
        name = self.user.get_full_name()
        if name:
            return name
        else:
            return self.user.username

    def get_absolute_url(self):
        return reverse('auth:profile-detail',
                       kwargs={'username': self.user.username})

    # Serving pages
    template = 'cs_auth/profile-detail.jinja2'

    def get_context(self, request, *args, **kwargs):
        context = super().get_context(request, *args, **kwargs)
        context['profile'] = self
        return context

    # Wagtail admin
    panels = [
        panels.MultiFieldPanel([
            panels.FieldPanel('school_id'),
        ],
                               heading='Required information'),
        panels.MultiFieldPanel([
            panels.FieldPanel('nickname'),
            panels.FieldPanel('phone'),
            panels.FieldPanel('gender'),
            panels.FieldPanel('date_of_birth'),
        ],
                               heading=_('Personal Info')),
        panels.MultiFieldPanel([
            panels.FieldPanel('website'),
        ],
                               heading=_('Web presence')),
        panels.RichTextFieldPanel('about_me'),
    ]
Esempio n. 27
0
class Conditions(models.PolymorphicModel):
    """
    Each activity can be bound to different sets of conditions that control
    aspects on how the activity should be graded and may place restrictions on
    how the students may respond to the given activity.
    """
    class Meta:
        verbose_name = _('conditions')
        verbose_name_plural = _('conditions')

    name = models.CharField(_('name'),
                            max_length=140,
                            blank=True,
                            help_text=_('A string identifier.'))
    single_submission = models.BooleanField(
        _('single submission'),
        default=False,
        help_text=_(
            'If set, students will be allowed to send only a single '
            'submission per activity.', ),
    )
    delay_feedback = models.BooleanField(
        _('delay feedback'),
        default=False,
        help_text=_(
            'If set, students will be only be able to see the feedback after '
            'the activity expires its deadline.'))
    programming_language = models.ForeignKey(
        'core.ProgrammingLanguage',
        blank=True,
        null=True,
        related_name='+',
        help_text=_(
            'Defines the required programming language for code-based student '
            'responses, when applicable. Leave it blank if you do not want to '
            'enforce any programming language.'))
    text_format = models.ForeignKey(
        'core.FileFormat',
        blank=True,
        null=True,
        related_name='+',
        help_text=_(
            'Defines the required file format for text-based responses, when '
            'applicable. Leave it blank if you do not want to enforce any '
            'text format.'))

    def __str__(self):
        return self.name or 'Untitled condition object.'

    panels = [
        panels.FieldPanel('name'),
        panels.MultiFieldPanel([
            panels.FieldPanel('single_submission'),
            panels.FieldPanel('delay_feedback'),
        ],
                               heading=_('Submissions')),
        panels.MultiFieldPanel([
            panels.FieldPanel('deadline'),
            panels.FieldPanel('hard_deadline'),
            panels.FieldPanel('delay_penalty'),
        ],
                               heading=_('Deadline')),
        panels.MultiFieldPanel([
            panels.FieldPanel('get_programming_language'),
            panels.FieldPanel('text_format'),
        ],
                               heading=_('Deadline')),
    ]
Esempio n. 28
0
class Profile(UserenaBaseProfile, models.Page):
    """
    Social information about users.
    """
    class Meta:
        permissions = (
            ('student', _('Can access/modify data visible to student\'s')),
            ('teacher',
             _('Can access/modify data visible only to Teacher\'s')),
        )

    user = models.OneToOneField(
        User,
        unique=True,
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
        verbose_name=_('user'),
        related_name='profile',
    )
    school_id = models.CharField(
        _('school id'),
        help_text=_('Identification number in your school issued id card.'),
        max_length=50,
        blank=True,
        null=True)
    nickname = models.CharField(max_length=50, blank=True, null=True)
    phone = models.CharField(max_length=20, blank=True, null=True)
    gender = models.SmallIntegerField(_('gender'),
                                      choices=[(0, _('male')),
                                               (1, _('female'))],
                                      blank=True,
                                      null=True)
    date_of_birth = models.DateField(_('date of birth'), blank=True, null=True)
    website = models.URLField(blank=True, null=True)
    about_me = models.RichTextField(blank=True, null=True)
    objects = ProfileManager()

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

    @property
    def short_description(self):
        return '%s (id: %s)' % (self.get_full_name_or_username(),
                                self.school_id)

    @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 save(self, *args, **kwargs):
        user = self.user
        if not self.title:
            self.title = self.title or __("%(name)s's profile") % {
                'name': user.get_full_name() or user.username
            }
        if not self.slug:
            self.slug = user.username.replace('.', '-')

        # Set parent page, if necessary
        if not self.path:
            root = ProfileList.objects.instance()
            root.add_child(instance=self)
        else:
            super().save(*args, **kwargs)

    def get_full_name_or_username(self):
        name = self.user.get_full_name()
        if name:
            return name
        else:
            return self.user.username

    # Serving pages
    template = 'cs_auth/profile-detail.jinja2'

    def get_context(self, request, *args, **kwargs):
        context = super().get_context(request, *args, **kwargs)
        context['profile'] = self
        return context

    # Wagtail admin
    parent_page_types = ['ProfileList']
    content_panels = models.Page.content_panels + [
        panels.MultiFieldPanel([
            panels.FieldPanel('school_id'),
        ],
                               heading='Required information'),
        panels.MultiFieldPanel([
            panels.FieldPanel('nickname'),
            panels.FieldPanel('phone'),
            panels.FieldPanel('gender'),
            panels.FieldPanel('date_of_birth'),
        ],
                               heading=_('Personal Info')),
        panels.MultiFieldPanel([
            panels.FieldPanel('website'),
        ],
                               heading=_('Web presence')),
        panels.RichTextFieldPanel('about_me'),
    ]
Esempio n. 29
0
class AnswerKey(models.Model):
    """
    Represents an answer to some question given in some specific computer
    language plus the placeholder text that should be displayed.
    """

    NULL_SOURCE_HASH = md5hash('')

    class ValidationError(Exception):
        pass

    class Meta:
        verbose_name = _('answer key')
        verbose_name_plural = _('answer keys')
        unique_together = [('question', 'language')]

    question = models.ParentalKey(
        CodingIoQuestion,
        related_name='answers'
    )
    language = models.ForeignKey(
        ProgrammingLanguage,
        related_name='+',
    )
    source = models.TextField(
        _('answer source code'),
        blank=True,
        help_text=_(
            'Source code for the correct answer in the given programming '
            'language.'
        ),
    )
    placeholder = models.TextField(
        _('placeholder source code'),
        blank=True,
        help_text=_(
            'This optional field controls which code should be placed in '
            'the source code editor when a question is opened. This is '
            'useful to put boilerplate or even a full program that the '
            'student should modify. It is possible to configure a global '
            'per-language boilerplate and leave this field blank.'
        ),
    )
    source_hash = models.CharField(
        max_length=32,
        default=NULL_SOURCE_HASH,
        help_text=_('Hash computed from the reference source'),
    )
    error_message = models.TextField(
        _('error message'),
        blank=True,
        help_text=_(
            'If an error is found on post-validation, an error message is '
            'stored in here.'
        )
    )

    def __repr__(self):
        return '<AnswerKey: %s>' % self

    def __str__(self):
        try:
            title = self.question.title
        except:
            title = '<untitled>'
        return '%s (%s)' % (title, self.language)

    def save(self, *args, **kwargs):
        self.source_hash = md5hash(self.source)
        super().save(*args, **kwargs)

    def clean(self):
        try:
            check_syntax(self.source, lang=self.language.ejudge_ref())
        except SyntaxError as ex:
            msg = _('Invalid syntax: %(msg)') % {'msg': str(ex)}
            raise ValidationError({'source': msg})
        super().clean()

        # Validation is async:
        #
        # We first run basic validations in the foreground and later attempt
        # at more detailed validations that requires us to run source code (and
        # thus possibly wait a long time).
        #
        # If this later validation step encounters errors, it saves them on
        # the model instance. The next time the model runs, we can re-raise
        # them on the interface. The user has an option to bypass these checks.
        # Changing the code or the iospec entries should expire these
        # errors.
        if self.error_message and not self.is_ignoring_validation_errors():
            raise ValidationError({'source': mark_safe(self.error_message)})

    def is_ignoring_validation_errors(self):
        """
        True to ignore errors found in post-validation.
        """

        return self.question.ignore_validation_errors

    def set_error_message(self, message):
        """
        Saves error message.
        """

        try:
            self.error_message = message.__html__()
        except AttributeError:
            self.error_message = escape(message)

    def has_changed_source(self):
        """
        Return True if source is not consistent with its hash.
        """

        return self.source_hash != md5hash(self.source)

    #
    #
    #

    def single_reference(self):
        """
        Return True if it is the only answer key in the set that defines a
        source attribute.
        """

        if not self.source:
            return False

        try:
            return self.question.answers.has_program().get() == self
        except self.DoesNotExist:
            return False

    # Wagtail admin
    panels = [
        panels.FieldPanel('language'),
        panels.FieldPanel('source'),
        panels.FieldPanel('placeholder'),
    ]
Esempio n. 30
0
class Activity(AbsoluteUrlMixin, HasScorePage, 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')

    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.'),
    )
    objects = ActivityManager()

    #: Define the default material icon used in conjunction with instances of
    #: the activity class.
    personalization_default_icon = 'material:help'

    #: The response class associated with the given activity.
    submission_class = None

    #: Model template
    template = 'lms/activities/activity.jinja2'

    #: Dictionary with extra static content that should be appended to the
    #: context for instances of the model.
    extra_context = {}

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

        if not self.author_name and self.owner:
            self.author_name = self.owner.get_full_name()

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

    # Response control
    # def get_response(self, user=None, context=None):
    #     """
    #     Get the response associated with given user and context.
    #
    #     If no user and context is given, use the bound values.
    #     """
    #
    #     user = self._normalize_user(user)
    #     context = self._normalize_response_context(context)
    #     get_response = apps.get_model('cs_core', 'Response').get_response
    #     return get_response(user=user, context=context, activity=self)

    def submit(self,
               user,
               response_data=None,
               autograde=False,
               recycle=False,
               submission_kwargs=None):
        """
        Create a new Submission object for the given question and saves it on
        the database.

        Args:
            user:
                The user who submitted the submission.
            response_data:
                A dictionary that is used to initialize the response_data
                attribute of the resulting submission object.
            autograde:
                If true, calls the autograde() method in the submission to
                give the automatic gradings.
            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 that submission.
                The resulting object have a boolean ``.recycled`` attribute
                that tells if it was recycled or not.
            submission_kwargs:
                A dictionary with extra kwargs to be passed to the class'
                submission_class constructor.
        """

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

        # Normalize inputs
        submission_kwargs = submission_kwargs or {}
        response_data = response_data or {}

        # Add response information to the given submission kwargs
        response = self.responses.response_for_user(user)

        # We compute the hash and compare it with values on the database
        # if recycle is enabled
        response_hash = submission_class.response_data_hash(response_data)
        submission = None
        recycled = False
        if recycle:
            recyclable = submission_class.objects.filter(
                response=response,
                response_hash=response_hash,
            ).order_by('created')
            for pk, value in recyclable.values_list('id', 'response_data'):
                if value == response_data:
                    submission = recyclable.get(pk=pk)
                    recycled = True
                    break

        # Proceed if no submission was created
        if submission is None:
            submission = submission_class(
                user=user,
                response=response,
                response_hash=response_hash,
                response_data=response_data,
                **submission_kwargs,
            )

        # Finalize submission item
        submission.autograde()
        submission.recycled = recycled
        return submission

    # def process_response_item(self, response, recycled=False):
    #     """
    #     Process this response item generated by other activities using a context
    #     that you own.
    #
    #     This might happen in compound activities like quizzes, in which the
    #     response to a question uses a context own by a quiz object. This
    #     function allows the container object to take any additional action
    #     after the response is created.
    #     """
    #
    # def has_response(self, user=None, context=None):
    #     """
    #     Return True if the user has responded to the activity.
    #     """
    #
    #     response = self.get_response(user, context)
    #     return response.response_items.count() >= 1
    #
    # def correct_responses(self, context=None):
    #     """
    #     Return a queryset with all correct responses for the given context.
    #     """
    #
    #     done = apps.get_model('cs_core', 'ResponseItem').STATUS_DONE
    #     items = self.response_items(context, status=done)
    #     return items.filter(given_grade=100)
    #
    # def import_responses_from_context(self, from_context, to_context,
    #                                   user=None,
    #                                   discard=False):
    #     """
    #     Import all responses associated with `from_context` to the `to_context`.
    #
    #     If discard=True, responses in the original context are discarded.
    #     """
    #
    #     if from_context == to_context:
    #         raise ValueError('contexts cannot be the same')
    #
    #     responses = self.response_items(user=user, context=from_context)
    #     for response_item in responses:
    #         old_response = response_item.response
    #         new_response = self.get_response(context=to_context,
    #                                          user=old_response.user)
    #         if not discard:
    #             response_item.pk = None
    #         response_item.response = new_response
    #         response_item.save()
    #
    # # Serving pages
    # def response_context_from_request(self, request):
    #     """
    #     Return the context from the request object.
    #     """
    #
    #     try:
    #         context_pk = request.GET['context']
    #         objects = apps.get_model('cs_core', 'ResponseContext').objects
    #         return objects.get(pk=context_pk)
    #     except KeyError:
    #         return self.default_context

    def get_context(self, request, *args, **kwargs):
        return dict(
            super().get_context(request, *args, **kwargs),
            activity=self,
            **self.extra_context,
        )

    # def get_user_response(self, user, method='first'):
    #     """
    #     Return some response given by the user or None if the user has not
    #     responded.
    #
    #     Allowed methods:
    #         unique:
    #             Expects that response is unique and return it (or None).
    #         any:
    #             Return a random user response.
    #         first:
    #             Return the first response given by the user.
    #         last:
    #             Return the last response given by the user.
    #         best:
    #             Return the response with the best final grade.
    #         worst:
    #             Return the response with the worst final grade.
    #         best-given:
    #             Return the response with the best given grade.
    #         worst-given:
    #             Return the response with the worst given grade.
    #
    #     """
    #
    #     responses = self.responses.filter(user=user)
    #     first = lambda x: x.select_subclasses().first()
    #
    #     if method == 'unique':
    #         N = self.responses.count()
    #         if N == 0:
    #             return None
    #         elif N == 1:
    #             return response.select_subclasses().first()
    #         else:
    #             raise ValueError(
    #                 'more than one response found for user %r' % user.username
    #             )
    #     elif method == 'any':
    #         return first(responses)
    #     elif method == 'first':
    #         return first(responses.order_by('created'))
    #     elif method == 'last':
    #         return first(responses.order_by('-created'))
    #     elif method in ['best', 'worst', 'best-given', 'worst-given']:
    #         raise NotImplementedError(
    #             'method = %r is not implemented yet' % method
    #         )
    #     else:
    #         raise ValueError('invalid method: %r' % method)
    #
    # def autograde_all(self, force=False, context=None):
    #     """
    #     Grade all responses that had not been graded yet.
    #
    #     This function may take a while to run, locking the server. Maybe it is
    #     a good idea to run it as a task or in a separate thread.
    #
    #     Args:
    #         force (boolean):
    #             If True, forces the response to be re-graded.
    #     """
    #
    #     # Run autograde on each responses
    #     for response in responses:
    #         response.autograde(force=force)
    #
    # def select_users(self):
    #     """
    #     Return a queryset with all users that responded to the activity.
    #     """
    #
    #     user_ids = self.responses.values_list('user', flat=True).distinct()
    #     users = models.User.objects.filter(id__in=user_ids)
    #     return users
    #
    # def get_grades(self, users=None):
    #     """
    #     Return a dictionary mapping each user to their respective grade in the
    #     activity.
    #
    #     If a list of users is given, include only the users in this list.
    #     """
    #
    #     if users is None:
    #         users = self.select_users()
    #
    #     grades = {}
    #     for user in users:
    #         grade = self.get_user_grade(user)
    #         grades[user] = grade
    #     return grades

    #
    # Plagiarism detection
    #
    def best_responses(self, context):
        """
        Return a dictionary mapping users to their best responses.
        """

        mapping = {}
        responses = self.responses.filter(context=context)
        for response in responses:
            mapping[response.user] = response.best_submission()
        return mapping

    def find_identical_responses(self, context, key=None, cmp=None, thresh=1):
        """
        Finds all responses with identical response_data in the set of best
        responses.

        Args:
            key:
                The result of key(response_data) is used for normalizing the
                different responses in the response set.
            cmp:
                A comparison function that take the outputs of key(x) for a
                pair of responses and return True if the two arguments are to
                be considered equal.
            thresh:
                Minimum threshold for the result of cmp(x, y) to be considered
                plagiarism.
        """

        key = key or (lambda x: x)
        responses = self.best_responses(context).values()
        response_data = [(x, key(x.response_data)) for x in responses
                         if x is not None]

        # We iterate this list in O^2 complexity by comparing every pair of
        # responses and checking if cmp(data1, data2) returns a value greater
        # than or equal thresh.
        bad_pairs = {}
        cmp = cmp or (lambda x, y: x == y)
        for i, (resp_a, key_a) in enumerate(response_data):
            for j in range(i + 1, len(response_data)):
                resp_b, key_b = response_data[j]
                value = cmp(key_a, key_b)
                if value >= thresh:
                    bad_pairs[resp_a, resp_b] = value
        return bad_pairs

    def group_identical_responses(self, context, key=None, keep_single=True):
        key = key or (lambda x: json.dumps(x))
        bad_values = {}
        for response in self.best_responses(context).values():
            if response is None:
                continue
            key_data = key(response.response_data)
            response_list = bad_values.setdefault(key_data, [])
            response_list.append(response)

        return bad_values

    #
    # Statistics
    #
    def response_items(self, context=None, status=None, user=None):
        """
        Return a queryset with all response items associated with the given
        activity.

        Can filter by context, status and user
        """

        items = self.response_item_class.objects
        queryset = items.filter(response__activity_id=self.id)

        # Filter context
        if context != 'any':
            context = context or self.context
            queryset = queryset.filter(response__context_id=context.id)

        # Filter user
        user = user or self.user
        if user:
            queryset = queryset.filter(response__user_id=user.id)

        # Filter by status
        if status:
            queryset = queryset.filter(status=status)

        return queryset

    def _stats(self, attr, context, by_item=False):
        if by_item:
            items = self.response_items(context, self.STATUS_DONE)
            values_list = items.values_list(attr, flat=True)
            return Statistics(attr, values_list)
        else:
            if context == 'any':
                items = self.responses.all()
            else:
                context = context or self.context
                items = self.responses.all().filter(context=context)
            return Statistics(attr, items.values_list(attr, flat=True))

    def best_final_grade(self, context=None):
        """
        Return the best final grade given for this activity.
        """

        return self._stats('final_grade', context).max()

    def best_given_grade(self, context=None):
        """
        Return the best grade given for this activity before applying any
        penalties and bonuses.
        """

        return self._stats('given_grade', context).min()

    def mean_final_grade(self, context=None, by_item=False):
        """
        Return the average value for the final grade for this activity.

        If by_item is True, compute the average over all response items instead
        of using the responses for each student.
        """

        return self._stats('final_grade', context, by_item).mean()

    def mean_given_grade(self, by_item=False):
        """
        Return the average value for the given grade for this activity.
        """

        return self._stats('given_grade', context, by_item).mean()

    # Permissions
    def can_edit(self, user):
        """
        Return True if user has permissions to edit activity.
        """

        return user == self.owner or self.course.can_edit(user)

    def can_view(self, user):
        """
        Return True if user has permission to view activity.
        """

        course = self.course
        return (self.can_edit(user) or user in course.students.all()
                or user in self.staff.all())

    # Wagtail admin
    # subpage_types = []
    # parent_page_types = []
    # content_panels = models.Page.content_panels + [
    #    panels.MultiFieldPanel([
    #        # panels.RichTextFieldPanel('short_description'),
    #    ], heading=_('Options')),
    # ]
    # promote_panels = models.Page.promote_panels + [
    #    panels.FieldPanel('icon_src')
    # ]
    settings_panels = models.Page.settings_panels + [
        panels.MultiFieldPanel([
            panels.FieldPanel('points_total'),
            panels.FieldPanel('stars_total'),
        ],
                               heading=_('Scores'))
    ]