From 16c21c17d2e6b4e4262adc2e2a21f86ce308929f Mon Sep 17 00:00:00 2001 From: Sicherhaven Date: Wed, 22 Apr 2026 11:20:37 +0530 Subject: [PATCH] =?UTF-8?q?feat(partner-portal):=20Sprint=202=20=E2=80=94?= =?UTF-8?q?=20partner-me=20events=20CRUD=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add partner-scoped event endpoints under /api/v1/partners/me/events/: - GET/POST /partners/me/events/ → list + create - GET/PATCH/DELETE /partners/me/events/{pk}/ → detail + update + delete - POST /partners/me/events/{pk}/duplicate/ → clone as draft All endpoints enforce partner ownership via _require_owned_event(). Create auto-sets partner FK + source='partner'. Duplicate always resets status to 'created' (draft). Co-Authored-By: Claude Sonnet 4.6 --- admin_api/urls.py | 4 + admin_api/views.py | 180 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) diff --git a/admin_api/urls.py b/admin_api/urls.py index e9934d0..eea7a92 100644 --- a/admin_api/urls.py +++ b/admin_api/urls.py @@ -26,6 +26,10 @@ urlpatterns = [ path('partners/me/notifications/', views.PartnerMeNotificationsView.as_view(), name='partner-me-notifications'), path('partners/me/payout/', views.PartnerMePayoutView.as_view(), name='partner-me-payout'), path('partners/me/change-password/', views.PartnerMeChangePasswordView.as_view(), name='partner-me-change-password'), + # Partner-Me: events (Sprint 2) + path('partners/me/events/', views.PartnerMeEventsView.as_view(), name='partner-me-events'), + path('partners/me/events//', views.PartnerMeEventDetailView.as_view(), name='partner-me-event-detail'), + path('partners/me/events//duplicate/', views.PartnerMeEventDuplicateView.as_view(), name='partner-me-event-duplicate'), path('users/metrics/', views.UserMetricsView.as_view(), name='user-metrics'), path('users/', views.UserListView.as_view(), name='user-list'), path('users//', views.UserDetailView.as_view(), name='user-detail'), diff --git a/admin_api/views.py b/admin_api/views.py index df3f78f..bf481fd 100644 --- a/admin_api/views.py +++ b/admin_api/views.py @@ -3539,3 +3539,183 @@ class PartnerMeChangePasswordView(APIView): request.user.set_password(new_password) request.user.save(update_fields=['password']) return Response({'success': True}) + + +# =========================================================================== +# Partner-Me Events (Sprint 2) +# =========================================================================== + +def _require_owned_event(request, pk): + """Return (event, None) or (None, error_response). Validates partner ownership.""" + from events.models import Event + from django.shortcuts import get_object_or_404 + partner, err = _require_partner(request) + if err: + return None, err + e = get_object_or_404( + Event.objects.select_related('partner', 'event_type').prefetch_related('eventimages_set'), + pk=pk, + ) + if e.partner_id != partner.id: + return None, Response({'error': 'Event not found or access denied.'}, status=404) + return e, None + + +class PartnerMeEventsView(APIView): + """ + GET /api/v1/partners/me/events/ — list partner's own events + POST /api/v1/partners/me/events/ — create event for this partner + """ + permission_classes = [IsAuthenticated] + + def get(self, request): + from events.models import Event + from django.db.models import Q + + partner, err = _require_partner(request) + if err: + return err + + qs = Event.objects.filter(partner=partner).select_related('event_type') + + if s := request.GET.get('status'): + reverse_map = {v: k for k, v in _EVENT_STATUS_MAP.items()} + qs = qs.filter(event_status=reverse_map.get(s, s)) + if q := request.GET.get('search'): + qs = qs.filter(Q(title__icontains=q) | Q(name__icontains=q) | Q(venue_name__icontains=q)) + + try: + page = max(1, int(request.GET.get('page', 1))) + page_size = min(100, int(request.GET.get('page_size', 20))) + except (ValueError, TypeError): + page, page_size = 1, 20 + + total = qs.count() + events = qs.order_by('-id')[(page - 1) * page_size: page * page_size] + return Response({'count': total, 'results': [_serialize_event(e) for e in events]}) + + def post(self, request): + from events.models import Event, EventType + + partner, err = _require_partner(request) + if err: + return err + + data = request.data + title = (data.get('title') or '').strip() + if not title: + return Response({'error': 'title is required'}, status=400) + + event_type = None + if eid := data.get('eventType'): + try: + event_type = EventType.objects.get(id=eid) + except EventType.DoesNotExist: + return Response({'error': 'Invalid event type'}, status=400) + + status_in = data.get('status', 'draft') + backend_status = {'draft': 'created', 'published': 'published'}.get(status_in, 'created') + + event = Event( + title=title, + name=data.get('name') or title, + description=data.get('description', ''), + event_type=event_type, + event_status=backend_status, + venue_name=data.get('venueName', ''), + place=data.get('place', ''), + is_bookable=True, + source='partner', + partner=partner, + ) + if data.get('startDate'): + event.start_date = data['startDate'] + if data.get('endDate'): + event.end_date = data['endDate'] + if data.get('startTime'): + event.start_time = data['startTime'] + if data.get('endTime'): + event.end_time = data['endTime'] + + event.save() + _audit_log(request, 'event.created', 'event', event.id, { + 'title': event.title, 'partner_id': str(partner.id), 'source': 'partner', + }) + return Response(_serialize_event_detail(event), status=201) + + +class PartnerMeEventDetailView(APIView): + """ + GET /api/v1/partners/me/events/{pk}/ — detail + PATCH /api/v1/partners/me/events/{pk}/ — update + DELETE /api/v1/partners/me/events/{pk}/ — delete + """ + permission_classes = [IsAuthenticated] + + def get(self, request, pk): + e, err = _require_owned_event(request, pk) + if err: + return err + return Response(_serialize_event_detail(e)) + + def patch(self, request, pk): + from events.models import Event + e, err = _require_owned_event(request, pk) + if err: + return err + + data = request.data + field_map = { + 'title': 'title', 'name': 'name', 'description': 'description', + 'venueName': 'venue_name', 'place': 'place', + 'district': 'district', 'state': 'state', 'pincode': 'pincode', + } + updated = [] + for api_key, model_field in field_map.items(): + if api_key in data: + setattr(e, model_field, data[api_key] or '') + updated.append(model_field) + + if 'status' in data: + e.event_status = {'draft': 'created', 'published': 'published'}.get( + data['status'], data['status'] + ) + updated.append('event_status') + + for src_key, model_field in [ + ('startDate', 'start_date'), ('endDate', 'end_date'), + ('startTime', 'start_time'), ('endTime', 'end_time'), + ]: + if src_key in data: + setattr(e, model_field, data[src_key] or None) + updated.append(model_field) + + if updated: + e.save(update_fields=updated) + + e = Event.objects.select_related('partner', 'event_type').prefetch_related('eventimages_set').get(pk=pk) + return Response(_serialize_event_detail(e)) + + def delete(self, request, pk): + e, err = _require_owned_event(request, pk) + if err: + return err + e.delete() + return Response({'status': 'deleted'}, status=204) + + +class PartnerMeEventDuplicateView(APIView): + """POST /api/v1/partners/me/events/{pk}/duplicate/""" + permission_classes = [IsAuthenticated] + + def post(self, request, pk): + e, err = _require_owned_event(request, pk) + if err: + return err + # Duplicate by clearing PK + e.pk = None + e.title = f"{e.title} (Copy)" + e.name = f"{e.name} (Copy)" + e.event_status = 'created' # always draft + e.save() + return Response(_serialize_event_detail(e), status=201)