You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

393 lines
11 KiB

import math
import random
from django.conf import settings
from django.db import models
from django.utils import timezone
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from apps.profiles.models.institution import Institution
class PostCategory(models.Model):
name = models.CharField(
max_length=255,
verbose_name=_('Category Name')
)
slug = models.SlugField(
max_length=255,
unique=True,
allow_unicode=True,
verbose_name=_('Slug / URL Identifier')
)
language = models.CharField(
max_length=10,
default='en',
choices=settings.LANGUAGES,
verbose_name=_('Language')
)
description = models.TextField(
blank=True,
null=True,
verbose_name=_('Category Description')
)
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name=_('Created At')
)
class Meta:
ordering = ('name',)
verbose_name = _('Post Category')
verbose_name_plural = _('Post Categories')
def __str__(self):
return f"{self.name} ({self.language})"
def save(self, *args, **kwargs):
if not self.slug:
base_slug = slugify(self.name, allow_unicode=True) or f"category-{random.randint(1000, 9999)}"
slug = base_slug
counter = 1
while PostCategory.objects.filter(slug=slug).exclude(pk=self.pk).exists():
slug = f"{base_slug}-{counter}"
counter += 1
self.slug = slug
super().save(*args, **kwargs)
class PostTag(models.Model):
name = models.CharField(
max_length=100,
unique=True,
verbose_name=_('Tag Name')
)
slug = models.SlugField(
max_length=100,
unique=True,
allow_unicode=True,
verbose_name=_('Slug')
)
class Meta:
ordering = ('name',)
verbose_name = _('Post Tag')
verbose_name_plural = _('Post Tags')
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name, allow_unicode=True) or f"tag-{random.randint(1000, 9999)}"
super().save(*args, **kwargs)
class Post(models.Model):
class PostType(models.TextChoices):
NEWS = 'news', _('News & Announcements')
ARTICLE = 'article', _('Analytical Article')
REPORT = 'report', _('Field & Diplomatic Report')
INTERVIEW = 'interview', _('Expert Interview')
class Status(models.TextChoices):
DRAFT = 'draft', _('Draft')
REVIEW = 'review', _('Under Review')
PUBLISHED = 'published', _('Published')
ARCHIVED = 'archived', _('Archived')
class LanguageChoices(models.TextChoices):
FA = 'fa', _('Persian')
AR = 'ar', _('Arabic')
EN = 'en', _('English')
UR = 'ur', _('Urdu')
FR = 'fr', _('French')
title = models.CharField(
max_length=255,
verbose_name=_('Post Title')
)
slug = models.SlugField(
max_length=255,
unique=True,
allow_unicode=True,
blank=True,
null=True,
verbose_name=_('Slug / URL Identifier')
)
post_type = models.CharField(
max_length=50,
choices=PostType.choices,
default=PostType.ARTICLE,
verbose_name=_('Post Type')
)
category = models.ForeignKey(
PostCategory,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='posts',
verbose_name=_('Category')
)
language = models.CharField(
max_length=10,
choices=LanguageChoices.choices,
default=LanguageChoices.EN,
verbose_name=_('Language')
)
status = models.CharField(
max_length=20,
choices=Status.choices,
default=Status.PUBLISHED,
verbose_name=_('Publication Status')
)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='cms_posts',
verbose_name=_('Author')
)
institution = models.ForeignKey(
Institution,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='cms_posts',
verbose_name=_('Associated Institution')
)
author_role_label = models.CharField(
max_length=150,
blank=True,
null=True,
verbose_name=_('Author Role Label'),
help_text=_('e.g. Senior Researcher, Center Director, Diplomatic Envoy')
)
author_custom_avatar = models.ImageField(
upload_to='cms/avatars/%Y/%m/',
blank=True,
null=True,
verbose_name=_('Custom Author Avatar')
)
featured_image = models.ImageField(
upload_to='cms/featured/%Y/%m/',
blank=True,
null=True,
verbose_name=_('Featured Cover Image')
)
excerpt = models.TextField(
blank=True,
null=True,
verbose_name=_('Excerpt / Summary'),
help_text=_('Brief summary for search results and cards.')
)
content = models.TextField(
verbose_name=_('Rich Text / Markdown Content')
)
tags = models.JSONField(
default=list,
blank=True,
verbose_name=_('Tags')
)
reading_time_minutes = models.PositiveIntegerField(
default=3,
verbose_name=_('Reading Time (Minutes)')
)
views_count = models.PositiveIntegerField(
default=0,
verbose_name=_('Views Count')
)
likes_count = models.PositiveIntegerField(
default=0,
verbose_name=_('Likes Count')
)
is_featured = models.BooleanField(
default=False,
verbose_name=_('Is Featured')
)
publish_date = models.DateTimeField(
default=timezone.now,
verbose_name=_('Publish Date')
)
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name=_('Created At')
)
updated_at = models.DateTimeField(
auto_now=True,
verbose_name=_('Updated At')
)
class Meta:
ordering = ('-is_featured', '-publish_date')
verbose_name = _('Post')
verbose_name_plural = _('Posts')
def __str__(self):
return f"{self.title} ({self.get_post_type_display()} - {self.language})"
def save(self, *args, **kwargs):
if not self.slug:
base_slug = slugify(self.title, allow_unicode=True) or f"post-{random.randint(1000, 9999)}"
slug = base_slug
counter = 1
while Post.objects.filter(slug=slug).exclude(pk=self.pk).exists():
slug = f"{base_slug}-{counter}"
counter += 1
self.slug = slug
# Calculate reading time based on word count (~200 words/min)
if self.content:
word_count = len(self.content.split())
self.reading_time_minutes = max(1, math.ceil(word_count / 200))
super().save(*args, **kwargs)
def is_liked_by(self, user):
if not user or not user.is_authenticated:
return False
return self.likes.filter(user=user).exists()
class PostComment(models.Model):
class Status(models.TextChoices):
PENDING = 'pending', _('Pending Moderation')
APPROVED = 'approved', _('Approved')
SPAM = 'spam', _('Spam / Rejected')
post = models.ForeignKey(
Post,
on_delete=models.CASCADE,
related_name='comments',
verbose_name=_('Post')
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='post_comments',
verbose_name=_('User')
)
author_name = models.CharField(
max_length=255,
blank=True,
null=True,
verbose_name=_('Author Name')
)
author_email = models.EmailField(
blank=True,
null=True,
verbose_name=_('Author Email')
)
content = models.TextField(
verbose_name=_('Comment Content')
)
status = models.CharField(
max_length=20,
choices=Status.choices,
default=Status.APPROVED,
verbose_name=_('Moderation Status')
)
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name=_('Created At')
)
class Meta:
ordering = ('-created_at',)
verbose_name = _('Post Comment')
verbose_name_plural = _('Post Comments')
def __str__(self):
author = self.author_name or (self.user.fullname if self.user else "Anonymous")
return f"Comment by {author} on {self.post.title}"
class PostLike(models.Model):
post = models.ForeignKey(
Post,
on_delete=models.CASCADE,
related_name='likes',
verbose_name=_('Post')
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='cms_post_likes',
verbose_name=_('User')
)
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name=_('Created At')
)
class Meta:
unique_together = ('post', 'user')
ordering = ('-created_at',)
verbose_name = _('Post Like')
verbose_name_plural = _('Post Likes')
def __str__(self):
return f"{self.user} likes {self.post.title}"
class MediaAsset(models.Model):
class MediaType(models.TextChoices):
IMAGE = 'image', _('Image')
VIDEO = 'video', _('Video')
AUDIO = 'audio', _('Audio')
DOCUMENT = 'document', _('Document / PDF')
file = models.FileField(
upload_to='cms/media/%Y/%m/',
verbose_name=_('Media File')
)
title = models.CharField(
max_length=255,
blank=True,
null=True,
verbose_name=_('Title / Description')
)
media_type = models.CharField(
max_length=20,
choices=MediaType.choices,
default=MediaType.IMAGE,
verbose_name=_('Media Type')
)
file_size = models.PositiveIntegerField(
default=0,
verbose_name=_('File Size (Bytes)')
)
dimensions = models.CharField(
max_length=50,
blank=True,
null=True,
verbose_name=_('Dimensions (WxH)')
)
uploaded_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='uploaded_cms_media',
verbose_name=_('Uploaded By')
)
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name=_('Created At')
)
class Meta:
ordering = ('-created_at',)
verbose_name = _('Media Asset')
verbose_name_plural = _('Media Asset Library')
def __str__(self):
return self.title or f"Media Asset #{self.id}"
def save(self, *args, **kwargs):
if self.file and not self.file_size:
try:
self.file_size = self.file.size
except Exception:
pass
super().save(*args, **kwargs)