utils.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. import json
  2. from collections import UserList
  3. from django.conf import settings
  4. from django.core.exceptions import ValidationError
  5. from django.forms.renderers import get_default_renderer
  6. from django.utils import timezone
  7. from django.utils.html import escape, format_html_join
  8. from django.utils.safestring import mark_safe
  9. from django.utils.translation import gettext_lazy as _
  10. def pretty_name(name):
  11. """Convert 'first_name' to 'First name'."""
  12. if not name:
  13. return ""
  14. return name.replace("_", " ").capitalize()
  15. def flatatt(attrs):
  16. """
  17. Convert a dictionary of attributes to a single string.
  18. The returned string will contain a leading space followed by key="value",
  19. XML-style pairs. In the case of a boolean value, the key will appear
  20. without a value. It is assumed that the keys do not need to be
  21. XML-escaped. If the passed dictionary is empty, then return an empty
  22. string.
  23. The result is passed through 'mark_safe' (by way of 'format_html_join').
  24. """
  25. key_value_attrs = []
  26. boolean_attrs = []
  27. for attr, value in attrs.items():
  28. if isinstance(value, bool):
  29. if value:
  30. boolean_attrs.append((attr,))
  31. elif value is not None:
  32. key_value_attrs.append((attr, value))
  33. return format_html_join("", ' {}="{}"', sorted(key_value_attrs)) + format_html_join(
  34. "", " {}", sorted(boolean_attrs)
  35. )
  36. class RenderableMixin:
  37. def get_context(self):
  38. raise NotImplementedError(
  39. "Subclasses of RenderableMixin must provide a get_context() method."
  40. )
  41. def render(self, template_name=None, context=None, renderer=None):
  42. renderer = renderer or self.renderer
  43. template = template_name or self.template_name
  44. context = context or self.get_context()
  45. return mark_safe(renderer.render(template, context))
  46. __str__ = render
  47. __html__ = render
  48. class RenderableFieldMixin(RenderableMixin):
  49. def as_field_group(self):
  50. return self.render()
  51. def as_hidden(self):
  52. raise NotImplementedError(
  53. "Subclasses of RenderableFieldMixin must provide an as_hidden() method."
  54. )
  55. def as_widget(self):
  56. raise NotImplementedError(
  57. "Subclasses of RenderableFieldMixin must provide an as_widget() method."
  58. )
  59. def __str__(self):
  60. """Render this field as an HTML widget."""
  61. if self.field.show_hidden_initial:
  62. return self.as_widget() + self.as_hidden(only_initial=True)
  63. return self.as_widget()
  64. __html__ = __str__
  65. class RenderableFormMixin(RenderableMixin):
  66. def as_p(self):
  67. """Render as <p> elements."""
  68. return self.render(self.template_name_p)
  69. def as_table(self):
  70. """Render as <tr> elements excluding the surrounding <table> tag."""
  71. return self.render(self.template_name_table)
  72. def as_ul(self):
  73. """Render as <li> elements excluding the surrounding <ul> tag."""
  74. return self.render(self.template_name_ul)
  75. def as_div(self):
  76. """Render as <div> elements."""
  77. return self.render(self.template_name_div)
  78. class RenderableErrorMixin(RenderableMixin):
  79. def as_json(self, escape_html=False):
  80. return json.dumps(self.get_json_data(escape_html))
  81. def as_text(self):
  82. return self.render(self.template_name_text)
  83. def as_ul(self):
  84. return self.render(self.template_name_ul)
  85. class ErrorDict(dict, RenderableErrorMixin):
  86. """
  87. A collection of errors that knows how to display itself in various formats.
  88. The dictionary keys are the field names, and the values are the errors.
  89. """
  90. template_name = "django/forms/errors/dict/default.html"
  91. template_name_text = "django/forms/errors/dict/text.txt"
  92. template_name_ul = "django/forms/errors/dict/ul.html"
  93. def __init__(self, *args, renderer=None, **kwargs):
  94. super().__init__(*args, **kwargs)
  95. self.renderer = renderer or get_default_renderer()
  96. def as_data(self):
  97. return {f: e.as_data() for f, e in self.items()}
  98. def get_json_data(self, escape_html=False):
  99. return {f: e.get_json_data(escape_html) for f, e in self.items()}
  100. def get_context(self):
  101. return {
  102. "errors": self.items(),
  103. "error_class": "errorlist",
  104. }
  105. class ErrorList(UserList, list, RenderableErrorMixin):
  106. """
  107. A collection of errors that knows how to display itself in various formats.
  108. """
  109. template_name = "django/forms/errors/list/default.html"
  110. template_name_text = "django/forms/errors/list/text.txt"
  111. template_name_ul = "django/forms/errors/list/ul.html"
  112. def __init__(self, initlist=None, error_class=None, renderer=None):
  113. super().__init__(initlist)
  114. if error_class is None:
  115. self.error_class = "errorlist"
  116. else:
  117. self.error_class = "errorlist {}".format(error_class)
  118. self.renderer = renderer or get_default_renderer()
  119. def as_data(self):
  120. return ValidationError(self.data).error_list
  121. def copy(self):
  122. copy = super().copy()
  123. copy.error_class = self.error_class
  124. return copy
  125. def get_json_data(self, escape_html=False):
  126. errors = []
  127. for error in self.as_data():
  128. message = next(iter(error))
  129. errors.append(
  130. {
  131. "message": escape(message) if escape_html else message,
  132. "code": error.code or "",
  133. }
  134. )
  135. return errors
  136. def get_context(self):
  137. return {
  138. "errors": self,
  139. "error_class": self.error_class,
  140. }
  141. def __repr__(self):
  142. return repr(list(self))
  143. def __contains__(self, item):
  144. return item in list(self)
  145. def __eq__(self, other):
  146. return list(self) == other
  147. def __getitem__(self, i):
  148. error = self.data[i]
  149. if isinstance(error, ValidationError):
  150. return next(iter(error))
  151. return error
  152. def __reduce_ex__(self, *args, **kwargs):
  153. # The `list` reduce function returns an iterator as the fourth element
  154. # that is normally used for repopulating. Since we only inherit from
  155. # `list` for `isinstance` backward compatibility (Refs #17413) we
  156. # nullify this iterator as it would otherwise result in duplicate
  157. # entries. (Refs #23594)
  158. info = super(UserList, self).__reduce_ex__(*args, **kwargs)
  159. return info[:3] + (None, None)
  160. # Utilities for time zone support in DateTimeField et al.
  161. def from_current_timezone(value):
  162. """
  163. When time zone support is enabled, convert naive datetimes
  164. entered in the current time zone to aware datetimes.
  165. """
  166. if settings.USE_TZ and value is not None and timezone.is_naive(value):
  167. current_timezone = timezone.get_current_timezone()
  168. try:
  169. if timezone._datetime_ambiguous_or_imaginary(value, current_timezone):
  170. raise ValueError("Ambiguous or non-existent time.")
  171. return timezone.make_aware(value, current_timezone)
  172. except Exception as exc:
  173. raise ValidationError(
  174. _(
  175. "%(datetime)s couldn’t be interpreted "
  176. "in time zone %(current_timezone)s; it "
  177. "may be ambiguous or it may not exist."
  178. ),
  179. code="ambiguous_timezone",
  180. params={"datetime": value, "current_timezone": current_timezone},
  181. ) from exc
  182. return value
  183. def to_current_timezone(value):
  184. """
  185. When time zone support is enabled, convert aware datetimes
  186. to naive datetimes in the current time zone for display.
  187. """
  188. if settings.USE_TZ and value is not None and timezone.is_aware(value):
  189. return timezone.make_naive(value)
  190. return value