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()