Skip to content

Commit d2079fd

Browse files
committedJan 15, 2013
ADD support for deleting objects during import
1 parent 36bea77 commit d2079fd

File tree

8 files changed

+123
-39
lines changed

8 files changed

+123
-39
lines changed
 

‎docs/changelog.rst

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ Change Log
55
0.1.2 (not released)
66
====================
77

8+
* support for deleting objects during import
9+
810
* bug fixes
911

1012
* Allowing a field to be 'dehydrated' with a custom method

‎docs/getting_started.rst

+20
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,26 @@ check for any errors and import data.
203203
:doc:`/import_workflow`
204204
for detailed import workflow descripton and customization options.
205205

206+
Deleting data
207+
^^^^^^^^^^^^^
208+
209+
To delete objects during import, implement ``for_delete`` method on resource
210+
class.
211+
212+
Example resource with ``delete`` field::
213+
214+
class BookResource(resources.ModelResource):
215+
delete = fields.Field(widget=widgets.BooleanWidget())
216+
217+
def for_delete(self, row, instance):
218+
return self.fields['delete'].clean(row)
219+
220+
class Meta:
221+
model = Book
222+
223+
Import of this resource will delete model instances for rows
224+
that have column ``delete`` set to ``1``.
225+
206226
Admin integration
207227
-----------------
208228

‎docs/import_workflow.rst

+38-31
Original file line numberDiff line numberDiff line change
@@ -57,38 +57,45 @@ responsible for import data from given `dataset`.
5757
arguments. You can override ``init_instance`` method to manipulate how
5858
new objects are initialized (ie: to set default values).
5959

60-
#. ``import_obj`` method is called with current object `instance` and
61-
current `row`.
62-
63-
``import_obj`` loop through all `Resource` `fields`, skipping
64-
many to many fields and calls ``import_field`` for each. (Many to many
65-
fields require that instance have a primary key, this is why assigning
66-
them is postponed, after object is saved).
67-
68-
``import_field`` calls ``field.save`` method, if ``field`` has
69-
both `attribute` and field `column_name` exists in given row.
70-
71-
#. ``save_instance`` method is called.
72-
73-
``save_instance`` receives ``dry_run`` argument and actually saves
74-
instance only when ``dry_run`` is False.
75-
76-
``save_instance`` calls two hooks methods that by default does not
77-
do anything but can be overriden to customize import process:
78-
79-
* ``before_save_instance``
80-
81-
* ``after_save_instance``
82-
83-
Both methods receive ``instance`` and ``dry_run`` arguments.
84-
85-
#. ``save_m2m`` method is called to save many to many fields.
86-
60+
#. ``for_delete`` method is called to determine if current `instance`
61+
should be deleted:
62+
63+
#. current `instance` is deleted
64+
65+
OR
66+
67+
#. ``import_obj`` method is called with current object `instance` and
68+
current `row`.
69+
70+
``import_obj`` loop through all `Resource` `fields`, skipping
71+
many to many fields and calls ``import_field`` for each. (Many to many
72+
fields require that instance have a primary key, this is why assigning
73+
them is postponed, after object is saved).
74+
75+
``import_field`` calls ``field.save`` method, if ``field`` has
76+
both `attribute` and field `column_name` exists in given row.
77+
78+
#. ``save_instance`` method is called.
79+
80+
``save_instance`` receives ``dry_run`` argument and actually saves
81+
instance only when ``dry_run`` is False.
82+
83+
``save_instance`` calls two hooks methods that by default does not
84+
do anything but can be overriden to customize import process:
85+
86+
* ``before_save_instance``
87+
88+
* ``after_save_instance``
89+
90+
Both methods receive ``instance`` and ``dry_run`` arguments.
91+
92+
#. ``save_m2m`` method is called to save many to many fields.
93+
8794
#. ``RowResult`` is assigned with diff between original and imported
88-
object fields as well as import type(new, updated).
89-
90-
If exception is raised inside row processing, and ``raise_errors`` is
91-
``False`` (default), traceback is appended to ``RowResult``.
95+
object fields as well as import type(new, updated).
96+
97+
If exception is raised inside row processing, and ``raise_errors`` is
98+
``False`` (default), traceback is appended to ``RowResult``.
9299

93100
#. ``result`` is returned.
94101

‎docs/todo.rst

-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
TODO
33
====
44

5-
* support for deleting objects for importing data
6-
75
* DB transactions support for importing data
86

97
* support for exporting multiple formats in admin integration

‎import_export/resources.py

+44-6
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,24 @@ def after_save_instance(self, instance, dry_run):
129129
"""
130130
pass
131131

132+
def delete_instance(self, instance, dry_run=False):
133+
self.before_delete_instance(instance, dry_run)
134+
if not dry_run:
135+
instance.delete()
136+
self.after_delete_instance(instance, dry_run)
137+
138+
def before_delete_instance(self, instance, dry_run):
139+
"""
140+
Override to add additional logic.
141+
"""
142+
pass
143+
144+
def after_delete_instance(self, instance, dry_run):
145+
"""
146+
Override to add additional logic.
147+
"""
148+
pass
149+
132150
def import_field(self, field, obj, data):
133151
if field.attribute and field.column_name in data:
134152
field.save(obj, data)
@@ -154,6 +172,15 @@ def save_m2m(self, obj, data, dry_run):
154172
continue
155173
self.import_field(field, obj, data)
156174

175+
def for_delete(self, row, instance):
176+
"""
177+
Returns ``True`` if ``row`` importing should delete instance.
178+
179+
Default implementation returns ``False``.
180+
Override this method to handle deletion.
181+
"""
182+
return False
183+
157184
def get_diff(self, original, current, dry_run=False):
158185
"""
159186
Get diff between original and current object when ``import_data``
@@ -165,8 +192,8 @@ def get_diff(self, original, current, dry_run=False):
165192
data = []
166193
dmp = diff_match_patch()
167194
for field in self.get_fields():
168-
v1 = self.export_field(field, original)
169-
v2 = self.export_field(field, current)
195+
v1 = self.export_field(field, original) if original else ""
196+
v2 = self.export_field(field, current) if current else ""
170197
diff = dmp.diff_main(unicode(v1), unicode(v2))
171198
dmp.diff_cleanupSemantic(diff)
172199
html = dmp.diff_prettyHtml(diff)
@@ -193,10 +220,21 @@ def import_data(self, dataset, dry_run=False, raise_errors=False):
193220
row_result.import_type = RowResult.IMPORT_TYPE_UPDATE
194221
row_result.new_record = new
195222
original = deepcopy(instance)
196-
self.import_obj(instance, row)
197-
self.save_instance(instance, dry_run)
198-
self.save_m2m(instance, row, dry_run)
199-
row_result.diff = self.get_diff(original, instance, dry_run)
223+
if self.for_delete(row, instance):
224+
if new:
225+
row_result.import_type = RowResult.IMPORT_TYPE_SKIP
226+
row_result.diff = self.get_diff(None, None, dry_run)
227+
else:
228+
row_result.import_type = RowResult.IMPORT_TYPE_DELETE
229+
self.delete_instance(instance, dry_run)
230+
row_result.diff = self.get_diff(original, None,
231+
dry_run)
232+
else:
233+
self.import_obj(instance, row)
234+
self.save_instance(instance, dry_run)
235+
self.save_m2m(instance, row, dry_run)
236+
row_result.diff = self.get_diff(original, instance,
237+
dry_run)
200238
except Exception, e:
201239
tb_info = traceback.format_exc(sys.exc_info()[2])
202240
row_result.errors.append(Error(repr(e), tb_info))

‎import_export/results.py

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class RowResult(object):
99
IMPORT_TYPE_UPDATE = 'update'
1010
IMPORT_TYPE_NEW = 'new'
1111
IMPORT_TYPE_DELETE = 'delete'
12+
IMPORT_TYPE_SKIP = 'skip'
1213

1314
def __init__(self):
1415
self.errors = []

‎import_export/templates/admin/import_export/import.html

+2
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ <h2>
9595
<td>
9696
{% if row.import_type == 'new' %}
9797
{% trans "New" %}
98+
{% elif row.import_type == 'skip' %}
99+
{% trans "Skipped" %}
98100
{% elif row.import_type == 'delete' %}
99101
{% trans "Delete" %}
100102
{% elif row.import_type == 'update' %}

‎tests/core/tests/resources_tests.py

+16
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,22 @@ def test_import_data_error_saving_model(self):
143143
msg = 'ValueError("invalid literal for int() with base 10: \'foo\'",)'
144144
self.assertTrue(result.rows[0].errors[0].error, msg)
145145

146+
def test_import_data_delete(self):
147+
148+
class B(BookResource):
149+
delete = fields.Field(widget=widgets.BooleanWidget())
150+
151+
def for_delete(self, row, instance):
152+
return self.fields['delete'].clean(row)
153+
154+
row = [self.book.pk, self.book.name, '1']
155+
dataset = tablib.Dataset(*[row], headers=['id', 'name', 'delete'])
156+
result = B().import_data(dataset, raise_errors=True)
157+
self.assertFalse(result.has_errors())
158+
self.assertEqual(result.rows[0].import_type,
159+
results.RowResult.IMPORT_TYPE_DELETE)
160+
self.assertFalse(Book.objects.filter(pk=self.book.pk))
161+
146162
def test_relationships_fields(self):
147163

148164
class B(resources.ModelResource):

0 commit comments

Comments
 (0)
Please sign in to comment.