django-snapadmin 0.1.0a1__tar.gz

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 (35) hide show
  1. django_snapadmin-0.1.0a1/LICENSE +21 -0
  2. django_snapadmin-0.1.0a1/PKG-INFO +197 -0
  3. django_snapadmin-0.1.0a1/README.md +161 -0
  4. django_snapadmin-0.1.0a1/pyproject.toml +46 -0
  5. django_snapadmin-0.1.0a1/snapadmin/__init__.py +0 -0
  6. django_snapadmin-0.1.0a1/snapadmin/admin.py +111 -0
  7. django_snapadmin-0.1.0a1/snapadmin/api/__init__.py +0 -0
  8. django_snapadmin-0.1.0a1/snapadmin/api/authentication.py +81 -0
  9. django_snapadmin-0.1.0a1/snapadmin/api/graphql.py +52 -0
  10. django_snapadmin-0.1.0a1/snapadmin/api/health.py +55 -0
  11. django_snapadmin-0.1.0a1/snapadmin/api/serializers.py +84 -0
  12. django_snapadmin-0.1.0a1/snapadmin/api/tasks.py +27 -0
  13. django_snapadmin-0.1.0a1/snapadmin/api/views.py +199 -0
  14. django_snapadmin-0.1.0a1/snapadmin/apps.py +22 -0
  15. django_snapadmin-0.1.0a1/snapadmin/fields.py +325 -0
  16. django_snapadmin-0.1.0a1/snapadmin/init.py +0 -0
  17. django_snapadmin-0.1.0a1/snapadmin/logging_config.py +155 -0
  18. django_snapadmin-0.1.0a1/snapadmin/management/__init__.py +0 -0
  19. django_snapadmin-0.1.0a1/snapadmin/management/commands/__init__.py +0 -0
  20. django_snapadmin-0.1.0a1/snapadmin/migrations/__init__.py +0 -0
  21. django_snapadmin-0.1.0a1/snapadmin/models.py +755 -0
  22. django_snapadmin-0.1.0a1/snapadmin/static/snapadmin/__init__.py +1 -0
  23. django_snapadmin-0.1.0a1/snapadmin/static/snapadmin/css/admin.css +226 -0
  24. django_snapadmin-0.1.0a1/snapadmin/static/snapadmin/css/select2.min.css +1 -0
  25. django_snapadmin-0.1.0a1/snapadmin/static/snapadmin/js/admin.js +78 -0
  26. django_snapadmin-0.1.0a1/snapadmin/static/snapadmin/js/jquery_bridge.js +1 -0
  27. django_snapadmin-0.1.0a1/snapadmin/static/snapadmin/js/model_selector.js +79 -0
  28. django_snapadmin-0.1.0a1/snapadmin/static/snapadmin/js/select2.min.js +2 -0
  29. django_snapadmin-0.1.0a1/snapadmin/static/snapadmin/snap-logo.svg +5 -0
  30. django_snapadmin-0.1.0a1/snapadmin/templates/snapadmin/dashboard.html +321 -0
  31. django_snapadmin-0.1.0a1/snapadmin/templates/snapadmin/widgets/smart_model_selector.html +30 -0
  32. django_snapadmin-0.1.0a1/snapadmin/urls.py +81 -0
  33. django_snapadmin-0.1.0a1/snapadmin/validators.py +115 -0
  34. django_snapadmin-0.1.0a1/snapadmin/views.py +148 -0
  35. django_snapadmin-0.1.0a1/snapadmin/widgets.py +52 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexander Wiese
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,197 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-snapadmin
3
+ Version: 0.1.0a1
4
+ Summary: Automatic and customizable admin interface for Django projects.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: django,admin,ui,Snap,extra-settings
8
+ Author: Alexander Wiese
9
+ Requires-Python: >=3.10
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Requires-Dist: Django (>=5.2)
18
+ Requires-Dist: colorama (>=0.4.6)
19
+ Requires-Dist: django-admin-autocomplete-filter (>=0.7.1)
20
+ Requires-Dist: django-admin-rangefilter (>=0.13.3)
21
+ Requires-Dist: django-ckeditor-5 (>=0.0.12)
22
+ Requires-Dist: django-colorfield (>=0.11.0)
23
+ Requires-Dist: django-cors-headers (>=4.4.0)
24
+ Requires-Dist: django-extra-settings (>=0.12.0)
25
+ Requires-Dist: django-unfold (>=0.40.0)
26
+ Requires-Dist: djangorestframework (>=3.15.0)
27
+ Requires-Dist: djangorestframework-simplejwt (>=5.3.0)
28
+ Requires-Dist: drf-spectacular (>=0.27.0)
29
+ Requires-Dist: graphene-django (>=3.2.0)
30
+ Requires-Dist: python-dotenv (>=1.0.0)
31
+ Requires-Dist: structlog (>=24.1.0)
32
+ Project-URL: Homepage, https://github.com/drofji/django-snapadmin
33
+ Project-URL: Repository, https://github.com/drofji/django-snapadmin
34
+ Description-Content-Type: text/markdown
35
+
36
+ # 🚀 SnapAdmin — Declarative Django Admin & API Package
37
+
38
+ **SnapAdmin** is a high-performance, declarative Django package that eliminates admin and API boilerplate. Define your model fields once — get a feature-rich, beautiful Django admin (powered by Unfold), a full REST API, and a dynamic GraphQL API automatically.
39
+
40
+ [![Python](https://img.shields.io/badge/Python-3.10+-blue?logo=python)](https://python.org)
41
+ [![Django](https://img.shields.io/badge/Django-5.2+-green?logo=django)](https://djangoproject.com)
42
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow)](LICENSE)
43
+
44
+ ---
45
+
46
+ ## 📦 SnapAdmin Package Features
47
+
48
+ The core `snapadmin` package provides everything you need to bootstrap your project's admin and API:
49
+
50
+ | Feature | Description |
51
+ |---------|-------------|
52
+ | **Declarative Admin** | Configure `list_display`, `search_fields`, `list_filter` directly in your models using `SnapField`. |
53
+ | **Beautiful UI** | Native integration with `django-unfold` for a modern, responsive admin experience. |
54
+ | **Status Badges** | Easily add color-coded HTML badges for choices and status fields. |
55
+ | **Advanced Layout** | Support for horizontal field rows and tabbed interfaces within the admin form. |
56
+ | **Range Filters** | Built-in date and numeric range filters for efficient data exploration. |
57
+ | **Change Logging** | Automatic tracking of field-level changes (`old → new`) with a dedicated history view. |
58
+ | **Automatic REST API** | Instantly generated CRUD endpoints for every `SnapModel` with zero extra code. |
59
+ | **Dynamic GraphQL API** | Automatically generated GraphQL schema with support for complex data fetching. |
60
+ | **Token Auth** | Secure, expirable API tokens with granular model-level access control. |
61
+ | **Configurable** | Easily enable/disable REST API, GraphQL, Swagger docs, and search modes via settings. |
62
+ | **Elasticsearch Ready** | Multi-mode storage (`DB_ONLY`, `DUAL`, `ES_ONLY`) for blazing fast search. |
63
+ | **Structured Logging** | Integrated `structlog` for readable local logs and JSON logs in production. |
64
+
65
+ ---
66
+
67
+ ## 🏗 Package Architecture
68
+
69
+ ```
70
+ snapadmin/
71
+ ├── api/ # REST & GraphQL API core: views, serializers, auth
72
+ ├── management/ # Custom management commands
73
+ ├── migrations/ # Core package migrations (e.g., APIToken)
74
+ ├── static/ # UI assets (CSS, JS, SVG logos)
75
+ ├── templates/ # Custom admin templates & dashboard
76
+ ├── fields.py # SnapField definitions with admin introspection
77
+ ├── models.py # SnapModel base, EsManager, and core logic
78
+ └── urls.py # Auto-configurable API and documentation routes
79
+ ```
80
+
81
+ ---
82
+
83
+ ## 🚀 Quickstart: Installation
84
+
85
+ ### From PyPI (Recommended)
86
+ ```bash
87
+ pip install django-snapadmin
88
+ ```
89
+
90
+ ### From GitHub (Latest/Development)
91
+ ```bash
92
+ pip install git+https://github.com/drofji/django-snapadmin.git
93
+ ```
94
+
95
+ ---
96
+
97
+ ## 🛠 Usage & Configuration
98
+
99
+ ### 1. Configure Settings
100
+ Add required apps to `INSTALLED_APPS` in `settings.py`:
101
+ ```python
102
+ INSTALLED_APPS = [
103
+ "unfold",
104
+ "snapadmin",
105
+ "rest_framework",
106
+ "drf_spectacular",
107
+ "graphene_django",
108
+ # ...
109
+ ]
110
+ ```
111
+
112
+ ### 2. Define your Model
113
+ ```python
114
+ from snapadmin import fields as snap, models as snap_models
115
+
116
+ class Product(snap_models.SnapModel):
117
+ name = snap.SnapCharField(max_length=200, searchable=True, show_in_list=True)
118
+ # Group fields into a single horizontal row
119
+ price = snap.SnapDecimalField(max_digits=10, decimal_places=2, row="pricing")
120
+ available = snap.SnapBooleanField(default=True, row="pricing")
121
+ ```
122
+
123
+ ### 3. Register Admin
124
+ ```python
125
+ # admin.py
126
+ from snapadmin.models import SnapModel
127
+ SnapModel.register_all_admins()
128
+ ```
129
+
130
+ ---
131
+
132
+ ## ⚙️ Advanced Settings
133
+
134
+ Control core features via Django settings:
135
+
136
+ ```python
137
+ SNAPADMIN_REST_API_ENABLED = True # Enable/Disable the REST API
138
+ SNAPADMIN_GRAPHQL_ENABLED = True # Enable/Disable the GraphQL API
139
+ SNAPADMIN_SWAGGER_ENABLED = True # Enable/Disable Swagger UI documentation
140
+ ELASTICSEARCH_ENABLED = False # Toggle ES search engine support
141
+ ```
142
+
143
+ ---
144
+
145
+ ## 🌟 Demo Application Features
146
+
147
+ The repository includes a `demo/` app and a `sandbox/` project to showcase SnapAdmin's power:
148
+
149
+ - **Complete Project Setup**: Ready-to-use Docker environment with PostgreSQL, Redis, and Elasticsearch.
150
+ - **Example Domain Models**: Product, Customer, and Order models showing complex relationships.
151
+ - **Interactive Dashboard**: A custom system dashboard with health checks and environment stats.
152
+ - **Seeder Command**: `python manage.py seed_demo` to instantly populate your environment.
153
+ - **Celery Integration**: Example background tasks for data indexing and stats generation.
154
+
155
+ ---
156
+
157
+ ## 🐳 Running the Demo (Docker)
158
+
159
+ ```bash
160
+ git clone https://github.com/drofji/django-snapadmin.git
161
+ cd django-snapadmin
162
+ cp dist.env .env
163
+ docker compose up --build
164
+ ```
165
+ - **Admin**: http://localhost:8000/admin/ (admin / admin)
166
+ - **REST API Docs**: http://localhost:8000/api/docs/
167
+ - **GraphQL API**: http://localhost:8000/api/graphql/
168
+
169
+ ---
170
+
171
+ ## 💻 Local Development Setup
172
+
173
+ ```bash
174
+ # Clone and setup environment
175
+ git clone https://github.com/drofji/django-snapadmin.git
176
+ cd django-snapadmin
177
+ python -m venv .venv
178
+ source .venv/bin/activate
179
+
180
+ # Install in editable mode
181
+ pip install -r requirements.txt
182
+ pip install -e .
183
+
184
+ # Initialize DB and run
185
+ python manage.py migrate
186
+ python manage.py seed_demo
187
+ python manage.py runserver
188
+ ```
189
+
190
+ ---
191
+
192
+ ## 📜 License
193
+
194
+ MIT License — see [LICENSE](LICENSE).
195
+
196
+
197
+
@@ -0,0 +1,161 @@
1
+ # 🚀 SnapAdmin — Declarative Django Admin & API Package
2
+
3
+ **SnapAdmin** is a high-performance, declarative Django package that eliminates admin and API boilerplate. Define your model fields once — get a feature-rich, beautiful Django admin (powered by Unfold), a full REST API, and a dynamic GraphQL API automatically.
4
+
5
+ [![Python](https://img.shields.io/badge/Python-3.10+-blue?logo=python)](https://python.org)
6
+ [![Django](https://img.shields.io/badge/Django-5.2+-green?logo=django)](https://djangoproject.com)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow)](LICENSE)
8
+
9
+ ---
10
+
11
+ ## 📦 SnapAdmin Package Features
12
+
13
+ The core `snapadmin` package provides everything you need to bootstrap your project's admin and API:
14
+
15
+ | Feature | Description |
16
+ |---------|-------------|
17
+ | **Declarative Admin** | Configure `list_display`, `search_fields`, `list_filter` directly in your models using `SnapField`. |
18
+ | **Beautiful UI** | Native integration with `django-unfold` for a modern, responsive admin experience. |
19
+ | **Status Badges** | Easily add color-coded HTML badges for choices and status fields. |
20
+ | **Advanced Layout** | Support for horizontal field rows and tabbed interfaces within the admin form. |
21
+ | **Range Filters** | Built-in date and numeric range filters for efficient data exploration. |
22
+ | **Change Logging** | Automatic tracking of field-level changes (`old → new`) with a dedicated history view. |
23
+ | **Automatic REST API** | Instantly generated CRUD endpoints for every `SnapModel` with zero extra code. |
24
+ | **Dynamic GraphQL API** | Automatically generated GraphQL schema with support for complex data fetching. |
25
+ | **Token Auth** | Secure, expirable API tokens with granular model-level access control. |
26
+ | **Configurable** | Easily enable/disable REST API, GraphQL, Swagger docs, and search modes via settings. |
27
+ | **Elasticsearch Ready** | Multi-mode storage (`DB_ONLY`, `DUAL`, `ES_ONLY`) for blazing fast search. |
28
+ | **Structured Logging** | Integrated `structlog` for readable local logs and JSON logs in production. |
29
+
30
+ ---
31
+
32
+ ## 🏗 Package Architecture
33
+
34
+ ```
35
+ snapadmin/
36
+ ├── api/ # REST & GraphQL API core: views, serializers, auth
37
+ ├── management/ # Custom management commands
38
+ ├── migrations/ # Core package migrations (e.g., APIToken)
39
+ ├── static/ # UI assets (CSS, JS, SVG logos)
40
+ ├── templates/ # Custom admin templates & dashboard
41
+ ├── fields.py # SnapField definitions with admin introspection
42
+ ├── models.py # SnapModel base, EsManager, and core logic
43
+ └── urls.py # Auto-configurable API and documentation routes
44
+ ```
45
+
46
+ ---
47
+
48
+ ## 🚀 Quickstart: Installation
49
+
50
+ ### From PyPI (Recommended)
51
+ ```bash
52
+ pip install django-snapadmin
53
+ ```
54
+
55
+ ### From GitHub (Latest/Development)
56
+ ```bash
57
+ pip install git+https://github.com/drofji/django-snapadmin.git
58
+ ```
59
+
60
+ ---
61
+
62
+ ## 🛠 Usage & Configuration
63
+
64
+ ### 1. Configure Settings
65
+ Add required apps to `INSTALLED_APPS` in `settings.py`:
66
+ ```python
67
+ INSTALLED_APPS = [
68
+ "unfold",
69
+ "snapadmin",
70
+ "rest_framework",
71
+ "drf_spectacular",
72
+ "graphene_django",
73
+ # ...
74
+ ]
75
+ ```
76
+
77
+ ### 2. Define your Model
78
+ ```python
79
+ from snapadmin import fields as snap, models as snap_models
80
+
81
+ class Product(snap_models.SnapModel):
82
+ name = snap.SnapCharField(max_length=200, searchable=True, show_in_list=True)
83
+ # Group fields into a single horizontal row
84
+ price = snap.SnapDecimalField(max_digits=10, decimal_places=2, row="pricing")
85
+ available = snap.SnapBooleanField(default=True, row="pricing")
86
+ ```
87
+
88
+ ### 3. Register Admin
89
+ ```python
90
+ # admin.py
91
+ from snapadmin.models import SnapModel
92
+ SnapModel.register_all_admins()
93
+ ```
94
+
95
+ ---
96
+
97
+ ## ⚙️ Advanced Settings
98
+
99
+ Control core features via Django settings:
100
+
101
+ ```python
102
+ SNAPADMIN_REST_API_ENABLED = True # Enable/Disable the REST API
103
+ SNAPADMIN_GRAPHQL_ENABLED = True # Enable/Disable the GraphQL API
104
+ SNAPADMIN_SWAGGER_ENABLED = True # Enable/Disable Swagger UI documentation
105
+ ELASTICSEARCH_ENABLED = False # Toggle ES search engine support
106
+ ```
107
+
108
+ ---
109
+
110
+ ## 🌟 Demo Application Features
111
+
112
+ The repository includes a `demo/` app and a `sandbox/` project to showcase SnapAdmin's power:
113
+
114
+ - **Complete Project Setup**: Ready-to-use Docker environment with PostgreSQL, Redis, and Elasticsearch.
115
+ - **Example Domain Models**: Product, Customer, and Order models showing complex relationships.
116
+ - **Interactive Dashboard**: A custom system dashboard with health checks and environment stats.
117
+ - **Seeder Command**: `python manage.py seed_demo` to instantly populate your environment.
118
+ - **Celery Integration**: Example background tasks for data indexing and stats generation.
119
+
120
+ ---
121
+
122
+ ## 🐳 Running the Demo (Docker)
123
+
124
+ ```bash
125
+ git clone https://github.com/drofji/django-snapadmin.git
126
+ cd django-snapadmin
127
+ cp dist.env .env
128
+ docker compose up --build
129
+ ```
130
+ - **Admin**: http://localhost:8000/admin/ (admin / admin)
131
+ - **REST API Docs**: http://localhost:8000/api/docs/
132
+ - **GraphQL API**: http://localhost:8000/api/graphql/
133
+
134
+ ---
135
+
136
+ ## 💻 Local Development Setup
137
+
138
+ ```bash
139
+ # Clone and setup environment
140
+ git clone https://github.com/drofji/django-snapadmin.git
141
+ cd django-snapadmin
142
+ python -m venv .venv
143
+ source .venv/bin/activate
144
+
145
+ # Install in editable mode
146
+ pip install -r requirements.txt
147
+ pip install -e .
148
+
149
+ # Initialize DB and run
150
+ python manage.py migrate
151
+ python manage.py seed_demo
152
+ python manage.py runserver
153
+ ```
154
+
155
+ ---
156
+
157
+ ## 📜 License
158
+
159
+ MIT License — see [LICENSE](LICENSE).
160
+
161
+
@@ -0,0 +1,46 @@
1
+ [tool.poetry]
2
+ name = "django-snapadmin"
3
+ version = "0.1.0a1"
4
+ description = "Automatic and customizable admin interface for Django projects."
5
+ authors = ["Alexander Wiese"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+ homepage = "https://github.com/drofji/django-snapadmin"
9
+ repository = "https://github.com/drofji/django-snapadmin"
10
+ keywords = ["django", "admin", "ui", "Snap", "extra-settings"]
11
+
12
+ packages = [{include = "snapadmin"}]
13
+
14
+ include = [
15
+ "snapadmin/templates/**/*",
16
+ "snapadmin/static/**/*",
17
+ ]
18
+
19
+ [tool.poetry.dependencies]
20
+ python = ">=3.10"
21
+ Django = ">=5.2"
22
+ python-dotenv = ">=1.0.0"
23
+ django-admin-rangefilter = ">=0.13.3"
24
+ django-unfold = ">=0.40.0"
25
+ django-ckeditor-5 = ">=0.0.12"
26
+ django-colorfield = ">=0.11.0"
27
+ django-cors-headers = ">=4.4.0"
28
+ django-admin-autocomplete-filter = ">=0.7.1"
29
+ django-extra-settings = ">=0.12.0"
30
+ djangorestframework = ">=3.15.0"
31
+ drf-spectacular = ">=0.27.0"
32
+ djangorestframework-simplejwt = ">=5.3.0"
33
+ structlog = ">=24.1.0"
34
+ colorama = ">=0.4.6"
35
+ graphene-django = ">=3.2.0"
36
+
37
+ [tool.poetry.group.dev.dependencies]
38
+ black = "^24.0.0"
39
+ flake8 = "^7.0.0"
40
+ pytest = "^8.0.0"
41
+ pytest-django = "^4.8.0"
42
+ pytest-cov = "^5.0.0"
43
+
44
+ [build-system]
45
+ requires = ["poetry-core>=1.0.0"]
46
+ build-backend = "poetry.core.masonry.api"
File without changes
@@ -0,0 +1,111 @@
1
+ from django.contrib import admin
2
+ from django.utils.html import format_html
3
+ from django.utils.translation import gettext_lazy as _
4
+
5
+ try:
6
+ from django.conf import settings
7
+ if 'unfold' not in settings.INSTALLED_APPS:
8
+ raise ImportError("Unfold not in INSTALLED_APPS")
9
+
10
+ from unfold.admin import ModelAdmin, TabularInline, StackedInline
11
+ from unfold.contrib.filters.admin import RelatedDropdownFilter, ChoicesDropdownFilter
12
+ from unfold.decorators import display
13
+ UNFOLD_INSTALLED = True
14
+ except (ImportError, RuntimeError):
15
+ from django.contrib.admin import ModelAdmin, TabularInline, StackedInline
16
+ RelatedDropdownFilter = admin.RelatedFieldListFilter
17
+ ChoicesDropdownFilter = admin.ChoicesFieldListFilter
18
+ UNFOLD_INSTALLED = False
19
+
20
+ def display(description=None, header=False, label=False, **kwargs):
21
+ def decorator(func):
22
+ if description:
23
+ func.short_description = description
24
+ return func
25
+ return decorator
26
+
27
+ from snapadmin.models import APIToken
28
+ from snapadmin.widgets import SmartModelSelectorWidget
29
+
30
+
31
+ class SnapTabularInline(TabularInline):
32
+ """
33
+ Standard inline class for SnapAdmin. Fallback to Django admin if Unfold is missing.
34
+ """
35
+ extra = 1
36
+
37
+
38
+ class SnapStackedInline(StackedInline):
39
+ """
40
+ Standard stacked inline class for SnapAdmin. Fallback to Django admin if Unfold is missing.
41
+ """
42
+ extra = 1
43
+
44
+
45
+ class APITokenAdmin(ModelAdmin):
46
+ """
47
+ Admin interface for managing API tokens using Unfold.
48
+ """
49
+
50
+ list_display = [
51
+ "token_name",
52
+ "user",
53
+ "masked_key",
54
+ "expiration_date",
55
+ "is_active",
56
+ "status_badge",
57
+ "last_used_at",
58
+ "created_at",
59
+ ]
60
+ list_filter = [
61
+ ("is_active", ChoicesDropdownFilter),
62
+ ("user", RelatedDropdownFilter),
63
+ ]
64
+ search_fields = ["token_name", "user__username"]
65
+ readonly_fields = ["token_key", "created_at", "last_used_at"]
66
+ ordering = ["-created_at"]
67
+
68
+ warn_unsaved_form = True
69
+ list_filter_submit = True
70
+
71
+ fieldsets = [
72
+ (None, {
73
+ "fields": ["token_name", "user", "token_key"],
74
+ }),
75
+ (_("Access Control"), {
76
+ "fields": ["is_active", "expiration_date", "allowed_models"],
77
+ }),
78
+ (_("Audit"), {
79
+ "fields": ["created_at", "last_used_at"],
80
+ "classes": ["collapse"],
81
+ }),
82
+ ]
83
+
84
+ def formfield_for_dbfield(self, db_field, request, **kwargs):
85
+ if db_field.name == "allowed_models":
86
+ kwargs["widget"] = SmartModelSelectorWidget()
87
+ return super().formfield_for_dbfield(db_field, request, **kwargs)
88
+
89
+ @display(description=_("Token Key"), header=True)
90
+ def masked_key(self, obj: APIToken):
91
+ """Show only the first 8 characters of the token key."""
92
+ val = f"{obj.token_key[:8]}••••••••"
93
+ if UNFOLD_INSTALLED:
94
+ return [val, None, None]
95
+ return val
96
+
97
+ @display(description=_("Status"), label=True)
98
+ def status_badge(self, obj: APIToken):
99
+ """Render a coloured pill badge reflecting the token's current state."""
100
+ if not obj.is_active:
101
+ res = (_("Disabled"), "danger")
102
+ elif obj.is_expired:
103
+ res = (_("Expired"), "warning")
104
+ else:
105
+ res = (_("Active"), "success")
106
+
107
+ if UNFOLD_INSTALLED:
108
+ return res
109
+
110
+ # Fallback for standard admin: just return the label
111
+ return res[0]
File without changes
@@ -0,0 +1,81 @@
1
+ """
2
+ snapadmin/api/authentication.py
3
+
4
+ Custom DRF authentication backend for SnapAdmin API Tokens.
5
+ """
6
+
7
+ import logging
8
+
9
+ from django.contrib.auth.models import User
10
+ from rest_framework import authentication, exceptions
11
+
12
+ from snapadmin.models import APIToken
13
+
14
+ logger = logging.getLogger("snapadmin.api.auth")
15
+
16
+
17
+ class APITokenAuthentication(authentication.BaseAuthentication):
18
+ keyword = "Token"
19
+
20
+ def authenticate(self, request):
21
+ auth_header = authentication.get_authorization_header(request).split()
22
+
23
+ if not auth_header or auth_header[0].lower() != b"token":
24
+ return None
25
+
26
+ if len(auth_header) == 1:
27
+ raise exceptions.AuthenticationFailed("Invalid token header: no token key provided.")
28
+ if len(auth_header) > 2:
29
+ raise exceptions.AuthenticationFailed("Invalid token header: spaces are not allowed in token keys.")
30
+
31
+ try:
32
+ token_key = auth_header[1].decode("utf-8")
33
+ except UnicodeDecodeError:
34
+ raise exceptions.AuthenticationFailed("Invalid token header: token key contained invalid characters.")
35
+
36
+ return self._validate_token(token_key)
37
+
38
+ def _validate_token(self, token_key: str):
39
+ try:
40
+ token = (
41
+ APIToken.objects
42
+ .select_related("user")
43
+ .get(token_key=token_key)
44
+ )
45
+ except APIToken.DoesNotExist:
46
+ raise exceptions.AuthenticationFailed("Invalid token.")
47
+
48
+ if not token.is_active:
49
+ raise exceptions.AuthenticationFailed("Token has been disabled.")
50
+
51
+ if token.is_expired:
52
+ raise exceptions.AuthenticationFailed("Token has expired.")
53
+
54
+ if not token.user.is_active:
55
+ raise exceptions.AuthenticationFailed("User account is disabled.")
56
+
57
+ token.touch()
58
+
59
+ logger.debug(
60
+ "api_token_authenticated",
61
+ extra={"token_name": token.token_name, "user": token.user.username},
62
+ )
63
+
64
+ return (token.user, token)
65
+
66
+ def authenticate_header(self, request):
67
+ return self.keyword
68
+
69
+
70
+ def token_has_permission(
71
+ token: APIToken,
72
+ user: User,
73
+ app_label: str,
74
+ model_name: str,
75
+ action: str,
76
+ ) -> bool:
77
+ if not token.can_access_model(app_label, model_name):
78
+ return False
79
+
80
+ perm_codename = f"{app_label}.{action}_{model_name.lower()}"
81
+ return user.has_perm(perm_codename)
@@ -0,0 +1,52 @@
1
+
2
+ import graphene
3
+ from graphene_django import DjangoObjectType
4
+ from django.apps import apps
5
+ from snapadmin.models import SnapModel, EsStorageMode
6
+
7
+ def get_dynamic_graphql_schema():
8
+ class Query(graphene.ObjectType):
9
+ pass
10
+
11
+ for model in apps.get_models():
12
+ if issubclass(model, SnapModel) and model is not SnapModel:
13
+ try:
14
+ type_name = f"{model._meta.app_label.capitalize()}{model.__name__}Type"
15
+
16
+ # Create the DjangoObjectType dynamically
17
+ # We use a closure for model_class to avoid the late binding issue in loops
18
+ meta_attr = type('Meta', (), {'model': model, 'fields': "__all__"})
19
+ object_type = type(type_name, (DjangoObjectType,), {'Meta': meta_attr})
20
+
21
+ # Add fields to Query
22
+ field_name = f"{model._meta.app_label}_{model.__name__.lower()}"
23
+ list_field_name = f"all_{model._meta.app_label}_{model.__name__.lower()}s"
24
+
25
+ # Use factories for resolvers to correctly bind the model class
26
+ def make_single_resolver(m):
27
+ def resolve_single(self, info, id):
28
+ if getattr(m, 'es_storage_mode', None) == EsStorageMode.ES_ONLY:
29
+ # For ES_ONLY, try to find in ES
30
+ return m.objects.get(pk=id)
31
+ return m.objects.get(pk=id)
32
+ return resolve_single
33
+
34
+ def make_list_resolver(m):
35
+ def resolve_list(self, info):
36
+ # If DUAL or ES_ONLY, we could potentially use snap_search here
37
+ # But objects.all() for SnapModel is already ES-aware if ES_ONLY
38
+ return m.objects.all()
39
+ return resolve_list
40
+
41
+ setattr(Query, field_name, graphene.Field(object_type, id=graphene.ID(required=True)))
42
+ setattr(Query, f"resolve_{field_name}", make_single_resolver(model))
43
+
44
+ setattr(Query, list_field_name, graphene.List(object_type))
45
+ setattr(Query, f"resolve_{list_field_name}", make_list_resolver(model))
46
+ except Exception:
47
+ # Skip models that can't be introspected (e.g. no DB table for non-managed)
48
+ continue
49
+
50
+ return graphene.Schema(query=Query)
51
+
52
+ schema = get_dynamic_graphql_schema()