• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

django-import-export / django-import-export / 17400817337

02 Sep 2025 10:32AM UTC coverage: 100.0%. Remained the same
17400817337

push

github

web-flow
[pre-commit.ci] pre-commit autoupdate (#2105)

updates:
- [github.com/adamchainz/django-upgrade: 1.25.0 → 1.27.0](https://github.com/adamchainz/django-upgrade/compare/1.25.0...1.27.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

2303 of 2303 relevant lines covered (100.0%)

4.98 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

100.0
/import_export/forms.py
1
import os.path
5✔
2
from collections.abc import Iterable
5✔
3
from copy import deepcopy
5✔
4
from itertools import chain
5✔
5

6
from django import forms
5✔
7
from django.conf import settings
5✔
8
from django.utils.translation import gettext_lazy as _
5✔
9

10
from .resources import ModelResource
5✔
11

12

13
class ImportExportFormBase(forms.Form):
5✔
14
    resource = forms.ChoiceField(
5✔
15
        label=_("Resource"),
16
        choices=(),
17
        required=False,
18
    )
19
    format = forms.ChoiceField(
5✔
20
        label=_("Format"),
21
        choices=(),
22
    )
23

24
    def __init__(self, formats, resources, **kwargs):
5✔
25
        super().__init__(**kwargs)
5✔
26
        self._init_resources(resources)
5✔
27
        self._init_formats(formats)
5✔
28

29
    def _init_resources(self, resources):
5✔
30
        if not resources:
5✔
31
            raise ValueError("no defined resources")
5✔
32
        self.fields["resource"].choices = [
5✔
33
            (i, resource.get_display_name()) for i, resource in enumerate(resources)
34
        ]
35
        if len(resources) == 1:
5✔
36
            self.fields["resource"].widget = forms.HiddenInput()
5✔
37
            self.initial["resource"] = "0"
5✔
38

39
    def _init_formats(self, formats):
5✔
40
        if not formats:
5✔
41
            raise ValueError("invalid formats list")
5✔
42

43
        choices = [(str(i), f().get_title()) for i, f in enumerate(formats)]
5✔
44
        if len(formats) == 1:
5✔
45
            field = self.fields["format"]
5✔
46
            field.value = formats[0]().get_title()
5✔
47
            field.initial = 0
5✔
48
            field.widget.attrs["readonly"] = True
5✔
49
        if len(formats) > 1:
5✔
50
            choices.insert(0, ("", "---"))
5✔
51

52
        self.fields["format"].choices = choices
5✔
53

54

55
class ImportForm(ImportExportFormBase):
5✔
56
    import_file = forms.FileField(label=_("File to import"))
5✔
57

58
    # field ordered for usability:
59
    # ensure that the 'file' select appears before 'format'
60
    # so that the 'guess_format' js logic makes sense
61
    field_order = ["resource", "import_file", "format"]
5✔
62

63
    def __init__(self, formats, resources, **kwargs):
5✔
64
        super().__init__(formats, resources, **kwargs)
5✔
65
        if len(formats) > 1:
5✔
66
            self.fields["import_file"].widget.attrs["class"] = "guess_format"
5✔
67
            self.fields["format"].widget.attrs["class"] = "guess_format"
5✔
68

69
    @property
5✔
70
    def media(self):
5✔
71
        media = super().media
5✔
72
        extra = "" if settings.DEBUG else ".min"
5✔
73
        return media + forms.Media(
5✔
74
            js=(
75
                f"admin/js/vendor/jquery/jquery{extra}.js",
76
                "admin/js/jquery.init.js",
77
                "import_export/guess_format.js",
78
            )
79
        )
80

81

82
class ConfirmImportForm(forms.Form):
5✔
83
    import_file_name = forms.CharField(widget=forms.HiddenInput())
5✔
84
    original_file_name = forms.CharField(widget=forms.HiddenInput())
5✔
85
    format = forms.CharField(widget=forms.HiddenInput())
5✔
86
    resource = forms.CharField(widget=forms.HiddenInput(), required=False)
5✔
87

88
    def clean_import_file_name(self):
5✔
89
        data = self.cleaned_data["import_file_name"]
5✔
90
        data = os.path.basename(data)
5✔
91
        return data
5✔
92

93

94
class ExportForm(ImportExportFormBase):
5✔
95
    export_items = forms.MultipleChoiceField(
5✔
96
        widget=forms.MultipleHiddenInput(), required=False
97
    )
98

99

100
class SelectableFieldsExportForm(ExportForm):
5✔
101
    def __init__(self, formats, resources, **kwargs):
5✔
102
        super().__init__(formats, resources, **kwargs)
5✔
103
        self._init_selectable_fields(resources)
5✔
104

105
    @property
5✔
106
    def media(self):
5✔
107
        media = super().media
5✔
108
        return media + forms.Media(
5✔
109
            js=("import_export/export_selectable_fields.js",),
110
            css={
111
                "all": ["import_export/export.css"],
112
            },
113
        )
114

115
    def _init_selectable_fields(self, resources: Iterable[ModelResource]) -> None:
5✔
116
        """
117
        Create `BooleanField(s)` for resource fields
118
        """
119
        self.resources = resources
5✔
120
        self.is_selectable_fields_form = True
5✔
121
        self.resource_fields = {resource.__name__: [] for resource in resources}
5✔
122

123
        for index, resource in enumerate(resources):
5✔
124
            boolean_fields = self._create_boolean_fields(resource, index)
5✔
125
            self.resource_fields[resource.__name__] = boolean_fields
5✔
126

127
        # Order fields by resource select then boolean fields
128
        ordered_fields = [
5✔
129
            "resource",
130
            # flatten resource fields lists
131
            *chain(*self.resource_fields.values()),
132
        ]
133
        self.order_fields(ordered_fields)
5✔
134

135
    def _get_field_label(self, resource: ModelResource, field_name: str) -> str:
5✔
136
        title = field_name.replace("_", " ").title()
5✔
137
        field = resource.fields.get(field_name)
5✔
138
        if field and field.column_name != field_name:
5✔
139
            title = f"{title} ({field.column_name})"
5✔
140
        return title
5✔
141

142
    def _create_boolean_fields(self, resource: ModelResource, index: int) -> None:
5✔
143
        # Initiate resource to get ordered export fields
144
        fields = resource().get_export_order()
5✔
145
        boolean_fields = []  # will be used for ordering the fields
5✔
146
        is_initial_field = False
5✔
147

148
        for field in fields:
5✔
149
            field_name = self.create_boolean_field_name(resource, field)
5✔
150
            boolean_field = forms.BooleanField(
5✔
151
                label=self._get_field_label(resource, field),
152
                label_suffix="",
153
                initial=True,
154
                required=False,
155
            )
156

157
            # These attributes will be used for rendering in template
158
            boolean_field.is_selectable_field = True
5✔
159
            boolean_field.resource_name = resource.__name__
5✔
160
            boolean_field.resource_index = index
5✔
161
            boolean_field.widget.attrs["resource-id"] = index
5✔
162
            if is_initial_field is False:
5✔
163
                boolean_field.initial_field = is_initial_field = True
5✔
164

165
            self.fields[field_name] = boolean_field
5✔
166
            boolean_fields.append(field_name)
5✔
167

168
        return boolean_fields
5✔
169

170
    @staticmethod
5✔
171
    def create_boolean_field_name(resource: ModelResource, field_name: str) -> str:
5✔
172
        """
173
        Create field name by combining `resource_name` + `field_name` to prevent
174
        conflict between resource fields with same name
175

176
        Example:
177
            BookResource            +   name -> bookresource_name
178
            BookResourceWithNames   +   name -> bookresourcewithnames_name
179
        """
180
        return resource.__name__.lower() + "_" + field_name
5✔
181

182
    def clean(self):
5✔
183
        selected_resource = self.get_selected_resource()
5✔
184

185
        if selected_resource:
5✔
186
            # Remove fields for not selected resources
187
            self._remove_unselected_resource_fields(selected_resource)
5✔
188
            # Normalize resource field names
189
            self._normalize_resource_fields(selected_resource)
5✔
190
            # Validate at least one field is selected for selected resource
191
            self._validate_any_field_selected(selected_resource)
5✔
192

193
        return self.cleaned_data
5✔
194

195
    def _remove_unselected_resource_fields(
5✔
196
        self, selected_resource: ModelResource
197
    ) -> None:
198
        """
199
        Remove boolean fields except the fields for selected resource
200
        """
201
        _cleaned_data = deepcopy(self.cleaned_data)
5✔
202

203
        for resource_name, fields in self.resource_fields.items():
5✔
204
            if selected_resource.__name__ == resource_name:
5✔
205
                # Skip selected resource
206
                continue
5✔
207

208
            for field in fields:
5✔
209
                del _cleaned_data[field]
5✔
210

211
        self.cleaned_data = _cleaned_data
5✔
212

213
    def get_selected_resource(self):
5✔
214
        if not getattr(self, "cleaned_data", None):
5✔
215
            raise forms.ValidationError(
5✔
216
                _("Form is not validated, call `is_valid` first")
217
            )
218

219
        # Return selected resource by index
220
        resource_index = 0
5✔
221
        if "resource" in self.cleaned_data:
5✔
222
            try:
5✔
223
                resource_index = int(self.cleaned_data["resource"])
5✔
224
            except ValueError:
5✔
225
                pass
5✔
226
        return self.resources[resource_index]
5✔
227

228
    def _normalize_resource_fields(self, selected_resource: ModelResource) -> None:
5✔
229
        """
230
        Field names are combination of resource_name + field_name,
231
        normalize field names by removing resource name
232
        """
233
        selected_resource_name = selected_resource.__name__.lower() + "_"
5✔
234
        _cleaned_data = {}
5✔
235
        self._selected_resource_fields = []
5✔
236

237
        for k, v in self.cleaned_data.items():
5✔
238
            if selected_resource_name in k:
5✔
239
                field_name = k.replace(selected_resource_name, "")
5✔
240
                _cleaned_data[field_name] = v
5✔
241
                if v is True:
5✔
242
                    # Add to _selected_resource_fields to determine what
243
                    # fields were selected for export
244
                    self._selected_resource_fields.append(field_name)
5✔
245
                continue
5✔
246
            _cleaned_data[k] = v
5✔
247

248
        self.cleaned_data = _cleaned_data
5✔
249

250
    def get_selected_resource_export_fields(self):
5✔
251
        selected_resource = self.get_selected_resource()
5✔
252
        # Initialize resource to use `get_export_order` method
253
        resource_fields = selected_resource().get_export_order()
5✔
254
        return [
5✔
255
            field
256
            for field, value in self.cleaned_data.items()
257
            if value is True and field in resource_fields
258
        ]
259

260
    def _validate_any_field_selected(self, resource) -> None:
5✔
261
        """
262
        Validate if any field for resource was selected in form data
263
        """
264
        resource_fields = list(resource().get_export_order())
5✔
265

266
        if not any(v for k, v in self.cleaned_data.items() if k in resource_fields):
5✔
267
            raise forms.ValidationError(
5✔
268
                _("""Select at least 1 field for "%(resource_name)s" to export"""),
269
                code="invalid",
270
                params={
271
                    "resource_name": resource.get_display_name(),
272
                },
273
            )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc