# -*- coding: utf-8 -*- from __future__ import absolute_import, print_function, unicode_literals import hashlib from aldryn_apphooks_config.fields import AppHookConfigField from aldryn_apphooks_config.managers.parler import AppHookConfigTranslatableManager from cms.models import CMSPlugin, PlaceholderField from django.conf import settings as dj_settings from django.contrib.auth import get_user_model from django.contrib.sites.shortcuts import get_current_site from django.core.cache import cache from django.core.urlresolvers import reverse from django.db import models from django.db.models.signals import post_save, pre_delete from django.dispatch import receiver from django.utils import timezone from django.utils.encoding import force_bytes, force_text, python_2_unicode_compatible from django.utils.functional import cached_property from django.utils.html import escape, strip_tags from django.utils.translation import get_language, ugettext_lazy as _ from djangocms_text_ckeditor.fields import HTMLField from filer.fields.image import FilerImageField from meta.models import ModelMeta from parler.models import TranslatableModel, TranslatedFields from parler.utils.context import switch_language from sortedm2m.fields import SortedManyToManyField from taggit_autosuggest.managers import TaggableManager from .cms_appconfig import BlogConfig from .fields import AutoSlugField, slugify from .managers import GenericDateTaggedManager from .settings import get_setting BLOG_CURRENT_POST_IDENTIFIER = get_setting('CURRENT_POST_IDENTIFIER') BLOG_CURRENT_NAMESPACE = get_setting('CURRENT_NAMESPACE') BLOG_PLUGIN_TEMPLATE_FOLDERS = get_setting('PLUGIN_TEMPLATE_FOLDERS') try: # pragma: no cover from cmsplugin_filer_image.models import ThumbnailOption # NOQA except ImportError: # pragma: no cover from filer.models import ThumbnailOption # NOQA thumbnail_model = '%s.%s' % ( ThumbnailOption._meta.app_label, ThumbnailOption.__name__ ) try: from knocker.mixins import KnockerModel except ImportError: class KnockerModel(object): """ Stub class if django-knocker is not installed """ pass class BlogMetaMixin(ModelMeta): def get_meta_attribute(self, param): """ Retrieves django-meta attributes from apphook config instance :param param: django-meta attribute passed as key """ return self._get_meta_value(param, getattr(self.app_config, param)) or '' def get_locale(self): return self.get_current_language() def get_full_url(self): """ Return the url with protocol and domain url """ return self.build_absolute_uri(self.get_absolute_url()) @python_2_unicode_compatible class BlogCategory(BlogMetaMixin, TranslatableModel): """ Blog category """ parent = models.ForeignKey( 'self', verbose_name=_('parent'), null=True, blank=True, related_name='children' ) date_created = models.DateTimeField(_('created at'), auto_now_add=True) date_modified = models.DateTimeField(_('modified at'), auto_now=True) app_config = AppHookConfigField( BlogConfig, null=True, verbose_name=_('app. config') ) translations = TranslatedFields( name=models.CharField(_('name'), max_length=767), slug=models.SlugField(_('slug'), max_length=767, blank=True, db_index=True), meta_description=models.TextField( verbose_name=_('category meta description'), blank=True, default='' ), meta={'unique_together': (('language_code', 'slug'),)} ) objects = AppHookConfigTranslatableManager() _metadata = { 'title': 'get_title', 'description': 'get_description', 'og_description': 'get_description', 'twitter_description': 'get_description', 'gplus_description': 'get_description', 'locale': 'get_locale', 'object_type': 'get_meta_attribute', 'og_type': 'get_meta_attribute', 'og_app_id': 'get_meta_attribute', 'og_profile_id': 'get_meta_attribute', 'og_publisher': 'get_meta_attribute', 'og_author_url': 'get_meta_attribute', 'og_author': 'get_meta_attribute', 'twitter_type': 'get_meta_attribute', 'twitter_site': 'get_meta_attribute', 'twitter_author': 'get_meta_attribute', 'gplus_type': 'get_meta_attribute', 'gplus_author': 'get_meta_attribute', 'url': 'get_absolute_url', } class Meta: verbose_name = _('blog category') verbose_name_plural = _('blog categories') def descendants(self): children = [] if self.children.exists(): children.extend(self.children.all()) for child in self.children.all(): children.extend(child.descendants()) return children @cached_property def linked_posts(self): return self.blog_posts.namespace(self.app_config.namespace) @cached_property def count(self): return self.linked_posts.published().count() @cached_property def count_all_sites(self): return self.linked_posts.published(current_site=False).count() def get_absolute_url(self, lang=None): if not lang or lang not in self.get_available_languages(): lang = get_language() if not lang or lang not in self.get_available_languages(): lang = self.get_current_language() if self.has_translation(lang): slug = self.safe_translation_getter('slug', language_code=lang) return reverse( '%s:posts-category' % self.app_config.namespace, kwargs={'category': slug}, current_app=self.app_config.namespace ) # in case category doesn't exist in this language, gracefully fallback # to posts-latest return reverse( '%s:posts-latest' % self.app_config.namespace, current_app=self.app_config.namespace ) def __str__(self): return self.safe_translation_getter('name', any_language=True) def save(self, *args, **kwargs): super(BlogCategory, self).save(*args, **kwargs) for lang in self.get_available_languages(): self.set_current_language(lang) if not self.slug and self.name: self.slug = slugify(force_text(self.name)) self.save_translations() def get_title(self): title = self.safe_translation_getter('name', any_language=True) return title.strip() def get_description(self): description = self.safe_translation_getter('meta_description', any_language=True) return escape(strip_tags(description)).strip() @python_2_unicode_compatible class Post(KnockerModel, BlogMetaMixin, TranslatableModel): """ Blog post """ author = models.ForeignKey(dj_settings.AUTH_USER_MODEL, verbose_name=_('author'), null=True, blank=True, related_name='djangocms_blog_post_author') date_created = models.DateTimeField(_('created'), auto_now_add=True) date_modified = models.DateTimeField(_('last modified'), auto_now=True) date_published = models.DateTimeField(_('published since'), null=True, blank=True) date_published_end = models.DateTimeField(_('published until'), null=True, blank=True) date_featured = models.DateTimeField(_('featured date'), null=True, blank=True) publish = models.BooleanField(_('publish'), default=False) categories = models.ManyToManyField('djangocms_blog.BlogCategory', verbose_name=_('category'), related_name='blog_posts', blank=True) main_image = FilerImageField(verbose_name=_('main image'), blank=True, null=True, on_delete=models.SET_NULL, related_name='djangocms_blog_post_image') main_image_thumbnail = models.ForeignKey(thumbnail_model, verbose_name=_('main image thumbnail'), related_name='djangocms_blog_post_thumbnail', on_delete=models.SET_NULL, blank=True, null=True) main_image_full = models.ForeignKey(thumbnail_model, verbose_name=_('main image full'), related_name='djangocms_blog_post_full', on_delete=models.SET_NULL, blank=True, null=True) enable_comments = models.BooleanField(verbose_name=_('enable comments on post'), default=get_setting('ENABLE_COMMENTS')) sites = models.ManyToManyField('sites.Site', verbose_name=_('Site(s)'), blank=True, help_text=_('Select sites in which to show the post. ' 'If none is set it will be ' 'visible in all the configured sites.')) app_config = AppHookConfigField( BlogConfig, null=True, verbose_name=_('app. config') ) translations = TranslatedFields( title=models.CharField(_('title'), max_length=767), slug=AutoSlugField(_('slug'), max_length=767, blank=True, db_index=True, allow_unicode=True), abstract=HTMLField(_('abstract'), blank=True, default=''), meta_description=models.TextField(verbose_name=_('post meta description'), blank=True, default=''), meta_keywords=models.TextField(verbose_name=_('post meta keywords'), blank=True, default=''), meta_title=models.CharField(verbose_name=_('post meta title'), help_text=_('used in title tag and social sharing'), max_length=2000, blank=True, default=''), post_text=HTMLField(_('text'), default='', blank=True), meta={'unique_together': (('language_code', 'slug'),)} ) content = PlaceholderField('post_content', related_name='post_content') liveblog = PlaceholderField('live_blog', related_name='live_blog') enable_liveblog = models.BooleanField(verbose_name=_('enable liveblog on post'), default=False) objects = GenericDateTaggedManager() tags = TaggableManager(blank=True, related_name='djangocms_blog_tags') related = SortedManyToManyField('self', verbose_name=_('Related Posts'), blank=True, symmetrical=False) _metadata = { 'title': 'get_title', 'description': 'get_description', 'keywords': 'get_keywords', 'og_description': 'get_description', 'twitter_description': 'get_description', 'gplus_description': 'get_description', 'locale': 'get_locale', 'image': 'get_image_full_url', 'object_type': 'get_meta_attribute', 'og_type': 'get_meta_attribute', 'og_app_id': 'get_meta_attribute', 'og_profile_id': 'get_meta_attribute', 'og_publisher': 'get_meta_attribute', 'og_author_url': 'get_meta_attribute', 'og_author': 'get_meta_attribute', 'twitter_type': 'get_meta_attribute', 'twitter_site': 'get_meta_attribute', 'twitter_author': 'get_meta_attribute', 'gplus_type': 'get_meta_attribute', 'gplus_author': 'get_meta_attribute', 'published_time': 'date_published', 'modified_time': 'date_modified', 'expiration_time': 'date_published_end', 'tag': 'get_tags', 'url': 'get_absolute_url', } class Meta: verbose_name = _('blog article') verbose_name_plural = _('blog articles') ordering = ('-date_published', '-date_created') get_latest_by = 'date_published' def __str__(self): return self.safe_translation_getter('title', any_language=True) @property def guid(self, language=None): if not language: language = self.get_current_language() base_string = '-{0}-{2}-{1}-'.format( language, self.app_config.namespace, self.safe_translation_getter('slug', language_code=language, any_language=True) ) return hashlib.sha256(force_bytes(base_string)).hexdigest() @property def date(self): if self.date_featured: return self.date_featured return self.date_published def save(self, *args, **kwargs): """ Handle some auto configuration during save """ if self.publish and self.date_published is None: self.date_published = timezone.now() if not self.slug and self.title: self.slug = slugify(self.title) super(Post, self).save(*args, **kwargs) def save_translation(self, translation, *args, **kwargs): """ Handle some auto configuration during save """ if not translation.slug and translation.title: translation.slug = slugify(translation.title) super(Post, self).save_translation(translation, *args, **kwargs) def get_absolute_url(self, lang=None): if not lang or lang not in self.get_available_languages(): lang = get_language() if not lang or lang not in self.get_available_languages(): lang = self.get_current_language() with switch_language(self, lang): category = self.categories.first() kwargs = {} if self.date_published: current_date = self.date_published else: current_date = self.date_created urlconf = get_setting('PERMALINK_URLS')[self.app_config.url_patterns] if '' in urlconf: kwargs['year'] = current_date.year if '' in urlconf: kwargs['month'] = '%02d' % current_date.month if '' in urlconf: kwargs['day'] = '%02d' % current_date.day if '' in urlconf: kwargs['slug'] = self.safe_translation_getter( 'slug', language_code=lang, any_language=True ) # NOQA if '' in urlconf: kwargs['category'] = category.safe_translation_getter( 'slug', language_code=lang, any_language=True) # NOQA return reverse('%s:post-detail' % self.app_config.namespace, kwargs=kwargs) def get_title(self): title = self.safe_translation_getter('meta_title', any_language=True) if not title: title = self.safe_translation_getter('title', any_language=True) return title.strip() def get_keywords(self): """ Returns the list of keywords (as python list) :return: list """ return self.safe_translation_getter('meta_keywords', default='').strip().split(',') def get_description(self): description = self.safe_translation_getter('meta_description', any_language=True) if not description: description = self.safe_translation_getter('abstract', any_language=True) return escape(strip_tags(description)).strip() def get_image_full_url(self): if self.main_image: return self.build_absolute_uri(self.main_image.url) return '' def get_tags(self): """ Returns the list of object tags as comma separated list """ taglist = [tag.name for tag in self.tags.all()] return ','.join(taglist) def get_author(self): """ Return the author (user) objects """ return self.author def _set_default_author(self, current_user): if not self.author_id and self.app_config.set_author: if get_setting('AUTHOR_DEFAULT') is True: user = current_user else: user = get_user_model().objects.get(username=get_setting('AUTHOR_DEFAULT')) self.author = user def thumbnail_options(self): if self.main_image_thumbnail_id: return self.main_image_thumbnail.as_dict else: return get_setting('IMAGE_THUMBNAIL_SIZE') def full_image_options(self): if self.main_image_full_id: return self.main_image_full.as_dict else: return get_setting('IMAGE_FULL_SIZE') @property def is_published(self): """ Checks wether the blog post is *really* published by checking publishing dates too """ return (self.publish and (self.date_published and self.date_published <= timezone.now()) and (self.date_published_end is None or self.date_published_end > timezone.now()) ) def should_knock(self, created=False): """ Returns whether to emit knocks according to the post state """ new = (self.app_config.send_knock_create and self.is_published and self.date_published == self.date_modified) updated = self.app_config.send_knock_update and self.is_published return new or updated def get_cache_key(self, language, prefix): return 'djangocms-blog:{2}:{0}:{1}'.format(language, self.guid, prefix) @property def liveblog_group(self): return 'liveblog-{apphook}-{lang}-{post}'.format( lang=self.get_current_language(), apphook=self.app_config.namespace, post=self.safe_translation_getter('slug', any_language=True) ) class BasePostPlugin(CMSPlugin): app_config = AppHookConfigField( BlogConfig, null=True, verbose_name=_('app. config'), blank=True ) current_site = models.BooleanField( _('current site'), default=True, help_text=_('Select items from the current site only') ) template_folder = models.CharField( max_length=200, verbose_name=_('Plugin template'), help_text=_('Select plugin template to load for this instance'), default=BLOG_PLUGIN_TEMPLATE_FOLDERS[0][0], choices=BLOG_PLUGIN_TEMPLATE_FOLDERS ) class Meta: abstract = True def optimize(self, qs): """ Apply select_related / prefetch_related to optimize the view queries :param qs: queryset to optimize :return: optimized queryset """ return qs.select_related('app_config').prefetch_related( 'translations', 'categories', 'categories__translations', 'categories__app_config' ) def post_queryset(self, request=None, published_only=True): language = get_language() posts = Post.objects if self.app_config: posts = posts.namespace(self.app_config.namespace) if self.current_site: posts = posts.on_site(get_current_site(request)) posts = posts.active_translations(language_code=language) if (published_only or not request or not getattr(request, 'toolbar', False) or not request.toolbar.edit_mode): posts = posts.published(current_site=self.current_site) return self.optimize(posts.all()) @python_2_unicode_compatible class LatestPostsPlugin(BasePostPlugin): latest_posts = models.IntegerField(_('articles'), default=get_setting('LATEST_POSTS'), help_text=_('The number of latests ' 'articles to be displayed.')) tags = TaggableManager(_('filter by tag'), blank=True, help_text=_('Show only the blog articles tagged with chosen tags.'), related_name='djangocms_blog_latest_post') categories = models.ManyToManyField('djangocms_blog.BlogCategory', blank=True, verbose_name=_('filter by category'), help_text=_('Show only the blog articles tagged ' 'with chosen categories.')) def __str__(self): return force_text(_('%s latest articles by tag') % self.latest_posts) def copy_relations(self, oldinstance): for tag in oldinstance.tags.all(): self.tags.add(tag) for category in oldinstance.categories.all(): self.categories.add(category) def get_posts(self, request, published_only=True): posts = self.post_queryset(request, published_only) if self.tags.exists(): posts = posts.filter(tags__in=list(self.tags.all())) if self.categories.exists(): posts = posts.filter(categories__in=list(self.categories.all())) return self.optimize(posts.distinct())[:self.latest_posts] @python_2_unicode_compatible class AuthorEntriesPlugin(BasePostPlugin): authors = models.ManyToManyField( dj_settings.AUTH_USER_MODEL, verbose_name=_('authors'), limit_choices_to={'djangocms_blog_post_author__publish': True} ) latest_posts = models.IntegerField( _('articles'), default=get_setting('LATEST_POSTS'), help_text=_('The number of author articles to be displayed.') ) def __str__(self): return force_text(_('%s latest articles by author') % self.latest_posts) def copy_relations(self, oldinstance): self.authors = oldinstance.authors.all() def get_posts(self, request, published_only=True): posts = self.post_queryset(request, published_only) return posts[:self.latest_posts] def get_authors(self): authors = self.authors.all() for author in authors: author.count = 0 qs = author.djangocms_blog_post_author if self.app_config: qs = qs.namespace(self.app_config.namespace) if self.current_site: qs = qs.published() else: qs = qs.published(current_site=False) count = qs.count() if count: author.count = count return authors @python_2_unicode_compatible class GenericBlogPlugin(BasePostPlugin): class Meta: abstract = False def __str__(self): return force_text(_('generic blog plugin')) @receiver(pre_delete, sender=Post) def pre_delete_post(sender, instance, **kwargs): for language in instance.get_available_languages(): key = instance.get_cache_key(language, 'feed') cache.delete(key) @receiver(post_save, sender=Post) def post_save_post(sender, instance, **kwargs): for language in instance.get_available_languages(): key = instance.get_cache_key(language, 'feed') cache.delete(key)