This commit is contained in:
Faheed 2025-09-15 17:39:24 +03:00
commit c90f8edc5a
4 changed files with 509 additions and 248 deletions

View File

@ -25,6 +25,7 @@ from api import routing
from inventory.notifications.sse import NotificationSSEApp from inventory.notifications.sse import NotificationSSEApp
from django.urls import re_path from django.urls import re_path
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application
from django.contrib.staticfiles.handlers import ASGIStaticFilesHandler
# application = ProtocolTypeRouter( # application = ProtocolTypeRouter(
# { # {
@ -32,6 +33,9 @@ from django.core.asgi import get_asgi_application
# # "websocket": AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns)), # # "websocket": AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns)),
# } # }
# ) # )
app = get_asgi_application()
application = ProtocolTypeRouter( application = ProtocolTypeRouter(
{ {
"http": AuthMiddlewareStack( "http": AuthMiddlewareStack(
@ -39,10 +43,13 @@ application = ProtocolTypeRouter(
[ [
path("sse/notifications/", NotificationSSEApp()), path("sse/notifications/", NotificationSSEApp()),
re_path( re_path(
r"", get_asgi_application() r"", app
), # All other routes go to Django ), # All other routes go to Django
] ]
) )
), ),
} }
) )
if django.conf.settings.DEBUG:
application = ASGIStaticFilesHandler(app)

View File

@ -1220,11 +1220,11 @@ urlpatterns = [
views.permenant_delete_account, views.permenant_delete_account,
name="permenant_delete_account", name="permenant_delete_account",
), ),
path( # path(
"<slug:dealer_slug>/management/audit_log_dashboard/", # "<slug:dealer_slug>/management/audit_log_dashboard/",
views.AuditLogDashboardView, # views.AuditLogDashboardView,
name="audit_log_dashboard", # name="audit_log_dashboard",
), # ),
######### #########
# Purchase Order # Purchase Order
path( path(

View File

@ -1909,23 +1909,22 @@ def create_make_accounts(dealer):
) )
def handle_payment(request, order): def handle_payment(request, dealer):
url = "https://api.moyasar.com/v1/payments" url = "https://api.moyasar.com/v1/payments"
callback_url = request.build_absolute_uri( callback_url = request.build_absolute_uri(
reverse("payment_callback", kwargs={"dealer_slug": request.dealer.slug}) reverse("payment_callback", kwargs={"dealer_slug": dealer.slug})
) )
if request.user.is_authenticated: email = request.POST.get("email")
email = request.POST["email"] first_name = request.POST.get("first_name")
first_name = request.POST["first_name"] last_name = request.POST.get("last_name")
last_name = request.POST["last_name"] phone = request.POST.get("phone")
phone = request.POST["phone"] card_name = request.POST.get("card_name")
card_number = str(request.POST.get("card_number", "")).replace(" ", "").strip()
card_name = request.POST["card_name"] expiry = request.POST.get("card_expiry", "").split("/")
card_number = str(request.POST["card_number"]).replace(" ", "").strip() month = int(expiry[0].strip()) if len(expiry) > 0 else 0
month = int(request.POST["card_expiry"].split("/")[0].strip()) year = int(expiry[1].strip()) if len(expiry) > 1 else 0
year = int(request.POST["card_expiry"].split("/")[1].strip()) cvv = request.POST.get("card_cvv")
cvv = request.POST["card_cvv"]
user_data = { user_data = {
"email": email, "email": email,
@ -1933,65 +1932,166 @@ def handle_payment(request, order):
"last_name": last_name, "last_name": last_name,
"phone": phone, "phone": phone,
} }
try:
total = int((order.total() + order.tax * order.total() / 100) * 100) # Get selected plan from session
except (AttributeError, TypeError): selected_plan_id = request.session.get('pending_plan_id')
raise ValueError("Order total or tax is invalid") if not selected_plan_id:
payload = json.dumps( raise ValueError("No pending plan found in session")
{ from plans.models import PlanPricing
"amount": total,
"currency": "SAR", pp = PlanPricing.objects.get(pk=selected_plan_id)
"description": f"payment issued for {email}", # Calculate amount without creating order
"callback_url": callback_url, amount_sar = pp.price # assuming price is in SAR
"source": { tax_amount = amount_sar * 15 / 100
"type": "creditcard", total = int((amount_sar + tax_amount) * 100) # convert to halalas
"name": card_name,
"number": card_number, # Pass plan & dealer info via metadata
"month": month, metadata = {
"year": year, **user_data,
"cvc": cvv, "plan_pricing_id": selected_plan_id,
"statement_descriptor": "Century Store", "dealer_slug": dealer.slug,
"3ds": True, }
"manual": False,
"save_card": False, payload = json.dumps({
}, "amount": total,
"metadata": user_data, "currency": "SAR",
} "description": f"Payment for plan {pp.plan.name}",
) "callback_url": callback_url,
"source": {
"type": "creditcard",
"name": card_name,
"number": card_number,
"month": month,
"year": year,
"cvc": cvv,
"statement_descriptor": "Century Store",
"3ds": True,
"manual": False,
"save_card": False,
},
"metadata": metadata,
})
headers = {"Content-Type": "application/json", "Accept": "application/json"} headers = {"Content-Type": "application/json", "Accept": "application/json"}
auth = (settings.MOYASAR_SECRET_KEY, "") auth = (settings.MOYASAR_SECRET_KEY, "")
response = requests.request("POST", url, auth=auth, headers=headers, data=payload) response = requests.post(url, auth=auth, headers=headers, data=payload)
if response.status_code == 400: if response.status_code != 201:
data = response.json() logger.error(f"Payment initiation failed: {response.text}")
if data["type"] == "validation_error": return None, response.get("message")
errors = data.get("errors", {})
if "source.year" in errors:
raise Exception("Invalid expiry year")
else:
raise Exception("Validation Error: ", errors)
else:
print("Failed to process payment:", data)
#
data = response.json() data = response.json()
# order.status = AbstractOrder.STATUS.NEW # Create PaymentHistory WITHOUT linking to order (since order doesn't exist yet)
order.save() amount_decimal = Decimal("{0:.2f}".format(Decimal(total) / Decimal(100)))
#
amount = Decimal("{0:.2f}".format(Decimal(total) / Decimal(100)))
models.PaymentHistory.objects.create( models.PaymentHistory.objects.create(
user=request.user, user=request.user,
user_data=user_data, user_data=json.dumps(metadata),
amount=amount, amount=amount_decimal,
currency=data["currency"], currency=data["currency"],
status=data["status"], status=data["status"],
transaction_id=data["id"], transaction_id=data["id"],
payment_date=data["created_at"], payment_date=data["created_at"],
gateway_response=data, gateway_response=data,
) )
transaction_url = data["source"]["transaction_url"] logger.info(f"Payment initiated: {data}")
return transaction_url return data["source"]["transaction_url"],None
# def handle_payment(request, order):
# logger.info(f"Handling payment for order {order}")
# url = "https://api.moyasar.com/v1/payments"
# callback_url = request.build_absolute_uri(
# reverse("payment_callback", kwargs={"dealer_slug": request.dealer.slug})
# )
# logger.info(f"Got callback_url: {callback_url}")
# if request.user.is_authenticated:
# email = request.POST["email"]
# first_name = request.POST["first_name"]
# last_name = request.POST["last_name"]
# phone = request.POST["phone"]
# logger.info(f"Got user data: {email}, {first_name}, {last_name}, {phone}")
# card_name = request.POST["card_name"]
# card_number = str(request.POST["card_number"]).replace(" ", "").strip()
# month = int(request.POST["card_expiry"].split("/")[0].strip())
# year = int(request.POST["card_expiry"].split("/")[1].strip())
# cvv = request.POST["card_cvv"]
# logger.info(f"Got card data: {card_name}, {card_number[:4]}****, {month}, {year}, {cvv}")
# user_data = {
# "email": email,
# "first_name": first_name,
# "last_name": last_name,
# "phone": phone,
# }
# try:
# total = int((order.total() + order.tax * order.total() / 100) * 100)
# except (AttributeError, TypeError):
# raise ValueError("Order total or tax is invalid")
# logger.info(f"Calculated total: {total}")
# payload = json.dumps(
# {
# "amount": total,
# "currency": "SAR",
# "description": f"payment issued for {email}",
# "callback_url": callback_url,
# "source": {
# "type": "creditcard",
# "name": card_name,
# "number": card_number,
# "month": month,
# "year": year,
# "cvc": cvv,
# "statement_descriptor": "Century Store",
# "3ds": False,
# "manual": False,
# "save_card": False,
# },
# "metadata": user_data,
# }
# )
# logger.info(f"Generated payload: {payload}")
# headers = {"Content-Type": "application/json", "Accept": "application/json"}
# auth = (settings.MOYASAR_SECRET_KEY, "")
# response = requests.request("POST", url, auth=auth, headers=headers, data=payload)
# logger.info(f"Sent request to Moyasar API: {response.status_code}")
# if response.status_code == 400:
# data = response.json()
# if data["type"] == "validation_error":
# errors = data.get("errors", {})
# if "source.year" in errors:
# raise Exception("Invalid expiry year")
# else:
# raise Exception("Validation Error: ", errors)
# else:
# logger.error(f"Failed to process payment: {data}")
# #
# data = response.json()
# # order.status = AbstractOrder.STATUS.NEW
# order.save()
# #
# amount = Decimal("{0:.2f}".format(Decimal(total) / Decimal(100)))
# models.PaymentHistory.objects.create(
# user=request.user,
# user_data=user_data,
# amount=amount,
# currency=data["currency"],
# status=data["status"],
# transaction_id=data["id"],
# payment_date=data["created_at"],
# gateway_response=data,
# )
# transaction_url = data["source"]["transaction_url"]
# logger.info(f"Created payment history and got transaction_url: {transaction_url}")
# return transaction_url
# def get_user_quota(user): # def get_user_quota(user):

View File

@ -218,7 +218,7 @@ from .utils import (
from .tasks import create_accounts_for_make, create_user_dealer, send_email from .tasks import create_accounts_for_make, create_user_dealer, send_email
# djago easy audit log # djago easy audit log
from easyaudit.models import RequestEvent, CRUDEvent, LoginEvent # from easyaudit.models import RequestEvent, CRUDEvent, LoginEvent
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
@ -504,6 +504,7 @@ def general_dashboard(request,dealer_slug):
total_vat_collected = total_vat_collected_from_cars + total_vat_collected_from_services total_vat_collected = total_vat_collected_from_cars + total_vat_collected_from_services
total_revenue_generated = total_revenue_from_cars + total_revenue_from_services total_revenue_generated = total_revenue_from_cars + total_revenue_from_services
# total_expenses=sum([x.amount_paid for x in dealer.entity.get_bills().filter(bill_items__item_role="expense")]) # total_expenses=sum([x.amount_paid for x in dealer.entity.get_bills().filter(bill_items__item_role="expense")])
total_expenses=dealer.entity.get_bills().filter(bill_items__item_role="expense").aggregate(total=Sum('amount_paid'))['total'] or 0 total_expenses=dealer.entity.get_bills().filter(bill_items__item_role="expense").aggregate(total=Sum('amount_paid'))['total'] or 0
gross_profit = net_profit_from_cars+total_revenue_from_services - total_expenses gross_profit = net_profit_from_cars+total_revenue_from_services - total_expenses
@ -9889,50 +9890,123 @@ def pricing_page(request, dealer_slug):
@login_required # @login_required
@permission_required("inventory.change_dealer", raise_exception=True) # @permission_required("inventory.change_dealer", raise_exception=True)
@require_POST # @require_POST
# def submit_plan(request, dealer_slug):
# logger.info(f"dealer slug : {dealer_slug}")
# if request.method == "GET":
# logger.info("method is GET, redirecting to pricing page")
# return redirect("pricing_page", dealer_slug=dealer_slug)
# dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
# logger.info(f"Selected dealer : {dealer}")
# selected_plan_id = request.POST.get("selected_plan")
# logger.info(f"Selected plan id : {selected_plan_id}")
# pp = PlanPricing.objects.get(pk=selected_plan_id)
# logger.info(f"Selected plan pricing : {pp}")
# order = None
# try:
# order = Order.objects.create(
# user=dealer.user,
# plan=pp.plan,
# pricing=pp.pricing,
# amount=pp.price,
# currency="SA",
# tax=15,
# status=1,
# )
# logger.info(f"order created {order}")
# except Exception as e:
# logger.error(e)
# if not order:
# messages.error(request, _("Error creating order"))
# logger.error("unable to create order")
# return redirect("pricing_page", dealer_slug=dealer_slug)
# transaction_url = handle_payment(request, order)
# logger.info(f"Redirecting to : {transaction_url}")
# return redirect(transaction_url)
def submit_plan(request, dealer_slug): def submit_plan(request, dealer_slug):
if request.method == "GET":
return redirect("pricing_page", dealer_slug=dealer_slug)
dealer = get_object_or_404(models.Dealer, slug=dealer_slug) dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
selected_plan_id = request.POST.get("selected_plan") selected_plan_id = request.POST.get("selected_plan")
pp = PlanPricing.objects.get(pk=selected_plan_id) if not selected_plan_id:
order = None messages.error(request, _("No plan selected."))
try:
order = Order.objects.create(
user=dealer.user,
plan=pp.plan,
pricing=pp.pricing,
amount=pp.price,
currency="SA",
tax=15,
status=1,
)
logger.info(f"order created {order}")
except Exception as e:
logger.error(e)
if not order:
messages.error(request, _("Error creating order"))
logger.error("unable to create order")
return redirect("pricing_page", dealer_slug=dealer_slug) return redirect("pricing_page", dealer_slug=dealer_slug)
transaction_url = handle_payment(request, order)
# Store plan & dealer info in session for use in callback
request.session['pending_plan_id'] = selected_plan_id
request.session['pending_dealer_slug'] = dealer_slug
# Initiate payment WITHOUT creating order
transaction_url,error = handle_payment(request, dealer)
if not transaction_url:
messages.error(request, _(f"Payment initiation failed. {error}"))
return redirect("pricing_page", dealer_slug=dealer_slug)
return redirect(transaction_url) return redirect(transaction_url)
@login_required # @login_required
@permission_required("inventory.change_dealer", raise_exception=True)
def payment_callback(request, dealer_slug): def payment_callback(request, dealer_slug):
message = request.GET.get("message")
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
payment_id = request.GET.get("id") payment_id = request.GET.get("id")
history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first()
payment_status = request.GET.get("status") payment_status = request.GET.get("status")
logger.info(f"Received payment callback for dealer_slug: {dealer_slug}, payment_id: {payment_id}, status: {payment_status}") message = request.GET.get("message", "")
order = Order.objects.filter(user=dealer.user, status=1).first() # Status 1 = NEW
if history.status == "paid":
return redirect('home')
if payment_status == "paid":
logger.info(f"Payment successful for transaction ID {payment_id}. Processing order completion.")
logger.info(f"Received payment callback for dealer_slug: {dealer_slug}, payment_id: {payment_id}, status: {payment_status}")
history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first()
if not history:
logger.error(f"No PaymentHistory found for transaction_id: {payment_id}")
return render(request, "payment_failed.html", {"message": "Invalid transaction"})
if history.status == "paid":
logger.info("Payment already processed. Redirecting to home.")
return redirect('home')
if payment_status == "paid":
logger.info(f"Payment successful for transaction ID {payment_id}. Creating order...")
# Get metadata from PaymentHistory (passed during handle_payment)
metadata = history.user_data
if isinstance(metadata, str):
try:
metadata = json.loads(metadata)
except json.JSONDecodeError:
logger.error(f"Failed to decode metadata JSON: {metadata}")
metadata = {}
plan_pricing_id = metadata.get("plan_pricing_id")
dealer_slug_from_meta = metadata.get("dealer_slug")
if not plan_pricing_id or dealer_slug_from_meta != dealer_slug:
logger.error("Invalid metadata in payment callback")
history.status = "failed"
history.save()
return render(request, "payment_failed.html", {"message": "Invalid payment data"})
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
pp = get_object_or_404(PlanPricing, pk=plan_pricing_id)
# ✅ CREATE ORDER HERE
try:
order = Order.objects.create(
user=dealer.user,
plan=pp.plan,
pricing=pp.pricing,
amount=pp.price,
currency="SAR", # Fixed typo: was "SA"
tax=15,
status=Order.STATUS.NEW, # Use constant if available
)
logger.info(f"Order {order.id} created for user {dealer.user}")
except Exception as e:
logger.exception(f"Failed to create order: {e}")
history.status = "failed"
history.save()
return render(request, "payment_failed.html", {"message": "Order creation failed"})
# Create or get BillingInfo
billing_info, created = BillingInfo.objects.get_or_create( billing_info, created = BillingInfo.objects.get_or_create(
user=dealer.user, user=dealer.user,
defaults={ defaults={
@ -9949,8 +10023,8 @@ def payment_callback(request, dealer_slug):
else: else:
logger.debug(f"Billing info already exists for user {dealer.user}.") logger.debug(f"Billing info already exists for user {dealer.user}.")
# Create or update UserPlan
if not hasattr(order.user, 'userplan'): if not hasattr(order.user, 'userplan'):
print(order.get_plan_pricing().pricing.period)
UserPlan.objects.create( UserPlan.objects.create(
user=order.user, user=order.user,
plan=order.plan, plan=order.plan,
@ -9958,26 +10032,22 @@ def payment_callback(request, dealer_slug):
) )
logger.info(f"Created new UserPlan for user {order.user} with plan {order.plan}.") logger.info(f"Created new UserPlan for user {order.user} with plan {order.plan}.")
else: else:
# Optional: upgrade existing plan
# user_plan = order.user.userplan
# user_plan.plan = order.plan
# user_plan.save()
logger.info(f"UserPlan already exists for user {order.user}.") logger.info(f"UserPlan already exists for user {order.user}.")
try: try:
# Complete the order (this may generate invoice, etc.)
# if order.user.userplan:
# user = order.user
# pricing = order.get_plan_pricing().pricing
# logger.info(f"Processing order completion for {user} - upgrading to {order.plan}")
# user.userplan.plan = order.plan
# user.userplan.expire = datetime.now() + timedelta(days=pricing.period)
# user.userplan.save()
# user.save()
# logger.info(f"User {user} upgraded to {order.plan} plan successfully")
order.complete_order() order.complete_order()
history.status = "paid" history.status = "paid"
history.order = order # Link payment to order
history.save() history.save()
logger.info(f"Order {order.id} for user {order.user} completed successfully. Payment history updated.")
invoice = order.get_invoices().first() invoice = order.get_invoices().first()
logger.info(f"Order {order.id} completed. Rendering success page.")
return render( return render(
request, request,
"payment_success.html", "payment_success.html",
@ -9985,13 +10055,14 @@ def payment_callback(request, dealer_slug):
) )
except Exception as e: except Exception as e:
logger.exception(f"Error completing order {order.id} for user {order.user}: {e}") logger.exception(f"Error completing order {order.id}: {e}")
logger.error(f"Plan activation failed: {str(e)}")
history.status = "failed" history.status = "failed"
history.save() history.save()
return render(request, "payment_failed.html", {"message": "Plan activation error"}) return render(request, "payment_failed.html", {"message": "Plan activation error"})
finally: finally:
if dealer := getattr(order.user,"dealer", None): # Activate dealer & staff if needed
if dealer := getattr(order.user, "dealer", None):
if not dealer.user.is_active: if not dealer.user.is_active:
dealer.user.is_active = True dealer.user.is_active = True
dealer.user.save() dealer.user.save()
@ -9999,7 +10070,6 @@ def payment_callback(request, dealer_slug):
if not staff.user.is_active: if not staff.user.is_active:
staff.activate_account() staff.activate_account()
logger.info(f"Order {order.id} for user {order.user} completed successfully. Payment history updated.")
elif payment_status == "failed": elif payment_status == "failed":
logger.warning(f"Payment failed for transaction ID {payment_id}. Message: {message}") logger.warning(f"Payment failed for transaction ID {payment_id}. Message: {message}")
history.status = "failed" history.status = "failed"
@ -10007,6 +10077,90 @@ def payment_callback(request, dealer_slug):
return render(request, "payment_failed.html", {"message": message}) return render(request, "payment_failed.html", {"message": message})
return render(request, "payment_failed.html", {"message": "Unknown payment status"}) return render(request, "payment_failed.html", {"message": "Unknown payment status"})
# @login_required
# @permission_required("inventory.change_dealer", raise_exception=True)
# def payment_callback(request, dealer_slug):
# message = request.GET.get("message")
# dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
# payment_id = request.GET.get("id")
# history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first()
# payment_status = request.GET.get("status")
# logger.info(f"Received payment callback for dealer_slug: {dealer_slug}, payment_id: {payment_id}, status: {payment_status}")
# order = Order.objects.filter(user=dealer.user, status=1).first() # Status 1 = NEW
# logger.info(f"Retrieved order {order.id} for user {order.user}.")
# if history.status == "paid":
# logger.info(f"Payment history already marked as paid for transaction ID {payment_id}. Redirecting to home.")
# return redirect('home')
# if payment_status == "paid":
# logger.info(f"Payment successful for transaction ID {payment_id}. Processing order completion.")
# billing_info, created = BillingInfo.objects.get_or_create(
# user=dealer.user,
# defaults={
# 'tax_number': dealer.vrn,
# 'name': dealer.arabic_name,
# 'street': dealer.address,
# 'zipcode': dealer.entity.zip_code or " ",
# 'city': dealer.entity.city or " ",
# 'country': dealer.entity.country or " ",
# }
# )
# if created:
# logger.info(f"Created new billing info for user {dealer.user}.")
# else:
# logger.debug(f"Billing info already exists for user {dealer.user}.")
# if not hasattr(order.user, 'userplan'):
# logger.info(f"Creating new UserPlan for user {order.user} with plan {order.plan}.")
# UserPlan.objects.create(
# user=order.user,
# plan=order.plan,
# # expire=datetime.now().date() + timedelta(days=order.get_plan_pricing().pricing.period)
# )
# else:
# logger.info(f"UserPlan already exists for user {order.user}.")
# try:
# logger.info(f"Processing order completion for {order.user} - upgrading to {order.plan}")
# order.complete_order()
# history.status = "paid"
# history.save()
# logger.info(f"Order {order.id} for user {order.user} completed successfully. Payment history updated.")
# invoice = order.get_invoices().first()
# logger.info(f"Redirecting to payment success page with invoice {invoice.id}.")
# return render(
# request,
# "payment_success.html",
# {"order": order, "invoice": invoice}
# )
# except Exception as e:
# logger.exception(f"Error completing order {order.id} for user {order.user}: {e}")
# logger.error(f"Plan activation failed: {str(e)}")
# history.status = "failed"
# history.save()
# logger.info(f"Redirecting to payment failed page with message {message}.")
# return render(request, "payment_failed.html", {"message": "Plan activation error"})
# finally:
# if dealer := getattr(order.user,"dealer", None):
# logger.info(f"Activating dealer {dealer} and its staff.")
# if not dealer.user.is_active:
# logger.info(f"Activating dealer {dealer}.")
# dealer.user.is_active = True
# dealer.user.save()
# for staff in dealer.get_staff():
# logger.info(f"Activating staff {staff}.")
# if not staff.user.is_active:
# staff.activate_account()
# logger.info(f"Order {order.id} for user {order.user} completed successfully. Payment history updated.")
# elif payment_status == "failed":
# logger.warning(f"Payment failed for transaction ID {payment_id}. Message: {message}")
# history.status = "failed"
# history.save()
# logger.info(f"Redirecting to payment failed page with message {message}.")
# return render(request, "payment_failed.html", {"message": message})
# logger.info(f"Redirecting to payment failed page with message {message}.")
# return render(request, "payment_failed.html", {"message": "Unknown payment status"})
# def payment_callback(request, dealer_slug): # def payment_callback(request, dealer_slug):
# message = request.GET.get("message") # message = request.GET.get("message")
# dealer = get_object_or_404(models.Dealer, slug=dealer_slug) # dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
@ -10350,140 +10504,140 @@ def user_management(request, dealer_slug):
return render(request, "admin_management/user_management.html", context) return render(request, "admin_management/user_management.html", context)
@login_required # @login_required
@permission_required("inventory.change_dealer", raise_exception=True) # @permission_required("inventory.change_dealer", raise_exception=True)
def AuditLogDashboardView(request, dealer_slug): # def AuditLogDashboardView(request, dealer_slug):
""" # """
Displays audit logs (User Actions, Login Events, Request Events) with pagination. # Displays audit logs (User Actions, Login Events, Request Events) with pagination.
Log type is determined by the 'q' query parameter (e.g., ?q=userActions). # Log type is determined by the 'q' query parameter (e.g., ?q=userActions).
Pagination page number is passed as a query parameter (e.g., ?page=2). # Pagination page number is passed as a query parameter (e.g., ?page=2).
""" # """
get_object_or_404(models.Dealer, slug=dealer_slug) # get_object_or_404(models.Dealer, slug=dealer_slug)
q = request.GET.get("q") # Get the log type from the 'q' query parameter # q = request.GET.get("q") # Get the log type from the 'q' query parameter
current_pagination_page = request.GET.get("page", 1) # current_pagination_page = request.GET.get("page", 1)
context = {} # context = {}
template_name = None # template_name = None
logs_per_page = 30 # Define logs per page once # logs_per_page = 30 # Define logs per page once
# --- Determine Data Source and Template based on 'q' parameter --- # # --- Determine Data Source and Template based on 'q' parameter ---
if ( # if (
q == "userRequests" # q == "userRequests"
): # This block handles cases where 'q' is 'requestEvents', None, or any other invalid value. # ): # This block handles cases where 'q' is 'requestEvents', None, or any other invalid value.
# It defaults to Request Logs if 'q' is not 'userActions' or 'loginEvents'. # # It defaults to Request Logs if 'q' is not 'userActions' or 'loginEvents'.
template_name = "admin_management/request_logs.html" # template_name = "admin_management/request_logs.html"
context["title"] = "Request Logs Dashboard" # context["title"] = "Request Logs Dashboard"
request_events = RequestEvent.objects.all().order_by("-datetime") # request_events = RequestEvent.objects.all().order_by("-datetime")
paginator = Paginator(request_events, logs_per_page) # paginator = Paginator(request_events, logs_per_page)
try: # try:
page_obj = paginator.page(current_pagination_page) # page_obj = paginator.page(current_pagination_page)
except PageNotAnInteger: # except PageNotAnInteger:
page_obj = paginator.page(1) # page_obj = paginator.page(1)
except EmptyPage: # except EmptyPage:
page_obj = paginator.page(paginator.num_pages) # page_obj = paginator.page(paginator.num_pages)
elif q == "loginEvents": # elif q == "loginEvents":
template_name = "admin_management/auth_logs.html" # template_name = "admin_management/auth_logs.html"
context["title"] = "Login Events Dashboard" # context["title"] = "Login Events Dashboard"
auth_events = LoginEvent.objects.all().order_by("-datetime") # auth_events = LoginEvent.objects.all().order_by("-datetime")
paginator = Paginator(auth_events, logs_per_page) # paginator = Paginator(auth_events, logs_per_page)
try: # try:
page_obj = paginator.page(current_pagination_page) # page_obj = paginator.page(current_pagination_page)
except PageNotAnInteger: # except PageNotAnInteger:
page_obj = paginator.page(1) # page_obj = paginator.page(1)
except EmptyPage: # except EmptyPage:
page_obj = paginator.page(paginator.num_pages) # page_obj = paginator.page(paginator.num_pages)
else: # else:
template_name = "admin_management/model_logs.html" # template_name = "admin_management/model_logs.html"
context["title"] = "User Actions Dashboard" # context["title"] = "User Actions Dashboard"
# OPTIMIZATION: Get the QuerySet but don't evaluate it yet # # OPTIMIZATION: Get the QuerySet but don't evaluate it yet
model_events_queryset = CRUDEvent.objects.all().order_by("-datetime") # model_events_queryset = CRUDEvent.objects.all().order_by("-datetime")
# 1. Paginate the raw QuerySet FIRST # # 1. Paginate the raw QuerySet FIRST
paginator = Paginator(model_events_queryset, logs_per_page) # paginator = Paginator(model_events_queryset, logs_per_page)
try: # try:
# Get the page object, which contains only the raw QuerySet objects for the current page # # Get the page object, which contains only the raw QuerySet objects for the current page
page_obj_raw = paginator.page(current_pagination_page) # page_obj_raw = paginator.page(current_pagination_page)
except PageNotAnInteger: # except PageNotAnInteger:
page_obj_raw = paginator.page(1) # page_obj_raw = paginator.page(1)
except EmptyPage: # except EmptyPage:
page_obj_raw = paginator.page(paginator.num_pages) # page_obj_raw = paginator.page(paginator.num_pages)
# 2. Now, process 'field_changes' ONLY for the events on the current page # # 2. Now, process 'field_changes' ONLY for the events on the current page
processed_model_events_for_page = [] # processed_model_events_for_page = []
for ( # for (
event # event
) in page_obj_raw.object_list: # Loop only through the current page's items # ) in page_obj_raw.object_list: # Loop only through the current page's items
event_data = { # event_data = {
"datetime": event.datetime, # "datetime": event.datetime,
"user": event.user, # "user": event.user,
"event_type_display": event.get_event_type_display(), # "event_type_display": event.get_event_type_display(),
"model_name": event.content_type.model, # "model_name": event.content_type.model,
"object_id": event.object_id, # "object_id": event.object_id,
"object_repr": event.object_repr, # "object_repr": event.object_repr,
"field_changes": [], # "field_changes": [],
} # }
if event.changed_fields: # if event.changed_fields:
try: # try:
changes = json.loads(event.changed_fields) # changes = json.loads(event.changed_fields)
if isinstance(changes, dict): # if isinstance(changes, dict):
for field_name, values in changes.items(): # for field_name, values in changes.items():
old_value = ( # old_value = (
values[0] # values[0]
if isinstance(values, list) and len(values) > 0 # if isinstance(values, list) and len(values) > 0
else None # else None
) # )
new_value = ( # new_value = (
values[1] # values[1]
if isinstance(values, list) and len(values) > 1 # if isinstance(values, list) and len(values) > 1
else None # else None
) # )
event_data["field_changes"].append( # event_data["field_changes"].append(
{ # {
"field": field_name, # "field": field_name,
"old": old_value, # "old": old_value,
"new": new_value, # "new": new_value,
} # }
) # )
elif changes is None: # elif changes is None:
event_data["field_changes"].append( # event_data["field_changes"].append(
{ # {
"field": "Info", # "field": "Info",
"old": "", # "old": "",
"new": "No specific field changes recorded (JSON was null)", # "new": "No specific field changes recorded (JSON was null)",
} # }
) # )
else: # Handle valid JSON but not a dictionary (e.g., "[]", 123) # else: # Handle valid JSON but not a dictionary (e.g., "[]", 123)
event_data["field_changes"].append( # event_data["field_changes"].append(
{ # {
"field": "Error", # "field": "Error",
"old": "", # "old": "",
"new": f"Unexpected JSON format: {type(changes).__name__}", # "new": f"Unexpected JSON format: {type(changes).__name__}",
} # }
) # )
except json.JSONDecodeError: # except json.JSONDecodeError:
# Handle invalid JSON; you might log this error # # Handle invalid JSON; you might log this error
event_data["field_changes"].append( # event_data["field_changes"].append(
{ # {
"field": "Error", # "field": "Error",
"old": "", # "old": "",
"new": "Invalid JSON in changed_fields", # "new": "Invalid JSON in changed_fields",
} # }
) # )
processed_model_events_for_page.append(event_data) # processed_model_events_for_page.append(event_data)
# 3. Replace the object_list of the original page_obj with the processed data # # 3. Replace the object_list of the original page_obj with the processed data
# This keeps all pagination properties (has_next, number, etc.) intact. # # This keeps all pagination properties (has_next, number, etc.) intact.
page_obj_raw.object_list = processed_model_events_for_page # page_obj_raw.object_list = processed_model_events_for_page
page_obj = page_obj_raw # This will be passed to the context # page_obj = page_obj_raw # This will be passed to the context
# Pass the final page object to the context # # Pass the final page object to the context
context["page_obj"] = page_obj # context["page_obj"] = page_obj
return render(request, template_name, context) # return render(request, template_name, context)
@login_required @login_required