from django.conf import settings from django.db.models import ( CASCADE, BooleanField, CharField, FileField, ForeignKey, ManyToManyField, TextField, ) from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ from engine.core.abstract import NiceModel from engine.core.utils.db import TweakedAutoSlugField, unicode_slugify_function from engine.core.utils.markdown import strip_markdown class Post(NiceModel): __doc__ = _( # pyright: ignore[reportUnknownVariableType] "Represents a blog post model. " "The Post class defines the structure and behavior of a blog post. " "It includes attributes for author, title, content, optional file attachment, slug, and associated tags. " "The class enforces constraints such as requiring either content or a file attachment but not both simultaneously. " "It also supports automatic slug generation based on the title." ) is_publicly_visible = True author = ForeignKey( to=settings.AUTH_USER_MODEL, on_delete=CASCADE, blank=False, null=False, related_name="posts", ) title = CharField( unique=True, max_length=128, blank=False, null=False, help_text=_("post title"), verbose_name=_("title"), ) content = TextField( verbose_name=_("content"), help_text=_("post content"), blank=True, null=True, ) file = FileField(upload_to="posts/", blank=True, null=True) slug = TweakedAutoSlugField( populate_from="title", slugify_function=unicode_slugify_function, allow_unicode=True, unique=True, editable=False, max_length=88, overwrite=True, null=True, ) tags = ManyToManyField(to="blog.PostTag", blank=True, related_name="posts") meta_description = CharField(max_length=150, blank=True, null=True) is_static_page = BooleanField( default=False, verbose_name=_("is static page"), help_text=_( "is this a post for a page with static URL (e.g. `/help/delivery`)?" ), ) def __str__(self): return f"{self.title} | {self.author.first_name} {self.author.last_name}" @cached_property def seo_description(self) -> str: return strip_markdown(self.content or "")[:180] class Meta: verbose_name = _("post") verbose_name_plural = _("posts") def save(self, *args, **kwargs): if self.file: raise ValueError( _("file uploads are not supported yet - use content instead") ) if not any([self.file, self.content]) or all([self.file, self.content]): raise ValueError( _("a file or content must be provided - mutually exclusive") ) super().save(*args, **kwargs) class PostTag(NiceModel): """ Represents a tag associated with a post. The PostTag class is used to define and manage tags that can be assigned to posts. These tags include an internal identifier and a user-friendly display name. The class supports internationalization for both the internal identifier and the display name. Attributes: is_publicly_visible (bool): Determines if the tag is visible publicly. tag_name (CharField): An internal tag identifier for the post's tag. It is a required field with a maximum length of 255 characters. name (CharField): A user-friendly, unique display name for the post's tag with a maximum length of 255 characters. Meta: verbose_name (str): Human-readable singular name of the PostTag model. verbose_name_plural (str): Human-readable plural name of the PostTag model. """ is_publicly_visible = True posts: "Post" tag_name = CharField( blank=False, null=False, max_length=255, help_text=_("internal tag identifier for the post tag"), verbose_name=_("tag name"), ) name = CharField( max_length=255, help_text=_("user-friendly name for the post tag"), verbose_name=_("tag display name"), unique=True, ) def __str__(self): return self.tag_name class Meta: verbose_name = _("post tag") verbose_name_plural = _("post tags")