diff --git a/fields/calendar.py b/fields/calendar.py index 3159a6f..c54c82c 100644 --- a/fields/calendar.py +++ b/fields/calendar.py @@ -401,11 +401,7 @@ class Calendar(Field): res = Object(eventTypes=eventTypes, message=message) if forBrowser: res.eventTypes = ','.join(res.eventTypes) - if not res.message: - res.message = '' - else: - res.message = obj.formatText(res.message, format='js') - return res.__dict__ + if not res.message: res.message = '' return res def getEventsAt(self, obj, date): diff --git a/fields/date.py b/fields/date.py index b09db01..2219f2a 100644 --- a/fields/date.py +++ b/fields/date.py @@ -49,7 +49,7 @@ class Date(Field): @@ -57,7 +57,7 @@ class Date(Field): - + @@ -112,7 +112,7 @@ class Date(Field): - @@ -145,7 +145,7 @@ class Date(Field): - diff --git a/fields/phase.py b/fields/phase.py index acb1fb9..55dccd8 100644 --- a/fields/phase.py +++ b/fields/phase.py @@ -31,7 +31,7 @@ class Phase: if="not singlePhase and not singlePage">::_(label) - +
::_('%s_page_%s' % \ (zobj.meta_type, aPage)) + editable=mayEdit and aPageInfo.showOnEdit and \ + aPageInfo.showEdit"> @@ -57,7 +58,7 @@ class Phase:
- +
:link.title
diff --git a/fields/workflow.py b/fields/workflow.py new file mode 100644 index 0000000..a175c42 --- /dev/null +++ b/fields/workflow.py @@ -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 . +# ------------------------------------------------------------------------------ +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) +# ------------------------------------------------------------------------------ diff --git a/gen/__init__.py b/gen/__init__.py index 34760c3..1f28963 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -1,12 +1,24 @@ # ------------------------------------------------------------------------------ -import types, string -from appy.gen.mail import sendNotification -from appy.gen import utils as gutils +# 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 . # ------------------------------------------------------------------------------ # 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. -# ------------------------------------------------------------------------------ +from appy import Object +from appy.px import Px from appy.fields import Field from appy.fields.action import Action 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.page import Page from appy.fields.phase import Phase +from appy.fields.workflow import * from appy.gen.layout import Table -from appy.px import Px -from appy import Object -No = gutils.No - -# Default Appy permissions ----------------------------------------------------- -r, w, d = ('read', 'write', 'delete') +from appy.gen.utils import No class Import: '''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. 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 Tool(Model): diff --git a/gen/descriptors.py b/gen/descriptors.py index dadc509..da86f65 100644 --- a/gen/descriptors.py +++ b/gen/descriptors.py @@ -429,16 +429,6 @@ class ToolClassDescriptor(ClassDescriptor): multiplicity=(1,None), default=('odt',), **pg) 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): '''Adds, for class p_classDescr, attributes related to the search functionality for class p_classDescr.''' diff --git a/gen/generator.py b/gen/generator.py index c34606a..8e2f238 100644 --- a/gen/generator.py +++ b/gen/generator.py @@ -712,12 +712,10 @@ class ZopeGenerator(Generator): 'classDoc': 'Standard Appy class', 'icon': icon}) self.copyFile('Class.pyt', repls, destName='%s.py' % klass.name) - # Before generating the Tool class, finalize it with query result - # columns, search-related and import-related fields. + # Before generating the Tool class, finalize it with search-related and + # import-related fields. for classDescr in self.getClasses(include='allButTool'): 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. self.tool.addSearchRelatedFields(classDescr) importMean = classDescr.getCreateMean('Import') diff --git a/gen/installer.py b/gen/installer.py index 8737cdc..7ac2d2d 100644 --- a/gen/installer.py +++ b/gen/installer.py @@ -293,8 +293,7 @@ class ZopeInstaller: title.init('title', None, 'appy') setattr(wrapperClass, 'title', title) # Special field "state" must be added for every class - state = gen.String(validator=gen.Selection('_appy_listStates'), - show='result') + state = gen.String(show='result') state.init('state', None, 'workflow') setattr(wrapperClass, 'state', state) names = self.config.attributes[wrapperClass.__name__[:-8]] diff --git a/gen/mixins/ToolMixin.py b/gen/mixins/ToolMixin.py index ef0694c..f63c06e 100644 --- a/gen/mixins/ToolMixin.py +++ b/gen/mixins/ToolMixin.py @@ -203,15 +203,6 @@ class ToolMixin(BaseMixin): cfg = self.getProductConfig() 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): '''Returns the (translated) names of fields that may be searched on objects of type p_className (=indexed fields).''' @@ -438,8 +429,8 @@ class ToolMixin(BaseMixin): if refInfo[0]: return refInfo[0].getAppyType(refInfo[1]).shownInfo else: - toolFieldName = 'resultColumnsFor%s' % className - return getattr(self.appy(), toolFieldName) + k = self.getAppyClass(className) + return hasattr(k, 'listColumns') and k.listColumns or ('title',) def truncateValue(self, value, width=15): '''Truncates the p_value according to p_width.''' @@ -469,7 +460,7 @@ class ToolMixin(BaseMixin): def quote(self, s): '''Returns the quoted version of p_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 def getLayoutType(self): diff --git a/gen/mixins/__init__.py b/gen/mixins/__init__.py index 2e2fbff..a0b07cc 100644 --- a/gen/mixins/__init__.py +++ b/gen/mixins/__init__.py @@ -786,7 +786,7 @@ class BaseMixin: return klass.styles[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 the user interface. * if p_includeFake is True, it retrieves transitions that the user @@ -824,7 +824,7 @@ class BaseMixin: 'confirm': '', 'may_trigger': True} if transition.confirm: cLabel = '%s_confirm' % label - tInfo['confirm'] = self.translate(cLabel, format='js') + tInfo['confirm'] = self.translate(cLabel) if not mayTrigger: tInfo['may_trigger'] = False tInfo['reason'] = mayTrigger.msg @@ -1167,7 +1167,9 @@ class BaseMixin: '''Gets, according to the workflow, the roles that are currently granted p_permission on this object.''' 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): '''Returns a wrapper object allowing to manipulate p_self the Appy @@ -1290,15 +1292,6 @@ class BaseMixin: if isinstance(showValue, basestring): return layoutType == 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} def getUrl(self, base=None, mode='view', **kwargs): '''Returns an URL for this object. @@ -1423,9 +1416,6 @@ class BaseMixin: if 'html' in format: if format == 'html_from_text': text = cgi.escape(text) res = text.replace('\r\n', '
').replace('\n', '
') - elif format == 'js': - res = text.replace('\r\n', '').replace('\n', '') - res = res.replace("'", "\\'") elif format == 'text': res = text.replace('
', '\n') else: diff --git a/gen/model.py b/gen/model.py index a46a0dd..608e65e 100644 --- a/gen/model.py +++ b/gen/model.py @@ -214,8 +214,8 @@ setattr(Page, Page.pages.back.attribute, Page.pages.back) # The Tool class --------------------------------------------------------------- # Prefixes of the fields generated on the Tool. -toolFieldPrefixes = ('podTemplate', 'formats', 'resultColumns', - 'numberOfSearchColumns', 'searchFields') +toolFieldPrefixes = ('podTemplate', 'formats', 'numberOfSearchColumns', + 'searchFields') defaultToolFields = ('title', 'mailHost', 'mailEnabled', 'mailFrom', 'appyVersion', 'dateFormat', 'hourFormat', 'users', 'connectedUsers', 'groups', 'translations', diff --git a/gen/po.py b/gen/po.py index ad7d663..503e782 100644 --- a/gen/po.py +++ b/gen/po.py @@ -221,7 +221,6 @@ CONFIG = "Configuration panel for product '%s'" # to MSG_). MSG_podTemplate = "POD template 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_searchFields = "Search fields" POD_ASKACTION = 'Trigger related action' diff --git a/gen/wrappers/ToolWrapper.py b/gen/wrappers/ToolWrapper.py index 26ad711..83fb911 100644 --- a/gen/wrappers/ToolWrapper.py +++ b/gen/wrappers/ToolWrapper.py @@ -335,18 +335,28 @@ class ToolWrapper(AbstractWrapper): style=":showSubTitles and 'display:inline' or 'display:none'" name="subTitle">::zobj.getSubTitle() - - + href=":zobj.getUrl(mode='edit', page=zobj.getDefaultEditPage(), \ + nav=navInfo)"> + + + + + + + +
@@ -703,12 +713,10 @@ class ToolWrapper(AbstractWrapper): return self.o.getAppyClass(zopeName) def getAttributeName(self, attributeType, klass, attrName=None): - '''Some names of Tool attributes are not easy to guess. For example, - the attribute that stores the names of the columns to display in - query results for class A that is in package x.y is - "tool.resultColumnsForx_y_A". This method generates the attribute - name based on p_attributeType, a p_klass from the application, and a - p_attrName (given only if needed). p_attributeType may be: + '''Some names of Tool attributes are not easy to guess. This method + generates the attribute name based on p_attributeType, a p_klass from + the application, and a p_attrName (given only if needed). + p_attributeType may be: "podTemplate" 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 p_attrName. - "resultColumns" - Stores the list of columns that must be shown when displaying - instances of a given root p_klass. - "numberOfSearchColumns" Determines in how many columns the search screen for p_klass is rendered. diff --git a/gen/wrappers/__init__.py b/gen/wrappers/__init__.py index bc6582b..178b377 100644 --- a/gen/wrappers/__init__.py +++ b/gen/wrappers/__init__.py @@ -354,7 +354,7 @@ class AbstractWrapper(object): # Displays an object's transitions(s). pxTransitions = Px(''' -
@@ -480,7 +480,8 @@ class AbstractWrapper(object): + editable=pageInfo.showOnEdit and pageInfo.showEdit and \ + zobj.mayEdit()">