Project

General

Profile

Bug #35903

Valeur choisie invalide dans la saisie d'un formulaire de workflow

Added by Nicolas Roche 9 days ago. Updated 1 day ago.

Status:
Nouveau
Priority:
Normal
Assignee:
-
Target version:
-
Start date:
07 Sep 2019
Due date:
% Done:

0%

Patch proposed:
No
Planning:
No

Description

Affiche "valeur choisie invalide" à la soumission d'un formulaire de workflow (c'est à dire ajouté en tant qu'action d'un état dans un workflow) sur une liste remplie depuis une source de donnée JSON tout en utilisant en paramètre un champ issu du même formulaire.

Tout est dit ci-dessus mais au risque de me répéter je préfère étayer un petit peu :

Dans un formulaire crée depuis la fabrique de workflow on a un champ issu de l'URL JSON suivante :
[passerelle_url]/csvdatasource/XXX/query/YYY/?param=[form2_var_var]
  • ok si var vient du formulaire FO
  • ok si var vient d'une variable de traitement
  • ok si var vient d'un autre formulaire BO
  • ko var si vient du même formulaire BO

Le champ n'a pas besoin d'être obligatoire pour reproduire le bug.
Le bug est reproductible en front-office et en back-office

menu.csv View (91 Bytes) Nicolas Roche, 09 Sep 2019 02:29 PM

workflow-form-bo-within-datasource.wcs (2.22 KB) Nicolas Roche, 09 Sep 2019 02:29 PM

History

#2 Updated by Nicolas Roche 7 days ago

En attendant d'avoir quelque chose de mieux borné, voici un workflow et un CSV qui mettent en évidence le bug.
Il faut configurer une requête 'choix' qui fitre le CSV sur l'id.
Le workflow peut être appelé depuis un formulaire vide.

#3 Updated by Nicolas Roche 5 days ago

Voici un test qui permet de reproduire l'erreur.
Il reprend test_field_live_select_content afin de tester #27173 non plus sur le formulaire principale, mais sur un second formulaire affiché au cours du workflow.

def test_field_live_select_content_on_display_form(pub):
    create_user(pub)    
    wf = Workflow(name='wf-title')
    st1 = wf.add_status('Status1', 'st1')

    data_from_data_source = [{'id': '1', 'text': 'un'}, {'id': '2', 'text': 'deux'}]

    # form displayed into workflow
    display_form = FormWorkflowStatusItem()
    display_form.id = '_x'
    display_form.by = ['_submitter']
    display_form.varname = 'xxx'
    display_form.formdef = WorkflowFormFieldsFormDef(item=display_form)
    display_form.formdef.fields = [
        fields.StringField(type='string', id='1', label='Foo', varname='foo'),
        fields.ItemField(type='item', id='2', label='Bar', varname='bar',
                         data_source={
                             'type': 'json',
                             'value': "https://api.example.com/json?foo=[xxx_var_foo]"}),
      ]

    st1.items.append(display_form)
    display_form.parent = st1
    wf.store()

    # initial empty 'test' form
    formdef = create_formdef()
    formdef.fields = []
    formdef.confirmation = False
    formdef.workflow_id = wf.id
    formdef.store()
    formdef.data_class().wipe()

    # submit initial empty form
    app = get_app(pub)
    resp = login(app, username='foo', password='foo').get('/test/')
    resp = resp.form.submit('submit').follow()

    # provide a value for 'f1' field
    assert resp.html.find('div', {'data-field-id': '1'}).attrs['data-live-source'] == 'true'
    assert resp.html.find('div', {'data-field-id': '2'}).find('select')
    resp.form['f1'].value = 'foo'
    live_resp = app.post('/test/1/live?modified_field_id=1', params=resp.form.submit_fields())
    assert live_resp.json['result']['1']['visible']
    assert live_resp.json['result']['2']['visible']

    # simulate javascript filling the <select>
    resp.form['f2'].options = []
    for item in data_from_data_source:
        resp.form['f2'].options.append((item['id'], False, item['text']))
    resp.form['f2'] = '2'

    # submit the form
    resp = resp.form.submit('submit')  # <- hang here ...
    # ... fails to retrieve the simulate javascript filled options for the select

    resp = resp.follow()
    assert 'The form has been recorded' in resp.body

Le test échoue dans quixote/form/widget.py::SelectWidget:_parse_single_selection() ...

for value, description, key in self.options:
    ...
else:
    if self.verify_selection:
        self.error = self.SELECTION_ERROR
        return default

... parce que les options (du select HTML) utilisées pour valider la valeur sont vides :
(Pdb) self.options
[(None, '---', '')]

#4 Updated by Nicolas Roche 5 days ago

  • Subject changed from Valeur choisie invalide dans la saisie en backoffice d'un formulaire to Valeur choisie invalide dans la saisie d'un formulaire de workflow

#5 Updated by Nicolas Roche 5 days ago

  • Description updated (diff)

#7 Updated by Stéphane Laget 2 days ago

également ici : #35874

#8 Updated by Benjamin Dauvergne 1 day ago

Pour les ItemField le contenu de la datasource est mise en cache dès le premier appel à ItemField.get_options() dans self._cached_data_source, je suppute un lien mais je vais lire le code de FormWorkflowStatusItem pour en être sûr...
Pas le bon code.

Donc non, ce serait plutôt dans wcs/forms/common.py:FormStatusPage au niveau de get_workflow_form() :

    def get_workflow_form(self, user):
        submitted_fields = []
        form = self.filled.get_workflow_form(user, displayed_fields=submitted_fields)
        if form:
            form.attrs['data-live-url'] = self.filled.get_url() + 'live'
        if form and form.is_submitted():
            with get_publisher().substitutions.temporary_feed(self.filled, force_mode='lazy'):
                # remove fields that could be required but are not visible
                self.filled.evaluate_live_workflow_form(user, form)
                get_publisher().substitutions.feed(self.filled)
                for field in submitted_fields:
                    if not field.is_visible(self.filled.data, self.formdef) and 'f%s' % field.id in form._names:
                        del form._names['f%s' % field.id]
        return form

on voit bien le feed des données actuelle du formulaire mais pas le feed des données qui viennent d'être soumises comme je pense on doit l'avoir lors de la soumission d'un formulaire "classique" avec les histoires de get_transient_formdata, comme là :

            submitted_fields = []
            transient_formdata = self.get_transient_formdata()
            with get_publisher().substitutions.temporary_feed(
                    transient_formdata, force_mode='lazy'):
                form = self.create_form(page=page,
                        displayed_fields=submitted_fields,
                        transient_formdata=transient_formdata)

Voilà je pense que c'est cerné, Nicolas je pense qu'il te faudra la science de Fred pour avancer plus loin.

#9 Updated by Benjamin Dauvergne 1 day ago

Mauvaise analyse, les données sont bien ajoutées aux substitutions par :

                self.filled.evaluate_live_workflow_form(user, form)

mais comme on est dans le contexte d'un temporary_feed c'est défait, et donc quand on arrive là :

279     def check_submitted_form(self, form):
280         if form and form.is_submitted() and not form.has_errors():
281             url = self.submit(form)

le form.has_errors() renvoie True puisqu'on a plus les valeurs, il faudrait ajouter le même code dans check_submitted_form, comme ceci :

     def check_submitted_form(self, form):
         if form and form.is_submitted():
             with get_publisher().substitutions.temporary_feed(self.filled, force_mode='lazy'):
                 # remove fields that could be required but are not visible
                 self.filled.evaluate_live_workflow_form(user, form)
                 get_publisher().substitutions.feed(self.filled)
                 for field in submitted_fields:
                     if not field.is_visible(self.filled.data, self.formdef) and 'f%s' % field.id in form._names:
                         del form._names['f%s' % field.id]

                 if not form.has_errors():
                     url = self.submit(form)
                     if url is None:
                         url = get_request().get_frontoffice_url()
                     response = get_response()
                     response.set_status(303)
                     response.headers[str('location')] = url
                     response.content_type = 'text/plain'
                     return "Your browser should redirect you" 

ou alors refactoriser un peu tout ce code la pour limiter la duplication.

Also available in: Atom PDF