class UserProfile(ModelBase): """ The UserProfile *must* exist for each django.contrib.auth.models.User object. This may be relaxed once Dekiwiki isn't the definitive db for user info. timezone and language fields are syndicated to Dekiwiki """ # Website fields defined for the profile form # TODO: Someday this will probably need to allow arbitrary per-profile # entries, and these will just be suggestions. website_choices = [ ('website', dict( label=_(u'Website'), prefix='http://', regex='^https?://', )), ('twitter', dict( label=_(u'Twitter'), prefix='http://twitter.com/', regex='^https?://twitter.com/', )), ('github', dict( label=_(u'GitHub'), prefix='http://github.com/', regex='^https?://github.com/', )), ('stackoverflow', dict( label=_(u'StackOverflow'), prefix='http://stackoverflow.com/users/', regex='^https?://stackoverflow.com/users/', )), ('linkedin', dict( label=_(u'LinkedIn'), prefix='http://www.linkedin.com/in/', regex='^https?://www.linkedin.com/in/', )), ] class Meta: db_table = 'user_profiles' # This could be a ForeignKey, except wikidb might be # a different db deki_user_id = models.PositiveIntegerField(default=0, editable=False) timezone = TimeZoneField(null=True, blank=True, verbose_name=_(u'Timezone')) locale = LocaleField(null=True, blank=True, db_index=True, verbose_name=_(u'Language')) homepage = models.URLField(max_length=255, blank=True, default='', error_messages={ 'invalid': _(u'This URL has an invalid format. ' u'Valid URLs look like ' u'http://example.com/my_page.') }) title = models.CharField(_(u'Title'), max_length=255, default='', blank=True) fullname = models.CharField(_(u'Name'), max_length=255, default='', blank=True) organization = models.CharField(_(u'Organization'), max_length=255, default='', blank=True) location = models.CharField(_(u'Location'), max_length=255, default='', blank=True) bio = models.TextField(_(u'About Me'), blank=True) irc_nickname = models.CharField(_(u'IRC nickname'), max_length=255, default='', blank=True) tags = NamespacedTaggableManager(_(u'Tags'), blank=True) # should this user receive contentflagging emails? content_flagging_email = models.BooleanField(default=False) user = models.ForeignKey(DjangoUser, null=True, editable=False, blank=True) # HACK: Grab-bag field for future expansion in profiles # We can store arbitrary data in here and later migrate to relational # tables if the data ever needs to be indexed & queried. Otherwise, # this keeps things nicely denormalized. Ideally, access to this field # should be gated through accessors on the model to make that transition # easier. misc = JSONField(blank=True, null=True) @models.permalink def get_absolute_url(self): return ('devmo.views.profile_view', [self.user.username]) @property def websites(self): if 'websites' not in self.misc: self.misc['websites'] = {} return self.misc['websites'] @websites.setter def websites(self, value): self.misc['websites'] = value _deki_user = None @property def deki_user(self): if not settings.DEKIWIKI_ENDPOINT: # There is no deki_user, if the MindTouch API is disabled. return None if not self._deki_user: # Need to find the DekiUser corresponding to the ID from dekicompat.backends import DekiUserBackend self._deki_user = (DekiUserBackend().get_deki_user( self.deki_user_id)) return self._deki_user def gravatar_url(self, secure=True, size=220, rating='pg', default=DEFAULT_AVATAR): """Produce a gravatar image URL from email address.""" base_url = (secure and 'https://secure.gravatar.com' or 'http://www.gravatar.com') m = hashlib.md5(self.user.email.lower().encode('utf8')) return '%(base_url)s/avatar/%(hash)s?%(params)s' % dict( base_url=base_url, hash=m.hexdigest(), params=urllib.urlencode(dict(s=size, d=default, r=rating))) @property def gravatar(self): return self.gravatar_url() def __unicode__(self): return '%s: %s' % (self.id, self.deki_user_id) def allows_editing_by(self, user): if user == self.user: return True if user.is_staff or user.is_superuser: return True return False @property def mindtouch_language(self): if not self.locale: return '' return settings.LANGUAGE_DEKI_MAP[self.locale] @property def mindtouch_timezone(self): if not self.timezone: return '' base_seconds = self.timezone._utcoffset.days * 86400 offset_seconds = self.timezone._utcoffset.seconds offset_hours = (base_seconds + offset_seconds) / 3600 return "%03d:00" % offset_hours def save(self, *args, **kwargs): skip_mindtouch_put = kwargs.get('skip_mindtouch_put', False) if 'skip_mindtouch_put' in kwargs: del kwargs['skip_mindtouch_put'] super(UserProfile, self).save(*args, **kwargs) if skip_mindtouch_put: return if not settings.DEKIWIKI_ENDPOINT: # Skip if the MindTouch API is unavailable return from dekicompat.backends import DekiUserBackend DekiUserBackend.put_mindtouch_user(self.user) def wiki_activity(self): return Revision.objects.filter( creator=self.user).order_by('-created')[:5]
class Submission(models.Model): """Representation of a demo submission""" objects = SubmissionManager() admin_manager = models.Manager() title = models.CharField(_("what is your demo's name?"), max_length=255, blank=False, unique=True) slug = models.SlugField(_("slug"), blank=False, unique=True, max_length=50) summary = models.CharField(_("describe your demo in one line"), max_length=255, blank=False) description = models.TextField( _("describe your demo in more detail (optional)"), blank=True) featured = models.BooleanField() hidden = models.BooleanField(_("Hide this demo from others?"), default=False) censored = models.BooleanField() censored_url = models.URLField(_("Redirect URL for censorship."), verify_exists=False, blank=True, null=True) navbar_optout = models.BooleanField( _('control how your demo is launched'), choices=((True, _('Disable navigation bar, launch demo in a new window')), (False, _('Use navigation bar, display demo in <iframe>')))) comments_total = models.PositiveIntegerField(default=0) launches = ActionCounterField() likes = ActionCounterField() taggit_tags = NamespacedTaggableManager(blank=True) screenshot_1 = ReplacingImageWithThumbField( _('Screenshot #1'), max_length=255, storage=demo_uploads_fs, upload_to=mk_upload_to('screenshot_1.png'), blank=False) screenshot_2 = ReplacingImageWithThumbField( _('Screenshot #2'), max_length=255, storage=demo_uploads_fs, upload_to=mk_upload_to('screenshot_2.png'), blank=True) screenshot_3 = ReplacingImageWithThumbField( _('Screenshot #3'), max_length=255, storage=demo_uploads_fs, upload_to=mk_upload_to('screenshot_3.png'), blank=True) screenshot_4 = ReplacingImageWithThumbField( _('Screenshot #4'), max_length=255, storage=demo_uploads_fs, upload_to=mk_upload_to('screenshot_4.png'), blank=True) screenshot_5 = ReplacingImageWithThumbField( _('Screenshot #5'), max_length=255, storage=demo_uploads_fs, upload_to=mk_upload_to('screenshot_5.png'), blank=True) video_url = VideoEmbedURLField( _("have a video of your demo in action? (optional)"), blank=True, null=True) demo_package = ReplacingZipFileField( _('select a ZIP file containing your demo'), max_length=255, max_upload_size=config_lazy('DEMO_MAX_ZIP_FILESIZE', 60 * 1024 * 1024), # overridden by constance storage=demo_uploads_fs, upload_to=mk_slug_upload_to('demo_package.zip'), blank=False) source_code_url = models.URLField(_( "Is your source code also available somewhere else on the web (e.g., github)? Please share the link." ), blank=True, null=True) license_name = models.CharField( _("Select the license that applies to your source code."), max_length=64, blank=False, choices=((x['name'], x['title']) for x in DEMO_LICENSES.values())) creator = models.ForeignKey(User, blank=False, null=True) created = models.DateTimeField(_('date created'), auto_now_add=True, blank=False) modified = models.DateTimeField(_('date last modified'), auto_now=True, blank=False) def natural_key(self): return (self.slug, ) def update(self, **kw): """ Shortcut for doing an UPDATE on this object. If _signal=False is in ``kw`` the post_save signal won't be sent. """ signal = kw.pop('_signal', True) cls = self.__class__ using = kw.pop('using', 'default') for k, v in kw.items(): setattr(self, k, v) if signal: # Detect any attribute changes during pre_save and add those to the # update kwargs. attrs = dict(self.__dict__) models.signals.pre_save.send(sender=cls, instance=self) for k, v in self.__dict__.items(): if attrs[k] != v: kw[k] = v setattr(self, k, v) cls.objects.using(using).filter(pk=self.pk).update(**kw) if signal: models.signals.post_save.send(sender=cls, instance=self, created=False) def censor(self, url=None): """Censor a demo, with optional link to explanation""" self.censored = True self.censored_url = url self.save() root = '%s/%s' % (DEMO_UPLOADS_ROOT, get_root_for_submission(self)) if isdir(root): rmtree(root) def __unicode__(self): return 'Submission "%(title)s"' % dict(title=self.title) def get_absolute_url(self): return reverse('kuma.demos.views.detail', kwargs={'slug': self.slug}) def _make_unique_slug(self, **kwargs): """ Try to generate a unique 50-character slug. """ if self.slug: slug = self.slug[:50] else: slug = slugify(self.title)[:50] using = kwargs['using'] if 'using' in kwargs else 'default' existing = Submission.objects.using(using).filter(slug=slug) if (not existing) or (self.id and self.id in [s.id for s in existing]): return slug # If the first 50 characters aren't unique, we chop off the # last two and try sticking a two-digit number there. # # If for some reason we get to 100 demos which all have the # same first fifty characters in their title, this will # break. Hopefully that's unlikely enough that it won't be a # problem, but we can always add a check at the end of the # while loop or come up with some other method if we actually # run into it. base_slug = slug[:-2] i = 0 while Submission.objects.filter(slug=slug).exists() and i < 100: slug = "%s%02d" % (base_slug, i) i += 1 return slug def save(self, **kwargs): """Save the submission, updating slug and screenshot thumbnails""" self.slug = self._make_unique_slug(**kwargs) super(Submission, self).save(**kwargs) def delete(self, using=None): root = '%s/%s' % (DEMO_UPLOADS_ROOT, get_root_for_submission(self)) if isdir(root): rmtree(root) super(Submission, self).delete(using) def clean(self): if self.demo_package: Submission.validate_demo_zipfile(self.demo_package) def next(self): """Find the next submission by created time, return None if not found.""" try: obj = self.get_next_by_created(hidden=False) return obj except Submission.DoesNotExist: return None def previous(self): """Find the previous submission by created time, return None if not found.""" try: obj = self.get_previous_by_created(hidden=False) return obj except Submission.DoesNotExist: return None def screenshot_url(self, index='1'): """Fetch the screenshot URL for a given index, swallowing errors""" try: return getattr(self, 'screenshot_%s' % index).url except: return '' def thumbnail_url(self, index='1'): """Fetch the screenshot thumbnail URL for a given index, swallowing errors""" try: return getattr(self, 'screenshot_%s' % index).thumbnail_url() except: return '' def get_flags(self): """ Assemble status flags, based on featured status and a set of special tags (eg. for Dev Derby). The flags are assembled in order of display priority, so the first flag on the list (if any) is the most important""" flags = [] # Iterate through known flags based on tag naming convention. Tag flags # are listed here in order of priority. tag_flags = ('firstplace', 'secondplace', 'thirdplace', 'finalist') or_queries = [] for tag_flag in tag_flags: term = 'system:challenge:%s:' % tag_flag or_queries.append(Q(**{'name__startswith': term})) for tag in self.taggit_tags.filter(reduce(operator.or_, or_queries)): split_tag_name = tag.name.split(':') if len(split_tag_name ) > 2: # the first two items are ['system', 'challenge'] flags.append( split_tag_name[2]) # the third item is the tag name # Featured is an odd-man-out before we had tags if self.featured: flags.append('featured') return flags def is_derby_submission(self): return bool(self.taggit_tags.all_ns('challenge:')) def challenge_closed(self): challenge_tags = self.taggit_tags.all_ns('challenge:') if not challenge_tags or 'challenge:none' in map(str, challenge_tags): return False return challenge_utils.challenge_closed(challenge_tags) @classmethod def allows_listing_hidden_by(cls, user): if user.is_staff or user.is_superuser: return True return False def allows_hiding_by(self, user): if user.is_staff or user.is_superuser or user == self.creator: return True return False def allows_viewing_by(self, user): if not self.censored: if user.is_staff or user.is_superuser or user == self.creator: return True if not self.hidden: return True return False def allows_editing_by(self, user): if user.is_staff or user.is_superuser or user == self.creator: return True return False def allows_deletion_by(self, user): if user.is_staff or user.is_superuser or user == self.creator: return True return False @classmethod def get_valid_demo_zipfile_entries(cls, zf): """Filter a ZIP file's entries for only accepted entries""" # TODO: Move to zip file field? return [ x for x in zf.infolist() if not (x.filename.startswith('/') or '/..' in x.filename) and not (basename(x.filename).startswith('.')) and x.file_size > 0 ] @classmethod def validate_demo_zipfile(cls, file): """Ensure a given file is a valid ZIP file without disallowed file entries and with an HTML index.""" # TODO: Move to zip file field? try: zf = zipfile.ZipFile(file) except: raise ValidationError(_('ZIP file contains no acceptable files')) if zf.testzip(): raise ValidationError(_('ZIP file corrupted')) valid_entries = Submission.get_valid_demo_zipfile_entries(zf) if len(valid_entries) == 0: raise ValidationError(_('ZIP file contains no acceptable files')) m_mime = magic.Magic(mime=True) index_found = False for zi in valid_entries: name = zi.filename # HACK: We're accepting {index,demo}.html as the root index and # normalizing on unpack if 'index.html' == name or 'demo.html' == name: index_found = True if zi.file_size > constance.config.DEMO_MAX_FILESIZE_IN_ZIP: raise ValidationError( _('ZIP file contains a file that is too large: %(filename)s' ) % {"filename": name}) file_data = zf.read(zi) # HACK: Sometimes we get "type; charset", even if charset wasn't asked for file_mime_type = m_mime.from_buffer(file_data).split(';')[0] extensions = constance.config.DEMO_BLACKLIST_OVERRIDE_EXTENSIONS.split( ) override_file_extensions = [ '.%s' % extension for extension in extensions ] if (file_mime_type in DEMO_MIMETYPE_BLACKLIST and not name.endswith(tuple(override_file_extensions))): raise ValidationError( _('ZIP file contains an unacceptable file: %(filename)s') % {'filename': name}) if not index_found: raise ValidationError(_('HTML index not found in ZIP')) def process_demo_package(self): """Unpack the demo ZIP file into the appropriate directory, filtering out any invalid file entries and normalizing demo.html to index.html if present.""" # TODO: Move to zip file field? # Derive a directory name from the zip filename, clean up any existing # directory before unpacking. new_root_dir = self.demo_package.path.replace('.zip', '') if isdir(new_root_dir): rmtree(new_root_dir) # Load up the zip file and extract the valid entries zf = zipfile.ZipFile(self.demo_package.file) valid_entries = Submission.get_valid_demo_zipfile_entries(zf) for zi in valid_entries: if type(zi.filename) is unicode: zi_filename = zi.filename else: zi_filename = zi.filename.decode('utf-8', 'ignore') # HACK: Normalize demo.html to index.html if zi_filename == u'demo.html': zi_filename = u'index.html' # Relocate all files from detected root dir to a directory named # for the zip file in storage out_fn = u'%s/%s' % (new_root_dir, zi_filename) out_dir = dirname(out_fn) # Create parent directories where necessary. if not isdir(out_dir): makedirs(out_dir.encode('utf-8'), 0775) # Extract the file from the zip into the desired location. fout = open(out_fn.encode('utf-8'), 'wb') copyfileobj(zf.open(zi), fout)
class Food(models.Model): name = models.CharField(max_length=50) tags = NamespacedTaggableManager() def __unicode__(self): return self.name
class UserProfile(ModelBase): """ Want to track some data that isn't in dekiwiki's db? This is the proper grab bag for user profile info. Also, dekicompat middleware and backends use this class to find Django user objects. The UserProfile *must* exist for each django.contrib.auth.models.User object. This may be relaxed once Dekiwiki isn't the definitive db for user info. """ # Website fields defined for the profile form # TODO: Someday this will probably need to allow arbitrary per-profile # entries, and these will just be suggestions. website_choices = [ ('website', dict( label=_('Website'), prefix='http://', )), ('twitter', dict( label=_('Twitter'), prefix='http://twitter.com/', )), ('github', dict( label=_('GitHub'), prefix='http://github.com/', )), ('stackoverflow', dict( label=_('StackOverflow'), prefix='http://stackoverflow.com/users/', )), ('linkedin', dict( label=_('LinkedIn'), prefix='http://www.linkedin.com/in/', )), ] class Meta: db_table = 'user_profiles' # This could be a ForeignKey, except wikidb might be # a different db deki_user_id = models.PositiveIntegerField(default=0, editable=False) homepage = models.URLField(max_length=255, blank=True, default='', verify_exists=False, error_messages={ 'invalid': _('This URL has an invalid format. ' 'Valid URLs look like ' 'http://example.com/my_page.') }) title = models.CharField(_('Title'), max_length=255, default='', blank=True) fullname = models.CharField(_('Name'), max_length=255, default='', blank=True) organization = models.CharField(_('Organization'), max_length=255, default='', blank=True) location = models.CharField(_('Location'), max_length=255, default='', blank=True) bio = models.TextField(_('About Me'), blank=True) irc_nickname = models.CharField(_('IRC nickname'), max_length=255, default='', blank=True) tags = NamespacedTaggableManager(_('Tags'), blank=True) # should this user receive contentflagging emails? content_flagging_email = models.BooleanField(default=False) user = models.ForeignKey(DjangoUser, null=True, editable=False, blank=True) # HACK: Grab-bag field for future expansion in profiles # We can store arbitrary data in here and later migrate to relational # tables if the data ever needs to be indexed & queried. Otherwise, # this keeps things nicely denormalized. Ideally, access to this field # should be gated through accessors on the model to make that transition # easier. misc = JSONField(blank=True, null=True) @property def websites(self): if 'websites' not in self.misc: self.misc['websites'] = {} return self.misc['websites'] @websites.setter def websites(self, value): self.misc['websites'] = value _deki_user = None @property def deki_user(self): if not self._deki_user: # Need to find the DekiUser corresponding to the ID from dekicompat.backends import DekiUserBackend self._deki_user = (DekiUserBackend().get_deki_user( self.deki_user_id)) return self._deki_user def gravatar_url( self, secure=True, size=220, rating='pg', default='http://developer.mozilla.org/media/img/avatar.png'): """Produce a gravatar image URL from email address.""" base_url = (secure and 'https://secure.gravatar.com' or 'http://www.gravatar.com') m = hashlib.md5(self.user.email) return '%(base_url)s/avatar/%(hash)s?%(params)s' % dict( base_url=base_url, hash=m.hexdigest(), params=urllib.urlencode(dict(s=size, d=default, r=rating))) @property def gravatar(self): return self.gravatar_url() def __unicode__(self): return '%s: %s' % (self.id, self.deki_user_id) def allows_editing_by(self, user): if user == self.user: return True if user.is_staff or user.is_superuser: return True return False
class UserProfile(ModelBase): """ The UserProfile *must* exist for each django.contrib.auth.models.User object. This may be relaxed once Dekiwiki isn't the definitive db for user info. timezone and language fields are syndicated to Dekiwiki """ # Website fields defined for the profile form # TODO: Someday this will probably need to allow arbitrary per-profile # entries, and these will just be suggestions. website_choices = [('website', dict( label=_(u'Website'), prefix='http://', regex='^https?://', fa_icon='icon-link', )), ('twitter', dict( label=_(u'Twitter'), prefix='https://twitter.com/', regex='^https?://twitter.com/', fa_icon='icon-twitter', )), ('github', dict( label=_(u'GitHub'), prefix='https://github.com/', regex='^https?://github.com/', fa_icon='icon-github', )), ('stackoverflow', dict( label=_(u'Stack Overflow'), prefix='https://stackoverflow.com/users/', regex='^https?://stackoverflow.com/users/', fa_icon='icon-stackexchange', )), ('linkedin', dict( label=_(u'LinkedIn'), prefix='https://www.linkedin.com/in/', regex='^https?://www.linkedin.com/in/', fa_icon='icon-linkedin', )), ('mozillians', dict( label=_(u'Mozillians'), prefix='https://mozillians.org/u/', regex='^https?://mozillians.org/u/', fa_icon='icon-group', )), ('facebook', dict( label=_(u'Facebook'), prefix='https://www.facebook.com/', regex='^https?://www.facebook.com/', fa_icon='icon-facebook', ))] # This could be a ForeignKey, except wikidb might be # a different db deki_user_id = models.PositiveIntegerField(default=0, editable=False) timezone = TimeZoneField(null=True, blank=True, verbose_name=_(u'Timezone')) locale = LocaleField(null=True, blank=True, db_index=True, verbose_name=_(u'Language')) homepage = models.URLField(max_length=255, blank=True, default='', error_messages={ 'invalid': _(u'This URL has an invalid format. ' u'Valid URLs look like ' u'http://example.com/my_page.') }) title = models.CharField(_(u'Title'), max_length=255, default='', blank=True) fullname = models.CharField(_(u'Name'), max_length=255, default='', blank=True) organization = models.CharField(_(u'Organization'), max_length=255, default='', blank=True) location = models.CharField(_(u'Location'), max_length=255, default='', blank=True) bio = models.TextField(_(u'About Me'), blank=True) irc_nickname = models.CharField(_(u'IRC nickname'), max_length=255, default='', blank=True) tags = NamespacedTaggableManager(_(u'Tags'), blank=True) # should this user receive contentflagging emails? content_flagging_email = models.BooleanField(default=False) user = models.ForeignKey(User, null=True, editable=False, blank=True) # HACK: Grab-bag field for future expansion in profiles # We can store arbitrary data in here and later migrate to relational # tables if the data ever needs to be indexed & queried. Otherwise, # this keeps things nicely denormalized. Ideally, access to this field # should be gated through accessors on the model to make that transition # easier. misc = JSONField(blank=True, null=True) class Meta: db_table = 'user_profiles' def __unicode__(self): return '%s: %s' % (self.id, self.deki_user_id) def get_absolute_url(self): return self.user.get_absolute_url() @property def websites(self): if 'websites' not in self.misc: self.misc['websites'] = {} return self.misc['websites'] @websites.setter def websites(self, value): self.misc['websites'] = value @cached_property def beta_tester(self): return (constance.config.BETA_GROUP_NAME in self.user.groups.values_list('name', flat=True)) @property def gravatar(self): return gravatar_url(self.user) def allows_editing_by(self, user): if user == self.user: return True if user.is_staff or user.is_superuser: return True return False def wiki_activity(self): return (Revision.objects.filter( creator=self.user).order_by('-created')[:5])
class Submission(models.Model): """Representation of a demo submission""" objects = SubmissionManager() admin_manager = models.Manager() title = models.CharField(_("what is your demo's name?"), max_length=255, blank=False, unique=True) slug = models.SlugField(_("slug"), blank=False, unique=True) summary = models.CharField(_("describe your demo in one line"), max_length=255, blank=False) description = models.TextField( _("describe your demo in more detail (optional)"), blank=True) featured = models.BooleanField() hidden = models.BooleanField(_("Hide this demo from others?"), default=False) censored = models.BooleanField() censored_url = models.URLField(_("Redirect URL for censorship."), verify_exists=False, blank=True, null=True) navbar_optout = models.BooleanField( _('control how your demo is launched'), choices=((True, _('Disable navigation bar, launch demo in a new window')), (False, _('Use navigation bar, display demo in <iframe>')))) comments_total = models.PositiveIntegerField(default=0) launches = ActionCounterField() likes = ActionCounterField() taggit_tags = NamespacedTaggableManager(blank=True) screenshot_1 = ReplacingImageWithThumbField( _('Screenshot #1'), max_length=255, storage=demo_uploads_fs, upload_to=mk_upload_to('screenshot_1.png'), blank=False) screenshot_2 = ReplacingImageWithThumbField( _('Screenshot #2'), max_length=255, storage=demo_uploads_fs, upload_to=mk_upload_to('screenshot_2.png'), blank=True) screenshot_3 = ReplacingImageWithThumbField( _('Screenshot #3'), max_length=255, storage=demo_uploads_fs, upload_to=mk_upload_to('screenshot_3.png'), blank=True) screenshot_4 = ReplacingImageWithThumbField( _('Screenshot #4'), max_length=255, storage=demo_uploads_fs, upload_to=mk_upload_to('screenshot_4.png'), blank=True) screenshot_5 = ReplacingImageWithThumbField( _('Screenshot #5'), max_length=255, storage=demo_uploads_fs, upload_to=mk_upload_to('screenshot_5.png'), blank=True) video_url = VideoEmbedURLField( _("have a video of your demo in action? (optional)"), verify_exists=False, blank=True, null=True) demo_package = ReplacingZipFileField( _('select a ZIP file containing your demo'), max_length=255, max_upload_size=DEMO_MAX_ZIP_FILESIZE, storage=demo_uploads_fs, upload_to=mk_slug_upload_to('demo_package.zip'), blank=False) source_code_url = models.URLField(_( "Is your source code also available somewhere else on the web (e.g., github)? Please share the link." ), verify_exists=False, blank=True, null=True) license_name = models.CharField( _("Select the license that applies to your source code."), max_length=64, blank=False, choices=((x['name'], x['title']) for x in DEMO_LICENSES.values())) creator = models.ForeignKey(User, blank=False, null=True) created = models.DateTimeField(_('date created'), auto_now_add=True, blank=False) modified = models.DateTimeField(_('date last modified'), auto_now=True, blank=False) def __unicode__(self): return 'Submission "%(title)s"' % dict(title=self.title) def get_absolute_url(self): return reverse('demos.views.detail', kwargs={'slug': self.slug}) def save(self): """Save the submission, updating slug and screenshot thumbnails""" self.slug = slugify(self.title) super(Submission, self).save() def delete(self, using=None): root = '%s/%s' % (settings.MEDIA_ROOT, get_root_for_submission(self)) if isdir(root): rmtree(root) super(Submission, self).delete(using) def clean(self): if self.demo_package: Submission.validate_demo_zipfile(self.demo_package) def next(self): """Find the next submission by created time, return None if not found.""" try: obj = self.get_next_by_created() return obj except Submission.DoesNotExist: return None def previous(self): """Find the previous submission by created time, return None if not found.""" try: obj = self.get_previous_by_created() return obj except Submission.DoesNotExist: return None def thumbnail_url(self, index='1'): return getattr(self, 'screenshot_%s' % index).url.replace( 'screenshot', 'screenshot_thumb') def get_flags(self): """Assemble status flags, based on featured status and a set of special tags (eg. for Dev Derby). The flags are assembled in order of display priority, so the first flag on the list (if any) is the most important""" flags = [] # Iterate through known flags based on tag naming convention. Tag flags # are listed here in order of priority. tag_flags = ('firstplace', 'secondplace', 'thirdplace', 'finalist') for p in tag_flags: for tag in self.taggit_tags.all(): # TODO: Is this 'system:challenge' too hard-codey? if tag.name.startswith('system:challenge:%s:' % p): flags.append(p) # Featured is an odd-man-out before we had tags if self.featured: flags.append('featured') return flags @classmethod def allows_listing_hidden_by(cls, user): if user.is_staff or user.is_superuser: return True return False def allows_hiding_by(self, user): if user.is_staff or user.is_superuser or user == self.creator: return True return False def allows_viewing_by(self, user): if not self.censored: if user.is_staff or user.is_superuser or user == self.creator: return True if not self.hidden: return True return False def allows_editing_by(self, user): if user.is_staff or user.is_superuser or user == self.creator: return True return False def allows_deletion_by(self, user): if user.is_staff or user.is_superuser or user == self.creator: return True return False @classmethod def get_valid_demo_zipfile_entries(cls, zf): """Filter a ZIP file's entries for only accepted entries""" # TODO: Move to zip file field? return [ x for x in zf.infolist() if not (x.filename.startswith('/') or '/..' in x.filename) and not (basename(x.filename).startswith('.')) and x.file_size > 0 ] @classmethod def validate_demo_zipfile(cls, file): """Ensure a given file is a valid ZIP file without disallowed file entries and with an HTML index.""" # TODO: Move to zip file field? try: zf = zipfile.ZipFile(file) except: raise ValidationError(_('ZIP file contains no acceptable files')) if zf.testzip(): raise ValidationError(_('ZIP file corrupted')) valid_entries = Submission.get_valid_demo_zipfile_entries(zf) if len(valid_entries) == 0: raise ValidationError(_('ZIP file contains no acceptable files')) m_mime = magic.Magic(mime=True) index_found = False for zi in valid_entries: name = zi.filename # HACK: We're accepting {index,demo}.html as the root index and # normalizing on unpack if 'index.html' == name or 'demo.html' == name: index_found = True if zi.file_size > DEMO_MAX_FILESIZE_IN_ZIP: raise ValidationError( _('ZIP file contains a file that is too large: %(filename)s' ) % {"filename": name}) file_data = zf.read(zi) # HACK: Sometimes we get "type; charset", even if charset wasn't asked for file_mime_type = m_mime.from_buffer(file_data).split(';')[0] if file_mime_type in DEMO_MIMETYPE_BLACKLIST: raise ValidationError( _('ZIP file contains an unacceptable file: %(filename)s') % {"filename": name}) if not index_found: raise ValidationError(_('HTML index not found in ZIP')) def process_demo_package(self): """Unpack the demo ZIP file into the appropriate directory, filtering out any invalid file entries and normalizing demo.html to index.html if present.""" # TODO: Move to zip file field? # Derive a directory name from the zip filename, clean up any existing # directory before unpacking. new_root_dir = self.demo_package.path.replace('.zip', '') if isdir(new_root_dir): rmtree(new_root_dir) # Load up the zip file and extract the valid entries zf = zipfile.ZipFile(self.demo_package.file) valid_entries = Submission.get_valid_demo_zipfile_entries(zf) for zi in valid_entries: # HACK: Normalize demo.html to index.html if zi.filename == 'demo.html': zi.filename = 'index.html' # Relocate all files from detected root dir to a directory named # for the zip file in storage out_fn = '%s/%s' % (new_root_dir, zi.filename) out_dir = dirname(out_fn) # Create parent directories where necessary. if not isdir(out_dir): makedirs(out_dir, 0775) # Extract the file from the zip into the desired location. open(out_fn, 'w').write(zf.read(zi))