import copy from unittest.mock import MagicMock, patch from django.conf import settings from django.contrib.admin import site from django.contrib.auth import get_permission_codename from django.contrib.sites.models import Site from django.db import connection from django.templatetags.static import static from django.test.utils import CaptureQueriesContext from django.utils.crypto import get_random_string from django.utils.translation import get_language, override as force_language from cms.admin.utils import CONTENT_PREFIX, GrouperModelAdmin from cms.test_utils.project.sampleapp.models import ( GrouperModel, GrouperModelContent, SimpleGrouperModel, SimpleGrouperModelContent, ) from cms.test_utils.testcases import CMSTestCase from cms.test_utils.util.grouper import wo_content_permission from cms.utils.i18n import get_language_list from cms.utils.urlutils import admin_reverse, static_with_version class SetupMixin: """Create one grouper object and retrieve the admin instance""" def setUp(self) -> None: self.grouper_instance = GrouperModel.objects.create(category_name="Grouper Category") self.add_url = admin_reverse("sampleapp_groupermodel_add") self.change_url = admin_reverse("sampleapp_groupermodel_change", args=(self.grouper_instance.pk,)) self.changelist_url = admin_reverse("sampleapp_groupermodel_changelist") self.admin_user = self.get_superuser() self.admin = site._registry[GrouperModel] self.groupermodel = "groupermodel" self.grouper_model = "grouper_model" def tearDown(self) -> None: self.grouper_instance.delete() self.admin.clear_content_cache() # The admin does this automatically for each new request. def createContentInstance(self, language="en"): """Creates a content instance with a random content for a language. The random content is returned to be able to check if it appears in forms etc.""" instance = GrouperModelContent.objects.create( grouper_model=self.grouper_instance, language=language, secret_greeting=get_random_string(16), ) self.admin.clear_content_cache() # The admin does this automatically for each new request. return instance class SimpleSetupMixin: """Create one grouper object and retrieve the admin instance""" def setUp(self) -> None: self.grouper_instance = SimpleGrouperModel.objects.create(category_name="Grouper Category") self.add_url = admin_reverse("sampleapp_simplegroupermodel_add") self.change_url = admin_reverse("sampleapp_simplegroupermodel_change", args=(self.grouper_instance.pk,)) self.changelist_url = admin_reverse("sampleapp_simplegroupermodel_changelist") self.admin_user = self.get_superuser() self.admin = site._registry[SimpleGrouperModel] self.groupermodel = "simplegroupermodel" self.grouper_model = "simple_grouper_model" def tearDown(self) -> None: self.grouper_instance.delete() self.admin.clear_content_cache() # The admin does this automatically for each new request. def createContentInstance(self, language="en", grouper_instance=None, secret_greeting=None): """Creates a content instance with a random content for a language. The random content is returned to be able to check if it appears in forms etc.""" assert language == "en", "Only English is supported for SimpleGrouperModelContent" grouper_instance = grouper_instance or self.grouper_instance secret_greeting = secret_greeting or get_random_string(16) instance = SimpleGrouperModelContent.objects.create( simple_grouper_model=grouper_instance, secret_greeting=secret_greeting, ) self.admin.clear_content_cache() # The admin does this automatically for each new request. return instance class SimpleChangeListActionsTestCase(SimpleSetupMixin, CMSTestCase): def test_action_js_css(self): """Are js and css files loaded? The js and css files are supposed to be arranged by the GrouperAdminMixin.""" with self.login_user_context(self.admin_user): # Act response = self.client.get(f"{self.changelist_url}?", follow=True) # Assert self.assertContains(response, static("admin/js/jquery.init.js")) self.assertContains(response, static("cms/js/admin/actions.js")) self.assertContains(response, static_with_version("cms/css/cms.admin.css")) def test_add_action(self): """Change list offers an add button if no content object exists for grouper. The button is supposed to be arranged by the GrouperAdminMixin.""" with self.login_user_context(self.admin_user): # Act response = self.client.get(f"{self.changelist_url}?language=en", follow=True) # Assert self.assertContains(response, 'class="cms-icon cms-icon-plus"') self.assertContains( response, f'href="/en/admin/sampleapp/{self.groupermodel}/{self.grouper_instance.pk}/change/?' ) self.assertNotContains(response, 'class="cms-icon cms-icon-view"') def test_change_action(self): """Change list offers a settings button if content object exists for grouper""" # Arrange self.createContentInstance("en") with self.login_user_context(self.admin_user): # Act response = self.client.get(f"{self.changelist_url}?language=en", follow=True) # Assert self.assertNotContains(response, 'class="cms-icon cms-icon-view"') # Not frontend-editable self.assertContains( response, f'href="/en/admin/sampleapp/{self.groupermodel}/{self.grouper_instance.pk}/change/?' ) def test_get_action(self): admin = site._registry[GrouperModel] get_action = admin.admin_action_button( "/some/url", icon="info", title="Info", action="get", ) self.assertIn("cms-form-get-method", get_action) self.assertNotIn("cms-form-post-method", get_action) def test_post_action(self): admin = site._registry[GrouperModel] get_action = admin.admin_action_button( "/some/url", icon="bin", title="Delete", action="post", ) self.assertNotIn("cms-form-get-method", get_action) self.assertIn("cms-form-post-method", get_action) class ChangeListActionsTestCase(SetupMixin, SimpleChangeListActionsTestCase): pass class GrouperModelAdminTestCase(SetupMixin, CMSTestCase): def get_list_editable_post_data(self, response, **values): formset = response.context["cl"].formset data = { "form-TOTAL_FORMS": formset.total_form_count(), "form-INITIAL_FORMS": formset.initial_form_count(), "form-MIN_NUM_FORMS": formset.min_num, "form-MAX_NUM_FORMS": formset.max_num, "_save": "Save", } for index, form in enumerate(formset.forms): data[f"form-{index}-id"] = form.instance.pk for field_name in self.admin.list_editable: data[f"form-{index}-{field_name}"] = values.get(field_name, form[field_name].value()) data[f"form-{index}-_content_object_id"] = form["_content_object_id"].value() or "" return data def test_list_editable_saves_grouper_field(self): with ( patch.object(self.admin, "list_display", ("category_name",)), patch.object(self.admin, "list_display_links", None), patch.object(self.admin, "list_editable", ("category_name",)), self.login_user_context(self.admin_user), ): response = self.client.get(f"{self.changelist_url}?language=en") data = self.get_list_editable_post_data(response, category_name="Changed grouper") response = self.client.post(f"{self.changelist_url}?language=en", data) self.assertEqual(response.status_code, 302) self.grouper_instance.refresh_from_db() self.assertEqual(self.grouper_instance.category_name, "Changed grouper") def test_list_editable_saves_content_field_for_current_grouping(self): english_content = self.createContentInstance("en") german_content = self.createContentInstance("de") with ( patch.object(self.admin, "list_display", ("category_name", "content__secret_greeting")), patch.object(self.admin, "list_editable", ("content__secret_greeting",)), self.login_user_context(self.admin_user), ): response = self.client.get(f"{self.changelist_url}?language=de") self.assertContains(response, 'name="form-0-_content_object_id"') data = self.get_list_editable_post_data(response, content__secret_greeting="Guten Tag") response = self.client.post(f"{self.changelist_url}?language=de", data) self.assertEqual(response.status_code, 302) english_content.refresh_from_db() german_content.refresh_from_db() self.assertNotEqual(english_content.secret_greeting, "Guten Tag") self.assertEqual(german_content.secret_greeting, "Guten Tag") def test_list_editable_saves_grouper_and_content_fields(self): content = self.createContentInstance("en") with ( patch.object(self.admin, "list_display", ("category_name", "content__secret_greeting")), patch.object(self.admin, "list_display_links", None), patch.object(self.admin, "list_editable", ("category_name", "content__secret_greeting")), self.login_user_context(self.admin_user), ): response = self.client.get(f"{self.changelist_url}?language=en") data = self.get_list_editable_post_data( response, category_name="Changed grouper", content__secret_greeting="Changed content", ) response = self.client.post(f"{self.changelist_url}?language=en", data) self.assertEqual(response.status_code, 302) self.grouper_instance.refresh_from_db() content.refresh_from_db() self.assertEqual(self.grouper_instance.category_name, "Changed grouper") self.assertEqual(content.secret_greeting, "Changed content") def test_list_editable_creates_content_for_current_grouping(self): with ( patch.object(self.admin, "list_display", ("category_name", "content__secret_greeting")), patch.object(self.admin, "list_editable", ("content__secret_greeting",)), self.login_user_context(self.admin_user), ): response = self.client.get(f"{self.changelist_url}?language=de") data = self.get_list_editable_post_data(response, content__secret_greeting="Guten Tag") response = self.client.post(f"{self.changelist_url}?language=de", data) self.assertEqual(response.status_code, 302) content = GrouperModelContent.objects.get(grouper_model=self.grouper_instance) self.assertEqual(content.language, "de") self.assertEqual(content.secret_greeting, "Guten Tag") def test_list_editable_rejects_content_from_another_grouper(self): self.createContentInstance("en") other_grouper = GrouperModel.objects.create(category_name="Other") other_content = GrouperModelContent.objects.create( grouper_model=other_grouper, language="en", secret_greeting="Other greeting", ) with ( patch.object(self.admin, "list_display", ("category_name", "content__secret_greeting")), patch.object(self.admin, "list_editable", ("content__secret_greeting",)), self.login_user_context(self.admin_user), ): response = self.client.get(f"{self.changelist_url}?language=en") data = self.get_list_editable_post_data(response, content__secret_greeting="Tampered") form_index = next( index for index, form in enumerate(response.context["cl"].formset.forms) if form.instance.pk == self.grouper_instance.pk ) data[f"form-{form_index}-_content_object_id"] = other_content.pk response = self.client.post(f"{self.changelist_url}?language=en", data) self.assertEqual(response.status_code, 200) self.assertContains(response, "The selected content does not match this object and grouping.") other_content.refresh_from_db() self.assertEqual(other_content.secret_greeting, "Other greeting") def test_form_class_created(self): """The form class has automatically been enhanced with the GrouperAdminFormMixin for the appropriate content model (actually its parent class _GrouperAdminFormMixin)""" # Arrange from cms.admin.utils import _GrouperAdminFormMixin # Assert self.assertTrue(issubclass(self.admin.form, _GrouperAdminFormMixin)) self.assertEqual(self.admin.form._content_model, GrouperModelContent) def test_content_field_is_valid_in_list_editable(self): admin = copy.copy(self.admin) admin.list_display = ("category_name", "content__secret_greeting") admin.list_editable = ("content__secret_greeting",) self.assertEqual(admin.check(), []) def test_grouping_field_is_invalid_in_list_editable(self): admin = copy.copy(self.admin) admin.list_display = ("category_name", "content__language") admin.list_editable = ("content__language",) errors = admin.check() self.assertIn("admin.E125", [error.id for error in errors]) def test_form_class_content_fields(self): """The content fields appear in the admin form with a prefix""" # Assert for field in self.admin.form._content_fields: self.assertIn(CONTENT_PREFIX + field, self.admin.form.base_fields) def test_content_model_detected(self) -> None: """Content model has been detected correctly for grouper admin""" # Assert admin = site._registry[GrouperModel] self.assertEqual(admin.content_model, GrouperModelContent) def test_extra_grouping_field_fixed(self): """Extra grouping fields are retrieved correctly""" with force_language("en"): expected_language = "zh" self.admin.language = expected_language admin_language = self.admin.get_language() current_content_filters = self.admin.current_content_filters self.assertEqual(admin_language, expected_language) self.assertEqual(current_content_filters["language"], expected_language) def test_extra_grouping_field_current(self): """Extra grouping fields (language) when not set return current default correctly""" del self.admin.language # No pre-set language expected_language = get_language() admin_language = self.admin.get_language() current_content_filters = self.admin.current_content_filters self.assertEqual(admin_language, expected_language) self.assertEqual(current_content_filters["language"], expected_language) def test_prepopulated_fields_pass_checks(self): """Prepopulated fields work for content field""" # Arrange admin = copy.copy(self.admin) admin.prepopulated_fields = dict( category_name=["category_name"], # Both key and value from GrouperModel some_field=["content__secret_greeting"], # Value from ContentModel content__secret_greeting=["category_name"], # Key from GrouperModel content__region=["content__secret_greeting"], # Both key and value from ContentModel ) # Act check_results = admin.check() # Assert self.assertEqual(check_results, []) # No errors def test_invalid_prepopulated_content_fields_fail_checks(self): """Prepopulated fields with invalid content field names fail checks""" # Arrange admin = copy.copy(self.admin) admin.prepopulated_fields = dict( some_field=["content__public_greeting"], # Value from ContentModel: 1 error content__public_greeting=["category_name"], # Key from GrouperModel: 1 error content__country=["content__public_greeting"], # Both key and value from ContentModel: 2 errors ) # Act check_results = admin.check() # Assert self.assertEqual(len(check_results), 4) # 4 errors expected (see above) @wo_content_permission def test_prepopulated_fields_exclude_readonly_fields(self): """Read-only fields are removed from prepopulated field keys.""" # Arrange admin = copy.copy(self.admin) admin.prepopulated_fields = { "content__secret_greeting": ["category_name"], "category_name": ["content__secret_greeting"], } # Act readonly_fields = admin.get_readonly_fields(None) prepopulated = admin.get_prepopulated_fields(None) # Assert self.assertIn("content__secret_greeting", readonly_fields) self.assertNotIn("content__secret_greeting", prepopulated) self.assertIn("category_name", prepopulated) # The class attribute is not mutated by get_readonly_fields/get_prepopulated_fields. self.assertIn("content__secret_greeting", admin.prepopulated_fields) def test_prepopulated_fields_respects_class_attribute_after_init(self): """``cls.prepopulated_fields`` set after the admin is instantiated is honored.""" # Arrange admin = copy.copy(self.admin) admin.prepopulated_fields = {"category_name": ["content__secret_greeting"]} # Act prepopulated = admin.get_prepopulated_fields(None) # Assert: no shadow instance attribute hides the class-level value. self.assertEqual(prepopulated, {"category_name": ["content__secret_greeting"]}) def test_changelist_view_does_not_call_get_content_obj(self): self.createContentInstance("en") with self.login_user_context(self.admin_user): with patch.object(self.admin, "get_content_obj", wraps=self.admin.get_content_obj) as mocked_get_content_obj: response = self.client.get(f"{self.changelist_url}?language=en", follow=True) self.assertEqual(response.status_code, 200) self.assertEqual(mocked_get_content_obj.call_count, 0) def test_change_view_calls_get_content_obj(self): self.createContentInstance("en") with self.login_user_context(self.admin_user): with patch.object(self.admin, "get_content_obj", wraps=self.admin.get_content_obj) as mocked_get_content_obj: response = self.client.get(f"{self.change_url}?language=en", follow=True) self.assertEqual(response.status_code, 200) self.assertGreater(mocked_get_content_obj.call_count, 0) self.assertTrue(any(call.args and call.args[0] == self.grouper_instance for call in mocked_get_content_obj.call_args_list)) def test_get_content_obj_caches_on_grouper_object(self): content_instance = self.createContentInstance("en") self.admin.language = "en" self.admin.clear_content_cache() self.assertFalse(hasattr(self.grouper_instance, "_grouper_admin_content_obj_cache")) with self.assertNumQueries(1): cached_content = self.admin.get_content_obj(self.grouper_instance) self.assertEqual(cached_content, content_instance) self.assertTrue(hasattr(self.grouper_instance, "_grouper_admin_content_obj_cache")) with self.assertNumQueries(0): cached_again = self.admin.get_content_obj(self.grouper_instance) self.assertIs(cached_again, cached_content) class GrouperChangeListTestCase(SetupMixin, CMSTestCase): def test_language_selector(self): """All languages available to select""" # Act with self.login_user_context(self.admin_user): response = self.client.get(self.changelist_url) # Assert for lang, verb in self.admin.get_language_tuple(site=Site.objects.get_current()): self.assertContains(response, f'