diff --git a/.travis.yml b/.travis.yml index f535dd5f..af94fd8f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,7 @@ env: - DJANGO_VERSION=dj17 - DJANGO_VERSION=dj18 - DJANGO_VERSION=dj19 + - DJANGO_VERSION=dj110 - DJANGO_VERSION=djdev matrix: @@ -21,6 +22,8 @@ matrix: include: - python: "2.7" env: MODE=flake8 + - python: "3.4" + env: DJANGO_VERSION=dj17 cache: directories: diff --git a/README.rst b/README.rst index 9534408a..56812f09 100644 --- a/README.rst +++ b/README.rst @@ -37,6 +37,7 @@ you can do following: class AdminSiteSmokeTest(AdminSiteSmokeTestMixin, TestCase): fixtures = ['data'] -And you can exclude certain (external) apps with:: +And you can exclude certain (external) apps or model admins with:: exclude_apps = ['constance',] + exclude_modeladmins = [apps.admin.ModelAdmin] diff --git a/django_admin_smoke_tests/tests.py b/django_admin_smoke_tests/tests.py index c7723bb4..011ca607 100644 --- a/django_admin_smoke_tests/tests.py +++ b/django_admin_smoke_tests/tests.py @@ -1,31 +1,43 @@ -import six +import django from django.contrib import admin, auth -from django.core.exceptions import ObjectDoesNotExist, PermissionDenied +from django.core.exceptions import ObjectDoesNotExist, PermissionDenied,\ + ValidationError +from django.http.request import QueryDict from django.test import TestCase from django.test.client import RequestFactory -import sys + +import six + + +class ModelAdminCheckException(Exception): + def __init__(self, message, original_exception): + self.original_exception = original_exception + return super(ModelAdminCheckException, self).__init__(message) def for_all_model_admins(fn): def test_deco(self): for model, model_admin in self.modeladmins: + if model_admin.__class__ in self.exclude_modeladmins: + continue if model._meta.app_label in self.exclude_apps: continue try: fn(self, model, model_admin) - except Exception: - raise Exception( - "Above exception occured while running test '%s'" + except Exception as e: + six.raise_from(ModelAdminCheckException( + "Above exception occured while running test '%s' " "on modeladmin %s (%s)" % - (fn.__name__, model_admin, model.__name__)).\ - with_traceback(sys.exc_info()[2]) + (fn.__name__, model_admin, model.__name__), + e), e) return test_deco class AdminSiteSmokeTestMixin(object): modeladmins = None exclude_apps = [] + exclude_modeladmins = [] fixtures = ['django_admin_smoke_tests'] single_attributes = ['date_hierarchy'] @@ -63,11 +75,19 @@ def setUp(self): except: pass - def get_request(self): - request = self.factory.get('/') + def get_request(self, params=None): + request = self.factory.get('/', params) + request.user = self.superuser return request + def post_request(self, post_data={}, params=None): + request = self.factory.post('/', params, post_data=post_data) + + request.user = self.superuser + request._dont_enforce_csrf_checks = True + return request + def strip_minus(self, attr, val): if attr in self.strip_minus_attrs and val[0] == '-': val = val[1:] @@ -75,10 +95,7 @@ def strip_minus(self, attr, val): def get_fieldsets(self, model, model_admin): request = self.get_request() - try: - return model_admin.get_fieldsets(request, obj=model()) - except AttributeError: - return model_admin.declared_fieldsets + return model_admin.get_fieldsets(request, obj=model()) def get_attr_set(self, model, model_admin): attr_set = [] @@ -163,8 +180,6 @@ def test_queryset(self, model, model_admin): # TODO: use model_mommy to generate a few instances to query against # make sure no errors happen here - if hasattr(model_admin, 'queryset'): - list(model_admin.queryset(request)) if hasattr(model_admin, 'get_queryset'): list(model_admin.get_queryset(request)) @@ -184,8 +199,28 @@ def test_changelist_view(self, model, model_admin): request = self.get_request() # make sure no errors happen here - response = model_admin.changelist_view(request) - self.assertEqual(response.status_code, 200) + try: + response = model_admin.changelist_view(request) + response.render() + self.assertEqual(response.status_code, 200) + except PermissionDenied: + # this error is commonly raised by ModelAdmins that don't allow + # changelist view + pass + + @for_all_model_admins + def test_changelist_view_search(self, model, model_admin): + request = self.get_request(params=QueryDict('q=test')) + + # make sure no errors happen here + try: + response = model_admin.changelist_view(request) + response.render() + self.assertEqual(response.status_code, 200) + except PermissionDenied: + # this error is commonly raised by ModelAdmins that don't allow + # changelist view. + pass @for_all_model_admins def test_add_view(self, model, model_admin): @@ -194,12 +229,46 @@ def test_add_view(self, model, model_admin): # make sure no errors happen here try: response = model_admin.add_view(request) + if isinstance(response, django.template.response.TemplateResponse): + response.render() self.assertEqual(response.status_code, 200) except PermissionDenied: # this error is commonly raised by ModelAdmins that don't allow # adding. pass + @for_all_model_admins + def test_change_view(self, model, model_admin): + item = model.objects.last() + if not item or model._meta.proxy: + return + pk = item.pk + request = self.get_request() + + # make sure no errors happen here + response = model_admin.change_view(request, object_id=str(pk)) + if isinstance(response, django.template.response.TemplateResponse): + response.render() + self.assertEqual(response.status_code, 200) + + @for_all_model_admins + def test_change_post(self, model, model_admin): + item = model.objects.last() + if not item or model._meta.proxy: + return + pk = item.pk + # TODO: If we generate default post_data for post request, + # the test would be stronger + request = self.post_request() + try: + response = model_admin.change_view(request, object_id=str(pk)) + if isinstance(response, django.template.response.TemplateResponse): + response.render() + self.assertEqual(response.status_code, 200) + except ValidationError: + # This the form was sent, but did not pass it's validation + pass + class AdminSiteSmokeTest(AdminSiteSmokeTestMixin, TestCase): pass diff --git a/setup.py b/setup.py index 51f77398..df2eec2f 100755 --- a/setup.py +++ b/setup.py @@ -1,9 +1,10 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from setuptools import setup import django_admin_smoke_tests +from setuptools import setup + package_name = 'django_admin_smoke_tests' @@ -17,7 +18,7 @@ def runtests(): os.environ['DJANGO_SETTINGS_MODULE'] = 'test_project.settings' if django.VERSION[0] == 1 and django.VERSION[1] >= 7: django.setup() - call_command('test', 'django_admin_smoke_tests') + call_command('test', 'test_project.main.tests') sys.exit() setup(name='django-admin-smoke-tests', diff --git a/test_project/main/admin.py b/test_project/main/admin.py index 657a99eb..3cdd2793 100644 --- a/test_project/main/admin.py +++ b/test_project/main/admin.py @@ -1,8 +1,12 @@ # Django imports +import django + from django.contrib import admin +from django.contrib.admin import SimpleListFilter # App imports -from .models import Channel, HasPrimarySlug, HasPrimaryUUID, Post +from .models import Channel, FailPost, ForbiddenPost,\ + HasPrimarySlug, HasPrimaryUUID, Post class ChannelAdmin(admin.ModelAdmin): @@ -11,14 +15,33 @@ class ChannelAdmin(admin.ModelAdmin): admin.site.register(Channel, ChannelAdmin) +class ListFilter(SimpleListFilter): + title = "list_filter" + parameter_name = "list_filter" + + def __init__(self, request, params, model, model_admin): + super(ListFilter, self).__init__(request, params, model, model_admin) + self.lookup_val = request.GET.getlist('a') + + def lookups(self, request, model_admin): + return () + + def queryset(self, request, queryset): + return queryset + + class PostAdmin(admin.ModelAdmin): prepopulated_fields = {"slug": ("title",)} list_editable = ['status'] list_display = ('title', 'author', 'status', 'modified', 'published',) list_filter = ('author', 'status', 'channel', 'created', 'modified', - 'published',) + 'published', ListFilter) readonly_fields = ['created', 'modified', 'time_diff'] ordering = ('title', '-id',) + fieldsets = [('Fielset', { + 'fields': ['created', ('slug', 'title', 'author', 'status')]}), + ] + date_hierarchy = 'created' search_fields = ['title', 'text'] @@ -28,7 +51,28 @@ def formfield_for_foreignkey(self, db_field, request, **kwargs): return super(PostAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) + +class FailPostAdmin(admin.ModelAdmin): + search_fields = ['nonexistent_field'] + + if django.VERSION >= (1, 8): + list_display = ['nonexistent_field'] + + +class ForbiddenPostAdmin(admin.ModelAdmin): + def has_add_permission(self, request): + False + + def has_change_permission(self, request, obj=None): + False + + def has_delete_permission(self, request): + False + + admin.site.register(Post, PostAdmin) +admin.site.register(FailPost, FailPostAdmin) +admin.site.register(ForbiddenPost, ForbiddenPostAdmin) admin.site.register(HasPrimarySlug) diff --git a/test_project/main/models.py b/test_project/main/models.py index 118c7fe5..6eab8427 100644 --- a/test_project/main/models.py +++ b/test_project/main/models.py @@ -1,10 +1,10 @@ import uuid # Django imports +from django.conf import settings from django.core.urlresolvers import reverse from django.db import models from django.utils import timezone -from django.conf import settings class _Abstract(models.Model): @@ -83,6 +83,14 @@ def get_absolute_url(self): return reverse('post-detail', kwargs={'pk': self.pk}) +class ForbiddenPost(Post): + pass + + +class FailPost(Post): + pass + + class HasPrimarySlug(models.Model): slug = models.SlugField(primary_key=True) title = models.CharField(max_length=140, unique=True) diff --git a/test_project/main/tests.py b/test_project/main/tests.py new file mode 100644 index 00000000..ddd943d4 --- /dev/null +++ b/test_project/main/tests.py @@ -0,0 +1,38 @@ +import django + +from django.test import TestCase +from django_admin_smoke_tests.tests import AdminSiteSmokeTestMixin,\ + ModelAdminCheckException, for_all_model_admins +from .admin import ChannelAdmin, FailPostAdmin, ForbiddenPostAdmin, PostAdmin + + +class AdminSiteSmokeTest(AdminSiteSmokeTestMixin, TestCase): + fixtures = [] + exclude_apps = ['auth'] + exclude_modeladmins = [FailPostAdmin, ForbiddenPostAdmin] + + +class FailAdminSiteSmokeTest(AdminSiteSmokeTestMixin, TestCase): + fixtures = [] + exclude_modeladmins = [ForbiddenPostAdmin, PostAdmin, ChannelAdmin] + + @for_all_model_admins + def test_specified_fields(self, model, model_admin): + with self.assertRaises(ModelAdminCheckException): + super(FailAdminSiteSmokeTest, self).test_specified_fields() + + @for_all_model_admins + def test_changelist_view_search(self, model, model_admin): + with self.assertRaises(ModelAdminCheckException): + super(FailAdminSiteSmokeTest, self).test_changelist_view_search() + + if django.VERSION >= (1, 8): + @for_all_model_admins + def test_changelist_view(self, model, model_admin): + with self.assertRaises(ModelAdminCheckException): + super(FailAdminSiteSmokeTest, self).test_changelist_view() + + +class ForbiddenAdminSiteSmokeTest(AdminSiteSmokeTestMixin, TestCase): + fixtures = [] + exclude_modeladmins = [FailPostAdmin, PostAdmin, ChannelAdmin] diff --git a/test_project/settings.py b/test_project/settings.py index 28accbde..85bed9dd 100644 --- a/test_project/settings.py +++ b/test_project/settings.py @@ -22,7 +22,24 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -TEMPLATE_DEBUG = True +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'APP_DIRS': True, + 'OPTIONS': { + 'debug': True, + 'context_processors': [ + 'django.contrib.auth.context_processors.auth', + 'django.template.context_processors.debug', + 'django.template.context_processors.i18n', + 'django.template.context_processors.media', + 'django.template.context_processors.static', + 'django.template.context_processors.tz', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] ALLOWED_HOSTS = [] diff --git a/tox.ini b/tox.ini index 8128d6e4..ec755050 100644 --- a/tox.ini +++ b/tox.ini @@ -1,29 +1,37 @@ [tox] envlist = py{35,27}-djdev, + py{35,27}-dj110, py{35,27}-dj19, py{35,27}-dj18, - py{35,27}-dj17, + py{34,27}-dj17, py27-flake8, skipsdist=True [testenv] usedevelop=True test-executable = - {envbindir}/coverage run --append --source=django_admin_smoke_tests + python --version + {envbindir}/python -Wall {envbindir}/coverage run --append --source=django_admin_smoke_tests commands = - dj{17,18,19,dev}: {[testenv]test-executable} setup.py test - - flake8: {envbindir}/flake8 --ignore=E128 --max-complexity 10 . + dj{17,18,19,110,dev}: {[testenv]test-executable} setup.py test basepython = py27: python2.7 + py34: python3.4 py35: python3.5 deps = dj17: Django>=1.7,<1.8 dj18: Django>=1.8,<1.9 dj19: Django>=1.9,<1.10 + dj110: Django>=1.10,<1.11 djdev: https://github.com/django/django/archive/master.tar.gz - dj{17,18,19,dev}: coverage - dj{17,18,19,dev}: ipdb + dj{17,18,19,110,dev}: coverage + dj{17,18,19,110,dev}: ipdb + dj{17,18,19,110,dev}: pytz - flake8: flake8 +[testenv:py27-flake8] +deps = + flake8 + flake8-import-order +commands = + {envbindir}/flake8 --ignore=E128 --max-complexity 10 .