858 lines
33 KiB
Plaintext
858 lines
33 KiB
Plaintext
# w.c.s. - web application for online forms
|
|
# Copyright (C) 2005-2010 Entr'ouvert
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import time
|
|
from StringIO import StringIO
|
|
from subprocess import Popen, PIPE
|
|
import textwrap
|
|
import xml.etree.ElementTree as etree
|
|
import re
|
|
|
|
from quixote import redirect, get_publisher
|
|
from quixote.directory import Directory
|
|
|
|
from qommon import errors
|
|
from qommon import misc
|
|
from qommon.form import *
|
|
from qommon.admin.menu import html_top, command_icon, error_page
|
|
from qommon import get_logger
|
|
|
|
from wcs.workflows import *
|
|
from wcs.formdef import FormDef
|
|
from wcs.formdata import Evolution
|
|
from wcs.admin.forms import ET, indent
|
|
|
|
|
|
def svg(tag):
|
|
return '{http://www.w3.org/2000/svg}%s' % tag
|
|
|
|
def xlink(tag):
|
|
return '{http://www.w3.org/1999/xlink}%s' % tag
|
|
|
|
TITLE = svg('title')
|
|
POLYGON = svg('polygon')
|
|
XLINK_TITLE = xlink('title')
|
|
|
|
def remove_tag(node, tag):
|
|
for child in node:
|
|
if child.tag == tag:
|
|
node.remove(child)
|
|
|
|
def remove_attribute(node, att):
|
|
if att in node.attrib:
|
|
del node.attrib[att]
|
|
|
|
|
|
def remove_style(node, top):
|
|
remove_tag(node, TITLE)
|
|
if node.get('fill') == 'white' and node.get('stroke') == 'white':
|
|
# this is the general white background, wipe it to be transparent
|
|
node.attrib['fill'] = 'transparent'
|
|
node.attrib['stroke'] = 'transparent'
|
|
for child in node:
|
|
remove_attribute(child, XLINK_TITLE)
|
|
style = child.get('style', None)
|
|
# Beware ! HACK ! salmon is matched and converted to class="page-subject"
|
|
if style:
|
|
m = re.search('(?:stroke|fill):salmon', style)
|
|
if m:
|
|
top.set('class', top.get('class','') + ' page-subject')
|
|
if child.get('fill') == 'salmon':
|
|
top.set('class', top.get('class', '') + ' page-subject')
|
|
if child.get('font-family'):
|
|
del child.attrib['font-family']
|
|
if child.get('font-size'):
|
|
child.attrib['font-size'] = str(float(child.attrib['font-size'])*0.8)
|
|
remove_attribute(child, 'style')
|
|
remove_style(child, top)
|
|
|
|
def graphviz_post_treatment(content):
|
|
''' Remove all svg:title and top-level svg:polygon nodes, remove style
|
|
attributes and xlink:title attributes.
|
|
|
|
If a color style is set to a name matching class-\w+, set the second
|
|
part on as class selector on the top level svg:g element.
|
|
'''
|
|
tree = etree.fromstring(content)
|
|
|
|
for root in tree:
|
|
remove_tag(root, TITLE)
|
|
# remove_tag(root, POLYGON)
|
|
for child in root:
|
|
remove_style(child, child)
|
|
return etree.tostring(tree)
|
|
|
|
def graphviz(workflow, url_prefix='', select=None, svg=True,
|
|
include=False):
|
|
out = StringIO()
|
|
print >>out, 'digraph main {'
|
|
# print >>out, 'graph [ rankdir=LR ];'
|
|
print >>out, 'node [shape=box,style=filled];'
|
|
print >>out, 'edge [];'
|
|
for status in workflow.possible_status:
|
|
i = status.id
|
|
print >>out, 'status%s' % i,
|
|
print >>out, '[label="%s"' % status.name,
|
|
if select == str(i):
|
|
print >>out, ',color=salmon'
|
|
print >>out, ' URL="%sstatus/%s/"];' % (url_prefix, i)
|
|
|
|
for status in workflow.possible_status:
|
|
i = status.id
|
|
for item in status.items:
|
|
next_status_ids = []
|
|
for status_key in item.__dict__:
|
|
if status_key == 'status' or \
|
|
status_key.startswith('status_') or \
|
|
status_key.endswith('_status'):
|
|
next_id = getattr(item, status_key, None)
|
|
if next_id:
|
|
next_status_ids.append(next_id)
|
|
if not next_status_ids:
|
|
next_status_ids = [status.id]
|
|
for next_id in next_status_ids:
|
|
print >>out, 'status%s -> status%s' % (i, next_id)
|
|
url = 'status/%s/items/%s/' % (i, item.id)
|
|
if getattr(item, 'label', None):
|
|
label = item.label
|
|
if getattr(item, 'by', None):
|
|
roles = render_list_of_roles(item.by)
|
|
label += ' %s %s' % (_('by'), roles)
|
|
else:
|
|
label = item.render_as_line()
|
|
label = textwrap.fill(label.replace('"', '\\"'), 20)
|
|
label = label.replace('\n', '\\n')
|
|
print >>out, '[label="%s"' % label,
|
|
if select == '%s-%s' % (i, item.id):
|
|
print >>out, ',color=salmon'
|
|
print >>out, ',URL="%s%s"]' % (url_prefix, url)
|
|
print >>out, '}'
|
|
out = out.getvalue()
|
|
if svg:
|
|
try:
|
|
process = Popen(['dot', '-Tsvg', '/dev/stdin'], stdin=PIPE, stdout=PIPE)
|
|
out, err = process.communicate(out)
|
|
except OSError:
|
|
return ''
|
|
if include:
|
|
out = graphviz_post_treatment(out)
|
|
# It seems webkit refuse to accept SVG when using its proper namespace,
|
|
# and xlink namespace prefix must be xlink: to be acceptable
|
|
out = out.replace('ns0:', '')
|
|
out = out.replace('xmlns:ns0', 'xmlns:svg')
|
|
out = out.replace('ns1:', 'xlink:')
|
|
out = out.replace(':ns1', ':xlink')
|
|
return out
|
|
|
|
|
|
class WorkflowUI:
|
|
def __init__(self, workflow):
|
|
self.workflow = workflow
|
|
|
|
def form_new(self):
|
|
form = Form(enctype='multipart/form-data')
|
|
form.add(StringWidget, 'name', title = _('Workflow Name'), required = True, size=30)
|
|
form.add_submit('submit', _('Submit'))
|
|
form.add_submit('cancel', _('Cancel'))
|
|
return form
|
|
|
|
def form_edit(self):
|
|
form = Form(enctype='multipart/form-data')
|
|
form.add_hidden('id', value = self.workflow.id)
|
|
form.add(StringWidget, 'name', title = _('Workflow Name'), required = True, size=30,
|
|
value = self.workflow.name)
|
|
form.add_submit('submit', _('Submit'))
|
|
form.add_submit('cancel', _('Cancel'))
|
|
return form
|
|
|
|
def submit_form(self, form):
|
|
if self.workflow:
|
|
workflow = self.workflow
|
|
else:
|
|
workflow = Workflow(name = form.get_widget('name').parse())
|
|
|
|
name = form.get_widget('name').parse()
|
|
workflows_name = [x.name for x in Workflow.select() if x.id != workflow.id]
|
|
if name in workflows_name:
|
|
form.get_widget('name').set_error(_('This name is already used'))
|
|
raise ValueError()
|
|
|
|
for f in ('name',):
|
|
setattr(workflow, f, form.get_widget(f).parse())
|
|
workflow.store()
|
|
return workflow
|
|
|
|
class WorkflowItemPage(Directory):
|
|
_q_exports = ['', 'delete']
|
|
|
|
def __init__(self, workflow, status, component):
|
|
try:
|
|
self.item = [x for x in status.items if x.id == component][0]
|
|
except (IndexError, ValueError):
|
|
raise errors.TraversalError()
|
|
self.workflow = workflow
|
|
self.status = status
|
|
get_response().breadcrumb.append(('items/%s/' % component, _(self.item.description)))
|
|
|
|
def _q_index [html] (self):
|
|
request = get_request()
|
|
if request.get_method() == 'GET' and request.form.get('file'):
|
|
value = getattr(self.item, request.form.get('file'), None)
|
|
if value:
|
|
return value.build_response()
|
|
|
|
form = Form(enctype='multipart/form-data')
|
|
self.item.fill_admin_form(form)
|
|
|
|
if not str(self.workflow.id).startswith(str('_')):
|
|
form.add_submit('submit', _('Submit'))
|
|
form.add_submit('cancel', _('Cancel'))
|
|
|
|
if form.get_widget('cancel').parse():
|
|
return redirect('..')
|
|
|
|
if not form.get_submit() == 'submit' or form.has_errors():
|
|
html_top('workflows', '%s - %s' % (_('Workflow'), self.workflow.name))
|
|
'<h2>%s - %s</h2>' % (self.workflow.name, self.status.name)
|
|
'<h3>%s</h3>' % _(self.item.description)
|
|
form.render()
|
|
if self.item.support_substitution_variables:
|
|
'<h3>%s</h3>' % _('Substitution Variables')
|
|
get_publisher().substitutions.get_substitution_html_table()
|
|
else:
|
|
self.item.submit_admin_form(form)
|
|
self.workflow.store()
|
|
return redirect('..')
|
|
|
|
get_response().filter['sidebar'] = self.get_sidebar()
|
|
|
|
def get_sidebar [html] (self):
|
|
if str(self.workflow.id).startswith(str('_')):
|
|
'<p>'
|
|
_('''This is the default workflow, you cannot edit it but you can
|
|
duplicate it to base your own workflow on it.''')
|
|
'</p>'
|
|
|
|
def delete [html] (self):
|
|
form = Form(enctype='multipart/form-data')
|
|
form.widgets.append(HtmlWidget('<p>%s</p>' % _('You are about to remove an item.')))
|
|
form.add_submit('submit', _('Submit'))
|
|
form.add_submit('cancel', _('Cancel'))
|
|
if form.get_widget('cancel').parse():
|
|
return redirect('../../')
|
|
if not form.is_submitted() or form.has_errors():
|
|
get_response().breadcrumb.append(('delete', _('Delete')))
|
|
html_top('workflows', title = _('Delete Item'))
|
|
'<h2>%s</h2>' % _('Deleting Item')
|
|
form.render()
|
|
else:
|
|
del self.status.items[self.status.items.index(self.item)]
|
|
self.workflow.store()
|
|
return redirect('../../')
|
|
|
|
|
|
|
|
class WorkflowItemsDir(Directory):
|
|
_q_exports = ['']
|
|
|
|
def __init__(self, workflow, status):
|
|
self.workflow = workflow
|
|
self.status = status
|
|
|
|
def _q_lookup(self, component):
|
|
return WorkflowItemPage(self.workflow, self.status, component)
|
|
|
|
def _q_index(self):
|
|
return redirect('..')
|
|
|
|
class WorkflowStatusPage(Directory):
|
|
_q_exports = ['', 'delete', 'newitem', ('items', 'items_dir'),
|
|
'update_order', 'edit', 'reassign', 'visibility',
|
|
'endpoint']
|
|
|
|
def __init__(self, workflow, status_id):
|
|
self.workflow = workflow
|
|
try:
|
|
self.status = [x for x in self.workflow.possible_status if x.id == status_id][0]
|
|
except IndexError:
|
|
raise errors.TraversalError()
|
|
|
|
self.items_dir = WorkflowItemsDir(workflow, self.status)
|
|
get_response().breadcrumb.append(('status/%s/' % status_id, _('Status: %s') % self.status.name))
|
|
|
|
def _q_index [html] (self):
|
|
html_top('workflows', '%s - %s' % (_('Workflow'), self.workflow.name))
|
|
get_response().add_javascript(['jquery.js', 'interface.js', 'biglist.js'])
|
|
|
|
'<h2>%s - ' % _('Workflow')
|
|
'%s - %s</h2>' % (self.workflow.name, self.status.name)
|
|
get_session().display_message()
|
|
|
|
'<div class="bo-block">'
|
|
'<h3>%s ' % _('Possible Status:')
|
|
'%s</h3>' % self.status.name
|
|
'</div>'
|
|
|
|
if self.status.visibility == ['_receiver']:
|
|
'<div class="bo-block">'
|
|
_('This status is hidden from the user.')
|
|
if not str(self.workflow.id).startswith(str('_')):
|
|
' '
|
|
'(<a href="visibility" rel="popup">%s</a>)' % _('change')
|
|
'</div>'
|
|
|
|
'<div class="bo-block">'
|
|
if not self.status.items:
|
|
'<p>%s</p>' % _('There are not yet any items in this status.')
|
|
else:
|
|
if str(self.workflow.id).startswith(str('_')):
|
|
'<ul id="items-list" class="biglist">'
|
|
else:
|
|
'<p>'
|
|
_('Use drag and drop to reorder items.')
|
|
'</p>'
|
|
'<ul id="items-list" class="biglist sortable">'
|
|
for i, item in enumerate(self.status.items):
|
|
'<li class="biglistitem" id="itemId_%s">' % item.id
|
|
if hasattr(item, str('fill_admin_form')):
|
|
'<a href="items/%s/">%s</a>' % (item.id, item.render_as_line())
|
|
else:
|
|
item.render_as_line()
|
|
if not str(self.workflow.id).startswith(str('_')):
|
|
'<p class="commands">'
|
|
if hasattr(item, str('fill_admin_form')):
|
|
command_icon('items/%s/' % item.id, 'edit')
|
|
command_icon('items/%s/delete' % item.id, 'remove', popup = True)
|
|
'</p>'
|
|
'</li>'
|
|
'</ul>'
|
|
'</div>' # bo-block
|
|
|
|
'<p><a href="../../">%s</a></p>' % _('Back to workflow main page')
|
|
|
|
'<div class="bo-block">'
|
|
htmltext(graphviz(self.workflow, url_prefix='../../', include=True,
|
|
select='%s' % self.status.id))
|
|
'</div>'
|
|
|
|
get_response().filter['sidebar'] = self.get_sidebar()
|
|
|
|
|
|
def get_sidebar [html] (self):
|
|
if str(self.workflow.id).startswith(str('_')):
|
|
'<p>'
|
|
_('''This is the default workflow, you cannot edit it but you can
|
|
duplicate it to base your own workflow on it.''')
|
|
'</p>'
|
|
else:
|
|
'<ul>'
|
|
'<li><a href="edit">%s</a></li>' % _('Change Status Name')
|
|
'<li><a href="visibility" rel="popup">%s</a></li>' % _('Change Status Visibility')
|
|
'<li><a href="endpoint" rel="popup">%s</a></li>' % _('Change Terminal Status')
|
|
'<li><a href="delete" rel="popup">%s</a></li>' % _('Delete')
|
|
'</ul>'
|
|
'<div id="new-field">'
|
|
'<h3>%s</h3>' % _('New Item')
|
|
self.get_new_item_form().render()
|
|
'</div>'
|
|
|
|
def get_new_item_form [html] (self):
|
|
form = Form(enctype='multipart/form-data', action = 'newitem')
|
|
options = [(x.key, _(x.description)) for x in item_classes if x.is_available()]
|
|
options.sort(cmp=lambda a, b: cmp(a[1],b[1]))
|
|
form.add(SingleSelectWidget, 'type', title = _('Type'),
|
|
required=True, options = options)
|
|
form.add_submit('submit', _('Add'))
|
|
return form
|
|
|
|
def update_order(self):
|
|
request = get_request()
|
|
new_order = request.form['order'].strip(';').split(';')
|
|
self.status.items = [ [x for x in self.status.items if x.id == y][0] for y in new_order]
|
|
self.workflow.store()
|
|
return 'ok'
|
|
|
|
|
|
def newitem(self):
|
|
form = self.get_new_item_form()
|
|
|
|
if not form.is_submitted() or form.has_errors():
|
|
get_session().message = ('error', _('Submitted form was not filled properly.'))
|
|
return redirect('.')
|
|
|
|
if form.get_widget('type').parse():
|
|
self.status.append_item(form.get_widget('type').parse())
|
|
else:
|
|
get_session().message = ('error', _('Submitted form was not filled properly.'))
|
|
return redirect('.')
|
|
|
|
self.workflow.store()
|
|
|
|
return redirect('.')
|
|
|
|
|
|
def delete [html] (self):
|
|
form = Form(enctype="multipart/form-data")
|
|
form.widgets.append(HtmlWidget('<p>%s</p>' % _(
|
|
"You are about to remove a status.")))
|
|
form.add_submit("submit", _("Submit"))
|
|
form.add_submit("cancel", _("Cancel"))
|
|
if form.get_widget('cancel').parse():
|
|
return redirect('../../')
|
|
if not form.is_submitted() or form.has_errors():
|
|
get_response().breadcrumb.append(('delete', _('Delete')))
|
|
html_top('workflows', title = _('Delete Status'))
|
|
'<h2>%s %s</h2>' % (_('Deleting Status:'), self.status.name)
|
|
form.render()
|
|
else:
|
|
# Before removing the status, scan formdata to know if it's in use.
|
|
for formdef in FormDef.select():
|
|
if formdef.workflow_id != self.workflow.id:
|
|
continue
|
|
if formdef.data_class().get_with_indexed_value(
|
|
str('status'), 'wf-%s' % self.status.id):
|
|
return redirect('reassign')
|
|
del self.workflow.possible_status[ self.workflow.possible_status.index(self.status) ]
|
|
self.workflow.store()
|
|
return redirect('../../')
|
|
|
|
def edit [html] (self):
|
|
form = Form(enctype = 'multipart/form-data')
|
|
form.add(StringWidget, 'name', title = _('Status Name'), required = True, size=30,
|
|
value = self.status.name)
|
|
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():
|
|
new_name = str(form.get_widget('name').parse())
|
|
if [x for x in self.workflow.possible_status if x.name == new_name]:
|
|
form.get_widget('name').set_error(
|
|
_('There is already a status with that name.'))
|
|
else:
|
|
self.status.name = new_name
|
|
self.workflow.store()
|
|
return redirect('.')
|
|
|
|
html_top('workflows', title = _('Edit Workflow Status'))
|
|
get_response().breadcrumb.append( ('edit', _('Edit')) )
|
|
'<h2>%s</h2>' % _('Edit Workflow Status')
|
|
form.render()
|
|
|
|
def reassign [html] (self):
|
|
options = [(None, 'Do nothing'),
|
|
('remove', _('Remove these forms'))]
|
|
for status in self.workflow.get_waitpoint_status():
|
|
if status.id == self.status.id:
|
|
continue
|
|
options.append(('reassign-%s' % status.id,
|
|
_('Change these forms status to "%s"') % status.name))
|
|
|
|
form = Form(enctype='multipart/form-data')
|
|
form.add(SingleSelectWidget, 'action', title=_('Pick an Action'),
|
|
options=options)
|
|
form.add_submit('submit', _('Submit'))
|
|
form.add_submit('cancel', _('Cancel'))
|
|
|
|
if form.get_submit() == 'cancel':
|
|
return redirect('.')
|
|
|
|
if form.is_submitted() and not form.get_widget('action').parse():
|
|
return redirect('.')
|
|
|
|
if not form.is_submitted() or form.has_errors():
|
|
get_response().breadcrumb.append(('reassign', _('Delete / Reassign')))
|
|
html_top('workflows', title = _('Delete Status'))
|
|
html_top('workflows', title = _('Delete Status / Reassign'))
|
|
'<h2>%s %s</h2>' % (_('Deleting Status:'), self.status.name)
|
|
'<p>'
|
|
_('''There are forms set to this status, they need to be changed before
|
|
this status can be deleted.''')
|
|
'</p>'
|
|
|
|
'<ul>'
|
|
for formdef in FormDef.select():
|
|
if formdef.workflow_id != self.workflow.id:
|
|
continue
|
|
items = formdef.data_class().get_with_indexed_value(
|
|
str('status'), 'wf-%s' % self.status.id)
|
|
'<li>%s: %s</li>' % (formdef.name, _('%s items') % len(items))
|
|
'</ul>'
|
|
|
|
form.render()
|
|
else:
|
|
self.submit_reassign(form)
|
|
del self.workflow.possible_status[ self.workflow.possible_status.index(self.status) ]
|
|
self.workflow.store()
|
|
return redirect('../..')
|
|
|
|
def submit_reassign(self, form):
|
|
nb_forms = 0
|
|
action = form.get_widget('action').parse()
|
|
if action.startswith(str('reassign-')):
|
|
new_status = 'wf-%s' % str(action)[9:]
|
|
for formdef in FormDef.select():
|
|
if formdef.workflow_id != self.workflow.id:
|
|
continue
|
|
for item in formdef.data_class().get_with_indexed_value(
|
|
str('status'), 'wf-%s' % self.status.id):
|
|
nb_forms += 1
|
|
if action == 'remove':
|
|
item.remove_self()
|
|
else:
|
|
item.status = new_status
|
|
evo = Evolution()
|
|
evo.time = time.localtime()
|
|
evo.status = new_status
|
|
evo.comment = _('Administrator reassigned status')
|
|
if not item.evolution:
|
|
item.evolution = []
|
|
item.evolution.append(evo)
|
|
item.store()
|
|
# delete all (old) status references in evolutions
|
|
for item in formdef.data_class().select():
|
|
if item.evolution:
|
|
modified = False
|
|
for evo in item.evolution:
|
|
if evo.status == self.status:
|
|
evo.status = None
|
|
modified = True
|
|
if modified:
|
|
item.store()
|
|
if action == 'remove':
|
|
get_logger().info('admin - delete status "%s" in workflow "%s": %d forms deleted' % (
|
|
self.status.name, self.workflow.name, nb_forms))
|
|
|
|
def visibility [html] (self):
|
|
form = Form(enctype = 'multipart/form-data')
|
|
form.add(CheckboxWidget, 'hide_status_from_user',
|
|
title=_('Hide status from user'),
|
|
value=(self.status.visibility == ['_receiver']))
|
|
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():
|
|
hide_status = form.get_widget('hide_status_from_user').parse()
|
|
if hide_status:
|
|
self.status.visibility = [str('_receiver')]
|
|
else:
|
|
self.status.visibility = None
|
|
self.workflow.store()
|
|
return redirect('.')
|
|
|
|
html_top('workflows', title = _('Edit Workflow Status Visibility'))
|
|
get_response().breadcrumb.append( ('visibility', _('Visibility')) )
|
|
'<h2>%s</h2>' % _('Edit Workflow Status Visibility')
|
|
form.render()
|
|
|
|
def endpoint [html] (self):
|
|
form = Form(enctype = 'multipart/form-data')
|
|
form.add(CheckboxWidget, 'force_terminal_status',
|
|
title=_('Force Terminal Status'),
|
|
value=(self.status.forced_endpoint == True))
|
|
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.status.forced_endpoint = form.get_widget('force_terminal_status').parse()
|
|
self.workflow.store()
|
|
return redirect('.')
|
|
|
|
html_top('workflows', title = _('Edit Terminal Status'))
|
|
get_response().breadcrumb.append( ('endpoint', _('Terminal Status')) )
|
|
form.render()
|
|
|
|
|
|
class WorkflowStatusDirectory(Directory):
|
|
_q_exports = ['']
|
|
|
|
def __init__(self, workflow):
|
|
self.workflow = workflow
|
|
|
|
def _q_lookup(self, component):
|
|
return WorkflowStatusPage(self.workflow, component)
|
|
|
|
def _q_index(self):
|
|
return redirect('..')
|
|
|
|
|
|
class WorkflowPage(Directory):
|
|
_q_exports = ['', 'edit', 'delete', 'newstatus', ('status', 'status_dir'), 'update_order',
|
|
'duplicate', 'export', 'svg']
|
|
|
|
def __init__(self, component):
|
|
try:
|
|
self.workflow = Workflow.get(component)
|
|
except KeyError:
|
|
raise errors.TraversalError()
|
|
self.workflow_ui = WorkflowUI(self.workflow)
|
|
self.status_dir = WorkflowStatusDirectory(self.workflow)
|
|
get_response().breadcrumb.append((component + '/', self.workflow.name))
|
|
|
|
def _q_index [html] (self):
|
|
html_top('workflows', title = _('Workflow - %s') % self.workflow.name)
|
|
get_response().add_javascript(['jquery.js', 'interface.js', 'biglist.js'])
|
|
'<h2>%s - ' % _('Workflow')
|
|
'%s</h2>'% self.workflow.name
|
|
get_session().display_message()
|
|
|
|
|
|
'<div class="bo-block">'
|
|
'<h3>%s</h3>' % _('Possible Status')
|
|
|
|
if not self.workflow.possible_status:
|
|
'<p>%s</p>' % _('There are not yet any status defined in this workflow.')
|
|
else:
|
|
if not str(self.workflow.id).startswith(str('_')):
|
|
'<p>'
|
|
_('Use drag and drop to reorder status.')
|
|
'</p>'
|
|
'<ul id="status-list" class="biglist sortable">'
|
|
else:
|
|
'<ul id="status-list" class="biglist">'
|
|
for status in self.workflow.possible_status:
|
|
'<li class="biglistitem" id="itemId_%s">' % status.id
|
|
'<a href="status/%s/">%s</a>' % (status.id, status.name)
|
|
'</li>'
|
|
'</ul>'
|
|
'</div>'
|
|
'<div class="bo-block">'
|
|
htmltext(graphviz(self.workflow, include=True))
|
|
'</div>' # bo-block
|
|
|
|
get_response().filter['sidebar'] = self.get_sidebar()
|
|
|
|
def get_sidebar [html] (self):
|
|
if str(self.workflow.id).startswith(str('_')):
|
|
'<p>'
|
|
_('''This is the default workflow, you cannot edit it but you can
|
|
duplicate it to base your own workflow on it.''')
|
|
'</p>'
|
|
|
|
'<ul>'
|
|
if not str(self.workflow.id).startswith(str('_')):
|
|
'<li><a href="edit">%s</a></li>' % _('Change Title')
|
|
'<li><a href="delete" rel="popup">%s</a></li>' % _('Delete')
|
|
if ET:
|
|
'<li><a href="duplicate">%s</a></li>' % _('Duplicate')
|
|
'<li><a href="export">%s</a></li>' % _('Export')
|
|
'</ul>'
|
|
if not str(self.workflow.id).startswith(str('_')):
|
|
self.get_new_status_form()
|
|
|
|
def svg(self):
|
|
response = get_response()
|
|
response.set_content_type('image/svg+xml')
|
|
return graphviz(self.workflow, include=False)
|
|
|
|
def export(self):
|
|
x = self.workflow.export_to_xml()
|
|
indent(x)
|
|
response = get_response()
|
|
response.set_content_type('application/x-wcs-form')
|
|
response.set_header('content-disposition',
|
|
'attachment; filename=%s.wcs' % misc.simplify(self.workflow.name))
|
|
return '<?xml version="1.0" encoding="utf-8"?>\n' + ET.tostring(x)
|
|
|
|
def get_new_status_form [html] (self):
|
|
'<div id="new-field">'
|
|
'<h3>%s</h3>' % _('New Status')
|
|
form = Form(enctype='multipart/form-data', action = 'newstatus')
|
|
form.add(StringWidget, 'name', title = _('Name'), required = True, size = 50)
|
|
form.add_submit('submit', _('Add'))
|
|
form.render()
|
|
'</div>'
|
|
|
|
def update_order(self):
|
|
request = get_request()
|
|
new_order = request.form['order'].strip(';').split(';')
|
|
self.workflow.possible_status = [ [x for x in self.workflow.possible_status if \
|
|
x.id == y][0] for y in new_order]
|
|
self.workflow.store()
|
|
return 'ok'
|
|
|
|
|
|
def newstatus(self):
|
|
form = Form(enctype='multipart/form-data', action = 'newstatus')
|
|
form.add(StringWidget, 'name', title = _('Name'), size = 50)
|
|
|
|
if not form.is_submitted() or form.has_errors():
|
|
get_session().message = ('error', _('Submitted form was not filled properly.'))
|
|
return redirect('.')
|
|
|
|
if form.get_widget('name').parse():
|
|
try:
|
|
self.workflow.add_status(form.get_widget('name').parse())
|
|
except DuplicateStatusNameError:
|
|
get_session().message = ('error', _('There is already a status with that name.'))
|
|
return redirect('.')
|
|
else:
|
|
get_session().message = ('error', _('Submitted form was not filled properly.'))
|
|
return redirect('.')
|
|
|
|
self.workflow.store()
|
|
|
|
return redirect('.')
|
|
|
|
|
|
|
|
def edit [html] (self, duplicate = False):
|
|
form = self.workflow_ui.form_edit()
|
|
if form.get_widget('cancel').parse():
|
|
return redirect('..')
|
|
|
|
if form.is_submitted() and not form.has_errors():
|
|
try:
|
|
self.workflow_ui.submit_form(form)
|
|
except ValueError:
|
|
pass
|
|
else:
|
|
return redirect('..')
|
|
|
|
html_top('workflows', title = _('Edit Workflow'))
|
|
if duplicate:
|
|
get_response().breadcrumb.append( ('edit', _('Duplicate')) )
|
|
'<h2>%s</h2>' % _('Duplicate Workflow')
|
|
else:
|
|
get_response().breadcrumb.append( ('edit', _('Edit')) )
|
|
'<h2>%s</h2>' % _('Edit Workflow')
|
|
form.render()
|
|
|
|
def delete [html] (self):
|
|
form = Form(enctype="multipart/form-data")
|
|
for formdef in FormDef.select():
|
|
if formdef.workflow_id == self.workflow.id:
|
|
form.widgets.append(HtmlWidget('<p>%s</p>' % _(
|
|
"This workflow is currently in use, you cannot remove it.")))
|
|
form.add_submit("cancel", _("Cancel"))
|
|
break
|
|
else:
|
|
form.widgets.append(HtmlWidget('<p>%s</p>' % _(
|
|
"You are about to irrevocably delete this workflow.")))
|
|
form.add_submit("submit", _("Submit"))
|
|
form.add_submit("cancel", _("Cancel"))
|
|
if form.get_widget('cancel').parse():
|
|
return redirect('..')
|
|
if not form.is_submitted() or form.has_errors():
|
|
get_response().breadcrumb.append(('delete', _('Delete')))
|
|
html_top('workflows', title = _('Delete Workflow'))
|
|
'<h2>%s %s</h2>' % (_('Deleting Workflow:'), self.workflow.name)
|
|
form.render()
|
|
else:
|
|
self.workflow.remove_self()
|
|
return redirect('..')
|
|
|
|
def duplicate [html] (self):
|
|
self.workflow_ui.workflow.id = None
|
|
original_name = self.workflow_ui.workflow.name
|
|
self.workflow_ui.workflow.name = self.workflow_ui.workflow.name + _(' (copy)')
|
|
workflow_names = [x.name for x in Workflow.select()]
|
|
no = 2
|
|
while self.workflow_ui.workflow.name in workflow_names:
|
|
self.workflow_ui.workflow.name = _('%(name)s (copy %(no)d)') % {
|
|
'name': original_name, 'no': no}
|
|
no += 1
|
|
self.workflow_ui.workflow.store()
|
|
return redirect('../%s/' % self.workflow_ui.workflow.id)
|
|
|
|
|
|
class WorkflowsDirectory(Directory):
|
|
_q_exports = ['', 'new', ('import', 'p_import')]
|
|
|
|
def _q_index [html] (self):
|
|
get_response().breadcrumb.append( ('workflows/', _('Workflows')) )
|
|
html_top('workflows', title = _('Workflows'))
|
|
|
|
'<ul id="main-actions">'
|
|
' <li><a class="new-item" href="new">%s</a></li>' % _('New Workflow')
|
|
' <li><a href="import" rel="popup">%s</a></li>' % _('Import')
|
|
'</ul>'
|
|
|
|
'<ul class="biglist">'
|
|
for workflow in [Workflow.get_default_workflow()] + Workflow.select(order_by = 'name'):
|
|
'<li>'
|
|
'<strong class="label"><a href="%s/">%s</a></strong>' % (workflow.id, workflow.name)
|
|
'<p class="details">'
|
|
'</p></li>'
|
|
'</ul>'
|
|
|
|
def new [html] (self):
|
|
get_response().breadcrumb.append( ('workflows/', _('Workflows')) )
|
|
get_response().breadcrumb.append( ('new', _('New')) )
|
|
workflow_ui = WorkflowUI(None)
|
|
|
|
form = workflow_ui.form_new()
|
|
if form.get_widget('cancel').parse():
|
|
return redirect('.')
|
|
|
|
if form.is_submitted() and not form.has_errors():
|
|
try:
|
|
workflow = workflow_ui.submit_form(form)
|
|
except ValueError:
|
|
pass
|
|
else:
|
|
return redirect('%s/' % workflow.id)
|
|
|
|
html_top('workflows', title = _('New Workflow'))
|
|
'<h2>%s</h2>' % _('New Workflow')
|
|
form.render()
|
|
|
|
def _q_lookup(self, component):
|
|
get_response().breadcrumb.append( ('workflows/', _('Workflows')) )
|
|
return WorkflowPage(component)
|
|
|
|
def p_import [html] (self):
|
|
form = Form(enctype = 'multipart/form-data')
|
|
|
|
form.add(FileWidget, 'file', title = _('File'), required = True)
|
|
form.add_submit('submit', _('Import Workflow'))
|
|
form.add_submit('cancel', _('Cancel'))
|
|
|
|
if form.get_submit() == 'cancel':
|
|
return redirect('.')
|
|
|
|
if form.is_submitted() and not form.has_errors():
|
|
try:
|
|
return self.import_submit(form)
|
|
except ValueError:
|
|
form.get_widget('file').set_error(_('Invalid File'))
|
|
|
|
get_response().breadcrumb.append( ('import', _('Import')) )
|
|
html_top('forms', title = _('Import Workflow'))
|
|
'<h2>%s</h2>' % _('Import Workflow')
|
|
form.render()
|
|
|
|
def import_submit(self, form):
|
|
workflow = Workflow.import_from_xml(form.get_widget('file').parse().fp)
|
|
initial_workflow_name = workflow.name
|
|
workflow_names = [x.name for x in Workflow.select()]
|
|
copy_no = 1
|
|
while workflow.name in workflow_names:
|
|
if copy_no == 1:
|
|
workflow.name = _('Copy of %s') % initial_workflow_name
|
|
else:
|
|
workflow.name = _('Copy of %(name)s (%(no)d)') % {
|
|
'name': initial_workflow_name, 'no': copy_no}
|
|
copy_no += 1
|
|
workflow.store()
|
|
return redirect('%s/' % workflow.id)
|
|
|