feat(partner-portal): Sprint 2 — partner-me events CRUD endpoints

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 <noreply@anthropic.com>
This commit is contained in:
2026-04-22 11:20:37 +05:30
parent 761b702e57
commit 16c21c17d2
2 changed files with 184 additions and 0 deletions

View File

@@ -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/<int:pk>/', views.PartnerMeEventDetailView.as_view(), name='partner-me-event-detail'),
path('partners/me/events/<int:pk>/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/<int:pk>/', views.UserDetailView.as_view(), name='user-detail'),

View File

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