""" Django Models for a lightwight Content Management.""" from ckeditor_uploader.fields import RichTextUploadingField from django.conf import settings from django.core.cache import cache from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse from django.db import models from django.template.defaultfilters import slugify from django.utils import timezone from django.utils.safestring import mark_safe from django.utils.translation import get_language, ugettext as _ from utils import STATUS_CHOICES, STATUS_WAITING, STATUS_PUBLISHED, CLEANER DJANGO_VIEW = 0 HTML_PAGE = 1 PDF_PAGE = 2 CONTENT_CHOICES = ( (DJANGO_VIEW, u'Django View'), (HTML_PAGE, u'HTML'), (PDF_PAGE, u'PDF') ) CONTENT_TYPE_EXTENSIONS = { DJANGO_VIEW: "/", HTML_PAGE: ".html", PDF_PAGE: ".pdf" } def get_upload_path(instance, filename): """Generates the desired file path and filename for an uploaded Image. With this function Django can save the uploaded images to subfolders that also have a meaning for humans. :param instance: an Django Object for which the Image has been uploaded. :param filename: The filename of the uploaded image. :return: relative path and filename for the image on the server. """ extension = filename[filename.rfind('.') + 1:] if isinstance(instance, Category): return "categories/%s.%s" % (instance.slug, extension) class ArticleManager(models.Manager): """Adds some predifined querys and joins some tables for faster querys.""" def get_queryset(self): """Join the author and category to the default query for more perfomance. :return: QuerySet""" return super(ArticleManager, self).get_queryset().select_related( 'author', 'category') def published(self): """Return articles that has been published by now. :return: QuerySet""" return self.filter( status=STATUS_PUBLISHED, date_created__lte=timezone.now() ) class Article(models.Model): """A news article, simmilar to an blog entry, it can be in german and english.""" headline_de = models.CharField(_('Headline'), max_length=255) headline_en = models.CharField('Headline', max_length=255, blank=True) content_de = RichTextUploadingField(_('Content')) content_en = RichTextUploadingField('Content', blank=True) category = models.ForeignKey('Category', verbose_name=_('Category')) image = models.ImageField(_('Image'), upload_to='news/', blank=True, null=True) slug = models.SlugField(_('Slug'), unique_for_month='date_created') author = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('Author')) status = models.SmallIntegerField(_('Status'), choices=STATUS_CHOICES, default=STATUS_PUBLISHED) date_created = models.DateTimeField(_('Created'), auto_now_add=True) date_modified = models.DateTimeField(_('Modified'), auto_now=True) objects = ArticleManager() class Meta(object): """Sort them by creation date, newes articles first.""" verbose_name = _('Article') verbose_name_plural = _('Articles') ordering = ('-date_created',) def clean(self): """Give the article an slug and scrub the html code.""" if not self.date_created: self.date_created = timezone.now() if not self.slug: self.slug = slugify(self.headline_de)[:50] self.content_de = CLEANER.clean_html(self.content_de) self.content_en = CLEANER.clean_html(self.content_en) def __str__(self): """Returns the headline of this article.""" return self.headline @property def get_image(self): """Return the article image, or the category image if unset.""" return self.image if self.image else self.category.image def get_absolute_url(self): """return the absolute URL for this article.""" return reverse('show-article', kwargs={'year': self.date_created.strftime('%Y'), 'month': self.date_created.strftime('%m'), 'slug': self.slug}) @property def headline(self): """Return the localized headline, fallback to german if necessary.""" return mark_safe( getattr(self, "headline_%s" % get_language(), self.headline_de) ) @property def content(self): """Return the localized content, fallback to german if necessary.""" return mark_safe( getattr(self, "content_%s" % get_language(), self.content_de) ) class Category(models.Model): """A news category, articles will be assicuated with it.""" name_de = models.CharField(_('Name'), max_length=80) name_en = models.CharField(_('Name'), max_length=80, blank=True) description_de = models.TextField(_('Description')) description_en = models.TextField(_('Description'), blank=True) image = models.ImageField(_('Image'), upload_to='news/categories/', blank=True, null=True) slug = models.SlugField(_('Slug'), unique=True, db_index=True) class Meta(object): """Set default the ordering.""" ordering = ('slug',) verbose_name = _('Category') verbose_name_plural = _('Categories') @property def name(self): """Return the localized name, fallback to german if necessary.""" return getattr(self, "name_%s" % get_language(), self.name_de) @property def description(self): """Return the localized description, fallback to german if necessary.""" return getattr(self, "description_%s" % get_language(), self.description_de) def get_absolute_url(self): """Return the URL of the article archive, filtered on this category.""" return reverse('article-archive', kwargs={'category': self.slug}) def __str__(self): """Return the localized name, fallback to german if necessary.""" return self.name class Page(models.Model): """A page on this homepage. It can have a "static" HTML page, the URL of a dynamic Django view, or a PDF document. Each page can be offered in German and English. If no English translation is available, the German version will be displayed.""" menu_name_de = models.CharField( max_length=255, verbose_name='MenĂ¼ Name', help_text=_('The short name for the menu-entry of this page') ) menu_name_en = models.CharField( max_length=255, blank=True, verbose_name='Menu Name', help_text=_('The short name for the menu-entry of this page') ) title_de = models.CharField( max_length=255, verbose_name='Titel', help_text=_("The page title as you'd like it to be seen by the public")) title_en = models.CharField( max_length=255, blank=True, verbose_name='Title', help_text=_("The page title as you'd like it to be seen by the public")) slug = models.SlugField( verbose_name=_('slug'), max_length=100, help_text=_( 'The name of the page as it will appear in URLs e.g ' 'http://domain.com/blog/[my-slug]/' ) ) path = models.CharField( max_length=255, db_index=True, editable=False, unique=True, verbose_name=_('Path'), ) parent = models.ForeignKey( 'self', blank=True, null=True, related_name='subpages', on_delete=models.SET_NULL ) position = models.PositiveSmallIntegerField( blank=True, null=True, verbose_name=_('Position') ) status = models.SmallIntegerField( choices=STATUS_CHOICES, default=STATUS_WAITING, verbose_name=_('status') ) description_de = models.TextField( verbose_name=_('search description'), blank=True) description_en = models.TextField( verbose_name=_('search description'), blank=True) content_type = models.IntegerField( choices=CONTENT_CHOICES, verbose_name=_('content type')) content_de = RichTextUploadingField('Inhalt', blank=True) content_en = RichTextUploadingField('Content', blank=True) enable_comments = models.BooleanField( default=True, verbose_name=_('enable comments') ) template = models.CharField( max_length=255, default="content/page.html", verbose_name=_('Template'), ) pdf_de = models.FileField(upload_to='pdf/de/', blank=True, null=True) pdf_en = models.FileField(upload_to='pdf/en/', blank=True, null=True) date_created = models.DateTimeField( auto_now_add=True, db_index=True, editable=False, verbose_name=_('first created at'), ) date_modified = models.DateTimeField( auto_now=True, editable=False, verbose_name=_('latest updated at'), ) def __str__(self): """Return the localized title, fallback to german if necessary.""" return self.title @property def content(self): """Return the localized content, fallback to german if necessary.""" return mark_safe( getattr(self, "content_%s" % get_language(), self.content_de) ) @property def css_class(self): """Returns the name of the content typ of this page as a CSS class. This allows easy styling of page links, depending on the content type. """ return CONTENT_CHOICES[self.content_type][1].lower().replace(' ', '_') @property def description(self): """Return the localized description, fallback to german if necessary.""" return getattr(self, "description_%s" % get_language(), self.description_de) @property def menu_name(self): """Return the localized menu name, fallback to german if necessary.""" return getattr(self, "menu_name_%s" % get_language(), self.menu_name_de) @property def pdf_file(self): """Return the localized PDF file, fallback to german if necessary.""" return getattr(self, "pdf_%s" % get_language(), self.pdf_de) @property def title(self): """Return the localized title, fallback to german if necessary.""" return getattr(self, "title_%s" % get_language(), self.title_de) def clean(self): """set the URL path, the right content type, and scrub the HTML code.""" if self.parent is None: self.path = self.slug else: self.path = '/'.join([self.parent.path, self.slug]) if self.content_type is None: if self.pdf_de: self.content_type = 2 elif self.content_de: self.content_type = 1 else: self.content_type = 0 if self.content_type == 1: self.content_de = CLEANER.clean_html(self.content_de) self.content_en = CLEANER.clean_html(self.content_en) elif self.content_type == 2 and not self.pdf_de.name: raise ValidationError( _(u'Please upload a PDF-File to this PDF-Page.')) def get_absolute_url(self): """Return the path with an extension that matches the content type. It's useful for an user to match the URL to the contenttype. :return: sting with the absolute URL of this page.""" return '/' + self.path + CONTENT_TYPE_EXTENSIONS[self.content_type] class Meta(object): """Set default ordering and an unique priamry key to avoid dupes.""" ordering = ['parent__id', 'position'] unique_together = (('slug', 'parent'),) verbose_name = _('Page') verbose_name_plural = _('Pages') def force_cache_update(sender, instance, **kwargs): # Ignore PyLintBear (W0613) """A Django signal to trigger the save() method on all subpages to catch possible URL changes and invalidate the cache.""" for page in instance.subpages.all(): page.clean() page.save() cache.delete('all_pages') cache.delete('top_level_pages') models.signals.post_delete.connect(force_cache_update, sender=Page) models.signals.post_save.connect(force_cache_update, sender=Page)