appy.gen: workflows are now implemented as full Appy worlflows. Plone (DC) workflow are not generated anymore. From the Appy user point of view (=developer), no change has occurred: it is a pure implementation concern. This is one more step towards Appy independence from Plone.
This commit is contained in:
parent
93eb16670b
commit
ddec7cd62c
14 changed files with 378 additions and 806 deletions
231
gen/__init__.py
231
gen/__init__.py
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# ------------------------------------------------------------------------------
|
||||
import re, time, copy, sys, types, os, os.path, mimetypes, StringIO
|
||||
import re, time, copy, sys, types, os, os.path, mimetypes, string, StringIO
|
||||
from appy.gen.layout import Table
|
||||
from appy.gen.layout import defaultFieldLayouts
|
||||
from appy.gen.po import PoMessage
|
||||
|
@ -2167,7 +2167,13 @@ class Pod(Type):
|
|||
value = value._atFile
|
||||
setattr(obj, self.name, value)
|
||||
|
||||
# Workflow-specific types ------------------------------------------------------
|
||||
# Workflow-specific types and default workflows --------------------------------
|
||||
appyToZopePermissions = {
|
||||
'read': ('View', 'Access contents information'),
|
||||
'write': 'Modify portal content',
|
||||
'delete': 'Delete objects',
|
||||
}
|
||||
|
||||
class Role:
|
||||
'''Represents a role.'''
|
||||
ploneRoles = ('Manager', 'Member', 'Owner', 'Reviewer', 'Anonymous',
|
||||
|
@ -2201,6 +2207,12 @@ class State:
|
|||
# Standardize the way roles are expressed within self.permissions
|
||||
self.standardizeRoles()
|
||||
|
||||
def getName(self, wf):
|
||||
'''Returns the name for this state in workflow p_wf.'''
|
||||
for name in dir(wf):
|
||||
value = getattr(wf, name)
|
||||
if (value == self): return name
|
||||
|
||||
def getRole(self, role):
|
||||
'''p_role can be the name of a role or a Role instance. If it is the
|
||||
name of a role, this method returns self.usedRoles[role] if it
|
||||
|
@ -2233,16 +2245,6 @@ class State:
|
|||
|
||||
def getUsedRoles(self): return self.usedRoles.values()
|
||||
|
||||
def getTransitions(self, transitions, selfIsFromState=True):
|
||||
'''Among p_transitions, returns those whose fromState is p_self (if
|
||||
p_selfIsFromState is True) or those whose toState is p_self (if
|
||||
p_selfIsFromState is False).'''
|
||||
res = []
|
||||
for t in transitions:
|
||||
if self in t.getStates(selfIsFromState):
|
||||
res.append(t)
|
||||
return res
|
||||
|
||||
def getPermissions(self):
|
||||
'''If you get the permissions mapping through self.permissions, dict
|
||||
values may be of different types (a list of roles, a single role or
|
||||
|
@ -2258,6 +2260,38 @@ class State:
|
|||
res[permission] = roleValue
|
||||
return res
|
||||
|
||||
def updatePermission(self, obj, zopePermission, roleNames):
|
||||
'''Updates, on p_obj, list of p_roleNames which are granted a given
|
||||
p_zopePermission.'''
|
||||
attr = Permission.getZopeAttrName(zopePermission)
|
||||
if not hasattr(obj.aq_base, attr) or \
|
||||
(getattr(obj.aq_base, attr) != roleNames):
|
||||
setattr(obj, attr, roleNames)
|
||||
|
||||
def updatePermissions(self, wf, obj):
|
||||
'''Zope requires permission-to-roles mappings to be stored as attributes
|
||||
on the object itself. This method does this job, duplicating the info
|
||||
from this state on p_obj.'''
|
||||
for permission, roles in self.getPermissions().iteritems():
|
||||
roleNames = tuple([role.name for role in roles])
|
||||
# Compute Zope permission(s) related to this permission.
|
||||
if appyToZopePermissions.has_key(permission):
|
||||
# It is a standard permission (r, w, d)
|
||||
zopePerm = appyToZopePermissions[permission]
|
||||
elif isinstance(permission, basestring):
|
||||
# It is a user-defined permission
|
||||
zopePerm = permission
|
||||
else:
|
||||
# It is a Permission instance
|
||||
appName = obj.getProductConfig().PROJECTNAME
|
||||
zopePerm = permission.getName(wf, appName)
|
||||
# zopePerm contains a single permission or a tuple of permissions
|
||||
if isinstance(zopePerm, basestring):
|
||||
self.updatePermission(obj, zopePerm, roleNames)
|
||||
else:
|
||||
for zPerm in zopePerm:
|
||||
self.updatePermission(obj, zPerm, roleNames)
|
||||
|
||||
class Transition:
|
||||
def __init__(self, states, condition=True, action=None, notify=None,
|
||||
show=True, confirm=False):
|
||||
|
@ -2277,6 +2311,12 @@ class Transition:
|
|||
# the transition. It will only be possible by code.
|
||||
self.confirm = confirm # If True, a confirm popup will show up.
|
||||
|
||||
def getName(self, wf):
|
||||
'''Returns the name for this state in workflow p_wf.'''
|
||||
for name in dir(wf):
|
||||
value = getattr(wf, name)
|
||||
if (value == self): return name
|
||||
|
||||
def getUsedRoles(self):
|
||||
'''self.condition can specify a role.'''
|
||||
res = []
|
||||
|
@ -2296,23 +2336,6 @@ class Transition:
|
|||
else:
|
||||
return self.show
|
||||
|
||||
def getStates(self, fromStates=True):
|
||||
'''Returns the fromState(s) if p_fromStates is True, the toState(s)
|
||||
else. If you want to get the states grouped in tuples
|
||||
(fromState, toState), simply use self.states.'''
|
||||
res = []
|
||||
stateIndex = 1
|
||||
if fromStates:
|
||||
stateIndex = 0
|
||||
if self.isSingle():
|
||||
res.append(self.states[stateIndex])
|
||||
else:
|
||||
for states in self.states:
|
||||
theState = states[stateIndex]
|
||||
if theState not in res:
|
||||
res.append(theState)
|
||||
return res
|
||||
|
||||
def hasState(self, state, isFrom):
|
||||
'''If p_isFrom is True, this method returns True if p_state is a
|
||||
starting state for p_self. If p_isFrom is False, this method returns
|
||||
|
@ -2330,6 +2353,117 @@ class Transition:
|
|||
break
|
||||
return res
|
||||
|
||||
def isTriggerable(self, obj, wf):
|
||||
'''Can this transition be triggered on p_obj?'''
|
||||
# Checks that the current state of the object is a start state for this
|
||||
# transition.
|
||||
objState = obj.getState(name=False)
|
||||
if self.isSingle():
|
||||
if objState != self.states[0]: return False
|
||||
else:
|
||||
startFound = False
|
||||
for startState, stopState in self.states:
|
||||
if startState == objState:
|
||||
startFound = True
|
||||
break
|
||||
if not startFound: return False
|
||||
# Check that the condition is met
|
||||
user = obj.portal_membership.getAuthenticatedMember()
|
||||
if isinstance(self.condition, Role):
|
||||
# Condition is a role. Transition may be triggered if the user has
|
||||
# this role.
|
||||
return user.has_role(self.condition.name, obj)
|
||||
elif type(self.condition) == types.FunctionType:
|
||||
return self.condition(wf, obj.appy())
|
||||
elif type(self.condition) in (tuple, list):
|
||||
# It is a list of roles and/or functions. Transition may be
|
||||
# triggered if user has at least one of those roles and if all
|
||||
# functions return True.
|
||||
hasRole = None
|
||||
for roleOrFunction in self.condition:
|
||||
if isinstance(roleOrFunction, basestring):
|
||||
if hasRole == None:
|
||||
hasRole = False
|
||||
if user.has_role(roleOrFunction, obj):
|
||||
hasRole = True
|
||||
elif type(roleOrFunction) == types.FunctionType:
|
||||
if not roleOrFunction(wf, obj.appy()):
|
||||
return False
|
||||
if hasRole != False:
|
||||
return True
|
||||
|
||||
def executeAction(self, obj, wf):
|
||||
'''Executes the action related to this transition.'''
|
||||
msg = ''
|
||||
if type(self.action) in (tuple, list):
|
||||
# We need to execute a list of actions
|
||||
for act in self.action:
|
||||
msgPart = act(wf, obj.appy())
|
||||
if msgPart: msg += msgPart
|
||||
else: # We execute a single action only.
|
||||
msgPart = self.action(wf, obj.appy())
|
||||
if msgPart: msg += msgPart
|
||||
return msg
|
||||
|
||||
def trigger(self, transitionName, obj, wf, comment, doAction=True,
|
||||
doNotify=True, doHistory=True, doSay=True):
|
||||
'''This method triggers this transition on p_obj. The transition is
|
||||
supposed to be triggerable (call to self.isTriggerable must have been
|
||||
performed before calling this method). If p_doAction is False, the
|
||||
action that must normally be executed after the transition has been
|
||||
triggered will not be executed. If p_doNotify is False, the
|
||||
notifications (email,...) that must normally be launched after the
|
||||
transition has been triggered will not be launched. If p_doHistory is
|
||||
False, there will be no trace from this transition triggering in the
|
||||
workflow history. If p_doSay is False, we consider the transition is
|
||||
trigger programmatically, and no message is returned to the user.'''
|
||||
# Create the workflow_history dict if it does not exist.
|
||||
if not hasattr(obj.aq_base, 'workflow_history'):
|
||||
from persistent.mapping import PersistentMapping
|
||||
obj.workflow_history = PersistentMapping()
|
||||
# Create the event list if it does not exist in the dict
|
||||
if not obj.workflow_history: obj.workflow_history['appy'] = ()
|
||||
# Get the key where object history is stored (this overstructure is
|
||||
# only there for backward compatibility reasons)
|
||||
key = obj.workflow_history.keys()[0]
|
||||
# Identify the target state for this transition
|
||||
if self.isSingle():
|
||||
targetState = self.states[1]
|
||||
targetStateName = targetState.getName(wf)
|
||||
else:
|
||||
startState = obj.getState(name=False)
|
||||
for sState, tState in self.states:
|
||||
if startState == sState:
|
||||
targetState = tState
|
||||
targetStateName = targetState.getName(wf)
|
||||
break
|
||||
# Create the event and put it in workflow_history
|
||||
from DateTime import DateTime
|
||||
action = transitionName
|
||||
if transitionName == '_init_': action = None
|
||||
userId = obj.portal_membership.getAuthenticatedMember().getId()
|
||||
if not doHistory: comment = '_invisible_'
|
||||
obj.workflow_history[key] += (
|
||||
{'action':action, 'review_state': targetStateName,
|
||||
'comments': comment, 'actor': userId, 'time': DateTime()},)
|
||||
# Update permissions-to-roles attributes
|
||||
targetState.updatePermissions(wf, obj)
|
||||
# Refresh catalog-related security if required
|
||||
if not obj.isTemporary():
|
||||
obj.reindexObject(idxs=('allowedRolesAndUsers','review_state'))
|
||||
# Execute the related action if needed
|
||||
msg = ''
|
||||
if doAction and self.action: msg = self.executeAction(obj, wf)
|
||||
# Send notifications if needed
|
||||
if doNotify and self.notify and obj.getTool(True).enableNotifications:
|
||||
notifier.sendMail(obj.appy(), self, transitionName, wf)
|
||||
# Return a message to the user if needed
|
||||
if not doSay or (transitionName == '_init_'): return
|
||||
if not msg:
|
||||
msg = obj.translate(u'Your content\'s status has been modified.',
|
||||
domain='plone')
|
||||
obj.say(msg)
|
||||
|
||||
class Permission:
|
||||
'''If you need to define a specific read or write permission of a given
|
||||
attribute of an Appy type, you use the specific boolean parameters
|
||||
|
@ -2346,9 +2480,37 @@ class Permission:
|
|||
and "specificWritePermission" as booleans. When defining named
|
||||
(string) permissions, for referring to it you simply use those strings,
|
||||
you do not create instances of ReadPermission or WritePermission.'''
|
||||
|
||||
allowedChars = string.digits + string.letters + '_'
|
||||
|
||||
def __init__(self, fieldDescriptor):
|
||||
self.fieldDescriptor = fieldDescriptor
|
||||
|
||||
def getName(self, wf, appName):
|
||||
'''Returns the name of the Zope permission that corresponds to this
|
||||
permission.'''
|
||||
className, fieldName = self.fieldDescriptor.rsplit('.', 1)
|
||||
if className.find('.') == -1:
|
||||
# The related class resides in the same module as the workflow
|
||||
fullClassName= '%s_%s' % (wf.__module__.replace('.', '_'),className)
|
||||
else:
|
||||
# className contains the full package name of the class
|
||||
fullClassName = className.replace('.', '_')
|
||||
# Read or Write ?
|
||||
if self.__class__.__name__ == 'ReadPermission': access = 'Read'
|
||||
else: access = 'Write'
|
||||
return '%s: %s %s %s' % (appName, access, fullClassName, fieldName)
|
||||
|
||||
@staticmethod
|
||||
def getZopeAttrName(zopePermission):
|
||||
'''Gets the name of the attribute where Zope stores, on every object,
|
||||
the tuple of roles who are granted a given p_zopePermission.'''
|
||||
res = ''
|
||||
for c in zopePermission:
|
||||
if c in Permission.allowedChars: res += c
|
||||
else: res += '_'
|
||||
return '_%s_Permission' % res
|
||||
|
||||
class ReadPermission(Permission): pass
|
||||
class WritePermission(Permission): pass
|
||||
|
||||
|
@ -2363,6 +2525,17 @@ class No:
|
|||
def __nonzero__(self):
|
||||
return False
|
||||
|
||||
class WorkflowAnonymous:
|
||||
'''One-state workflow allowing anyone to consult and Manager to edit.'''
|
||||
mgr = 'Manager'
|
||||
active = State({r:[mgr, 'Anonymous'], w:mgr, d:mgr}, initial=True)
|
||||
|
||||
class WorkflowAuthenticated:
|
||||
'''One-state workflow allowing authenticated users to consult and Manager
|
||||
to edit.'''
|
||||
mgr = 'Manager'
|
||||
active = State({r:[mgr, 'Authenticated'], w:mgr, d:mgr}, initial=True)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Selection:
|
||||
'''Instances of this class may be given as validator of a String, in order
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue