[gen] Allow to group transitions.

This commit is contained in:
Gaetan Delannay 2013-09-24 12:26:31 +02:00
parent 180b3473e8
commit 91e0bd2240
8 changed files with 166 additions and 95 deletions

View file

@ -130,15 +130,14 @@ class Group:
if self.group: return self.group.getMasterData() if self.group: return self.group.getMasterData()
def generateLabels(self, messages, classDescr, walkedGroups, def generateLabels(self, messages, classDescr, walkedGroups,
forSearch=False): content='fields'):
'''This method allows to generate all the needed i18n labels related to '''This method allows to generate all the needed i18n labels related to
this group. p_messages is the list of i18n p_messages (a PoMessages this group. p_messages is the list of i18n p_messages (a PoMessages
instance) that we are currently building; p_classDescr is the instance) that we are currently building; p_classDescr is the
descriptor of the class where this group is defined. If p_forSearch descriptor of the class where this group is defined. The type of
is True, this group is used for grouping searches, and not fields.''' content in this group is specified by p_content.'''
# A part of the group label depends on p_forSearch. # A part of the group label depends on p_content.
if forSearch: gp = 'searchgroup' gp = (content == 'searches') and 'searchgroup' or 'group'
else: gp = 'group'
if self.hasLabel: if self.hasLabel:
msgId = '%s_%s_%s' % (classDescr.name, gp, self.name) msgId = '%s_%s_%s' % (classDescr.name, gp, self.name)
messages.append(msgId, self.name) messages.append(msgId, self.name)
@ -157,25 +156,25 @@ class Group:
not self.group.label: not self.group.label:
# We remember walked groups for avoiding infinite recursion. # We remember walked groups for avoiding infinite recursion.
self.group.generateLabels(messages, classDescr, walkedGroups, self.group.generateLabels(messages, classDescr, walkedGroups,
forSearch=forSearch) content=content)
def insertInto(self, fields, uiGroups, page, metaType, forSearch=False): def insertInto(self, elems, uiGroups, page, className, content='fields'):
'''Inserts the UiGroup instance corresponding to this Group instance '''Inserts the UiGroup instance corresponding to this Group instance
into p_fields, the recursive structure used for displaying all into p_elems, the recursive structure used for displaying all
fields in a given p_page (or all searches), and returns this elements in a given p_page (fields, searches, transitions...) and
UiGroup instance.''' returns this UiGroup instance.'''
# First, create the corresponding UiGroup if not already in p_uiGroups. # First, create the corresponding UiGroup if not already in p_uiGroups.
if self.name not in uiGroups: if self.name not in uiGroups:
uiGroup = uiGroups[self.name] = UiGroup(self, page, metaType, uiGroup = uiGroups[self.name] = UiGroup(self, page, className,
forSearch=forSearch) content=content)
# Insert the group at the higher level (ie, directly in p_fields) # Insert the group at the higher level (ie, directly in p_elems)
# if the group is not itself in a group. # if the group is not itself in a group.
if not self.group: if not self.group:
fields.append(uiGroup) elems.append(uiGroup)
else: else:
outerGroup = self.group.insertInto(fields, uiGroups, page, outerGroup = self.group.insertInto(elems, uiGroups, page,
metaType,forSearch=forSearch) className, content=content)
outerGroup.addField(uiGroup) outerGroup.addElement(uiGroup)
else: else:
uiGroup = uiGroups[self.name] uiGroup = uiGroups[self.name]
return uiGroup return uiGroup
@ -187,14 +186,15 @@ class Column:
self.align = align self.align = align
class UiGroup: class UiGroup:
'''On-the-fly-generated data structure that groups all fields sharing the '''On-the-fly-generated data structure that groups all elements
same appy.fields.Group instance, that some logged user can see.''' (fields, searches, transitions...) sharing the same Group instance, that
the currently logged user can see.'''
# PX that renders a help icon for a group. # PX that renders a help icon for a group.
pxHelp = Px('''<acronym title="obj.translate('help', field=field)"><img pxHelp = Px('''<acronym title="obj.translate('help', field=field)"><img
src=":url('help')"/></acronym>''') src=":url('help')"/></acronym>''')
# PX that renders the content of a group. # PX that renders the content of a group (which is refered as var "field").
pxContent = Px(''' pxContent = Px('''
<table var="cellgap=field.cellgap" width=":field.wide" <table var="cellgap=field.cellgap" width=":field.wide"
align=":ztool.flipLanguageDirection(field.align, dir)" align=":ztool.flipLanguageDirection(field.align, dir)"
@ -219,7 +219,7 @@ class UiGroup:
_('%s_col%d' % (field.labelId, (colNb+1))) or ''</th> _('%s_col%d' % (field.labelId, (colNb+1))) or ''</th>
</tr> </tr>
<!-- The rows of widgets --> <!-- The rows of widgets -->
<tr valign=":field.valign" for="row in field.fields"> <tr valign=":field.valign" for="row in field.elements">
<td for="field in row" <td for="field in row"
colspan="field.colspan" colspan="field.colspan"
style=":not loop.field.last and ('padding-right:%s'% cellgap) or ''"> style=":not loop.field.last and ('padding-right:%s'% cellgap) or ''">
@ -231,7 +231,7 @@ class UiGroup:
</tr> </tr>
</table>''') </table>''')
# PX that renders a group of fields. # PX that renders a group of fields (the group is refered as var "field").
pxView = Px(''' pxView = Px('''
<x var="tagCss=field.master and ('slave_%s_%s' % \ <x var="tagCss=field.master and ('slave_%s_%s' % \
(field.masterName, '_'.join(field.masterValue))) or ''; (field.masterName, '_'.join(field.masterValue))) or '';
@ -253,14 +253,14 @@ class UiGroup:
<x if="field.style not in ('fieldset', 'tabs')">:field.pxContent</x> <x if="field.style not in ('fieldset', 'tabs')">:field.pxContent</x>
<!-- Render the group as tabs if required --> <!-- Render the group as tabs if required -->
<x if="field.style == 'tabs'" var2="lenFields=len(field.fields)"> <x if="field.style == 'tabs'" var2="lenFields=len(field.elements)">
<table width=":field.wide" class=":groupCss" id=":tagId" name=":tagName"> <table width=":field.wide" class=":groupCss" id=":tagId" name=":tagName">
<!-- First row: the tabs. --> <!-- First row: the tabs. -->
<tr valign="middle"><td style="border-bottom: 1px solid #ff8040"> <tr valign="middle"><td style="border-bottom: 1px solid #ff8040">
<table style="position:relative; bottom:-2px" <table style="position:relative; bottom:-2px"
cellpadding="0" cellspacing="0"> cellpadding="0" cellspacing="0">
<tr valign="bottom"> <tr valign="bottom">
<x for="row in field.fields" <x for="row in field.elements"
var2="rowNb=loop.row.nb; var2="rowNb=loop.row.nb;
tabId='tab_%s_%d_%d' % (field.name, rowNb, lenFields)"> tabId='tab_%s_%d_%d' % (field.name, rowNb, lenFields)">
<td><img src=":url('tabLeft')" id=":'%s_left' % tabId"/></td> <td><img src=":url('tabLeft')" id=":'%s_left' % tabId"/></td>
@ -276,7 +276,7 @@ class UiGroup:
</td></tr> </td></tr>
<!-- Other rows: the fields --> <!-- Other rows: the fields -->
<tr for="row in field.fields" <tr for="row in field.elements"
id=":'tabcontent_%s_%d_%d' % (field.name, loop.row.nb, lenFields)" id=":'tabcontent_%s_%d_%d' % (field.name, loop.row.nb, lenFields)"
style=":loop.row.nb==0 and 'display:table-row' or 'display:none')"> style=":loop.row.nb==0 and 'display:table-row' or 'display:none')">
<td var="field=row[0]"> <td var="field=row[0]">
@ -318,53 +318,77 @@ class UiGroup:
</div> </div>
</x>''') </x>''')
def __init__(self, group, page, metaType, forSearch=False): # PX that renders a group of transitions.
pxViewTransitions = Px('''
<!-- Render a group of transitions, as a one-column table -->
<table>
<x for="row in uiGroup.elements">
<x for="transition in row"><tr><td>:transition.pxView</td></tr></x>
</x>
</table>''')
# What PX to use, depending on group content?
pxByContent = {'fields': pxView, 'searches': pxViewSearches,
'transitions': pxViewTransitions}
def __init__(self, group, page, className, content='fields'):
'''A UiGroup can group various kinds of elements: fields, searches,
transitions..., The type of content that one may find in this group
is given in p_content.
* p_group is the Group instance corresponding to this UiGroup;
* p_page is the Page instance where the group is rendered (for
transitions, it corresponds to a virtual page
"workflow");
* p_className is the name of the class that holds the elements to
group.'''
self.type = 'group' self.type = 'group'
# All p_group attributes become self attributes. # All p_group attributes become self attributes. This is required
# because a UiGroup, in some PXs, must behave like a Field (ie, have
# the same attributes, like "master".
for name, value in group.__dict__.iteritems(): for name, value in group.__dict__.iteritems():
if not name.startswith('_'): if not name.startswith('_'):
setattr(self, name, value) setattr(self, name, value)
self.group = group
self.columnsWidths = [col.width for col in group.columns] self.columnsWidths = [col.width for col in group.columns]
self.columnsAligns = [col.align for col in group.columns] self.columnsAligns = [col.align for col in group.columns]
# Names of i18n labels # Names of i18n labels for this group.
labelName = self.name labelName = self.name
prefix = metaType prefix = className
if group.label: if group.label:
if isinstance(group.label, basestring): prefix = group.label if isinstance(group.label, basestring): prefix = group.label
else: # It is a tuple (metaType, name) else: # It is a tuple (className, name)
if group.label[1]: labelName = group.label[1] if group.label[1]: labelName = group.label[1]
if group.label[0]: prefix = group.label[0] if group.label[0]: prefix = group.label[0]
if forSearch: gp = 'searchgroup' gp = (content == 'searches') and 'searchgroup' or 'group'
else: gp = 'group'
self.labelId = '%s_%s_%s' % (prefix, gp, labelName) self.labelId = '%s_%s_%s' % (prefix, gp, labelName)
self.descrId = self.labelId + '_descr' self.descrId = self.labelId + '_descr'
self.helpId = self.labelId + '_help' self.helpId = self.labelId + '_help'
# The name of the page where the group lies # The name of the page where the group lies
self.page = page.name self.page = page.name
# The fields belonging to the group that the current user may see. # The elements contained in the group, that the current user may see.
# They will be stored by m_addField below as a list of lists because # They will be stored by m_addElement below as a list of lists because
# they will be rendered as a table. # they will be rendered as a table.
self.fields = [[]] self.elements = [[]]
# PX to user for rendering this group. # PX to use for rendering this group.
self.px = forSearch and self.pxViewSearches or self.pxView self.px = self.pxByContent[content]
def addField(self, field): def addElement(self, element):
'''Adds p_field into self.fields. We try first to add p_field into the '''Adds p_element into self.elements. We try first to add p_element into
last row. If it is not possible, we create a new row.''' the last row. If it is not possible, we create a new row.'''
# Get the last row # Get the last row
lastRow = self.fields[-1] lastRow = self.elements[-1]
numberOfColumns = len(self.columnsWidths) numberOfColumns = len(self.columnsWidths)
# Compute the number of columns already filled in the last row. # Compute the number of columns already filled in the last row.
filledColumns = 0 filledColumns = 0
for rowField in lastRow: filledColumns += rowField.colspan for rowElem in lastRow: filledColumns += rowElem.colspan
freeColumns = numberOfColumns - filledColumns freeColumns = numberOfColumns - filledColumns
if freeColumns >= field.colspan: if freeColumns >= element.colspan:
# We can add the widget in the last row. # We can add the element in the last row.
lastRow.append(field) lastRow.append(element)
else: else:
if freeColumns: if freeColumns:
# Terminate the current row by appending empty cells # Terminate the current row by appending empty cells
for i in range(freeColumns): lastRow.append('') for i in range(freeColumns): lastRow.append('')
# Create a new row # Create a new row
self.fields.append([field]) self.elements.append([element])
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -15,6 +15,8 @@
# Appy. If not, see <http://www.gnu.org/licenses/>. # Appy. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import types, string import types, string
from group import Group
from appy.px import Px
from appy.gen.mail import sendNotification from appy.gen.mail import sendNotification
# Default Appy permissions ----------------------------------------------------- # Default Appy permissions -----------------------------------------------------
@ -97,7 +99,7 @@ class State:
class Transition: class Transition:
'''Represents a workflow transition.''' '''Represents a workflow transition.'''
def __init__(self, states, condition=True, action=None, notify=None, def __init__(self, states, condition=True, action=None, notify=None,
show=True, confirm=False): show=True, confirm=False, group=None):
self.states = states # In its simpler form, it is a tuple with 2 self.states = states # In its simpler form, it is a tuple with 2
# states: (fromState, toState). But it can also be a tuple of several # states: (fromState, toState). But it can also be a tuple of several
# (fromState, toState) sub-tuples. This way, you may define only 1 # (fromState, toState) sub-tuples. This way, you may define only 1
@ -113,6 +115,7 @@ class Transition:
self.show = show # If False, the end user will not be able to trigger self.show = show # If False, the end user will not be able to trigger
# the transition. It will only be possible by code. # the transition. It will only be possible by code.
self.confirm = confirm # If True, a confirm popup will show up. self.confirm = confirm # If True, a confirm popup will show up.
self.group = Group.get(group)
def getName(self, wf): def getName(self, wf):
'''Returns the name for this state in workflow p_wf.''' '''Returns the name for this state in workflow p_wf.'''
@ -266,6 +269,44 @@ class Transition:
if not msg: msg = obj.translate('object_saved') if not msg: msg = obj.translate('object_saved')
obj.say(msg) obj.say(msg)
class UiTransition:
'''Represents a widget that displays a transition.'''
pxView = Px('''<x>
<!-- Real button -->
<input if="transition.mayTrigger"
type="button" class="button" title=":transition.title"
style=":url('buttonTransition', bg=True)"
value=":ztool.truncateValue(transition.title)"
onclick=":'triggerTransition(%s,%s,%s)' % (q(formId), \
q(transition.name), q(transition.confirm))"/>
<!-- Fake button, explaining why the transition can't be triggered -->
<input type="button" class="button" if="not transition.mayTrigger"
style=":url('buttonFake', bg=True) + ';cursor: help'"
value=":ztool.truncateValue(transition.title)"
title=":'%s: %s' % (transition.title, transition.reason)"/>
</x>''')
def __init__(self, name, transition, obj, mayTrigger, ):
self.name = name
self.transition = transition
self.type = 'transition'
label = obj.getWorkflowLabel(name)
self.title = obj.translate(label)
if transition.confirm:
self.confirm = obj.translate('%s_confirm' % label)
else:
self.confirm = ''
# May this transition be triggered via the UI?
self.mayTrigger = True
self.reason = ''
if not mayTrigger:
self.mayTrigger = False
self.reason = mayTrigger.msg
# Require by the UiGroup.
self.colspan = 1
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class Permission: class Permission:
'''If you need to define a specific read or write permission for some field '''If you need to define a specific read or write permission for some field

View file

@ -761,7 +761,7 @@ class ZopeGenerator(Generator):
# Generate labels for groups of searches # Generate labels for groups of searches
if search.group and not search.group.label: if search.group and not search.group.label:
search.group.generateLabels(self.labels, classDescr, set(), search.group.generateLabels(self.labels, classDescr, set(),
forSearch=True) content='searches')
# Generate the resulting Zope class. # Generate the resulting Zope class.
self.copyFile('Class.pyt', repls, destName=fileName) self.copyFile('Class.pyt', repls, destName=fileName)

View file

@ -261,8 +261,11 @@ class ZopeInstaller:
indexed=True, searchable=True) indexed=True, searchable=True)
title.init('title', None, 'appy') title.init('title', None, 'appy')
setattr(wrapperClass, 'title', title) setattr(wrapperClass, 'title', title)
# Special field "state" must be added for every class # Special field "state" must be added for every class. It must be a
state = gen.String(show='result') # "select" field, because it will be necessary for displaying the
# translated state name.
state = gen.String(validator=gen.Selection('listStates'),
show='result')
state.init('state', None, 'workflow') state.init('state', None, 'workflow')
setattr(wrapperClass, 'state', state) setattr(wrapperClass, 'state', state)
names = self.config.attributes[wrapperClass.__name__[:-8]] names = self.config.attributes[wrapperClass.__name__[:-8]]

View file

@ -738,8 +738,9 @@ class ToolMixin(BaseMixin):
res = [] res = []
default = None # Also retrieve the default one here. default = None # Also retrieve the default one here.
groups = {} # The already encountered groups groups = {} # The already encountered groups
page = Page('main') # A dummy page required by class UiGroup page = Page('searches') # A dummy page required by class UiGroup
# Get the searches statically defined on the class # Get the searches statically defined on the class
className = self.getPortalType(klass)
searches = ClassDescriptor.getSearches(klass, tool=self.appy()) searches = ClassDescriptor.getSearches(klass, tool=self.appy())
# Get the dynamically computed searches # Get the dynamically computed searches
if hasattr(klass, 'getDynamicSearches'): if hasattr(klass, 'getDynamicSearches'):
@ -752,8 +753,8 @@ class ToolMixin(BaseMixin):
res.append(uiSearch) res.append(uiSearch)
else: else:
uiGroup = search.group.insertInto(res, groups, page, className, uiGroup = search.group.insertInto(res, groups, page, className,
forSearch=True) content='searches')
uiGroup.addField(uiSearch) uiGroup.addElement(uiSearch)
# Is this search the default search? # Is this search the default search?
if search.default: default = uiSearch if search.default: default = uiSearch
return Object(searches=res, default=default) return Object(searches=res, default=default)

View file

@ -5,6 +5,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import os, os.path, sys, types, urllib, cgi import os, os.path, sys, types, urllib, cgi
from appy import Object from appy import Object
from appy.fields.workflow import UiTransition
import appy.gen as gen import appy.gen as gen
from appy.gen.utils import * from appy.gen.utils import *
from appy.gen.layout import Table, defaultPageLayouts from appy.gen.layout import Table, defaultPageLayouts
@ -742,11 +743,10 @@ class BaseMixin:
if not field.group: if not field.group:
res.append(field) res.append(field)
else: else:
# Insert the UiGroup instance corresponding to field.group at # Insert the UiGroup instance corresponding to field.group.
# the right place.
uiGroup = field.group.insertInto(res, groups, field.page, uiGroup = field.group.insertInto(res, groups, field.page,
self.meta_type) self.meta_type)
uiGroup.addField(field) uiGroup.addElement(field)
if collectCssJs: if collectCssJs:
cssJs['css'] = css or () cssJs['css'] = css or ()
cssJs['js'] = js or () cssJs['js'] = js or ()
@ -786,9 +786,10 @@ class BaseMixin:
return klass.styles[elem] return klass.styles[elem]
return elem return elem
def getTransitions(self, includeFake=True, includeNotShowable=False): def getTransitions(self, includeFake=True, includeNotShowable=False,
'''This method returns info about transitions that one can trigger from grouped=True):
the user interface. '''This method returns info about transitions (as UiTransition
instances) that one can trigger from the user interface.
* if p_includeFake is True, it retrieves transitions that the user * if p_includeFake is True, it retrieves transitions that the user
can't trigger, but for which he needs to know for what reason he can't trigger, but for which he needs to know for what reason he
can't trigger it; can't trigger it;
@ -797,8 +798,12 @@ class BaseMixin:
and not a security concern, in some cases it has sense to set and not a security concern, in some cases it has sense to set
includeNotShowable=True, because those transitions are triggerable includeNotShowable=True, because those transitions are triggerable
from a security point of view. from a security point of view.
* If p_grouped is True, transitions are grouped according to their
"group" attribute, in a similar way to fields or searches.
''' '''
res = [] res = []
groups = {} # The already encountered groups of transitions.
wfPage = gen.Page('workflow')
wf = self.getWorkflow() wf = self.getWorkflow()
currentState = self.State(name=False) currentState = self.State(name=False)
# Loop on every transition # Loop on every transition
@ -818,17 +823,16 @@ class BaseMixin:
if not includeNotShowable: if not includeNotShowable:
includeIt = includeIt and transition.isShowable(wf, self) includeIt = includeIt and transition.isShowable(wf, self)
if not includeIt: continue if not includeIt: continue
# Add transition-info to the result. # Create the UiTransition instance.
label = self.getWorkflowLabel(name) info = UiTransition(name, transition, self, mayTrigger)
tInfo = {'name': name, 'title': self.translate(label), # Add the transition into the result.
'confirm': '', 'may_trigger': True} if not transition.group or not grouped:
if transition.confirm: res.append(info)
cLabel = '%s_confirm' % label else:
tInfo['confirm'] = self.translate(cLabel) # Insert the UiGroup instance corresponding to transition.group.
if not mayTrigger: uiGroup = transition.group.insertInto(res, groups, wfPage,
tInfo['may_trigger'] = False self.__class__.__name__, content='transitions')
tInfo['reason'] = mayTrigger.msg uiGroup.addElement(info)
res.append(tInfo)
return res return res
def getAppyPhases(self, currentOnly=False, layoutType='view'): def getAppyPhases(self, currentOnly=False, layoutType='view'):

View file

@ -326,10 +326,7 @@ class ToolWrapper(AbstractWrapper):
<!-- Actions --> <!-- Actions -->
<table class="noStyle" if="zobj.mayAct()"> <table class="noStyle" if="zobj.mayAct()">
<tr> <tr valign="top">
<!-- Workflow transitions -->
<td if="zobj.showTransitions('result')"
var2="targetObj=zobj">:targetObj.appy().pxTransitions</td>
<!-- Edit --> <!-- Edit -->
<td if="zobj.mayEdit()"> <td if="zobj.mayEdit()">
<a var="navInfo='search.%s.%s.%d.%d' % \ <a var="navInfo='search.%s.%s.%d.%d' % \
@ -344,6 +341,9 @@ class ToolWrapper(AbstractWrapper):
title=":_('object_delete')" title=":_('object_delete')"
onClick=":'onDeleteObject(%s)' % q(zobj.UID())"/> onClick=":'onDeleteObject(%s)' % q(zobj.UID())"/>
</td> </td>
<!-- Workflow transitions -->
<td if="zobj.showTransitions('result')"
var2="targetObj=zobj">:targetObj.appy().pxTransitions</td>
</tr> </tr>
</table> </table>
</x> </x>

View file

@ -349,34 +349,22 @@ class AbstractWrapper(object):
</table> </table>
</x>''') </x>''')
# Displays an object's transitions(s).
pxTransitions = Px(''' pxTransitions = Px('''
<form var="transitions=targetObj.getTransitions()" if="transitions" <form var="transitions=targetObj.getTransitions()" if="transitions"
var2="formId='trigger_%s' % targetObj.UID()" method="post" var2="formId='trigger_%s' % targetObj.UID()" method="post"
id=":formId" action=":targetObj.absolute_url() + '/do'"> id=":formId" action=":targetObj.absolute_url() + '/do'">
<input type="hidden" name="action" value="Trigger"/> <input type="hidden" name="action" value="Trigger"/>
<input type="hidden" name="workflow_action"/> <input type="hidden" name="workflow_action"/>
<table> <!-- Input field for storing the comment coming from the popup -->
<tr valign="middle">
<!-- Input field for storing comment -->
<textarea id="comment" name="comment" cols="30" rows="3" <textarea id="comment" name="comment" cols="30" rows="3"
style="display:none"></textarea> style="display:none"></textarea>
<!-- Buttons for triggering transitions --> <table>
<tr valign="middle">
<td align=":dright" for="transition in transitions"> <td align=":dright" for="transition in transitions">
<!-- Real button --> <!-- Render a transition or a group of transitions. -->
<input type="button" class="button" if="transition['may_trigger']" <x if="transition.type == 'transition'">:transition.pxView</x>
style=":url('buttonTransition', bg=True)" <x if="transition.type == 'group'"
title=":transition['title']" var2="uiGroup=transition">:uiGroup.px</x>
value=":ztool.truncateValue(transition['title'])"
onclick=":'triggerTransition(%s,%s,%s)' % (q(formId), \
q(transition['name']), q(transition['confirm']))"/>
<!-- Fake button, explaining why the transition can't be triggered -->
<input type="button" class="button" if="not transition['may_trigger']"
style=":url('buttonFake', bg=True) + ';cursor: help'"
value=":ztool.truncateValue(transition['title'])"
title=":'%s: %s' % (transition['title'], \
transition['reason'])"/>
</td> </td>
</tr> </tr>
</table> </table>
@ -446,7 +434,7 @@ class AbstractWrapper(object):
nextPage=phaseObj.getNextPage(page)[0]; nextPage=phaseObj.getNextPage(page)[0];
isEdit=layoutType == 'edit'; isEdit=layoutType == 'edit';
pageInfo=phaseObj.pagesInfo[page]"> pageInfo=phaseObj.pagesInfo[page]">
<tr> <tr valign="top">
<!-- Previous --> <!-- Previous -->
<td if="previousPage and pageInfo.showPrevious"> <td if="previousPage and pageInfo.showPrevious">
<!-- Button on the edit page --> <!-- Button on the edit page -->
@ -1045,4 +1033,14 @@ class AbstractWrapper(object):
'''Produces a representation of p_text into the desired p_format, which '''Produces a representation of p_text into the desired p_format, which
is 'html' by default.''' is 'html' by default.'''
return self.o.formatText(text, format) return self.o.formatText(text, format)
def listStates(self):
'''Lists the possible states for this object.'''
res = []
o = self.o
workflow = o.getWorkflow()
for name in dir(workflow):
if getattr(workflow, name).__class__.__name__ != 'State': continue
res.append((name, o.translate(o.getWorkflowLabel(name))))
return res
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------