Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/import_export/widgets.py: 35%
218 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1import json
2from datetime import date, datetime, time
3from decimal import Decimal
5import django
6from django.conf import settings
7from django.core.exceptions import ObjectDoesNotExist
8from django.utils import timezone
9from django.utils.dateparse import parse_duration
10from django.utils.encoding import force_str, smart_str
11from django.utils.formats import number_format
14def format_datetime(value, datetime_format):
15 # conditional logic to handle correct formatting of dates
16 # see https://code.djangoproject.com/ticket/32738
17 if django.VERSION[0] >= 4:
18 format = django.utils.formats.sanitize_strftime_format(datetime_format)
19 return value.strftime(format)
20 else:
21 return django.utils.datetime_safe.new_datetime(value).strftime(datetime_format)
24class Widget:
25 """
26 A Widget takes care of converting between import and export representations.
28 This is achieved by the two methods,
29 :meth:`~import_export.widgets.Widget.clean` and
30 :meth:`~import_export.widgets.Widget.render`.
31 """
32 def clean(self, value, row=None, **kwargs):
33 """
34 Returns an appropriate Python object for an imported value.
36 For example, if you import a value from a spreadsheet,
37 :meth:`~import_export.widgets.Widget.clean` handles conversion
38 of this value into the corresponding Python object.
40 Numbers or dates can be *cleaned* to their respective data types and
41 don't have to be imported as Strings.
42 """
43 return value
45 def render(self, value, obj=None):
46 """
47 Returns an export representation of a Python value.
49 For example, if you have an object you want to export,
50 :meth:`~import_export.widgets.Widget.render` takes care of converting
51 the object's field to a value that can be written to a spreadsheet.
52 """
53 return force_str(value)
56class NumberWidget(Widget):
57 """
58 Takes optional ``coerce_to_string`` parameter, set to ``True`` the
59 :meth:`~import_export.widgets.Widget.render` method will return a string
60 else it will return a value.
61 """
63 def __init__(self, coerce_to_string=False):
64 self.coerce_to_string = coerce_to_string
66 def is_empty(self, value):
67 if isinstance(value, str):
68 value = value.strip()
69 # 0 is not empty
70 return value is None or value == ""
72 def render(self, value, obj=None):
73 return number_format(value) if self.coerce_to_string else value
76class FloatWidget(NumberWidget):
77 """
78 Widget for converting floats fields.
79 """
81 def clean(self, value, row=None, **kwargs):
82 if self.is_empty(value):
83 return None
84 return float(value)
87class IntegerWidget(NumberWidget):
88 """
89 Widget for converting integer fields.
90 """
92 def clean(self, value, row=None, **kwargs):
93 if self.is_empty(value):
94 return None
95 return int(Decimal(value))
98class DecimalWidget(NumberWidget):
99 """
100 Widget for converting decimal fields.
101 """
103 def clean(self, value, row=None, **kwargs):
104 if self.is_empty(value):
105 return None
106 return Decimal(force_str(value))
109class CharWidget(Widget):
110 """
111 Widget for converting text fields.
112 """
113 pass
116class BooleanWidget(Widget):
117 """
118 Widget for converting boolean fields.
120 The widget assumes that ``True``, ``False``, and ``None`` are all valid
121 values, as to match Django's `BooleanField
122 <https://docs.djangoproject.com/en/dev/ref/models/fields/#booleanfield>`_.
123 That said, whether the database/Django will actually accept NULL values
124 will depend on if you have set ``null=True`` on that Django field.
126 While the BooleanWidget is set up to accept as input common variations of
127 "True" and "False" (and "None"), you may need to munge less common values
128 to ``True``/``False``/``None``. Probably the easiest way to do this is to
129 override the :func:`~import_export.resources.Resource.before_import_row`
130 function of your Resource class. A short example::
132 from import_export import fields, resources, widgets
134 class BooleanExample(resources.ModelResource):
135 warn = fields.Field(widget=widgets.BooleanWidget())
137 def before_import_row(self, row, row_number=None, **kwargs):
138 if "warn" in row.keys():
139 # munge "warn" to "True"
140 if row["warn"] in ["warn", "WARN"]:
141 row["warn"] = True
143 return super().before_import_row(row, row_number, **kwargs)
144 """
145 TRUE_VALUES = ["1", 1, True, "true", "TRUE", "True"]
146 FALSE_VALUES = ["0", 0, False, "false", "FALSE", "False"]
147 NULL_VALUES = ["", None, "null", "NULL", "none", "NONE", "None"]
149 def render(self, value, obj=None):
150 """
151 On export, ``True`` is represented as ``1``, ``False`` as ``0``, and
152 ``None``/NULL as a empty string.
154 Note that these values are also used on the import confirmation view.
155 """
156 if value in self.NULL_VALUES:
157 return ""
158 return self.TRUE_VALUES[0] if value else self.FALSE_VALUES[0]
160 def clean(self, value, row=None, **kwargs):
161 if value in self.NULL_VALUES:
162 return None
163 return True if value in self.TRUE_VALUES else False
166class DateWidget(Widget):
167 """
168 Widget for converting date fields.
170 Takes optional ``format`` parameter. If none is set, either
171 ``settings.DATE_INPUT_FORMATS`` or ``"%Y-%m-%d"`` is used.
172 """
174 def __init__(self, format=None):
175 if format is None: 175 ↛ 181line 175 didn't jump to line 181, because the condition on line 175 was never false
176 if not settings.DATE_INPUT_FORMATS: 176 ↛ 177line 176 didn't jump to line 177, because the condition on line 176 was never true
177 formats = ("%Y-%m-%d",)
178 else:
179 formats = settings.DATE_INPUT_FORMATS
180 else:
181 formats = (format,)
182 self.formats = formats
184 def clean(self, value, row=None, **kwargs):
185 if not value:
186 return None
187 if isinstance(value, date):
188 return value
189 for format in self.formats:
190 try:
191 return datetime.strptime(value, format).date()
192 except (ValueError, TypeError):
193 continue
194 raise ValueError("Enter a valid date.")
196 def render(self, value, obj=None):
197 if not value:
198 return ""
199 return format_datetime(value, self.formats[0])
202class DateTimeWidget(Widget):
203 """
204 Widget for converting date fields.
206 Takes optional ``format`` parameter. If none is set, either
207 ``settings.DATETIME_INPUT_FORMATS`` or ``"%Y-%m-%d %H:%M:%S"`` is used.
208 """
210 def __init__(self, format=None):
211 if format is None: 211 ↛ 217line 211 didn't jump to line 217, because the condition on line 211 was never false
212 if not settings.DATETIME_INPUT_FORMATS: 212 ↛ 213line 212 didn't jump to line 213, because the condition on line 212 was never true
213 formats = ("%Y-%m-%d %H:%M:%S",)
214 else:
215 formats = settings.DATETIME_INPUT_FORMATS
216 else:
217 formats = (format,)
218 self.formats = formats
220 def clean(self, value, row=None, **kwargs):
221 dt = None
222 if not value:
223 return None
224 if isinstance(value, datetime):
225 dt = value
226 else:
227 for format_ in self.formats:
228 try:
229 dt = datetime.strptime(value, format_)
230 except (ValueError, TypeError):
231 continue
232 if dt:
233 if settings.USE_TZ and timezone.is_naive(dt):
234 dt = timezone.make_aware(dt)
235 return dt
236 raise ValueError("Enter a valid date/time.")
238 def render(self, value, obj=None):
239 if not value:
240 return ""
241 if settings.USE_TZ:
242 value = timezone.localtime(value)
243 return format_datetime(value, self.formats[0])
246class TimeWidget(Widget):
247 """
248 Widget for converting time fields.
250 Takes optional ``format`` parameter. If none is set, either
251 ``settings.DATETIME_INPUT_FORMATS`` or ``"%H:%M:%S"`` is used.
252 """
254 def __init__(self, format=None):
255 if format is None:
256 if not settings.TIME_INPUT_FORMATS:
257 formats = ("%H:%M:%S",)
258 else:
259 formats = settings.TIME_INPUT_FORMATS
260 else:
261 formats = (format,)
262 self.formats = formats
264 def clean(self, value, row=None, **kwargs):
265 if not value:
266 return None
267 if isinstance(value, time):
268 return value
269 for format in self.formats:
270 try:
271 return datetime.strptime(value, format).time()
272 except (ValueError, TypeError):
273 continue
274 raise ValueError("Enter a valid time.")
276 def render(self, value, obj=None):
277 if not value:
278 return ""
279 return value.strftime(self.formats[0])
282class DurationWidget(Widget):
283 """
284 Widget for converting time duration fields.
285 """
287 def clean(self, value, row=None, **kwargs):
288 if not value:
289 return None
291 try:
292 return parse_duration(value)
293 except (ValueError, TypeError):
294 raise ValueError("Enter a valid duration.")
296 def render(self, value, obj=None):
297 if value is None:
298 return ""
299 return str(value)
302class SimpleArrayWidget(Widget):
303 """
304 Widget for an Array field. Can be used for Postgres' Array field.
306 :param separator: Defaults to ``','``
307 """
309 def __init__(self, separator=None):
310 if separator is None:
311 separator = ','
312 self.separator = separator
313 super().__init__()
315 def clean(self, value, row=None, **kwargs):
316 return value.split(self.separator) if value else []
318 def render(self, value, obj=None):
319 return self.separator.join(str(v) for v in value)
322class JSONWidget(Widget):
323 """
324 Widget for a JSON object (especially required for jsonb fields in PostgreSQL database.)
326 :param value: Defaults to JSON format.
327 The widget covers two cases: Proper JSON string with double quotes, else it
328 tries to use single quotes and then convert it to proper JSON.
329 """
331 def clean(self, value, row=None, **kwargs):
332 val = super().clean(value)
333 if val:
334 try:
335 return json.loads(val)
336 except json.decoder.JSONDecodeError:
337 return json.loads(val.replace("'", "\""))
339 def render(self, value, obj=None):
340 if value:
341 return json.dumps(value)
344class ForeignKeyWidget(Widget):
345 """
346 Widget for a ``ForeignKey`` field which looks up a related model using
347 either the PK or a user specified field that uniquely identifies the
348 instance in both export and import.
350 The lookup field defaults to using the primary key (``pk``) as lookup
351 criterion but can be customized to use any field on the related model.
353 Unlike specifying a related field in your resource like so…
355 ::
357 class Meta:
358 fields = ('author__name',)
360 …using a :class:`~import_export.widgets.ForeignKeyWidget` has the
361 advantage that it can not only be used for exporting, but also importing
362 data with foreign key relationships.
364 Here's an example on how to use
365 :class:`~import_export.widgets.ForeignKeyWidget` to lookup related objects
366 using ``Author.name`` instead of ``Author.pk``::
368 from import_export import fields, resources
369 from import_export.widgets import ForeignKeyWidget
371 class BookResource(resources.ModelResource):
372 author = fields.Field(
373 column_name='author',
374 attribute='author',
375 widget=ForeignKeyWidget(Author, 'name'))
377 class Meta:
378 fields = ('author',)
380 :param model: The Model the ForeignKey refers to (required).
381 :param field: A field on the related model used for looking up a particular
382 object.
383 :param use_natural_foreign_keys: Use natural key functions to identify
384 related object, default to False
385 """
386 def __init__(self, model, field='pk', use_natural_foreign_keys=False, **kwargs):
387 self.model = model
388 self.field = field
389 self.use_natural_foreign_keys = use_natural_foreign_keys
390 super().__init__(**kwargs)
392 def get_queryset(self, value, row, *args, **kwargs):
393 """
394 Returns a queryset of all objects for this Model.
396 Overwrite this method if you want to limit the pool of objects from
397 which the related object is retrieved.
399 :param value: The field's value in the datasource.
400 :param row: The datasource's current row.
402 As an example; if you'd like to have ForeignKeyWidget look up a Person
403 by their pre- **and** lastname column, you could subclass the widget
404 like so::
406 class FullNameForeignKeyWidget(ForeignKeyWidget):
407 def get_queryset(self, value, row, *args, **kwargs):
408 return self.model.objects.filter(
409 first_name__iexact=row["first_name"],
410 last_name__iexact=row["last_name"]
411 )
412 """
413 return self.model.objects.all()
415 def clean(self, value, row=None, **kwargs):
416 val = super().clean(value)
417 if val:
418 if self.use_natural_foreign_keys:
419 # natural keys will always be a tuple, which ends up as a json list.
420 value = json.loads(value)
421 return self.model.objects.get_by_natural_key(*value)
422 else:
423 return self.get_queryset(value, row, **kwargs).get(**{self.field: val})
424 else:
425 return None
427 def render(self, value, obj=None):
428 if value is None:
429 return ""
431 attrs = self.field.split('__')
432 for attr in attrs:
433 try:
434 if self.use_natural_foreign_keys:
435 # inbound natural keys must be a json list.
436 return json.dumps(value.natural_key())
437 else:
438 value = getattr(value, attr, None)
439 except (ValueError, ObjectDoesNotExist):
440 # needs to have a primary key value before a many-to-many
441 # relationship can be used.
442 return None
443 if value is None:
444 return None
446 return value
449class ManyToManyWidget(Widget):
450 """
451 Widget that converts between representations of a ManyToMany relationships
452 as a list and an actual ManyToMany field.
454 :param model: The model the ManyToMany field refers to (required).
455 :param separator: Defaults to ``','``.
456 :param field: A field on the related model. Default is ``pk``.
457 """
459 def __init__(self, model, separator=None, field=None, **kwargs):
460 if separator is None: 460 ↛ 462line 460 didn't jump to line 462, because the condition on line 460 was never false
461 separator = ','
462 if field is None: 462 ↛ 464line 462 didn't jump to line 464, because the condition on line 462 was never false
463 field = 'pk'
464 self.model = model
465 self.separator = separator
466 self.field = field
467 super().__init__(**kwargs)
469 def clean(self, value, row=None, **kwargs):
470 if not value:
471 return self.model.objects.none()
472 if isinstance(value, (float, int)):
473 ids = [int(value)]
474 else:
475 ids = value.split(self.separator)
476 ids = filter(None, [i.strip() for i in ids])
477 return self.model.objects.filter(**{
478 '%s__in' % self.field: ids
479 })
481 def render(self, value, obj=None):
482 ids = [smart_str(getattr(obj, self.field)) for obj in value.all()]
483 return self.separator.join(ids)