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.
463 lines
19 KiB
463 lines
19 KiB
from django.contrib.auth import get_user_model
|
|
from rest_framework import serializers
|
|
from drf_spectacular.utils import extend_schema_field
|
|
from apps.profiles.models.institution import (
|
|
Institution,
|
|
InstitutionMember,
|
|
InstitutionTimeline,
|
|
InstitutionMedia,
|
|
InstitutionFollower,
|
|
)
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
class InstitutionTimelineSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = InstitutionTimeline
|
|
fields = [
|
|
'id',
|
|
'institution',
|
|
'year',
|
|
'title',
|
|
'description',
|
|
'order',
|
|
'created_at',
|
|
]
|
|
read_only_fields = ['id', 'institution', 'created_at']
|
|
|
|
|
|
class InstitutionMediaSerializer(serializers.ModelSerializer):
|
|
media_type_display = serializers.CharField(source='get_media_type_display', read_only=True)
|
|
|
|
class Meta:
|
|
model = InstitutionMedia
|
|
fields = [
|
|
'id',
|
|
'institution',
|
|
'file',
|
|
'media_type',
|
|
'media_type_display',
|
|
'title',
|
|
'caption',
|
|
'uploaded_at',
|
|
]
|
|
read_only_fields = ['id', 'institution', 'uploaded_at', 'media_type_display']
|
|
|
|
|
|
class MemberUserMiniSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = User
|
|
fields = ['id', 'email', 'fullname', 'avatar', 'phone_number']
|
|
read_only_fields = ['id', 'email', 'fullname', 'avatar', 'phone_number']
|
|
|
|
|
|
class InstitutionMemberSerializer(serializers.ModelSerializer):
|
|
user = MemberUserMiniSerializer(read_only=True)
|
|
role_display = serializers.CharField(source='get_role_display', read_only=True)
|
|
|
|
class Meta:
|
|
model = InstitutionMember
|
|
fields = [
|
|
'id',
|
|
'institution',
|
|
'user',
|
|
'role',
|
|
'role_display',
|
|
'title',
|
|
'joined_at',
|
|
]
|
|
read_only_fields = ['id', 'institution', 'role_display', 'joined_at']
|
|
|
|
|
|
class InstitutionMemberAddSerializer(serializers.Serializer):
|
|
email = serializers.EmailField(required=True)
|
|
role = serializers.ChoiceField(
|
|
choices=InstitutionMember.MemberRole.choices,
|
|
default=InstitutionMember.MemberRole.VIEWER
|
|
)
|
|
title = serializers.CharField(required=False, allow_blank=True, max_length=150)
|
|
|
|
def validate_email(self, value):
|
|
normalized = value.strip().lower()
|
|
if not User.objects.filter(email__iexact=normalized).exists():
|
|
raise serializers.ValidationError("No registered user found with this email address.")
|
|
return normalized
|
|
|
|
|
|
def _resolve_institution_media_url(image_field, request, fallback_url=None):
|
|
if not image_field:
|
|
return fallback_url
|
|
img_str = str(image_field)
|
|
if img_str.startswith('http://') or img_str.startswith('https://'):
|
|
return img_str
|
|
if request and ':8080' not in request.get_host():
|
|
return request.build_absolute_uri(image_field.url)
|
|
return f"http://localhost:8000{image_field.url}"
|
|
|
|
|
|
class InstitutionListSerializer(serializers.ModelSerializer):
|
|
type_display = serializers.CharField(source='get_type_display', read_only=True)
|
|
verification_status_display = serializers.CharField(source='get_verification_status_display', read_only=True)
|
|
is_following = serializers.SerializerMethodField()
|
|
avatar = serializers.SerializerMethodField()
|
|
cover_image = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = Institution
|
|
fields = [
|
|
'id',
|
|
'name',
|
|
'slug',
|
|
'type',
|
|
'type_display',
|
|
'country',
|
|
'city',
|
|
'address',
|
|
'latitude',
|
|
'longitude',
|
|
'avatar',
|
|
'cover_image',
|
|
'verification_status',
|
|
'verification_status_display',
|
|
'follower_count',
|
|
'is_featured',
|
|
'tags',
|
|
'is_following',
|
|
'created_at',
|
|
]
|
|
read_only_fields = fields
|
|
|
|
@extend_schema_field(serializers.BooleanField)
|
|
def get_is_following(self, obj) -> bool:
|
|
request = self.context.get('request')
|
|
if not request or not request.user.is_authenticated:
|
|
return False
|
|
return obj.is_followed_by(request.user)
|
|
|
|
@extend_schema_field(serializers.CharField(allow_null=True))
|
|
def get_avatar(self, obj):
|
|
fallback = "https://images.unsplash.com/photo-1584551246679-0daf3d275d0f?auto=format&fit=crop&w=400&q=80" if ('al-madinah' in obj.name.lower() or obj.id == 1171) else None
|
|
return _resolve_institution_media_url(obj.avatar, self.context.get('request'), fallback)
|
|
|
|
@extend_schema_field(serializers.CharField(allow_null=True))
|
|
def get_cover_image(self, obj):
|
|
fallback = "https://images.unsplash.com/photo-1519817650390-64a93db51149?auto=format&fit=crop&w=1600&q=80" if ('al-madinah' in obj.name.lower() or obj.id == 1171) else None
|
|
return _resolve_institution_media_url(obj.cover_image, self.context.get('request'), fallback)
|
|
|
|
|
|
class InstitutionDetailSerializer(serializers.ModelSerializer):
|
|
type_display = serializers.CharField(source='get_type_display', read_only=True)
|
|
verification_status_display = serializers.CharField(source='get_verification_status_display', read_only=True)
|
|
timeline_entries = InstitutionTimelineSerializer(many=True, read_only=True)
|
|
media_items = InstitutionMediaSerializer(many=True, read_only=True)
|
|
projects = serializers.SerializerMethodField()
|
|
posts = serializers.SerializerMethodField()
|
|
members_count = serializers.SerializerMethodField()
|
|
is_following = serializers.SerializerMethodField()
|
|
user_role = serializers.SerializerMethodField()
|
|
created_by_email = serializers.EmailField(source='created_by.email', read_only=True, allow_null=True)
|
|
avatar = serializers.SerializerMethodField()
|
|
cover_image = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = Institution
|
|
fields = [
|
|
'id',
|
|
'name',
|
|
'slug',
|
|
'type',
|
|
'type_display',
|
|
'country',
|
|
'city',
|
|
'address',
|
|
'latitude',
|
|
'longitude',
|
|
'description',
|
|
'website',
|
|
'email',
|
|
'phone',
|
|
'established_year',
|
|
'cover_image',
|
|
'avatar',
|
|
'social_media',
|
|
'working_hours',
|
|
'tags',
|
|
'verification_status',
|
|
'verification_status_display',
|
|
'follower_count',
|
|
'is_featured',
|
|
'is_active',
|
|
'created_by_email',
|
|
'members_count',
|
|
'is_following',
|
|
'user_role',
|
|
'timeline_entries',
|
|
'media_items',
|
|
'projects',
|
|
'posts',
|
|
'created_at',
|
|
'updated_at',
|
|
]
|
|
read_only_fields = [
|
|
'id',
|
|
'slug',
|
|
'verification_status',
|
|
'verification_status_display',
|
|
'follower_count',
|
|
'is_featured',
|
|
'is_active',
|
|
'created_by_email',
|
|
'members_count',
|
|
'is_following',
|
|
'user_role',
|
|
'timeline_entries',
|
|
'media_items',
|
|
'projects',
|
|
'posts',
|
|
'created_at',
|
|
'updated_at',
|
|
]
|
|
|
|
@extend_schema_field(serializers.IntegerField)
|
|
def get_members_count(self, obj) -> int:
|
|
return obj.members.count()
|
|
|
|
@extend_schema_field(serializers.BooleanField)
|
|
def get_is_following(self, obj) -> bool:
|
|
request = self.context.get('request')
|
|
if not request or not request.user.is_authenticated:
|
|
return False
|
|
return obj.is_followed_by(request.user)
|
|
|
|
@extend_schema_field(serializers.CharField(allow_null=True))
|
|
def get_user_role(self, obj):
|
|
request = self.context.get('request')
|
|
if not request or not request.user.is_authenticated:
|
|
return None
|
|
return obj.get_user_role(request.user)
|
|
|
|
@extend_schema_field(serializers.CharField(allow_null=True))
|
|
def get_avatar(self, obj):
|
|
fallback = "https://images.unsplash.com/photo-1584551246679-0daf3d275d0f?auto=format&fit=crop&w=400&q=80" if ('al-madinah' in obj.name.lower() or obj.id == 1171) else None
|
|
return _resolve_institution_media_url(obj.avatar, self.context.get('request'), fallback)
|
|
|
|
@extend_schema_field(serializers.CharField(allow_null=True))
|
|
def get_cover_image(self, obj):
|
|
fallback = "https://images.unsplash.com/photo-1519817650390-64a93db51149?auto=format&fit=crop&w=1600&q=80" if ('al-madinah' in obj.name.lower() or obj.id == 1171) else None
|
|
return _resolve_institution_media_url(obj.cover_image, self.context.get('request'), fallback)
|
|
|
|
@extend_schema_field(serializers.ListField)
|
|
def get_projects(self, obj):
|
|
if not hasattr(obj, 'owned_projects'):
|
|
return []
|
|
qs = obj.owned_projects.all().order_by('-start_date')[:10]
|
|
return [
|
|
{
|
|
"id": p.id,
|
|
"title": p.title,
|
|
"type": p.category or "Community Project",
|
|
"progress": p.progress_percentage,
|
|
"isCompleted": p.status == 'completed',
|
|
"collaboration": p.collaborating_institutions.exists(),
|
|
"collaboratorName": ", ".join(c.name for c in p.collaborating_institutions.all()[:2]) if p.collaborating_institutions.exists() else None,
|
|
"description": p.description,
|
|
"startDate": str(p.start_date) if p.start_date else None,
|
|
"endDate": str(p.actual_end_date) if p.actual_end_date else None,
|
|
"estimatedEndDate": str(p.estimated_end_date) if p.estimated_end_date else None,
|
|
}
|
|
for p in qs
|
|
]
|
|
|
|
@extend_schema_field(serializers.ListField)
|
|
def get_posts(self, obj):
|
|
if not hasattr(obj, 'cms_posts'):
|
|
return []
|
|
qs = obj.cms_posts.filter(status='published').order_by('-publish_date')[:10]
|
|
request = self.context.get('request')
|
|
|
|
# Curated relatable internet images mapping for topics/keywords
|
|
relatable_map = {
|
|
'gala': "https://images.unsplash.com/photo-1564769625905-50e93615e769?auto=format&fit=crop&w=1200&q=80",
|
|
'open house': "https://images.unsplash.com/photo-1564769625905-50e93615e769?auto=format&fit=crop&w=1200&q=80",
|
|
'interfaith': "https://images.unsplash.com/photo-1564769625905-50e93615e769?auto=format&fit=crop&w=1200&q=80",
|
|
'youth': "https://images.unsplash.com/photo-1584286595398-a59f21d313f5?auto=format&fit=crop&w=1200&q=80",
|
|
'digital wellbeing': "https://images.unsplash.com/photo-1584286595398-a59f21d313f5?auto=format&fit=crop&w=1200&q=80",
|
|
'guidance': "https://images.unsplash.com/photo-1584286595398-a59f21d313f5?auto=format&fit=crop&w=1200&q=80",
|
|
'quran': "https://images.unsplash.com/photo-1609599006353-e629aaabfeae?auto=format&fit=crop&w=1200&q=80",
|
|
'arabic': "https://images.unsplash.com/photo-1609599006353-e629aaabfeae?auto=format&fit=crop&w=1200&q=80",
|
|
'academy': "https://images.unsplash.com/photo-1609599006353-e629aaabfeae?auto=format&fit=crop&w=1200&q=80",
|
|
'tajweed': "https://images.unsplash.com/photo-1609599006353-e629aaabfeae?auto=format&fit=crop&w=1200&q=80",
|
|
'meal': "https://images.unsplash.com/photo-1593113598332-cd288d649433?auto=format&fit=crop&w=1200&q=80",
|
|
'food': "https://images.unsplash.com/photo-1593113598332-cd288d649433?auto=format&fit=crop&w=1200&q=80",
|
|
'hamper': "https://images.unsplash.com/photo-1593113598332-cd288d649433?auto=format&fit=crop&w=1200&q=80",
|
|
'impact': "https://images.unsplash.com/photo-1593113598332-cd288d649433?auto=format&fit=crop&w=1200&q=80",
|
|
'charity': "https://images.unsplash.com/photo-1593113598332-cd288d649433?auto=format&fit=crop&w=1200&q=80",
|
|
}
|
|
|
|
posts_data = []
|
|
for post in qs:
|
|
image_url = None
|
|
|
|
# 1. Check if post has a direct internet URL stored in tags
|
|
if isinstance(post.tags, list):
|
|
for tag in post.tags:
|
|
if isinstance(tag, dict) and tag.get('image_url'):
|
|
image_url = tag['image_url']
|
|
break
|
|
elif isinstance(tag, str) and (tag.startswith('http://') or tag.startswith('https://')):
|
|
image_url = tag
|
|
break
|
|
|
|
# 2. Check keyword matching for relatable internet image
|
|
if not image_url:
|
|
title_lower = (post.title or '').lower()
|
|
cat_lower = (post.category.name if post.category else '').lower()
|
|
for kw, url in relatable_map.items():
|
|
if kw in title_lower or kw in cat_lower:
|
|
image_url = url
|
|
break
|
|
|
|
# 3. Fallback to featured_image with proper host (avoid Vite non-proxied media 404)
|
|
if not image_url and post.featured_image:
|
|
feat_str = str(post.featured_image)
|
|
if feat_str.startswith('http://') or feat_str.startswith('https://'):
|
|
image_url = feat_str
|
|
else:
|
|
if request and ':8080' not in request.get_host():
|
|
image_url = request.build_absolute_uri(post.featured_image.url)
|
|
else:
|
|
image_url = f"http://localhost:8000{post.featured_image.url}"
|
|
|
|
# 4. Final fallback
|
|
if not image_url:
|
|
image_url = "https://images.unsplash.com/photo-1519817650390-64a93db51149?auto=format&fit=crop&w=1200&q=80"
|
|
|
|
posts_data.append({
|
|
"id": post.id,
|
|
"title": post.title,
|
|
"category": post.category.name if post.category else post.get_post_type_display(),
|
|
"content": post.content,
|
|
"excerpt": post.excerpt or (post.content[:160] + "..." if post.content else ""),
|
|
"image": image_url,
|
|
"author": post.author.fullname or post.author.get_full_name() or post.author_role_label or "Editorial Team",
|
|
"publishDate": post.publish_date.strftime('%Y/%m/%d') if post.publish_date else None,
|
|
"readTime": f"{post.reading_time_minutes} min",
|
|
"likes": post.likes_count,
|
|
})
|
|
return posts_data
|
|
|
|
|
|
class InstitutionCreateUpdateSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = Institution
|
|
fields = [
|
|
'name',
|
|
'type',
|
|
'country',
|
|
'city',
|
|
'address',
|
|
'latitude',
|
|
'longitude',
|
|
'description',
|
|
'website',
|
|
'email',
|
|
'phone',
|
|
'established_year',
|
|
'cover_image',
|
|
'avatar',
|
|
'social_media',
|
|
'working_hours',
|
|
'tags',
|
|
'is_featured',
|
|
]
|
|
|
|
def to_internal_value(self, data):
|
|
if hasattr(data, 'copy'):
|
|
data = data.copy()
|
|
else:
|
|
data = dict(data)
|
|
|
|
# Normalize institution type if passed in Persian or other aliases
|
|
inst_type = data.get('type')
|
|
if inst_type:
|
|
type_map = {
|
|
'مؤسسه فرهنگی و مذهبی': 'cultural_center',
|
|
'مرکز فرهنگی': 'cultural_center',
|
|
'مسجد': 'mosque',
|
|
'حسینیه': 'hussainiya',
|
|
'حوزه علمیه': 'institute',
|
|
'مرکز آموزشی': 'institute',
|
|
'مرکز پژوهشی': 'institute',
|
|
'مرکز خیریه': 'charity',
|
|
'خیریه': 'charity',
|
|
'کتابخانه': 'library',
|
|
'سایر': 'other',
|
|
}
|
|
if inst_type in type_map:
|
|
data['type'] = type_map[inst_type]
|
|
|
|
# Normalize established / established_year
|
|
est = data.get('established') if data.get('established') is not None else data.get('established_year')
|
|
if est is not None and not data.get('established_year'):
|
|
try:
|
|
persian_digits = '۰۱۲۳۴۵۶۷۸۹'
|
|
arabic_digits = '٠١٢٣٤٥٦٧٨٩'
|
|
est_str = str(est).strip()
|
|
for i, d in enumerate(persian_digits):
|
|
est_str = est_str.replace(d, str(i))
|
|
for i, d in enumerate(arabic_digits):
|
|
est_str = est_str.replace(d, str(i))
|
|
if est_str:
|
|
data['established_year'] = int(est_str)
|
|
except (ValueError, TypeError):
|
|
data['established_year'] = None
|
|
|
|
# Normalize website: prepend https:// if missing scheme
|
|
website = data.get('website')
|
|
if website and isinstance(website, str) and website.strip():
|
|
website = website.strip()
|
|
if not (website.startswith('http://') or website.startswith('https://')):
|
|
data['website'] = f'https://{website}'
|
|
|
|
# Normalize camelCase socialMedia / workingHours if provided
|
|
if 'socialMedia' in data and not data.get('social_media'):
|
|
data['social_media'] = data.get('socialMedia') or {}
|
|
if 'workingHours' in data and not data.get('working_hours'):
|
|
data['working_hours'] = data.get('workingHours') or {}
|
|
|
|
# If cover_image or avatar is not an actual uploaded file (e.g. string path or URL), ignore it so ImageField doesn't fail
|
|
for img_field in ['cover_image', 'avatar']:
|
|
val = data.get(img_field)
|
|
if val is not None and not hasattr(val, 'read'):
|
|
data.pop(img_field, None)
|
|
|
|
return super().to_internal_value(data)
|
|
|
|
def create(self, validated_data):
|
|
user = self.context.get('request').user if self.context.get('request') else None
|
|
if user and user.is_authenticated:
|
|
validated_data['created_by'] = user
|
|
|
|
# Only super admins or regional admins can set is_featured directly upon creation
|
|
if user and not (getattr(user, 'is_super_admin', False) or getattr(user, 'is_regional_admin', False)):
|
|
validated_data['is_featured'] = False
|
|
|
|
institution = super().create(validated_data)
|
|
|
|
# Creator automatically becomes the Admin member
|
|
if user and user.is_authenticated:
|
|
InstitutionMember.objects.create(
|
|
institution=institution,
|
|
user=user,
|
|
role=InstitutionMember.MemberRole.ADMIN,
|
|
title="Founder / Primary Administrator"
|
|
)
|
|
|
|
return institution
|
|
|
|
|
|
class FollowResponseSerializer(serializers.Serializer):
|
|
is_following = serializers.BooleanField()
|
|
follower_count = serializers.IntegerField()
|
|
message = serializers.CharField()
|