Projet

Général

Profil

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

Serghei Mihai, 06 février 2020 15:06

Télécharger (12,3 ko)

Voir les différences:

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

 tests/test_backoffice_pages.py                |  63 +++++++-
 wcs/backoffice/data_management.py             | 146 +++++++++++++++++-
 wcs/backoffice/management.py                  |   2 +
 .../wcs/backoffice/card-data-import-form.html |  24 +++
 4 files changed, 233 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.StringField(id='1', label='Test'),
5546
        fields.ItemField(id='2', label='List',
5547
                         items=['item1', 'item2']),
5548
        fields.DateField(id='3', label='Date',),
5549
        fields.BoolField(id='4', label='Boolean'),
5550
        fields.FileField(id='5', label='File')
5551
    ]
5552
    carddef.backoffice_submission_roles = user.roles
5553
    carddef.workflow_roles = {'_editor': user.roles[0]}
5554
    carddef.store()
5555
    carddef.data_class().wipe()
5556

  
5557
    app = login(get_app(pub))
5558

  
5559
    resp = app.get(carddef.get_url())
5560
    assert 'import-csv' in resp
5561
    resp = resp.click('Import data from a CSV file')
5562

  
5563
    assert 'Download sample file' in resp
5564
    sample_resp = resp.click('Download sample file')
5565
    today = datetime.date.today()
5566
    assert sample_resp.text == 'Test,List,Date,Boolean,File\r\nvalue,value,%s,Yes,\r\n' % today
5567

  
5568
    resp.forms[0]['file'] = Upload('test.csv', b'\0', 'text/csv')
5569
    resp = resp.forms[0].submit()
5570
    assert 'Invalid file format.' in resp
5571

  
5572
    resp.forms[0]['file'] = Upload('test.csv', b'', 'text/csv')
5573
    resp = resp.forms[0].submit()
5574
    assert 'Invalid CSV file.' in resp
5575

  
5576
    resp.forms[0]['file'] = Upload('test.csv',
5577
                                   b'Test,List,Date\ndata1,item1,invalid',
5578
                                   'text/csv')
5579
    resp = resp.forms[0].submit()
5580
    assert 'CSV file contains less columns than card fields.' in resp.text
5581

  
5582
    resp.forms[0]['file'] = Upload('test.csv',
5583
                                   b'Test,List,Date,Boolean,File\ndata1,item1,invalid,Yes,',
5584
                                   'text/csv')
5585
    resp = resp.forms[0].submit()
5586
    assert 'time data \'invalid\' does not match format \'%Y-%m-%d\'' in resp.text
5587

  
5588
    data = [b'Test,List,Date,Boolean,File']
5589
    for i in range(1, 150):
5590
        data.append(b'data%d,item%d,2020-01-%02d,%d,filename-%d' % (i, i, i%31+1, i%2,i))
5591

  
5592
    resp.forms[0]['file'] = Upload('test.csv',b'\n'.join(data),
5593
                                   'text/csv')
5594
    resp = resp.forms[0].submit().follow()
5595
    assert 'Importing data into ' in resp
5596

  
5597

  
5537 5598
def test_backoffice_cards_wscall_failure_display(http_requests, pub, studio):
5538 5599
    LoggedError.wipe()
5539 5600
    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_supporting_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_supporting_fields:
134
                value = ''
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
        try:
147
            job = AfterJob.get(get_request().form.get('job'))
148
            get_response().add_javascript(['jquery.js', 'afterjob.js'])
149
            context['job'] = job
150
        except KeyError:
151
            form = Form(enctype='multipart/form-data', use_tokens=False)
152
            form.add(FileWidget, 'file', title=_('File'), required=False)
153
            form.add_submit('submit', _('Submit'))
154
            form.add_submit('cancel', _('Cancel'))
155
            if form.get_widget('cancel').parse():
156
                return redirect('.')
157

  
158
            if form.is_submitted() and not form.has_errors():
159
                try:
160
                    return self.import_csv_submit(form)
161
                except ValueError as e:
162
                    form.set_error('file', e)
163
            context['form'] = form
164

  
165
        get_response().breadcrumb.append(('import_csv', _('Import CSV')))
166
        html_top('data_management', _('Import CSV'))
167
        return template.QommonTemplateResponse(
168
            templates=['wcs/backoffice/card-data-import-form.html'],
169
            context=context)
170

  
171
    def import_csv_submit(self, form):
172
        if form.get_widget('file').parse():
173
            content = form.get_widget('file').parse().fp.read()
174
            if b'\0' in content:
175
                raise ValueError(_('Invalid file format.'))
176
        else:
177
            raise ValueError(_('You have to enter a file.'))
178

  
179
        for charset in ('utf-8', 'iso-8859-15'):
180
            try:
181
                content = content.decode(charset)
182
                break
183
            except UnicodeDecodeError:
184
                continue
185

  
186
        try:
187
            dialect = csv.Sniffer().sniff(content)
188
        except csv.Error:
189
            dialect = None
190

  
191
        carddef_fields = self.formdef.get_all_fields()
192
        reader = csv.reader(content.splitlines(), dialect=dialect)
193
        try:
194
            caption = next(reader)
195
        except StopIteration:
196
            raise ValueError(_('Invalid CSV file.'))
197

  
198
        if len(caption) < len(carddef_fields):
199
            raise ValueError(_('CSV file contains less columns than card fields.'))
200

  
201
        data_lines = []
202
        for csv_line in reader:
203
            data_line = {}
204
            for i, field in enumerate(self.formdef.get_all_fields()):
205
                value = csv_line[i]
206
                # skip empty values
207
                if not value:
208
                    continue
209
                # skip unsupported field types
210
                if field.__class__.__name__ not in self.import_supporting_fields:
211
                    continue
212
                field_id = str(field.id)
213
                if field.convert_value_from_str:
214
                    value = field.convert_value_from_str(value)
215
                data_line[field_id] = value
216
                if field.store_display_value:
217
                    display_value = field.store_display_value(data_line, field_id)
218
                    data_line['%s_display' % field_id] = display_value
219
                if value and field.store_structured_value:
220
                    structured_value = field.store_structured_value(data_line, field_id)
221
                    if structured_value:
222
                        if isinstance(structured_value, dict) and structured_value.get('id'):
223
                            formdata.data[field_id] = str(structured_value.get('id'))
224
                        data_line['%s_structured' % field_id] = structured_value
225
            data_lines.append(data_line)
226

  
227
        class ImportAction:
228
            def __init__(self, data_class, lines):
229
                self.data_class = data_class
230
                self.lines = lines
231

  
232
            def execute(self, job):
233
                for item in self.lines:
234
                    data_instance = self.data_class()
235
                    data_instance.data = item
236
                    data_instance.just_created()
237
                    data_instance.store()
238
                    data_instance.perform_workflow()
239

  
240
        action = ImportAction(self.formdef.data_class(), data_lines)
241
        job = get_response().add_after_job(str(N_('Importing data into \'%s\'') % self.formdef.name),
242
                                           action.execute)
243
        job.store()
244
        return redirect('import-csv?job=%s' % job.id)
245

  
102 246
    def _q_lookup(self, component):
103 247
        try:
104 248
            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
<dl class="job-status">
13
  <dt>{{ job.label }}</dt>
14
  <dd><span class="afterjob" id="{{ job.id }}">{{ job.status }}</span></dd>
15
</dl>
16
<div class="done">
17
  <a data-redirect-auto="true" href=".">{% trans "Back to Listing" %}</a>
18
</div>
19
{% else %}
20
<p>{% trans "You can add data to this card by uploading a file." %}</p>
21
<p><a href="data-sample-csv">{% trans 'Download sample file' %}</a> {% trans "for this card" %}</p>
22
{{ form.render|safe }}
23
{% endif %}
24
{% endblock %}
0
-