Esempio n. 1
0
class FileDownloadActivity(Activity):
    """
    Students complete this activity by downloading all provided files from the
    server.

    This activity allows teachers to share files with the students.
    """
    class Meta:
        verbose_name = _('file download list')
        verbose_name_plural = _('file download activities')

    provide_compressed = models.BooleanField(default=True)
    zip_file = models.FileField(blank=True, null=True)
    targz_file = models.FileField(blank=True, null=True)
    items = models.ListItemSequence.as_items(FileItem)
    files = lazy(lambda x: [item.file for item in x.items])
Esempio n. 2
0
class FreeAnswerQuestion(Question):
    DATA_FILE = 'file'
    DATA_IMAGE = 'image'
    DATA_PDF = 'pdf'
    DATA_PLAIN = 'plain'
    DATA_RICHTEXT = 'richtext'
    DATA_CHOICES = (
        (DATA_FILE, _('Arbitary file')),
        (DATA_IMAGE, _('Image file')),
        (DATA_PDF, _('PDF file')),
        (DATA_RICHTEXT, _('Rich text input')),
        (DATA_RICHTEXT, _('Plain text input')),
    )
    metadata = models.TextField()
    data_type = models.CharField(choices=DATA_CHOICES, max_length=10)
    data_file = models.FileField(blank=True, null=True)
Esempio n. 3
0
class FileItem(models.ListItemModel):
    """A file item for the FileDownloadActivity."""
    class Meta:
        root_field = 'activity'

    activity = models.ForeignKey('FileDownloadActivity')
    file = models.FileField(upload_to='file-activities/')
    name = models.TextField(blank=True)
    description = models.TextField(blank=True)

    # Derived properties
    size = property(lambda x: x.file.size)
    url = property(lambda x: x.file.url)
    open = property(lambda x: x.file.open)
    close = property(lambda x: x.file.close)
    save_file = property(lambda x: x.file.save)
    delete_file = property(lambda x: x.file.delete)

    def save(self, *args, **kwargs):
        if not self.name:
            self.name = os.path.basename(self.file.name)
        super().save(*args, **kwargs)
Esempio n. 4
0
class Question(models.DecoupledAdminPage, mixins.ShortDescriptionPage,
               Activity):
    """
    Base abstract class for all question types.
    """
    class Meta:
        abstract = True
        permissions = (("download_question", "Can download question files"), )

    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.'))

    def get_navbar(self, user):
        """
        Returns the navbar for the given question.
        """

        from .components import navbar_question

        return navbar_question(self, user)

    # Serve pages
    def get_submission_kwargs(self, request, kwargs):
        return {}

    def get_context(self, request, *args, **kwargs):
        context = dict(super().get_context(request, *args, **kwargs),
                       question=self,
                       form_name='response-form',
                       navbar=self.get_navbar(request.user))
        return context

    #
    # Routes
    #
    def serve_ajax_submission(self, client, **kwargs):
        """
        Serve AJAX request for a question submission.
        """
        kwargs = self.get_submission_kwargs(client.request, kwargs)
        submission = self.submit(client.request, **kwargs)
        if submission.recycled:
            client.dialog(html='You already submitted this response!')
        elif self._meta.instant_feedback:
            feedback = submission.auto_feedback()
            data = feedback.render_message()
            client.dialog(html=data)
        else:
            client.dialog(html='Your submission is on the correction queue!')

    @srvice.route(r'^submit-response.api/$', name='submit-ajax')
    def route_ajax_submission(self, client, **kwargs):
        return self.serve_ajax_submission(client, **kwargs)
Esempio n. 5
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'),
    ]