[gen] Replaced database fields tool.resultColumnsFor[class] by static attributes class.listColumns. [gen] Bugfixes and removed unused code.
This commit is contained in:
parent
204d7644b2
commit
809a553cf4
|
@ -401,11 +401,7 @@ class Calendar(Field):
|
||||||
res = Object(eventTypes=eventTypes, message=message)
|
res = Object(eventTypes=eventTypes, message=message)
|
||||||
if forBrowser:
|
if forBrowser:
|
||||||
res.eventTypes = ','.join(res.eventTypes)
|
res.eventTypes = ','.join(res.eventTypes)
|
||||||
if not res.message:
|
if not res.message: res.message = ''
|
||||||
res.message = ''
|
|
||||||
else:
|
|
||||||
res.message = obj.formatText(res.message, format='js')
|
|
||||||
return res.__dict__
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def getEventsAt(self, obj, date):
|
def getEventsAt(self, obj, date):
|
||||||
|
|
|
@ -49,7 +49,7 @@ class Date(Field):
|
||||||
<select name=":'%s_year' % name" id=":'%s_year' % name">
|
<select name=":'%s_year' % name" id=":'%s_year' % name">
|
||||||
<option value="">-</option>
|
<option value="">-</option>
|
||||||
<option for="year in years" value=":year"
|
<option for="year in years" value=":year"
|
||||||
selected=":field.isSelected(zobj, name, 'year', year, \
|
selected=":field.isSelected(zobj, 'year', year, \
|
||||||
rawValue)">:year</option>
|
rawValue)">:year</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ class Date(Field):
|
||||||
<x if="field.calendar">
|
<x if="field.calendar">
|
||||||
<input type="hidden" id=":name" name=":name"/>
|
<input type="hidden" id=":name" name=":name"/>
|
||||||
<img id=":'%s_img' % name" src=":url('calendar.gif')"/>
|
<img id=":'%s_img' % name" src=":url('calendar.gif')"/>
|
||||||
<script type="text/javascript">:field.getJsInit(name, years)</script>
|
<script type="text/javascript">::field.getJsInit(name, years)</script>
|
||||||
</x>
|
</x>
|
||||||
|
|
||||||
<!-- Hour and minutes -->
|
<!-- Hour and minutes -->
|
||||||
|
@ -112,7 +112,7 @@ class Date(Field):
|
||||||
<x if="field.calendar">
|
<x if="field.calendar">
|
||||||
<input type="hidden" id=":fromName" name=":fromName"/>
|
<input type="hidden" id=":fromName" name=":fromName"/>
|
||||||
<img id=":'%s_img' % fromName" src=":url('calendar.gif')"/>
|
<img id=":'%s_img' % fromName" src=":url('calendar.gif')"/>
|
||||||
<script type="text/javascript">:field.getJsInit(fromName, years)
|
<script type="text/javascript">::field.getJsInit(fromName, years)
|
||||||
</script>
|
</script>
|
||||||
</x>
|
</x>
|
||||||
</td>
|
</td>
|
||||||
|
@ -145,7 +145,7 @@ class Date(Field):
|
||||||
<x if="widget.calendar">
|
<x if="widget.calendar">
|
||||||
<input type="hidden" id=":toName" name=":toName"/>
|
<input type="hidden" id=":toName" name=":toName"/>
|
||||||
<img id=":'%s_img' % toName" src=":url('calendar.gif')"/>
|
<img id=":'%s_img' % toName" src=":url('calendar.gif')"/>
|
||||||
<script type="text/javascript">:field.getJsInit(toName, years)">
|
<script type="text/javascript">::field.getJsInit(toName, years)">
|
||||||
</script>
|
</script>
|
||||||
</x>
|
</x>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -31,7 +31,7 @@ class Phase:
|
||||||
if="not singlePhase and not singlePage">::_(label)</div>
|
if="not singlePhase and not singlePage">::_(label)</div>
|
||||||
|
|
||||||
<!-- The page(s) within the phase -->
|
<!-- The page(s) within the phase -->
|
||||||
<x for="aPage in phase.pages">
|
<x for="aPage in phase.pages" var2="aPageInfo=phase.pagesInfo[aPage]">
|
||||||
<!-- First line: page name and icons -->
|
<!-- First line: page name and icons -->
|
||||||
<div if="not (singlePhase and singlePage)"
|
<div if="not (singlePhase and singlePage)"
|
||||||
class=":aPage==page and 'portletCurrent portletPage' or \
|
class=":aPage==page and 'portletCurrent portletPage' or \
|
||||||
|
@ -39,7 +39,8 @@ class Phase:
|
||||||
<a href=":zobj.getUrl(page=aPage)">::_('%s_page_%s' % \
|
<a href=":zobj.getUrl(page=aPage)">::_('%s_page_%s' % \
|
||||||
(zobj.meta_type, aPage))</a>
|
(zobj.meta_type, aPage))</a>
|
||||||
<x var="locked=zobj.isLocked(user, aPage);
|
<x var="locked=zobj.isLocked(user, aPage);
|
||||||
editable=mayEdit and phase.pagesInfo[aPage].showOnEdit">
|
editable=mayEdit and aPageInfo.showOnEdit and \
|
||||||
|
aPageInfo.showEdit">
|
||||||
<a if="editable and not locked"
|
<a if="editable and not locked"
|
||||||
href=":zobj.getUrl(mode='edit', page=aPage)">
|
href=":zobj.getUrl(mode='edit', page=aPage)">
|
||||||
<img src=":url('edit')" title=":_('object_edit')"/></a>
|
<img src=":url('edit')" title=":_('object_edit')"/></a>
|
||||||
|
@ -57,7 +58,7 @@ class Phase:
|
||||||
</x>
|
</x>
|
||||||
</div>
|
</div>
|
||||||
<!-- Next lines: links -->
|
<!-- Next lines: links -->
|
||||||
<x var="links=phase.pagesInfo[aPage].links" if="links">
|
<x var="links=aPageInfo.links" if="links">
|
||||||
<div for="link in links"><a href=":link.url">:link.title</a></div>
|
<div for="link in links"><a href=":link.url">:link.title</a></div>
|
||||||
</x>
|
</x>
|
||||||
</x>
|
</x>
|
||||||
|
|
329
fields/workflow.py
Normal file
329
fields/workflow.py
Normal file
|
@ -0,0 +1,329 @@
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# This file is part of Appy, a framework for building applications in the Python
|
||||||
|
# language. Copyright (C) 2007 Gaetan Delannay
|
||||||
|
|
||||||
|
# Appy is free software; you can redistribute it and/or modify it under the
|
||||||
|
# terms of the GNU General Public License as published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
|
||||||
|
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# Appy. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
import types, string
|
||||||
|
from appy.gen.mail import sendNotification
|
||||||
|
|
||||||
|
# Default Appy permissions -----------------------------------------------------
|
||||||
|
r, w, d = ('read', 'write', 'delete')
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
class Role:
|
||||||
|
'''Represents a role, be it local or global.'''
|
||||||
|
zopeRoles = ('Manager', 'Owner', 'Anonymous', 'Authenticated')
|
||||||
|
zopeLocalRoles = ('Owner',)
|
||||||
|
zopeUngrantableRoles = ('Anonymous', 'Authenticated')
|
||||||
|
def __init__(self, name, local=False, grantable=True):
|
||||||
|
self.name = name
|
||||||
|
self.local = local # True if it can be used as local role only.
|
||||||
|
# It is a standard Zope role or an application-specific one?
|
||||||
|
self.zope = name in self.zopeRoles
|
||||||
|
if self.zope and (name in self.zopeLocalRoles):
|
||||||
|
self.local = True
|
||||||
|
self.grantable = grantable
|
||||||
|
if self.zope and (name in self.zopeUngrantableRoles):
|
||||||
|
self.grantable = False
|
||||||
|
# An ungrantable role is one that is, like the Anonymous or
|
||||||
|
# Authenticated roles, automatically attributed to a user.
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
class State:
|
||||||
|
'''Represents a workflow state.'''
|
||||||
|
def __init__(self, permissions, initial=False, phase=None, show=True):
|
||||||
|
self.usedRoles = {}
|
||||||
|
# The following dict ~{s_permissionName:[s_roleName|Role_role]}~
|
||||||
|
# gives, for every permission managed by a workflow, the list of roles
|
||||||
|
# for which the permission is granted in this state. Standard
|
||||||
|
# permissions are 'read', 'write' and 'delete'.
|
||||||
|
self.permissions = permissions
|
||||||
|
self.initial = initial
|
||||||
|
self.phase = phase
|
||||||
|
self.show = show
|
||||||
|
# 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
|
||||||
|
exists, or creates a Role instance, puts it in self.usedRoles and
|
||||||
|
returns it else. If it is a Role instance, the method stores it in
|
||||||
|
self.usedRoles if it is not in it yet and returns it.'''
|
||||||
|
if isinstance(role, basestring):
|
||||||
|
if role in self.usedRoles:
|
||||||
|
return self.usedRoles[role]
|
||||||
|
else:
|
||||||
|
theRole = Role(role)
|
||||||
|
self.usedRoles[role] = theRole
|
||||||
|
return theRole
|
||||||
|
else:
|
||||||
|
if role.name not in self.usedRoles:
|
||||||
|
self.usedRoles[role.name] = role
|
||||||
|
return role
|
||||||
|
|
||||||
|
def standardizeRoles(self):
|
||||||
|
'''This method converts, within self.permissions, every role to a
|
||||||
|
Role instance. Every used role is stored in self.usedRoles.'''
|
||||||
|
for permission, roles in self.permissions.items():
|
||||||
|
if isinstance(roles, basestring) or isinstance(roles, Role):
|
||||||
|
self.permissions[permission] = [self.getRole(roles)]
|
||||||
|
elif roles:
|
||||||
|
rolesList = []
|
||||||
|
for role in roles:
|
||||||
|
rolesList.append(self.getRole(role))
|
||||||
|
self.permissions[permission] = rolesList
|
||||||
|
|
||||||
|
def getUsedRoles(self): return self.usedRoles.values()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
class Transition:
|
||||||
|
'''Represents a workflow transition.'''
|
||||||
|
def __init__(self, states, condition=True, action=None, notify=None,
|
||||||
|
show=True, confirm=False):
|
||||||
|
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
|
||||||
|
# (fromState, toState) sub-tuples. This way, you may define only 1
|
||||||
|
# transition at several places in the state-transition diagram. It may
|
||||||
|
# be useful for "undo" transitions, for example.
|
||||||
|
self.condition = condition
|
||||||
|
if isinstance(condition, basestring):
|
||||||
|
# The condition specifies the name of a role.
|
||||||
|
self.condition = Role(condition)
|
||||||
|
self.action = action
|
||||||
|
self.notify = notify # If not None, it is a method telling who must be
|
||||||
|
# notified by email after the transition has been executed.
|
||||||
|
self.show = show # If False, the end user will not be able to trigger
|
||||||
|
# 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 = []
|
||||||
|
if isinstance(self.condition, Role):
|
||||||
|
res.append(self.condition)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def isSingle(self):
|
||||||
|
'''If this transition is only defined between 2 states, returns True.
|
||||||
|
Else, returns False.'''
|
||||||
|
return isinstance(self.states[0], State)
|
||||||
|
|
||||||
|
def isShowable(self, workflow, obj):
|
||||||
|
'''Is this transition showable?'''
|
||||||
|
if callable(self.show):
|
||||||
|
return self.show(workflow, obj.appy())
|
||||||
|
else:
|
||||||
|
return self.show
|
||||||
|
|
||||||
|
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
|
||||||
|
True if p_state is an ending state for p_self.'''
|
||||||
|
stateIndex = 1
|
||||||
|
if isFrom:
|
||||||
|
stateIndex = 0
|
||||||
|
if self.isSingle():
|
||||||
|
res = state == self.states[stateIndex]
|
||||||
|
else:
|
||||||
|
res = False
|
||||||
|
for states in self.states:
|
||||||
|
if states[stateIndex] == state:
|
||||||
|
res = True
|
||||||
|
break
|
||||||
|
return res
|
||||||
|
|
||||||
|
def isTriggerable(self, obj, wf, noSecurity=False):
|
||||||
|
'''Can this transition be triggered on p_obj?'''
|
||||||
|
wf = wf.__instance__ # We need the prototypical instance here.
|
||||||
|
# Checks that the current state of the object is a start state for this
|
||||||
|
# transition.
|
||||||
|
objState = obj.State(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, excepted if noSecurity is True.
|
||||||
|
if noSecurity: return True
|
||||||
|
user = obj.getTool().getUser()
|
||||||
|
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 = ''
|
||||||
|
obj = obj.appy()
|
||||||
|
wf = wf.__instance__ # We need the prototypical instance here.
|
||||||
|
if type(self.action) in (tuple, list):
|
||||||
|
# We need to execute a list of actions
|
||||||
|
for act in self.action:
|
||||||
|
msgPart = act(wf, obj)
|
||||||
|
if msgPart: msg += msgPart
|
||||||
|
else: # We execute a single action only.
|
||||||
|
msgPart = self.action(wf, obj)
|
||||||
|
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
|
||||||
|
email notifications 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.State(name=False)
|
||||||
|
for sState, tState in self.states:
|
||||||
|
if startState == sState:
|
||||||
|
targetState = tState
|
||||||
|
targetStateName = targetState.getName(wf)
|
||||||
|
break
|
||||||
|
# Create the event and add it in the object history
|
||||||
|
action = transitionName
|
||||||
|
if transitionName == '_init_': action = None
|
||||||
|
if not doHistory: comment = '_invisible_'
|
||||||
|
obj.addHistoryEvent(action, review_state=targetStateName,
|
||||||
|
comments=comment)
|
||||||
|
# Reindex the object if required. Not only security-related indexes
|
||||||
|
# (Allowed, State) need to be updated here.
|
||||||
|
if not obj.isTemporary(): obj.reindex()
|
||||||
|
# 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).mailEnabled:
|
||||||
|
sendNotification(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('object_saved')
|
||||||
|
obj.say(msg)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
class Permission:
|
||||||
|
'''If you need to define a specific read or write permission for some field
|
||||||
|
on a gen-class, you use the specific boolean attrs
|
||||||
|
"specificReadPermission" or "specificWritePermission". When you want to
|
||||||
|
refer to those specific read or write permissions when
|
||||||
|
defining a workflow, for example, you need to use instances of
|
||||||
|
"ReadPermission" and "WritePermission", the 2 children classes of this
|
||||||
|
class. For example, if you need to refer to write permission of
|
||||||
|
attribute "t1" of class A, write: WritePermission("A.t1") or
|
||||||
|
WritePermission("x.y.A.t1") if class A is not in the same module as
|
||||||
|
where you instantiate the class.
|
||||||
|
|
||||||
|
Note that this holds only if you use attributes "specificReadPermission"
|
||||||
|
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 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)
|
||||||
|
|
||||||
|
class ReadPermission(Permission): pass
|
||||||
|
class WritePermission(Permission): pass
|
||||||
|
|
||||||
|
# Standard workflows -----------------------------------------------------------
|
||||||
|
class WorkflowAnonymous:
|
||||||
|
'''One-state workflow allowing anyone to consult and Manager to edit.'''
|
||||||
|
mgr = 'Manager'
|
||||||
|
o = 'Owner'
|
||||||
|
active = State({r:(mgr, 'Anonymous', 'Authenticated'), w:(mgr,o),d:(mgr,o)},
|
||||||
|
initial=True)
|
||||||
|
|
||||||
|
class WorkflowAuthenticated:
|
||||||
|
'''One-state workflow allowing authenticated users to consult and Manager
|
||||||
|
to edit.'''
|
||||||
|
mgr = 'Manager'
|
||||||
|
o = 'Owner'
|
||||||
|
active = State({r:(mgr, 'Authenticated'), w:(mgr,o), d:(mgr,o)},
|
||||||
|
initial=True)
|
||||||
|
|
||||||
|
class WorkflowOwner:
|
||||||
|
'''One-state workflow allowing only manager and owner to consult and
|
||||||
|
edit.'''
|
||||||
|
mgr = 'Manager'
|
||||||
|
o = 'Owner'
|
||||||
|
active = State({r:(mgr, o), w:(mgr, o), d:mgr}, initial=True)
|
||||||
|
# ------------------------------------------------------------------------------
|
329
gen/__init__.py
329
gen/__init__.py
|
@ -1,12 +1,24 @@
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
import types, string
|
# This file is part of Appy, a framework for building applications in the Python
|
||||||
from appy.gen.mail import sendNotification
|
# language. Copyright (C) 2007 Gaetan Delannay
|
||||||
from appy.gen import utils as gutils
|
|
||||||
|
# Appy is free software; you can redistribute it and/or modify it under the
|
||||||
|
# terms of the GNU General Public License as published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
|
||||||
|
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# Appy. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Import stuff from appy.fields (and from a few other places too).
|
# Import stuff from appy.fields (and from a few other places too).
|
||||||
# This way, when an app gets "from appy.gen import *", everything is available.
|
# This way, when an app gets "from appy.gen import *", everything is available.
|
||||||
# ------------------------------------------------------------------------------
|
from appy import Object
|
||||||
|
from appy.px import Px
|
||||||
from appy.fields import Field
|
from appy.fields import Field
|
||||||
from appy.fields.action import Action
|
from appy.fields.action import Action
|
||||||
from appy.fields.boolean import Boolean
|
from appy.fields.boolean import Boolean
|
||||||
|
@ -24,13 +36,9 @@ from appy.fields.search import Search, UiSearch
|
||||||
from appy.fields.group import Group, Column
|
from appy.fields.group import Group, Column
|
||||||
from appy.fields.page import Page
|
from appy.fields.page import Page
|
||||||
from appy.fields.phase import Phase
|
from appy.fields.phase import Phase
|
||||||
|
from appy.fields.workflow import *
|
||||||
from appy.gen.layout import Table
|
from appy.gen.layout import Table
|
||||||
from appy.px import Px
|
from appy.gen.utils import No
|
||||||
from appy import Object
|
|
||||||
No = gutils.No
|
|
||||||
|
|
||||||
# Default Appy permissions -----------------------------------------------------
|
|
||||||
r, w, d = ('read', 'write', 'delete')
|
|
||||||
|
|
||||||
class Import:
|
class Import:
|
||||||
'''Used for describing the place where to find the data to use for creating
|
'''Used for describing the place where to find the data to use for creating
|
||||||
|
@ -58,307 +66,6 @@ class Import:
|
||||||
# and must return a similar, sorted, list.
|
# and must return a similar, sorted, list.
|
||||||
self.sort = sort
|
self.sort = sort
|
||||||
|
|
||||||
# Workflow-specific types and default workflows --------------------------------
|
|
||||||
class Role:
|
|
||||||
'''Represents a role.'''
|
|
||||||
zopeRoles = ('Manager', 'Owner', 'Anonymous', 'Authenticated')
|
|
||||||
zopeLocalRoles = ('Owner',)
|
|
||||||
zopeUngrantableRoles = ('Anonymous', 'Authenticated')
|
|
||||||
def __init__(self, name, local=False, grantable=True):
|
|
||||||
self.name = name
|
|
||||||
self.local = local # True if it can be used as local role only.
|
|
||||||
# It is a standard Zope role or an application-specific one?
|
|
||||||
self.zope = name in self.zopeRoles
|
|
||||||
if self.zope and (name in self.zopeLocalRoles):
|
|
||||||
self.local = True
|
|
||||||
self.grantable = grantable
|
|
||||||
if self.zope and (name in self.zopeUngrantableRoles):
|
|
||||||
self.grantable = False
|
|
||||||
# An ungrantable role is one that is, like the Anonymous or
|
|
||||||
# Authenticated roles, automatically attributed to a user.
|
|
||||||
|
|
||||||
class State:
|
|
||||||
def __init__(self, permissions, initial=False, phase=None, show=True):
|
|
||||||
self.usedRoles = {}
|
|
||||||
# The following dict ~{s_permissionName:[s_roleName|Role_role]}~
|
|
||||||
# gives, for every permission managed by a workflow, the list of roles
|
|
||||||
# for which the permission is granted in this state. Standard
|
|
||||||
# permissions are 'read', 'write' and 'delete'.
|
|
||||||
self.permissions = permissions
|
|
||||||
self.initial = initial
|
|
||||||
self.phase = phase
|
|
||||||
self.show = show
|
|
||||||
# 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
|
|
||||||
exists, or creates a Role instance, puts it in self.usedRoles and
|
|
||||||
returns it else. If it is a Role instance, the method stores it in
|
|
||||||
self.usedRoles if it is not in it yet and returns it.'''
|
|
||||||
if isinstance(role, basestring):
|
|
||||||
if role in self.usedRoles:
|
|
||||||
return self.usedRoles[role]
|
|
||||||
else:
|
|
||||||
theRole = Role(role)
|
|
||||||
self.usedRoles[role] = theRole
|
|
||||||
return theRole
|
|
||||||
else:
|
|
||||||
if role.name not in self.usedRoles:
|
|
||||||
self.usedRoles[role.name] = role
|
|
||||||
return role
|
|
||||||
|
|
||||||
def standardizeRoles(self):
|
|
||||||
'''This method converts, within self.permissions, every role to a
|
|
||||||
Role instance. Every used role is stored in self.usedRoles.'''
|
|
||||||
for permission, roles in self.permissions.items():
|
|
||||||
if isinstance(roles, basestring) or isinstance(roles, Role):
|
|
||||||
self.permissions[permission] = [self.getRole(roles)]
|
|
||||||
elif roles:
|
|
||||||
rolesList = []
|
|
||||||
for role in roles:
|
|
||||||
rolesList.append(self.getRole(role))
|
|
||||||
self.permissions[permission] = rolesList
|
|
||||||
|
|
||||||
def getUsedRoles(self): return self.usedRoles.values()
|
|
||||||
|
|
||||||
class Transition:
|
|
||||||
def __init__(self, states, condition=True, action=None, notify=None,
|
|
||||||
show=True, confirm=False):
|
|
||||||
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
|
|
||||||
# (fromState, toState) sub-tuples. This way, you may define only 1
|
|
||||||
# transition at several places in the state-transition diagram. It may
|
|
||||||
# be useful for "undo" transitions, for example.
|
|
||||||
self.condition = condition
|
|
||||||
if isinstance(condition, basestring):
|
|
||||||
# The condition specifies the name of a role.
|
|
||||||
self.condition = Role(condition)
|
|
||||||
self.action = action
|
|
||||||
self.notify = notify # If not None, it is a method telling who must be
|
|
||||||
# notified by email after the transition has been executed.
|
|
||||||
self.show = show # If False, the end user will not be able to trigger
|
|
||||||
# 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 = []
|
|
||||||
if isinstance(self.condition, Role):
|
|
||||||
res.append(self.condition)
|
|
||||||
return res
|
|
||||||
|
|
||||||
def isSingle(self):
|
|
||||||
'''If this transition is only defined between 2 states, returns True.
|
|
||||||
Else, returns False.'''
|
|
||||||
return isinstance(self.states[0], State)
|
|
||||||
|
|
||||||
def isShowable(self, workflow, obj):
|
|
||||||
'''Is this transition showable?'''
|
|
||||||
if callable(self.show):
|
|
||||||
return self.show(workflow, obj.appy())
|
|
||||||
else:
|
|
||||||
return self.show
|
|
||||||
|
|
||||||
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
|
|
||||||
True if p_state is an ending state for p_self.'''
|
|
||||||
stateIndex = 1
|
|
||||||
if isFrom:
|
|
||||||
stateIndex = 0
|
|
||||||
if self.isSingle():
|
|
||||||
res = state == self.states[stateIndex]
|
|
||||||
else:
|
|
||||||
res = False
|
|
||||||
for states in self.states:
|
|
||||||
if states[stateIndex] == state:
|
|
||||||
res = True
|
|
||||||
break
|
|
||||||
return res
|
|
||||||
|
|
||||||
def isTriggerable(self, obj, wf, noSecurity=False):
|
|
||||||
'''Can this transition be triggered on p_obj?'''
|
|
||||||
wf = wf.__instance__ # We need the prototypical instance here.
|
|
||||||
# Checks that the current state of the object is a start state for this
|
|
||||||
# transition.
|
|
||||||
objState = obj.State(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, excepted if noSecurity is True.
|
|
||||||
if noSecurity: return True
|
|
||||||
user = obj.getTool().getUser()
|
|
||||||
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 = ''
|
|
||||||
obj = obj.appy()
|
|
||||||
wf = wf.__instance__ # We need the prototypical instance here.
|
|
||||||
if type(self.action) in (tuple, list):
|
|
||||||
# We need to execute a list of actions
|
|
||||||
for act in self.action:
|
|
||||||
msgPart = act(wf, obj)
|
|
||||||
if msgPart: msg += msgPart
|
|
||||||
else: # We execute a single action only.
|
|
||||||
msgPart = self.action(wf, obj)
|
|
||||||
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
|
|
||||||
email notifications 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.State(name=False)
|
|
||||||
for sState, tState in self.states:
|
|
||||||
if startState == sState:
|
|
||||||
targetState = tState
|
|
||||||
targetStateName = targetState.getName(wf)
|
|
||||||
break
|
|
||||||
# Create the event and add it in the object history
|
|
||||||
action = transitionName
|
|
||||||
if transitionName == '_init_': action = None
|
|
||||||
if not doHistory: comment = '_invisible_'
|
|
||||||
obj.addHistoryEvent(action, review_state=targetStateName,
|
|
||||||
comments=comment)
|
|
||||||
# Reindex the object if required. Not only security-related indexes
|
|
||||||
# (Allowed, State) need to be updated here.
|
|
||||||
if not obj.isTemporary(): obj.reindex()
|
|
||||||
# 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).mailEnabled:
|
|
||||||
sendNotification(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('object_saved')
|
|
||||||
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
|
|
||||||
"specificReadPermission" or "specificWritePermission" for this attribute.
|
|
||||||
When you want to refer to those specific read or write permissions when
|
|
||||||
defining a workflow, for example, you need to use instances of
|
|
||||||
"ReadPermission" and "WritePermission", the 2 children classes of this
|
|
||||||
class. For example, if you need to refer to write permission of
|
|
||||||
attribute "t1" of class A, write: WritePermission("A.t1") or
|
|
||||||
WritePermission("x.y.A.t1") if class A is not in the same module as
|
|
||||||
where you instantiate the class.
|
|
||||||
|
|
||||||
Note that this holds only if you use attributes "specificReadPermission"
|
|
||||||
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 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)
|
|
||||||
|
|
||||||
class ReadPermission(Permission): pass
|
|
||||||
class WritePermission(Permission): pass
|
|
||||||
|
|
||||||
class WorkflowAnonymous:
|
|
||||||
'''One-state workflow allowing anyone to consult and Manager to edit.'''
|
|
||||||
mgr = 'Manager'
|
|
||||||
o = 'Owner'
|
|
||||||
active = State({r:(mgr, 'Anonymous', 'Authenticated'), w:(mgr,o),d:(mgr,o)},
|
|
||||||
initial=True)
|
|
||||||
|
|
||||||
class WorkflowAuthenticated:
|
|
||||||
'''One-state workflow allowing authenticated users to consult and Manager
|
|
||||||
to edit.'''
|
|
||||||
mgr = 'Manager'
|
|
||||||
o = 'Owner'
|
|
||||||
active = State({r:(mgr, 'Authenticated'), w:(mgr,o), d:(mgr,o)},
|
|
||||||
initial=True)
|
|
||||||
|
|
||||||
class WorkflowOwner:
|
|
||||||
'''One-state workflow allowing only manager and owner to consult and
|
|
||||||
edit.'''
|
|
||||||
mgr = 'Manager'
|
|
||||||
o = 'Owner'
|
|
||||||
active = State({r:(mgr, o), w:(mgr, o), d:mgr}, initial=True)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
class Model: pass
|
class Model: pass
|
||||||
class Tool(Model):
|
class Tool(Model):
|
||||||
|
|
|
@ -429,16 +429,6 @@ class ToolClassDescriptor(ClassDescriptor):
|
||||||
multiplicity=(1,None), default=('odt',), **pg)
|
multiplicity=(1,None), default=('odt',), **pg)
|
||||||
self.addField(fieldName, fieldType)
|
self.addField(fieldName, fieldType)
|
||||||
|
|
||||||
def addQueryResultColumns(self, classDescr):
|
|
||||||
'''Adds, for class p_classDescr, the attribute in the tool that allows
|
|
||||||
to select what default columns will be shown on query results.'''
|
|
||||||
className = classDescr.name
|
|
||||||
fieldName = 'resultColumnsFor%s' % className
|
|
||||||
fieldType = gen.String(multiplicity=(0,None), validator=gen.Selection(
|
|
||||||
'_appy_getAllFields*%s' % className), page='userInterface',
|
|
||||||
group=classDescr.klass.__name__, default=['title'])
|
|
||||||
self.addField(fieldName, fieldType)
|
|
||||||
|
|
||||||
def addSearchRelatedFields(self, classDescr):
|
def addSearchRelatedFields(self, classDescr):
|
||||||
'''Adds, for class p_classDescr, attributes related to the search
|
'''Adds, for class p_classDescr, attributes related to the search
|
||||||
functionality for class p_classDescr.'''
|
functionality for class p_classDescr.'''
|
||||||
|
|
|
@ -712,12 +712,10 @@ class ZopeGenerator(Generator):
|
||||||
'classDoc': 'Standard Appy class', 'icon': icon})
|
'classDoc': 'Standard Appy class', 'icon': icon})
|
||||||
self.copyFile('Class.pyt', repls, destName='%s.py' % klass.name)
|
self.copyFile('Class.pyt', repls, destName='%s.py' % klass.name)
|
||||||
|
|
||||||
# Before generating the Tool class, finalize it with query result
|
# Before generating the Tool class, finalize it with search-related and
|
||||||
# columns, search-related and import-related fields.
|
# import-related fields.
|
||||||
for classDescr in self.getClasses(include='allButTool'):
|
for classDescr in self.getClasses(include='allButTool'):
|
||||||
if not classDescr.isRoot(): continue
|
if not classDescr.isRoot(): continue
|
||||||
# We must be able to configure query results from the tool.
|
|
||||||
self.tool.addQueryResultColumns(classDescr)
|
|
||||||
# Add the search-related fields.
|
# Add the search-related fields.
|
||||||
self.tool.addSearchRelatedFields(classDescr)
|
self.tool.addSearchRelatedFields(classDescr)
|
||||||
importMean = classDescr.getCreateMean('Import')
|
importMean = classDescr.getCreateMean('Import')
|
||||||
|
|
|
@ -293,8 +293,7 @@ class ZopeInstaller:
|
||||||
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
|
||||||
state = gen.String(validator=gen.Selection('_appy_listStates'),
|
state = gen.String(show='result')
|
||||||
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]]
|
||||||
|
|
|
@ -203,15 +203,6 @@ class ToolMixin(BaseMixin):
|
||||||
cfg = self.getProductConfig()
|
cfg = self.getProductConfig()
|
||||||
return [self.getAppyClass(k) for k in cfg.rootClasses]
|
return [self.getAppyClass(k) for k in cfg.rootClasses]
|
||||||
|
|
||||||
def _appy_getAllFields(self, className):
|
|
||||||
'''Returns the (translated) names of fields of p_className.'''
|
|
||||||
res = []
|
|
||||||
for field in self.getAllAppyTypes(className=className):
|
|
||||||
res.append((className.name, self.translate(className.labelId)))
|
|
||||||
# Add object state
|
|
||||||
res.append(('state', self.translate('workflow_state')))
|
|
||||||
return res
|
|
||||||
|
|
||||||
def _appy_getSearchableFields(self, className):
|
def _appy_getSearchableFields(self, className):
|
||||||
'''Returns the (translated) names of fields that may be searched on
|
'''Returns the (translated) names of fields that may be searched on
|
||||||
objects of type p_className (=indexed fields).'''
|
objects of type p_className (=indexed fields).'''
|
||||||
|
@ -438,8 +429,8 @@ class ToolMixin(BaseMixin):
|
||||||
if refInfo[0]:
|
if refInfo[0]:
|
||||||
return refInfo[0].getAppyType(refInfo[1]).shownInfo
|
return refInfo[0].getAppyType(refInfo[1]).shownInfo
|
||||||
else:
|
else:
|
||||||
toolFieldName = 'resultColumnsFor%s' % className
|
k = self.getAppyClass(className)
|
||||||
return getattr(self.appy(), toolFieldName)
|
return hasattr(k, 'listColumns') and k.listColumns or ('title',)
|
||||||
|
|
||||||
def truncateValue(self, value, width=15):
|
def truncateValue(self, value, width=15):
|
||||||
'''Truncates the p_value according to p_width.'''
|
'''Truncates the p_value according to p_width.'''
|
||||||
|
@ -469,7 +460,7 @@ class ToolMixin(BaseMixin):
|
||||||
def quote(self, s):
|
def quote(self, s):
|
||||||
'''Returns the quoted version of p_s.'''
|
'''Returns the quoted version of p_s.'''
|
||||||
if not isinstance(s, basestring): s = str(s)
|
if not isinstance(s, basestring): s = str(s)
|
||||||
if "'" in s: return '"%s"' % s
|
s = s.replace('\r\n', '').replace('\n', '').replace("'", "\\'")
|
||||||
return "'%s'" % s
|
return "'%s'" % s
|
||||||
|
|
||||||
def getLayoutType(self):
|
def getLayoutType(self):
|
||||||
|
|
|
@ -786,7 +786,7 @@ class BaseMixin:
|
||||||
return klass.styles[elem]
|
return klass.styles[elem]
|
||||||
return elem
|
return elem
|
||||||
|
|
||||||
def getAppyTransitions(self, includeFake=True, includeNotShowable=False):
|
def getTransitions(self, includeFake=True, includeNotShowable=False):
|
||||||
'''This method returns info about transitions that one can trigger from
|
'''This method returns info about transitions that one can trigger from
|
||||||
the user interface.
|
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
|
||||||
|
@ -824,7 +824,7 @@ class BaseMixin:
|
||||||
'confirm': '', 'may_trigger': True}
|
'confirm': '', 'may_trigger': True}
|
||||||
if transition.confirm:
|
if transition.confirm:
|
||||||
cLabel = '%s_confirm' % label
|
cLabel = '%s_confirm' % label
|
||||||
tInfo['confirm'] = self.translate(cLabel, format='js')
|
tInfo['confirm'] = self.translate(cLabel)
|
||||||
if not mayTrigger:
|
if not mayTrigger:
|
||||||
tInfo['may_trigger'] = False
|
tInfo['may_trigger'] = False
|
||||||
tInfo['reason'] = mayTrigger.msg
|
tInfo['reason'] = mayTrigger.msg
|
||||||
|
@ -1167,7 +1167,9 @@ class BaseMixin:
|
||||||
'''Gets, according to the workflow, the roles that are currently granted
|
'''Gets, according to the workflow, the roles that are currently granted
|
||||||
p_permission on this object.'''
|
p_permission on this object.'''
|
||||||
state = self.State(name=False)
|
state = self.State(name=False)
|
||||||
return [role.name for role in state.permissions[permission]]
|
roles = state.permissions[permission]
|
||||||
|
if roles: return [role.name for role in roles]
|
||||||
|
return ()
|
||||||
|
|
||||||
def appy(self):
|
def appy(self):
|
||||||
'''Returns a wrapper object allowing to manipulate p_self the Appy
|
'''Returns a wrapper object allowing to manipulate p_self the Appy
|
||||||
|
@ -1290,15 +1292,6 @@ class BaseMixin:
|
||||||
if isinstance(showValue, basestring): return layoutType == showValue
|
if isinstance(showValue, basestring): return layoutType == showValue
|
||||||
return layoutType in showValue
|
return layoutType in showValue
|
||||||
|
|
||||||
def _appy_listStates(self):
|
|
||||||
'''Lists the possible states for this object.'''
|
|
||||||
res = []
|
|
||||||
workflow = self.getWorkflow()
|
|
||||||
for elem in dir(workflow):
|
|
||||||
if getattr(workflow, elem).__class__.__name__ != 'State': continue
|
|
||||||
res.append((elem, self.translate(self.getWorkflowLabel(elem))))
|
|
||||||
return res
|
|
||||||
|
|
||||||
getUrlDefaults = {'page':True, 'nav':True}
|
getUrlDefaults = {'page':True, 'nav':True}
|
||||||
def getUrl(self, base=None, mode='view', **kwargs):
|
def getUrl(self, base=None, mode='view', **kwargs):
|
||||||
'''Returns an URL for this object.
|
'''Returns an URL for this object.
|
||||||
|
@ -1423,9 +1416,6 @@ class BaseMixin:
|
||||||
if 'html' in format:
|
if 'html' in format:
|
||||||
if format == 'html_from_text': text = cgi.escape(text)
|
if format == 'html_from_text': text = cgi.escape(text)
|
||||||
res = text.replace('\r\n', '<br/>').replace('\n', '<br/>')
|
res = text.replace('\r\n', '<br/>').replace('\n', '<br/>')
|
||||||
elif format == 'js':
|
|
||||||
res = text.replace('\r\n', '').replace('\n', '')
|
|
||||||
res = res.replace("'", "\\'")
|
|
||||||
elif format == 'text':
|
elif format == 'text':
|
||||||
res = text.replace('<br/>', '\n')
|
res = text.replace('<br/>', '\n')
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -214,8 +214,8 @@ setattr(Page, Page.pages.back.attribute, Page.pages.back)
|
||||||
|
|
||||||
# The Tool class ---------------------------------------------------------------
|
# The Tool class ---------------------------------------------------------------
|
||||||
# Prefixes of the fields generated on the Tool.
|
# Prefixes of the fields generated on the Tool.
|
||||||
toolFieldPrefixes = ('podTemplate', 'formats', 'resultColumns',
|
toolFieldPrefixes = ('podTemplate', 'formats', 'numberOfSearchColumns',
|
||||||
'numberOfSearchColumns', 'searchFields')
|
'searchFields')
|
||||||
defaultToolFields = ('title', 'mailHost', 'mailEnabled', 'mailFrom',
|
defaultToolFields = ('title', 'mailHost', 'mailEnabled', 'mailFrom',
|
||||||
'appyVersion', 'dateFormat', 'hourFormat', 'users',
|
'appyVersion', 'dateFormat', 'hourFormat', 'users',
|
||||||
'connectedUsers', 'groups', 'translations',
|
'connectedUsers', 'groups', 'translations',
|
||||||
|
|
|
@ -221,7 +221,6 @@ CONFIG = "Configuration panel for product '%s'"
|
||||||
# to MSG_<attributePrefix>).
|
# to MSG_<attributePrefix>).
|
||||||
MSG_podTemplate = "POD template for field '%s'"
|
MSG_podTemplate = "POD template for field '%s'"
|
||||||
MSG_formats = "Output format(s) for field '%s'"
|
MSG_formats = "Output format(s) for field '%s'"
|
||||||
MSG_resultColumns = "Columns to display while showing query results"
|
|
||||||
MSG_numberOfSearchColumns = "Number of search columns"
|
MSG_numberOfSearchColumns = "Number of search columns"
|
||||||
MSG_searchFields = "Search fields"
|
MSG_searchFields = "Search fields"
|
||||||
POD_ASKACTION = 'Trigger related action'
|
POD_ASKACTION = 'Trigger related action'
|
||||||
|
|
|
@ -335,18 +335,28 @@ class ToolWrapper(AbstractWrapper):
|
||||||
style=":showSubTitles and 'display:inline' or 'display:none'"
|
style=":showSubTitles and 'display:inline' or 'display:none'"
|
||||||
name="subTitle">::zobj.getSubTitle()</span>
|
name="subTitle">::zobj.getSubTitle()</span>
|
||||||
|
|
||||||
<!-- Actions: edit, delete -->
|
<!-- Actions -->
|
||||||
<div if="zobj.mayAct()">
|
<table class="noStyle" if="zobj.mayAct()">
|
||||||
<a if="zobj.mayEdit()"
|
<tr>
|
||||||
var2="navInfo='search.%s.%s.%d.%d' % \
|
<!-- Workflow transitions -->
|
||||||
|
<td if="zobj.showTransitions('result')"
|
||||||
|
var2="targetObj=zobj">:targetObj.appy().pxTransitions</td>
|
||||||
|
<!-- Edit -->
|
||||||
|
<td if="zobj.mayEdit()">
|
||||||
|
<a var="navInfo='search.%s.%s.%d.%d' % \
|
||||||
(className, searchName, loop.zobj.nb+1+startNumber, totalNumber)"
|
(className, searchName, loop.zobj.nb+1+startNumber, totalNumber)"
|
||||||
href=":zobj.getUrl(mode='edit', page=zobj.getDefaultEditPage(), \
|
href=":zobj.getUrl(mode='edit', page=zobj.getDefaultEditPage(), \
|
||||||
nav=navInfo)">
|
nav=navInfo)">
|
||||||
<img src=":url('edit')" title=":_('object_edit')"/></a>
|
<img src=":url('edit')" title=":_('object_edit')"/></a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<!-- Delete -->
|
||||||
<img if="zobj.mayDelete()" class="clickable" src=":url('delete')"
|
<img if="zobj.mayDelete()" class="clickable" src=":url('delete')"
|
||||||
title=":_('object_delete')"
|
title=":_('object_delete')"
|
||||||
onClick=":'onDeleteObject(%s)' % q(zobj.UID())"/>
|
onClick=":'onDeleteObject(%s)' % q(zobj.UID())"/>
|
||||||
</div>
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</x>
|
</x>
|
||||||
<!-- Any other field -->
|
<!-- Any other field -->
|
||||||
<x if="field.name != 'title'">
|
<x if="field.name != 'title'">
|
||||||
|
@ -703,12 +713,10 @@ class ToolWrapper(AbstractWrapper):
|
||||||
return self.o.getAppyClass(zopeName)
|
return self.o.getAppyClass(zopeName)
|
||||||
|
|
||||||
def getAttributeName(self, attributeType, klass, attrName=None):
|
def getAttributeName(self, attributeType, klass, attrName=None):
|
||||||
'''Some names of Tool attributes are not easy to guess. For example,
|
'''Some names of Tool attributes are not easy to guess. This method
|
||||||
the attribute that stores the names of the columns to display in
|
generates the attribute name based on p_attributeType, a p_klass from
|
||||||
query results for class A that is in package x.y is
|
the application, and a p_attrName (given only if needed).
|
||||||
"tool.resultColumnsForx_y_A". This method generates the attribute
|
p_attributeType may be:
|
||||||
name based on p_attributeType, a p_klass from the application, and a
|
|
||||||
p_attrName (given only if needed). p_attributeType may be:
|
|
||||||
|
|
||||||
"podTemplate"
|
"podTemplate"
|
||||||
Stores the pod template for p_attrName.
|
Stores the pod template for p_attrName.
|
||||||
|
@ -717,10 +725,6 @@ class ToolWrapper(AbstractWrapper):
|
||||||
Stores the output format(s) of a given pod template for
|
Stores the output format(s) of a given pod template for
|
||||||
p_attrName.
|
p_attrName.
|
||||||
|
|
||||||
"resultColumns"
|
|
||||||
Stores the list of columns that must be shown when displaying
|
|
||||||
instances of a given root p_klass.
|
|
||||||
|
|
||||||
"numberOfSearchColumns"
|
"numberOfSearchColumns"
|
||||||
Determines in how many columns the search screen for p_klass
|
Determines in how many columns the search screen for p_klass
|
||||||
is rendered.
|
is rendered.
|
||||||
|
|
|
@ -354,7 +354,7 @@ class AbstractWrapper(object):
|
||||||
|
|
||||||
# Displays an object's transitions(s).
|
# Displays an object's transitions(s).
|
||||||
pxTransitions = Px('''
|
pxTransitions = Px('''
|
||||||
<form var="transitions=targetObj.getAppyTransitions()" 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"/>
|
||||||
|
@ -480,7 +480,8 @@ class AbstractWrapper(object):
|
||||||
|
|
||||||
<td if="not isEdit"
|
<td if="not isEdit"
|
||||||
var2="locked=zobj.isLocked(user, page);
|
var2="locked=zobj.isLocked(user, page);
|
||||||
editable=pageInfo.showOnEdit and zobj.mayEdit()">
|
editable=pageInfo.showOnEdit and pageInfo.showEdit and \
|
||||||
|
zobj.mayEdit()">
|
||||||
|
|
||||||
<!-- Edit -->
|
<!-- Edit -->
|
||||||
<input type="button" class="button" if="editable and not locked"
|
<input type="button" class="button" if="editable and not locked"
|
||||||
|
|
Loading…
Reference in a new issue