Custom messages can now be returned as result of triggering transitions; added a mechanism for asking a confirmation to the user before saving it; bugfix in navigation (navigation info disappeared when firing workflow actions.

This commit is contained in:
Gaetan Delannay 2010-11-22 09:36:14 +01:00
parent cccdc12372
commit 502c86dab8
7 changed files with 95 additions and 39 deletions

View file

@ -47,6 +47,10 @@ class ToolMixin(BaseMixin):
res['action'] = appyType.action res['action'] = appyType.action
return res return res
def getSiteUrl(self):
'''Returns the absolute URL of this site.'''
return self.portal_url.getPortalObject().absolute_url()
def generateDocument(self): def generateDocument(self):
'''Generates the document from field-related info. UID of object that '''Generates the document from field-related info. UID of object that
is the template target is given in the request.''' is the template target is given in the request.'''

View file

@ -18,7 +18,7 @@ class BaseMixin:
_appy_meta_type = 'Class' _appy_meta_type = 'Class'
def get_o(self): def get_o(self):
'''In some cases, we wand the Zope object, we don't know if the current '''In some cases, we want the Zope object, we don't know if the current
object is a Zope or Appy object. By defining this property, object is a Zope or Appy object. By defining this property,
"someObject.o" produces always the Zope object, be someObject an Appy "someObject.o" produces always the Zope object, be someObject an Appy
or Zope object.''' or Zope object.'''
@ -79,6 +79,18 @@ class BaseMixin:
'''This methods is self's suicide.''' '''This methods is self's suicide.'''
self.getParentNode().manage_delObjects([self.id]) self.getParentNode().manage_delObjects([self.id])
def onDelete(self):
rq = self.REQUEST
self.delete()
if self.getUrl(rq['HTTP_REFERER'],mode='raw') ==self.getUrl(mode='raw'):
# We were consulting the object that has been deleted. Go back to
# the main page.
urlBack = self.getTool().getSiteUrl()
else:
urlBack = self.getUrl(rq['HTTP_REFERER'])
self.plone_utils.addPortalMessage(self.translate('delete_done'))
self.goto(urlBack)
def onCreate(self): def onCreate(self):
'''This method is called when a user wants to create a root object in '''This method is called when a user wants to create a root object in
the application folder or an object through a reference field.''' the application folder or an object through a reference field.'''
@ -152,6 +164,7 @@ class BaseMixin:
the "final" object in the database. If the object is not a temporary the "final" object in the database. If the object is not a temporary
one, this method updates its fields in the database.''' one, this method updates its fields in the database.'''
rq = self.REQUEST rq = self.REQUEST
tool = self.getTool()
errorMessage = self.translate( errorMessage = self.translate(
'Please correct the indicated errors.', domain='plone') 'Please correct the indicated errors.', domain='plone')
isNew = rq.get('is_new') == 'True' isNew = rq.get('is_new') == 'True'
@ -161,12 +174,12 @@ class BaseMixin:
if rq.get('nav', ''): if rq.get('nav', ''):
# We can go back to the initiator page. # We can go back to the initiator page.
splitted = rq['nav'].split('.') splitted = rq['nav'].split('.')
initiator = self.getTool().getObject(splitted[1]) initiator = tool.getObject(splitted[1])
initiatorPage = splitted[2].split(':')[1] initiatorPage = splitted[2].split(':')[1]
urlBack = initiator.getUrl(page=initiatorPage, nav='') urlBack = initiator.getUrl(page=initiatorPage, nav='')
else: else:
# Go back to the root of the site. # Go back to the root of the site.
urlBack = self.portal_url.getPortalObject().absolute_url() urlBack = tool.getSiteUrl()
else: else:
urlBack = self.getUrl() urlBack = self.getUrl()
self.plone_utils.addPortalMessage( self.plone_utils.addPortalMessage(
@ -192,12 +205,21 @@ class BaseMixin:
self.plone_utils.addPortalMessage(errorMessage) self.plone_utils.addPortalMessage(errorMessage)
return self.skyn.edit(self) return self.skyn.edit(self)
# Before saving data, must we ask a confirmation by the user ?
appyObj = self.appy()
saveConfirmed = rq.get('confirmed') == 'True'
if hasattr(appyObj, 'confirm') and not saveConfirmed:
msg = appyObj.confirm(values)
if msg:
rq.set('confirmMsg', msg.replace("'", "\\'"))
return self.skyn.edit(self)
# Create or update the object in the database # Create or update the object in the database
obj = self.createOrUpdate(isNew, values) obj = self.createOrUpdate(isNew, values)
# Redirect the user to the appropriate page # Redirect the user to the appropriate page
msg = obj.translate('Changes saved.', domain='plone') msg = obj.translate('Changes saved.', domain='plone')
if rq.get('buttonOk.x', None): if rq.get('buttonOk.x', None) or saveConfirmed:
# Go to the consult view for this object # Go to the consult view for this object
obj.plone_utils.addPortalMessage(msg) obj.plone_utils.addPortalMessage(msg)
return self.goto(obj.getUrl()) return self.goto(obj.getUrl())
@ -237,13 +259,6 @@ class BaseMixin:
return self.goto(obj.getUrl()) return self.goto(obj.getUrl())
return obj.skyn.edit(obj) return obj.skyn.edit(obj)
def onDelete(self):
rq = self.REQUEST
msg = self.translate('delete_done')
self.delete()
self.plone_utils.addPortalMessage(msg)
self.goto(self.getUrl(rq['HTTP_REFERER']))
def rememberPreviousData(self): def rememberPreviousData(self):
'''This method is called before updating an object and remembers, for '''This method is called before updating an object and remembers, for
every historized field, the previous value. Result is a dict every historized field, the previous value. Result is a dict
@ -754,17 +769,11 @@ class BaseMixin:
rq = self.REQUEST rq = self.REQUEST
self.portal_workflow.doActionFor(self, rq['workflow_action'], self.portal_workflow.doActionFor(self, rq['workflow_action'],
comment = rq.get('comment', '')) comment = rq.get('comment', ''))
# Where to redirect the user back ?
urlBack = rq['HTTP_REFERER']
if urlBack.find('?') != -1:
# Remove params; this way, the user may be redirected to correct
# phase when relevant.
urlBack = urlBack[:urlBack.find('?')]
msg = self.translate(u'Your content\'s status has been modified.',
domain='plone')
self.plone_utils.addPortalMessage(msg)
self.reindexObject() self.reindexObject()
return self.goto(urlBack) # Where to redirect the user back ?
# TODO (?): remove the "phase" param for redirecting the user to the
# next phase when relevant.
return self.goto(self.getUrl(rq['HTTP_REFERER']))
def fieldValueSelected(self, fieldName, vocabValue, dbValue): def fieldValueSelected(self, fieldName, vocabValue, dbValue):
'''When displaying a selection box (ie a String with a validator being a '''When displaying a selection box (ie a String with a validator being a
@ -916,15 +925,28 @@ class BaseMixin:
'''Returns a Appy URL. '''Returns a Appy URL.
* If p_base is None, it will be the base URL for this object * If p_base is None, it will be the base URL for this object
(ie, self.absolute_url()). (ie, self.absolute_url()).
* p_mode can de "edit" or "view". * p_mode can be "edit", "view" or "raw" (a non-param, base URL)
* p_kwargs can store additional parameters to add to the URL. * p_kwargs can store additional parameters to add to the URL.
In this dict, every value that is a string will be added to the In this dict, every value that is a string will be added to the
URL as-is. Every value that is True will be replaced by the value URL as-is. Every value that is True will be replaced by the value
in the request for the corresponding key (if existing; else, the in the request for the corresponding key (if existing; else, the
param will not be included in the URL at all).''' param will not be included in the URL at all).'''
# Define base URL if ommitted # Define the URL suffix
suffix = ''
if mode != 'raw': suffix = '/skyn/%s' % mode
# Define base URL if omitted
if not base: if not base:
base = '%s/skyn/%s' % (self.absolute_url(), mode) base = self.absolute_url() + suffix
# If a raw URL is asked, remove any param and suffix.
if mode == 'raw':
if '?' in base: base = base[:base.index('?')]
base = base.strip('/')
for mode in ('view', 'edit'):
suffix = 'skyn/%s' % mode
if base.endswith(suffix):
base = base[:-len(suffix)].strip('/')
break
return base
# Manage default args # Manage default args
if not kwargs: kwargs = self.getUrlDefaults if not kwargs: kwargs = self.getUrlDefaults
if 'page' not in kwargs: kwargs['page'] = True if 'page' not in kwargs: kwargs['page'] = True

View file

@ -11,7 +11,8 @@
page request/page|python:'main'; page request/page|python:'main';
cssAndJs python: contextObj.getCssAndJs(layoutType, page); cssAndJs python: contextObj.getCssAndJs(layoutType, page);
css python: cssAndJs[0]; css python: cssAndJs[0];
js python: cssAndJs[1]"> js python: cssAndJs[1];
confirmMsg request/confirmMsg | nothing;">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"
xmlns:tal="http://xml.zope.org/namespaces/tal" xmlns:tal="http://xml.zope.org/namespaces/tal"
@ -46,15 +47,19 @@
<body> <body>
<metal:fill fill-slot="main"> <metal:fill fill-slot="main">
<metal:prologue use-macro="here/skyn/page/macros/prologue"/> <metal:prologue use-macro="here/skyn/page/macros/prologue"/>
<form name="edit_form" method="post" enctype="multipart/form-data" <form id="appyEditForm" name="appyEditForm" method="post" enctype="multipart/form-data"
class="enableUnloadProtection atBaseEditForm" tal:attributes="action python: contextObj.absolute_url()+'/skyn/do';
tal:attributes="action python: contextObj.absolute_url()+'/skyn/do'"> class python: test(confirmMsg, 'atBaseEditForm', 'enableUnloadProtection atBaseEditForm')">
<input type="hidden" name="action" value="Update"/> <input type="hidden" name="action" value="Update"/>
<input type="hidden" name="page" tal:attributes="value page"/> <input type="hidden" name="page" tal:attributes="value page"/>
<input type="hidden" name="nav" tal:attributes="value request/nav|nothing"/> <input type="hidden" name="nav" tal:attributes="value request/nav|nothing"/>
<input type="hidden" name="is_new" tal:attributes="value contextObj/isTemporary"/> <input type="hidden" name="is_new" tal:attributes="value contextObj/isTemporary"/>
<input type="hidden" name="confirmed" value="False"/>
<metal:show use-macro="here/skyn/page/macros/show"/> <metal:show use-macro="here/skyn/page/macros/show"/>
</form> </form>
<script tal:condition="confirmMsg"
tal:content="python: 'askConfirm(\'script\', \'postConfirmedEditForm()\', \'%s\')' % confirmMsg">
</script>
<metal:footer use-macro="here/skyn/page/macros/footer"/> <metal:footer use-macro="here/skyn/page/macros/footer"/>
</metal:fill> </metal:fill>
</body> </body>

View file

@ -46,11 +46,16 @@
if (xhrObjects[pos].onGet) { if (xhrObjects[pos].onGet) {
xhrObjects[pos].onGet(xhrObjects[pos], hookElem); xhrObjects[pos].onGet(xhrObjects[pos], hookElem);
} }
// Eval inner scripts if any.
var innerScripts = document.getElementsByName("appyHook");
for (var i=0; i<innerScripts.length; i++) {
eval(innerScripts[i].innerHTML);
} }
xhrObjects[pos].freed = 1; xhrObjects[pos].freed = 1;
} }
} }
} }
}
function askAjaxChunk(hook,mode,url,page,macro,params,beforeSend,onGet) { function askAjaxChunk(hook,mode,url,page,macro,params,beforeSend,onGet) {
/* This function will ask to get a chunk of HTML on the server through a /* This function will ask to get a chunk of HTML on the server through a
@ -304,7 +309,13 @@
// We must execute Javascript code in "action" // We must execute Javascript code in "action"
eval(action); eval(action);
} }
}
// Function that finally posts the edit form after the user has confirmed that
// she really wants to post it.
function postConfirmedEditForm() {
var theForm = document.getElementById('appyEditForm');
theForm.confirmed.value = "True";
theForm.submit();
} }
// Function that shows or hides a tab. p_action is 'show' or 'hide'. // Function that shows or hides a tab. p_action is 'show' or 'hide'.
function manageTab(tabId, action) { function manageTab(tabId, action) {

View file

@ -92,8 +92,7 @@
tal:define= "innerRef innerRef|python:False; tal:define= "innerRef innerRef|python:False;
ajaxHookId python: contextObj.UID() + name" ajaxHookId python: contextObj.UID() + name"
tal:attributes = "id ajaxHookId"> tal:attributes = "id ajaxHookId">
<script language="javascript" <script name="appyHook" tal:content="python: 'askRefField(\'%s\',\'%s\',\'%s\',\'%s\',0)' % (ajaxHookId, contextObj.absolute_url(), name, innerRef)">
tal:content="python: 'askRefField(\'%s\',\'%s\',\'%s\',\'%s\',0)' % (ajaxHookId, contextObj.absolute_url(), name, innerRef)">
</script> </script>
</div> </div>
@ -148,9 +147,6 @@
<tal:objectIsPresent condition="objs"> <tal:objectIsPresent condition="objs">
<tal:obj repeat="obj objs"> <tal:obj repeat="obj objs">
<td tal:define="includeShownInfo python:True"><metal:showObjectTitle use-macro="portal/skyn/widgets/ref/macros/objectTitle" /></td> <td tal:define="includeShownInfo python:True"><metal:showObjectTitle use-macro="portal/skyn/widgets/ref/macros/objectTitle" /></td>
<td tal:condition="not: appyType/isBack">
<metal:showObjectActions use-macro="portal/skyn/widgets/ref/macros/objectActions" />
</td>
</tal:obj> </tal:obj>
</tal:objectIsPresent> </tal:objectIsPresent>
</tr></table> </tr></table>

View file

@ -142,6 +142,7 @@ def do(transitionName, stateChange, logger):
ploneObj = stateChange.object ploneObj = stateChange.object
workflow = ploneObj.getWorkflow() workflow = ploneObj.getWorkflow()
transition = workflow._transitionsMapping[transitionName] transition = workflow._transitionsMapping[transitionName]
msg = ''
# Must I execute transition-related actions and notifications? # Must I execute transition-related actions and notifications?
doAction = False doAction = False
if transition.action: if transition.action:
@ -161,11 +162,25 @@ def do(transitionName, stateChange, logger):
if doAction or doNotify: if doAction or doNotify:
obj = ploneObj.appy() obj = ploneObj.appy()
if doAction: if doAction:
msg = ''
if type(transition.action) in (tuple, list): if type(transition.action) in (tuple, list):
# We need to execute a list of actions # We need to execute a list of actions
for act in transition.action: act(workflow, obj) for act in transition.action:
msgPart = act(workflow, obj)
if msgPart: msg += msgPart
else: # We execute a single action only. else: # We execute a single action only.
transition.action(workflow, obj) msgPart = transition.action(workflow, obj)
if msgPart: msg += msgPart
if doNotify: if doNotify:
notifier.sendMail(obj, transition, transitionName, workflow, logger) notifier.sendMail(obj, transition, transitionName, workflow, logger)
# Produce a message to the user
if hasattr(ploneObj, '_v_appy_do') and not ploneObj._v_appy_do['doSay']:
# We do not produce any message if the transition was triggered
# programmatically.
return
# Produce a default message if no transition has given a custom one.
if not msg:
msg = ploneObj.translate(u'Your content\'s status has been modified.',
domain='plone')
ploneObj.plone_utils.addPortalMessage(msg)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -90,6 +90,8 @@ class AbstractWrapper:
typeName = property(get_typeName) typeName = property(get_typeName)
def get_id(self): return self.o.id def get_id(self): return self.o.id
id = property(get_id) id = property(get_id)
def get_uid(self): return self.o.UID()
uid = property(get_uid)
def get_state(self): def get_state(self):
return self.o.portal_workflow.getInfoFor(self.o, 'review_state') return self.o.portal_workflow.getInfoFor(self.o, 'review_state')
state = property(get_state) state = property(get_state)
@ -238,7 +240,8 @@ class AbstractWrapper:
# Set in a versatile attribute details about what to execute or not # Set in a versatile attribute details about what to execute or not
# (actions, notifications) after the transition has been executed by DC # (actions, notifications) after the transition has been executed by DC
# workflow. # workflow.
self.o._v_appy_do = {'doAction': doAction, 'doNotify': doNotify} self.o._v_appy_do = {'doAction': doAction, 'doNotify': doNotify,
'doSay': False}
if not doHistory: if not doHistory:
comment = '_invisible_' # Will not be displayed. comment = '_invisible_' # Will not be displayed.
# At first sight, I wanted to remove the entry from # At first sight, I wanted to remove the entry from