├── .gitignore
├── .travis.yml
├── CHANGES.md
├── LICENSE.txt
├── MANIFEST
├── Makefile
├── README.md
├── requirements.txt
├── setup.cfg
├── setup.py
├── singlemodeladmin
└── __init__.py
└── test
├── app
├── __init__.py
├── admin.py
├── migrations
│ ├── 0001_initial.py
│ └── __init__.py
├── models.py
└── tests.py
├── manage.py
└── project
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *.sqlite3
3 | *.egg-info/
4 | dist/
5 | env/
6 | build/
7 |
8 | \.idea/
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 |
3 | matrix:
4 | include:
5 | # Python 2.6 is supported by Django < 1.7
6 | - python: 2.6
7 | env: DJANGO_VERSION=1.4.22
8 | - python: 2.6
9 | env: DJANGO_VERSION=1.5.12
10 | - python: 2.6
11 | env: DJANGO_VERSION=1.6.11
12 | # Python 2.7 is supported by Django < 2.0
13 | - python: 2.7
14 | env: DJANGO_VERSION=1.4.22
15 | - python: 2.7
16 | env: DJANGO_VERSION=1.5.12
17 | - python: 2.7
18 | env: DJANGO_VERSION=1.6.11
19 | - python: 2.7
20 | env: DJANGO_VERSION=1.7.11
21 | - python: 2.7
22 | env: DJANGO_VERSION=1.8.19
23 | - python: 2.7
24 | env: DJANGO_VERSION=1.9.13
25 | - python: 2.7
26 | env: DJANGO_VERSION=1.10.8
27 | - python: 2.7
28 | env: DJANGO_VERSION=1.11.15
29 | # Python 3.3 is supported by Django >= 1.5 and Django < 1.9
30 | - python: 3.3
31 | env: DJANGO_VERSION=1.5.12
32 | - python: 3.3
33 | env: DJANGO_VERSION=1.6.11
34 | - python: 3.3
35 | env: DJANGO_VERSION=1.7.11
36 | - python: 3.3
37 | env: DJANGO_VERSION=1.8.19
38 | # Python 3.4 is supported by Django >= 1.5 and Django < 1.11
39 | - python: 3.4
40 | env: DJANGO_VERSION=1.5.12
41 | - python: 3.4
42 | env: DJANGO_VERSION=1.6.11
43 | - python: 3.4
44 | env: DJANGO_VERSION=1.7.11
45 | - python: 3.4
46 | env: DJANGO_VERSION=1.8.19
47 | - python: 3.4
48 | env: DJANGO_VERSION=1.9.13
49 | - python: 3.4
50 | env: DJANGO_VERSION=1.10.8
51 | # Python 3.5 is supported by Django >= 1.8
52 | - python: 3.5
53 | env: DJANGO_VERSION=1.8.19
54 | - python: 3.5
55 | env: DJANGO_VERSION=1.9.13
56 | - python: 3.5
57 | env: DJANGO_VERSION=1.10.8
58 | - python: 3.5
59 | env: DJANGO_VERSION=1.11.15
60 | - python: 3.5
61 | env: DJANGO_VERSION=2.0.8
62 | - python: 3.5
63 | env: DJANGO_VERSION=2.1
64 | # Python 3.6 is supported by Django >= 1.11
65 | - python: 3.6
66 | env: DJANGO_VERSION=1.11.15
67 | - python: 3.6
68 | env: DJANGO_VERSION=2.0.8
69 | - python: 3.6
70 | env: DJANGO_VERSION=2.1
71 | # Python 3.7 is supported by Django >= 2.0
72 | # The dist and sudo are needed to run 3.7
73 | # on Travis until 3.7 is officially supported.
74 | - python: 3.7
75 | env: DJANGO_VERSION=2.0.8
76 | dist: xenial
77 | sudo: true
78 | - python: 3.7
79 | env: DJANGO_VERSION=2.1
80 | dist: xenial
81 | sudo: true
82 |
83 | install:
84 | - pip install --pre -q Django==$DJANGO_VERSION
85 | - pip install -r requirements.txt
86 | - pip install .
87 |
88 | script:
89 | - make lint
90 | - make test
91 |
--------------------------------------------------------------------------------
/CHANGES.md:
--------------------------------------------------------------------------------
1 | Changelog
2 | =========
3 |
4 | Version 0.9
5 | -----------
6 |
7 | * Add classifiers
8 | * Add support for Django 2.0
9 | * Drop support for Django 2.0b1
10 | * Add support for Python 3.7
11 |
12 |
13 | Version 0.8
14 | -----------
15 |
16 | * Add support for Django 2.0b1
17 |
18 | Version 0.7
19 | -----------
20 |
21 | * Add support for Django 1.11.
22 | * Add support for Python 3.6.
23 |
24 | Version 0.6
25 | -----------
26 |
27 | * Add support for Django 1.10.
28 | * Drop support for Python 3.2.
29 |
30 | Version 0.5
31 | -----------
32 |
33 | * Add support for Python 3.4 and 3.5.
34 | * Add support for Django 1.9.
35 |
36 | Version 0.4
37 | -----------
38 |
39 | * Ensure support for Python 2.6, 2.7, 3.2, and 3.3.
40 |
41 | Version 0.3
42 | -----------
43 |
44 | * Add support for Django 1.8.
45 |
46 | Version 0.2
47 | -----------
48 |
49 | * Add support for Django 1.6.
50 | * Hide the "Add" button in the UI if a model exists.
51 |
52 | Version 0.1
53 | -----------
54 |
55 | * Initial release.
56 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2011-2018 Alexander Meng
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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MANIFEST:
--------------------------------------------------------------------------------
1 | # file GENERATED by distutils, do NOT edit
2 | setup.cfg
3 | setup.py
4 | singlemodeladmin/__init__.py
5 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: test
2 |
3 | install:
4 | pip install .
5 | pip install -r requirements.txt
6 |
7 | test:
8 | cd test && python manage.py test app
9 |
10 | lint:
11 | flake8
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Django Single ModelAdmin
2 | ========================
3 |
4 | [][pypi]
5 | [][travis]
6 |
7 | [travis]: http://travis-ci.org/AMeng/django-single-model-admin
8 | [pypi]: https://pypi.python.org/pypi/singlemodeladmin
9 |
10 | A subclass of Django's `ModelAdmin` for use with models that are only meant to
11 | have one record. This is useful for things like site-wide settings.
12 |
13 | Usage:
14 | ------
15 |
16 | Register a model with `SingleModelAdmin`:
17 |
18 | ```python
19 | from django.contrib import admin
20 | from singlemodeladmin import SingleModelAdmin
21 | from my_app.models import MyModel
22 |
23 | admin.site.register(MyModel, SingleModelAdmin)
24 | ```
25 | You can also subclass `SingleModelAdmin` instead of Django's `ModelAdmin`, and
26 | it will work as expected:
27 |
28 | ```python
29 | from django.contrib import admin
30 | from singlemodeladmin import SingleModelAdmin
31 | from my_app.models import MyModel
32 |
33 | class MyModelAdmin(SingleModelAdmin):
34 | list_display = ['my_field']
35 |
36 | admin.site.register(MyModel, MyModelAdmin)
37 | ```
38 |
39 | Installation:
40 | -------------
41 | ```
42 | pip install singlemodeladmin
43 | ```
44 |
45 | Behavior:
46 | ---------
47 |
48 | * If there is only one object, the changelist will redirect to that object.
49 | * If there are no objects, the changelist will redirect to the add form.
50 | * If there are multiple objects, the changelist is displayed with a warning
51 | * Attempting to add a new record when there is already one will result in a
52 | warning and a redirect away from the add form.
53 |
54 | Supported Django Versions:
55 | --------------------------
56 | * 1.4
57 | * 1.5
58 | * 1.6
59 | * 1.7
60 | * 1.8
61 | * 1.9
62 | * 1.10
63 | * 1.11
64 | * 2.0
65 | * 2.1
66 |
67 | Supported Python Versions:
68 | --------------------------
69 | * 2.6
70 | * 2.7
71 | * 3.3
72 | * 3.4
73 | * 3.5
74 | * 3.6
75 | * 3.7
76 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Django>=1.4
2 | flake8<=2.6.2 # Last version with Python 2.6 support.
3 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | description-file = README.md
3 |
4 | [flake8]
5 | exclude = env,build
6 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools # noqa: F401
2 | from distutils.core import setup
3 |
4 | setup(
5 | name='singlemodeladmin',
6 | packages=['singlemodeladmin'],
7 | version='0.9',
8 | description='ModelAdmin for models that are meant to have one record',
9 | author='Alexander Meng',
10 | author_email='alexbmeng@gmail.com',
11 | url='https://github.com/AMeng/django-single-model-admin',
12 | keywords=['django', 'model', 'admin'],
13 | install_requires=[
14 | 'Django>=1.4'
15 | ],
16 | classifiers=[
17 | "Development Status :: 5 - Production/Stable",
18 | "Framework :: Django",
19 | "Framework :: Django :: 1.4",
20 | "Framework :: Django :: 1.5",
21 | "Framework :: Django :: 1.6",
22 | "Framework :: Django :: 1.7",
23 | "Framework :: Django :: 1.8",
24 | "Framework :: Django :: 1.9",
25 | "Framework :: Django :: 1.10",
26 | "Framework :: Django :: 1.11",
27 | "Framework :: Django :: 2.0",
28 | "Framework :: Django :: 2.1",
29 | "Intended Audience :: Developers",
30 | "License :: OSI Approved :: MIT License",
31 | "Natural Language :: English",
32 | "Operating System :: OS Independent",
33 | "Programming Language :: Python",
34 | "Programming Language :: Python :: 2",
35 | "Programming Language :: Python :: 2.6",
36 | "Programming Language :: Python :: 2.7",
37 | "Programming Language :: Python :: 3",
38 | "Programming Language :: Python :: 3.5",
39 | "Programming Language :: Python :: 3.6",
40 | "Programming Language :: Python :: 3.7",
41 | "Topic :: Utilities"
42 | ]
43 | )
44 |
--------------------------------------------------------------------------------
/singlemodeladmin/__init__.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin, messages
2 | from django.core.exceptions import MultipleObjectsReturned
3 | from django.shortcuts import redirect
4 | try:
5 | from django.core.urlresolvers import reverse
6 | except ImportError:
7 | from django.urls import reverse
8 |
9 |
10 | class SingleModelAdmin(admin.ModelAdmin):
11 |
12 | """
13 | Django ModelAdmin for models that are only meant to have one record.
14 | This is useful for a site-wide settings model, among other things.
15 |
16 | If there is only one object, the changelist will redirect to that object.
17 | If there are no objects, the changelist will redirect to the add form.
18 | If there are multiple objects, the changelist is displayed with a warning.
19 |
20 | Attempting to add a new record when there is already one will result in a
21 | warning and a redirect away from the add form.
22 | """
23 |
24 | def _get_model_name(self):
25 | try:
26 | return self.model._meta.model_name
27 | except AttributeError:
28 | return self.model._meta.module_name
29 |
30 | def changelist_view(self, request, extra_context=None):
31 | app_and_model = '{0}_{1}'.format(self.model._meta.app_label,
32 | self._get_model_name())
33 | try:
34 | instance = self.model.objects.get()
35 | except self.model.DoesNotExist:
36 | return redirect(reverse('admin:{0}_add'.format(app_and_model)))
37 | except MultipleObjectsReturned:
38 | warning = ('There are multiple instances of {0}. There should only'
39 | ' be one.').format(self._get_model_name())
40 | messages.warning(request, warning, fail_silently=True)
41 | return super(SingleModelAdmin, self).changelist_view(
42 | request, extra_context=extra_context)
43 | else:
44 | return redirect(reverse('admin:{0}_change'.format(app_and_model),
45 | args=[instance.pk]))
46 |
47 | def add_view(self, request, form_url='', extra_context=None):
48 | if self.model.objects.count():
49 | warning = ('Do not add additional instances of {0}. Only one is'
50 | ' needed.').format(self._get_model_name())
51 | messages.warning(request, warning, fail_silently=True)
52 | return redirect(reverse('admin:{0}_{1}_changelist'.format(
53 | self.model._meta.app_label,
54 | self._get_model_name())))
55 | return super(SingleModelAdmin, self).add_view(
56 | request,
57 | form_url=form_url,
58 | extra_context=extra_context)
59 |
60 | def has_add_permission(self, request):
61 | try:
62 | self.model.objects.get()
63 | except self.model.DoesNotExist:
64 | return super(SingleModelAdmin, self).has_add_permission(request)
65 | except MultipleObjectsReturned:
66 | pass
67 | return False
68 |
--------------------------------------------------------------------------------
/test/app/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AMeng/django-single-model-admin/85c7dcbedcf9264bfdfab24fdb27c96afbf6a7ee/test/app/__init__.py
--------------------------------------------------------------------------------
/test/app/admin.py:
--------------------------------------------------------------------------------
1 | from singlemodeladmin import SingleModelAdmin
2 | from django.contrib import admin
3 | from app.models import TestModel
4 |
5 |
6 | admin.site.register(TestModel, SingleModelAdmin)
7 |
--------------------------------------------------------------------------------
/test/app/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 | from django.db import models, migrations
3 |
4 |
5 | class Migration(migrations.Migration):
6 | operations = [
7 | migrations.CreateModel(
8 | name='TestModel',
9 | fields=[
10 | ('id', models.AutoField(verbose_name='ID',
11 | serialize=False,
12 | auto_created=True,
13 | primary_key=True)),
14 | ('field', models.CharField(max_length=25))
15 | ],
16 | bases=(models.Model,)
17 | )
18 | ]
19 |
--------------------------------------------------------------------------------
/test/app/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AMeng/django-single-model-admin/85c7dcbedcf9264bfdfab24fdb27c96afbf6a7ee/test/app/migrations/__init__.py
--------------------------------------------------------------------------------
/test/app/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 |
4 | class TestModel(models.Model):
5 | field = models.CharField(max_length=25)
6 |
--------------------------------------------------------------------------------
/test/app/tests.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import User
2 | from django.test import Client, TestCase
3 | try:
4 | from django.core.urlresolvers import reverse
5 | except ImportError:
6 | from django.urls import reverse
7 |
8 | from app.models import TestModel
9 |
10 |
11 | class AbstractTestCase(TestCase):
12 | def setUp(self):
13 | User.objects.create_superuser('user', 'email@test.com', 'password')
14 | self.client = Client()
15 | self.client.login(username='user', password='password')
16 |
17 | def assertMessage(self, response, message):
18 | messages = [m.message for m in response.context['messages']]
19 | error_text = 'Message "{0}" not found. Messages: {1}'.format(message,
20 | messages)
21 | self.assertTrue(message in messages, error_text)
22 |
23 |
24 | class NoObjectsTestCase(AbstractTestCase):
25 |
26 | def test_can_add_new_model(self):
27 | response = self.client.get(reverse('admin:app_testmodel_add'),
28 | follow=True)
29 | self.assertEqual(response.status_code, 200)
30 |
31 | def test_changelist_redirects_to_add(self):
32 | response = self.client.get(reverse('admin:app_testmodel_changelist'),
33 | follow=True)
34 | self.assertRedirects(response, reverse('admin:app_testmodel_add'))
35 |
36 | def test_renders_add_button(self):
37 | response = self.client.get(reverse('admin:app_list', args=['app']),
38 | follow=True)
39 | self.assertContains(
40 | response,
41 | 'Add',
42 | html=True)
43 |
44 |
45 | class SingleObjectTestCase(AbstractTestCase):
46 |
47 | def setUp(self):
48 | super(SingleObjectTestCase, self).setUp()
49 | TestModel.objects.create(field='value')
50 |
51 | def test_cannot_add_new_model(self):
52 | response = self.client.get(reverse('admin:app_testmodel_add'),
53 | follow=True)
54 | self.assertRedirects(response,
55 | reverse('admin:app_testmodel_change', args=[1]))
56 | self.assertMessage(
57 | response,
58 | 'Do not add additional instances of testmodel. Only one is needed.'
59 | )
60 |
61 | def test_cannot_see_changelist(self):
62 | response = self.client.get(reverse('admin:app_testmodel_changelist'),
63 | follow=True)
64 | self.assertRedirects(response,
65 | reverse('admin:app_testmodel_change', args=[1]))
66 |
67 | def test_does_not_render_add_button(self):
68 | response = self.client.get(reverse('admin:app_list', args=['app']),
69 | follow=True)
70 | self.assertNotContains(
71 | response,
72 | 'Add',
73 | html=True)
74 |
75 |
76 | class MultipleObjectsTestCase(AbstractTestCase):
77 |
78 | def setUp(self):
79 | super(MultipleObjectsTestCase, self).setUp()
80 | TestModel.objects.create(field='value1')
81 | TestModel.objects.create(field='value2')
82 | TestModel.objects.create(field='value3')
83 |
84 | def test_cannot_add_new_model(self):
85 | response = self.client.get(reverse('admin:app_testmodel_add'),
86 | follow=True)
87 | self.assertRedirects(response,
88 | reverse('admin:app_testmodel_changelist'))
89 | self.assertMessage(
90 | response,
91 | 'Do not add additional instances of testmodel. Only one is needed.'
92 | )
93 |
94 | def test_can_see_changelist(self):
95 | response = self.client.get(reverse('admin:app_testmodel_changelist'),
96 | follow=True)
97 | self.assertEqual(response.status_code, 200)
98 | self.assertMessage(
99 | response,
100 | ('There are multiple instances of testmodel. There should only be '
101 | 'one.'))
102 |
103 | def test_does_not_render_add_button(self):
104 | response = self.client.get(reverse('admin:app_list', args=['app']),
105 | follow=True)
106 | self.assertNotContains(
107 | response,
108 | 'Add',
109 | html=True)
110 |
--------------------------------------------------------------------------------
/test/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
7 |
8 | from django.core.management import execute_from_command_line
9 |
10 | execute_from_command_line(sys.argv)
11 |
--------------------------------------------------------------------------------
/test/project/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AMeng/django-single-model-admin/85c7dcbedcf9264bfdfab24fdb27c96afbf6a7ee/test/project/__init__.py
--------------------------------------------------------------------------------
/test/project/settings.py:
--------------------------------------------------------------------------------
1 | import os
2 | BASE_DIR = os.path.dirname(os.path.dirname(__file__))
3 | SECRET_KEY = '$jq0^7*hj$7_!2$$bmm^ay@2g(8p4=9!n+ysmxajuhnj*z%ds6'
4 | DEBUG = True
5 | ALLOWED_HOSTS = []
6 |
7 | INSTALLED_APPS = (
8 | 'django.contrib.admin',
9 | 'django.contrib.auth',
10 | 'django.contrib.contenttypes',
11 | 'django.contrib.sessions',
12 | 'django.contrib.messages',
13 | 'django.contrib.staticfiles',
14 | 'app'
15 | )
16 |
17 | MIDDLEWARE_CLASSES = (
18 | 'django.contrib.sessions.middleware.SessionMiddleware',
19 | 'django.middleware.common.CommonMiddleware',
20 | 'django.middleware.csrf.CsrfViewMiddleware',
21 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
22 | 'django.contrib.messages.middleware.MessageMiddleware',
23 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
24 | )
25 |
26 | MIDDLEWARE = MIDDLEWARE_CLASSES # Renamed variable in Django 1.10
27 |
28 | ROOT_URLCONF = 'project.urls'
29 | WSGI_APPLICATION = 'project.wsgi.application'
30 |
31 | TEMPLATES = [
32 | {
33 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
34 | 'DIRS': [],
35 | 'APP_DIRS': True,
36 | 'OPTIONS': {
37 | 'context_processors': [
38 | 'django.template.context_processors.debug',
39 | 'django.template.context_processors.request',
40 | 'django.contrib.auth.context_processors.auth',
41 | 'django.contrib.messages.context_processors.messages',
42 | ],
43 | },
44 | },
45 | ]
46 |
47 | DATABASES = {
48 | 'default': {
49 | 'ENGINE': 'django.db.backends.sqlite3',
50 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
51 | }
52 | }
53 | LANGUAGE_CODE = 'en-us'
54 | TIME_ZONE = 'UTC'
55 | USE_I18N = True
56 | USE_L10N = True
57 | USE_TZ = True
58 | STATIC_URL = '/static/'
59 |
--------------------------------------------------------------------------------
/test/project/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import include, url
2 | from django.contrib import admin
3 |
4 | admin.autodiscover()
5 |
6 | try:
7 | from django.conf.urls import patterns
8 | except ImportError:
9 | urlpatterns = [url(r'^admin/', admin.site.urls)]
10 | else:
11 | urlpatterns = patterns('', url(r'^admin/', include(admin.site.urls)))
12 |
--------------------------------------------------------------------------------
/test/project/wsgi.py:
--------------------------------------------------------------------------------
1 | import os
2 | from django.core.wsgi import get_wsgi_application
3 |
4 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
5 | application = get_wsgi_application()
6 |
--------------------------------------------------------------------------------