Skip to content

Commit eb9e005

Browse files
committedDec 23, 2012
Work on admin integration:
* added ExportForm to admin integration for choosing export file format * refactor admin integration to allow better handling of specific formats supported features and better handling of reading text files * include all avialable formats in Admin integration
1 parent 8edbe65 commit eb9e005

File tree

7 files changed

+308
-81
lines changed

7 files changed

+308
-81
lines changed
 

‎docs/changelog.rst

+9
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22
Change Log
33
===========
44

5+
0.1.1dev
6+
========
7+
8+
* added ExportForm to admin integration for choosing export file format
9+
10+
* refactor admin integration to allow better handling of specific formats
11+
supported features and better handling of reading text files
12+
13+
* include all avialable formats in Admin integration
514

615
0.1.0
716
=====

‎import_export/admin.py

+86-78
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,34 @@
11
import tempfile
22
from datetime import datetime
33

4-
import tablib
5-
64
from django.contrib import admin
75
from django.utils.translation import ugettext_lazy as _
86
from django.conf.urls.defaults import patterns, url
97
from django.template.response import TemplateResponse
108
from django.contrib import messages
119
from django.http import HttpResponseRedirect, HttpResponse
12-
from django.utils.importlib import import_module
1310
from django.core.urlresolvers import reverse
1411

1512
from .forms import (
1613
ImportForm,
1714
ConfirmImportForm,
15+
ExportForm,
1816
)
1917
from .resources import (
2018
modelresource_factory,
2119
)
20+
from .formats import base_formats
21+
22+
23+
DEFAULT_FORMATS = (
24+
base_formats.CSV,
25+
base_formats.XLS,
26+
base_formats.TSV,
27+
base_formats.ODS,
28+
base_formats.JSON,
29+
base_formats.YAML,
30+
base_formats.HTML,
31+
)
2232

2333

2434
class ImportMixin(object):
@@ -29,10 +39,7 @@ class ImportMixin(object):
2939
change_list_template = 'admin/import_export/change_list_import.html'
3040
import_template_name = 'admin/import_export/import.html'
3141
resource_class = None
32-
format_choices = (
33-
('', '---'),
34-
('tablib.formats._csv', 'CSV'),
35-
)
42+
formats = DEFAULT_FORMATS
3643
from_encoding = "utf-8"
3744

3845
def get_urls(self):
@@ -54,50 +61,29 @@ def get_resource_class(self):
5461
else:
5562
return self.resource_class
5663

57-
def get_format(self, format_class):
58-
if format_class:
59-
return import_module(format_class)
60-
return None
61-
62-
def get_mode_for_format(self, format):
63-
"""
64-
Returns mode for opening files.
64+
def get_import_formats(self):
6565
"""
66-
return 'rU'
67-
68-
def load_dataset(self, stream, input_format=None, from_encoding=None):
69-
"""
70-
Loads data from ``stream`` given valid tablib ``input_format``
71-
and returns tablib dataset
72-
73-
If ``from_encoding`` is specified, data will be converted to `utf-8`
74-
characterset.
66+
Returns available import formats.
7567
"""
76-
if from_encoding:
77-
text = unicode(stream.read(), from_encoding).encode('utf-8')
78-
else:
79-
text = stream.read()
80-
if not input_format:
81-
data = tablib.import_set(text)
82-
else:
83-
data = tablib.Dataset()
84-
input_format.import_set(data, text)
85-
return data
68+
return [f for f in self.formats if f().can_import()]
8669

8770
def process_import(self, request, *args, **kwargs):
8871
opts = self.model._meta
8972
resource = self.get_resource_class()()
9073

9174
confirm_form = ConfirmImportForm(request.POST)
9275
if confirm_form.is_valid():
93-
input_format = self.get_format(
94-
confirm_form.cleaned_data['input_format'])
95-
import_mode = self.get_mode_for_format(input_format)
76+
import_formats = self.get_import_formats()
77+
input_format = import_formats[
78+
int(confirm_form.cleaned_data['input_format'])
79+
]()
9680
import_file = open(confirm_form.cleaned_data['import_file_name'],
97-
import_mode)
81+
input_format.get_read_mode())
82+
data = import_file.read()
83+
if not input_format.is_binary() and self.from_encoding:
84+
data = unicode(data, self.from_encoding).encode('utf-8')
85+
dataset = input_format.create_dataset(data)
9886

99-
dataset = self.load_dataset(import_file, input_format,
100-
self.from_encoding)
10187
resource.import_data(dataset, dry_run=False,
10288
raise_errors=True)
10389

@@ -115,34 +101,34 @@ def import_action(self, request, *args, **kwargs):
115101

116102
context = {}
117103

118-
form = ImportForm(self.format_choices,
104+
import_formats = self.get_import_formats()
105+
form = ImportForm(import_formats,
119106
request.POST or None,
120107
request.FILES or None)
121108

122-
if request.POST:
123-
if form.is_valid():
124-
input_format = self.get_format(
125-
form.cleaned_data['input_format'])
126-
import_mode = self.get_mode_for_format(input_format)
127-
import_file = form.cleaned_data['import_file']
128-
import_file.open(import_mode)
129-
130-
dataset = self.load_dataset(import_file, input_format,
131-
self.from_encoding)
132-
result = resource.import_data(dataset, dry_run=True,
133-
raise_errors=False)
134-
135-
context['result'] = result
136-
137-
if not result.has_errors():
138-
tmp_file = tempfile.NamedTemporaryFile(delete=False)
139-
for chunk in import_file.chunks():
140-
tmp_file.write(chunk)
141-
tmp_file.close()
142-
context['confirm_form'] = ConfirmImportForm(initial={
143-
'import_file_name': tmp_file.name,
144-
'input_format': form.cleaned_data['input_format'],
145-
})
109+
if request.POST and form.is_valid():
110+
input_format = import_formats[
111+
int(form.cleaned_data['input_format'])
112+
]()
113+
import_file = form.cleaned_data['import_file']
114+
import_file.open(input_format.get_read_mode())
115+
data = import_file.read()
116+
if not input_format.is_binary() and self.from_encoding:
117+
data = unicode(data, self.from_encoding).encode('utf-8')
118+
dataset = input_format.create_dataset(data)
119+
result = resource.import_data(dataset, dry_run=True,
120+
raise_errors=False)
121+
122+
context['result'] = result
123+
124+
if not result.has_errors():
125+
tmp_file = tempfile.NamedTemporaryFile(delete=False)
126+
tmp_file.write(data)
127+
tmp_file.close()
128+
context['confirm_form'] = ConfirmImportForm(initial={
129+
'import_file_name': tmp_file.name,
130+
'input_format': form.cleaned_data['input_format'],
131+
})
146132

147133
context['form'] = form
148134
context['opts'] = self.model._meta
@@ -158,7 +144,8 @@ class ExportMixin(object):
158144
"""
159145
resource_class = None
160146
change_list_template = 'admin/import_export/change_list_export.html'
161-
export_format = 'csv'
147+
export_template_name = 'admin/import_export/export.html'
148+
formats = DEFAULT_FORMATS
162149
to_encoding = "utf-8"
163150

164151
def get_urls(self):
@@ -177,10 +164,17 @@ def get_resource_class(self):
177164
else:
178165
return self.resource_class
179166

180-
def get_export_filename(self):
167+
def get_export_formats(self):
168+
"""
169+
Returns available import formats.
170+
"""
171+
return [f for f in self.formats if f().can_export()]
172+
173+
def get_export_filename(self, file_format):
181174
date_str = datetime.now().strftime('%Y-%m-%d')
182175
filename = "%s-%s.%s" % (self.model.__name__,
183-
date_str, self.export_format)
176+
date_str,
177+
file_format.get_extension())
184178
return filename
185179

186180
def get_export_queryset(self, request):
@@ -203,16 +197,30 @@ def get_export_queryset(self, request):
203197
return cl.query_set
204198

205199
def export_action(self, request, *args, **kwargs):
206-
resource_class = self.get_resource_class()
207-
queryset = self.get_export_queryset(request)
208-
data = resource_class().export(queryset)
209-
filename = self.get_export_filename()
210-
response = HttpResponse(
211-
getattr(data, self.export_format),
212-
mimetype='application/octet-stream',
213-
)
214-
response['Content-Disposition'] = 'attachment; filename=%s' % filename
215-
return response
200+
formats = self.get_export_formats()
201+
form = ExportForm(formats, request.POST or None)
202+
if form.is_valid():
203+
file_format = formats[
204+
int(form.cleaned_data['file_format'])
205+
]()
206+
207+
resource_class = self.get_resource_class()
208+
queryset = self.get_export_queryset(request)
209+
data = resource_class().export(queryset)
210+
response = HttpResponse(
211+
file_format.export_data(data),
212+
mimetype='application/octet-stream',
213+
)
214+
response['Content-Disposition'] = 'attachment; filename=%s' % (
215+
self.get_export_filename(file_format),
216+
)
217+
return response
218+
219+
context = {}
220+
context['form'] = form
221+
context['opts'] = self.model._meta
222+
return TemplateResponse(request, [self.export_template_name],
223+
context, current_app=self.admin_site.name)
216224

217225

218226
class ImportExportMixin(ImportMixin, ExportMixin):

‎import_export/formats/__init__.py

Whitespace-only changes.

‎import_export/formats/base_formats.py

+144
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import warnings
2+
import tablib
3+
4+
try:
5+
from tablib.compat import xlrd
6+
XLS_IMPORT = True
7+
except ImportError:
8+
xls_warning = "Installed `tablib` library does not include"
9+
"import support for 'xls' format."
10+
warnings.warn(xls_warning, ImportWarning)
11+
XLS_IMPORT = False
12+
13+
from django.utils.importlib import import_module
14+
15+
16+
class Format(object):
17+
18+
def get_title(self):
19+
return type(self)
20+
21+
def create_dataset(self, in_stream):
22+
"""
23+
Create dataset from given string.
24+
"""
25+
raise NotImplementedError()
26+
27+
def export_data(self, dataset):
28+
"""
29+
Returns format representation for given dataset.
30+
"""
31+
raise NotImplementedError()
32+
33+
def is_binary(self):
34+
"""
35+
Returns if this format is binary.
36+
"""
37+
return True
38+
39+
def get_read_mode(self):
40+
"""
41+
Returns mode for opening files.
42+
"""
43+
return 'r'
44+
45+
def get_extension(self):
46+
"""
47+
Returns extension for this format files.
48+
"""
49+
return ""
50+
51+
def can_import(self):
52+
return False
53+
54+
def can_export(self):
55+
return False
56+
57+
58+
class TablibFormat(Format):
59+
TABLIB_MODULE = None
60+
61+
def get_format(self):
62+
"""
63+
Import and returns tablib module.
64+
"""
65+
return import_module(self.TABLIB_MODULE)
66+
67+
def get_title(self):
68+
return self.get_format().title
69+
70+
def create_dataset(self, in_stream):
71+
data = tablib.Dataset()
72+
self.get_format().import_set(data, in_stream)
73+
return data
74+
75+
def export_data(self, dataset):
76+
return self.get_format().export_set(dataset)
77+
78+
def get_extension(self):
79+
return self.get_format().extensions[0]
80+
81+
def can_import(self):
82+
return hasattr(self.get_format(), 'import_set')
83+
84+
def can_export(self):
85+
return hasattr(self.get_format(), 'export_set')
86+
87+
88+
class TextFormat(TablibFormat):
89+
90+
def get_read_mode(self):
91+
return 'rU'
92+
93+
def is_binary(self):
94+
return False
95+
96+
97+
class CSV(TextFormat):
98+
TABLIB_MODULE = 'tablib.formats._csv'
99+
100+
101+
class JSON(TextFormat):
102+
TABLIB_MODULE = 'tablib.formats._json'
103+
104+
105+
class YAML(TextFormat):
106+
TABLIB_MODULE = 'tablib.formats._yaml'
107+
108+
109+
class TSV(TextFormat):
110+
TABLIB_MODULE = 'tablib.formats._tsv'
111+
112+
113+
class ODS(TextFormat):
114+
TABLIB_MODULE = 'tablib.formats._ods'
115+
116+
117+
class XLSX(TextFormat):
118+
TABLIB_MODULE = 'tablib.formats._xlsx'
119+
120+
121+
class HTML(TextFormat):
122+
TABLIB_MODULE = 'tablib.formats._html'
123+
124+
125+
class XLS(TextFormat):
126+
TABLIB_MODULE = 'tablib.formats._xls'
127+
128+
def can_import(self):
129+
return XLS_IMPORT
130+
131+
def create_dataset(self, in_stream):
132+
"""
133+
Create dataset from first sheet.
134+
"""
135+
assert XLS_IMPORT
136+
xls_book = xlrd.open_workbook(file_contents=in_stream)
137+
dataset = tablib.Dataset()
138+
sheet = xls_book.sheets()[0]
139+
for i in xrange(sheet.nrows):
140+
if i == 0:
141+
dataset.headers = sheet.row_values(0)
142+
else:
143+
dataset.append(sheet.row_values(i))
144+
return dataset

‎import_export/forms.py

+25-2
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,34 @@ class ImportForm(forms.Form):
1111
choices=(),
1212
)
1313

14-
def __init__(self, format_choices, *args, **kwargs):
14+
def __init__(self, import_formats, *args, **kwargs):
1515
super(ImportForm, self).__init__(*args, **kwargs)
16-
self.fields['input_format'].choices = format_choices
16+
choices = []
17+
for i, f in enumerate(import_formats):
18+
choices.append((str(i), f().get_title(),))
19+
if len(import_formats) > 1:
20+
choices.insert(0, ('', '---'))
21+
22+
self.fields['input_format'].choices = choices
1723

1824

1925
class ConfirmImportForm(forms.Form):
2026
import_file_name = forms.CharField(widget=forms.HiddenInput())
2127
input_format = forms.CharField(widget=forms.HiddenInput())
28+
29+
30+
class ExportForm(forms.Form):
31+
file_format = forms.ChoiceField(
32+
label=_('Format'),
33+
choices=(),
34+
)
35+
36+
def __init__(self, formats, *args, **kwargs):
37+
super(ExportForm, self).__init__(*args, **kwargs)
38+
choices = []
39+
for i, f in enumerate(formats):
40+
choices.append((str(i), f().get_title(),))
41+
if len(formats) > 1:
42+
choices.insert(0, ('', '---'))
43+
44+
self.fields['file_format'].choices = choices
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{% extends "admin/import_export/base.html" %}
2+
{% load url from future %}
3+
{% load i18n %}
4+
{% load admin_urls %}
5+
{% load import_export_tags %}
6+
7+
{% block breadcrumbs_last %}
8+
{% trans "Export" %}
9+
{% endblock %}
10+
11+
{% block content %}
12+
<h1>{% trans "Export" %}</h1>
13+
14+
<form action="{% url opts|admin_urlname:"export" %}" method="POST">
15+
{% csrf_token %}
16+
17+
<fieldset class="module aligned">
18+
{% for field in form %}
19+
<div class="form-row">
20+
{{ field.errors }}
21+
22+
{{ field.label_tag }}
23+
24+
{{ field }}
25+
26+
{% if field.field.help_text %}
27+
<p class="help">{{ field.field.help_text|safe }}</p>
28+
{% endif %}
29+
</div>
30+
{% endfor %}
31+
</fieldset>
32+
33+
<div class="submit-row">
34+
<input type="submit" class="default" value="{% trans "Submit" %}">
35+
</div>
36+
</form>
37+
{% endblock %}

‎tests/core/tests/admin_integration_tests.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def test_import_export_template(self):
2424
self.assertContains(response, _('Export'))
2525

2626
def test_import(self):
27-
input_format = 'tablib.formats._csv'
27+
input_format = '0'
2828
filename = os.path.join(
2929
os.path.dirname(__file__),
3030
os.path.pardir,
@@ -50,4 +50,10 @@ def test_import(self):
5050
def test_export(self):
5151
response = self.client.get('/admin/core/book/export/')
5252
self.assertEqual(response.status_code, 200)
53+
54+
data = {
55+
'file_format': '0',
56+
}
57+
response = self.client.post('/admin/core/book/export/', data)
58+
self.assertEqual(response.status_code, 200)
5359
self.assertTrue(response.has_header("Content-Disposition"))

0 commit comments

Comments
 (0)
Please sign in to comment.