Projet

Général

Profil

0001-cards-add-import-data-from-CSV-39473.patch

Serghei Mihai, 13 février 2020 15:32

Télécharger (14,4 ko)

Voir les différences:

Subject: [PATCH] cards: add import data from CSV (#39473)

 tests/test_backoffice_pages.py                |  74 +++++++-
 wcs/backoffice/data_management.py             | 158 +++++++++++++++++-
 wcs/backoffice/management.py                  |   2 +
 .../wcs/backoffice/card-data-import-form.html |  55 ++++++
 4 files changed, 287 insertions(+), 2 deletions(-)
 create mode 100644 wcs/templates/wcs/backoffice/card-data-import-form.html
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
-