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.
322 lines
11 KiB
322 lines
11 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
|
|
|
|
|
|
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()
|
|
|
|
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)
|
|
|
|
|
|
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)
|
|
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)
|
|
|
|
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',
|
|
'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',
|
|
'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)
|
|
|
|
|
|
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()
|