diff --git a/django/contrib/sitemaps/__init__.py b/django/contrib/sitemaps/__init__.py index 3d276b60d4..df57f1cd5c 100644 --- a/django/contrib/sitemaps/__init__.py +++ b/django/contrib/sitemaps/__init__.py @@ -92,6 +92,10 @@ class Sitemap: return attr(item) return attr + def get_languages_for_item(self, item): + """Languages for which this item is displayed.""" + return self._languages() + def _languages(self): if self.languages is not None: return self.languages @@ -103,8 +107,8 @@ class Sitemap: # This is necessary to paginate with all languages already considered. items = [ (item, lang_code) - for lang_code in self._languages() for item in self.items() + for lang_code in self.get_languages_for_item(item) ] return items return self.items() @@ -201,7 +205,8 @@ class Sitemap: } if self.i18n and self.alternates: - for lang_code in self._languages(): + item_languages = self.get_languages_for_item(item[0]) + for lang_code in item_languages: loc = f"{protocol}://{domain}{self._location(item, lang_code)}" url_info["alternates"].append( { @@ -209,7 +214,7 @@ class Sitemap: "lang_code": lang_code, } ) - if self.x_default: + if self.x_default and settings.LANGUAGE_CODE in item_languages: lang_code = settings.LANGUAGE_CODE loc = f"{protocol}://{domain}{self._location(item, lang_code)}" loc = loc.replace(f"/{lang_code}/", "/", 1) diff --git a/docs/ref/contrib/sitemaps.txt b/docs/ref/contrib/sitemaps.txt index d3225405a3..7dc3dced51 100644 --- a/docs/ref/contrib/sitemaps.txt +++ b/docs/ref/contrib/sitemaps.txt @@ -311,6 +311,15 @@ Note: The latest ``lastmod`` returned by calling the method with all items returned by :meth:`Sitemap.items`. + .. method:: Sitemap.get_languages_for_item(item, lang_code) + + .. versionadded:: 4.2 + + **Optional.** A method that returns the sequence of language codes for + which the item is displayed. By default + :meth:`~Sitemap.get_languages_for_item` returns + :attr:`~Sitemap.languages`. + Shortcuts ========= diff --git a/docs/releases/4.2.txt b/docs/releases/4.2.txt index 682fce2a53..7e93df0702 100644 --- a/docs/releases/4.2.txt +++ b/docs/releases/4.2.txt @@ -145,7 +145,8 @@ Minor features :mod:`django.contrib.sitemaps` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -* ... +* The new :meth:`.Sitemap.get_languages_for_item` method allows customizing the + list of languages for which the item is displayed. :mod:`django.contrib.sites` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/sitemaps_tests/test_http.py b/tests/sitemaps_tests/test_http.py index 8c16f66896..12e387757b 100644 --- a/tests/sitemaps_tests/test_http.py +++ b/tests/sitemaps_tests/test_http.py @@ -10,7 +10,7 @@ from django.utils.deprecation import RemovedInDjango50Warning from django.utils.formats import localize from .base import SitemapTestsBase -from .models import TestModel +from .models import I18nTestModel, TestModel class HTTPSitemapTests(SitemapTestsBase): @@ -440,6 +440,72 @@ class HTTPSitemapTests(SitemapTestsBase): ) 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 = ( diff --git a/tests/sitemaps_tests/urls/http.py b/tests/sitemaps_tests/urls/http.py index 75dd4834c0..2b512cfd69 100644 --- a/tests/sitemaps_tests/urls/http.py +++ b/tests/sitemaps_tests/urls/http.py @@ -48,6 +48,22 @@ class XDefaultI18nSitemap(AlternatesI18nSitemap): x_default = True +class ItemByLangSitemap(SimpleI18nSitemap): + def get_languages_for_item(self, item): + if item.name == "Only for PT": + return ["pt"] + return super().get_languages_for_item(item) + + +class ItemByLangAlternatesSitemap(AlternatesI18nSitemap): + x_default = True + + def get_languages_for_item(self, item): + if item.name == "Only for PT": + return ["pt"] + return super().get_languages_for_item(item) + + class EmptySitemap(Sitemap): changefreq = "never" priority = 0.5 @@ -168,6 +184,14 @@ xdefault_i18n_sitemaps = { "i18n-xdefault": XDefaultI18nSitemap, } +item_by_lang_i18n_sitemaps = { + "i18n-item-by-lang": ItemByLangSitemap, +} + +item_by_lang_alternates_i18n_sitemaps = { + "i18n-item-by-lang-alternates": ItemByLangAlternatesSitemap, +} + simple_sitemaps_not_callable = { "simple": SimpleSitemap(), } @@ -358,6 +382,18 @@ urlpatterns = [ {"sitemaps": sitemaps_lastmod_ascending}, name="django.contrib.sitemaps.views.sitemap", ), + path( + "item-by-lang/i18n.xml", + views.sitemap, + {"sitemaps": item_by_lang_i18n_sitemaps}, + name="django.contrib.sitemaps.views.sitemap", + ), + path( + "item-by-lang-alternates/i18n.xml", + views.sitemap, + {"sitemaps": item_by_lang_alternates_i18n_sitemaps}, + name="django.contrib.sitemaps.views.sitemap", + ), path( "lastmod-sitemaps/descending.xml", views.sitemap,