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