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
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
+ [![PyPI version](https://img.shields.io/pypi/v/django-adminflow.svg)](https://pypi.org/project/django-adminflow/)
51
+ [![Python versions](https://img.shields.io/pypi/pyversions/django-adminflow.svg)](https://pypi.org/project/django-adminflow/)
52
+ [![Django versions](https://img.shields.io/pypi/djversions/django-adminflow.svg)](https://pypi.org/project/django-adminflow/)
53
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
54
+ [![Docs](https://readthedocs.org/projects/django-adminflow/badge/?version=latest)](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
+ | ![Login](docs/_static/screenshots/login.png) | ![Dashboard](docs/_static/screenshots/dashboard.png) |
81
+
82
+ | Customer List | Change Form |
83
+ |---|---|
84
+ | ![Changelist](docs/_static/screenshots/changelist.png) | ![Change form](docs/_static/screenshots/change_form.png) |
85
+
86
+ | Export Page | Audit History |
87
+ |---|---|
88
+ | ![Export](docs/_static/screenshots/export.png) | ![History](docs/_static/screenshots/history.png) |
89
+
90
+ | Import Page | 2FA / User Security |
91
+ |---|---|
92
+ | ![Import](docs/_static/screenshots/import_page.png) | ![2FA](docs/_static/screenshots/user_2fa.png) |
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ adminflow