import json from django import template from django.core.cache import cache from django.core.cache.utils import make_template_fragment_key from django.http import HttpRequest from django.template import TemplateSyntaxError, VariableDoesNotExist from django.test import TestCase from django.test.utils import override_settings from django.urls.exceptions import NoReverseMatch from django.utils.safestring import SafeString from django.utils.translation import gettext_lazy from wagtail.coreutils import ( get_dummy_request, make_wagtail_template_fragment_key, resolve_model_string, ) from wagtail.models import Locale, Page, Site, SiteRootPath from wagtail.models.sites import ( SITE_ROOT_PATHS_CACHE_KEY, SITE_ROOT_PATHS_CACHE_VERSION, ) from wagtail.templatetags.wagtail_cache import WagtailPageCacheNode from wagtail.templatetags.wagtailcore_tags import richtext, slugurl from wagtail.test.testapp.models import SimplePage class TestPageUrlTags(TestCase): fixtures = ["test.json"] def setUp(self): super().setUp() # Clear caches cache.clear() def test_pageurl_tag(self): response = self.client.get("/events/") self.assertEqual(response.status_code, 200) self.assertContains(response, 'Christmas') def test_pageurl_with_named_url_fallback(self): tpl = template.Template( """{% load wagtailcore_tags %}Fallback""" ) with self.assertNumQueries(0): result = tpl.render(template.Context({"page": None})) self.assertIn('Fallback', result) def test_pageurl_with_get_absolute_url_object_fallback(self): class ObjectWithURLMethod: def get_absolute_url(self): return "/object-specific-url/" tpl = template.Template( """{% load wagtailcore_tags %}Fallback""" ) result = tpl.render( template.Context( {"page": None, "object_with_url_method": ObjectWithURLMethod()} ) ) self.assertIn('Fallback', result) def test_pageurl_with_valid_url_string_fallback(self): """ `django.shortcuts.resolve_url` accepts strings containing '.' or '/' as they are. """ tpl = template.Template( """ {% load wagtailcore_tags %} Same page fallback Homepage fallback Up one step fallback """ ) result = tpl.render(template.Context({"page": None})) self.assertIn('Same page fallback', result) self.assertIn('Homepage fallback', result) self.assertIn('Up one step fallback', result) def test_pageurl_with_invalid_url_string_fallback(self): """ Strings not containing '.' or '/', and not matching a named URL will error. """ tpl = template.Template( """{% load wagtailcore_tags %}Fallback""" ) with self.assertRaises(NoReverseMatch): tpl.render(template.Context({"page": None})) def test_slugurl_tag(self): response = self.client.get("/events/christmas/") self.assertEqual(response.status_code, 200) self.assertContains(response, 'Back to events index') def test_pageurl_without_request_in_context(self): page = Page.objects.get(url_path="/home/events/") tpl = template.Template( """{% load wagtailcore_tags %}{{ page.title }}""" ) # no 'request' object in context with self.assertNumQueries(7): result = tpl.render(template.Context({"page": page})) self.assertIn('Events', result) # 'request' object in context, but no 'site' attribute result = tpl.render( template.Context({"page": page, "request": get_dummy_request()}) ) self.assertIn('Events', result) def test_pageurl_caches(self): page = Page.objects.get(url_path="/home/events/") tpl = template.Template( """{% load wagtailcore_tags %}{{ page.title }}""" ) request = get_dummy_request() with self.assertNumQueries(8): result = tpl.render(template.Context({"page": page, "request": request})) self.assertIn('Events', result) with self.assertNumQueries(0): result = tpl.render(template.Context({"page": page, "request": request})) self.assertIn('Events', result) @override_settings(ALLOWED_HOSTS=["testserver", "localhost", "unknown.example.com"]) def test_pageurl_with_unknown_site(self): page = Page.objects.get(url_path="/home/events/") tpl = template.Template( """{% load wagtailcore_tags %}{{ page.title }}""" ) # 'request' object in context, but site is None request = get_dummy_request() request.META["HTTP_HOST"] = "unknown.example.com" with self.assertNumQueries(8): result = tpl.render(template.Context({"page": page, "request": request})) self.assertIn('Events', result) def test_bad_pageurl(self): tpl = template.Template( """{% load wagtailcore_tags %}{{ page.title }}""" ) with self.assertRaisesRegex( ValueError, "pageurl tag expected a Page object, got None" ): tpl.render(template.Context({"page": None})) def test_bad_slugurl(self): # no 'request' object in context result = slugurl(template.Context({}), "bad-slug-doesnt-exist") self.assertIsNone(result) # 'request' object in context, but no 'site' attribute result = slugurl( context=template.Context({"request": HttpRequest()}), slug="bad-slug-doesnt-exist", ) self.assertIsNone(result) @override_settings(ALLOWED_HOSTS=["testserver", "localhost", "site2.example.com"]) def test_slugurl_tag_returns_url_for_current_site(self): home_page = Page.objects.get(url_path="/home/") new_home_page = home_page.copy( update_attrs={"title": "New home page", "slug": "new-home"} ) second_site = Site.objects.create( hostname="site2.example.com", root_page=new_home_page ) # Add a page to the new site that has a slug that is the same as one on # the first site, but is in a different position in the treeself. new_christmas_page = Page(title="Christmas", slug="christmas") new_home_page.add_child(instance=new_christmas_page) request = get_dummy_request(site=second_site) url = slugurl(context=template.Context({"request": request}), slug="christmas") self.assertEqual(url, "/christmas/") @override_settings(ALLOWED_HOSTS=["testserver", "localhost", "site2.example.com"]) def test_slugurl_tag_returns_url_for_other_site(self): home_page = Page.objects.get(url_path="/home/") new_home_page = home_page.copy( update_attrs={"title": "New home page", "slug": "new-home"} ) second_site = Site.objects.create( hostname="site2.example.com", root_page=new_home_page ) request = get_dummy_request(site=second_site) # There is no page with this slug on the current site, so this # should return an absolute URL for the page on the first site. url = slugurl(slug="christmas", context=template.Context({"request": request})) self.assertEqual(url, "http://localhost/events/christmas/") def test_slugurl_without_request_in_context(self): # no 'request' object in context result = slugurl(template.Context({}), "events") self.assertEqual(result, "/events/") # 'request' object in context, but no 'site' attribute with self.assertNumQueries(3): result = slugurl( template.Context({"request": get_dummy_request()}), "events" ) self.assertEqual(result, "/events/") @override_settings(ALLOWED_HOSTS=["testserver", "localhost", "unknown.example.com"]) def test_slugurl_with_null_site_in_request(self): # 'request' object in context, but site is None request = get_dummy_request() request.META["HTTP_HOST"] = "unknown.example.com" result = slugurl(template.Context({"request": request}), "events") self.assertEqual(result, "/events/") def test_fullpageurl(self): tpl = template.Template( """{% load wagtailcore_tags %}Events""" ) page = Page.objects.get(url_path="/home/events/") with self.assertNumQueries(7): result = tpl.render(template.Context({"page": page})) self.assertIn('Events', result) def test_fullpageurl_with_named_url_fallback(self): tpl = template.Template( """{% load wagtailcore_tags %}Fallback""" ) with self.assertNumQueries(0): result = tpl.render(template.Context({"page": None})) self.assertIn('Fallback', result) def test_fullpageurl_with_absolute_fallback(self): tpl = template.Template( """{% load wagtailcore_tags %}Fallback""" ) with self.assertNumQueries(0): result = tpl.render( template.Context({"page": None, "request": get_dummy_request()}) ) self.assertIn('Fallback', result) def test_fullpageurl_with_invalid_page(self): tpl = template.Template( """{% load wagtailcore_tags %}Events""" ) with self.assertRaises(ValueError): tpl.render(template.Context({"page": 123})) def test_pageurl_with_invalid_page(self): tpl = template.Template( """{% load wagtailcore_tags %}Events""" ) with self.assertRaises(ValueError): tpl.render(template.Context({"page": 123})) class TestWagtailSiteTag(TestCase): fixtures = ["test.json"] def test_wagtail_site_tag(self): request = get_dummy_request(site=Site.objects.first()) tpl = template.Template( """{% load wagtailcore_tags %}{% wagtail_site as current_site %}{{ current_site.hostname }}""" ) result = tpl.render(template.Context({"request": request})) self.assertEqual("localhost", result) def test_wagtail_site_tag_with_missing_request_context(self): tpl = template.Template( """{% load wagtailcore_tags %}{% wagtail_site as current_site %}{{ current_site.hostname }}""" ) result = tpl.render(template.Context({})) # should fail silently self.assertEqual("", result) class TestSiteRootPathsCache(TestCase): fixtures = ["test.json"] def get_cached_site_root_paths(self): return cache.get( SITE_ROOT_PATHS_CACHE_KEY, version=SITE_ROOT_PATHS_CACHE_VERSION ) def test_cache(self): """ This tests that the cache is populated when building URLs """ # Get homepage homepage = Page.objects.get(url_path="/home/") # Warm up the cache by getting the url _ = homepage.url # Check that the cache has been set correctly self.assertEqual( self.get_cached_site_root_paths(), [ SiteRootPath( site_id=1, root_path="/home/", root_url="http://localhost", language_code="en", ) ], ) def test_cache_backend_uses_json_serialization(self): """ This tests that, even if the cache backend uses JSON serialization, get_site_root_paths() returns a list of SiteRootPath objects. """ result = Site.get_site_root_paths() self.assertEqual( result, [ SiteRootPath( site_id=1, root_path="/home/", root_url="http://localhost", language_code="en", ) ], ) # Go through JSON (de)serialisation to check that the result is # still a list of named tuples. cache.set( SITE_ROOT_PATHS_CACHE_KEY, json.loads(json.dumps(result)), version=SITE_ROOT_PATHS_CACHE_VERSION, ) result = Site.get_site_root_paths() self.assertIsInstance(result[0], SiteRootPath) def test_cache_clears_when_site_saved(self): """ This tests that the cache is cleared whenever a site is saved """ # Get homepage homepage = Page.objects.get(url_path="/home/") # Warm up the cache by getting the url _ = homepage.url # Check that the cache has been set self.assertEqual( self.get_cached_site_root_paths(), [ SiteRootPath( site_id=1, root_path="/home/", root_url="http://localhost", language_code="en", ) ], ) # Save the site Site.objects.get(is_default_site=True).save() # Check that the cache has been cleared self.assertIsNone(self.get_cached_site_root_paths()) def test_cache_clears_when_site_deleted(self): """ This tests that the cache is cleared whenever a site is deleted """ # Get homepage homepage = Page.objects.get(url_path="/home/") # Warm up the cache by getting the url _ = homepage.url # Check that the cache has been set self.assertEqual( self.get_cached_site_root_paths(), [ SiteRootPath( site_id=1, root_path="/home/", root_url="http://localhost", language_code="en", ) ], ) # Delete the site Site.objects.get(is_default_site=True).delete() # Check that the cache has been cleared self.assertIsNone(self.get_cached_site_root_paths()) def test_cache_clears_when_site_root_moves(self): """ This tests for an issue where if a site root page was moved, all the page urls in that site would change to None. The issue was caused by the 'wagtail_site_root_paths' cache variable not being cleared when a site root page was moved. Which left all the child pages thinking that they are no longer in the site and return None as their url. Fix: d6cce69a397d08d5ee81a8cbc1977ab2c9db2682 Discussion: https://github.com/wagtail/wagtail/issues/7 """ # Get homepage, root page and site root_page = Page.objects.get(id=1) homepage = Page.objects.get(url_path="/home/") default_site = Site.objects.get(is_default_site=True) # Create a new homepage under current homepage new_homepage = SimplePage( title="New Homepage", slug="new-homepage", content="hello" ) homepage.add_child(instance=new_homepage) # Set new homepage as the site root page default_site.root_page = new_homepage default_site.save() # Warm up the cache by getting the url _ = homepage.url # Move new homepage to root new_homepage.move(root_page, pos="last-child") # Get fresh instance of new_homepage new_homepage = Page.objects.get(id=new_homepage.id) # Check url self.assertEqual(new_homepage.url, "/") def test_cache_clears_when_site_root_slug_changes(self): """ This tests for an issue where if a site root pages slug was changed, all the page urls in that site would change to None. The issue was caused by the 'wagtail_site_root_paths' cache variable not being cleared when a site root page was changed. Which left all the child pages thinking that they are no longer in the site and return None as their url. Fix: d6cce69a397d08d5ee81a8cbc1977ab2c9db2682 Discussion: https://github.com/wagtail/wagtail/issues/157 """ # Get homepage homepage = Page.objects.get(url_path="/home/") # Warm up the cache by getting the url _ = homepage.url # Change homepage title and slug homepage.title = "New home" homepage.slug = "new-home" homepage.save() # Get fresh instance of homepage homepage = Page.objects.get(id=homepage.id) # Check url self.assertEqual(homepage.url, "/") @override_settings(WAGTAIL_I18N_ENABLED=True) def test_cache_clears_when_site_root_is_translated_as_alias(self): # Get homepage homepage = Page.objects.get(url_path="/home/") # Warm up the cache by getting the url _ = homepage.url # Translate the homepage translated_homepage = homepage.copy_for_translation( Locale.objects.create(language_code="fr"), alias=True ) # Check url self.assertEqual(translated_homepage.url, "/") class TestResolveModelString(TestCase): def test_resolve_from_string(self): model = resolve_model_string("wagtailcore.Page") self.assertEqual(model, Page) def test_resolve_from_string_with_default_app(self): model = resolve_model_string("Page", default_app="wagtailcore") self.assertEqual(model, Page) def test_resolve_from_string_with_different_default_app(self): model = resolve_model_string("wagtailcore.Page", default_app="wagtailadmin") self.assertEqual(model, Page) def test_resolve_from_class(self): model = resolve_model_string(Page) self.assertEqual(model, Page) def test_resolve_from_string_invalid(self): self.assertRaises(ValueError, resolve_model_string, "wagtail.core.Page") def test_resolve_from_string_with_incorrect_default_app(self): self.assertRaises( LookupError, resolve_model_string, "Page", default_app="wagtailadmin" ) def test_resolve_from_string_with_unknown_model_string(self): self.assertRaises(LookupError, resolve_model_string, "wagtailadmin.Page") def test_resolve_from_string_with_no_default_app(self): self.assertRaises(ValueError, resolve_model_string, "Page") def test_resolve_from_class_that_isnt_a_model(self): model = resolve_model_string(object) self.assertEqual(model, object) def test_resolve_from_bad_type(self): self.assertRaises(ValueError, resolve_model_string, resolve_model_string) def test_resolve_from_none(self): self.assertRaises(ValueError, resolve_model_string, None) class TestRichtextTag(TestCase): def test_call_with_text(self): result = richtext("Hello world!") self.assertEqual(result, "Hello world!") self.assertIsInstance(result, SafeString) def test_call_with_lazy(self): result = richtext(gettext_lazy("test")) self.assertEqual(result, "test") def test_call_with_none(self): result = richtext(None) self.assertEqual(result, "") def test_call_with_invalid_value(self): with self.assertRaisesRegex( TypeError, "'richtext' template filter received an invalid value" ): richtext(42) def test_call_with_bytes(self): with self.assertRaisesRegex( TypeError, "'richtext' template filter received an invalid value" ): richtext(b"Hello world!") class TestWagtailCacheTag(TestCase): def setUp(self): cache.clear() def test_caches(self): request = get_dummy_request() tpl = template.Template( """{% load wagtail_cache %}{% wagtailcache 100 test %}{{ foo.bar }}{% endwagtailcache %}""" ) result = tpl.render( template.Context({"request": request, "foo": {"bar": "foobar"}}) ) self.assertEqual(result, "foobar") result2 = tpl.render( template.Context({"request": request, "foo": {"bar": "baz"}}) ) self.assertEqual(result2, "foobar") self.assertEqual(cache.get(make_template_fragment_key("test")), "foobar") def test_caches_on_additional_parameters(self): request = get_dummy_request() tpl = template.Template( """{% load wagtail_cache %}{% wagtailcache 100 test foo %}{{ foo.bar }}{% endwagtailcache %}""" ) result = tpl.render( template.Context({"request": request, "foo": {"bar": "foobar"}}) ) self.assertEqual(result, "foobar") result2 = tpl.render( template.Context({"request": request, "foo": {"bar": "baz"}}) ) self.assertEqual(result2, "baz") self.assertEqual( cache.get(make_template_fragment_key("test", [{"bar": "foobar"}])), "foobar" ) self.assertEqual( cache.get(make_template_fragment_key("test", [{"bar": "baz"}])), "baz" ) def test_skips_cache_in_preview(self): request = get_dummy_request() request.is_preview = True tpl = template.Template( """{% load wagtail_cache %}{% wagtailcache 100 test %}{{ foo.bar }}{% endwagtailcache %}""" ) result = tpl.render( template.Context({"request": request, "foo": {"bar": "foobar"}}) ) self.assertEqual(result, "foobar") result2 = tpl.render( template.Context({"request": request, "foo": {"bar": "baz"}}) ) self.assertEqual(result2, "baz") self.assertIsNone(cache.get(make_template_fragment_key("test"))) def test_no_request(self): tpl = template.Template( """{% load wagtail_cache %}{% wagtailcache 100 test %}{{ foo.bar }}{% endwagtailcache %}""" ) result = tpl.render(template.Context({"foo": {"bar": "foobar"}})) self.assertEqual(result, "foobar") result2 = tpl.render(template.Context({"foo": {"bar": "baz"}})) self.assertEqual(result2, "baz") self.assertIsNone(cache.get(make_template_fragment_key("test"))) # def test_invalid_usage(self): with self.assertRaises(TemplateSyntaxError) as e: template.Template( """{% load wagtail_cache %}{% wagtailcache 100 %}{{ foo.bar }}{% endwagtailcache %}""" ) self.assertEqual( e.exception.args[0], "'wagtailcache' tag requires at least 2 arguments." ) class TestWagtailPageCacheTag(TestCase): fixtures = ["test.json"] @classmethod def setUpTestData(cls): cls.page_1 = Page.objects.first() cls.page_2 = Page.objects.all()[2] cls.site = Site.objects.get(hostname="localhost", port=80) def test_caches(self): request = get_dummy_request(site=self.site) tpl = template.Template( """{% load wagtail_cache %}{% wagtailpagecache 100 test %}{{ foo.bar }}{% endwagtailpagecache %}""" ) result = tpl.render( template.Context( {"request": request, "foo": {"bar": "foobar"}, "page": self.page_1} ) ) self.assertEqual(result, "foobar") result2 = tpl.render( template.Context( {"request": request, "foo": {"bar": "baz"}, "page": self.page_1} ) ) self.assertEqual(result2, "foobar") self.assertEqual( cache.get( make_wagtail_template_fragment_key("test", self.page_1, self.site) ), "foobar", ) def test_caches_additional_parameters(self): request = get_dummy_request(site=self.site) tpl = template.Template( """{% load wagtail_cache %}{% wagtailpagecache 100 test foo %}{{ foo.bar }}{% endwagtailpagecache %}""" ) result = tpl.render( template.Context( {"request": request, "foo": {"bar": "foobar"}, "page": self.page_1} ) ) self.assertEqual(result, "foobar") result2 = tpl.render( template.Context( {"request": request, "foo": {"bar": "baz"}, "page": self.page_1} ) ) self.assertEqual(result2, "baz") self.assertEqual( cache.get( make_wagtail_template_fragment_key( "test", self.page_1, self.site, [{"bar": "foobar"}] ) ), "foobar", ) self.assertEqual( cache.get( make_wagtail_template_fragment_key( "test", self.page_1, self.site, [{"bar": "baz"}] ) ), "baz", ) def test_doesnt_pollute_cache(self): request = get_dummy_request(site=self.site) tpl = template.Template( """{% load wagtail_cache %}{% wagtailpagecache 100 test %}{{ foo.bar }}{% endwagtailpagecache %}""" ) context = template.Context( {"request": request, "foo": {"bar": "foobar"}, "page": self.page_1} ) result = tpl.render(context) self.assertEqual(result, "foobar") self.assertNotIn(WagtailPageCacheNode.CACHE_SITE_TEMPLATE_VAR, context) def test_skips_cache_in_preview(self): request = get_dummy_request(site=self.site) request.is_preview = True tpl = template.Template( """{% load wagtail_cache %}{% wagtailpagecache 100 test %}{{ foo.bar }}{% endwagtailpagecache %}""" ) result = tpl.render( template.Context( {"request": request, "foo": {"bar": "foobar"}, "page": self.page_1} ) ) self.assertEqual(result, "foobar") result2 = tpl.render( template.Context( {"request": request, "foo": {"bar": "baz"}, "page": self.page_1} ) ) self.assertEqual(result2, "baz") self.assertIsNone( cache.get( make_wagtail_template_fragment_key("test", self.page_1, self.site) ) ) def test_no_request(self): tpl = template.Template( """{% load wagtail_cache %}{% wagtailpagecache 100 test %}{{ foo.bar }}{% endwagtailpagecache %}""" ) result = tpl.render( template.Context({"foo": {"bar": "foobar"}, "page": self.page_1}) ) self.assertEqual(result, "foobar") result2 = tpl.render( template.Context({"foo": {"bar": "baz"}, "page": self.page_1}) ) self.assertEqual(result2, "baz") self.assertIsNone( cache.get( make_wagtail_template_fragment_key("test", self.page_1, self.site) ) ) def test_no_page(self): request = get_dummy_request() tpl = template.Template( """{% load wagtail_cache %}{% wagtailpagecache 100 test %}{{ foo.bar }}{% endwagtailpagecache %}""" ) with self.assertRaises(VariableDoesNotExist) as e: tpl.render(template.Context({"request": request, "foo": {"bar": "foobar"}})) self.assertEqual(e.exception.params[0], "page") def test_cache_key(self): self.assertEqual( make_wagtail_template_fragment_key("test", self.page_1, self.site), make_template_fragment_key( "test", vary_on=[self.page_1.cache_key, self.site.id] ), ) def test_invalid_usage(self): with self.assertRaises(TemplateSyntaxError) as e: template.Template( """{% load wagtail_cache %}{% wagtailpagecache 100 %}{{ foo.bar }}{% endwagtailpagecache %}""" ) self.assertEqual( e.exception.args[0], "'wagtailpagecache' tag requires at least 2 arguments." )