Merge branch 'master' into master

This commit is contained in:
Serhiy Zahoriya 2017-12-28 21:56:15 +02:00 committed by GitHub
commit 0d08f30468
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 173 additions and 37 deletions

View File

@ -40,6 +40,8 @@ matrix:
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.5"
env: DJANGO="Django>=1.6,<1.7"
- python: "3.5"
@ -55,5 +57,4 @@ matrix:
- python: "3.6"
env: DJANGO="Django>=1.10,<1.11"
allow_failures:
- env: DJANGO="Django>=2.0,<2.1"
- env: DJANGO="https://github.com/django/django/archive/master.tar.gz"

View File

@ -92,4 +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)

View File

@ -5,7 +5,9 @@ Changelog
0.6.2 (unreleased)
------------------
- Nothing changed yet.
- discourage installation as a zipped egg (#548)
- Fixed middleware settings in test app for Django 2.x (#696)
0.6.1 (2017-12-04)

View File

@ -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

View File

@ -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):
"""

View File

@ -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
@ -316,13 +317,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()
@ -351,7 +352,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):
"""
@ -467,8 +468,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()
@ -519,10 +519,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()()
@ -535,15 +533,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)
@ -554,30 +549,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:

27
import_export/utils.py Normal file
View File

@ -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)

View File

@ -44,4 +44,5 @@ setup(
include_package_data=True,
install_requires=install_requires,
classifiers=CLASSIFIERS,
zip_safe=False,
)

View File

@ -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]
}
}
]

View File

@ -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),
),
]

View File

@ -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

View File

@ -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 VERSION >= (1, 8) and 'postgresql' in settings.DATABASES['default']['ENGINE']
class ManyRelatedManagerDiffTest(TestCase):
fixtures = ["category"]
fixtures = ["category", "book"]
def setUp(self):
pass

Binary file not shown.

View File

@ -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',