array.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. import copy
  2. from itertools import chain
  3. from django import forms
  4. from django.contrib.postgres.validators import (
  5. ArrayMaxLengthValidator,
  6. ArrayMinLengthValidator,
  7. )
  8. from django.core.exceptions import ValidationError
  9. from django.utils.translation import gettext_lazy as _
  10. from ..utils import prefix_validation_error
  11. class SimpleArrayField(forms.CharField):
  12. default_error_messages = {
  13. "item_invalid": _("Item %(nth)s in the array did not validate:"),
  14. }
  15. def __init__(
  16. self, base_field, *, delimiter=",", max_length=None, min_length=None, **kwargs
  17. ):
  18. self.base_field = base_field
  19. self.delimiter = delimiter
  20. super().__init__(**kwargs)
  21. if min_length is not None:
  22. self.min_length = min_length
  23. self.validators.append(ArrayMinLengthValidator(int(min_length)))
  24. if max_length is not None:
  25. self.max_length = max_length
  26. self.validators.append(ArrayMaxLengthValidator(int(max_length)))
  27. def clean(self, value):
  28. value = super().clean(value)
  29. return [self.base_field.clean(val) for val in value]
  30. def prepare_value(self, value):
  31. if isinstance(value, list):
  32. return self.delimiter.join(
  33. str(self.base_field.prepare_value(v)) for v in value
  34. )
  35. return value
  36. def to_python(self, value):
  37. if isinstance(value, list):
  38. items = value
  39. elif value:
  40. items = value.split(self.delimiter)
  41. else:
  42. items = []
  43. errors = []
  44. values = []
  45. for index, item in enumerate(items):
  46. try:
  47. values.append(self.base_field.to_python(item))
  48. except ValidationError as error:
  49. errors.append(
  50. prefix_validation_error(
  51. error,
  52. prefix=self.error_messages["item_invalid"],
  53. code="item_invalid",
  54. params={"nth": index + 1},
  55. )
  56. )
  57. if errors:
  58. raise ValidationError(errors)
  59. return values
  60. def validate(self, value):
  61. super().validate(value)
  62. errors = []
  63. for index, item in enumerate(value):
  64. try:
  65. self.base_field.validate(item)
  66. except ValidationError as error:
  67. errors.append(
  68. prefix_validation_error(
  69. error,
  70. prefix=self.error_messages["item_invalid"],
  71. code="item_invalid",
  72. params={"nth": index + 1},
  73. )
  74. )
  75. if errors:
  76. raise ValidationError(errors)
  77. def run_validators(self, value):
  78. super().run_validators(value)
  79. errors = []
  80. for index, item in enumerate(value):
  81. try:
  82. self.base_field.run_validators(item)
  83. except ValidationError as error:
  84. errors.append(
  85. prefix_validation_error(
  86. error,
  87. prefix=self.error_messages["item_invalid"],
  88. code="item_invalid",
  89. params={"nth": index + 1},
  90. )
  91. )
  92. if errors:
  93. raise ValidationError(errors)
  94. def has_changed(self, initial, data):
  95. try:
  96. value = self.to_python(data)
  97. except ValidationError:
  98. pass
  99. else:
  100. if initial in self.empty_values and value in self.empty_values:
  101. return False
  102. return super().has_changed(initial, data)
  103. class SplitArrayWidget(forms.Widget):
  104. template_name = "postgres/widgets/split_array.html"
  105. def __init__(self, widget, size, **kwargs):
  106. self.widget = widget() if isinstance(widget, type) else widget
  107. self.size = size
  108. super().__init__(**kwargs)
  109. @property
  110. def is_hidden(self):
  111. return self.widget.is_hidden
  112. def value_from_datadict(self, data, files, name):
  113. return [
  114. self.widget.value_from_datadict(data, files, "%s_%s" % (name, index))
  115. for index in range(self.size)
  116. ]
  117. def value_omitted_from_data(self, data, files, name):
  118. return all(
  119. self.widget.value_omitted_from_data(data, files, "%s_%s" % (name, index))
  120. for index in range(self.size)
  121. )
  122. def id_for_label(self, id_):
  123. # See the comment for RadioSelect.id_for_label()
  124. if id_:
  125. id_ += "_0"
  126. return id_
  127. def get_context(self, name, value, attrs=None):
  128. attrs = {} if attrs is None else attrs
  129. context = super().get_context(name, value, attrs)
  130. if self.is_localized:
  131. self.widget.is_localized = self.is_localized
  132. value = value or []
  133. context["widget"]["subwidgets"] = []
  134. final_attrs = self.build_attrs(attrs)
  135. id_ = final_attrs.get("id")
  136. for i in range(max(len(value), self.size)):
  137. try:
  138. widget_value = value[i]
  139. except IndexError:
  140. widget_value = None
  141. if id_:
  142. final_attrs = {**final_attrs, "id": "%s_%s" % (id_, i)}
  143. context["widget"]["subwidgets"].append(
  144. self.widget.get_context(name + "_%s" % i, widget_value, final_attrs)[
  145. "widget"
  146. ]
  147. )
  148. return context
  149. @property
  150. def media(self):
  151. return self.widget.media
  152. def __deepcopy__(self, memo):
  153. obj = super().__deepcopy__(memo)
  154. obj.widget = copy.deepcopy(self.widget)
  155. return obj
  156. @property
  157. def needs_multipart_form(self):
  158. return self.widget.needs_multipart_form
  159. class SplitArrayField(forms.Field):
  160. default_error_messages = {
  161. "item_invalid": _("Item %(nth)s in the array did not validate:"),
  162. }
  163. def __init__(self, base_field, size, *, remove_trailing_nulls=False, **kwargs):
  164. self.base_field = base_field
  165. self.size = size
  166. self.remove_trailing_nulls = remove_trailing_nulls
  167. widget = SplitArrayWidget(widget=base_field.widget, size=size)
  168. kwargs.setdefault("widget", widget)
  169. super().__init__(**kwargs)
  170. def _remove_trailing_nulls(self, values):
  171. index = None
  172. if self.remove_trailing_nulls:
  173. for i, value in reversed(list(enumerate(values))):
  174. if value in self.base_field.empty_values:
  175. index = i
  176. else:
  177. break
  178. if index is not None:
  179. values = values[:index]
  180. return values, index
  181. def to_python(self, value):
  182. value = super().to_python(value)
  183. return [self.base_field.to_python(item) for item in value]
  184. def clean(self, value):
  185. cleaned_data = []
  186. errors = []
  187. if not any(value) and self.required:
  188. raise ValidationError(self.error_messages["required"])
  189. max_size = max(self.size, len(value))
  190. for index in range(max_size):
  191. item = value[index]
  192. try:
  193. cleaned_data.append(self.base_field.clean(item))
  194. except ValidationError as error:
  195. errors.append(
  196. prefix_validation_error(
  197. error,
  198. self.error_messages["item_invalid"],
  199. code="item_invalid",
  200. params={"nth": index + 1},
  201. )
  202. )
  203. cleaned_data.append(None)
  204. else:
  205. errors.append(None)
  206. cleaned_data, null_index = self._remove_trailing_nulls(cleaned_data)
  207. if null_index is not None:
  208. errors = errors[:null_index]
  209. errors = list(filter(None, errors))
  210. if errors:
  211. raise ValidationError(list(chain.from_iterable(errors)))
  212. return cleaned_data
  213. def has_changed(self, initial, data):
  214. try:
  215. data = self.to_python(data)
  216. except ValidationError:
  217. pass
  218. else:
  219. data, _ = self._remove_trailing_nulls(data)
  220. if initial in self.empty_values and data in self.empty_values:
  221. return False
  222. return super().has_changed(initial, data)