import logging from django.contrib.auth import get_user_model from django.db.models import Q, F from django.shortcuts import get_object_or_404 from django.utils.translation import gettext_lazy as _ from rest_framework import status from rest_framework.generics import GenericAPIView from rest_framework.parsers import MultiPartParser, FormParser, JSONParser from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiResponse, inline_serializer from apps.profiles.models.institution import ( Institution, InstitutionMember, InstitutionTimeline, InstitutionMedia, InstitutionFollower, ) from apps.profiles.permissions import ( IsInstitutionAdminOrReadOnly, IsInstitutionEditorOrReadOnly, CanManageInstitutionMembers, ) from apps.profiles.serializers import ( InstitutionListSerializer, InstitutionDetailSerializer, InstitutionCreateUpdateSerializer, InstitutionTimelineSerializer, InstitutionMediaSerializer, InstitutionMemberSerializer, InstitutionMemberAddSerializer, FollowResponseSerializer, ) from utils.pagination import StandardResultsSetPagination, InstitutionPagination logger = logging.getLogger(__name__) User = get_user_model() def get_institution_by_id_or_slug(lookup_val): if str(lookup_val).isdigit(): return get_object_or_404(Institution, id=int(lookup_val), is_active=True) return get_object_or_404(Institution, slug=lookup_val, is_active=True) class InstitutionListCreateView(GenericAPIView): serializer_class = InstitutionListSerializer pagination_class = InstitutionPagination def get_permissions(self): if self.request.method == 'POST': return [IsAuthenticated()] return [AllowAny()] def get_queryset(self): if getattr(self, 'swagger_fake_view', False): return Institution.objects.none() qs = Institution.objects.filter(is_active=True) # Filters search = self.request.query_params.get('search') if search: qs = qs.filter( Q(name__icontains=search) | Q(city__icontains=search) | Q(country__icontains=search) | Q(description__icontains=search) ) country = self.request.query_params.get('country') if country: qs = qs.filter(country__iexact=country) city = self.request.query_params.get('city') if city: qs = qs.filter(city__iexact=city) inst_type = self.request.query_params.get('type') if inst_type: qs = qs.filter(type=inst_type) tag = self.request.query_params.get('tag') if tag: qs = qs.filter(tags__icontains=tag) is_featured = self.request.query_params.get('is_featured') if is_featured is not None: qs = qs.filter(is_featured=is_featured.lower() in ['true', '1']) verification = self.request.query_params.get('verification_status') if verification: qs = qs.filter(verification_status=verification) return qs.order_by('-is_featured', '-created_at') @extend_schema( summary="List active institutions", description="Returns a paginated list of mosques, cultural centers, and institutes with filtering.", parameters=[ OpenApiParameter('search', str, description='Search query across name, city, country, description'), OpenApiParameter('country', str, description='Filter by country name'), OpenApiParameter('city', str, description='Filter by city name'), OpenApiParameter('type', str, description='Filter by institution type (mosque, cultural_center, etc.)'), OpenApiParameter('tag', str, description='Filter by tag string'), OpenApiParameter('is_featured', bool, description='Filter by featured status'), OpenApiParameter('verification_status', str, description='Filter by verification status (pending, approved, rejected)'), ], responses={200: InstitutionListSerializer(many=True)}, tags=["Institutions & Profiles"], ) def get(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) page = self.paginate_queryset(queryset) if page is not None: serializer = self.get_serializer(page, many=True, context={'request': request}) return self.get_paginated_response(serializer.data) serializer = self.get_serializer(queryset, many=True, context={'request': request}) return Response(serializer.data, status=status.HTTP_200_OK) @extend_schema( summary="Register a new institution", description="Registers a new institution profile and designates the creator as primary Administrator.", request=InstitutionCreateUpdateSerializer, responses={ 201: InstitutionDetailSerializer, 400: OpenApiResponse(description="Validation error"), 401: OpenApiResponse(description="Authentication required"), }, tags=["Institutions & Profiles"], ) def post(self, request, *args, **kwargs): serializer = InstitutionCreateUpdateSerializer(data=request.data, context={'request': request}) serializer.is_valid(raise_exception=True) institution = serializer.save() response_serializer = InstitutionDetailSerializer(institution, context={'request': request}) return Response(response_serializer.data, status=status.HTTP_201_CREATED) class InstitutionDetailView(GenericAPIView): serializer_class = InstitutionDetailSerializer queryset = Institution.objects.filter(is_active=True) def get_permissions(self): if self.request.method in ['PATCH', 'PUT', 'DELETE']: return [IsAuthenticated(), IsInstitutionEditorOrReadOnly()] return [AllowAny()] def get_object(self): lookup = self.kwargs.get('pk_or_slug') inst = get_institution_by_id_or_slug(lookup) self.check_object_permissions(self.request, inst) return inst @extend_schema( summary="Get full institution profile", description="Retrieves complete institution information, historical timeline milestones, and media gallery.", responses={ 200: InstitutionDetailSerializer, 404: OpenApiResponse(description="Institution not found"), }, tags=["Institutions & Profiles"], ) def get(self, request, pk_or_slug, *args, **kwargs): institution = self.get_object() serializer = InstitutionDetailSerializer(institution, context={'request': request}) return Response(serializer.data, status=status.HTTP_200_OK) @extend_schema( summary="Update institution profile", description="Updates details of an institution. Requires Admin or Editor role on the institution.", request=InstitutionCreateUpdateSerializer, responses={ 200: InstitutionDetailSerializer, 403: OpenApiResponse(description="Permission denied"), }, tags=["Institutions & Profiles"], ) def patch(self, request, pk_or_slug, *args, **kwargs): institution = self.get_object() serializer = InstitutionCreateUpdateSerializer( institution, data=request.data, partial=True, context={'request': request} ) serializer.is_valid(raise_exception=True) serializer.save() response_serializer = InstitutionDetailSerializer(institution, context={'request': request}) return Response(response_serializer.data, status=status.HTTP_200_OK) @extend_schema( summary="Deactivate / Delete institution", description="Deactivates an institution. Permitted for institution administrator or platform administrator.", responses={ 204: OpenApiResponse(description="Institution successfully deactivated"), 403: OpenApiResponse(description="Permission denied"), }, tags=["Institutions & Profiles"], ) def delete(self, request, pk_or_slug, *args, **kwargs): institution = self.get_object() # Enforce Admin role for deletion if not institution.is_admin(request.user): return Response({'error': _("Only institution administrators can delete this profile.")}, status=status.HTTP_403_FORBIDDEN) institution.is_active = False institution.save() return Response(status=status.HTTP_204_NO_CONTENT) class InstitutionTimelineView(GenericAPIView): serializer_class = InstitutionTimelineSerializer queryset = InstitutionTimeline.objects.all() def get_permissions(self): if self.request.method == 'POST': return [IsAuthenticated(), IsInstitutionEditorOrReadOnly()] return [AllowAny()] @extend_schema( summary="List timeline milestones", description="Returns historical milestone entries for an institution.", responses={200: InstitutionTimelineSerializer(many=True)}, tags=["Institutions & Profiles"], ) def get(self, request, pk_or_slug, *args, **kwargs): institution = get_institution_by_id_or_slug(pk_or_slug) entries = institution.timeline_entries.all().order_by('order', 'id') serializer = InstitutionTimelineSerializer(entries, many=True) return Response(serializer.data, status=status.HTTP_200_OK) @extend_schema( summary="Add timeline milestone", description="Creates a new milestone in the institution historical timeline.", request=InstitutionTimelineSerializer, responses={201: InstitutionTimelineSerializer}, tags=["Institutions & Profiles"], ) def post(self, request, pk_or_slug, *args, **kwargs): institution = get_institution_by_id_or_slug(pk_or_slug) self.check_object_permissions(request, institution) serializer = InstitutionTimelineSerializer(data=request.data) serializer.is_valid(raise_exception=True) timeline_entry = serializer.save(institution=institution) return Response(InstitutionTimelineSerializer(timeline_entry).data, status=status.HTTP_201_CREATED) class InstitutionTimelineDetailView(GenericAPIView): serializer_class = InstitutionTimelineSerializer permission_classes = [IsAuthenticated, IsInstitutionEditorOrReadOnly] queryset = InstitutionTimeline.objects.all() @extend_schema( summary="Delete timeline milestone", description="Removes a timeline entry from the institution profile.", responses={204: OpenApiResponse(description="Entry deleted")}, tags=["Institutions & Profiles"], ) def delete(self, request, pk_or_slug, entry_id, *args, **kwargs): institution = get_institution_by_id_or_slug(pk_or_slug) self.check_object_permissions(request, institution) entry = get_object_or_404(InstitutionTimeline, id=entry_id, institution=institution) entry.delete() return Response(status=status.HTTP_204_NO_CONTENT) class InstitutionMediaView(GenericAPIView): serializer_class = InstitutionMediaSerializer parser_classes = [MultiPartParser, FormParser, JSONParser] queryset = InstitutionMedia.objects.all() def get_permissions(self): if self.request.method == 'POST': return [IsAuthenticated(), IsInstitutionEditorOrReadOnly()] return [AllowAny()] @extend_schema( summary="List media gallery assets", description="Returns uploaded photos, videos, and documents for an institution.", responses={200: InstitutionMediaSerializer(many=True)}, tags=["Institutions & Profiles"], ) def get(self, request, pk_or_slug, *args, **kwargs): institution = get_institution_by_id_or_slug(pk_or_slug) media_items = institution.media_items.all().order_by('-uploaded_at') serializer = InstitutionMediaSerializer(media_items, many=True, context={'request': request}) return Response(serializer.data, status=status.HTTP_200_OK) @extend_schema( summary="Upload media to gallery", description="Uploads a photo, video, or publication to the institution media gallery.", request=InstitutionMediaSerializer, responses={201: InstitutionMediaSerializer}, tags=["Institutions & Profiles"], ) def post(self, request, pk_or_slug, *args, **kwargs): institution = get_institution_by_id_or_slug(pk_or_slug) self.check_object_permissions(request, institution) serializer = InstitutionMediaSerializer(data=request.data) serializer.is_valid(raise_exception=True) media_item = serializer.save(institution=institution) return Response(InstitutionMediaSerializer(media_item, context={'request': request}).data, status=status.HTTP_201_CREATED) class InstitutionMediaDetailView(GenericAPIView): serializer_class = InstitutionMediaSerializer permission_classes = [IsAuthenticated, IsInstitutionEditorOrReadOnly] queryset = InstitutionMedia.objects.all() @extend_schema( summary="Delete media item", description="Removes an asset from the institution media gallery.", responses={204: OpenApiResponse(description="Media deleted")}, tags=["Institutions & Profiles"], ) def delete(self, request, pk_or_slug, media_id, *args, **kwargs): institution = get_institution_by_id_or_slug(pk_or_slug) self.check_object_permissions(request, institution) item = get_object_or_404(InstitutionMedia, id=media_id, institution=institution) item.delete() return Response(status=status.HTTP_204_NO_CONTENT) class InstitutionFollowToggleView(GenericAPIView): serializer_class = FollowResponseSerializer permission_classes = [IsAuthenticated] queryset = InstitutionFollower.objects.all() @extend_schema( summary="Follow institution", description="Follows the specified institution to receive updates and news.", responses={200: FollowResponseSerializer}, tags=["Institutions & Profiles"], ) def post(self, request, pk_or_slug, *args, **kwargs): institution = get_institution_by_id_or_slug(pk_or_slug) follower, created = InstitutionFollower.objects.get_or_create( institution=institution, user=request.user ) if created: Institution.objects.filter(id=institution.id).update(follower_count=F('follower_count') + 1) institution.refresh_from_db() return Response({ 'is_following': True, 'follower_count': institution.follower_count, 'message': _("You are now following this institution.") }, status=status.HTTP_200_OK) @extend_schema( summary="Unfollow institution", description="Unfollows the specified institution.", responses={200: FollowResponseSerializer}, tags=["Institutions & Profiles"], ) def delete(self, request, pk_or_slug, *args, **kwargs): institution = get_institution_by_id_or_slug(pk_or_slug) deleted_count, deleted_dict = InstitutionFollower.objects.filter( institution=institution, user=request.user ).delete() if deleted_count > 0: Institution.objects.filter(id=institution.id, follower_count__gt=0).update(follower_count=F('follower_count') - 1) institution.refresh_from_db() return Response({ 'is_following': False, 'follower_count': institution.follower_count, 'message': _("You have unfollowed this institution.") }, status=status.HTTP_200_OK) class InstitutionMembersView(GenericAPIView): serializer_class = InstitutionMemberSerializer queryset = InstitutionMember.objects.all() def get_permissions(self): if self.request.method == 'POST': return [IsAuthenticated(), CanManageInstitutionMembers()] return [AllowAny()] @extend_schema( summary="List institution team members", description="Retrieves administrative and editorial team members of the institution.", responses={200: InstitutionMemberSerializer(many=True)}, tags=["Institutions & Profiles"], ) def get(self, request, pk_or_slug, *args, **kwargs): institution = get_institution_by_id_or_slug(pk_or_slug) members = institution.members.all().select_related('user') serializer = InstitutionMemberSerializer(members, many=True) return Response(serializer.data, status=status.HTTP_200_OK) @extend_schema( summary="Delegate role / Add team member", description="Adds or updates a member role (admin, editor, viewer) for an existing registered user by email.", request=InstitutionMemberAddSerializer, responses={ 201: InstitutionMemberSerializer, 400: OpenApiResponse(description="User not found or invalid payload"), 403: OpenApiResponse(description="Only institution administrators can delegate roles"), }, tags=["Institutions & Profiles"], ) def post(self, request, pk_or_slug, *args, **kwargs): institution = get_institution_by_id_or_slug(pk_or_slug) self.check_object_permissions(request, institution) serializer = InstitutionMemberAddSerializer(data=request.data) serializer.is_valid(raise_exception=True) user = User.objects.get(email__iexact=serializer.validated_data['email']) role = serializer.validated_data['role'] title = serializer.validated_data.get('title', '') member, created = InstitutionMember.objects.update_or_create( institution=institution, user=user, defaults={ 'role': role, 'title': title } ) response_status = status.HTTP_201_CREATED if created else status.HTTP_200_OK return Response(InstitutionMemberSerializer(member).data, status=response_status) class InstitutionMemberDetailView(GenericAPIView): serializer_class = InstitutionMemberSerializer permission_classes = [IsAuthenticated, CanManageInstitutionMembers] queryset = InstitutionMember.objects.all() @extend_schema( summary="Remove team member", description="Revokes institution membership and delegated role for a user.", responses={204: OpenApiResponse(description="Member removed")}, tags=["Institutions & Profiles"], ) def delete(self, request, pk_or_slug, member_id, *args, **kwargs): institution = get_institution_by_id_or_slug(pk_or_slug) self.check_object_permissions(request, institution) member = get_object_or_404(InstitutionMember, id=member_id, institution=institution) # Prevent removing the sole administrator if member.role == InstitutionMember.MemberRole.ADMIN and institution.members.filter(role=InstitutionMember.MemberRole.ADMIN).count() <= 1: return Response({'error': _("Cannot remove the sole administrator of an institution.")}, status=status.HTTP_400_BAD_REQUEST) member.delete() return Response(status=status.HTTP_204_NO_CONTENT)