feat: add source field with 3 options, fix EventListAPI fallback, add is_eventify_event to API response
- Event.source field updated: eventify, community, partner (radio select in form) - EventListAPI: fallback to all events when pincode returns < 6 - EventListAPI: include is_eventify_event and source in serializer - Admin API: add source to list serializer - Django admin: source in list_display, list_filter, list_editable - Event form template: proper radio button rendering for source field Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@ from rest_framework.permissions import AllowAny, IsAuthenticated
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework_simplejwt.tokens import RefreshToken
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
from rest_framework_simplejwt.views import TokenRefreshView
|
from rest_framework_simplejwt.views import TokenRefreshView
|
||||||
from django.db import connection
|
from django.db import connection, transaction
|
||||||
from .serializers import UserSerializer
|
from .serializers import UserSerializer
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
@@ -30,16 +30,44 @@ class AdminLoginView(APIView):
|
|||||||
if not user.is_active:
|
if not user.is_active:
|
||||||
return Response({'error': 'Account is disabled'}, status=status.HTTP_403_FORBIDDEN)
|
return Response({'error': 'Account is disabled'}, status=status.HTTP_403_FORBIDDEN)
|
||||||
refresh = RefreshToken.for_user(user)
|
refresh = RefreshToken.for_user(user)
|
||||||
|
user_data = UserSerializer(user).data
|
||||||
|
# RBAC: prefer StaffProfile for allowed_modules and scopes
|
||||||
|
try:
|
||||||
|
sp = user.staff_profile
|
||||||
|
allowed_modules = sp.get_allowed_modules()
|
||||||
|
effective_scopes = sp.get_effective_scopes()
|
||||||
|
user_data['staff_role'] = sp.staff_role
|
||||||
|
user_data['department'] = sp.department.name if sp.department else None
|
||||||
|
user_data['squad'] = sp.squad.name if sp.squad else None
|
||||||
|
except Exception:
|
||||||
|
allowed_modules = user.get_allowed_modules()
|
||||||
|
effective_scopes = []
|
||||||
|
user_data['allowed_modules'] = allowed_modules
|
||||||
|
user_data['effective_scopes'] = effective_scopes
|
||||||
return Response({
|
return Response({
|
||||||
'access': str(refresh.access_token),
|
'access': str(refresh.access_token),
|
||||||
'refresh': str(refresh),
|
'refresh': str(refresh),
|
||||||
'user': UserSerializer(user).data,
|
'user': user_data,
|
||||||
})
|
})
|
||||||
|
|
||||||
class MeView(APIView):
|
class MeView(APIView):
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return Response({'user': UserSerializer(request.user).data})
|
me_data = UserSerializer(request.user).data
|
||||||
|
# RBAC: prefer StaffProfile for allowed_modules and scopes
|
||||||
|
try:
|
||||||
|
sp = request.user.staff_profile
|
||||||
|
allowed_modules = sp.get_allowed_modules()
|
||||||
|
effective_scopes = sp.get_effective_scopes()
|
||||||
|
me_data['staff_role'] = sp.staff_role
|
||||||
|
me_data['department'] = sp.department.name if sp.department else None
|
||||||
|
me_data['squad'] = sp.squad.name if sp.squad else None
|
||||||
|
except Exception:
|
||||||
|
allowed_modules = request.user.get_allowed_modules()
|
||||||
|
effective_scopes = []
|
||||||
|
me_data['allowed_modules'] = allowed_modules
|
||||||
|
me_data['effective_scopes'] = effective_scopes
|
||||||
|
return Response({'user': me_data})
|
||||||
|
|
||||||
class HealthView(APIView):
|
class HealthView(APIView):
|
||||||
permission_classes = [AllowAny]
|
permission_classes = [AllowAny]
|
||||||
@@ -653,6 +681,7 @@ def _serialize_event(e):
|
|||||||
'createdAt': e.created_date.isoformat() if e.created_date else '',
|
'createdAt': e.created_date.isoformat() if e.created_date else '',
|
||||||
'isFeatured': bool(e.is_featured),
|
'isFeatured': bool(e.is_featured),
|
||||||
'isTopEvent': bool(e.is_top_event),
|
'isTopEvent': bool(e.is_top_event),
|
||||||
|
'source': e.source or 'eventify',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1289,3 +1318,923 @@ class EventPrimaryImageView(APIView):
|
|||||||
img.save()
|
img.save()
|
||||||
|
|
||||||
return Response({"success": True, "primaryImageId": image_id})
|
return Response({"success": True, "primaryImageId": image_id})
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# RBAC Views
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
from admin_api.models import Department, Squad, StaffProfile, CustomRole, AuditLog
|
||||||
|
from django.utils.text import slugify
|
||||||
|
from django.db.models import Q
|
||||||
|
import json
|
||||||
|
|
||||||
|
SCOPE_DEFINITIONS = {
|
||||||
|
'users.read': {'label': 'View Users', 'category': 'Users'},
|
||||||
|
'users.write': {'label': 'Edit Users', 'category': 'Users'},
|
||||||
|
'users.delete': {'label': 'Delete Users', 'category': 'Users'},
|
||||||
|
'users.ban': {'label': 'Ban/Suspend Users', 'category': 'Users'},
|
||||||
|
'events.read': {'label': 'View Events', 'category': 'Events'},
|
||||||
|
'events.write': {'label': 'Create/Edit Events', 'category': 'Events'},
|
||||||
|
'events.approve': {'label': 'Approve Events', 'category': 'Events'},
|
||||||
|
'events.delete': {'label': 'Delete Events', 'category': 'Events'},
|
||||||
|
'finance.read': {'label': 'View Finance Dashboard', 'category': 'Finance'},
|
||||||
|
'finance.refunds.read': {'label': 'View Refund Requests', 'category': 'Finance'},
|
||||||
|
'finance.refunds.execute': {'label': 'Process Refunds', 'category': 'Finance'},
|
||||||
|
'finance.payouts.read': {'label': 'View Payouts', 'category': 'Finance'},
|
||||||
|
'finance.payouts.execute': {'label': 'Execute Payouts', 'category': 'Finance'},
|
||||||
|
'partners.read': {'label': 'View Partners', 'category': 'Partners'},
|
||||||
|
'partners.write': {'label': 'Edit Partners', 'category': 'Partners'},
|
||||||
|
'partners.kyc': {'label': 'Verify Partner KYC', 'category': 'Partners'},
|
||||||
|
'partners.impersonate': {'label': 'Login as Partner', 'category': 'Partners'},
|
||||||
|
'partners.events.review': {'label': 'Review Partner Events', 'category': 'Partners'},
|
||||||
|
'partners.suspend': {'label': 'Suspend/Unsuspend Partners', 'category': 'Partners'},
|
||||||
|
'tickets.read': {'label': 'View Tickets', 'category': 'Support'},
|
||||||
|
'tickets.write': {'label': 'Respond to Tickets', 'category': 'Support'},
|
||||||
|
'tickets.assign': {'label': 'Assign Tickets', 'category': 'Support'},
|
||||||
|
'tickets.escalate': {'label': 'Escalate Tickets', 'category': 'Support'},
|
||||||
|
'settings.read': {'label': 'View Settings', 'category': 'Settings'},
|
||||||
|
'settings.write': {'label': 'Modify Settings', 'category': 'Settings'},
|
||||||
|
'settings.staff': {'label': 'Manage Staff', 'category': 'Settings'},
|
||||||
|
'ads.read': {'label': 'View Ad Campaigns', 'category': 'Ad Control'},
|
||||||
|
'ads.write': {'label': 'Create/Edit Campaigns', 'category': 'Ad Control'},
|
||||||
|
'ads.approve': {'label': 'Approve Campaigns', 'category': 'Ad Control'},
|
||||||
|
'ads.report': {'label': 'View Ad Reports', 'category': 'Ad Control'},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _audit_log(request, action, target_type, target_id, details=None):
|
||||||
|
AuditLog.objects.create(
|
||||||
|
user=request.user if request.user.is_authenticated else None,
|
||||||
|
action=action,
|
||||||
|
target_type=target_type,
|
||||||
|
target_id=str(target_id),
|
||||||
|
details=details or {},
|
||||||
|
ip_address=request.META.get('REMOTE_ADDR'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_department(dept):
|
||||||
|
return {
|
||||||
|
'id': dept.id,
|
||||||
|
'name': dept.name,
|
||||||
|
'slug': dept.slug,
|
||||||
|
'description': dept.description,
|
||||||
|
'base_scopes': dept.base_scopes,
|
||||||
|
'color': dept.color,
|
||||||
|
'squad_count': dept.squads.count(),
|
||||||
|
'member_count': dept.staff_members.count(),
|
||||||
|
'created_at': dept.created_at.isoformat() if dept.created_at else None,
|
||||||
|
'updated_at': dept.updated_at.isoformat() if dept.updated_at else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_squad(squad):
|
||||||
|
return {
|
||||||
|
'id': squad.id,
|
||||||
|
'name': squad.name,
|
||||||
|
'department_id': squad.department_id,
|
||||||
|
'department_name': squad.department.name,
|
||||||
|
'manager': {
|
||||||
|
'id': squad.manager.id,
|
||||||
|
'username': squad.manager.username,
|
||||||
|
'first_name': squad.manager.first_name,
|
||||||
|
'last_name': squad.manager.last_name,
|
||||||
|
} if squad.manager else None,
|
||||||
|
'extra_scopes': squad.extra_scopes,
|
||||||
|
'member_count': squad.members.count(),
|
||||||
|
'created_at': squad.created_at.isoformat() if squad.created_at else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_staff(sp):
|
||||||
|
u = sp.user
|
||||||
|
return {
|
||||||
|
'id': sp.id,
|
||||||
|
'user_id': u.id,
|
||||||
|
'username': u.username,
|
||||||
|
'email': u.email,
|
||||||
|
'name': (u.first_name + ' ' + u.last_name).strip() or u.username,
|
||||||
|
'first_name': u.first_name,
|
||||||
|
'last_name': u.last_name,
|
||||||
|
'phone_number': u.phone_number,
|
||||||
|
'profile_picture': u.profile_picture.url if u.profile_picture else None,
|
||||||
|
'staff_role': sp.staff_role,
|
||||||
|
'status': sp.status,
|
||||||
|
'department': {
|
||||||
|
'id': sp.department.id,
|
||||||
|
'name': sp.department.name,
|
||||||
|
'slug': sp.department.slug,
|
||||||
|
'color': sp.department.color,
|
||||||
|
} if sp.department else None,
|
||||||
|
'squad': {
|
||||||
|
'id': sp.squad.id,
|
||||||
|
'name': sp.squad.name,
|
||||||
|
} if sp.squad else None,
|
||||||
|
'effective_scopes': sp.get_effective_scopes(),
|
||||||
|
'allowed_modules': sp.get_allowed_modules(),
|
||||||
|
'joined_at': sp.joined_at.isoformat() if sp.joined_at else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# 1. DepartmentListCreateView
|
||||||
|
class DepartmentListCreateView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
depts = Department.objects.all()
|
||||||
|
return Response([_serialize_department(d) for d in depts])
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
data = request.data
|
||||||
|
name = data.get('name', '').strip()
|
||||||
|
if not name:
|
||||||
|
return Response({'error': 'name is required'}, status=400)
|
||||||
|
slug = data.get('slug') or slugify(name)
|
||||||
|
if Department.objects.filter(slug=slug).exists():
|
||||||
|
return Response({'error': 'Department with this slug already exists'}, status=400)
|
||||||
|
dept = Department.objects.create(
|
||||||
|
name=name,
|
||||||
|
slug=slug,
|
||||||
|
description=data.get('description', ''),
|
||||||
|
base_scopes=data.get('base_scopes', []),
|
||||||
|
color=data.get('color', '#3B82F6'),
|
||||||
|
)
|
||||||
|
_audit_log(request, 'department.created', 'Department', dept.id, {'name': name})
|
||||||
|
return Response(_serialize_department(dept), status=201)
|
||||||
|
|
||||||
|
|
||||||
|
# 2. DepartmentDetailView
|
||||||
|
class DepartmentDetailView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def patch(self, request, pk):
|
||||||
|
try:
|
||||||
|
dept = Department.objects.get(pk=pk)
|
||||||
|
except Department.DoesNotExist:
|
||||||
|
return Response({'error': 'Department not found'}, status=404)
|
||||||
|
data = request.data
|
||||||
|
if 'name' in data:
|
||||||
|
dept.name = data['name']
|
||||||
|
if 'description' in data:
|
||||||
|
dept.description = data['description']
|
||||||
|
if 'base_scopes' in data:
|
||||||
|
dept.base_scopes = data['base_scopes']
|
||||||
|
if 'color' in data:
|
||||||
|
dept.color = data['color']
|
||||||
|
dept.save()
|
||||||
|
_audit_log(request, 'department.updated', 'Department', dept.id, data)
|
||||||
|
return Response(_serialize_department(dept))
|
||||||
|
|
||||||
|
def delete(self, request, pk):
|
||||||
|
try:
|
||||||
|
dept = Department.objects.get(pk=pk)
|
||||||
|
except Department.DoesNotExist:
|
||||||
|
return Response({'error': 'Department not found'}, status=404)
|
||||||
|
if dept.squads.exists():
|
||||||
|
return Response({'error': 'Cannot delete department that has squads. Remove squads first.'}, status=400)
|
||||||
|
dept_name = dept.name
|
||||||
|
dept.delete()
|
||||||
|
_audit_log(request, 'department.deleted', 'Department', pk, {'name': dept_name})
|
||||||
|
return Response({'success': True}, status=200)
|
||||||
|
|
||||||
|
|
||||||
|
# 3. SquadListCreateView
|
||||||
|
class SquadListCreateView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
qs = Squad.objects.select_related('department', 'manager').all()
|
||||||
|
dept_id = request.query_params.get('department')
|
||||||
|
if dept_id:
|
||||||
|
qs = qs.filter(department_id=dept_id)
|
||||||
|
return Response([_serialize_squad(s) for s in qs])
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
data = request.data
|
||||||
|
name = data.get('name', '').strip()
|
||||||
|
department_id = data.get('department_id')
|
||||||
|
if not name or not department_id:
|
||||||
|
return Response({'error': 'name and department_id are required'}, status=400)
|
||||||
|
try:
|
||||||
|
dept = Department.objects.get(pk=department_id)
|
||||||
|
except Department.DoesNotExist:
|
||||||
|
return Response({'error': 'Department not found'}, status=404)
|
||||||
|
manager_id = data.get('manager_id')
|
||||||
|
manager = None
|
||||||
|
if manager_id:
|
||||||
|
try:
|
||||||
|
manager = User.objects.get(pk=manager_id)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
pass
|
||||||
|
squad = Squad.objects.create(
|
||||||
|
name=name,
|
||||||
|
department=dept,
|
||||||
|
manager=manager,
|
||||||
|
extra_scopes=data.get('extra_scopes', []),
|
||||||
|
)
|
||||||
|
_audit_log(request, 'squad.created', 'Squad', squad.id, {'name': name, 'department': dept.name})
|
||||||
|
return Response(_serialize_squad(squad), status=201)
|
||||||
|
|
||||||
|
|
||||||
|
# 4. SquadDetailView
|
||||||
|
class SquadDetailView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def patch(self, request, pk):
|
||||||
|
try:
|
||||||
|
squad = Squad.objects.select_related('department', 'manager').get(pk=pk)
|
||||||
|
except Squad.DoesNotExist:
|
||||||
|
return Response({'error': 'Squad not found'}, status=404)
|
||||||
|
data = request.data
|
||||||
|
if 'name' in data:
|
||||||
|
squad.name = data['name']
|
||||||
|
if 'extra_scopes' in data:
|
||||||
|
squad.extra_scopes = data['extra_scopes']
|
||||||
|
if 'manager_id' in data:
|
||||||
|
if data['manager_id']:
|
||||||
|
try:
|
||||||
|
squad.manager = User.objects.get(pk=data['manager_id'])
|
||||||
|
except User.DoesNotExist:
|
||||||
|
return Response({'error': 'Manager user not found'}, status=404)
|
||||||
|
else:
|
||||||
|
squad.manager = None
|
||||||
|
squad.save()
|
||||||
|
_audit_log(request, 'squad.updated', 'Squad', squad.id, data)
|
||||||
|
return Response(_serialize_squad(squad))
|
||||||
|
|
||||||
|
def delete(self, request, pk):
|
||||||
|
try:
|
||||||
|
squad = Squad.objects.get(pk=pk)
|
||||||
|
except Squad.DoesNotExist:
|
||||||
|
return Response({'error': 'Squad not found'}, status=404)
|
||||||
|
if squad.members.exists():
|
||||||
|
return Response({'error': 'Cannot delete squad that has members. Move members first.'}, status=400)
|
||||||
|
squad_name = squad.name
|
||||||
|
squad.delete()
|
||||||
|
_audit_log(request, 'squad.deleted', 'Squad', pk, {'name': squad_name})
|
||||||
|
return Response({'success': True}, status=200)
|
||||||
|
|
||||||
|
|
||||||
|
# 5. StaffListView
|
||||||
|
class StaffListView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
qs = StaffProfile.objects.select_related('user', 'department', 'squad').all()
|
||||||
|
# Filters
|
||||||
|
dept = request.query_params.get('department')
|
||||||
|
if dept:
|
||||||
|
qs = qs.filter(department_id=dept)
|
||||||
|
squad_id = request.query_params.get('squad')
|
||||||
|
if squad_id:
|
||||||
|
qs = qs.filter(squad_id=squad_id)
|
||||||
|
role = request.query_params.get('role')
|
||||||
|
if role:
|
||||||
|
qs = qs.filter(staff_role=role)
|
||||||
|
status_filter = request.query_params.get('status')
|
||||||
|
if status_filter:
|
||||||
|
qs = qs.filter(status=status_filter)
|
||||||
|
search = request.query_params.get('search')
|
||||||
|
if search:
|
||||||
|
qs = qs.filter(
|
||||||
|
Q(user__username__icontains=search) |
|
||||||
|
Q(user__email__icontains=search) |
|
||||||
|
Q(user__first_name__icontains=search) |
|
||||||
|
Q(user__last_name__icontains=search)
|
||||||
|
)
|
||||||
|
return Response([_serialize_staff(sp) for sp in qs])
|
||||||
|
|
||||||
|
|
||||||
|
# 6. StaffInviteView
|
||||||
|
class StaffInviteView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
data = request.data
|
||||||
|
email = data.get('email', '').strip()
|
||||||
|
if not email:
|
||||||
|
return Response({'error': 'email is required'}, status=400)
|
||||||
|
if User.objects.filter(email=email).exists():
|
||||||
|
return Response({'error': 'User with this email already exists'}, status=400)
|
||||||
|
|
||||||
|
username = data.get('username') or email.split('@')[0]
|
||||||
|
first_name = data.get('first_name', '')
|
||||||
|
last_name = data.get('last_name', '')
|
||||||
|
|
||||||
|
user = User.objects.create_user(
|
||||||
|
username=username,
|
||||||
|
email=email,
|
||||||
|
first_name=first_name,
|
||||||
|
last_name=last_name,
|
||||||
|
password=data.get('password', 'TempPass123!'),
|
||||||
|
is_customer=False,
|
||||||
|
role='staff',
|
||||||
|
)
|
||||||
|
|
||||||
|
department = None
|
||||||
|
if data.get('department_id'):
|
||||||
|
try:
|
||||||
|
department = Department.objects.get(pk=data['department_id'])
|
||||||
|
except Department.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
squad = None
|
||||||
|
if data.get('squad_id'):
|
||||||
|
try:
|
||||||
|
squad = Squad.objects.get(pk=data['squad_id'])
|
||||||
|
except Squad.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
sp = StaffProfile.objects.create(
|
||||||
|
user=user,
|
||||||
|
staff_role=data.get('staff_role', 'MEMBER'),
|
||||||
|
department=department,
|
||||||
|
squad=squad,
|
||||||
|
status='invited',
|
||||||
|
)
|
||||||
|
_audit_log(request, 'staff.invited', 'StaffProfile', sp.id, {
|
||||||
|
'email': email, 'staff_role': sp.staff_role,
|
||||||
|
})
|
||||||
|
return Response(_serialize_staff(sp), status=201)
|
||||||
|
|
||||||
|
|
||||||
|
# 7. StaffUpdateView
|
||||||
|
class StaffUpdateView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def patch(self, request, pk):
|
||||||
|
try:
|
||||||
|
sp = StaffProfile.objects.select_related('user', 'department', 'squad').get(pk=pk)
|
||||||
|
except StaffProfile.DoesNotExist:
|
||||||
|
return Response({'error': 'Staff profile not found'}, status=404)
|
||||||
|
data = request.data
|
||||||
|
if 'staff_role' in data:
|
||||||
|
sp.staff_role = data['staff_role']
|
||||||
|
if 'department_id' in data:
|
||||||
|
if data['department_id']:
|
||||||
|
try:
|
||||||
|
sp.department = Department.objects.get(pk=data['department_id'])
|
||||||
|
except Department.DoesNotExist:
|
||||||
|
return Response({'error': 'Department not found'}, status=404)
|
||||||
|
else:
|
||||||
|
sp.department = None
|
||||||
|
if 'squad_id' in data:
|
||||||
|
if data['squad_id']:
|
||||||
|
try:
|
||||||
|
sp.squad = Squad.objects.get(pk=data['squad_id'])
|
||||||
|
except Squad.DoesNotExist:
|
||||||
|
return Response({'error': 'Squad not found'}, status=404)
|
||||||
|
else:
|
||||||
|
sp.squad = None
|
||||||
|
sp.save()
|
||||||
|
_audit_log(request, 'staff.updated', 'StaffProfile', sp.id, data)
|
||||||
|
return Response(_serialize_staff(sp))
|
||||||
|
|
||||||
|
|
||||||
|
# 8. StaffDeactivateView
|
||||||
|
class StaffDeactivateView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def patch(self, request, pk):
|
||||||
|
try:
|
||||||
|
sp = StaffProfile.objects.select_related('user', 'department', 'squad').get(pk=pk)
|
||||||
|
except StaffProfile.DoesNotExist:
|
||||||
|
return Response({'error': 'Staff profile not found'}, status=404)
|
||||||
|
sp.status = 'deactivated'
|
||||||
|
sp.save()
|
||||||
|
_audit_log(request, 'staff.deactivated', 'StaffProfile', sp.id, {
|
||||||
|
'username': sp.user.username,
|
||||||
|
})
|
||||||
|
return Response(_serialize_staff(sp))
|
||||||
|
|
||||||
|
|
||||||
|
# 9. StaffMoveView
|
||||||
|
class StaffMoveView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def patch(self, request, pk):
|
||||||
|
try:
|
||||||
|
sp = StaffProfile.objects.select_related('user', 'department', 'squad').get(pk=pk)
|
||||||
|
except StaffProfile.DoesNotExist:
|
||||||
|
return Response({'error': 'Staff profile not found'}, status=404)
|
||||||
|
data = request.data
|
||||||
|
old_dept = sp.department.name if sp.department else None
|
||||||
|
old_squad = sp.squad.name if sp.squad else None
|
||||||
|
|
||||||
|
if 'department_id' in data:
|
||||||
|
if data['department_id']:
|
||||||
|
try:
|
||||||
|
sp.department = Department.objects.get(pk=data['department_id'])
|
||||||
|
except Department.DoesNotExist:
|
||||||
|
return Response({'error': 'Department not found'}, status=404)
|
||||||
|
else:
|
||||||
|
sp.department = None
|
||||||
|
if 'squad_id' in data:
|
||||||
|
if data['squad_id']:
|
||||||
|
try:
|
||||||
|
sp.squad = Squad.objects.get(pk=data['squad_id'])
|
||||||
|
except Squad.DoesNotExist:
|
||||||
|
return Response({'error': 'Squad not found'}, status=404)
|
||||||
|
else:
|
||||||
|
sp.squad = None
|
||||||
|
sp.save()
|
||||||
|
_audit_log(request, 'staff.moved', 'StaffProfile', sp.id, {
|
||||||
|
'from_department': old_dept,
|
||||||
|
'to_department': sp.department.name if sp.department else None,
|
||||||
|
'from_squad': old_squad,
|
||||||
|
'to_squad': sp.squad.name if sp.squad else None,
|
||||||
|
})
|
||||||
|
return Response(_serialize_staff(sp))
|
||||||
|
|
||||||
|
|
||||||
|
# 10. RoleListCreateView
|
||||||
|
class RoleListCreateView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
roles = CustomRole.objects.all()
|
||||||
|
return Response([{
|
||||||
|
'id': r.id,
|
||||||
|
'name': r.name,
|
||||||
|
'slug': r.slug,
|
||||||
|
'description': r.description,
|
||||||
|
'scopes': r.scopes,
|
||||||
|
'is_system': r.is_system,
|
||||||
|
'created_at': r.created_at.isoformat() if r.created_at else None,
|
||||||
|
} for r in roles])
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
data = request.data
|
||||||
|
name = data.get('name', '').strip()
|
||||||
|
if not name:
|
||||||
|
return Response({'error': 'name is required'}, status=400)
|
||||||
|
slug = data.get('slug') or slugify(name)
|
||||||
|
if CustomRole.objects.filter(slug=slug).exists():
|
||||||
|
return Response({'error': 'Role with this slug already exists'}, status=400)
|
||||||
|
role = CustomRole.objects.create(
|
||||||
|
name=name,
|
||||||
|
slug=slug,
|
||||||
|
description=data.get('description', ''),
|
||||||
|
scopes=data.get('scopes', []),
|
||||||
|
is_system=data.get('is_system', False),
|
||||||
|
)
|
||||||
|
_audit_log(request, 'role.created', 'CustomRole', role.id, {'name': name})
|
||||||
|
return Response({
|
||||||
|
'id': role.id,
|
||||||
|
'name': role.name,
|
||||||
|
'slug': role.slug,
|
||||||
|
'description': role.description,
|
||||||
|
'scopes': role.scopes,
|
||||||
|
'is_system': role.is_system,
|
||||||
|
'created_at': role.created_at.isoformat() if role.created_at else None,
|
||||||
|
}, status=201)
|
||||||
|
|
||||||
|
|
||||||
|
# 11. RoleDetailView
|
||||||
|
class RoleDetailView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def patch(self, request, pk):
|
||||||
|
try:
|
||||||
|
role = CustomRole.objects.get(pk=pk)
|
||||||
|
except CustomRole.DoesNotExist:
|
||||||
|
return Response({'error': 'Role not found'}, status=404)
|
||||||
|
data = request.data
|
||||||
|
if 'name' in data:
|
||||||
|
role.name = data['name']
|
||||||
|
if 'description' in data:
|
||||||
|
role.description = data['description']
|
||||||
|
if 'scopes' in data:
|
||||||
|
role.scopes = data['scopes']
|
||||||
|
role.save()
|
||||||
|
_audit_log(request, 'role.updated', 'CustomRole', role.id, data)
|
||||||
|
return Response({
|
||||||
|
'id': role.id,
|
||||||
|
'name': role.name,
|
||||||
|
'slug': role.slug,
|
||||||
|
'description': role.description,
|
||||||
|
'scopes': role.scopes,
|
||||||
|
'is_system': role.is_system,
|
||||||
|
'created_at': role.created_at.isoformat() if role.created_at else None,
|
||||||
|
})
|
||||||
|
|
||||||
|
def delete(self, request, pk):
|
||||||
|
try:
|
||||||
|
role = CustomRole.objects.get(pk=pk)
|
||||||
|
except CustomRole.DoesNotExist:
|
||||||
|
return Response({'error': 'Role not found'}, status=404)
|
||||||
|
if role.is_system:
|
||||||
|
return Response({'error': 'Cannot delete system roles'}, status=400)
|
||||||
|
role_name = role.name
|
||||||
|
role.delete()
|
||||||
|
_audit_log(request, 'role.deleted', 'CustomRole', pk, {'name': role_name})
|
||||||
|
return Response({'success': True}, status=200)
|
||||||
|
|
||||||
|
|
||||||
|
# 12. ScopeListView
|
||||||
|
class ScopeListView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
# Group by category
|
||||||
|
grouped = {}
|
||||||
|
for key, val in SCOPE_DEFINITIONS.items():
|
||||||
|
cat = val['category']
|
||||||
|
if cat not in grouped:
|
||||||
|
grouped[cat] = []
|
||||||
|
grouped[cat].append({
|
||||||
|
'key': key,
|
||||||
|
'label': val['label'],
|
||||||
|
})
|
||||||
|
return Response({
|
||||||
|
'scopes': SCOPE_DEFINITIONS,
|
||||||
|
'grouped': grouped,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# 13. OrgTreeView
|
||||||
|
class OrgTreeView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
departments = Department.objects.prefetch_related(
|
||||||
|
'squads__members__user',
|
||||||
|
'squads__manager',
|
||||||
|
'staff_members__user',
|
||||||
|
).all()
|
||||||
|
|
||||||
|
tree = []
|
||||||
|
for dept in departments:
|
||||||
|
squads_data = []
|
||||||
|
for squad in dept.squads.all():
|
||||||
|
members_data = []
|
||||||
|
for member in squad.members.filter(status='active'):
|
||||||
|
members_data.append({
|
||||||
|
'id': member.id,
|
||||||
|
'user_id': member.user.id,
|
||||||
|
'username': member.user.username,
|
||||||
|
'first_name': member.user.first_name,
|
||||||
|
'last_name': member.user.last_name,
|
||||||
|
'email': member.user.email,
|
||||||
|
'staff_role': member.staff_role,
|
||||||
|
'profile_picture': member.user.profile_picture.url if member.user.profile_picture else None,
|
||||||
|
})
|
||||||
|
squads_data.append({
|
||||||
|
'id': squad.id,
|
||||||
|
'name': squad.name,
|
||||||
|
'manager': {
|
||||||
|
'id': squad.manager.id,
|
||||||
|
'username': squad.manager.username,
|
||||||
|
'first_name': squad.manager.first_name,
|
||||||
|
'last_name': squad.manager.last_name,
|
||||||
|
} if squad.manager else None,
|
||||||
|
'extra_scopes': squad.extra_scopes,
|
||||||
|
'members': members_data,
|
||||||
|
'member_count': len(members_data),
|
||||||
|
})
|
||||||
|
|
||||||
|
# Unassigned staff (in department but no squad)
|
||||||
|
unassigned = dept.staff_members.filter(squad__isnull=True, status='active')
|
||||||
|
unassigned_data = []
|
||||||
|
for member in unassigned:
|
||||||
|
unassigned_data.append({
|
||||||
|
'id': member.id,
|
||||||
|
'user_id': member.user.id,
|
||||||
|
'username': member.user.username,
|
||||||
|
'first_name': member.user.first_name,
|
||||||
|
'last_name': member.user.last_name,
|
||||||
|
'email': member.user.email,
|
||||||
|
'staff_role': member.staff_role,
|
||||||
|
})
|
||||||
|
|
||||||
|
tree.append({
|
||||||
|
'id': dept.id,
|
||||||
|
'name': dept.name,
|
||||||
|
'slug': dept.slug,
|
||||||
|
'color': dept.color,
|
||||||
|
'description': dept.description,
|
||||||
|
'base_scopes': dept.base_scopes,
|
||||||
|
'squads': squads_data,
|
||||||
|
'unassigned_members': unassigned_data,
|
||||||
|
'total_members': dept.staff_members.filter(status='active').count(),
|
||||||
|
})
|
||||||
|
|
||||||
|
# Also include staff with no department
|
||||||
|
orphans = StaffProfile.objects.filter(
|
||||||
|
department__isnull=True, status='active'
|
||||||
|
).select_related('user')
|
||||||
|
orphan_data = []
|
||||||
|
for sp in orphans:
|
||||||
|
orphan_data.append({
|
||||||
|
'id': sp.id,
|
||||||
|
'user_id': sp.user.id,
|
||||||
|
'username': sp.user.username,
|
||||||
|
'first_name': sp.user.first_name,
|
||||||
|
'last_name': sp.user.last_name,
|
||||||
|
'email': sp.user.email,
|
||||||
|
'staff_role': sp.staff_role,
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'departments': tree,
|
||||||
|
'unassigned_staff': orphan_data,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# 14. AuditLogListView
|
||||||
|
class AuditLogListView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
qs = AuditLog.objects.select_related('user').all()
|
||||||
|
# Filters
|
||||||
|
user_id = request.query_params.get('user')
|
||||||
|
if user_id:
|
||||||
|
qs = qs.filter(user_id=user_id)
|
||||||
|
action = request.query_params.get('action')
|
||||||
|
if action:
|
||||||
|
qs = qs.filter(action__icontains=action)
|
||||||
|
target_type = request.query_params.get('target_type')
|
||||||
|
if target_type:
|
||||||
|
qs = qs.filter(target_type=target_type)
|
||||||
|
date_from = request.query_params.get('date_from')
|
||||||
|
if date_from:
|
||||||
|
qs = qs.filter(created_at__date__gte=date_from)
|
||||||
|
date_to = request.query_params.get('date_to')
|
||||||
|
if date_to:
|
||||||
|
qs = qs.filter(created_at__date__lte=date_to)
|
||||||
|
|
||||||
|
# Pagination
|
||||||
|
page = int(request.query_params.get('page', 1))
|
||||||
|
page_size = int(request.query_params.get('page_size', 50))
|
||||||
|
total = qs.count()
|
||||||
|
start = (page - 1) * page_size
|
||||||
|
end = start + page_size
|
||||||
|
logs = qs[start:end]
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'results': [{
|
||||||
|
'id': log.id,
|
||||||
|
'user': {
|
||||||
|
'id': log.user.id,
|
||||||
|
'username': log.user.username,
|
||||||
|
'email': log.user.email,
|
||||||
|
} if log.user else None,
|
||||||
|
'action': log.action,
|
||||||
|
'target_type': log.target_type,
|
||||||
|
'target_id': log.target_id,
|
||||||
|
'details': log.details,
|
||||||
|
'ip_address': log.ip_address,
|
||||||
|
'created_at': log.created_at.isoformat(),
|
||||||
|
} for log in logs],
|
||||||
|
'total': total,
|
||||||
|
'page': page,
|
||||||
|
'page_size': page_size,
|
||||||
|
'total_pages': (total + page_size - 1) // page_size if total else 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Partner Onboarding API
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class PartnerOnboardView(APIView):
|
||||||
|
"""
|
||||||
|
POST /api/v1/partners/onboard/
|
||||||
|
Creates a Partner + its first partner_manager User atomically.
|
||||||
|
Requires admin or manager JWT auth.
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
from partner.models import Partner
|
||||||
|
|
||||||
|
data = request.data
|
||||||
|
|
||||||
|
# --- Validate required fields ---
|
||||||
|
required = {
|
||||||
|
"partner_name": data.get("partner_name"),
|
||||||
|
"contact_email": data.get("contact_email"),
|
||||||
|
"manager_username": data.get("manager_username"),
|
||||||
|
"manager_password": data.get("manager_password"),
|
||||||
|
}
|
||||||
|
missing = [k for k, v in required.items() if not v]
|
||||||
|
if missing:
|
||||||
|
return Response(
|
||||||
|
{"success": False, "error": "Missing required fields: " + ", ".join(missing)},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
manager_username = data["manager_username"].strip()
|
||||||
|
contact_email = data["contact_email"].strip().lower()
|
||||||
|
manager_password = data["manager_password"]
|
||||||
|
|
||||||
|
# --- Check uniqueness ---
|
||||||
|
if User.objects.filter(username=manager_username).exists():
|
||||||
|
return Response(
|
||||||
|
{"success": False, "error": "Username '" + manager_username + "' already exists"},
|
||||||
|
status=status.HTTP_409_CONFLICT,
|
||||||
|
)
|
||||||
|
if User.objects.filter(email=contact_email).exists():
|
||||||
|
return Response(
|
||||||
|
{"success": False, "error": "Email '" + contact_email + "' already exists"},
|
||||||
|
status=status.HTTP_409_CONFLICT,
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Split contact_name into first/last ---
|
||||||
|
contact_name = (data.get("contact_name") or "").strip()
|
||||||
|
name_parts = contact_name.split(None, 1)
|
||||||
|
first_name = name_parts[0] if name_parts else ""
|
||||||
|
last_name = name_parts[1] if len(name_parts) > 1 else ""
|
||||||
|
|
||||||
|
# --- Validate partner_type ---
|
||||||
|
partner_type = (data.get("partner_type") or "other").strip().lower()
|
||||||
|
valid_types = ("venue", "promoter", "sponsor", "vendor", "affiliate", "other")
|
||||||
|
if partner_type not in valid_types:
|
||||||
|
partner_type = "other"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
# 1) Create Partner
|
||||||
|
partner = Partner.objects.create(
|
||||||
|
name=data["partner_name"].strip(),
|
||||||
|
partner_type=partner_type,
|
||||||
|
primary_contact_person_name=contact_name,
|
||||||
|
primary_contact_person_email=contact_email,
|
||||||
|
primary_contact_person_phone=(data.get("contact_phone") or ""),
|
||||||
|
status="pending",
|
||||||
|
is_kyc_compliant=False,
|
||||||
|
kyc_compliance_status="pending",
|
||||||
|
address=(data.get("address") or ""),
|
||||||
|
city=(data.get("city") or ""),
|
||||||
|
state=(data.get("state") or ""),
|
||||||
|
pincode=(data.get("pincode") or ""),
|
||||||
|
website_url=(data.get("website_url") or None),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2) Create partner_manager User
|
||||||
|
manager_user = User(
|
||||||
|
username=manager_username,
|
||||||
|
email=contact_email,
|
||||||
|
first_name=first_name,
|
||||||
|
last_name=last_name,
|
||||||
|
role="partner_manager",
|
||||||
|
is_customer=False,
|
||||||
|
partner=partner,
|
||||||
|
phone_number=(data.get("contact_phone") or ""),
|
||||||
|
)
|
||||||
|
manager_user.set_password(manager_password)
|
||||||
|
manager_user.save()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return Response(
|
||||||
|
{"success": False, "error": "Failed to create partner: " + str(e)},
|
||||||
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"partner": {
|
||||||
|
"id": partner.id,
|
||||||
|
"name": partner.name,
|
||||||
|
"partner_type": partner.partner_type,
|
||||||
|
"status": partner.status,
|
||||||
|
"contact_name": partner.primary_contact_person_name,
|
||||||
|
"contact_email": partner.primary_contact_person_email,
|
||||||
|
"contact_phone": partner.primary_contact_person_phone,
|
||||||
|
"address": partner.address,
|
||||||
|
"city": partner.city,
|
||||||
|
"state": partner.state,
|
||||||
|
"pincode": partner.pincode,
|
||||||
|
"website_url": partner.website_url or "",
|
||||||
|
"is_kyc_compliant": partner.is_kyc_compliant,
|
||||||
|
"kyc_compliance_status": partner.kyc_compliance_status,
|
||||||
|
},
|
||||||
|
"manager": {
|
||||||
|
"id": manager_user.id,
|
||||||
|
"username": manager_user.username,
|
||||||
|
"email": manager_user.email,
|
||||||
|
"first_name": manager_user.first_name,
|
||||||
|
"last_name": manager_user.last_name,
|
||||||
|
"role": manager_user.role,
|
||||||
|
},
|
||||||
|
"login_url": "https://partner.eventifyplus.com",
|
||||||
|
"message": "Partner onboarded successfully. Manager can login at partner.eventifyplus.com",
|
||||||
|
},
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PartnerStaffCreateView(APIView):
|
||||||
|
"""
|
||||||
|
POST /api/v1/partners/<partner_id>/staff/
|
||||||
|
Allows a partner_manager (or admin) to add staff to their partner org.
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request, partner_id):
|
||||||
|
from partner.models import Partner
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
|
# --- Authorization: must be admin or partner_manager of this partner ---
|
||||||
|
user = request.user
|
||||||
|
is_admin = user.role in ("admin", "manager") or user.is_superuser
|
||||||
|
is_partner_manager = (
|
||||||
|
user.role == "partner_manager"
|
||||||
|
and user.partner_id is not None
|
||||||
|
and user.partner_id == partner_id
|
||||||
|
)
|
||||||
|
if not is_admin and not is_partner_manager:
|
||||||
|
return Response(
|
||||||
|
{"success": False, "error": "You do not have permission to add staff to this partner"},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Validate partner exists ---
|
||||||
|
partner = get_object_or_404(Partner, pk=partner_id)
|
||||||
|
|
||||||
|
data = request.data
|
||||||
|
|
||||||
|
# --- Validate required fields ---
|
||||||
|
required = {
|
||||||
|
"username": data.get("username"),
|
||||||
|
"email": data.get("email"),
|
||||||
|
"password": data.get("password"),
|
||||||
|
}
|
||||||
|
missing = [k for k, v in required.items() if not v]
|
||||||
|
if missing:
|
||||||
|
return Response(
|
||||||
|
{"success": False, "error": "Missing required fields: " + ", ".join(missing)},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
username = data["username"].strip()
|
||||||
|
email = data["email"].strip().lower()
|
||||||
|
password = data["password"]
|
||||||
|
|
||||||
|
# --- Check uniqueness ---
|
||||||
|
if User.objects.filter(username=username).exists():
|
||||||
|
return Response(
|
||||||
|
{"success": False, "error": "Username '" + username + "' already exists"},
|
||||||
|
status=status.HTTP_409_CONFLICT,
|
||||||
|
)
|
||||||
|
if User.objects.filter(email=email).exists():
|
||||||
|
return Response(
|
||||||
|
{"success": False, "error": "Email '" + email + "' already exists"},
|
||||||
|
status=status.HTTP_409_CONFLICT,
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Determine role (default partner_staff, allow partner_manager) ---
|
||||||
|
requested_role = (data.get("role") or "partner_staff").strip().lower()
|
||||||
|
allowed_roles = ("partner_staff", "partner_manager")
|
||||||
|
if requested_role not in allowed_roles:
|
||||||
|
return Response(
|
||||||
|
{"success": False, "error": "Role must be one of: " + ", ".join(allowed_roles)},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Non-admin users cannot create partner_manager
|
||||||
|
if requested_role == "partner_manager" and not is_admin:
|
||||||
|
return Response(
|
||||||
|
{"success": False, "error": "Only admins can create additional partner managers"},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
staff_user = User(
|
||||||
|
username=username,
|
||||||
|
email=email,
|
||||||
|
first_name=(data.get("first_name") or "").strip(),
|
||||||
|
last_name=(data.get("last_name") or "").strip(),
|
||||||
|
role=requested_role,
|
||||||
|
is_customer=False,
|
||||||
|
partner=partner,
|
||||||
|
phone_number=(data.get("phone_number") or ""),
|
||||||
|
)
|
||||||
|
staff_user.set_password(password)
|
||||||
|
staff_user.save()
|
||||||
|
except Exception as e:
|
||||||
|
return Response(
|
||||||
|
{"success": False, "error": "Failed to create staff user: " + str(e)},
|
||||||
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"user": {
|
||||||
|
"id": staff_user.id,
|
||||||
|
"username": staff_user.username,
|
||||||
|
"email": staff_user.email,
|
||||||
|
"first_name": staff_user.first_name,
|
||||||
|
"last_name": staff_user.last_name,
|
||||||
|
"role": staff_user.role,
|
||||||
|
"phone_number": staff_user.phone_number or "",
|
||||||
|
"partner_id": partner.id,
|
||||||
|
"partner_name": partner.name,
|
||||||
|
},
|
||||||
|
"message": f"Staff user '{username}' created successfully for partner '{partner.name}'",
|
||||||
|
},
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ from .models import Event, EventImages
|
|||||||
|
|
||||||
@admin.register(Event)
|
@admin.register(Event)
|
||||||
class EventAdmin(admin.ModelAdmin):
|
class EventAdmin(admin.ModelAdmin):
|
||||||
list_display = ('id', 'name', 'start_date', 'end_date', 'event_type', 'event_status', 'is_featured', 'is_top_event')
|
list_display = ('id', 'name', 'start_date', 'end_date', 'event_type', 'event_status', 'source', 'is_featured', 'is_top_event')
|
||||||
list_filter = ('event_status', 'event_type', 'is_featured', 'is_top_event')
|
list_filter = ('event_status', 'event_type', 'source', 'is_featured', 'is_top_event')
|
||||||
list_editable = ('is_featured', 'is_top_event')
|
list_editable = ('is_featured', 'is_top_event', 'source')
|
||||||
search_fields = ('name', 'place', 'district')
|
search_fields = ('name', 'place', 'district')
|
||||||
|
|
||||||
@admin.register(EventImages)
|
@admin.register(EventImages)
|
||||||
|
|||||||
@@ -36,9 +36,13 @@ class EventForm(forms.ModelForm):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
# Set source to 'official' only and hide the field
|
# Show source as visible radio buttons with Bootstrap styling
|
||||||
self.fields['source'].initial = 'official'
|
self.fields['source'].widget = forms.RadioSelect(
|
||||||
self.fields['source'].widget = forms.HiddenInput()
|
choices=self.fields['source'].choices,
|
||||||
|
attrs={'class': 'form-check-input'}
|
||||||
|
)
|
||||||
|
if not self.instance.pk:
|
||||||
|
self.fields['source'].initial = 'eventify'
|
||||||
|
|
||||||
# Check if all_year_event is True (from instance or initial data)
|
# Check if all_year_event is True (from instance or initial data)
|
||||||
all_year_event = False
|
all_year_event = False
|
||||||
@@ -60,8 +64,7 @@ class EventForm(forms.ModelForm):
|
|||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
all_year_event = cleaned_data.get('all_year_event', False)
|
all_year_event = cleaned_data.get('all_year_event', False)
|
||||||
|
|
||||||
# Force source to be 'official' only
|
# Source is now user-selectable (eventify/community/partner)
|
||||||
cleaned_data['source'] = 'official'
|
|
||||||
|
|
||||||
# If all_year_event is True, clear date/time fields
|
# If all_year_event is True, clear date/time fields
|
||||||
if all_year_event:
|
if all_year_event:
|
||||||
|
|||||||
48
events/migrations/migrations/0001_initial.py
Normal file
48
events/migrations/migrations/0001_initial.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# Generated by Django 4.2.21 on 2025-11-26 22:07
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('master_data', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Event',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_date', models.DateField(auto_now_add=True)),
|
||||||
|
('name', models.CharField(max_length=200)),
|
||||||
|
('description', models.TextField()),
|
||||||
|
('start_date', models.DateField()),
|
||||||
|
('end_date', models.DateField()),
|
||||||
|
('latitude', models.DecimalField(decimal_places=6, max_digits=9)),
|
||||||
|
('longitude', models.DecimalField(decimal_places=6, max_digits=9)),
|
||||||
|
('pincode', models.CharField(max_length=10)),
|
||||||
|
('district', models.CharField(max_length=100)),
|
||||||
|
('state', models.CharField(max_length=100)),
|
||||||
|
('place', models.CharField(max_length=200)),
|
||||||
|
('is_bookable', models.BooleanField(default=False)),
|
||||||
|
('is_eventify_event', models.BooleanField(default=True)),
|
||||||
|
('outside_event_url', models.URLField(default='NA')),
|
||||||
|
('event_status', models.CharField(choices=[('created', 'Created'), ('cancelled', 'Cancelled'), ('pending', 'Pending'), ('completed', 'Completed'), ('postponed', 'Postponed')], default='pending', max_length=250)),
|
||||||
|
('cancelled_reason', models.TextField(default='NA')),
|
||||||
|
('event_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='master_data.eventtype')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='EventImages',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('is_primary', models.BooleanField(default=False)),
|
||||||
|
('event_image', models.ImageField(upload_to='event_images')),
|
||||||
|
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.event')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 4.2.21 on 2025-11-28 20:56
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('events', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='important_information',
|
||||||
|
field=models.TextField(blank=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='title',
|
||||||
|
field=models.CharField(blank=True, max_length=250),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='venue_name',
|
||||||
|
field=models.CharField(blank=True, max_length=250),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 4.2.21 on 2025-11-28 21:01
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('events', '0002_event_important_information_event_title_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='end_time',
|
||||||
|
field=models.TimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='start_time',
|
||||||
|
field=models.TimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 5.0 on 2025-12-19 22:14
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('events', '0003_event_end_time_event_start_time'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='all_year_event',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='end_date',
|
||||||
|
field=models.DateField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='start_date',
|
||||||
|
field=models.DateField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
events/migrations/migrations/0005_event_source.py
Normal file
18
events/migrations/migrations/0005_event_source.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0 on 2025-12-19 22:31
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('events', '0004_event_all_year_event_alter_event_end_date_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='source',
|
||||||
|
field=models.CharField(blank=True, choices=[('eventify', 'Eventify'), ('community', 'Community')], max_length=250),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
events/migrations/migrations/0006_alter_event_source.py
Normal file
18
events/migrations/migrations/0006_alter_event_source.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0 on 2025-12-19 22:33
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('events', '0005_event_source'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='source',
|
||||||
|
field=models.CharField(blank=True, choices=[('official', 'Official'), ('community', 'Community')], max_length=250),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('events', '0006_alter_event_source'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='is_featured',
|
||||||
|
field=models.BooleanField(default=False, help_text='Show this event in the featured section'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='is_top_event',
|
||||||
|
field=models.BooleanField(default=False, help_text='Show this event in the Top Events section'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 4.2.27 on 2026-03-13 16:55
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('events', '0006_alter_event_source'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='gst_percentage_1',
|
||||||
|
field=models.IntegerField(default=0),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='gst_percentage_2',
|
||||||
|
field=models.IntegerField(default=0),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='include_gst',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='event_status',
|
||||||
|
field=models.CharField(choices=[('created', 'Created'), ('cancelled', 'Cancelled'), ('pending', 'Pending'), ('completed', 'Completed'), ('postponed', 'Postponed'), ('published', 'Published'), ('live', 'Live'), ('flagged', 'Flagged')], default='pending', max_length=250),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 4.2.27 on 2026-03-14 15:57
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('partner', '0001_initial'),
|
||||||
|
('events', '0007_event_gst_percentage_1_event_gst_percentage_2_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='is_partner_event',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='partner',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='partner.partner'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-03-14 19:34
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('events', '0008_event_is_partner_event_event_partner'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='eventimages',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
]
|
||||||
14
events/migrations/migrations/0010_merge_20260324_1443.py
Normal file
14
events/migrations/migrations/0010_merge_20260324_1443.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Generated by Django 4.2.21 on 2026-03-24 14:43
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('events', '0007_add_is_featured_is_top_event'),
|
||||||
|
('events', '0009_alter_event_id_alter_eventimages_id'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
]
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Generated by Django 4.2.21 on 2026-03-30 10:30
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('events', '0010_merge_20260324_1443'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='created_date',
|
||||||
|
field=models.DateField(auto_now_add=True, db_index=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='event_status',
|
||||||
|
field=models.CharField(choices=[('created', 'Created'), ('cancelled', 'Cancelled'), ('pending', 'Pending'), ('completed', 'Completed'), ('postponed', 'Postponed'), ('published', 'Published'), ('live', 'Live'), ('flagged', 'Flagged')], db_index=True, default='pending', max_length=250),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='id',
|
||||||
|
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='source',
|
||||||
|
field=models.CharField(choices=[('eventify', 'Added by Eventify'), ('community', 'Community Contribution'), ('partner', 'Partner Event')], default='eventify', max_length=50),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='start_date',
|
||||||
|
field=models.DateField(blank=True, db_index=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='eventimages',
|
||||||
|
name='id',
|
||||||
|
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
events/migrations/migrations/__init__.py
Normal file
0
events/migrations/migrations/__init__.py
Normal file
@@ -49,9 +49,10 @@ class Event(models.Model):
|
|||||||
important_information = models.TextField(blank=True)
|
important_information = models.TextField(blank=True)
|
||||||
venue_name = models.CharField(max_length=250, blank=True)
|
venue_name = models.CharField(max_length=250, blank=True)
|
||||||
|
|
||||||
source = models.CharField(max_length=250, blank=True, choices=[
|
source = models.CharField(max_length=50, default='eventify', choices=[
|
||||||
('official', 'Official'),
|
('eventify', 'Added by Eventify'),
|
||||||
('community', 'Community'),
|
('community', 'Community Contribution'),
|
||||||
|
('partner', 'Partner Event'),
|
||||||
])
|
])
|
||||||
|
|
||||||
is_featured = models.BooleanField(default=False, help_text='Show this event in the featured section')
|
is_featured = models.BooleanField(default=False, help_text='Show this event in the featured section')
|
||||||
|
|||||||
@@ -37,6 +37,36 @@ class EventTypeListAPIView(APIView):
|
|||||||
class EventListAPI(APIView):
|
class EventListAPI(APIView):
|
||||||
permission_classes = [AllowAny]
|
permission_classes = [AllowAny]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _serialize_event(e, thumb_map):
|
||||||
|
"""Slim serialization for list views — only fields the Flutter app uses."""
|
||||||
|
img = thumb_map.get(e.id)
|
||||||
|
lat = e.latitude
|
||||||
|
lng = e.longitude
|
||||||
|
desc = e.description or ''
|
||||||
|
return {
|
||||||
|
'id': e.id,
|
||||||
|
'name': e.name or '',
|
||||||
|
'title': e.title or '',
|
||||||
|
'description': desc[:200] if len(desc) > 200 else desc,
|
||||||
|
'start_date': str(e.start_date) if e.start_date else '',
|
||||||
|
'end_date': str(e.end_date) if e.end_date else '',
|
||||||
|
'start_time': str(e.start_time) if e.start_time else '',
|
||||||
|
'end_time': str(e.end_time) if e.end_time else '',
|
||||||
|
'pincode': e.pincode or '',
|
||||||
|
'place': e.place or '',
|
||||||
|
'is_bookable': bool(e.is_bookable),
|
||||||
|
'event_type': e.event_type_id,
|
||||||
|
'event_status': e.event_status or '',
|
||||||
|
'venue_name': getattr(e, 'venue_name', '') or '',
|
||||||
|
'latitude': float(lat) if lat is not None else None,
|
||||||
|
'longitude': float(lng) if lng is not None else None,
|
||||||
|
'location_name': getattr(e, 'location_name', '') or '',
|
||||||
|
'thumb_img': img.event_image.url if img and img.event_image else '',
|
||||||
|
'is_eventify_event': bool(e.is_eventify_event),
|
||||||
|
'source': e.source or 'eventify',
|
||||||
|
}
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
@@ -44,40 +74,53 @@ class EventListAPI(APIView):
|
|||||||
except Exception:
|
except Exception:
|
||||||
data = {}
|
data = {}
|
||||||
|
|
||||||
paginate = "page" in data
|
pincode = data.get("pincode", "all")
|
||||||
page = int(data.get("page", 1))
|
page = int(data.get("page", 1))
|
||||||
page_size = int(data.get("page_size", 15))
|
page_size = int(data.get("page_size", 50))
|
||||||
|
per_type = int(data.get("per_type", 0))
|
||||||
|
|
||||||
events = Event.objects.select_related('event_type').order_by('-created_date')
|
# Build base queryset (lazy - no DB hit yet)
|
||||||
|
MIN_EVENTS_THRESHOLD = 6
|
||||||
|
qs = Event.objects.all()
|
||||||
|
if pincode and pincode != 'all':
|
||||||
|
pincode_qs = qs.filter(pincode=pincode)
|
||||||
|
# Fallback to all events if pincode has too few
|
||||||
|
if pincode_qs.count() >= MIN_EVENTS_THRESHOLD:
|
||||||
|
qs = pincode_qs
|
||||||
|
# else: keep qs as Event.objects.all()
|
||||||
|
|
||||||
total = events.count()
|
if per_type > 0 and page == 1:
|
||||||
|
# Diverse mode: one bounded query per event type
|
||||||
if paginate:
|
type_ids = list(qs.values_list('event_type_id', flat=True).distinct())
|
||||||
start = (page - 1) * page_size
|
events_page = []
|
||||||
end = start + page_size
|
for tid in sorted(type_ids):
|
||||||
page_qs = list(events[start:end])
|
chunk = list(qs.filter(event_type_id=tid).order_by('-created_date')[:per_type])
|
||||||
|
events_page.extend(chunk)
|
||||||
|
total_count = qs.count()
|
||||||
|
end = len(events_page)
|
||||||
else:
|
else:
|
||||||
page_qs = list(events)
|
# Standard pagination at DB level
|
||||||
start, end = 0, total
|
total_count = qs.count()
|
||||||
|
qs = qs.order_by('-created_date')
|
||||||
|
start = (page - 1) * page_size
|
||||||
|
end = start + page_size
|
||||||
|
events_page = list(qs[start:end])
|
||||||
|
|
||||||
event_ids = [e.id for e in page_qs]
|
# Fetch images ONLY for the events we will return
|
||||||
primary_images = EventImages.objects.filter(event_id__in=event_ids, is_primary=True)
|
page_ids = [e.id for e in events_page]
|
||||||
|
primary_images = EventImages.objects.filter(event_id__in=page_ids, is_primary=True)
|
||||||
thumb_map = {img.event_id: img for img in primary_images}
|
thumb_map = {img.event_id: img for img in primary_images}
|
||||||
|
|
||||||
event_list = []
|
# Serialize with direct attribute access (fast)
|
||||||
for e in page_qs:
|
event_list = [self._serialize_event(e, thumb_map) for e in events_page]
|
||||||
d = model_to_dict(e)
|
|
||||||
img = thumb_map.get(e.id)
|
|
||||||
d['thumb_img'] = img.event_image.url if img else ''
|
|
||||||
event_list.append(d)
|
|
||||||
|
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"events": event_list,
|
"events": event_list,
|
||||||
"total": total,
|
"total_count": total_count,
|
||||||
"page": page,
|
"page": page,
|
||||||
"page_size": page_size,
|
"page_size": page_size,
|
||||||
"has_next": end < total,
|
"has_next": end < total_count,
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JsonResponse({"status": "error", "message": str(e)})
|
return JsonResponse({"status": "error", "message": str(e)})
|
||||||
@@ -110,6 +153,8 @@ class EventDetailAPI(APIView):
|
|||||||
|
|
||||||
|
|
||||||
class EventImagesListAPI(APIView):
|
class EventImagesListAPI(APIView):
|
||||||
|
authentication_classes = []
|
||||||
|
permission_classes = [AllowAny]
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
try:
|
try:
|
||||||
user, token, data, error_response = validate_token_and_get_user(request)
|
user, token, data, error_response = validate_token_and_get_user(request)
|
||||||
@@ -139,6 +184,8 @@ class EventImagesListAPI(APIView):
|
|||||||
|
|
||||||
@method_decorator(csrf_exempt, name='dispatch')
|
@method_decorator(csrf_exempt, name='dispatch')
|
||||||
class EventsByCategoryAPI(APIView):
|
class EventsByCategoryAPI(APIView):
|
||||||
|
authentication_classes = []
|
||||||
|
permission_classes = [AllowAny]
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
try:
|
try:
|
||||||
user, token, data, error_response = validate_token_and_get_user(request)
|
user, token, data, error_response = validate_token_and_get_user(request)
|
||||||
@@ -176,6 +223,8 @@ class EventsByCategoryAPI(APIView):
|
|||||||
|
|
||||||
@method_decorator(csrf_exempt, name='dispatch')
|
@method_decorator(csrf_exempt, name='dispatch')
|
||||||
class EventsByMonthYearAPI(APIView):
|
class EventsByMonthYearAPI(APIView):
|
||||||
|
authentication_classes = []
|
||||||
|
permission_classes = [AllowAny]
|
||||||
"""
|
"""
|
||||||
API to get events by month and year.
|
API to get events by month and year.
|
||||||
Returns dates that have events, total count, and date-wise breakdown.
|
Returns dates that have events, total count, and date-wise breakdown.
|
||||||
@@ -298,6 +347,8 @@ class EventsByMonthYearAPI(APIView):
|
|||||||
|
|
||||||
@method_decorator(csrf_exempt, name='dispatch')
|
@method_decorator(csrf_exempt, name='dispatch')
|
||||||
class EventsByDateAPI(APIView):
|
class EventsByDateAPI(APIView):
|
||||||
|
authentication_classes = []
|
||||||
|
permission_classes = [AllowAny]
|
||||||
"""
|
"""
|
||||||
API to get events occurring on a specific date.
|
API to get events occurring on a specific date.
|
||||||
Returns complete event information with primary images.
|
Returns complete event information with primary images.
|
||||||
@@ -354,6 +405,8 @@ class EventsByDateAPI(APIView):
|
|||||||
|
|
||||||
@method_decorator(csrf_exempt, name='dispatch')
|
@method_decorator(csrf_exempt, name='dispatch')
|
||||||
class FeaturedEventsAPI(APIView):
|
class FeaturedEventsAPI(APIView):
|
||||||
|
authentication_classes = []
|
||||||
|
permission_classes = [AllowAny]
|
||||||
"""Returns events where is_featured=True — used for the homepage hero carousel."""
|
"""Returns events where is_featured=True — used for the homepage hero carousel."""
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
@@ -380,6 +433,8 @@ class FeaturedEventsAPI(APIView):
|
|||||||
|
|
||||||
@method_decorator(csrf_exempt, name='dispatch')
|
@method_decorator(csrf_exempt, name='dispatch')
|
||||||
class TopEventsAPI(APIView):
|
class TopEventsAPI(APIView):
|
||||||
|
authentication_classes = []
|
||||||
|
permission_classes = [AllowAny]
|
||||||
"""Returns events where is_top_event=True — used for the Top Events section."""
|
"""Returns events where is_top_event=True — used for the Top Events section."""
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
|
|||||||
@@ -10,15 +10,28 @@
|
|||||||
{% for field in form %}
|
{% for field in form %}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
{{ field.label_tag }}
|
{{ field.label_tag }}
|
||||||
|
{% if field.name == 'source' %}
|
||||||
|
<div class="mt-2">
|
||||||
|
{% for radio in field %}
|
||||||
|
<div class="form-check">
|
||||||
|
{{ radio.tag }}
|
||||||
|
<label class="form-check-label" for="{{ radio.id_for_label }}">{{ radio.choice_label }}</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
{{ field }}
|
{{ field }}
|
||||||
|
{% endif %}
|
||||||
{% for error in field.errors %}
|
{% for error in field.errors %}
|
||||||
<div class="text-danger">{{ error }}</div>
|
<div class="text-danger">{{ error }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<button class="btn btn-primary">Save</button>
|
<div class="mb-3 mt-4">
|
||||||
<a class="btn btn-secondary" href="{% url 'events:event_list' %}">Cancel</a>
|
<button class="btn btn-primary">Save</button>
|
||||||
|
<a class="btn btn-secondary" href="{% url 'events:event_list' %}">Cancel</a>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user