Development of an internal social media platform with personalised dashboards for students
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

array.py 7.2KB

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