Add admin import action and lot of other work

This commit is contained in:
Bojan Mihelac 2012-11-16 08:51:32 +01:00
parent 704c9a6a66
commit a0ae56e4e3
16 changed files with 318 additions and 12 deletions

View File

@ -1,3 +1,16 @@
====================
django-import-export
====================
Example app
-----------
::
cd tests && ./manage.py runserver
Username and password for admin are 'admin', 'password'.
TODO
----
* exporting

88
import_export/admin.py Normal file
View File

@ -0,0 +1,88 @@
import tempfile
import tablib
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from django.conf.urls.defaults import patterns, url
from django.template.response import TemplateResponse
from django.contrib import messages
from django.http import HttpResponseRedirect
from .forms import ImportForm
from .core import Importer
class ImportMixin(object):
change_list_template = 'admin/import_export/change_list_import.html'
import_template_name = 'admin/import_export/import.html'
importer_class = Importer
format_choices = (
('', '---'),
('csv', 'CSV'),
('xls', 'Excel XLS'),
)
def get_urls(self):
urls = super(ImportMixin, self).get_urls()
info = self.model._meta.app_label, self.model._meta.module_name
my_urls = patterns('',
url(r'^import/$',
self.admin_site.admin_view(self.import_action),
name='%s_%s_import' % info),
)
return my_urls + urls
def get_importer_class(self):
return self.importer_class
def get_format(self, format_name):
return getattr(tablib.formats, format_name)
def import_action(self, request, *args, **kwargs):
importer_class = self.get_importer_class()
if request.POST and request.POST.get('tmp_file'):
import_file = open(request.POST.get('tmp_file'))
result = self.importer_class(import_file,
model=self.model,
format=self.get_format(request.POST.get('input_format')),
dry_run=False,
raise_errors=True).run()
success_message = _('Import finished')
messages.success(request, success_message)
import_file.close()
return HttpResponseRedirect('..')
form = ImportForm(self.format_choices,
request.POST or None,
request.FILES or None)
result = None
tmp_file_name = None
input_format = None
if request.POST:
if form.is_valid():
import_file = request.FILES['import_file']
input_format = form.cleaned_data['input_format']
result = importer_class(import_file,
model=self.model,
raise_errors=False,
format=self.get_format(input_format),
dry_run=True).run()
if not result.has_errors():
tmp_file = tempfile.NamedTemporaryFile(delete=False)
for chunk in import_file.chunks():
tmp_file.write(chunk)
tmp_file.close()
tmp_file_name = tmp_file.name
fields = importer_class(model=self.model).get_mapping().keys()
context = {
'form': form,
'result': result,
'opts': self.model._meta,
'fields': fields,
'tmp_file': tmp_file_name,
'input_format': input_format,
}
return TemplateResponse(request, [self.import_template_name],
context, current_app=self.admin_site.name)

View File

@ -12,6 +12,9 @@ class RowResult(object):
self.orig_fields = []
self.fields = []
def combined_fields(self):
return zip(self.orig_fields, self.fields)
class Result(list):
@ -20,7 +23,8 @@ class Result(list):
self.base_errors = []
def row_errors(self):
return [(i, row.errors) for i, row in enumerate(self) if row.errors]
return [(i + 1, row.errors)
for i, row in enumerate(self) if row.errors]
def has_errors(self):
return self.base_errors or self.row_errors()
@ -30,15 +34,19 @@ class Importer(object):
model = None
format = None
import_code = "ID"
raise_errors = True
raise_errors = False
dry_run = True
mapping = None
from_encoding = None
def __init__(self, f, **kwargs):
def __init__(self, f=None, **kwargs):
self.f = f
for key, value in kwargs.iteritems():
setattr(self, key, value)
def set_stream(self, f):
self.f = f
def get_mapping(self):
if self.mapping:
return self.mapping
@ -46,7 +54,10 @@ class Importer(object):
return OrderedDict(mapping)
def load_dataset(self):
text = unicode(self.f.read(), 'cp1250').encode('utf-8')
if self.from_encoding:
text = unicode(self.f.read(), self.from_encoding).encode('utf-8')
else:
text = self.f.read()
if not self.format:
self.data = tablib.import_set(text)
else:
@ -85,7 +96,7 @@ class Importer(object):
self.load_dataset()
except Exception, e:
result.base_errors.append(_('Loading error') +
u': %s' % unicode(e))
u': %s' % repr(e))
if self.raise_errors:
raise
return result
@ -100,7 +111,7 @@ class Importer(object):
self.save_instance(instance)
row_result.fields = self.get_representation(instance)
except Exception, e:
row_result.errors.append(unicode(e))
row_result.errors.append(repr(e))
if self.raise_errors:
raise
result.append(row_result)

16
import_export/forms.py Normal file
View File

@ -0,0 +1,16 @@
from django import forms
from django.utils.translation import ugettext_lazy as _
class ImportForm(forms.Form):
import_file = forms.FileField(
label=_('File to import')
)
input_format = forms.ChoiceField(
label=_('Format'),
choices=(),
)
def __init__(self, format_choices, *args, **kwargs):
super(ImportForm, self).__init__(*args, **kwargs)
self.fields['input_format'].choices = format_choices

View File

@ -0,0 +1,17 @@
{% extends "admin/base_site.html" %}
{% load i18n admin_static admin_modify %}
{% load admin_urls %}
{% load url from future %}
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}" />{% endblock %}
{% block bodyclass %}{{ opts.app_label }}-{{ opts.object_name.lower }} change-form{% endblock %}
{% if not is_popup %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_label|capfirst|escape }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
&rsaquo; {% block breadcrumbs_last %}{% endblock %}
</div>
{% endblock %}
{% endif %}

View File

@ -0,0 +1,8 @@
{% extends "admin/change_list.html" %}
{% load i18n %}
{% block object-tools-items %}
<li><a href="import/" class="import_link">{% trans "Import" %}</a></li>
{{ block.super }}
{% endblock %}

View File

@ -0,0 +1,93 @@
{% extends "admin/import_export/base.html" %}
{% load i18n %}
{% load import_export_tags %}
{% block breadcrumbs_last %}
{% trans "Import" %}
{% endblock %}
{% block content %}
<h1>{% trans "Import" %}</h1>
{% if not result or result.has_errors %}
<form action="{{ form_url }}" method="post" id="{{ opts.module_name }}_form" enctype="multipart/form-data">
{% csrf_token %}
<p>
{% trans "This importer excpect and will import following fields: " %}
{{ fields|join:", " }}.
</p>
<fieldset class="module aligned">
{% for field in form %}
<div class="form-row">
{{ field.errors }}
{{ field.label_tag }}
{{ field }}
{% if field.field.help_text %}
<p class="help">{{ field.field.help_text|safe }}</p>
{% endif %}
</div>
{% endfor %}
</fieldset>
<div class="submit-row">
<input type="submit" class="default" value="{% trans "Submit" %}">
</div>
</form>
{% endif %}
{% if result %}
{% if result.has_errors %}
<h2>{% trans "Errors" %}</h2>
<ul>
{% for error in result.base_errors %}
<li>{{ error }}</li>
{% endfor %}
{% for line, error in result.row_errors %}
<li>{% trans "Line number" %}: {{ line }} - {{ error }}</li>
{% endfor %}
</ul>
{% else %}
<form action="" method="POST">
{% csrf_token %}
<fieldset class="module aligned">
<input type="hidden" name="tmp_file" value="{{ tmp_file }}">
<input type="hidden" name="input_format" value="{{ input_format }}">
<p>
{% trans "Bellow is a preview of applied import data. If you are satisfied with a result click Confirm import" %}
</p>
<div class="submit-row">
<input type="submit" class="default" name="confirm" value="{% trans "Confirm import" %}">
</div>
</fieldset>
</form>
{% endif %}
<h2>
{% trans "Preview" %}
</h2>
<table>
<thead>
<tr>
{% for field in fields %}
<th>{{ field }}</th>
{% endfor %}
</tr>
</thead>
{% for row in result %}
<tr>
{% for f in row.combined_fields %}
<td>
{% compare_values f.0 f.1 %}
</td>
{% endfor %}
</tr>
{% endfor %}
</table>
{% endif %}
{% endblock %}

View File

View File

@ -0,0 +1,13 @@
from diff_match_patch import diff_match_patch
from django import template
register = template.Library()
@register.simple_tag
def compare_values(value1, value2):
dmp = diff_match_patch()
diff = dmp.diff_main(value1, value2)
dmp.diff_cleanupSemantic(diff)
html = dmp.diff_prettyHtml(diff)
return html

View File

@ -1,2 +1,3 @@
Django>=1.4
tablib
diff-match-patch

View File

@ -15,6 +15,7 @@ CLASSIFIERS = [
install_requires = [
'tablib',
'Django>=1.4.2',
'diff-match-patch',
]
setup(

12
tests/core/admin.py Normal file
View File

@ -0,0 +1,12 @@
from django.contrib import admin
from import_export.admin import ImportMixin
from .models import Book
class BookAdmin(ImportMixin, admin.ModelAdmin):
pass
admin.site.register(Book, BookAdmin)

BIN
tests/database.db Normal file

Binary file not shown.

12
tests/manage.py Executable file
View File

@ -0,0 +1,12 @@
#!/usr/bin/env python
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.path.pardir))
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)

View File

@ -1,9 +1,4 @@
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'haystack_tests.db',
}
}
import os.path
INSTALLED_APPS = [
'django.contrib.admin',
@ -18,3 +13,16 @@ INSTALLED_APPS = [
]
SITE_ID = 1
ROOT_URLCONF = "urls"
DEBUG = True
STATIC_URL = '/static/'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(os.path.dirname(__file__), 'database.db'),
}
}

13
tests/urls.py Normal file
View File

@ -0,0 +1,13 @@
from django.conf.urls.defaults import patterns, include
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
(r'^admin/', include(admin.site.urls)),
)
urlpatterns += staticfiles_urlpatterns()