feat: Make the admin interface less ugh

This commit is contained in:
Kumi 2025-06-24 16:56:35 +02:00
parent 9c754f55c6
commit 925a4158ac
Signed by: kumi
GPG key ID: ECBCC9082395383F
10 changed files with 2048 additions and 747 deletions

View file

@ -1,2 +1,3 @@
migrations/ migrations/
.ruff_cache/ .ruff_cache/
admin/

View file

@ -1,5 +1,4 @@
from django.contrib import admin from django.contrib import admin
from django.urls import path
from django.contrib.admin import AdminSite from django.contrib.admin import AdminSite
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -7,6 +6,7 @@ from django.urls import reverse
from django.utils.html import format_html from django.utils.html import format_html
from django.contrib.contenttypes.admin import GenericTabularInline from django.contrib.contenttypes.admin import GenericTabularInline
from django.utils import timezone from django.utils import timezone
from django.contrib.admin.models import LogEntry
from datetime import timedelta from datetime import timedelta
@ -47,6 +47,10 @@ class DuckpondAdminSite(AdminSite):
site_title = "Duckpond Admin" site_title = "Duckpond Admin"
index_title = "Dashboard" index_title = "Dashboard"
index_template = "admin/index.html"
login_template = "admin/login.html"
app_index_template = "admin/app_index.html"
def index(self, request, extra_context=None): def index(self, request, extra_context=None):
# Get counts for dashboard stats # Get counts for dashboard stats
from duckpond.core.models import User, Membership from duckpond.core.models import User, Membership
@ -55,6 +59,9 @@ class DuckpondAdminSite(AdminSite):
# Calculate date for "recent" items # Calculate date for "recent" items
recent_date = timezone.now() - timedelta(days=30) recent_date = timezone.now() - timedelta(days=30)
# Get recent admin actions
recent_actions = LogEntry.objects.select_related("content_type", "user")[:10]
# Prepare stats # Prepare stats
stats = { stats = {
"user_count": User.objects.count(), "user_count": User.objects.count(),
@ -63,6 +70,7 @@ class DuckpondAdminSite(AdminSite):
"recent_payments": Payment.objects.filter( "recent_payments": Payment.objects.filter(
created_at__gte=recent_date created_at__gte=recent_date
).count(), ).count(),
"recent_actions": recent_actions,
} }
context = extra_context or {} context = extra_context or {}
@ -114,7 +122,16 @@ class UserAdmin(BaseUserAdmin):
(None, {"fields": ("username", "password")}), (None, {"fields": ("username", "password")}),
( (
_("Personal info"), _("Personal info"),
{"fields": ("preferred_name", "first_name", "last_name", "company_name", "email", "address")}, {
"fields": (
"preferred_name",
"first_name",
"last_name",
"company_name",
"email",
"address",
)
},
), ),
( (
_("Permissions"), _("Permissions"),
@ -131,7 +148,14 @@ class UserAdmin(BaseUserAdmin):
(_("Important dates"), {"fields": ("last_login", "date_joined")}), (_("Important dates"), {"fields": ("last_login", "date_joined")}),
( (
_("Membership"), _("Membership"),
{"fields": ("is_approved", "email_verified", "gnucash_customer_id", "payment_reference")}, {
"fields": (
"is_approved",
"email_verified",
"gnucash_customer_id",
"payment_reference",
)
},
), ),
) )
list_display = ( list_display = (
@ -479,4 +503,4 @@ admin.site.register(User, UserAdmin)
admin.site.register(Client, ClientAdmin) admin.site.register(Client, ClientAdmin)
admin.site.register(Code, CodeAdmin) admin.site.register(Code, CodeAdmin)
admin.site.register(Token, TokenAdmin) admin.site.register(Token, TokenAdmin)
admin.site.register(RSAKey, RSAKeyAdmin) admin.site.register(RSAKey, RSAKeyAdmin)

View file

@ -1,589 +0,0 @@
/* static/core/css/admin_override.css */
:root {
/* Admin Color Palette (Derived from your main style.css) */
--admin-primary: #4361ee; /* Main accent color */
--admin-primary-dark: #3f37c9; /* Darker accent for hover */
--admin-primary-light: #a1aff7; /* Lighter accent */
--admin-primary-fg: #ffffff; /* Text color on primary */
--admin-secondary: #4cc9f0; /* Secondary accent (less used) */
--admin-body-bg: #f8f9fa; /* Main background */
--admin-content-bg: #ffffff; /* Background for content areas */
--admin-body-fg: #212529; /* Main text color */
--admin-body-quiet-color: #6c757d; /* Lighter text */
--admin-body-loud-color: #000000; /* Used rarely */
--admin-header-bg: var(--admin-primary);
--admin-header-fg: var(--admin-primary-fg);
--admin-module-header-bg: #e9ecef; /* Lighter header for modules */
--admin-module-header-fg: var(--admin-body-fg);
--admin-module-border-color: #dee2e6;
--admin-link-fg: var(--admin-primary);
--admin-link-hover-color: var(--admin-primary-dark);
--admin-button-bg: var(--admin-primary);
--admin-button-fg: var(--admin-primary-fg);
--admin-button-hover-bg: var(--admin-primary-dark);
--admin-delete-button-bg: #dc3545; /* Danger color */
--admin-delete-button-hover-bg: #c82333;
--admin-message-success-bg: #d1e7dd;
--admin-message-success-fg: #0f5132;
--admin-message-warning-bg: #fff3cd;
--admin-message-warning-fg: #664d03;
--admin-message-error-bg: #f8d7da;
--admin-message-error-fg: #58151c;
--admin-border-radius: 0.375rem; /* Consistent border radius */
--admin-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--admin-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);
}
body {
background-color: var(--admin-body-bg);
color: var(--admin-body-fg);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-size: 0.95rem; /* Slightly smaller base font for admin */
}
a {
color: var(--admin-link-fg);
}
a:hover {
color: var(--admin-link-hover-color);
}
/* --- Header --- */
#header {
background-color: var(--admin-header-bg);
color: var(--admin-header-fg);
box-shadow: var(--admin-box-shadow-sm);
height: auto;
padding: 0.75rem 1.5rem;
}
#branding h1, #branding h1 a {
color: var(--admin-header-fg);
font-weight: 500;
font-size: 1.25rem;
}
#branding img {
vertical-align: -5px; /* Better alignment for the logo */
}
#user-tools {
color: var(--admin-header-fg);
font-size: 0.9rem;
font-weight: 500;
}
#user-tools a {
color: var(--admin-header-fg);
border-bottom: none;
padding: 0.5rem 0.75rem;
border-radius: var(--admin-border-radius);
transition: background-color 0.2s ease;
}
#user-tools a:hover {
background-color: rgba(255, 255, 255, 0.15);
color: var(--admin-header-fg);
}
/* --- Breadcrumbs --- */
div.breadcrumbs {
background: var(--admin-content-bg);
padding: 0.75rem 1.5rem;
border-bottom: 1px solid var(--admin-module-border-color);
box-shadow: var(--admin-box-shadow-sm);
color: var(--admin-body-quiet-color);
font-size: 0.85rem;
}
div.breadcrumbs a {
color: var(--admin-link-fg);
}
div.breadcrumbs a:hover {
color: var(--admin-link-hover-color);
}
/* --- Main Content Area --- */
#content {
padding: 1.5rem;
}
/* --- Modules --- */
.module {
border: none;
background: var(--admin-content-bg);
border-radius: var(--admin-border-radius);
box-shadow: var(--admin-box-shadow-sm);
margin-bottom: 1.5rem;
}
.module caption, .module h2, .inline-group h2 {
background: var(--admin-module-header-bg);
color: var(--admin-module-header-fg);
border-bottom: 1px solid var(--admin-module-border-color);
padding: 0.75rem 1.25rem;
margin: 0;
font-size: 1rem;
font-weight: 500;
}
.module table {
border-collapse: collapse; /* Ensure borders look clean */
}
.module td, .module th {
padding: 0.75rem 1.25rem;
border-bottom: 1px solid var(--admin-module-border-color);
font-size: 0.9rem;
vertical-align: middle; /* Ensure vertical alignment is consistent */
}
.module tr:last-child td, .module tr:last-child th {
border-bottom: none; /* Remove border from last row */
}
.module th {
font-weight: 600;
color: var(--admin-body-fg);
background-color: #f8f9fa; /* Slightly different bg for table headers */
}
.module table thead th {
text-align: left;
border-bottom-width: 2px; /* Thicker border below table main headers */
border-bottom-color: var(--admin-module-border-color);
}
/* Alternating Row Colors (Applied to both td and th in tbody) */
.module table tbody tr:nth-child(even) td,
.module table tbody tr:nth-child(even) th {
background-color: var(--admin-body-bg);
}
/* --- Forms --- */
.form-row {
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--admin-module-border-color);
margin-bottom: 0; /* Remove default margin */
}
.form-row:last-child {
border-bottom: none;
}
.aligned label {
font-weight: 500;
padding-top: 0.6em; /* Align label better with input */
color: var(--admin-body-quiet-color);
width: 12em; /* Adjust label width as needed */
}
form .aligned p.help, form .aligned div.help {
font-size: 0.85rem;
color: var(--admin-body-quiet-color);
margin-left: 13em; /* Match label width + spacing */
}
input[type="text"], input[type="password"], input[type="email"], input[type="url"],
input[type="number"], input[type="date"], input[type="datetime-local"],
input[type="month"], input[type="week"], input[type="time"],
input[type="search"], input[type="tel"], textarea, select {
border: 1px solid var(--admin-module-border-color);
border-radius: var(--admin-border-radius);
padding: 0.5rem 0.75rem;
font-size: 0.9rem;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
input:focus, textarea:focus, select:focus {
border-color: var(--admin-primary-light);
box-shadow: 0 0 0 0.2rem rgba(67, 97, 238, 0.25);
outline: none;
}
.selector-available h2, .selector-chosen h2 {
font-size: 0.9rem;
font-weight: 500;
background: var(--admin-module-header-bg);
border: 1px solid var(--admin-module-border-color);
border-bottom: none;
padding: 0.5rem 0.75rem;
}
/* --- Buttons --- */
.button, input[type=submit], input[type=button], .submit-row input, a.button {
background: var(--admin-button-bg);
color: var(--admin-button-fg);
border: none;
border-radius: var(--admin-border-radius);
padding: 0.6rem 1.2rem;
font-size: 0.85rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
cursor: pointer;
transition: background-color 0.2s ease;
text-decoration: none;
line-height: normal; /* Reset line-height */
}
.button:hover, input[type=submit]:hover, input[type=button]:hover, .submit-row input:hover, a.button:hover {
background: var(--admin-button-hover-bg);
color: var(--admin-button-fg);
}
.button.default, input[type=submit].default, .submit-row input.default {
background: var(--admin-primary); /* Ensure default uses primary */
}
.button.default:hover, input[type=submit].default:hover, .submit-row input.default:hover {
background: var(--admin-primary-dark);
}
.button.cancel-link, a.button.cancel-link {
background: var(--admin-body-quiet-color);
}
.button.cancel-link:hover, a.button.cancel-link:hover {
background: var(--admin-body-fg);
}
a.deletelink {
background: var(--admin-delete-button-bg);
color: var(--admin-button-fg);
}
a.deletelink:hover {
background: var(--admin-delete-button-hover-bg);
}
.submit-row {
background: var(--admin-content-bg);
border-top: 1px solid var(--admin-module-border-color);
padding: 1rem 1.25rem;
overflow: visible;
border-radius: 0 0 var(--admin-border-radius) var(--admin-border-radius);
}
.submit-row p.deletelink-box {
float: left; /* Align delete button left */
}
.submit-row input {
margin-left: 0.5rem;
}
/* --- Object Tools --- */
.object-tools {
padding: 0;
margin: 0;
}
.object-tools a {
background: var(--admin-button-bg);
color: var(--admin-button-fg);
padding: 0.5rem 1rem;
border-radius: var(--admin-border-radius);
font-size: 0.8rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-left: 0.5rem;
transition: background-color 0.2s ease;
}
.object-tools a:hover {
background: var(--admin-button-hover-bg);
color: var(--admin-button-fg);
}
.object-tools a.addlink {
background-color: #28a745; /* Green for add */
}
.object-tools a.addlink:hover {
background-color: #218838;
}
.object-tools a.historylink {
background-color: #ffc107; /* Yellow for history */
color: var(--admin-body-fg);
}
.object-tools a.historylink:hover {
background-color: #e0a800;
}
/* --- Changelist --- */
#changelist-filter {
background: var(--admin-content-bg);
border-radius: var(--admin-border-radius);
box-shadow: var(--admin-box-shadow-sm);
padding: 1rem 1.25rem;
border: none;
margin-bottom: 1.5rem;
}
#changelist-filter h2 {
font-size: 1rem;
font-weight: 500;
color: var(--admin-body-fg);
margin-bottom: 0.75rem;
}
#changelist-filter h3 {
font-size: 0.9rem;
font-weight: 500;
color: var(--admin-body-quiet-color);
margin-bottom: 0.5rem;
}
#changelist-filter ul {
padding-left: 0;
list-style: none;
}
#changelist-filter li {
margin-bottom: 0.25rem;
}
#changelist-filter li.selected a {
font-weight: bold;
color: var(--admin-link-hover-color);
}
#changelist table thead th {
background-color: var(--admin-module-header-bg); /* Header background for changelist */
}
#changelist .actions {
background: var(--admin-module-header-bg);
border-top: 1px solid var(--admin-module-border-color);
border-bottom: 1px solid var(--admin-module-border-color);
padding: 0.75rem 1.25rem;
border-radius: 0; /* Remove radius from actions bar */
}
#changelist .paginator {
border-top: 1px solid var(--admin-module-border-color);
padding: 0.75rem 1.25rem;
background: var(--admin-content-bg);
border-radius: 0 0 var(--admin-border-radius) var(--admin-border-radius);
}
#changelist table tbody tr:hover {
background-color: #f0f3ff; /* Subtle hover effect */
}
#changelist table tbody th a {
font-weight: 500; /* Make main link slightly bolder */
}
/* --- Messages --- */
ul.messagelist li {
background-color: var(--admin-message-success-bg);
color: var(--admin-message-success-fg);
border: 1px solid rgba(0,0,0,0.1);
padding: 0.75rem 1.25rem;
border-radius: var(--admin-border-radius);
font-size: 0.9rem;
margin-bottom: 1rem;
}
ul.messagelist li.warning {
background-color: var(--admin-message-warning-bg);
color: var(--admin-message-warning-fg);
}
ul.messagelist li.error {
background-color: var(--admin-message-error-bg);
color: var(--admin-message-error-fg);
}
/* --- Related Widget Wrapper --- */
.related-widget-wrapper {
border: 1px solid var(--admin-module-border-color);
border-radius: var(--admin-border-radius);
}
.related-widget-wrapper > select {
border: none;
border-radius: 0;
}
/* --- Dashboard / Index --- */
.dashboard #content {
width: auto; /* Let content flow */
}
.dashboard .module table caption a {
color: var(--admin-module-header-fg); /* Match caption text color */
}
.dashboard .module table td a {
background: none;
color: var(--admin-link-fg);
display: inline;
padding: 0;
border-radius: 0;
text-align: left;
margin: 0;
text-decoration: underline;
}
.dashboard .module table td a:hover {
background: none;
color: var(--admin-link-hover-color);
}
.dashboard .addlink, .dashboard .changelink, .dashboard .viewlink {
margin-left: 0.5em;
}
.dashboard .module table th a {
font-weight: bold;
}
.dashboard .module table td, .dashboard .module table th {
white-space: normal; /* Allow wrapping in dashboard tables */
}
/* Custom Dashboard Widgets from index.html */
.dashboard-widget {
background: var(--admin-content-bg);
border-radius: var(--admin-border-radius);
box-shadow: var(--admin-box-shadow-sm);
margin-bottom: 1.5rem;
overflow: hidden;
}
.dashboard-widget-title {
background: var(--admin-module-header-bg);
color: var(--admin-module-header-fg);
border-bottom: 1px solid var(--admin-module-border-color);
padding: 0.75rem 1.25rem;
font-size: 1rem;
font-weight: 500;
}
.dashboard-widget-content {
padding: 1.25rem;
}
.dashboard-widget .module { /* Reset styling for modules inside widgets */
box-shadow: none;
border-radius: 0;
margin: 0;
background: none;
}
.dashboard-widget .module table {
margin-top: 1rem;
}
.dashboard-widget .module table caption {
background: none;
color: var(--admin-body-fg);
padding: 0 0 0.5rem 0;
border-bottom: none;
text-align: left;
font-size: 1.1rem;
font-weight: 600;
}
.dashboard-widget ul {
list-style: none;
padding: 0;
margin: 0;
}
.dashboard-widget ul li {
margin-bottom: 0.5rem;
}
/* Dashboard Stats */
.dashboard-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1.5rem;
margin-bottom: 1.5rem;
}
.stat-box {
background: var(--admin-content-bg);
border-radius: var(--admin-border-radius);
box-shadow: var(--admin-box-shadow-sm);
padding: 1.25rem;
text-align: center;
}
.stat-number {
font-size: 2rem;
font-weight: 600;
color: var(--admin-primary);
margin-bottom: 0.25rem;
line-height: 1.2;
}
.stat-label {
color: var(--admin-body-quiet-color);
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* --- Sidebar --- */
#content-related {
background: var(--admin-body-bg); /* Match main bg */
border: none;
margin-left: 1.5rem;
}
#content-related .module {
background: var(--admin-content-bg);
border: none; /* Remove border */
}
#content-related h2 {
font-size: 1rem;
font-weight: 500;
padding: 0.75rem 1.25rem;
background: var(--admin-module-header-bg);
color: var(--admin-module-header-fg);
border-bottom: 1px solid var(--admin-module-border-color);
}
#content-related .actionlist {
padding: 0.75rem 1.25rem;
}
#content-related .actionlist li {
margin-bottom: 0.5rem;
}
/* --- Responsive Adjustments --- */
@media (max-width: 1024px) {
#header {
padding: 0.5rem 1rem;
}
#content {
padding: 1rem;
}
.aligned label {
width: 10em;
}
form .aligned p.help, form .aligned div.help {
margin-left: 11em;
}
}
@media (max-width: 767px) {
#header {
display: flex;
flex-direction: column;
align-items: flex-start;
height: auto;
}
#branding {
margin-bottom: 0.5rem;
}
#user-tools {
width: 100%;
text-align: right;
padding-top: 0.5rem;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
.object-tools {
position: static;
margin: 1rem calc(-1 * var(--content-padding)) 0; /* Adjust based on #content padding */
padding: 0.75rem var(--content-padding);
background: var(--admin-module-header-bg);
border-top: 1px solid var(--admin-module-border-color);
border-bottom: 1px solid var(--admin-module-border-color);
}
.object-tools a {
margin-left: 0;
margin-right: 0.5rem;
}
#content-related {
margin-left: 0;
margin-top: 1.5rem;
}
.colM { /* Make main column full width on mobile */
width: 100%;
}
.colSM { /* Make sidebar full width on mobile */
width: 100%;
}
.aligned label {
width: 100%;
text-align: left;
float: none;
padding-bottom: 0.25rem;
}
.aligned .form-row div {
float: none;
width: 100%;
padding-left: 0;
}
form .aligned p.help, form .aligned div.help {
margin-left: 0;
margin-top: 0.25rem;
}
.submit-row {
padding: 1rem;
}
.submit-row input, .submit-row p.deletelink-box a {
display: block;
width: 100%;
margin: 0.5rem 0;
}
.submit-row p.deletelink-box {
float: none;
text-align: center;
margin-bottom: 1rem;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,108 @@
document.addEventListener('DOMContentLoaded', function () {
// User menu toggle
const userInfo = document.querySelector('.user-info');
const userMenu = document.querySelector('.user-menu');
if (userInfo && userMenu) {
userInfo.addEventListener('click', function (e) {
e.preventDefault();
userMenu.classList.toggle('show');
});
// Close menu when clicking outside
document.addEventListener('click', function (e) {
if (!userInfo.contains(e.target) && !userMenu.contains(e.target)) {
userMenu.classList.remove('show');
}
});
}
// Add icons to sidebar menu items based on model
const modelIcons = {
'user': 'fas fa-user',
'membership': 'fas fa-id-card',
'membershiptier': 'fas fa-layer-group',
'paymentcycle': 'fas fa-calendar-alt',
'customfield': 'fas fa-list-alt',
'payment': 'fas fa-money-bill-wave',
'paymentmethod': 'fas fa-credit-card',
'content': 'fas fa-file-alt',
'category': 'fas fa-folder',
'contenttype': 'fas fa-file-code',
'eventregistration': 'fas fa-calendar-check',
'certificate': 'fas fa-certificate',
'download': 'fas fa-download',
'page': 'fas fa-file',
'section': 'fas fa-puzzle-piece',
'form': 'fas fa-wpforms',
'formsubmission': 'fas fa-paper-plane',
'announcement': 'fas fa-bullhorn'
};
// Add default icon for models without specific icon
const defaultIcon = 'fas fa-cube';
// Find all model links in the sidebar
const modelLinks = document.querySelectorAll('.model-link');
modelLinks.forEach(link => {
const modelName = link.parentElement.className.split('model-')[1];
if (modelName) {
const icon = document.createElement('i');
icon.className = modelIcons[modelName] || defaultIcon;
icon.style.marginRight = '8px';
link.insertBefore(icon, link.firstChild);
}
});
// Enhance form widgets
enhanceFormWidgets();
});
function enhanceFormWidgets() {
// Add datepicker to date inputs
const dateInputs = document.querySelectorAll('input[type="date"]');
dateInputs.forEach(input => {
// Add calendar icon
const wrapper = document.createElement('div');
wrapper.className = 'date-input-wrapper';
wrapper.style.position = 'relative';
const icon = document.createElement('i');
icon.className = 'fas fa-calendar-alt';
icon.style.position = 'absolute';
icon.style.right = '10px';
icon.style.top = '50%';
icon.style.transform = 'translateY(-50%)';
icon.style.color = '#6c757d';
icon.style.pointerEvents = 'none';
input.parentNode.insertBefore(wrapper, input);
wrapper.appendChild(input);
wrapper.appendChild(icon);
});
// Add select2-like styling to select elements
const selectElements = document.querySelectorAll('select:not([multiple])');
selectElements.forEach(select => {
const wrapper = document.createElement('div');
wrapper.className = 'select-wrapper';
wrapper.style.position = 'relative';
const icon = document.createElement('i');
icon.className = 'fas fa-chevron-down';
icon.style.position = 'absolute';
icon.style.right = '10px';
icon.style.top = '50%';
icon.style.transform = 'translateY(-50%)';
icon.style.color = '#6c757d';
icon.style.pointerEvents = 'none';
select.parentNode.insertBefore(wrapper, select);
wrapper.appendChild(select);
wrapper.appendChild(icon);
// Style the select element
select.style.appearance = 'none';
select.style.paddingRight = '30px';
});
}

View file

@ -0,0 +1,47 @@
{% load i18n %}
{% if app_list %}
{% for app in app_list %}
<div class="app-{{ app.app_label }} module{% if app.app_url in request.path %} current-app{% endif %}">
<table>
<caption>
<a href="{{ app.app_url }}"
class="section"
title="{% blocktranslate with name=app.name %}Models in the {{ name }} application{% endblocktranslate %}">
<i class="fas fa-folder"></i> {{ app.name }}
</a>
</caption>
{% for model in app.models %}
<tr class="model-{{ model.object_name|lower }}{% if model.admin_url in request.path %} current-model{% endif %}">
{% if model.admin_url %}
<th scope="row">
<a href="{{ model.admin_url }}"
{% if model.admin_url in request.path %}aria-current="page"{% endif %}>
<i class="fas fa-table"></i> {{ model.name }}</a>
</th>
{% else %}
<th scope="row">{{ model.name }}</th>
{% endif %}
{% if model.add_url %}
<td>
<a href="{{ model.add_url }}" class="addlink">
<i class="fas fa-plus"></i> {% translate 'Add' %}</a>
</td>
{% else %}
<td></td>
{% endif %}
{% if model.admin_url and show_changelinks %}
<td>
<a href="{{ model.admin_url }}" class="changelink">
<i class="fas fa-list"></i> {% translate 'Change' %}</a>
</td>
{% else %}
<td></td>
{% endif %}
</tr>
{% endfor %}
</table>
</div>
{% endfor %}
{% else %}
<p>{% translate 'You don\'t have permission to view or edit anything.' %}</p>
{% endif %}

View file

@ -0,0 +1,200 @@
{% load i18n static %}
<!DOCTYPE html>
{% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %}
<html lang="{{ LANGUAGE_CODE|default:"en-us" }}"
dir="{{ LANGUAGE_BIDI|yesno:'rtl,ltr,auto' }}">
<head>
<title>
{% block title %}{% endblock %}
</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet"
href="{% block stylesheet %}{% static "admin/css/base.css" %}{% endblock %}">
<link rel="stylesheet"
href="https://nocdnbs.private.coffee/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="{% static 'admin/css/duckpond-admin.css' %}">
{% if not is_popup and is_nav_sidebar_enabled %}
<link rel="stylesheet" href="{% static "admin/css/nav_sidebar.css" %}">
<script src="{% static 'admin/js/nav_sidebar.js' %}" defer></script>
{% endif %}
{% block extrastyle %}{% endblock %}
{% if LANGUAGE_BIDI %}
<link rel="stylesheet"
href="{% block stylesheet_rtl %}{% static "admin/css/rtl.css" %}{% endblock %}">
{% endif %}
{% block extrahead %}{% endblock %}
{% block responsive %}
<meta name="viewport"
content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0">
<link rel="stylesheet" href="{% static "admin/css/responsive.css" %}">
{% if LANGUAGE_BIDI %}
<link rel="stylesheet" href="{% static "admin/css/responsive_rtl.css" %}">
{% endif %}
{% endblock %}
{% block blockbots %}<meta name="robots" content="NONE,NOARCHIVE">{% endblock %}
<style type="text/css">
/* Force full width layout */
html, body {
width: 100%;
max-width: 100%;
margin: 0;
padding: 0;
}
#container {
width: 100%;
max-width: 100%;
margin: 0;
padding: 0;
}
#main {
width: 100%;
max-width: 100%;
}
#content {
width: 100%;
max-width: 100%;
padding: 20px;
box-sizing: border-box;
}
#content-main {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
/* Fix for app list */
#app-list {
width: 100%;
}
.app-section {
width: 100%;
}
/* Fix for colMS layout */
.colMS {
display: flex;
flex-wrap: wrap;
}
.colMS #content-main {
flex: 1;
min-width: 0;
max-width: 100%;
}
.colMS #content-related {
width: 260px;
margin-left: 20px;
}
@media (max-width: 1024px) {
.colMS {
flex-direction: column;
}
.colMS #content-main {
width: 100%;
}
.colMS #content-related {
width: 100%;
margin-left: 0;
margin-top: 20px;
}
}
</style>
</head>
<body class="{% if is_popup %}popup {% endif %}{% block bodyclass %}{% endblock %}"
data-admin-utc-offset="{% now "Z" %}">
<!-- Container -->
<div id="container">
{% if not is_popup %}
<!-- Header -->
<div id="header">
<div id="branding">
{% block branding %}{% endblock %}
</div>
{% block usertools %}
{% if has_permission %}
<div id="user-tools">
{% block welcome-msg %}
{% translate 'Welcome,' %}
<strong>{% firstof user.get_short_name user.get_username %}</strong>.
{% endblock %}
{% block userlinks %}
{% if site_url %}
<a href="{{ site_url }}">{% translate 'View site' %}</a> /
{% endif %}
{% if user.is_active and user.is_staff %}
{% url 'django-admindocs-docroot' as docsroot %}
{% if docsroot %}
<a href="{{ docsroot }}">{% translate 'Documentation' %}</a> /
{% endif %}
{% endif %}
{% if user.has_usable_password %}
<a href="{% url 'admin:password_change' %}">{% translate 'Change password' %}</a> /
{% endif %}
<form id="logout-form" method="post" action="{% url 'admin:logout' %}">
{% csrf_token %}
<button type="submit" class="logout-button">{% translate 'Log out' %}</button>
</form>
{% endblock %}
</div>
{% endif %}
{% endblock %}
{% block nav-global %}{% endblock %}
</div>
<!-- END Header -->
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
{% if title %}&rsaquo; {{ title }}{% endif %}
</div>
{% endblock %}
{% endif %}
<div id="main" class="{% if not is_popup %}full-width{% endif %}">
{% if not is_popup and is_nav_sidebar_enabled %}
{% block nav-sidebar %}
{% include "admin/nav_sidebar.html" %}
{% endblock %}
{% endif %}
<div id="content-wrapper"
class="{% if not is_popup and is_nav_sidebar_enabled %}shifted{% endif %}">
{% block messages %}
{% if messages %}
<ul class="messagelist">
{% for message in messages %}
<li {% if message.tags %}class="{{ message.tags }}"{% endif %}>{{ message|capfirst }}</li>
{% endfor %}
</ul>
{% endif %}
{% endblock messages %}
<!-- Content -->
<div id="content" class="{% block coltype %}colM{% endblock %}">
{% block pretitle %}{% endblock %}
{% block content_title %}
{% if title %}<h1>{{ title }}</h1>{% endif %}
{% endblock %}
{% block content_subtitle %}
{% if subtitle %}<h2>{{ subtitle }}</h2>{% endif %}
{% endblock %}
{% block content %}
{% block object-tools %}{% endblock %}
{{ content }}
{% endblock %}
{% block sidebar %}{% endblock %}
<br class="clear">
</div>
<!-- END Content -->
</div>
</div>
</div>
<!-- END Container -->
<script src="{% static 'admin/js/duckpond-admin.js' %}"></script>
</body>
</html>

View file

@ -1,21 +1,46 @@
{% extends "admin/base_site.html" %} {% extends "admin/base.html" %}
{% load static i18n %} {% load static %}
{% load i18n %}
{% block title %}{{ title }} | {{ site_title|default:_('Duckpond Admin') }}{% endblock %} {% block title %}{{ title }} | {{ site_title|default:_("Duckpond Admin") }}{% endblock %}
{% block branding %}
<h1 id="site-name">
<a href="{% url 'admin:index' %}">
<img src="{% static 'core/img/logo_admin.png' %}" alt="{{ site_header|default:_('Duckpond Administration') }}" height="30" style="margin-right: 10px; vertical-align: middle;">
</a>
</h1>
{% endblock %}
{% block nav-global %}{% endblock %}
{% block extrastyle %} {% block extrastyle %}
{{ block.super }} {# Loads default admin CSS #} <link rel="stylesheet" href="{% static 'admin/css/duckpond-admin.css' %}">
<link rel="preconnect" href="https://googledonts.private.coffee" crossorigin> <link rel="stylesheet"
<link href="https://googledonts.private.coffee/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> href="https://nocdnbs.private.coffee/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/admin_override.css' %}"> {% endblock %}
{% endblock %} {% block branding %}
<h1 id="site-name">
<a href="{% url 'admin:index' %}">
<span class="brand-icon"><i class="fas fa-water"></i></span>
<span class="brand-text">{{ site_header|default:_("Duckpond Administration") }}</span>
</a>
</h1>
{% endblock %}
{% block nav-global %}{% endblock %}
{% block usertools %}
<div id="user-tools" class="user-tools-container">
{% if user.is_active and user.is_staff %}
<div class="user-info">
<span class="user-avatar">
<i class="fas fa-user-circle"></i>
</span>
<span class="user-name">
{% firstof user.get_short_name user.get_username %}
<i class="fas fa-chevron-down"></i>
</span>
</div>
<div class="user-menu">
{% if user.has_usable_password %}
<a href="{% url 'admin:password_change' %}">
<i class="fas fa-key"></i> {% translate 'Change password' %}
</a>
{% endif %}
<a href="{% url 'admin:logout' %}">
<i class="fas fa-sign-out-alt"></i> {% translate 'Log out' %}
</a>
</div>
{% endif %}
</div>
{% endblock %}
{% block extrajs %}
<script src="{% static 'admin/js/duckpond-admin.js' %}"></script>
{% endblock %}

View file

@ -1,34 +0,0 @@
{% extends "admin/base_site.html" %}
{% load i18n admin_urls static admin_modify %}
{% block extrahead %}
{{ block.super }}
<script src="{% url 'admin:jsi18n' %}"></script>
{{ form.media }}
{% endblock %}
{% block content %}
<div id="content-main">
<form method="post" id="add_section_form">
{% csrf_token %}
<div>
<fieldset class="module aligned">
{% for field in form %}
<div class="form-row">
<div class="field-box">
{{ field.errors }}
{{ field.label_tag }}
{{ field }}
{% if field.help_text %}<div class="help">{{ field.help_text|safe }}</div>{% endif %}
</div>
</div>
{% endfor %}
</fieldset>
<div class="submit-row">
<input type="submit"
value="{% trans 'Add Section' %}"
class="default"
name="_save">
</div>
</div>
</form>
</div>
{% endblock %}

View file

@ -1,116 +1,194 @@
{% extends "admin/index.html" %} {% extends "admin/base_site.html" %}
{% load i18n %} {% load i18n static %}
{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet"
type="text/css"
href="{% static "admin/css/dashboard.css" %}">
{% endblock %}
{% block coltype %}colMS{% endblock %}
{% block bodyclass %}{{ block.super }} dashboard{% endblock %}
{% block breadcrumbs %}{% endblock %}
{% block nav-sidebar %}{% endblock %}
{% block content %} {% block content %}
<div id="content-main"> <div id="content-main">
<div class="dashboard-stats"> <!-- Stats Dashboard -->
<div class="stat-box"> <div class="dashboard">
<div class="stat-number">{{ user_count }}</div> <div class="stat-card stat-users">
<div class="stat-label">Users</div> <div class="stat-icon">
</div> <i class="fas fa-users"></i>
<div class="stat-box"> </div>
<div class="stat-number">{{ active_memberships }}</div> <div class="stat-info">
<div class="stat-label">Active Memberships</div> <h3 class="stat-value">{{ user_count }}</h3>
</div> <p class="stat-label">Total Users</p>
<div class="stat-box"> </div>
<div class="stat-number">{{ pending_memberships }}</div> </div>
<div class="stat-label">Pending Memberships</div> <div class="stat-card stat-memberships">
</div> <div class="stat-icon">
<div class="stat-box"> <i class="fas fa-id-card"></i>
<div class="stat-number">{{ recent_payments }}</div> </div>
<div class="stat-label">Recent Payments</div> <div class="stat-info">
</div> <h3 class="stat-value">{{ active_memberships }}</h3>
</div> <p class="stat-label">Active Memberships</p>
</div>
<div class="dashboard-widget"> </div>
<div class="dashboard-widget-title">{% trans 'Quick Actions' %}</div> <div class="stat-card stat-payments">
<div class="dashboard-widget-content"> <div class="stat-icon">
<div class="module"> <i class="fas fa-money-bill-wave"></i>
<table> </div>
<tr> <div class="stat-info">
<td><a href="{% url 'import_member' %}">Import Existing Member</a></td> <h3 class="stat-value">{{ recent_payments }}</h3>
</tr> <p class="stat-label">Recent Payments</p>
<tr> </div>
<td><a href="{% url 'admin:core_membership_add' %}">Add New Membership</a></td> </div>
</tr> <div class="stat-card stat-pending">
<tr> <div class="stat-icon">
<td><a href="{% url 'admin:core_page_add' %}">Create New Page</a></td> <i class="fas fa-clock"></i>
</tr> </div>
<tr> <div class="stat-info">
<td><a href="{% url 'admin:core_announcement_add' %}">Add Announcement</a></td> <h3 class="stat-value">{{ pending_memberships }}</h3>
</tr> <p class="stat-label">Pending Approvals</p>
</table> </div>
</div> </div>
</div> </div>
</div> <!-- Quick Actions -->
<div class="quick-actions">
{% if app_list %} <h2>Quick Actions</h2>
<div class="dashboard-widget"> <div class="action-buttons">
<div class="dashboard-widget-title">{% trans 'Applications' %}</div> <a href="{% url 'admin:core_user_add' %}" class="action-button primary">
<div class="dashboard-widget-content"> <i class="fas fa-user-plus"></i> Add User
</a>
<a href="{% url 'admin:core_membership_add' %}"
class="action-button primary">
<i class="fas fa-id-card"></i> Create Membership
</a>
<a href="{% url 'admin:payments_payment_add' %}"
class="action-button primary">
<i class="fas fa-money-bill"></i> Record Payment
</a>
<a href="{% url 'admin:core_formsubmission_changelist' %}"
class="action-button">
<i class="fas fa-clipboard-list"></i> View Form Submissions
</a>
<a href="{% url 'admin:member_area_content_add' %}"
class="action-button">
<i class="fas fa-file-alt"></i> Add Content
</a>
<a href="{% url 'admin:member_area_eventregistration_changelist' %}"
class="action-button">
<i class="fas fa-calendar-check"></i> Event Registrations
</a>
</div>
</div>
<!-- Recent Activity -->
<div class="recent-activity">
<h2>Recent Activity</h2>
<ul class="activity-list">
{% for log in recent_actions %}
<li class="activity-item">
<div class="activity-icon">
{% if log.is_addition %}
<i class="fas fa-plus"></i>
{% elif log.is_change %}
<i class="fas fa-edit"></i>
{% elif log.is_deletion %}
<i class="fas fa-trash"></i>
{% endif %}
</div>
<div class="activity-content">
<p class="activity-text">
{{ log.user.username }}
{% if log.is_addition %}
added
{% elif log.is_change %}
changed
{% elif log.is_deletion %}
deleted
{% endif %}
{{ log.content_type.name }}: {{ log.object_repr }}
</p>
<p class="activity-meta">{{ log.action_time|timesince }} ago</p>
</div>
{% if log.is_addition or log.is_change %}
<a href="{{ log.get_admin_url }}" class="activity-action">
<i class="fas fa-eye"></i>
</a>
{% endif %}
</li>
{% empty %}
<li class="activity-item">
<p>No recent activity.</p>
</li>
{% endfor %}
</ul>
</div>
<!-- App List -->
<div id="app-list">
{% for app in app_list %} {% for app in app_list %}
<div class="app-{{ app.app_label }} module{% if app.app_url in request.path %} current-app{% endif %}"> <div class="app-section">
<table> <h2 class="app-name">{{ app.name }}</h2>
<caption> <div class="app-models">
<a href="{{ app.app_url }}" class="section" title="{% blocktrans with name=app.name %}Models in the {{ name }} application{% endblocktrans %}">{{ app.name }}</a>
</caption>
{% for model in app.models %} {% for model in app.models %}
<tr class="model-{{ model.object_name|lower }}{% if model.admin_url in request.path %} current-model{% endif %}"> <div class="model-item">
{% if model.admin_url %} {% if model.admin_url %}
<th scope="row"><a href="{{ model.admin_url }}">{{ model.name }}</a></th> <a href="{{ model.admin_url }}" class="model-link">
<i class="fas fa-table"></i> {{ model.name }}
</a>
{% else %} {% else %}
<th scope="row">{{ model.name }}</th> <span class="model-link disabled">{{ model.name }}</span>
{% endif %} {% endif %}
<div class="model-actions">
{% if model.add_url %} {% if model.add_url %}
<td><a href="{{ model.add_url }}" class="addlink">{% trans 'Add' %}</a></td> <a href="{{ model.add_url }}" class="add-link">
{% else %} <i class="fas fa-plus"></i>
<td>&nbsp;</td> </a>
{% endif %}
{% if model.admin_url %}
{% if model.view_only %}
<td><a href="{{ model.admin_url }}" class="viewlink">{% trans 'View' %}</a></td>
{% else %}
<td><a href="{{ model.admin_url }}" class="changelink">{% trans 'Change' %}</a></td>
{% endif %} {% endif %}
{% else %} {% if model.admin_url %}
<td>&nbsp;</td> <a href="{{ model.admin_url }}" class="change-link">
{% endif %} <i class="fas fa-list"></i>
</tr> </a>
{% endif %}
</div>
</div>
{% endfor %} {% endfor %}
</table> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
{% else %}
<p>{% trans "You don't have permission to view or edit anything." %}</p>
{% endif %}
</div>
{% endblock %} {% endblock %}
{% block sidebar %} {% block sidebar %}
{{ block.super }} <div id="content-related">
<div class="dashboard-widget"> <div class="module" id="recent-actions-module">
<div class="dashboard-widget-title">{% trans 'Member Management' %}</div> <h2>{% translate 'My Actions' %}</h2>
<div class="dashboard-widget-content"> <h3>{% translate 'My Recent Actions' %}</h3>
<ul> {% load log %}
<li><a href="{% url 'import_member' %}">Import Existing Members</a></li> {% get_admin_log 10 as admin_log for_user user %}
<li><a href="{% url 'admin:core_membership_changelist' %}">Manage Memberships</a></li> {% if not admin_log %}
<li><a href="{% url 'admin:core_user_changelist' %}">Manage Users</a></li> <p>{% translate 'None available' %}</p>
</ul> {% else %}
<ul class="actionlist">
{% for entry in admin_log %}
<li class="{% if entry.is_addition %}addlink{% endif %}{% if entry.is_change %}changelink{% endif %}{% if entry.is_deletion %}deletelink{% endif %}">
{% if entry.is_deletion or not entry.get_admin_url %}
{{ entry.object_repr }}
{% else %}
<a href="{{ entry.get_admin_url }}">{{ entry.object_repr }}</a>
{% endif %}
<br>
{% if entry.content_type %}
<span class="mini quiet">
{% filter capfirst %}
{{ entry.content_type.name }}
{% endfilter %}
</span>
{% else %}
<span class="mini quiet">{% translate 'Unknown content' %}</span>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div> </div>
</div> {% endblock %}
<div class="dashboard-widget">
<div class="dashboard-widget-title">{% trans 'Content Management' %}</div>
<div class="dashboard-widget-content">
<ul>
<li><a href="{% url 'admin:core_page_changelist' %}">Manage Pages</a></li>
<li><a href="{% url 'admin:core_announcement_changelist' %}">Manage Announcements</a></li>
<li><a href="{% url 'admin:member_area_content_changelist' %}">Manage Content</a></li>
</ul>
</div>
</div>
{% endblock %}