debian-django-import-export/import_export/widgets.py

409 lines
12 KiB
Python

# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from decimal import Decimal
from datetime import datetime, date
from django.utils import datetime_safe, timezone, six
from django.utils.encoding import smart_text, force_text
from django.utils.dateparse import parse_duration
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
import json
import ast
class Widget(object):
"""
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_text(value)
class NumberWidget(Widget):
"""
"""
def is_empty(self, value):
if isinstance(value, six.string_types):
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_text(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 ""
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):
def __init__(self, separator=None):
if separator is None:
separator = ','
self.separator = separator
super(SimpleArrayWidget, self).__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(six.text_type(v) for v in value)
class JSONWidget(Widget):
"""
Widget for a JSON object (especially required for jsonb fields in PostgreSQL database.)
"""
def clean(self, value, row=None, *args, **kwargs):
val = super(JSONWidget, self).clean(value)
if val:
return ast.literal_eval(val)
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(ForeignKeyWidget, self).__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(ForeignKeyWidget, self).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(ManyToManyWidget, self).__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_text(getattr(obj, self.field)) for obj in value.all()]
return self.separator.join(ids)