class Listing(models.Model): """ Listing of an ``Publishable`` in a ``Category``. Each and every object that have it's own detail page must have a ``Listing`` object that is valid (not expired) and places it in the object's main category. Any object can be listed in any number of categories (but only once per category). Even if the object is listed in other categories besides its main category, its detail page's url still belongs to the main one. """ box_class = staticmethod(ListingBox) publishable = CachedForeignKey(Publishable, verbose_name=_('Publishable')) category = CategoryForeignKey(verbose_name=_('Category'), db_index=True) publish_from = models.DateTimeField(_("Start of listing"), db_index=True) publish_to = models.DateTimeField(_("End of listing"), null=True, blank=True) commercial = models.BooleanField( _("Commercial"), default=False, help_text=_("Check this if the listing is of a commercial content.")) objects = ListingManager() class Meta: app_label = 'core' verbose_name = _('Listing') verbose_name_plural = _('Listings') def __unicode__(self): try: return ugettext(u'%(pub)s listed in %(cat)s') % { 'pub': self.publishable, 'cat': self.category } except: return ugettext('Broken listing') def clean(self): if not self.publishable: return if self.publish_from and self.publish_from < self.publishable.publish_from: raise ValidationError( _('A publishable cannot be listed before it\'s published.')) if self.publishable.publish_to: if not self.publish_to or self.publish_to > self.publishable.publish_to: raise ValidationError( _('A publishable cannot be listed longer than it\'s published.' )) def get_absolute_url(self, domain=False): return self.publishable.get_absolute_url(domain) def get_domain_url(self): return self.get_absolute_url(domain=True)
class Publishable(models.Model): """ Base class for all objects that can be published in Ella. """ box_class = staticmethod(PublishableBox) content_type = ContentTypeForeignKey(editable=False) target = CachedGenericForeignKey('content_type', 'id') category = CategoryForeignKey(verbose_name=_('Category')) # Titles title = models.CharField(_('Title'), max_length=255) slug = models.SlugField(_('Slug'), max_length=255, validators=[validate_slug]) # Authors and Sources authors = models.ManyToManyField(Author, verbose_name=_('Authors')) source = CachedForeignKey(Source, blank=True, null=True, verbose_name=_('Source'), on_delete=models.SET_NULL) # Main Photo photo = CachedForeignKey('photos.Photo', blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_('Photo')) # Description description = models.TextField(_('Description'), blank=True) # Publish data published = models.BooleanField(_('Published')) publish_from = models.DateTimeField( _('Publish from'), default=core_settings.PUBLISH_FROM_WHEN_EMPTY, db_index=True) publish_to = models.DateTimeField(_("End of visibility"), null=True, blank=True) static = models.BooleanField(_('static'), default=False) # Last updated last_updated = models.DateTimeField(_('Last updated'), blank=True) # generic JSON field to store app cpecific data app_data = AppDataField(default='{}', editable=False) # has the content_published signal been sent for this instance? announced = models.BooleanField(help_text='Publish signal sent', default=False, editable=False) objects = PublishableManager() class Meta: app_label = 'core' verbose_name = _('Publishable object') verbose_name_plural = _('Publishable objects') def __unicode__(self): return self.title def __eq__(self, other): return isinstance(other, Publishable) and self.pk == other.pk def get_absolute_url(self, domain=False): " Get object's URL. " category = self.category kwargs = { 'slug': self.slug, } if self.static: kwargs['id'] = self.pk if category.tree_parent_id: kwargs['category'] = category.tree_path url = reverse('static_detail', kwargs=kwargs) else: url = reverse('home_static_detail', kwargs=kwargs) else: publish_from = localize(self.publish_from) kwargs.update({ 'year': publish_from.year, 'month': publish_from.month, 'day': publish_from.day, }) if category.tree_parent_id: kwargs['category'] = category.tree_path url = reverse('object_detail', kwargs=kwargs) else: url = reverse('home_object_detail', kwargs=kwargs) if category.site_id != settings.SITE_ID or domain: return 'http://' + category.site.domain + url return url def get_domain_url(self): return self.get_absolute_url(domain=True) def clean(self): if self.static or not self.published: return # fields are missing, validating uniqueness is pointless if not self.category_id or not self.publish_from or not self.slug: return qset = self.__class__.objects.filter( category=self.category, published=True, publish_from__day=self.publish_from.day, publish_from__month=self.publish_from.month, publish_from__year=self.publish_from.year, slug=self.slug) if self.pk: qset = qset.exclude(pk=self.pk) if qset: raise ValidationError( _('Another %s already published at this URL.') % self._meta.verbose_name) def save(self, **kwargs): # update the content_type if it isn't already set if not self.content_type_id: self.content_type = ContentType.objects.get_for_model(self) send_signal = None old_self = None if self.pk: try: old_self = self.__class__.objects.get(pk=self.pk) except Publishable.DoesNotExist: pass if old_self: old_path = old_self.get_absolute_url() new_path = self.get_absolute_url() # detect change in URL and not a static one if old_path != new_path and new_path and not old_self.static: # and create a redirect redirect = Redirect.objects.get_or_create( old_path=old_path, site=self.category.site)[0] redirect.new_path = new_path redirect.save(force_update=True) # also update all potentially already existing redirects Redirect.objects.filter(new_path=old_path).exclude( pk=redirect.pk).update(new_path=new_path) # detect change in publication status if old_self.is_published() != self.is_published(): if self.is_published(): send_signal = content_published self.announced = True else: send_signal = content_unpublished self.announced = False # @note: We also need to check for `published` flag even if both # old and new self `is_published()` method returns false. # This method can report false since we might be in time *before* # publication should take place but we still need to fire signal # that content has been unpublished. if old_self.published != self.published and self.published is False: send_signal = content_unpublished self.announced = False # changed publish_from and last_updated was default, change it too if old_self.last_updated == old_self.publish_from and self.last_updated == old_self.last_updated: self.last_updated = self.publish_from #TODO: shift Listing in case publish_(to|from) changes # published, send the proper signal elif self.is_published(): send_signal = content_published self.announced = True if not self.last_updated: self.last_updated = self.publish_from super(Publishable, self).save(**kwargs) if send_signal: send_signal.send(sender=self.__class__, publishable=self) def delete(self): url = self.get_absolute_url() Redirect.objects.filter(new_path=url).delete() if self.announced: content_unpublished.send(sender=self.__class__, publishable=self) return super(Publishable, self).delete() def is_published(self): "Return True if the Publishable is currently active." cur_time = now() return self.published and cur_time > self.publish_from and \ (self.publish_to is None or cur_time < self.publish_to)
class Category(models.Model): """ ``Category`` is the **basic building block of Ella-based sites**. All the published content is divided into categories - every ``Publishable`` object has a ``ForeignKey`` to it's primary ``Category``. Primary category is then used to build up object's URL when using `Category.get_absolute_url` method. Besides that, objects can be published in other categories (aka "secondary" categories) via ``Listing``. Every site has exactly one root category (without a parent) that serve's as the sites's homepage. """ template_choices = tuple((x, _(y)) for x, y in core_settings.CATEGORY_TEMPLATES) title = models.CharField(_("Title"), max_length=200) description = models.TextField(_("Description"), blank=True, help_text=_( 'Description which can be used in link titles, syndication etc.')) content = models.TextField(_('Content'), default='', blank=True, help_text=_( 'Optional content to use when rendering this category.')) template = models.CharField(_('Template'), max_length=100, help_text=_( 'Template to use to render detail page of this category.'), choices=template_choices, default=template_choices[0][0]) slug = models.SlugField(_('Slug'), max_length=255, validators=[category_slug_validator]) tree_parent = CategoryForeignKey(null=True, blank=True, verbose_name=_("Parent category")) tree_path = models.CharField(verbose_name=_("Path from root category"), max_length=255, editable=False) site = SiteForeignKey() # generic JSON field to store app cpecific data app_data = AppDataField(_('Custom meta data'), help_text=_('If you need to define custom data for ' 'category objects, you can use this field to do so.')) objects = CategoryManager() class Meta: app_label = 'core' unique_together = (('site', 'tree_path'),) verbose_name = _('Category') verbose_name_plural = _('Categories') def __unicode__(self): return '%s/%s' % (self.site.name, self.tree_path) def save(self, **kwargs): "Override save() to construct tree_path based on the category's parent." old_tree_path = self.tree_path if self.tree_parent: if self.tree_parent.tree_path: self.tree_path = '%s/%s' % (self.tree_parent.tree_path, self.slug) else: self.tree_path = self.slug else: self.tree_path = '' Category.objects.clear_cache() super(Category, self).save(**kwargs) if old_tree_path != self.tree_path: # the tree_path has changed, update children children = Category.objects.filter(tree_parent=self) for child in children: child.save(force_update=True) def get_root_category(self): if '/' not in self.tree_path: return self path = self.tree_path.split('/')[0] return Category.objects.get_by_tree_path(path) def get_children(self, recursive=False): return Category.objects.get_children(self, recursive) @property def path(self): """ Returns tree path of the category. Tree path is string that describes the whole path from the category root to the position of this category. @see: Category.tree_path """ if self.tree_parent_id: return self.tree_path else: return self.slug def get_absolute_url(self): """ Returns absolute URL for the category. """ if not self.tree_parent_id: url = reverse('root_homepage') else: url = reverse('category_detail', kwargs={'category' : self.tree_path}) if self.site_id != settings.SITE_ID: # prepend the domain if it doesn't match current Site return 'http://' + self.site.domain + url return url def draw_title(self): """ Returns title indented by * * elements that can be used to show users a category tree. Examples: **Category with no direct parent (the category root)** TITLE **Category with one parent** &nsbp;TITLE **Category on third level of the tree** TITLE """ return mark_safe((' ' * self.tree_path.count('/')) + self.title) draw_title.allow_tags = True
class Position(models.Model): """ Represents a position -- a placeholder -- on a page belonging to a certain category. """ box_class = staticmethod(PositionBox) name = models.CharField(_('Name'), max_length=200) category = CategoryForeignKey(verbose_name=_('Category')) target_ct = ContentTypeForeignKey(verbose_name=_('Target content type'), null=True, blank=True) target_id = models.PositiveIntegerField(_('Target id'), null=True, blank=True) target = CachedGenericForeignKey('target_ct', 'target_id') text = models.TextField(_('Definition'), blank=True) box_type = models.CharField(_('Box type'), max_length=200, blank=True) active_from = models.DateTimeField(_('Position active from'), null=True, blank=True) active_till = models.DateTimeField(_('Position active till'), null=True, blank=True) disabled = models.BooleanField(_('Disabled'), default=False) objects = PositionManager() class Meta: verbose_name = _('Position') verbose_name_plural = _('Positions') def clean(self): if not self.category or not self.name: return if self.target_ct: try: get_cached_object(self.target_ct, pk=self.target_id) except self.target_ct.model_class().DoesNotExist: raise ValidationError( _('This position doesn\'t point to a valid object.')) qset = Position.objects.filter(category=self.category, name=self.name) if self.pk: qset = qset.exclude(pk=self.pk) if self.active_from: qset = qset.exclude(active_till__lte=self.active_from) if self.active_till: qset = qset.exclude(active_from__gt=self.active_till) if qset.count(): raise ValidationError( _('There already is a postion for %(cat)s named %(name)s fo this time.' ) % { 'cat': self.category, 'name': self.name }) def __unicode__(self): return u'%s:%s' % (self.category, self.name) def render(self, context, nodelist, box_type): " Render the position. " if not self.target: if self.target_ct: # broken Generic FK: log.warning('Broken target for position with pk %r', self.pk) return '' try: return Template(self.text, name="position-%s" % self.name).render(context) except TemplateSyntaxError: log.error('Broken definition for position with pk %r', self.pk) return '' if self.box_type: box_type = self.box_type if self.text: nodelist = Template('%s\n%s' % (nodelist.render({}), self.text), name="position-%s" % self.name).nodelist b = self.box_class(self, box_type, nodelist) return b.render(context)