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
adminflow/views.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from django.shortcuts import render, redirect
|
|
3
|
+
from django.contrib.auth.forms import AuthenticationForm
|
|
4
|
+
from django.contrib.auth import login as auth_login, get_user_model
|
|
5
|
+
from django.contrib import messages
|
|
6
|
+
from django.utils.translation import gettext as _
|
|
7
|
+
from django.conf import settings
|
|
8
|
+
|
|
9
|
+
User = get_user_model()
|
|
10
|
+
|
|
11
|
+
# 15 minutes session duration for 2FA verification screen (in seconds)
|
|
12
|
+
OTP_SESSION_TIMEOUT_SECONDS = 900
|
|
13
|
+
|
|
14
|
+
from .forms import AdminFlowLoginForm
|
|
15
|
+
|
|
16
|
+
def adminflow_login_view(request, extra_context=None):
|
|
17
|
+
if request.user.is_authenticated:
|
|
18
|
+
return redirect(request.GET.get('next', '/admin/'))
|
|
19
|
+
|
|
20
|
+
reason = request.GET.get('reason')
|
|
21
|
+
if reason == 'refreshed':
|
|
22
|
+
# Clear 2FA session if user refreshed the verification page
|
|
23
|
+
for key in ['pending_2fa_user_id', 'pending_2fa_next', 'pending_2fa_email', 'pending_2fa_created_at']:
|
|
24
|
+
request.session.pop(key, None)
|
|
25
|
+
messages.warning(request, "2FA verification was cancelled because the page was refreshed. Please log in again.")
|
|
26
|
+
elif reason == 'expired':
|
|
27
|
+
for key in ['pending_2fa_user_id', 'pending_2fa_next', 'pending_2fa_email', 'pending_2fa_created_at']:
|
|
28
|
+
request.session.pop(key, None)
|
|
29
|
+
messages.error(request, "Your 2FA verification session expired after 15 minutes. Please log in again.")
|
|
30
|
+
|
|
31
|
+
if request.method == 'POST':
|
|
32
|
+
form = AdminFlowLoginForm(request, data=request.POST)
|
|
33
|
+
if form.is_valid():
|
|
34
|
+
user = form.get_user()
|
|
35
|
+
|
|
36
|
+
# Check 2FA requirement
|
|
37
|
+
is_2fa_enabled = getattr(settings, 'ADMINFLOW_DJANGO_OTP', False)
|
|
38
|
+
has_2fa_device = False
|
|
39
|
+
|
|
40
|
+
if is_2fa_enabled:
|
|
41
|
+
try:
|
|
42
|
+
import django_otp
|
|
43
|
+
has_2fa_device = django_otp.user_has_device(user, confirmed=True)
|
|
44
|
+
except Exception:
|
|
45
|
+
has_2fa_device = False
|
|
46
|
+
|
|
47
|
+
if not has_2fa_device:
|
|
48
|
+
# Direct login to dashboard if no 2FA device configured
|
|
49
|
+
auth_login(request, user)
|
|
50
|
+
messages.success(request, f"🎉 Login Successful! Welcome back, {user.email or user.get_full_name() or user.get_username()}.")
|
|
51
|
+
return redirect(request.GET.get('next', '/admin/'))
|
|
52
|
+
else:
|
|
53
|
+
# 2FA is required -> store pending user id and timestamp in session
|
|
54
|
+
request.session['pending_2fa_user_id'] = user.pk
|
|
55
|
+
request.session['pending_2fa_next'] = request.GET.get('next', '/admin/')
|
|
56
|
+
now_ts = time.time()
|
|
57
|
+
request.session['pending_2fa_created_at'] = now_ts
|
|
58
|
+
request.session['pending_2fa_last_sent_at'] = now_ts
|
|
59
|
+
|
|
60
|
+
# Check if Email OTP is active -> send code first!
|
|
61
|
+
try:
|
|
62
|
+
from django_otp.plugins.otp_email.models import EmailDevice
|
|
63
|
+
email_device = EmailDevice.objects.filter(user=user, confirmed=True).first()
|
|
64
|
+
if email_device:
|
|
65
|
+
app_title = getattr(settings, 'ADMINFLOW_TITLE', getattr(settings, 'ADMINFLOW_COMPANY', 'AdminFlow'))
|
|
66
|
+
app_company = getattr(settings, 'ADMINFLOW_COMPANY', getattr(settings, 'ADMINFLOW_TITLE', 'AdminFlow'))
|
|
67
|
+
email_device.generate_challenge(extra_context={
|
|
68
|
+
'user': user,
|
|
69
|
+
'adminflow_title': app_title,
|
|
70
|
+
'adminflow_company': app_company,
|
|
71
|
+
})
|
|
72
|
+
request.session['pending_2fa_email'] = email_device.email
|
|
73
|
+
except Exception:
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
return redirect('adminflow_verify_2fa')
|
|
77
|
+
else:
|
|
78
|
+
form = AdminFlowLoginForm(request)
|
|
79
|
+
|
|
80
|
+
context = {
|
|
81
|
+
'form': form,
|
|
82
|
+
'next': request.GET.get('next', '/admin/'),
|
|
83
|
+
'title': _('Log in'),
|
|
84
|
+
}
|
|
85
|
+
if extra_context:
|
|
86
|
+
context.update(extra_context)
|
|
87
|
+
|
|
88
|
+
return render(request, 'admin/login.html', context)
|
|
89
|
+
|
|
90
|
+
def adminflow_verify_2fa_view(request):
|
|
91
|
+
user_id = request.session.get('pending_2fa_user_id')
|
|
92
|
+
created_at = request.session.get('pending_2fa_created_at')
|
|
93
|
+
|
|
94
|
+
# Check 15-minute 2FA session expiration
|
|
95
|
+
if not user_id or not created_at or (time.time() - created_at > OTP_SESSION_TIMEOUT_SECONDS):
|
|
96
|
+
for key in ['pending_2fa_user_id', 'pending_2fa_next', 'pending_2fa_email', 'pending_2fa_created_at', 'pending_2fa_last_sent_at']:
|
|
97
|
+
request.session.pop(key, None)
|
|
98
|
+
if user_id:
|
|
99
|
+
messages.error(request, "Your 2FA verification session expired after 15 minutes. Please log in again.")
|
|
100
|
+
return redirect('/admin/login/')
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
user = User.objects.get(pk=user_id)
|
|
104
|
+
except User.DoesNotExist:
|
|
105
|
+
for key in ['pending_2fa_user_id', 'pending_2fa_next', 'pending_2fa_email', 'pending_2fa_created_at', 'pending_2fa_last_sent_at']:
|
|
106
|
+
request.session.pop(key, None)
|
|
107
|
+
return redirect('/admin/login/')
|
|
108
|
+
|
|
109
|
+
next_url = request.session.get('pending_2fa_next', '/admin/')
|
|
110
|
+
sent_email = request.session.get('pending_2fa_email')
|
|
111
|
+
elapsed = time.time() - created_at
|
|
112
|
+
remaining_seconds = max(0, int(OTP_SESSION_TIMEOUT_SECONDS - elapsed))
|
|
113
|
+
|
|
114
|
+
last_sent_at = request.session.get('pending_2fa_last_sent_at', created_at)
|
|
115
|
+
resend_cooldown_remaining = max(0, int(60 - (time.time() - last_sent_at)))
|
|
116
|
+
|
|
117
|
+
if request.method == 'POST':
|
|
118
|
+
# Resend email action
|
|
119
|
+
if 'resend_email' in request.POST:
|
|
120
|
+
try:
|
|
121
|
+
from django_otp.plugins.otp_email.models import EmailDevice
|
|
122
|
+
email_device = EmailDevice.objects.filter(user=user, confirmed=True).first()
|
|
123
|
+
if email_device:
|
|
124
|
+
app_title = getattr(settings, 'ADMINFLOW_TITLE', getattr(settings, 'ADMINFLOW_COMPANY', 'AdminFlow'))
|
|
125
|
+
app_company = getattr(settings, 'ADMINFLOW_COMPANY', getattr(settings, 'ADMINFLOW_TITLE', 'AdminFlow'))
|
|
126
|
+
res = email_device.generate_challenge(extra_context={
|
|
127
|
+
'user': user,
|
|
128
|
+
'adminflow_title': app_title,
|
|
129
|
+
'adminflow_company': app_company,
|
|
130
|
+
})
|
|
131
|
+
request.session['pending_2fa_email'] = email_device.email
|
|
132
|
+
request.session['pending_2fa_last_sent_at'] = time.time()
|
|
133
|
+
if isinstance(res, str) and ("cooldown" in res.lower() or "not allowed" in res.lower()):
|
|
134
|
+
messages.warning(request, f"⏳ {res}")
|
|
135
|
+
else:
|
|
136
|
+
messages.success(request, f"📧 Sent new 6-digit code to {email_device.email}!")
|
|
137
|
+
else:
|
|
138
|
+
messages.error(request, "No confirmed Email OTP device found for this account.")
|
|
139
|
+
except Exception as e:
|
|
140
|
+
messages.error(request, f"Error sending email verification code: {str(e)}")
|
|
141
|
+
return redirect('adminflow_verify_2fa')
|
|
142
|
+
|
|
143
|
+
otp_token = request.POST.get('otp_token', '').strip()
|
|
144
|
+
if not otp_token:
|
|
145
|
+
messages.error(request, "Please enter your 6-digit verification code or emergency backup code.")
|
|
146
|
+
else:
|
|
147
|
+
try:
|
|
148
|
+
from django_otp import match_token
|
|
149
|
+
device = match_token(user, otp_token)
|
|
150
|
+
if device:
|
|
151
|
+
auth_login(request, user)
|
|
152
|
+
for key in ['pending_2fa_user_id', 'pending_2fa_next', 'pending_2fa_email', 'pending_2fa_created_at', 'pending_2fa_last_sent_at']:
|
|
153
|
+
request.session.pop(key, None)
|
|
154
|
+
messages.success(request, f"🔐 2FA Verification Successful! Welcome back, {user.email or user.get_full_name() or user.get_username()}.")
|
|
155
|
+
return redirect(next_url)
|
|
156
|
+
else:
|
|
157
|
+
messages.error(request, "Invalid 2FA Verification Code or Emergency Backup Code. Please try again.")
|
|
158
|
+
except Exception as e:
|
|
159
|
+
messages.error(request, f"Authentication error: {str(e)}")
|
|
160
|
+
|
|
161
|
+
has_totp_device = False
|
|
162
|
+
has_email_device = False
|
|
163
|
+
has_static_device = False
|
|
164
|
+
try:
|
|
165
|
+
from django_otp.plugins.otp_totp.models import TOTPDevice
|
|
166
|
+
has_totp_device = TOTPDevice.objects.filter(user=user).exists()
|
|
167
|
+
except Exception:
|
|
168
|
+
pass
|
|
169
|
+
try:
|
|
170
|
+
from django_otp.plugins.otp_email.models import EmailDevice
|
|
171
|
+
email_dev = EmailDevice.objects.filter(user=user).first()
|
|
172
|
+
if email_dev:
|
|
173
|
+
has_email_device = True
|
|
174
|
+
sent_email = sent_email or email_dev.email or user.email
|
|
175
|
+
if not email_dev.confirmed:
|
|
176
|
+
email_dev.confirmed = True
|
|
177
|
+
email_dev.save()
|
|
178
|
+
elif user.email:
|
|
179
|
+
has_email_device = True
|
|
180
|
+
sent_email = sent_email or user.email
|
|
181
|
+
email_dev, _ = EmailDevice.objects.get_or_create(user=user, name='default', defaults={'confirmed': True, 'email': user.email})
|
|
182
|
+
if not email_dev.confirmed:
|
|
183
|
+
email_dev.confirmed = True
|
|
184
|
+
email_dev.save()
|
|
185
|
+
except Exception:
|
|
186
|
+
pass
|
|
187
|
+
try:
|
|
188
|
+
from django_otp.plugins.otp_static.models import StaticDevice
|
|
189
|
+
has_static_device = StaticDevice.objects.filter(user=user).exists()
|
|
190
|
+
except Exception:
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
confirmed_count = sum([1 for x in [has_totp_device, has_email_device, has_static_device] if x])
|
|
194
|
+
|
|
195
|
+
context = {
|
|
196
|
+
'user': user,
|
|
197
|
+
'sent_email': sent_email,
|
|
198
|
+
'next': next_url,
|
|
199
|
+
'remaining_seconds': remaining_seconds,
|
|
200
|
+
'resend_cooldown_remaining': resend_cooldown_remaining,
|
|
201
|
+
'has_totp_device': has_totp_device,
|
|
202
|
+
'has_email_device': has_email_device,
|
|
203
|
+
'has_static_device': has_static_device,
|
|
204
|
+
'confirmed_count': confirmed_count,
|
|
205
|
+
}
|
|
206
|
+
return render(request, 'admin/verify_2fa.html', context)
|
|
207
|
+
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-adminflow
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A premium, modern SaaS design system for the Django Admin interface with built-in 2FA, history tracking, and import/export
|
|
5
|
+
Author-email: Edgar <edgar@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://pypi.org/project/django-adminflow/
|
|
8
|
+
Project-URL: Documentation, https://github.com/edgar/django-adminflow#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/edgar/django-adminflow
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/edgar/django-adminflow/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/edgar/django-adminflow/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: django,admin,saas,design-system,2fa,otp,history,import,export,adminflow
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Environment :: Web Environment
|
|
15
|
+
Classifier: Framework :: Django
|
|
16
|
+
Classifier: Framework :: Django :: 4.2
|
|
17
|
+
Classifier: Framework :: Django :: 5.0
|
|
18
|
+
Classifier: Framework :: Django :: 5.1
|
|
19
|
+
Classifier: Intended Audience :: Developers
|
|
20
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
21
|
+
Classifier: Operating System :: OS Independent
|
|
22
|
+
Classifier: Programming Language :: Python :: 3
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
26
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
27
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
Requires-Dist: Django>=4.2
|
|
31
|
+
Provides-Extra: otp
|
|
32
|
+
Requires-Dist: django-otp>=1.4; extra == "otp"
|
|
33
|
+
Provides-Extra: history
|
|
34
|
+
Requires-Dist: django-simple-history>=3.4; extra == "history"
|
|
35
|
+
Provides-Extra: import-export
|
|
36
|
+
Requires-Dist: django-import-export>=4.0; extra == "import-export"
|
|
37
|
+
Requires-Dist: tablib[xlsx]>=3.5; extra == "import-export"
|
|
38
|
+
Requires-Dist: openpyxl>=3.1; extra == "import-export"
|
|
39
|
+
Provides-Extra: all
|
|
40
|
+
Requires-Dist: django-otp>=1.4; extra == "all"
|
|
41
|
+
Requires-Dist: django-simple-history>=3.4; extra == "all"
|
|
42
|
+
Requires-Dist: django-import-export>=4.0; extra == "all"
|
|
43
|
+
Requires-Dist: tablib[xlsx]>=3.5; extra == "all"
|
|
44
|
+
Requires-Dist: openpyxl>=3.1; extra == "all"
|
|
45
|
+
|
|
46
|
+
# django-adminflow
|
|
47
|
+
|
|
48
|
+
**A modern Django Admin UI template with built-in integrations for popular 3rd party packages.**
|
|
49
|
+
|
|
50
|
+
[](https://pypi.org/project/django-adminflow/)
|
|
51
|
+
[](https://pypi.org/project/django-adminflow/)
|
|
52
|
+
[](https://pypi.org/project/django-adminflow/)
|
|
53
|
+
[](LICENSE)
|
|
54
|
+
[](https://django-adminflow.readthedocs.io/)
|
|
55
|
+
|
|
56
|
+
AdminFlow is a **drop-in Django Admin UI template** that replaces the default Django admin with a clean, minimalist SaaS-grade interface. Beyond the UI refresh, it ships with **ready-to-use integrations** for the most popular Django ecosystem packages — two-factor authentication, per-record audit history, and multi-format import/export — so you get powerful admin features with zero boilerplate.
|
|
57
|
+
|
|
58
|
+
> **One package. Install it, add it to `INSTALLED_APPS`, and your admin instantly looks and works like a premium SaaS product.**
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## What is AdminFlow?
|
|
63
|
+
|
|
64
|
+
The default Django admin is functional but dated. AdminFlow wraps it with:
|
|
65
|
+
|
|
66
|
+
- A **modern, minimalist UI** built with Tailwind utility classes and Material Symbols icons
|
|
67
|
+
- A **collapsible sidebar** with app grouping, icons per model, and active state tracking
|
|
68
|
+
- **Responsive layouts** — works on tablet and desktop
|
|
69
|
+
- **Clean typography** using Inter font (loaded from Google Fonts)
|
|
70
|
+
- **Integrated 3rd party packages** — install the extras you need, and the UI for them is already there
|
|
71
|
+
|
|
72
|
+
AdminFlow is **not** a full CMS replacement. It is a **UI skin + integration layer** for the Django admin you already use.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Screenshots
|
|
77
|
+
|
|
78
|
+
| Login | Dashboard |
|
|
79
|
+
|---|---|
|
|
80
|
+
|  |  |
|
|
81
|
+
|
|
82
|
+
| Customer List | Change Form |
|
|
83
|
+
|---|---|
|
|
84
|
+
|  |  |
|
|
85
|
+
|
|
86
|
+
| Export Page | Audit History |
|
|
87
|
+
|---|---|
|
|
88
|
+
|  |  |
|
|
89
|
+
|
|
90
|
+
| Import Page | 2FA / User Security |
|
|
91
|
+
|---|---|
|
|
92
|
+
|  |  |
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Features
|
|
97
|
+
|
|
98
|
+
| Feature | Description |
|
|
99
|
+
|---|---|
|
|
100
|
+
| 🎨 **Modern UI** | Minimalist SaaS design — sidebar navigation, clean typography, responsive layout |
|
|
101
|
+
| 🔒 **Two-Factor Auth** | TOTP (Google/Authy), email OTP, backup codes — multi-device support |
|
|
102
|
+
| 📜 **Audit History** | Per-record change history with field-level diffs |
|
|
103
|
+
| 📤 **Import / Export** | Multi-sheet XLSX + nested JSON with related models in one file |
|
|
104
|
+
| 🔌 **Plug & Play** | Add to `INSTALLED_APPS` — no extra config required for core UI |
|
|
105
|
+
| 🌐 **i18n Ready** | All UI strings use `{% translate %}` |
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Installation
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Core UI only
|
|
113
|
+
pip install django-adminflow
|
|
114
|
+
|
|
115
|
+
# With 2FA (django-otp)
|
|
116
|
+
pip install "django-adminflow[otp]"
|
|
117
|
+
|
|
118
|
+
# With audit history (django-simple-history)
|
|
119
|
+
pip install "django-adminflow[history]"
|
|
120
|
+
|
|
121
|
+
# With import / export (django-import-export)
|
|
122
|
+
pip install "django-adminflow[import-export]"
|
|
123
|
+
|
|
124
|
+
# Everything
|
|
125
|
+
pip install "django-adminflow[all]"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Setup — `settings.py`
|
|
131
|
+
|
|
132
|
+
### `INSTALLED_APPS`
|
|
133
|
+
|
|
134
|
+
> ⚠️ `"adminflow"` must come **before** `"django.contrib.admin"`
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
INSTALLED_APPS = [
|
|
138
|
+
"adminflow", # ← FIRST, before django.contrib.admin
|
|
139
|
+
"django.contrib.admin",
|
|
140
|
+
"django.contrib.auth",
|
|
141
|
+
"django.contrib.contenttypes",
|
|
142
|
+
"django.contrib.sessions",
|
|
143
|
+
"django.contrib.messages",
|
|
144
|
+
"django.contrib.staticfiles",
|
|
145
|
+
|
|
146
|
+
# Add only what you installed:
|
|
147
|
+
"simple_history", # audit history
|
|
148
|
+
"import_export", # import / export
|
|
149
|
+
"django_otp", # 2FA core
|
|
150
|
+
"django_otp.plugins.otp_totp", # TOTP (Google Authenticator, Authy)
|
|
151
|
+
"django_otp.plugins.otp_email", # email one-time passwords
|
|
152
|
+
"django_otp.plugins.otp_static", # backup / recovery codes
|
|
153
|
+
]
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### `MIDDLEWARE`
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
MIDDLEWARE = [
|
|
160
|
+
"django.middleware.security.SecurityMiddleware",
|
|
161
|
+
"django.contrib.sessions.middleware.SessionMiddleware",
|
|
162
|
+
"django.middleware.common.CommonMiddleware",
|
|
163
|
+
"django.middleware.csrf.CsrfViewMiddleware",
|
|
164
|
+
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
|
165
|
+
"django_otp.middleware.OTPMiddleware", # ← 2FA (after Auth)
|
|
166
|
+
"django.contrib.messages.middleware.MessageMiddleware",
|
|
167
|
+
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
|
168
|
+
"simple_history.middleware.HistoryRequestMiddleware", # ← history
|
|
169
|
+
]
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### `TEMPLATES` — context processor (required)
|
|
173
|
+
|
|
174
|
+
> ⚠️ Without `adminflow.context_processors.adminflow_settings`, the sidebar title, colours and login panel will not render.
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
TEMPLATES = [
|
|
178
|
+
{
|
|
179
|
+
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
|
180
|
+
"DIRS": [],
|
|
181
|
+
"APP_DIRS": True,
|
|
182
|
+
"OPTIONS": {
|
|
183
|
+
"context_processors": [
|
|
184
|
+
"django.template.context_processors.request",
|
|
185
|
+
"django.contrib.auth.context_processors.auth",
|
|
186
|
+
"django.contrib.messages.context_processors.messages",
|
|
187
|
+
"adminflow.context_processors.adminflow_settings", # ← required
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
]
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### AdminFlow Settings
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
# ── Branding ──────────────────────────────────────────────────────────────────
|
|
198
|
+
ADMINFLOW_TITLE = "My App" # sidebar header + browser tab title
|
|
199
|
+
ADMINFLOW_COMPANY = "My Company" # admin footer text
|
|
200
|
+
# ADMINFLOW_LOGO = "logo.svg" # path relative to MEDIA_URL (optional)
|
|
201
|
+
|
|
202
|
+
# ── Login page hero ───────────────────────────────────────────────────────────
|
|
203
|
+
ADMINFLOW_LOGIN_TITLE = "Welcome back"
|
|
204
|
+
ADMINFLOW_LOGIN_SUBTITLE = "Sign in to manage your application."
|
|
205
|
+
|
|
206
|
+
# ── Theme colours ─────────────────────────────────────────────────────────────
|
|
207
|
+
ADMINFLOW_PRIMARY_COLOR = "#1f2021"
|
|
208
|
+
ADMINFLOW_SECONDARY_COLOR = "#475569"
|
|
209
|
+
ADMINFLOW_SUCCESS_COLOR = "#10B981"
|
|
210
|
+
ADMINFLOW_WARNING_COLOR = "#F59E0B"
|
|
211
|
+
ADMINFLOW_DANGER_COLOR = "#EF4444"
|
|
212
|
+
|
|
213
|
+
# ── Sidebar ───────────────────────────────────────────────────────────────────
|
|
214
|
+
ADMINFLOW_SIDEBAR_COLOR = "#0f172a" # background
|
|
215
|
+
ADMINFLOW_SIDEBAR_TEXT_COLOR = "#94a3b8" # inactive item text
|
|
216
|
+
ADMINFLOW_SIDEBAR_ACTIVE_TEXT_COLOR = "#ffffff" # active item text
|
|
217
|
+
ADMINFLOW_SIDEBAR_COLLAPSIBLE = True
|
|
218
|
+
ADMINFLOW_SIDEBAR_WIDTH = 280 # pixels
|
|
219
|
+
|
|
220
|
+
# ── UI ────────────────────────────────────────────────────────────────────────
|
|
221
|
+
ADMINFLOW_BORDER_RADIUS = "16px"
|
|
222
|
+
ADMINFLOW_FONT = "Inter" # any Google Font name
|
|
223
|
+
ADMINFLOW_ENABLE_COMMAND_PALETTE = True # Cmd+K quick search
|
|
224
|
+
|
|
225
|
+
# ── 2FA ───────────────────────────────────────────────────────────────────────
|
|
226
|
+
ADMINFLOW_DJANGO_OTP = True # show OTP verification screen after login
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Email (for Email OTP)
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
|
|
233
|
+
EMAIL_HOST = "smtp.gmail.com"
|
|
234
|
+
EMAIL_PORT = 587
|
|
235
|
+
EMAIL_USE_TLS = True
|
|
236
|
+
EMAIL_HOST_USER = "noreply@myapp.com"
|
|
237
|
+
EMAIL_HOST_PASSWORD = "your-smtp-password" # use .env, never commit
|
|
238
|
+
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
|
|
239
|
+
|
|
240
|
+
OTP_EMAIL_SENDER = EMAIL_HOST_USER
|
|
241
|
+
OTP_EMAIL_FROM_EMAIL = EMAIL_HOST_USER
|
|
242
|
+
OTP_EMAIL_COOLDOWN_DURATION = 60 # seconds between OTP requests
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## 3rd Party Integrations
|
|
248
|
+
|
|
249
|
+
### 🔒 Two-Factor Authentication — `django-otp`
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
# admin.py
|
|
253
|
+
from django.contrib import admin
|
|
254
|
+
from django.contrib.auth import get_user_model
|
|
255
|
+
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
|
256
|
+
from adminflow.user_admin import AdminFlowUserAdminMixin, get_2fa_inlines, unregister_otp_models
|
|
257
|
+
|
|
258
|
+
User = get_user_model()
|
|
259
|
+
|
|
260
|
+
@admin.register(User)
|
|
261
|
+
class UserAdmin(AdminFlowUserAdminMixin, BaseUserAdmin):
|
|
262
|
+
inlines = get_2fa_inlines()
|
|
263
|
+
|
|
264
|
+
unregister_otp_models() # removes raw django-otp admin entries
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**What you get:** TOTP + email OTP + backup codes, multi-device per user, admin bulk actions (*Enable 2FA*, *Generate backup codes*).
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
### 📜 Audit History — `django-simple-history`
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
# models.py
|
|
275
|
+
from simple_history.models import HistoricalRecords
|
|
276
|
+
|
|
277
|
+
class Customer(models.Model):
|
|
278
|
+
name = models.CharField(max_length=200)
|
|
279
|
+
email = models.EmailField(unique=True)
|
|
280
|
+
history = HistoricalRecords() # ← add this
|
|
281
|
+
|
|
282
|
+
# admin.py
|
|
283
|
+
from simple_history.admin import SimpleHistoryAdmin
|
|
284
|
+
|
|
285
|
+
@admin.register(Customer)
|
|
286
|
+
class CustomerAdmin(SimpleHistoryAdmin):
|
|
287
|
+
list_display = ('name', 'email')
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
### 📤 Import / Export — `django-import-export`
|
|
293
|
+
|
|
294
|
+
Add `import_export` to `INSTALLED_APPS`, then use `MultiSheetExportImportMixin`:
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
# resources.py
|
|
298
|
+
from import_export import resources, fields
|
|
299
|
+
from import_export.widgets import ForeignKeyWidget
|
|
300
|
+
from .models import Customer, Order
|
|
301
|
+
|
|
302
|
+
class CustomerResource(resources.ModelResource):
|
|
303
|
+
class Meta:
|
|
304
|
+
model = Customer
|
|
305
|
+
fields = ('id', 'name', 'email', 'phone', 'notes', 'date_created')
|
|
306
|
+
|
|
307
|
+
class OrderResource(resources.ModelResource):
|
|
308
|
+
customer = fields.Field(
|
|
309
|
+
column_name='customer_email',
|
|
310
|
+
attribute='customer',
|
|
311
|
+
widget=ForeignKeyWidget(Customer, field='email'),
|
|
312
|
+
)
|
|
313
|
+
class Meta:
|
|
314
|
+
model = Order
|
|
315
|
+
fields = ('id', 'customer', 'product', 'order_date', 'status', 'total_amount')
|
|
316
|
+
|
|
317
|
+
# admin.py
|
|
318
|
+
from adminflow.import_export import MultiSheetExportImportMixin
|
|
319
|
+
from import_export.admin import ImportExportModelAdmin
|
|
320
|
+
|
|
321
|
+
@admin.register(Customer)
|
|
322
|
+
class CustomerAdmin(MultiSheetExportImportMixin, ImportExportModelAdmin, admin.ModelAdmin):
|
|
323
|
+
combined_sheets = [
|
|
324
|
+
('General Details', CustomerResource, None),
|
|
325
|
+
('Orders', OrderResource, lambda qs: Order.objects.filter(customer__in=qs)),
|
|
326
|
+
]
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
**Export formats:**
|
|
330
|
+
|
|
331
|
+
| Format | Output |
|
|
332
|
+
|---|---|
|
|
333
|
+
| **xlsx** | One `.xlsx` file — one sheet per resource |
|
|
334
|
+
| **json** | One `.json` file — children nested inside each parent record |
|
|
335
|
+
|
|
336
|
+
**JSON output example:**
|
|
337
|
+
|
|
338
|
+
```json
|
|
339
|
+
[
|
|
340
|
+
{
|
|
341
|
+
"id": "1",
|
|
342
|
+
"name": "Alice Johnson",
|
|
343
|
+
"email": "alice@example.com",
|
|
344
|
+
"orders": [
|
|
345
|
+
{ "id": "1", "customer_email": "alice@example.com", "order_date": "2026-07-22", "status": "delivered" }
|
|
346
|
+
]
|
|
347
|
+
}
|
|
348
|
+
]
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## Customisation
|
|
354
|
+
|
|
355
|
+
```python
|
|
356
|
+
# admin.py — Material Symbols icon per model
|
|
357
|
+
@admin.register(Product)
|
|
358
|
+
class ProductAdmin(admin.ModelAdmin):
|
|
359
|
+
icon = "inventory_2" # browse at fonts.google.com/icons
|
|
360
|
+
|
|
361
|
+
# apps.py — sidebar group label
|
|
362
|
+
class CrmConfig(AppConfig):
|
|
363
|
+
name = "crm"
|
|
364
|
+
verbose_name = "Customer Relations (CRM)"
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## Changelog
|
|
370
|
+
|
|
371
|
+
### v1.0.0 — 2026-07-23
|
|
372
|
+
- Initial stable release
|
|
373
|
+
- Modern minimalist admin UI (sidebar, Inter font, responsive)
|
|
374
|
+
- Full 2FA: TOTP, email OTP, backup codes, multi-device
|
|
375
|
+
- Audit history with field-level diffs (django-simple-history)
|
|
376
|
+
- Multi-sheet XLSX export/import (one sheet per related model)
|
|
377
|
+
- Nested JSON export (children embedded inside parent records)
|
|
378
|
+
- `MultiSheetExportImportMixin` for plug-and-play multi-resource export
|
|
379
|
+
- Styled Import / Export buttons, export page with field chips, import page
|
|
380
|
+
- All `ADMINFLOW_*` theme settings
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
## Publishing to PyPI
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
pip install build twine
|
|
388
|
+
python -m build
|
|
389
|
+
twine upload dist/*
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## Documentation
|
|
393
|
+
|
|
394
|
+
Full documentation: [django-adminflow.readthedocs.io](https://django-adminflow.readthedocs.io/)
|
|
395
|
+
|
|
396
|
+
## License
|
|
397
|
+
|
|
398
|
+
MIT
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
adminflow/__init__.py,sha256=mgQubIzwU-cY3p7kpBn0qVDsGliGDTt3SF2FRPOFVpY,33
|
|
2
|
+
adminflow/apps.py,sha256=-jJTbALQ8fkw7FxqcKTkl3Mr069F5VeYpH6pPeZO6DI,1090
|
|
3
|
+
adminflow/context_processors.py,sha256=jyFNYnQsgB-gvjF4cTppmEozcyn_ITpzc3tPeFCzjao,2631
|
|
4
|
+
adminflow/forms.py,sha256=9pn30Bvh57FK5nu29G9JO9drhs5pKhFjNeyA91qdKuQ,3908
|
|
5
|
+
adminflow/import_export.py,sha256=G7Sg7XIABfTbupeP3HYLyGaS6V3QSS-pTOX51xcONCs,9315
|
|
6
|
+
adminflow/user_admin.py,sha256=BBxzv_Nfw4teK0T9h-7uDR-KTPacJjZacVGIr9VIrc8,30827
|
|
7
|
+
adminflow/views.py,sha256=JIbGubCE7f3_NOFY4_SIJvQ_9cyXRNAf3f-7tVc1duk,9882
|
|
8
|
+
adminflow/static/adminflow/img/logo.png,sha256=GJaUHLCpz5FYX0FboC3m4F6RkUNB0gBxQMP5SL6rAnA,152621
|
|
9
|
+
adminflow/templates/admin/actions.html,sha256=S0tTp4fIYRXcx3CHCmHdxpx_U5ZUjiPmcXMJg3MJzaI,3608
|
|
10
|
+
adminflow/templates/admin/base.html,sha256=N_ghysQVyM2xq6qooptB2AgWH3FEPAG8YLPeT2KFKXw,62169
|
|
11
|
+
adminflow/templates/admin/base_site.html,sha256=D9O6f_6VyEUQVHO8OeX794P6Y0UjbpuFeAlY_Ffz-MA,127
|
|
12
|
+
adminflow/templates/admin/change_form.html,sha256=4CwtwiPXqZ2xw4MijP7UgCTe2i-5kNtCuGPp7L28MTc,19872
|
|
13
|
+
adminflow/templates/admin/change_list.html,sha256=7GCwSykXXL3nasJ8nMXBgxwAYGduiiK2-ljvnR3dOpw,10546
|
|
14
|
+
adminflow/templates/admin/change_list_results.html,sha256=dPMdIBuL8PTyokwZTAk2C2dJg8ulRezB3Yk4a-XO-ms,9228
|
|
15
|
+
adminflow/templates/admin/delete_confirmation.html,sha256=DL6rn-7rK6Fs37TxGGPE52QKd6ClGFD9LzHgrfOWrrU,3412
|
|
16
|
+
adminflow/templates/admin/delete_selected_confirmation.html,sha256=wj62sTjnGvf8jMLl3I8GyG7suHwd5GDK_lhcQ6bodAA,6811
|
|
17
|
+
adminflow/templates/admin/filter.html,sha256=pZzCkUYLiC2_Deh0r7BRKNvSroSTaBpjLxMTASaBNaw,850
|
|
18
|
+
adminflow/templates/admin/index.html,sha256=9VnzE4hfD0RoSlazNz3tSmODICQX39fAs_p0jqLio9U,14118
|
|
19
|
+
adminflow/templates/admin/logged_out.html,sha256=I202If9eq5LIfjgCTzKoQMn_sIyiJnh6VT88fidTmBc,1915
|
|
20
|
+
adminflow/templates/admin/login.html,sha256=qSjptLXd7DcUk5gb5Lcr8hGMRiRSxJu7FLpyt0PQmqo,8498
|
|
21
|
+
adminflow/templates/admin/object_history.html,sha256=9tcSx6nqORm1HcjosswNnS8c0YWpsXaXLZ9O4yiEt_k,2933
|
|
22
|
+
adminflow/templates/admin/pagination.html,sha256=WReR0lNUbmi8z3-anJgvNwJoJiOkEUp16_Wbgdp3WRo,1490
|
|
23
|
+
adminflow/templates/admin/search_form.html,sha256=tTQ9bWXc1qQLfSzXZMZP_jagyWLzYQrRfINnA7Z76MU,1626
|
|
24
|
+
adminflow/templates/admin/submit_line.html,sha256=ukhK47E4UZH5ciJP-Q2kX4F8u3sVOQ6gvTHpv-l-pX4,3033
|
|
25
|
+
adminflow/templates/admin/verify_2fa.html,sha256=uOV_mQcz4J7m2Re_ALAuiT0XrJWAuoIDP3PLp0zHWlM,16407
|
|
26
|
+
adminflow/templates/admin/edit_inline/stacked.html,sha256=yLQVYmPaEmFt9N9PYWT99I6LhFwnAP0n6utVB04VC3k,5666
|
|
27
|
+
adminflow/templates/admin/edit_inline/tabular.html,sha256=rnCx24LupAJNMXgwijZEicSVYlWDYZGKk3zEMn9G6Rc,5908
|
|
28
|
+
adminflow/templates/admin/import_export/change_list_export_item.html,sha256=AoKVaUBANOn-gismiNpF1gY4o536cAINvxq8bhZs2MA,481
|
|
29
|
+
adminflow/templates/admin/import_export/change_list_import_item.html,sha256=Je28OM4CbsuOVddU6_OZODMJGZlhsuDTjpCdplL-AKc,454
|
|
30
|
+
adminflow/templates/admin/import_export/export.html,sha256=-NqOoLf8ptxxepwd7GSPI7eu5FI77SZxbjDGz68EnxA,8838
|
|
31
|
+
adminflow/templates/admin/includes/fieldset.html,sha256=O5yc7cmd872yBZTPHiUTDMd-kb5gQsxK5qafH-l5nFM,5010
|
|
32
|
+
adminflow/templates/admin/includes/toast.html,sha256=MFydlfwqMlYlycToH0izeaUsVJfw7BUaEdDDmUvOcGI,6819
|
|
33
|
+
adminflow/templates/otp/email/token.html,sha256=YVaipBPGyjNFtwqk14xK7s0DyrO5Hhi4NgMJRI6jFtA,3748
|
|
34
|
+
adminflow/templates/otp/email/token.txt,sha256=_Qu1jRfJlSIus2O4Kd-IwHe2m5y_gvZ_FlPYBmPpjig,339
|
|
35
|
+
adminflow/templates/registration/logged_out.html,sha256=I202If9eq5LIfjgCTzKoQMn_sIyiJnh6VT88fidTmBc,1915
|
|
36
|
+
adminflow/templates/registration/password_change_form.html,sha256=Blbx-ALPWtUbJx_H3QQUIqMMBigGfdfqJFq0cCLP-Qs,11675
|
|
37
|
+
adminflow/templates/simple_history/object_history.html,sha256=esfT1Y08lKgXYSV_JgzS4e3cS8wKWThb6anC_0wAsEU,9108
|
|
38
|
+
adminflow/templates/simple_history/object_history_form.html,sha256=A3LOcMY_l9wwVndudadeAgUbHmvooAB7Lt8c1TkisSk,5097
|
|
39
|
+
adminflow/templates/simple_history/submit_line.html,sha256=nKBHJqgTYmlKBjnaaZG3sR_kvmcn9oSYZumfTSr_dQk,1059
|
|
40
|
+
adminflow/templatetags/__init__.py,sha256=K8PcAAZu-q6LgCbrH29BSxwsWH0GQx-PhT7J-hBATMM,24
|
|
41
|
+
adminflow/templatetags/adminflow_tags.py,sha256=GXqjxSBG7ckh_7KkUJn215I4hGONxDoPtXOC1GadSdM,7697
|
|
42
|
+
adminflow/templatetags/__pycache__/__init__.cpython-314.pyc,sha256=xhJGFEqO5ViriOTM1G1atyRCaYo63cfD8fNdPNtIUMM,202
|
|
43
|
+
adminflow/templatetags/__pycache__/adminflow_tags.cpython-314.pyc,sha256=ml7Bjmm5ZXlE7tCwdoEhyyfYDPelWPAxVKUcMq3uJZg,12009
|
|
44
|
+
django_adminflow-1.0.0.dist-info/METADATA,sha256=1BLjTNydD1femQo304wugeB_aSzAyFSr_VNE2nmKLb0,14796
|
|
45
|
+
django_adminflow-1.0.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
46
|
+
django_adminflow-1.0.0.dist-info/top_level.txt,sha256=dH2xIFDkuXE8fNiK7J8yKyejcQtusjBf8PHTfcx-sjY,10
|
|
47
|
+
django_adminflow-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
adminflow
|