Merge branch 'master' of https://github.com/django-import-export/django-import-export into require-django-1-8
This commit is contained in:
commit
87afec415b
|
@ -11,6 +11,7 @@ env:
|
|||
- DJANGO="Django>=1.9,<1.10"
|
||||
- DJANGO="Django>=1.10,<1.11"
|
||||
- DJANGO="Django>=1.11,<1.12"
|
||||
- DJANGO="Django>=2.0,<2.1"
|
||||
- DJANGO="https://github.com/django/django/archive/master.tar.gz"
|
||||
install:
|
||||
- pip install -q $DJANGO
|
||||
|
@ -29,10 +30,16 @@ matrix:
|
|||
env: DJANGO="Django>=1.10,<1.11"
|
||||
- python: "2.7"
|
||||
env: DJANGO="https://github.com/django/django/archive/master.tar.gz"
|
||||
- python: "2.7"
|
||||
env: DJANGO="Django>=2.0,<2.1"
|
||||
- python: "3.3"
|
||||
env: DJANGO="Django>=1.11,<1.12"
|
||||
- python: "3.3"
|
||||
env: DJANGO="https://github.com/django/django/archive/master.tar.gz"
|
||||
- python: "3.3"
|
||||
env: DJANGO="Django>=2.0,<2.1"
|
||||
- python: "3.4"
|
||||
env: DJANGO="https://github.com/django/django/archive/master.tar.gz"
|
||||
- python: "3.6"
|
||||
env: DJANGO="Django>=1.8,<1.9"
|
||||
- python: "3.6"
|
||||
|
|
5
AUTHORS
5
AUTHORS
|
@ -67,7 +67,7 @@ The following is a list of much appreciated contributors:
|
|||
* carlosp420 (Carlos Peña)
|
||||
* freelancersunion (Freelancers Union)
|
||||
* jbradberry (Jeff Bradberry)
|
||||
* antoniablair
|
||||
* antoniablair
|
||||
* kellerkind
|
||||
* yueyoum (Johnnie Wang)
|
||||
* shaggyfrog (Thomas Hauk)
|
||||
|
@ -92,3 +92,6 @@ The following is a list of much appreciated contributors:
|
|||
* gatsinski (Hristo Gatsinski)
|
||||
* raghavsethi (Raghav Sethi)
|
||||
* jdufresne (Jon Dufresne)
|
||||
* trik (Marco Marche)
|
||||
* krishraghuram (Raghuram Krishnaswami)
|
||||
* int_ua (Serhiy Zahoriya)
|
||||
|
|
|
@ -2,12 +2,36 @@ Changelog
|
|||
=========
|
||||
|
||||
|
||||
0.6.1 (unreleased)
|
||||
0.7.1 (unreleased)
|
||||
------------------
|
||||
|
||||
- Nothing changed yet.
|
||||
|
||||
|
||||
0.7.0 (2018-01-17)
|
||||
------------------
|
||||
|
||||
- skip_row override example (#702)
|
||||
|
||||
- Testing against Django 2.0 should not fail (#709)
|
||||
|
||||
- Refactor transaction handling (#690)
|
||||
|
||||
- Resolves #703 fields shadowed (#703)
|
||||
|
||||
- discourage installation as a zipped egg (#548)
|
||||
|
||||
- Fixed middleware settings in test app for Django 2.x (#696)
|
||||
|
||||
|
||||
0.6.1 (2017-12-04)
|
||||
------------------
|
||||
|
||||
- Refactors and optimizations (#686, #632, #684, #636, #631, #629, #635, #683)
|
||||
|
||||
- Travis tests for Django 2.0.x (#691)
|
||||
|
||||
|
||||
0.6.0 (2017-11-23)
|
||||
------------------
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ master_doc = 'index'
|
|||
|
||||
# General information about the project.
|
||||
project = u'django-import-export'
|
||||
copyright = u'2012–2016, Bojan Mihelac'
|
||||
copyright = u'2012–2017, Bojan Mihelac'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
|
|
|
@ -145,20 +145,20 @@ Declaring fields
|
|||
It is possible to override a resource field to change some of its
|
||||
options::
|
||||
|
||||
from import_export import fields
|
||||
from import_export.fields import Field
|
||||
|
||||
class BookResource(resources.ModelResource):
|
||||
published = fields.Field(column_name='published_date')
|
||||
published = Field(column_name='published_date')
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
|
||||
Other fields that don't exist in the target model may be added::
|
||||
|
||||
from import_export import fields
|
||||
from import_export.fields import Field
|
||||
|
||||
class BookResource(resources.ModelResource):
|
||||
myfield = fields.Field(column_name='myfield')
|
||||
myfield = Field(column_name='myfield')
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
|
@ -176,10 +176,10 @@ Not all data can be easily extracted from an object/model attribute.
|
|||
In order to turn complicated data model into a (generally simpler) processed
|
||||
data structure, ``dehydrate_<fieldname>`` method should be defined::
|
||||
|
||||
from import_export import fields
|
||||
from import_export.fields import Field
|
||||
|
||||
class BookResource(resources.ModelResource):
|
||||
full_title = fields.Field()
|
||||
full_title = Field()
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
|
|
|
@ -1 +1 @@
|
|||
__version__ = '0.6.1.dev0'
|
||||
__version__ = '0.7.1.dev0'
|
||||
|
|
|
@ -5,6 +5,7 @@ from . import widgets
|
|||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db.models.manager import Manager
|
||||
from django.db.models.fields import NOT_PROVIDED
|
||||
from django import VERSION
|
||||
|
||||
|
||||
class Field(object):
|
||||
|
@ -102,7 +103,7 @@ class Field(object):
|
|||
value = value()
|
||||
return value
|
||||
|
||||
def save(self, obj, data):
|
||||
def save(self, obj, data, is_m2m=False):
|
||||
"""
|
||||
If this field is not declared readonly, the object's attribute will
|
||||
be set to the value returned by :meth:`~import_export.fields.Field.clean`.
|
||||
|
@ -113,7 +114,12 @@ class Field(object):
|
|||
obj = getattr(obj, attr, None)
|
||||
cleaned = self.clean(data)
|
||||
if cleaned is not None or self.saves_null_values:
|
||||
setattr(obj, attrs[-1], cleaned)
|
||||
if VERSION < (1, 9, 0):
|
||||
setattr(obj, attrs[-1], cleaned)
|
||||
elif not is_m2m:
|
||||
setattr(obj, attrs[-1], cleaned)
|
||||
else:
|
||||
getattr(obj, attrs[-1]).set(cleaned)
|
||||
|
||||
def export(self, obj):
|
||||
"""
|
||||
|
|
|
@ -12,7 +12,7 @@ from django import VERSION
|
|||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.management.color import no_style
|
||||
from django.db import connections, transaction, DEFAULT_DB_ALIAS
|
||||
from django.db import connections, DEFAULT_DB_ALIAS
|
||||
from django.db.models.fields import FieldDoesNotExist
|
||||
from django.db.models.query import QuerySet
|
||||
from django.db.transaction import TransactionManagementError
|
||||
|
@ -23,6 +23,7 @@ from . import widgets
|
|||
from .fields import Field
|
||||
from .instance_loaders import ModelInstanceLoader
|
||||
from .results import Error, Result, RowResult
|
||||
from .utils import atomic_if_using_transaction
|
||||
|
||||
try:
|
||||
from django.db.transaction import atomic, savepoint, savepoint_rollback, savepoint_commit # noqa
|
||||
|
@ -306,13 +307,13 @@ class Resource(six.with_metaclass(DeclarativeMetaclass)):
|
|||
"""
|
||||
pass
|
||||
|
||||
def import_field(self, field, obj, data):
|
||||
def import_field(self, field, obj, data, is_m2m=False):
|
||||
"""
|
||||
Calls :meth:`import_export.fields.Field.save` if ``Field.attribute``
|
||||
and ``Field.column_name`` are found in ``data``.
|
||||
"""
|
||||
if field.attribute and field.column_name in data:
|
||||
field.save(obj, data)
|
||||
field.save(obj, data, is_m2m)
|
||||
|
||||
def get_import_fields(self):
|
||||
return self.get_fields()
|
||||
|
@ -341,7 +342,7 @@ class Resource(six.with_metaclass(DeclarativeMetaclass)):
|
|||
for field in self.get_import_fields():
|
||||
if not isinstance(field.widget, widgets.ManyToManyWidget):
|
||||
continue
|
||||
self.import_field(field, obj, data)
|
||||
self.import_field(field, obj, data, True)
|
||||
|
||||
def for_delete(self, row, instance):
|
||||
"""
|
||||
|
@ -359,6 +360,14 @@ class Resource(six.with_metaclass(DeclarativeMetaclass)):
|
|||
Default implementation returns ``False`` unless skip_unchanged == True.
|
||||
Override this method to handle skipping rows meeting certain
|
||||
conditions.
|
||||
|
||||
Use ``super`` if you want to preserve default handling while overriding
|
||||
::
|
||||
class YourResource(ModelResource):
|
||||
def skip_row(self, instance, original):
|
||||
# Add code here
|
||||
return super(YourResource, self).skip_row(instance, original)
|
||||
|
||||
"""
|
||||
if not self._meta.skip_unchanged:
|
||||
return False
|
||||
|
@ -449,8 +458,7 @@ class Resource(six.with_metaclass(DeclarativeMetaclass)):
|
|||
if self.skip_row(instance, original):
|
||||
row_result.import_type = RowResult.IMPORT_TYPE_SKIP
|
||||
else:
|
||||
with transaction.atomic():
|
||||
self.save_instance(instance, using_transactions, dry_run)
|
||||
self.save_instance(instance, using_transactions, dry_run)
|
||||
self.save_m2m(instance, row, using_transactions, dry_run)
|
||||
diff.compare_with(self, instance, dry_run)
|
||||
row_result.diff = diff.as_html()
|
||||
|
@ -501,10 +509,8 @@ class Resource(six.with_metaclass(DeclarativeMetaclass)):
|
|||
|
||||
using_transactions = (use_transactions or dry_run) and supports_transactions
|
||||
|
||||
if using_transactions:
|
||||
with transaction.atomic():
|
||||
return self.import_data_inner(dataset, dry_run, raise_errors, using_transactions, collect_failed_rows, **kwargs)
|
||||
return self.import_data_inner(dataset, dry_run, raise_errors, using_transactions, collect_failed_rows, **kwargs)
|
||||
with atomic_if_using_transaction(using_transactions):
|
||||
return self.import_data_inner(dataset, dry_run, raise_errors, using_transactions, collect_failed_rows, **kwargs)
|
||||
|
||||
def import_data_inner(self, dataset, dry_run, raise_errors, using_transactions, collect_failed_rows, **kwargs):
|
||||
result = self.get_result_class()()
|
||||
|
@ -517,15 +523,12 @@ class Resource(six.with_metaclass(DeclarativeMetaclass)):
|
|||
sp1 = savepoint()
|
||||
|
||||
try:
|
||||
self.before_import(dataset, using_transactions, dry_run, **kwargs)
|
||||
with atomic_if_using_transaction(using_transactions):
|
||||
self.before_import(dataset, using_transactions, dry_run, **kwargs)
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
tb_info = traceback.format_exc()
|
||||
result.append_base_error(self.get_error_result_class()(e, tb_info))
|
||||
if raise_errors:
|
||||
if using_transactions:
|
||||
savepoint_rollback(sp1)
|
||||
raise
|
||||
|
||||
instance_loader = self._meta.instance_loader_class(self, dataset)
|
||||
|
||||
|
@ -536,30 +539,32 @@ class Resource(six.with_metaclass(DeclarativeMetaclass)):
|
|||
result.add_dataset_headers(dataset.headers)
|
||||
|
||||
for row in dataset.dict:
|
||||
row_result = self.import_row(row, instance_loader,
|
||||
using_transactions=using_transactions, dry_run=dry_run,
|
||||
**kwargs)
|
||||
with atomic_if_using_transaction(using_transactions):
|
||||
row_result = self.import_row(
|
||||
row,
|
||||
instance_loader,
|
||||
using_transactions=using_transactions,
|
||||
dry_run=dry_run,
|
||||
**kwargs
|
||||
)
|
||||
result.increment_row_result_total(row_result)
|
||||
if row_result.errors:
|
||||
if collect_failed_rows:
|
||||
result.append_failed_row(row, row_result.errors[0])
|
||||
if raise_errors:
|
||||
if using_transactions:
|
||||
savepoint_rollback(sp1)
|
||||
raise row_result.errors[-1].error
|
||||
if (row_result.import_type != RowResult.IMPORT_TYPE_SKIP or
|
||||
self._meta.report_skipped):
|
||||
result.append_row_result(row_result)
|
||||
|
||||
try:
|
||||
self.after_import(dataset, result, using_transactions, dry_run, **kwargs)
|
||||
with atomic_if_using_transaction(using_transactions):
|
||||
self.after_import(dataset, result, using_transactions, dry_run, **kwargs)
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
tb_info = traceback.format_exc()
|
||||
result.append_base_error(self.get_error_result_class()(e, tb_info))
|
||||
if raise_errors:
|
||||
if using_transactions:
|
||||
savepoint_rollback(sp1)
|
||||
raise
|
||||
|
||||
if using_transactions:
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
|
||||
class atomic_if_using_transaction(object):
|
||||
"""Context manager wraps `atomic` if `using_transactions`.
|
||||
|
||||
Replaces code::
|
||||
|
||||
if using_transactions:
|
||||
with transaction.atomic():
|
||||
return somethng()
|
||||
return something()
|
||||
"""
|
||||
def __init__(self, using_transactions):
|
||||
self.using_transactions = using_transactions
|
||||
if using_transactions:
|
||||
self.context_manager = transaction.atomic()
|
||||
|
||||
def __enter__(self):
|
||||
if self.using_transactions:
|
||||
self.context_manager.__enter__()
|
||||
|
||||
def __exit__(self, *args):
|
||||
if self.using_transactions:
|
||||
self.context_manager.__exit__(*args)
|
1
setup.py
1
setup.py
|
@ -44,4 +44,5 @@ setup(
|
|||
include_package_data=True,
|
||||
install_requires=install_requires,
|
||||
classifiers=CLASSIFIERS,
|
||||
zip_safe=False,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
[
|
||||
{
|
||||
"model": "core.author",
|
||||
"pk": 11,
|
||||
"fields": {
|
||||
"name": "George R. R. Martin",
|
||||
"birthday": "1948-09-20"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "core.book",
|
||||
"pk": 11,
|
||||
"fields": {
|
||||
"name": "A Game of Thrones",
|
||||
"author": 11,
|
||||
"author_email": "martin@got.com",
|
||||
"imported": false,
|
||||
"published": "1996-08-01",
|
||||
"published_time": "21:00",
|
||||
"price": 25.0,
|
||||
"categories": [1]
|
||||
}
|
||||
}
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.7 on 2017-11-30 01:47
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0005_addparentchild'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='category',
|
||||
name='name',
|
||||
field=models.CharField(max_length=100, unique=True),
|
||||
),
|
||||
]
|
|
@ -17,7 +17,10 @@ class Author(models.Model):
|
|||
|
||||
@python_2_unicode_compatible
|
||||
class Category(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
unique=True,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
|
|
@ -9,7 +9,7 @@ from unittest import skip, skipUnless
|
|||
from django import VERSION
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import IntegrityError
|
||||
from django.db import IntegrityError, DatabaseError
|
||||
from django.db.models import Count
|
||||
from django.db.models.fields import FieldDoesNotExist
|
||||
from django.test import TestCase, TransactionTestCase, skipUnlessDBFeature
|
||||
|
@ -118,6 +118,12 @@ class BookResource(resources.ModelResource):
|
|||
exclude = ('imported', )
|
||||
|
||||
|
||||
class CategoryResource(resources.ModelResource):
|
||||
|
||||
class Meta:
|
||||
model = Category
|
||||
|
||||
|
||||
class ProfileResource(resources.ModelResource):
|
||||
class Meta:
|
||||
model = Profile
|
||||
|
@ -761,6 +767,50 @@ class ModelResourceTransactionTest(TransactionTestCase):
|
|||
# Ensure the rollback has worked properly.
|
||||
self.assertEqual(Profile.objects.count(), 0)
|
||||
|
||||
@skipUnlessDBFeature('supports_transactions')
|
||||
def test_integrity_error_rollback_on_savem2m(self):
|
||||
# savepoint_rollback() after an IntegrityError gives
|
||||
# TransactionManagementError (#399)
|
||||
class CategoryResourceRaisesIntegrityError(CategoryResource):
|
||||
def save_m2m(self, instance, *args, **kwargs):
|
||||
# force raising IntegrityError
|
||||
Category.objects.create(name=instance.name)
|
||||
|
||||
resource = CategoryResourceRaisesIntegrityError()
|
||||
headers = ['id', 'name']
|
||||
rows = [
|
||||
[None, 'foo'],
|
||||
]
|
||||
dataset = tablib.Dataset(*rows, headers=headers)
|
||||
result = resource.import_data(
|
||||
dataset,
|
||||
use_transactions=True,
|
||||
)
|
||||
self.assertTrue(result.has_errors())
|
||||
|
||||
@skipUnlessDBFeature('supports_transactions')
|
||||
def test_multiple_database_errors(self):
|
||||
|
||||
class CategoryResourceDbErrorsResource(CategoryResource):
|
||||
|
||||
def before_import(self, *args, **kwargs):
|
||||
raise DatabaseError()
|
||||
|
||||
def save_instance(self):
|
||||
raise DatabaseError()
|
||||
|
||||
resource = CategoryResourceDbErrorsResource()
|
||||
headers = ['id', 'name']
|
||||
rows = [
|
||||
[None, 'foo'],
|
||||
]
|
||||
dataset = tablib.Dataset(*rows, headers=headers)
|
||||
result = resource.import_data(
|
||||
dataset,
|
||||
use_transactions=True,
|
||||
)
|
||||
self.assertTrue(result.has_errors())
|
||||
|
||||
|
||||
class ModelResourceFactoryTest(TestCase):
|
||||
|
||||
|
@ -834,7 +884,7 @@ if 'postgresql' in settings.DATABASES['default']['ENGINE']:
|
|||
|
||||
|
||||
class ManyRelatedManagerDiffTest(TestCase):
|
||||
fixtures = ["category"]
|
||||
fixtures = ["category", "book"]
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
|
Binary file not shown.
|
@ -24,7 +24,7 @@ STATIC_URL = '/static/'
|
|||
|
||||
SECRET_KEY = '2n6)=vnp8@bu0om9d05vwf7@=5vpn%)97-!d*t4zq1mku%0-@j'
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
MIDDLEWARE = (
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
|
@ -33,6 +33,9 @@ MIDDLEWARE_CLASSES = (
|
|||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
)
|
||||
|
||||
# For backwards compatibility for Django 1.8
|
||||
MIDDLEWARE_CLASSES = MIDDLEWARE
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
|
|
Loading…
Reference in New Issue