diff --git a/polymorphic/tests/admintestcase.py b/polymorphic/tests/admintestcase.py new file mode 100644 index 0000000..7bc7e97 --- /dev/null +++ b/polymorphic/tests/admintestcase.py @@ -0,0 +1,197 @@ +from django.conf import settings +from django.conf.urls import include, url +from django.contrib.admin import AdminSite +from django.contrib.admin.templatetags.admin_urls import admin_urlname +from django.contrib.auth.models import User +from django.contrib.messages.middleware import MessageMiddleware +from django.test import RequestFactory, TestCase +from django.urls import clear_url_caches, reverse, set_urlconf + + +class AdminTestCase(TestCase): + """ + Testing the admin site + """ + #: The model to test + model = None + #: The admin class to test + admin_class = None + + @classmethod + def setUpClass(cls): + super(AdminTestCase, cls).setUpClass() + cls.admin_user = User.objects.create_superuser('admin', 'admin@example.org', password='admin') + + def setUp(self): + super(AdminTestCase, self).setUp() + + # Have a separate site, to avoid dependency on polymorphic wrapping or standard admin configuration + self.admin_site = AdminSite() + + if self.model is not None: + self.admin_register(self.model, self.admin_class) + + def tearDown(self): + clear_url_caches() + set_urlconf(None) + + def register(self, model): + """Decorator, like admin.register()""" + def _dec(admin_class): + self.admin_register(model, admin_class) + return admin_class + return _dec + + def admin_register(self, model, admin_site): + """Register an model with admin to the test case, test client and URL reversing code.""" + self.admin_site.register(model, admin_site) + + # Make sure the URLs are reachable by reverse() + clear_url_caches() + set_urlconf(tuple([ + url('^', include(self.admin_site.urls)) + ])) + + def get_admin_instance(self, model): + try: + return self.admin_site._registry[model] + except KeyError: + raise ValueError("Model not registered with admin: {}".format(model)) + + @classmethod + def tearDownClass(cls): + super(AdminTestCase, cls).tearDownClass() + clear_url_caches() + set_urlconf(None) + + def get_add_url(self, model): + admin_instance = self.get_admin_instance(model) + return reverse(admin_urlname(admin_instance.opts, 'add')) + + def get_changelist_url(self, model): + admin_instance = self.get_admin_instance(model) + return reverse(admin_urlname(admin_instance.opts, 'changelist')) + + def get_change_url(self, model, object_id): + admin_instance = self.get_admin_instance(model) + return reverse(admin_urlname(admin_instance.opts, 'change'), args=(object_id,)) + + def get_delete_url(self, model, object_id): + admin_instance = self.get_admin_instance(model) + return reverse(admin_urlname(admin_instance.opts, 'delete'), args=(object_id,)) + + def admin_post_add(self, model, formdata): + """ + Make a direct "add" call to the admin page, circumvening login checks. + """ + admin_instance = self.get_admin_instance(model) + request = self.create_admin_request('post', self.get_add_url(model), data=formdata) + response = admin_instance.add_view(request) + self.assertFormSuccess(request.path, response) + return response + + def admin_get_changelist(self, model): + """ + Make a direct "add" call to the admin page, circumvening login checks. + """ + admin_instance = self.get_admin_instance(model) + request = self.create_admin_request('get', self.get_changelist_url(model)) + response = admin_instance.changelist_view(request) + self.assertEqual(response.status_code, 200) + return response + + def admin_get_change(self, model, object_id, query=None, **extra): + """ + Perform a GET request on the admin page + """ + admin_instance = self.get_admin_instance(model) + request = self.create_admin_request('get', self.get_change_url(model, object_id), data=query, **extra) + response = admin_instance.change_view(request, str(object_id)) + self.assertEqual(response.status_code, 200) + return response + + def admin_post_change(self, model, object_id, formdata, **extra): + """ + Make a direct "add" call to the admin page, circumvening login checks. + """ + admin_instance = self.get_admin_instance(model) + request = self.create_admin_request('post', self.get_change_url(model, object_id), data=formdata, **extra) + response = admin_instance.change_view(request, str(object_id)) + self.assertFormSuccess(request.path, response) + return response + + def admin_post_delete(self, model, object_id, **extra): + """ + Make a direct "add" call to the admin page, circumvening login checks. + """ + admin_instance = self.get_admin_instance(model) + request = self.create_admin_request('post', self.get_delete_url(model, object_id), **extra) + response = admin_instance.delete_view(request, str(object_id)) + self.assertEqual(response.status_code, 302, "Form errors in calling {0}".format(request.path)) + return response + + def create_admin_request(self, method, url, data=None, **extra): + """ + Construct an Request instance for the admin view. + """ + factory_method = getattr(RequestFactory(), method) + + if data is not None: + if method != 'get': + data['csrfmiddlewaretoken'] = 'foo' + dummy_request = factory_method(url, data=data) + dummy_request.user = self.admin_user + + # Add the management form fields if needed. + # base_data = self._get_management_form_data(dummy_request) + # base_data.update(data) + # data = base_data + + request = factory_method(url, data=data, **extra) + request.COOKIES[settings.CSRF_COOKIE_NAME] = 'foo' + request.csrf_processing_done = True + + # Add properties which middleware would typically do + request.session = {} + request.user = self.admin_user + MessageMiddleware().process_request(request) + return request + + def _get_management_form_data(self, admin_instance, request): + """ + Return the formdata that the management forms need. + """ + inline_instances = admin_instance.get_inline_instances(request) + forms = [] + for inline_instance in inline_instances: + FormSet = inline_instance.get_formset(request) + formset = FormSet(instance=admin_instance.model()) + forms.append(formset.management_form) + + # In a primitive way, get the form fields. + # This is not exactly the same as a POST, since that runs through clean() + formdata = {} + for form in forms: + for boundfield in form: + formdata[boundfield.html_name] = boundfield.value() + + return formdata + + def assertFormSuccess(self, request_url, response): + """ + Assert that the response was a redirect, not a form error. + """ + self.assertIn(response.status_code, [200, 302]) + if response.status_code != 302: + context_data = response.context_data + if 'errors' in context_data: + errors = response.context_data['errors'] + elif 'form' in context_data: + errors = context_data['form'].errors + else: + raise KeyError("Unknown field for errors in the TemplateResponse!") + + self.assertEqual(response.status_code, 302, + "Form errors in calling {0}:\n{1}".format(request_url, errors.as_text())) + self.assertTrue('/login/?next=' not in response['Location'], + "Received login response for {0}".format(request_url)) diff --git a/polymorphic/tests/migrations/0001_initial.py b/polymorphic/tests/migrations/0001_initial.py index c3c5ed3..2ce76ac 100644 --- a/polymorphic/tests/migrations/0001_initial.py +++ b/polymorphic/tests/migrations/0001_initial.py @@ -1005,4 +1005,54 @@ class Migration(migrations.Migration): }, bases=('tests.model2c',), ), + migrations.CreateModel( + name='InlineModelBase', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + options={ + 'abstract': False, + 'base_manager_name': 'objects', + }, + ), + migrations.CreateModel( + name='InlineParent', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=10)), + ], + ), + migrations.CreateModel( + name='InlineModelA', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('field1', models.CharField(max_length=10)), + ], + options={ + 'abstract': False, + 'base_manager_name': 'objects', + }, + ), + migrations.CreateModel( + name='InlineModelB', + fields=[ + ('inlinemodela_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.InlineModelA')), + ('field2', models.CharField(max_length=10)), + ], + options={ + 'abstract': False, + 'base_manager_name': 'objects', + }, + bases=('tests.inlinemodela',), + ), + migrations.AddField( + model_name='inlinemodela', + name='parent', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.InlineParent'), + ), + migrations.AddField( + model_name='inlinemodela', + name='polymorphic_ctype', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_tests.inlinemodela_set+', to='contenttypes.ContentType'), + ), ] diff --git a/polymorphic/tests/models.py b/polymorphic/tests/models.py index e2794ea..47683eb 100644 --- a/polymorphic/tests/models.py +++ b/polymorphic/tests/models.py @@ -414,3 +414,16 @@ class SwappableModel(AbstractModel): class SwappedModel(AbstractModel): pass + + +class InlineParent(models.Model): + title = models.CharField(max_length=10) + + +class InlineModelA(PolymorphicModel): + parent = models.ForeignKey(InlineParent) + field1 = models.CharField(max_length=10) + + +class InlineModelB(InlineModelA): + field2 = models.CharField(max_length=10) diff --git a/polymorphic/tests/test_admin.py b/polymorphic/tests/test_admin.py index 2b2a2fe..72fc2a8 100644 --- a/polymorphic/tests/test_admin.py +++ b/polymorphic/tests/test_admin.py @@ -1,21 +1,27 @@ -from django.contrib.admin import AdminSite -from django.test import TestCase +from django.contrib import admin +from django.utils.html import escape -from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin, PolymorphicChildModelFilter -from polymorphic.tests.models import Model2A, Model2B, Model2C, Model2D +from polymorphic.admin import PolymorphicChildModelAdmin, PolymorphicChildModelFilter, PolymorphicInlineSupportMixin, \ + PolymorphicParentModelAdmin, StackedPolymorphicInline +from polymorphic.tests.admintestcase import AdminTestCase +from polymorphic.tests.models import InlineModelA, InlineModelB, InlineParent, Model2A, Model2B, Model2C, Model2D -class MultipleDatabasesTests(TestCase): +class PolymorphicAdminTests(AdminTestCase): def test_admin_registration(self): """ Test how the registration works """ + @self.register(Model2A) class Model2Admin(PolymorphicParentModelAdmin): base_model = Model2A list_filter = (PolymorphicChildModelFilter,) child_models = (Model2B, Model2C, Model2D) + @self.register(Model2B) + @self.register(Model2C) + @self.register(Model2D) class Model2ChildAdmin(PolymorphicChildModelAdmin): base_model = Model2A base_fieldsets = ( @@ -24,8 +30,39 @@ class MultipleDatabasesTests(TestCase): }), ) - admin_site = AdminSite() - admin_site.register(Model2A, Model2Admin) - admin_site.register(Model2B, Model2ChildAdmin) - admin_site.register(Model2C, Model2ChildAdmin) - admin_site.register(Model2D, Model2ChildAdmin) + # Now test which results are returned + d_obj = Model2D.objects.create(field1='A', field2='B', field3='C', field4='D') + self.admin_get_changelist(Model2A) # asserts 200 + + # See that the child object was returned + response = self.admin_get_change(Model2A, d_obj.pk) + self.assertContains(response, 'field4') + + def test_admin_inlines(self): + """ + Test the registration of inline models. + """ + class InlineModelAChild(StackedPolymorphicInline.Child): + model = InlineModelA + + class InlineModelBChild(StackedPolymorphicInline.Child): + model = InlineModelB + + class Inline(StackedPolymorphicInline): + model = InlineModelA + child_inlines = ( + InlineModelAChild, + InlineModelBChild, + ) + + @self.register(InlineParent) + class InlineParentAdmin(PolymorphicInlineSupportMixin, admin.ModelAdmin): + inlines = (Inline,) + + obj = InlineParent.objects.create(title='FOO') + response = self.admin_get_change(InlineParent, obj.pk) + + # Make sure the fieldset has the right data exposed in data-inline-formset + self.assertContains(response, 'childTypes') + self.assertContains(response, escape('"type": "inlinemodela"')) + self.assertContains(response, escape('"type": "inlinemodelb"')) diff --git a/runtests.py b/runtests.py index 57d2f52..bb3074d 100755 --- a/runtests.py +++ b/runtests.py @@ -61,6 +61,7 @@ if not settings.configured: }, ], POLYMORPHIC_TEST_SWAPPABLE='polymorphic.swappedmodel', + ROOT_URLCONF=None, )