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)