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

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
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 = StandardResultsSetPagination
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)