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.
363 lines
11 KiB
363 lines
11 KiB
import random
|
|
from django.conf import settings
|
|
from django.db import models
|
|
from django.utils.text import slugify
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
|
|
class Institution(models.Model):
|
|
class InstitutionType(models.TextChoices):
|
|
MOSQUE = 'mosque', _('Mosque')
|
|
HUSSAINIYA = 'hussainiya', _('Hussainiya / Islamic Center')
|
|
CULTURAL_CENTER = 'cultural_center', _('Cultural Center')
|
|
LIBRARY = 'library', _('Islamic Library')
|
|
INSTITUTE = 'institute', _('Seminary & Institute')
|
|
CHARITY = 'charity', _('Charity & Foundation')
|
|
OTHER = 'other', _('Other Organization')
|
|
|
|
class VerificationStatus(models.TextChoices):
|
|
PENDING = 'pending', _('Pending Review')
|
|
APPROVED = 'approved', _('Approved & Verified')
|
|
REJECTED = 'rejected', _('Rejected')
|
|
|
|
name = models.CharField(
|
|
max_length=255,
|
|
verbose_name=_('Institution Name'),
|
|
help_text=_('Official name of the mosque, center, or institute.')
|
|
)
|
|
slug = models.SlugField(
|
|
max_length=255,
|
|
unique=True,
|
|
blank=True,
|
|
null=True,
|
|
allow_unicode=True,
|
|
verbose_name=_('Slug / URL Identifier')
|
|
)
|
|
type = models.CharField(
|
|
max_length=50,
|
|
choices=InstitutionType.choices,
|
|
default=InstitutionType.CULTURAL_CENTER,
|
|
verbose_name=_('Institution Type')
|
|
)
|
|
country = models.CharField(
|
|
max_length=255,
|
|
verbose_name=_('Country')
|
|
)
|
|
city = models.CharField(
|
|
max_length=255,
|
|
verbose_name=_('City')
|
|
)
|
|
address = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_('Full Address')
|
|
)
|
|
latitude = models.FloatField(
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_('Latitude')
|
|
)
|
|
longitude = models.FloatField(
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_('Longitude')
|
|
)
|
|
description = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_('Description & Background')
|
|
)
|
|
website = models.URLField(
|
|
max_length=500,
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_('Website')
|
|
)
|
|
email = models.EmailField(
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_('Official Email')
|
|
)
|
|
phone = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_('Official Phone')
|
|
)
|
|
established_year = models.IntegerField(
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_('Established Year (CE)')
|
|
)
|
|
cover_image = models.ImageField(
|
|
upload_to='institutions/covers/%Y/%m/',
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_('Cover Image')
|
|
)
|
|
avatar = models.ImageField(
|
|
upload_to='institutions/avatars/%Y/%m/',
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_('Logo / Avatar')
|
|
)
|
|
social_media = models.JSONField(
|
|
default=dict,
|
|
blank=True,
|
|
verbose_name=_('Social Media Links'),
|
|
help_text=_('Dictionary with keys: facebook, twitter, instagram, linkedin, youtube, telegram')
|
|
)
|
|
working_hours = models.JSONField(
|
|
default=dict,
|
|
blank=True,
|
|
verbose_name=_('Working Hours / Prayer Times Schedule')
|
|
)
|
|
tags = models.JSONField(
|
|
default=list,
|
|
blank=True,
|
|
verbose_name=_('Tags & Specializations')
|
|
)
|
|
verification_status = models.CharField(
|
|
max_length=20,
|
|
choices=VerificationStatus.choices,
|
|
default=VerificationStatus.PENDING,
|
|
verbose_name=_('Verification Status')
|
|
)
|
|
follower_count = models.PositiveIntegerField(
|
|
default=0,
|
|
verbose_name=_('Followers Count')
|
|
)
|
|
is_featured = models.BooleanField(
|
|
default=False,
|
|
verbose_name=_('Is Featured')
|
|
)
|
|
is_active = models.BooleanField(
|
|
default=True,
|
|
verbose_name=_('Is Active')
|
|
)
|
|
created_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='created_institutions',
|
|
verbose_name=_('Created By')
|
|
)
|
|
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', '-created_at')
|
|
verbose_name = _('Institution')
|
|
verbose_name_plural = _('Institutions')
|
|
|
|
def __str__(self):
|
|
return f"{self.name} ({self.city}, {self.country})"
|
|
|
|
def save(self, *args, **kwargs):
|
|
if not self.slug:
|
|
base_slug = slugify(self.name, allow_unicode=True) or f"institution-{random.randint(1000, 9999)}"
|
|
slug = base_slug
|
|
counter = 1
|
|
while Institution.objects.filter(slug=slug).exclude(pk=self.pk).exists():
|
|
slug = f"{base_slug}-{counter}"
|
|
counter += 1
|
|
self.slug = slug
|
|
super().save(*args, **kwargs)
|
|
|
|
def is_admin(self, user):
|
|
if not user or not user.is_authenticated:
|
|
return False
|
|
if getattr(user, 'is_super_admin', False) or getattr(user, 'is_regional_admin', False):
|
|
return True
|
|
return self.members.filter(user=user, role=InstitutionMember.MemberRole.ADMIN).exists()
|
|
|
|
def is_editor(self, user):
|
|
if not user or not user.is_authenticated:
|
|
return False
|
|
if self.is_admin(user):
|
|
return True
|
|
return self.members.filter(user=user, role__in=[InstitutionMember.MemberRole.ADMIN, InstitutionMember.MemberRole.EDITOR]).exists()
|
|
|
|
def get_user_role(self, user):
|
|
if not user or not user.is_authenticated:
|
|
return None
|
|
member = self.members.filter(user=user).first()
|
|
if member:
|
|
return member.role
|
|
if getattr(user, 'is_super_admin', False):
|
|
return 'super_admin'
|
|
if getattr(user, 'is_regional_admin', False):
|
|
return 'regional_admin'
|
|
return None
|
|
|
|
def is_followed_by(self, user):
|
|
if not user or not user.is_authenticated:
|
|
return False
|
|
return self.followers.filter(user=user).exists()
|
|
|
|
|
|
class InstitutionMember(models.Model):
|
|
class MemberRole(models.TextChoices):
|
|
ADMIN = 'admin', _('Administrator')
|
|
EDITOR = 'editor', _('Content & Project Editor')
|
|
VIEWER = 'viewer', _('Viewer / Member')
|
|
|
|
institution = models.ForeignKey(
|
|
Institution,
|
|
on_delete=models.CASCADE,
|
|
related_name='members',
|
|
verbose_name=_('Institution')
|
|
)
|
|
user = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.CASCADE,
|
|
related_name='institution_memberships',
|
|
verbose_name=_('User')
|
|
)
|
|
role = models.CharField(
|
|
max_length=20,
|
|
choices=MemberRole.choices,
|
|
default=MemberRole.VIEWER,
|
|
verbose_name=_('Role')
|
|
)
|
|
title = models.CharField(
|
|
max_length=150,
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_('Organizational Title'),
|
|
help_text=_('e.g., Director, Media Manager, Cultural Affairs Officer')
|
|
)
|
|
joined_at = models.DateTimeField(
|
|
auto_now_add=True,
|
|
verbose_name=_('Joined At')
|
|
)
|
|
|
|
class Meta:
|
|
unique_together = ('institution', 'user')
|
|
ordering = ('-joined_at',)
|
|
verbose_name = _('Institution Member')
|
|
verbose_name_plural = _('Institution Members')
|
|
|
|
def __str__(self):
|
|
return f"{self.user} - {self.institution.name} ({self.get_role_display()})"
|
|
|
|
|
|
class InstitutionTimeline(models.Model):
|
|
institution = models.ForeignKey(
|
|
Institution,
|
|
on_delete=models.CASCADE,
|
|
related_name='timeline_entries',
|
|
verbose_name=_('Institution')
|
|
)
|
|
year = models.CharField(
|
|
max_length=50,
|
|
verbose_name=_('Year / Period'),
|
|
help_text=_('e.g. 1995, 1416 AH, or 2020-Present')
|
|
)
|
|
title = models.CharField(
|
|
max_length=255,
|
|
verbose_name=_('Milestone Title')
|
|
)
|
|
description = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_('Milestone Description')
|
|
)
|
|
order = models.IntegerField(
|
|
default=0,
|
|
verbose_name=_('Display Order')
|
|
)
|
|
created_at = models.DateTimeField(
|
|
auto_now_add=True,
|
|
verbose_name=_('Created At')
|
|
)
|
|
|
|
class Meta:
|
|
ordering = ('order', 'id')
|
|
verbose_name = _('Timeline Milestone')
|
|
verbose_name_plural = _('Timeline Milestones')
|
|
|
|
def __str__(self):
|
|
return f"{self.institution.name} - {self.year}: {self.title}"
|
|
|
|
|
|
class InstitutionMedia(models.Model):
|
|
class MediaType(models.TextChoices):
|
|
IMAGE = 'image', _('Image')
|
|
VIDEO = 'video', _('Video')
|
|
DOCUMENT = 'document', _('Document / Publication')
|
|
|
|
institution = models.ForeignKey(
|
|
Institution,
|
|
on_delete=models.CASCADE,
|
|
related_name='media_items',
|
|
verbose_name=_('Institution')
|
|
)
|
|
file = models.FileField(
|
|
upload_to='institutions/gallery/%Y/%m/',
|
|
verbose_name=_('Media File')
|
|
)
|
|
media_type = models.CharField(
|
|
max_length=20,
|
|
choices=MediaType.choices,
|
|
default=MediaType.IMAGE,
|
|
verbose_name=_('Media Type')
|
|
)
|
|
title = models.CharField(
|
|
max_length=255,
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_('Media Title')
|
|
)
|
|
caption = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_('Caption / Notes')
|
|
)
|
|
uploaded_at = models.DateTimeField(
|
|
auto_now_add=True,
|
|
verbose_name=_('Uploaded At')
|
|
)
|
|
|
|
class Meta:
|
|
ordering = ('-uploaded_at',)
|
|
verbose_name = _('Institution Media')
|
|
verbose_name_plural = _('Institution Media Gallery')
|
|
|
|
def __str__(self):
|
|
return f"{self.institution.name} - {self.title or self.media_type}"
|
|
|
|
|
|
class InstitutionFollower(models.Model):
|
|
institution = models.ForeignKey(
|
|
Institution,
|
|
on_delete=models.CASCADE,
|
|
related_name='followers',
|
|
verbose_name=_('Institution')
|
|
)
|
|
user = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.CASCADE,
|
|
related_name='followed_institutions',
|
|
verbose_name=_('User')
|
|
)
|
|
created_at = models.DateTimeField(
|
|
auto_now_add=True,
|
|
verbose_name=_('Followed At')
|
|
)
|
|
|
|
class Meta:
|
|
unique_together = ('institution', 'user')
|
|
ordering = ('-created_at',)
|
|
verbose_name = _('Institution Follower')
|
|
verbose_name_plural = _('Institution Followers')
|
|
|
|
def __str__(self):
|
|
return f"{self.user} follows {self.institution.name}"
|