├── .gitignore ├── .travis.yml ├── LICENSE ├── README.rst ├── django_admin_smoke_tests ├── __init__.py └── tests.py ├── manage.py ├── setup.cfg ├── setup.py ├── test_project ├── __init__.py ├── main │ ├── __init__.py │ ├── admin.py │ ├── models.py │ ├── tests.py │ └── urls.py ├── settings.py ├── urls.py └── wsgi.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: python 4 | 5 | python: 6 | - "2.7" 7 | - "3.5" 8 | 9 | env: 10 | - DJANGO_VERSION=dj17 11 | - DJANGO_VERSION=dj18 12 | - DJANGO_VERSION=dj19 13 | - DJANGO_VERSION=dj110 14 | - DJANGO_VERSION=djdev 15 | 16 | matrix: 17 | allow_failures: 18 | - env: DJANGO_VERSION=djdev 19 | exclude: 20 | - python: "3.5" 21 | env: DJANGO_VERSION=dj17 22 | include: 23 | - python: "2.7" 24 | env: MODE=flake8 25 | - python: "3.4" 26 | env: DJANGO_VERSION=dj17 27 | 28 | cache: 29 | directories: 30 | - $HOME/.cache/pip 31 | 32 | before_cache: 33 | - rm -f $HOME/.cache/pip/log/debug.log 34 | 35 | install: 36 | - pip install -U pip 37 | - pip install -U wheel virtualenv 38 | - pip install tox coveralls 39 | - python setup.py install 40 | 41 | after_success: 42 | - coveralls 43 | 44 | script: 45 | - coverage erase 46 | - tox -e py${TRAVIS_PYTHON_VERSION/./}-${DJANGO_VERSION}${MODE} 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, via680 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 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | * Neither the name of the {organization} nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software 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 HOLDER 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 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======================== 2 | django-admin-smoke-tests 3 | ======================== 4 | 5 | .. image:: https://travis-ci.org/greyside/django-admin-smoke-tests.svg?branch=master 6 | :target: https://travis-ci.org/greyside/django-admin-smoke-tests 7 | .. image:: https://coveralls.io/repos/greyside/django-admin-smoke-tests/badge.png?branch=master 8 | :target: https://coveralls.io/r/greyside/django-admin-smoke-tests?branch=master 9 | 10 | Run with ``./manage.py test django_admin_smoke_tests.tests``. 11 | 12 | You don't have to add anything ``INSTALLED_APPS`` 13 | 14 | Usage in your tests 15 | ------------------- 16 | 17 | Import into your own code: 18 | 19 | .. code:: python 20 | 21 | from django.test import TestCase 22 | from django_admin_smoke_tests.tests import AdminSiteSmokeTestMixin 23 | 24 | class AdminSiteSmokeTest(AdminSiteSmokeTestMixin, TestCase): 25 | def setUp(self): 26 | super(AdminSiteSmokeTest, self).setUp() 27 | # custom setup goes here 28 | 29 | If you want to use admin smoke tests as part of your tests with data from fixtures, 30 | you can do following: 31 | 32 | .. code:: python 33 | 34 | from django.test import TestCase 35 | from django_admin_smoke_tests.tests import AdminSiteSmokeTestMixin 36 | 37 | class AdminSiteSmokeTest(AdminSiteSmokeTestMixin, TestCase): 38 | fixtures = ['data'] 39 | 40 | And you can exclude certain (external) apps or model admins with:: 41 | 42 | exclude_apps = ['constance',] 43 | exclude_modeladmins = [apps.admin.ModelAdmin] 44 | -------------------------------------------------------------------------------- /django_admin_smoke_tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) 2014 via680 3 | # 4 | # Licensed under a BSD 3-Clause License. See LICENSE file. 5 | 6 | VERSION = (0, 3, 0) 7 | 8 | __version__ = "".join([".".join(map(str, VERSION[0:3])), "".join(VERSION[3:])]) 9 | -------------------------------------------------------------------------------- /django_admin_smoke_tests/tests.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import django 4 | 5 | from django.contrib import admin, auth 6 | from django.core.exceptions import ObjectDoesNotExist, PermissionDenied,\ 7 | ValidationError 8 | from django.http.request import QueryDict 9 | from django.test import TestCase 10 | from django.test.client import RequestFactory 11 | 12 | import six 13 | 14 | 15 | class ModelAdminCheckException(Exception): 16 | def __init__(self, message, original_exception): 17 | self.original_exception = original_exception 18 | super(ModelAdminCheckException, self).__init__(message) 19 | 20 | 21 | def for_all_model_admins(fn): 22 | def test_deco(self): 23 | for model, model_admin in self.modeladmins: 24 | if model_admin.__class__ in self.exclude_modeladmins: 25 | continue 26 | if model._meta.app_label in self.exclude_apps: 27 | continue 28 | try: 29 | fn(self, model, model_admin) 30 | except Exception as e: 31 | if six.PY2: 32 | # Approximate Py3's `raise ModelAdminCheckException from e` 33 | # by raising e's traceback with some extra information 34 | # prepended to the error message. Specifically handle PY2 35 | # this way because `six.raise_from` just throws away the 36 | # second argument and swallows the original exception under 37 | # PY2. 38 | six.reraise(ModelAdminCheckException( 39 | "%s occured while running test '%s' " 40 | "on modeladmin %s (%s): %s" % ( 41 | e.__class__.__name__, 42 | fn.__name__, 43 | model_admin, 44 | model.__name__, 45 | e), 46 | e), None, sys.exc_info()[2]) 47 | else: 48 | six.raise_from(ModelAdminCheckException( 49 | "Above exception occured while running test '%s' " 50 | "on modeladmin %s (%s)" % 51 | (fn.__name__, model_admin, model.__name__), 52 | e), e) 53 | return test_deco 54 | 55 | 56 | class AdminSiteSmokeTestMixin(object): 57 | modeladmins = None 58 | exclude_apps = [] 59 | exclude_modeladmins = [] 60 | fixtures = ['django_admin_smoke_tests'] 61 | 62 | single_attributes = ['date_hierarchy'] 63 | iter_attributes = [ 64 | 'filter_horizontal', 65 | 'filter_vertical', 66 | 'list_display', 67 | 'list_display_links', 68 | 'list_editable', 69 | 'list_filter', 70 | 'readonly_fields', 71 | 'search_fields', 72 | ] 73 | iter_or_falsy_attributes = [ 74 | 'exclude', 75 | 'fields', 76 | 'ordering', 77 | ] 78 | 79 | strip_minus_attrs = ('ordering',) 80 | 81 | def setUp(self): 82 | super(AdminSiteSmokeTestMixin, self).setUp() 83 | 84 | self.superuser = auth.get_user_model().objects.create_superuser( 85 | 'testuser', 'testuser@example.com', 'foo') 86 | 87 | self.factory = RequestFactory() 88 | 89 | if not self.modeladmins: 90 | self.modeladmins = admin.site._registry.items() 91 | 92 | try: 93 | admin.autodiscover() 94 | except: 95 | pass 96 | 97 | def get_request(self, params=None): 98 | request = self.factory.get('/', params) 99 | 100 | request.user = self.superuser 101 | return request 102 | 103 | def post_request(self, post_data={}, params=None): 104 | request = self.factory.post('/', params, post_data=post_data) 105 | 106 | request.user = self.superuser 107 | request._dont_enforce_csrf_checks = True 108 | return request 109 | 110 | def strip_minus(self, attr, val): 111 | if attr in self.strip_minus_attrs and val[0] == '-': 112 | val = val[1:] 113 | return val 114 | 115 | def get_fieldsets(self, model, model_admin): 116 | request = self.get_request() 117 | return model_admin.get_fieldsets(request, obj=model()) 118 | 119 | def get_attr_set(self, model, model_admin): 120 | attr_set = [] 121 | 122 | for attr in self.iter_attributes: 123 | attr_set += [ 124 | self.strip_minus(attr, a) 125 | for a in getattr(model_admin, attr) 126 | ] 127 | 128 | for attr in self.iter_or_falsy_attributes: 129 | attrs = getattr(model_admin, attr, None) 130 | 131 | if isinstance(attrs, list) or isinstance(attrs, tuple): 132 | attr_set += [self.strip_minus(attr, a) for a in attrs] 133 | 134 | for fieldset in self.get_fieldsets(model, model_admin): 135 | for attr in fieldset[1]['fields']: 136 | if isinstance(attr, list) or isinstance(attr, tuple): 137 | attr_set += [self.strip_minus(fieldset, a) 138 | for a in attr] 139 | else: 140 | attr_set.append(attr) 141 | 142 | attr_set = set(attr_set) 143 | 144 | for attr in self.single_attributes: 145 | val = getattr(model_admin, attr, None) 146 | 147 | if val: 148 | attr_set.add(self.strip_minus(attr, val)) 149 | 150 | return attr_set 151 | 152 | @for_all_model_admins 153 | def test_specified_fields(self, model, model_admin): 154 | attr_set = self.get_attr_set(model, model_admin) 155 | 156 | # FIXME: not all attributes can be used everywhere (e.g. you can't 157 | # use list_filter with a form field). This will have to be fixed 158 | # later. 159 | try: 160 | model_field_names = frozenset(model._meta.get_fields()) 161 | except AttributeError: # Django<1.10 162 | model_field_names = frozenset( 163 | model._meta.get_all_field_names() 164 | ) 165 | form_field_names = frozenset(getattr(model_admin.form, 166 | 'base_fields', [])) 167 | 168 | model_instance = model() 169 | 170 | for attr in attr_set: 171 | # for now we'll just check attributes, not strings 172 | if not isinstance(attr, six.string_types): 173 | continue 174 | 175 | # don't split attributes that start with underscores (such as 176 | # __str__) 177 | if attr[0] != '_': 178 | attr = attr.split('__')[0] 179 | 180 | has_model_field = attr in model_field_names 181 | has_form_field = attr in form_field_names 182 | has_model_class_attr = hasattr(model_instance.__class__, attr) 183 | has_admin_attr = hasattr(model_admin, attr) 184 | 185 | try: 186 | has_model_attr = hasattr(model_instance, attr) 187 | except (ValueError, ObjectDoesNotExist): 188 | has_model_attr = attr in model_instance.__dict__ 189 | 190 | has_field_or_attr = has_model_field or has_form_field or\ 191 | has_model_attr or has_admin_attr or has_model_class_attr 192 | 193 | self.assertTrue(has_field_or_attr, '%s not found on %s (%s)' % 194 | (attr, model, model_admin,)) 195 | 196 | @for_all_model_admins 197 | def test_queryset(self, model, model_admin): 198 | request = self.get_request() 199 | 200 | # TODO: use model_mommy to generate a few instances to query against 201 | # make sure no errors happen here 202 | if hasattr(model_admin, 'get_queryset'): 203 | list(model_admin.get_queryset(request)) 204 | 205 | @for_all_model_admins 206 | def test_get_absolute_url(self, model, model_admin): 207 | if hasattr(model, 'get_absolute_url'): 208 | # Use fixture data if it exists 209 | instance = model.objects.first() 210 | # Otherwise create a minimal instance 211 | if not instance: 212 | instance = model(pk=1) 213 | # make sure no errors happen here 214 | instance.get_absolute_url() 215 | 216 | @for_all_model_admins 217 | def test_changelist_view(self, model, model_admin): 218 | request = self.get_request() 219 | 220 | # make sure no errors happen here 221 | try: 222 | response = model_admin.changelist_view(request) 223 | response.render() 224 | self.assertEqual(response.status_code, 200) 225 | except PermissionDenied: 226 | # this error is commonly raised by ModelAdmins that don't allow 227 | # changelist view 228 | pass 229 | 230 | @for_all_model_admins 231 | def test_changelist_view_search(self, model, model_admin): 232 | request = self.get_request(params=QueryDict('q=test')) 233 | 234 | # make sure no errors happen here 235 | try: 236 | response = model_admin.changelist_view(request) 237 | response.render() 238 | self.assertEqual(response.status_code, 200) 239 | except PermissionDenied: 240 | # this error is commonly raised by ModelAdmins that don't allow 241 | # changelist view. 242 | pass 243 | 244 | @for_all_model_admins 245 | def test_add_view(self, model, model_admin): 246 | request = self.get_request() 247 | 248 | # make sure no errors happen here 249 | try: 250 | response = model_admin.add_view(request) 251 | if isinstance(response, django.template.response.TemplateResponse): 252 | response.render() 253 | self.assertEqual(response.status_code, 200) 254 | except PermissionDenied: 255 | # this error is commonly raised by ModelAdmins that don't allow 256 | # adding. 257 | pass 258 | 259 | @for_all_model_admins 260 | def test_change_view(self, model, model_admin): 261 | item = model.objects.last() 262 | if not item or model._meta.proxy: 263 | return 264 | pk = item.pk 265 | request = self.get_request() 266 | 267 | # make sure no errors happen here 268 | response = model_admin.change_view(request, object_id=str(pk)) 269 | if isinstance(response, django.template.response.TemplateResponse): 270 | response.render() 271 | self.assertEqual(response.status_code, 200) 272 | 273 | @for_all_model_admins 274 | def test_change_post(self, model, model_admin): 275 | item = model.objects.last() 276 | if not item or model._meta.proxy: 277 | return 278 | pk = item.pk 279 | # TODO: If we generate default post_data for post request, 280 | # the test would be stronger 281 | request = self.post_request() 282 | try: 283 | response = model_admin.change_view(request, object_id=str(pk)) 284 | if isinstance(response, django.template.response.TemplateResponse): 285 | response.render() 286 | self.assertEqual(response.status_code, 200) 287 | except ValidationError: 288 | # This the form was sent, but did not pass it's validation 289 | pass 290 | 291 | 292 | class AdminSiteSmokeTest(AdminSiteSmokeTestMixin, TestCase): 293 | pass 294 | -------------------------------------------------------------------------------- /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", "test_project.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import django_admin_smoke_tests 5 | 6 | from setuptools import setup 7 | 8 | package_name = 'django_admin_smoke_tests' 9 | 10 | 11 | def runtests(): 12 | import os 13 | import sys 14 | 15 | import django 16 | from django.core.management import call_command 17 | 18 | os.environ['DJANGO_SETTINGS_MODULE'] = 'test_project.settings' 19 | if django.VERSION[0] == 1 and django.VERSION[1] >= 7: 20 | django.setup() 21 | call_command('test', 'test_project.main.tests') 22 | sys.exit() 23 | 24 | 25 | setup( 26 | name='django-admin-smoke-tests', 27 | version=django_admin_smoke_tests.__version__, 28 | description="Runs some quick tests on your admin site objects to make sure \ 29 | there aren't non-existant fields listed, etc.", 30 | author='Seán Hayes', 31 | author_email='sean@seanhayes.name', 32 | classifiers=[ 33 | "Development Status :: 3 - Alpha", 34 | "Intended Audience :: Developers", 35 | "License :: OSI Approved :: BSD License", 36 | "Programming Language :: Python", 37 | "Topic :: Software Development :: Build Tools", 38 | "Topic :: Software Development :: Libraries", 39 | "Topic :: Software Development :: Libraries :: Python Modules", 40 | ], 41 | keywords='django admin smoke test', 42 | url='https://github.com/SeanHayes/django-admin-smoke-tests', 43 | download_url='https://github.com/SeanHayes/django-admin-smoke-tests', 44 | license='BSD', 45 | install_requires=[ 46 | 'django>=1.6', 47 | 'six', 48 | ], 49 | packages=[ 50 | package_name, 51 | ], 52 | include_package_data=True, 53 | zip_safe=False, 54 | test_suite='setup.runtests', 55 | ) 56 | -------------------------------------------------------------------------------- /test_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greyside/django-admin-smoke-tests/80a05e4f7d4c11f3af84145ed70540d6e472ff8f/test_project/__init__.py -------------------------------------------------------------------------------- /test_project/main/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greyside/django-admin-smoke-tests/80a05e4f7d4c11f3af84145ed70540d6e472ff8f/test_project/main/__init__.py -------------------------------------------------------------------------------- /test_project/main/admin.py: -------------------------------------------------------------------------------- 1 | # Django imports 2 | import django 3 | 4 | from django.contrib import admin 5 | from django.contrib.admin import SimpleListFilter 6 | 7 | # App imports 8 | from .models import Channel, FailPost, ForbiddenPost,\ 9 | HasPrimarySlug, HasPrimaryUUID, Post 10 | 11 | 12 | class ChannelAdmin(admin.ModelAdmin): 13 | prepopulated_fields = {"slug": ("title",)} 14 | 15 | 16 | admin.site.register(Channel, ChannelAdmin) 17 | 18 | 19 | class ListFilter(SimpleListFilter): 20 | title = "list_filter" 21 | parameter_name = "list_filter" 22 | 23 | def __init__(self, request, params, model, model_admin): 24 | super(ListFilter, self).__init__(request, params, model, model_admin) 25 | self.lookup_val = request.GET.getlist('a') 26 | 27 | def lookups(self, request, model_admin): 28 | return () 29 | 30 | def queryset(self, request, queryset): 31 | return queryset 32 | 33 | 34 | class PostAdmin(admin.ModelAdmin): 35 | prepopulated_fields = {"slug": ("title",)} 36 | list_editable = ['status'] 37 | list_display = ('title', 'author', 'status', 'modified', 'published',) 38 | list_filter = ('author', 'status', 'channel', 'created', 'modified', 39 | 'published', ListFilter) 40 | readonly_fields = ['created', 'modified', 'time_diff'] 41 | ordering = ('title', '-id',) 42 | fieldsets = [('Fielset', { 43 | 'fields': ['created', ('slug', 'title', 'author', 'status')]}), 44 | ] 45 | date_hierarchy = 'created' 46 | 47 | search_fields = ['title', 'text'] 48 | 49 | def formfield_for_foreignkey(self, db_field, request, **kwargs): 50 | if db_field.name == 'author': 51 | db_field.default = request.user 52 | return super(PostAdmin, self).formfield_for_foreignkey(db_field, 53 | request, **kwargs) 54 | 55 | 56 | class FailPostAdmin(admin.ModelAdmin): 57 | search_fields = ['nonexistent_field'] 58 | 59 | if django.VERSION >= (1, 8): 60 | list_display = ['nonexistent_field'] 61 | 62 | 63 | class ForbiddenPostAdmin(admin.ModelAdmin): 64 | def has_add_permission(self, request): 65 | False 66 | 67 | def has_change_permission(self, request, obj=None): 68 | False 69 | 70 | def has_delete_permission(self, request): 71 | False 72 | 73 | 74 | admin.site.register(Post, PostAdmin) 75 | admin.site.register(FailPost, FailPostAdmin) 76 | admin.site.register(ForbiddenPost, ForbiddenPostAdmin) 77 | 78 | 79 | admin.site.register(HasPrimarySlug) 80 | 81 | 82 | if HasPrimaryUUID: 83 | admin.site.register(HasPrimaryUUID) 84 | -------------------------------------------------------------------------------- /test_project/main/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | # Django imports 4 | from django.conf import settings 5 | from django.core.urlresolvers import reverse 6 | from django.db import models 7 | from django.utils import timezone 8 | 9 | 10 | class _Abstract(models.Model): 11 | slug = models.SlugField(unique=True) 12 | title = models.CharField(max_length=140, unique=True) 13 | text = models.TextField(default='') 14 | rendered_text = models.TextField(default='', blank=True) 15 | 16 | def __unicode__(self): 17 | return self.title 18 | 19 | class Meta: 20 | abstract = True 21 | 22 | 23 | class Channel(_Abstract): 24 | followers = models.ManyToManyField(settings.AUTH_USER_MODEL) 25 | 26 | ENROLLMENTS = ( 27 | (0, 'Self'), 28 | (1, 'Author'), 29 | ) 30 | 31 | public = models.BooleanField(default=True, 32 | help_text="If False, only followers will be able to see content.") 33 | 34 | enrollment = models.IntegerField(max_length=1, default=0, 35 | choices=ENROLLMENTS) 36 | 37 | class Meta: 38 | ordering = ['title'] 39 | 40 | 41 | class Post(_Abstract): 42 | SUMMARY_LENGTH = 50 43 | 44 | STATUSES = [ 45 | (0, 'Draft',), 46 | (1, 'Published',), 47 | ] 48 | 49 | channel = models.ForeignKey(Channel) 50 | author = models.ForeignKey(settings.AUTH_USER_MODEL) 51 | status = models.IntegerField(max_length=1, default=0, choices=STATUSES) 52 | custom_summary = models.TextField(default='') 53 | created = models.DateTimeField(auto_now_add=True, editable=False) 54 | modified = models.DateTimeField(auto_now=True, editable=False) 55 | published = models.DateTimeField(default=timezone.now()) 56 | 57 | @property 58 | def teaser(self): 59 | """ 60 | A small excerpt of text that can be used in the absence of a custom 61 | summary. 62 | """ 63 | return self.text[:Post.SUMMARY_LENGTH] 64 | 65 | @property 66 | def summary(self): 67 | "Returns custom_summary, or teaser if not available." 68 | if len(self.custom_summary) > 0: 69 | return self.custom_summary 70 | else: 71 | return self.teaser 72 | 73 | @property 74 | def time_diff(self): 75 | if self.modified and self.created: 76 | return self.modified - self.created 77 | return None 78 | 79 | class Meta: 80 | ordering = ['published'] 81 | 82 | def get_absolute_url(self): 83 | return reverse('post-detail', kwargs={'pk': self.pk}) 84 | 85 | 86 | class ForbiddenPost(Post): 87 | pass 88 | 89 | 90 | class FailPost(Post): 91 | pass 92 | 93 | 94 | class HasPrimarySlug(models.Model): 95 | slug = models.SlugField(primary_key=True) 96 | title = models.CharField(max_length=140, unique=True) 97 | 98 | def get_absolute_url(self): 99 | return reverse('hasprimaryslug-detail', kwargs={'pk': self.pk}) 100 | 101 | 102 | HasPrimaryUUID = None 103 | 104 | if hasattr(models, 'UUIDField'): 105 | class HasPrimaryUUID(models.Model): 106 | id = models.UUIDField( 107 | primary_key=True, default=uuid.uuid4, editable=False) 108 | title = models.CharField(max_length=140, unique=True) 109 | 110 | def get_absolute_url(self): 111 | return reverse('hasprimaryuuid-detail', kwargs={'pk': self.pk}) 112 | -------------------------------------------------------------------------------- /test_project/main/tests.py: -------------------------------------------------------------------------------- 1 | import django 2 | 3 | from django.test import TestCase 4 | 5 | from django_admin_smoke_tests.tests import AdminSiteSmokeTestMixin,\ 6 | ModelAdminCheckException, for_all_model_admins 7 | from .admin import ChannelAdmin, FailPostAdmin, ForbiddenPostAdmin, PostAdmin 8 | 9 | 10 | class AdminSiteSmokeTest(AdminSiteSmokeTestMixin, TestCase): 11 | fixtures = [] 12 | exclude_apps = ['auth'] 13 | exclude_modeladmins = [FailPostAdmin, ForbiddenPostAdmin] 14 | 15 | 16 | class FailAdminSiteSmokeTest(AdminSiteSmokeTestMixin, TestCase): 17 | fixtures = [] 18 | exclude_modeladmins = [ForbiddenPostAdmin, PostAdmin, ChannelAdmin] 19 | 20 | @for_all_model_admins 21 | def test_specified_fields(self, model, model_admin): 22 | with self.assertRaises(ModelAdminCheckException): 23 | super(FailAdminSiteSmokeTest, self).test_specified_fields() 24 | 25 | @for_all_model_admins 26 | def test_changelist_view_search(self, model, model_admin): 27 | with self.assertRaises(ModelAdminCheckException): 28 | super(FailAdminSiteSmokeTest, self).test_changelist_view_search() 29 | 30 | if django.VERSION >= (1, 8): 31 | @for_all_model_admins 32 | def test_changelist_view(self, model, model_admin): 33 | with self.assertRaises(ModelAdminCheckException): 34 | super(FailAdminSiteSmokeTest, self).test_changelist_view() 35 | 36 | 37 | class ForbiddenAdminSiteSmokeTest(AdminSiteSmokeTestMixin, TestCase): 38 | fixtures = [] 39 | exclude_modeladmins = [FailPostAdmin, PostAdmin, ChannelAdmin] 40 | -------------------------------------------------------------------------------- /test_project/main/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | 4 | urlpatterns = [ 5 | url(r'^posts/(?P.+)/$', lambda **kwargs: '', name="post-detail"), 6 | url(r'^hasprimaryslug/(?P[\w-]+)/$', lambda **kwargs: '', 7 | name="hasprimaryslug-detail"), 8 | url(r'^hasprimaryuuid/(?P[\w-]+)/$', lambda **kwargs: '', 9 | name="hasprimaryuuid-detail"), 10 | ] 11 | -------------------------------------------------------------------------------- /test_project/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for test_project project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.7/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.7/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | import os 13 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 14 | 15 | 16 | # Quick-start development settings - unsuitable for production 17 | # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ 18 | 19 | # SECURITY WARNING: keep the secret key used in production secret! 20 | SECRET_KEY = 'f8fkupu8pa%%u$wgk6c!os39el41v7i7^u*8xf#g5p@+c&)b#^' 21 | 22 | # SECURITY WARNING: don't run with debug turned on in production! 23 | DEBUG = True 24 | 25 | TEMPLATES = [ 26 | { 27 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 28 | 'APP_DIRS': True, 29 | 'OPTIONS': { 30 | 'debug': True, 31 | 'context_processors': [ 32 | 'django.contrib.auth.context_processors.auth', 33 | 'django.template.context_processors.debug', 34 | 'django.template.context_processors.i18n', 35 | 'django.template.context_processors.media', 36 | 'django.template.context_processors.static', 37 | 'django.template.context_processors.tz', 38 | 'django.contrib.messages.context_processors.messages', 39 | ], 40 | }, 41 | }, 42 | ] 43 | 44 | ALLOWED_HOSTS = [] 45 | 46 | 47 | # Application definition 48 | 49 | INSTALLED_APPS = ( 50 | 'django.contrib.admin', 51 | 'django.contrib.auth', 52 | 'django.contrib.contenttypes', 53 | 'django.contrib.sessions', 54 | 'django.contrib.messages', 55 | 'django.contrib.staticfiles', 56 | 'test_project.main', 57 | ) 58 | 59 | MIDDLEWARE_CLASSES = ( 60 | 'django.contrib.sessions.middleware.SessionMiddleware', 61 | 'django.middleware.common.CommonMiddleware', 62 | 'django.middleware.csrf.CsrfViewMiddleware', 63 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 64 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 65 | 'django.contrib.messages.middleware.MessageMiddleware', 66 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 67 | ) 68 | 69 | ROOT_URLCONF = 'test_project.urls' 70 | 71 | WSGI_APPLICATION = 'test_project.wsgi.application' 72 | 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/1.7/ref/settings/#databases 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'ENGINE': 'django.db.backends.sqlite3', 80 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 81 | } 82 | } 83 | 84 | # Internationalization 85 | # https://docs.djangoproject.com/en/1.7/topics/i18n/ 86 | 87 | LANGUAGE_CODE = 'en-us' 88 | 89 | TIME_ZONE = 'UTC' 90 | 91 | USE_I18N = True 92 | 93 | USE_L10N = True 94 | 95 | USE_TZ = True 96 | 97 | 98 | # Static files (CSS, JavaScript, Images) 99 | # https://docs.djangoproject.com/en/1.7/howto/static-files/ 100 | 101 | STATIC_URL = '/static/' 102 | -------------------------------------------------------------------------------- /test_project/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | from django.contrib import admin 3 | 4 | 5 | urlpatterns = [ 6 | url(r'^admin/', include(admin.site.urls)), 7 | url(r'^main/', include('test_project.main.urls')), 8 | ] 9 | -------------------------------------------------------------------------------- /test_project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for test_project project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{35,27}-djdev, 4 | py{35,27}-dj110, 5 | py{35,27}-dj19, 6 | py{35,27}-dj18, 7 | py{34,27}-dj17, 8 | py27-flake8, 9 | skipsdist=True 10 | 11 | [testenv] 12 | usedevelop=True 13 | test-executable = 14 | python --version 15 | {envbindir}/python -Wall {envbindir}/coverage run --append --source=django_admin_smoke_tests 16 | commands = 17 | dj{17,18,19,110,dev}: {[testenv]test-executable} setup.py test 18 | basepython = 19 | py27: python2.7 20 | py34: python3.4 21 | py35: python3.5 22 | deps = 23 | dj17: Django>=1.7,<1.8 24 | dj18: Django>=1.8,<1.9 25 | dj19: Django>=1.9,<1.10 26 | dj110: Django>=1.10,<1.11 27 | djdev: https://github.com/django/django/archive/master.tar.gz 28 | dj{17,18,19,110,dev}: coverage 29 | dj{17,18,19,110,dev}: ipdb 30 | dj{17,18,19,110,dev}: pytz 31 | 32 | [testenv:py27-flake8] 33 | deps = 34 | flake8 35 | flake8-import-order 36 | commands = 37 | {envbindir}/flake8 --ignore=E128 --max-complexity 10 . 38 | --------------------------------------------------------------------------------