From 0a12216e4030825a885836de71a7f959b2b20349 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Tue, 4 Jun 2019 08:44:13 +0200
Subject: [PATCH] workflows: add live conditions to workflow form action
 (#32960)

---
 tests/test_admin_pages.py      |   2 +-
 tests/test_backoffice_pages.py | 113 +++++++++++++++++++++++++++++
 tests/test_form_pages.py       | 116 ++++++++++++++++++++++++++++++
 wcs/fields.py                  |  10 +--
 wcs/formdata.py                |  10 ++-
 wcs/formdef.py                 |  33 +++++++++
 wcs/forms/common.py            | 125 ++++++++++++++++++++++++---------
 wcs/forms/root.py              |  60 +---------------
 wcs/wf/attachment.py           |   2 +-
 wcs/wf/export_to_model.py      |   2 +-
 wcs/wf/form.py                 |  25 ++++---
 wcs/wf/resubmit.py             |   2 +-
 wcs/workflows.py               |  61 +++++++++-------
 13 files changed, 425 insertions(+), 136 deletions(-)

diff --git a/tests/test_admin_pages.py b/tests/test_admin_pages.py
index 965021ad6..36b762541 100644
--- a/tests/test_admin_pages.py
+++ b/tests/test_admin_pages.py
@@ -2586,7 +2586,7 @@ def test_workflows_edit_display_form_action(pub):
     assert 'foobar' in resp.body
     resp = resp.click('Edit')
     assert not 'in_listing' in resp.form.fields.keys()
-    assert not 'condition$type' in resp.form.fields.keys()
+    assert 'condition$type' in resp.form.fields.keys()
     resp = resp.form.submit('cancel')
     resp = resp.follow()
     resp = resp.click('Remove')
diff --git a/tests/test_backoffice_pages.py b/tests/test_backoffice_pages.py
index 375114b8a..321be8ee6 100644
--- a/tests/test_backoffice_pages.py
+++ b/tests/test_backoffice_pages.py
@@ -3620,6 +3620,119 @@ def test_backoffice_workflow_display_form(pub):
     assert formdef.data_class().get(formdata.id).workflow_data == {
             'blah_var_str': 'blah', 'blah_var_radio': 'c', 'blah_var_radio_raw': 'c'}
 
+def test_backoffice_workflow_form_with_conditions(pub):
+    user = create_user(pub)
+    create_environment(pub)
+
+    wf = Workflow.get_default_workflow()
+    wf.id = '2'
+    wf.store()
+    wf = Workflow.get(wf.id)
+    status = wf.get_status('new')
+    status.items = []
+    display_form = FormWorkflowStatusItem()
+    display_form.id = '_display_form'
+    display_form.by = [user.roles[0]]
+    display_form.varname = 'blah'
+    display_form.formdef = WorkflowFormFieldsFormDef(item=display_form)
+    display_form.formdef.fields = [
+            fields.StringField(id='1', label='Test', varname='str', type='string', required=True),
+            fields.StringField(id='2', label='Test2', varname='str2', type='string', required=True),
+    ]
+    status.items.append(display_form)
+    display_form.parent = status
+
+    wf.store()
+    formdef = FormDef.get_by_urlname('form-title')
+    formdef.workflow_id = wf.id
+    formdef.fields[0].varname = 'plop'
+    formdef.store()
+
+    for formdata in formdef.data_class().select():
+        if formdata.status == 'wf-new':
+            break
+    app = login(get_app(pub))
+    resp = app.get(formdata.get_url(backoffice=True))
+    assert 'f1' in resp.form.fields
+    assert 'f2' in resp.form.fields
+
+    # check with static condition
+    display_form.formdef.fields = [
+            fields.StringField(id='1', label='Test', varname='str', type='string', required=True),
+            fields.StringField(id='2', label='Test2', varname='str2',
+                type='string', required=True,
+                condition={'type': 'django', 'value': '0'}),
+    ]
+    wf.store()
+
+    resp = login(get_app(pub)).get(formdata.get_url(backoffice=True))
+    assert 'f1' in resp.form.fields
+    assert 'f2' not in resp.form.fields
+
+    # check condition based on formdata
+    display_form.formdef.fields = [
+            fields.StringField(id='1', label='Test', varname='str', type='string', required=True),
+            fields.StringField(id='2', label='Test2', varname='str2',
+                type='string', required=True,
+                condition={'type': 'django', 'value': 'form_var_plop'}),
+    ]
+    wf.store()
+
+    resp = login(get_app(pub)).get(formdata.get_url(backoffice=True))
+    assert 'f1' in resp.form.fields
+    assert 'f2' in resp.form.fields
+
+    display_form.formdef.fields = [
+            fields.StringField(id='1', label='Test', varname='str', type='string', required=True),
+            fields.StringField(id='2', label='Test2', varname='str2',
+                type='string', required=True,
+                condition={'type': 'django', 'value': 'form_var_plop != "xxx"'}),
+    ]
+    wf.store()
+
+    resp = login(get_app(pub)).get(formdata.get_url(backoffice=True))
+    assert 'f1' in resp.form.fields
+    assert 'f2' in resp.form.fields
+
+    # check with live conditions
+    display_form.formdef.fields = [
+            fields.StringField(id='1', label='Test', varname='str', type='string', required=True),
+            fields.StringField(id='2', label='Test2', varname='str2',
+                type='string', required=True,
+                condition={'type': 'django', 'value': 'blah_var_str == "xxx"'}),
+    ]
+    wf.store()
+
+    resp = login(get_app(pub)).get(formdata.get_url(backoffice=True))
+    assert 'f1' in resp.form.fields
+    assert 'f2' in resp.form.fields
+    assert resp.html.find('div', {'data-field-id': '1'}).attrs['data-live-source'] == 'true'
+    assert resp.html.find('div', {'data-field-id': '2'}).attrs.get('style') == 'display: none'
+    live_url = resp.html.find('form').attrs['data-live-url']
+    resp.form['f1'] = ''
+    live_resp = app.post(live_url, params=resp.form.submit_fields())
+    assert live_resp.json['result']['1']['visible']
+    assert not live_resp.json['result']['2']['visible']
+
+    resp.form['f1'] = 'xxx'
+    live_resp = app.post(live_url, params=resp.form.submit_fields())
+    assert live_resp.json['result']['1']['visible']
+    assert live_resp.json['result']['2']['visible']
+
+    # check submit doesn't work
+    resp = resp.form.submit('submit')
+    assert 'There were errors processing your form.' in resp.body
+
+    resp.form['f1'] = 'xxx2'
+    live_resp = app.post(live_url, params=resp.form.submit_fields())
+    assert live_resp.json['result']['1']['visible']
+    assert not live_resp.json['result']['2']['visible']
+
+    # check submit does work when second field is hidden
+    resp = resp.form.submit('submit').follow()
+
+    assert formdef.data_class().get(formdata.id).workflow_data == {'blah_var_str': 'xxx2', 'blah_var_str2': None}
+
 def test_backoffice_criticality_in_formdef_listing(pub):
     if not pub.is_using_postgresql():
         pytest.skip('this requires SQL')
diff --git a/tests/test_form_pages.py b/tests/test_form_pages.py
index 5d0491869..e48c9063b 100644
--- a/tests/test_form_pages.py
+++ b/tests/test_form_pages.py
@@ -6465,3 +6465,119 @@ def test_form_recall_draft(pub):
     assert 'You already started to fill this form.' in resp.body
     assert 'href="%s"' % draft.id in resp.body
     assert 'href="%s"' % draft2.id in resp.body
+
+def test_frontoffice_workflow_form_with_conditions(pub):
+    user = create_user(pub)
+    wf = Workflow.get_default_workflow()
+    wf.id = '2'
+    wf.store()
+    wf = Workflow.get(wf.id)
+    status = wf.get_status('new')
+    status.items = []
+    display_form = FormWorkflowStatusItem()
+    display_form.id = '_display_form'
+    display_form.by = ['_submitter']
+    display_form.varname = 'blah'
+    display_form.formdef = WorkflowFormFieldsFormDef(item=display_form)
+    display_form.formdef.fields = [
+            fields.StringField(id='1', label='Test', varname='str', type='string', required=True),
+            fields.StringField(id='2', label='Test2', varname='str2', type='string', required=True),
+    ]
+    status.items.append(display_form)
+    display_form.parent = status
+
+    wf.store()
+    formdef = create_formdef()
+    formdef.workflow_id = wf.id
+    formdef.fields = [fields.StringField(id='0', label='string', varname='plop')]
+    formdef.store()
+
+    formdef.data_class().wipe()
+
+    formdata = formdef.data_class()()
+    formdata.user_id = user.id
+    formdata.status = 'wf-new'
+    formdata.data = {'0': 'plop'}
+    formdata.store()
+
+    app = login(get_app(pub), username='foo', password='foo')
+    resp = app.get(formdata.get_url(backoffice=False))
+    assert 'f1' in resp.form.fields
+    assert 'f2' in resp.form.fields
+
+    # check with static condition
+    display_form.formdef.fields = [
+            fields.StringField(id='1', label='Test', varname='str', type='string', required=True),
+            fields.StringField(id='2', label='Test2', varname='str2',
+                type='string', required=True,
+                condition={'type': 'django', 'value': '0'}),
+    ]
+    wf.store()
+
+    resp = login(get_app(pub), username='foo', password='foo').get(formdata.get_url(backoffice=False))
+    assert 'f1' in resp.form.fields
+    assert 'f2' not in resp.form.fields
+
+    # check condition based on formdata
+    display_form.formdef.fields = [
+            fields.StringField(id='1', label='Test', varname='str', type='string', required=True),
+            fields.StringField(id='2', label='Test2', varname='str2',
+                type='string', required=True,
+                condition={'type': 'django', 'value': 'form_var_plop'}),
+    ]
+    wf.store()
+
+    resp = login(get_app(pub), username='foo', password='foo').get(formdata.get_url(backoffice=False))
+    assert 'f1' in resp.form.fields
+    assert 'f2' in resp.form.fields
+
+    display_form.formdef.fields = [
+            fields.StringField(id='1', label='Test', varname='str', type='string', required=True),
+            fields.StringField(id='2', label='Test2', varname='str2',
+                type='string', required=True,
+                condition={'type': 'django', 'value': 'form_var_plop != "xxx"'}),
+    ]
+    wf.store()
+
+    resp = login(get_app(pub), username='foo', password='foo').get(formdata.get_url(backoffice=False))
+    assert 'f1' in resp.form.fields
+    assert 'f2' in resp.form.fields
+
+    # check with live conditions
+    display_form.formdef.fields = [
+            fields.StringField(id='1', label='Test', varname='str', type='string', required=True),
+            fields.StringField(id='2', label='Test2', varname='str2',
+                type='string', required=True,
+                condition={'type': 'django', 'value': 'blah_var_str == "xxx"'}),
+    ]
+    wf.store()
+
+    resp = login(get_app(pub), username='foo', password='foo').get(formdata.get_url(backoffice=False))
+    assert 'f1' in resp.form.fields
+    assert 'f2' in resp.form.fields
+    assert resp.html.find('div', {'data-field-id': '1'}).attrs['data-live-source'] == 'true'
+    assert resp.html.find('div', {'data-field-id': '2'}).attrs.get('style') == 'display: none'
+    live_url = resp.html.find('form').attrs['data-live-url']
+    resp.form['f1'] = ''
+    live_resp = app.post(live_url, params=resp.form.submit_fields())
+    assert live_resp.json['result']['1']['visible']
+    assert not live_resp.json['result']['2']['visible']
+
+    resp.form['f1'] = 'xxx'
+    live_resp = app.post(live_url, params=resp.form.submit_fields())
+    assert live_resp.json['result']['1']['visible']
+    assert live_resp.json['result']['2']['visible']
+
+    # check submit doesn't work
+    resp = resp.form.submit('submit')
+    assert 'There were errors processing your form.' in resp.body
+
+    resp.form['f1'] = 'xxx2'
+    live_resp = app.post(live_url, params=resp.form.submit_fields())
+    assert live_resp.json['result']['1']['visible']
+    assert not live_resp.json['result']['2']['visible']
+
+    # check submit does work when second field is hidden
+    resp = resp.form.submit('submit').follow()
+
+    assert formdef.data_class().get(formdata.id).workflow_data == {'blah_var_str': 'xxx2', 'blah_var_str2': None}
diff --git a/wcs/fields.py b/wcs/fields.py
index 4ec710ec4..748895c8c 100644
--- a/wcs/fields.py
+++ b/wcs/fields.py
@@ -403,14 +403,14 @@ class Field(object):
         except RuntimeError:
             return True
 
-    def get_referenced_varnames(self, value):
-        return re.findall(r'\bform[_\.]var[_\.]([a-zA-Z0-9_]+?)(?:_raw|\b)', value or '')
+    def get_referenced_varnames(self, formdef, value):
+        return re.findall(r'\b%s[_\.]var[_\.]([a-zA-Z0-9_]+?)(?:_raw|\b)' % formdef.var_prefix, value or '')
 
-    def get_condition_varnames(self):
-        return self.get_referenced_varnames(self.condition['value'])
+    def get_condition_varnames(self, formdef):
+        return self.get_referenced_varnames(formdef, self.condition['value'])
 
     def has_live_conditions(self, formdef):
-        varnames = self.get_condition_varnames()
+        varnames = self.get_condition_varnames(formdef)
         if not varnames:
             return False
         field_position = formdef.fields.index(self)
diff --git a/wcs/formdata.py b/wcs/formdata.py
index 97c7e6ee2..a303955b0 100644
--- a/wcs/formdata.py
+++ b/wcs/formdata.py
@@ -525,11 +525,11 @@ class FormData(StorableObject):
             last_seen_author = evolution_part.who or last_seen_author
             yield evolution_part
 
-    def get_workflow_form(self, user):
+    def get_workflow_form(self, user, displayed_fields=None):
         wf_status = self.get_status()
         if not wf_status:
             return None
-        return wf_status.get_action_form(self, user)
+        return wf_status.get_action_form(self, user, displayed_fields=displayed_fields)
 
     def handle_workflow_form(self, user, form):
         wf_status = self.get_status()
@@ -537,6 +537,12 @@ class FormData(StorableObject):
             return None
         return wf_status.handle_form(form, self, user)
 
+    def evaluate_live_workflow_form(self, user, form):
+        wf_status = self.get_status()
+        if not wf_status:
+            return None
+        wf_status.evaluate_live_form(form, self, user)
+
     def pop_previous_marked_status(self):
         if not self.workflow_data or not '_markers_stack' in self.workflow_data:
             return None
diff --git a/wcs/formdef.py b/wcs/formdef.py
index 8874018a2..c4644f0f6 100644
--- a/wcs/formdef.py
+++ b/wcs/formdef.py
@@ -110,6 +110,9 @@ class FormDef(StorableObject):
     # store fields in a separate pickle chunk
     lightweight = True
 
+    # prefix for formdata variables
+    var_prefix = 'form'
+
     # declarations for serialization
     TEXT_ATTRIBUTES = ['name', 'url_name', 'description', 'keywords',
             'publication_date', 'expiration_date', 'internal_identifier',
@@ -627,6 +630,36 @@ class FormDef(StorableObject):
 
         return form
 
+    def set_live_condition_sources(self, form, fields):
+        live_condition_fields = {}
+        for field in fields:
+            if field.condition:
+                field.varnames = field.get_condition_varnames(formdef=self)
+                for varname in field.varnames:
+                    if not varname in live_condition_fields:
+                        live_condition_fields[varname] = []
+                    live_condition_fields[varname].append(field)
+            if field.key == 'item' and field.data_source:
+                real_data_source = data_sources.get_real(field.data_source)
+                if real_data_source.get('type') != 'json':
+                    continue
+                varnames = field.get_referenced_varnames(
+                        formdef=self,
+                        value=real_data_source.get('value'))
+                for varname in varnames:
+                    if not varname in live_condition_fields:
+                        live_condition_fields[varname] = []
+                    live_condition_fields[varname].append(field)
+            if field.key == 'comment':
+                for varname in field.get_referenced_varnames(formdef=self, value=field.label):
+                    if not varname in live_condition_fields:
+                        live_condition_fields[varname] = []
+                    live_condition_fields[varname].append(field)
+
+        for field in fields:
+            if field.varname in live_condition_fields:
+                form.get_widget('f%s' % field.id).live_condition_source = True
+
     def get_field_data(self, field, widget):
         d = {}
         d[field.id] = widget.parse()
diff --git a/wcs/forms/common.py b/wcs/forms/common.py
index b9e508817..185419307 100644
--- a/wcs/forms/common.py
+++ b/wcs/forms/common.py
@@ -21,6 +21,7 @@ from quixote import get_publisher, get_request, get_response, get_session, redir
 from quixote.directory import Directory
 from quixote.html import TemplateIO, htmltext
 
+from wcs import data_sources
 from wcs.api_utils import get_user_from_api_query_string, is_url_signed
 from wcs.fields import WidgetField, FileField
 from wcs.workflows import EditableWorkflowStatusItem
@@ -114,7 +115,7 @@ class FormTemplateMixin(object):
 
 
 class FormStatusPage(Directory, FormTemplateMixin):
-    _q_exports_orig = ['', 'download', 'json', 'action']
+    _q_exports_orig = ['', 'download', 'json', 'action', 'live']
     _q_extra_exports = []
     form_page_class = None
 
@@ -238,18 +239,10 @@ class FormStatusPage(Directory, FormTemplateMixin):
         get_logger().info('form %s - id: %s - view' % (self.formdef.name, self.filled.id))
 
         user = get_request().user
-        form = self.filled.get_workflow_form(user)
-        if form and form.is_submitted():
-            if not form.has_errors():
-                url = self.submit(form, comment_only = True)
-            if not form.has_errors():
-                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"
+        form = self.get_workflow_form(user)
+        response = self.check_submitted_form(form)
+        if response:
+            return response
 
         if form:
             form.add_media()
@@ -267,6 +260,34 @@ class FormStatusPage(Directory, FormTemplateMixin):
                 templates=list(self.get_formdef_template_variants(self.status_templates)),
                 context=context)
 
+    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
+
+
+    def check_submitted_form(self, form):
+        if form and form.is_submitted() and 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"
+
+
     def export_to_json(self, anonymise=False):
         get_response().set_content_type('application/json')
         return self.filled.export_to_json(anonymise=anonymise)
@@ -467,26 +488,11 @@ class FormStatusPage(Directory, FormTemplateMixin):
             return redirect('./#lock-notice')
 
         user = self.check_receiver()
-        form = None
-
-        try:
-            form = self.filled.get_workflow_form(user)
-        except:
-            # XXX: probably because there are mixed forms, with and without
-            # workflow; send a trace nevertheless.
-            get_publisher().notify_of_exception(sys.exc_info(), context='[BACKOFFICE]')
-            form = Form()
-
-        if form and form.is_submitted() and not form.has_errors():
-            url = self.submit(form)
+        form = self.get_workflow_form(user)
+        response = self.check_submitted_form(form)
+        if response:
             get_session().unmark_visited_object(object_key)
-            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"
+            return response
 
         get_logger().info('form %s - id: %s - view status' % (self.formdef.name, self.filled.id))
         get_response().add_javascript(['jquery.js', 'qommon.forms.js'])
@@ -566,7 +572,7 @@ class FormStatusPage(Directory, FormTemplateMixin):
         r += htmltext('<a href="..">%s</a>') % _('Back to Listing')
         return r.getvalue()
 
-    def submit(self, form, comment_only = False):
+    def submit(self, form):
         current_status = self.filled.status
         user = get_request().user
         next_url = self.filled.handle_workflow_form(user, form)
@@ -612,6 +618,59 @@ class FormStatusPage(Directory, FormTemplateMixin):
             file_url += file.base_filename
         return redirect(file_url)
 
+    @classmethod
+    def live_process_fields(cls, form, formdata, displayed_fields):
+        result = {}
+        for field in displayed_fields:
+            result[field.id] = {'visible': field.is_visible(formdata.data, formdata.formdef)}
+
+        modified_field_varname = None
+        for field in displayed_fields:
+            if field.id == get_request().form.get('modified_field_id'):
+                modified_field_varname = field.varname
+
+        for field in displayed_fields:
+            if field.key == 'item' and field.data_source:
+                data_source = data_sources.get_object(field.data_source)
+                if data_source.type != 'json':
+                    continue
+                varnames = field.get_referenced_varnames(
+                        formdef=formdata.formdef,
+                        value=data_source.data_source.get('value'))
+                if (modified_field_varname is None or modified_field_varname in varnames) and (
+                        field.display_mode == 'autocomplete' and data_source.query_parameter):
+                    # computed earlier, in perform_more_widget_changes, when the field
+                    # was added to the form
+                    result[field.id]['source_url'] = field.url
+                if modified_field_varname in varnames:
+                    result[field.id]['items'] = [
+                            {'id': x[2], 'text': x[1]} for x in field.get_options(mode='lazy')]
+        for widget in form.widgets:
+            if not getattr(widget, 'field', None):
+                continue
+            if widget.field.key == 'comment':
+                result[widget.field.id]['content'] = widget.content
+
+        return json.dumps({'result': result})
+
+    def live(self):
+        get_request().ignore_session = True
+        # live evaluation of fields
+        get_response().set_content_type('application/json')
+        def result_error(reason):
+            return json.dumps({'result': 'error', 'reason': reason})
+
+        session = get_session()
+        if not session:
+            return result_error('missing session')
+
+        displayed_fields = []
+        user = get_request().user
+        form = self.filled.get_workflow_form(user, displayed_fields=displayed_fields)
+        self.filled.evaluate_live_workflow_form(user, form)
+        get_publisher().substitutions.feed(self.filled)
+        return self.live_process_fields(form, self.filled, displayed_fields)
+
     def _q_lookup(self, component):
         if component == 'files':
             self.check_receiver()
diff --git a/wcs/forms/root.py b/wcs/forms/root.py
index 502429555..fd66da3fa 100644
--- a/wcs/forms/root.py
+++ b/wcs/forms/root.py
@@ -386,31 +386,8 @@ class FormPage(Directory, FormTemplateMixin):
                 # always set additional attributes as they will be used for
                 # "live prefill", regardless of existing data.
                 form.get_widget('f%s' % field.id).prefill_attributes = field.get_prefill_attributes()
-            if field.condition:
-                field.varnames = field.get_condition_varnames()
-                for varname in field.varnames:
-                    if not varname in live_condition_fields:
-                        live_condition_fields[varname] = []
-                    live_condition_fields[varname].append(field)
-            if field.key == 'item' and field.data_source:
-                real_data_source = data_sources.get_real(field.data_source)
-                if real_data_source.get('type') != 'json':
-                    continue
-                varnames = re.findall(r'\bform[_\.]var[_\.]([a-zA-Z0-9_]+?)(?:_raw|\b)',
-                        real_data_source.get('value'))
-                for varname in varnames:
-                    if not varname in live_condition_fields:
-                        live_condition_fields[varname] = []
-                    live_condition_fields[varname].append(field)
-            if field.key == 'comment':
-                for varname in field.get_referenced_varnames(field.label):
-                    if not varname in live_condition_fields:
-                        live_condition_fields[varname] = []
-                    live_condition_fields[varname].append(field)
 
-        for field in displayed_fields:
-            if field.varname in live_condition_fields:
-                form.get_widget('f%s' % field.id).live_condition_source = True
+        self.formdef.set_live_condition_sources(form, displayed_fields)
 
         self.html_top(self.formdef.name)
 
@@ -1105,38 +1082,7 @@ class FormPage(Directory, FormTemplateMixin):
                     displayed_fields=displayed_fields,
                     transient_formdata=formdata)
         formdata.data.update(self.formdef.get_data(form))
-
-        result = {}
-        for field in displayed_fields:
-            result[field.id] = {'visible': field.is_visible(formdata.data, self.formdef)}
-
-        modified_field_varname = None
-        for field in displayed_fields:
-            if field.id == get_request().form.get('modified_field_id'):
-                modified_field_varname = field.varname
-
-        for field in displayed_fields:
-            if field.key == 'item' and field.data_source:
-                data_source = data_sources.get_object(field.data_source)
-                if data_source.type != 'json':
-                    continue
-                varnames = re.findall(r'\bform[_\.]var[_\.]([a-zA-Z0-9_]+?)(?:_raw|\b)',
-                        data_source.data_source.get('value'))
-                if (modified_field_varname is None or modified_field_varname in varnames) and (
-                        field.display_mode == 'autocomplete' and data_source.query_parameter):
-                    # computed earlier, in perform_more_widget_changes, when the field
-                    # was added to the form
-                    result[field.id]['source_url'] = field.url
-                if modified_field_varname in varnames:
-                    result[field.id]['items'] = [
-                            {'id': x[2], 'text': x[1]} for x in field.get_options(mode='lazy')]
-        for widget in form.widgets:
-            if not getattr(widget, 'field', None):
-                continue
-            if widget.field.key == 'comment':
-                result[widget.field.id]['content'] = widget.content
-
-        return json.dumps({'result': result})
+        return FormStatusPage.live_process_fields(form, formdata, displayed_fields)
 
     def submitted(self, form, existing_formdata = None):
         if existing_formdata: # modifying
@@ -1678,7 +1624,7 @@ class RootDirectory(AccessControlled, Directory):
 
 
 class PublicFormStatusPage(FormStatusPage):
-    _q_exports_orig = ['', 'download', 'status']
+    _q_exports_orig = ['', 'download', 'status', 'live']
     form_page_class = FormPage
     history_templates = ['wcs/front/formdata_history.html', 'wcs/formdata_history.html']
     status_templates = ['wcs/front/formdata_status.html', 'wcs/formdata_status.html']
diff --git a/wcs/wf/attachment.py b/wcs/wf/attachment.py
index 3b7667d5f..f72e1cfb8 100644
--- a/wcs/wf/attachment.py
+++ b/wcs/wf/attachment.py
@@ -100,7 +100,7 @@ class AddAttachmentWorkflowStatusItem(WorkflowStatusItem):
         else:
             return _('not completed')
 
-    def fill_form(self, form, formdata, user):
+    def fill_form(self, form, formdata, user, **kwargs):
         if self.display_title:
             title = self.title or _('Upload File')
         else:
diff --git a/wcs/wf/export_to_model.py b/wcs/wf/export_to_model.py
index 4b54fd5c3..b81982fa1 100644
--- a/wcs/wf/export_to_model.py
+++ b/wcs/wf/export_to_model.py
@@ -207,7 +207,7 @@ class ExportToModel(WorkflowStatusItem):
         else:
             return _('no model set')
 
-    def fill_form(self, form, formdata, user):
+    def fill_form(self, form, formdata, user, **kwargs):
         if not self.method == 'interactive':
             return
         label = self.label
diff --git a/wcs/wf/form.py b/wcs/wf/form.py
index 1c3980f0b..5a43da8c6 100644
--- a/wcs/wf/form.py
+++ b/wcs/wf/form.py
@@ -56,7 +56,7 @@ class WorkflowFormFieldsFormDef(FormDef):
 
 class WorkflowFormFieldDefPage(FieldDefPage):
     section = 'workflows'
-    blacklisted_attributes = ['condition', 'in_listing']
+    blacklisted_attributes = ['in_listing']
 
     def get_deletion_extra_warning(self):
         return None
@@ -66,7 +66,6 @@ class WorkflowFormFieldsDirectory(FieldsDirectory):
     section = 'workflows'
     support_import = False
     blacklisted_types = ['page']
-    blacklisted_attributes = ['condition']
     field_def_page_class = WorkflowFormFieldDefPage
 
 
@@ -154,10 +153,11 @@ class FormWorkflowStatusItem(WorkflowStatusItem):
             return fields_directory
         return None
 
-    def fill_form(self, form, formdata, user):
+    def fill_form(self, form, formdata, user, displayed_fields=None, **kwargs):
         if not self.formdef:
             return
-        self.formdef.add_fields_to_form(form)
+        self.formdef.var_prefix = self.varname
+        self.formdef.add_fields_to_form(form, displayed_fields=displayed_fields)
         form.add_submit('submit', _('Submit'))
 
         # put varname in a form attribute so it can be used in templates to
@@ -167,6 +167,8 @@ class FormWorkflowStatusItem(WorkflowStatusItem):
         formdata.feed_session()
 
         req = get_request()
+        self.formdef.set_live_condition_sources(form, self.formdef.fields)
+
         for field in self.formdef.fields:
             if ('f%s' % field.id) in req.form:
                 continue
@@ -187,16 +189,19 @@ class FormWorkflowStatusItem(WorkflowStatusItem):
                 form.get_widget('f%s' % field.id).set_value(v)
                 req.form['f%s' % field.id] = v
 
+    def evaluate_live_form(self, form, formdata, user):
+        workflow_data = {}
+        for k, v in get_dict_with_varnames(
+                        self.formdef.fields, self.formdef.get_data(form),
+                        varnames_only=True).items():
+            workflow_data['%s_%s' % (self.varname, k)] = v
+        formdata.update_workflow_data(workflow_data)
+
     def submit_form(self, form, formdata, user, evo):
         if not self.formdef:
             return
         if form.get_submit() == 'submit' and not form.has_errors():
-            workflow_data = {}
-            for k, v in get_dict_with_varnames(
-                            self.formdef.fields, self.formdef.get_data(form),
-                            varnames_only=True).items():
-                workflow_data['%s_%s' % (self.varname, k)] = v
-            formdata.update_workflow_data(workflow_data)
+            self.evaluate_live_form(form, formdata, user)
             formdata.store()
 
     def get_parameters_view(self):
diff --git a/wcs/wf/resubmit.py b/wcs/wf/resubmit.py
index 18891b8ea..f19428e68 100644
--- a/wcs/wf/resubmit.py
+++ b/wcs/wf/resubmit.py
@@ -46,7 +46,7 @@ class ResubmitWorkflowStatusItem(WorkflowStatusItem):
         else:
             return _('not completed')
 
-    def fill_form(self, form, formdata, user):
+    def fill_form(self, form, formdata, user, **kwargs):
         label = self.label
         if not label:
             label = _('Resubmit')
diff --git a/wcs/workflows.py b/wcs/workflows.py
index c3b12d214..44e6a7057 100644
--- a/wcs/workflows.py
+++ b/wcs/workflows.py
@@ -1352,7 +1352,7 @@ class WorkflowStatus(object):
                 return item
         raise KeyError()
 
-    def get_action_form(self, filled, user):
+    def get_action_form(self, filled, user, displayed_fields=None):
         form = Form(enctype='multipart/form-data', use_tokens=False)
         form.attrs['id'] = 'wf-actions'
         for item in self.items:
@@ -1360,7 +1360,7 @@ class WorkflowStatus(object):
                 continue
             if not item.check_condition(filled):
                 continue
-            item.fill_form(form, filled, user)
+            item.fill_form(form, filled, user, displayed_fields=displayed_fields)
 
         for action in filled.formdef.workflow.get_global_actions_for_user(filled, user):
             form.add_submit('button-action-%s' % action.id, action.name)
@@ -1372,25 +1372,7 @@ class WorkflowStatus(object):
         else:
             return None
 
-    def handle_form(self, form, filled, user):
-        # check for global actions
-        for action in filled.formdef.workflow.get_global_actions_for_user(filled, user):
-            if 'button-action-%s' % action.id in get_request().form:
-                url = perform_items(action.items, filled)
-                if url:
-                    return url
-                return
-
-        evo = Evolution()
-        evo.time = time.localtime()
-        if user:
-            if filled.is_submitter(user):
-                evo.who = '_submitter'
-            else:
-                evo.who = user.id
-        if not filled.evolution:
-            filled.evolution = []
-
+    def get_active_items(self, form, filled, user):
         for item in self.items:
             if hasattr(item, 'by'):
                 for role in item.by or []:
@@ -1410,6 +1392,32 @@ class WorkflowStatus(object):
                     continue
             if not item.check_condition(filled):
                 continue
+            yield item
+
+    def evaluate_live_form(self, form, filled, user):
+        for item in self.get_active_items(form, filled, user):
+            item.evaluate_live_form(form, filled, user)
+
+    def handle_form(self, form, filled, user):
+        # check for global actions
+        for action in filled.formdef.workflow.get_global_actions_for_user(filled, user):
+            if 'button-action-%s' % action.id in get_request().form:
+                url = perform_items(action.items, filled)
+                if url:
+                    return url
+                return
+
+        evo = Evolution()
+        evo.time = time.localtime()
+        if user:
+            if filled.is_submitter(user):
+                evo.who = '_submitter'
+            else:
+                evo.who = user.id
+        if not filled.evolution:
+            filled.evolution = []
+
+        for item in self.get_active_items(form, filled, user):
             next_url = item.submit_form(form, filled, user, evo)
             if next_url is True:
                 break
@@ -1611,7 +1619,10 @@ class WorkflowStatusItem(XmlSerialisable):
     def perform(self, formdata):
         pass
 
-    def fill_form(self, form, formdata, user):
+    def fill_form(self, form, formdata, user, **kwargs):
+        pass
+
+    def evaluate_live_form(self, form, formdata, user):
         pass
 
     def submit_form(self, form, formdata, user, evo):
@@ -2005,7 +2016,7 @@ class CommentableWorkflowStatusItem(WorkflowStatusItem):
         else:
             return _('not completed')
 
-    def fill_form(self, form, formdata, user):
+    def fill_form(self, form, formdata, user, **kwargs):
         if not 'comment' in [x.name for x in form.widgets]:
             if self.label is None:
                 title = _('Comment')
@@ -2144,7 +2155,7 @@ class ChoiceWorkflowStatusItem(WorkflowStatusJumpItem):
         else:
             return _('not completed')
 
-    def fill_form(self, form, formdata, user):
+    def fill_form(self, form, formdata, user, **kwargs):
         label = self.compute(self.label)
         if not label:
             return
@@ -2727,7 +2738,7 @@ class EditableWorkflowStatusItem(WorkflowStatusItem):
         else:
             return _('not completed')
 
-    def fill_form(self, form, formdata, user):
+    def fill_form(self, form, formdata, user, **kwargs):
         label = self.label
         if not label:
             label = _('Edit Form')
-- 
2.20.1

