__init__.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. from django.apps import apps as django_apps
  2. from django.conf import settings
  3. from django.core import paginator
  4. from django.core.exceptions import ImproperlyConfigured
  5. from django.utils import translation
  6. class Sitemap:
  7. # This limit is defined by Google. See the index documentation at
  8. # https://www.sitemaps.org/protocol.html#index.
  9. limit = 50000
  10. # If protocol is None, the URLs in the sitemap will use the protocol
  11. # with which the sitemap was requested.
  12. protocol = None
  13. # Enables generating URLs for all languages.
  14. i18n = False
  15. # Override list of languages to use.
  16. languages = None
  17. # Enables generating alternate/hreflang links.
  18. alternates = False
  19. # Add an alternate/hreflang link with value 'x-default'.
  20. x_default = False
  21. def _get(self, name, item, default=None):
  22. try:
  23. attr = getattr(self, name)
  24. except AttributeError:
  25. return default
  26. if callable(attr):
  27. if self.i18n:
  28. # Split the (item, lang_code) tuples again for the location,
  29. # priority, lastmod and changefreq method calls.
  30. item, lang_code = item
  31. return attr(item)
  32. return attr
  33. def get_languages_for_item(self, item):
  34. """Languages for which this item is displayed."""
  35. return self._languages()
  36. def _languages(self):
  37. if self.languages is not None:
  38. return self.languages
  39. return [lang_code for lang_code, _ in settings.LANGUAGES]
  40. def _items(self):
  41. if self.i18n:
  42. # Create (item, lang_code) tuples for all items and languages.
  43. # This is necessary to paginate with all languages already considered.
  44. items = [
  45. (item, lang_code)
  46. for item in self.items()
  47. for lang_code in self.get_languages_for_item(item)
  48. ]
  49. return items
  50. return self.items()
  51. def _location(self, item, force_lang_code=None):
  52. if self.i18n:
  53. obj, lang_code = item
  54. # Activate language from item-tuple or forced one before calling location.
  55. with translation.override(force_lang_code or lang_code):
  56. return self._get("location", item)
  57. return self._get("location", item)
  58. @property
  59. def paginator(self):
  60. return paginator.Paginator(self._items(), self.limit)
  61. def items(self):
  62. return []
  63. def location(self, item):
  64. return item.get_absolute_url()
  65. def get_protocol(self, protocol=None):
  66. # Determine protocol
  67. return self.protocol or protocol or "https"
  68. def get_domain(self, site=None):
  69. # Determine domain
  70. if site is None:
  71. if django_apps.is_installed("django.contrib.sites"):
  72. Site = django_apps.get_model("sites.Site")
  73. try:
  74. site = Site.objects.get_current()
  75. except Site.DoesNotExist:
  76. pass
  77. if site is None:
  78. raise ImproperlyConfigured(
  79. "To use sitemaps, either enable the sites framework or pass "
  80. "a Site/RequestSite object in your view."
  81. )
  82. return site.domain
  83. def get_urls(self, page=1, site=None, protocol=None):
  84. protocol = self.get_protocol(protocol)
  85. domain = self.get_domain(site)
  86. return self._urls(page, protocol, domain)
  87. def get_latest_lastmod(self):
  88. if not hasattr(self, "lastmod"):
  89. return None
  90. if callable(self.lastmod):
  91. try:
  92. return max([self.lastmod(item) for item in self.items()], default=None)
  93. except TypeError:
  94. return None
  95. else:
  96. return self.lastmod
  97. def _urls(self, page, protocol, domain):
  98. urls = []
  99. latest_lastmod = None
  100. all_items_lastmod = True # track if all items have a lastmod
  101. paginator_page = self.paginator.page(page)
  102. for item in paginator_page.object_list:
  103. loc = f"{protocol}://{domain}{self._location(item)}"
  104. priority = self._get("priority", item)
  105. lastmod = self._get("lastmod", item)
  106. if all_items_lastmod:
  107. all_items_lastmod = lastmod is not None
  108. if all_items_lastmod and (
  109. latest_lastmod is None or lastmod > latest_lastmod
  110. ):
  111. latest_lastmod = lastmod
  112. url_info = {
  113. "item": item,
  114. "location": loc,
  115. "lastmod": lastmod,
  116. "changefreq": self._get("changefreq", item),
  117. "priority": str(priority if priority is not None else ""),
  118. "alternates": [],
  119. }
  120. if self.i18n and self.alternates:
  121. item_languages = self.get_languages_for_item(item[0])
  122. for lang_code in item_languages:
  123. loc = f"{protocol}://{domain}{self._location(item, lang_code)}"
  124. url_info["alternates"].append(
  125. {
  126. "location": loc,
  127. "lang_code": lang_code,
  128. }
  129. )
  130. if self.x_default and settings.LANGUAGE_CODE in item_languages:
  131. lang_code = settings.LANGUAGE_CODE
  132. loc = f"{protocol}://{domain}{self._location(item, lang_code)}"
  133. loc = loc.replace(f"/{lang_code}/", "/", 1)
  134. url_info["alternates"].append(
  135. {
  136. "location": loc,
  137. "lang_code": "x-default",
  138. }
  139. )
  140. urls.append(url_info)
  141. if all_items_lastmod and latest_lastmod:
  142. self.latest_lastmod = latest_lastmod
  143. return urls
  144. class GenericSitemap(Sitemap):
  145. priority = None
  146. changefreq = None
  147. def __init__(self, info_dict, priority=None, changefreq=None, protocol=None):
  148. self.queryset = info_dict["queryset"]
  149. self.date_field = info_dict.get("date_field")
  150. self.priority = self.priority or priority
  151. self.changefreq = self.changefreq or changefreq
  152. self.protocol = self.protocol or protocol
  153. def items(self):
  154. # Make sure to return a clone; we don't want premature evaluation.
  155. return self.queryset.filter()
  156. def lastmod(self, item):
  157. if self.date_field is not None:
  158. return getattr(item, self.date_field)
  159. return None
  160. def get_latest_lastmod(self):
  161. if self.date_field is not None:
  162. return (
  163. self.queryset.order_by("-" + self.date_field)
  164. .values_list(self.date_field, flat=True)
  165. .first()
  166. )
  167. return None