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:
2026-03-30 11:23:03 +00:00
parent 388057b641
commit 43123d0ff1
19 changed files with 1381 additions and 38 deletions

View File

@@ -5,7 +5,7 @@ from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework import status
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.views import TokenRefreshView
from django.db import connection
from django.db import connection, transaction
from .serializers import UserSerializer
User = get_user_model()
@@ -30,16 +30,44 @@ class AdminLoginView(APIView):
if not user.is_active:
return Response({'error': 'Account is disabled'}, status=status.HTTP_403_FORBIDDEN)
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({
'access': str(refresh.access_token),
'refresh': str(refresh),
'user': UserSerializer(user).data,
'user': user_data,
})
class MeView(APIView):
permission_classes = [IsAuthenticated]
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):
permission_classes = [AllowAny]
@@ -653,6 +681,7 @@ def _serialize_event(e):
'createdAt': e.created_date.isoformat() if e.created_date else '',
'isFeatured': bool(e.is_featured),
'isTopEvent': bool(e.is_top_event),
'source': e.source or 'eventify',
}
@@ -1289,3 +1318,923 @@ class EventPrimaryImageView(APIView):
img.save()
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,
)