pax_global_header00006660000000000000000000000064145336732040014521gustar00rootroot0000000000000052 comment=d8a0834b774ac1b33f652fc923a80beff3f681fd django-adminplus-0.6/000077500000000000000000000000001453367320400146225ustar00rootroot00000000000000django-adminplus-0.6/.github/000077500000000000000000000000001453367320400161625ustar00rootroot00000000000000django-adminplus-0.6/.github/actions/000077500000000000000000000000001453367320400176225ustar00rootroot00000000000000django-adminplus-0.6/.github/actions/test/000077500000000000000000000000001453367320400206015ustar00rootroot00000000000000django-adminplus-0.6/.github/actions/test/action.yml000066400000000000000000000013611453367320400226020ustar00rootroot00000000000000name: test description: 'runs a test matrix' inputs: python-version: required: true django-version: required: true runs: using: "composite" steps: - name: Set up Python uses: actions/setup-python@v4 with: python-version: ${{ inputs.python-version }} - name: Install dependencies shell: sh run: | python -m pip install --upgrade pip if [ "${{ inputs.django-version }}" != 'main' ]; then pip install --pre -q "Django>=${{ inputs.django-version }},<${{ inputs.django-version }}.99"; fi if [ "${{ inputs.django-version }}" = 'main' ]; then pip install https://github.com/django/django/archive/main.tar.gz; fi pip install flake8 - name: Test shell: sh run: | ./run.sh test django-adminplus-0.6/.github/workflows/000077500000000000000000000000001453367320400202175ustar00rootroot00000000000000django-adminplus-0.6/.github/workflows/ci.yml000066400000000000000000000024501453367320400213360ustar00rootroot00000000000000name: ci on: push: branches: - main pull_request: types: [opened, synchronize, reopened, ready_for_review] schedule: - cron: '17 7 * * 0' # run weekly on sundays jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ['3.8', '3.9', '3.10', '3.11'] django: ['3.2', '4.1', '4.2', '5.0', 'main'] exclude: - python-version: '3.7' django: 'main' - python-version: '3.11' django: '3.2' - python-version: '3.8' django: '5.0' - python-version: '3.9' django: '5.0' - python-version: '3.8' django: 'main' - python-version: '3.9' django: 'main' include: - python-version: '3.12' django: '5.0' steps: - uses: actions/checkout@v3 - uses: ./.github/actions/test with: python-version: ${{ matrix.python-version }} django-version: ${{ matrix.django }} lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: '3.11' - name: install flake8 run: pip install flake8 - name: Lint with flake8 run: | ./run.sh lint django-adminplus-0.6/.github/workflows/release.yml000066400000000000000000000020211453367320400223550ustar00rootroot00000000000000name: release on: push: tags: - v* jobs: test: runs-on: ubuntu-latest strategy: fail-fast: true matrix: python-version: ['3.10', '3.11'] django: ['3.2', '4.2', '5.0'] include: - python-version: '3.12' django: '5.0' steps: - uses: actions/checkout@v3 - uses: ./.github/actions/test with: python-version: ${{ matrix.python-version }} django-version: ${{ matrix.django }} release: runs-on: ubuntu-latest needs: [test] environment: release permissions: id-token: write steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: 3.11 - name: install dependencies run: | python -m pip install --upgrade pip pip install build twine - name: build run: ./run.sh build - name: check run: ./run.sh check - name: release uses: pypa/gh-action-pypi-publish@release/v1 django-adminplus-0.6/.gitignore000066400000000000000000000000341453367320400166070ustar00rootroot00000000000000*.pyc *.egg-info build dist django-adminplus-0.6/CHANGELOG000066400000000000000000000011031453367320400160270ustar00rootroot00000000000000Changelog ========= v0.6 ---- - Update supported Django and Python versions. - Fix multiple bugs in urlconfs. - Update test and build tooling to use GitHub Actions as a Trusted Publisher for PyPI v0.5 ---- - Drop support for unsupported Django versions. - Test on 1.8 and 1.9. - Remove deprecated patterns() call. v0.4 ---- - Update supported Django versions. - Support registering class-based views. - Allow callables for `visible`. v0.3 ---- - Prioritize custom views over generic patterns. - Fix a Django>=1.5 template bug. v0.2.1 ------ - Fix Django 1.6 support. django-adminplus-0.6/LICENSE000066400000000000000000000027561453367320400156410ustar00rootroot00000000000000Copyright (c) 2023, James Socol All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of AdminPlus nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. django-adminplus-0.6/MANIFEST.in000066400000000000000000000001541453367320400163600ustar00rootroot00000000000000include CHANGELOG include LICENSE include README.rst recursive-include adminplus/templates/adminplus *.html django-adminplus-0.6/README.rst000066400000000000000000000100651453367320400163130ustar00rootroot00000000000000================ Django AdminPlus ================ **AdminPlus** aims to be the smallest possible extension to the excellent Django admin component that lets you add admin views that are not tied to models. There are packages out there, like `Nexus `_ and `django-admin-tools `_ that replace the entire admin. Nexus supports adding completely new "modules" (the Django model admin is a default module) but there seems to be a lot of boiler plate code to do it. django-admin-tools does not, as far as I can tell, support adding custom pages. All AdminPlus does is allow you to add simple custom views (well, they can be as complex as you like!) without mucking about with hijacking URLs, and providing links to them right in the admin index. .. image:: https://github.com/jsocol/django-adminplus/actions/workflows/ci.yml/badge.svg?branch=main :target: https://github.com/jsocol/django-adminplus Installing AdminPlus ==================== Install from `PyPI `_ with pip: .. code-block:: bash pip install django-adminplus Or get AdminPlus from `GitHub `_ with pip: .. code-block:: bash pip install -e git://github.com/jsocol/django-adminplus#egg=django-adminplus And add ``adminplus`` to your installed apps, and replace ``django.contrib.admin`` with ``django.contrib.admin.apps.SimpleAdminConfig``: .. code-block:: python INSTALLED_APPS = ( 'django.contrib.admin.apps.SimpleAdminConfig', # ... 'adminplus', # ... ) 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()``: .. code-block:: python # urls.py from django.contrib import admin from adminplus.sites import AdminSitePlus admin.site = AdminSitePlus() admin.autodiscover() urlpatterns = [ # ... # Include the admin URL conf as normal. (r'^admin', include(admin.site.urls)), # ... ] Congratulations! You're now using AdminPlus. Using AdminPlus =============== So now that you've installed AdminPlus, you'll want to use it. AdminPlus is 100% compatible with the built in admin module, so if you've been using that, you shouldn't have to change anything. AdminPlus offers a new function, ``admin.site.register_view``, to attach arbitrary views to the admin: .. code-block:: python # someapp/admin.py # Assuming you've replaced django.contrib.admin.site as above. from django.contrib import admin def my_view(request, *args, **kwargs): pass admin.site.register_view('somepath', view=my_view) # And of course, this still works: from someapp.models import MyModel admin.site.register(MyModel) Now ``my_view`` will be accessible at ``admin/somepath`` and there will be a link to it in the *Custom Views* section of the admin index. You can also use ``register_view`` as a decorator: .. code-block:: python @admin.site.register_view('somepath') def my_view(request): pass ``register_view`` takes some optional arguments: * ``name``: a friendly name for display in the list of custom views. For example: .. code-block:: python def my_view(request): """Does something fancy!""" admin.site.register_view('somepath', 'My Fancy Admin View!', view=my_view) * ``urlname``: give a name to the urlpattern so it can be called by ``redirect()``, ``reverse()``, etc. The view will be added to the ``admin`` namespace, so a urlname of ``foo`` would be reversed with ``reverse("admin:foo")``. * `visible`: a boolean or a callable returning one, that defines if the custom view is visible in the admin dashboard. All registered views are wrapped in ``admin.site.admin_view``. .. note:: Views with URLs that match auto-discovered URLs (e.g. those created via ModelAdmins) will override the auto-discovered URL. django-adminplus-0.6/adminplus/000077500000000000000000000000001453367320400166165ustar00rootroot00000000000000django-adminplus-0.6/adminplus/__init__.py000066400000000000000000000001341453367320400207250ustar00rootroot00000000000000""" Django-AdminPlus module """ VERSION = (0, 6) __version__ = '.'.join(map(str, VERSION)) django-adminplus-0.6/adminplus/apps.py000066400000000000000000000002311453367320400201270ustar00rootroot00000000000000from django.apps import AppConfig class AdminPlusConfig(AppConfig): label = 'adminplus' name = 'adminplus' verbose_name = 'Administration' django-adminplus-0.6/adminplus/models.py000066400000000000000000000000771453367320400204570ustar00rootroot00000000000000# This module intentionally left blank. Only here for testing. django-adminplus-0.6/adminplus/sites.py000066400000000000000000000063141453367320400203230ustar00rootroot00000000000000from collections import namedtuple import inspect from typing import Any, Callable, NewType, Sequence, Union from django.contrib.admin.sites import AdminSite from django.urls import URLPattern, URLResolver, path from django.utils.text import capfirst from django.views.generic import View _FuncT = NewType('_FuncT', Callable[..., Any]) AdminView = namedtuple('AdminView', ['path', 'view', 'name', 'urlname', 'visible']) def is_class_based_view(view): return inspect.isclass(view) and issubclass(view, View) class AdminPlusMixin(object): """Mixin for AdminSite to allow registering custom admin views.""" index_template = 'adminplus/index.html' # That was easy. def __init__(self, *args, **kwargs): self.custom_views: list[AdminView] = [] return super().__init__(*args, **kwargs) def register_view(self, slug, name=None, urlname=None, visible=True, view=None) -> Union[None, Callable[[_FuncT], _FuncT]]: """Add a custom admin view. Can be used as a function or a decorator. * `path` is the path in the admin where the view will live, e.g. http://example.com/admin/somepath * `name` is an optional pretty name for the list of custom views. If empty, we'll guess based on view.__name__. * `urlname` is an optional parameter to be able to call the view with a redirect() or reverse() * `visible` is a boolean or predicate returning one, to set if the custom view should be visible in the admin dashboard or not. * `view` is any view function you can imagine. """ def decorator(fn: _FuncT): if is_class_based_view(fn): fn = fn.as_view() self.custom_views.append( AdminView(slug, fn, name, urlname, visible)) return fn if view is not None: decorator(view) return return decorator def get_urls(self) -> Sequence[Union[URLPattern, URLResolver]]: """Add our custom views to the admin urlconf.""" urls: list[Union[URLPattern, URLResolver]] = super().get_urls() for av in self.custom_views: urls.insert( 0, path(av.path, self.admin_view(av.view), name=av.urlname)) return urls def index(self, request, extra_context=None): """Make sure our list of custom views is on the index page.""" if not extra_context: extra_context = {} custom_list = [] for slug, view, name, _, visible in self.custom_views: if callable(visible): visible = visible(request) if visible: if name: custom_list.append((slug, name)) else: custom_list.append((slug, capfirst(view.__name__))) # Sort views alphabetically. custom_list.sort(key=lambda x: x[1]) extra_context.update({ 'custom_list': custom_list }) return super().index(request, extra_context) class AdminSitePlus(AdminPlusMixin, AdminSite): """A Django AdminSite with the AdminPlusMixin to allow registering custom views not connected to models.""" django-adminplus-0.6/adminplus/templates/000077500000000000000000000000001453367320400206145ustar00rootroot00000000000000django-adminplus-0.6/adminplus/templates/adminplus/000077500000000000000000000000001453367320400226105ustar00rootroot00000000000000django-adminplus-0.6/adminplus/templates/adminplus/base.html000066400000000000000000000002661453367320400244140ustar00rootroot00000000000000{% extends "admin/base_site.html" %} {% block breadcrumbs %} {% endblock %} django-adminplus-0.6/adminplus/templates/adminplus/index.html000066400000000000000000000006621453367320400246110ustar00rootroot00000000000000{% extends "admin/index.html" %} {% block sidebar %} {{ block.super }} {% if custom_list %}
{% for path, name in custom_list %} {% endfor %}
Custom Views
{{ name }}
{% endif %} {% endblock %} django-adminplus-0.6/adminplus/templates/adminplus/test/000077500000000000000000000000001453367320400235675ustar00rootroot00000000000000django-adminplus-0.6/adminplus/templates/adminplus/test/index.html000066400000000000000000000001301453367320400255560ustar00rootroot00000000000000{% extends "adminplus/base.html" %} {% block content %}

Ohai

{% endblock %} django-adminplus-0.6/adminplus/tests.py000066400000000000000000000106641453367320400203410ustar00rootroot00000000000000from django.template.loader import render_to_string from django.test import TestCase, RequestFactory from django.views.generic import View from adminplus.sites import AdminSitePlus class AdminPlusTests(TestCase): def test_decorator(self): """register_view works as a decorator.""" site = AdminSitePlus() @site.register_view(r'foo/bar') def foo_bar(request): return 'foo-bar' @site.register_view(r'foobar') class FooBar(View): def get(self, request): return 'foo-bar' urls = site.get_urls() assert any(u.resolve('foo/bar') for u in urls) assert any(u.resolve('foobar') for u in urls) def test_function(self): """register_view works as a function.""" site = AdminSitePlus() def foo(request): return 'foo' site.register_view('foo', view=foo) class Foo(View): def get(self, request): return 'foo' site.register_view('bar', view=Foo) urls = site.get_urls() assert any(u.resolve('foo') for u in urls) assert any(u.resolve('bar') for u in urls) def test_path(self): """Setting the path works correctly.""" site = AdminSitePlus() def foo(request): return 'foo' site.register_view('foo', view=foo) site.register_view('bar/baz', view=foo) site.register_view('baz-qux', view=foo) urls = site.get_urls() # the default admin contains a catchall view, so each will match 2 foo_urls = [u for u in urls if u.resolve('foo')] self.assertEqual(2, len(foo_urls)) bar_urls = [u for u in urls if u.resolve('bar/baz')] self.assertEqual(2, len(bar_urls)) qux_urls = [u for u in urls if u.resolve('baz-qux')] self.assertEqual(2, len(qux_urls)) def test_urlname(self): """Set URL pattern names correctly.""" site = AdminSitePlus() @site.register_view('foo', urlname='foo') def foo(request): return 'foo' @site.register_view('bar') def bar(request): return 'bar' urls = site.get_urls() foo_urls = [u for u in urls if u.resolve('foo')] # the default admin contains a catchall view, so this will capture two self.assertEqual(2, len(foo_urls)) self.assertEqual('foo', foo_urls[0].name) bar_urls = [u for u in urls if u.resolve('bar')] self.assertEqual(2, len(bar_urls)) assert bar_urls[0].name is None def test_base_template(self): """Make sure extending the base template works everywhere.""" result = render_to_string('adminplus/test/index.html') assert 'Ohai' in result def test_visibility(self): """Make sure visibility works.""" site = AdminSitePlus() req_factory = RequestFactory() def always_visible(request): return 'i am here' site.register_view('always-visible', view=always_visible, visible=True) def always_hidden(request): return 'i am here, but not shown' site.register_view('always-hidden', view=always_visible, visible=False) cond = lambda req: req.user.pk == 1 # noqa: E731 b = lambda s: s.encode('ascii') if hasattr(s, 'encode') else s # noqa: #731 @site.register_view(r'conditional-view', visible=cond) class ConditionallyVisible(View): def get(self, request): return 'hi there' urls = site.get_urls() assert any(u.resolve('always-visible') for u in urls) assert any(u.resolve('always-hidden') for u in urls) assert any(u.resolve('conditional-view') for u in urls) class MockUser(object): is_active = True is_staff = True def __init__(self, pk): self.pk = pk self.id = pk req_show = req_factory.get('/admin/') req_show.user = MockUser(1) result = site.index(req_show).render().content assert b('always-visible') in result assert b('always-hidden') not in result assert b('conditional-view') in result req_hide = req_factory.get('/admin/') req_hide.user = MockUser(2) result = site.index(req_hide).render().content assert b('always-visible') in result assert b('always-hidden') not in result assert b('conditional-view') not in result django-adminplus-0.6/docs/000077500000000000000000000000001453367320400155525ustar00rootroot00000000000000django-adminplus-0.6/docs/custom-admin-views.rst000066400000000000000000000030561453367320400220430ustar00rootroot00000000000000=========================== Creating Custom Admin Views =========================== Any view can be used as a custom admin view in AdminPlus. All the normal rules apply: accept a request and possibly other parameters, return a response, and you're good. Making views look like the rest of the admin is pretty straight-forward, too. Extending the Admin Templates ============================= AdminPlus contains an base template you can easily extend. It includes the breadcrumb boilerplate. You can also extend ``admin/base_site.html`` directly. Your view should pass a ``title`` value to the template to make things pretty. Here's an example template:: {# myapp/admin/myview.html #} {% extends 'adminplus/base.html' %} {% block content %} {# Do what you gotta do. #} {% endblock %} That's pretty much it! Now here's how you use it:: # myapp/admin.py # Using AdminPlus from django.contrib import admin from django.shortcuts import render_to_response from django.template import RequestContext def myview(request): # Fanciness. return render_to_response('myapp/admin/myview.html', {'title': 'My View'}, RequestContext(request, {})) admin.site.register_view('mypath', myview, 'My View') Or, you can use it as a decorator:: from django.contrib import admin @admin.site.register_view def myview(request): # Fancy goes here. return render_to_response(...) Voila! Instant custom admin page that looks great. django-adminplus-0.6/pyproject.toml000066400000000000000000000022211453367320400175330ustar00rootroot00000000000000[build-system] requires = ["setuptools>=61.2"] build-backend = "setuptools.build_meta" [project] name = "django-adminplus" version = "0.6" authors = [{name = "James Socol", email = "me@jamessocol.com"}] requires-python = ">= 3.7" license = {file = "LICENSE"} description = "Add new pages to the Django admin." readme = "README.rst" classifiers = [ "Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Framework :: Django", "Topic :: Software Development :: Libraries :: Python Modules", ] urls = {Homepage = "https://github.com/jsocol/django-adminplus"} [tool.distutils.bdist_wheel] universal = 1 [tool.setuptools] include-package-data = true [tool.setuptools.packages] find = {namespaces = false} django-adminplus-0.6/run.sh000077500000000000000000000015671453367320400157760ustar00rootroot00000000000000#!/bin/sh export PYTHONPATH=".:$PYTHONPATH" export DJANGO_SETTINGS_MODULE="test_settings" PROG="$0" CMD="$1" shift usage() { echo "USAGE: $PROG [command]" echo " test - run the adminplus tests" echo " lint - run flake8 (alias: flake8)" echo " shell - open the Django shell" echo " build - build a package for release" echo " check - run twine check on build artifacts" exit 1 } case "$CMD" in "test" ) echo "Django version: $(python -m django --version)" python -m django test adminplus ;; "lint"|"flake8" ) echo "Flake8 version: $(flake8 --version)" flake8 "$@" adminplus/ ;; "shell" ) python -m django shell ;; "build" ) rm -rf dist/* python -m build ;; "check" ) twine check dist/* ;; * ) usage ;; esac django-adminplus-0.6/setup.cfg000066400000000000000000000000341453367320400164400ustar00rootroot00000000000000[bdist_wheel] universal = 1 django-adminplus-0.6/test_settings.py000066400000000000000000000025731453367320400201020ustar00rootroot00000000000000INSTALLED_APPS = ( 'django.contrib.sessions', 'django.contrib.contenttypes', 'django.contrib.messages', 'django.contrib.auth', 'django.contrib.admin', 'adminplus', ) SECRET_KEY = 'adminplus' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': (), 'OPTIONS': { 'autoescape': False, 'loaders': ( 'django.template.loaders.filesystem.Loader', 'django.template.loaders.app_directories.Loader', ), 'context_processors': ( 'django.template.context_processors.debug', 'django.template.context_processors.i18n', 'django.template.context_processors.media', 'django.template.context_processors.request', 'django.template.context_processors.static', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ), }, }, ] DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': 'test.db', }, } ROOT_URLCONF = 'test_urlconf' MIDDLEWARE = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', ) django-adminplus-0.6/test_urlconf.py000066400000000000000000000004231453367320400177020ustar00rootroot00000000000000from django.contrib import admin from django.urls import re_path, include from adminplus.sites import AdminSitePlus admin.site = AdminSitePlus() admin.autodiscover() urlpatterns = [ re_path(r'^admin/', include((admin.site.get_urls(), 'admin'), namespace='admin')), ]