import hashlib
from cms.models import CMSPlugin, PlaceholderRelationField
from cms.utils.placeholder import get_placeholder_from_slot
from django.apps import apps
from django.conf import settings as dj_settings
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.contrib.sites.models import Site
from django.contrib.sites.shortcuts import get_current_site
from django.core.cache import cache
from django.db import models
from django.db.models import F, Q
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from django.urls import NoReverseMatch, reverse
from django.utils import translation
from django.utils.encoding import force_bytes, force_str
from django.utils.functional import cached_property
from django.utils.html import strip_tags
from django.utils.timezone import now
from django.utils.translation import get_language, gettext, gettext_lazy as _
from easy_thumbnails.files import get_thumbnailer
from filer.fields.image import FilerImageField
from filer.models import ThumbnailOption
from menus.menu_pool import menu_pool
from meta.models import ModelMeta
from parler.models import TranslatableModel, TranslatedFields
from sortedm2m.fields import SortedManyToManyField
from taggit_autosuggest.managers import TaggableManager
from .cms_appconfig import StoriesConfig
from .fields import slugify
from .managers import AdminManager, GenericDateTaggedManager, SiteManager
from .settings import STORIES_PLUGIN_TEMPLATE_FOLDERS as DEFAULT_TEMPLATE_FOLDERS, get_setting
STORIES_CURRENT_POST_IDENTIFIER = get_setting("CURRENT_POST_IDENTIFIER")
STORIES_CURRENT_NAMESPACE = get_setting("CURRENT_NAMESPACE")
STORIES_PLUGIN_TEMPLATE_FOLDERS = get_setting("PLUGIN_TEMPLATE_FOLDERS")
STORIES_ALLOW_UNICODE_SLUGS = get_setting("ALLOW_UNICODE_SLUGS")
thumbnail_model = f"{ThumbnailOption._meta.app_label}.{ThumbnailOption.__name__}"
# HTMLField is a custom field that allows to use a rich text editor
# Probe for djangocms_text first, then for djangocms_text_ckeditor
# and finally fallback to a simple textarea
if apps.is_installed("djangocms_text"):
from djangocms_text.fields import HTMLField
elif apps.is_installed("djangocms_text_ckeditor"): # pragma: no cover
from djangocms_text_ckeditor.fields import HTMLField
else: # pragma: no cover
from django import forms
class HTMLField(models.TextField):
def __init__(self, *args, **kwargs):
kwargs.setdefault("widget", forms.Textarea)
super().__init__(*args, **kwargs)
def _get_language(instance, language):
available_languages = instance.get_available_languages()
if language and language in available_languages:
return language
language = translation.get_language()
if language and language in available_languages:
return language
language = instance.get_current_language()
if language and language in available_languages:
return language
if get_setting("USE_FALLBACK_LANGUAGE_IN_URL"):
for fallback_language in instance.get_fallback_languages():
if fallback_language in available_languages:
return fallback_language
return language
class PostMetaMixin:
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_full_url(self):
"""
Return the url with protocol and domain url
"""
return self.build_absolute_uri(self.get_absolute_url())
[docs]
class PostCategory(PostMetaMixin, ModelMeta, TranslatableModel):
"""
Post category allows to structure content in a hierarchy of categories.
"""
parent = models.ForeignKey(
"self", verbose_name=_("parent"), null=True, blank=True, related_name="children", on_delete=models.CASCADE
)
date_created = models.DateTimeField(_("created at"), auto_now_add=True)
date_modified = models.DateTimeField(_("modified at"), auto_now=True)
app_config = models.ForeignKey(
StoriesConfig,
on_delete=models.CASCADE,
null=True,
verbose_name=_("app. config"),
help_text=_("When selecting a value, the form is reloaded to get the updated default"),
)
priority = models.IntegerField(_("priority"), blank=True, null=True)
main_image = FilerImageField(
verbose_name=_("main image"),
blank=True,
null=True,
on_delete=models.SET_NULL,
related_name="djangocms_category_image",
)
main_image_thumbnail = models.ForeignKey(
thumbnail_model,
verbose_name=_("main image thumbnail"),
related_name="djangocms_category_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_category_full",
on_delete=models.SET_NULL,
blank=True,
null=True,
)
translations = TranslatedFields(
name=models.CharField(_("name"), max_length=752),
slug=models.SlugField(
_("slug"),
max_length=752,
blank=True,
db_index=True,
allow_unicode=STORIES_ALLOW_UNICODE_SLUGS,
),
meta_description=models.TextField(verbose_name=_("category meta description"), blank=True, default=""),
meta={"unique_together": (("language_code", "slug"),)},
abstract=HTMLField(_("abstract"), blank=True, default="", configuration="STORIES_ABSTRACT_EDITOR_CONF"),
)
_metadata = {
"title": "get_title",
"og_title": "get_title",
"twitter_title": "get_title",
"schemaorg_title": "get_title",
"description": "get_description",
"og_description": "get_description",
"twitter_description": "get_description",
"schemaorg_description": "get_description",
"schemaorg_type": "get_meta_attribute",
"locale": "get_current_language",
"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",
"url": "get_absolute_url",
}
class Meta:
verbose_name = _("post category")
verbose_name_plural = _("post categories")
ordering = (F("priority").asc(nulls_last=True),)
[docs]
def descendants(self): # pragma: no cover
import warnings
warnings.warn(
"The `descendants` method is deprecated and will be removed in future versions. "
"Use `get_descendants` instead.",
DeprecationWarning,
stacklevel=2,
)
return self.get_descendants()
[docs]
def get_descendants(self):
children = []
if self.children.exists():
children.extend(self.children.all())
for child in self.children.all():
children.extend(child.get_descendants())
return children
@cached_property
def linked_posts(self):
"""returns all linked posts in the same appconfig namespace"""
return self.posts.filter(app_config=self.app_config)
@cached_property
def count(self):
return self.linked_posts.filter(Q(sites__isnull=True) | Q(sites=Site.objects.get_current())).count()
@cached_property
def count_all_sites(self):
return self.linked_posts.count()
[docs]
def get_absolute_url(self, lang=None):
"""
Returns the absolute URL for the category overview in the specified language.
If the category has a translation in the given language, returns the URL for the category's detail page using its slug.
If the category does not exist in the specified language, falls back to the URL for the latest posts.
Args:
lang (str, optional): The language code to use for the URL. If not provided, determines the language automatically.
Returns:
str: The absolute URL for the category or the latest posts, depending on translation availability.
"""
lang = _get_language(self, lang)
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):
default = gettext("PostCategory (no translation)")
return self.safe_translation_getter("name", any_language=True, default=default)
[docs]
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
menu_pool.clear(all=True)
for lang in self.get_available_languages():
self.set_current_language(lang)
if not self.slug and self.name:
self.slug = slugify(force_str(self.name))
self.save_translations()
[docs]
def delete(self, *args, **kwargs):
menu_pool.clear(all=True)
return super().delete(*args, **kwargs)
[docs]
def get_title(self):
title = self.safe_translation_getter("name", any_language=True)
return title.strip()
[docs]
def get_description(self):
description = self.safe_translation_getter("meta_description", any_language=True)
return strip_tags(description).strip()
[docs]
class Post(models.Model):
"""
Represents a blog post or story entry with multilingual content, images, categories, tags, and publication metadata.
It is the "grouper model" for :class:`PostContent`.
This model supports translation, site-specific publishing, image handling, tagging, and flexible URL generation for posts.
"""
author = models.ForeignKey(
dj_settings.AUTH_USER_MODEL,
verbose_name=_("author"),
null=True,
blank=True,
related_name="djangocms_stories_post_author",
on_delete=models.PROTECT,
)
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)
include_in_rss = models.BooleanField(_("include in RSS feed"), default=True)
categories = models.ManyToManyField(
"djangocms_stories.PostCategory", verbose_name=_("category"), related_name="posts", blank=True
)
main_image = FilerImageField(
verbose_name=_("main image"),
blank=True,
null=True,
on_delete=models.SET_NULL,
related_name="djangocms_stories_post_image",
)
main_image_thumbnail = models.ForeignKey(
thumbnail_model,
verbose_name=_("main image thumbnail"),
related_name="djangocms_stories_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_stories_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 = models.ForeignKey(
StoriesConfig,
on_delete=models.CASCADE,
null=True,
verbose_name=_("app. config"),
help_text=_("When selecting a value, the form is reloaded to get the updated default"),
)
tags = TaggableManager(
blank=True,
related_name="djangocms_stories_tags",
help_text=_("Type a tag and hit tab, or start typing and select from autocomplete list."),
)
related = SortedManyToManyField("self", verbose_name=_("Related Posts"), blank=True, symmetrical=False)
objects = GenericDateTaggedManager()
class Meta:
verbose_name = _("post")
verbose_name_plural = _("posts")
ordering = ("-date_published", "-date_created")
get_latest_by = "date_published"
[docs]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._content_cache = {}
self._language_cache = None
def __str__(self):
default = gettext("Post (no translation)")
return self.safe_translation_getter("title", any_language=True, default=default, show_draft_content=True)
[docs]
@admin.display(boolean=True)
def featured(self):
if not self.date_featured:
return False
return bool(self.date_featured <= now())
[docs]
def get_content(self, language=None, show_draft_content=False):
if not language:
language = translation.get_language()
key = f"{language}_{'latest' if show_draft_content else 'public'}"
try:
return self._content_cache[key]
except KeyError:
if show_draft_content:
qs = self.postcontent_set(manager="admin_manager").current_content()
else:
qs = self.postcontent_set
qs = qs.prefetch_related(
"placeholders",
"post__categories",
).filter(language=language)
self._content_cache[key] = qs.first()
return self._content_cache[key]
[docs]
def safe_translation_getter(
self, field, default=None, language_code=None, any_language=False, show_draft_content=False
):
"""
Fetch a content property, and return a default value
when both the translation and fallback language are missing.
When ``any_language=True`` is used, the function also looks
into other languages to find a suitable value. This feature can be useful
for "title" attributes for example, to make sure there is at least something being displayed.
Also consider using ``field = TranslatedField(any_language=True)`` in the model itself,
to make this behavior the default for the given field.
"""
content_obj = self.get_content(language_code, show_draft_content=show_draft_content)
if content_obj is None and any_language and self.get_available_languages():
content_obj = self.get_content(self.get_available_languages()[0], show_draft_content=show_draft_content)
return getattr(content_obj, field, default)
@property
def guid(self):
language = get_language()
slug = self.safe_translation_getter("slug", language_code=language, any_language=True)
base_string = f"-{language}-{slug}-{self.app_config.namespace}-"
return hashlib.sha256(force_bytes(base_string)).hexdigest()
@property
def date(self):
if self.date_featured:
return self.date_featured
return self.date_published or self.date_created
[docs]
def get_available_languages(self):
if not (self._language_cache):
self._language_cache = list(self.postcontent_set.all().values_list("language", flat=True))
return self._language_cache
[docs]
def get_absolute_url(self, language=None):
lang = language or translation.get_language()
with translation.override(lang):
kwargs = {}
current_date = self.date
urlconf = get_setting("PERMALINK_URLS").get(self.app_config.url_patterns)
if "<int:year>" in urlconf:
kwargs["year"] = current_date.year
if "<int:month>" in urlconf:
kwargs["month"] = "%02d" % current_date.month
if "<int:day>" in urlconf:
kwargs["day"] = "%02d" % current_date.day
if "<str:slug>" in urlconf or "<slug:slug>" in urlconf:
kwargs["slug"] = self.safe_translation_getter("slug", language_code=lang, any_language=True) # NOQA
if kwargs["slug"] is None:
return ""
if "<slug:category>" in urlconf or "<str:category>" in urlconf:
category = self.categories.first()
kwargs["category"] = category.safe_translation_getter("slug", language_code=lang, any_language=True) # NOQA
if kwargs["category"] is None:
return ""
try:
return reverse(
"%s:post-detail" % self.app_config.namespace, kwargs=kwargs, current_app=self.app_config.namespace
)
except NoReverseMatch:
return ""
[docs]
def get_title(self, language=None):
title = self.safe_translation_getter("meta_title", language_code=language, any_language=True)
if not title:
title = self.safe_translation_getter("title", language_code=language, any_language=True) or _("No title")
return title.strip()
[docs]
def get_keywords(self, language=None):
"""
Returns the list of keywords (as python list)
:return: list
"""
keywords = self.safe_translation_getter("meta_keywords", language_code=language, any_language=True).strip()
if "".join(keywords) == "":
return []
return [keyword.strip() for keyword in keywords.split(",")]
[docs]
def get_description(self, language=None):
description = self.safe_translation_getter("meta_description", language_code=language, any_language=True)
if not description:
description = self.safe_translation_getter("abstract", any_language=True)
return strip_tags(description).strip()
[docs]
def get_image_full_url(self):
if self.main_image:
if thumbnail_options := get_setting("META_IMAGE_SIZE"):
thumbnail_url = get_thumbnailer(self.main_image).get_thumbnail(thumbnail_options).url
return self.build_absolute_uri(thumbnail_url)
return self.build_absolute_uri(self.main_image.url)
return ""
[docs]
def get_image_width(self):
if self.main_image:
thumbnail_options = get_setting("META_IMAGE_SIZE")
if thumbnail_options:
return get_thumbnailer(self.main_image).get_thumbnail(thumbnail_options).width
return self.main_image.width
[docs]
def get_image_height(self):
if self.main_image:
thumbnail_options = get_setting("META_IMAGE_SIZE")
if thumbnail_options:
return get_thumbnailer(self.main_image).get_thumbnail(thumbnail_options).height
return self.main_image.height
[docs]
def get_author(self):
"""
Return the author (user) objects
"""
return self.author
[docs]
def get_author_url(self):
"""
Return the author URL if available
"""
if self.author:
return reverse(
f"{self.app_config.namespace}:posts-author",
kwargs={"username": self.author.username},
current_app=self.app_config.namespace,
)
return None
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
[docs]
def thumbnail_options(self):
if self.main_image_thumbnail_id:
return self.main_image_thumbnail.as_dict
else:
return get_setting("IMAGE_THUMBNAIL_SIZE")
[docs]
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")
[docs]
def get_cache_key(self, language, prefix):
return f"djangocms-stories:{prefix}:{language}:{self.guid}"
[docs]
class PostContent(PostMetaMixin, ModelMeta, models.Model):
structure_template = "post_detail.html"
no_structure_template = "no_post_structure.html"
class Meta:
verbose_name = _("post content")
verbose_name_plural = _("post contents")
ordering = ("-post__date_published", "-post__date_created")
get_latest_by = "date_published"
# Gruping fields
post = models.ForeignKey(Post, on_delete=models.CASCADE)
language = models.CharField(_("language"), max_length=15, db_index=True)
# Content fields (by post and language
title = models.CharField(_("title"), max_length=752)
slug = models.SlugField(
_("slug"),
max_length=752,
blank=True,
db_index=True,
allow_unicode=STORIES_ALLOW_UNICODE_SLUGS,
)
subtitle = models.CharField(verbose_name=_("subtitle"), max_length=767, blank=True, default="")
abstract = HTMLField(_("abstract"), blank=True, default="", configuration="STORIES_ABSTRACT_CKEDITOR")
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, configuration="STORIES_POST_TEXT_EDITOR_CONF")
placeholders = PlaceholderRelationField()
objects = SiteManager()
admin_manager = AdminManager()
# objects = GenericDateTaggedManager()
# admin_manager = AdminDateTaggedManager()
_metadata = {
"title": "get_title",
"og_title": "get_title",
"twitter_title": "get_title",
"schemaorg_title": "get_title",
"description": "get_description",
"keywords": "get_keywords",
"og_description": "get_description",
"twitter_description": "get_description",
"schemaorg_description": "get_description",
"locale": "language",
"image": "get_image_full_url",
"image_width": "get_image_width",
"image_height": "get_image_height",
"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",
"schemaorg_type": "get_meta_attribute",
"published_time": "date_published",
"modified_time": "date_modified",
"expiration_time": "date_published_end",
"tag": "get_tags",
"url": "get_absolute_url",
}
@property
def author(self):
return self.post.author
@property
def date_published(self):
return self.post.date_published
@property
def date_published_end(self):
return self.post.date_published_end
@property
def date_modified(self):
return self.post.date_modified
@property
def app_config(self):
return self.post.app_config
@property
def categories(self):
return self.post.categories
@cached_property
def media(self):
return get_placeholder_from_slot(self.placeholders, "media")
@cached_property
def content(self):
return get_placeholder_from_slot(self.placeholders, "content")
[docs]
def save(self, *args, **kwargs):
"""
Handle some auto-configuration during save
"""
if not self.slug and self.title:
self.slug = slugify(self.title)
super().save(*args, **kwargs)
[docs]
def get_absolute_url(self, language=None):
return self.post.get_absolute_url(language=language)
[docs]
def get_template(self):
# Used for the cms structure endpoint
if self.app_config and self.app_config.template_prefix:
folder = self.app_config.template_prefix
else:
folder = self._meta.app_label
# Use the structure template if the app_config is not set or if it requests structure mode
if not self.app_config or self.app_config.use_placeholder:
return f"{folder}/{self.structure_template}"
else:
return f"{folder}/{self.no_structure_template}"
[docs]
def get_title(self):
title = self.meta_title
if not title:
title = self.title or _("No title")
return title.strip()
[docs]
def get_keywords(self):
"""
Returns the list of keywords (as python list)
:return: list
"""
keywords = self.meta_keywords.strip()
if "".join(keywords) == "":
return []
return [keyword.strip() for keyword in keywords.split(",")]
[docs]
def get_description(self):
description = self.meta_description
if not description:
description = self.abstract
return strip_tags(description).strip()
[docs]
def get_image_full_url(self):
if self.post.main_image:
if thumbnail_options := get_setting("META_IMAGE_SIZE"):
thumbnail_url = get_thumbnailer(self.post.main_image).get_thumbnail(thumbnail_options).url
return self.build_absolute_uri(thumbnail_url)
return self.build_absolute_uri(self.post.main_image.url)
return ""
[docs]
def get_image_width(self):
if self.post.main_image:
thumbnail_options = get_setting("META_IMAGE_SIZE")
if thumbnail_options:
return get_thumbnailer(self.post.main_image).get_thumbnail(thumbnail_options).width
return self.post.main_image.width
[docs]
def get_image_height(self):
if self.post.main_image:
thumbnail_options = get_setting("META_IMAGE_SIZE")
if thumbnail_options:
return get_thumbnailer(self.post.main_image).get_thumbnail(thumbnail_options).height
return self.post.main_image.height
[docs]
def get_tags(self):
"""
Returns the list of object tags as comma separated list
"""
taglist = [tag.name for tag in self.post.tags.all()]
return ",".join(taglist)
def __str__(self):
return self.title or _("Untitled")
class BasePostPlugin(CMSPlugin):
app_config = models.ForeignKey(
StoriesConfig,
on_delete=models.CASCADE,
null=True,
verbose_name=_("app. config"),
help_text=_("When selecting a value, the form is reloaded to get the updated default"),
)
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 layout"),
help_text=_("Select plugin layout to load for this instance"),
default=DEFAULT_TEMPLATE_FOLDERS[0][0],
choices=DEFAULT_TEMPLATE_FOLDERS,
)
class Meta:
abstract = True
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._meta.get_field("template_folder").choices = STORIES_PLUGIN_TEMPLATE_FOLDERS
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("post", "post__app_config").prefetch_related(
"post__categories", "post__categories__translations", "post__categories__app_config"
)
def post_content_queryset(self, request=None, selected_posts=None):
language = translation.get_language()
if (
request
and getattr(request, "toolbar", False)
and (request.toolbar.edit_mode_active or request.toolbar.preview_mode_active)
):
post_contents = PostContent.admin_manager.latest_content()
else:
post_contents = PostContent.objects.all()
if self.app_config:
post_contents = post_contents.filter(post__app_config=self.app_config)
if self.current_site:
post_contents = post_contents.on_site(get_current_site(request))
if selected_posts:
post_contents = post_contents.filter(post__in=selected_posts)
post_contents = post_contents.prefetch_related("post").filter(language=language)
return self.optimize(post_contents)
class LatestPostsPlugin(BasePostPlugin):
latest_posts = models.IntegerField(
_("entries"),
default=get_setting("LATEST_POSTS"),
help_text=_("The number of latests entries to be displayed."),
)
tags = TaggableManager(
_("filter by tag"),
blank=True,
help_text=_("Show only the entries tagged with chosen tags."),
related_name="djangocms_stories_latest_post",
)
categories = models.ManyToManyField(
"djangocms_stories.PostCategory",
blank=True,
verbose_name=_("filter by category"),
help_text=_("Show only the posts of the chosen categories."),
)
def __str__(self):
return force_str(_("%s latest posts by tag") % self.latest_posts)
def copy_relations(self, old_instance):
for tag in old_instance.tags.all():
self.tags.add(tag)
for category in old_instance.categories.all():
self.categories.add(category)
def get_post_contents(self, request):
post_contents = self.post_content_queryset(request)
if self.tags.exists():
post_contents = post_contents.filter(post__tags__in=list(self.tags.all()))
if self.categories.exists():
post_contents = post_contents.filter(post__categories__in=list(self.categories.all()))
return self.optimize(post_contents.distinct())[: self.latest_posts]
class AuthorEntriesPlugin(BasePostPlugin):
authors = models.ManyToManyField(
dj_settings.AUTH_USER_MODEL,
verbose_name=_("authors"),
)
latest_posts = models.IntegerField(
_("entries"),
default=get_setting("LATEST_POSTS"),
help_text=_("The number of author entries to be displayed."),
)
def __str__(self):
return force_str(_("%s latest entries by author") % self.latest_posts)
def copy_relations(self, oldinstance):
self.authors.set(oldinstance.authors.all())
def get_post_contents(self, request):
return self.post_content_queryset(request)
def get_authors(self, request):
authors = self.authors.all()
for author in authors:
qs = self.get_post_contents(request).filter(post__author=author)
# total nb of posts
author.count = qs.count()
# "the number of author posts to be displayed"
author.post_contents = qs[: self.latest_posts]
return authors
class FeaturedPostsPlugin(BasePostPlugin):
posts = SortedManyToManyField(Post, verbose_name=_("Featured posts"))
def __str__(self):
return force_str(_("Featured posts"))
def copy_relations(self, oldinstance):
self.posts.set(oldinstance.posts.all())
def get_posts(
self,
request,
):
posts = self.posts.all()
map = {
post_content.post_id: post_content
for post_content in self.post_content_queryset(request, selected_posts=posts)
}
return [map.get(post.pk) for post in posts if post.pk in map]
class GenericBlogPlugin(BasePostPlugin):
class Meta:
abstract = False
def __str__(self):
return force_str(_("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)