Projet

Général

Profil

0004-add-new-action-create-formdata-33186.patch

Benjamin Dauvergne, 23 janvier 2020 15:42

Télécharger (17,6 ko)

Voir les différences:

Subject: [PATCH 04/11] add new action create-formdata (#33186)

 wcs/wf/create_formdata.py | 441 ++++++++++++++++++++++++++++++++++++++
 wcs/workflows.py          |   1 +
 2 files changed, 442 insertions(+)
 create mode 100644 wcs/wf/create_formdata.py
wcs/wf/create_formdata.py
1
# w.c.s. - web application for online forms
2
# Copyright (C) 2005-2016  Entr'ouvert
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16

  
17
import collections
18
import copy
19
import time
20
import xml.etree.ElementTree as ET
21

  
22
from quixote import get_request, get_session
23

  
24
from django.utils.functional import cached_property
25

  
26
from wcs.qommon import _
27
from wcs.qommon.form import (WidgetListAsTable, CompositeWidget,
28
                             SingleSelectWidget, ComputedExpressionWidget,
29
                             CheckboxWidget, StringWidget, VarnameWidget)
30

  
31
from wcs.logged_errors import LoggedError
32
from wcs.workflows import WorkflowStatusItem, register_item_class
33
from wcs.formdef import FormDef
34
from wcs.variables import LazyFormData
35

  
36

  
37
Mapping = collections.namedtuple('Mapping', ['varname', 'expression'])
38

  
39

  
40
class MappingWidget(CompositeWidget):
41
    def __init__(self, name, value=None, to_formdef=None, **kwargs):
42
        value = value or Mapping(None, '')
43
        super(MappingWidget, self).__init__(name, value, **kwargs)
44

  
45
        to_fields = self._fields_to_options(to_formdef)
46

  
47
        self.add(SingleSelectWidget,
48
                 name='varname',
49
                 title=_('Field'),
50
                 value=value.varname,
51
                 options=to_fields)
52

  
53
        self.add(ComputedExpressionWidget,
54
                 name='expression',
55
                 title=_('Expression'),
56
                 value=value.expression)
57

  
58
    def _fields_to_options(self, formdef):
59
        return [(None, '---', '')] + [
60
            (field.varname, field.label, field.varname) for field in formdef.get_all_varname_fields()]
61

  
62
    def _parse(self, request):
63
        super(MappingWidget, self)._parse(request)
64
        if self.get('varname') is not None and self.get('expression') is not None:
65
            self.value = Mapping(varname=self.get('varname'), expression=self.get('expression'))
66
        else:
67
            self.value = None
68

  
69

  
70
class MappingsWidget(WidgetListAsTable):
71
    readonly = False
72

  
73
    # widget_list.js does not work with ComputedExpressionWidget,
74
    # so we revert to quixote behabiour for adding a line
75
    def add_media(self):
76
        pass
77

  
78
    def __init__(self, name, to_formdef=None, **kwargs):
79
        self.to_formdef = to_formdef
80

  
81
        value = kwargs.get('value')
82
        if value:
83
            # reorder mappings based on to_formdef fields order
84
            value.sort(key=lambda mapping: self.ranks.get(mapping.varname, 9999))
85

  
86
        super(MappingsWidget, self).__init__(
87
            name,
88
            element_type=MappingWidget,
89
            element_kwargs={
90
                'to_formdef': to_formdef,
91
            },
92
            **kwargs)
93

  
94
    @cached_property
95
    def ranks(self):
96
        return {field.varname: i for i, field in enumerate(
97
            field for field in self.to_formdef.get_all_varname_fields())}
98

  
99
    def _parse(self, request):
100
        super(MappingsWidget, self)._parse(request)
101

  
102
        if self.value:
103
            # prevent many mappings to the same field
104
            if len(set(mapping.varname for mapping in self.value)) != len(self.value):
105
                self.error = _('Some destination fields are duplicated')
106
                return
107

  
108
            # reorder mappings based on to_formdef fields order
109
            self.value.sort(key=lambda mapping: self.ranks.get(mapping.varname, 9999))
110

  
111

  
112
class LazyLinkedFormData(object):
113
    __part = None
114

  
115
    def __init__(self, part):
116
        self.__part = part
117

  
118
    def __getattr__(self, name):
119
        if name.startswith('__'):
120
            raise AttributeError(name)
121

  
122
        formdata = self.__part.formdata
123
        if formdata is not None:
124
            return getattr(LazyFormData(self.__part.formdata), name)
125
        raise AttributeError(name)
126

  
127
    @property
128
    def url(self):
129
        formdata = self.__part.formdata
130
        if formdata is None:
131
            return None
132
        if self.__part.formdata.is_draft():
133
            return self.__part.url
134
        else:
135
            return self.__getattr__('url')
136

  
137

  
138
class LinkedFormdataSubstitutionProxy(object):
139
    __varname = None
140

  
141
    def __init__(self, formdata):
142
        self.formdata = formdata
143

  
144
    @property
145
    def __parts(self):
146
        return [part for part in self.formdata.iter_evolution_parts()
147
                if isinstance(part, LinkedFormdataEvolutionPart)]
148

  
149
    @property
150
    def __formdatas(self):
151
        formdatas = [(part.varname, part, LazyLinkedFormData(part)) for part in self.__parts]
152
        formdatas.reverse()
153
        return formdatas
154

  
155
    @property
156
    def __varnames(self):
157
        return set(varname for varname, _1, _2 in self.__formdatas)
158

  
159
    def __iter__(self):
160
        for varname, part, formdata in self.__formdatas or []:
161
            if self.__varname is None or self.__varname == varname:
162
                yield formdata
163

  
164
    def __getitem__(self, idx):
165
        return list(self)[idx]
166

  
167
    def __getattr__(self, name):
168
        if name.startswith('__'):
169
            raise AttributeError(name)
170

  
171
        if self.__varname or name not in (self.__varnames or []):
172
            try:
173
                first = self[0]
174
            except IndexError:
175
                raise AttributeError(name)
176
            return getattr(first, name)
177
        elif name in (self.__varnames or []):
178
            new_sub = copy.copy(self)
179
            new_sub.varname = name
180
            return new_sub
181

  
182
    def __repr__(self):
183
        s = '<LinkedFormdataSubstitutionProxy'
184
        if self.__varname:
185
            s += ' varname=%r' % self.__varname
186
        else:
187
            s += ' varnames=%s' % list(self.__varnames)
188
        parts = [part for part in self.__parts if not self.__varname or part.varname == self.__varname]
189
        s += ' forms=%s>' % ['%s-%s' % (part.formdef_id, part.formdata_id) for part in parts]
190
        return s
191

  
192

  
193
class LinkedFormdataEvolutionPart(object):
194
    def __init__(self, formdata, varname=None, label=None):
195
        self.__formdef = formdata.formdef
196
        self.__formdata = formdata
197
        self.formdef_id = formdata.formdef.id
198
        self.formdata_id = formdata.id
199
        self.varname = varname
200
        self.label = label
201

  
202
    @property
203
    def formdef(self):
204
        if not hasattr(self, '__formdef'):
205
            self.__formdef = FormDef.get(self.formdef_id)
206
        return self.__formdef
207

  
208
    @property
209
    def formdata(self):
210
        if not hasattr(self, '__formdata'):
211
            self.__formdata = self.formdef.data_class().get(self.formdata_id, ignore_errors=True)
212
        return self.__formdata
213

  
214
    @classmethod
215
    def get_substitution_variables(cls, formdata):
216
        return {
217
            'linked': LinkedFormdataSubstitutionProxy(formdata),
218
        }
219

  
220
    @property
221
    def url(self):
222
        if not self.formdata.is_draft():
223
            return self.formdata.get_url(backoffice=get_request().is_in_backoffice())
224
        elif self.formdata.backoffice_submission:
225
            return '%s%s' % (self.formdata.formdef.get_backoffice_submission_url(), self.formdata.id)
226
        else:
227
            return self.formdata.get_url().rstrip('/')
228

  
229
    def __getstate__(self):
230
        # Forget cached values
231
        return {k: v for k, v in self.__dict__.items() if not k.startswith('_')}
232

  
233

  
234
class CreateFormdataWorkflowStatusItem(WorkflowStatusItem):
235
    description = N_('New formdata')
236
    key = 'create_formdata'
237
    category = 'formdata-action'
238
    support_substitution_variables = True
239

  
240
    label = None
241
    formdef_slug = None
242
    draft = True
243
    backoffice_submission = False
244
    keep_user = True
245
    keep_submission_context = False
246
    mappings = None
247
    varname = None
248

  
249
    def get_line_details(self):
250
        if self.label:
251
            s = self.label
252
            if self.varname:
253
                s += ' - %s' % self.varname
254
                return s
255
        else:
256
            return None
257

  
258
    @property
259
    def formdef(self):
260
        if self.formdef_slug and self.formdef_slug != '_same':
261
            try:
262
                return FormDef.get_by_urlname(self.formdef_slug)
263
            except KeyError:
264
                pass
265
        return None
266

  
267
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
268
        super(CreateFormdataWorkflowStatusItem, self).add_parameters_widgets(
269
            form, parameters, prefix=prefix, formdef=formdef)
270
        if 'label' in parameters:
271
            form.add(StringWidget, '%slabel' % prefix,
272
                     title=_('Label'),
273
                     value=self.label or _('Create formdata'))
274
        if 'formdef_slug' in parameters:
275
            list_forms = [(None, '---', '')]
276
            list_forms += [(x.url_name, x.name, x.url_name) for x in FormDef.select(order_by='name')]
277
            form.add(SingleSelectWidget, 'formdef_slug',
278
                     title=_('Form'),
279
                     value=self.formdef_slug,
280
                     options=list_forms)
281
        if 'draft' in parameters:
282
            form.add(CheckboxWidget, '%sdraft' % prefix,
283
                     title=_('Draft'),
284
                     value=self.draft,
285
                     advanced=self.draft == CreateFormdataWorkflowStatusItem.draft)
286
        if 'backoffice_submission' in parameters:
287
            form.add(CheckboxWidget, '%sbackoffice_submission' % prefix,
288
                     title=_('Backoffice submission'),
289
                     value=self.backoffice_submission,
290
                     advanced=self.backoffice_submission == CreateFormdataWorkflowStatusItem.backoffice_submission)
291
        if 'keep_user' in parameters:
292
            form.add(CheckboxWidget, '%skeep_user' % prefix,
293
                     title=_('Keep user'),
294
                     value=self.keep_user,
295
                     advanced=self.keep_user == CreateFormdataWorkflowStatusItem.keep_user)
296
        if 'keep_submission_context' in parameters and self.keep_user:
297
            form.add(CheckboxWidget, '%skeep_submission_context' % prefix,
298
                     title=_('Keep submission context'),
299
                     value=self.keep_submission_context,
300
                     advanced=self.keep_submission_context == CreateFormdataWorkflowStatusItem.keep_submission_context)
301
        formdef = None
302
        if form.get('formdef_slug'):
303
            try:
304
                formdef = FormDef.get_by_urlname(form.get('formdef_slug'))
305
            except KeyError:
306
                pass
307
        if 'mappings' in parameters and formdef:
308
            form.add(MappingsWidget, '%smappings' % prefix,
309
                     title=_('Mappings to new form fields'),
310
                     to_formdef=formdef,
311
                     required=True,
312
                     value=self.mappings)
313
        if 'varname' in parameters:
314
            form.add(VarnameWidget, '%svarname' % prefix,
315
                     title=_('Identifier'), value=self.varname,
316
                     hint=_('This is used to get generated document in expressions.'),
317
                     advanced=not(self.varname))
318

  
319
    def submit_admin_form(self, form):
320
        self.mappings = []
321
        super(CreateFormdataWorkflowStatusItem, self).submit_admin_form(form)
322

  
323
    def get_parameters(self):
324
        return ('label', 'formdef_slug', 'draft', 'backoffice_submission',
325
                'keep_user', 'keep_submission_context', 'mappings', 'varname')
326

  
327
    def perform(self, formdata):
328
        formdef = self.formdef
329
        if not formdef:
330
            return
331

  
332
        new_formdata = formdef.data_class()()
333
        new_formdata.receipt_time = time.localtime()
334

  
335
        if self.keep_user:
336
            new_formdata.user_id = formdata.user_id
337

  
338
        if self.keep_submission_context and self.keep_user:
339
            new_formdata.submission_context = formdata.submission_context or {}
340
            new_formdata.submission_channel = formdata.submission_channel
341
        else:
342
            new_formdata.submission_context = {}
343

  
344
        new_formdata.backoffice_submission = self.backoffice_submission
345
        if self.backoffice_submission and get_request() and get_request().user is not None:
346
            new_formdata.submission_context['agent_id'] = str(get_request().user.id)
347

  
348
        new_formdata.submission_context['orig_formdef_id'] = str(formdata.formdef.id)
349
        new_formdata.submission_context['orig_formdata_id'] = str(formdata.id)
350
        new_formdata.data = {}
351

  
352
        self.apply_mappings(dest=new_formdata, src=formdata)
353

  
354
        if self.draft:
355
            new_formdata.status = 'draft'
356
            new_formdata.store()
357
        else:
358
            new_formdata.just_created()
359
            new_formdata.store()
360
            new_formdata.perform_workflow()
361
            new_formdata.store()
362

  
363
        if new_formdata.user_id is None and not new_formdata.backoffice_submission:
364
            get_session().mark_anonymous_formdata(new_formdata)
365

  
366
        evo = formdata.evolution[-1]
367
        evo.add_part(LinkedFormdataEvolutionPart(new_formdata, varname=self.varname))
368
        formdata.store()
369

  
370
    def apply_mappings(self, dest, src):
371
        if not self.mappings:
372
            return
373

  
374
        to_id_fields = {field.varname: field for field in self.formdef.get_all_varname_fields()}
375

  
376
        missing_fields = []
377

  
378
        for mapping in self.mappings:
379
            try:
380
                dest_field = to_id_fields[mapping.varname]
381
            except KeyError:
382
                missing_fields.append(mapping.varname)
383
                continue
384
            try:
385
                value = self.compute(mapping.expression, formdata=src, raises=True, status_item=self)
386
            except Exception:
387
                # already logged by self.compute
388
                continue
389

  
390
            try:
391
                self._set_value(
392
                    formdata=dest,
393
                    field=dest_field,
394
                    value=value)
395
            except Exception as e:
396
                expression = self.get_expression(mapping.expression)
397
                LoggedError.record('Could not assign value to field %s' % dest_field,
398
                                   formdata=src, status_item=self,
399
                                   expression=expression['value'], expression_type=expression['type'],
400
                                   exception=e)
401

  
402
        if missing_fields:
403
            summary = _('Missing field %r') % missing_fields
404
            LoggedError.record(summary, formdata=src, status_item=self)
405

  
406
    def _set_value(self, formdata, field, value):
407
        if field.convert_value_from_anything:
408
            old_value = value  # noqa: F841, copy value for debug
409
            value = field.convert_value_from_anything(value)
410

  
411
        formdata.data['%s' % field.id] = value
412
        if field.store_display_value:
413
            display_value = field.store_display_value(
414
                formdata.data, field.id)
415
            if display_value:
416
                formdata.data['%s_display' % field.id] = display_value
417
        if value and field.store_structured_value:
418
            structured_value = field.store_structured_value(
419
                formdata.data, field.id)
420
            if structured_value:
421
                if isinstance(structured_value, dict) and structured_value.get('id'):
422
                    # in case of list field, override id
423
                    formdata.data['%s' % field.id] = str(structured_value.get('id'))
424
                formdata.data['%s_structured' % field.id] = structured_value
425

  
426
    def mappings_export_to_xml(self, parent, charset, include_id=False):
427
        container = ET.SubElement(parent, 'mappings')
428
        for mapping in self.mappings or []:
429
            item = ET.SubElement(container, 'mapping')
430
            item.attrib['varname'] = unicode(mapping.varname, charset, 'replace')
431
            item.text = unicode(mapping.expression, charset, 'replace')
432

  
433
    def mappings_init_with_xml(self, container, charset, include_id=False):
434
        self.mappings = []
435
        for child in container:
436
            varname = child.attrib.get('varname', '').encode(charset)
437
            expression = child.text.encode(charset)
438
            if varname:
439
                self.mappings.append(Mapping(varname=varname, expression=expression))
440

  
441
register_item_class(CreateFormdataWorkflowStatusItem)
wcs/workflows.py
2917 2917
    from .wf import backoffice_fields
2918 2918
    from .wf import redirect_to_url
2919 2919
    from .wf import notification
2920
    from .wf import create_formdata
2920 2921

  
2921 2922
from .wf.export_to_model import ExportToModel
2922
-