general: redo the workflow options mecanism with targeted variables (#6170)

The current system is kept in place but not advertised anymore.
This commit is contained in:
Frédéric Péters 2015-02-02 16:51:45 +01:00
parent c54ff30be0
commit ce8254372a
9 changed files with 402 additions and 6 deletions

View File

@ -16,7 +16,7 @@ from wcs.qommon.http_request import HTTPRequest
from wcs.qommon.template import get_current_theme
from wcs.categories import Category
from wcs.roles import Role
from wcs.workflows import Workflow
from wcs.workflows import Workflow, DisplayMessageWorkflowStatusItem
from wcs.formdef import FormDef
from wcs import fields
@ -298,6 +298,68 @@ def test_form_workflow_role():
resp = resp.forms[0].submit('submit')
assert FormDef.get(1).workflow_roles == {'_receiver': 1}
def test_form_workflow_options():
create_superuser()
create_role()
Workflow.wipe()
workflow = Workflow(name='Workflow One')
workflow.store()
FormDef.wipe()
formdef = FormDef()
formdef.name = 'form title'
formdef.fields = []
formdef.workflow_id = workflow.id
formdef.workflow_options = {'2*1*body': 'xxx'}
formdef.store()
app = login(get_app(pub))
resp = app.get('/admin/forms/1/')
assert '"workflow-options"' in resp.body
def test_form_workflow_variables():
create_superuser()
create_role()
Workflow.wipe()
workflow = Workflow(name='Workflow One')
from wcs.workflows import WorkflowVariablesFieldsFormDef
workflow.variables_formdef = WorkflowVariablesFieldsFormDef(workflow=workflow)
workflow.variables_formdef.fields.append(
fields.StringField(id='1', varname='test', label='Test', type='string'))
workflow.store()
FormDef.wipe()
formdef = FormDef()
formdef.name = 'form title'
formdef.fields = []
formdef.workflow_id = workflow.id
formdef.store()
app = login(get_app(pub))
resp = app.get('/admin/forms/1/')
assert '"workflow-variables"' in resp.body
# visit the variables page
resp = resp.click(href='workflow-variables')
# and set a value
resp.forms[0]['f1'] = 'foobar'
resp = resp.forms[0].submit()
assert resp.location == 'http://example.net/admin/forms/1/'
# check the value has been correctly saved
assert FormDef.get(formdef.id).workflow_options == {'test': 'foobar'}
# go back to the variables page, also check value
resp = resp.follow()
resp = resp.click(href='workflow-variables')
assert resp.forms[0]['f1'].value == 'foobar'
resp.forms[0]['f1'] = 'barbaz'
resp = resp.forms[0].submit('cancel')
assert resp.location == 'http://example.net/admin/forms/1/'
def test_form_acl_read():
create_superuser()
create_role()
@ -694,6 +756,64 @@ def test_workflows_add_all_actions():
resp = resp.follow() # redirect to items/
resp = resp.follow() # redirect to ./
def test_workflows_variables():
create_superuser()
create_role()
Workflow.wipe()
workflow = Workflow(name='foo')
workflow.store()
app = login(get_app(pub))
resp = app.get('/admin/workflows/1/')
resp = resp.click(href='variables/')
assert resp.location == 'http://example.net/admin/workflows/1/variables/fields/'
resp = resp.follow()
# makes sure we can't add page fields
assert 'value="New Page"' not in resp.body
# add a simple field
resp.forms[0]['label'] = 'foobar'
resp.forms[0]['type'] = 'Text (line)'
resp = resp.forms[0].submit()
assert resp.location == 'http://example.net/admin/workflows/1/variables/fields/'
resp = resp.follow()
# check it's been saved correctly
assert 'foobar' in resp.body
assert len(Workflow.get(1).variables_formdef.fields) == 1
assert Workflow.get(1).variables_formdef.fields[0].key == 'string'
assert Workflow.get(1).variables_formdef.fields[0].label == 'foobar'
def test_workflows_variables_edit():
test_workflows_variables()
app = login(get_app(pub))
resp = app.get('/admin/workflows/1/')
resp = resp.click(href='variables/')
assert resp.location == 'http://example.net/admin/workflows/1/variables/fields/'
resp = resp.follow()
resp = resp.click('Edit', href='1/')
assert resp.forms[0]['varname$name'].value == 'foobar'
assert not 'varname$select' in resp.forms[0].fields
workflow = Workflow.get(1)
baz_status = workflow.add_status(name='baz')
display_message = DisplayMessageWorkflowStatusItem()
display_message.parent = baz_status
baz_status.items.append(display_message)
workflow.store()
resp = app.get('/admin/workflows/1/variables/fields/')
resp = resp.click('Edit', href='1/')
assert 'varname$select' in resp.forms[0].fields
resp.forms[0]['varname$select'].value = '1*1*message'
resp = resp.forms[0].submit('submit')
assert Workflow.get(1).variables_formdef.fields[0].key == 'string'
assert Workflow.get(1).variables_formdef.fields[0].varname == '1*1*message'
def test_users():
create_superuser()
app = login(get_app(pub))

View File

@ -5,6 +5,8 @@ import shutil
from quixote import cleanup
from wcs import formdef
from wcs.formdef import FormDef
from wcs.workflows import Workflow
from wcs.fields import StringField
from utilities import create_temporary_pub
@ -89,3 +91,20 @@ def test_title_change():
formdef.store()
assert FormDef.get(formdef.id).name == 'baz'
assert FormDef.get(formdef.id).url_name == 'bar' # didn't change
def test_substitution_variables():
formdef = FormDef()
formdef.name = 'foo'
formdef.store()
from wcs.workflows import WorkflowVariablesFieldsFormDef
wf = Workflow(name='variables')
wf.variables_formdef = WorkflowVariablesFieldsFormDef(workflow=wf)
wf.variables_formdef.fields.append(
StringField(label='Test', type='string', varname='foo'))
wf.store()
formdef.workflow_id = wf.id
assert formdef.get_substitution_variables() == {'form_name': 'foo'}
formdef.workflow_options = {'foo': 'bar'}
assert formdef.get_substitution_variables() == {'form_name': 'foo', 'form_option_foo': 'bar'}

View File

@ -135,3 +135,12 @@ def test_modification_time():
assert_xml_import_export_works(formdef)
f2 = assert_json_import_export_works(formdef)
assert tuple(f2.last_modification_time)[:6] == tuple(formdef.last_modification_time)[:6]
def test_workflow_options():
formdef = FormDef()
formdef.name = 'workflow options'
formdef.workflow_options = {'foo': 'bar'}
fd2 = assert_xml_import_export_works(formdef)
assert fd2.workflow_options == formdef.workflow_options
fd2 = assert_json_import_export_works(formdef)
assert fd2.workflow_options == formdef.workflow_options

View File

@ -265,3 +265,12 @@ def test_commentable_action():
wf2 = assert_import_export_works(wf)
assert wf2.possible_status[0].items[0].button_label is None
def test_variables_formdef():
wf = Workflow(name='variables')
from wcs.workflows import WorkflowVariablesFieldsFormDef
wf.variables_formdef = WorkflowVariablesFieldsFormDef(workflow=wf)
wf.variables_formdef.fields.append(StringField(label='Test', type='string'))
wf2 = assert_import_export_works(wf)
assert wf2.variables_formdef.fields[0].label == 'Test'

View File

@ -171,6 +171,7 @@ class FormDefPage(Directory):
_q_exports = ['', 'fields', 'delete', 'duplicate', 'export',
'anonymise', 'archive', 'invite', 'enable', 'workflow',
'category', 'role', ('workflow-options', 'workflow_options'),
('workflow-variables', 'workflow_variables'),
('workflow-status-remapping', 'workflow_status_remapping'),
'roles', 'title', 'options', ('acl-read', 'acl_read'),
'overwrite', 'qrcode', 'information']
@ -260,8 +261,14 @@ class FormDefPage(Directory):
r += '-'
if self.formdef.workflow_id:
pristine_workflow = Workflow.get(self.formdef.workflow_id)
if pristine_workflow.has_options():
r += htmltext(' (<a href="workflow-options">%s</a>)') % _('options')
if pristine_workflow.variables_formdef:
r += htmltext(' (<a rel="popup" href="workflow-variables">%s</a>)') % _('options')
elif self.formdef.workflow_options:
# there are no variables defined but there are some values
# in workflow_options, this is probably the legacy stuff.
if any((x for x in self.formdef.workflow_options if '*' in x)):
r += htmltext(' (<a rel="popup" href="workflow-options">%s</a>)') % _('options')
r += ' '
r += htmltext('(<a href="workflow" rel="popup">%s</a>)') % _('change')
r += htmltext('</li>')
@ -1300,6 +1307,28 @@ class FormDefPage(Directory):
return redirect('fields/')
return redirect('.')
def workflow_variables(self):
self.html_top(title=_('Options'))
form = Form(enctype='multipart/form-data')
self.formdef.workflow.variables_formdef.add_fields_to_form(form,
form_data=self.formdef.get_variable_options_for_form())
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
if form.get_widget('cancel').parse():
return redirect('.')
if form.is_submitted() and not form.has_errors():
self.formdef.set_variable_options(form)
self.formdef.store()
return redirect('.')
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('Options')
r += form.render()
return r.getvalue()
def workflow_options(self):
request = get_request()
if request.get_method() == 'GET' and request.form.get('file'):

View File

@ -35,6 +35,7 @@ from qommon import get_logger
from wcs.workflows import *
from wcs.formdef import FormDef
from wcs.formdata import Evolution
from .fields import FieldDefPage, FieldsDirectory
def svg(tag):
@ -658,9 +659,97 @@ class WorkflowStatusDirectory(Directory):
return redirect('..')
class WorkflowVariableWidget(CompositeWidget):
def __init__(self, name, value=None, workflow=None, **kwargs):
CompositeWidget.__init__(self, name, **kwargs)
if value and '*' in value:
varname = None
else:
varname = value
self.add(VarnameWidget, 'name', render_br=False, value=varname)
options = []
if workflow:
for status in workflow.possible_status:
for item in status.items:
prefix = '%s*%s*' % (status.id, item.id)
parameters = [x for x in item.get_parameters() if not getattr(item, x)]
label = getattr(item, 'label', None) or _(item.description)
for parameter in parameters:
key = prefix + parameter
fake_form = Form()
item.add_parameters_widgets(fake_form, [parameter])
parameter_label = fake_form.widgets[0].title
option_value = '%s / %s / %s' % (status.name, label, parameter_label)
options.append((key, option_value, key))
if not options:
return
options = [('', '---', '')] + options
self.widgets.append(HtmlWidget(
_('or you can use this field to directly replace a workflow parameter:')))
self.add(SingleSelectWidget, 'select', options=options,
value=value,
hint=_('This takes priority over a variable name'))
def _parse(self, request):
super(WorkflowVariableWidget, self)._parse(request)
if self.get('select'):
self.value = self.get('select')
elif self.get('name'):
self.value = self.get('name')
class WorkflowVariablesFieldDefPage(FieldDefPage):
section = 'workflows'
def form(self):
form = super(WorkflowVariablesFieldDefPage, self).form()
form.remove('varname')
form.add(WorkflowVariableWidget, 'varname', title=_('Variable'),
value=self.field.varname, advanced=False, required=True,
workflow=self.objectdef.workflow)
return form
class WorkflowVariablesFieldsDirectory(FieldsDirectory):
_q_exports = ['', 'update_order', 'new']
section = 'settings'
field_def_page_class = WorkflowVariablesFieldDefPage
support_import = False
blacklisted_types = ['page']
def index_top(self):
r = TemplateIO(html=True)
r += htmltext('<h2>%s - %s - %s</h2>') % (_('Workflow'),
self.objectdef.name, _('Variables'))
r += get_session().display_message()
if not self.objectdef.fields:
r += htmltext('<p>%s</p>') % _('There are not yet any variables.')
return r.getvalue()
def index_bottom(self):
pass
class VariablesDirectory(Directory):
_q_exports = ['', 'fields']
def __init__(self, workflow):
self.workflow = workflow
def _q_index(self):
return redirect('fields/')
def _q_traverse(self, path):
get_response().breadcrumb.append(('variables/', _('Variables')))
self.fields = WorkflowVariablesFieldsDirectory(
WorkflowVariablesFieldsFormDef(self.workflow))
return Directory._q_traverse(self, path)
class WorkflowPage(Directory):
_q_exports = ['', 'edit', 'delete', 'newstatus', ('status', 'status_dir'), 'update_order',
'duplicate', 'export', 'svg']
'duplicate', 'export', 'svg', ('variables', 'variables_dir')]
def __init__(self, component, html_top):
try:
@ -670,6 +759,7 @@ class WorkflowPage(Directory):
self.html_top = html_top
self.workflow_ui = WorkflowUI(self.workflow)
self.status_dir = WorkflowStatusDirectory(self.workflow, html_top)
self.variables_dir = VariablesDirectory(self.workflow)
get_response().breadcrumb.append((component + '/', self.workflow.name))
def _q_index(self):
@ -735,6 +825,23 @@ class WorkflowPage(Directory):
r += htmltext('</li>')
r += htmltext('</ul>')
r += htmltext('</div>')
if not str(self.workflow.id).startswith('_'):
r += htmltext('<div class="bo-block">')
r += htmltext('<h3>%s') % _('Workflow Variables')
r += htmltext(' <span class="change">(<a href="variables/">%s</a>)</span></h3>') % _('change')
if self.workflow.variables_formdef:
r += htmltext('<ul>')
for field in self.workflow.variables_formdef.fields:
if field.varname:
if '*' in field.varname:
r += htmltext('<li>%s</li>') % field.label
else:
r += htmltext('<li>%s (<code>%s</code>)</li>') % (field.label,
field.varname)
r += htmltext('</ul>')
r += htmltext('</div>')
r += htmltext('</div>') # .splitcontent-right
r += htmltext('<br style="clear:both;"/>')

View File

@ -31,7 +31,6 @@ from qommon.substitution import Substitutions
from formdata import FormData
from roles import logged_users_role
from categories import Category
from wcs.workflows import Workflow, get_role_translation
import fields
@ -260,6 +259,7 @@ class FormDef(StorableObject):
def get_workflow(self):
if self._workflow:
return self._workflow
from wcs.workflows import Workflow
if self.workflow_id:
try:
workflow = Workflow.get(self.workflow_id)
@ -291,6 +291,41 @@ class FormDef(StorableObject):
self.workflow_id = None
workflow = property(get_workflow, set_workflow)
def get_variable_options(self):
variables = {}
if not self.workflow.variables_formdef:
return variables
if not self.workflow_options:
return variables
for field in self.workflow.variables_formdef.fields:
if not field.varname:
continue
variables['form_option_' + field.varname] = self.workflow_options.get(field.varname)
return variables
def get_variable_options_for_form(self):
variables = {}
if not self.workflow.variables_formdef:
return variables
if not self.workflow_options:
return {}
for field in self.workflow.variables_formdef.fields:
if not field.varname:
continue
variables[str(field.id)] = self.workflow_options.get(field.varname)
return variables
def set_variable_options(self, form):
data = self.workflow.variables_formdef.get_data(form)
variables = {}
for field in self.workflow.variables_formdef.fields:
if not field.varname:
continue
variables[field.varname] = data.get(field.id)
if not self.workflow_options:
self.workflow_options = {}
self.workflow_options.update(variables)
def get_by_urlname(cls, url_name, ignore_migration=False):
return cls.get_on_index(url_name, 'url_name', ignore_migration=ignore_migration)
get_by_urlname = classmethod(get_by_urlname)
@ -430,6 +465,9 @@ class FormDef(StorableObject):
for field in self.fields:
root['fields'].append(field.export_to_json(include_id=include_id))
if self.workflow_options:
root['options'] = self.workflow_options
return json.dumps(root, indent=indent)
def import_from_json(cls, fd, charset=None, include_id=False):
@ -466,6 +504,7 @@ class FormDef(StorableObject):
formdef.workflow_id = value.get('workflow_id')
elif 'workflow' in value:
workflow = value.get('workflow')
from wcs.workflows import Workflow
for w in Workflow.select():
if w.name == workflow:
formdef.workflow_id = w.id
@ -497,6 +536,9 @@ class FormDef(StorableObject):
if formdef.fields and not formdef.max_field_id:
formdef.max_field_id = max([lax_int(x.id) for x in formdef.fields])+1
if value.get('options'):
formdef.workflow_options = value.get('options')
return formdef
import_from_json = classmethod(import_from_json)
@ -548,6 +590,16 @@ class FormDef(StorableObject):
for field in self.fields or []:
fields.append(field.export_to_xml(charset=charset, include_id=include_id))
options = ET.SubElement(root, 'options')
for option in self.workflow_options or []:
element = ET.SubElement(options, 'option')
element.attrib['varname'] = option
option_value = self.workflow_options.get(option)
if isinstance(option_value, basestring):
element.text = self.workflow_options.get(option)
else:
pass # TODO: extend support to other types
return root
def import_from_xml(cls, fd, charset=None, include_id=False):
@ -607,6 +659,10 @@ class FormDef(StorableObject):
else:
formdef.max_field_id = max([lax_int(x.id) for x in formdef.fields])+1
formdef.workflow_options = {}
for option in tree.find('options') or []:
formdef.workflow_options[option.attrib.get('varname')] = option.text
if tree.find('last_modification') is not None:
node = tree.find('last_modification')
formdef.last_modification_time = time.strptime(node.text, '%Y-%m-%d %H:%M:%S')
@ -630,6 +686,7 @@ class FormDef(StorableObject):
formdef.workflow_id = workflow_node.attrib.get('workflow_id')
else:
workflow = workflow_node.text.encode(charset)
from wcs.workflows import Workflow
for w in Workflow.select():
if w.name == workflow:
formdef.workflow_id = w.id
@ -715,9 +772,9 @@ class FormDef(StorableObject):
}
if self.category:
d.update(self.category.get_substitution_variables(minimal=minimal))
d.update(self.get_variable_options())
return d
def get_detailed_evolution(self, formdata):
if not formdata.evolution:
return None
@ -790,6 +847,7 @@ class FormDef(StorableObject):
if self.acl_read == 'roles':
form_roles = (self.roles or [])
if formdata:
from wcs.workflows import get_role_translation
form_roles.extend([get_role_translation(formdata, x)
for x in self.workflow_roles.keys() if x])
form_roles = ensure_role_are_strings(form_roles)

View File

@ -683,6 +683,10 @@ class PicklableUpload(Upload):
self.fp = cStringIO.StringIO(self.data)
del self.data
def get_file(self):
# quack like UploadedFile
return self.get_file_pointer()
class EmailWidget(StringWidget):
HTML_TYPE = 'email'

View File

@ -33,6 +33,7 @@ from quixote.html import htmltext
import qommon.errors
from wcs.roles import Role, logged_users_role, get_user_roles
from wcs.formdef import FormDef
from wcs.formdata import Evolution
from wcs.fields import SubtitleField, TitleField, CommentField, PageField
@ -130,11 +131,35 @@ def get_varnames(fields):
return varnames
class WorkflowVariablesFieldsFormDef(FormDef):
'''Class to handle workflow variables, it loads and saves from/to
the workflow object 'variables_formdef' attribute.'''
def __init__(self, workflow):
self.id = None
self.name = workflow.name
self.workflow = workflow
if workflow.variables_formdef:
self.fields = self.workflow.variables_formdef.fields
self.max_field_id = self.workflow.variables_formdef.max_field_id
else:
self.fields = []
def store(self):
for field in self.fields:
if hasattr(field, 'widget_class'):
if not field.varname:
field.varname = misc.simplify(field.label, space='_')
self.workflow.variables_formdef = self
self.workflow.store()
class Workflow(StorableObject):
_names = 'workflows'
name = None
possible_status = None
roles = None
variables_formdef = None
last_modification_time = None
last_modification_user_id = None
@ -280,6 +305,15 @@ class Workflow(StorableObject):
for status in self.possible_status:
possible_status.append(status.export_to_xml(charset=charset,
include_id=include_id))
if self.variables_formdef:
variables = ET.SubElement(root, 'variables')
formdef = ET.SubElement(variables, 'formdef')
ET.SubElement(formdef, 'name').text = '-' # required by formdef xml import
fields = ET.SubElement(formdef, 'fields')
for field in self.variables_formdef.fields:
fields.append(field.export_to_xml(charset=charset, include_id=include_id))
return root
def import_from_xml(cls, fd, include_id=False):
@ -326,6 +360,13 @@ class Workflow(StorableObject):
status_o.parent = workflow
status_o.init_with_xml(status, charset, include_id=include_id)
workflow.possible_status.append(status_o)
variables = tree.find('variables')
if variables is not None:
formdef = variables.find('formdef')
imported_formdef = FormDef.import_from_xml_tree(formdef, include_id=True)
workflow.variables_formdef = WorkflowVariablesFieldsFormDef(workflow=workflow)
workflow.variables_formdef.fields = imported_formdef.fields
return workflow
import_from_xml_tree = classmethod(import_from_xml_tree)