0001-cards-add-import-data-from-CSV-39473.patch
tests/test_backoffice_pages.py | ||
---|---|---|
23 | 23 |
from django.utils.six.moves.urllib import parse as urllib |
24 | 24 |
from django.utils.six.moves.urllib import parse as urlparse |
25 | 25 | |
26 |
from quixote import cleanup, get_publisher |
|
26 |
from webtest import Upload |
|
27 | ||
28 |
from quixote import cleanup, get_publisher, get_response |
|
27 | 29 |
from wcs.qommon import ods |
28 | 30 |
from wcs.api_utils import sign_url |
29 | 31 |
from wcs.qommon import errors, sessions |
... | ... | |
5534 | 5536 |
resp.click('card plop') |
5535 | 5537 | |
5536 | 5538 | |
5539 |
def test_backoffice_cards_import_data_from_csv(pub, studio): |
|
5540 |
user = create_user(pub) |
|
5541 |
CardDef.wipe() |
|
5542 |
carddef = CardDef() |
|
5543 |
carddef.name = 'test' |
|
5544 |
carddef.fields = [ |
|
5545 |
fields.MapField(id='1', label='Map'), |
|
5546 |
fields.StringField(id='2', label='Test'), |
|
5547 |
fields.BoolField(id='3', label='Boolean'), |
|
5548 |
fields.ItemField(id='4', label='List', |
|
5549 |
items=['item1', 'item2']), |
|
5550 |
fields.DateField(id='5', label='Date'), |
|
5551 |
fields.TitleField(id='6', label='Title', type='title'), |
|
5552 |
fields.FileField(id='7', label='File') |
|
5553 |
] |
|
5554 |
carddef.backoffice_submission_roles = user.roles |
|
5555 |
carddef.workflow_roles = {'_editor': user.roles[0]} |
|
5556 |
carddef.store() |
|
5557 |
carddef.data_class().wipe() |
|
5558 | ||
5559 |
app = login(get_app(pub)) |
|
5560 | ||
5561 |
resp = app.get(carddef.get_url()) |
|
5562 |
resp = resp.click('Import data from a CSV file') |
|
5563 | ||
5564 |
assert 'Map, File are required but cannot be filled from CSV.' in resp |
|
5565 |
assert 'Download sample file' not in resp |
|
5566 |
carddef.fields[0].required = False |
|
5567 |
carddef.fields[6].required = False |
|
5568 |
carddef.store() |
|
5569 | ||
5570 |
resp = app.get(carddef.get_url()) |
|
5571 |
resp = resp.click('Import data from a CSV file') |
|
5572 |
sample_resp = resp.click('Download sample file') |
|
5573 |
today = datetime.date.today() |
|
5574 |
assert sample_resp.text == 'Map,Test,Boolean,List,Date,Title,File\r\nwill be ignored - type Map not supported,value,Yes,value,%s,will be ignored - type Title not supported,will be ignored - type File Upload not supported\r\n' % today |
|
5575 | ||
5576 |
resp.forms[0]['file'] = Upload('test.csv', b'\0', 'text/csv') |
|
5577 |
resp = resp.forms[0].submit() |
|
5578 |
assert 'Invalid file format.' in resp |
|
5579 | ||
5580 |
resp.forms[0]['file'] = Upload('test.csv', b'', 'text/csv') |
|
5581 |
resp = resp.forms[0].submit() |
|
5582 |
assert 'Invalid CSV file.' in resp |
|
5583 | ||
5584 |
resp.forms[0]['file'] = Upload('test.csv', |
|
5585 |
b'Test,List,Date\ndata1,item1,invalid', |
|
5586 |
'text/csv') |
|
5587 |
resp = resp.forms[0].submit() |
|
5588 |
assert 'CSV file contains less columns than card fields.' in resp.text |
|
5589 | ||
5590 |
resp.forms[0]['file'] = Upload('test.csv', |
|
5591 |
b'Map,Test,Boolean,List,Date,Title,File\nmap,data1,Yes,item1,invalid,,', |
|
5592 |
'text/csv') |
|
5593 |
resp = resp.forms[0].submit() |
|
5594 |
assert 'time data \'invalid\' does not match format \'%Y-%m-%d\'' in resp.text |
|
5595 | ||
5596 |
data = [b'Map,Test,Boolean,List,Date,Title,File'] |
|
5597 |
for i in range(1, 150): |
|
5598 |
data.append(b'map,data%d,%d,item%d,2020-01-%02d,filename-%d,title' % (i, i%2, i, i%31+1, i)) |
|
5599 | ||
5600 |
resp.forms[0]['file'] = Upload('test.csv',b'\n'.join(data), |
|
5601 |
'text/csv') |
|
5602 |
resp = resp.forms[0].submit().follow() |
|
5603 |
assert 'Importing data into cards' in resp |
|
5604 |
assert 'Column Title will be ignored: type Title not supported.' in resp |
|
5605 |
assert 'Column File will be ignored: type File Upload not supported.' in resp |
|
5606 |
assert len(carddef.data_class().select()) == 149 |
|
5607 | ||
5608 | ||
5537 | 5609 |
def test_backoffice_cards_wscall_failure_display(http_requests, pub, studio): |
5538 | 5610 |
LoggedError.wipe() |
5539 | 5611 |
user = create_user(pub) |
wcs/backoffice/data_management.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU General Public License |
15 | 15 |
# along with this program; if not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 |
import csv |
|
18 |
import datetime |
|
19 | ||
17 | 20 |
from quixote import get_request, get_response, get_session, redirect |
18 | 21 |
from quixote.html import TemplateIO, htmltext, htmlescape |
19 | 22 | |
23 |
from django.utils.encoding import force_text |
|
24 |
from django.utils.six import StringIO |
|
25 | ||
26 | ||
20 | 27 |
from ..qommon import _ |
21 | 28 |
from ..qommon import errors |
22 | 29 |
from ..qommon import template |
30 |
from ..qommon.form import Form, FileWidget |
|
23 | 31 |
from ..qommon.backoffice.menu import html_top |
32 |
from ..qommon import template |
|
33 |
from ..qommon.afterjobs import AfterJob |
|
24 | 34 | |
25 | 35 |
from wcs.carddef import CardDef |
36 |
from wcs import fields |
|
37 | ||
26 | 38 |
from .management import ManagementDirectory, FormPage, FormFillPage, FormBackOfficeStatusPage |
27 | 39 | |
28 | 40 | |
... | ... | |
67 | 79 | |
68 | 80 | |
69 | 81 |
class CardPage(FormPage): |
70 |
_q_exports = ['', 'csv', 'xls', 'ods', 'json', 'export', 'map', 'geojson', 'add'] |
|
82 |
_q_exports = ['', 'csv', 'xls', 'ods', 'json', 'export', 'map', 'geojson', 'add', |
|
83 |
('import-csv', 'import_csv'), |
|
84 |
('data-sample-csv', 'data_sample_csv')] |
|
85 | ||
86 |
import_supported_fields = ['StringField', 'TextField', 'DateField', |
|
87 |
'BoolField', 'ItemField', 'ItemsField', |
|
88 |
'PasswordField'] |
|
71 | 89 | |
72 | 90 |
def __init__(self, component): |
73 | 91 |
try: |
... | ... | |
99 | 117 |
def get_filter_from_query(self, default='waiting'): |
100 | 118 |
return 'all' |
101 | 119 | |
120 |
def data_sample_csv(self): |
|
121 |
carddef_fields = self.formdef.get_all_fields() |
|
122 |
output = StringIO() |
|
123 |
csv_output = csv.writer(output) |
|
124 |
csv_output.writerow([f.label for f in carddef_fields]) |
|
125 |
sample_line = [] |
|
126 |
for f in carddef_fields: |
|
127 |
if isinstance(f, fields.DateField): |
|
128 |
value = datetime.date.today() |
|
129 |
elif isinstance(f, fields.BoolField): |
|
130 |
value = _('Yes') |
|
131 |
elif isinstance(f, fields.EmailField): |
|
132 |
value = 'foo@example.com' |
|
133 |
elif f.__class__.__name__ not in self.import_supported_fields: |
|
134 |
value = _('will be ignored - type %s not supported') % f.description |
|
135 |
else: |
|
136 |
value = 'value' |
|
137 |
sample_line.append(value) |
|
138 |
csv_output.writerow(sample_line) |
|
139 |
response = get_response() |
|
140 |
response.set_content_type('text/plain') |
|
141 |
response.set_header('content-disposition', 'attachment; filename=%s-sample.csv' % self.formdef.url_name) |
|
142 |
return output.getvalue() |
|
143 | ||
144 |
def import_csv(self): |
|
145 |
context = { |
|
146 |
'unsupported_fields': [], |
|
147 |
'required_fields': [] |
|
148 |
} |
|
149 |
try: |
|
150 |
job = AfterJob.get(get_request().form.get('job')) |
|
151 |
get_response().add_javascript(['jquery.js', 'afterjob.js']) |
|
152 |
context['job'] = job |
|
153 |
except KeyError: |
|
154 |
form = Form(enctype='multipart/form-data', use_tokens=False) |
|
155 |
form.add(FileWidget, 'file', title=_('File'), required=False) |
|
156 |
form.add_submit('submit', _('Submit')) |
|
157 |
form.add_submit('cancel', _('Cancel')) |
|
158 |
if form.get_widget('cancel').parse(): |
|
159 |
return redirect('.') |
|
160 | ||
161 |
if form.is_submitted() and not form.has_errors(): |
|
162 |
try: |
|
163 |
return self.import_csv_submit(form) |
|
164 |
except ValueError as e: |
|
165 |
form.set_error('file', e) |
|
166 |
context['form'] = form |
|
167 | ||
168 |
get_response().breadcrumb.append(('import_csv', _('Import CSV'))) |
|
169 |
html_top('data_management', _('Import CSV')) |
|
170 | ||
171 |
for field in self.formdef.get_all_fields(): |
|
172 |
if field.__class__.__name__ not in self.import_supported_fields: |
|
173 |
context['unsupported_fields'].append(field) |
|
174 |
if not hasattr(field, 'required'): |
|
175 |
continue |
|
176 |
if field.required and field.__class__.__name__ not in self.import_supported_fields: |
|
177 |
context['required_fields'].append(field.label) |
|
178 | ||
179 |
return template.QommonTemplateResponse( |
|
180 |
templates=['wcs/backoffice/card-data-import-form.html'], |
|
181 |
context=context) |
|
182 | ||
183 |
def import_csv_submit(self, form): |
|
184 |
if form.get_widget('file').parse(): |
|
185 |
content = form.get_widget('file').parse().fp.read() |
|
186 |
if b'\0' in content: |
|
187 |
raise ValueError(_('Invalid file format.')) |
|
188 |
else: |
|
189 |
raise ValueError(_('You have to enter a file.')) |
|
190 | ||
191 |
for charset in ('utf-8', 'iso-8859-15'): |
|
192 |
try: |
|
193 |
content = content.decode(charset) |
|
194 |
break |
|
195 |
except UnicodeDecodeError: |
|
196 |
continue |
|
197 | ||
198 |
try: |
|
199 |
dialect = csv.Sniffer().sniff(content) |
|
200 |
except csv.Error: |
|
201 |
dialect = None |
|
202 | ||
203 |
reader = csv.reader(content.splitlines(), dialect=dialect) |
|
204 |
try: |
|
205 |
caption = next(reader) |
|
206 |
except StopIteration: |
|
207 |
raise ValueError(_('Invalid CSV file.')) |
|
208 | ||
209 |
carddef_fields = self.formdef.get_all_fields() |
|
210 |
if len(caption) < len(carddef_fields): |
|
211 |
raise ValueError(_('CSV file contains less columns than card fields.')) |
|
212 | ||
213 |
data_lines = [] |
|
214 |
for csv_line in reader: |
|
215 |
data_line = {} |
|
216 |
for i, field in enumerate(self.formdef.get_all_fields()): |
|
217 |
value = csv_line[i] |
|
218 |
# skip empty values |
|
219 |
if not value: |
|
220 |
continue |
|
221 |
# skip unsupported field types |
|
222 |
if field.__class__.__name__ not in self.import_supported_fields: |
|
223 |
continue |
|
224 |
field_id = str(field.id) |
|
225 |
if field.convert_value_from_str: |
|
226 |
value = field.convert_value_from_str(value) |
|
227 |
data_line[field_id] = value |
|
228 |
if field.store_display_value: |
|
229 |
display_value = field.store_display_value(data_line, field_id) |
|
230 |
data_line['%s_display' % field_id] = display_value |
|
231 |
if value and field.store_structured_value: |
|
232 |
structured_value = field.store_structured_value(data_line, field_id) |
|
233 |
if structured_value: |
|
234 |
if isinstance(structured_value, dict) and structured_value.get('id'): |
|
235 |
formdata.data[field_id] = str(structured_value.get('id')) |
|
236 |
data_line['%s_structured' % field_id] = structured_value |
|
237 |
data_lines.append(data_line) |
|
238 | ||
239 |
class ImportAction: |
|
240 |
def __init__(self, data_class, lines): |
|
241 |
self.data_class = data_class |
|
242 |
self.lines = lines |
|
243 | ||
244 |
def execute(self, job): |
|
245 |
for item in self.lines: |
|
246 |
data_instance = self.data_class() |
|
247 |
data_instance.data = item |
|
248 |
data_instance.just_created() |
|
249 |
data_instance.store() |
|
250 |
data_instance.perform_workflow() |
|
251 | ||
252 |
action = ImportAction(self.formdef.data_class(), data_lines) |
|
253 |
job = get_response().add_after_job(N_('Importing data into cards'), |
|
254 |
action.execute) |
|
255 |
job.store() |
|
256 |
return redirect('import-csv?job=%s' % job.id) |
|
257 | ||
102 | 258 |
def _q_lookup(self, component): |
103 | 259 |
try: |
104 | 260 |
filled = self.formdef.data_class().get(component) |
wcs/backoffice/management.py | ||
---|---|---|
1039 | 1039 |
qs, _('Plot on a Map')) |
1040 | 1040 |
if 'stats' in self._q_exports: |
1041 | 1041 |
r += htmltext(' <li class="stats"><a href="stats">%s</a></li>') % _('Statistics') |
1042 |
if ('import-csv', 'import_csv') in self._q_exports: |
|
1043 |
r += htmltext('<li><a rel="popup" href="import-csv">%s</a></li>') % _('Import data from a CSV file') |
|
1042 | 1044 |
r += htmltext('</ul>') |
1043 | 1045 |
return r.getvalue() |
1044 | 1046 |
wcs/templates/wcs/backoffice/card-data-import-form.html | ||
---|---|---|
1 |
{% extends "wcs/backoffice/base.html" %} |
|
2 |
{% load i18n %} |
|
3 | ||
4 |
{% block appbar %} |
|
5 |
<div id="appbar" class="highlight"> |
|
6 |
<h2>{% trans "Import CSV" %}</h2> |
|
7 |
</div> |
|
8 |
{% endblock %} |
|
9 | ||
10 |
{% block content %} |
|
11 |
{% if job %} |
|
12 | ||
13 |
{% if unsupported_fields %} |
|
14 |
{% for field in unsupported_fields %} |
|
15 |
<div class="warningnotice"> |
|
16 |
{% blocktrans with label=field.label description=field.description %} |
|
17 |
Column {{ label }} will be ignored: type {{ description }} not supported. |
|
18 |
{% endblocktrans %} |
|
19 |
</div> |
|
20 |
{% endfor %} |
|
21 |
{% endif %} |
|
22 | ||
23 |
<dl class="job-status"> |
|
24 |
<dt>{{ job.label }}</dt> |
|
25 |
{% if warnings %} |
|
26 |
<dt> |
|
27 |
{% for warning in warnings %} |
|
28 |
<div class="warningnotice">{{ warning }}</div> |
|
29 |
{% endfor %} |
|
30 |
</dt> |
|
31 |
{% endif %} |
|
32 |
<dd><span class="afterjob" id="{{ job.id }}">{% trans job.status %}</span></dd> |
|
33 |
</dl> |
|
34 |
<div class="done"> |
|
35 |
<a data-redirect-auto="true" href=".">{% trans "Back to Listing" %}</a> |
|
36 |
</div> |
|
37 |
{% else %} |
|
38 | ||
39 |
{% if required_fields %} |
|
40 |
<div class="errornotice"> |
|
41 |
<p> |
|
42 |
{% blocktrans count fields=required_fields|length with labels=required_fields|join:", " label=required_fields.0 %} |
|
43 |
{{ label }} is required but cannot be filled from CSV. |
|
44 |
{% plural %} |
|
45 |
{{ labels }} are required but cannot be filled from CSV. |
|
46 |
{% endblocktrans %} |
|
47 |
</p> |
|
48 |
</div> |
|
49 |
{% else %} |
|
50 |
<p>{% trans "You can add data to this card by uploading a file." %}</p> |
|
51 |
<p><a href="data-sample-csv">{% trans 'Download sample file' %}</a> {% trans "for this card" %}</p> |
|
52 |
{{ form.render|safe }} |
|
53 |
{% endif %} |
|
54 |
{% endif %} |
|
55 |
{% endblock %} |
|
0 |
- |