2
0

csv.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. from django import forms
  2. from django.utils.translation import gettext_lazy as _
  3. from django.contrib.contenttypes.models import ContentType
  4. from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist, FieldError
  5. from django.db.models import Q
  6. from utilities.choices import unpack_grouped_choices
  7. from utilities.object_types import object_type_identifier
  8. __all__ = (
  9. 'CSVChoiceField',
  10. 'CSVContentTypeField',
  11. 'CSVModelChoiceField',
  12. 'CSVModelMultipleChoiceField',
  13. 'CSVMultipleChoiceField',
  14. 'CSVMultipleContentTypeField',
  15. 'CSVTypedChoiceField',
  16. )
  17. class CSVSelectWidget(forms.Select):
  18. """
  19. Custom Select widget for CSV imports that treats blank values as omitted.
  20. This allows model defaults to be applied when a CSV field is present but empty.
  21. """
  22. def value_omitted_from_data(self, data, files, name):
  23. # Check if value is omitted using parent behavior
  24. if super().value_omitted_from_data(data, files, name):
  25. return True
  26. # Treat blank/empty strings as omitted to allow model defaults
  27. value = data.get(name)
  28. return value == '' or value is None
  29. class CSVChoicesMixin:
  30. STATIC_CHOICES = True
  31. def __init__(self, *, choices=(), **kwargs):
  32. super().__init__(choices=choices, **kwargs)
  33. self.choices = unpack_grouped_choices(choices)
  34. class CSVChoiceField(CSVChoicesMixin, forms.ChoiceField):
  35. """
  36. A CSV field which accepts a single selection value.
  37. Treats blank CSV values as omitted to allow model defaults.
  38. """
  39. widget = CSVSelectWidget
  40. class CSVMultipleChoiceField(CSVChoicesMixin, forms.MultipleChoiceField):
  41. """
  42. A CSV field which accepts multiple selection values.
  43. """
  44. def to_python(self, value):
  45. if not value:
  46. return []
  47. if not isinstance(value, str):
  48. raise forms.ValidationError(_("Invalid value for a multiple choice field: {value}").format(value=value))
  49. return value.split(',')
  50. class CSVTypedChoiceField(forms.TypedChoiceField):
  51. """
  52. A CSV field for typed choice values.
  53. Treats blank CSV values as omitted to allow model defaults.
  54. """
  55. STATIC_CHOICES = True
  56. widget = CSVSelectWidget
  57. class CSVModelChoiceField(forms.ModelChoiceField):
  58. """
  59. Extends Django's `ModelChoiceField` to provide additional validation for CSV values.
  60. """
  61. default_error_messages = {
  62. 'invalid_choice': _('Object not found: %(value)s'),
  63. }
  64. def to_python(self, value):
  65. try:
  66. return super().to_python(value)
  67. except MultipleObjectsReturned:
  68. raise forms.ValidationError(
  69. _('"{value}" is not a unique value for this field; multiple objects were found').format(value=value)
  70. )
  71. except FieldError:
  72. raise forms.ValidationError(
  73. _('"{field_name}" is an invalid accessor field name.').format(field_name=self.to_field_name)
  74. )
  75. class CSVModelMultipleChoiceField(forms.ModelMultipleChoiceField):
  76. """
  77. Extends Django's `ModelMultipleChoiceField` to support comma-separated values.
  78. """
  79. default_error_messages = {
  80. 'invalid_choice': _('Object not found: %(value)s'),
  81. }
  82. def clean(self, value):
  83. if not isinstance(value, list):
  84. value = value.split(',') if value else []
  85. return super().clean(value)
  86. class CSVContentTypeField(CSVModelChoiceField):
  87. """
  88. CSV field for referencing a single content type, in the form `<app>.<model>`.
  89. """
  90. STATIC_CHOICES = True
  91. def prepare_value(self, value):
  92. return object_type_identifier(value)
  93. def to_python(self, value):
  94. if not value:
  95. return None
  96. try:
  97. app_label, model = value.split('.')
  98. except ValueError:
  99. raise forms.ValidationError(_('Object type must be specified as "<app>.<model>"'))
  100. try:
  101. return self.queryset.get(app_label=app_label, model=model)
  102. except ObjectDoesNotExist:
  103. raise forms.ValidationError(_('Invalid object type'))
  104. class CSVMultipleContentTypeField(forms.ModelMultipleChoiceField):
  105. """
  106. CSV field for referencing one or more content types, in the form `<app>.<model>`.
  107. """
  108. STATIC_CHOICES = True
  109. # TODO: Improve validation of selected ContentTypes
  110. def prepare_value(self, value):
  111. if not value:
  112. return None
  113. if type(value) is str:
  114. ct_filter = Q()
  115. for name in value.split(','):
  116. app_label, model = name.split('.')
  117. ct_filter |= Q(app_label=app_label, model=model)
  118. return list(ContentType.objects.filter(ct_filter).values_list('pk', flat=True))
  119. return object_type_identifier(value)