__init__.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. import warnings
  2. from urllib.parse import urlencode
  3. from urllib.request import urlopen
  4. from django.apps import apps as django_apps
  5. from django.conf import settings
  6. from django.core import paginator
  7. from django.core.exceptions import ImproperlyConfigured
  8. from django.urls import NoReverseMatch, reverse
  9. from django.utils import translation
  10. from django.utils.deprecation import RemovedInDjango50Warning
  11. PING_URL = "https://www.google.com/webmasters/tools/ping"
  12. class SitemapNotFound(Exception):
  13. pass
  14. def ping_google(sitemap_url=None, ping_url=PING_URL, sitemap_uses_https=True):
  15. """
  16. Alert Google that the sitemap for the current site has been updated.
  17. If sitemap_url is provided, it should be an absolute path to the sitemap
  18. for this site -- e.g., '/sitemap.xml'. If sitemap_url is not provided, this
  19. function will attempt to deduce it by using urls.reverse().
  20. """
  21. sitemap_full_url = _get_sitemap_full_url(sitemap_url, sitemap_uses_https)
  22. params = urlencode({"sitemap": sitemap_full_url})
  23. urlopen("%s?%s" % (ping_url, params))
  24. def _get_sitemap_full_url(sitemap_url, sitemap_uses_https=True):
  25. if not django_apps.is_installed("django.contrib.sites"):
  26. raise ImproperlyConfigured(
  27. "ping_google requires django.contrib.sites, which isn't installed."
  28. )
  29. if sitemap_url is None:
  30. try:
  31. # First, try to get the "index" sitemap URL.
  32. sitemap_url = reverse("django.contrib.sitemaps.views.index")
  33. except NoReverseMatch:
  34. try:
  35. # Next, try for the "global" sitemap URL.
  36. sitemap_url = reverse("django.contrib.sitemaps.views.sitemap")
  37. except NoReverseMatch:
  38. pass
  39. if sitemap_url is None:
  40. raise SitemapNotFound(
  41. "You didn't provide a sitemap_url, and the sitemap URL couldn't be "
  42. "auto-detected."
  43. )
  44. Site = django_apps.get_model("sites.Site")
  45. current_site = Site.objects.get_current()
  46. scheme = "https" if sitemap_uses_https else "http"
  47. return "%s://%s%s" % (scheme, current_site.domain, sitemap_url)
  48. class Sitemap:
  49. # This limit is defined by Google. See the index documentation at
  50. # https://www.sitemaps.org/protocol.html#index.
  51. limit = 50000
  52. # If protocol is None, the URLs in the sitemap will use the protocol
  53. # with which the sitemap was requested.
  54. protocol = None
  55. # Enables generating URLs for all languages.
  56. i18n = False
  57. # Override list of languages to use.
  58. languages = None
  59. # Enables generating alternate/hreflang links.
  60. alternates = False
  61. # Add an alternate/hreflang link with value 'x-default'.
  62. x_default = False
  63. def _get(self, name, item, default=None):
  64. try:
  65. attr = getattr(self, name)
  66. except AttributeError:
  67. return default
  68. if callable(attr):
  69. if self.i18n:
  70. # Split the (item, lang_code) tuples again for the location,
  71. # priority, lastmod and changefreq method calls.
  72. item, lang_code = item
  73. return attr(item)
  74. return attr
  75. def get_languages_for_item(self, item):
  76. """Languages for which this item is displayed."""
  77. return self._languages()
  78. def _languages(self):
  79. if self.languages is not None:
  80. return self.languages
  81. return [lang_code for lang_code, _ in settings.LANGUAGES]
  82. def _items(self):
  83. if self.i18n:
  84. # Create (item, lang_code) tuples for all items and languages.
  85. # This is necessary to paginate with all languages already considered.
  86. items = [
  87. (item, lang_code)
  88. for item in self.items()
  89. for lang_code in self.get_languages_for_item(item)
  90. ]
  91. return items
  92. return self.items()
  93. def _location(self, item, force_lang_code=None):
  94. if self.i18n:
  95. obj, lang_code = item
  96. # Activate language from item-tuple or forced one before calling location.
  97. with translation.override(force_lang_code or lang_code):
  98. return self._get("location", item)
  99. return self._get("location", item)
  100. @property
  101. def paginator(self):
  102. return paginator.Paginator(self._items(), self.limit)
  103. def items(self):
  104. return []
  105. def location(self, item):
  106. return item.get_absolute_url()
  107. def get_protocol(self, protocol=None):
  108. # Determine protocol
  109. if self.protocol is None and protocol is None:
  110. warnings.warn(
  111. "The default sitemap protocol will be changed from 'http' to "
  112. "'https' in Django 5.0. Set Sitemap.protocol to silence this "
  113. "warning.",
  114. category=RemovedInDjango50Warning,
  115. stacklevel=2,
  116. )
  117. # RemovedInDjango50Warning: when the deprecation ends, replace 'http'
  118. # with 'https'.
  119. return self.protocol or protocol or "http"
  120. def get_domain(self, site=None):
  121. # Determine domain
  122. if site is None:
  123. if django_apps.is_installed("django.contrib.sites"):
  124. Site = django_apps.get_model("sites.Site")
  125. try:
  126. site = Site.objects.get_current()
  127. except Site.DoesNotExist:
  128. pass
  129. if site is None:
  130. raise ImproperlyConfigured(
  131. "To use sitemaps, either enable the sites framework or pass "
  132. "a Site/RequestSite object in your view."
  133. )
  134. return site.domain
  135. def get_urls(self, page=1, site=None, protocol=None):
  136. protocol = self.get_protocol(protocol)
  137. domain = self.get_domain(site)
  138. return self._urls(page, protocol, domain)
  139. def get_latest_lastmod(self):
  140. if not hasattr(self, "lastmod"):
  141. return None
  142. if callable(self.lastmod):
  143. try:
  144. return max([self.lastmod(item) for item in self.items()], default=None)
  145. except TypeError:
  146. return None
  147. else:
  148. return self.lastmod
  149. def _urls(self, page, protocol, domain):
  150. urls = []
  151. latest_lastmod = None
  152. all_items_lastmod = True # track if all items have a lastmod
  153. paginator_page = self.paginator.page(page)
  154. for item in paginator_page.object_list:
  155. loc = f"{protocol}://{domain}{self._location(item)}"
  156. priority = self._get("priority", item)
  157. lastmod = self._get("lastmod", item)
  158. if all_items_lastmod:
  159. all_items_lastmod = lastmod is not None
  160. if all_items_lastmod and (
  161. latest_lastmod is None or lastmod > latest_lastmod
  162. ):
  163. latest_lastmod = lastmod
  164. url_info = {
  165. "item": item,
  166. "location": loc,
  167. "lastmod": lastmod,
  168. "changefreq": self._get("changefreq", item),
  169. "priority": str(priority if priority is not None else ""),
  170. "alternates": [],
  171. }
  172. if self.i18n and self.alternates:
  173. item_languages = self.get_languages_for_item(item[0])
  174. for lang_code in item_languages:
  175. loc = f"{protocol}://{domain}{self._location(item, lang_code)}"
  176. url_info["alternates"].append(
  177. {
  178. "location": loc,
  179. "lang_code": lang_code,
  180. }
  181. )
  182. if self.x_default and settings.LANGUAGE_CODE in item_languages:
  183. lang_code = settings.LANGUAGE_CODE
  184. loc = f"{protocol}://{domain}{self._location(item, lang_code)}"
  185. loc = loc.replace(f"/{lang_code}/", "/", 1)
  186. url_info["alternates"].append(
  187. {
  188. "location": loc,
  189. "lang_code": "x-default",
  190. }
  191. )
  192. urls.append(url_info)
  193. if all_items_lastmod and latest_lastmod:
  194. self.latest_lastmod = latest_lastmod
  195. return urls
  196. class GenericSitemap(Sitemap):
  197. priority = None
  198. changefreq = None
  199. def __init__(self, info_dict, priority=None, changefreq=None, protocol=None):
  200. self.queryset = info_dict["queryset"]
  201. self.date_field = info_dict.get("date_field")
  202. self.priority = self.priority or priority
  203. self.changefreq = self.changefreq or changefreq
  204. self.protocol = self.protocol or protocol
  205. def items(self):
  206. # Make sure to return a clone; we don't want premature evaluation.
  207. return self.queryset.filter()
  208. def lastmod(self, item):
  209. if self.date_field is not None:
  210. return getattr(item, self.date_field)
  211. return None
  212. def get_latest_lastmod(self):
  213. if self.date_field is not None:
  214. return (
  215. self.queryset.order_by("-" + self.date_field)
  216. .values_list(self.date_field, flat=True)
  217. .first()
  218. )
  219. return None