import os from datetime import date from django.contrib.sitemaps import Sitemap from django.contrib.sites.models import Site from django.core.exceptions import ImproperlyConfigured from django.test import ignore_warnings, modify_settings, override_settings from django.utils import translation from django.utils.deprecation import RemovedInDjango50Warning from django.utils.formats import localize from .base import SitemapTestsBase from .models import I18nTestModel, TestModel class HTTPSitemapTests(SitemapTestsBase): use_sitemap_err_msg = ( "To use sitemaps, either enable the sites framework or pass a " "Site/RequestSite object in your view." ) def test_simple_sitemap_index(self): "A simple sitemap index can be rendered" response = self.client.get("/simple/index.xml") expected_content = """ %s/simple/sitemap-simple.xml%s """ % ( self.base_url, date.today(), ) self.assertXMLEqual(response.content.decode(), expected_content) def test_sitemap_not_callable(self): """A sitemap may not be callable.""" response = self.client.get("/simple-not-callable/index.xml") expected_content = """ %s/simple/sitemap-simple.xml%s """ % ( self.base_url, date.today(), ) self.assertXMLEqual(response.content.decode(), expected_content) def test_paged_sitemap(self): """A sitemap may have multiple pages.""" response = self.client.get("/simple-paged/index.xml") expected_content = """ {0}/simple/sitemap-simple.xml{1}{0}/simple/sitemap-simple.xml?p=2{1} """.format( self.base_url, date.today() ) self.assertXMLEqual(response.content.decode(), expected_content) @override_settings( TEMPLATES=[ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [os.path.join(os.path.dirname(__file__), "templates")], } ] ) def test_simple_sitemap_custom_lastmod_index(self): "A simple sitemap index can be rendered with a custom template" response = self.client.get("/simple/custom-lastmod-index.xml") expected_content = """ %s/simple/sitemap-simple.xml%s """ % ( self.base_url, date.today(), ) self.assertXMLEqual(response.content.decode(), expected_content) def test_simple_sitemap_section(self): "A simple sitemap section can be rendered" response = self.client.get("/simple/sitemap-simple.xml") expected_content = ( '\n' '\n' "%s/location/%s" "never0.5\n" "" ) % ( self.base_url, date.today(), ) self.assertXMLEqual(response.content.decode(), expected_content) def test_no_section(self): response = self.client.get("/simple/sitemap-simple2.xml") self.assertEqual( str(response.context["exception"]), "No sitemap available for section: 'simple2'", ) self.assertEqual(response.status_code, 404) def test_empty_page(self): response = self.client.get("/simple/sitemap-simple.xml?p=0") self.assertEqual(str(response.context["exception"]), "Page 0 empty") self.assertEqual(response.status_code, 404) def test_page_not_int(self): response = self.client.get("/simple/sitemap-simple.xml?p=test") self.assertEqual(str(response.context["exception"]), "No page 'test'") self.assertEqual(response.status_code, 404) def test_simple_sitemap(self): "A simple sitemap can be rendered" response = self.client.get("/simple/sitemap.xml") expected_content = ( '\n' '\n' "%s/location/%s" "never0.5\n" "" ) % ( self.base_url, date.today(), ) self.assertXMLEqual(response.content.decode(), expected_content) @override_settings( TEMPLATES=[ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [os.path.join(os.path.dirname(__file__), "templates")], } ] ) def test_simple_custom_sitemap(self): "A simple sitemap can be rendered with a custom template" response = self.client.get("/simple/custom-sitemap.xml") expected_content = """ %s/location/%snever0.5 """ % ( self.base_url, date.today(), ) self.assertXMLEqual(response.content.decode(), expected_content) def test_sitemap_last_modified(self): "Last-Modified header is set correctly" response = self.client.get("/lastmod/sitemap.xml") self.assertEqual( response.headers["Last-Modified"], "Wed, 13 Mar 2013 10:00:00 GMT" ) def test_sitemap_last_modified_date(self): """ The Last-Modified header should be support dates (without time). """ response = self.client.get("/lastmod/date-sitemap.xml") self.assertEqual( response.headers["Last-Modified"], "Wed, 13 Mar 2013 00:00:00 GMT" ) def test_sitemap_last_modified_tz(self): """ The Last-Modified header should be converted from timezone aware dates to GMT. """ response = self.client.get("/lastmod/tz-sitemap.xml") self.assertEqual( response.headers["Last-Modified"], "Wed, 13 Mar 2013 15:00:00 GMT" ) def test_sitemap_last_modified_missing(self): "Last-Modified header is missing when sitemap has no lastmod" response = self.client.get("/generic/sitemap.xml") self.assertFalse(response.has_header("Last-Modified")) def test_sitemap_last_modified_mixed(self): "Last-Modified header is omitted when lastmod not on all items" response = self.client.get("/lastmod-mixed/sitemap.xml") self.assertFalse(response.has_header("Last-Modified")) def test_sitemaps_lastmod_mixed_ascending_last_modified_missing(self): """ The Last-Modified header is omitted when lastmod isn't found in all sitemaps. Test sitemaps are sorted by lastmod in ascending order. """ response = self.client.get("/lastmod-sitemaps/mixed-ascending.xml") self.assertFalse(response.has_header("Last-Modified")) def test_sitemaps_lastmod_mixed_descending_last_modified_missing(self): """ The Last-Modified header is omitted when lastmod isn't found in all sitemaps. Test sitemaps are sorted by lastmod in descending order. """ response = self.client.get("/lastmod-sitemaps/mixed-descending.xml") self.assertFalse(response.has_header("Last-Modified")) def test_sitemaps_lastmod_ascending(self): """ The Last-Modified header is set to the most recent sitemap lastmod. Test sitemaps are sorted by lastmod in ascending order. """ response = self.client.get("/lastmod-sitemaps/ascending.xml") self.assertEqual( response.headers["Last-Modified"], "Sat, 20 Apr 2013 05:00:00 GMT" ) def test_sitemaps_lastmod_descending(self): """ The Last-Modified header is set to the most recent sitemap lastmod. Test sitemaps are sorted by lastmod in descending order. """ response = self.client.get("/lastmod-sitemaps/descending.xml") self.assertEqual( response.headers["Last-Modified"], "Sat, 20 Apr 2013 05:00:00 GMT" ) def test_sitemap_get_latest_lastmod_none(self): """ sitemapindex.lastmod is omitted when Sitemap.lastmod is callable and Sitemap.get_latest_lastmod is not implemented """ response = self.client.get("/lastmod/get-latest-lastmod-none-sitemap.xml") self.assertNotContains(response, "") def test_sitemap_get_latest_lastmod(self): """ sitemapindex.lastmod is included when Sitemap.lastmod is attribute and Sitemap.get_latest_lastmod is implemented """ response = self.client.get("/lastmod/get-latest-lastmod-sitemap.xml") self.assertContains(response, "2013-03-13T10:00:00") def test_sitemap_latest_lastmod_timezone(self): """ lastmod datestamp shows timezones if Sitemap.get_latest_lastmod returns an aware datetime. """ response = self.client.get("/lastmod/latest-lastmod-timezone-sitemap.xml") self.assertContains(response, "2013-03-13T10:00:00-05:00") def test_localized_priority(self): """The priority value should not be localized.""" with translation.override("fr"): self.assertEqual("0,3", localize(0.3)) # Priorities aren't rendered in localized format. response = self.client.get("/simple/sitemap.xml") self.assertContains(response, "0.5") self.assertContains(response, "%s" % date.today()) @modify_settings(INSTALLED_APPS={"remove": "django.contrib.sites"}) def test_requestsite_sitemap(self): # Hitting the flatpages sitemap without the sites framework installed # doesn't raise an exception. response = self.client.get("/simple/sitemap.xml") expected_content = ( '\n' '\n' "http://testserver/location/%s" "never0.5\n" "" ) % date.today() self.assertXMLEqual(response.content.decode(), expected_content) @ignore_warnings(category=RemovedInDjango50Warning) def test_sitemap_get_urls_no_site_1(self): """ Check we get ImproperlyConfigured if we don't pass a site object to Sitemap.get_urls and no Site objects exist """ Site.objects.all().delete() with self.assertRaisesMessage(ImproperlyConfigured, self.use_sitemap_err_msg): Sitemap().get_urls() @modify_settings(INSTALLED_APPS={"remove": "django.contrib.sites"}) @ignore_warnings(category=RemovedInDjango50Warning) def test_sitemap_get_urls_no_site_2(self): """ Check we get ImproperlyConfigured when we don't pass a site object to Sitemap.get_urls if Site objects exists, but the sites framework is not actually installed. """ with self.assertRaisesMessage(ImproperlyConfigured, self.use_sitemap_err_msg): Sitemap().get_urls() @ignore_warnings(category=RemovedInDjango50Warning) def test_sitemap_item(self): """ Check to make sure that the raw item is included with each Sitemap.get_url() url result. """ test_sitemap = Sitemap() test_sitemap.items = TestModel.objects.order_by("pk").all def is_testmodel(url): return isinstance(url["item"], TestModel) item_in_url_info = all(map(is_testmodel, test_sitemap.get_urls())) self.assertTrue(item_in_url_info) def test_cached_sitemap_index(self): """ A cached sitemap index can be rendered (#2713). """ response = self.client.get("/cached/index.xml") expected_content = """ %s/cached/sitemap-simple.xml%s """ % ( self.base_url, date.today(), ) self.assertXMLEqual(response.content.decode(), expected_content) def test_x_robots_sitemap(self): response = self.client.get("/simple/index.xml") self.assertEqual(response.headers["X-Robots-Tag"], "noindex, noodp, noarchive") response = self.client.get("/simple/sitemap.xml") self.assertEqual(response.headers["X-Robots-Tag"], "noindex, noodp, noarchive") def test_empty_sitemap(self): response = self.client.get("/empty/sitemap.xml") self.assertEqual(response.status_code, 200) @override_settings(LANGUAGES=(("en", "English"), ("pt", "Portuguese"))) def test_simple_i18n_sitemap_index(self): """ A simple i18n sitemap index can be rendered, without logging variable lookup errors. """ with self.assertNoLogs("django.template", "DEBUG"): response = self.client.get("/simple/i18n.xml") expected_content = ( '\n' '\n' "{0}/en/i18n/testmodel/{1}/never" "0.5{0}/pt/i18n/testmodel/{1}/" "never0.5\n" "" ).format(self.base_url, self.i18n_model.pk) self.assertXMLEqual(response.content.decode(), expected_content) @override_settings(LANGUAGES=(("en", "English"), ("pt", "Portuguese"))) def test_alternate_i18n_sitemap_index(self): """ A i18n sitemap with alternate/hreflang links can be rendered. """ response = self.client.get("/alternates/i18n.xml") url, pk = self.base_url, self.i18n_model.pk expected_urls = f""" {url}/en/i18n/testmodel/{pk}/never0.5 {url}/pt/i18n/testmodel/{pk}/never0.5 """.replace( "\n", "" ) expected_content = ( f'\n' f'\n' f"{expected_urls}\n" f"" ) self.assertXMLEqual(response.content.decode(), expected_content) @override_settings( LANGUAGES=(("en", "English"), ("pt", "Portuguese"), ("es", "Spanish")) ) def test_alternate_i18n_sitemap_limited(self): """ A i18n sitemap index with limited languages can be rendered. """ response = self.client.get("/limited/i18n.xml") url, pk = self.base_url, self.i18n_model.pk expected_urls = f""" {url}/en/i18n/testmodel/{pk}/never0.5 {url}/es/i18n/testmodel/{pk}/never0.5 """.replace( "\n", "" ) expected_content = ( f'\n' f'\n' f"{expected_urls}\n" f"" ) self.assertXMLEqual(response.content.decode(), expected_content) @override_settings(LANGUAGES=(("en", "English"), ("pt", "Portuguese"))) def test_alternate_i18n_sitemap_xdefault(self): """ A i18n sitemap index with x-default can be rendered. """ response = self.client.get("/x-default/i18n.xml") url, pk = self.base_url, self.i18n_model.pk expected_urls = f""" {url}/en/i18n/testmodel/{pk}/never0.5 {url}/pt/i18n/testmodel/{pk}/never0.5 """.replace( "\n", "" ) expected_content = ( f'\n' f'\n' f"{expected_urls}\n" f"" ) self.assertXMLEqual(response.content.decode(), expected_content) @override_settings(LANGUAGES=(("en", "English"), ("pt", "Portuguese"))) def test_language_for_item_i18n_sitemap(self): """ A i18n sitemap index in which item can be chosen to be displayed for a lang or not. """ only_pt = I18nTestModel.objects.create(name="Only for PT") response = self.client.get("/item-by-lang/i18n.xml") url, pk, only_pt_pk = self.base_url, self.i18n_model.pk, only_pt.pk expected_urls = ( f"{url}/en/i18n/testmodel/{pk}/" f"never0.5" f"{url}/pt/i18n/testmodel/{pk}/" f"never0.5" f"{url}/pt/i18n/testmodel/{only_pt_pk}/" f"never0.5" ) expected_content = ( f'\n' f'\n' f"{expected_urls}\n" f"" ) self.assertXMLEqual(response.content.decode(), expected_content) @override_settings(LANGUAGES=(("en", "English"), ("pt", "Portuguese"))) def test_alternate_language_for_item_i18n_sitemap(self): """ A i18n sitemap index in which item can be chosen to be displayed for a lang or not. """ only_pt = I18nTestModel.objects.create(name="Only for PT") response = self.client.get("/item-by-lang-alternates/i18n.xml") url, pk, only_pt_pk = self.base_url, self.i18n_model.pk, only_pt.pk expected_urls = ( f"{url}/en/i18n/testmodel/{pk}/" f"never0.5" f'' f'' f'' f"{url}/pt/i18n/testmodel/{pk}/" f"never0.5" f'' f'' f'' f"{url}/pt/i18n/testmodel/{only_pt_pk}/" f"never0.5" f'' ) expected_content = ( f'\n' f'\n' f"{expected_urls}\n" f"" ) self.assertXMLEqual(response.content.decode(), expected_content) def test_sitemap_without_entries(self): response = self.client.get("/sitemap-without-entries/sitemap.xml") expected_content = ( '\n' '\n\n' "" ) self.assertXMLEqual(response.content.decode(), expected_content) def test_callable_sitemod_partial(self): """ Not all items have `lastmod`. Therefore the `Last-Modified` header is not set by the detail or index sitemap view. """ index_response = self.client.get("/callable-lastmod-partial/index.xml") sitemap_response = self.client.get("/callable-lastmod-partial/sitemap.xml") self.assertNotIn("Last-Modified", index_response) self.assertNotIn("Last-Modified", sitemap_response) expected_content_index = """ http://example.com/simple/sitemap-callable-lastmod.xml """ expected_content_sitemap = ( '\n' '\n' "http://example.com/location/" "2013-03-13" "http://example.com/location/\n" "" ) self.assertXMLEqual(index_response.content.decode(), expected_content_index) self.assertXMLEqual(sitemap_response.content.decode(), expected_content_sitemap) def test_callable_sitemod_full(self): """ All items in the sitemap have `lastmod`. The `Last-Modified` header is set for the detail and index sitemap view. """ index_response = self.client.get("/callable-lastmod-full/index.xml") sitemap_response = self.client.get("/callable-lastmod-full/sitemap.xml") self.assertEqual( index_response.headers["Last-Modified"], "Thu, 13 Mar 2014 10:00:00 GMT" ) self.assertEqual( sitemap_response.headers["Last-Modified"], "Thu, 13 Mar 2014 10:00:00 GMT" ) expected_content_index = """ http://example.com/simple/sitemap-callable-lastmod.xml2014-03-13T10:00:00 """ expected_content_sitemap = ( '\n' '\n' "http://example.com/location/" "2013-03-13" "http://example.com/location/" "2014-03-13\n" "" ) self.assertXMLEqual(index_response.content.decode(), expected_content_index) self.assertXMLEqual(sitemap_response.content.decode(), expected_content_sitemap) def test_callable_sitemod_no_items(self): index_response = self.client.get("/callable-lastmod-no-items/index.xml") self.assertNotIn("Last-Modified", index_response) expected_content_index = """ http://example.com/simple/sitemap-callable-lastmod.xml """ self.assertXMLEqual(index_response.content.decode(), expected_content_index) # RemovedInDjango50Warning class DeprecatedTests(SitemapTestsBase): @override_settings( TEMPLATES=[ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [os.path.join(os.path.dirname(__file__), "templates")], } ] ) def test_simple_sitemap_custom_index_warning(self): msg = ( "Calling `__str__` on SitemapIndexItem is deprecated, use the `location` " "attribute instead." ) with self.assertRaisesMessage(RemovedInDjango50Warning, msg): self.client.get("/simple/custom-index.xml") @ignore_warnings(category=RemovedInDjango50Warning) @override_settings( TEMPLATES=[ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [os.path.join(os.path.dirname(__file__), "templates")], } ] ) def test_simple_sitemap_custom_index(self): "A simple sitemap index can be rendered with a custom template" response = self.client.get("/simple/custom-index.xml") expected_content = """ %s/simple/sitemap-simple.xml """ % ( self.base_url ) self.assertXMLEqual(response.content.decode(), expected_content)