[gen] Bugfix in the Ref field; added method workflow.Transition.getBack that finds the 'back' transition of a given transition.

This commit is contained in:
Gaetan Delannay 2014-05-02 12:35:09 +02:00
parent 14f85509e1
commit 1d0ee7a614
19 changed files with 206 additions and 128 deletions

View file

@ -219,8 +219,7 @@ class UiGroup:
</tr>
<!-- The rows of widgets -->
<tr valign=":field.valign" for="row in field.elements">
<td for="field in row"
colspan="field.colspan"
<td for="field in row" colspan=":field.colspan"
style=":not loop.field.last and ('padding-right:%s'% cellgap) or ''">
<x if="field">
<x if="field.type == 'group'">:field.pxView</x>

View file

@ -195,86 +195,85 @@ class Ref(Field):
# PX that displays referred objects as a list.
pxViewList = Px('''
<!-- No object at all -->
<div if="not objects" class="smaller">:_('no_ref')</div>
<div if="not innerRef or showPlusIcon" style="margin-bottom: 4px">
<span if="subLabel" class="discreet">:_(subLabel)</span>
(<span class="discreet">:totalNumber</span>)
<x>:field.pxAdd</x>
<!-- The search button if field is queryable -->
<input if="objects and field.queryable" type="button"
class="buttonSmall button"
var2="label=_('search_button')" value=":label"
style=":'%s; %s' % (url('search', bg=True), \
ztool.getButtonWidth(label))"
onclick=":'goto(%s)' % \
q('%s/search?className=%s&amp;ref=%s:%s' % \
(ztool.absolute_url(), tiedClassName, zobj.id, field.name))"/>
</div>
<x if="objects">
<div if="not innerRef or showPlusIcon" style="margin-bottom: 4px">
<span if="subLabel" class="discreet">:_(subLabel)</span>
(<span class="discreet">:totalNumber</span>)
<x>:field.pxAdd</x>
<!-- The search button if field is queryable -->
<input if="objects and field.queryable" type="button"
class="buttonSmall button"
var2="label=_('search_button')" value=":label"
style=":'%s; %s' % (url('search', bg=True), \
ztool.getButtonWidth(label))"
onclick=":'goto(%s)' % \
q('%s/search?className=%s&amp;ref=%s:%s' % \
(ztool.absolute_url(), tiedClassName, zobj.id, field.name))"/>
</div>
<!-- (Top) navigation -->
<x>:tool.pxNavigate</x>
<!-- (Top) navigation -->
<x>:tool.pxNavigate</x>
<!-- No object is present -->
<p class="discreet" if="not objects and not showPlusIcon">:_('no_ref')</p>
<!-- Linked objects -->
<table if="objects" class=":not innerRef and 'list' or ''"
width=":innerRef and '100%' or field.layouts['view'].width"
var2="columns=ztool.getColumnsSpecifiers(tiedClassName, \
field.shownInfo, dir)">
<tr if="field.showHeaders">
<th if="not inPickList and numbered" width=":numbered"></th>
<th for="column in columns" width=":column.width"
align=":column.align" var2="refField=column.field">
<span>:_(refField.labelId)</span>
<x>:field.pxSortIcons</x>
<x var="className=tiedClassName;
field=refField">:tool.pxShowDetails</x>
</th>
<th if="checkboxes" class="cbCell">
<img src=":url('checkall')" class="clickable"
title=":_('check_uncheck')"
onclick=":'toggleAllRefCbs(%s)' % q(ajaxHookId)"/>
</th>
</tr>
<!-- Loop on every (tied or selectable) object. -->
<tr for="tied in objects" valign="top"
class=":loop.tied.odd and 'even' or 'odd'"
var2="tiedUid=tied.o.id;
objectIndex=field.getIndexOf(zobj, tiedUid)|None">
<td if="not inPickList and numbered">:field.pxNumber</td>
<td for="column in columns" width=":column.width" align=":column.align"
var2="refField=column.field">
<!-- The "title" field -->
<x if="refField.name == 'title'">
<x>:field.pxObjectTitle</x>
<div if="tied.o.mayAct()">:field.pxObjectActions</div>
</x>
<!-- Any other field -->
<x if="refField.name != 'title'">
<x var="zobj=tied.o; obj=tied; layoutType='cell';
innerRef=True; field=refField"
if="field.isShowable(zobj, 'result')">:field.pxRender</x>
</x>
</td>
<td if="checkboxes" class="cbCell">
<input type="checkbox" name=":ajaxHookId" checked="checked"
value=":tiedUid" onclick="toggleRefCb(this)"/>
</td>
</tr>
</table>
<!-- Linked objects -->
<table if="objects" class=":not innerRef and 'list' or ''"
width=":innerRef and '100%' or field.layouts['view'].width"
var2="columns=ztool.getColumnsSpecifiers(tiedClassName, \
field.shownInfo, dir)">
<tr if="field.showHeaders">
<th if="not inPickList and numbered" width=":numbered"></th>
<th for="column in columns" width=":column.width"
align=":column.align" var2="refField=column.field">
<span>:_(refField.labelId)</span>
<x>:field.pxSortIcons</x>
<x var="className=tiedClassName;
field=refField">:tool.pxShowDetails</x>
</th>
<th if="checkboxes" class="cbCell">
<img src=":url('checkall')" class="clickable"
title=":_('check_uncheck')"
onclick=":'toggleAllRefCbs(%s)' % q(ajaxHookId)"/>
</th>
</tr>
<!-- Loop on every (tied or selectable) object. -->
<tr for="tied in objects" valign="top"
class=":loop.tied.odd and 'even' or 'odd'"
var2="tiedUid=tied.o.id;
objectIndex=field.getIndexOf(zobj, tiedUid)|None">
<td if="not inPickList and numbered">:field.pxNumber</td>
<td for="column in columns" width=":column.width" align=":column.align"
var2="refField=column.field">
<!-- The "title" field -->
<x if="refField.name == 'title'">
<x>:field.pxObjectTitle</x>
<div if="tied.o.mayAct()">:field.pxObjectActions</div>
</x>
<!-- Any other field -->
<x if="refField.name != 'title'">
<x var="zobj=tied.o; obj=tied; layoutType='cell';
innerRef=True; field=refField"
if="field.isShowable(zobj, 'result')">:field.pxRender</x>
</x>
</td>
<td if="checkboxes" class="cbCell">
<input type="checkbox" name=":ajaxHookId" checked="checked"
value=":tiedUid" onclick="toggleRefCb(this)"/>
</td>
</tr>
</table>
<!-- Global actions -->
<div if="canWrite and (totalNumber &gt; 1)"
align=":dright">:field.pxGlobalActions</div>
<!-- Global actions -->
<div if="canWrite and (totalNumber &gt; 1)"
align=":dright">:field.pxGlobalActions</div>
<!-- (Bottom) navigation -->
<x>:tool.pxNavigate</x>
<!-- (Bottom) navigation -->
<x>:tool.pxNavigate</x>
<!-- Init checkboxes if present. -->
<script if="checkboxes"
type="text/javascript">:'initRefCbs(%s)' % q(ajaxHookId)</script>
</x>''')
<!-- Init checkboxes if present. -->
<script if="checkboxes"
type="text/javascript">:'initRefCbs(%s)' % q(ajaxHookId)
</script>''')
# PX that displays the list of objects the user may select to insert into a
# ref field with link="list".
@ -410,7 +409,7 @@ class Ref(Field):
title=field.getReferenceLabel(tied, unlimited=True)"
selected=":inRequest and (uid in requestValue) or \
(uid in uids)" value=":uid"
title=":title">:ztool.truncateValue(title, field.swidth)</option>
title=":title">:ztool.truncateValue(title, field.width)</option>
</select>''')
pxSearch = Px('''

View file

@ -397,6 +397,30 @@ class Transition:
if not obj.isTemporary(): obj.reindex()
return tool.goto(obj.getUrl(rq['HTTP_REFERER']))
@staticmethod
def getBack(workflow, transition):
'''Returns the name of the transition (in p_workflow) that "cancels" the
triggering of p_transition and allows to go back to p_transition's
start state.'''
# Get the end state(s) of p_transition
transition = getattr(workflow, transition)
# Browse all transitions and find the one starting at p_transition's end
# state and coming back to p_transition's start state.
for trName, tr in workflow.__dict__.iteritems():
if not isinstance(tr, Transition) or (tr == transition): continue
if transition.isSingle():
if tr.hasState(transition.states[1], True) and \
tr.hasState(transition.states[0], False): return trName
else:
startOk = False
endOk = False
for start, end in transition.states:
if (not startOk) and tr.hasState(end, True):
startOk = True
if (not endOk) and tr.hasState(start, False):
endOk = True
if startOk and endOk: return trName
class UiTransition:
'''Represents a widget that displays a transition.'''
pxView = Px('''<x var="buttonCss = (buttonsMode == 'small') and \

View file

@ -18,8 +18,9 @@ try:
except ImportError:
_noroles = []
# Errors -----------------------------------------------------------------------
jsMessages = ('no_elem_selected', 'action_confirm', 'warn_leave_form')
# Global JS internationalized messages that will be computed in every page -----
jsMessages = ('no_elem_selected', 'action_confirm', 'save_confirm',
'warn_leave_form')
# ------------------------------------------------------------------------------
class ToolMixin(BaseMixin):
@ -389,25 +390,26 @@ class ToolMixin(BaseMixin):
k = self.getAppyClass(className)
return hasattr(k, 'listColumns') and k.listColumns or ('title',)
def truncateValue(self, value, width=15):
'''Truncates the p_value according to p_width.'''
def truncateValue(self, value, width=20):
'''Truncates the p_value according to p_width. p_value has to be
unicode-encoded for being truncated (else, one char may be spread on
2 chars).'''
# Param p_width can be None.
if not width: width = 20
if isinstance(value, str): value = value.decode('utf-8')
if len(value) > width:
return value[:width].encode('utf-8') + '...'
return value.encode('utf-8')
if len(value) > width: return value[:width] + '...'
return value
def truncateText(self, text, width=15):
def truncateText(self, text, width=20):
'''Truncates p_text to max p_width chars. If the text is longer than
p_width, the truncated part is put in a "acronym" html tag.'''
# p_text has to be unicode-encoded for being truncated (else, one char
# may be spread on 2 chars). But this method must return an encoded
# string, else, ZPT crashes. The same remark holds for m_truncateValue
# above.
uText = text # uText will store the unicode version
if isinstance(text, str): uText = text.decode('utf-8')
if len(uText) <= width: return text
return '<acronym title="%s">%s</acronym>' % \
(text, uText[:width].encode('utf-8') + '...')
p_width, the truncated part is put in a "acronym" html tag. p_text
has to be unicode-encoded for being truncated (else, one char may be
spread on 2 chars).'''
# Param p_width can be None.
if not width: width = 20
if isinstance(text, str): text = text.decode('utf-8')
if len(text) <= width: return text
return '<acronym title="%s">%s...</acronym>' % (text, text[:width])
def splitList(self, l, sub):
'''Returns a list made of the same elements as p_l, but grouped into
@ -1226,13 +1228,13 @@ class ToolMixin(BaseMixin):
return [f for f in self.getAllAppyTypes(contentType) \
if (f.type == 'Pod') and (f.show == 'result')]
def formatDate(self, aDate, withHour=True):
'''Returns aDate formatted as specified by tool.dateFormat.
def formatDate(self, date, withHour=True):
'''Returns p_date formatted as specified by tool.dateFormat.
If p_withHour is True, hour is appended, with a format specified
in tool.hourFormat.'''
tool = self.appy()
res = aDate.strftime(tool.dateFormat)
if withHour: res += ' (%s)' % aDate.strftime(tool.hourFormat)
res = date.strftime(tool.dateFormat)
if withHour: res += ' (%s)' % date.strftime(tool.hourFormat)
return res
def generateUid(self, className):

View file

@ -942,12 +942,11 @@ class BaseMixin:
if not name: return wf
return WorkflowDescriptor.getWorkflowName(wf)
def getWorkflowLabel(self, stateName=None):
'''Gets the i18n label for p_stateName, or for the current object state
if p_stateName is not given. Note that if p_stateName is given, it
can also represent the name of a transition.'''
stateName = stateName or self.State()
return '%s_%s' % (self.getWorkflow(name=True), stateName)
def getWorkflowLabel(self, name=None):
'''Gets the i18n label for p_name (which can denote a state or a
transition), or for the current object state if p_name is None.'''
name = name or self.State()
return '%s_%s' % (self.getWorkflow(name=True), name)
def getTransitions(self, includeFake=True, includeNotShowable=False,
grouped=True):

View file

@ -203,7 +203,7 @@ class Translation(ModelClass):
title = gen.String(show=False, indexed=True,
page=gen.Page('main',label='Main'))
def getPoFile(self): pass
po = gen.Action(action=getPoFile, result='filetmp')
po = gen.Action(action=getPoFile, result='file')
sourceLanguage = gen.String(width=4)
def label(self): pass
def show(self, name): pass

View file

@ -247,6 +247,10 @@ msgstr ""
msgid "action_null"
msgstr ""
#. Default: "Are you sure you want to apply this change?"
msgid "save_confirm"
msgstr ""
#. Default: "Go to top"
msgid "goto_first"
msgstr ""

View file

@ -247,6 +247,10 @@ msgstr ""
msgid "action_null"
msgstr ""
#. Default: "Are you sure you want to apply this change?"
msgid "save_confirm"
msgstr ""
#. Default: "Go to top"
msgid "goto_first"
msgstr ""

View file

@ -247,6 +247,10 @@ msgstr ""
msgid "action_null"
msgstr ""
#. Default: "Are you sure you want to apply this change?"
msgid "save_confirm"
msgstr ""
#. Default: "Go to top"
msgid "goto_first"
msgstr "Zurück zum Anfang"

View file

@ -248,6 +248,10 @@ msgstr "Action could not be performed on ${nb} element(s)."
msgid "action_null"
msgstr "Action had no effect."
#. Default: "Are you sure you want to apply this change?"
msgid "save_confirm"
msgstr "Are you sure you want to apply this change?"
#. Default: "Go to top"
msgid "goto_first"
msgstr "Go to top"

View file

@ -247,6 +247,10 @@ msgstr ""
msgid "action_null"
msgstr ""
#. Default: "Are you sure you want to apply this change?"
msgid "save_confirm"
msgstr ""
#. Default: "Go to top"
msgid "goto_first"
msgstr "Ir al inicio"

View file

@ -248,6 +248,10 @@ msgstr "L'action n'a pu être réalisée pour ${nb} élément(s)."
msgid "action_null"
msgstr "L'action n'a eu aucun effet."
#. Default: "Are you sure you want to apply this change?"
msgid "save_confirm"
msgstr "Êtes-vous sûr de vouloir appliquer ce changement?"
#. Default: "Go to top"
msgid "goto_first"
msgstr "Aller au début"

View file

@ -247,6 +247,10 @@ msgstr ""
msgid "action_null"
msgstr ""
#. Default: "Are you sure you want to apply this change?"
msgid "save_confirm"
msgstr ""
#. Default: "Go to top"
msgid "goto_first"
msgstr "Andare all'inizio"

View file

@ -247,6 +247,10 @@ msgstr ""
msgid "action_null"
msgstr ""
#. Default: "Are you sure you want to apply this change?"
msgid "save_confirm"
msgstr ""
#. Default: "Go to top"
msgid "goto_first"
msgstr "Ga naar het begin"

View file

@ -62,7 +62,7 @@ img { border: 0; vertical-align: middle }
.userStripText { padding: 0 0.3em 0 0.3em; color: white }
.userStrip a { color: #e7e7e7 }
.userStrip a:visited { color: #e7e7e7 }
.breadcrumb { font-size: 11pt }
.breadcrumb { font-size: 11pt; padding-bottom: 6px }
.login { margin: 3px; color: black }
input.button { color: #666666; height: 20px; width: 130px;
cursor:pointer; font-size: 90%; padding: 1px 0 0 10px;
@ -104,12 +104,12 @@ td.search { padding-top: 8px }
.dropdownMenu { cursor: pointer; padding-right: 4px; font-size: 93% }
.dropdown a:hover { text-decoration: underline }
.list { margin-bottom: 3px }
.list td, .list th { border: 1px solid grey;
padding-left: 5px; padding-right: 5px; padding-top: 3px }
.list td, .list th { border: 3px solid #ededed; color: grey;
padding: 3px 5px 0 5px }
.list th { background-color: #e5e5e5; font-style: italic; font-weight: normal }
.grid th { font-style: italic; font-weight: normal;
border-bottom: 2px solid grey; padding: 2px 2px }
.grid td { padding-right: 5px }
border-bottom: 5px solid #fdfdfd; padding: 3px 5px 0 5px }
.grid td { padding: 3px 3px 0 3px }
.cellGap { padding-right: 0.4em }
.cellDashed { border: 1px dashed grey !important }
.noStyle { border: 0 !important; padding: 0 !important; margin: 0 !important }
@ -127,7 +127,8 @@ td.search { padding-top: 8px }
color: white }
.even { background-color: #f9f9f9 }
.odd { background-color: #f4f4f4 }
.summary { margin-bottom: 5px; background-color: #f9f9f9 }
.summary { margin-bottom: 5px; background-color: #f9f9f9;
border: 2px solid #f9f9f9 }
.by { padding: 5px; color: grey; font-size: 97% }
.underline { border-bottom: 1px dotted grey }
.state { font-weight: bold; border-bottom: 1px dashed grey }

View file

@ -279,7 +279,7 @@ function doInlineSave(objectUid, name, objectUrl, content){
/* Ajax-saves p_content of field named p_name on object whose id is
p_objectUid and whose URL is p_objectUrl. Asks a confirmation before
doing it. */
var doIt = confirm('Do it?');
var doIt = confirm(save_confirm);
var params = {'action': 'storeFromAjax', 'layoutType': 'view'};
var hook = null;
if (!doIt) {

View file

@ -587,6 +587,14 @@ class ToolWrapper(AbstractWrapper):
'''Sends a mail. See doc for appy.gen.mail.sendMail.'''
sendMail(self, to, subject, body, attachments=attachments)
def formatDate(self, date, withHour=True):
'''Check doc @ToolMixin::formatDate.'''
if not date: return
return self.o.formatDate(date, withHour=withHour)
def getUserName(self, login=None, normalized=False):
return self.o.getUserName(login=login, normalized=normalized)
def refreshCatalog(self, startObject=None):
'''Reindex all Appy objects. For some unknown reason, method
catalog.refreshCatalog is not able to recatalog Appy objects.'''

View file

@ -329,18 +329,17 @@ class AbstractWrapper(object):
<th align=":dleft">:_('action_comment')</th>
</tr>
<tr for="event in objs"
var2="odd=loop.event.odd;
rhComments=event.get('comments', None);
var2="rhComments=event.get('comments', None);
state=event.get('review_state', None);
action=event['action'];
isDataChange=action == '_datachange_'"
class="odd and 'even' or 'odd'" valign="top">
class=":loop.event.odd and 'even' or 'odd'" valign="top">
<td if="isDataChange">
<x>:_('data_change')</x>
<img if="user.has_role('Manager')" class="clickable"
src=":url('delete')"
onclick=":'onDeleteEvent(%s,%s)' % \
(q(zobj.UID()), q(event['time']))"/>
(q(zobj.id), q(event['time']))"/>
</td>
<td if="not isDataChange">:_(zobj.getWorkflowLabel(action))</td>
<td var="actorId=event.get('actor')">
@ -408,7 +407,7 @@ class AbstractWrapper(object):
<img class="clickable" onclick="toggleCookie('appyHistory')"
src=":historyExpanded and url('collapse.gif') or url('expand.gif')"
align=":dleft" id="appyHistory_img" style="padding-right:4px"/>
<x>:_('object_history')</x> ||
<x>:_('object_history')</x> &mdash;
</x>
<!-- Creator and last modification date -->
@ -437,7 +436,7 @@ class AbstractWrapper(object):
<td colspan="2">
<span id="appyHistory"
style=":historyExpanded and 'display:block' or 'display:none'">
<div var="ajaxHookId=zobj.UID() + '_history'" id=":ajaxHookId">
<div var="ajaxHookId=zobj.id + '_history'" id=":ajaxHookId">
<script type="text/javascript">::'askObjectHistory(%s,%s,%d,0)' % \
(q(ajaxHookId), q(zobj.absolute_url()), \
historyMaxPerPage)</script>
@ -701,9 +700,10 @@ class AbstractWrapper(object):
elif name == 'session': return self.o.REQUEST.SESSION
elif name == 'typeName': return self.__class__.__bases__[-1].__name__
elif name == 'id': return self.o.id
elif name == 'uid': return self.o.UID()
elif name == 'uid': return self.o.id
elif name == 'klass': return self.__class__.__bases__[-1]
elif name == 'created': return self.o.created
elif name == 'creator': return self.o.creator
elif name == 'modified': return self.o.modified
elif name == 'url': return self.o.absolute_url()
elif name == 'state': return self.o.State()
@ -751,6 +751,14 @@ class AbstractWrapper(object):
if custom: return custom(self, *args, **kwargs)
def getField(self, name): return self.o.getAppyType(name)
def getLabel(self, name, type='field'):
'''Gets the translated label of field named p_name. If p_type is
"workflow", p_name denotes a workflow state or transition, not a
field.'''
o = self.o
if type == 'field': return o.translate(o.getAppyType(name).labelId)
elif type == 'workflow': return o.translate(o.getWorkflowLabel(name))
def isEmpty(self, name):
'''Returns True if value of field p_name is considered as being
empty.'''
@ -1004,7 +1012,7 @@ class AbstractWrapper(object):
# Determine where to put the result
toDisk = (at != 'string')
if toDisk and not at:
at = getOsTempFolder() + '/' + self.o.UID() + '.xml'
at = getOsTempFolder() + '/' + self.o.id + '.xml'
# Create the XML version of the object
marshaller = XmlMarshaller(cdata=True, dumpUnicode=True,
dumpXmlPrologue=toDisk,
@ -1036,7 +1044,7 @@ class AbstractWrapper(object):
whose values are the previous field values.'''
self.o.addDataChange(data)
def getLastEvent(self, transition, notBefore=''):
def getLastEvent(self, transition, notBefore=None):
'''Gets, from the object history, the last occurrence of transition
named p_transition. p_transition can be a list of names: in this
case, it returns the most recent occurrence of those transitions. If
@ -1069,4 +1077,10 @@ class AbstractWrapper(object):
if getattr(workflow, name).__class__.__name__ != 'State': continue
res.append((name, o.translate(o.getWorkflowLabel(name))))
return res
def path(self, name):
'''Returns the absolute file name of file stored in File field p_nnamed
p_name.'''
v = getattr(self, name)
if v: return v.getFilePath(self)
# ------------------------------------------------------------------------------

View file

@ -288,17 +288,18 @@ class Renderer:
supposed to be in binary format in p_content. The document
p_format may be: odt or any format in imageFormats.
p_anchor, p_wrapInPara and p_size are only relevant for images:
p_anchor, p_wrapInPara and p_size, p_sizeUnit and p_style are only
relevant for images:
* p_anchor defines the way the image is anchored into the document;
Valid values are 'page','paragraph', 'char' and 'as-char';
* p_wrapInPara, if true, wraps the resulting 'image' tag into a 'p'
tag;
* p_size, if specified, is a tuple of float or integers
(width, height) expressing size in p_sizeUnit (see below).
If not specified, size will be computed from image info.
If not specified, size will be computed from image info;
* p_sizeUnit is the unit for p_size elements, it can be "cm"
(centimeters) or "px" (pixels).
* If p_style is given, it is the content of a "style" attribute,
(centimeters) or "px" (pixels);
* if p_style is given, it is the content of a "style" attribute,
containing CSS attributes. If "width" and "heigth" attributes are
found there, they will override p_size and p_sizeUnit.
@ -308,8 +309,7 @@ class Renderer:
'''
importer = None
# Is there someting to import?
if not content and not at:
raise PodError(DOC_NOT_SPECIFIED)
if not content and not at: raise PodError(DOC_NOT_SPECIFIED)
# Convert Zope files into Appy wrappers.
if content.__class__.__name__ in ('File', 'Image'):
content = FileWrapper(content)