├── .github ├── actions │ └── test │ │ └── action.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── CHANGELOG ├── LICENSE ├── MANIFEST.in ├── README.rst ├── adminplus ├── __init__.py ├── apps.py ├── models.py ├── sites.py ├── templates │ └── adminplus │ │ ├── base.html │ │ ├── index.html │ │ └── test │ │ └── index.html └── tests.py ├── docs └── custom-admin-views.rst ├── pyproject.toml ├── run.sh ├── setup.cfg ├── test_settings.py └── test_urlconf.py /.github/actions/test/action.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | description: 'runs a test matrix' 3 | inputs: 4 | python-version: 5 | required: true 6 | django-version: 7 | required: true 8 | 9 | runs: 10 | using: "composite" 11 | steps: 12 | - name: Set up Python 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: ${{ inputs.python-version }} 16 | 17 | - name: Install dependencies 18 | shell: sh 19 | run: | 20 | python -m pip install --upgrade pip 21 | if [ "${{ inputs.django-version }}" != 'main' ]; then pip install --pre -q "Django>=${{ inputs.django-version }},<${{ inputs.django-version }}.99"; fi 22 | if [ "${{ inputs.django-version }}" = 'main' ]; then pip install https://github.com/django/django/archive/main.tar.gz; fi 23 | pip install flake8 24 | 25 | - name: Test 26 | shell: sh 27 | run: | 28 | ./run.sh test 29 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [opened, synchronize, reopened, ready_for_review] 9 | schedule: 10 | - cron: '17 7 * * 0' # run weekly on sundays 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | python-version: ['3.8', '3.9', '3.10', '3.11'] 19 | django: ['3.2', '4.1', '4.2', '5.0', 'main'] 20 | exclude: 21 | - python-version: '3.7' 22 | django: 'main' 23 | - python-version: '3.11' 24 | django: '3.2' 25 | - python-version: '3.8' 26 | django: '5.0' 27 | - python-version: '3.9' 28 | django: '5.0' 29 | - python-version: '3.8' 30 | django: 'main' 31 | - python-version: '3.9' 32 | django: 'main' 33 | include: 34 | - python-version: '3.12' 35 | django: '5.0' 36 | 37 | steps: 38 | - uses: actions/checkout@v3 39 | 40 | - uses: ./.github/actions/test 41 | with: 42 | python-version: ${{ matrix.python-version }} 43 | django-version: ${{ matrix.django }} 44 | 45 | lint: 46 | runs-on: ubuntu-latest 47 | 48 | steps: 49 | - uses: actions/checkout@v3 50 | 51 | - uses: actions/setup-python@v4 52 | with: 53 | python-version: '3.11' 54 | 55 | - name: install flake8 56 | run: pip install flake8 57 | 58 | - name: Lint with flake8 59 | run: | 60 | ./run.sh lint 61 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | fail-fast: true 13 | matrix: 14 | python-version: ['3.10', '3.11'] 15 | django: ['3.2', '4.2', '5.0'] 16 | include: 17 | - python-version: '3.12' 18 | django: '5.0' 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | 23 | - uses: ./.github/actions/test 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | django-version: ${{ matrix.django }} 27 | 28 | release: 29 | runs-on: ubuntu-latest 30 | needs: [test] 31 | environment: release 32 | permissions: 33 | id-token: write 34 | steps: 35 | 36 | - uses: actions/checkout@v3 37 | 38 | - name: Set up Python 39 | uses: actions/setup-python@v4 40 | with: 41 | python-version: 3.11 42 | 43 | - name: install dependencies 44 | run: | 45 | python -m pip install --upgrade pip 46 | pip install build twine 47 | 48 | - name: build 49 | run: ./run.sh build 50 | 51 | - name: check 52 | run: ./run.sh check 53 | 54 | - name: release 55 | uses: pypa/gh-action-pypi-publish@release/v1 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | build 4 | dist 5 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | v0.6 5 | ---- 6 | 7 | - Update supported Django and Python versions. 8 | - Fix multiple bugs in urlconfs. 9 | - Update test and build tooling to use GitHub Actions as a Trusted Publisher 10 | for PyPI 11 | 12 | v0.5 13 | ---- 14 | 15 | - Drop support for unsupported Django versions. 16 | - Test on 1.8 and 1.9. 17 | - Remove deprecated patterns() call. 18 | 19 | v0.4 20 | ---- 21 | 22 | - Update supported Django versions. 23 | - Support registering class-based views. 24 | - Allow callables for `visible`. 25 | 26 | v0.3 27 | ---- 28 | 29 | - Prioritize custom views over generic patterns. 30 | - Fix a Django>=1.5 template bug. 31 | 32 | v0.2.1 33 | ------ 34 | 35 | - Fix Django 1.6 support. 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023, James Socol 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of AdminPlus nor the names of its contributors may 15 | be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG 2 | include LICENSE 3 | include README.rst 4 | recursive-include adminplus/templates/adminplus *.html 5 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | Django AdminPlus 3 | ================ 4 | 5 | **AdminPlus** aims to be the smallest possible extension to the excellent 6 | Django admin component that lets you add admin views that are not tied to 7 | models. 8 | 9 | There are packages out there, like `Nexus `_ 10 | and `django-admin-tools `_ that 11 | replace the entire admin. Nexus supports adding completely new "modules" (the 12 | Django model admin is a default module) but there seems to be a lot of boiler 13 | plate code to do it. django-admin-tools does not, as far as I can tell, support 14 | adding custom pages. 15 | 16 | All AdminPlus does is allow you to add simple custom views (well, they can be 17 | as complex as you like!) without mucking about with hijacking URLs, and 18 | providing links to them right in the admin index. 19 | 20 | 21 | .. image:: https://github.com/jsocol/django-adminplus/actions/workflows/ci.yml/badge.svg?branch=main 22 | :target: https://github.com/jsocol/django-adminplus 23 | 24 | 25 | Installing AdminPlus 26 | ==================== 27 | 28 | Install from `PyPI `_ with pip: 29 | 30 | .. code-block:: bash 31 | 32 | pip install django-adminplus 33 | 34 | Or get AdminPlus from `GitHub `_ 35 | with pip: 36 | 37 | .. code-block:: bash 38 | 39 | pip install -e git://github.com/jsocol/django-adminplus#egg=django-adminplus 40 | 41 | And add ``adminplus`` to your installed apps, and replace ``django.contrib.admin`` with ``django.contrib.admin.apps.SimpleAdminConfig``: 42 | 43 | .. code-block:: python 44 | 45 | INSTALLED_APPS = ( 46 | 'django.contrib.admin.apps.SimpleAdminConfig', 47 | # ... 48 | 'adminplus', 49 | # ... 50 | ) 51 | 52 | To use AdminPlus in your Django project, you'll need to replace ``django.contrib.admin.site``, which is an instance of ``django.contrib.admin.sites.AdminSite``. I recommend doing this in ``urls.py`` right before calling ``admin.autodiscover()``: 53 | 54 | .. code-block:: python 55 | 56 | # urls.py 57 | from django.contrib import admin 58 | from adminplus.sites import AdminSitePlus 59 | 60 | admin.site = AdminSitePlus() 61 | admin.autodiscover() 62 | 63 | urlpatterns = [ 64 | # ... 65 | # Include the admin URL conf as normal. 66 | (r'^admin', include(admin.site.urls)), 67 | # ... 68 | ] 69 | 70 | Congratulations! You're now using AdminPlus. 71 | 72 | 73 | Using AdminPlus 74 | =============== 75 | 76 | So now that you've installed AdminPlus, you'll want to use it. AdminPlus is 77 | 100% compatible with the built in admin module, so if you've been using that, 78 | you shouldn't have to change anything. 79 | 80 | AdminPlus offers a new function, ``admin.site.register_view``, to attach arbitrary views to the admin: 81 | 82 | .. code-block:: python 83 | 84 | # someapp/admin.py 85 | # Assuming you've replaced django.contrib.admin.site as above. 86 | from django.contrib import admin 87 | 88 | def my_view(request, *args, **kwargs): 89 | pass 90 | admin.site.register_view('somepath', view=my_view) 91 | 92 | # And of course, this still works: 93 | from someapp.models import MyModel 94 | admin.site.register(MyModel) 95 | 96 | Now ``my_view`` will be accessible at ``admin/somepath`` and there will be a 97 | link to it in the *Custom Views* section of the admin index. 98 | 99 | You can also use ``register_view`` as a decorator: 100 | 101 | .. code-block:: python 102 | 103 | @admin.site.register_view('somepath') 104 | def my_view(request): 105 | pass 106 | 107 | ``register_view`` takes some optional arguments: 108 | 109 | * ``name``: a friendly name for display in the list of custom views. For example: 110 | 111 | .. code-block:: python 112 | 113 | def my_view(request): 114 | """Does something fancy!""" 115 | admin.site.register_view('somepath', 'My Fancy Admin View!', view=my_view) 116 | 117 | * ``urlname``: give a name to the urlpattern so it can be called by 118 | ``redirect()``, ``reverse()``, etc. The view will be added 119 | to the ``admin`` namespace, so a urlname of ``foo`` would be reversed 120 | with ``reverse("admin:foo")``. 121 | * `visible`: a boolean or a callable returning one, that defines if 122 | the custom view is visible in the admin dashboard. 123 | 124 | All registered views are wrapped in ``admin.site.admin_view``. 125 | 126 | .. note:: 127 | 128 | Views with URLs that match auto-discovered URLs (e.g. those created via 129 | ModelAdmins) will override the auto-discovered URL. 130 | -------------------------------------------------------------------------------- /adminplus/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django-AdminPlus module 3 | """ 4 | 5 | VERSION = (0, 6) 6 | __version__ = '.'.join(map(str, VERSION)) 7 | -------------------------------------------------------------------------------- /adminplus/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AdminPlusConfig(AppConfig): 5 | label = 'adminplus' 6 | name = 'adminplus' 7 | verbose_name = 'Administration' 8 | -------------------------------------------------------------------------------- /adminplus/models.py: -------------------------------------------------------------------------------- 1 | # This module intentionally left blank. Only here for testing. 2 | -------------------------------------------------------------------------------- /adminplus/sites.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | import inspect 3 | from typing import Any, Callable, NewType, Sequence, Union 4 | 5 | from django.contrib.admin.sites import AdminSite 6 | from django.urls import URLPattern, URLResolver, path 7 | from django.utils.text import capfirst 8 | from django.views.generic import View 9 | 10 | 11 | _FuncT = NewType('_FuncT', Callable[..., Any]) 12 | 13 | AdminView = namedtuple('AdminView', 14 | ['path', 'view', 'name', 'urlname', 'visible']) 15 | 16 | 17 | def is_class_based_view(view): 18 | return inspect.isclass(view) and issubclass(view, View) 19 | 20 | 21 | class AdminPlusMixin(object): 22 | """Mixin for AdminSite to allow registering custom admin views.""" 23 | 24 | index_template = 'adminplus/index.html' # That was easy. 25 | 26 | def __init__(self, *args, **kwargs): 27 | self.custom_views: list[AdminView] = [] 28 | return super().__init__(*args, **kwargs) 29 | 30 | def register_view(self, slug, name=None, urlname=None, visible=True, 31 | view=None) -> Union[None, Callable[[_FuncT], _FuncT]]: 32 | """Add a custom admin view. Can be used as a function or a decorator. 33 | 34 | * `path` is the path in the admin where the view will live, e.g. 35 | http://example.com/admin/somepath 36 | * `name` is an optional pretty name for the list of custom views. If 37 | empty, we'll guess based on view.__name__. 38 | * `urlname` is an optional parameter to be able to call the view with a 39 | redirect() or reverse() 40 | * `visible` is a boolean or predicate returning one, to set if 41 | the custom view should be visible in the admin dashboard or not. 42 | * `view` is any view function you can imagine. 43 | """ 44 | def decorator(fn: _FuncT): 45 | if is_class_based_view(fn): 46 | fn = fn.as_view() 47 | self.custom_views.append( 48 | AdminView(slug, fn, name, urlname, visible)) 49 | return fn 50 | if view is not None: 51 | decorator(view) 52 | return 53 | return decorator 54 | 55 | def get_urls(self) -> Sequence[Union[URLPattern, URLResolver]]: 56 | """Add our custom views to the admin urlconf.""" 57 | urls: list[Union[URLPattern, URLResolver]] = super().get_urls() 58 | for av in self.custom_views: 59 | urls.insert( 60 | 0, path(av.path, self.admin_view(av.view), name=av.urlname)) 61 | return urls 62 | 63 | def index(self, request, extra_context=None): 64 | """Make sure our list of custom views is on the index page.""" 65 | if not extra_context: 66 | extra_context = {} 67 | custom_list = [] 68 | for slug, view, name, _, visible in self.custom_views: 69 | if callable(visible): 70 | visible = visible(request) 71 | if visible: 72 | if name: 73 | custom_list.append((slug, name)) 74 | else: 75 | custom_list.append((slug, capfirst(view.__name__))) 76 | 77 | # Sort views alphabetically. 78 | custom_list.sort(key=lambda x: x[1]) 79 | extra_context.update({ 80 | 'custom_list': custom_list 81 | }) 82 | return super().index(request, extra_context) 83 | 84 | 85 | class AdminSitePlus(AdminPlusMixin, AdminSite): 86 | """A Django AdminSite with the AdminPlusMixin to allow registering custom 87 | views not connected to models.""" 88 | -------------------------------------------------------------------------------- /adminplus/templates/adminplus/base.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | 3 | {% block breadcrumbs %} 4 | 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /adminplus/templates/adminplus/index.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/index.html" %} 2 | 3 | {% block sidebar %} 4 | {{ block.super }} 5 | 6 | {% if custom_list %} 7 |
8 | 9 | 10 | 11 | {% for path, name in custom_list %} 12 | 13 | {% endfor %} 14 | 15 |
Custom Views
{{ name }}
16 |
17 | {% endif %} 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /adminplus/templates/adminplus/test/index.html: -------------------------------------------------------------------------------- 1 | {% extends "adminplus/base.html" %} 2 | 3 | {% block content %} 4 |

Ohai

5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /adminplus/tests.py: -------------------------------------------------------------------------------- 1 | from django.template.loader import render_to_string 2 | from django.test import TestCase, RequestFactory 3 | from django.views.generic import View 4 | 5 | from adminplus.sites import AdminSitePlus 6 | 7 | 8 | class AdminPlusTests(TestCase): 9 | def test_decorator(self): 10 | """register_view works as a decorator.""" 11 | site = AdminSitePlus() 12 | 13 | @site.register_view(r'foo/bar') 14 | def foo_bar(request): 15 | return 'foo-bar' 16 | 17 | @site.register_view(r'foobar') 18 | class FooBar(View): 19 | def get(self, request): 20 | return 'foo-bar' 21 | 22 | urls = site.get_urls() 23 | assert any(u.resolve('foo/bar') for u in urls) 24 | assert any(u.resolve('foobar') for u in urls) 25 | 26 | def test_function(self): 27 | """register_view works as a function.""" 28 | site = AdminSitePlus() 29 | 30 | def foo(request): 31 | return 'foo' 32 | site.register_view('foo', view=foo) 33 | 34 | class Foo(View): 35 | def get(self, request): 36 | return 'foo' 37 | site.register_view('bar', view=Foo) 38 | 39 | urls = site.get_urls() 40 | assert any(u.resolve('foo') for u in urls) 41 | assert any(u.resolve('bar') for u in urls) 42 | 43 | def test_path(self): 44 | """Setting the path works correctly.""" 45 | site = AdminSitePlus() 46 | 47 | def foo(request): 48 | return 'foo' 49 | site.register_view('foo', view=foo) 50 | site.register_view('bar/baz', view=foo) 51 | site.register_view('baz-qux', view=foo) 52 | 53 | urls = site.get_urls() 54 | 55 | # the default admin contains a catchall view, so each will match 2 56 | foo_urls = [u for u in urls if u.resolve('foo')] 57 | self.assertEqual(2, len(foo_urls)) 58 | bar_urls = [u for u in urls if u.resolve('bar/baz')] 59 | self.assertEqual(2, len(bar_urls)) 60 | qux_urls = [u for u in urls if u.resolve('baz-qux')] 61 | self.assertEqual(2, len(qux_urls)) 62 | 63 | def test_urlname(self): 64 | """Set URL pattern names correctly.""" 65 | site = AdminSitePlus() 66 | 67 | @site.register_view('foo', urlname='foo') 68 | def foo(request): 69 | return 'foo' 70 | 71 | @site.register_view('bar') 72 | def bar(request): 73 | return 'bar' 74 | 75 | urls = site.get_urls() 76 | foo_urls = [u for u in urls if u.resolve('foo')] 77 | 78 | # the default admin contains a catchall view, so this will capture two 79 | self.assertEqual(2, len(foo_urls)) 80 | self.assertEqual('foo', foo_urls[0].name) 81 | 82 | bar_urls = [u for u in urls if u.resolve('bar')] 83 | self.assertEqual(2, len(bar_urls)) 84 | assert bar_urls[0].name is None 85 | 86 | def test_base_template(self): 87 | """Make sure extending the base template works everywhere.""" 88 | result = render_to_string('adminplus/test/index.html') 89 | assert 'Ohai' in result 90 | 91 | def test_visibility(self): 92 | """Make sure visibility works.""" 93 | site = AdminSitePlus() 94 | req_factory = RequestFactory() 95 | 96 | def always_visible(request): 97 | return 'i am here' 98 | site.register_view('always-visible', view=always_visible, visible=True) 99 | 100 | def always_hidden(request): 101 | return 'i am here, but not shown' 102 | site.register_view('always-hidden', view=always_visible, visible=False) 103 | 104 | cond = lambda req: req.user.pk == 1 # noqa: E731 105 | b = lambda s: s.encode('ascii') if hasattr(s, 'encode') else s # noqa: #731 106 | 107 | @site.register_view(r'conditional-view', visible=cond) 108 | class ConditionallyVisible(View): 109 | def get(self, request): 110 | return 'hi there' 111 | 112 | urls = site.get_urls() 113 | assert any(u.resolve('always-visible') for u in urls) 114 | assert any(u.resolve('always-hidden') for u in urls) 115 | assert any(u.resolve('conditional-view') for u in urls) 116 | 117 | class MockUser(object): 118 | is_active = True 119 | is_staff = True 120 | 121 | def __init__(self, pk): 122 | self.pk = pk 123 | self.id = pk 124 | 125 | req_show = req_factory.get('/admin/') 126 | req_show.user = MockUser(1) 127 | result = site.index(req_show).render().content 128 | assert b('always-visible') in result 129 | assert b('always-hidden') not in result 130 | assert b('conditional-view') in result 131 | 132 | req_hide = req_factory.get('/admin/') 133 | req_hide.user = MockUser(2) 134 | result = site.index(req_hide).render().content 135 | assert b('always-visible') in result 136 | assert b('always-hidden') not in result 137 | assert b('conditional-view') not in result 138 | -------------------------------------------------------------------------------- /docs/custom-admin-views.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | Creating Custom Admin Views 3 | =========================== 4 | 5 | Any view can be used as a custom admin view in AdminPlus. All the normal 6 | rules apply: accept a request and possibly other parameters, return a 7 | response, and you're good. 8 | 9 | Making views look like the rest of the admin is pretty straight-forward, 10 | too. 11 | 12 | 13 | Extending the Admin Templates 14 | ============================= 15 | 16 | AdminPlus contains an base template you can easily extend. It includes 17 | the breadcrumb boilerplate. You can also extend ``admin/base_site.html`` 18 | directly. 19 | 20 | Your view should pass a ``title`` value to the template to make things 21 | pretty. 22 | 23 | Here's an example template:: 24 | 25 | {# myapp/admin/myview.html #} 26 | {% extends 'adminplus/base.html' %} 27 | 28 | {% block content %} 29 | {# Do what you gotta do. #} 30 | {% endblock %} 31 | 32 | That's pretty much it! Now here's how you use it:: 33 | 34 | # myapp/admin.py 35 | # Using AdminPlus 36 | from django.contrib import admin 37 | from django.shortcuts import render_to_response 38 | from django.template import RequestContext 39 | 40 | def myview(request): 41 | # Fanciness. 42 | return render_to_response('myapp/admin/myview.html', 43 | {'title': 'My View'}, 44 | RequestContext(request, {})) 45 | admin.site.register_view('mypath', myview, 'My View') 46 | 47 | Or, you can use it as a decorator:: 48 | 49 | from django.contrib import admin 50 | 51 | @admin.site.register_view 52 | def myview(request): 53 | # Fancy goes here. 54 | return render_to_response(...) 55 | 56 | Voila! Instant custom admin page that looks great. 57 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "django-adminplus" 7 | version = "0.6" 8 | authors = [{name = "James Socol", email = "me@jamessocol.com"}] 9 | requires-python = ">= 3.7" 10 | license = {file = "LICENSE"} 11 | description = "Add new pages to the Django admin." 12 | readme = "README.rst" 13 | classifiers = [ 14 | "Development Status :: 4 - Beta", 15 | "Environment :: Web Environment", 16 | "Intended Audience :: Developers", 17 | "License :: OSI Approved :: BSD License", 18 | "Operating System :: OS Independent", 19 | "Programming Language :: Python", 20 | "Programming Language :: Python :: 3", 21 | "Programming Language :: Python :: 3.7", 22 | "Programming Language :: Python :: 3.8", 23 | "Programming Language :: Python :: 3.9", 24 | "Programming Language :: Python :: 3.10", 25 | "Programming Language :: Python :: 3.11", 26 | "Framework :: Django", 27 | "Topic :: Software Development :: Libraries :: Python Modules", 28 | ] 29 | urls = {Homepage = "https://github.com/jsocol/django-adminplus"} 30 | 31 | [tool.distutils.bdist_wheel] 32 | universal = 1 33 | 34 | [tool.setuptools] 35 | include-package-data = true 36 | 37 | [tool.setuptools.packages] 38 | find = {namespaces = false} 39 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export PYTHONPATH=".:$PYTHONPATH" 4 | export DJANGO_SETTINGS_MODULE="test_settings" 5 | 6 | PROG="$0" 7 | CMD="$1" 8 | shift 9 | 10 | usage() { 11 | echo "USAGE: $PROG [command]" 12 | echo " test - run the adminplus tests" 13 | echo " lint - run flake8 (alias: flake8)" 14 | echo " shell - open the Django shell" 15 | echo " build - build a package for release" 16 | echo " check - run twine check on build artifacts" 17 | exit 1 18 | } 19 | 20 | case "$CMD" in 21 | "test" ) 22 | echo "Django version: $(python -m django --version)" 23 | python -m django test adminplus 24 | ;; 25 | "lint"|"flake8" ) 26 | echo "Flake8 version: $(flake8 --version)" 27 | flake8 "$@" adminplus/ 28 | ;; 29 | "shell" ) 30 | python -m django shell 31 | ;; 32 | "build" ) 33 | rm -rf dist/* 34 | python -m build 35 | ;; 36 | "check" ) 37 | twine check dist/* 38 | ;; 39 | * ) 40 | usage 41 | ;; 42 | esac 43 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /test_settings.py: -------------------------------------------------------------------------------- 1 | INSTALLED_APPS = ( 2 | 'django.contrib.sessions', 3 | 'django.contrib.contenttypes', 4 | 'django.contrib.messages', 5 | 'django.contrib.auth', 6 | 'django.contrib.admin', 7 | 'adminplus', 8 | ) 9 | 10 | SECRET_KEY = 'adminplus' 11 | 12 | TEMPLATES = [ 13 | { 14 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 15 | 'DIRS': (), 16 | 'OPTIONS': { 17 | 'autoescape': False, 18 | 'loaders': ( 19 | 'django.template.loaders.filesystem.Loader', 20 | 'django.template.loaders.app_directories.Loader', 21 | ), 22 | 'context_processors': ( 23 | 'django.template.context_processors.debug', 24 | 'django.template.context_processors.i18n', 25 | 'django.template.context_processors.media', 26 | 'django.template.context_processors.request', 27 | 'django.template.context_processors.static', 28 | 'django.contrib.auth.context_processors.auth', 29 | 'django.contrib.messages.context_processors.messages', 30 | ), 31 | }, 32 | }, 33 | ] 34 | 35 | DATABASES = { 36 | 'default': { 37 | 'ENGINE': 'django.db.backends.sqlite3', 38 | 'NAME': 'test.db', 39 | }, 40 | } 41 | 42 | ROOT_URLCONF = 'test_urlconf' 43 | 44 | MIDDLEWARE = ( 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.contrib.messages.middleware.MessageMiddleware', 47 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 48 | ) 49 | -------------------------------------------------------------------------------- /test_urlconf.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import re_path, include 3 | 4 | from adminplus.sites import AdminSitePlus 5 | 6 | 7 | admin.site = AdminSitePlus() 8 | admin.autodiscover() 9 | 10 | urlpatterns = [ 11 | re_path(r'^admin/', include((admin.site.get_urls(), 'admin'), namespace='admin')), 12 | ] 13 | --------------------------------------------------------------------------------