import sys import types import uuid from django.apps import apps from django.contrib.auth import get_user_model from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType from django.contrib.sites.models import Site from django.core import checks from django.core.cache import cache from django.core.checks.urls import check_url_config from django.http import Http404, HttpResponse from django.shortcuts import render from django.template import Context, Template from django.template.response import TemplateResponse from django.test import RequestFactory, SimpleTestCase from django.test.utils import override_settings from django.urls import NoReverseMatch, clear_url_caches, include, path, resolve, reverse from django.utils.timezone import now from django.utils.translation import activate, override as force_language from django.views.generic import TemplateView from cms.admin.forms import AdvancedSettingsForm from cms.api import create_page, create_page_content from cms.app_base import CMSApp from cms.apphook_pool import apphook_pool from cms.appresolver import applications_page_check, clear_app_resolvers, get_app_patterns from cms.middleware.page import get_page from cms.models import PageContent from cms.test_utils.project.placeholderapp.models import Example1 from cms.test_utils.testcases import CMSTestCase from cms.tests.test_menu_utils import DumbPageLanguageUrl from cms.toolbar.toolbar import CMSToolbar from cms.views import render_object_edit from menus.menu_pool import menu_pool from menus.utils import DefaultLanguageChanger APP_NAME = 'SampleApp' NS_APP_NAME = 'NamespacedApp' APP_MODULE = "cms.test_utils.project.sampleapp.cms_apps" MENU_MODULE = "cms.test_utils.project.sampleapp.cms_menus" class BaseApphooksTestCase(CMSTestCase): """ Test cases for apphooks """ def setUp(self): clear_app_resolvers() clear_url_caches() if APP_MODULE in sys.modules: del sys.modules[APP_MODULE] self.reload_urls() self.apphook_clear() def tearDown(self): clear_app_resolvers() clear_url_caches() if APP_MODULE in sys.modules: del sys.modules[APP_MODULE] self.reload_urls() self.apphook_clear() def reload_urls(self): from django.conf import settings url_modules = [ 'cms.urls', # TODO: Add here intermediary modules which may # include() the 'cms.urls' if it isn't included # directly in the root urlconf. # '...', 'cms.test_utils.project.second_cms_urls_for_apphook_tests', 'cms.test_utils.project.urls_for_apphook_tests', APP_MODULE, settings.ROOT_URLCONF, ] clear_app_resolvers() clear_url_caches() for module in url_modules: if module in sys.modules: del sys.modules[module] class ApphooksTestCase(BaseApphooksTestCase): def create_base_structure(self, apphook, content_langs, namespace=None): self.apphook_clear() superuser = get_user_model().objects.create_superuser('admin', 'admin@admin.com', 'admin') self.superuser = superuser page = create_page("home", "nav_playground.html", "en", created_by=superuser) create_page_content('de', page.get_title(), page) child_page = create_page("child_page", "nav_playground.html", "en", created_by=superuser, parent=page) create_page_content('de', child_page.get_title(), child_page) child_child_page = create_page("child_child_page", "nav_playground.html", "en", created_by=superuser, parent=child_page, apphook=apphook, apphook_namespace=namespace) create_page_content("de", child_child_page.get_title(), child_child_page) # publisher_public is set to draft on publish, issue with onetoone reverse child_child_page = self.reload(child_child_page) if isinstance(content_langs, str): contents = child_child_page.get_content_obj(content_langs) else: contents = [child_child_page.get_content_obj(lang) for lang in content_langs] self.reload_urls() return contents @override_settings(ROOT_URLCONF='cms.test_utils.project.fourth_urls_for_apphook_tests') def test_check_url_config(self): """ Test for urls config check. """ self.apphook_clear() result = check_url_config(None) self.assertEqual(len(result), 0) @override_settings(CMS_APPHOOKS=[f'{APP_MODULE}.{APP_NAME}']) def test_explicit_apphooks(self): """ Test explicit apphook loading with the CMS_APPHOOKS setting. """ self.apphook_clear() hooks = apphook_pool.get_apphooks() app_names = [hook[0] for hook in hooks] self.assertEqual(len(hooks), 1) self.assertEqual(app_names, [APP_NAME]) self.apphook_clear() @override_settings( INSTALLED_APPS=['cms.test_utils.project.sampleapp'], ROOT_URLCONF='cms.test_utils.project.urls_for_apphook_tests', ) def test_implicit_apphooks(self): """ Test implicit apphook loading with INSTALLED_APPS cms_apps.py """ self.apphook_clear() hooks = apphook_pool.get_apphooks() app_names = [hook[0] for hook in hooks] self.assertEqual(len(hooks), 9) self.assertIn(NS_APP_NAME, app_names) self.assertIn(APP_NAME, app_names) self.apphook_clear() def test_apphook_on_homepage(self): self.apphook_clear() superuser = get_user_model().objects.create_superuser('admin', 'admin@admin.com', 'admin') page = self.create_homepage("apphooked-page", "nav_playground.html", "en", created_by=superuser, apphook="SampleApp") create_page("not-apphooked-page", "nav_playground.html", "en", created_by=superuser, apphook="", slug='blankapp') english_title = page.pagecontent_set.all()[0] self.assertEqual(english_title.language, 'en') create_page_content("de", "aphooked-page-de", page) with force_language("en"): response = self.client.get(self.get_pages_root()) self.assertTemplateUsed(response, 'sampleapp/home.html') self.assertContains(response, '<--noplaceholder-->') response = self.client.get('/en/blankapp/') self.assertTemplateUsed(response, 'nav_playground.html') self.apphook_clear() def test_pagecontent_get_template_uses_apphook_root_template(self): """When the apphook exposes a root template, PageContent.get_template returns it instead of the page's own template — so the structure board can show placeholders matching what the apphook actually renders.""" self.apphook_clear() superuser = get_user_model().objects.create_superuser('admin', 'admin@admin.com', 'admin') page = create_page( 'apphooked-page', 'nav_playground.html', 'en', created_by=superuser, apphook=APP_NAME, ) page_content = page.pagecontent_set.get(language='en') # SampleApp's root view is a function-based view, so without a custom # get_root_template override the lookup returns None and PageContent # falls back to the page's configured template. self.assertEqual(page_content.get_template(), 'nav_playground.html') sample_app = apphook_pool.get_apphook(APP_NAME) original = sample_app.get_root_template sample_app.get_root_template = lambda **kwargs: 'apphook/from_root_view.html' try: del page_content._template_cache self.assertEqual( page_content.get_template(), 'apphook/from_root_view.html', ) finally: sample_app.get_root_template = original self.apphook_clear() def test_apphook_without_landing_page(self): """test rendering the apphook without landing page. tests for https://github.com/django-cms/django-cms/issues/7765 """ from django.contrib.contenttypes.models import ContentType from cms import views from cms.utils.urlutils import admin_reverse APP_NAME = 'SampleAppWithoutLandingPage' [content] = self.create_base_structure( APP_NAME, ["en"], "apphook-without-landing" ) content_type = ContentType.objects.get(app_label='cms', model='pagecontent') endpoint = admin_reverse('cms_placeholder_render_object_edit', args=(content_type.pk, content.pk,)) # make sure, we do not get views.details as this always uses the # published page content self.assertIsNot(endpoint, views.details) @override_settings(ROOT_URLCONF='cms.test_utils.project.urls_for_apphook_tests') def test_apphook_does_not_crash_django_checks(self): # This test case reproduced the situation causing the error reported in issue #6717. self.apphook_clear() superuser = get_user_model().objects.create_superuser('admin', 'admin@admin.com', 'admin') create_page("apphooked-page", "nav_playground.html", "en", created_by=superuser, apphook="SampleApp") self.reload_urls() checks.run_checks() self.apphook_clear() @override_settings(ROOT_URLCONF='cms.test_utils.project.urls_for_apphook_tests') def test_apphook_on_root_reverse(self): self.apphook_clear() superuser = get_user_model().objects.create_superuser('admin', 'admin@admin.com', 'admin') page = create_page("apphooked-page", "nav_playground.html", "en", created_by=superuser, apphook="SampleApp") create_page_content("de", "aphooked-page-de", page) self.reload_urls() self.assertFalse(reverse('sample-settings').startswith('//')) self.apphook_clear() @override_settings(ROOT_URLCONF='cms.test_utils.project.urls_for_apphook_tests') def test_multisite_apphooks(self): self.apphook_clear() site1, _ = Site.objects.get_or_create(pk=1) site2, _ = Site.objects.get_or_create(pk=2) superuser = get_user_model().objects.create_superuser('admin', 'admin@admin.com', 'admin') request = self.get_request() request.user = superuser home_site_1 = create_page( "home", "nav_playground.html", "en", created_by=superuser, site=site1 ) home_site_2 = create_page( "home", "nav_playground.html", "de", created_by=superuser, site=site2 ) page_a_1 = create_page( "apphooked-page", "nav_playground.html", "en", created_by=superuser, parent=home_site_1, apphook=NS_APP_NAME, apphook_namespace="instance" ) page_a_2 = create_page( "apphooked-page", "nav_playground.html", "de", created_by=superuser, parent=home_site_1, ) page_b_1 = create_page( "apphooked-page", "nav_playground.html", "de", created_by=superuser, parent=home_site_2, site=site2 ) # AdvancedSettingsForm expects a _site and _request attribute # on __init__. These are normally set by the admin. # In this case, we need to set these in a new class. Meta = type('Meta', (AdvancedSettingsForm.Meta, object), {}) formClass = type(AdvancedSettingsForm)( 'AdvancedSettingsForm', (AdvancedSettingsForm,), {'Meta': Meta, '_site': site1, '_request': request}, ) form = formClass(instance=page_a_1) self.assertFalse(form._check_unique_namespace_instance("instance")) form = formClass(instance=page_a_2) self.assertTrue(form._check_unique_namespace_instance("instance")) formClass = type(AdvancedSettingsForm)( 'AdvancedSettingsForm', (AdvancedSettingsForm,), {'Meta': Meta, '_site': site2, '_request': request}, ) form = formClass(instance=page_b_1) self.assertFalse(form._check_unique_namespace_instance("instance")) self.apphook_clear() @override_settings(ROOT_URLCONF='cms.test_utils.project.second_urls_for_apphook_tests') def test_get_page_for_apphook(self): en_title, de_title = self.create_base_structure(APP_NAME, ['en', 'de']) with force_language("en"): path = reverse('sample-settings') request = self.get_request(path) request.LANGUAGE_CODE = 'en' attached_to_page = applications_page_check(request) self.assertEqual(attached_to_page.pk, en_title.page.pk) response = self.client.get(path) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'sampleapp/home.html') self.assertContains(response, en_title.title) with force_language("de"): path = reverse('sample-settings') request = self.get_request(path) request.LANGUAGE_CODE = 'de' attached_to_page = applications_page_check(request) self.assertEqual(attached_to_page.pk, de_title.page.pk) response = self.client.get(path) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'sampleapp/home.html') self.assertContains(response, de_title.title) self.apphook_clear() @override_settings(ROOT_URLCONF='cms.test_utils.project.second_urls_for_apphook_tests') def test_apphook_on_page_type_is_not_routed(self): """Page types are not served on the public site. An apphook attached to one must not be dispatched either -- its patterns are matched before the catch-all ``cms.views.details`` route, which would bypass the check there.""" en_title, de_title = self.create_base_structure(APP_NAME, ['en', 'de']) with force_language("en"): path = reverse('sample-settings') self.assertEqual(self.client.get(path).status_code, 200) en_title.page.update(is_page_type=True) self.reload_urls() self.assertEqual(self.client.get(path).status_code, 404) self.apphook_clear() @override_settings(ROOT_URLCONF='cms.test_utils.project.second_urls_for_apphook_tests') def test_apphook_permissions(self): en_title, de_title = self.create_base_structure(APP_NAME, ['en', 'de']) with force_language("en"): path = reverse('sample-settings') response = self.client.get(path) self.assertEqual(response.status_code, 200) en_title.page.update(login_required=True) response = self.client.get(path) self.assertEqual(response.status_code, 302) self.apphook_clear() @override_settings(ROOT_URLCONF='cms.test_utils.project.second_urls_for_apphook_tests') def test_apphook_permissions_preserves_view_name(self): self.create_base_structure(APP_NAME, ['en', 'de']) view_names = ( ('sample-settings', 'sample_view'), ('sample-class-view', 'ClassView'), ('sample-class-based-view', 'view'), # Naming convention changed in Django 4 ) with force_language("en"): for url_name, view_name in view_names: path = reverse(url_name) match = resolve(path) self.assertEqual(match.func.__name__, view_name) def test_apphooks_with_excluded_permissions(self): en_title = self.create_base_structure('SampleAppWithExcludedPermissions', 'en') with force_language("en"): excluded_path = reverse('excluded:example') not_excluded_path = reverse('not_excluded:example') en_title.page.update(login_required=True) excluded_response = self.client.get(excluded_path) not_excluded_response = self.client.get(not_excluded_path) self.assertEqual(excluded_response.status_code, 200) self.assertEqual(not_excluded_response.status_code, 302) self.apphook_clear() @override_settings(CMS_APPHOOKS=[f'{APP_MODULE}.{APP_NAME}']) def test_apphook_without_name_defaults_to_class_name(self): """ Test that an apphook without a name defaults to the class name. """ @apphook_pool.register class AppWithoutName(CMSApp): def get_urls(self, page=None, language=None, **kwargs): return ["sampleapp.urls"] @apphook_pool.register class AppWithName(CMSApp): name = "Custom name" def get_urls(self, page=None, language=None, **kwargs): return ["sampleapp.urls"] hooks = apphook_pool.get_apphooks() hook_names = dict(hooks) self.assertEqual(hook_names.get('AppWithoutName'), 'AppWithoutName') self.assertEqual(hook_names.get('AppWithName'), 'Custom name') @override_settings(ROOT_URLCONF='cms.test_utils.project.urls_3') def test_get_page_for_apphook_on_preview_or_edit(self): if get_user_model().USERNAME_FIELD == 'email': superuser = get_user_model().objects.create_superuser('admin', 'admin@admin.com', 'admin@admin.com') else: superuser = get_user_model().objects.create_superuser('admin', 'admin@admin.com', 'admin') page = create_page("home", "nav_playground.html", "en", created_by=superuser, apphook=APP_NAME) create_page_content('de', page.get_title(), page) with force_language("en"): path = reverse('sample-settings') request = self.get_request(path) request.LANGUAGE_CODE = 'en' attached_to_page = applications_page_check(request) self.assertEqual(attached_to_page.pk, page.pk) with force_language("de"): path = reverse('sample-settings') request = self.get_request(path) request.LANGUAGE_CODE = 'de' attached_to_page = applications_page_check(request) self.assertEqual(attached_to_page.pk, page.pk) with self.login_user_context(superuser): with force_language("en"): path = reverse('sample-settings') request = self.get_request(path) request.LANGUAGE_CODE = 'en' attached_to_page = applications_page_check(request) self.assertEqual(attached_to_page.pk, page.pk) with force_language("de"): path = reverse('sample-settings') request = self.get_request(path) request.LANGUAGE_CODE = 'de' attached_to_page = applications_page_check(request) self.assertEqual(attached_to_page.pk, page.pk) @override_settings(ROOT_URLCONF='cms.test_utils.project.second_urls_for_apphook_tests') def test_get_root_page_for_apphook_with_instance_namespace(self): en_title = self.create_base_structure(NS_APP_NAME, 'en', 'instance_ns') self.reload_urls() with force_language("en"): reverse("example_app:example") reverse("example1:example") reverse("example2:example") path = reverse('namespaced_app_ns:sample-root') path_instance = reverse('instance_ns:sample-root') self.assertEqual(path, path_instance) request = self.get_request(path) request.LANGUAGE_CODE = 'en' attached_to_page = applications_page_check(request) self.assertEqual(attached_to_page.pk, en_title.page.pk) self.apphook_clear() @override_settings(ROOT_URLCONF='cms.test_utils.project.second_urls_for_apphook_tests') def test_get_child_page_for_apphook_with_instance_namespace(self): en_title = self.create_base_structure(NS_APP_NAME, 'en', 'instance_ns') with force_language("en"): path = reverse('namespaced_app_ns:sample-settings') path_instance1 = reverse('instance_ns:sample-settings') path_instance2 = reverse('namespaced_app_ns:sample-settings', current_app='instance_ns') self.assertEqual(path, path_instance1) self.assertEqual(path, path_instance2) request = self.get_request(path) request.LANGUAGE_CODE = 'en' attached_to_page = applications_page_check(request) self.assertEqual(attached_to_page.pk, en_title.page_id) self.apphook_clear() @override_settings(ROOT_URLCONF='cms.test_utils.project.second_urls_for_apphook_tests') def test_get_sub_page_for_apphook_with_implicit_current_app(self): en_title = self.create_base_structure(NS_APP_NAME, 'en', 'namespaced_app_ns') with force_language("en"): path = reverse('namespaced_app_ns:current-app') request = self.get_request(path) request.LANGUAGE_CODE = 'en' attached_to_page = applications_page_check(request) self.assertEqual(attached_to_page.pk, en_title.page.pk) response = self.client.get(path) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'sampleapp/app.html') self.assertContains(response, 'namespaced_app_ns') self.assertContains(response, path) self.apphook_clear() @override_settings(ROOT_URLCONF='cms.test_utils.project.second_urls_for_apphook_tests') def test_default_language_changer_with_implicit_current_app(self): self.create_base_structure(NS_APP_NAME, ['en', 'de'], 'namespaced_app_ns') self.reload_urls() with force_language("en"): path = reverse('namespaced_app_ns:translated-url') request = self.get_request(path) request.LANGUAGE_CODE = 'en' url = DefaultLanguageChanger(request)('en') self.assertEqual(url, path) url = DefaultLanguageChanger(request)('de') self.assertEqual(url, '/de%s' % path[3:].replace('/page', '/Seite')) self.apphook_clear() @override_settings(ROOT_URLCONF='cms.test_utils.project.second_urls_for_apphook_tests') def test_get_i18n_apphook_with_explicit_current_app(self): self.apphook_clear() titles = self.create_base_structure(NS_APP_NAME, ['en', 'de'], 'instance_1') public_de_title = titles[1] de_title = PageContent.objects.get(page=public_de_title.page, language="de") self.reload_urls() self.apphook_clear() page2 = create_page("page2", "nav_playground.html", language="en", created_by=self.superuser, parent=de_title.page.parent, apphook=NS_APP_NAME, apphook_namespace="instance_2") create_page_content("de", "de_title", page2, slug="slug") clear_app_resolvers() clear_url_caches() if APP_MODULE in sys.modules: del sys.modules[APP_MODULE] self.reload_urls() with force_language("de"): reverse('namespaced_app_ns:current-app', current_app="instance_1") reverse('namespaced_app_ns:current-app', current_app="instance_2") reverse('namespaced_app_ns:current-app') with force_language("en"): reverse('namespaced_app_ns:current-app', current_app="instance_1") reverse('namespaced_app_ns:current-app', current_app="instance_2") reverse('namespaced_app_ns:current-app') self.apphook_clear() @override_settings(ROOT_URLCONF='cms.test_utils.project.second_urls_for_apphook_tests') def test_apphook_include_extra_parameters(self): self.create_base_structure(NS_APP_NAME, ['en', 'de'], 'instance_1') with force_language("en"): path = reverse('namespaced_app_ns:extra_second') request = self.get_request(path) request.LANGUAGE_CODE = 'en' response = self.client.get(path) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'sampleapp/extra.html') self.assertContains(response, 'someopts') @override_settings(ROOT_URLCONF='cms.test_utils.project.second_urls_for_apphook_tests') def test_get_sub_page_for_apphook_with_explicit_current_app(self): en_title = self.create_base_structure(NS_APP_NAME, 'en', 'instance_ns') with force_language("en"): path = reverse('namespaced_app_ns:current-app') request = self.get_request(path) request.LANGUAGE_CODE = 'en' attached_to_page = applications_page_check(request) self.assertEqual(attached_to_page.pk, en_title.page.pk) response = self.client.get(path) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'sampleapp/app.html') self.assertContains(response, 'instance_ns') self.assertContains(response, path) self.apphook_clear() @override_settings(ROOT_URLCONF='cms.test_utils.project.second_urls_for_apphook_tests') def test_include_urlconf(self): self.create_base_structure(APP_NAME, 'en') path = reverse('extra_second') response = self.client.get(path) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'sampleapp/extra.html') self.assertContains(response, "test included urlconf") path = reverse('extra_first') response = self.client.get(path) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'sampleapp/extra.html') self.assertContains(response, "test urlconf") with force_language("de"): path = reverse('extra_first') response = self.client.get(path) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'sampleapp/extra.html') self.assertContains(response, "test urlconf") with force_language("de"): path = reverse('extra_second') response = self.client.get(path) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'sampleapp/extra.html') self.assertContains(response, "test included urlconf") self.apphook_clear() @override_settings(CMS_PERMISSION=False, ROOT_URLCONF='cms.test_utils.project.urls_2') def test_apphook_breaking_under_home_with_new_path_caching(self): home = self.create_homepage("home", "nav_playground.html", "en") child = create_page("child", "nav_playground.html", "en", parent=home) # not-home is what breaks stuff, because it contains the slug of the home page not_home = create_page("not-home", "nav_playground.html", "en", parent=child) create_page("subchild", "nav_playground.html", "en", parent=not_home, apphook='SampleApp') with force_language("en"): self.reload_urls() urlpatterns = get_app_patterns() resolver = urlpatterns[0] url = resolver.reverse('sample-root') self.assertEqual(url, 'child/not-home/subchild/') @override_settings(CMS_PERMISSION=False, ROOT_URLCONF='cms.test_utils.project.urls_2') def test_apphook_on_page_without_path_is_skipped(self): """A page whose ``PageUrl.path`` is ``None`` must not break url resolution. Regression test for #8886: ``_get_app_patterns`` built ``page_url.path + "/"`` unconditionally, so a single such page raised ``TypeError`` and took down every apphook on the site -- including the admin -- until the database was edited by hand. Unpublishing a page with djangocms-versioning leaves exactly this state. """ home = self.create_homepage("home", "nav_playground.html", "en") create_page("live-app", "nav_playground.html", "en", parent=home, apphook='SampleApp') pathless = create_page("pathless-app", "nav_playground.html", "en", parent=home, apphook='SampleApp') # Without page content the url keeps its slug but loses its path -- the core # equivalent of an unpublished page. PageContent.objects.filter(page=pathless, language="en").delete() pathless.update_urls_from_content("en") self.assertIsNone(pathless.urls.get(language="en").path) with force_language("en"): self.reload_urls() urlpatterns = get_app_patterns() # must not raise TypeError # the remaining apphook still resolves, i.e. the pathless page was skipped # rather than the whole pattern list being abandoned self.assertEqual(urlpatterns[0].reverse('sample-root'), 'live-app/') @override_settings(ROOT_URLCONF='cms.test_utils.project.urls') def test_apphook_urlpattern_order(self): # this one includes the actual cms.urls, so it can be tested if # they are loaded in the correct order (the cms page pattern must be last) # (the other testcases replicate the inclusion code and thus don't test this) self.create_base_structure(APP_NAME, 'en') path = reverse('extra_second') response = self.client.get(path) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'sampleapp/extra.html') self.assertContains(response, "test included urlconf") @override_settings(ROOT_URLCONF='cms.test_utils.project.urls') def test_apphooks_receive_url_params(self): # make sure that urlparams actually reach the apphook views self.create_base_structure(APP_NAME, 'en') path = reverse('sample-params', kwargs=dict(my_params='is-my-param-really-in-the-context-QUESTIONMARK')) response = self.client.get(path) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'sampleapp/home.html') self.assertContains(response, 'my_params: is-my-param-really-in-the-context-QUESTIONMARK') @override_settings(ROOT_URLCONF='cms.test_utils.project.third_urls_for_apphook_tests') def test_multiple_apphooks(self): # test for #1538 self.apphook_clear() superuser = get_user_model().objects.create_superuser('admin', 'admin@admin.com', 'admin') create_page("home", "nav_playground.html", "en", created_by=superuser, ) create_page("apphook1-page", "nav_playground.html", "en", created_by=superuser, apphook="SampleApp") create_page("apphook2-page", "nav_playground.html", "en", created_by=superuser, apphook="SampleApp2") reverse('sample-root') reverse('sample2-root') self.apphook_clear() @override_settings(ROOT_URLCONF='cms.test_utils.project.fourth_urls_for_apphook_tests') def test_apphooks_return_urls_directly(self): self.apphook_clear() superuser = get_user_model().objects.create_superuser('admin', 'admin@admin.com', 'admin') create_page("apphooked3-page", "nav_playground.html", "en", created_by=superuser, apphook="SampleApp3") self.reload_urls() path = reverse('sample3-root') response = self.client.get(path) self.assertEqual(response.status_code, 200) self.assertContains(response, 'Sample App 3 Response') self.apphook_clear() def test_apphook_pool_register_returns_apphook(self): @apphook_pool.register class TestApp(CMSApp): name = "Test App" self.assertIsNotNone(TestApp) # Now test the quick return codepath, when apphooks is not empty apphook_pool.apphooks.append("foo") @apphook_pool.register class TestApp2(CMSApp): name = "Test App 2" self.assertIsNotNone(TestApp2) @override_settings(ROOT_URLCONF='cms.test_utils.project.second_urls_for_apphook_tests') def test_apphook_csrf_exempt_endpoint(self): self.create_base_structure(NS_APP_NAME, 'en', 'instance_ns') client = self.client_class(enforce_csrf_checks=True) with force_language("en"): path = reverse('namespaced_app_ns:sample-exempt') response = client.post(path) # Assert our POST request went through self.assertEqual(response.status_code, 200) with force_language("en"): path = reverse('namespaced_app_ns:sample-account') response = client.post(path) # Assert our POST request did not go through self.assertEqual(response.status_code, 403) @override_settings(ROOT_URLCONF='cms.test_utils.project.second_urls_for_apphook_tests') def test_toolbar_current_app_namespace(self): self.create_base_structure(NS_APP_NAME, 'en', 'instance_ns') with force_language("en"): path = reverse('namespaced_app_ns:sample-settings') request = self.get_request(path) toolbar = CMSToolbar(request) self.assertTrue( toolbar.toolbars['cms.test_utils.project.sampleapp.cms_toolbars.CategoryToolbar'].is_current_app ) self.assertFalse( toolbar.toolbars[ 'cms.test_utils.project.extensionapp.cms_toolbars.MyPageContentExtensionToolbar' ].is_current_app ) # Testing a decorated view with force_language("en"): path = reverse('namespaced_app_ns:sample-exempt') request = self.get_request(path) toolbar = CMSToolbar(request) self.assertEqual( toolbar.toolbars['cms.test_utils.project.sampleapp.cms_toolbars.CategoryToolbar'].app_path, 'cms.test_utils.project.sampleapp' ) self.assertTrue( toolbar.toolbars['cms.test_utils.project.sampleapp.cms_toolbars.CategoryToolbar'].is_current_app ) self.assertEqual( toolbar.toolbars[ 'cms.test_utils.project.extensionapp.cms_toolbars.MyPageContentExtensionToolbar' ].app_path, 'cms.test_utils.project.sampleapp' ) self.assertFalse( toolbar.toolbars[ 'cms.test_utils.project.extensionapp.cms_toolbars.MyPageContentExtensionToolbar' ].is_current_app ) @override_settings(ROOT_URLCONF='cms.test_utils.project.second_urls_for_apphook_tests') def test_toolbar_current_app_apphook_with_implicit_current_app(self): self.create_base_structure(NS_APP_NAME, 'en', 'namespaced_app_ns') with force_language("en"): path = reverse('namespaced_app_ns:current-app') request = self.get_request(path) toolbar = CMSToolbar(request) self.assertEqual( toolbar.toolbars['cms.test_utils.project.sampleapp.cms_toolbars.CategoryToolbar'].app_path, 'cms.test_utils.project.sampleapp' ) self.assertTrue( toolbar.toolbars['cms.test_utils.project.sampleapp.cms_toolbars.CategoryToolbar'].is_current_app ) self.assertEqual( toolbar.toolbars[ 'cms.test_utils.project.extensionapp.cms_toolbars.MyPageContentExtensionToolbar' ].app_path, 'cms.test_utils.project.sampleapp' ) self.assertFalse( toolbar.toolbars[ 'cms.test_utils.project.extensionapp.cms_toolbars.MyPageContentExtensionToolbar' ].is_current_app ) @override_settings(ROOT_URLCONF='cms.test_utils.project.placeholderapp_urls') def test_toolbar_no_namespace(self): # Test with a basic application with no defined app_name and no namespace self.create_base_structure(APP_NAME, 'en') path = reverse('detail', kwargs={'id': 20}) request = self.get_request(path) toolbar = CMSToolbar(request) self.assertFalse( toolbar.toolbars['cms.test_utils.project.sampleapp.cms_toolbars.CategoryToolbar'].is_current_app ) self.assertFalse( toolbar.toolbars[ 'cms.test_utils.project.extensionapp.cms_toolbars.MyPageContentExtensionToolbar' ].is_current_app ) self.assertTrue( toolbar.toolbars['cms.test_utils.project.placeholderapp.cms_toolbars.Example1Toolbar'].is_current_app ) @override_settings(ROOT_URLCONF='cms.test_utils.project.placeholderapp_urls') def test_toolbar_multiple_supported_apps(self): # Test with a basic application with no defined app_name and no namespace self.create_base_structure(APP_NAME, 'en') path = reverse('detail', kwargs={'id': 20}) request = self.get_request(path) toolbar = CMSToolbar(request) self.assertEqual(toolbar.toolbars['cms.test_utils.project.sampleapp.cms_toolbars.CategoryToolbar'].app_path, 'cms.test_utils.project.placeholderapp') self.assertFalse( toolbar.toolbars['cms.test_utils.project.sampleapp.cms_toolbars.CategoryToolbar'].is_current_app ) self.assertEqual( toolbar.toolbars[ 'cms.test_utils.project.extensionapp.cms_toolbars.MyPageContentExtensionToolbar'] .app_path, 'cms.test_utils.project.placeholderapp' ) self.assertFalse( toolbar.toolbars[ 'cms.test_utils.project.extensionapp.cms_toolbars.MyPageContentExtensionToolbar' ].is_current_app ) self.assertEqual( toolbar.toolbars['cms.test_utils.project.extensionapp.cms_toolbars.MyPageExtensionToolbar'].app_path, 'cms.test_utils.project.placeholderapp' ) self.assertTrue( toolbar.toolbars['cms.test_utils.project.extensionapp.cms_toolbars.MyPageExtensionToolbar'].is_current_app ) self.assertEqual( toolbar.toolbars['cms.test_utils.project.placeholderapp.cms_toolbars.Example1Toolbar'].app_path, 'cms.test_utils.project.placeholderapp' ) self.assertTrue( toolbar.toolbars['cms.test_utils.project.placeholderapp.cms_toolbars.Example1Toolbar'].is_current_app ) @override_settings( CMS_APPHOOKS=['cms.test_utils.project.placeholderapp.cms_apps.Example1App'], ROOT_URLCONF='cms.test_utils.project.placeholderapp_urls', ) def test_toolbar_staff(self): # Test that the toolbar contains edit mode switcher if placeholders are available apphooks = ( 'cms.test_utils.project.placeholderapp.cms_apps.Example1App', ) switcher_id = 'Mode Switcher' with self.settings(CMS_APPHOOKS=apphooks, ROOT_URLCONF='cms.test_utils.project.placeholderapp_urls'): self.create_base_structure('Example1App', 'en') ex1 = Example1.objects.create(char_1='1', char_2='2', char_3='3', char_4='4', date_field=now()) path = reverse('example_detail', kwargs={'pk': ex1.pk}) self.user = self._create_user('admin_staff', True, True) with self.login_user_context(self.user): response = self.client.get(path + "?toolbar_on") request = response.context['request'] toolbar = request.toolbar items = toolbar.get_right_items() switchers = [item.identifier for item in items if getattr(item, 'identifier', '') == switcher_id] self.assertEqual(len(switchers), 1) self.user = self._create_user('staff', True, False) with self.login_user_context(self.user): response = self.client.get(path + "?toolbar_on") request = response.context['request'] request.user = get_user_model().objects.get(pk=self.user.pk) toolbar = request.toolbar items = toolbar.get_right_items() switchers = [item.identifier for item in items if getattr(item, 'identifier', '') == switcher_id] self.assertEqual(len(switchers), 0) self.user.user_permissions.add(Permission.objects.get(codename='change_example1')) with self.login_user_context(self.user): response = self.client.get(path + "?toolbar_on") request = response.context['request'] request.user = get_user_model().objects.get(pk=self.user.pk) toolbar = request.toolbar items = toolbar.get_right_items() switchers = [item.identifier for item in items if getattr(item, 'identifier', '') == switcher_id] self.assertEqual(len(switchers), 0) self.user.user_permissions.add(Permission.objects.get(codename='use_structure')) with self.login_user_context(self.user): response = self.client.get(path + "?toolbar_on") request = response.context['request'] request.user = get_user_model().objects.get(pk=self.user.pk) toolbar = request.toolbar items = toolbar.get_right_items() switchers = [item.identifier for item in items if getattr(item, 'identifier', '') == switcher_id] self.assertEqual(len(switchers), 1) self.user = None def test_nested_apphooks_urls(self): # make sure that urlparams actually reach the apphook views with self.settings(ROOT_URLCONF='cms.test_utils.project.urls'): self.apphook_clear() superuser = get_user_model().objects.create_superuser('admin', 'admin@admin.com', 'admin') create_page("home", "nav_playground.html", "en", created_by=superuser, ) parent_page = create_page("parent-apphook-page", "nav_playground.html", "en", created_by=superuser, apphook="ParentApp") create_page("child-apphook-page", "nav_playground.html", "en", parent=parent_page, created_by=superuser, apphook="ChildApp") parent_app_path = reverse('parentapp_view', kwargs={'path': 'parent/path/'}) child_app_path = reverse('childapp_view', kwargs={'path': 'child-path/'}) # Ensure the page structure is ok before getting responses self.assertEqual(parent_app_path, '/en/parent-apphook-page/parent/path/') self.assertEqual(child_app_path, '/en/parent-apphook-page/child-apphook-page/child-path/') # Get responses for both paths and ensure that the right view will answer response = self.client.get(parent_app_path) self.assertContains(response, 'parent app content', status_code=200) response = self.client.get(child_app_path) self.assertContains(response, 'child app content', status_code=200) self.apphook_clear() @override_settings(ROOT_URLCONF='cms.test_utils.project.second_urls_for_apphook_tests') def test_get_apps(self): """ Check that urlconf are dynamically loaded according to the different page the apphook is attached to """ titles = self.create_base_structure('VariableUrlsApp', ['en', 'de']) titles[0].page.reverse_id = 'page1' titles[0].page.save() self.reload_urls() # only one urlconf is configured given that only one page is created with force_language('de'): reverse('extra_first') with self.assertRaises(NoReverseMatch): reverse('sample2-root') self.reload_urls() self.apphook_clear() page2 = create_page('page2', 'nav_playground.html', 'en', created_by=self.superuser, parent=titles[0].page.parent, apphook='VariableUrlsApp', reverse_id='page2') create_page_content('de', 'de_title', page2, slug='slug') self.reload_urls() with force_language('de'): reverse('sample2-root') reverse('extra_first') self.apphook_clear() @override_settings(ROOT_URLCONF='cms.test_utils.project.second_urls_for_apphook_tests') def test_get_menus(self): """ Check that menus are dynamically loaded according to the different page the apphook is attached to """ titles = self.create_base_structure('VariableUrlsApp', ['en', 'de']) titles[0].page.reverse_id = 'page1' titles[0].page.save() cache.clear() self.reload_urls() menu_pool.discover_menus() cache.clear() request = self.get_request('/') renderer = menu_pool.get_renderer(request) nodes = renderer.get_nodes() nodes_urls = [node.url for node in nodes] self.assertTrue(reverse('sample-account') in nodes_urls) self.assertFalse('/en/child_page/page2/' in nodes_urls) self.reload_urls() self.apphook_clear() cache.clear() self.reload_urls() page2 = create_page('page2', 'nav_playground.html', 'en', created_by=self.superuser, parent=titles[0].page.parent, in_navigation=True, apphook='VariableUrlsApp', reverse_id='page2') create_page_content('de', 'de_title', page2, slug='slug') request = self.get_request('/page2/') renderer = menu_pool.get_renderer(request) nodes = renderer.get_nodes() nodes_urls = [node.url for node in nodes] self.assertTrue(reverse('sample-account') in nodes_urls) self.assertTrue(reverse('sample2-root') in nodes_urls) self.assertTrue('/static/fresh/' in nodes_urls) self.apphook_clear() @override_settings( CMS_APPHOOKS=['cms.test_utils.project.sampleapp.cms_apps.AppWithNoMenu'], ) def test_menu_node_is_selected_on_app_root(self): """ If a user requests a page with an apphook, the menu should mark the node for that page as selected. """ defaults = { 'language': 'en', 'in_navigation': True, 'template': 'nav_playground.html', } homepage = create_page('EN-P1', **defaults) homepage.set_as_homepage() app_root = create_page('EN-P2', apphook='AppWithNoMenu', apphook_namespace='app_with_no_menu', **defaults) # Public version request = self.get_request('/en/en-p2/') request.current_page = get_page(request) menu_nodes = menu_pool.get_renderer(request).get_nodes() self.assertEqual(len(menu_nodes), 2) self.assertEqual(menu_nodes[0].id, homepage.pk) self.assertEqual(menu_nodes[0].selected, False) self.assertEqual(menu_nodes[1].id, app_root.pk) self.assertEqual(menu_nodes[1].selected, True) @override_settings( CMS_APPHOOKS=['cms.test_utils.project.sampleapp.cms_apps.AppWithNoMenu'], ) def test_menu_node_is_selected_on_app_sub_path(self): """ If a user requests a path belonging to an apphook, the menu should mark the node for the apphook page as selected. """ # Refs - https://github.com/divio/django-cms/issues/6336 defaults = { 'language': 'en', 'in_navigation': True, 'template': 'nav_playground.html', } homepage = create_page('EN-P1', **defaults) homepage.set_as_homepage() app_root = create_page('EN-P2', apphook='AppWithNoMenu', apphook_namespace='app_with_no_menu', **defaults) request = self.get_request('/en/en-p2/settings/') request.current_page = get_page(request) menu_nodes = menu_pool.get_renderer(request).get_nodes() self.assertEqual(len(menu_nodes), 2) self.assertEqual(menu_nodes[0].id, homepage.pk) self.assertEqual(menu_nodes[0].selected, False) self.assertEqual(menu_nodes[1].id, app_root.pk) self.assertEqual(menu_nodes[1].selected, True) @override_settings(CMS_APPHOOKS=['cms.test_utils.project.sampleapp.cms_apps.SampleApp3'],) def test_apphook_root_page_placeholders_in_endpoints(self): """ Test that page content placeholders are available in edit and preview endpoints for the root page of an apphook. """ from django.contrib.contenttypes.models import ContentType from cms.toolbar.utils import get_object_edit_url, get_object_preview_url, get_object_structure_url # Create a page with an apphook attached self.apphook_clear() superuser = get_user_model().objects.create_superuser('admin', 'admin@admin.com', 'admin') page = create_page( "apphook-root", "nav_playground.html", "en", created_by=superuser, apphook="SampleApp3" ) content = page.get_content_obj("en") self.reload_urls() # Test edit endpoint edit_url = get_object_edit_url(content, language="en") with self.login_user_context(superuser): response = self.client.get(edit_url) self.assertEqual(response.status_code, 200) # Verify the apphook view is called self.assertContains(response, 'Sample App 3 Response') # Check that toolbar was set with the page content object self.assertTrue(hasattr(response.wsgi_request, 'toolbar')) toolbar = response.wsgi_request.toolbar # Verify the toolbar object is the page content self.assertIsNotNone(toolbar.obj, "toolbar.obj should be set for edit endpoint") self.assertEqual(toolbar.obj, content, "toolbar.obj should match the page content") # Test preview endpoint preview_url = get_object_preview_url(content, language="en") with self.login_user_context(superuser): response = self.client.get(preview_url) self.assertEqual(response.status_code, 200) # Verify the apphook view is called self.assertContains(response, 'Sample App 3 Response') # Check that toolbar was set with the page content object self.assertTrue(hasattr(response.wsgi_request, 'toolbar')) toolbar = response.wsgi_request.toolbar # Verify the toolbar object is the page content self.assertIsNotNone(toolbar.obj, "toolbar.obj should be set for preview endpoint") self.assertEqual(toolbar.obj, content, "toolbar.obj should match the page content") # Test structure endpoint structure_url = get_object_structure_url(content, language="en") with self.login_user_context(superuser): response = self.client.get(structure_url) self.assertEqual(response.status_code, 200) # Check that toolbar was set with the page content object self.assertTrue(hasattr(response.wsgi_request, 'toolbar')) toolbar = response.wsgi_request.toolbar # Verify the toolbar object is the page content self.assertIsNotNone(toolbar.obj, "toolbar.obj should be set for structure endpoint") self.assertEqual(toolbar.obj, content, "toolbar.obj should match the page content") # Verify that page content placeholders are present in the markup for placeholder in content.get_placeholders(): # Check that placeholder ID is in the response content self.assertContains( response, f'"placeholder_id": "{placeholder.pk}"', msg_prefix=f"Placeholder {placeholder.slot} should be present in structure markup" ) self.apphook_clear() class ApphooksPageLanguageUrlTestCase(CMSTestCase): def setUp(self): clear_app_resolvers() clear_url_caches() if APP_MODULE in sys.modules: del sys.modules[APP_MODULE] self.reload_urls() def tearDown(self): clear_app_resolvers() clear_url_caches() if APP_MODULE in sys.modules: del sys.modules[APP_MODULE] self.apphook_clear() def reload_urls(self): from django.conf import settings url_modules = [ 'cms.urls', 'cms.test_utils.project.second_cms_urls_for_apphook_tests', settings.ROOT_URLCONF, ] clear_app_resolvers() clear_url_caches() for module in url_modules: if module in sys.modules: del sys.modules[module] def test_page_language_url_for_apphook(self): self.apphook_clear() superuser = get_user_model().objects.create_superuser('admin', 'admin@admin.com', 'admin') page = self.create_homepage("home", "nav_playground.html", "en", created_by=superuser) create_page_content('de', page.get_title(), page) child_page = create_page("child_page", "nav_playground.html", "en", created_by=superuser, parent=page) create_page_content('de', child_page.get_title(), child_page) child_page.refresh_from_db() child_child_page = create_page("child_child_page", "nav_playground.html", "en", created_by=superuser, parent=child_page, apphook='SampleApp') create_page_content("de", f'{child_child_page.get_title()}_de', child_child_page) child_child_page.refresh_from_db() with force_language("en"): path = reverse('extra_first') request = self.get_request(path) request.LANGUAGE_CODE = 'en' request.current_page = child_child_page fake_context = {'request': request} tag = DumbPageLanguageUrl() output = tag.get_context(fake_context, 'en') url = output['content'] self.assertEqual(url, '/en/child_page/child_child_page/extra_1/') output = tag.get_context(fake_context, 'de') url = output['content'] # look the extra "_de" self.assertEqual(url, '/de/child_page/child_child_page_de/extra_1/') output = tag.get_context(fake_context, 'fr') url = output['content'] self.assertEqual(url, '/en/child_page/child_child_page/extra_1/') self.apphook_clear() @override_settings(SITE_ID="") class ApphooksSiteTestCase(BaseApphooksTestCase): """ Test cases for apphooks in a multisite configuration using a single instance. Sites are mocked for the test, in reality either taken from the request or from a middleware. These test cases verify that apphooks are only accessible on their respective sites when multiple sites are configured in Django CMS. """ def create_pages(self): self.apphook_clear() self.site1 = Site.objects.get(pk=1) self.site2 = Site.objects.create(domain='otherserver', name='Site 2') superuser = self.get_superuser() self.page1 = create_page("Page 1", "nav_playground.html", "de", site=self.site1, apphook='SampleApp', created_by=superuser) create_page_content("en", "en_title", self.page1, slug="page-1") self.page2 = create_page("Page 2", "nav_playground.html", "de", site=self.site1, created_by=superuser) self.page3 = create_page("Page 3", "nav_playground.html", "de", site=self.site2, apphook='SampleApp', created_by=superuser) self.page4 = create_page("Page 4", "nav_playground.html", "de", site=self.site2, created_by=superuser) self.page2.set_as_homepage() self.page4.set_as_homepage() activate("de") def get_for_site(self, site, url): resolver_match = resolve(url) request = self.get_request(url) request.META["host"] = f"{site.domain}:80" request.site = site request.resolver_match = resolver_match try: return resolver_match.func(request, *resolver_match.args, **resolver_match.kwargs) except Http404 as e: return HttpResponse(str(e), status=404) def test_apphook_access_on_site1(self): self.create_pages() # Verify that the cms_site_filter decorator was called url = self.page1.get_absolute_url("de") response = self.get_for_site(self.site1, url) self.assertEqual(response.status_code, 200) response = self.get_for_site(self.site1, '/de/page-3/') self.assertEqual(response.status_code, 404) # Sollte nicht erreichbar sein def test_apphook_access_on_site2(self): self.create_pages() # Verify that the cms_site_filter decorator was called url = self.page3.get_absolute_url("de") response = self.get_for_site(self.site2, url) self.assertEqual(response.status_code, 200) response = self.get_for_site(self.site2, '/de/page-1/') self.assertEqual(response.status_code, 404) # Sollte nicht erreichbar sein @override_settings(ROOT_URLCONF='cms.test_utils.project.second_urls_for_apphook_tests') def test_apphook_site_filter_preserves_view_name(self): self.create_pages() # Force a fresh import of the urlconf now that the apphook pages exist so # that the global APP_RESOLVERS are rebuilt for the overridden # ROOT_URLCONF. Without this, reverse() below depends on whether prior # tests left the url modules cached in sys.modules (order dependence). self.reload_urls() view_names = ( ('sample-settings', 'sample_view'), ('sample-class-view', 'ClassView'), ('sample-class-based-view', 'view'), ) with force_language("de"): for url_name, view_name in view_names: path = reverse(url_name) match = resolve(path) self.assertEqual(match.func.__name__, view_name) class ApphookFrontendEditingTests(BaseApphooksTestCase): """ Tests for rendering external models attached via apphooks in frontend editing mode. Ensures that context remains consistent (e.g., current_page is resolved). """ @override_settings(ROOT_URLCONF='cms.test_utils.project.urls') def test_current_page_resolution_in_render_object_edit(self): from django.contrib.sessions.backends.base import SessionBase from cms.test_utils.project.sampleapp.models import Category # 1. Setup: Create an apphooked page superuser = self.get_superuser() create_page( title="Apphook Page", template="nav_playground.html", language="en", created_by=superuser, apphook="SampleApp" ) self.reload_urls() # 2. Setup: Create an external model instance (Category) category = Category.add_root(name="Test Category") ct = ContentType.objects.get_for_model(Category) # 3. Setup: Register a mock renderer cms_extension = apps.get_app_config('cms').cms_extension def mock_render_category(request, obj): t = Template("{% load cms_tags %}[{% page_attribute 'page_title' %}]") return HttpResponse(t.render(Context({"request": request}))) original_renderer = cms_extension.toolbar_enabled_models.get(Category) cms_extension.toolbar_enabled_models[Category] = mock_render_category try: # 4. Action: Create a request url = f"/admin/cms/placeholder/render-object-edit/{ct.pk}/{category.pk}/" request = RequestFactory().get(url) request.user = superuser request.session = SessionBase() request.toolbar = CMSToolbar(request) # 5. Execution response = render_object_edit(request, ct.pk, category.pk) content = response.content.decode('utf-8') # 6. Verification self.assertEqual(content, "[Apphook Page]") finally: if original_renderer: cms_extension.toolbar_enabled_models[Category] = original_renderer elif Category in cms_extension.toolbar_enabled_models: del cms_extension.toolbar_enabled_models[Category] def _register_root_template_urlconf(patterns): """Build a unique, throwaway urlconf module so it can be referenced by string path (which is what ``django.urls.get_resolver`` expects).""" name = f'cms.tests._root_template_test_urlconf_{uuid.uuid4().hex}' module = types.ModuleType(name) module.urlpatterns = patterns sys.modules[name] = module return name class GetRootTemplateTests(SimpleTestCase): """Unit tests for :meth:`cms.app_base.CMSApp.get_root_template`.""" def setUp(self): clear_url_caches() def tearDown(self): clear_url_caches() def _make_app(self, urlconf): class _App(CMSApp): _urls = [urlconf] return _App() def test_class_based_view_with_class_level_template_name(self): class HomeView(TemplateView): template_name = 'apphook/cbv_home.html' urlconf = _register_root_template_urlconf([path('', HomeView.as_view())]) self.assertEqual( self._make_app(urlconf).get_root_template(), 'apphook/cbv_home.html', ) def test_class_based_view_with_template_name_via_as_view_kwargs(self): # ``TemplateView.as_view(template_name=...)`` stashes the template on # ``view_initkwargs``; the lookup must pick it up from there. urlconf = _register_root_template_urlconf([ path('', TemplateView.as_view(template_name='apphook/inline.html')), ]) self.assertEqual( self._make_app(urlconf).get_root_template(), 'apphook/inline.html', ) def test_fbv_returning_template_response_returns_none(self): # The template name lives inside the response, not on the callable. # Without rendering the view, get_root_template can't see it. def home(request): return TemplateResponse(request, 'apphook/fbv_tr.html', {}) urlconf = _register_root_template_urlconf([path('', home)]) self.assertIsNone(self._make_app(urlconf).get_root_template()) def test_fbv_using_render_shortcut_returns_none(self): # ``render()`` returns a plain HttpResponse with the body already # rendered — by the time we'd inspect a response, the template name # is gone. def home(request): return render(request, 'apphook/fbv_render.html') urlconf = _register_root_template_urlconf([path('', home)]) self.assertIsNone(self._make_app(urlconf).get_root_template()) def test_subclass_can_override_get_root_template(self): # The advertised escape hatch when inference doesn't fit: override # the method on the apphook subclass. class _App(CMSApp): _urls = [] def get_root_template(self, page=None, language=None, **kwargs): return 'apphook/manual.html' self.assertEqual(_App().get_root_template(), 'apphook/manual.html') def test_returns_none_when_no_root_pattern(self): class DetailView(TemplateView): template_name = 'apphook/detail.html' urlconf = _register_root_template_urlconf([ path('detail/', DetailView.as_view()), ]) self.assertIsNone(self._make_app(urlconf).get_root_template()) def test_root_pattern_via_include(self): # ``path('', include(...))`` — the root pattern lives inside the # included urlconf and must still be discovered. class HomeView(TemplateView): template_name = 'apphook/inner_home.html' inner = _register_root_template_urlconf([path('', HomeView.as_view())]) outer = _register_root_template_urlconf([path('', include(inner))]) self.assertEqual( self._make_app(outer).get_root_template(), 'apphook/inner_home.html', ) def test_non_root_pattern_template_is_ignored(self): # When the root view doesn't expose a template, we must NOT fall back # to a sibling pattern's template — that would be misleading. def root_fbv(request): return TemplateResponse(request, 'apphook/root.html', {}) class AboutView(TemplateView): template_name = 'apphook/about.html' urlconf = _register_root_template_urlconf([ path('', root_fbv), path('about/', AboutView.as_view()), ]) self.assertIsNone(self._make_app(urlconf).get_root_template()) def test_inferred_template_is_not_cached(self): # Inferred templates must NOT be cached on the apphook instance: # apphook instances are shared singletons, and apphooks like # VariableUrlsApp return different urlconfs depending on page/language. # Caching the first inferred template would lock it in for every # subsequent (possibly differently-routed) call. class HomeOne(TemplateView): template_name = 'apphook/one.html' class HomeTwo(TemplateView): template_name = 'apphook/two.html' urlconf_one = _register_root_template_urlconf([path('', HomeOne.as_view())]) urlconf_two = _register_root_template_urlconf([path('', HomeTwo.as_view())]) urlconfs = iter([urlconf_one, urlconf_two]) class _App(CMSApp): def get_urls(self, page=None, language=None, **kwargs): return [next(urlconfs)] app = _App() self.assertEqual(app.get_root_template(), 'apphook/one.html') self.assertEqual(app.get_root_template(), 'apphook/two.html') def test_invalid_urlconf_string_returns_none(self): # A urlconf that fails to import shouldn't crash the lookup. class _App(CMSApp): _urls = ['cms.tests._does_not_exist_xyz'] self.assertIsNone(_App().get_root_template())