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])
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)
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)
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)
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'), ]