phase 1 partial
This commit is contained in:
1624
DJANGO-ADMIN-IMPROVEMENT-PLAN.md
Normal file
1624
DJANGO-ADMIN-IMPROVEMENT-PLAN.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -23,57 +23,42 @@ class Igny8AdminSite(admin.AdminSite):
|
||||
|
||||
def get_app_list(self, request):
|
||||
"""
|
||||
Customize the app list to organize models into proper groups
|
||||
Customize the app list to organize models into logical groups
|
||||
"""
|
||||
# Get the default app list
|
||||
app_dict = self._build_app_dict(request)
|
||||
|
||||
# Define our custom groups with their models (using object_name)
|
||||
# Organized by business function with emoji icons for visual recognition
|
||||
custom_groups = {
|
||||
'Billing & Tenancy': {
|
||||
'💰 Billing & Accounts': {
|
||||
'models': [
|
||||
('igny8_core_auth', 'Plan'),
|
||||
('igny8_core_auth', 'Account'),
|
||||
('igny8_core_auth', 'Subscription'),
|
||||
('billing', 'CreditTransaction'),
|
||||
('billing', 'CreditUsageLog'),
|
||||
('billing', 'Invoice'),
|
||||
('billing', 'Payment'),
|
||||
('billing', 'CreditTransaction'),
|
||||
('billing', 'CreditUsageLog'),
|
||||
('billing', 'CreditPackage'),
|
||||
('billing', 'PaymentMethodConfig'),
|
||||
('billing', 'AccountPaymentMethod'),
|
||||
('billing', 'CreditCostConfig'),
|
||||
],
|
||||
},
|
||||
'Sites & Users': {
|
||||
'👥 Sites & Users': {
|
||||
'models': [
|
||||
('igny8_core_auth', 'Site'),
|
||||
('igny8_core_auth', 'Sector'),
|
||||
('igny8_core_auth', 'User'),
|
||||
('igny8_core_auth', 'SiteUserAccess'),
|
||||
('igny8_core_auth', 'PasswordResetToken'),
|
||||
('igny8_core_auth', 'Sector'),
|
||||
],
|
||||
},
|
||||
'Global Reference Data': {
|
||||
'📚 Content Management': {
|
||||
'models': [
|
||||
('igny8_core_auth', 'Industry'),
|
||||
('igny8_core_auth', 'IndustrySector'),
|
||||
('igny8_core_auth', 'SeedKeyword'),
|
||||
('site_building', 'BusinessType'),
|
||||
('site_building', 'AudienceProfile'),
|
||||
('site_building', 'BrandPersonality'),
|
||||
('site_building', 'HeroImageryDirection'),
|
||||
],
|
||||
},
|
||||
'Planner': {
|
||||
'models': [
|
||||
('planner', 'Keywords'),
|
||||
('planner', 'Clusters'),
|
||||
('planner', 'ContentIdeas'),
|
||||
],
|
||||
},
|
||||
'Writer Module': {
|
||||
'models': [
|
||||
('writer', 'Tasks'),
|
||||
('writer', 'Content'),
|
||||
('writer', 'Tasks'),
|
||||
('writer', 'Images'),
|
||||
('writer', 'ContentTaxonomy'),
|
||||
('writer', 'ContentAttribute'),
|
||||
@@ -81,54 +66,53 @@ class Igny8AdminSite(admin.AdminSite):
|
||||
('writer', 'ContentClusterMap'),
|
||||
],
|
||||
},
|
||||
'Thinker Module': {
|
||||
'🎯 Planning & Strategy': {
|
||||
'models': [
|
||||
('system', 'AIPrompt'),
|
||||
('system', 'AuthorProfile'),
|
||||
('planner', 'Clusters'),
|
||||
('planner', 'Keywords'),
|
||||
('planner', 'ContentIdeas'),
|
||||
('system', 'Strategy'),
|
||||
('ai', 'AITaskLog'),
|
||||
],
|
||||
},
|
||||
'System Configuration': {
|
||||
'🔗 Integrations & Publishing': {
|
||||
'models': [
|
||||
('integration', 'SiteIntegration'),
|
||||
('integration', 'SyncEvent'),
|
||||
('publishing', 'PublishingRecord'),
|
||||
('publishing', 'DeploymentRecord'),
|
||||
],
|
||||
},
|
||||
'🤖 AI & Automation': {
|
||||
'models': [
|
||||
('ai', 'AITaskLog'),
|
||||
('system', 'AIPrompt'),
|
||||
('automation', 'AutomationConfig'),
|
||||
('automation', 'AutomationRun'),
|
||||
('optimization', 'OptimizationTask'),
|
||||
],
|
||||
},
|
||||
'🌍 Global Reference Data': {
|
||||
'models': [
|
||||
('igny8_core_auth', 'Industry'),
|
||||
('igny8_core_auth', 'IndustrySector'),
|
||||
('igny8_core_auth', 'SeedKeyword'),
|
||||
],
|
||||
},
|
||||
'⚙️ System Configuration': {
|
||||
'models': [
|
||||
('system', 'IntegrationSettings'),
|
||||
('system', 'SystemLog'),
|
||||
('system', 'SystemStatus'),
|
||||
('system', 'AuthorProfile'),
|
||||
('system', 'SystemSettings'),
|
||||
('system', 'AccountSettings'),
|
||||
('system', 'UserSettings'),
|
||||
('system', 'ModuleSettings'),
|
||||
('system', 'AISettings'),
|
||||
('system', 'ModuleEnableSettings'),
|
||||
# Automation config lives under the automation app - include here
|
||||
('automation', 'AutomationConfig'),
|
||||
('automation', 'AutomationRun'),
|
||||
('system', 'SystemLog'),
|
||||
('system', 'SystemStatus'),
|
||||
],
|
||||
},
|
||||
'Payments': {
|
||||
'models': [
|
||||
('billing', 'PaymentMethodConfig'),
|
||||
('billing', 'AccountPaymentMethod'),
|
||||
],
|
||||
},
|
||||
'Integrations & Sync': {
|
||||
'models': [
|
||||
('integration', 'SiteIntegration'),
|
||||
('integration', 'SyncEvent'),
|
||||
],
|
||||
},
|
||||
'Publishing': {
|
||||
'models': [
|
||||
('publishing', 'PublishingRecord'),
|
||||
('publishing', 'DeploymentRecord'),
|
||||
],
|
||||
},
|
||||
'Optimization': {
|
||||
'models': [
|
||||
('optimization', 'OptimizationTask'),
|
||||
],
|
||||
},
|
||||
'Django Internals': {
|
||||
'🔧 Django System': {
|
||||
'models': [
|
||||
('admin', 'LogEntry'),
|
||||
('auth', 'Group'),
|
||||
@@ -159,7 +143,7 @@ class Igny8AdminSite(admin.AdminSite):
|
||||
if group_models:
|
||||
app_list.append({
|
||||
'name': group_name,
|
||||
'app_label': group_name.lower().replace(' ', '_').replace('&', ''),
|
||||
'app_label': group_name.lower().replace(' ', '_').replace('&', '').replace('emoji', ''),
|
||||
'app_url': None,
|
||||
'has_module_perms': True,
|
||||
'models': group_models,
|
||||
@@ -167,18 +151,15 @@ class Igny8AdminSite(admin.AdminSite):
|
||||
|
||||
# Sort the app list by our custom order
|
||||
order = [
|
||||
'Billing & Tenancy',
|
||||
'Sites & Users',
|
||||
'Global Reference Data',
|
||||
'Planner',
|
||||
'Writer Module',
|
||||
'Thinker Module',
|
||||
'System Configuration',
|
||||
'Payments',
|
||||
'Integrations & Sync',
|
||||
'Publishing',
|
||||
'Optimization',
|
||||
'Django Internals',
|
||||
'💰 Billing & Accounts',
|
||||
'👥 Sites & Users',
|
||||
'📚 Content Management',
|
||||
'🎯 Planning & Strategy',
|
||||
'🔗 Integrations & Publishing',
|
||||
'🤖 AI & Automation',
|
||||
'🌍 Global Reference Data',
|
||||
'⚙️ System Configuration',
|
||||
'🔧 Django System',
|
||||
]
|
||||
|
||||
app_list.sort(key=lambda x: order.index(x['name']) if x['name'] in order else 999)
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
"""
|
||||
Billing Business Logic Admin
|
||||
|
||||
NOTE: Most billing models are registered in modules/billing/admin.py
|
||||
with full workflow functionality. This file contains legacy/minimal registrations.
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
@@ -14,133 +17,36 @@ from .models import (
|
||||
)
|
||||
|
||||
|
||||
@admin.register(CreditCostConfig)
|
||||
class CreditCostConfigAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'operation_type',
|
||||
'display_name',
|
||||
'credits_cost_display',
|
||||
'unit',
|
||||
'is_active',
|
||||
'cost_change_indicator',
|
||||
'updated_at',
|
||||
'updated_by'
|
||||
]
|
||||
|
||||
list_filter = ['is_active', 'unit', 'updated_at']
|
||||
search_fields = ['operation_type', 'display_name', 'description']
|
||||
|
||||
fieldsets = (
|
||||
('Operation', {
|
||||
'fields': ('operation_type', 'display_name', 'description')
|
||||
}),
|
||||
('Cost Configuration', {
|
||||
'fields': ('credits_cost', 'unit', 'is_active')
|
||||
}),
|
||||
('Audit Trail', {
|
||||
'fields': ('previous_cost', 'updated_by', 'created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
readonly_fields = ['created_at', 'updated_at', 'previous_cost']
|
||||
|
||||
def credits_cost_display(self, obj):
|
||||
"""Show cost with color coding"""
|
||||
if obj.credits_cost >= 20:
|
||||
color = 'red'
|
||||
elif obj.credits_cost >= 10:
|
||||
color = 'orange'
|
||||
else:
|
||||
color = 'green'
|
||||
return format_html(
|
||||
'<span style="color: {}; font-weight: bold;">{} credits</span>',
|
||||
color,
|
||||
obj.credits_cost
|
||||
)
|
||||
credits_cost_display.short_description = 'Cost'
|
||||
|
||||
def cost_change_indicator(self, obj):
|
||||
"""Show if cost changed recently"""
|
||||
if obj.previous_cost is not None:
|
||||
if obj.credits_cost > obj.previous_cost:
|
||||
icon = '📈' # Increased
|
||||
color = 'red'
|
||||
elif obj.credits_cost < obj.previous_cost:
|
||||
icon = '📉' # Decreased
|
||||
color = 'green'
|
||||
else:
|
||||
icon = '➡️' # Same
|
||||
color = 'gray'
|
||||
|
||||
return format_html(
|
||||
'{} <span style="color: {};">({} → {})</span>',
|
||||
icon,
|
||||
color,
|
||||
obj.previous_cost,
|
||||
obj.credits_cost
|
||||
)
|
||||
return '—'
|
||||
cost_change_indicator.short_description = 'Recent Change'
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
"""Track who made the change"""
|
||||
obj.updated_by = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
# CreditCostConfig - DUPLICATE - Registered in modules/billing/admin.py with better features
|
||||
# Commenting out to avoid conflicts
|
||||
# @admin.register(CreditCostConfig)
|
||||
# class CreditCostConfigAdmin(admin.ModelAdmin):
|
||||
# ...existing implementation...
|
||||
|
||||
|
||||
@admin.register(Invoice)
|
||||
class InvoiceAdmin(AccountAdminMixin, admin.ModelAdmin):
|
||||
list_display = [
|
||||
'invoice_number',
|
||||
'account',
|
||||
'status',
|
||||
'total',
|
||||
'currency',
|
||||
'invoice_date',
|
||||
'due_date',
|
||||
'subscription',
|
||||
]
|
||||
list_filter = ['status', 'currency', 'invoice_date', 'account']
|
||||
search_fields = ['invoice_number', 'account__name', 'subscription__id']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
# Invoice - DUPLICATE - Registered in modules/billing/admin.py
|
||||
# Commenting out to avoid conflicts
|
||||
# @admin.register(Invoice)
|
||||
# class InvoiceAdmin(AccountAdminMixin, admin.ModelAdmin):
|
||||
# ...existing implementation...
|
||||
|
||||
|
||||
@admin.register(Payment)
|
||||
class PaymentAdmin(AccountAdminMixin, admin.ModelAdmin):
|
||||
\"\"\"
|
||||
Payment admin - DO NOT USE.
|
||||
Use the Payment admin in modules/billing/admin.py which has approval workflow actions.
|
||||
This is kept for backward compatibility only.
|
||||
\"\"\"
|
||||
list_display = [
|
||||
'id',
|
||||
'invoice',
|
||||
'account',
|
||||
'payment_method',
|
||||
'status',
|
||||
'amount',
|
||||
'currency',
|
||||
'processed_at',
|
||||
]
|
||||
list_filter = ['status', 'payment_method', 'currency', 'created_at']
|
||||
search_fields = ['invoice__invoice_number', 'account__name', 'stripe_payment_intent_id', 'paypal_order_id']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
|
||||
def has_add_permission(self, request):\n return False # Prevent creating payments here
|
||||
\n def has_delete_permission(self, request, obj=None):\n return False # Prevent deleting payments here
|
||||
# Payment - DUPLICATE - Registered in modules/billing/admin.py with full approval workflow
|
||||
# Commenting out to avoid conflicts
|
||||
# @admin.register(Payment)
|
||||
# class PaymentAdmin(AccountAdminMixin, admin.ModelAdmin):
|
||||
# ...existing implementation...
|
||||
|
||||
|
||||
@admin.register(CreditPackage)
|
||||
class CreditPackageAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'credits', 'price', 'discount_percentage', 'is_active', 'is_featured', 'sort_order']
|
||||
list_filter = ['is_active', 'is_featured']
|
||||
search_fields = ['name', 'slug']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
# CreditPackage - DUPLICATE - Registered in modules/billing/admin.py
|
||||
# Commenting out to avoid conflicts
|
||||
# @admin.register(CreditPackage)
|
||||
# class CreditPackageAdmin(admin.ModelAdmin):
|
||||
# ...existing implementation...
|
||||
|
||||
|
||||
# PaymentMethodConfig admin is in modules/billing/admin.py - do not duplicate
|
||||
# @admin.register(PaymentMethodConfig)
|
||||
# PaymentMethodConfig and AccountPaymentMethod are kept here as they're not duplicated
|
||||
# or have minimal implementations that don't conflict
|
||||
|
||||
@admin.register(AccountPaymentMethod)
|
||||
class AccountPaymentMethodAdmin(admin.ModelAdmin):
|
||||
|
||||
290
backend/igny8_core/static/admin/css/igny8_admin.css
Normal file
290
backend/igny8_core/static/admin/css/igny8_admin.css
Normal file
@@ -0,0 +1,290 @@
|
||||
/* IGNY8 Custom Admin Styles */
|
||||
|
||||
/* Status badges */
|
||||
.status-active {
|
||||
color: #28a745 !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
color: #dc3545 !important;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
color: #ffc107 !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-succeeded, .status-completed {
|
||||
color: #28a745 !important;
|
||||
}
|
||||
|
||||
.status-failed, .status-error {
|
||||
color: #dc3545 !important;
|
||||
}
|
||||
|
||||
/* Credit indicators */
|
||||
.credits-low {
|
||||
color: #dc3545 !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.credits-medium {
|
||||
color: #ffc107 !important;
|
||||
}
|
||||
|
||||
.credits-high {
|
||||
color: #28a745 !important;
|
||||
}
|
||||
|
||||
/* Quick action buttons */
|
||||
.admin-action-button {
|
||||
padding: 5px 15px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
margin: 2px;
|
||||
background-color: #417690;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.admin-action-button:hover {
|
||||
background-color: #305d75;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* List view enhancements */
|
||||
#content-main table tr:hover {
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
/* Improve sidebar menu appearance */
|
||||
#content-related h3 {
|
||||
background: #417690;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
/* Better form field spacing */
|
||||
.form-row {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* Highlight required fields */
|
||||
.required label:after {
|
||||
content: " *";
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
/* Success messages */
|
||||
.success, .messagelist .success {
|
||||
background-color: #d4edda !important;
|
||||
border-color: #c3e6cb !important;
|
||||
color: #155724 !important;
|
||||
}
|
||||
|
||||
/* Warning messages */
|
||||
.warning, .messagelist .warning {
|
||||
background-color: #fff3cd !important;
|
||||
border-color: #ffeaa7 !important;
|
||||
color: #856404 !important;
|
||||
}
|
||||
|
||||
/* Error messages */
|
||||
.error, .messagelist .error {
|
||||
background-color: #f8d7da !important;
|
||||
border-color: #f5c6cb !important;
|
||||
color: #721c24 !important;
|
||||
}
|
||||
|
||||
/* Improve table readability */
|
||||
#result_list tbody tr:nth-child(odd) {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
#result_list tbody tr:nth-child(even) {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* Better button styling */
|
||||
.button, input[type=submit], input[type=button], .submit-row input {
|
||||
background: #417690 !important;
|
||||
color: white !important;
|
||||
border: none !important;
|
||||
padding: 10px 15px !important;
|
||||
border-radius: 4px !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
.button:hover, input[type=submit]:hover, input[type=button]:hover {
|
||||
background: #305d75 !important;
|
||||
}
|
||||
|
||||
/* Delete button styling */
|
||||
.deletelink, .deletelink-box a {
|
||||
background: #dc3545 !important;
|
||||
}
|
||||
|
||||
.deletelink:hover, .deletelink-box a:hover {
|
||||
background: #c82333 !important;
|
||||
}
|
||||
|
||||
/* Improve filter sidebar */
|
||||
#changelist-filter h2 {
|
||||
background: #417690;
|
||||
color: white;
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#changelist-filter h3 {
|
||||
font-weight: bold;
|
||||
margin-top: 15px;
|
||||
padding-bottom: 5px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
/* Better pagination */
|
||||
.paginator {
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.paginator a {
|
||||
padding: 5px 10px;
|
||||
margin: 0 2px;
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.paginator a:hover {
|
||||
background: #417690;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Responsive improvements */
|
||||
@media (max-width: 768px) {
|
||||
#content-main {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.module table {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Admin header improvements */
|
||||
#header {
|
||||
background: #417690;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#header a:link, #header a:visited {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#branding h1 {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Fieldset legend styling */
|
||||
fieldset.module h2 {
|
||||
background: #417690;
|
||||
color: white;
|
||||
padding: 8px 10px;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
/* Inline forms */
|
||||
.inline-group {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.inline-group .tabular {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* Help text styling */
|
||||
.help {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* Dashboard widget styling */
|
||||
.dashboard-card {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 10px 0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.dashboard-card h2 {
|
||||
margin-top: 0;
|
||||
border-bottom: 2px solid #417690;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: inline-block;
|
||||
margin: 10px 20px 10px 0;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #417690;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* Alert styling */
|
||||
.alert {
|
||||
padding: 15px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background-color: #f8d7da;
|
||||
border-left-color: #dc3545;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background-color: #fff3cd;
|
||||
border-left-color: #ffc107;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background-color: #d1ecf1;
|
||||
border-left-color: #17a2b8;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: #d4edda;
|
||||
border-left-color: #28a745;
|
||||
color: #155724;
|
||||
}
|
||||
19
backend/igny8_core/templates/admin/base_site.html
Normal file
19
backend/igny8_core/templates/admin/base_site.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{% extends "admin/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ title }} | IGNY8 Admin{% endblock %}
|
||||
|
||||
{% block branding %}
|
||||
<h1 id="site-name">
|
||||
<a href="{% url 'admin:index' %}">
|
||||
🚀 IGNY8 Administration
|
||||
</a>
|
||||
</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block extrastyle %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" href="{% static 'admin/css/igny8_admin.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block nav-global %}{% endblock %}
|
||||
Reference in New Issue
Block a user