summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBenjamin Dauvergne <bdauvergne@entrouvert.com>2015-05-27 13:40:45 (GMT)
committerBenjamin Dauvergne <bdauvergne@entrouvert.com>2015-05-27 13:40:45 (GMT)
commitf2c55bd74761feacccee9c420a14cceafdd9ae39 (patch)
tree847badacdda69ea9b8f65d76b0533a7785b3e8e3
parent88f26c290b9758996a342d27c044044db7be3f14 (diff)
parent63024a5d6d9ebdf42586aafd8bb51d752472c6e0 (diff)
downloaddjango-import-export-f2c55bd74761feacccee9c420a14cceafdd9ae39.zip
django-import-export-f2c55bd74761feacccee9c420a14cceafdd9ae39.tar.gz
django-import-export-f2c55bd74761feacccee9c420a14cceafdd9ae39.tar.bz2
Merge tag '0.2.7'
Tagging 0.2.7
-rw-r--r--.gitignore1
-rw-r--r--.travis.yml4
-rw-r--r--AUTHORS16
-rw-r--r--docs/changelog.rst31
-rw-r--r--docs/getting_started.rst32
-rw-r--r--docs/import_workflow.rst3
-rw-r--r--docs/installation.rst2
-rw-r--r--docs/settings.rst8
-rw-r--r--import_export/__init__.py2
-rw-r--r--import_export/admin.py70
-rw-r--r--import_export/django_compat.py23
-rw-r--r--import_export/formats/base_formats.py16
-rw-r--r--import_export/locale/es/LC_MESSAGES/django.mobin0 -> 1630 bytes
-rw-r--r--import_export/locale/es/LC_MESSAGES/django.po107
-rw-r--r--import_export/locale/pl/LC_MESSAGES/django.mobin0 -> 1646 bytes
-rw-r--r--import_export/locale/pl/LC_MESSAGES/django.po107
-rw-r--r--import_export/locale/ru/LC_MESSAGES/django.mobin0 -> 1962 bytes
-rw-r--r--import_export/locale/ru/LC_MESSAGES/django.po108
-rw-r--r--import_export/locale/sk/LC_MESSAGES/django.mobin0 -> 1635 bytes
-rw-r--r--import_export/locale/sk/LC_MESSAGES/django.po107
-rw-r--r--import_export/locale/zh_Hans/LC_MESSAGES/django.mobin0 -> 1483 bytes
-rw-r--r--import_export/locale/zh_Hans/LC_MESSAGES/django.po105
-rw-r--r--import_export/resources.py110
-rw-r--r--import_export/templates/admin/import_export/import.html10
-rw-r--r--import_export/widgets.py18
-rw-r--r--tests/core/models.py1
-rw-r--r--tests/core/tests/admin_integration_tests.py10
-rw-r--r--tests/core/tests/resources_tests.py59
-rw-r--r--tests/core/tests/widgets_tests.py23
-rw-r--r--tox.ini25
30 files changed, 907 insertions, 91 deletions
diff --git a/.gitignore b/.gitignore
index 31f5de8..153b2d9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ build/
dist/
*.egg-info/
.tox/
+.idea/ \ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
index 72e054d..e4c173b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,14 +2,16 @@ language: python
python:
- "2.7"
- "3.3"
+ - "3.4"
env:
- DJANGO=1.4.10
- DJANGO=1.5.5
- DJANGO=1.6.1
- DJANGO=1.7.0
+ - DJANGO=1.8
install:
- pip install -q Django==$DJANGO --use-mirrors
- pip install -e git+https://github.com/kennethreitz/tablib.git#egg=tablib
- pip install -r requirements/base.txt --use-mirrors
script:
- - if [[ $TRAVIS_PYTHON_VERSION != '3.3' || $DJANGO != "1.4.10" ]]; then python tests/manage.py test core --settings=settings; fi
+ - if [[ ($TRAVIS_PYTHON_VERSION != '3.3' && $TRAVIS_PYTHON_VERSION != '3.4') || $DJANGO != "1.4.10" ]]; then python tests/manage.py test core --settings=settings; fi
diff --git a/AUTHORS b/AUTHORS
index 449680a..f18f4df 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -25,6 +25,22 @@ The following is a list of much appreciated contributors:
* frank-u (Oleksandr Poliatykin)
* Kobold (Andy Kish)
* cherepski
+* ludwiktrammer (Ludwik Trammer)
* cuchac
* aidanlister (Aidan Lister)
* tabac
+* GroSte
+* w0rp
+* nastyako (Nastya Konak)
+* ianwalter (Ian Walter)
+* amarandon (Alex Marandon)
+* caljess599
+* mightygraf (Denis K)
+* spookylukey (Luke Plant)
+* kane-c
+* ddiazpinto (David Díaz)
+* jeberger (Jérôme M. Berger)
+* schemacs (Lele Long)
+* jbub (Juraj Bubniak)
+* manelclos (Manel Clos)
+* pkozlov (Pavel Kozlov)
diff --git a/docs/changelog.rst b/docs/changelog.rst
index dcf9e1a..d0f0b38 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,9 +1,38 @@
Changelog for django-import-export
==================================
-0.2.5 (unreleased)
+0.2.7 (2015-05-04)
------------------
+- Django 1.8 compatibility
+
+- add attribute inheritance to Resource (#140)
+
+- make the filename and user available to import_data (#237)
+
+- Add to_encoding functionality (#244)
+
+- Call before_import before creating the instance_loader - fixes #193
+
+
+0.2.6 (2014-10-09)
+------------------
+
+- added use of get_diff_headers method into import.html template (#158)
+
+- Try to use OrderedDict instead of SortedDict, which is deprecated in
+ Django 1.7 (#157)
+
+- fixed #105 unicode import
+
+- remove invalid form action "form_url" #154
+
+
+0.2.5 (2014-10-04)
+------------------
+
+- Do not convert numeric types to string (#149)
+
- implement export as an admin action (#124)
diff --git a/docs/getting_started.rst b/docs/getting_started.rst
index 90dfbd5..71fd8a6 100644
--- a/docs/getting_started.rst
+++ b/docs/getting_started.rst
@@ -2,7 +2,7 @@
Getting started
===============
-For example purposes, we'll be use simplified book app, here is our
+For example purposes, we'll use a simplified book app. Here is our
``core.models.py``::
class Author(models.Model):
@@ -39,7 +39,7 @@ Creating import-export resource
-------------------------------
To integrate `django-import-export` with ``Book`` model, we will create
-a resource class that will describe how this resource can be imported or
+a resource class in ``admin.py`` that will describe how this resource can be imported or
exported.
::
@@ -71,8 +71,15 @@ By default ``ModelResource`` introspects model fields and creates
for each field.
To affect which model fields will be included in an import-export
-resource, use the ``fields`` option to whitelist fields or ``exclude``
-option to blacklist fields::
+resource, use the ``fields`` option to whitelist fields::
+
+ class BookResource(resources.ModelResource):
+
+ class Meta:
+ model = Book
+ fields = ('id', 'name', 'price',)
+
+Or the ``exclude`` option to blacklist fields::
class BookResource(resources.ModelResource):
@@ -80,6 +87,15 @@ option to blacklist fields::
model = Book
exclude = ('imported', )
+An explicit order for exporting fields can be set using the ``export_order`` option::
+
+ class BookResource(resources.ModelResource):
+
+ class Meta:
+ model = Book
+ fields = ('id', 'name', 'author', 'price',)
+ export_order = ('id', 'price', 'author', 'name')
+
When defining ``ModelResource`` fields it is possible to follow
model relationships::
@@ -102,7 +118,7 @@ model relationships::
Declaring fields
----------------
-It is possible to override a resource field to change some of it's
+It is possible to override a resource field to change some of its
options::
from import_export import fields
@@ -113,7 +129,7 @@ options::
class Meta:
model = Book
-Other fields that are not existing in the target model may be added::
+Other fields that don't exist in the target model may be added::
from import_export import fields
@@ -145,7 +161,7 @@ data structure, ``dehydrate_<fieldname>`` method should be defined::
model = Book
def dehydrate_full_title(self, book):
- return '%s by %s' % (book.name, book.name.author)
+ return '%s by %s' % (book.name, book.author.name)
Customize widgets
@@ -226,7 +242,7 @@ that have column ``delete`` set to ``1``.
Admin integration
-----------------
-Admin integration is achived by subclassing
+Admin integration is achived by subclassing (in ``admin.py``)
``ImportExportModelAdmin`` or one of the available mixins (``ImportMixin``,
``ExportMixin``, or ``ImportExportMixin``)::
diff --git a/docs/import_workflow.rst b/docs/import_workflow.rst
index 5415742..40f3b9f 100644
--- a/docs/import_workflow.rst
+++ b/docs/import_workflow.rst
@@ -46,7 +46,8 @@ responsible for import data from given `dataset`.
#. ``import_data`` calls the ``before_import`` hook method which by default does
not do anything but can be overriden to customize the import process. The
- method receives the ``dataset`` and ``dry_run`` arguments.
+ method receives the ``dataset`` and ``dry_run`` arguments as well as any
+ additional keyword arguments passed to ``import_data`` in a ``kwargs`` dict.
#. Process each `row` in ``dataset``
diff --git a/docs/installation.rst b/docs/installation.rst
index 3f2288a..8c16989 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -7,4 +7,4 @@ so it can be installed with standard Python tools like pip or easy_install:
::
- $pip install django-import-export
+ $ pip install django-import-export
diff --git a/docs/settings.rst b/docs/settings.rst
index 0ca1b1c..d3ef3b8 100644
--- a/docs/settings.rst
+++ b/docs/settings.rst
@@ -5,3 +5,11 @@ Settings
``IMPORT_EXPORT_USE_TRANSACTIONS``
Global setting controls if resource importing should use database
transactions. Default is ``False``.
+
+``IMPORT_EXPORT_SKIP_ADMIN_LOG``
+ Global setting controls if creating log entries for
+ the admin changelist should be skipped when importing resource.
+ The skip_admin_log attribute of `ImportMixin` is checked first,
+ which defaults to None. If not found, this global option is used.
+ This will speed up importing large datasets, but will lose
+ changing logs in the admin changelist view. Default is ``False``.
diff --git a/import_export/__init__.py b/import_export/__init__.py
index 8c77aec..407b8a2 100644
--- a/import_export/__init__.py
+++ b/import_export/__init__.py
@@ -1 +1 @@
-__version__ = '0.2.5.dev0'
+__version__ = '0.2.7'
diff --git a/import_export/admin.py b/import_export/admin.py
index 0fdffb6..9812d1a 100644
--- a/import_export/admin.py
+++ b/import_export/admin.py
@@ -4,6 +4,7 @@ import tempfile
from datetime import datetime
import os.path
+import django
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from django.conf.urls import patterns, url
@@ -13,6 +14,7 @@ from django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION
from django.contrib.contenttypes.models import ContentType
from django.http import HttpResponseRedirect, HttpResponse
from django.core.urlresolvers import reverse
+from django.conf import settings
from .forms import (
ImportForm,
@@ -31,6 +33,7 @@ try:
except ImportError:
from django.utils.encoding import force_unicode as force_text
+SKIP_ADMIN_LOG = getattr(settings, 'IMPORT_EXPORT_SKIP_ADMIN_LOG', False)
#: import / export formats
DEFAULT_FORMATS = (
@@ -69,6 +72,13 @@ class ImportMixin(ImportExportMixinBase):
formats = DEFAULT_FORMATS
#: import data encoding
from_encoding = "utf-8"
+ skip_admin_log = None
+
+ def get_skip_admin_log(self):
+ if self.skip_admin_log is None:
+ return SKIP_ADMIN_LOG
+ else:
+ return self.skip_admin_log
def get_urls(self):
urls = super(ImportMixin, self).get_urls()
@@ -127,25 +137,28 @@ class ImportMixin(ImportExportMixinBase):
dataset = input_format.create_dataset(data)
result = resource.import_data(dataset, dry_run=False,
- raise_errors=True)
-
- # Add imported objects to LogEntry
- logentry_map = {
- RowResult.IMPORT_TYPE_NEW: ADDITION,
- RowResult.IMPORT_TYPE_UPDATE: CHANGE,
- RowResult.IMPORT_TYPE_DELETE: DELETION,
- }
- content_type_id=ContentType.objects.get_for_model(self.model).pk
- for row in result:
- if row.import_type != row.IMPORT_TYPE_SKIP:
- LogEntry.objects.log_action(
- user_id=request.user.pk,
- content_type_id=content_type_id,
- object_id=row.object_id,
- object_repr=row.object_repr,
- action_flag=logentry_map[row.import_type],
- change_message="%s through import_export" % row.import_type,
- )
+ raise_errors=True,
+ file_name=import_file_name,
+ user=request.user)
+
+ if not self.get_skip_admin_log():
+ # Add imported objects to LogEntry
+ logentry_map = {
+ RowResult.IMPORT_TYPE_NEW: ADDITION,
+ RowResult.IMPORT_TYPE_UPDATE: CHANGE,
+ RowResult.IMPORT_TYPE_DELETE: DELETION,
+ }
+ content_type_id = ContentType.objects.get_for_model(self.model).pk
+ for row in result:
+ if row.import_type != row.IMPORT_TYPE_SKIP:
+ LogEntry.objects.log_action(
+ user_id=request.user.pk,
+ content_type_id=content_type_id,
+ object_id=row.object_id,
+ object_repr=row.object_repr,
+ action_flag=logentry_map[row.import_type],
+ change_message="%s through import_export" % row.import_type,
+ )
success_message = _('Import finished')
messages.success(request, success_message)
@@ -191,7 +204,9 @@ class ImportMixin(ImportExportMixinBase):
data = force_text(data, self.from_encoding)
dataset = input_format.create_dataset(data)
result = resource.import_data(dataset, dry_run=True,
- raise_errors=False)
+ raise_errors=False,
+ file_name=uploaded_file.name,
+ user=request.user)
context['result'] = result
@@ -201,6 +216,11 @@ class ImportMixin(ImportExportMixinBase):
'input_format': form.cleaned_data['input_format'],
})
+ if django.VERSION >= (1, 8, 0):
+ context.update(self.admin_site.each_context(request))
+ elif django.VERSION >= (1, 7, 0):
+ context.update(self.admin_site.each_context())
+
context['form'] = form
context['opts'] = self.model._meta
context['fields'] = [f.column_name for f in resource.get_fields()]
@@ -302,7 +322,7 @@ class ExportMixin(ImportExportMixinBase):
queryset = self.get_export_queryset(request)
export_data = self.get_export_data(file_format, queryset)
- content_type = 'application/octet-stream'
+ content_type = file_format.get_content_type()
# Django 1.7 uses the content_type kwarg instead of mimetype
try:
response = HttpResponse(export_data, content_type=content_type)
@@ -314,6 +334,12 @@ class ExportMixin(ImportExportMixinBase):
return response
context = {}
+
+ if django.VERSION >= (1, 8, 0):
+ context.update(self.admin_site.each_context(request))
+ elif django.VERSION >= (1, 7, 0):
+ context.update(self.admin_site.each_context())
+
context['form'] = form
context['opts'] = self.model._meta
return TemplateResponse(request, [self.export_template_name],
@@ -371,7 +397,7 @@ class ExportActionModelAdmin(ExportMixin, admin.ModelAdmin):
file_format = formats[int(export_format)]()
export_data = self.get_export_data(file_format, queryset)
- content_type = 'application/octet-stream'
+ content_type = file_format.get_content_type()
# Django 1.7 uses the content_type kwarg instead of mimetype
try:
response = HttpResponse(export_data, content_type=content_type)
diff --git a/import_export/django_compat.py b/import_export/django_compat.py
new file mode 100644
index 0000000..c5d1ca4
--- /dev/null
+++ b/import_export/django_compat.py
@@ -0,0 +1,23 @@
+from __future__ import unicode_literals
+
+from django.db import transaction
+
+# transaction management for Django < 1.6
+
+def atomic(*args, **kw):
+ def noop_decorator(func):
+ return func # pass through
+
+ return noop_decorator
+
+def savepoint(*args, **kwargs):
+ transaction.enter_transaction_management()
+ transaction.managed(True)
+
+def savepoint_rollback(*args, **kwargs):
+ transaction.rollback()
+ transaction.leave_transaction_management()
+
+def savepoint_commit(*args, **kwargs):
+ transaction.commit()
+ transaction.leave_transaction_management()
diff --git a/import_export/formats/base_formats.py b/import_export/formats/base_formats.py
index 7d439b1..9d7518b 100644
--- a/import_export/formats/base_formats.py
+++ b/import_export/formats/base_formats.py
@@ -57,6 +57,10 @@ class Format(object):
"""
return ""
+ def get_content_type(self):
+ # For content types see http://www.iana.org/assignments/media-types/media-types.xhtml
+ return 'application/octet-stream'
+
def can_import(self):
return False
@@ -66,6 +70,7 @@ class Format(object):
class TablibFormat(Format):
TABLIB_MODULE = None
+ CONTENT_TYPE = 'application/octet-stream'
def get_format(self):
"""
@@ -92,6 +97,9 @@ class TablibFormat(Format):
return self.get_format().extentions[0]
return self.get_format().extensions[0]
+ def get_content_type(self):
+ return self.CONTENT_TYPE
+
def can_import(self):
return hasattr(self.get_format(), 'import_set')
@@ -112,6 +120,7 @@ class CSV(TablibFormat):
CSV is treated as binary in Python 2.
"""
TABLIB_MODULE = 'tablib.formats._csv'
+ CONTENT_TYPE = 'text/csv'
def get_read_mode(self):
return 'rU' if six.PY3 else 'rb'
@@ -122,30 +131,37 @@ class CSV(TablibFormat):
class JSON(TextFormat):
TABLIB_MODULE = 'tablib.formats._json'
+ CONTENT_TYPE = 'application/json'
class YAML(TextFormat):
TABLIB_MODULE = 'tablib.formats._yaml'
+ CONTENT_TYPE = 'text/yaml' # See http://stackoverflow.com/questions/332129/yaml-mime-type
class TSV(TextFormat):
TABLIB_MODULE = 'tablib.formats._tsv'
+ CONTENT_TYPE = 'text/tab-separated-values'
class ODS(TextFormat):
TABLIB_MODULE = 'tablib.formats._ods'
+ CONTENT_TYPE = 'application/vnd.oasis.opendocument.spreadsheet'
class XLSX(TextFormat):
TABLIB_MODULE = 'tablib.formats._xlsx'
+ CONTENT_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
class HTML(TextFormat):
TABLIB_MODULE = 'tablib.formats._html'
+ CONTENT_TYPE = 'text/html'
class XLS(TablibFormat):
TABLIB_MODULE = 'tablib.formats._xls'
+ CONTENT_TYPE = 'application/vnd.ms-excel'
def can_import(self):
return XLS_IMPORT
diff --git a/import_export/locale/es/LC_MESSAGES/django.mo b/import_export/locale/es/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..070d81d
--- /dev/null
+++ b/import_export/locale/es/LC_MESSAGES/django.mo
Binary files differ
diff --git a/import_export/locale/es/LC_MESSAGES/django.po b/import_export/locale/es/LC_MESSAGES/django.po
new file mode 100644
index 0000000..4a9757f
--- /dev/null
+++ b/import_export/locale/es/LC_MESSAGES/django.po
@@ -0,0 +1,107 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# David Díaz <d.diazp@gmail.com>, 2015.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-02-21 12:07+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: admin.py:150
+msgid "Import finished"
+msgstr "Importación finalizada"
+
+#: admin.py:368
+msgid "You must select an export format."
+msgstr "Debes selecciona un formato de exportación."
+
+#: admin.py:385
+#, python-format
+msgid "Export selected %(verbose_name_plural)s"
+msgstr "Exportación seleccionada %(verbose_name_plural)s"
+
+#: forms.py:12
+msgid "File to import"
+msgstr "Fichero a importar"
+
+#: forms.py:15 forms.py:42 forms.py:67
+msgid "Format"
+msgstr "Formato"
+
+#: templates/admin/import_export/base.html:11
+msgid "Home"
+msgstr "Inicio"
+
+#: templates/admin/import_export/change_list_export.html:5
+#: templates/admin/import_export/change_list_import_export.html:6
+#: templates/admin/import_export/export.html:8
+#: templates/admin/import_export/export.html:12
+msgid "Export"
+msgstr "Exportar"
+
+#: templates/admin/import_export/change_list_import.html:5
+#: templates/admin/import_export/change_list_import_export.html:5
+#: templates/admin/import_export/import.html:8
+#: templates/admin/import_export/import.html:12
+msgid "Import"
+msgstr "Importar"
+
+#: templates/admin/import_export/export.html:34
+#: templates/admin/import_export/import.html:57
+msgid "Submit"
+msgstr ""
+
+#: templates/admin/import_export/import.html:19
+msgid ""
+"Below is a preview of data to be imported. If you are satisfied with the "
+"results, click 'Confirm import'"
+msgstr ""
+"A continuación se muestra una vista previa de los datos a importar. Si estás "
+"satisfecho con los resultados, haz clic en 'Confirmar importación'"
+
+#: templates/admin/import_export/import.html:22
+msgid "Confirm import"
+msgstr "Confirmar importación"
+
+#: templates/admin/import_export/import.html:31
+msgid "This importer will import the following fields: "
+msgstr "Este importador importará los siguientes campos:"
+
+#: templates/admin/import_export/import.html:65
+msgid "Errors"
+msgstr "Errores"
+
+#: templates/admin/import_export/import.html:73
+msgid "Line number"
+msgstr "Número de línea"
+
+#: templates/admin/import_export/import.html:82
+msgid "Preview"
+msgstr "Vista previa"
+
+#: templates/admin/import_export/import.html:97
+msgid "New"
+msgstr "Nuevo"
+
+#: templates/admin/import_export/import.html:99
+msgid "Skipped"
+msgstr "Omitido"
+
+#: templates/admin/import_export/import.html:101
+msgid "Delete"
+msgstr "Borrado"
+
+#: templates/admin/import_export/import.html:103
+msgid "Update"
+msgstr "Actualizado"
diff --git a/import_export/locale/pl/LC_MESSAGES/django.mo b/import_export/locale/pl/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..478e888
--- /dev/null
+++ b/import_export/locale/pl/LC_MESSAGES/django.mo
Binary files differ
diff --git a/import_export/locale/pl/LC_MESSAGES/django.po b/import_export/locale/pl/LC_MESSAGES/django.po
new file mode 100644
index 0000000..65b0df3
--- /dev/null
+++ b/import_export/locale/pl/LC_MESSAGES/django.po
@@ -0,0 +1,107 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Ludwik Trammer <ludwik@gmail.com>, 2015.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-03-06 14:44+0800\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
+"|| n%100>=20) ? 1 : 2);\n"
+
+#: admin.py:160
+msgid "Import finished"
+msgstr "Zakończono importowanie"
+
+#: admin.py:378
+msgid "You must select an export format."
+msgstr "Musisz wybrać format eksportu."
+
+#: admin.py:395
+#, python-format
+msgid "Export selected %(verbose_name_plural)s"
+msgstr "Eksportuj wybrane %(verbose_name_plural)s"
+
+#: forms.py:12
+msgid "File to import"
+msgstr "Plik do importu"
+
+#: forms.py:15 forms.py:42 forms.py:67
+msgid "Format"
+msgstr "Format"
+
+#: templates/admin/import_export/base.html:11
+msgid "Home"
+msgstr "Powrót"
+
+#: templates/admin/import_export/change_list_export.html:5
+#: templates/admin/import_export/change_list_import_export.html:6
+#: templates/admin/import_export/export.html:8
+#: templates/admin/import_export/export.html:12
+msgid "Export"
+msgstr "Eksport"
+
+#: templates/admin/import_export/change_list_import.html:5
+#: templates/admin/import_export/change_list_import_export.html:5
+#: templates/admin/import_export/import.html:8
+#: templates/admin/import_export/import.html:12
+msgid "Import"
+msgstr "Import"
+
+#: templates/admin/import_export/export.html:34
+#: templates/admin/import_export/import.html:57
+msgid "Submit"
+msgstr "Wyślij"
+
+#: templates/admin/import_export/import.html:19
+msgid ""
+"Below is a preview of data to be imported. If you are satisfied with the "
+"results, click 'Confirm import'"
+msgstr "Poniżej znajdują się przykładowe dane do zaimportowania. Jeśli"
+"satysfakcjonuje Cię wynik, kliknij 'Potwierdź import'"
+
+#: templates/admin/import_export/import.html:22
+msgid "Confirm import"
+msgstr "Potwierdź import"
+
+#: templates/admin/import_export/import.html:31
+msgid "This importer will import the following fields: "
+msgstr "Zostaną zaimportowane następujące pola: "
+
+#: templates/admin/import_export/import.html:65
+msgid "Errors"
+msgstr "Błąd"
+
+#: templates/admin/import_export/import.html:73
+msgid "Line number"
+msgstr "Numer linii"
+
+#: templates/admin/import_export/import.html:82
+msgid "Preview"
+msgstr "Podgląd"
+
+#: templates/admin/import_export/import.html:97
+msgid "New"
+msgstr "Nowy"
+
+#: templates/admin/import_export/import.html:99
+msgid "Skipped"
+msgstr "Pominięty"
+
+#: templates/admin/import_export/import.html:101
+msgid "Delete"
+msgstr "Usuń"
+
+#: templates/admin/import_export/import.html:103
+msgid "Update"
+msgstr "Aktualizuj"
diff --git a/import_export/locale/ru/LC_MESSAGES/django.mo b/import_export/locale/ru/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..6d91d6b
--- /dev/null
+++ b/import_export/locale/ru/LC_MESSAGES/django.mo
Binary files differ
diff --git a/import_export/locale/ru/LC_MESSAGES/django.po b/import_export/locale/ru/LC_MESSAGES/django.po
new file mode 100644
index 0000000..57aca2c
--- /dev/null
+++ b/import_export/locale/ru/LC_MESSAGES/django.po
@@ -0,0 +1,108 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-09 20:07+0300\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
+"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: admin.py:150
+msgid "Import finished"
+msgstr "Импорт завершен"
+
+#: admin.py:368
+msgid "You must select an export format."
+msgstr "Необходимо выбрать формат экспорта"
+
+#: admin.py:385
+#, python-format
+msgid "Export selected %(verbose_name_plural)s"
+msgstr "Экспортировать выбранные %(verbose_name_plural)s"
+
+#: forms.py:12
+msgid "File to import"
+msgstr "Файл для импорта"
+
+#: forms.py:15 forms.py:42 forms.py:67
+msgid "Format"
+msgstr "Формат"
+
+#: templates/admin/import_export/base.html:11
+msgid "Home"
+msgstr "Главная"
+
+#: templates/admin/import_export/change_list_export.html:5
+#: templates/admin/import_export/change_list_import_export.html:6
+#: templates/admin/import_export/export.html:8
+#: templates/admin/import_export/export.html:12
+msgid "Export"
+msgstr "Экспорт"
+
+#: templates/admin/import_export/change_list_import.html:5
+#: templates/admin/import_export/change_list_import_export.html:5
+#: templates/admin/import_export/import.html:8
+#: templates/admin/import_export/import.html:12
+msgid "Import"
+msgstr "Импорт"
+
+#: templates/admin/import_export/export.html:34
+#: templates/admin/import_export/import.html:57
+msgid "Submit"
+msgstr "Отправить"
+
+#: templates/admin/import_export/import.html:19
+msgid ""
+"Below is a preview of data to be imported. If you are satisfied with the "
+"results, click 'Confirm import'"
+msgstr ""
+"Ниже показано то, что будет импортировано. Нажмите 'Подтвердить импорт',"
+"если Вас устраивает результат"
+
+#: templates/admin/import_export/import.html:22
+msgid "Confirm import"
+msgstr "Подтвердить импорт"
+
+#: templates/admin/import_export/import.html:31
+msgid "This importer will import the following fields: "
+msgstr "Будут импортированы следующие поля: "
+
+#: templates/admin/import_export/import.html:65
+msgid "Errors"
+msgstr "Ошибки"
+
+#: templates/admin/import_export/import.html:73
+msgid "Line number"
+msgstr "Номер строки"
+
+#: templates/admin/import_export/import.html:82
+msgid "Preview"
+msgstr "Предпросмотр"
+
+#: templates/admin/import_export/import.html:97
+msgid "New"
+msgstr "Добавлено"
+
+#: templates/admin/import_export/import.html:99
+msgid "Skipped"
+msgstr "Пропущено"
+
+#: templates/admin/import_export/import.html:101
+msgid "Delete"
+msgstr "Удалено"
+
+#: templates/admin/import_export/import.html:103
+msgid "Update"
+msgstr "Обновлено"
diff --git a/import_export/locale/sk/LC_MESSAGES/django.mo b/import_export/locale/sk/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..596e0fc
--- /dev/null
+++ b/import_export/locale/sk/LC_MESSAGES/django.mo
Binary files differ
diff --git a/import_export/locale/sk/LC_MESSAGES/django.po b/import_export/locale/sk/LC_MESSAGES/django.po
new file mode 100644
index 0000000..c3b9c0b
--- /dev/null
+++ b/import_export/locale/sk/LC_MESSAGES/django.po
@@ -0,0 +1,107 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Juraj Bubniak <contact@jbub.eu>, 2015.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-03-24 10:11+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+
+#: admin.py:160
+msgid "Import finished"
+msgstr "Import dokončený"
+
+#: admin.py:378
+msgid "You must select an export format."
+msgstr "Je potrebné vybrať formát exportu."
+
+#: admin.py:395
+#, python-format
+msgid "Export selected %(verbose_name_plural)s"
+msgstr "Exportovať vybrané %(verbose_name_plural)s"
+
+#: forms.py:12
+msgid "File to import"
+msgstr "Importovať súbor"
+
+#: forms.py:15 forms.py:42 forms.py:67
+msgid "Format"
+msgstr "Formát"
+
+#: templates/admin/import_export/base.html:11
+msgid "Home"
+msgstr "Domov"
+
+#: templates/admin/import_export/change_list_export.html:5
+#: templates/admin/import_export/change_list_import_export.html:6
+#: templates/admin/import_export/export.html:8
+#: templates/admin/import_export/export.html:12
+msgid "Export"
+msgstr "Exportovať"
+
+#: templates/admin/import_export/change_list_import.html:5
+#: templates/admin/import_export/change_list_import_export.html:5
+#: templates/admin/import_export/import.html:8
+#: templates/admin/import_export/import.html:12
+msgid "Import"
+msgstr "Importovať"
+
+#: templates/admin/import_export/export.html:34
+#: templates/admin/import_export/import.html:57
+msgid "Submit"
+msgstr "Odoslať"
+
+#: templates/admin/import_export/import.html:19
+msgid ""
+"Below is a preview of data to be imported. If you are satisfied with the "
+"results, click 'Confirm import'"
+msgstr ""
+"Nižšie je zobrazený náhľad importovaných dát. Ak je všetko v poriadku, "
+"kliknite na tlačidlo 'Potvrdiť import'"
+
+#: templates/admin/import_export/import.html:22
+msgid "Confirm import"
+msgstr "Potvrdiť import"
+
+#: templates/admin/import_export/import.html:31
+msgid "This importer will import the following fields: "
+msgstr "Budú importované nasledujúce polia: "
+
+#: templates/admin/import_export/import.html:65
+msgid "Errors"
+msgstr "Chyby"
+
+#: templates/admin/import_export/import.html:73
+msgid "Line number"
+msgstr "Číslo riadku"
+
+#: templates/admin/import_export/import.html:82
+msgid "Preview"
+msgstr "Náhľad"
+
+#: templates/admin/import_export/import.html:97
+msgid "New"
+msgstr "Nový"
+
+#: templates/admin/import_export/import.html:99
+msgid "Skipped"
+msgstr "Preskočený"
+
+#: templates/admin/import_export/import.html:101
+msgid "Delete"
+msgstr "Vymazaný"
+
+#: templates/admin/import_export/import.html:103
+msgid "Update"
+msgstr "Aktualizovaný"
diff --git a/import_export/locale/zh_Hans/LC_MESSAGES/django.mo b/import_export/locale/zh_Hans/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..c22158e
--- /dev/null
+++ b/import_export/locale/zh_Hans/LC_MESSAGES/django.mo
Binary files differ
diff --git a/import_export/locale/zh_Hans/LC_MESSAGES/django.po b/import_export/locale/zh_Hans/LC_MESSAGES/django.po
new file mode 100644
index 0000000..5b5861e
--- /dev/null
+++ b/import_export/locale/zh_Hans/LC_MESSAGES/django.po
@@ -0,0 +1,105 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-03-06 14:44+0800\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: admin.py:160
+msgid "Import finished"
+msgstr "导入完成"
+
+#: admin.py:378
+msgid "You must select an export format."
+msgstr "您必须选择一个导出格式。"
+
+#: admin.py:395
+#, python-format
+msgid "Export selected %(verbose_name_plural)s"
+msgstr "导出选中的 %(verbose_name_plural)s"
+
+#: forms.py:12
+msgid "File to import"
+msgstr "导入文件"
+
+#: forms.py:15 forms.py:42 forms.py:67
+msgid "Format"
+msgstr "格式"
+
+#: templates/admin/import_export/base.html:11
+msgid "Home"
+msgstr ""
+
+#: templates/admin/import_export/change_list_export.html:5
+#: templates/admin/import_export/change_list_import_export.html:6
+#: templates/admin/import_export/export.html:8
+#: templates/admin/import_export/export.html:12
+msgid "Export"
+msgstr "导出"
+
+#: templates/admin/import_export/change_list_import.html:5
+#: templates/admin/import_export/change_list_import_export.html:5
+#: templates/admin/import_export/import.html:8
+#: templates/admin/import_export/import.html:12
+msgid "Import"
+msgstr "导入"
+
+#: templates/admin/import_export/export.html:34
+#: templates/admin/import_export/import.html:57
+msgid "Submit"
+msgstr "提交"
+
+#: templates/admin/import_export/import.html:19
+msgid ""
+"Below is a preview of data to be imported. If you are satisfied with the "
+"results, click 'Confirm import'"
+msgstr "以下是导入数据的预览。如果确认结果没有问题,可以点击 “确认导入”"
+
+#: templates/admin/import_export/import.html:22
+msgid "Confirm import"
+msgstr "确认导入"
+
+#: templates/admin/import_export/import.html:31
+msgid "This importer will import the following fields: "
+msgstr "此次将导入以下字段:"
+
+#: templates/admin/import_export/import.html:65
+msgid "Errors"
+msgstr "错误"
+
+#: templates/admin/import_export/import.html:73
+msgid "Line number"
+msgstr "行号"
+
+#: templates/admin/import_export/import.html:82
+msgid "Preview"
+msgstr "预览"
+
+#: templates/admin/import_export/import.html:97
+msgid "New"
+msgstr "新增"
+
+#: templates/admin/import_export/import.html:99
+msgid "Skipped"
+msgstr "忽略"
+
+#: templates/admin/import_export/import.html:101
+msgid "Delete"
+msgstr "删除"
+
+#: templates/admin/import_export/import.html:103
+msgid "Update"
+msgstr "跟新"
diff --git a/import_export/resources.py b/import_export/resources.py
index 40b65b5..e0f8b6e 100644
--- a/import_export/resources.py
+++ b/import_export/resources.py
@@ -8,13 +8,11 @@ import traceback
import tablib
from diff_match_patch import diff_match_patch
+from django import VERSION
from django.utils.safestring import mark_safe
-from django.utils.datastructures import SortedDict
from django.utils import six
-from django.db import transaction
from django.db.models.fields import FieldDoesNotExist
from django.db.models.query import QuerySet
-from django.db.models.related import RelatedObject
from django.conf import settings
from .results import Error, Result, RowResult
@@ -24,12 +22,28 @@ from .instance_loaders import (
ModelInstanceLoader,
)
+try:
+ from django.db.transaction import atomic, savepoint, savepoint_rollback, savepoint_commit # noqa
+except ImportError:
+ from .django_compat import atomic, savepoint, savepoint_rollback, savepoint_commit # noqa
+
+
+if VERSION < (1, 8):
+ from django.db.models.related import RelatedObject
+ ForeignObjectRel = RelatedObject
+else:
+ from django.db.models.fields.related import ForeignObjectRel
+ RelatedObject = None
try:
from django.utils.encoding import force_text
except ImportError:
from django.utils.encoding import force_unicode as force_text
+try:
+ from collections import OrderedDict
+except ImportError:
+ from django.utils.datastructures import SortedDict as OrderedDict
USE_TRANSACTIONS = getattr(settings, 'IMPORT_EXPORT_USE_TRANSACTIONS', False)
@@ -80,22 +94,26 @@ class ResourceOptions(object):
skip_unchanged = False
report_skipped = True
- def __new__(cls, meta=None):
- overrides = {}
-
- if meta:
- for override_name in dir(meta):
- if not override_name.startswith('_'):
- overrides[override_name] = getattr(meta, override_name)
-
- return object.__new__(type(str('ResourceOptions'), (cls,), overrides))
-
class DeclarativeMetaclass(type):
def __new__(cls, name, bases, attrs):
declared_fields = []
-
+ meta = ResourceOptions()
+
+ # If this class is subclassing another Resource, add that Resource's fields.
+ # Note that we loop over the bases in *reverse*. This is necessary in
+ # order to preserve the correct order of fields.
+ for base in bases[::-1]:
+ if hasattr(base, 'fields'):
+ declared_fields = list(six.iteritems(base.fields)) + declared_fields
+ # Collect the Meta options
+ options = getattr(base, 'Meta', None)
+ for option in [option for option in dir(options)
+ if not option.startswith('_')]:
+ setattr(meta, option, getattr(options, option))
+
+ # Add direct fields
for field_name, obj in attrs.copy().items():
if isinstance(obj, Field):
field = attrs.pop(field_name)
@@ -103,11 +121,16 @@ class DeclarativeMetaclass(type):
field.column_name = field_name
declared_fields.append((field_name, field))
- attrs['fields'] = SortedDict(declared_fields)
+ attrs['fields'] = OrderedDict(declared_fields)
new_class = super(DeclarativeMetaclass, cls).__new__(cls, name,
bases, attrs)
- opts = getattr(new_class, 'Meta', None)
- new_class._meta = ResourceOptions(opts)
+
+ # Add direct options
+ options = getattr(new_class, 'Meta', None)
+ for option in [option for option in dir(options)
+ if not option.startswith('_')]:
+ setattr(meta, option, getattr(options, option))
+ new_class._meta = meta
return new_class
@@ -270,14 +293,15 @@ class Resource(six.with_metaclass(DeclarativeMetaclass)):
"""
return self.get_export_headers()
- def before_import(self, dataset, dry_run):
+ def before_import(self, dataset, dry_run, **kwargs):
"""
Override to add additional logic.
"""
pass
+ @atomic()
def import_data(self, dataset, dry_run=False, raise_errors=False,
- use_transactions=None):
+ use_transactions=None, **kwargs):
"""
Imports data from ``dataset``.
@@ -287,6 +311,7 @@ class Resource(six.with_metaclass(DeclarativeMetaclass)):
back.
"""
result = Result()
+ result.diff_headers = self.get_diff_headers()
if use_transactions is None:
use_transactions = self.get_use_transactions()
@@ -295,24 +320,22 @@ class Resource(six.with_metaclass(DeclarativeMetaclass)):
# when transactions are used we want to create/update/delete object
# as transaction will be rolled back if dry_run is set
real_dry_run = False
- transaction.enter_transaction_management()
- transaction.managed(True)
+ sp1 = savepoint()
else:
real_dry_run = dry_run
- instance_loader = self._meta.instance_loader_class(self, dataset)
-
try:
- self.before_import(dataset, real_dry_run)
+ self.before_import(dataset, real_dry_run, **kwargs)
except Exception as e:
tb_info = traceback.format_exc(2)
result.base_errors.append(Error(repr(e), tb_info))
if raise_errors:
if use_transactions:
- transaction.rollback()
- transaction.leave_transaction_management()
+ savepoint_rollback(sp1)
raise
+ instance_loader = self._meta.instance_loader_class(self, dataset)
+
for row in dataset.dict:
try:
row_result = RowResult()
@@ -341,7 +364,7 @@ class Resource(six.with_metaclass(DeclarativeMetaclass)):
self.save_instance(instance, real_dry_run)
self.save_m2m(instance, row, real_dry_run)
# Add object info to RowResult for LogEntry
- row_result.object_repr = str(instance)
+ row_result.object_repr = force_text(instance)
row_result.object_id = instance.pk
row_result.diff = self.get_diff(original, instance,
real_dry_run)
@@ -350,8 +373,7 @@ class Resource(six.with_metaclass(DeclarativeMetaclass)):
row_result.errors.append(Error(e, tb_info))
if raise_errors:
if use_transactions:
- transaction.rollback()
- transaction.leave_transaction_management()
+ savepoint_rollback(sp1)
six.reraise(*sys.exc_info())
if (row_result.import_type != RowResult.IMPORT_TYPE_SKIP or
self._meta.report_skipped):
@@ -359,15 +381,15 @@ class Resource(six.with_metaclass(DeclarativeMetaclass)):
if use_transactions:
if dry_run or result.has_errors():
- transaction.rollback()
+ savepoint_rollback(sp1)
else:
- transaction.commit()
- transaction.leave_transaction_management()
+ savepoint_commit(sp1)
return result
def get_export_order(self):
- return self._meta.export_order or self.fields.keys()
+ order = tuple (self._meta.export_order or ())
+ return order + tuple (k for k in self.fields.keys() if k not in order)
def export_field(self, field, obj):
field_name = self.get_field_name(field)
@@ -431,7 +453,7 @@ class ModelDeclarativeMetaclass(DeclarativeMetaclass):
readonly=False)
field_list.append((f.name, field, ))
- new_class.fields.update(SortedDict(field_list))
+ new_class.fields.update(OrderedDict(field_list))
#add fields that follow relationships
if opts.fields is not None:
@@ -457,18 +479,25 @@ class ModelDeclarativeMetaclass(DeclarativeMetaclass):
# We're not at the last attribute yet, so check that
# we're looking at a relation, and move on to the
# next model.
- if f.rel is None:
- raise KeyError('%s is not a relation' % verbose_path)
- model = f.rel.to
-
- if isinstance(f, RelatedObject):
+ if isinstance(f, ForeignObjectRel):
+ if RelatedObject is None:
+ model = f.related_model
+ else:
+ # Django < 1.8
+ model = f.model
+ else:
+ if f.rel is None:
+ raise KeyError('%s is not a relation' % verbose_path)
+ model = f.rel.to
+
+ if isinstance(f, ForeignObjectRel):
f = f.field
field = new_class.field_from_django_field(field_name, f,
readonly=True)
field_list.append((field_name, field))
- new_class.fields.update(SortedDict(field_list))
+ new_class.fields.update(OrderedDict(field_list))
return new_class
@@ -551,3 +580,4 @@ def modelresource_factory(model, resource_class=ModelResource):
metaclass = ModelDeclarativeMetaclass
return metaclass(class_name, (resource_class,), class_attrs)
+
diff --git a/import_export/templates/admin/import_export/import.html b/import_export/templates/admin/import_export/import.html
index 1bb5fd5..6e0ef89 100644
--- a/import_export/templates/admin/import_export/import.html
+++ b/import_export/templates/admin/import_export/import.html
@@ -24,16 +24,16 @@
</form>
{% else %}
- <form action="{{ form_url }}" method="post" id="{{ opts.module_name }}_form" enctype="multipart/form-data">
+ <form action="" method="post" id="{{ opts.module_name }}_form" enctype="multipart/form-data">
{% csrf_token %}
<p>
{% trans "This importer will import the following fields: " %}
{% for f in fields %}
{% if forloop.counter0 %}
- ,
+ ,
{% endif %}
- <tt>{{ f }}</tt>
+ <code>{{ f }}</code>
{% endfor %}
</p>
@@ -85,7 +85,7 @@
<thead>
<tr>
<th></th>
- {% for field in fields %}
+ {% for field in result.diff_headers %}
<th>{{ field }}</th>
{% endfor %}
</tr>
@@ -108,7 +108,7 @@
{{ field }}
</td>
{% endfor %}
- </etr>
+ </tr>
{% endfor %}
</table>
{% endif %}
diff --git a/import_export/widgets.py b/import_export/widgets.py
index bc657cd..ffb977d 100644
--- a/import_export/widgets.py
+++ b/import_export/widgets.py
@@ -36,18 +36,24 @@ class Widget(object):
return force_text(value)
-class IntegerWidget(Widget):
+class NumberWidget(Widget):
+
+ def render(self, value):
+ return value
+
+
+class IntegerWidget(NumberWidget):
"""
Widget for converting integer fields.
"""
def clean(self, value):
- if not value:
+ if not value and value is not 0:
return None
return int(value)
-class DecimalWidget(Widget):
+class DecimalWidget(NumberWidget):
"""
Widget for converting decimal fields.
"""
@@ -76,12 +82,12 @@ class BooleanWidget(Widget):
def render(self, value):
if value is None:
- return ""
+ return ""
return self.TRUE_VALUES[0] if value else self.FALSE_VALUE
def clean(self, value):
if value == "":
- return None
+ return None
return True if value in self.TRUE_VALUES else False
@@ -157,7 +163,7 @@ class DateTimeWidget(Widget):
class ForeignKeyWidget(Widget):
"""
Widget for ``ForeignKey`` which looks up a related model.
-
+
The lookup field defaults to using the primary key (``pk``), but
can be customised to use any field on the related model.
diff --git a/tests/core/models.py b/tests/core/models.py
index 4fe6d4a..1e07095 100644
--- a/tests/core/models.py
+++ b/tests/core/models.py
@@ -38,6 +38,7 @@ class Book(models.Model):
class Profile(models.Model):
user = models.OneToOneField('auth.User')
+ is_private = models.BooleanField(default=True)
class Entry(models.Model):
diff --git a/tests/core/tests/admin_integration_tests.py b/tests/core/tests/admin_integration_tests.py
index baa39f1..69825c0 100644
--- a/tests/core/tests/admin_integration_tests.py
+++ b/tests/core/tests/admin_integration_tests.py
@@ -2,6 +2,7 @@ from __future__ import unicode_literals
import os.path
+from django.test.utils import override_settings
from django.test.testcases import TestCase
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _
@@ -29,7 +30,15 @@ class ImportExportAdminIntegrationTest(TestCase):
self.assertContains(response, _('Import'))
self.assertContains(response, _('Export'))
+ @override_settings(TEMPLATE_STRING_IF_INVALID='INVALID_VARIABLE')
def test_import(self):
+ # GET the import form
+ response = self.client.get('/admin/core/book/import/')
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, 'admin/import_export/import.html')
+ self.assertContains(response, 'form action=""')
+
+ # POST the import form
input_format = '0'
filename = os.path.join(
os.path.dirname(__file__),
@@ -64,6 +73,7 @@ class ImportExportAdminIntegrationTest(TestCase):
response = self.client.post('/admin/core/book/export/', data)
self.assertEqual(response.status_code, 200)
self.assertTrue(response.has_header("Content-Disposition"))
+ self.assertEqual(response['Content-Type'], 'text/csv')
def test_import_export_buttons_visible_without_add_permission(self):
# issue 38 - Export button not visible when no add permission
diff --git a/tests/core/tests/resources_tests.py b/tests/core/tests/resources_tests.py
index 4bb796c..7c74392 100644
--- a/tests/core/tests/resources_tests.py
+++ b/tests/core/tests/resources_tests.py
@@ -33,6 +33,7 @@ except ImportError:
class MyResource(resources.Resource):
name = fields.Field()
email = fields.Field()
+ extra = fields.Field()
class Meta:
export_order = ('email', 'name')
@@ -57,8 +58,43 @@ class ResourceTestCase(TestCase):
def test_get_export_order(self):
self.assertEqual(self.my_resource.get_export_headers(),
- ['email', 'name'])
+ ['email', 'name', 'extra'])
+ # Issue 140 Attributes aren't inherited by subclasses
+ def test_inheritance(self):
+ class A(MyResource):
+ inherited = fields.Field()
+
+ class Meta:
+ import_id_fields = ('email',)
+
+ class B(A):
+ local = fields.Field()
+
+ class Meta:
+ export_order = ('email', 'extra')
+
+ resource = B()
+ self.assertIn('name', resource.fields)
+ self.assertIn('inherited', resource.fields)
+ self.assertIn('local', resource.fields)
+ self.assertEqual(resource.get_export_headers(),
+ ['email', 'extra', 'name', 'inherited', 'local'])
+ self.assertEqual(resource._meta.import_id_fields, ('email',))
+
+ def test_inheritance_with_custom_attributes(self):
+ class A(MyResource):
+ inherited = fields.Field()
+
+ class Meta:
+ import_id_fields = ('email',)
+ custom_attribute = True
+
+ class B(A):
+ local = fields.Field()
+
+ resource = B()
+ self.assertEqual(resource._meta.custom_attribute, True)
class BookResource(resources.ModelResource):
published = fields.Field(column_name='published_date')
@@ -347,12 +383,14 @@ class ModelResourceTest(TestCase):
class EntryResource(resources.ModelResource):
class Meta:
model = Entry
- fields = ('user__profile',)
+ fields = ('user__profile', 'user__profile__is_private')
resource = EntryResource()
dataset = resource.export(Entry.objects.all())
self.assertEqual(dataset.dict[0]['user__profile'], profile.pk)
+ self.assertEqual(dataset.dict[0]['user__profile__is_private'], '1')
self.assertEqual(dataset.dict[1]['user__profile'], '')
+ self.assertEqual(dataset.dict[1]['user__profile__is_private'], '')
def test_empty_get_queryset(self):
# issue #25 - Overriding queryset on export() fails when passed
@@ -390,6 +428,23 @@ class ModelResourceTest(TestCase):
self.assertFalse(result.has_errors())
self.assertEqual(len(result.rows), 0)
+ def test_before_import_access_to_kwargs(self):
+ class B(BookResource):
+ def before_import(self, dataset, dry_run, **kwargs):
+ if 'extra_arg' in kwargs:
+ dataset.headers[dataset.headers.index('author_email')] = 'old_email'
+ dataset.insert_col(0,
+ lambda row: kwargs['extra_arg'],
+ header='author_email')
+
+ resource = B()
+ result = resource.import_data(self.dataset, raise_errors=True,
+ extra_arg='extra@example.com')
+ self.assertFalse(result.has_errors())
+ self.assertEqual(len(result.rows), 1)
+ instance = Book.objects.get(pk=self.book.pk)
+ self.assertEqual(instance.author_email, 'extra@example.com')
+
def test_link_to_nonexistent_field(self):
with self.assertRaises(FieldDoesNotExist) as cm:
class BrokenBook(resources.ModelResource):
diff --git a/tests/core/tests/widgets_tests.py b/tests/core/tests/widgets_tests.py
index 4dbcb2a..630b0da 100644
--- a/tests/core/tests/widgets_tests.py
+++ b/tests/core/tests/widgets_tests.py
@@ -77,9 +77,28 @@ class DateWidgetBefore1900Test(TestCase):
class DecimalWidgetTest(TestCase):
+ def setUp(self):
+ self.value = Decimal("11.111")
+ self.widget = widgets.DecimalWidget()
+
def test_clean(self):
- widget = widgets.DecimalWidget()
- self.assertEqual(widget.clean("11.111"), Decimal("11.111"))
+ self.assertEqual(self.widget.clean("11.111"), self.value)
+
+ def test_render(self):
+ self.assertEqual(self.widget.render(self.value), self.value)
+
+
+class IntegerWidgetTest(TestCase):
+
+ def setUp(self):
+ self.value = 0
+ self.widget = widgets.IntegerWidget()
+
+ def test_clean_integer_zero(self):
+ self.assertEqual(self.widget.clean(0), self.value)
+
+ def test_clean_string_zero(self):
+ self.assertEqual(self.widget.clean("0"), self.value)
class ForeignKeyWidgetTest(TestCase):
diff --git a/tox.ini b/tox.ini
index bb2e027..babf6f5 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
[tox]
-envlist = py26-1.4, py27-1.4, py27-tablib-dev-1.4, py27-mysql-innodb-1.4, py27-1.5, py27-1.6, py33-1.6, py27-1.7, py33-1.7
+envlist = py26-1.4, py27-1.4, py27-tablib-dev-1.4, py27-mysql-innodb-1.4, py27-1.5, py27-1.6, py33-1.6, py27-1.7, py33-1.7, py34-1.7, py27-1.8, py33-1.8, py34-1.8
[testenv]
commands=python {toxinidir}/tests/manage.py test core
@@ -54,3 +54,26 @@ basepython = python3.3
deps =
django==1.7.0
-egit+https://github.com/kennethreitz/tablib.git#egg=tablib
+
+[testenv:py34-1.7]
+basepython = python3.4
+deps =
+ django==1.7.0
+ -egit+https://github.com/kennethreitz/tablib.git#egg=tablib
+
+[testenv:py27-1.8]
+basepython = python2.7
+deps =
+ django==1.8
+
+[testenv:py33-1.8]
+basepython = python3.3
+deps =
+ django==1.8
+ -egit+https://github.com/kennethreitz/tablib.git#egg=tablib
+
+[testenv:py34-1.8]
+basepython = python3.4
+deps =
+ django==1.8
+ -egit+https://github.com/kennethreitz/tablib.git#egg=tablib