The new updates of partners and user
Made-with: Cursor
This commit is contained in:
1
.claude/worktrees/strange-ellis
Submodule
1
.claude/worktrees/strange-ellis
Submodule
Submodule .claude/worktrees/strange-ellis added at 88b3aafb0b
946
accounts/api.py
Normal file
946
accounts/api.py
Normal file
@@ -0,0 +1,946 @@
|
||||
from django.http import JsonResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.forms.models import model_to_dict
|
||||
from django.contrib.auth import authenticate, logout
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.authtoken.models import Token
|
||||
import json
|
||||
|
||||
from .models import User
|
||||
from mobile_api.utils import validate_token_and_get_user
|
||||
|
||||
|
||||
def _partner_user_to_dict(user, request=None):
|
||||
"""Serialize partner-related User for JSON (same structure as _user_to_dict)."""
|
||||
data = model_to_dict(
|
||||
user,
|
||||
fields=[
|
||||
"id",
|
||||
"username",
|
||||
"email",
|
||||
"phone_number",
|
||||
"role",
|
||||
"is_staff",
|
||||
"is_customer",
|
||||
"is_user",
|
||||
"pincode",
|
||||
"district",
|
||||
"state",
|
||||
"country",
|
||||
"place",
|
||||
"latitude",
|
||||
"longitude",
|
||||
"first_name",
|
||||
"last_name",
|
||||
],
|
||||
)
|
||||
# Add profile picture URL if exists
|
||||
if getattr(user, "profile_picture", None):
|
||||
if request:
|
||||
data["profile_picture"] = request.build_absolute_uri(user.profile_picture.url)
|
||||
else:
|
||||
data["profile_picture"] = user.profile_picture.url
|
||||
else:
|
||||
data["profile_picture"] = None
|
||||
return data
|
||||
|
||||
|
||||
def _user_to_dict(user, request=None):
|
||||
"""Serialize any User for JSON (admin/staff or partner)."""
|
||||
return _partner_user_to_dict(user, request)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class PartnerLoginAPI(APIView):
|
||||
"""
|
||||
Partner Login API.
|
||||
Body: username (or email), password (required).
|
||||
Returns: token, user data.
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# Parse JSON or form data
|
||||
is_multipart = request.content_type and "multipart/form-data" in request.content_type
|
||||
if is_multipart:
|
||||
data = request.POST.dict()
|
||||
else:
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "Invalid JSON"},
|
||||
status=400,
|
||||
)
|
||||
|
||||
username = data.get("username") or data.get("email")
|
||||
password = data.get("password")
|
||||
|
||||
if not username or not password:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "username and password are required."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
# Authenticate user
|
||||
user = authenticate(request, username=username, password=password)
|
||||
if not user:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "Invalid username or password."},
|
||||
status=401,
|
||||
)
|
||||
|
||||
# Check if user has partner role
|
||||
partner_roles = ["partner", "partner_manager", "partner_staff"]
|
||||
if user.role not in partner_roles:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "You are not authorized to access partner portal."},
|
||||
status=403,
|
||||
)
|
||||
|
||||
# Get or create token
|
||||
token, _ = Token.objects.get_or_create(user=user)
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Login successful",
|
||||
"token": token.key,
|
||||
"user": _partner_user_to_dict(user, request),
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class PartnerLogoutAPI(APIView):
|
||||
"""
|
||||
Partner Logout API.
|
||||
Body: token, username (required).
|
||||
Returns: success message.
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request, error_status_code=True)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
# Check if user has partner role
|
||||
partner_roles = ["partner", "partner_manager", "partner_staff"]
|
||||
if user.role not in partner_roles:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "You are not authorized to access partner portal."},
|
||||
status=403,
|
||||
)
|
||||
|
||||
# Delete token
|
||||
token.delete()
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Logged out successfully.",
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class PartnerDashboardAPI(APIView):
|
||||
"""
|
||||
Partner Dashboard API.
|
||||
Body: token, username (required).
|
||||
Returns: dashboard statistics.
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request, error_status_code=True)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
# Check if user has partner role
|
||||
partner_roles = ["partner", "partner_manager", "partner_staff"]
|
||||
if user.role not in partner_roles:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "You are not authorized to access this page."},
|
||||
status=403,
|
||||
)
|
||||
|
||||
# Get statistics for partner users (including partner_customer)
|
||||
all_partner_roles = ["partner", "partner_manager", "partner_staff", "partner_customer"]
|
||||
partner_users = User.objects.filter(role__in=all_partner_roles)
|
||||
total_partner_users = partner_users.count()
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "success",
|
||||
"dashboard": {
|
||||
"total_partner_users": total_partner_users,
|
||||
},
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class PartnerListUsersAPI(APIView):
|
||||
"""
|
||||
Partner List Users API.
|
||||
Body: token, username (required);
|
||||
role (optional filter: partner, partner_manager, partner_staff, partner_customer).
|
||||
Returns: list of partner users.
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request, error_status_code=True)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
# Check if user has partner role
|
||||
partner_roles = ["partner", "partner_manager", "partner_staff"]
|
||||
if user.role not in partner_roles:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "You are not authorized to access this page."},
|
||||
status=403,
|
||||
)
|
||||
|
||||
# Filter users by partner-related roles
|
||||
all_partner_roles = ["partner", "partner_manager", "partner_staff", "partner_customer"]
|
||||
qs = User.objects.filter(role__in=all_partner_roles).order_by("-id")
|
||||
|
||||
# Optional role filter
|
||||
role_filter = data.get("role")
|
||||
if role_filter:
|
||||
if role_filter not in all_partner_roles:
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "error",
|
||||
"message": f"Invalid role filter. Must be one of: {', '.join(all_partner_roles)}",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
qs = qs.filter(role=role_filter)
|
||||
|
||||
users = [_partner_user_to_dict(u, request) for u in qs]
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "success",
|
||||
"users": users,
|
||||
"total_count": len(users),
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class PartnerCreateUserAPI(APIView):
|
||||
"""
|
||||
Partner Create User API.
|
||||
Body: token, username, username (for new user), email, password, role (required);
|
||||
full_name, phone_number, pincode, district, state, country, place, latitude, longitude (optional).
|
||||
Returns: created user data.
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request, error_status_code=True)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
# Check if user has partner role
|
||||
partner_roles = ["partner", "partner_manager", "partner_staff"]
|
||||
if user.role not in partner_roles:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "You are not authorized to access this page."},
|
||||
status=403,
|
||||
)
|
||||
|
||||
# Extract user data
|
||||
new_username = data.get("username")
|
||||
email = data.get("email")
|
||||
password = data.get("password")
|
||||
role = data.get("role")
|
||||
full_name = data.get("full_name", "").strip()
|
||||
|
||||
if not all([new_username, email, password, role]):
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "error",
|
||||
"message": "username, email, password, and role are required.",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
# Validate role - must be one of the partner-related roles
|
||||
valid_partner_roles = ["partner", "partner_manager", "partner_staff", "partner_customer"]
|
||||
if role not in valid_partner_roles:
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "error",
|
||||
"message": f"Invalid role. Must be one of: {', '.join(valid_partner_roles)}",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
# Check if username already exists
|
||||
if User.objects.filter(username=new_username).exists():
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "Username already exists."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
# Check if email already exists
|
||||
if User.objects.filter(email=email).exists():
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "Email already exists."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
# Create user
|
||||
new_user = User.objects.create_user(
|
||||
username=new_username,
|
||||
email=email,
|
||||
password=password,
|
||||
role=role,
|
||||
phone_number=data.get("phone_number"),
|
||||
pincode=data.get("pincode"),
|
||||
district=data.get("district"),
|
||||
state=data.get("state"),
|
||||
country=data.get("country"),
|
||||
place=data.get("place"),
|
||||
)
|
||||
|
||||
# Handle full_name - split into first_name and last_name
|
||||
if full_name:
|
||||
parts = full_name.split(None, 1)
|
||||
new_user.first_name = parts[0]
|
||||
if len(parts) > 1:
|
||||
new_user.last_name = parts[1]
|
||||
|
||||
# Set location coordinates if provided
|
||||
if data.get("latitude") is not None:
|
||||
try:
|
||||
latitude = float(data["latitude"])
|
||||
if latitude < -90 or latitude > 90:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "latitude must be between -90 and 90."},
|
||||
status=400,
|
||||
)
|
||||
new_user.latitude = latitude
|
||||
except (TypeError, ValueError):
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "latitude must be numeric."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
if data.get("longitude") is not None:
|
||||
try:
|
||||
longitude = float(data["longitude"])
|
||||
if longitude < -180 or longitude > 180:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "longitude must be between -180 and 180."},
|
||||
status=400,
|
||||
)
|
||||
new_user.longitude = longitude
|
||||
except (TypeError, ValueError):
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "longitude must be numeric."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
# Handle profile picture upload if provided
|
||||
if "profile_picture" in request.FILES:
|
||||
new_user.profile_picture = request.FILES["profile_picture"]
|
||||
|
||||
new_user.save()
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "success",
|
||||
"message": f"User created successfully with role: {role}.",
|
||||
"user": _partner_user_to_dict(new_user, request),
|
||||
},
|
||||
status=201,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class PartnerUpdateUserAPI(APIView):
|
||||
"""
|
||||
Partner Update User API.
|
||||
Body: token, username, user_id (required);
|
||||
email, phone_number, role, full_name, pincode, district, state,
|
||||
country, place, latitude, longitude, password, profile_picture (optional).
|
||||
Returns: updated user data.
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request, error_status_code=True)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
# Check if user has partner role
|
||||
partner_roles = ["partner", "partner_manager", "partner_staff"]
|
||||
if user.role not in partner_roles:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "You are not authorized to access this page."},
|
||||
status=403,
|
||||
)
|
||||
|
||||
user_id = data.get("user_id")
|
||||
if not user_id:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "user_id is required."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
try:
|
||||
target_user = User.objects.get(id=user_id)
|
||||
except User.DoesNotExist:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "User not found."},
|
||||
status=404,
|
||||
)
|
||||
|
||||
# Validate that the user has a partner-related role
|
||||
all_partner_roles = ["partner", "partner_manager", "partner_staff", "partner_customer"]
|
||||
if target_user.role not in all_partner_roles:
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "error",
|
||||
"message": "User is not a partner-related user. Only users with partner roles can be updated.",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
# Update fields if provided
|
||||
if data.get("email") is not None:
|
||||
new_email = data["email"]
|
||||
# Check if email already exists for another user
|
||||
if User.objects.filter(email=new_email).exclude(id=user_id).exists():
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "Email already exists for another user."},
|
||||
status=400,
|
||||
)
|
||||
target_user.email = new_email
|
||||
|
||||
if data.get("phone_number") is not None:
|
||||
target_user.phone_number = data["phone_number"] or None
|
||||
|
||||
if data.get("role") is not None:
|
||||
new_role = data["role"]
|
||||
if new_role not in all_partner_roles:
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "error",
|
||||
"message": f"Invalid role. Must be one of: {', '.join(all_partner_roles)}",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
target_user.role = new_role
|
||||
|
||||
# Handle full_name
|
||||
if data.get("full_name"):
|
||||
full_name = data["full_name"].strip()
|
||||
if full_name:
|
||||
parts = full_name.split(None, 1)
|
||||
target_user.first_name = parts[0]
|
||||
if len(parts) > 1:
|
||||
target_user.last_name = parts[1]
|
||||
else:
|
||||
target_user.last_name = ""
|
||||
|
||||
if "pincode" in data:
|
||||
target_user.pincode = data["pincode"] or None
|
||||
if "district" in data:
|
||||
target_user.district = data["district"] or None
|
||||
if "state" in data:
|
||||
target_user.state = data["state"] or None
|
||||
if "country" in data:
|
||||
target_user.country = data["country"] or None
|
||||
if "place" in data:
|
||||
target_user.place = data["place"] or None
|
||||
|
||||
if data.get("latitude") is not None:
|
||||
try:
|
||||
latitude = float(data["latitude"])
|
||||
if latitude < -90 or latitude > 90:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "latitude must be between -90 and 90."},
|
||||
status=400,
|
||||
)
|
||||
target_user.latitude = latitude
|
||||
except (TypeError, ValueError):
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "latitude must be numeric."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
if data.get("longitude") is not None:
|
||||
try:
|
||||
longitude = float(data["longitude"])
|
||||
if longitude < -180 or longitude > 180:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "longitude must be between -180 and 180."},
|
||||
status=400,
|
||||
)
|
||||
target_user.longitude = longitude
|
||||
except (TypeError, ValueError):
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "longitude must be numeric."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
# Handle profile picture upload if provided
|
||||
if "profile_picture" in request.FILES:
|
||||
target_user.profile_picture = request.FILES["profile_picture"]
|
||||
|
||||
# Handle password update if provided
|
||||
if data.get("password"):
|
||||
target_user.set_password(data["password"])
|
||||
|
||||
target_user.save()
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Partner user updated successfully.",
|
||||
"user": _partner_user_to_dict(target_user, request),
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class PartnerDeleteUserAPI(APIView):
|
||||
"""
|
||||
Partner Delete User API.
|
||||
Body: token, username, user_id (required).
|
||||
Returns: success message.
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request, error_status_code=True)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
# Check if user has partner role
|
||||
partner_roles = ["partner", "partner_manager", "partner_staff"]
|
||||
if user.role not in partner_roles:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "You are not authorized to access this page."},
|
||||
status=403,
|
||||
)
|
||||
|
||||
user_id = data.get("user_id")
|
||||
if not user_id:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "user_id is required."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
try:
|
||||
target_user = User.objects.get(id=user_id)
|
||||
except User.DoesNotExist:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "User not found."},
|
||||
status=404,
|
||||
)
|
||||
|
||||
# Validate that the user has a partner-related role
|
||||
all_partner_roles = ["partner", "partner_manager", "partner_staff", "partner_customer"]
|
||||
if target_user.role not in all_partner_roles:
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "error",
|
||||
"message": "User is not a partner-related user. Only users with partner roles can be deleted.",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
# Prevent deleting yourself
|
||||
if target_user.id == user.id:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "You cannot delete your own account."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
username = target_user.username
|
||||
target_user.delete()
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "success",
|
||||
"message": f"Partner user '{username}' deleted successfully.",
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class LoginAPI(APIView):
|
||||
"""
|
||||
Admin/Staff Login API (accounts).
|
||||
Body: username (or email), password (required).
|
||||
Returns: token and user details for admin/manager/staff roles.
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# Parse JSON or form data
|
||||
is_multipart = request.content_type and "multipart/form-data" in request.content_type
|
||||
if is_multipart:
|
||||
data = request.POST.dict()
|
||||
else:
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "Invalid JSON"},
|
||||
status=400,
|
||||
)
|
||||
|
||||
username = data.get("username") or data.get("email")
|
||||
password = data.get("password")
|
||||
|
||||
if not username or not password:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "username and password are required."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
user = authenticate(request, username=username, password=password)
|
||||
if not user:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "Invalid username or password."},
|
||||
status=401,
|
||||
)
|
||||
|
||||
# Only allow admin/manager/staff to use this login
|
||||
allowed_roles = ["admin", "manager", "staff"]
|
||||
if user.role not in allowed_roles:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "You are not authorized to access the admin portal."},
|
||||
status=403,
|
||||
)
|
||||
|
||||
token, _ = Token.objects.get_or_create(user=user)
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Login successful",
|
||||
"token": token.key,
|
||||
"user": _user_to_dict(user, request),
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class LogoutAPI(APIView):
|
||||
"""
|
||||
Logout API for token-based sessions.
|
||||
Body: token, username (required).
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request, error_status_code=True)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
logout(request)
|
||||
token.delete()
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Logout successful.",
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class UserListAPI(APIView):
|
||||
"""
|
||||
List users (admin / manager / staff only).
|
||||
Body: token, username (required); optional role filter.
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request, error_status_code=True)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
# Only allow admin/manager/staff to list users
|
||||
allowed_roles = ["admin", "manager", "staff"]
|
||||
if user.role not in allowed_roles:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "You are not authorized to list users."},
|
||||
status=403,
|
||||
)
|
||||
|
||||
qs = User.objects.all().order_by("-id")
|
||||
role_filter = data.get("role")
|
||||
if role_filter:
|
||||
qs = qs.filter(role=role_filter)
|
||||
|
||||
users = [_user_to_dict(u, request) for u in qs]
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "success",
|
||||
"users": users,
|
||||
"total_count": len(users),
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class UserCreateAPI(APIView):
|
||||
"""
|
||||
Create a user (admin / manager / staff only).
|
||||
Body: token, username, new_username, email, password, role ('admin'|'manager'|'staff'), phone_number (optional).
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request, error_status_code=True)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
allowed_roles = ["admin", "manager", "staff"]
|
||||
if user.role not in allowed_roles:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "You are not authorized to create users."},
|
||||
status=403,
|
||||
)
|
||||
|
||||
new_username = data.get("username") or data.get("new_username")
|
||||
email = data.get("email")
|
||||
password = data.get("password")
|
||||
role = data.get("role")
|
||||
|
||||
if not all([new_username, email, password, role]):
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "error",
|
||||
"message": "username, email, password, and role are required.",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
valid_roles = ["admin", "manager", "staff"]
|
||||
if role not in valid_roles:
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "error",
|
||||
"message": f"Invalid role. Must be one of: {', '.join(valid_roles)}",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
if User.objects.filter(username=new_username).exists():
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "Username already exists."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
if User.objects.filter(email=email).exists():
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "Email already exists."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
new_user = User.objects.create_user(
|
||||
username=new_username,
|
||||
email=email,
|
||||
password=password,
|
||||
role=role,
|
||||
phone_number=data.get("phone_number"),
|
||||
)
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "success",
|
||||
"message": f"User created successfully with role: {role}.",
|
||||
"user": _user_to_dict(new_user, request),
|
||||
},
|
||||
status=201,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class UserUpdateAPI(APIView):
|
||||
"""
|
||||
Update a user (admin / manager / staff only).
|
||||
Body: token, username, user_id (required); email, phone_number, role (optional).
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request, error_status_code=True)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
allowed_roles = ["admin", "manager", "staff"]
|
||||
if user.role not in allowed_roles:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "You are not authorized to update users."},
|
||||
status=403,
|
||||
)
|
||||
|
||||
user_id = data.get("user_id")
|
||||
if not user_id:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "user_id is required."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
try:
|
||||
target_user = User.objects.get(id=user_id)
|
||||
except User.DoesNotExist:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "User not found."},
|
||||
status=404,
|
||||
)
|
||||
|
||||
if data.get("email") is not None:
|
||||
new_email = data["email"]
|
||||
if User.objects.filter(email=new_email).exclude(id=user_id).exists():
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "Email already exists for another user."},
|
||||
status=400,
|
||||
)
|
||||
target_user.email = new_email
|
||||
|
||||
if data.get("phone_number") is not None:
|
||||
target_user.phone_number = data["phone_number"] or None
|
||||
|
||||
if data.get("role") is not None:
|
||||
new_role = data["role"]
|
||||
valid_roles = ["admin", "manager", "staff"]
|
||||
if new_role not in valid_roles:
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "error",
|
||||
"message": f"Invalid role. Must be one of: {', '.join(valid_roles)}",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
target_user.role = new_role
|
||||
|
||||
target_user.save()
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "success",
|
||||
"message": "User updated successfully.",
|
||||
"user": _user_to_dict(target_user, request),
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class UserDeleteAPI(APIView):
|
||||
"""
|
||||
Delete a user (admin / manager / staff only).
|
||||
Body: token, username, user_id (required).
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request, error_status_code=True)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
allowed_roles = ["admin", "manager", "staff"]
|
||||
if user.role not in allowed_roles:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "You are not authorized to delete users."},
|
||||
status=403,
|
||||
)
|
||||
|
||||
user_id = data.get("user_id")
|
||||
if not user_id:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "user_id is required."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
try:
|
||||
target_user = User.objects.get(id=user_id)
|
||||
except User.DoesNotExist:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "User not found."},
|
||||
status=404,
|
||||
)
|
||||
|
||||
if target_user.id == user.id:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "You cannot delete your own account."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
username = target_user.username
|
||||
target_user.delete()
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "success",
|
||||
"message": f"User '{username}' deleted successfully.",
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
@@ -88,3 +88,110 @@ class LoginForm(AuthenticationForm):
|
||||
"placeholder": "Enter password"
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
class PartnerUserForm(forms.ModelForm):
|
||||
full_name = forms.CharField(
|
||||
max_length=150,
|
||||
required=True,
|
||||
label="Full Name"
|
||||
)
|
||||
password = forms.CharField(
|
||||
widget=forms.PasswordInput,
|
||||
label="Password",
|
||||
required=True,
|
||||
help_text="Required for new users. Leave blank if you don't want to change the password when editing."
|
||||
)
|
||||
confirm_password = forms.CharField(
|
||||
widget=forms.PasswordInput,
|
||||
label="Confirm Password",
|
||||
required=True
|
||||
)
|
||||
|
||||
phone_number = forms.CharField(
|
||||
max_length=15,
|
||||
required=False,
|
||||
label="Phone Number"
|
||||
)
|
||||
|
||||
ROLE_CHOICES = [
|
||||
('partner', 'Partner'),
|
||||
('partner_manager', 'Partner Manager'),
|
||||
('partner_staff', 'Partner Staff'),
|
||||
('partner_customer', 'Partner Customer'),
|
||||
]
|
||||
|
||||
role = forms.ChoiceField(
|
||||
choices=ROLE_CHOICES,
|
||||
required=True,
|
||||
label="Role"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["username", "email", "phone_number", "role"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for field in self.fields.values():
|
||||
field.widget.attrs.update({"class": "form-control"})
|
||||
|
||||
# Make password fields optional for updates, required for new users
|
||||
if self.instance and self.instance.pk:
|
||||
self.fields['password'].required = False
|
||||
self.fields['confirm_password'].required = False
|
||||
# Pre-populate full_name from first_name and last_name
|
||||
if self.instance.first_name or self.instance.last_name:
|
||||
self.fields['full_name'].initial = f"{self.instance.first_name} {self.instance.last_name}".strip()
|
||||
else:
|
||||
# For new users, password is required
|
||||
self.fields['password'].required = True
|
||||
self.fields['confirm_password'].required = True
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
password = cleaned_data.get("password")
|
||||
confirm_password = cleaned_data.get("confirm_password")
|
||||
|
||||
# For new users, password is required
|
||||
if not self.instance or not self.instance.pk:
|
||||
if not password:
|
||||
self.add_error("password", "Password is required for new users.")
|
||||
if not confirm_password:
|
||||
self.add_error("confirm_password", "Please confirm your password.")
|
||||
|
||||
# Validate password match if password is provided
|
||||
if password or confirm_password:
|
||||
if password != confirm_password:
|
||||
self.add_error("confirm_password", "Passwords do not match!")
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def save(self, commit=True):
|
||||
user = super().save(commit=False)
|
||||
|
||||
# Set password - required for new users, optional for updates
|
||||
password = self.cleaned_data.get('password')
|
||||
if password:
|
||||
user.set_password(password)
|
||||
elif not user.pk:
|
||||
# New user must have a password
|
||||
raise ValueError("Password is required for new users.")
|
||||
|
||||
# Save phone_number and role to the User model
|
||||
user.phone_number = self.cleaned_data.get("phone_number")
|
||||
user.role = self.cleaned_data.get("role")
|
||||
|
||||
# Handle full_name - split into first_name and last_name
|
||||
full_name = self.cleaned_data.get("full_name", "").strip()
|
||||
if full_name:
|
||||
parts = full_name.split(None, 1)
|
||||
user.first_name = parts[0]
|
||||
if len(parts) > 1:
|
||||
user.last_name = parts[1]
|
||||
else:
|
||||
user.last_name = ""
|
||||
|
||||
if commit:
|
||||
user.save()
|
||||
return user
|
||||
|
||||
18
accounts/migrations/0008_alter_user_role.py
Normal file
18
accounts/migrations/0008_alter_user_role.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.27 on 2026-03-13 16:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0007_alter_user_profile_picture'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='role',
|
||||
field=models.CharField(choices=[('admin', 'Admin'), ('manager', 'Manager'), ('staff', 'Staff'), ('customer', 'Customer'), ('partner', 'Partner'), ('partner_manager', 'Partner Manager'), ('partner_staff', 'Partner Staff'), ('partner_customer', 'Partner Customer')], default='Staff', max_length=20),
|
||||
),
|
||||
]
|
||||
20
accounts/migrations/0009_user_partner.py
Normal file
20
accounts/migrations/0009_user_partner.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 4.2.27 on 2026-03-14 07:00
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('partner', '0001_initial'),
|
||||
('accounts', '0008_alter_user_role'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='partner',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='partner.partner'),
|
||||
),
|
||||
]
|
||||
@@ -2,18 +2,24 @@ from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
|
||||
from accounts.manager import UserManager
|
||||
|
||||
from partner.models import Partner
|
||||
ROLE_CHOICES = (
|
||||
('admin', 'Admin'),
|
||||
('manager', 'Manager'),
|
||||
('staff', 'Staff'),
|
||||
('customer', 'Customer'),
|
||||
('partner', 'Partner'),
|
||||
('partner_manager', 'Partner Manager'),
|
||||
('partner_staff', 'Partner Staff'),
|
||||
('partner_customer', 'Partner Customer'),
|
||||
)
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
phone_number = models.CharField(max_length=15, blank=True, null=True)
|
||||
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='Staff')
|
||||
|
||||
partner = models.ForeignKey(Partner, on_delete=models.CASCADE, blank=True, null=True)
|
||||
|
||||
is_staff = models.BooleanField(default=False)
|
||||
is_customer = models.BooleanField(default=False)
|
||||
is_user = models.BooleanField(default=False)
|
||||
|
||||
@@ -1,14 +1,37 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
from . import views, api
|
||||
|
||||
app_name = 'accounts'
|
||||
app_name = "accounts"
|
||||
|
||||
urlpatterns = [
|
||||
path('login/', views.login_view, name='login'),
|
||||
path('logout/', views.logout_view, name='logout'),
|
||||
path('dashboard/', views.dashboard, name='dashboard'),
|
||||
path('users/', views.UserListView.as_view(), name='user_list'),
|
||||
path('users/add/', views.UserCreateView.as_view(), name='user_add'),
|
||||
path('users/<int:pk>/edit/', views.UserUpdateView.as_view(), name='user_edit'),
|
||||
path('users/<int:pk>/delete/', views.UserDeleteView.as_view(), name='user_delete'),
|
||||
path("login/", views.login_view, name="login"),
|
||||
path("logout/", views.logout_view, name="logout"),
|
||||
path("dashboard/", views.dashboard, name="dashboard"),
|
||||
path("users/", views.UserListView.as_view(), name="user_list"),
|
||||
path("users/add/", views.UserCreateView.as_view(), name="user_add"),
|
||||
path("users/<int:pk>/edit/", views.UserUpdateView.as_view(), name="user_edit"),
|
||||
path("users/<int:pk>/delete/", views.UserDeleteView.as_view(), name="user_delete"),
|
||||
]
|
||||
|
||||
|
||||
# Core account APIs (admin/staff)
|
||||
urlpatterns += [
|
||||
path("api/login/", api.LoginAPI.as_view(), name="api_login"),
|
||||
path("api/logout/", api.LogoutAPI.as_view(), name="api_logout"),
|
||||
path("api/users/list/", api.UserListAPI.as_view(), name="api_user_list"),
|
||||
path("api/users/create/", api.UserCreateAPI.as_view(), name="api_user_create"),
|
||||
path("api/users/update/", api.UserUpdateAPI.as_view(), name="api_user_update"),
|
||||
path("api/users/delete/", api.UserDeleteAPI.as_view(), name="api_user_delete"),
|
||||
]
|
||||
|
||||
|
||||
# Partner APIs
|
||||
urlpatterns += [
|
||||
path("api/partner/login/", api.PartnerLoginAPI.as_view(), name="partner_api_login"),
|
||||
path("api/partner/logout/", api.PartnerLogoutAPI.as_view(), name="partner_api_logout"),
|
||||
path("api/partner/dashboard/", api.PartnerDashboardAPI.as_view(), name="partner_api_dashboard"),
|
||||
path("api/partner/users/list/", api.PartnerListUsersAPI.as_view(), name="partner_api_user_list"),
|
||||
path("api/partner/users/create/", api.PartnerCreateUserAPI.as_view(), name="partner_api_user_create"),
|
||||
path("api/partner/users/update/", api.PartnerUpdateUserAPI.as_view(), name="partner_api_user_update"),
|
||||
path("api/partner/users/delete/", api.PartnerDeleteUserAPI.as_view(), name="partner_api_user_delete"),
|
||||
]
|
||||
@@ -1,16 +1,16 @@
|
||||
from django.shortcuts import render
|
||||
from django.shortcuts import render, redirect
|
||||
from django.views import generic
|
||||
from django.urls import reverse_lazy
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import authenticate, login, logout
|
||||
|
||||
from .models import User
|
||||
from .forms import LoginForm
|
||||
from .forms import UserForm
|
||||
from .forms import LoginForm, UserForm, PartnerUserForm
|
||||
from events.models import Event
|
||||
from master_data.models import EventType
|
||||
|
||||
from django.contrib.auth import authenticate, login, logout
|
||||
from django.shortcuts import redirect
|
||||
from django.contrib import messages
|
||||
from eventify_logger.services import log
|
||||
|
||||
|
||||
def dashboard(request):
|
||||
@@ -62,16 +62,150 @@ def login_view(request):
|
||||
user = form.get_user()
|
||||
login(request, user)
|
||||
if user.role == 'admin' or user.role == 'manager' or user.role == 'staff':
|
||||
log("info", "Admin/Manager/Staff login", request=request, user=user)
|
||||
return redirect("accounts:dashboard")
|
||||
else:
|
||||
log("warning", "Login attempt - user not authorized", request=request, user=user)
|
||||
messages.error(request, "You are not authorized to access this page.")
|
||||
else:
|
||||
log("warning", "Invalid login attempt", request=request)
|
||||
messages.error(request, "Invalid username or password")
|
||||
|
||||
return render(request, "accounts/login.html", {"form": form})
|
||||
|
||||
|
||||
def logout_view(request):
|
||||
if request.user.is_authenticated:
|
||||
log("info", "User logout", request=request, user=request.user)
|
||||
logout(request)
|
||||
messages.success(request, "You have been logged out successfully.")
|
||||
return redirect("accounts:login")
|
||||
return redirect("accounts:login")
|
||||
|
||||
|
||||
# Partner Views Mixin
|
||||
class PartnerRequiredMixin(LoginRequiredMixin):
|
||||
"""Mixin to ensure user has partner role (partner, partner_manager, partner_staff)"""
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.user.is_authenticated:
|
||||
return self.handle_no_permission()
|
||||
partner_roles = ['partner', 'partner_manager', 'partner_staff']
|
||||
if request.user.role not in partner_roles:
|
||||
raise PermissionDenied("You are not authorized to access this page.")
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
# Partner Login/Logout/Dashboard
|
||||
def partner_login_view(request):
|
||||
if request.user.is_authenticated:
|
||||
partner_roles = ['partner', 'partner_manager', 'partner_staff']
|
||||
if request.user.role in partner_roles:
|
||||
return redirect("accounts:partner_dashboard")
|
||||
else:
|
||||
messages.error(request, "You are not authorized to access partner portal.")
|
||||
return redirect("accounts:login")
|
||||
|
||||
form = LoginForm(request, data=request.POST or None)
|
||||
|
||||
if request.method == "POST":
|
||||
if form.is_valid():
|
||||
user = form.get_user()
|
||||
partner_roles = ['partner', 'partner_manager', 'partner_staff']
|
||||
if user.role in partner_roles:
|
||||
log("info", "Partner portal login", request=request, user=user)
|
||||
login(request, user)
|
||||
return redirect("accounts:partner_dashboard")
|
||||
else:
|
||||
log("warning", "Partner login - user not authorized", request=request, user=user)
|
||||
messages.error(request, "You are not authorized to access partner portal.")
|
||||
else:
|
||||
log("warning", "Partner portal - invalid login attempt", request=request)
|
||||
messages.error(request, "Invalid username or password")
|
||||
|
||||
return render(request, "partner/login.html", {"form": form})
|
||||
|
||||
|
||||
def partner_logout_view(request):
|
||||
if request.user.is_authenticated:
|
||||
log("info", "Partner portal logout", request=request, user=request.user)
|
||||
logout(request)
|
||||
messages.success(request, "You have been logged out successfully.")
|
||||
return redirect("accounts:partner_login")
|
||||
|
||||
|
||||
def partner_dashboard(request):
|
||||
"""Partner dashboard view"""
|
||||
partner_roles = ['partner', 'partner_manager', 'partner_staff']
|
||||
if not request.user.is_authenticated or request.user.role not in partner_roles:
|
||||
messages.error(request, "You are not authorized to access this page.")
|
||||
return redirect("accounts:partner_login")
|
||||
|
||||
# Get statistics for partner users (including partner_customer)
|
||||
all_partner_roles = ['partner', 'partner_manager', 'partner_staff', 'partner_customer']
|
||||
partner_users = User.objects.filter(role__in=all_partner_roles)
|
||||
total_partner_users = partner_users.count()
|
||||
|
||||
# You can add more partner-specific statistics here
|
||||
# For example, events created by partner, bookings, etc.
|
||||
|
||||
return render(request, 'partner/dashboard.html', {
|
||||
'total_partner_users': total_partner_users,
|
||||
})
|
||||
|
||||
|
||||
# Partner User Management Views
|
||||
class PartnerUserListView(PartnerRequiredMixin, generic.ListView):
|
||||
model = User
|
||||
template_name = 'partner/user_list.html'
|
||||
context_object_name = 'users'
|
||||
paginate_by = 20
|
||||
|
||||
def get_queryset(self):
|
||||
"""Filter users to show only partner-related roles"""
|
||||
partner_roles = ['partner', 'partner_manager', 'partner_staff', 'partner_customer']
|
||||
return User.objects.filter(role__in=partner_roles).order_by('-id')
|
||||
|
||||
|
||||
class PartnerUserCreateView(PartnerRequiredMixin, generic.CreateView):
|
||||
model = User
|
||||
form_class = PartnerUserForm
|
||||
template_name = 'partner/user_form.html'
|
||||
success_url = reverse_lazy('accounts:partner_user_list')
|
||||
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, "Partner user created successfully.")
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class PartnerUserUpdateView(PartnerRequiredMixin, generic.UpdateView):
|
||||
model = User
|
||||
form_class = PartnerUserForm
|
||||
template_name = 'partner/user_form.html'
|
||||
success_url = reverse_lazy('accounts:partner_user_list')
|
||||
|
||||
def get_queryset(self):
|
||||
"""Only allow editing users with partner-related roles"""
|
||||
partner_roles = ['partner', 'partner_manager', 'partner_staff', 'partner_customer']
|
||||
return User.objects.filter(role__in=partner_roles)
|
||||
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, "Partner user updated successfully.")
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class PartnerUserDeleteView(PartnerRequiredMixin, generic.DeleteView):
|
||||
model = User
|
||||
template_name = 'partner/user_confirm_delete.html'
|
||||
success_url = reverse_lazy('accounts:partner_user_list')
|
||||
|
||||
def get_queryset(self):
|
||||
"""Only allow deleting users with partner-related roles"""
|
||||
partner_roles = ['partner', 'partner_manager', 'partner_staff', 'partner_customer']
|
||||
return User.objects.filter(role__in=partner_roles)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
# Prevent users from deleting themselves
|
||||
if self.get_object().id == request.user.id:
|
||||
messages.error(request, "You cannot delete your own account.")
|
||||
return redirect(self.success_url)
|
||||
messages.success(request, "Partner user deleted successfully.")
|
||||
return super().delete(request, *args, **kwargs)
|
||||
0
banking_operations/__init__.py
Normal file
0
banking_operations/__init__.py
Normal file
3
banking_operations/admin.py
Normal file
3
banking_operations/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
432
banking_operations/api.py
Normal file
432
banking_operations/api.py
Normal file
@@ -0,0 +1,432 @@
|
||||
import uuid
|
||||
from django.http import JsonResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.forms.models import model_to_dict
|
||||
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from banking_operations.models import PaymentGateway, PaymentGatewayCredentials
|
||||
from mobile_api.utils import validate_token_and_get_user
|
||||
|
||||
|
||||
def _payment_gateway_to_dict(gateway, request=None):
|
||||
"""Serialize PaymentGateway for JSON."""
|
||||
data = model_to_dict(
|
||||
gateway,
|
||||
fields=[
|
||||
"id",
|
||||
"payment_gateway_id",
|
||||
"payment_gateway_name",
|
||||
"payment_gateway_description",
|
||||
"payment_gateway_url",
|
||||
"payment_gateway_api_key",
|
||||
"payment_gateway_api_secret",
|
||||
"payment_gateway_api_url",
|
||||
"payment_gateway_api_version",
|
||||
"payment_gateway_api_method",
|
||||
"is_active",
|
||||
"created_date",
|
||||
"updated_date",
|
||||
"gateway_priority",
|
||||
],
|
||||
)
|
||||
# Add logo URL if exists
|
||||
if gateway.payment_gateway_logo:
|
||||
if request:
|
||||
data["payment_gateway_logo"] = request.build_absolute_uri(gateway.payment_gateway_logo.url)
|
||||
else:
|
||||
data["payment_gateway_logo"] = gateway.payment_gateway_logo.url
|
||||
else:
|
||||
data["payment_gateway_logo"] = None
|
||||
return data
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class PaymentGatewayCreateAPI(APIView):
|
||||
"""
|
||||
Create a new PaymentGateway.
|
||||
Body: token, username, payment_gateway_name, payment_gateway_description,
|
||||
payment_gateway_api_key, payment_gateway_api_secret, payment_gateway_api_version,
|
||||
payment_gateway_api_method (required);
|
||||
payment_gateway_logo (file upload), payment_gateway_url, payment_gateway_api_url,
|
||||
is_active, gateway_priority (optional).
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
payment_gateway_name = data.get("payment_gateway_name")
|
||||
payment_gateway_description = data.get("payment_gateway_description")
|
||||
payment_gateway_api_key = data.get("payment_gateway_api_key")
|
||||
payment_gateway_api_secret = data.get("payment_gateway_api_secret")
|
||||
payment_gateway_api_version = data.get("payment_gateway_api_version")
|
||||
payment_gateway_api_method = data.get("payment_gateway_api_method")
|
||||
|
||||
if not all([
|
||||
payment_gateway_name,
|
||||
payment_gateway_description,
|
||||
payment_gateway_api_key,
|
||||
payment_gateway_api_secret,
|
||||
payment_gateway_api_version,
|
||||
payment_gateway_api_method,
|
||||
]):
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "error",
|
||||
"message": "payment_gateway_name, payment_gateway_description, payment_gateway_api_key, "
|
||||
"payment_gateway_api_secret, payment_gateway_api_version, and "
|
||||
"payment_gateway_api_method are required.",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
# Generate payment_gateway_id if not provided
|
||||
payment_gateway_id = data.get("payment_gateway_id")
|
||||
if not payment_gateway_id:
|
||||
payment_gateway_id = str(uuid.uuid4().hex[:10]).upper()
|
||||
|
||||
gateway = PaymentGateway.objects.create(
|
||||
payment_gateway_id=payment_gateway_id,
|
||||
payment_gateway_name=payment_gateway_name,
|
||||
payment_gateway_description=payment_gateway_description,
|
||||
payment_gateway_url=data.get("payment_gateway_url"),
|
||||
payment_gateway_api_key=payment_gateway_api_key,
|
||||
payment_gateway_api_secret=payment_gateway_api_secret,
|
||||
payment_gateway_api_url=data.get("payment_gateway_api_url"),
|
||||
payment_gateway_api_version=payment_gateway_api_version,
|
||||
payment_gateway_api_method=payment_gateway_api_method,
|
||||
is_active=data.get("is_active", True),
|
||||
gateway_priority=data.get("gateway_priority", 0),
|
||||
)
|
||||
|
||||
# Handle logo upload if provided
|
||||
if "payment_gateway_logo" in request.FILES:
|
||||
gateway.payment_gateway_logo = request.FILES["payment_gateway_logo"]
|
||||
gateway.save()
|
||||
|
||||
return JsonResponse(
|
||||
{"status": "success", "payment_gateway": _payment_gateway_to_dict(gateway, request)},
|
||||
status=201,
|
||||
)
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class PaymentGatewayListAPI(APIView):
|
||||
"""
|
||||
List PaymentGateways, optionally filtered by is_active.
|
||||
Body: token, username, is_active (optional).
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
qs = PaymentGateway.objects.all().order_by("-gateway_priority", "-created_date")
|
||||
|
||||
is_active = data.get("is_active")
|
||||
if is_active is not None:
|
||||
qs = qs.filter(is_active=bool(is_active))
|
||||
|
||||
gateways = [_payment_gateway_to_dict(g, request) for g in qs]
|
||||
return JsonResponse({"status": "success", "payment_gateways": gateways}, status=200)
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class PaymentGatewayUpdateAPI(APIView):
|
||||
"""
|
||||
Update an existing PaymentGateway.
|
||||
Body: token, username, payment_gateway_id (required);
|
||||
payment_gateway_name, payment_gateway_description, payment_gateway_url,
|
||||
payment_gateway_api_key, payment_gateway_api_secret, payment_gateway_api_url,
|
||||
payment_gateway_api_version, payment_gateway_api_method, is_active,
|
||||
gateway_priority (optional);
|
||||
payment_gateway_logo (file upload, optional).
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
payment_gateway_id = data.get("payment_gateway_id")
|
||||
if not payment_gateway_id:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "payment_gateway_id is required."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
try:
|
||||
gateway = PaymentGateway.objects.get(payment_gateway_id=payment_gateway_id)
|
||||
except PaymentGateway.DoesNotExist:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "PaymentGateway not found."},
|
||||
status=404,
|
||||
)
|
||||
|
||||
# Update fields if provided
|
||||
if data.get("payment_gateway_name") is not None:
|
||||
gateway.payment_gateway_name = data["payment_gateway_name"]
|
||||
if data.get("payment_gateway_description") is not None:
|
||||
gateway.payment_gateway_description = data["payment_gateway_description"]
|
||||
if "payment_gateway_url" in data:
|
||||
gateway.payment_gateway_url = data["payment_gateway_url"] or None
|
||||
if data.get("payment_gateway_api_key") is not None:
|
||||
gateway.payment_gateway_api_key = data["payment_gateway_api_key"]
|
||||
if data.get("payment_gateway_api_secret") is not None:
|
||||
gateway.payment_gateway_api_secret = data["payment_gateway_api_secret"]
|
||||
if "payment_gateway_api_url" in data:
|
||||
gateway.payment_gateway_api_url = data["payment_gateway_api_url"] or None
|
||||
if data.get("payment_gateway_api_version") is not None:
|
||||
gateway.payment_gateway_api_version = data["payment_gateway_api_version"]
|
||||
if data.get("payment_gateway_api_method") is not None:
|
||||
gateway.payment_gateway_api_method = data["payment_gateway_api_method"]
|
||||
if data.get("is_active") is not None:
|
||||
gateway.is_active = bool(data["is_active"])
|
||||
if data.get("gateway_priority") is not None:
|
||||
try:
|
||||
gateway.gateway_priority = int(data["gateway_priority"])
|
||||
except (TypeError, ValueError):
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "gateway_priority must be an integer."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
# Handle logo upload if provided
|
||||
if "payment_gateway_logo" in request.FILES:
|
||||
gateway.payment_gateway_logo = request.FILES["payment_gateway_logo"]
|
||||
|
||||
gateway.save()
|
||||
return JsonResponse(
|
||||
{"status": "success", "payment_gateway": _payment_gateway_to_dict(gateway, request)},
|
||||
status=200,
|
||||
)
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class PaymentGatewayDeleteAPI(APIView):
|
||||
"""
|
||||
Delete an existing PaymentGateway.
|
||||
Body: token, username, payment_gateway_id.
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
payment_gateway_id = data.get("payment_gateway_id")
|
||||
if not payment_gateway_id:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "payment_gateway_id is required."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
try:
|
||||
gateway = PaymentGateway.objects.get(payment_gateway_id=payment_gateway_id)
|
||||
except PaymentGateway.DoesNotExist:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "PaymentGateway not found."},
|
||||
status=404,
|
||||
)
|
||||
|
||||
gateway_name = gateway.payment_gateway_name
|
||||
gateway.delete()
|
||||
return JsonResponse(
|
||||
{"status": "success", "message": f"PaymentGateway '{gateway_name}' deleted successfully."},
|
||||
status=200,
|
||||
)
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
def _payment_gateway_credentials_to_dict(credentials, request=None):
|
||||
"""Serialize PaymentGatewayCredentials for JSON."""
|
||||
data = model_to_dict(
|
||||
credentials,
|
||||
fields=[
|
||||
"id",
|
||||
"payment_gateway_credentials_value",
|
||||
"created_date",
|
||||
"updated_date",
|
||||
],
|
||||
)
|
||||
data["payment_gateway_id"] = credentials.payment_gateway_id
|
||||
data["payment_gateway_name"] = credentials.payment_gateway.payment_gateway_name
|
||||
data["payment_gateway_payment_gateway_id"] = credentials.payment_gateway.payment_gateway_id
|
||||
return data
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class PaymentGatewayCredentialsCreateAPI(APIView):
|
||||
"""
|
||||
Create a new PaymentGatewayCredentials.
|
||||
Body: token, username, payment_gateway_id (or payment_gateway_payment_gateway_id),
|
||||
payment_gateway_credentials_value (required).
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
payment_gateway_id = data.get("payment_gateway_id") or data.get("payment_gateway_payment_gateway_id")
|
||||
payment_gateway_credentials_value = data.get("payment_gateway_credentials_value")
|
||||
|
||||
if not payment_gateway_id or not payment_gateway_credentials_value:
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "error",
|
||||
"message": "payment_gateway_id (or payment_gateway_payment_gateway_id) and payment_gateway_credentials_value are required.",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
try:
|
||||
gateway = PaymentGateway.objects.get(payment_gateway_id=payment_gateway_id)
|
||||
except PaymentGateway.DoesNotExist:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "PaymentGateway not found."},
|
||||
status=404,
|
||||
)
|
||||
|
||||
credentials = PaymentGatewayCredentials.objects.create(
|
||||
payment_gateway=gateway,
|
||||
payment_gateway_credentials_value=payment_gateway_credentials_value,
|
||||
)
|
||||
|
||||
return JsonResponse(
|
||||
{"status": "success", "credentials": _payment_gateway_credentials_to_dict(credentials, request)},
|
||||
status=201,
|
||||
)
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class PaymentGatewayCredentialsListAPI(APIView):
|
||||
"""
|
||||
List PaymentGatewayCredentials, optionally filtered by payment_gateway_id.
|
||||
Body: token, username, payment_gateway_id (optional).
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
qs = PaymentGatewayCredentials.objects.select_related("payment_gateway").all().order_by("-created_date")
|
||||
|
||||
payment_gateway_id = data.get("payment_gateway_id") or data.get("payment_gateway_payment_gateway_id")
|
||||
if payment_gateway_id:
|
||||
try:
|
||||
gateway = PaymentGateway.objects.get(payment_gateway_id=payment_gateway_id)
|
||||
qs = qs.filter(payment_gateway=gateway)
|
||||
except PaymentGateway.DoesNotExist:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "PaymentGateway not found."},
|
||||
status=404,
|
||||
)
|
||||
|
||||
credentials_list = [_payment_gateway_credentials_to_dict(c, request) for c in qs]
|
||||
return JsonResponse({"status": "success", "credentials": credentials_list}, status=200)
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class PaymentGatewayCredentialsUpdateAPI(APIView):
|
||||
"""
|
||||
Update an existing PaymentGatewayCredentials.
|
||||
Body: token, username, credentials_id (required);
|
||||
payment_gateway_credentials_value (optional).
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
credentials_id = data.get("credentials_id")
|
||||
if not credentials_id:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "credentials_id is required."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
try:
|
||||
credentials = PaymentGatewayCredentials.objects.select_related("payment_gateway").get(id=credentials_id)
|
||||
except PaymentGatewayCredentials.DoesNotExist:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "PaymentGatewayCredentials not found."},
|
||||
status=404,
|
||||
)
|
||||
|
||||
# Update credentials value if provided
|
||||
if data.get("payment_gateway_credentials_value") is not None:
|
||||
credentials.payment_gateway_credentials_value = data["payment_gateway_credentials_value"]
|
||||
|
||||
credentials.save()
|
||||
return JsonResponse(
|
||||
{"status": "success", "credentials": _payment_gateway_credentials_to_dict(credentials, request)},
|
||||
status=200,
|
||||
)
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class PaymentGatewayCredentialsDeleteAPI(APIView):
|
||||
"""
|
||||
Delete an existing PaymentGatewayCredentials.
|
||||
Body: token, username, credentials_id.
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
credentials_id = data.get("credentials_id")
|
||||
if not credentials_id:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "credentials_id is required."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
try:
|
||||
credentials = PaymentGatewayCredentials.objects.select_related("payment_gateway").get(id=credentials_id)
|
||||
except PaymentGatewayCredentials.DoesNotExist:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "PaymentGatewayCredentials not found."},
|
||||
status=404,
|
||||
)
|
||||
|
||||
gateway_name = credentials.payment_gateway.payment_gateway_name
|
||||
credentials.delete()
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "success",
|
||||
"message": f"PaymentGatewayCredentials for '{gateway_name}' deleted successfully.",
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
6
banking_operations/apps.py
Normal file
6
banking_operations/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BankingOperationsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'banking_operations'
|
||||
69
banking_operations/migrations/0001_initial.py
Normal file
69
banking_operations/migrations/0001_initial.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# Generated by Django 4.2.27 on 2026-03-13 16:55
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PaymentGateway',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('payment_gateway_id', models.CharField(max_length=250)),
|
||||
('payment_gateway_name', models.CharField(max_length=250)),
|
||||
('payment_gateway_description', models.TextField()),
|
||||
('payment_gateway_logo', models.ImageField(blank=True, null=True, upload_to='payment_gateways/')),
|
||||
('payment_gateway_url', models.URLField(blank=True, null=True)),
|
||||
('payment_gateway_api_key', models.CharField(max_length=250)),
|
||||
('payment_gateway_api_secret', models.CharField(max_length=250)),
|
||||
('payment_gateway_api_url', models.URLField(blank=True, null=True)),
|
||||
('payment_gateway_api_version', models.CharField(max_length=250)),
|
||||
('payment_gateway_api_method', models.CharField(max_length=250)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('created_date', models.DateField(auto_now_add=True)),
|
||||
('updated_date', models.DateField(auto_now=True)),
|
||||
('gateway_priority', models.IntegerField(default=0)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PaymentTransaction',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('payment_transaction_id', models.CharField(max_length=250)),
|
||||
('payment_type', models.CharField(choices=[('credit', 'Credit'), ('debit', 'Debit'), ('transfer', 'Transfer'), ('other', 'Other')], max_length=250)),
|
||||
('payment_sub_type', models.CharField(choices=[('online', 'Online'), ('offline', 'Offline'), ('other', 'Other')], max_length=250)),
|
||||
('payment_transaction_amount', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
('payment_transaction_currency', models.CharField(max_length=10)),
|
||||
('payment_transaction_status', models.CharField(choices=[('pending', 'Pending'), ('completed', 'Completed'), ('failed', 'Failed'), ('refunded', 'Refunded'), ('cancelled', 'Cancelled')], max_length=250)),
|
||||
('payment_transaction_date', models.DateField(auto_now_add=True)),
|
||||
('payment_transaction_time', models.TimeField(auto_now_add=True)),
|
||||
('payment_transaction_notes', models.TextField(blank=True, null=True)),
|
||||
('payment_transaction_raw_data', models.JSONField(blank=True, null=True)),
|
||||
('payment_transaction_response', models.JSONField(blank=True, null=True)),
|
||||
('payment_transaction_error', models.JSONField(blank=True, null=True)),
|
||||
('last_updated_date', models.DateField(blank=True, null=True)),
|
||||
('last_updated_time', models.TimeField(blank=True, null=True)),
|
||||
('last_updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
('payment_gateway', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='banking_operations.paymentgateway')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PaymentGatewayCredentials',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('payment_gateway_credentials_value', models.TextField()),
|
||||
('created_date', models.DateField(auto_now_add=True)),
|
||||
('updated_date', models.DateField(auto_now=True)),
|
||||
('payment_gateway', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='banking_operations.paymentgateway')),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
banking_operations/migrations/__init__.py
Normal file
0
banking_operations/migrations/__init__.py
Normal file
85
banking_operations/models.py
Normal file
85
banking_operations/models.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
import uuid
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
# Create your models here.
|
||||
class PaymentGateway(models.Model):
|
||||
payment_gateway_id = models.CharField(max_length=250)
|
||||
payment_gateway_name = models.CharField(max_length=250)
|
||||
payment_gateway_description = models.TextField()
|
||||
payment_gateway_logo = models.ImageField(upload_to='payment_gateways/', blank=True, null=True)
|
||||
payment_gateway_url = models.URLField(blank=True, null=True)
|
||||
payment_gateway_api_key = models.CharField(max_length=250)
|
||||
payment_gateway_api_secret = models.CharField(max_length=250)
|
||||
payment_gateway_api_url = models.URLField(blank=True, null=True)
|
||||
payment_gateway_api_version = models.CharField(max_length=250)
|
||||
payment_gateway_api_method = models.CharField(max_length=250)
|
||||
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
created_date = models.DateField(auto_now_add=True)
|
||||
updated_date = models.DateField(auto_now=True)
|
||||
|
||||
gateway_priority = models.IntegerField(default=0)
|
||||
|
||||
def __str__(self):
|
||||
return self.payment_gateway_name
|
||||
|
||||
def __save__(self):
|
||||
if not self.payment_gateway_id:
|
||||
self.payment_gateway_id = str(uuid.uuid4().hex[:10]).upper()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class PaymentGatewayCredentials(models.Model):
|
||||
payment_gateway = models.ForeignKey(PaymentGateway, on_delete=models.CASCADE)
|
||||
payment_gateway_credentials_value = models.TextField()
|
||||
created_date = models.DateField(auto_now_add=True)
|
||||
updated_date = models.DateField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.payment_gateway.payment_gateway_name + " - " + self.payment_gateway_credentials_value
|
||||
|
||||
|
||||
class PaymentTransaction(models.Model):
|
||||
payment_transaction_id = models.CharField(max_length=250)
|
||||
payment_type = models.CharField(max_length=250, choices=[
|
||||
('credit', 'Credit'),
|
||||
('debit', 'Debit'),
|
||||
('transfer', 'Transfer'),
|
||||
('other', 'Other'),
|
||||
])
|
||||
payment_sub_type = models.CharField(max_length=250, choices=[
|
||||
('online', 'Online'),
|
||||
('offline', 'Offline'),
|
||||
('other', 'Other'),
|
||||
])
|
||||
payment_gateway = models.ForeignKey(PaymentGateway, on_delete=models.CASCADE)
|
||||
payment_transaction_amount = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
payment_transaction_currency = models.CharField(max_length=10)
|
||||
payment_transaction_status = models.CharField(max_length=250, choices=[
|
||||
('pending', 'Pending'),
|
||||
('completed', 'Completed'),
|
||||
('failed', 'Failed'),
|
||||
('refunded', 'Refunded'),
|
||||
('cancelled', 'Cancelled'),
|
||||
])
|
||||
payment_transaction_date = models.DateField(auto_now_add=True)
|
||||
payment_transaction_time = models.TimeField(auto_now_add=True)
|
||||
payment_transaction_notes = models.TextField(blank=True, null=True)
|
||||
payment_transaction_raw_data = models.JSONField(blank=True, null=True)
|
||||
payment_transaction_response = models.JSONField(blank=True, null=True)
|
||||
payment_transaction_error = models.JSONField(blank=True, null=True)
|
||||
|
||||
last_updated_date = models.DateField(blank=True, null=True)
|
||||
last_updated_time = models.TimeField(blank=True, null=True)
|
||||
last_updated_by = models.ForeignKey(User, on_delete=models.CASCADE, blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.payment_gateway.payment_gateway_name + " - " + self.payment_transaction_id
|
||||
|
||||
def __save__(self):
|
||||
if not self.payment_transaction_id:
|
||||
self.payment_transaction_id = str(self.payment_gateway.payment_gateway_name[:3].upper()) + str(uuid.uuid4().hex[:10]).upper()
|
||||
super().save(*args, **kwargs)
|
||||
170
banking_operations/services.py
Normal file
170
banking_operations/services.py
Normal file
@@ -0,0 +1,170 @@
|
||||
"""
|
||||
Banking/payment services. transaction_initiate is called by checkout (and others)
|
||||
to start a payment flow. Replace the stub with real gateway integration (e.g. Razorpay).
|
||||
"""
|
||||
from decimal import Decimal
|
||||
from django.contrib.auth import get_user_model
|
||||
from banking_operations.models import PaymentTransaction, PaymentGateway
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
def transaction_initiate(
|
||||
request,
|
||||
user,
|
||||
amount,
|
||||
currency="INR",
|
||||
reference_type="checkout",
|
||||
reference_id=None,
|
||||
bookings=None,
|
||||
extra_data=None,
|
||||
):
|
||||
"""
|
||||
Initiate a payment transaction (e.g. create Razorpay order and return payment URL).
|
||||
|
||||
Args:
|
||||
request: Django request (for building URLs, gateway config, etc.).
|
||||
user: User instance (customer).
|
||||
amount: Total amount in payment currency (Decimal or float).
|
||||
currency: Currency code, e.g. "INR".
|
||||
reference_type: Application reference type, e.g. "checkout".
|
||||
reference_id: Application reference id (e.g. booking_ids or order id).
|
||||
bookings: Optional list of Booking instances or IDs linked to this transaction.
|
||||
extra_data: Optional dict for gateway-specific data.
|
||||
|
||||
Returns:
|
||||
dict: On success: {"success": True, "transaction_id": "...", "payment_url": "...", "message": "..."}
|
||||
On failure: {"success": False, "message": "..."}
|
||||
"""
|
||||
# Stub: replace with real gateway call when banking_operations payment flow is implemented.
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Transaction initiation stub",
|
||||
"transaction_id": None,
|
||||
"payment_url": None,
|
||||
}
|
||||
|
||||
|
||||
def create_payment_transaction(
|
||||
payment_type,
|
||||
payment_sub_type,
|
||||
payment_gateway,
|
||||
transaction_amount,
|
||||
currency="INR",
|
||||
notes=None,
|
||||
raw_data=None,
|
||||
user=None,
|
||||
):
|
||||
"""
|
||||
Create a PaymentTransaction with pending status.
|
||||
|
||||
Args:
|
||||
payment_type: Payment type - 'credit', 'debit', 'transfer', or 'other'
|
||||
payment_sub_type: Payment sub type - 'online', 'offline', or 'other'
|
||||
payment_gateway: PaymentGateway instance or payment_gateway_id (str)
|
||||
transaction_amount: Transaction amount (Decimal, float, or string)
|
||||
currency: Currency code, e.g. "INR" (default: "INR")
|
||||
notes: Optional transaction notes (str)
|
||||
raw_data: Optional raw data dict to store in payment_transaction_raw_data (dict)
|
||||
user: Optional User instance for last_updated_by
|
||||
|
||||
Returns:
|
||||
tuple: (success: bool, transaction: PaymentTransaction or None, error_message: str or None)
|
||||
"""
|
||||
try:
|
||||
# Validate payment_type
|
||||
valid_payment_types = ['credit', 'debit', 'transfer', 'other']
|
||||
if payment_type not in valid_payment_types:
|
||||
return False, None, f"Invalid payment_type. Must be one of: {', '.join(valid_payment_types)}"
|
||||
|
||||
# Validate payment_sub_type
|
||||
valid_payment_sub_types = ['online', 'offline', 'other']
|
||||
if payment_sub_type not in valid_payment_sub_types:
|
||||
return False, None, f"Invalid payment_sub_type. Must be one of: {', '.join(valid_payment_sub_types)}"
|
||||
|
||||
# Get PaymentGateway instance
|
||||
if isinstance(payment_gateway, PaymentGateway):
|
||||
gateway = payment_gateway
|
||||
elif isinstance(payment_gateway, str):
|
||||
try:
|
||||
gateway = PaymentGateway.objects.get(payment_gateway_id=payment_gateway)
|
||||
except PaymentGateway.DoesNotExist:
|
||||
return False, None, f"PaymentGateway with id '{payment_gateway}' not found."
|
||||
else:
|
||||
return False, None, "payment_gateway must be a PaymentGateway instance or payment_gateway_id string."
|
||||
|
||||
# Validate transaction_amount
|
||||
try:
|
||||
amount = Decimal(str(transaction_amount))
|
||||
if amount <= 0:
|
||||
return False, None, "transaction_amount must be greater than zero."
|
||||
except (ValueError, TypeError):
|
||||
return False, None, "transaction_amount must be a valid number."
|
||||
|
||||
# Validate currency
|
||||
if not currency or len(currency) > 10:
|
||||
return False, None, "currency must be a valid currency code (max 10 characters)."
|
||||
|
||||
# Create PaymentTransaction
|
||||
transaction = PaymentTransaction.objects.create(
|
||||
payment_type=payment_type,
|
||||
payment_sub_type=payment_sub_type,
|
||||
payment_gateway=gateway,
|
||||
payment_transaction_amount=amount,
|
||||
payment_transaction_currency=currency,
|
||||
payment_transaction_status='pending', # Initial state as requested
|
||||
payment_transaction_notes=notes,
|
||||
payment_transaction_raw_data=raw_data,
|
||||
last_updated_by=user if isinstance(user, User) else None,
|
||||
)
|
||||
|
||||
return True, transaction.payment_transaction_id, None
|
||||
|
||||
except Exception as e:
|
||||
return False, None, str(e)
|
||||
|
||||
def update_payment_transaction(
|
||||
payment_transaction_id,
|
||||
payment_transaction_status,
|
||||
payment_transaction_notes=None,
|
||||
payment_transaction_raw_data=None,
|
||||
payment_transaction_response=None,
|
||||
payment_transaction_error=None,
|
||||
user=None,
|
||||
):
|
||||
"""
|
||||
Update a PaymentTransaction with the given status and notes.
|
||||
Args:
|
||||
payment_transaction_id: PaymentTransaction id (str)
|
||||
payment_transaction_status: PaymentTransaction status - 'pending', 'completed', 'failed', 'refunded', 'cancelled'
|
||||
payment_transaction_notes: Optional transaction notes (str)
|
||||
payment_transaction_raw_data: Optional raw data dict to store in payment_transaction_raw_data (dict)
|
||||
payment_transaction_response: Optional response dict to store in payment_transaction_response (dict)
|
||||
payment_transaction_error: Optional error dict to store in payment_transaction_error (dict)
|
||||
user: Optional User instance for last_updated_by
|
||||
"""
|
||||
try:
|
||||
# Get PaymentTransaction instance
|
||||
if isinstance(payment_transaction_id, PaymentTransaction):
|
||||
transaction = payment_transaction_id
|
||||
elif isinstance(payment_transaction_id, str):
|
||||
try:
|
||||
transaction = PaymentTransaction.objects.get(payment_transaction_id=payment_transaction_id)
|
||||
except PaymentTransaction.DoesNotExist:
|
||||
return False, None, f"PaymentTransaction with id '{payment_transaction_id}' not found."
|
||||
else:
|
||||
return False, None, "payment_transaction_id must be a PaymentTransaction instance or payment_transaction_id string."
|
||||
|
||||
# Update PaymentTransaction
|
||||
transaction.payment_transaction_status = payment_transaction_status
|
||||
transaction.payment_transaction_notes = payment_transaction_notes
|
||||
transaction.payment_transaction_raw_data = payment_transaction_raw_data
|
||||
transaction.payment_transaction_response = payment_transaction_response
|
||||
transaction.payment_transaction_error = payment_transaction_error
|
||||
transaction.last_updated_by = user if isinstance(user, User) else None
|
||||
transaction.save()
|
||||
|
||||
return True, transaction.payment_transaction_id, None
|
||||
|
||||
except Exception as e:
|
||||
return False, None, str(e)
|
||||
3
banking_operations/tests.py
Normal file
3
banking_operations/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
24
banking_operations/urls.py
Normal file
24
banking_operations/urls.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from django.urls import path
|
||||
|
||||
from banking_operations.api import (
|
||||
PaymentGatewayCreateAPI,
|
||||
PaymentGatewayListAPI,
|
||||
PaymentGatewayUpdateAPI,
|
||||
PaymentGatewayDeleteAPI,
|
||||
PaymentGatewayCredentialsCreateAPI,
|
||||
PaymentGatewayCredentialsListAPI,
|
||||
PaymentGatewayCredentialsUpdateAPI,
|
||||
PaymentGatewayCredentialsDeleteAPI,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("payment-gateway/create/", PaymentGatewayCreateAPI.as_view(), name="payment_gateway_create"),
|
||||
path("payment-gateway/list/", PaymentGatewayListAPI.as_view(), name="payment_gateway_list"),
|
||||
path("payment-gateway/update/", PaymentGatewayUpdateAPI.as_view(), name="payment_gateway_update"),
|
||||
path("payment-gateway/delete/", PaymentGatewayDeleteAPI.as_view(), name="payment_gateway_delete"),
|
||||
path("payment-gateway-credentials/create/", PaymentGatewayCredentialsCreateAPI.as_view(), name="payment_gateway_credentials_create"),
|
||||
path("payment-gateway-credentials/list/", PaymentGatewayCredentialsListAPI.as_view(), name="payment_gateway_credentials_list"),
|
||||
path("payment-gateway-credentials/update/", PaymentGatewayCredentialsUpdateAPI.as_view(), name="payment_gateway_credentials_update"),
|
||||
path("payment-gateway-credentials/delete/", PaymentGatewayCredentialsDeleteAPI.as_view(), name="payment_gateway_credentials_delete"),
|
||||
]
|
||||
3
banking_operations/views.py
Normal file
3
banking_operations/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
101
bookings/migrations/0001_initial.py
Normal file
101
bookings/migrations/0001_initial.py
Normal file
@@ -0,0 +1,101 @@
|
||||
# Generated by Django 4.2.27 on 2026-03-13 16:55
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('events', '0007_event_gst_percentage_1_event_gst_percentage_2_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Booking',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('booking_id', models.CharField(max_length=250)),
|
||||
('quantity', models.IntegerField()),
|
||||
('price', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
('created_date', models.DateField(auto_now_add=True)),
|
||||
('updated_date', models.DateField(auto_now=True)),
|
||||
('transaction_id', models.CharField(blank=True, max_length=250, null=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TicketMeta',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('ticket_name', models.CharField(max_length=250)),
|
||||
('maximum_quantity', models.IntegerField()),
|
||||
('available_quantity', models.IntegerField(default=0)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('created_date', models.DateField(auto_now_add=True)),
|
||||
('updated_date', models.DateField(auto_now=True)),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.event')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TicketType',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('ticket_type', models.CharField(max_length=250)),
|
||||
('ticket_type_description', models.TextField()),
|
||||
('ticket_type_quantity', models.IntegerField()),
|
||||
('price', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('created_date', models.DateField(auto_now_add=True)),
|
||||
('updated_date', models.DateField(auto_now=True)),
|
||||
('is_offer', models.BooleanField(default=False)),
|
||||
('offer_percentage', models.IntegerField(default=0)),
|
||||
('offer_price', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
|
||||
('offer_start_date', models.DateField(blank=True, null=True)),
|
||||
('offer_end_date', models.DateField(blank=True, null=True)),
|
||||
('ticket_meta', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bookings.ticketmeta')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Ticket',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('ticket_id', models.CharField(max_length=250)),
|
||||
('is_checked_in', models.BooleanField(default=False)),
|
||||
('checked_in_date_time', models.DateTimeField(blank=True, null=True)),
|
||||
('booking', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bookings.booking')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Cart',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('quantity', models.IntegerField()),
|
||||
('price', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('created_date', models.DateField(auto_now_add=True)),
|
||||
('updated_date', models.DateField(auto_now=True)),
|
||||
('ticket_meta', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bookings.ticketmeta')),
|
||||
('ticket_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bookings.tickettype')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='booking',
|
||||
name='ticket_meta',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bookings.ticketmeta'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='booking',
|
||||
name='ticket_type',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bookings.tickettype'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='booking',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
@@ -4,10 +4,9 @@ from events.models import Event
|
||||
from accounts.models import User
|
||||
|
||||
# Create your models here.
|
||||
class Ticket(models.Model):
|
||||
class TicketMeta(models.Model):
|
||||
event = models.ForeignKey(Event, on_delete=models.CASCADE)
|
||||
ticket_name = models.CharField(max_length=250)
|
||||
price_per_ticket = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
maximum_quantity = models.IntegerField()
|
||||
available_quantity = models.IntegerField(default=0)
|
||||
is_active = models.BooleanField(default=True)
|
||||
@@ -19,11 +18,13 @@ class Ticket(models.Model):
|
||||
|
||||
|
||||
class TicketType(models.Model):
|
||||
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
|
||||
ticket_meta = models.ForeignKey(TicketMeta, on_delete=models.CASCADE)
|
||||
ticket_type = models.CharField(max_length=250)
|
||||
ticket_type_description = models.TextField()
|
||||
quantity = models.IntegerField()
|
||||
ticket_type_quantity = models.IntegerField()
|
||||
|
||||
price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_date = models.DateField(auto_now_add=True)
|
||||
updated_date = models.DateField(auto_now=True)
|
||||
@@ -41,10 +42,11 @@ class TicketType(models.Model):
|
||||
|
||||
class Cart(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
|
||||
ticket_meta = models.ForeignKey(TicketMeta, on_delete=models.CASCADE)
|
||||
ticket_type = models.ForeignKey(TicketType, on_delete=models.CASCADE)
|
||||
quantity = models.IntegerField()
|
||||
price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_date = models.DateField(auto_now_add=True)
|
||||
updated_date = models.DateField(auto_now=True)
|
||||
|
||||
@@ -55,7 +57,7 @@ class Cart(models.Model):
|
||||
class Booking(models.Model):
|
||||
booking_id = models.CharField(max_length=250)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
|
||||
ticket_meta = models.ForeignKey(TicketMeta, on_delete=models.CASCADE)
|
||||
ticket_type = models.ForeignKey(TicketType, on_delete=models.CASCADE)
|
||||
quantity = models.IntegerField()
|
||||
price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
@@ -70,4 +72,26 @@ class Booking(models.Model):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return self.booking_id
|
||||
return self.booking_id
|
||||
|
||||
class Ticket(models.Model):
|
||||
booking = models.ForeignKey(Booking, on_delete=models.CASCADE)
|
||||
ticket_id = models.CharField(max_length=250)
|
||||
is_checked_in = models.BooleanField(default=False)
|
||||
checked_in_date_time = models.DateTimeField(blank=True, null=True)
|
||||
|
||||
def __save__(self):
|
||||
if not self.ticket_id:
|
||||
self.ticket_id = str(self.booking.ticket_meta.event.name[:3].upper()) + str(uuid.uuid4().hex[:10]).upper()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return self.ticket_id
|
||||
|
||||
def check_in(self, ticket_id):
|
||||
if self.ticket_id == ticket_id:
|
||||
self.is_checked_in = True
|
||||
self.checked_in_date_time = datetime.now()
|
||||
self.save()
|
||||
return True
|
||||
return False
|
||||
61
bookings/services.py
Normal file
61
bookings/services.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from typing import List
|
||||
import uuid
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from bookings.models import Booking, Ticket
|
||||
|
||||
|
||||
def _generate_ticket_id(booking: Booking) -> str:
|
||||
"""
|
||||
Generate a ticket_id based on the event name and a random UUID segment.
|
||||
|
||||
Pattern: <EVT><RANDOM_HEX>
|
||||
- EVT: first 3 characters of event name (uppercase), or 'EVT' fallback
|
||||
- RANDOM_HEX: first 10 chars of uuid4 hex (uppercase)
|
||||
"""
|
||||
event = getattr(booking.ticket_meta, "event", None)
|
||||
if event and getattr(event, "name", None):
|
||||
prefix = (event.name or "EVT")[:3].upper()
|
||||
else:
|
||||
prefix = "EVT"
|
||||
|
||||
return prefix + uuid.uuid4().hex[:10].upper()
|
||||
|
||||
|
||||
def generate_tickets_for_booking(booking: Booking) -> List[Ticket]:
|
||||
"""
|
||||
Generate Ticket instances for a given Booking based on its quantity.
|
||||
|
||||
This function does NOT perform any payment or business-rule validation.
|
||||
It simply creates one Ticket per quantity on the booking.
|
||||
|
||||
Args:
|
||||
booking: Booking instance for which tickets should be generated.
|
||||
|
||||
Returns:
|
||||
List[Ticket]: List of created Ticket instances.
|
||||
"""
|
||||
if not isinstance(booking, Booking):
|
||||
raise TypeError("booking must be a Booking instance")
|
||||
|
||||
if booking.quantity <= 0:
|
||||
return []
|
||||
|
||||
tickets: List[Ticket] = []
|
||||
for _ in range(booking.quantity):
|
||||
tickets.append(
|
||||
Ticket(
|
||||
booking=booking,
|
||||
ticket_id=_generate_ticket_id(booking),
|
||||
is_checked_in=False,
|
||||
checked_in_date_time=None,
|
||||
)
|
||||
)
|
||||
|
||||
# Bulk create for efficiency
|
||||
Ticket.objects.bulk_create(tickets)
|
||||
|
||||
# Refresh from DB to ensure we have primary keys and any defaults
|
||||
return list[Ticket](Ticket.objects.filter(booking=booking).order_by("id"))
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
from . import ticket_meta_type
|
||||
from . import booking_api
|
||||
@@ -1,301 +0,0 @@
|
||||
from django.http import JsonResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.forms.models import model_to_dict
|
||||
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from bookings.models import Ticket
|
||||
from events.models import Event
|
||||
from mobile_api.utils import validate_token_and_get_user
|
||||
|
||||
|
||||
def _ticket_to_dict(ticket):
|
||||
"""
|
||||
Helper to serialise a Ticket instance into a simple dict suitable for JSON.
|
||||
"""
|
||||
data = model_to_dict(
|
||||
ticket,
|
||||
fields=[
|
||||
"id",
|
||||
"ticket_name",
|
||||
"price_per_ticket",
|
||||
"maximum_quantity",
|
||||
"available_quantity",
|
||||
"is_active",
|
||||
"created_date",
|
||||
"updated_date",
|
||||
],
|
||||
)
|
||||
data["event_id"] = ticket.event_id
|
||||
return data
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class TicketCreateAPI(APIView):
|
||||
"""
|
||||
Create a new Ticket.
|
||||
|
||||
Expected JSON body (along with token & username used across mobile_api):
|
||||
{
|
||||
"token": "...",
|
||||
"username": "...",
|
||||
"event_id": 1,
|
||||
"ticket_name": "VIP",
|
||||
"price_per_ticket": 1000.0,
|
||||
"maximum_quantity": 50,
|
||||
"available_quantity": 50, # optional, defaults to maximum_quantity
|
||||
"is_active": true # optional, defaults to true
|
||||
}
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
event_id = data.get("event_id")
|
||||
ticket_name = data.get("ticket_name")
|
||||
price_per_ticket = data.get("price_per_ticket")
|
||||
maximum_quantity = data.get("maximum_quantity")
|
||||
available_quantity = data.get("available_quantity")
|
||||
is_active = data.get("is_active", True)
|
||||
|
||||
if not event_id or not ticket_name or price_per_ticket is None or maximum_quantity is None:
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "error",
|
||||
"message": "event_id, ticket_name, price_per_ticket and maximum_quantity are required.",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
try:
|
||||
event = Event.objects.get(id=event_id)
|
||||
except Event.DoesNotExist:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "Event not found."},
|
||||
status=404,
|
||||
)
|
||||
|
||||
try:
|
||||
price_per_ticket = float(price_per_ticket)
|
||||
maximum_quantity = int(maximum_quantity)
|
||||
if available_quantity is not None:
|
||||
available_quantity = int(available_quantity)
|
||||
else:
|
||||
available_quantity = maximum_quantity
|
||||
except (TypeError, ValueError):
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "error",
|
||||
"message": "price_per_ticket, maximum_quantity and available_quantity must be numeric.",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
if maximum_quantity <= 0:
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "error",
|
||||
"message": "maximum_quantity must be greater than zero.",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
ticket = Ticket.objects.create(
|
||||
event=event,
|
||||
ticket_name=ticket_name,
|
||||
price_per_ticket=price_per_ticket,
|
||||
maximum_quantity=maximum_quantity,
|
||||
available_quantity=available_quantity,
|
||||
is_active=bool(is_active),
|
||||
)
|
||||
|
||||
return JsonResponse(
|
||||
{"status": "success", "ticket": _ticket_to_dict(ticket)},
|
||||
status=201,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": str(e)},
|
||||
status=500,
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class TicketListAPI(APIView):
|
||||
"""
|
||||
List tickets, optionally filtered by event_id.
|
||||
|
||||
Expected JSON body:
|
||||
{
|
||||
"token": "...",
|
||||
"username": "...",
|
||||
"event_id": 1 # optional
|
||||
}
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
event_id = data.get("event_id")
|
||||
|
||||
tickets_qs = Ticket.objects.all().order_by("-created_date")
|
||||
if event_id:
|
||||
tickets_qs = tickets_qs.filter(event_id=event_id)
|
||||
|
||||
tickets = [_ticket_to_dict(t) for t in tickets_qs]
|
||||
|
||||
return JsonResponse(
|
||||
{"status": "success", "tickets": tickets},
|
||||
status=200,
|
||||
)
|
||||
except Exception as e:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": str(e)},
|
||||
status=500,
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class TicketUpdateAPI(APIView):
|
||||
"""
|
||||
Update an existing Ticket.
|
||||
|
||||
Expected JSON body:
|
||||
{
|
||||
"token": "...",
|
||||
"username": "...",
|
||||
"ticket_id": 1,
|
||||
"ticket_name": "...", # optional
|
||||
"price_per_ticket": 1000.0, # optional
|
||||
"maximum_quantity": 50, # optional
|
||||
"available_quantity": 50, # optional
|
||||
"is_active": true # optional
|
||||
}
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
ticket_id = data.get("ticket_id")
|
||||
if not ticket_id:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "ticket_id is required."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
try:
|
||||
ticket = Ticket.objects.get(id=ticket_id)
|
||||
except Ticket.DoesNotExist:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "Ticket not found."},
|
||||
status=404,
|
||||
)
|
||||
|
||||
# Optional updates
|
||||
ticket_name = data.get("ticket_name")
|
||||
price_per_ticket = data.get("price_per_ticket")
|
||||
maximum_quantity = data.get("maximum_quantity")
|
||||
available_quantity = data.get("available_quantity")
|
||||
is_active = data.get("is_active")
|
||||
|
||||
if ticket_name is not None:
|
||||
ticket.ticket_name = ticket_name
|
||||
|
||||
try:
|
||||
if price_per_ticket is not None:
|
||||
ticket.price_per_ticket = float(price_per_ticket)
|
||||
if maximum_quantity is not None:
|
||||
maximum_quantity = int(maximum_quantity)
|
||||
if maximum_quantity <= 0:
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "error",
|
||||
"message": "maximum_quantity must be greater than zero.",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
ticket.maximum_quantity = maximum_quantity
|
||||
if available_quantity is not None:
|
||||
ticket.available_quantity = int(available_quantity)
|
||||
except (TypeError, ValueError):
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "error",
|
||||
"message": "price_per_ticket, maximum_quantity and available_quantity must be numeric.",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
if is_active is not None:
|
||||
ticket.is_active = bool(is_active)
|
||||
|
||||
ticket.save()
|
||||
|
||||
return JsonResponse(
|
||||
{"status": "success", "ticket": _ticket_to_dict(ticket)},
|
||||
status=200,
|
||||
)
|
||||
except Exception as e:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": str(e)},
|
||||
status=500,
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class TicketDeleteAPI(APIView):
|
||||
"""
|
||||
Delete an existing Ticket.
|
||||
|
||||
Expected JSON body:
|
||||
{
|
||||
"token": "...",
|
||||
"username": "...",
|
||||
"ticket_id": 1
|
||||
}
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
ticket_id = data.get("ticket_id")
|
||||
if not ticket_id:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "ticket_id is required."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
try:
|
||||
ticket = Ticket.objects.get(id=ticket_id)
|
||||
except Ticket.DoesNotExist:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "Ticket not found."},
|
||||
status=404,
|
||||
)
|
||||
|
||||
ticket.delete()
|
||||
|
||||
return JsonResponse(
|
||||
{"status": "success", "message": "Ticket deleted successfully."},
|
||||
status=200,
|
||||
)
|
||||
except Exception as e:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": str(e)},
|
||||
status=500,
|
||||
)
|
||||
|
||||
369
bookings/tickets_view/booking_api.py
Normal file
369
bookings/tickets_view/booking_api.py
Normal file
@@ -0,0 +1,369 @@
|
||||
import uuid
|
||||
from decimal import Decimal
|
||||
from django.http import JsonResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.forms.models import model_to_dict
|
||||
from django.utils import timezone
|
||||
from datetime import date
|
||||
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from bookings.models import Cart, TicketType, TicketMeta, Booking, Ticket
|
||||
from mobile_api.utils import validate_token_and_get_user
|
||||
from banking_operations.services import transaction_initiate
|
||||
from eventify_logger.services import log
|
||||
|
||||
|
||||
def _cart_to_dict(cart):
|
||||
"""Serialize Cart for JSON."""
|
||||
data = model_to_dict(
|
||||
cart,
|
||||
fields=["id", "quantity", "price", "created_date", "updated_date"],
|
||||
)
|
||||
data["user_id"] = cart.user_id
|
||||
data["ticket_meta_id"] = cart.ticket_meta_id
|
||||
data["ticket_type_id"] = cart.ticket_type_id
|
||||
return data
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class AddToCartAPI(APIView):
|
||||
"""
|
||||
Add TicketType to Cart (when customer clicks plus button).
|
||||
Body: token, username, ticket_type_id, quantity (optional, defaults to 1).
|
||||
|
||||
If cart item already exists for this user + ticket_type, quantity is incremented.
|
||||
Price is taken from TicketType (offer_price if offer is active, else price).
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
ticket_type_id = data.get("ticket_type_id")
|
||||
quantity = data.get("quantity", 1) # Default to 1 for plus button click
|
||||
|
||||
if not ticket_type_id:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "ticket_type_id is required."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
try:
|
||||
ticket_type = TicketType.objects.select_related("ticket_meta").get(id=ticket_type_id)
|
||||
except TicketType.DoesNotExist:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "TicketType not found."},
|
||||
status=404,
|
||||
)
|
||||
|
||||
if not ticket_type.is_active:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "TicketType is not active."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
try:
|
||||
quantity = int(quantity)
|
||||
if quantity <= 0:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "quantity must be greater than zero."},
|
||||
status=400,
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "quantity must be a positive integer."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
# Determine price: use offer_price if offer is active, else regular price
|
||||
price = ticket_type.price
|
||||
if ticket_type.is_offer:
|
||||
# Check if offer is currently valid (if dates are set)
|
||||
today = date.today()
|
||||
offer_valid = True
|
||||
if ticket_type.offer_start_date and ticket_type.offer_start_date > today:
|
||||
offer_valid = False
|
||||
if ticket_type.offer_end_date and ticket_type.offer_end_date < today:
|
||||
offer_valid = False
|
||||
|
||||
if offer_valid and ticket_type.offer_price > 0:
|
||||
price = ticket_type.offer_price
|
||||
|
||||
# Check if cart item already exists for this user + ticket_type
|
||||
cart_item, created = Cart.objects.get_or_create(
|
||||
user=user,
|
||||
ticket_type=ticket_type,
|
||||
defaults={
|
||||
"ticket_meta": ticket_type.ticket_meta,
|
||||
"quantity": quantity,
|
||||
"price": price,
|
||||
},
|
||||
)
|
||||
|
||||
if not created:
|
||||
# Update existing cart item: increment quantity
|
||||
cart_item.quantity += quantity
|
||||
cart_item.price = price # Update price in case offer changed
|
||||
cart_item.save()
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "success",
|
||||
"message": "TicketType added to cart." if created else "Cart item updated.",
|
||||
"cart_item": _cart_to_dict(cart_item),
|
||||
},
|
||||
status=201 if created else 200,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class DeleteFromCartAPI(APIView):
|
||||
"""
|
||||
Remove or decrement TicketType from Cart (when customer clicks minus button).
|
||||
Body: token, username, ticket_type_id, quantity (optional, defaults to 1).
|
||||
|
||||
Decrements quantity by 1 (or by given quantity). If quantity becomes 0 or less,
|
||||
the cart item is deleted.
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
ticket_type_id = data.get("ticket_type_id")
|
||||
quantity = data.get("quantity", 1) # Default to 1 for minus button click
|
||||
|
||||
if not ticket_type_id:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "ticket_type_id is required."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
try:
|
||||
quantity = int(quantity)
|
||||
if quantity <= 0:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "quantity must be greater than zero."},
|
||||
status=400,
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "quantity must be a positive integer."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
try:
|
||||
cart_item = Cart.objects.get(user=user, ticket_type_id=ticket_type_id)
|
||||
except Cart.DoesNotExist:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "Cart item not found for this ticket type."},
|
||||
status=404,
|
||||
)
|
||||
|
||||
cart_item.quantity -= quantity
|
||||
if cart_item.quantity <= 0:
|
||||
cart_item.delete()
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "success",
|
||||
"message": "TicketType removed from cart.",
|
||||
"cart_item": None,
|
||||
"removed": True,
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
cart_item.save()
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Cart quantity updated.",
|
||||
"cart_item": _cart_to_dict(cart_item),
|
||||
"removed": False,
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class CheckoutAPI(APIView):
|
||||
"""
|
||||
Checkout the authenticated user's cart: create one Booking per cart line,
|
||||
call transaction_initiate in banking_operations, then clear the checked-out cart items.
|
||||
Body: token, username.
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
cart_items = list(
|
||||
Cart.objects.filter(user=user, is_active=True).select_related(
|
||||
"ticket_meta", "ticket_type", "ticket_meta__event"
|
||||
)
|
||||
)
|
||||
if not cart_items:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "Cart is empty. Add ticket types before checkout."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
total_amount = Decimal("0")
|
||||
created_bookings = []
|
||||
cart_ids_to_clear = []
|
||||
|
||||
for item in cart_items:
|
||||
event_name = (
|
||||
(item.ticket_meta.event.name or "EVT")[:3].upper()
|
||||
if item.ticket_meta.event_id else "EVT"
|
||||
)
|
||||
booking_id = event_name + uuid.uuid4().hex[:10].upper()
|
||||
line_total = Decimal(str(item.price)) * item.quantity
|
||||
total_amount += line_total
|
||||
|
||||
booking = Booking.objects.create(
|
||||
booking_id=booking_id,
|
||||
user=user,
|
||||
ticket_meta=item.ticket_meta,
|
||||
ticket_type=item.ticket_type,
|
||||
quantity=item.quantity,
|
||||
price=item.price,
|
||||
)
|
||||
created_bookings.append(booking)
|
||||
cart_ids_to_clear.append(item.id)
|
||||
|
||||
reference_id = ",".join(b.booking_id for b in created_bookings)
|
||||
result = transaction_initiate(
|
||||
request=request,
|
||||
user=user,
|
||||
amount=float(total_amount),
|
||||
currency="INR",
|
||||
reference_type="checkout",
|
||||
reference_id=reference_id,
|
||||
bookings=created_bookings,
|
||||
extra_data=None,
|
||||
)
|
||||
|
||||
if not result.get("success"):
|
||||
for b in created_bookings:
|
||||
b.delete()
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "error",
|
||||
"message": result.get("message", "Transaction initiation failed."),
|
||||
},
|
||||
status=502,
|
||||
)
|
||||
|
||||
transaction_id = result.get("transaction_id")
|
||||
if transaction_id:
|
||||
for b in created_bookings:
|
||||
b.transaction_id = transaction_id
|
||||
b.save(update_fields=["transaction_id"])
|
||||
|
||||
Cart.objects.filter(id__in=cart_ids_to_clear).delete()
|
||||
|
||||
log("info", "Checkout complete", request=request, user=user, logger_data={
|
||||
"booking_ids": [b.booking_id for b in created_bookings],
|
||||
"total_amount": str(total_amount),
|
||||
})
|
||||
response_payload = {
|
||||
"status": "success",
|
||||
"message": "Checkout complete. Proceed to payment.",
|
||||
"booking_ids": [b.booking_id for b in created_bookings],
|
||||
"total_amount": str(total_amount),
|
||||
"transaction_id": result.get("transaction_id"),
|
||||
"payment_url": result.get("payment_url"),
|
||||
}
|
||||
return JsonResponse(response_payload, status=200)
|
||||
|
||||
except Exception as e:
|
||||
log("error", "Checkout exception", request=request, logger_data={"error": str(e)})
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class CheckInAPI(APIView):
|
||||
"""
|
||||
Check-in a ticket by scanning QR code (ticket_id).
|
||||
Body: token, username, ticket_id (required).
|
||||
|
||||
Looks up Ticket by ticket_id. If found and not already checked in,
|
||||
sets is_checked_in=True and checked_in_date_time=now. Returns success or
|
||||
appropriate error (ticket not found / already checked in).
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request, error_status_code=True)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
ticket_id = data.get("ticket_id")
|
||||
if not ticket_id or not str(ticket_id).strip():
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "ticket_id is required."},
|
||||
status=400,
|
||||
)
|
||||
ticket_id = str(ticket_id).strip()
|
||||
|
||||
try:
|
||||
ticket = Ticket.objects.select_related(
|
||||
"booking", "booking__ticket_meta", "booking__ticket_meta__event", "booking__ticket_type"
|
||||
).get(ticket_id=ticket_id)
|
||||
except Ticket.DoesNotExist:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "Ticket not found."},
|
||||
status=404,
|
||||
)
|
||||
|
||||
if ticket.is_checked_in:
|
||||
log("info", "Check-in duplicate - ticket already checked in", request=request, user=user, logger_data={"ticket_id": ticket_id})
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Ticket already checked in.",
|
||||
"ticket_id": ticket.ticket_id,
|
||||
"is_checked_in": True,
|
||||
"checked_in_date_time": ticket.checked_in_date_time.isoformat() if ticket.checked_in_date_time else None,
|
||||
"event_name": ticket.booking.ticket_meta.event.name if ticket.booking.ticket_meta_id else None,
|
||||
"booking_id": ticket.booking.booking_id,
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
ticket.is_checked_in = True
|
||||
ticket.checked_in_date_time = timezone.now()
|
||||
ticket.save(update_fields=["is_checked_in", "checked_in_date_time"])
|
||||
|
||||
log("info", "Check-in successful", request=request, user=user, logger_data={"ticket_id": ticket_id, "booking_id": ticket.booking.booking_id})
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Check-in successful.",
|
||||
"ticket_id": ticket.ticket_id,
|
||||
"is_checked_in": True,
|
||||
"checked_in_date_time": ticket.checked_in_date_time.isoformat(),
|
||||
"event_name": ticket.booking.ticket_meta.event.name if ticket.booking.ticket_meta_id else None,
|
||||
"booking_id": ticket.booking.booking_id,
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
log("error", "Check-in exception", request=request, logger_data={"error": str(e)})
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
489
bookings/tickets_view/ticket_meta_type.py
Normal file
489
bookings/tickets_view/ticket_meta_type.py
Normal file
@@ -0,0 +1,489 @@
|
||||
from django.http import JsonResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.forms.models import model_to_dict
|
||||
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from bookings.models import TicketMeta, TicketType
|
||||
from events.models import Event
|
||||
from mobile_api.utils import validate_token_and_get_user
|
||||
|
||||
|
||||
def _ticket_meta_to_dict(meta):
|
||||
"""Serialize TicketMeta for JSON."""
|
||||
data = model_to_dict(
|
||||
meta,
|
||||
fields=[
|
||||
"id",
|
||||
"ticket_name",
|
||||
"maximum_quantity",
|
||||
"available_quantity",
|
||||
"is_active",
|
||||
"created_date",
|
||||
"updated_date",
|
||||
],
|
||||
)
|
||||
data["event_id"] = meta.event_id
|
||||
return data
|
||||
|
||||
|
||||
def _ticket_type_to_dict(tt):
|
||||
"""Serialize TicketType for JSON."""
|
||||
data = model_to_dict(
|
||||
tt,
|
||||
fields=[
|
||||
"id",
|
||||
"ticket_type",
|
||||
"ticket_type_description",
|
||||
"quantity",
|
||||
"price",
|
||||
"is_active",
|
||||
"created_date",
|
||||
"updated_date",
|
||||
"is_offer",
|
||||
"offer_percentage",
|
||||
"offer_price",
|
||||
"offer_start_date",
|
||||
"offer_end_date",
|
||||
],
|
||||
)
|
||||
data["ticket_meta_id"] = tt.ticket_meta_id
|
||||
return data
|
||||
|
||||
|
||||
# ---------- TicketMeta (event-level ticket config) CRUD ----------
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class TicketMetaCreateAPI(APIView):
|
||||
"""
|
||||
Create a new TicketMeta (event-level ticket config).
|
||||
Body: token, username, event_id, ticket_name, maximum_quantity,
|
||||
available_quantity (optional, defaults to maximum_quantity), is_active (optional, default true).
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
event_id = data.get("event_id")
|
||||
ticket_name = data.get("ticket_name")
|
||||
maximum_quantity = data.get("maximum_quantity")
|
||||
available_quantity = data.get("available_quantity")
|
||||
is_active = data.get("is_active", True)
|
||||
|
||||
if not event_id or not ticket_name or maximum_quantity is None:
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "error",
|
||||
"message": "event_id, ticket_name and maximum_quantity are required.",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
try:
|
||||
event = Event.objects.get(id=event_id)
|
||||
except Event.DoesNotExist:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "Event not found."},
|
||||
status=404,
|
||||
)
|
||||
|
||||
try:
|
||||
maximum_quantity = int(maximum_quantity)
|
||||
available_quantity = int(available_quantity) if available_quantity is not None else maximum_quantity
|
||||
except (TypeError, ValueError):
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "maximum_quantity and available_quantity must be integers."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
if maximum_quantity <= 0:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "maximum_quantity must be greater than zero."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
meta = TicketMeta.objects.create(
|
||||
event=event,
|
||||
ticket_name=ticket_name,
|
||||
maximum_quantity=maximum_quantity,
|
||||
available_quantity=available_quantity,
|
||||
is_active=bool(is_active),
|
||||
)
|
||||
return JsonResponse(
|
||||
{"status": "success", "ticket_meta": _ticket_meta_to_dict(meta)},
|
||||
status=201,
|
||||
)
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class TicketMetaListAPI(APIView):
|
||||
"""List TicketMeta, optionally filtered by event_id. Body: token, username, event_id (optional)."""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
event_id = data.get("event_id")
|
||||
qs = TicketMeta.objects.filter(is_active=True).order_by("-created_date")
|
||||
if event_id:
|
||||
qs = qs.filter(event_id=event_id)
|
||||
items = [_ticket_meta_to_dict(m) for m in qs]
|
||||
return JsonResponse({"status": "success", "ticket_metas": items}, status=200)
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class TicketMetaUpdateAPI(APIView):
|
||||
"""
|
||||
Update TicketMeta. Body: token, username, ticket_meta_id (required);
|
||||
ticket_name, maximum_quantity, available_quantity, is_active (optional).
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
pk = data.get("ticket_meta_id")
|
||||
if not pk:
|
||||
return JsonResponse({"status": "error", "message": "ticket_meta_id is required."}, status=400)
|
||||
|
||||
try:
|
||||
meta = TicketMeta.objects.get(id=pk)
|
||||
except TicketMeta.DoesNotExist:
|
||||
return JsonResponse({"status": "error", "message": "TicketMeta not found."}, status=404)
|
||||
|
||||
if data.get("ticket_name") is not None:
|
||||
meta.ticket_name = data["ticket_name"]
|
||||
if data.get("maximum_quantity") is not None:
|
||||
try:
|
||||
val = int(data["maximum_quantity"])
|
||||
if val <= 0:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "maximum_quantity must be greater than zero."},
|
||||
status=400,
|
||||
)
|
||||
meta.maximum_quantity = val
|
||||
except (TypeError, ValueError):
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "maximum_quantity must be an integer."},
|
||||
status=400,
|
||||
)
|
||||
if data.get("available_quantity") is not None:
|
||||
try:
|
||||
meta.available_quantity = int(data["available_quantity"])
|
||||
except (TypeError, ValueError):
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "available_quantity must be an integer."},
|
||||
status=400,
|
||||
)
|
||||
if data.get("is_active") is not None:
|
||||
meta.is_active = bool(data["is_active"])
|
||||
|
||||
meta.save()
|
||||
return JsonResponse({"status": "success", "ticket_meta": _ticket_meta_to_dict(meta)}, status=200)
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class TicketMetaDeleteAPI(APIView):
|
||||
"""Delete TicketMeta. Body: token, username, ticket_meta_id."""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
pk = data.get("ticket_meta_id")
|
||||
if not pk:
|
||||
return JsonResponse({"status": "error", "message": "ticket_meta_id is required."}, status=400)
|
||||
|
||||
try:
|
||||
meta = TicketMeta.objects.get(id=pk)
|
||||
except TicketMeta.DoesNotExist:
|
||||
return JsonResponse({"status": "error", "message": "TicketMeta not found."}, status=404)
|
||||
|
||||
meta.delete()
|
||||
return JsonResponse({"status": "success", "message": "TicketMeta deleted successfully."}, status=200)
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class TicketMetaDeactivateAPI(APIView):
|
||||
"""Deactivate a TicketMeta (set is_active=False). Body: token, username, ticket_meta_id."""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
pk = data.get("ticket_meta_id")
|
||||
if not pk:
|
||||
return JsonResponse({"status": "error", "message": "ticket_meta_id is required."}, status=400)
|
||||
|
||||
try:
|
||||
meta = TicketMeta.objects.get(id=pk)
|
||||
except TicketMeta.DoesNotExist:
|
||||
return JsonResponse({"status": "error", "message": "TicketMeta not found."}, status=404)
|
||||
|
||||
meta.is_active = False
|
||||
meta.save()
|
||||
return JsonResponse(
|
||||
{"status": "success", "message": "TicketMeta deactivated.", "ticket_meta": _ticket_meta_to_dict(meta)},
|
||||
status=200,
|
||||
)
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
# ---------- TicketType CRUD ----------
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class TicketTypeCreateAPI(APIView):
|
||||
"""
|
||||
Create a new TicketType.
|
||||
Body: token, username, ticket_meta_id, ticket_type, ticket_type_description, quantity, price (required);
|
||||
is_active (optional, default true); is_offer, offer_percentage, offer_price, offer_start_date, offer_end_date (optional).
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
ticket_meta_id = data.get("ticket_meta_id")
|
||||
ticket_type_name = data.get("ticket_type")
|
||||
ticket_type_description = data.get("ticket_type_description")
|
||||
quantity = data.get("quantity")
|
||||
price = data.get("price")
|
||||
is_active = data.get("is_active", True)
|
||||
is_offer = data.get("is_offer", False)
|
||||
offer_percentage = data.get("offer_percentage", 0)
|
||||
offer_price = data.get("offer_price", 0)
|
||||
offer_start_date = data.get("offer_start_date")
|
||||
offer_end_date = data.get("offer_end_date")
|
||||
|
||||
if not ticket_meta_id or not ticket_type_name or quantity is None or price is None:
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "error",
|
||||
"message": "ticket_meta_id, ticket_type, ticket_type_description, quantity and price are required.",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
try:
|
||||
ticket_meta = TicketMeta.objects.get(id=ticket_meta_id)
|
||||
except TicketMeta.DoesNotExist:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "TicketMeta not found."},
|
||||
status=404,
|
||||
)
|
||||
|
||||
try:
|
||||
quantity = int(quantity)
|
||||
price = float(price)
|
||||
offer_percentage = int(offer_percentage) if offer_percentage is not None else 0
|
||||
offer_price = float(offer_price) if offer_price is not None else 0
|
||||
except (TypeError, ValueError):
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "quantity, price, offer_percentage and offer_price must be numeric."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
if quantity <= 0:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "quantity must be greater than zero."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
tt = TicketType.objects.create(
|
||||
ticket_meta=ticket_meta,
|
||||
ticket_type=ticket_type_name,
|
||||
ticket_type_description=ticket_type_description or "",
|
||||
quantity=quantity,
|
||||
price=price,
|
||||
is_active=bool(is_active),
|
||||
is_offer=bool(is_offer),
|
||||
offer_percentage=offer_percentage,
|
||||
offer_price=offer_price,
|
||||
offer_start_date=offer_start_date,
|
||||
offer_end_date=offer_end_date,
|
||||
)
|
||||
return JsonResponse(
|
||||
{"status": "success", "ticket_type": _ticket_type_to_dict(tt)},
|
||||
status=201,
|
||||
)
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class TicketTypeListAPI(APIView):
|
||||
"""List TicketType, optionally filtered by ticket_meta_id. Body: token, username, ticket_meta_id (optional)."""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
ticket_meta_id = data.get("ticket_meta_id")
|
||||
qs = TicketType.objects.filter(is_active=True).order_by("-created_date")
|
||||
if ticket_meta_id:
|
||||
qs = qs.filter(ticket_meta_id=ticket_meta_id)
|
||||
items = [_ticket_type_to_dict(tt) for tt in qs]
|
||||
return JsonResponse({"status": "success", "ticket_types": items}, status=200)
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class TicketTypeUpdateAPI(APIView):
|
||||
"""
|
||||
Update TicketType. Body: token, username, ticket_type_id (required);
|
||||
ticket_type, ticket_type_description, quantity, price, is_active,
|
||||
is_offer, offer_percentage, offer_price, offer_start_date, offer_end_date (optional).
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
pk = data.get("ticket_type_id")
|
||||
if not pk:
|
||||
return JsonResponse({"status": "error", "message": "ticket_type_id is required."}, status=400)
|
||||
|
||||
try:
|
||||
tt = TicketType.objects.get(id=pk)
|
||||
except TicketType.DoesNotExist:
|
||||
return JsonResponse({"status": "error", "message": "TicketType not found."}, status=404)
|
||||
|
||||
if data.get("ticket_type") is not None:
|
||||
tt.ticket_type = data["ticket_type"]
|
||||
if data.get("ticket_type_description") is not None:
|
||||
tt.ticket_type_description = data["ticket_type_description"]
|
||||
if data.get("quantity") is not None:
|
||||
try:
|
||||
val = int(data["quantity"])
|
||||
if val <= 0:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "quantity must be greater than zero."},
|
||||
status=400,
|
||||
)
|
||||
tt.quantity = val
|
||||
except (TypeError, ValueError):
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "quantity must be an integer."},
|
||||
status=400,
|
||||
)
|
||||
if data.get("price") is not None:
|
||||
try:
|
||||
tt.price = float(data["price"])
|
||||
except (TypeError, ValueError):
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "price must be numeric."},
|
||||
status=400,
|
||||
)
|
||||
if data.get("is_active") is not None:
|
||||
tt.is_active = bool(data["is_active"])
|
||||
if data.get("is_offer") is not None:
|
||||
tt.is_offer = bool(data["is_offer"])
|
||||
if data.get("offer_percentage") is not None:
|
||||
try:
|
||||
tt.offer_percentage = int(data["offer_percentage"])
|
||||
except (TypeError, ValueError):
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "offer_percentage must be an integer."},
|
||||
status=400,
|
||||
)
|
||||
if data.get("offer_price") is not None:
|
||||
try:
|
||||
tt.offer_price = float(data["offer_price"])
|
||||
except (TypeError, ValueError):
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "offer_price must be numeric."},
|
||||
status=400,
|
||||
)
|
||||
if "offer_start_date" in data:
|
||||
tt.offer_start_date = data["offer_start_date"]
|
||||
if "offer_end_date" in data:
|
||||
tt.offer_end_date = data["offer_end_date"]
|
||||
|
||||
tt.save()
|
||||
return JsonResponse({"status": "success", "ticket_type": _ticket_type_to_dict(tt)}, status=200)
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class TicketTypeDeactivateAPI(APIView):
|
||||
"""Deactivate a TicketType (set is_active=False). Body: token, username, ticket_type_id."""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
pk = data.get("ticket_type_id")
|
||||
if not pk:
|
||||
return JsonResponse({"status": "error", "message": "ticket_type_id is required."}, status=400)
|
||||
|
||||
try:
|
||||
tt = TicketType.objects.get(id=pk)
|
||||
except TicketType.DoesNotExist:
|
||||
return JsonResponse({"status": "error", "message": "TicketType not found."}, status=404)
|
||||
|
||||
tt.is_active = False
|
||||
tt.save()
|
||||
return JsonResponse(
|
||||
{"status": "success", "message": "TicketType deactivated.", "ticket_type": _ticket_type_to_dict(tt)},
|
||||
status=200,
|
||||
)
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class TicketTypeDeleteAPI(APIView):
|
||||
"""Delete TicketType. Body: token, username, ticket_type_id."""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
pk = data.get("ticket_type_id")
|
||||
if not pk:
|
||||
return JsonResponse({"status": "error", "message": "ticket_type_id is required."}, status=400)
|
||||
|
||||
try:
|
||||
tt = TicketType.objects.get(id=pk)
|
||||
except TicketType.DoesNotExist:
|
||||
return JsonResponse({"status": "error", "message": "TicketType not found."}, status=404)
|
||||
|
||||
tt.delete()
|
||||
return JsonResponse({"status": "success", "message": "TicketType deleted successfully."}, status=200)
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
@@ -1,17 +1,34 @@
|
||||
from django.urls import path
|
||||
|
||||
from bookings.tickets_view.api import (
|
||||
TicketCreateAPI,
|
||||
TicketListAPI,
|
||||
TicketUpdateAPI,
|
||||
TicketDeleteAPI,
|
||||
from bookings.tickets_view.ticket_meta_type import (
|
||||
TicketMetaCreateAPI,
|
||||
TicketMetaListAPI,
|
||||
TicketMetaUpdateAPI,
|
||||
TicketMetaDeleteAPI,
|
||||
TicketMetaDeactivateAPI,
|
||||
TicketTypeCreateAPI,
|
||||
TicketTypeListAPI,
|
||||
TicketTypeUpdateAPI,
|
||||
TicketTypeDeleteAPI,
|
||||
TicketTypeDeactivateAPI,
|
||||
)
|
||||
from bookings.tickets_view.booking_api import AddToCartAPI, DeleteFromCartAPI, CheckoutAPI, CheckInAPI
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("tickets/create/", TicketCreateAPI.as_view(), name="ticket_create"),
|
||||
path("tickets/list/", TicketListAPI.as_view(), name="ticket_list"),
|
||||
path("tickets/update/", TicketUpdateAPI.as_view(), name="ticket_update"),
|
||||
path("tickets/delete/", TicketDeleteAPI.as_view(), name="ticket_delete"),
|
||||
path("ticket-meta/create/", TicketMetaCreateAPI.as_view(), name="ticket_meta_create"),
|
||||
path("ticket-meta/list/", TicketMetaListAPI.as_view(), name="ticket_meta_list"),
|
||||
path("ticket-meta/update/", TicketMetaUpdateAPI.as_view(), name="ticket_meta_update"),
|
||||
path("ticket-meta/delete/", TicketMetaDeleteAPI.as_view(), name="ticket_meta_delete"),
|
||||
path("ticket-meta/deactivate/", TicketMetaDeactivateAPI.as_view(), name="ticket_meta_deactivate"),
|
||||
path("ticket-type/create/", TicketTypeCreateAPI.as_view(), name="ticket_type_create"),
|
||||
path("ticket-type/list/", TicketTypeListAPI.as_view(), name="ticket_type_list"),
|
||||
path("ticket-type/update/", TicketTypeUpdateAPI.as_view(), name="ticket_type_update"),
|
||||
path("ticket-type/delete/", TicketTypeDeleteAPI.as_view(), name="ticket_type_delete"),
|
||||
path("ticket-type/deactivate/", TicketTypeDeactivateAPI.as_view(), name="ticket_type_deactivate"),
|
||||
path("cart/add/", AddToCartAPI.as_view(), name="add_to_cart"),
|
||||
path("cart/delete/", DeleteFromCartAPI.as_view(), name="delete_from_cart"),
|
||||
path("checkout/", CheckoutAPI.as_view(), name="checkout"),
|
||||
path("check-in/", CheckInAPI.as_view(), name="check_in"),
|
||||
]
|
||||
|
||||
|
||||
BIN
db_1.sqlite3
BIN
db_1.sqlite3
Binary file not shown.
@@ -22,12 +22,16 @@ INSTALLED_APPS = [
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'eventify_logger',
|
||||
'master_data',
|
||||
'events',
|
||||
'accounts',
|
||||
'partner',
|
||||
'templatetags',
|
||||
'mobile_api',
|
||||
'web_api',
|
||||
'bookings',
|
||||
'banking_operations',
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken'
|
||||
]
|
||||
@@ -42,6 +46,7 @@ MIDDLEWARE = [
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'eventify_logger.middleware.EventifyLoggingMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
|
||||
@@ -32,6 +32,8 @@ urlpatterns = [
|
||||
path('events/', include('events.urls')),
|
||||
path('accounts/', include('accounts.urls')),
|
||||
path('bookings/', include('bookings.urls')),
|
||||
path('partner/', include('partner.urls')),
|
||||
path('banking/', include('banking_operations.urls')),
|
||||
path('api/', include('mobile_api.urls')),
|
||||
# path('web-api/', include('web_api.urls')),
|
||||
]
|
||||
|
||||
0
eventify_logger/__init__.py
Normal file
0
eventify_logger/__init__.py
Normal file
10
eventify_logger/admin.py
Normal file
10
eventify_logger/admin.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.contrib import admin
|
||||
from .models import EventifyLogger
|
||||
|
||||
|
||||
@admin.register(EventifyLogger)
|
||||
class EventifyLoggerAdmin(admin.ModelAdmin):
|
||||
list_display = ("logger_type", "logger_message", "logged_user", "logger_created_at")
|
||||
list_filter = ("logger_type", "logger_created_at")
|
||||
search_fields = ("logger_message",)
|
||||
readonly_fields = ("logger_created_at",)
|
||||
6
eventify_logger/apps.py
Normal file
6
eventify_logger/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class EventifyLoggerConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'eventify_logger'
|
||||
28
eventify_logger/middleware.py
Normal file
28
eventify_logger/middleware.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Request logging middleware - logs every HTTP request to EventifyLogger.
|
||||
"""
|
||||
from eventify_logger.services import log
|
||||
|
||||
|
||||
class EventifyLoggingMiddleware:
|
||||
"""Log each request (method, path, status) after response."""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
response = self.get_response(request)
|
||||
try:
|
||||
status = getattr(response, "status_code", 0)
|
||||
if 500 <= status < 600:
|
||||
logger_type = "error"
|
||||
elif 400 <= status < 500:
|
||||
logger_type = "warning"
|
||||
else:
|
||||
logger_type = "info"
|
||||
message = f"{request.method} {request.path} -> {status}"
|
||||
logger_data = {"path": request.path, "method": request.method, "status_code": status}
|
||||
log(logger_type=logger_type, logger_message=message, request=request, logger_data=logger_data)
|
||||
except Exception:
|
||||
pass
|
||||
return response
|
||||
31
eventify_logger/migrations/0001_initial.py
Normal file
31
eventify_logger/migrations/0001_initial.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 4.2.27 on 2026-03-09 04:26
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='EventifyLogger',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('logger_type', models.CharField(choices=[('info', 'Info'), ('warning', 'Warning'), ('error', 'Error'), ('critical', 'Critical')], max_length=250)),
|
||||
('logger_message', models.TextField()),
|
||||
('logger_data', models.TextField(blank=True, null=True)),
|
||||
('logger_created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('logged_ip_address', models.GenericIPAddressField(blank=True, null=True)),
|
||||
('logged_user_device', models.CharField(blank=True, max_length=250, null=True)),
|
||||
('logged_user_browser', models.CharField(blank=True, max_length=250, null=True)),
|
||||
('logged_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
eventify_logger/migrations/__init__.py
Normal file
0
eventify_logger/migrations/__init__.py
Normal file
27
eventify_logger/models.py
Normal file
27
eventify_logger/models.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
|
||||
class EventifyLogger(models.Model):
|
||||
logger_type = models.CharField(max_length=250, choices=[
|
||||
('info', 'Info'),
|
||||
('warning', 'Warning'),
|
||||
('error', 'Error'),
|
||||
('critical', 'Critical'),
|
||||
])
|
||||
logger_message = models.TextField()
|
||||
logger_data = models.TextField(blank=True, null=True)
|
||||
logger_created_at = models.DateTimeField(auto_now_add=True)
|
||||
logged_user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
logged_ip_address = models.GenericIPAddressField(blank=True, null=True)
|
||||
logged_user_device = models.CharField(max_length=250, blank=True, null=True)
|
||||
logged_user_browser = models.CharField(max_length=250, blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
user_str = str(self.logged_user) if self.logged_user else "anonymous"
|
||||
return f"{user_str}-{self.logger_type} - {self.logger_message}"
|
||||
58
eventify_logger/services.py
Normal file
58
eventify_logger/services.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
Central logging service for EventifyLogger.
|
||||
"""
|
||||
import json
|
||||
|
||||
from eventify_logger.models import EventifyLogger
|
||||
|
||||
|
||||
def _get_client_ip(request):
|
||||
"""Extract client IP from request."""
|
||||
if not request:
|
||||
return None
|
||||
x_forwarded = request.META.get("HTTP_X_FORWARDED_FOR")
|
||||
if x_forwarded:
|
||||
return x_forwarded.split(",")[0].strip() or None
|
||||
return request.META.get("REMOTE_ADDR")
|
||||
|
||||
|
||||
def _get_user_agent(request):
|
||||
"""Extract User-Agent from request."""
|
||||
if not request:
|
||||
return None
|
||||
return request.META.get("HTTP_USER_AGENT", "")
|
||||
|
||||
|
||||
def log(logger_type, logger_message, request=None, user=None, logger_data=None):
|
||||
"""
|
||||
Create an EventifyLogger record.
|
||||
|
||||
Args:
|
||||
logger_type: 'info' | 'warning' | 'error' | 'critical'
|
||||
logger_message: str
|
||||
request: optional HttpRequest (used for IP, user-agent, user if not provided)
|
||||
user: optional User (overrides request.user)
|
||||
logger_data: optional str or dict (dict will be JSON-serialized)
|
||||
"""
|
||||
try:
|
||||
resolved_user = user
|
||||
if resolved_user is None and request and hasattr(request, "user"):
|
||||
resolved_user = getattr(request.user, "is_authenticated", False) and request.user or None
|
||||
|
||||
ip_address = _get_client_ip(request) if request else None
|
||||
user_agent = _get_user_agent(request) if request else None
|
||||
|
||||
if isinstance(logger_data, dict):
|
||||
logger_data = json.dumps(logger_data)
|
||||
|
||||
EventifyLogger.objects.create(
|
||||
logger_type=logger_type,
|
||||
logger_message=str(logger_message)[:10000], # cap message length
|
||||
logger_data=logger_data[:10000] if logger_data else None, # cap data length
|
||||
logged_user=resolved_user,
|
||||
logged_ip_address=ip_address,
|
||||
logged_user_device=None, # defer UA parsing
|
||||
logged_user_browser=user_agent[:250] if user_agent else None,
|
||||
)
|
||||
except Exception:
|
||||
pass # Never let logging break the app
|
||||
3
eventify_logger/tests.py
Normal file
3
eventify_logger/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
3
eventify_logger/views.py
Normal file
3
eventify_logger/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
246
events/api.py
Normal file
246
events/api.py
Normal file
@@ -0,0 +1,246 @@
|
||||
from django.http import JsonResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.forms.models import model_to_dict
|
||||
from rest_framework.views import APIView
|
||||
from datetime import datetime
|
||||
|
||||
from events.models import Event
|
||||
from master_data.models import EventType
|
||||
from mobile_api.utils import validate_token_and_get_user
|
||||
from eventify_logger.services import log
|
||||
|
||||
|
||||
def _event_to_dict(event, request=None):
|
||||
"""Serialize Event for JSON."""
|
||||
data = model_to_dict(
|
||||
event,
|
||||
fields=[
|
||||
"id",
|
||||
"name",
|
||||
"description",
|
||||
"start_date",
|
||||
"end_date",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"all_year_event",
|
||||
"latitude",
|
||||
"longitude",
|
||||
"pincode",
|
||||
"district",
|
||||
"state",
|
||||
"place",
|
||||
"venue_name",
|
||||
"event_status",
|
||||
"cancelled_reason",
|
||||
"important_information",
|
||||
"source",
|
||||
"created_date",
|
||||
],
|
||||
)
|
||||
# Add event_type info
|
||||
data["event_type"] = {
|
||||
"id": event.event_type.id,
|
||||
"event_type": event.event_type.event_type,
|
||||
}
|
||||
if event.event_type.event_type_icon:
|
||||
if request:
|
||||
data["event_type"]["event_type_icon"] = request.build_absolute_uri(event.event_type.event_type_icon.url)
|
||||
else:
|
||||
data["event_type"]["event_type_icon"] = event.event_type.event_type_icon.url
|
||||
else:
|
||||
data["event_type"]["event_type_icon"] = None
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class EventCreateAPI(APIView):
|
||||
"""
|
||||
Create Event API.
|
||||
Body: token, username (required);
|
||||
name, description, latitude, longitude, pincode, place, event_type_id (required);
|
||||
start_date, end_date, start_time, end_time, all_year_event, venue_name,
|
||||
event_status, cancelled_reason, important_information, source, district, state (optional).
|
||||
Returns: created event data.
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request, error_status_code=True)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
# Extract required fields
|
||||
name = data.get("name")
|
||||
description = data.get("description")
|
||||
latitude = data.get("latitude")
|
||||
longitude = data.get("longitude")
|
||||
pincode = data.get("pincode")
|
||||
place = data.get("place")
|
||||
event_type_id = data.get("event_type_id") or data.get("event_type")
|
||||
|
||||
# Validate required fields
|
||||
if not all([name, description, latitude, longitude, pincode, place, event_type_id]):
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "error",
|
||||
"message": "name, description, latitude, longitude, pincode, place, and event_type_id are required.",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
# Validate event_type exists
|
||||
try:
|
||||
event_type = EventType.objects.get(id=event_type_id)
|
||||
except EventType.DoesNotExist:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "EventType not found."},
|
||||
status=404,
|
||||
)
|
||||
|
||||
# Validate latitude and longitude
|
||||
try:
|
||||
latitude = float(latitude)
|
||||
if latitude < -90 or latitude > 90:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "latitude must be between -90 and 90."},
|
||||
status=400,
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "latitude must be numeric."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
try:
|
||||
longitude = float(longitude)
|
||||
if longitude < -180 or longitude > 180:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "longitude must be between -180 and 180."},
|
||||
status=400,
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "longitude must be numeric."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
# Handle all_year_event
|
||||
all_year_event = data.get("all_year_event", False)
|
||||
if isinstance(all_year_event, str):
|
||||
all_year_event = all_year_event.lower() in ['true', '1', 'yes', 'on']
|
||||
|
||||
# Handle dates and times - clear if all_year_event is True
|
||||
start_date = None
|
||||
end_date = None
|
||||
start_time = None
|
||||
end_time = None
|
||||
|
||||
if not all_year_event:
|
||||
# Parse start_date
|
||||
if data.get("start_date"):
|
||||
try:
|
||||
start_date = datetime.strptime(data["start_date"], "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "Invalid start_date format. Expected YYYY-MM-DD."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
# Parse end_date
|
||||
if data.get("end_date"):
|
||||
try:
|
||||
end_date = datetime.strptime(data["end_date"], "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "Invalid end_date format. Expected YYYY-MM-DD."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
# Parse start_time
|
||||
if data.get("start_time"):
|
||||
try:
|
||||
start_time = datetime.strptime(data["start_time"], "%H:%M:%S").time()
|
||||
except ValueError:
|
||||
try:
|
||||
start_time = datetime.strptime(data["start_time"], "%H:%M").time()
|
||||
except ValueError:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "Invalid start_time format. Expected HH:MM or HH:MM:SS."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
# Parse end_time
|
||||
if data.get("end_time"):
|
||||
try:
|
||||
end_time = datetime.strptime(data["end_time"], "%H:%M:%S").time()
|
||||
except ValueError:
|
||||
try:
|
||||
end_time = datetime.strptime(data["end_time"], "%H:%M").time()
|
||||
except ValueError:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "Invalid end_time format. Expected HH:MM or HH:MM:SS."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
# Validate event_status if provided
|
||||
event_status = data.get("event_status", "pending")
|
||||
valid_statuses = ['created', 'cancelled', 'pending', 'completed', 'postponed']
|
||||
if event_status not in valid_statuses:
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "error",
|
||||
"message": f"Invalid event_status. Must be one of: {', '.join(valid_statuses)}",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
# Validate source if provided
|
||||
source = data.get("source", "official")
|
||||
valid_sources = ['official', 'community']
|
||||
if source not in valid_sources:
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "error",
|
||||
"message": f"Invalid source. Must be one of: {', '.join(valid_sources)}",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
# Create event
|
||||
event = Event.objects.create(
|
||||
name=name,
|
||||
description=description,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
all_year_event=all_year_event,
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
pincode=pincode,
|
||||
district=data.get("district", ""),
|
||||
state=data.get("state", ""),
|
||||
place=place,
|
||||
venue_name=data.get("venue_name", ""),
|
||||
event_type=event_type,
|
||||
event_status=event_status,
|
||||
cancelled_reason=data.get("cancelled_reason", "NA"),
|
||||
important_information=data.get("important_information", ""),
|
||||
source=source,
|
||||
)
|
||||
|
||||
log("info", "Event created", request=request, user=user, logger_data={"event_id": event.id, "event_name": name})
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Event created successfully.",
|
||||
"event": _event_to_dict(event, request),
|
||||
},
|
||||
status=201,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
log("error", "Event create exception", request=request, logger_data={"error": str(e)})
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -1,6 +1,7 @@
|
||||
from random import choices
|
||||
from django.db import models
|
||||
from master_data.models import EventType
|
||||
from partner.models import Partner
|
||||
|
||||
|
||||
class Event(models.Model):
|
||||
@@ -21,10 +22,16 @@ class Event(models.Model):
|
||||
place = models.CharField(max_length=200)
|
||||
|
||||
is_bookable = models.BooleanField(default=False)
|
||||
include_gst = models.BooleanField(default=False)
|
||||
gst_percentage_1 = models.IntegerField(default=0)
|
||||
gst_percentage_2 = models.IntegerField(default=0)
|
||||
|
||||
is_eventify_event = models.BooleanField(default=True)
|
||||
outside_event_url = models.URLField(default='NA')
|
||||
|
||||
is_partner_event = models.BooleanField(default=False)
|
||||
partner = models.ForeignKey(Partner, on_delete=models.CASCADE, blank=True, null=True)
|
||||
|
||||
event_type = models.ForeignKey(EventType, on_delete=models.CASCADE)
|
||||
event_status = models.CharField(max_length=250, choices=[
|
||||
('created', 'Created'),
|
||||
@@ -32,6 +39,9 @@ class Event(models.Model):
|
||||
('pending', 'Pending'),
|
||||
('completed', 'Completed'),
|
||||
('postponed', 'Postponed'),
|
||||
('published', 'Published'),
|
||||
('live', 'Live'),
|
||||
('flagged', 'Flagged'),
|
||||
], default='pending')
|
||||
cancelled_reason = models.TextField(default='NA')
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
from . import api
|
||||
|
||||
app_name = 'events'
|
||||
|
||||
@@ -14,3 +15,8 @@ urlpatterns = [
|
||||
path('<int:pk>/images/<int:img_id>/delete/', views.delete_event_image, name='delete_event_image'),
|
||||
path('<int:pk>/images/<int:img_id>/primary/', views.set_primary_image, name='set_primary_image'),
|
||||
]
|
||||
|
||||
# Event API URLs
|
||||
urlpatterns += [
|
||||
path('api/create/', api.EventCreateAPI.as_view(), name='event_api_create'),
|
||||
]
|
||||
|
||||
@@ -10,8 +10,6 @@ from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
|
||||
from django.http import JsonResponse
|
||||
from .models import Event
|
||||
|
||||
|
||||
class EventListView(LoginRequiredMixin, generic.ListView):
|
||||
|
||||
@@ -2,7 +2,7 @@ from django.urls import path
|
||||
from .views import *
|
||||
|
||||
|
||||
# User URLS
|
||||
# Customer URLS
|
||||
urlpatterns = [
|
||||
path('user/register/', RegisterView.as_view(), name='json_register'),
|
||||
path('user/login/', LoginView.as_view(), name='json_login'),
|
||||
|
||||
@@ -11,6 +11,7 @@ from django.contrib.auth import logout
|
||||
from mobile_api.utils import validate_token_and_get_user
|
||||
from utils.errors_json_convertor import simplify_form_errors
|
||||
from accounts.models import User
|
||||
from eventify_logger.services import log
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
@@ -22,9 +23,12 @@ class RegisterView(View):
|
||||
if form.is_valid():
|
||||
user = form.save()
|
||||
token, _ = Token.objects.get_or_create(user=user)
|
||||
log("info", "API user registration", request=request, user=user)
|
||||
return JsonResponse({'message': 'User registered successfully', 'token': token.key}, status=201)
|
||||
log("warning", "API registration failed", request=request, logger_data=dict(errors=form.errors))
|
||||
return JsonResponse({'errors': form.errors}, status=400)
|
||||
except Exception as e:
|
||||
log("error", "API registration exception", request=request, logger_data={"error": str(e)})
|
||||
return JsonResponse({'error': str(e)}, status=500)
|
||||
|
||||
|
||||
@@ -47,6 +51,7 @@ class WebRegisterView(View):
|
||||
user = form.save()
|
||||
token, _ = Token.objects.get_or_create(user=user)
|
||||
print('3')
|
||||
log("info", "Web user registration", request=request, user=user)
|
||||
response = {
|
||||
'message': 'User registered successfully',
|
||||
'token': token.key,
|
||||
@@ -55,8 +60,10 @@ class WebRegisterView(View):
|
||||
'phone_number': user.phone_number,
|
||||
}
|
||||
return JsonResponse(response, status=201)
|
||||
log("warning", "Web registration failed", request=request, logger_data=dict(errors=form.errors))
|
||||
return JsonResponse({'errors': form.errors}, status=400)
|
||||
except Exception as e:
|
||||
log("error", "Web registration exception", request=request, logger_data={"error": str(e)})
|
||||
return JsonResponse({'error': str(e)}, status=500)
|
||||
|
||||
|
||||
@@ -68,11 +75,12 @@ class LoginView(View):
|
||||
data = json.loads(request.body)
|
||||
form = LoginForm(data)
|
||||
print('1')
|
||||
if form.is_valid():
|
||||
if form.is_valid():
|
||||
print('2')
|
||||
user = form.cleaned_data['user']
|
||||
token, _ = Token.objects.get_or_create(user=user)
|
||||
print('3')
|
||||
log("info", "API login", request=request, user=user)
|
||||
response = {
|
||||
'message': 'Login successful',
|
||||
'token': token.key,
|
||||
@@ -94,9 +102,11 @@ class LoginView(View):
|
||||
print('4')
|
||||
print(response)
|
||||
return JsonResponse(response, status=200)
|
||||
|
||||
|
||||
log("warning", "API login failed", request=request, logger_data=dict(errors=form.errors))
|
||||
return JsonResponse(simplify_form_errors(form), status=401)
|
||||
except Exception as e:
|
||||
log("error", "API login exception", request=request, logger_data={"error": str(e)})
|
||||
return JsonResponse({'error': str(e)}, status=500)
|
||||
|
||||
|
||||
@@ -126,6 +136,7 @@ class LogoutView(View):
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
log("info", "API logout", request=request, user=user)
|
||||
# 🔍 Call Django's built-in logout
|
||||
logout(request)
|
||||
|
||||
@@ -138,6 +149,7 @@ class LogoutView(View):
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
log("error", "API logout exception", request=request, logger_data={"error": str(e)})
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
|
||||
0
partner/__init__.py
Normal file
0
partner/__init__.py
Normal file
24
partner/admin.py
Normal file
24
partner/admin.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from django.contrib import admin
|
||||
from .models import Partner
|
||||
|
||||
|
||||
@admin.register(Partner)
|
||||
class PartnerAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'partner_type', 'primary_contact_person_name', 'primary_contact_person_email', 'is_kyc_compliant', 'kyc_compliance_status')
|
||||
list_filter = ('partner_type', 'is_kyc_compliant', 'kyc_compliance_status')
|
||||
search_fields = ('name', 'primary_contact_person_name', 'primary_contact_person_email', 'primary_contact_person_phone')
|
||||
fieldsets = (
|
||||
('Basic Information', {
|
||||
'fields': ('name', 'partner_type')
|
||||
}),
|
||||
('Contact Information', {
|
||||
'fields': ('primary_contact_person_name', 'primary_contact_person_email', 'primary_contact_person_phone', 'website_url')
|
||||
}),
|
||||
('Address', {
|
||||
'fields': ('address', 'city', 'state', 'country', 'pincode', 'latitude', 'longitude')
|
||||
}),
|
||||
('KYC Compliance', {
|
||||
'fields': ('is_kyc_compliant', 'kyc_compliance_status', 'kyc_compliance_reason', 'kyc_compliance_document_type',
|
||||
'kyc_compliance_document_other_type', 'kyc_compliance_document_file', 'kyc_compliance_document_number')
|
||||
}),
|
||||
)
|
||||
1118
partner/api.py
Normal file
1118
partner/api.py
Normal file
File diff suppressed because it is too large
Load Diff
6
partner/apps.py
Normal file
6
partner/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PartnerConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'partner'
|
||||
41
partner/migrations/0001_initial.py
Normal file
41
partner/migrations/0001_initial.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# Generated by Django 4.2.27 on 2026-03-13 16:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Partner',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=250)),
|
||||
('partner_type', models.CharField(choices=[('venue', 'Venue'), ('promoter', 'Promoter'), ('sponsor', 'Sponsor'), ('vendor', 'Vendor'), ('affiliate', 'Affiliate'), ('other', 'Other')], max_length=250)),
|
||||
('primary_contact_person_name', models.CharField(max_length=250)),
|
||||
('primary_contact_person_email', models.EmailField(max_length=254)),
|
||||
('primary_contact_person_phone', models.CharField(max_length=15)),
|
||||
('status', models.CharField(choices=[('active', 'Active'), ('inactive', 'Inactive'), ('pending', 'Pending'), ('suspended', 'Suspended'), ('deleted', 'Deleted'), ('archived', 'Archived')], default='active', max_length=250)),
|
||||
('address', models.TextField(blank=True, null=True)),
|
||||
('city', models.CharField(blank=True, max_length=250, null=True)),
|
||||
('state', models.CharField(blank=True, max_length=250, null=True)),
|
||||
('country', models.CharField(blank=True, max_length=250, null=True)),
|
||||
('website_url', models.URLField(blank=True, null=True)),
|
||||
('pincode', models.CharField(blank=True, max_length=10, null=True)),
|
||||
('latitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
|
||||
('longitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
|
||||
('is_kyc_compliant', models.BooleanField(default=False)),
|
||||
('kyc_compliance_status', models.CharField(choices=[('pending', 'Pending'), ('approved', 'Approved'), ('rejected', 'Rejected'), ('high_risk', 'High Risk'), ('low_risk', 'Low Risk'), ('medium_risk', 'Medium Risk')], default='pending', max_length=250)),
|
||||
('kyc_compliance_reason', models.TextField(blank=True, null=True)),
|
||||
('kyc_compliance_document_type', models.CharField(blank=True, choices=[('aadhaar', 'Aadhaar'), ('pan', 'PAN'), ('driving_license', 'Driving License'), ('voter_id', 'Voter ID'), ('passport', 'Passport'), ('other', 'Other')], max_length=250, null=True)),
|
||||
('kyc_compliance_document_other_type', models.CharField(blank=True, max_length=250, null=True)),
|
||||
('kyc_compliance_document_file', models.FileField(blank=True, null=True, upload_to='kyc_documents/')),
|
||||
('kyc_compliance_document_number', models.CharField(blank=True, max_length=250, null=True)),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
partner/migrations/__init__.py
Normal file
0
partner/migrations/__init__.py
Normal file
69
partner/models.py
Normal file
69
partner/models.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from django.db import models
|
||||
|
||||
PARTNER_TYPE_CHOICES = (
|
||||
('venue', 'Venue'),
|
||||
('promoter', 'Promoter'),
|
||||
('sponsor', 'Sponsor'),
|
||||
('vendor', 'Vendor'),
|
||||
('affiliate', 'Affiliate'),
|
||||
('other', 'Other'),
|
||||
)
|
||||
|
||||
KYC_DOCUMENT_TYPE_CHOICES = (
|
||||
('aadhaar', 'Aadhaar'),
|
||||
('pan', 'PAN'),
|
||||
('driving_license', 'Driving License'),
|
||||
('voter_id', 'Voter ID'),
|
||||
('passport', 'Passport'),
|
||||
('other', 'Other'),
|
||||
)
|
||||
|
||||
KYC_COMPLIANCE_STATUS_CHOICES = (
|
||||
('pending', 'Pending'),
|
||||
('approved', 'Approved'),
|
||||
('rejected', 'Rejected'),
|
||||
('high_risk', 'High Risk'),
|
||||
('low_risk', 'Low Risk'),
|
||||
('medium_risk', 'Medium Risk'),
|
||||
)
|
||||
|
||||
STATUS_CHOICES = (
|
||||
('active', 'Active'),
|
||||
('inactive', 'Inactive'),
|
||||
('pending', 'Pending'),
|
||||
('suspended', 'Suspended'),
|
||||
('deleted', 'Deleted'),
|
||||
('archived', 'Archived'),
|
||||
)
|
||||
|
||||
|
||||
class Partner(models.Model):
|
||||
name = models.CharField(max_length=250)
|
||||
partner_type = models.CharField(max_length=250, choices=PARTNER_TYPE_CHOICES)
|
||||
|
||||
primary_contact_person_name = models.CharField(max_length=250)
|
||||
primary_contact_person_email = models.EmailField()
|
||||
primary_contact_person_phone = models.CharField(max_length=15)
|
||||
|
||||
status = models.CharField(max_length=250, choices=STATUS_CHOICES, default='active')
|
||||
|
||||
address = models.TextField(blank=True, null=True)
|
||||
city = models.CharField(max_length=250, blank=True, null=True)
|
||||
state = models.CharField(max_length=250, blank=True, null=True)
|
||||
country = models.CharField(max_length=250, blank=True, null=True)
|
||||
website_url = models.URLField(blank=True, null=True)
|
||||
|
||||
pincode = models.CharField(max_length=10, blank=True, null=True)
|
||||
latitude = models.DecimalField(max_digits=9, decimal_places=6, blank=True, null=True)
|
||||
longitude = models.DecimalField(max_digits=9, decimal_places=6, blank=True, null=True)
|
||||
|
||||
is_kyc_compliant = models.BooleanField(default=False)
|
||||
kyc_compliance_status = models.CharField(max_length=250, choices=KYC_COMPLIANCE_STATUS_CHOICES, default='pending')
|
||||
kyc_compliance_reason = models.TextField(blank=True, null=True)
|
||||
kyc_compliance_document_type = models.CharField(max_length=250, choices=KYC_DOCUMENT_TYPE_CHOICES, blank=True, null=True)
|
||||
kyc_compliance_document_other_type = models.CharField(max_length=250, blank=True, null=True)
|
||||
kyc_compliance_document_file = models.FileField(upload_to='kyc_documents/', blank=True, null=True)
|
||||
kyc_compliance_document_number = models.CharField(max_length=250, blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
3
partner/tests.py
Normal file
3
partner/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
40
partner/urls.py
Normal file
40
partner/urls.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from django.urls import path
|
||||
|
||||
from partner.api import (
|
||||
PartnerCreateAPI,
|
||||
PartnerListAPI,
|
||||
PartnerUpdateAPI,
|
||||
PartnerDeleteAPI,
|
||||
PartnerUpdateKYCDocumentsAPI,
|
||||
PartnerUpdateAddressLocationAPI,
|
||||
PartnerCreateUserAPI,
|
||||
PartnerListUsersAPI,
|
||||
PartnerUpdateUserAPI,
|
||||
PartnerDeleteUserAPI,
|
||||
PartnerListAllAPI,
|
||||
PartnerListKYCCompliantAPI,
|
||||
PartnerListKYCPendingAPI,
|
||||
PartnerListHighRiskAPI,
|
||||
PartnerListJoinedThisWeekAPI,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("create/", PartnerCreateAPI.as_view(), name="partner_create"),
|
||||
path("list/", PartnerListAPI.as_view(), name="partner_list"),
|
||||
path("update/", PartnerUpdateAPI.as_view(), name="partner_update"),
|
||||
path("delete/", PartnerDeleteAPI.as_view(), name="partner_delete"),
|
||||
path("update-kyc-documents/", PartnerUpdateKYCDocumentsAPI.as_view(), name="partner_update_kyc_documents"),
|
||||
path("update-address-location/", PartnerUpdateAddressLocationAPI.as_view(), name="partner_update_address_location"),
|
||||
path("create-user/", PartnerCreateUserAPI.as_view(), name="partner_create_user"),
|
||||
path("list-users/", PartnerListUsersAPI.as_view(), name="partner_list_users"),
|
||||
path("update-user/", PartnerUpdateUserAPI.as_view(), name="partner_update_user"),
|
||||
path("delete-user/", PartnerDeleteUserAPI.as_view(), name="partner_delete_user"),
|
||||
|
||||
# Partner List APIs (Filtered by Criteria)
|
||||
path("list-all/", PartnerListAllAPI.as_view(), name="partner_list_all"),
|
||||
path("list-kyc-compliant/", PartnerListKYCCompliantAPI.as_view(), name="partner_list_kyc_compliant"),
|
||||
path("list-kyc-pending/", PartnerListKYCPendingAPI.as_view(), name="partner_list_kyc_pending"),
|
||||
path("list-high-risk/", PartnerListHighRiskAPI.as_view(), name="partner_list_high_risk"),
|
||||
path("list-joined-this-week/", PartnerListJoinedThisWeekAPI.as_view(), name="partner_list_joined_this_week"),
|
||||
]
|
||||
3
partner/views.py
Normal file
3
partner/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
56
templates/partner/base.html
Normal file
56
templates/partner/base.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1'>
|
||||
<title>Eventify Partner Portal</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{% url 'accounts:partner_dashboard' %}">Eventify Partner Portal</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navmenu">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navmenu">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'accounts:partner_dashboard' %}">Dashboard</a>
|
||||
</li>
|
||||
{% if user.role == "partner" or user.role == "partner_manager" %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'accounts:partner_user_list' %}">Users</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
{% if user.is_authenticated %}
|
||||
<li class="nav-item"><a class="nav-link" href="#">
|
||||
{% if user.first_name and user.last_name %}
|
||||
{{ user.first_name }} {{ user.last_name }}
|
||||
{% elif user.username %}
|
||||
{{ user.username }}
|
||||
{% else %}
|
||||
{{ user.email }}
|
||||
{% endif %}
|
||||
</a></li>
|
||||
<li class="nav-item"><a class="nav-link text-danger" href="{% url 'accounts:partner_logout' %}">Logout</a></li>
|
||||
{% else %}
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'accounts:partner_login' %}">Login</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container mt-4">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
12
templates/partner/dashboard.html
Normal file
12
templates/partner/dashboard.html
Normal file
@@ -0,0 +1,12 @@
|
||||
{% extends 'partner/base.html' %}
|
||||
{% block content %}
|
||||
<div class="row text-center">
|
||||
<div class="col-md-4">
|
||||
<div class="card p-3 shadow-sm">
|
||||
<h5>Total Partner Users</h5>
|
||||
<h2>{{ total_partner_users }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Add more partner-specific statistics here -->
|
||||
</div>
|
||||
{% endblock %}
|
||||
56
templates/partner/login.html
Normal file
56
templates/partner/login.html
Normal file
@@ -0,0 +1,56 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Partner Login</title>
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body class="bg-light">
|
||||
|
||||
<div class="container d-flex justify-content-center mt-5">
|
||||
<div class="col-md-4">
|
||||
|
||||
<div class="card shadow-sm p-4">
|
||||
|
||||
<h2 class="text-center mb-3">Eventify Partner Portal</h2>
|
||||
|
||||
<h4 class="text-center mb-3">Login</h4>
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-danger py-2">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.username.label_tag }}
|
||||
{{ form.username }}
|
||||
{% for error in form.username.errors %}
|
||||
<small class="text-danger">{{ error }}</small>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.password.label_tag }}
|
||||
{{ form.password }}
|
||||
{% for error in form.password.errors %}
|
||||
<small class="text-danger">{{ error }}</small>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary w-100">Login</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
7
templates/partner/user_confirm_delete.html
Normal file
7
templates/partner/user_confirm_delete.html
Normal file
@@ -0,0 +1,7 @@
|
||||
{% extends 'partner/base.html' %}
|
||||
{% block content %}
|
||||
<h3>Delete Partner User</h3>
|
||||
<p>Are you sure you want to delete <strong>{{ object.username }}</strong>?</p>
|
||||
<form method="post">{% csrf_token %}<button class="btn btn-danger">Yes, delete</button>
|
||||
<a class="btn btn-secondary" href="{% url 'accounts:partner_user_list' %}">Cancel</a></form>
|
||||
{% endblock %}
|
||||
26
templates/partner/user_form.html
Normal file
26
templates/partner/user_form.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% extends 'partner/base.html' %}
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h3>{% if object %}Edit{% else %}Add{% endif %} Partner User</h3>
|
||||
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
{% for field in form %}
|
||||
<div class="mb-3">
|
||||
{{ field.label_tag }}
|
||||
{{ field }}
|
||||
{% if field.help_text %}
|
||||
<small class="form-text text-muted">{{ field.help_text }}</small>
|
||||
{% endif %}
|
||||
{% for error in field.errors %}
|
||||
<div class="text-danger">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<button class="btn btn-primary">Save</button>
|
||||
<a class="btn btn-secondary" href="{% url 'accounts:partner_user_list' %}">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
27
templates/partner/user_list.html
Normal file
27
templates/partner/user_list.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{% extends 'partner/base.html' %}
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between mb-3">
|
||||
<h3>Partner Users</h3>
|
||||
<a class="btn btn-success" href="{% url 'accounts:partner_user_add' %}">Add User</a>
|
||||
</div>
|
||||
<table class="table table-striped">
|
||||
<thead><tr><th>#</th><th>Username</th><th>Email</th><th>Phone</th><th>Role</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
<tr>
|
||||
<td>{{ forloop.counter }}</td>
|
||||
<td>{{ u.username }}</td>
|
||||
<td>{{ u.email }}</td>
|
||||
<td>{{ u.phone_number }}</td>
|
||||
<td>{{ u.get_role_display }}</td>
|
||||
<td>
|
||||
<a class="btn btn-sm btn-primary" href="{% url 'accounts:partner_user_edit' u.pk %}">Edit</a>
|
||||
<a class="btn btn-sm btn-danger" href="{% url 'accounts:partner_user_delete' u.pk %}">Delete</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="6">No users yet.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
98
urls_text.txt
Normal file
98
urls_text.txt
Normal file
@@ -0,0 +1,98 @@
|
||||
/admin/
|
||||
/
|
||||
/register/
|
||||
/logout/
|
||||
/dashboard/
|
||||
/users/
|
||||
/users/add/
|
||||
/users/<int:pk>/edit/
|
||||
/users/<int:pk>/delete/
|
||||
|
||||
/master-data/event-types/
|
||||
/master-data/event-types/add/
|
||||
/master-data/event-types/<int:pk>/edit/
|
||||
/master-data/event-types/<int:pk>/delete/
|
||||
|
||||
/events/
|
||||
/events/add/
|
||||
/events/<int:pk>/edit/
|
||||
/events/<int:pk>/delete/
|
||||
/events/<int:pk>/images/
|
||||
/events/<int:pk>/images/add/
|
||||
/events/<int:pk>/images/<int:img_id>/delete/
|
||||
/events/<int:pk>/images/<int:img_id>/primary/
|
||||
/events/api/create/
|
||||
|
||||
/accounts/login/
|
||||
/accounts/logout/
|
||||
/accounts/dashboard/
|
||||
/accounts/users/
|
||||
/accounts/accounts/users/add/
|
||||
/accounts/users/<int:pk>/edit/
|
||||
/accounts/users/<int:pk>/delete/
|
||||
/accounts/api/login/
|
||||
/accounts/api/logout/
|
||||
/accounts/api/users/list/
|
||||
/accounts/api/users/create/
|
||||
/accounts/api/users/update/
|
||||
/accounts/api/users/delete/
|
||||
/accounts/api/partner/login/
|
||||
/accounts/api/partner/logout/
|
||||
/accounts/api/partner/dashboard/
|
||||
/accounts/api/partner/users/list/
|
||||
/accounts/api/partner/users/create/
|
||||
/accounts/api/partner/users/update/
|
||||
/accounts/api/partner/users/delete/
|
||||
|
||||
/bookings/ticket-meta/create/
|
||||
/bookings/ticket-meta/list/
|
||||
/bookings/ticket-meta/update/
|
||||
/bookings/ticket-meta/delete/
|
||||
/bookings/ticket-meta/deactivate/
|
||||
/bookings/ticket-type/create/
|
||||
/bookings/ticket-type/list/
|
||||
/bookings/ticket-type/update/
|
||||
/bookings/ticket-type/delete/
|
||||
/bookings/ticket-type/deactivate/
|
||||
/bookings/cart/add/
|
||||
/bookings/cart/delete/
|
||||
/bookings/checkout/
|
||||
/bookings/check-in/
|
||||
|
||||
/partner/create/
|
||||
/partner/list/
|
||||
/partner/update/
|
||||
/partner/delete/
|
||||
/partner/update-kyc-documents/
|
||||
/partner/update-address-location/
|
||||
/partner/create-user/
|
||||
/partner/list-users/
|
||||
/partner/update-user/
|
||||
/partner/delete-user/
|
||||
/partner/list-all/
|
||||
/partner/list-kyc-compliant/
|
||||
/partner/list-kyc-pending/
|
||||
/partner/list-high-risk/
|
||||
/partner/list-joined-this-week/
|
||||
|
||||
/banking/payment-gateway/create/
|
||||
/banking/payment-gateway/list/
|
||||
/banking/payment-gateway/update/
|
||||
/banking/payment-gateway/delete/
|
||||
/banking/payment-gateway-credentials/create/
|
||||
/banking/payment-gateway-credentials/list/
|
||||
/banking/payment-gateway-credentials/update/
|
||||
/banking/payment-gateway-credentials/delete/
|
||||
|
||||
/api/user/register/
|
||||
/api/user/login/
|
||||
/api/user/status/
|
||||
/api/user/logout/
|
||||
/api/user/update-profile/
|
||||
/api/events/type-list/
|
||||
/api/events/pincode-events/
|
||||
/api/events/event-details/
|
||||
/api/events/event-images/
|
||||
/api/events/events-by-category/
|
||||
/api/events/events-by-month-year/
|
||||
/api/events/events-by-date/
|
||||
Reference in New Issue
Block a user