django-admin-mobile 0.1.0__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 (75) hide show
  1. django_admin_mobile-0.1.0/.gitignore +21 -0
  2. django_admin_mobile-0.1.0/CHANGELOG.md +10 -0
  3. django_admin_mobile-0.1.0/CONTRIBUTING.md +36 -0
  4. django_admin_mobile-0.1.0/LICENSE +21 -0
  5. django_admin_mobile-0.1.0/PKG-INFO +188 -0
  6. django_admin_mobile-0.1.0/README.md +148 -0
  7. django_admin_mobile-0.1.0/docs/screenshots/admindocs-375.png +0 -0
  8. django_admin_mobile-0.1.0/docs/screenshots/admindocs-model-375.png +0 -0
  9. django_admin_mobile-0.1.0/docs/screenshots/app-index-375.png +0 -0
  10. django_admin_mobile-0.1.0/docs/screenshots/calendar-375.png +0 -0
  11. django_admin_mobile-0.1.0/docs/screenshots/change-form-375.png +0 -0
  12. django_admin_mobile-0.1.0/docs/screenshots/changelist-375-dark.png +0 -0
  13. django_admin_mobile-0.1.0/docs/screenshots/changelist-375.png +0 -0
  14. django_admin_mobile-0.1.0/docs/screenshots/changelist-editable-375.png +0 -0
  15. django_admin_mobile-0.1.0/docs/screenshots/clock-375.png +0 -0
  16. django_admin_mobile-0.1.0/docs/screenshots/delete-confirm-375.png +0 -0
  17. django_admin_mobile-0.1.0/docs/screenshots/edition-375.png +0 -0
  18. django_admin_mobile-0.1.0/docs/screenshots/filters-375.png +0 -0
  19. django_admin_mobile-0.1.0/docs/screenshots/history-375.png +0 -0
  20. django_admin_mobile-0.1.0/docs/screenshots/index-375.png +0 -0
  21. django_admin_mobile-0.1.0/docs/screenshots/inline-errors-375.png +0 -0
  22. django_admin_mobile-0.1.0/docs/screenshots/logged-out-375.png +0 -0
  23. django_admin_mobile-0.1.0/docs/screenshots/login-375.png +0 -0
  24. django_admin_mobile-0.1.0/docs/screenshots/message-375.png +0 -0
  25. django_admin_mobile-0.1.0/docs/screenshots/nav-sidebar-375.png +0 -0
  26. django_admin_mobile-0.1.0/docs/screenshots/password-change-375.png +0 -0
  27. django_admin_mobile-0.1.0/docs/screenshots/popup-375.png +0 -0
  28. django_admin_mobile-0.1.0/docs/screenshots/rtl-changelist-375.png +0 -0
  29. django_admin_mobile-0.1.0/example/example/__init__.py +0 -0
  30. django_admin_mobile-0.1.0/example/example/settings.py +75 -0
  31. django_admin_mobile-0.1.0/example/example/urls.py +7 -0
  32. django_admin_mobile-0.1.0/example/example/wsgi.py +7 -0
  33. django_admin_mobile-0.1.0/example/library/__init__.py +0 -0
  34. django_admin_mobile-0.1.0/example/library/admin.py +137 -0
  35. django_admin_mobile-0.1.0/example/library/apps.py +13 -0
  36. django_admin_mobile-0.1.0/example/library/demo.py +143 -0
  37. django_admin_mobile-0.1.0/example/library/management/__init__.py +0 -0
  38. django_admin_mobile-0.1.0/example/library/management/commands/__init__.py +0 -0
  39. django_admin_mobile-0.1.0/example/library/management/commands/seed_demo.py +14 -0
  40. django_admin_mobile-0.1.0/example/library/migrations/0001_initial.py +111 -0
  41. django_admin_mobile-0.1.0/example/library/migrations/0002_admin_surfaces.py +234 -0
  42. django_admin_mobile-0.1.0/example/library/migrations/0003_shelf_plural.py +17 -0
  43. django_admin_mobile-0.1.0/example/library/migrations/__init__.py +0 -0
  44. django_admin_mobile-0.1.0/example/library/models.py +189 -0
  45. django_admin_mobile-0.1.0/example/manage.py +19 -0
  46. django_admin_mobile-0.1.0/pyproject.toml +91 -0
  47. django_admin_mobile-0.1.0/src/admin_mobile/__init__.py +3 -0
  48. django_admin_mobile-0.1.0/src/admin_mobile/apps.py +16 -0
  49. django_admin_mobile-0.1.0/src/admin_mobile/checks.py +72 -0
  50. django_admin_mobile-0.1.0/src/admin_mobile/conf.py +60 -0
  51. django_admin_mobile-0.1.0/src/admin_mobile/mixins.py +29 -0
  52. django_admin_mobile-0.1.0/src/admin_mobile/overrides.py +144 -0
  53. django_admin_mobile-0.1.0/src/admin_mobile/py.typed +0 -0
  54. django_admin_mobile-0.1.0/src/admin_mobile/static/admin_mobile/css/admin_mobile.css +885 -0
  55. django_admin_mobile-0.1.0/src/admin_mobile/static/admin_mobile/js/admin_mobile.js +263 -0
  56. django_admin_mobile-0.1.0/src/admin_mobile/templates/admin/base.html +13 -0
  57. django_admin_mobile-0.1.0/src/admin_mobile/templates/admin_mobile/widgets/split_datetime.html +13 -0
  58. django_admin_mobile-0.1.0/src/admin_mobile/templatetags/__init__.py +0 -0
  59. django_admin_mobile-0.1.0/src/admin_mobile/templatetags/admin_mobile.py +101 -0
  60. django_admin_mobile-0.1.0/src/admin_mobile/widgets.py +118 -0
  61. django_admin_mobile-0.1.0/tests/__init__.py +0 -0
  62. django_admin_mobile-0.1.0/tests/formparse.py +60 -0
  63. django_admin_mobile-0.1.0/tests/settings.py +70 -0
  64. django_admin_mobile-0.1.0/tests/test_admin_save.py +92 -0
  65. django_admin_mobile-0.1.0/tests/test_checks.py +33 -0
  66. django_admin_mobile-0.1.0/tests/test_config.py +43 -0
  67. django_admin_mobile-0.1.0/tests/test_demo.py +30 -0
  68. django_admin_mobile-0.1.0/tests/test_formparse.py +26 -0
  69. django_admin_mobile-0.1.0/tests/test_mixin.py +153 -0
  70. django_admin_mobile-0.1.0/tests/test_playwright.py +369 -0
  71. django_admin_mobile-0.1.0/tests/test_static_contract.py +78 -0
  72. django_admin_mobile-0.1.0/tests/test_templates.py +95 -0
  73. django_admin_mobile-0.1.0/tests/test_widgets.py +112 -0
  74. django_admin_mobile-0.1.0/tests/urls.py +9 -0
  75. django_admin_mobile-0.1.0/tox.ini +16 -0
@@ -0,0 +1,21 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ .tox/
7
+ .mypy_cache/
8
+ dist/
9
+ build/
10
+ *.egg-info/
11
+ example/db.sqlite3
12
+ example/media/
13
+ tests/media/
14
+ example/*.sqlite3
15
+ tests/.test.sqlite3
16
+ tests/test_*.sqlite3
17
+ htmlcov/
18
+ .coverage
19
+ playwright-report/
20
+ test-results/
21
+ *.log
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - Phone layout for the native Django admin: changelist cards, stacked tabular inlines, sticky submit row, 44px targets, 16px inputs.
6
+ - Collapsible `#changelist-filter` toggle that does not depend on third-party filter packages.
7
+ - `NativeDateInput`, `NativeTimeInput`, `NativeSplitDateTimeWidget`, and `MobileAdminMixin`.
8
+ - Opt-in `ADMIN_MOBILE["USE_NATIVE_DATETIME"]` for every `ModelAdmin` and inline.
9
+ - Light and dark surfaces use Django admin CSS variables only.
10
+ - Phone layout for the rest of the built-in admin: login, password change, logout, index, nav sidebar, header, breadcrumbs, date hierarchy, pagination, list editing, select-all, sort links, facets and the empty filter, related-object widgets, raw id lookup, autocomplete, filter horizontal/vertical, radios, file clear, collapsed fieldsets, readonly fields, errors, stacked inlines, history, delete confirmation, popups, messages, calendar/clock, admindocs, and RTL.
@@ -0,0 +1,36 @@
1
+ # Contributing
2
+
3
+ `django-admin-mobile` keeps the stock Django admin and only adds a stylesheet, a small script, and optional native date widgets.
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ uv sync --extra dev
9
+ uv run python example/manage.py migrate
10
+ ```
11
+
12
+ ## Checks
13
+
14
+ ```bash
15
+ uv run ruff check .
16
+ uv run ruff format --check .
17
+ uv run pytest -m "not playwright"
18
+ uv run pytest -m playwright
19
+ uv run tox
20
+ ```
21
+
22
+ Playwright needs Chromium once: `uv run playwright install chromium`.
23
+
24
+ The screenshot test writes `docs/screenshots/*.png`. Update those files in the same change when the phone layout changes.
25
+
26
+ ## Compatibility
27
+
28
+ The tox and GitHub Actions matrices cover Django 4.2, 5.2, and 6.0 on Python 3.10+ (Django 6 requires 3.12+). Do not add a build step or a CDN. Static files ship as plain CSS and JavaScript.
29
+
30
+ ## Project layout
31
+
32
+ - `src/admin_mobile/` is the installable app.
33
+ - `example/` is a local admin with books, chapters, dates, and filters.
34
+ - `tests/` covers settings, widgets, template injection, and the phone screenshots.
35
+
36
+ Feature-Sliced Design is for product frontends. This repository stays a single Django app so `INSTALLED_APPS` can load it.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Daniil Shamraev
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,188 @@
1
+ Metadata-Version: 2.5
2
+ Name: django-admin-mobile
3
+ Version: 0.1.0
4
+ Summary: Make the native Django admin usable on phones without replacing its look.
5
+ Project-URL: Homepage, https://github.com/daniilshamraev/django-admin-mobile
6
+ Project-URL: Repository, https://github.com/daniilshamraev/django-admin-mobile
7
+ Project-URL: Issues, https://github.com/daniilshamraev/django-admin-mobile/issues
8
+ Author: Daniil Shamraev
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: admin,django,mobile,responsive
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Web Environment
14
+ Classifier: Framework :: Django
15
+ Classifier: Framework :: Django :: 4.2
16
+ Classifier: Framework :: Django :: 5.2
17
+ Classifier: Framework :: Django :: 6.0
18
+ Classifier: Framework :: Django :: 6.1
19
+ Classifier: Intended Audience :: Developers
20
+ Classifier: License :: OSI Approved :: MIT License
21
+ Classifier: Operating System :: OS Independent
22
+ Classifier: Programming Language :: JavaScript
23
+ Classifier: Programming Language :: Python :: 3
24
+ Classifier: Programming Language :: Python :: 3.10
25
+ Classifier: Programming Language :: Python :: 3.11
26
+ Classifier: Programming Language :: Python :: 3.12
27
+ Classifier: Programming Language :: Python :: 3.13
28
+ Classifier: Topic :: Internet :: WWW/HTTP
29
+ Requires-Python: >=3.10
30
+ Requires-Dist: django>=4.2
31
+ Provides-Extra: dev
32
+ Requires-Dist: docutils>=0.20; extra == 'dev'
33
+ Requires-Dist: playwright>=1.49; extra == 'dev'
34
+ Requires-Dist: pytest-django>=4.9; extra == 'dev'
35
+ Requires-Dist: pytest-playwright>=0.6; extra == 'dev'
36
+ Requires-Dist: pytest>=8.3; extra == 'dev'
37
+ Requires-Dist: ruff>=0.8; extra == 'dev'
38
+ Requires-Dist: tox>=4.23; extra == 'dev'
39
+ Description-Content-Type: text/markdown
40
+
41
+ # django-admin-mobile
42
+
43
+ Phone layout for the **native** Django admin. It is not a theme: colors, typography, and chrome stay on Django's own CSS variables, including `html[data-theme]` and `prefers-color-scheme`.
44
+
45
+ Targets Django 4.2 LTS, 5.x, and 6.x on Python 3.10+.
46
+
47
+ ![Changelist at 375px](docs/screenshots/changelist-375.png)
48
+
49
+ ![Change form at 375px](docs/screenshots/change-form-375.png)
50
+
51
+ ## Install
52
+
53
+ ```bash
54
+ pip install django-admin-mobile
55
+ ```
56
+
57
+ Put the app **before** `django.contrib.admin`:
58
+
59
+ ```python
60
+ INSTALLED_APPS = [
61
+ "admin_mobile",
62
+ "django.contrib.admin",
63
+ # ...
64
+ ]
65
+ ```
66
+
67
+ That is enough. `admin_mobile` overrides `admin/base.html`, calls `{{ block.super }}`, and appends its CSS and JS inside `extrastyle` and `extrahead`. Projects that only override `admin/base_site.html` keep working, because that template still extends `admin/base.html`.
68
+
69
+ No CDN and no build step. The package ships plain files under `admin_mobile/static/`.
70
+
71
+ ## If you already override `admin/base.html`
72
+
73
+ Django will use your template and skip this app's override. Load the tag yourself, and still call `{{ block.super }}`:
74
+
75
+ ```django
76
+ {% extends "admin/base.html" %}
77
+ {% load admin_mobile %}
78
+
79
+ {% block extrastyle %}
80
+ {{ block.super }}
81
+ {% admin_mobile_assets "style" %}
82
+ {% endblock %}
83
+
84
+ {% block extrahead %}
85
+ {{ block.super }}
86
+ {% admin_mobile_assets "script" %}
87
+ {% endblock %}
88
+ ```
89
+
90
+ `{% admin_mobile_assets %}` emits both. If `admin_mobile` is listed after `django.contrib.admin`, system check `admin_mobile.W001` warns you.
91
+
92
+ ## Settings
93
+
94
+ All keys are optional booleans.
95
+
96
+ ```python
97
+ ADMIN_MOBILE = {
98
+ "ENABLED": True,
99
+ "CHANGELIST_CARDS": True,
100
+ "TABULAR_INLINE_STACK": True,
101
+ "STICKY_SUBMIT": True,
102
+ "TOUCH_TARGETS": True,
103
+ "COLLAPSIBLE_FILTERS": True,
104
+ "USE_NATIVE_DATETIME": False,
105
+ }
106
+ ```
107
+
108
+ | Key | Default | Effect |
109
+ | --- | --- | --- |
110
+ | `ENABLED` | `True` | Turn the stylesheet and script off. |
111
+ | `CHANGELIST_CARDS` | `True` | At `≤600px`, changelist rows become cards. `data-label` is copied from the column header. From 601px to 1024px the table scrolls sideways instead. Checkboxes and links stay in the row. |
112
+ | `TABULAR_INLINE_STACK` | `True` | Tabular inlines stack into labeled fields. The empty template row stays hidden, so Django's `inlines.js` can still add and delete rows. |
113
+ | `STICKY_SUBMIT` | `True` | The last `.submit-row` sticks to the bottom of the viewport. The login page is left alone. |
114
+ | `TOUCH_TARGETS` | `True` | Targets at least 44px, inputs at 16px (stops iOS from zooming), larger checkboxes. |
115
+ | `COLLAPSIBLE_FILTERS` | `True` | `#changelist-filter` collapses behind a button. Django's filters, django-admin-rangefilter, and django-more-admin-filters render inside that node; this package does not import them. |
116
+ | `USE_NATIVE_DATETIME` | `False` | Opt in to native date/time widgets on **every** `ModelAdmin` and inline. A custom `formfield_overrides` widget is kept. |
117
+
118
+ Unknown keys and non-booleans raise `ImproperlyConfigured`.
119
+
120
+ ## Native date and time widgets
121
+
122
+ Per model, without the global flag:
123
+
124
+ ```python
125
+ from django.contrib import admin
126
+ from admin_mobile.mixins import MobileAdminMixin
127
+ from library.models import Book
128
+
129
+
130
+ @admin.register(Book)
131
+ class BookAdmin(MobileAdminMixin, admin.ModelAdmin):
132
+ pass
133
+ ```
134
+
135
+ `MobileAdminMixin` must come before `admin.ModelAdmin` or `admin.TabularInline`. It sets `formfield_overrides` to:
136
+
137
+ - `admin_mobile.widgets.NativeDateInput` (`<input type="date">`)
138
+ - `admin_mobile.widgets.NativeTimeInput` (`<input type="time" step="1">`)
139
+ - `admin_mobile.widgets.NativeSplitDateTimeWidget`
140
+
141
+ Values stay on `SplitDateTimeField` / `DateField` / `TimeField`, so Django still validates them and still applies the current time zone. The widget renders the active zone (not the raw UTC instant) and prints that zone name under the inputs. ISO formats are required (`YYYY-MM-DD`, `HH:MM` or `HH:MM:SS`); a locale string such as `15.06.2024` is rejected. Microseconds are dropped, matching Django's admin split widget.
142
+
143
+ These widgets do **not** load `DateTimeShortcuts`, so you do not get a second calendar on top of the native picker.
144
+
145
+ ## Example
146
+
147
+ ```bash
148
+ uv sync --extra dev
149
+ uv run python example/manage.py migrate
150
+ uv run python example/manage.py runserver 127.0.0.1:8741
151
+ ```
152
+
153
+ With `DEBUG` and `ADMIN_MOBILE_DEMO` (both set in `example/example/settings.py`) migrate creates a local superuser `admin` / `admin` and a few books. That password exists only for this demo. Do not set `ADMIN_MOBILE_DEMO` on a shared host; the seed refuses to run when `DEBUG` is off.
154
+
155
+ Open http://127.0.0.1:8741/admin/ and narrow the window to 375px, or use device mode.
156
+
157
+ ![Filters open](docs/screenshots/filters-375.png)
158
+
159
+ ![Changelist, dark](docs/screenshots/changelist-375-dark.png)
160
+
161
+ The same stylesheet also covers the rest of the built-in admin at `≤600px`: login, password change, and logout; the index and app list; the nav sidebar and its filter; header, user tools, and breadcrumbs; changelist search, actions, select-all, sortable headers, date hierarchy, pagination, result counts, and `list_editable`; every built-in filter (choices, related, date, empty, and facets on Django 5+); change-form fieldsets (including collapse), help, errors, readonly fields, related-object icons, raw id lookup, autocomplete, filter horizontal/vertical, radios, and file Currently/Change/Clear; stacked and tabular inlines; object history; delete confirmation; related-object popups; messages; the stock calendar and clock when native widgets are off; admindocs; and RTL (`dir="rtl"`, logical alignment).
162
+
163
+ Further 375px captures live in `docs/screenshots/` (`login`, `index`, `nav-sidebar`, `edition`, `calendar`, `clock`, `history`, `delete-confirm`, `popup`, `admindocs`, `rtl-changelist`, and others).
164
+
165
+ `Appointment` in the example keeps Django's calendar and clock. `Book` and `Edition` use `MobileAdminMixin`. `USE_NATIVE_DATETIME` stays off in the example so both modes are visible.
166
+
167
+ ## Tests
168
+
169
+ ```bash
170
+ uv run ruff check .
171
+ uv run pytest -m "not playwright"
172
+ uv run playwright install chromium
173
+ uv run pytest -m playwright
174
+ uv run tox
175
+ ```
176
+
177
+ `tox.ini` runs Django 4.2, 5.2, and 6.0. GitHub Actions adds Python 3.10–3.13 where each Django release allows it.
178
+
179
+ ## Publishing
180
+
181
+ The package is not on PyPI yet.
182
+
183
+ 1. Create a GitHub repository and push this project.
184
+ 2. Add `[project.urls]` in `pyproject.toml` (`Homepage`, `Repository`, `Changelog`).
185
+ 3. Build: `uv build` (hatchling, src layout, static files and templates included).
186
+ 4. Publish: `uv publish` with a PyPI API token. The name `django-admin-mobile` has to be free on PyPI; the import name is `admin_mobile`.
187
+
188
+ Author: Daniil Shamraev. License: MIT.
@@ -0,0 +1,148 @@
1
+ # django-admin-mobile
2
+
3
+ Phone layout for the **native** Django admin. It is not a theme: colors, typography, and chrome stay on Django's own CSS variables, including `html[data-theme]` and `prefers-color-scheme`.
4
+
5
+ Targets Django 4.2 LTS, 5.x, and 6.x on Python 3.10+.
6
+
7
+ ![Changelist at 375px](docs/screenshots/changelist-375.png)
8
+
9
+ ![Change form at 375px](docs/screenshots/change-form-375.png)
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install django-admin-mobile
15
+ ```
16
+
17
+ Put the app **before** `django.contrib.admin`:
18
+
19
+ ```python
20
+ INSTALLED_APPS = [
21
+ "admin_mobile",
22
+ "django.contrib.admin",
23
+ # ...
24
+ ]
25
+ ```
26
+
27
+ That is enough. `admin_mobile` overrides `admin/base.html`, calls `{{ block.super }}`, and appends its CSS and JS inside `extrastyle` and `extrahead`. Projects that only override `admin/base_site.html` keep working, because that template still extends `admin/base.html`.
28
+
29
+ No CDN and no build step. The package ships plain files under `admin_mobile/static/`.
30
+
31
+ ## If you already override `admin/base.html`
32
+
33
+ Django will use your template and skip this app's override. Load the tag yourself, and still call `{{ block.super }}`:
34
+
35
+ ```django
36
+ {% extends "admin/base.html" %}
37
+ {% load admin_mobile %}
38
+
39
+ {% block extrastyle %}
40
+ {{ block.super }}
41
+ {% admin_mobile_assets "style" %}
42
+ {% endblock %}
43
+
44
+ {% block extrahead %}
45
+ {{ block.super }}
46
+ {% admin_mobile_assets "script" %}
47
+ {% endblock %}
48
+ ```
49
+
50
+ `{% admin_mobile_assets %}` emits both. If `admin_mobile` is listed after `django.contrib.admin`, system check `admin_mobile.W001` warns you.
51
+
52
+ ## Settings
53
+
54
+ All keys are optional booleans.
55
+
56
+ ```python
57
+ ADMIN_MOBILE = {
58
+ "ENABLED": True,
59
+ "CHANGELIST_CARDS": True,
60
+ "TABULAR_INLINE_STACK": True,
61
+ "STICKY_SUBMIT": True,
62
+ "TOUCH_TARGETS": True,
63
+ "COLLAPSIBLE_FILTERS": True,
64
+ "USE_NATIVE_DATETIME": False,
65
+ }
66
+ ```
67
+
68
+ | Key | Default | Effect |
69
+ | --- | --- | --- |
70
+ | `ENABLED` | `True` | Turn the stylesheet and script off. |
71
+ | `CHANGELIST_CARDS` | `True` | At `≤600px`, changelist rows become cards. `data-label` is copied from the column header. From 601px to 1024px the table scrolls sideways instead. Checkboxes and links stay in the row. |
72
+ | `TABULAR_INLINE_STACK` | `True` | Tabular inlines stack into labeled fields. The empty template row stays hidden, so Django's `inlines.js` can still add and delete rows. |
73
+ | `STICKY_SUBMIT` | `True` | The last `.submit-row` sticks to the bottom of the viewport. The login page is left alone. |
74
+ | `TOUCH_TARGETS` | `True` | Targets at least 44px, inputs at 16px (stops iOS from zooming), larger checkboxes. |
75
+ | `COLLAPSIBLE_FILTERS` | `True` | `#changelist-filter` collapses behind a button. Django's filters, django-admin-rangefilter, and django-more-admin-filters render inside that node; this package does not import them. |
76
+ | `USE_NATIVE_DATETIME` | `False` | Opt in to native date/time widgets on **every** `ModelAdmin` and inline. A custom `formfield_overrides` widget is kept. |
77
+
78
+ Unknown keys and non-booleans raise `ImproperlyConfigured`.
79
+
80
+ ## Native date and time widgets
81
+
82
+ Per model, without the global flag:
83
+
84
+ ```python
85
+ from django.contrib import admin
86
+ from admin_mobile.mixins import MobileAdminMixin
87
+ from library.models import Book
88
+
89
+
90
+ @admin.register(Book)
91
+ class BookAdmin(MobileAdminMixin, admin.ModelAdmin):
92
+ pass
93
+ ```
94
+
95
+ `MobileAdminMixin` must come before `admin.ModelAdmin` or `admin.TabularInline`. It sets `formfield_overrides` to:
96
+
97
+ - `admin_mobile.widgets.NativeDateInput` (`<input type="date">`)
98
+ - `admin_mobile.widgets.NativeTimeInput` (`<input type="time" step="1">`)
99
+ - `admin_mobile.widgets.NativeSplitDateTimeWidget`
100
+
101
+ Values stay on `SplitDateTimeField` / `DateField` / `TimeField`, so Django still validates them and still applies the current time zone. The widget renders the active zone (not the raw UTC instant) and prints that zone name under the inputs. ISO formats are required (`YYYY-MM-DD`, `HH:MM` or `HH:MM:SS`); a locale string such as `15.06.2024` is rejected. Microseconds are dropped, matching Django's admin split widget.
102
+
103
+ These widgets do **not** load `DateTimeShortcuts`, so you do not get a second calendar on top of the native picker.
104
+
105
+ ## Example
106
+
107
+ ```bash
108
+ uv sync --extra dev
109
+ uv run python example/manage.py migrate
110
+ uv run python example/manage.py runserver 127.0.0.1:8741
111
+ ```
112
+
113
+ With `DEBUG` and `ADMIN_MOBILE_DEMO` (both set in `example/example/settings.py`) migrate creates a local superuser `admin` / `admin` and a few books. That password exists only for this demo. Do not set `ADMIN_MOBILE_DEMO` on a shared host; the seed refuses to run when `DEBUG` is off.
114
+
115
+ Open http://127.0.0.1:8741/admin/ and narrow the window to 375px, or use device mode.
116
+
117
+ ![Filters open](docs/screenshots/filters-375.png)
118
+
119
+ ![Changelist, dark](docs/screenshots/changelist-375-dark.png)
120
+
121
+ The same stylesheet also covers the rest of the built-in admin at `≤600px`: login, password change, and logout; the index and app list; the nav sidebar and its filter; header, user tools, and breadcrumbs; changelist search, actions, select-all, sortable headers, date hierarchy, pagination, result counts, and `list_editable`; every built-in filter (choices, related, date, empty, and facets on Django 5+); change-form fieldsets (including collapse), help, errors, readonly fields, related-object icons, raw id lookup, autocomplete, filter horizontal/vertical, radios, and file Currently/Change/Clear; stacked and tabular inlines; object history; delete confirmation; related-object popups; messages; the stock calendar and clock when native widgets are off; admindocs; and RTL (`dir="rtl"`, logical alignment).
122
+
123
+ Further 375px captures live in `docs/screenshots/` (`login`, `index`, `nav-sidebar`, `edition`, `calendar`, `clock`, `history`, `delete-confirm`, `popup`, `admindocs`, `rtl-changelist`, and others).
124
+
125
+ `Appointment` in the example keeps Django's calendar and clock. `Book` and `Edition` use `MobileAdminMixin`. `USE_NATIVE_DATETIME` stays off in the example so both modes are visible.
126
+
127
+ ## Tests
128
+
129
+ ```bash
130
+ uv run ruff check .
131
+ uv run pytest -m "not playwright"
132
+ uv run playwright install chromium
133
+ uv run pytest -m playwright
134
+ uv run tox
135
+ ```
136
+
137
+ `tox.ini` runs Django 4.2, 5.2, and 6.0. GitHub Actions adds Python 3.10–3.13 where each Django release allows it.
138
+
139
+ ## Publishing
140
+
141
+ The package is not on PyPI yet.
142
+
143
+ 1. Create a GitHub repository and push this project.
144
+ 2. Add `[project.urls]` in `pyproject.toml` (`Homepage`, `Repository`, `Changelog`).
145
+ 3. Build: `uv build` (hatchling, src layout, static files and templates included).
146
+ 4. Publish: `uv publish` with a PyPI API token. The name `django-admin-mobile` has to be free on PyPI; the import name is `admin_mobile`.
147
+
148
+ Author: Daniil Shamraev. License: MIT.
File without changes
@@ -0,0 +1,75 @@
1
+ """Local demo settings. Do not deploy this module."""
2
+
3
+ from pathlib import Path
4
+
5
+ BASE_DIR = Path(__file__).resolve().parent.parent
6
+
7
+ SECRET_KEY = "example-only-not-a-secret"
8
+ DEBUG = True
9
+ ALLOWED_HOSTS = ["127.0.0.1", "localhost"]
10
+ ADMIN_MOBILE_DEMO = True
11
+
12
+ INSTALLED_APPS = [
13
+ "admin_mobile",
14
+ "django.contrib.admin",
15
+ "django.contrib.auth",
16
+ "django.contrib.contenttypes",
17
+ "django.contrib.sessions",
18
+ "django.contrib.messages",
19
+ "django.contrib.staticfiles",
20
+ "django.contrib.admindocs",
21
+ "library",
22
+ ]
23
+
24
+ MIDDLEWARE = [
25
+ "django.middleware.security.SecurityMiddleware",
26
+ "django.contrib.sessions.middleware.SessionMiddleware",
27
+ "django.middleware.common.CommonMiddleware",
28
+ "django.middleware.csrf.CsrfViewMiddleware",
29
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
30
+ "django.contrib.messages.middleware.MessageMiddleware",
31
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
32
+ ]
33
+
34
+ ROOT_URLCONF = "example.urls"
35
+ WSGI_APPLICATION = "example.wsgi.application"
36
+
37
+ TEMPLATES = [
38
+ {
39
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
40
+ "DIRS": [],
41
+ "APP_DIRS": True,
42
+ "OPTIONS": {
43
+ "context_processors": [
44
+ "django.template.context_processors.request",
45
+ "django.contrib.auth.context_processors.auth",
46
+ "django.contrib.messages.context_processors.messages",
47
+ ],
48
+ },
49
+ }
50
+ ]
51
+
52
+ DATABASES = {
53
+ "default": {
54
+ "ENGINE": "django.db.backends.sqlite3",
55
+ "NAME": BASE_DIR / "db.sqlite3",
56
+ }
57
+ }
58
+
59
+ AUTH_PASSWORD_VALIDATORS = []
60
+
61
+ LANGUAGE_CODE = "en-us"
62
+ TIME_ZONE = "Europe/Moscow"
63
+ USE_I18N = True
64
+ USE_TZ = True
65
+
66
+ STATIC_URL = "static/"
67
+ MEDIA_URL = "media/"
68
+ MEDIA_ROOT = BASE_DIR / "media"
69
+ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
70
+
71
+ # Native widgets are opt-in per model via MobileAdminMixin.
72
+ # AppointmentAdmin keeps Django's calendar and clock.
73
+ ADMIN_MOBILE = {
74
+ "USE_NATIVE_DATETIME": False,
75
+ }
@@ -0,0 +1,7 @@
1
+ from django.contrib import admin
2
+ from django.urls import include, path
3
+
4
+ urlpatterns = [
5
+ path("admin/doc/", include("django.contrib.admindocs.urls")),
6
+ path("admin/", admin.site.urls),
7
+ ]
@@ -0,0 +1,7 @@
1
+ import os
2
+
3
+ from django.core.wsgi import get_wsgi_application
4
+
5
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings")
6
+
7
+ application = get_wsgi_application()
File without changes