2009-06-29 07:06:01 -05:00
|
|
|
# ------------------------------------------------------------------------------
|
2013-07-08 16:39:16 -05:00
|
|
|
import types, string
|
2012-05-03 03:51:54 -05:00
|
|
|
from appy.gen.mail import sendNotification
|
2013-07-08 16:39:16 -05:00
|
|
|
from appy.gen.indexer import defaultIndexes
|
2013-06-09 17:13:29 -05:00
|
|
|
from appy.gen import utils as gutils
|
|
|
|
from appy.shared import utils as sutils
|
2009-06-29 07:06:01 -05:00
|
|
|
|
2013-07-08 16:39:16 -05:00
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
# 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.fields import Page, Group, Field, Column, No
|
|
|
|
from appy.fields.action import Action
|
|
|
|
from appy.fields.boolean import Boolean
|
|
|
|
from appy.fields.computed import Computed
|
|
|
|
from appy.fields.date import Date
|
|
|
|
from appy.fields.file import File
|
|
|
|
from appy.fields.float import Float
|
|
|
|
from appy.fields.info import Info
|
|
|
|
from appy.fields.integer import Integer
|
|
|
|
from appy.fields.list import List
|
|
|
|
from appy.fields.pod import Pod
|
|
|
|
from appy.fields.ref import Ref, autoref
|
|
|
|
from appy.fields.string import String, Selection
|
|
|
|
from appy.gen.layout import Table
|
|
|
|
from appy import Object
|
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
# Default Appy permissions -----------------------------------------------------
|
|
|
|
r, w, d = ('read', 'write', 'delete')
|
2010-08-05 11:23:17 -05:00
|
|
|
|
2009-10-20 09:57:00 -05:00
|
|
|
class Import:
|
|
|
|
'''Used for describing the place where to find the data to use for creating
|
|
|
|
an object.'''
|
2010-01-06 11:36:16 -06:00
|
|
|
def __init__(self, path, onElement=None, headers=(), sort=None):
|
2009-10-20 09:57:00 -05:00
|
|
|
self.id = 'import'
|
|
|
|
self.path = path
|
2010-01-06 11:36:16 -06:00
|
|
|
# p_onElement hereafter must be a function (or a static method) that
|
|
|
|
# will be called every time an element to import is found. It takes a
|
|
|
|
# single arg that is the absolute filen name of the file to import,
|
|
|
|
# within p_path. It must return a list of info about the element, or
|
|
|
|
# None if the element must be ignored. The list will be used to display
|
|
|
|
# information about the element in a tabular form.
|
|
|
|
self.onElement = onElement
|
|
|
|
# The following attribute must contain the names of the column headers
|
|
|
|
# of the table that will display elements to import (retrieved from
|
|
|
|
# calls to self.onElement). Every not-None element retrieved from
|
|
|
|
# self.onElement must have the same length as self.headers.
|
|
|
|
self.headers = headers
|
|
|
|
# The following attribute must store a function or static method that
|
|
|
|
# will be used to sort elements to import. It will be called with a
|
|
|
|
# single param containing the list of all not-None elements as retrieved
|
|
|
|
# by calls to self.onElement (but with one additional first element in
|
|
|
|
# every list, which is the absolute file name of the element to import)
|
|
|
|
# and must return a similar, sorted, list.
|
|
|
|
self.sort = sort
|
2009-10-20 09:57:00 -05:00
|
|
|
|
2009-10-30 15:31:39 -05:00
|
|
|
class Search:
|
2013-07-08 16:39:16 -05:00
|
|
|
'''Used for specifying a search for a given class.'''
|
2012-05-25 07:27:53 -05:00
|
|
|
def __init__(self, name, group=None, sortBy='', sortOrder='asc', limit=None,
|
2013-06-07 09:37:00 -05:00
|
|
|
default=False, colspan=1, translated=None, show=True,
|
2012-11-14 10:40:52 -06:00
|
|
|
translatedDescr=None, **fields):
|
2009-10-30 15:31:39 -05:00
|
|
|
self.name = name
|
2012-11-06 04:32:39 -06:00
|
|
|
# Searches may be visually grouped in the portlet.
|
|
|
|
self.group = Group.get(group)
|
2009-10-30 15:31:39 -05:00
|
|
|
self.sortBy = sortBy
|
2012-05-25 07:27:53 -05:00
|
|
|
self.sortOrder = sortOrder
|
2009-10-30 15:31:39 -05:00
|
|
|
self.limit = limit
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
# If this search is the default one, it will be triggered by clicking
|
|
|
|
# on main link.
|
|
|
|
self.default = default
|
2012-11-14 04:36:48 -06:00
|
|
|
self.colspan = colspan
|
2012-11-14 10:40:52 -06:00
|
|
|
# If a translated name or description is already given here, we will
|
|
|
|
# use it instead of trying to translate from labels.
|
|
|
|
self.translated = translated
|
|
|
|
self.translatedDescr = translatedDescr
|
2013-06-07 09:37:00 -05:00
|
|
|
# Condition for showing or not this search
|
|
|
|
self.show = show
|
2013-04-30 04:12:02 -05:00
|
|
|
# In the dict below, keys are indexed field names or names of standard
|
|
|
|
# indexes, and values are search values.
|
2010-12-06 04:11:40 -06:00
|
|
|
self.fields = fields
|
2013-02-18 08:03:26 -06:00
|
|
|
|
2010-04-30 05:05:29 -05:00
|
|
|
@staticmethod
|
|
|
|
def getIndexName(fieldName, usage='search'):
|
|
|
|
'''Gets the name of the technical index that corresponds to field named
|
|
|
|
p_fieldName. Indexes can be used for searching (p_usage="search") or
|
|
|
|
for sorting (usage="sort"). The method returns None if the field
|
|
|
|
named p_fieldName can't be used for p_usage.'''
|
|
|
|
if fieldName == 'title':
|
|
|
|
if usage == 'search': return 'Title'
|
2011-11-25 11:01:20 -06:00
|
|
|
else: return 'SortableTitle'
|
|
|
|
# Indeed, for field 'title', Appy has a specific index
|
2012-09-26 16:13:02 -05:00
|
|
|
# 'SortableTitle', because index 'Title' is a TextIndex
|
2010-04-30 05:05:29 -05:00
|
|
|
# (for searchability) and can't be used for sorting.
|
2012-04-25 09:21:23 -05:00
|
|
|
elif fieldName == 'state': return 'State'
|
2013-06-07 09:37:00 -05:00
|
|
|
elif fieldName == 'created': return 'Created'
|
|
|
|
elif fieldName == 'modified': return 'Modified'
|
2012-09-19 04:29:29 -05:00
|
|
|
elif fieldName in defaultIndexes: return fieldName
|
2010-04-30 05:05:29 -05:00
|
|
|
else:
|
|
|
|
return 'get%s%s'% (fieldName[0].upper(),fieldName[1:])
|
2013-02-18 08:03:26 -06:00
|
|
|
|
2010-04-30 05:05:29 -05:00
|
|
|
@staticmethod
|
2013-04-30 04:12:02 -05:00
|
|
|
def getSearchValue(fieldName, fieldValue, klass):
|
2010-04-30 05:05:29 -05:00
|
|
|
'''Returns a transformed p_fieldValue for producing a valid search
|
|
|
|
value as required for searching in the index corresponding to
|
|
|
|
p_fieldName.'''
|
2013-04-30 04:12:02 -05:00
|
|
|
field = getattr(klass, fieldName, None)
|
|
|
|
if (field and (field.getIndexType() == 'TextIndex')) or \
|
|
|
|
(fieldName == 'SearchableText'):
|
|
|
|
# For TextIndex indexes. We must split p_fieldValue into keywords.
|
2013-06-09 17:13:29 -05:00
|
|
|
res = gutils.Keywords(fieldValue).get()
|
2010-04-30 05:05:29 -05:00
|
|
|
elif isinstance(fieldValue, basestring) and fieldValue.endswith('*'):
|
|
|
|
v = fieldValue[:-1]
|
|
|
|
# Warning: 'z' is higher than 'Z'!
|
|
|
|
res = {'query':(v,v+'z'), 'range':'min:max'}
|
2013-06-09 17:13:29 -05:00
|
|
|
elif type(fieldValue) in sutils.sequenceTypes:
|
2010-04-30 05:05:29 -05:00
|
|
|
if fieldValue and isinstance(fieldValue[0], basestring):
|
|
|
|
# We have a list of string values (ie: we need to
|
|
|
|
# search v1 or v2 or...)
|
|
|
|
res = fieldValue
|
|
|
|
else:
|
|
|
|
# We have a range of (int, float, DateTime...) values
|
|
|
|
minv, maxv = fieldValue
|
|
|
|
rangev = 'minmax'
|
|
|
|
queryv = fieldValue
|
|
|
|
if minv == None:
|
|
|
|
rangev = 'max'
|
|
|
|
queryv = maxv
|
|
|
|
elif maxv == None:
|
|
|
|
rangev = 'min'
|
|
|
|
queryv = minv
|
|
|
|
res = {'query':queryv, 'range':rangev}
|
|
|
|
else:
|
|
|
|
res = fieldValue
|
|
|
|
return res
|
2009-10-30 15:31:39 -05:00
|
|
|
|
2013-04-30 04:12:02 -05:00
|
|
|
def updateSearchCriteria(self, criteria, klass, advanced=False):
|
2013-02-18 08:03:26 -06:00
|
|
|
'''This method updates dict p_criteria with all the search criteria
|
|
|
|
corresponding to this Search instance. If p_advanced is True,
|
|
|
|
p_criteria correspond to an advanced search, to be stored in the
|
|
|
|
session: in this case we need to keep the Appy names for parameters
|
|
|
|
sortBy and sortOrder (and not "resolve" them to Zope's sort_on and
|
|
|
|
sort_order).'''
|
|
|
|
# Put search criteria in p_criteria
|
|
|
|
for fieldName, fieldValue in self.fields.iteritems():
|
|
|
|
# Management of searches restricted to objects linked through a
|
|
|
|
# Ref field: not implemented yet.
|
|
|
|
if fieldName == '_ref': continue
|
|
|
|
# Make the correspondence between the name of the field and the
|
|
|
|
# name of the corresponding index, excepted if advanced is True: in
|
|
|
|
# that case, the correspondence will be done later.
|
|
|
|
if not advanced:
|
|
|
|
attrName = Search.getIndexName(fieldName)
|
|
|
|
# Express the field value in the way needed by the index
|
2013-04-30 04:12:02 -05:00
|
|
|
criteria[attrName] = Search.getSearchValue(fieldName,
|
|
|
|
fieldValue, klass)
|
2013-02-18 08:03:26 -06:00
|
|
|
else:
|
|
|
|
criteria[fieldName]= fieldValue
|
|
|
|
# Add a sort order if specified
|
|
|
|
if self.sortBy:
|
|
|
|
if not advanced:
|
|
|
|
criteria['sort_on'] = Search.getIndexName(self.sortBy,
|
|
|
|
usage='sort')
|
|
|
|
if self.sortOrder == 'desc': criteria['sort_order'] = 'reverse'
|
|
|
|
else: criteria['sort_order'] = None
|
|
|
|
else:
|
|
|
|
criteria['sortBy'] = self.sortBy
|
|
|
|
criteria['sortOrder'] = self.sortOrder
|
|
|
|
|
2013-06-07 09:37:00 -05:00
|
|
|
def isShowable(self, klass, tool):
|
|
|
|
'''Is this Search instance (defined in p_klass) showable?'''
|
|
|
|
if self.show.__class__.__name__ == 'staticmethod':
|
2013-06-09 17:13:29 -05:00
|
|
|
return gutils.callMethod(tool, self.show, klass=klass)
|
2013-06-07 09:37:00 -05:00
|
|
|
return self.show
|
|
|
|
|
2011-07-26 15:15:04 -05:00
|
|
|
# Workflow-specific types and default workflows --------------------------------
|
|
|
|
appyToZopePermissions = {
|
|
|
|
'read': ('View', 'Access contents information'),
|
|
|
|
'write': 'Modify portal content',
|
|
|
|
'delete': 'Delete objects',
|
|
|
|
}
|
|
|
|
|
2010-09-02 09:16:08 -05:00
|
|
|
class Role:
|
|
|
|
'''Represents a role.'''
|
2011-12-05 08:11:29 -06:00
|
|
|
zopeRoles = ('Manager', 'Owner', 'Anonymous', 'Authenticated')
|
|
|
|
zopeLocalRoles = ('Owner',)
|
|
|
|
zopeUngrantableRoles = ('Anonymous', 'Authenticated')
|
2010-09-02 09:16:08 -05:00
|
|
|
def __init__(self, name, local=False, grantable=True):
|
|
|
|
self.name = name
|
|
|
|
self.local = local # True if it can be used as local role only.
|
2011-12-05 08:11:29 -06:00
|
|
|
# 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):
|
2010-09-02 09:16:08 -05:00
|
|
|
self.local = True
|
|
|
|
self.grantable = grantable
|
2011-12-05 08:11:29 -06:00
|
|
|
if self.zope and (name in self.zopeUngrantableRoles):
|
2010-09-02 09:16:08 -05:00
|
|
|
self.grantable = False
|
|
|
|
# An ungrantable role is one that is, like the Anonymous or
|
|
|
|
# Authenticated roles, automatically attributed to a user.
|
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
class State:
|
2011-10-06 09:27:58 -05:00
|
|
|
def __init__(self, permissions, initial=False, phase=None, show=True):
|
2010-09-02 09:16:08 -05:00
|
|
|
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
|
2009-06-29 07:06:01 -05:00
|
|
|
self.initial = initial
|
|
|
|
self.phase = phase
|
|
|
|
self.show = show
|
2010-09-02 09:16:08 -05:00
|
|
|
# Standardize the way roles are expressed within self.permissions
|
|
|
|
self.standardizeRoles()
|
|
|
|
|
2011-07-26 15:15:04 -05:00
|
|
|
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
|
|
|
|
|
2010-09-02 09:16:08 -05:00
|
|
|
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 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()
|
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
def getPermissions(self):
|
|
|
|
'''If you get the permissions mapping through self.permissions, dict
|
|
|
|
values may be of different types (a list of roles, a single role or
|
|
|
|
None). Iy you call this method, you will always get a list which
|
|
|
|
may be empty.'''
|
|
|
|
res = {}
|
|
|
|
for permission, roleValue in self.permissions.iteritems():
|
|
|
|
if roleValue == None:
|
|
|
|
res[permission] = []
|
|
|
|
elif isinstance(roleValue, basestring):
|
|
|
|
res[permission] = [roleValue]
|
|
|
|
else:
|
|
|
|
res[permission] = roleValue
|
|
|
|
return res
|
|
|
|
|
2011-07-26 15:15:04 -05:00
|
|
|
def updatePermission(self, obj, zopePermission, roleNames):
|
|
|
|
'''Updates, on p_obj, list of p_roleNames which are granted a given
|
2011-09-08 09:33:16 -05:00
|
|
|
p_zopePermission. This method returns True if the list has been
|
|
|
|
effectively updated.'''
|
2011-07-26 15:15:04 -05:00
|
|
|
attr = Permission.getZopeAttrName(zopePermission)
|
|
|
|
if not hasattr(obj.aq_base, attr) or \
|
2011-11-25 11:01:20 -06:00
|
|
|
(getattr(obj.aq_base, attr) != roleNames):
|
2011-07-26 15:15:04 -05:00
|
|
|
setattr(obj, attr, roleNames)
|
2011-09-08 09:33:16 -05:00
|
|
|
return True
|
|
|
|
return False
|
2011-07-26 15:15:04 -05:00
|
|
|
|
|
|
|
def updatePermissions(self, wf, obj):
|
|
|
|
'''Zope requires permission-to-roles mappings to be stored as attributes
|
|
|
|
on the object itself. This method does this job, duplicating the info
|
2011-09-08 09:33:16 -05:00
|
|
|
from this state definition on p_obj. p_res is True if at least one
|
|
|
|
change has been effectively performed.'''
|
|
|
|
res = False
|
2011-07-26 15:15:04 -05:00
|
|
|
for permission, roles in self.getPermissions().iteritems():
|
|
|
|
roleNames = tuple([role.name for role in roles])
|
|
|
|
# Compute Zope permission(s) related to this permission.
|
|
|
|
if appyToZopePermissions.has_key(permission):
|
|
|
|
# It is a standard permission (r, w, d)
|
|
|
|
zopePerm = appyToZopePermissions[permission]
|
|
|
|
elif isinstance(permission, basestring):
|
|
|
|
# It is a user-defined permission
|
|
|
|
zopePerm = permission
|
|
|
|
else:
|
|
|
|
# It is a Permission instance
|
|
|
|
appName = obj.getProductConfig().PROJECTNAME
|
|
|
|
zopePerm = permission.getName(wf, appName)
|
|
|
|
# zopePerm contains a single permission or a tuple of permissions
|
|
|
|
if isinstance(zopePerm, basestring):
|
2011-09-08 09:33:16 -05:00
|
|
|
changed = self.updatePermission(obj, zopePerm, roleNames)
|
|
|
|
res = res or changed
|
2011-07-26 15:15:04 -05:00
|
|
|
else:
|
|
|
|
for zPerm in zopePerm:
|
2011-09-08 09:33:16 -05:00
|
|
|
changed = self.updatePermission(obj, zPerm, roleNames)
|
|
|
|
res = res or changed
|
|
|
|
return res
|
2011-07-26 15:15:04 -05:00
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
class Transition:
|
2009-11-06 04:33:56 -06:00
|
|
|
def __init__(self, states, condition=True, action=None, notify=None,
|
2011-01-28 19:18:14 -06:00
|
|
|
show=True, confirm=False):
|
2009-06-29 07:06:01 -05:00
|
|
|
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
|
2010-09-02 09:16:08 -05:00
|
|
|
if isinstance(condition, basestring):
|
|
|
|
# The condition specifies the name of a role.
|
|
|
|
self.condition = Role(condition)
|
2009-06-29 07:06:01 -05:00
|
|
|
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.
|
2009-11-06 04:33:56 -06:00
|
|
|
self.show = show # If False, the end user will not be able to trigger
|
|
|
|
# the transition. It will only be possible by code.
|
2011-01-28 19:18:14 -06:00
|
|
|
self.confirm = confirm # If True, a confirm popup will show up.
|
2009-06-29 07:06:01 -05:00
|
|
|
|
2011-07-26 15:15:04 -05:00
|
|
|
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
|
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
def getUsedRoles(self):
|
2010-09-02 09:16:08 -05:00
|
|
|
'''self.condition can specify a role.'''
|
2009-06-29 07:06:01 -05:00
|
|
|
res = []
|
2010-09-02 09:16:08 -05:00
|
|
|
if isinstance(self.condition, Role):
|
|
|
|
res.append(self.condition)
|
2009-06-29 07:06:01 -05:00
|
|
|
return res
|
|
|
|
|
|
|
|
def isSingle(self):
|
2010-09-02 09:16:08 -05:00
|
|
|
'''If this transition is only defined between 2 states, returns True.
|
2009-06-29 07:06:01 -05:00
|
|
|
Else, returns False.'''
|
|
|
|
return isinstance(self.states[0], State)
|
|
|
|
|
2011-02-01 04:09:54 -06:00
|
|
|
def isShowable(self, workflow, obj):
|
|
|
|
'''Is this transition showable?'''
|
|
|
|
if callable(self.show):
|
|
|
|
return self.show(workflow, obj.appy())
|
|
|
|
else:
|
|
|
|
return self.show
|
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
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
|
|
|
|
|
2012-07-23 08:24:44 -05:00
|
|
|
def isTriggerable(self, obj, wf, noSecurity=False):
|
2011-07-26 15:15:04 -05:00
|
|
|
'''Can this transition be triggered on p_obj?'''
|
2011-09-09 10:39:58 -05:00
|
|
|
wf = wf.__instance__ # We need the prototypical instance here.
|
2011-07-26 15:15:04 -05:00
|
|
|
# Checks that the current state of the object is a start state for this
|
|
|
|
# transition.
|
2011-11-25 11:01:20 -06:00
|
|
|
objState = obj.State(name=False)
|
2011-07-26 15:15:04 -05:00
|
|
|
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
|
2011-11-25 11:01:20 -06:00
|
|
|
user = obj.getUser()
|
2011-07-26 15:15:04 -05:00
|
|
|
if isinstance(self.condition, Role):
|
|
|
|
# Condition is a role. Transition may be triggered if the user has
|
|
|
|
# this role.
|
2012-07-23 08:24:44 -05:00
|
|
|
if noSecurity: return True
|
2011-07-26 15:15:04 -05:00
|
|
|
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
|
2012-07-23 08:24:44 -05:00
|
|
|
if user.has_role(roleOrFunction, obj) or noSecurity:
|
2011-07-26 15:15:04 -05:00
|
|
|
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 = ''
|
2011-09-09 10:39:58 -05:00
|
|
|
obj = obj.appy()
|
|
|
|
wf = wf.__instance__ # We need the prototypical instance here.
|
2011-07-26 15:15:04 -05:00
|
|
|
if type(self.action) in (tuple, list):
|
|
|
|
# We need to execute a list of actions
|
|
|
|
for act in self.action:
|
2011-09-09 10:39:58 -05:00
|
|
|
msgPart = act(wf, obj)
|
2011-07-26 15:15:04 -05:00
|
|
|
if msgPart: msg += msgPart
|
|
|
|
else: # We execute a single action only.
|
2011-09-09 10:39:58 -05:00
|
|
|
msgPart = self.action(wf, obj)
|
2011-07-26 15:15:04 -05:00
|
|
|
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
|
2012-05-03 03:51:54 -05:00
|
|
|
email notifications that must normally be launched after the
|
2011-07-26 15:15:04 -05:00
|
|
|
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:
|
2011-11-25 11:01:20 -06:00
|
|
|
startState = obj.State(name=False)
|
2011-07-26 15:15:04 -05:00
|
|
|
for sState, tState in self.states:
|
|
|
|
if startState == sState:
|
|
|
|
targetState = tState
|
|
|
|
targetStateName = targetState.getName(wf)
|
|
|
|
break
|
2011-09-08 09:33:16 -05:00
|
|
|
# Create the event and add it in the object history
|
2011-07-26 15:15:04 -05:00
|
|
|
action = transitionName
|
|
|
|
if transitionName == '_init_': action = None
|
|
|
|
if not doHistory: comment = '_invisible_'
|
2011-09-08 09:33:16 -05:00
|
|
|
obj.addHistoryEvent(action, review_state=targetStateName,
|
|
|
|
comments=comment)
|
2011-07-26 15:15:04 -05:00
|
|
|
# Update permissions-to-roles attributes
|
|
|
|
targetState.updatePermissions(wf, obj)
|
2011-11-28 15:50:01 -06:00
|
|
|
# Reindex the object if required. Not only security-related indexes
|
|
|
|
# (Allowed, State) need to be updated here.
|
|
|
|
if not obj.isTemporary(): obj.reindex()
|
2011-07-26 15:15:04 -05:00
|
|
|
# Execute the related action if needed
|
|
|
|
msg = ''
|
|
|
|
if doAction and self.action: msg = self.executeAction(obj, wf)
|
|
|
|
# Send notifications if needed
|
2012-05-03 03:51:54 -05:00
|
|
|
if doNotify and self.notify and obj.getTool(True).mailEnabled:
|
|
|
|
sendNotification(obj.appy(), self, transitionName, wf)
|
2011-07-26 15:15:04 -05:00
|
|
|
# Return a message to the user if needed
|
|
|
|
if not doSay or (transitionName == '_init_'): return
|
2012-06-03 11:34:56 -05:00
|
|
|
if not msg: msg = obj.translate('object_saved')
|
2011-07-26 15:15:04 -05:00
|
|
|
obj.say(msg)
|
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
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
|
2010-08-12 04:56:42 -05:00
|
|
|
attribute "t1" of class A, write: WritePermission("A.t1") or
|
2009-06-29 07:06:01 -05:00
|
|
|
WritePermission("x.y.A.t1") if class A is not in the same module as
|
2010-08-12 04:56:42 -05:00
|
|
|
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.'''
|
2011-07-26 15:15:04 -05:00
|
|
|
|
|
|
|
allowedChars = string.digits + string.letters + '_'
|
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
def __init__(self, fieldDescriptor):
|
|
|
|
self.fieldDescriptor = fieldDescriptor
|
|
|
|
|
2011-07-26 15:15:04 -05:00
|
|
|
def getName(self, wf, appName):
|
|
|
|
'''Returns the name of the Zope permission that corresponds to this
|
|
|
|
permission.'''
|
|
|
|
className, fieldName = self.fieldDescriptor.rsplit('.', 1)
|
|
|
|
if className.find('.') == -1:
|
|
|
|
# The related class resides in the same module as the workflow
|
|
|
|
fullClassName= '%s_%s' % (wf.__module__.replace('.', '_'),className)
|
|
|
|
else:
|
|
|
|
# className contains the full package name of the class
|
|
|
|
fullClassName = className.replace('.', '_')
|
|
|
|
# Read or Write ?
|
|
|
|
if self.__class__.__name__ == 'ReadPermission': access = 'Read'
|
|
|
|
else: access = 'Write'
|
|
|
|
return '%s: %s %s %s' % (appName, access, fullClassName, fieldName)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def getZopeAttrName(zopePermission):
|
|
|
|
'''Gets the name of the attribute where Zope stores, on every object,
|
|
|
|
the tuple of roles who are granted a given p_zopePermission.'''
|
|
|
|
res = ''
|
|
|
|
for c in zopePermission:
|
|
|
|
if c in Permission.allowedChars: res += c
|
|
|
|
else: res += '_'
|
|
|
|
return '_%s_Permission' % res
|
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
class ReadPermission(Permission): pass
|
|
|
|
class WritePermission(Permission): pass
|
|
|
|
|
2011-07-26 15:15:04 -05:00
|
|
|
class WorkflowAnonymous:
|
|
|
|
'''One-state workflow allowing anyone to consult and Manager to edit.'''
|
|
|
|
mgr = 'Manager'
|
2012-02-16 11:13:51 -06:00
|
|
|
o = 'Owner'
|
2012-04-19 02:20:15 -05:00
|
|
|
active = State({r:(mgr, 'Anonymous', 'Authenticated'), w:(mgr,o),d:(mgr,o)},
|
|
|
|
initial=True)
|
2011-07-26 15:15:04 -05:00
|
|
|
|
|
|
|
class WorkflowAuthenticated:
|
|
|
|
'''One-state workflow allowing authenticated users to consult and Manager
|
|
|
|
to edit.'''
|
|
|
|
mgr = 'Manager'
|
2012-02-16 11:13:51 -06:00
|
|
|
o = 'Owner'
|
|
|
|
active = State({r:(mgr, 'Authenticated'), w:(mgr,o), d:(mgr,o)},
|
|
|
|
initial=True)
|
2011-07-26 15:15:04 -05:00
|
|
|
|
2012-05-05 10:04:19 -05:00
|
|
|
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)
|
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
# ------------------------------------------------------------------------------
|
2010-09-02 09:16:08 -05:00
|
|
|
class Model: pass
|
|
|
|
class Tool(Model):
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
'''If you want to extend or modify the Tool class, subclass me.'''
|
2010-09-02 09:16:08 -05:00
|
|
|
class User(Model):
|
|
|
|
'''If you want to extend or modify the User class, subclass me.'''
|
2009-06-29 07:06:01 -05:00
|
|
|
|
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
class Config:
|
|
|
|
'''If you want to specify some configuration parameters for appy.gen and
|
|
|
|
your application, please create an instance of this class and modify its
|
|
|
|
attributes. You may put your instance anywhere in your application
|
|
|
|
(main package, sub-package, etc).'''
|
|
|
|
|
|
|
|
# The default Config instance, used if the application does not give one.
|
|
|
|
defaultConfig = None
|
|
|
|
def getDefault():
|
|
|
|
if not Config.defaultConfig:
|
|
|
|
Config.defaultConfig = Config()
|
|
|
|
return Config.defaultConfig
|
|
|
|
getDefault = staticmethod(getDefault)
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
# For every language code that you specify in this list, appy.gen will
|
|
|
|
# produce and maintain translation files.
|
|
|
|
self.languages = ['en']
|
2010-09-02 09:16:08 -05:00
|
|
|
# If languageSelector is True, on every page, a language selector will
|
|
|
|
# allow to switch between languages defined in self.languages. Else,
|
|
|
|
# the browser-defined language will be used for choosing the language
|
|
|
|
# of returned pages.
|
|
|
|
self.languageSelector = False
|
2009-06-29 07:06:01 -05:00
|
|
|
# People having one of these roles will be able to create instances
|
|
|
|
# of classes defined in your application.
|
|
|
|
self.defaultCreators = ['Manager', 'Owner']
|
2011-01-14 02:06:25 -06:00
|
|
|
# Number of translations for every page on a Translation object
|
|
|
|
self.translationsPerPage = 30
|
|
|
|
# Language that will be used as a basis for translating to other
|
|
|
|
# languages.
|
|
|
|
self.sourceLanguage = 'en'
|
2012-07-27 04:01:35 -05:00
|
|
|
# Activate or not the button on home page for asking a new password
|
|
|
|
self.activateForgotPassword = True
|
2012-12-15 16:36:56 -06:00
|
|
|
# Enable session timeout?
|
|
|
|
self.enableSessionTimeout = False
|
2013-05-10 05:16:57 -05:00
|
|
|
# If the following field is True, the login/password widget will be
|
|
|
|
# discreet. This is for sites where authentication is not foreseen for
|
|
|
|
# the majority of visitors (just for some administrators).
|
|
|
|
self.discreetLogin = False
|
2012-07-26 10:22:22 -05:00
|
|
|
# When using Ogone, place an instance of appy.gen.ogone.OgoneConfig in
|
|
|
|
# the field below.
|
|
|
|
self.ogone = None
|
2013-04-29 14:32:05 -05:00
|
|
|
# When using Google analytics, specify here the Analytics ID
|
|
|
|
self.googleAnalyticsId = None
|
2009-06-29 07:06:01 -05:00
|
|
|
# ------------------------------------------------------------------------------
|