diff --git a/src/pfwbged/policy/browser/guards.py b/src/pfwbged/policy/browser/guards.py
index ceac13b..4d3aa62 100644
--- a/src/pfwbged/policy/browser/guards.py
+++ b/src/pfwbged/policy/browser/guards.py
@@ -107,6 +107,40 @@ class CanValidateOrRefuse(grok.View):
return False
+class CanCancelRefusal(grok.View):
+ grok.name("can_cancel_refusal")
+ grok.context(IDmsFile)
+ grok.require('zope2.View')
+
+ def render(self):
+
+ workflow = api.portal.get_tool('portal_workflow')
+ with api.env.adopt_user('admin'):
+ review_history = workflow.getInfoFor(self.context, 'review_history')
+ if not review_history:
+ return False
+ last_transition = review_history[-1]
+ return last_transition.get('action') == 'refuse' and \
+ last_transition.get('actor') == api.user.get_current().id
+
+
+class CanCancelValidation(grok.View):
+ grok.name("can_cancel_validation")
+ grok.context(IDmsFile)
+ grok.require('zope2.View')
+
+ def render(self):
+
+ workflow = api.portal.get_tool('portal_workflow')
+ with api.env.adopt_user('admin'):
+ review_history = workflow.getInfoFor(self.context, 'review_history')
+ if not review_history:
+ return False
+ last_transition = review_history[-1]
+ return last_transition.get('action') == 'validate' and \
+ last_transition.get('actor') == api.user.get_current().id
+
+
class CanBeTrashedDmsFile(grok.View):
""""""
grok.name('can_be_trashed')
diff --git a/src/pfwbged/policy/locales/fr/LC_MESSAGES/pfwbged.policy.po b/src/pfwbged/policy/locales/fr/LC_MESSAGES/pfwbged.policy.po
index 64af93f..1abf0af 100644
--- a/src/pfwbged/policy/locales/fr/LC_MESSAGES/pfwbged.policy.po
+++ b/src/pfwbged/policy/locales/fr/LC_MESSAGES/pfwbged.policy.po
@@ -27,7 +27,15 @@ msgstr "L'élément ${title} a été supprimé."
msgid "${title} is locked and cannot be deleted."
msgstr "L'élément ${title} est verrouillé et ne peut être supprimé."
-#: ../subscribers/document.py:430
+#: ../subscribers/document.py:417
+msgid "A previously refused version has returned to waiting validation"
+msgstr "Un document précédemment refusé est revenu en attente de validation"
+
+#: ../subscribers/document.py:415
+msgid "A previously validated version has returned to waiting validation"
+msgstr "Un document précédemment validé est revenu en attente de validation"
+
+#: ../subscribers/document.py:495
msgid "A validation request has been refused"
msgstr "Une demande de validation a été refusée."
@@ -86,10 +94,18 @@ msgstr "Visible par"
msgid "Cancel"
msgstr "Annuler"
+#: ../menu.py:58
+msgid "Cancel refusal of ${version}"
+msgstr "Annuler le refus de la version ${version}"
+
#: ../menu.py:56
msgid "Cancel validate and finish ${version}"
msgstr "Annuler la validation et finalisation de la version ${version}"
+#: ../menu.py:57
+msgid "Cancel validation of ${version}"
+msgstr "Annuler la validation de la version ${version}"
+
#: ../browser/refuse.py:33
#: ../browser/send_by_email.py:35
#: ../browser/to_process.py:25
diff --git a/src/pfwbged/policy/locales/fr/LC_MESSAGES/plone.po b/src/pfwbged/policy/locales/fr/LC_MESSAGES/plone.po
index 3fd2699..e54e683 100644
--- a/src/pfwbged/policy/locales/fr/LC_MESSAGES/plone.po
+++ b/src/pfwbged/policy/locales/fr/LC_MESSAGES/plone.po
@@ -61,6 +61,14 @@ msgstr "Retour en à traiter"
msgid "Back to writing"
msgstr "Retour à la rédaction"
+#: ../profiles/default/workflows/versionnote_workflow/definition.xml
+msgid "Cancel refusal"
+msgstr "Annuler le refus"
+
+#: ../profiles/default/workflows/versionnote_workflow/definition.xml
+msgid "Cancel validation"
+msgstr "Annuler la validation"
+
#: ../profiles/default/workflows/incomingapfmail_workflow/definition.xml
#: ../profiles/default/workflows/incomingmail_workflow/definition.xml
#: ../profiles/default/workflows/pfwbgeddocument_workflow/definition.xml
diff --git a/src/pfwbged/policy/locales/pfwbged.policy.pot b/src/pfwbged/policy/locales/pfwbged.policy.pot
index 2f63f89..8d7a1f4 100644
--- a/src/pfwbged/policy/locales/pfwbged.policy.pot
+++ b/src/pfwbged/policy/locales/pfwbged.policy.pot
@@ -29,7 +29,15 @@ msgstr ""
msgid "${title} is locked and cannot be deleted."
msgstr ""
-#: ../subscribers/document.py:430
+#: ../subscribers/document.py:417
+msgid "A previously refused version has returned to waiting validation"
+msgstr ""
+
+#: ../subscribers/document.py:415
+msgid "A previously validated version has returned to waiting validation"
+msgstr ""
+
+#: ../subscribers/document.py:495
msgid "A validation request has been refused"
msgstr ""
@@ -88,10 +96,18 @@ msgstr ""
msgid "Cancel"
msgstr ""
+#: ../menu.py:58
+msgid "Cancel refusal of ${version}"
+msgstr ""
+
#: ../menu.py:56
msgid "Cancel validate and finish ${version}"
msgstr ""
+#: ../menu.py:57
+msgid "Cancel validation of ${version}"
+msgstr ""
+
#: ../browser/refuse.py:33
#: ../browser/send_by_email.py:35
#: ../browser/to_process.py:25
diff --git a/src/pfwbged/policy/locales/plone.pot b/src/pfwbged/policy/locales/plone.pot
index 1fc88aa..6d82c8e 100644
--- a/src/pfwbged/policy/locales/plone.pot
+++ b/src/pfwbged/policy/locales/plone.pot
@@ -62,6 +62,14 @@ msgstr ""
msgid "Back to writing"
msgstr ""
+#: ../profiles/default/workflows/versionnote_workflow/definition.xml
+msgid "Cancel refusal"
+msgstr ""
+
+#: ../profiles/default/workflows/versionnote_workflow/definition.xml
+msgid "Cancel validation"
+msgstr ""
+
#: ../profiles/default/workflows/incomingapfmail_workflow/definition.xml
#: ../profiles/default/workflows/incomingmail_workflow/definition.xml
#: ../profiles/default/workflows/pfwbgeddocument_workflow/definition.xml
diff --git a/src/pfwbged/policy/menu.py b/src/pfwbged/policy/menu.py
index e9a31b0..16769a4 100644
--- a/src/pfwbged/policy/menu.py
+++ b/src/pfwbged/policy/menu.py
@@ -54,6 +54,8 @@ dmsfile_wfactions_mapping = {'ask_opinion': _(u"Ask opinion about version ${vers
'send_with_docbow': _(u"Send version ${version} with PES"),
'restore_from_trash': _(u"Restore version ${version}"),
'back_to_draft': _(u"Cancel validate and finish ${version}"),
+ 'cancel-validation': _(u"Cancel validation of ${version}"),
+ 'cancel-refusal': _(u"Cancel refusal of ${version}"),
}
@@ -232,6 +234,21 @@ class CustomMenu(menu.WorkflowMenu):
title = IGNORE_(title, mapping={'version': version})
cssClass += ' version-action version-id-%s' % context.id
cssClass += ' version-action-%s' % action['id']
+
+ # limit cancelling actions to a version's rightful owner
+ # guards are bypassed for Managers, so call them at least once
+ action_guards = {
+ 'cancel-refusal': 'can_cancel_refusal',
+ 'cancel-validation': 'can_cancel_validation',
+ }
+ if action['id'] in action_guards:
+ view = queryMultiAdapter(
+ (context, request),
+ name=action_guards.get(action['id'])
+ )
+ if view and not view.render():
+ action['allowed'] = False
+
else:
title = action['title']
diff --git a/src/pfwbged/policy/profiles/default/workflows/versionnote_workflow/definition.xml b/src/pfwbged/policy/profiles/default/workflows/versionnote_workflow/definition.xml
index 60e50c8..3dd09f4 100644
--- a/src/pfwbged/policy/profiles/default/workflows/versionnote_workflow/definition.xml
+++ b/src/pfwbged/policy/profiles/default/workflows/versionnote_workflow/definition.xml
@@ -16,6 +16,7 @@
+
Editor
Manager
@@ -109,6 +110,7 @@
+
Editor
Manager
@@ -203,6 +205,18 @@
here/@@can_validate_or_refuse
+
+ Cancel validation
+
+ here/@@can_cancel_validation
+
+
+
+ Cancel validation
+
+ here/@@can_cancel_refusal
+
+
Previous transition
diff --git a/src/pfwbged/policy/subscribers/document.py b/src/pfwbged/policy/subscribers/document.py
index 516419a..6f47610 100644
--- a/src/pfwbged/policy/subscribers/document.py
+++ b/src/pfwbged/policy/subscribers/document.py
@@ -100,6 +100,16 @@ def change_validation_state(context, event):
if api.content.get_state(validation) == 'todo':
api.content.transition(validation, 'validate')
validation.reindexObject(idxs=['review_state'])
+ elif event.transition.id == 'cancel-validation':
+ for ref in catalog.findRelations(query):
+ validation = ref.from_object
+ api.content.transition(validation, 'cancel-validation')
+ validation.reindexObject(idxs=['review_state'])
+ elif event.transition.id == 'cancel-refusal':
+ for ref in catalog.findRelations(query):
+ validation = ref.from_object
+ api.content.transition(validation, 'cancel-refusal')
+ validation.reindexObject(idxs=['review_state'])
@grok.subscribe(IDmsFile, IObjectWillBeRemovedEvent)
@@ -397,6 +407,63 @@ def email_notification_of_tasks(context, event):
job = async.queueJob(email_notification_of_tasks_sync, **kwargs)
+@grok.subscribe(IValidation, IAfterTransitionEvent)
+def email_notification_of_validation_reversal(context, event):
+ """Notify a validation requester when their previously validated
+ (or refused) request has returned to pending state"""
+ if not event.transition:
+ return
+ elif event.transition.id == 'cancel-validation':
+ comment = translate(_('A previously validated version has returned to waiting validation'), context=context.REQUEST)
+ elif event.transition.id == 'cancel-refusal':
+ comment = translate(_('A previously refused version has returned to waiting validation'), context=context.REQUEST)
+ else:
+ return
+
+ # go up in the acquisition chain to find the document
+ document = None
+ for obj in aq_chain(context):
+ obj = aq_parent(obj)
+ if IDmsDocument.providedBy(obj):
+ document = obj
+ break
+ if not document:
+ return
+
+ email_enquirer = None
+ for enquirer in (context.enquirer or []):
+ member = context.portal_membership.getMemberById(enquirer)
+ if member:
+ email_enquirer = member.getProperty('email', None)
+ if email_enquirer:
+ break
+
+ if not email_enquirer:
+ return
+
+ email_from = api.user.get_current().email or api.portal.get().getProperty('email_from_address') or 'admin@localhost'
+
+ subject = '%s - %s' % (context.title, document.title)
+
+ body = comment + \
+ '\n\n' + \
+ translate(_('Title: %s'), context=context.REQUEST) % context.title + \
+ '\n\n' + \
+ translate(_('Document: %s'), context=context.REQUEST) % document.title + \
+ '\n\n' + \
+ translate(_('Document Address: %s'), context=context.REQUEST) % document.absolute_url()
+
+ body += '\n\n\n-- \n' + translate(_('Sent by GED'))
+ body = body.encode('utf-8')
+
+ try:
+ context.MailHost.send(body, email_enquirer, email_from, subject, charset='utf-8')
+ except Exception as e:
+ # do not abort transaction in case of email error
+ log = logging.getLogger('pfwbged.policy')
+ log.exception(e)
+
+
@grok.subscribe(IValidation, IAfterTransitionEvent)
def email_notification_of_refused_task(context, event):
if event.new_state.id != 'refused':