import json from datetime import date, datetime from decimal import Decimal from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.utils import datetime_safe, timezone from django.utils.dateparse import parse_duration from django.utils.encoding import force_str, smart_str class Widget: """ A Widget takes care of converting between import and export representations. This is achieved by the two methods, :meth:`~import_export.widgets.Widget.clean` and :meth:`~import_export.widgets.Widget.render`. """ def clean(self, value, row=None, *args, **kwargs): """ Returns an appropriate Python object for an imported value. For example, if you import a value from a spreadsheet, :meth:`~import_export.widgets.Widget.clean` handles conversion of this value into the corresponding Python object. Numbers or dates can be *cleaned* to their respective data types and don't have to be imported as Strings. """ return value def render(self, value, obj=None): """ Returns an export representation of a Python value. For example, if you have an object you want to export, :meth:`~import_export.widgets.Widget.render` takes care of converting the object's field to a value that can be written to a spreadsheet. """ return force_str(value) class NumberWidget(Widget): """ """ def is_empty(self, value): if isinstance(value, str): value = value.strip() # 0 is not empty return value is None or value == "" def render(self, value, obj=None): return value class FloatWidget(NumberWidget): """ Widget for converting floats fields. """ def clean(self, value, row=None, *args, **kwargs): if self.is_empty(value): return None return float(value) class IntegerWidget(NumberWidget): """ Widget for converting integer fields. """ def clean(self, value, row=None, *args, **kwargs): if self.is_empty(value): return None return int(float(value)) class DecimalWidget(NumberWidget): """ Widget for converting decimal fields. """ def clean(self, value, row=None, *args, **kwargs): if self.is_empty(value): return None return Decimal(value) class CharWidget(Widget): """ Widget for converting text fields. """ def render(self, value, obj=None): return force_str(value) class BooleanWidget(Widget): """ Widget for converting boolean fields. """ TRUE_VALUES = ["1", 1] FALSE_VALUE = "0" def render(self, value, obj=None): if value is None: return "" return self.TRUE_VALUES[0] if value else self.FALSE_VALUE def clean(self, value, row=None, *args, **kwargs): if value == "": return None return True if value in self.TRUE_VALUES else False class DateWidget(Widget): """ Widget for converting date fields. Takes optional ``format`` parameter. """ def __init__(self, format=None): if format is None: if not settings.DATE_INPUT_FORMATS: formats = ("%Y-%m-%d",) else: formats = settings.DATE_INPUT_FORMATS else: formats = (format,) self.formats = formats def clean(self, value, row=None, *args, **kwargs): if not value: return None if isinstance(value, date): return value for format in self.formats: try: return datetime.strptime(value, format).date() except (ValueError, TypeError): continue raise ValueError("Enter a valid date.") def render(self, value, obj=None): if not value: return "" try: return value.strftime(self.formats[0]) except: return datetime_safe.new_date(value).strftime(self.formats[0]) class DateTimeWidget(Widget): """ Widget for converting date fields. Takes optional ``format`` parameter. If none is set, either ``settings.DATETIME_INPUT_FORMATS`` or ``"%Y-%m-%d %H:%M:%S"`` is used. """ def __init__(self, format=None): if format is None: if not settings.DATETIME_INPUT_FORMATS: formats = ("%Y-%m-%d %H:%M:%S",) else: formats = settings.DATETIME_INPUT_FORMATS else: formats = (format,) self.formats = formats def clean(self, value, row=None, *args, **kwargs): if not value: return None if isinstance(value, datetime): return value for format in self.formats: try: dt = datetime.strptime(value, format) if settings.USE_TZ: # make datetime timezone aware so we don't compare # naive datetime to an aware one dt = timezone.make_aware(dt, timezone.get_default_timezone()) return dt except (ValueError, TypeError): continue raise ValueError("Enter a valid date/time.") def render(self, value, obj=None): if not value: return "" if settings.USE_TZ: value = timezone.localtime(value) return value.strftime(self.formats[0]) class TimeWidget(Widget): """ Widget for converting time fields. Takes optional ``format`` parameter. """ def __init__(self, format=None): if format is None: if not settings.TIME_INPUT_FORMATS: formats = ("%H:%M:%S",) else: formats = settings.TIME_INPUT_FORMATS else: formats = (format,) self.formats = formats def clean(self, value, row=None, *args, **kwargs): if not value: return None for format in self.formats: try: return datetime.strptime(value, format).time() except (ValueError, TypeError): continue raise ValueError("Enter a valid time.") def render(self, value, obj=None): if not value: return "" return value.strftime(self.formats[0]) class DurationWidget(Widget): """ Widget for converting time duration fields. """ def clean(self, value, row=None, *args, **kwargs): if not value: return None try: return parse_duration(value) except (ValueError, TypeError): raise ValueError("Enter a valid duration.") def render(self, value, obj=None): if not value: return "" return str(value) class SimpleArrayWidget(Widget): """ Widget for an Array field. Can be used for Postgres' Array field. :param separator: Defaults to ``','`` """ def __init__(self, separator=None): if separator is None: separator = ',' self.separator = separator super().__init__() def clean(self, value, row=None, *args, **kwargs): return value.split(self.separator) if value else [] def render(self, value, obj=None): return self.separator.join(str(v) for v in value) class JSONWidget(Widget): """ Widget for a JSON object (especially required for jsonb fields in PostgreSQL database.) :param value: Defaults to JSON format. The widget covers two cases: Proper JSON string with double quotes, else it tries to use single quotes and then convert it to proper JSON. """ def clean(self, value, row=None, *args, **kwargs): val = super().clean(value) if val: try: return json.loads(val) except json.decoder.JSONDecodeError: return json.loads(val.replace("'", "\"")) def render(self, value, obj=None): if value: return json.dumps(value) class ForeignKeyWidget(Widget): """ Widget for a ``ForeignKey`` field which looks up a related model using "natural keys" in both export an import. The lookup field defaults to using the primary key (``pk``) as lookup criterion but can be customised to use any field on the related model. Unlike specifying a related field in your resource like so… :: class Meta: fields = ('author__name',) …using a :class:`~import_export.widgets.ForeignKeyWidget` has the advantage that it can not only be used for exporting, but also importing data with foreign key relationships. Here's an example on how to use :class:`~import_export.widgets.ForeignKeyWidget` to lookup related objects using ``Author.name`` instead of ``Author.pk``:: from import_export import fields, resources from import_export.widgets import ForeignKeyWidget class BookResource(resources.ModelResource): author = fields.Field( column_name='author', attribute='author', widget=ForeignKeyWidget(Author, 'name')) class Meta: fields = ('author',) :param model: The Model the ForeignKey refers to (required). :param field: A field on the related model used for looking up a particular object. """ def __init__(self, model, field='pk', *args, **kwargs): self.model = model self.field = field super().__init__(*args, **kwargs) def get_queryset(self, value, row, *args, **kwargs): """ Returns a queryset of all objects for this Model. Overwrite this method if you want to limit the pool of objects from which the related object is retrieved. :param value: The field's value in the datasource. :param row: The datasource's current row. As an example; if you'd like to have ForeignKeyWidget look up a Person by their pre- **and** lastname column, you could subclass the widget like so:: class FullNameForeignKeyWidget(ForeignKeyWidget): def get_queryset(self, value, row): return self.model.objects.filter( first_name__iexact=row["first_name"], last_name__iexact=row["last_name"] ) """ return self.model.objects.all() def clean(self, value, row=None, *args, **kwargs): val = super().clean(value) if val: return self.get_queryset(value, row, *args, **kwargs).get(**{self.field: val}) else: return None def render(self, value, obj=None): if value is None: return "" attrs = self.field.split('__') for attr in attrs: try: value = getattr(value, attr, None) except (ValueError, ObjectDoesNotExist): # needs to have a primary key value before a many-to-many # relationship can be used. return None if value is None: return None return value class ManyToManyWidget(Widget): """ Widget that converts between representations of a ManyToMany relationships as a list and an actual ManyToMany field. :param model: The model the ManyToMany field refers to (required). :param separator: Defaults to ``','``. :param field: A field on the related model. Default is ``pk``. """ def __init__(self, model, separator=None, field=None, *args, **kwargs): if separator is None: separator = ',' if field is None: field = 'pk' self.model = model self.separator = separator self.field = field super().__init__(*args, **kwargs) def clean(self, value, row=None, *args, **kwargs): if not value: return self.model.objects.none() if isinstance(value, (float, int)): ids = [int(value)] else: ids = value.split(self.separator) ids = filter(None, [i.strip() for i in ids]) return self.model.objects.filter(**{ '%s__in' % self.field: ids }) def render(self, value, obj=None): ids = [smart_str(getattr(obj, self.field)) for obj in value.all()] return self.separator.join(ids)