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

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 _ 

6 

7from utilities.choices import unpack_grouped_choices 

8from utilities.object_types import object_type_identifier 

9 

10__all__ = ( 

11 'CSVChoiceField', 

12 'CSVContentTypeField', 

13 'CSVModelChoiceField', 

14 'CSVModelMultipleChoiceField', 

15 'CSVMultipleChoiceField', 

16 'CSVMultipleContentTypeField', 

17 'CSVTypedChoiceField', 

18) 

19 

20 

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 

33 

34 

35class CSVChoicesMixin: 

36 STATIC_CHOICES = True 

37 

38 def __init__(self, *, choices=(), **kwargs): 

39 super().__init__(choices=choices, **kwargs) 

40 self.choices = unpack_grouped_choices(choices) 

41 

42 

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 

49 

50 

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(',') 

61 

62 

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 

70 

71 

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 } 

79 

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 ) 

91 

92 

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 } 

100 

101 def clean(self, value): 

102 if not isinstance(value, list): 

103 value = value.split(',') if value else [] 

104 return super().clean(value) 

105 

106 

107class CSVContentTypeField(CSVModelChoiceField): 

108 """ 

109 CSV field for referencing a single content type, in the form `<app>.<model>`. 

110 """ 

111 STATIC_CHOICES = True 

112 

113 def prepare_value(self, value): 

114 return object_type_identifier(value) 

115 

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')) 

127 

128 

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 

134 

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)