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