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.
Files changed (47) hide show
  1. adminflow/__init__.py +1 -0
  2. adminflow/apps.py +22 -0
  3. adminflow/context_processors.py +72 -0
  4. adminflow/forms.py +89 -0
  5. adminflow/import_export.py +224 -0
  6. adminflow/static/adminflow/img/logo.png +0 -0
  7. adminflow/templates/admin/actions.html +79 -0
  8. adminflow/templates/admin/base.html +1263 -0
  9. adminflow/templates/admin/base_site.html +5 -0
  10. adminflow/templates/admin/change_form.html +315 -0
  11. adminflow/templates/admin/change_list.html +216 -0
  12. adminflow/templates/admin/change_list_results.html +211 -0
  13. adminflow/templates/admin/delete_confirmation.html +55 -0
  14. adminflow/templates/admin/delete_selected_confirmation.html +118 -0
  15. adminflow/templates/admin/edit_inline/stacked.html +85 -0
  16. adminflow/templates/admin/edit_inline/tabular.html +81 -0
  17. adminflow/templates/admin/filter.html +17 -0
  18. adminflow/templates/admin/import_export/change_list_export_item.html +8 -0
  19. adminflow/templates/admin/import_export/change_list_import_item.html +8 -0
  20. adminflow/templates/admin/import_export/export.html +199 -0
  21. adminflow/templates/admin/includes/fieldset.html +73 -0
  22. adminflow/templates/admin/includes/toast.html +102 -0
  23. adminflow/templates/admin/index.html +227 -0
  24. adminflow/templates/admin/logged_out.html +38 -0
  25. adminflow/templates/admin/login.html +144 -0
  26. adminflow/templates/admin/object_history.html +60 -0
  27. adminflow/templates/admin/pagination.html +30 -0
  28. adminflow/templates/admin/search_form.html +28 -0
  29. adminflow/templates/admin/submit_line.html +48 -0
  30. adminflow/templates/admin/verify_2fa.html +280 -0
  31. adminflow/templates/otp/email/token.html +64 -0
  32. adminflow/templates/otp/email/token.txt +11 -0
  33. adminflow/templates/registration/logged_out.html +38 -0
  34. adminflow/templates/registration/password_change_form.html +272 -0
  35. adminflow/templates/simple_history/object_history.html +186 -0
  36. adminflow/templates/simple_history/object_history_form.html +103 -0
  37. adminflow/templates/simple_history/submit_line.html +17 -0
  38. adminflow/templatetags/__init__.py +1 -0
  39. adminflow/templatetags/__pycache__/__init__.cpython-314.pyc +0 -0
  40. adminflow/templatetags/__pycache__/adminflow_tags.cpython-314.pyc +0 -0
  41. adminflow/templatetags/adminflow_tags.py +233 -0
  42. adminflow/user_admin.py +662 -0
  43. adminflow/views.py +207 -0
  44. django_adminflow-1.0.0.dist-info/METADATA +398 -0
  45. django_adminflow-1.0.0.dist-info/RECORD +47 -0
  46. django_adminflow-1.0.0.dist-info/WHEEL +5 -0
  47. 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
@@ -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
+ }