csv.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. import csv
  2. from io import StringIO
  3. from django import forms
  4. from django.contrib.contenttypes.models import ContentType
  5. from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
  6. from django.db.models import Q
  7. from django.utils.translation import gettext as _
  8. from utilities.choices import unpack_grouped_choices
  9. from utilities.forms.utils import parse_csv, validate_csv
  10. from utilities.utils import content_type_identifier
  11. __all__ = (
  12. 'CSVChoiceField',
  13. 'CSVContentTypeField',
  14. 'CSVModelChoiceField',
  15. 'CSVModelMultipleChoiceField',
  16. 'CSVMultipleChoiceField',
  17. 'CSVMultipleContentTypeField',
  18. 'CSVTypedChoiceField',
  19. )
  20. class CSVChoicesMixin:
  21. STATIC_CHOICES = True
  22. def __init__(self, *, choices=(), **kwargs):
  23. super().__init__(choices=choices, **kwargs)
  24. self.choices = unpack_grouped_choices(choices)
  25. class CSVChoiceField(CSVChoicesMixin, forms.ChoiceField):
  26. """
  27. A CSV field which accepts a single selection value.
  28. """
  29. pass
  30. class CSVMultipleChoiceField(CSVChoicesMixin, forms.MultipleChoiceField):
  31. """
  32. A CSV field which accepts multiple selection values.
  33. """
  34. def to_python(self, value):
  35. if not value:
  36. return []
  37. if not isinstance(value, str):
  38. raise forms.ValidationError(f"Invalid value for a multiple choice field: {value}")
  39. return value.split(',')
  40. class CSVTypedChoiceField(forms.TypedChoiceField):
  41. STATIC_CHOICES = True
  42. class CSVModelChoiceField(forms.ModelChoiceField):
  43. """
  44. Extends Django's `ModelChoiceField` to provide additional validation for CSV values.
  45. """
  46. default_error_messages = {
  47. 'invalid_choice': 'Object not found: %(value)s',
  48. }
  49. def to_python(self, value):
  50. try:
  51. return super().to_python(value)
  52. except MultipleObjectsReturned:
  53. raise forms.ValidationError(
  54. f'"{value}" is not a unique value for this field; multiple objects were found'
  55. )
  56. class CSVModelMultipleChoiceField(forms.ModelMultipleChoiceField):
  57. """
  58. Extends Django's `ModelMultipleChoiceField` to support comma-separated values.
  59. """
  60. default_error_messages = {
  61. 'invalid_choice': 'Object not found: %(value)s',
  62. }
  63. def clean(self, value):
  64. value = value.split(',') if value else []
  65. return super().clean(value)
  66. class CSVContentTypeField(CSVModelChoiceField):
  67. """
  68. CSV field for referencing a single content type, in the form `<app>.<model>`.
  69. """
  70. STATIC_CHOICES = True
  71. def prepare_value(self, value):
  72. return content_type_identifier(value)
  73. def to_python(self, value):
  74. if not value:
  75. return None
  76. try:
  77. app_label, model = value.split('.')
  78. except ValueError:
  79. raise forms.ValidationError(f'Object type must be specified as "<app>.<model>"')
  80. try:
  81. return self.queryset.get(app_label=app_label, model=model)
  82. except ObjectDoesNotExist:
  83. raise forms.ValidationError(f'Invalid object type')
  84. class CSVMultipleContentTypeField(forms.ModelMultipleChoiceField):
  85. """
  86. CSV field for referencing one or more content types, in the form `<app>.<model>`.
  87. """
  88. STATIC_CHOICES = True
  89. # TODO: Improve validation of selected ContentTypes
  90. def prepare_value(self, value):
  91. if type(value) is str:
  92. ct_filter = Q()
  93. for name in value.split(','):
  94. app_label, model = name.split('.')
  95. ct_filter |= Q(app_label=app_label, model=model)
  96. return list(ContentType.objects.filter(ct_filter).values_list('pk', flat=True))
  97. return content_type_identifier(value)