django-adminflow 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- adminflow/__init__.py +1 -0
- adminflow/apps.py +22 -0
- adminflow/context_processors.py +72 -0
- adminflow/forms.py +89 -0
- adminflow/import_export.py +224 -0
- adminflow/static/adminflow/img/logo.png +0 -0
- adminflow/templates/admin/actions.html +79 -0
- adminflow/templates/admin/base.html +1263 -0
- adminflow/templates/admin/base_site.html +5 -0
- adminflow/templates/admin/change_form.html +315 -0
- adminflow/templates/admin/change_list.html +216 -0
- adminflow/templates/admin/change_list_results.html +211 -0
- adminflow/templates/admin/delete_confirmation.html +55 -0
- adminflow/templates/admin/delete_selected_confirmation.html +118 -0
- adminflow/templates/admin/edit_inline/stacked.html +85 -0
- adminflow/templates/admin/edit_inline/tabular.html +81 -0
- adminflow/templates/admin/filter.html +17 -0
- adminflow/templates/admin/import_export/change_list_export_item.html +8 -0
- adminflow/templates/admin/import_export/change_list_import_item.html +8 -0
- adminflow/templates/admin/import_export/export.html +199 -0
- adminflow/templates/admin/includes/fieldset.html +73 -0
- adminflow/templates/admin/includes/toast.html +102 -0
- adminflow/templates/admin/index.html +227 -0
- adminflow/templates/admin/logged_out.html +38 -0
- adminflow/templates/admin/login.html +144 -0
- adminflow/templates/admin/object_history.html +60 -0
- adminflow/templates/admin/pagination.html +30 -0
- adminflow/templates/admin/search_form.html +28 -0
- adminflow/templates/admin/submit_line.html +48 -0
- adminflow/templates/admin/verify_2fa.html +280 -0
- adminflow/templates/otp/email/token.html +64 -0
- adminflow/templates/otp/email/token.txt +11 -0
- adminflow/templates/registration/logged_out.html +38 -0
- adminflow/templates/registration/password_change_form.html +272 -0
- adminflow/templates/simple_history/object_history.html +186 -0
- adminflow/templates/simple_history/object_history_form.html +103 -0
- adminflow/templates/simple_history/submit_line.html +17 -0
- adminflow/templatetags/__init__.py +1 -0
- adminflow/templatetags/__pycache__/__init__.cpython-314.pyc +0 -0
- adminflow/templatetags/__pycache__/adminflow_tags.cpython-314.pyc +0 -0
- adminflow/templatetags/adminflow_tags.py +233 -0
- adminflow/user_admin.py +662 -0
- adminflow/views.py +207 -0
- django_adminflow-1.0.0.dist-info/METADATA +398 -0
- django_adminflow-1.0.0.dist-info/RECORD +47 -0
- django_adminflow-1.0.0.dist-info/WHEEL +5 -0
- django_adminflow-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
{% extends "admin/change_form.html" %}
|
|
2
|
+
{% load i18n admin_urls %}
|
|
3
|
+
{% load url from simple_history_compat %}
|
|
4
|
+
|
|
5
|
+
{% block breadcrumbs %}
|
|
6
|
+
<div class="flex items-center gap-1.5 text-xs text-on-surface-variant font-medium px-6 py-3">
|
|
7
|
+
<a href="{% url "admin:index" %}" class="hover:text-primary transition-colors">{% trans "Home" %}</a>
|
|
8
|
+
<span class="material-symbols-outlined text-[14px]">chevron_right</span>
|
|
9
|
+
<a href="{% url "admin:app_list" app_label %}" class="hover:text-primary transition-colors">{{app_label|capfirst|escape}}</a>
|
|
10
|
+
<span class="material-symbols-outlined text-[14px]">chevron_right</span>
|
|
11
|
+
<a href="{{changelist_url}}" class="hover:text-primary transition-colors">{{opts.verbose_name_plural|capfirst}}</a>
|
|
12
|
+
<span class="material-symbols-outlined text-[14px]">chevron_right</span>
|
|
13
|
+
<a href="{{change_url}}" class="hover:text-primary transition-colors">{{original|truncatewords:"18"}}</a>
|
|
14
|
+
<span class="material-symbols-outlined text-[14px]">chevron_right</span>
|
|
15
|
+
<a href="../" class="hover:text-primary transition-colors">{% trans "History" %}</a>
|
|
16
|
+
<span class="material-symbols-outlined text-[14px]">chevron_right</span>
|
|
17
|
+
<span class="text-on-surface font-bold">
|
|
18
|
+
{% if revert_disabled %}{% blocktrans with original_opts.verbose_name as verbose_name %}View {{verbose_name}}{% endblocktrans %}{% else %}{% blocktrans with original_opts.verbose_name as verbose_name %}Revert {{verbose_name}}{% endblocktrans %}{% endif %}
|
|
19
|
+
</span>
|
|
20
|
+
</div>
|
|
21
|
+
{% endblock %}
|
|
22
|
+
|
|
23
|
+
{% block submit_buttons_top %}
|
|
24
|
+
{% include "simple_history/submit_line.html" %}
|
|
25
|
+
{% endblock %}
|
|
26
|
+
|
|
27
|
+
{% block submit_buttons_bottom %}
|
|
28
|
+
{% include "simple_history/submit_line.html" %}
|
|
29
|
+
{% endblock %}
|
|
30
|
+
|
|
31
|
+
{% block object-tools-items %}
|
|
32
|
+
<li>
|
|
33
|
+
<a href="{% add_preserved_filters history_url %}" class="historylink">{% translate "History" %}</a>
|
|
34
|
+
</li>
|
|
35
|
+
{% if has_absolute_url %}<li><a href="{{ absolute_url }}" class="viewsitelink">{% translate "View on site" %}</a></li>{% endif %}
|
|
36
|
+
{% endblock %}
|
|
37
|
+
|
|
38
|
+
{% block form_top %}
|
|
39
|
+
<div class="flex items-start gap-3 p-4 rounded-xl border mb-4" style="background:color-mix(in srgb, var(--info) 5%, transparent);border-color:color-mix(in srgb, var(--info) 20%, transparent);">
|
|
40
|
+
<span class="material-symbols-outlined text-xl shrink-0" style="color:var(--info);">info</span>
|
|
41
|
+
<div class="text-xs">
|
|
42
|
+
<p class="font-bold text-on-surface mb-0.5">{% trans "Read-Only Historical View" %}</p>
|
|
43
|
+
<p class="text-on-surface-variant">
|
|
44
|
+
{% if not revert_disabled %}{% blocktrans %}This is a snapshot of the object at this point in time. Press <strong>'Revert'</strong> to restore this version, or <strong>'Close'</strong> to go back.{% endblocktrans %}{% else %}{% trans "This is a read-only view of the object at this point in time." %}{% endif %}
|
|
45
|
+
</p>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
{% endblock %}
|
|
49
|
+
|
|
50
|
+
{% block admin_change_form_document_ready %}
|
|
51
|
+
{{ block.super }}
|
|
52
|
+
<script>
|
|
53
|
+
document.addEventListener("DOMContentLoaded", function() {
|
|
54
|
+
// Make all form fields read-only/disabled on history detail view
|
|
55
|
+
const form = document.getElementById('{{ opts.model_name }}_form') || document.querySelector('form');
|
|
56
|
+
if (!form) return;
|
|
57
|
+
|
|
58
|
+
// Disable all input, select, textarea elements
|
|
59
|
+
form.querySelectorAll('input, select, textarea').forEach(el => {
|
|
60
|
+
// Skip the revert submit button and hidden CSRF/form fields
|
|
61
|
+
if (el.type === 'submit' || el.type === 'hidden') return;
|
|
62
|
+
el.disabled = true;
|
|
63
|
+
el.style.opacity = '0.7';
|
|
64
|
+
el.style.cursor = 'not-allowed';
|
|
65
|
+
el.style.pointerEvents = 'none';
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Hide file input "Choose File" buttons and "Remove" buttons
|
|
69
|
+
form.querySelectorAll('input[type="file"]').forEach(el => {
|
|
70
|
+
const wrapper = el.closest('div');
|
|
71
|
+
if (wrapper) wrapper.style.display = 'none';
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Hide the clear/remove buttons from file preview cards
|
|
75
|
+
form.querySelectorAll('.adminflow-clear-btn').forEach(btn => {
|
|
76
|
+
btn.style.display = 'none';
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Add a read-only visual badge to each fieldset
|
|
80
|
+
form.querySelectorAll('fieldset').forEach(fieldset => {
|
|
81
|
+
const legend = fieldset.querySelector('legend, h2, .fieldset-header');
|
|
82
|
+
if (legend) {
|
|
83
|
+
const badge = document.createElement('span');
|
|
84
|
+
badge.className = 'ml-2 inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-[9px] font-bold uppercase';
|
|
85
|
+
badge.style.cssText = 'color:var(--info);background:color-mix(in srgb, var(--info) 10%, transparent);vertical-align:middle;';
|
|
86
|
+
badge.innerHTML = '<span class="material-symbols-outlined text-[11px]">lock</span> Read-only';
|
|
87
|
+
legend.appendChild(badge);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Hide tabs if any (no editing needed)
|
|
92
|
+
// Disable inline add buttons
|
|
93
|
+
form.querySelectorAll('.add-row, .inline-group .add-row a').forEach(el => {
|
|
94
|
+
el.style.display = 'none';
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Disable delete checkboxes on inlines
|
|
98
|
+
form.querySelectorAll('.inline-deletelink, .delete input').forEach(el => {
|
|
99
|
+
el.style.display = 'none';
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
</script>
|
|
103
|
+
{% endblock %}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{% load i18n %}
|
|
2
|
+
<div class="flex flex-wrap items-center gap-2 p-4 border border-outline-variant bg-surface-low/50 rounded-xl mt-6">
|
|
3
|
+
{% if not revert_disabled %}
|
|
4
|
+
<button type="submit" name="_save" class="flex items-center gap-1.5 px-6 py-2 bg-primary text-white rounded-lg text-xs font-bold hover:bg-primary/95 transition-all shadow-md active:scale-[0.98]">
|
|
5
|
+
<span class="material-symbols-outlined text-sm">settings_backup_restore</span>
|
|
6
|
+
{% trans 'Revert' %}
|
|
7
|
+
</button>
|
|
8
|
+
{% endif %}
|
|
9
|
+
{% if change_history %}
|
|
10
|
+
<button type="submit" name="_change_history" class="px-4 py-2 bg-surface-lowest border border-outline-variant text-on-surface-variant hover:bg-surface-low rounded-lg text-xs font-bold transition-all shadow-sm">
|
|
11
|
+
{% trans 'Change History' %}
|
|
12
|
+
</button>
|
|
13
|
+
{% endif %}
|
|
14
|
+
<a href="{{ history_url }}" class="px-4 py-2 bg-surface-lowest border border-outline-variant text-on-surface-variant hover:bg-surface-low rounded-lg text-xs font-bold transition-all shadow-sm closelink">
|
|
15
|
+
{% trans 'Close' %}
|
|
16
|
+
</a>
|
|
17
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Template tags package
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
from django import template
|
|
2
|
+
from django.contrib import admin
|
|
3
|
+
|
|
4
|
+
register = template.Library()
|
|
5
|
+
|
|
6
|
+
@register.filter(name='list_item')
|
|
7
|
+
def list_item(lst, index):
|
|
8
|
+
"""Return the item at the given index from a list."""
|
|
9
|
+
try:
|
|
10
|
+
return lst[int(index)]
|
|
11
|
+
except (IndexError, TypeError, ValueError):
|
|
12
|
+
return ''
|
|
13
|
+
|
|
14
|
+
@register.simple_tag(takes_context=True)
|
|
15
|
+
def get_admin_app_list(context):
|
|
16
|
+
request = context.get('request')
|
|
17
|
+
if not request:
|
|
18
|
+
return []
|
|
19
|
+
try:
|
|
20
|
+
# Use default admin site
|
|
21
|
+
return admin.site.get_app_list(request)
|
|
22
|
+
except Exception:
|
|
23
|
+
return []
|
|
24
|
+
|
|
25
|
+
@register.filter(name='zip_results')
|
|
26
|
+
def zip_results(results, cl):
|
|
27
|
+
try:
|
|
28
|
+
return zip(results, cl.result_list)
|
|
29
|
+
except Exception:
|
|
30
|
+
return zip(results, [])
|
|
31
|
+
|
|
32
|
+
@register.filter(name='add_class')
|
|
33
|
+
def add_class(value, arg):
|
|
34
|
+
try:
|
|
35
|
+
widget = value.field.widget
|
|
36
|
+
if hasattr(widget, 'widgets'):
|
|
37
|
+
for subwidget in widget.widgets:
|
|
38
|
+
css_classes = subwidget.attrs.get('class', '')
|
|
39
|
+
if css_classes:
|
|
40
|
+
subwidget.attrs['class'] = f"{css_classes} {arg}"
|
|
41
|
+
else:
|
|
42
|
+
subwidget.attrs['class'] = arg
|
|
43
|
+
return value.as_widget()
|
|
44
|
+
else:
|
|
45
|
+
css_classes = widget.attrs.get('class', '')
|
|
46
|
+
if css_classes:
|
|
47
|
+
css_classes = f"{css_classes} {arg}"
|
|
48
|
+
else:
|
|
49
|
+
css_classes = arg
|
|
50
|
+
return value.as_widget(attrs={'class': css_classes})
|
|
51
|
+
except Exception:
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
@register.filter(name='is_checkbox')
|
|
55
|
+
def is_checkbox(field):
|
|
56
|
+
try:
|
|
57
|
+
return field.field.widget.__class__.__name__ in ['CheckboxInput', 'CheckboxSelectMultiple']
|
|
58
|
+
except Exception:
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
@register.filter(name='is_select')
|
|
62
|
+
def is_select(field):
|
|
63
|
+
try:
|
|
64
|
+
return field.field.widget.__class__.__name__ in ['Select', 'SelectMultiple', 'AutocompleteSelect', 'AutocompleteSelectMultiple', 'Select2']
|
|
65
|
+
except Exception:
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
@register.filter(name='is_textarea')
|
|
69
|
+
def is_textarea(field):
|
|
70
|
+
try:
|
|
71
|
+
return field.field.widget.__class__.__name__ in ['Textarea']
|
|
72
|
+
except Exception:
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
@register.filter(name='is_file')
|
|
76
|
+
def is_file(field):
|
|
77
|
+
try:
|
|
78
|
+
return field.field.widget.__class__.__name__ in ['ClearableFileInput', 'FileInput']
|
|
79
|
+
except Exception:
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
@register.filter(name='adminflow_icon')
|
|
83
|
+
def adminflow_icon(value):
|
|
84
|
+
val_clean = str(value).lower().replace(' ', '').replace('_', '')
|
|
85
|
+
|
|
86
|
+
# 1. Check if value is a model dict or Model class with ModelAdmin attribute
|
|
87
|
+
if isinstance(value, dict):
|
|
88
|
+
model_cls = value.get('model')
|
|
89
|
+
model_name = value.get('object_name', str(value))
|
|
90
|
+
if model_cls and model_cls in admin.site._registry:
|
|
91
|
+
model_admin = admin.site._registry[model_cls]
|
|
92
|
+
icon = getattr(model_admin, 'icon', None) or getattr(model_admin, 'admin_icon', None)
|
|
93
|
+
if icon:
|
|
94
|
+
return icon
|
|
95
|
+
val_clean = str(model_name).lower().replace(' ', '').replace('_', '')
|
|
96
|
+
elif hasattr(value, '_meta'):
|
|
97
|
+
model_cls = value if isinstance(value, type) else value.__class__
|
|
98
|
+
if model_cls in admin.site._registry:
|
|
99
|
+
model_admin = admin.site._registry[model_cls]
|
|
100
|
+
icon = getattr(model_admin, 'icon', None) or getattr(model_admin, 'admin_icon', None)
|
|
101
|
+
if icon:
|
|
102
|
+
return icon
|
|
103
|
+
val_clean = str(model_cls._meta.object_name).lower().replace(' ', '').replace('_', '')
|
|
104
|
+
|
|
105
|
+
# 2. Check custom user model icons from settings.py
|
|
106
|
+
from django.conf import settings
|
|
107
|
+
custom_icons = getattr(settings, 'ADMINFLOW_MODEL_ICONS', {})
|
|
108
|
+
if custom_icons:
|
|
109
|
+
for k, icon in custom_icons.items():
|
|
110
|
+
if str(k).lower().replace(' ', '').replace('_', '') == val_clean:
|
|
111
|
+
return icon
|
|
112
|
+
|
|
113
|
+
# 3. Predefined smart model icon mapping
|
|
114
|
+
icons = {
|
|
115
|
+
'auth': 'security',
|
|
116
|
+
'user': 'person',
|
|
117
|
+
'users': 'group',
|
|
118
|
+
'group': 'badge',
|
|
119
|
+
'groups': 'folder_shared',
|
|
120
|
+
'permission': 'verified_user',
|
|
121
|
+
'permissions': 'verified_user',
|
|
122
|
+
'userinformation': 'badge',
|
|
123
|
+
'userinformations': 'badge',
|
|
124
|
+
'site': 'public',
|
|
125
|
+
'sites': 'language',
|
|
126
|
+
'logentry': 'history',
|
|
127
|
+
'logentries': 'history',
|
|
128
|
+
'contenttype': 'layers',
|
|
129
|
+
'contenttypes': 'category',
|
|
130
|
+
'session': 'key',
|
|
131
|
+
'sessions': 'lock',
|
|
132
|
+
'customer': 'contact_mail',
|
|
133
|
+
'customers': 'contact_mail',
|
|
134
|
+
'order': 'shopping_cart',
|
|
135
|
+
'orders': 'shopping_cart',
|
|
136
|
+
'product': 'inventory_2',
|
|
137
|
+
'products': 'inventory_2',
|
|
138
|
+
'item': 'widgets',
|
|
139
|
+
'items': 'widgets',
|
|
140
|
+
'category': 'category',
|
|
141
|
+
'categories': 'category',
|
|
142
|
+
'tag': 'label',
|
|
143
|
+
'tags': 'label',
|
|
144
|
+
'setting': 'settings',
|
|
145
|
+
'settings': 'settings',
|
|
146
|
+
'notification': 'notifications',
|
|
147
|
+
'notifications': 'notifications',
|
|
148
|
+
'profile': 'account_circle',
|
|
149
|
+
'profiles': 'account_circle',
|
|
150
|
+
'file': 'description',
|
|
151
|
+
'files': 'folder',
|
|
152
|
+
'image': 'image',
|
|
153
|
+
'images': 'photo_library',
|
|
154
|
+
'crm': 'hub',
|
|
155
|
+
}
|
|
156
|
+
return icons.get(val_clean, 'grid_view')
|
|
157
|
+
|
|
158
|
+
@register.simple_tag
|
|
159
|
+
def get_user_stats():
|
|
160
|
+
from django.contrib.auth import get_user_model
|
|
161
|
+
User = get_user_model()
|
|
162
|
+
try:
|
|
163
|
+
total = User.objects.count()
|
|
164
|
+
active = User.objects.filter(is_active=True).count()
|
|
165
|
+
inactive = User.objects.filter(is_active=False).count()
|
|
166
|
+
staff = User.objects.filter(is_staff=True).count()
|
|
167
|
+
superuser = User.objects.filter(is_superuser=True).count()
|
|
168
|
+
return {
|
|
169
|
+
'total': total,
|
|
170
|
+
'active': active,
|
|
171
|
+
'inactive': inactive,
|
|
172
|
+
'staff': staff,
|
|
173
|
+
'superuser': superuser,
|
|
174
|
+
}
|
|
175
|
+
except Exception:
|
|
176
|
+
return {
|
|
177
|
+
'total': 0,
|
|
178
|
+
'active': 0,
|
|
179
|
+
'inactive': 0,
|
|
180
|
+
'staff': 0,
|
|
181
|
+
'superuser': 0,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
@register.simple_tag
|
|
185
|
+
def get_system_stats():
|
|
186
|
+
import django
|
|
187
|
+
import sys
|
|
188
|
+
from django.conf import settings
|
|
189
|
+
from django.contrib import admin
|
|
190
|
+
|
|
191
|
+
db_engine = "Unknown"
|
|
192
|
+
db_name = "Unknown"
|
|
193
|
+
try:
|
|
194
|
+
db_config = settings.DATABASES['default']
|
|
195
|
+
engine_path = db_config.get('ENGINE', '')
|
|
196
|
+
db_engine = engine_path.split('.')[-1].replace('_', ' ').title()
|
|
197
|
+
if 'sqlite' in engine_path:
|
|
198
|
+
db_engine = 'SQLite'
|
|
199
|
+
elif 'postgresql' in engine_path:
|
|
200
|
+
db_engine = 'PostgreSQL'
|
|
201
|
+
elif 'mysql' in engine_path:
|
|
202
|
+
db_engine = 'MySQL'
|
|
203
|
+
elif 'oracle' in engine_path:
|
|
204
|
+
db_engine = 'Oracle'
|
|
205
|
+
db_name = db_config.get('NAME', '')
|
|
206
|
+
import os
|
|
207
|
+
db_name = os.path.basename(db_name) if db_name else 'default'
|
|
208
|
+
except Exception:
|
|
209
|
+
pass
|
|
210
|
+
|
|
211
|
+
app_count = 0
|
|
212
|
+
model_count = 0
|
|
213
|
+
model_names = ''
|
|
214
|
+
try:
|
|
215
|
+
model_count = len(admin.site._registry)
|
|
216
|
+
distinct_apps = set(m._meta.app_label for m in admin.site._registry.keys())
|
|
217
|
+
app_count = len(distinct_apps)
|
|
218
|
+
model_names = '\n'.join(f'• {name}' for name in sorted(m._meta.verbose_name.title() for m in admin.site._registry.keys()))
|
|
219
|
+
except Exception:
|
|
220
|
+
pass
|
|
221
|
+
|
|
222
|
+
django_ver = django.get_version()
|
|
223
|
+
python_ver = f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
'db_engine': db_engine,
|
|
227
|
+
'db_name': db_name,
|
|
228
|
+
'model_count': model_count,
|
|
229
|
+
'app_count': app_count,
|
|
230
|
+
'model_names': model_names,
|
|
231
|
+
'django_version': django_ver,
|
|
232
|
+
'python_version': python_ver,
|
|
233
|
+
}
|