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:
@@ -26,6 +26,10 @@ urlpatterns = [
|
|||||||
path('partners/me/notifications/', views.PartnerMeNotificationsView.as_view(), name='partner-me-notifications'),
|
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/payout/', views.PartnerMePayoutView.as_view(), name='partner-me-payout'),
|
||||||
path('partners/me/change-password/', views.PartnerMeChangePasswordView.as_view(), name='partner-me-change-password'),
|
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/metrics/', views.UserMetricsView.as_view(), name='user-metrics'),
|
||||||
path('users/', views.UserListView.as_view(), name='user-list'),
|
path('users/', views.UserListView.as_view(), name='user-list'),
|
||||||
path('users/<int:pk>/', views.UserDetailView.as_view(), name='user-detail'),
|
path('users/<int:pk>/', views.UserDetailView.as_view(), name='user-detail'),
|
||||||
|
|||||||
@@ -3539,3 +3539,183 @@ class PartnerMeChangePasswordView(APIView):
|
|||||||
request.user.set_password(new_password)
|
request.user.set_password(new_password)
|
||||||
request.user.save(update_fields=['password'])
|
request.user.save(update_fields=['password'])
|
||||||
return Response({'success': True})
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user