Projet

Général

Profil

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

Serghei Mihai, 04 février 2020 19:56

Télécharger (11,3 ko)

Voir les différences:

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

 tests/test_backoffice_pages.py                |  55 ++++++++
 wcs/backoffice/data_management.py             | 121 +++++++++++++++++-
 wcs/backoffice/management.py                  |   2 +
 wcs/formdef.py                                |   3 +
 .../wcs/backoffice/card-data-import-form.html |  14 ++
 5 files changed, 194 insertions(+), 1 deletion(-)
 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 webtest import Upload
27

  
26 28
from quixote import cleanup, get_publisher
27 29
from wcs.qommon import ods
28 30
from wcs.api_utils import sign_url
......
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', varname='string'),
5546
        fields.ItemField(id='2', label='List', varname='list',
5547
                         items=['item1', 'item2']),
5548
        fields.DateField(id='3', label='Date', varname='date'),
5549
        fields.BoolField(id='4', label='Boolean', varname='boolean')
5550
    ]
5551
    carddef.backoffice_submission_roles = user.roles
5552
    carddef.workflow_roles = {'_editor': user.roles[0]}
5553
    carddef.store()
5554
    carddef.data_class().wipe()
5555

  
5556
    app = login(get_app(pub))
5557

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

  
5562
    assert 'Download sample file' in resp
5563
    sample_resp = resp.click('Download sample file')
5564
    today = datetime.date.today()
5565
    assert sample_resp.text == 'string,list,date,boolean\r\nstring-value,list-value,%s,Yes\r\n' % today
5566

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

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

  
5575
    resp.forms[0]['file'] = Upload('test.csv', b'data1;data2', 'text/csv')
5576
    resp = resp.forms[0].submit()
5577
    assert 'File contains less columns that fields with varnames.' in resp
5578

  
5579
    resp.forms[0]['file'] = Upload('test.csv',
5580
                                   b'string,list,date,boolean\ndata1,item1,invalid,Yes',
5581
                                   'text/csv')
5582
    resp = resp.forms[0].submit()
5583
    assert 'invalid date value: invalid' in resp.text
5584

  
5585
    resp.forms[0]['file'] = Upload('test.csv',
5586
                                   b'string,list,date,boolean\ndata1,item1,2020-01-01,Yes',
5587
                                   'text/csv')
5588
    resp = resp.forms[0].submit().follow()
5589
    assert 'Data imported successfully.' in resp
5590

  
5591

  
5537 5592
def test_backoffice_cards_wscall_failure_display(http_requests, pub, studio):
5538 5593
    LoggedError.wipe()
5539 5594
    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
24 33

  
25 34
from wcs.carddef import CardDef
35
from wcs import fields
36

  
26 37
from .management import ManagementDirectory, FormPage, FormFillPage, FormBackOfficeStatusPage
27 38

  
28 39

  
......
67 78

  
68 79

  
69 80
class CardPage(FormPage):
70
    _q_exports = ['', 'csv', 'xls', 'ods', 'json', 'export', 'map', 'geojson', 'add']
81
    _q_exports = ['', 'csv', 'xls', 'ods', 'json', 'export', 'map', 'geojson', 'add',
82
                  ('import-csv', 'import_csv'), ('data-sample-csv', 'data_sample_csv')]
71 83

  
72 84
    def __init__(self, component):
73 85
        try:
......
99 111
    def get_filter_from_query(self, default='waiting'):
100 112
        return 'all'
101 113

  
114
    def data_sample_csv(self):
115
        carddef_fields = self.formdef.get_varname_fields()
116
        output = StringIO()
117
        csv_output = csv.writer(output)
118
        csv_output.writerow([f.varname for f in carddef_fields])
119
        sample_line = []
120
        for f in carddef_fields:
121
            if isinstance(f, fields.DateField):
122
                value = datetime.date.today()
123
            elif isinstance(f, fields.BoolField):
124
                value = _('Yes')
125
            elif isinstance(f, fields.EmailField):
126
                value = 'foo@example.com'
127
            else:
128
                value = '%s-value' % f.varname
129
            sample_line.append(value)
130
        csv_output.writerow(sample_line)
131
        response = get_response()
132
        response.set_content_type('text/plain')
133
        response.set_header('content-disposition', 'attachment; filename=%s-sample.csv' % self.formdef.url_name)
134
        return output.getvalue()
135

  
136
    def import_csv(self):
137

  
138
        form = Form(enctype='multipart/form-data', use_tokens=False)
139
        form.add(FileWidget, 'file', title=_('File'), required=False)
140
        form.add_submit('submit', _('Submit'))
141
        form.add_submit('cancel', _('Cancel'))
142
        if form.get_widget('cancel').parse():
143
            return redirect('.')
144

  
145
        if form.is_submitted() and not form.has_errors():
146
            try:
147
                return self.import_csv_submit(form)
148
            except ValueError as e:
149
                form.set_error('file', str(e))
150

  
151
        get_response().breadcrumb.append(('import_csv', _('Import CSV')))
152
        html_top('data_management', _('Import CSV'))
153
        return template.QommonTemplateResponse(
154
                templates=['wcs/backoffice/card-data-import-form.html'],
155
                context={'form': form})
156

  
157

  
158

  
159
    def import_csv_submit(self, form):
160
        if form.get_widget('file').parse():
161
            content = form.get_widget('file').parse().fp.read()
162
            if b'\0' in content:
163
                raise ValueError(_('Invalid file format.'))
164
        else:
165
            raise ValueError(_('You have to enter a file.'))
166

  
167
        for charset in ('utf-8', 'iso-8859-15'):
168
            try:
169
                content = content.decode(charset)
170
                break
171
            except UnicodeDecodeError:
172
                continue
173

  
174
        try:
175
            dialect = csv.Sniffer().sniff(content)
176
        except csv.Error:
177
            dialect = None
178

  
179
        carddef_fields = self.formdef.get_varname_fields()
180
        carddef_fields_names = [f.varname for f in carddef_fields]
181
        reader = csv.reader(content.splitlines(), dialect=dialect)
182
        try:
183
            caption = next(reader)
184
        except StopIteration:
185
            raise ValueError(_('Invalid CSV file.'))
186

  
187
        if len(caption) < len(carddef_fields_names):
188
            raise ValueError(_('File contains less columns that fields with varnames.'))
189

  
190
        missing_fields = set(carddef_fields_names) - set(caption)
191
        if missing_fields:
192
            raise ValueError(_('File misses fields: %s.') % ', '.join(missing_fields))
193
        data_class = self.formdef.data_class()
194
        for csv_line in reader:
195
            data_line = dict(zip(caption, csv_line))
196
            data_instance = data_class()
197
            data = {}
198
            for field in carddef_fields:
199
                value = data_line[field.varname]
200
                field_id = str(field.id)
201
                if field.convert_value_from_anything:
202
                    value = field.convert_value_from_anything(value)
203
                data[field_id] = value
204
                if field.store_display_value:
205
                    display_value = field.store_display_value(data, field_id)
206
                    data['%s_display' % field_id] = display_value
207
                if value and field.store_structured_value:
208
                    structured_value = field.store_structured_value(data, field_id)
209
                    if structured_value:
210
                        if isinstance(structured_value, dict) and structured_value.get('id'):
211
                            formdata.data[field_id] = str(structured_value.get('id'))
212
                        data['%s_structured' % field_id] = structured_value
213
            data_instance.data = data
214
            data_instance.just_created()
215
            data_instance.store()
216

  
217
        get_session().message = ('info', N_('Data imported successfully.'))
218
        return redirect('.')
219

  
220

  
102 221
    def _q_lookup(self, component):
103 222
        try:
104 223
            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 get_publisher().has_site_option('studio') and ('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/formdef.py
385 385
    def get_all_fields(self):
386 386
        return (self.fields or []) + self.workflow.get_backoffice_fields()
387 387

  
388
    def get_varname_fields(self):
389
        return [field for field in self.fields or [] if isinstance(field, fields.WidgetField) and field.varname]
390

  
388 391
    def rebuild(self):
389 392
        if get_publisher().is_using_postgresql():
390 393
            from . import sql
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
<p>{% trans "You can add data to this card by uploading a file containing var names in columns captions." %}</p>
12
<p><a href="data-sample-csv">{% trans 'Download sample file' %}</a> {% trans "for this card" %}</p>
13
{{ form.render|safe }}
14
{% endblock %}
0
-