Fixed #25916 -- Added lastmod support to sitemap index view.
Co-authored-by: Matthew Downey <matthew.downey@webit.com.au>
This commit is contained in:
parent
2ce03a2bac
commit
480191244d
|
@ -157,6 +157,17 @@ class Sitemap:
|
||||||
domain = self.get_domain(site)
|
domain = self.get_domain(site)
|
||||||
return self._urls(page, protocol, domain)
|
return self._urls(page, protocol, domain)
|
||||||
|
|
||||||
|
def get_latest_lastmod(self):
|
||||||
|
if not hasattr(self, 'lastmod'):
|
||||||
|
return None
|
||||||
|
if callable(self.lastmod):
|
||||||
|
try:
|
||||||
|
return max([self.lastmod(item) for item in self.items()])
|
||||||
|
except TypeError:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return self.lastmod
|
||||||
|
|
||||||
def _urls(self, page, protocol, domain):
|
def _urls(self, page, protocol, domain):
|
||||||
urls = []
|
urls = []
|
||||||
latest_lastmod = None
|
latest_lastmod = None
|
||||||
|
@ -226,3 +237,8 @@ class GenericSitemap(Sitemap):
|
||||||
if self.date_field is not None:
|
if self.date_field is not None:
|
||||||
return getattr(item, self.date_field)
|
return getattr(item, self.date_field)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_latest_lastmod(self):
|
||||||
|
if self.date_field is not None:
|
||||||
|
return self.queryset.order_by('-' + self.date_field).values_list(self.date_field, flat=True).first()
|
||||||
|
return None
|
||||||
|
|
|
@ -1,4 +1,13 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
{% for location in sitemaps %}<sitemap><loc>{{ location }}</loc></sitemap>{% endfor %}
|
{% spaceless %}
|
||||||
|
{% for site in sitemaps %}
|
||||||
|
<sitemap>
|
||||||
|
<loc>{{ site.location }}</loc>
|
||||||
|
{% if site.last_mod %}
|
||||||
|
<lastmod>{{ site.last_mod|date:"c" }}</lastmod>
|
||||||
|
{% endif %}
|
||||||
|
</sitemap>
|
||||||
|
{% endfor %}
|
||||||
|
{% endspaceless %}
|
||||||
</sitemapindex>
|
</sitemapindex>
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
import warnings
|
||||||
|
from dataclasses import dataclass
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from django.contrib.sites.shortcuts import get_current_site
|
from django.contrib.sites.shortcuts import get_current_site
|
||||||
|
@ -7,9 +9,22 @@ from django.http import Http404
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.deprecation import RemovedInDjango50Warning
|
||||||
from django.utils.http import http_date
|
from django.utils.http import http_date
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SitemapIndexItem:
|
||||||
|
location: str
|
||||||
|
last_mod: bool = None
|
||||||
|
|
||||||
|
# RemovedInDjango50Warning
|
||||||
|
def __str__(self):
|
||||||
|
msg = 'Calling `__str__` on SitemapIndexItem is deprecated, use the `location` attribute instead.'
|
||||||
|
warnings.warn(msg, RemovedInDjango50Warning, stacklevel=2)
|
||||||
|
return self.location
|
||||||
|
|
||||||
|
|
||||||
def x_robots_tag(func):
|
def x_robots_tag(func):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def inner(request, *args, **kwargs):
|
def inner(request, *args, **kwargs):
|
||||||
|
@ -19,6 +34,18 @@ def x_robots_tag(func):
|
||||||
return inner
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
def _get_latest_lastmod(current_lastmod, new_lastmod):
|
||||||
|
"""
|
||||||
|
Returns the latest `lastmod` where `lastmod` can be either a date or a
|
||||||
|
datetime.
|
||||||
|
"""
|
||||||
|
if not isinstance(new_lastmod, datetime.datetime):
|
||||||
|
new_lastmod = datetime.datetime.combine(new_lastmod, datetime.time.min)
|
||||||
|
if timezone.is_naive(new_lastmod):
|
||||||
|
new_lastmod = timezone.make_aware(new_lastmod, timezone.utc)
|
||||||
|
return new_lastmod if current_lastmod is None else max(current_lastmod, new_lastmod)
|
||||||
|
|
||||||
|
|
||||||
@x_robots_tag
|
@x_robots_tag
|
||||||
def index(request, sitemaps,
|
def index(request, sitemaps,
|
||||||
template_name='sitemap_index.xml', content_type='application/xml',
|
template_name='sitemap_index.xml', content_type='application/xml',
|
||||||
|
@ -28,6 +55,8 @@ def index(request, sitemaps,
|
||||||
req_site = get_current_site(request)
|
req_site = get_current_site(request)
|
||||||
|
|
||||||
sites = [] # all sections' sitemap URLs
|
sites = [] # all sections' sitemap URLs
|
||||||
|
all_indexes_lastmod = True
|
||||||
|
latest_lastmod = None
|
||||||
for section, site in sitemaps.items():
|
for section, site in sitemaps.items():
|
||||||
# For each section label, add links of all pages of its sitemap
|
# For each section label, add links of all pages of its sitemap
|
||||||
# (usually generated by the `sitemap` view).
|
# (usually generated by the `sitemap` view).
|
||||||
|
@ -36,13 +65,29 @@ def index(request, sitemaps,
|
||||||
protocol = req_protocol if site.protocol is None else site.protocol
|
protocol = req_protocol if site.protocol is None else site.protocol
|
||||||
sitemap_url = reverse(sitemap_url_name, kwargs={'section': section})
|
sitemap_url = reverse(sitemap_url_name, kwargs={'section': section})
|
||||||
absolute_url = '%s://%s%s' % (protocol, req_site.domain, sitemap_url)
|
absolute_url = '%s://%s%s' % (protocol, req_site.domain, sitemap_url)
|
||||||
sites.append(absolute_url)
|
site_lastmod = site.get_latest_lastmod()
|
||||||
|
if all_indexes_lastmod:
|
||||||
|
if site_lastmod is not None:
|
||||||
|
latest_lastmod = _get_latest_lastmod(latest_lastmod, site_lastmod)
|
||||||
|
else:
|
||||||
|
all_indexes_lastmod = False
|
||||||
|
sites.append(SitemapIndexItem(absolute_url, site_lastmod))
|
||||||
# Add links to all pages of the sitemap.
|
# Add links to all pages of the sitemap.
|
||||||
for page in range(2, site.paginator.num_pages + 1):
|
for page in range(2, site.paginator.num_pages + 1):
|
||||||
sites.append('%s?p=%s' % (absolute_url, page))
|
sites.append(SitemapIndexItem('%s?p=%s' % (absolute_url, page), site_lastmod))
|
||||||
|
# If lastmod is defined for all sites, set header so as
|
||||||
return TemplateResponse(request, template_name, {'sitemaps': sites},
|
# ConditionalGetMiddleware is able to send 304 NOT MODIFIED
|
||||||
content_type=content_type)
|
if all_indexes_lastmod and latest_lastmod:
|
||||||
|
headers = {'Last-Modified': http_date(latest_lastmod.timestamp())}
|
||||||
|
else:
|
||||||
|
headers = None
|
||||||
|
return TemplateResponse(
|
||||||
|
request,
|
||||||
|
template_name,
|
||||||
|
{'sitemaps': sites},
|
||||||
|
content_type=content_type,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@x_robots_tag
|
@x_robots_tag
|
||||||
|
@ -72,21 +117,23 @@ def sitemap(request, sitemaps, section=None,
|
||||||
if all_sites_lastmod:
|
if all_sites_lastmod:
|
||||||
site_lastmod = getattr(site, 'latest_lastmod', None)
|
site_lastmod = getattr(site, 'latest_lastmod', None)
|
||||||
if site_lastmod is not None:
|
if site_lastmod is not None:
|
||||||
if not isinstance(site_lastmod, datetime.datetime):
|
lastmod = _get_latest_lastmod(lastmod, site_lastmod)
|
||||||
site_lastmod = datetime.datetime.combine(site_lastmod, datetime.time.min)
|
|
||||||
if timezone.is_naive(site_lastmod):
|
|
||||||
site_lastmod = timezone.make_aware(site_lastmod, timezone.utc)
|
|
||||||
lastmod = site_lastmod if lastmod is None else max(lastmod, site_lastmod)
|
|
||||||
else:
|
else:
|
||||||
all_sites_lastmod = False
|
all_sites_lastmod = False
|
||||||
except EmptyPage:
|
except EmptyPage:
|
||||||
raise Http404("Page %s empty" % page)
|
raise Http404("Page %s empty" % page)
|
||||||
except PageNotAnInteger:
|
except PageNotAnInteger:
|
||||||
raise Http404("No page '%s'" % page)
|
raise Http404("No page '%s'" % page)
|
||||||
response = TemplateResponse(request, template_name, {'urlset': urls},
|
# If lastmod is defined for all sites, set header so as
|
||||||
content_type=content_type)
|
|
||||||
if all_sites_lastmod and lastmod is not None:
|
|
||||||
# if lastmod is defined for all sites, set header so as
|
|
||||||
# ConditionalGetMiddleware is able to send 304 NOT MODIFIED
|
# ConditionalGetMiddleware is able to send 304 NOT MODIFIED
|
||||||
response.headers['Last-Modified'] = http_date(lastmod.timestamp())
|
if all_sites_lastmod:
|
||||||
return response
|
headers = {'Last-Modified': http_date(lastmod.timestamp())} if lastmod else None
|
||||||
|
else:
|
||||||
|
headers = None
|
||||||
|
return TemplateResponse(
|
||||||
|
request,
|
||||||
|
template_name,
|
||||||
|
{'urlset': urls},
|
||||||
|
content_type=content_type,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
|
@ -65,6 +65,8 @@ details on these changes.
|
||||||
See the :ref:`Django 4.1 release notes <deprecated-features-4.1>` for more
|
See the :ref:`Django 4.1 release notes <deprecated-features-4.1>` for more
|
||||||
details on these changes.
|
details on these changes.
|
||||||
|
|
||||||
|
* The ``SitemapIndexItem.__str__()`` method will be removed.
|
||||||
|
|
||||||
.. _deprecation-removed-in-4.1:
|
.. _deprecation-removed-in-4.1:
|
||||||
|
|
||||||
4.1
|
4.1
|
||||||
|
|
|
@ -294,6 +294,23 @@ Note:
|
||||||
fallback entry with a value of :setting:`LANGUAGE_CODE`. The default is
|
fallback entry with a value of :setting:`LANGUAGE_CODE`. The default is
|
||||||
``False``.
|
``False``.
|
||||||
|
|
||||||
|
.. method:: Sitemap.get_latest_lastmod()
|
||||||
|
|
||||||
|
.. versionadded:: 4.1
|
||||||
|
|
||||||
|
**Optional.** A method that returns the latest value returned by
|
||||||
|
:attr:`~Sitemap.lastmod`. This function is used to add the ``lastmod``
|
||||||
|
attribute to :ref:`Sitemap index context
|
||||||
|
variables<sitemap-index-context-variables>`.
|
||||||
|
|
||||||
|
By default :meth:`~Sitemap.get_latest_lastmod` returns:
|
||||||
|
|
||||||
|
* If :attr:`~Sitemap.lastmod` is an attribute:
|
||||||
|
:attr:`~Sitemap.lastmod`.
|
||||||
|
* If :attr:`~Sitemap.lastmod` is a method:
|
||||||
|
The latest ``lastmod`` returned by calling the method with all
|
||||||
|
items returned by :meth:`Sitemap.items`.
|
||||||
|
|
||||||
Shortcuts
|
Shortcuts
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
@ -306,7 +323,8 @@ The sitemap framework provides a convenience class for a common case:
|
||||||
a ``queryset`` entry. This queryset will be used to generate the items
|
a ``queryset`` entry. This queryset will be used to generate the items
|
||||||
of the sitemap. It may also have a ``date_field`` entry that
|
of the sitemap. It may also have a ``date_field`` entry that
|
||||||
specifies a date field for objects retrieved from the ``queryset``.
|
specifies a date field for objects retrieved from the ``queryset``.
|
||||||
This will be used for the :attr:`~Sitemap.lastmod` attribute in the
|
This will be used for the :attr:`~Sitemap.lastmod` attribute and
|
||||||
|
:meth:`~Sitemap.get_latest_lastmod` methods in the in the
|
||||||
generated sitemap.
|
generated sitemap.
|
||||||
|
|
||||||
The :attr:`~Sitemap.priority`, :attr:`~Sitemap.changefreq`,
|
The :attr:`~Sitemap.priority`, :attr:`~Sitemap.changefreq`,
|
||||||
|
@ -413,6 +431,10 @@ both :file:`sitemap-flatpages.xml` and :file:`sitemap-blog.xml`. The
|
||||||
:class:`~django.contrib.sitemaps.Sitemap` classes and the ``sitemaps``
|
:class:`~django.contrib.sitemaps.Sitemap` classes and the ``sitemaps``
|
||||||
dict don't change at all.
|
dict don't change at all.
|
||||||
|
|
||||||
|
If all sitemaps have a ``lastmod`` returned by
|
||||||
|
:meth:`Sitemap.get_latest_lastmod` the sitemap index will have a
|
||||||
|
``Last-Modified`` header equal to the latest ``lastmod``.
|
||||||
|
|
||||||
You should create an index file if one of your sitemaps has more than 50,000
|
You should create an index file if one of your sitemaps has more than 50,000
|
||||||
URLs. In this case, Django will automatically paginate the sitemap, and the
|
URLs. In this case, Django will automatically paginate the sitemap, and the
|
||||||
index will reflect that.
|
index will reflect that.
|
||||||
|
@ -433,6 +455,9 @@ with a caching decorator -- you must name your sitemap view and pass
|
||||||
{'sitemaps': sitemaps}, name='sitemaps'),
|
{'sitemaps': sitemaps}, name='sitemaps'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
.. versionchanged:: 4.1
|
||||||
|
|
||||||
|
Use of the ``Last-Modified`` header was added.
|
||||||
|
|
||||||
Template customization
|
Template customization
|
||||||
======================
|
======================
|
||||||
|
@ -468,10 +493,23 @@ When customizing the templates for the
|
||||||
:func:`~django.contrib.sitemaps.views.sitemap` views, you can rely on the
|
:func:`~django.contrib.sitemaps.views.sitemap` views, you can rely on the
|
||||||
following context variables.
|
following context variables.
|
||||||
|
|
||||||
|
.. _sitemap-index-context-variables:
|
||||||
|
|
||||||
Index
|
Index
|
||||||
-----
|
-----
|
||||||
|
|
||||||
The variable ``sitemaps`` is a list of absolute URLs to each of the sitemaps.
|
The variable ``sitemaps`` is a list of objects containing the ``location`` and
|
||||||
|
``lastmod`` attribute for each of the sitemaps. Each URL exposes the following
|
||||||
|
attributes:
|
||||||
|
|
||||||
|
- ``location``: The location (url & page) of the sitemap.
|
||||||
|
- ``lastmod``: Populated by the :meth:`~Sitemap.get_latest_lastmod`
|
||||||
|
method for each sitemap.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.1
|
||||||
|
|
||||||
|
The context was changed to a list of objects with ``location`` and optional
|
||||||
|
``lastmod`` attributes.
|
||||||
|
|
||||||
Sitemap
|
Sitemap
|
||||||
-------
|
-------
|
||||||
|
|
|
@ -88,7 +88,11 @@ Minor features
|
||||||
:mod:`django.contrib.sitemaps`
|
:mod:`django.contrib.sitemaps`
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
* ...
|
* The default sitemap index template ``<sitemapindex>`` now includes the
|
||||||
|
``<lastmod>`` timestamp where available, through the new
|
||||||
|
:meth:`~django.contrib.sitemaps.Sitemap.get_latest_lastmod` method. Custom
|
||||||
|
sitemap index templates should be updated for the adjusted :ref:`context
|
||||||
|
variables <sitemap-index-context-variables>`.
|
||||||
|
|
||||||
:mod:`django.contrib.sites`
|
:mod:`django.contrib.sites`
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
@ -253,7 +257,10 @@ Features deprecated in 4.1
|
||||||
Miscellaneous
|
Miscellaneous
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
* ...
|
* The context for sitemap index templates of a flat list of URLs is deprecated.
|
||||||
|
Custom sitemap index templates should be updated for the adjusted
|
||||||
|
:ref:`context variables <sitemap-index-context-variables>`, expecting a list
|
||||||
|
of objects with ``location`` and optional ``lastmod`` attributes.
|
||||||
|
|
||||||
Features removed in 4.1
|
Features removed in 4.1
|
||||||
=======================
|
=======================
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- This is a customised template -->
|
||||||
|
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
{% spaceless %}
|
||||||
|
{% for site in sitemaps %}
|
||||||
|
<sitemap>
|
||||||
|
<loc>{{ site.location }}</loc>
|
||||||
|
{% if site.last_mod %}
|
||||||
|
<lastmod>{{ site.last_mod|date:"c" }}</lastmod>
|
||||||
|
{% endif %}
|
||||||
|
</sitemap>
|
||||||
|
{% endfor %}
|
||||||
|
{% endspaceless %}
|
||||||
|
</sitemapindex>
|
|
@ -85,3 +85,12 @@ class GenericViewsSitemapTests(SitemapTestsBase):
|
||||||
)
|
)
|
||||||
with self.assertWarnsMessage(RemovedInDjango50Warning, msg):
|
with self.assertWarnsMessage(RemovedInDjango50Warning, msg):
|
||||||
sitemap.get_protocol()
|
sitemap.get_protocol()
|
||||||
|
|
||||||
|
def test_generic_sitemap_index(self):
|
||||||
|
TestModel.objects.update(lastmod=datetime(2013, 3, 13, 10, 0, 0))
|
||||||
|
response = self.client.get('/generic-lastmod/index.xml')
|
||||||
|
expected_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<sitemap><loc>http://example.com/simple/sitemap-generic.xml</loc><lastmod>2013-03-13T10:00:00</lastmod></sitemap>
|
||||||
|
</sitemapindex>"""
|
||||||
|
self.assertXMLEqual(response.content.decode('utf-8'), expected_content)
|
||||||
|
|
|
@ -24,9 +24,9 @@ class HTTPSitemapTests(SitemapTestsBase):
|
||||||
response = self.client.get('/simple/index.xml')
|
response = self.client.get('/simple/index.xml')
|
||||||
expected_content = """<?xml version="1.0" encoding="UTF-8"?>
|
expected_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<sitemap><loc>%s/simple/sitemap-simple.xml</loc></sitemap>
|
<sitemap><loc>%s/simple/sitemap-simple.xml</loc><lastmod>%s</lastmod></sitemap>
|
||||||
</sitemapindex>
|
</sitemapindex>
|
||||||
""" % self.base_url
|
""" % (self.base_url, date.today())
|
||||||
self.assertXMLEqual(response.content.decode(), expected_content)
|
self.assertXMLEqual(response.content.decode(), expected_content)
|
||||||
|
|
||||||
def test_sitemap_not_callable(self):
|
def test_sitemap_not_callable(self):
|
||||||
|
@ -34,9 +34,9 @@ class HTTPSitemapTests(SitemapTestsBase):
|
||||||
response = self.client.get('/simple-not-callable/index.xml')
|
response = self.client.get('/simple-not-callable/index.xml')
|
||||||
expected_content = """<?xml version="1.0" encoding="UTF-8"?>
|
expected_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<sitemap><loc>%s/simple/sitemap-simple.xml</loc></sitemap>
|
<sitemap><loc>%s/simple/sitemap-simple.xml</loc><lastmod>%s</lastmod></sitemap>
|
||||||
</sitemapindex>
|
</sitemapindex>
|
||||||
""" % self.base_url
|
""" % (self.base_url, date.today())
|
||||||
self.assertXMLEqual(response.content.decode(), expected_content)
|
self.assertXMLEqual(response.content.decode(), expected_content)
|
||||||
|
|
||||||
def test_paged_sitemap(self):
|
def test_paged_sitemap(self):
|
||||||
|
@ -44,24 +44,24 @@ class HTTPSitemapTests(SitemapTestsBase):
|
||||||
response = self.client.get('/simple-paged/index.xml')
|
response = self.client.get('/simple-paged/index.xml')
|
||||||
expected_content = """<?xml version="1.0" encoding="UTF-8"?>
|
expected_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<sitemap><loc>{0}/simple/sitemap-simple.xml</loc></sitemap><sitemap><loc>{0}/simple/sitemap-simple.xml?p=2</loc></sitemap>
|
<sitemap><loc>{0}/simple/sitemap-simple.xml</loc><lastmod>{1}</lastmod></sitemap><sitemap><loc>{0}/simple/sitemap-simple.xml?p=2</loc><lastmod>{1}</lastmod></sitemap>
|
||||||
</sitemapindex>
|
</sitemapindex>
|
||||||
""".format(self.base_url)
|
""".format(self.base_url, date.today())
|
||||||
self.assertXMLEqual(response.content.decode(), expected_content)
|
self.assertXMLEqual(response.content.decode(), expected_content)
|
||||||
|
|
||||||
@override_settings(TEMPLATES=[{
|
@override_settings(TEMPLATES=[{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
'DIRS': [os.path.join(os.path.dirname(__file__), 'templates')],
|
'DIRS': [os.path.join(os.path.dirname(__file__), 'templates')],
|
||||||
}])
|
}])
|
||||||
def test_simple_sitemap_custom_index(self):
|
def test_simple_sitemap_custom_lastmod_index(self):
|
||||||
"A simple sitemap index can be rendered with a custom template"
|
"A simple sitemap index can be rendered with a custom template"
|
||||||
response = self.client.get('/simple/custom-index.xml')
|
response = self.client.get('/simple/custom-lastmod-index.xml')
|
||||||
expected_content = """<?xml version="1.0" encoding="UTF-8"?>
|
expected_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!-- This is a customised template -->
|
<!-- This is a customised template -->
|
||||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<sitemap><loc>%s/simple/sitemap-simple.xml</loc></sitemap>
|
<sitemap><loc>%s/simple/sitemap-simple.xml</loc><lastmod>%s</lastmod></sitemap>
|
||||||
</sitemapindex>
|
</sitemapindex>
|
||||||
""" % self.base_url
|
""" % (self.base_url, date.today())
|
||||||
self.assertXMLEqual(response.content.decode(), expected_content)
|
self.assertXMLEqual(response.content.decode(), expected_content)
|
||||||
|
|
||||||
def test_simple_sitemap_section(self):
|
def test_simple_sitemap_section(self):
|
||||||
|
@ -176,7 +176,30 @@ class HTTPSitemapTests(SitemapTestsBase):
|
||||||
response = self.client.get('/lastmod-sitemaps/descending.xml')
|
response = self.client.get('/lastmod-sitemaps/descending.xml')
|
||||||
self.assertEqual(response.headers['Last-Modified'], 'Sat, 20 Apr 2013 05:00:00 GMT')
|
self.assertEqual(response.headers['Last-Modified'], 'Sat, 20 Apr 2013 05:00:00 GMT')
|
||||||
|
|
||||||
@override_settings(USE_I18N=True)
|
def test_sitemap_get_latest_lastmod_none(self):
|
||||||
|
"""
|
||||||
|
sitemapindex.lastmod is ommitted 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, '<lastmod>')
|
||||||
|
|
||||||
|
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, '<lastmod>2013-03-13T10:00:00</lastmod>')
|
||||||
|
|
||||||
|
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, '<lastmod>2013-03-13T10:00:00-05:00</lastmod>')
|
||||||
|
|
||||||
def test_localized_priority(self):
|
def test_localized_priority(self):
|
||||||
"""The priority value should not be localized."""
|
"""The priority value should not be localized."""
|
||||||
with translation.override('fr'):
|
with translation.override('fr'):
|
||||||
|
@ -240,9 +263,9 @@ class HTTPSitemapTests(SitemapTestsBase):
|
||||||
response = self.client.get('/cached/index.xml')
|
response = self.client.get('/cached/index.xml')
|
||||||
expected_content = """<?xml version="1.0" encoding="UTF-8"?>
|
expected_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<sitemap><loc>%s/cached/sitemap-simple.xml</loc></sitemap>
|
<sitemap><loc>%s/cached/sitemap-simple.xml</loc><lastmod>%s</lastmod></sitemap>
|
||||||
</sitemapindex>
|
</sitemapindex>
|
||||||
""" % self.base_url
|
""" % (self.base_url, date.today())
|
||||||
self.assertXMLEqual(response.content.decode(), expected_content)
|
self.assertXMLEqual(response.content.decode(), expected_content)
|
||||||
|
|
||||||
def test_x_robots_sitemap(self):
|
def test_x_robots_sitemap(self):
|
||||||
|
@ -356,7 +379,7 @@ class HTTPSitemapTests(SitemapTestsBase):
|
||||||
def test_callable_sitemod_partial(self):
|
def test_callable_sitemod_partial(self):
|
||||||
"""
|
"""
|
||||||
Not all items have `lastmod`. Therefore the `Last-Modified` header
|
Not all items have `lastmod`. Therefore the `Last-Modified` header
|
||||||
is not set by the detail sitemap view.
|
is not set by the detail or index sitemap view.
|
||||||
"""
|
"""
|
||||||
index_response = self.client.get('/callable-lastmod-partial/index.xml')
|
index_response = self.client.get('/callable-lastmod-partial/index.xml')
|
||||||
sitemap_response = self.client.get('/callable-lastmod-partial/sitemap.xml')
|
sitemap_response = self.client.get('/callable-lastmod-partial/sitemap.xml')
|
||||||
|
@ -378,16 +401,15 @@ class HTTPSitemapTests(SitemapTestsBase):
|
||||||
def test_callable_sitemod_full(self):
|
def test_callable_sitemod_full(self):
|
||||||
"""
|
"""
|
||||||
All items in the sitemap have `lastmod`. The `Last-Modified` header
|
All items in the sitemap have `lastmod`. The `Last-Modified` header
|
||||||
is set for the detail sitemap view. The index view does not (currently)
|
is set for the detail and index sitemap view.
|
||||||
set the `Last-Modified` header.
|
|
||||||
"""
|
"""
|
||||||
index_response = self.client.get('/callable-lastmod-full/index.xml')
|
index_response = self.client.get('/callable-lastmod-full/index.xml')
|
||||||
sitemap_response = self.client.get('/callable-lastmod-full/sitemap.xml')
|
sitemap_response = self.client.get('/callable-lastmod-full/sitemap.xml')
|
||||||
self.assertNotIn('Last-Modified', index_response)
|
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')
|
self.assertEqual(sitemap_response.headers['Last-Modified'], 'Thu, 13 Mar 2014 10:00:00 GMT')
|
||||||
expected_content_index = """<?xml version="1.0" encoding="UTF-8"?>
|
expected_content_index = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<sitemap><loc>http://example.com/simple/sitemap-callable-lastmod.xml</loc></sitemap>
|
<sitemap><loc>http://example.com/simple/sitemap-callable-lastmod.xml</loc><lastmod>2014-03-13T10:00:00</lastmod></sitemap>
|
||||||
</sitemapindex>
|
</sitemapindex>
|
||||||
"""
|
"""
|
||||||
expected_content_sitemap = """<?xml version="1.0" encoding="UTF-8"?>
|
expected_content_sitemap = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
@ -397,3 +419,31 @@ class HTTPSitemapTests(SitemapTestsBase):
|
||||||
"""
|
"""
|
||||||
self.assertXMLEqual(index_response.content.decode(), expected_content_index)
|
self.assertXMLEqual(index_response.content.decode(), expected_content_index)
|
||||||
self.assertXMLEqual(sitemap_response.content.decode(), expected_content_sitemap)
|
self.assertXMLEqual(sitemap_response.content.decode(), expected_content_sitemap)
|
||||||
|
|
||||||
|
|
||||||
|
# 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 = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- This is a customised template -->
|
||||||
|
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<sitemap><loc>%s/simple/sitemap-simple.xml</loc></sitemap>
|
||||||
|
</sitemapindex>
|
||||||
|
""" % (self.base_url)
|
||||||
|
self.assertXMLEqual(response.content.decode(), expected_content)
|
||||||
|
|
|
@ -14,9 +14,9 @@ class HTTPSSitemapTests(SitemapTestsBase):
|
||||||
response = self.client.get('/secure/index.xml')
|
response = self.client.get('/secure/index.xml')
|
||||||
expected_content = """<?xml version="1.0" encoding="UTF-8"?>
|
expected_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<sitemap><loc>%s/secure/sitemap-simple.xml</loc></sitemap>
|
<sitemap><loc>%s/secure/sitemap-simple.xml</loc><lastmod>%s</lastmod></sitemap>
|
||||||
</sitemapindex>
|
</sitemapindex>
|
||||||
""" % self.base_url
|
""" % (self.base_url, date.today())
|
||||||
self.assertXMLEqual(response.content.decode(), expected_content)
|
self.assertXMLEqual(response.content.decode(), expected_content)
|
||||||
|
|
||||||
def test_secure_sitemap_section(self):
|
def test_secure_sitemap_section(self):
|
||||||
|
@ -39,9 +39,9 @@ class HTTPSDetectionSitemapTests(SitemapTestsBase):
|
||||||
response = self.client.get('/simple/index.xml', **self.extra)
|
response = self.client.get('/simple/index.xml', **self.extra)
|
||||||
expected_content = """<?xml version="1.0" encoding="UTF-8"?>
|
expected_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<sitemap><loc>%s/simple/sitemap-simple.xml</loc></sitemap>
|
<sitemap><loc>%s/simple/sitemap-simple.xml</loc><lastmod>%s</lastmod></sitemap>
|
||||||
</sitemapindex>
|
</sitemapindex>
|
||||||
""" % self.base_url.replace('http://', 'https://')
|
""" % (self.base_url.replace('http://', 'https://'), date.today())
|
||||||
self.assertXMLEqual(response.content.decode(), expected_content)
|
self.assertXMLEqual(response.content.decode(), expected_content)
|
||||||
|
|
||||||
def test_sitemap_section_with_https_request(self):
|
def test_sitemap_section_with_https_request(self):
|
||||||
|
|
|
@ -14,13 +14,15 @@ class SimpleSitemap(Sitemap):
|
||||||
changefreq = "never"
|
changefreq = "never"
|
||||||
priority = 0.5
|
priority = 0.5
|
||||||
location = '/location/'
|
location = '/location/'
|
||||||
lastmod = datetime.now()
|
lastmod = date.today()
|
||||||
|
|
||||||
def items(self):
|
def items(self):
|
||||||
return [object()]
|
return [object()]
|
||||||
|
|
||||||
|
|
||||||
class SimplePagedSitemap(Sitemap):
|
class SimplePagedSitemap(Sitemap):
|
||||||
|
lastmod = date.today()
|
||||||
|
|
||||||
def items(self):
|
def items(self):
|
||||||
return [object() for x in range(Sitemap.limit + 1)]
|
return [object() for x in range(Sitemap.limit + 1)]
|
||||||
|
|
||||||
|
@ -110,6 +112,26 @@ class CallableLastmodFullSitemap(Sitemap):
|
||||||
return obj.lastmod
|
return obj.lastmod
|
||||||
|
|
||||||
|
|
||||||
|
class GetLatestLastmodNoneSiteMap(Sitemap):
|
||||||
|
changefreq = "never"
|
||||||
|
priority = 0.5
|
||||||
|
location = '/location/'
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return [object()]
|
||||||
|
|
||||||
|
def lastmod(self, obj):
|
||||||
|
return datetime(2013, 3, 13, 10, 0, 0)
|
||||||
|
|
||||||
|
def get_latest_lastmod(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class GetLatestLastmodSiteMap(SimpleSitemap):
|
||||||
|
def get_latest_lastmod(self):
|
||||||
|
return datetime(2013, 3, 13, 10, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
def testmodelview(request, id):
|
def testmodelview(request, id):
|
||||||
return HttpResponse()
|
return HttpResponse()
|
||||||
|
|
||||||
|
@ -180,6 +202,18 @@ generic_sitemaps = {
|
||||||
'generic': GenericSitemap({'queryset': TestModel.objects.order_by('pk').all()}),
|
'generic': GenericSitemap({'queryset': TestModel.objects.order_by('pk').all()}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get_latest_lastmod_none_sitemaps = {
|
||||||
|
'get-latest-lastmod-none': GetLatestLastmodNoneSiteMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
get_latest_lastmod_sitemaps = {
|
||||||
|
'get-latest-lastmod': GetLatestLastmodSiteMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
latest_lastmod_timezone_sitemaps = {
|
||||||
|
'latest-lastmod-timezone': TimezoneSiteMap,
|
||||||
|
}
|
||||||
|
|
||||||
generic_sitemaps_lastmod = {
|
generic_sitemaps_lastmod = {
|
||||||
'generic': GenericSitemap({
|
'generic': GenericSitemap({
|
||||||
'queryset': TestModel.objects.order_by('pk').all(),
|
'queryset': TestModel.objects.order_by('pk').all(),
|
||||||
|
@ -202,6 +236,10 @@ urlpatterns = [
|
||||||
path(
|
path(
|
||||||
'simple/custom-index.xml', views.index,
|
'simple/custom-index.xml', views.index,
|
||||||
{'sitemaps': simple_sitemaps, 'template_name': 'custom_sitemap_index.xml'}),
|
{'sitemaps': simple_sitemaps, 'template_name': 'custom_sitemap_index.xml'}),
|
||||||
|
path(
|
||||||
|
'simple/custom-lastmod-index.xml', views.index,
|
||||||
|
{'sitemaps': simple_sitemaps, 'template_name': 'custom_sitemap_lastmod_index.xml'},
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
'simple/sitemap-<section>.xml', views.sitemap,
|
'simple/sitemap-<section>.xml', views.sitemap,
|
||||||
{'sitemaps': simple_sitemaps},
|
{'sitemaps': simple_sitemaps},
|
||||||
|
@ -266,6 +304,21 @@ urlpatterns = [
|
||||||
'lastmod-sitemaps/descending.xml', views.sitemap,
|
'lastmod-sitemaps/descending.xml', views.sitemap,
|
||||||
{'sitemaps': sitemaps_lastmod_descending},
|
{'sitemaps': sitemaps_lastmod_descending},
|
||||||
name='django.contrib.sitemaps.views.sitemap'),
|
name='django.contrib.sitemaps.views.sitemap'),
|
||||||
|
path(
|
||||||
|
'lastmod/get-latest-lastmod-none-sitemap.xml', views.index,
|
||||||
|
{'sitemaps': get_latest_lastmod_none_sitemaps},
|
||||||
|
name='django.contrib.sitemaps.views.index',
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'lastmod/get-latest-lastmod-sitemap.xml', views.index,
|
||||||
|
{'sitemaps': get_latest_lastmod_sitemaps},
|
||||||
|
name='django.contrib.sitemaps.views.index',
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'lastmod/latest-lastmod-timezone-sitemap.xml', views.index,
|
||||||
|
{'sitemaps': latest_lastmod_timezone_sitemaps},
|
||||||
|
name='django.contrib.sitemaps.views.index',
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
'generic/sitemap.xml', views.sitemap,
|
'generic/sitemap.xml', views.sitemap,
|
||||||
{'sitemaps': generic_sitemaps},
|
{'sitemaps': generic_sitemaps},
|
||||||
|
@ -287,6 +340,11 @@ urlpatterns = [
|
||||||
path('callable-lastmod-partial/sitemap.xml', views.sitemap, {'sitemaps': callable_lastmod_partial_sitemap}),
|
path('callable-lastmod-partial/sitemap.xml', views.sitemap, {'sitemaps': callable_lastmod_partial_sitemap}),
|
||||||
path('callable-lastmod-full/index.xml', views.index, {'sitemaps': callable_lastmod_full_sitemap}),
|
path('callable-lastmod-full/index.xml', views.index, {'sitemaps': callable_lastmod_full_sitemap}),
|
||||||
path('callable-lastmod-full/sitemap.xml', views.sitemap, {'sitemaps': callable_lastmod_full_sitemap}),
|
path('callable-lastmod-full/sitemap.xml', views.sitemap, {'sitemaps': callable_lastmod_full_sitemap}),
|
||||||
|
path(
|
||||||
|
'generic-lastmod/index.xml', views.index,
|
||||||
|
{'sitemaps': generic_sitemaps_lastmod},
|
||||||
|
name='django.contrib.sitemaps.views.index',
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns += i18n_patterns(
|
urlpatterns += i18n_patterns(
|
||||||
|
|
Loading…
Reference in New Issue