Fix admin totals template syntax error and enhance search box styling

This commit is contained in:
Flatlogic Bot 2026-02-01 04:09:57 +00:00
parent 82b68976c5
commit f1de11cc52
6 changed files with 227 additions and 215 deletions

View File

@ -66,6 +66,7 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.humanize',
'rest_framework',
'rest_framework.authtoken',
'drf_yasg',
@ -287,7 +288,7 @@ JAZZMIN_SETTINGS = {
"default_icon_children": "fas fa-circle",
"related_modal_active": False,
"custom_css": "css/custom.css",
"custom_js": None,
"custom_js": "js/admin_date_range_dropdown.js",
"use_google_fonts_cdn": True,
"show_ui_builder": False,
"language_chooser": True,

View File

@ -16,9 +16,10 @@ from django.http import HttpResponse, HttpResponseRedirect
from rangefilter.filters import DateRangeFilter
from django.template.loader import render_to_string
import weasyprint
from django.db.models import Sum
class DropdownDateRangeFilter(DateRangeFilter):
template = 'admin/dropdown_date_range_filter.html'
pass
class ProfileInline(admin.StackedInline):
model = Profile
@ -97,6 +98,8 @@ class CustomUserAdmin(UserAdmin):
send_whatsapp_link.allow_tags = True
class ParcelAdmin(admin.ModelAdmin):
change_list_template = 'admin/core/parcel/change_list.html'
list_display = ('tracking_number', 'shipper', 'carrier', 'price', 'driver_amount', 'platform_fee', 'distance_km', 'status', 'payment_status', 'created_at')
list_filter = (
'status',
@ -106,6 +109,9 @@ class ParcelAdmin(admin.ModelAdmin):
search_fields = ('tracking_number', 'shipper__username', 'receiver_name', 'carrier__username')
actions = ['export_as_csv', 'print_parcels', 'export_pdf']
class Media:
js = ('js/admin_date_range_dropdown.js',)
fieldsets = (
(None, {
'fields': ('tracking_number', 'shipper', 'carrier', 'status', 'payment_status', 'thawani_session_id')
@ -124,6 +130,21 @@ class ParcelAdmin(admin.ModelAdmin):
'fields': ('delivery_country', 'delivery_governate', 'delivery_city', 'delivery_address', 'delivery_lat', 'delivery_lng')
}),
)
def changelist_view(self, request, extra_context=None):
response = super().changelist_view(request, extra_context)
# Calculate totals for the filtered queryset
if hasattr(response, 'context_data') and 'cl' in response.context_data:
qs = response.context_data['cl'].queryset
metrics = qs.aggregate(
total_price=Sum('price'),
total_driver_amount=Sum('driver_amount'),
total_platform_fee=Sum('platform_fee')
)
response.context_data['summary_metrics'] = metrics
return response
def export_as_csv(self, request, queryset):
response = HttpResponse(content_type='text/csv')

View File

@ -0,0 +1,33 @@
{% extends "admin/change_list.html" %}
{% load i18n %}
{% block result_list %}
{{ block.super }}
{% if summary_metrics %}
<div class="card" style="margin-top: 15px; border: 1px solid #dee2e6;">
<div class="card-header bg-info text-white">
<h5 class="card-title m-0">
<i class="fas fa-calculator mr-2"></i> {% trans "Totals (Current Page/Filter)" %}
</h5>
</div>
<div class="card-body p-0">
<table class="table table-striped table-bordered m-0">
<thead>
<tr>
<th class="text-center">{% trans "Total Price" %}</th>
<th class="text-center">{% trans "Total Driver Amount" %}</th>
<th class="text-center">{% trans "Total Platform Fee" %}</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center text-bold">{{ summary_metrics.total_price|default:0|stringformat:".3f" }} OMR</td>
<td class="text-center text-bold">{{ summary_metrics.total_driver_amount|default:0|stringformat:".3f" }} OMR</td>
<td class="text-center text-bold">{{ summary_metrics.total_platform_fee|default:0|stringformat:".3f" }} OMR</td>
</tr>
</tbody>
</table>
</div>
</div>
{% endif %}
{% endblock %}

View File

@ -1,213 +0,0 @@
{% load i18n rangefilter_compat static %}
<h3>{{ title }}</h3>
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/widgets.css' %}">
<style nonce="{{ spec.request.csp_nonce }}">
{% default_css_vars_if_needed %}
.admindatefilter .button, .admindatefilter input[type=submit], .admindatefilter input[type=button], .admindatefilter .submit-row input, .admindatefilter a.button,
.admindatefilter .button, .admindatefilter input[type=reset] {
background: var(--button-bg);
padding: 4px 5px;
border: none;
border-radius: 4px;
color: var(--button-fg);
cursor: pointer;
}
.admindatefilter {
padding-left: 15px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border-color);
}
.admindatefilter p {
padding-left: 0px;
line-height: 0;
}
.admindatefilter p.datetime {
line-height: 0;
}
.admindatefilter .timezonewarning {
display: none;
}
.admindatefilter .datetimeshortcuts a:first-child {
margin-right: 4px;
display: none;
}
.calendarbox {
z-index: 1100;
}
.clockbox {
z-index: 1100;
margin-left: -8em !important;
margin-top: 5em !important;
}
.admindatefilter .datetimeshortcuts {
font-size: 0;
float: right;
position: absolute;
padding-top: 4px;
}
.admindatefilter a {
color: #999;
position: absolute;
padding-top: 3px;
padding-left: 4px;
}
.range-preset-select {
width: 95%;
margin: 10px 0 10px 15px;
padding: 6px;
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--body-bg);
color: var(--body-fg);
}
@media (min-width: 768px) {
.calendarbox {
margin-left: -16em !important;
margin-top: 9em !important;
}
}
@media (max-width: 767px) {
.calendarbox {
overflow: visible;
}
}
</style>
<script type="text/javascript" src="{% url 'admin:jsi18n' %}"></script>
<script type="text/javascript" nonce="{{ spec.request.csp_nonce }}">
function embedScript(url) {
return new Promise(function pr(resolve, reject) {
var newScript = document.createElement("script");
newScript.type = "text/javascript";
newScript.src = url;
newScript.onload = resolve;
if ("{{ spec.request.csp_nonce }}" !== "") {
newScript.setAttribute("nonce", "{{ spec.request.csp_nonce }}");
}
document.head.appendChild(newScript);
});
}
django.jQuery(window).on('load', function () {
if (!('DateTimeShortcuts' in window)) {
var promiseList = [];
{% for m in spec.form.js %}
promiseList.push(embedScript("{{ m }}"));
{% endfor %}
Promise.all(promiseList).then(function() {
django.jQuery('.datetimeshortcuts').remove();
if ('DateTimeShortcuts' in window) {
window.DateTimeShortcuts.init();
}
});
}
});
django.jQuery('document').ready(function () {
// Original logic for submit/reset
django.jQuery('.admindatefilter #{{ choices.0.system_name }}-form input[type="submit"]').click(function(event) {
event.preventDefault();
var form = django.jQuery(this).closest('div.admindatefilter').find('form');
var query_string = django.jQuery('input#{{ choices.0.system_name }}-query-string').val();
var form_data = form.serialize();
var amp = query_string === "?" ? "" : "&";
window.location = window.location.pathname + query_string + amp + form_data;
});
django.jQuery('.admindatefilter #{{ choices.0.system_name }}-form input[type="reset"]').click(function() {
var form = django.jQuery(this).closest('div.admindatefilter').find('form');
var query_string = form.find('input#{{ choices.0.system_name }}-query-string').val();
window.location = window.location.pathname + query_string;
});
// Custom Dropdown Logic
var $ = django.jQuery;
var formId = '#{{ choices.0.system_name }}-form';
var $form = $(formId);
var $container = $form.closest('.admindatefilter');
// Build Select
var $select = $('<select class="range-preset-select">' +
'<option value="all">{% trans "Any Date" %}</option>' +
'<option value="today">{% trans "Today" %}</option>' +
'<option value="7days">{% trans "Last 7 Days" %}</option>' +
'<option value="month">{% trans "This Month" %}</option>' +
'<option value="year">{% trans "This Year" %}</option>' +
'<option value="custom">{% trans "Custom Range..." %}</option>' +
'</select>');
$container.before($select);
var $gte = $form.find('input[name$="__gte"]');
var $lte = $form.find('input[name$="__lte"]');
var $submit = $form.find('input[type="submit"]');
function formatDate(d) {
var year = d.getFullYear();
var month = ('0' + (d.getMonth() + 1)).slice(-2);
var day = ('0' + d.getDate()).slice(-2);
return year + '-' + month + '-' + day;
}
// Check initial state
if ($gte.val() || $lte.val()) {
$select.val('custom');
$container.show();
} else {
$select.val('all');
$container.hide();
}
$select.on('change', function() {
var val = $(this).val();
var today = new Date();
if (val === 'custom') {
$container.slideDown();
} else {
// If not custom, calculate and submit
if (val === 'all') {
// Reset
$form.find('input[type="reset"]').click();
return;
}
var startStr = '';
var endStr = formatDate(today); // Default end is today
if (val === 'today') {
startStr = formatDate(today);
} else if (val === '7days') {
var past = new Date();
past.setDate(today.getDate() - 7);
startStr = formatDate(past);
} else if (val === 'month') {
var firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
startStr = formatDate(firstDay);
} else if (val === 'year') {
var firstDay = new Date(today.getFullYear(), 0, 1);
startStr = formatDate(firstDay);
}
$gte.val(startStr);
$lte.val(endStr);
// Trigger submit
$submit.click();
}
});
});
</script>
<div class="admindatefilter">
<form method="GET" action="." id="{{ choices.0.system_name }}-form">
{{ spec.form.as_p }}
{% for choice in choices %}
<input type="hidden" id="{{ choice.system_name }}-query-string" value="{{ choice.query_string }}">
{% endfor %}
<div class="controls">
<input type="submit" class="button" value="{% trans "Search" %}">
<input type="reset" class="button" value="{% trans "Reset" %}">
</div>
</form>
</div>

View File

@ -177,4 +177,39 @@ body.model-platformprofile label[for="id_admin_panel_logo"] {
.form-row {
padding: 15px 10px;
border-bottom: 1px solid #f0f0f0;
}
/* --- Fix Admin Search Box Layout --- */
/* Target the search form container in Jazzmin/AdminLTE */
#changelist-search,
#changelist-search .form-group,
#changelist-search .input-group {
display: flex !important;
align-items: center !important;
flex-wrap: nowrap !important; /* Prevent wrapping */
max-width: 400px !important; /* Reduce the overall width */
}
/* Make the input field take available space but respect the container width */
#changelist-search input[type="text"] {
width: auto !important;
flex-grow: 1 !important;
margin-right: 8px !important; /* Space between input and button */
}
/* Fix for RTL */
[dir="rtl"] #changelist-search input[type="text"] {
margin-right: 0 !important;
margin-left: 8px !important;
}
/* Ensure the button stays inline and overrides any block behavior */
#changelist-search button[type="submit"],
#changelist-search .btn {
margin-top: 0 !important;
margin-bottom: 0 !important;
white-space: nowrap !important;
width: auto !important;
height: auto !important;
}

View File

@ -0,0 +1,135 @@
(function($) {
$(document).ready(function() {
// Helper to format date as YYYY-MM-DD
function formatDate(d) {
var year = d.getFullYear();
var month = ('0' + (d.getMonth() + 1)).slice(-2);
var day = ('0' + d.getDate()).slice(-2);
return year + '-' + month + '-' + day;
}
function initDateRangeDropdown() {
// Find start date inputs for 'created_at' (or generic generic approach for any range filter)
// rangefilter inputs typically have names ending in __gte and __lte
var $gteInputs = $('input[name$="__gte"]');
$gteInputs.each(function() {
var $gte = $(this);
var name = $gte.attr('name');
var prefix = name.substring(0, name.lastIndexOf('__gte'));
var $lte = $('input[name="' + prefix + '__lte"]');
if ($lte.length === 0) return; // Not a pair
// Find a container to inject the dropdown.
// In Jazzmin/Standard, this might be inside a .admindatefilter div,
// or just a li, or a div.controls.
// We'll look for the closest container that seems to wrap the filter.
// Try to find .admindatefilter first
var $container = $gte.closest('.admindatefilter');
if ($container.length === 0) {
// Fallback for Jazzmin or other themes: closest .card-body or similar?
// Or just the parent form/div
$container = $gte.closest('div[data-filter-name], li, .form-row, .card-body');
}
if ($container.length === 0) $container = $gte.parent();
if ($container.data('dropdown-init')) return;
$container.data('dropdown-init', true);
// Hide the original inputs/controls container
// We need to be careful not to hide the form itself if it's the main filter form
// Usually rangefilter puts inputs in a 'controls' div or paragraphs.
var $controls = $gte.closest('.controls');
if ($controls.length === 0) {
// Try to find the immediate parent if it contains both inputs
$controls = $gte.parent();
}
// Create Select
var $select = $('<select class="form-control admin-date-dropdown" style="width: 100%; margin-bottom: 10px; margin-top: 5px;">' +
'<option value="any">Any Date</option>' +
'<option value="today">Today</option>' +
'<option value="7days">Last 7 Days</option>' +
'<option value="month">This Month</option>' +
'<option value="year">This Year</option>' +
'<option value="custom">Custom Range...</option>' +
'</select>');
// Inject before the controls (inputs)
if ($controls.length) {
$controls.before($select);
} else {
$gte.before($select);
}
// Initial State
var gteVal = $gte.val();
var lteVal = $lte.val();
if (gteVal || lteVal) {
$select.val('custom');
$controls.show();
} else {
$select.val('any');
$controls.hide();
}
$select.on('change', function() {
var val = $(this).val();
var today = new Date();
if (val === 'custom') {
$controls.slideDown();
} else {
if (val === 'any') {
$gte.val('');
$lte.val('');
} else {
var startStr = '';
var endStr = formatDate(today);
if (val === 'today') {
startStr = formatDate(today);
} else if (val === '7days') {
var past = new Date();
past.setDate(today.getDate() - 7);
startStr = formatDate(past);
} else if (val === 'month') {
var firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
startStr = formatDate(firstDay);
} else if (val === 'year') {
var firstDay = new Date(today.getFullYear(), 0, 1);
startStr = formatDate(firstDay);
}
$gte.val(startStr);
$lte.val(endStr);
}
// Submit form
// In Jazzmin, the filter form might be #changelist-search or similiar
var $form = $gte.closest('form');
if ($form.length) {
$form.submit();
} else {
// Try to find a global apply button or trigger change?
// Some admin themes auto-submit on change.
// rangefilter usually has a submit button.
var $btn = $container.find('input[type="submit"], button[type="submit"]');
if ($btn.length) $btn.click();
}
}
});
});
}
// Run init
initDateRangeDropdown();
// Safety: Run again after a slight delay in case of dynamic loading (unlikely in admin but possible)
setTimeout(initDateRangeDropdown, 500);
});
})(django.jQuery);