Files
kasu/src/content/models.py

349 lines
12 KiB
Python

""" 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.db import models
from django.template.defaultfilters import slugify
from django.urls import reverse
from django.utils import timezone
from django.utils.safestring import mark_safe
from django.utils.translation import get_language, gettext 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)
def get_localized(obj, attr):
""" Return the localilzed field, or the fallback if the localized is empty.
"""
fallback = attr + '_de'
localized = attr + '_' + get_language()[:2]
return getattr(obj, localized) or getattr(obj, fallback)
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',
on_delete=models.PROTECT,
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,
on_delete=models.PROTECT,
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
# noinspection PyUnresolvedReferences
@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(get_localized(self, 'headline'))
@property
def content(self):
"""Return the localized content, fallback to german if necessary."""
return mark_safe(get_localized(self, 'content'))
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 get_localized(self, 'name')
@property
def description(self):
"""Return the localized description, fallback to german if necessary."""
return get_localized(self, 'description')
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
# noinspection PyUnresolvedReferences
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.CASCADE
)
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(get_localized(self, 'content'))
@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 get_localized(self, 'description')
@property
def menu_name(self):
"""Return the localized menu name, fallback to german if necessary."""
return get_localized(self, 'menu_name')
@property
def pdf_file(self):
"""Return the localized PDF file, fallback to german if necessary."""
return get_localized(self, 'pdf_file')
@property
def title(self):
"""Return the localized title, fallback to german if necessary."""
return get_localized(self, 'title')
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) -> str:
"""Return the path with an extension that matches the content type.
It's useful for a user to match the URL to the contenttype.
:return: string with the absolute URL of this page."""
return '/' + str(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)