appy.gen: first Ploneless version.

This commit is contained in:
Gaetan Delannay 2011-11-25 18:01:20 +01:00
parent 5672c81553
commit d0cbe7e573
360 changed files with 1003 additions and 1017 deletions

View file

@ -6,8 +6,7 @@ class TestMixin:
'''This class is mixed in with any PloneTestCase.'''
def createUser(self, userId, roles):
'''Creates a user with id p_userId with some p_roles.'''
pms = self.portal.portal_membership
pms.addMember(userId, 'password', [], [])
self.acl_users.addMember(userId, 'password', [], [])
self.setRoles(roles, name=userId)
def changeUser(self, userId):

View file

@ -1,13 +1,18 @@
# ------------------------------------------------------------------------------
import re, os, os.path, time, types
import re, os, os.path, time, random, types, base64, urllib
from appy.shared import mimeTypes
from appy.shared.utils import getOsTempFolder
from appy.shared.data import languages
import appy.gen
from appy.gen import Type, Search, Selection
from appy.gen.utils import SomeObjects, sequenceTypes, getClassName
from appy.gen.plone25.mixins import BaseMixin
from appy.gen.plone25.wrappers import AbstractWrapper
from appy.gen.plone25.descriptors import ClassDescriptor
try:
from AccessControl.ZopeSecurityPolicy import _noroles
except ImportError:
_noroles = []
# Errors -----------------------------------------------------------------------
jsMessages = ('no_elem_selected', 'delete_confirm')
@ -27,13 +32,17 @@ class ToolMixin(BaseMixin):
res = '%s%s' % (elems[1], elems[4])
return res
def getCatalog(self):
'''Returns the catalog object.'''
return self.getParentNode().catalog
def getApp(self):
'''Returns the root application object.'''
return self.portal_url.getPortalObject()
'''Returns the root Zope object.'''
return self.getPhysicalRoot()
def getSiteUrl(self):
'''Returns the absolute URL of this site.'''
return self.portal_url.getPortalObject().absolute_url()
return self.getApp().absolute_url()
def getPodInfo(self, obj, name):
'''Gets the available POD formats for Pod field named p_name on
@ -61,20 +70,43 @@ class ToolMixin(BaseMixin):
return res.content
def getAttr(self, name):
'''Gets attribute named p_attrName. Useful because we can't use getattr
directly in Zope Page Templates.'''
'''Gets attribute named p_name.'''
return getattr(self.appy(), name, None)
def getAppName(self):
'''Returns the name of this application.'''
'''Returns the name of the application.'''
return self.getProductConfig().PROJECTNAME
def getAppFolder(self):
'''Returns the folder at the root of the Plone site that is dedicated
to this application.'''
cfg = self.getProductConfig()
portal = cfg.getToolByName(self, 'portal_url').getPortalObject()
return getattr(portal, self.getAppName())
def getPath(self, path):
'''Returns the folder or object whose absolute path p_path.'''
res = self.getPhysicalRoot()
if path == '/': return res
path = path[1:]
if '/' not in path: return res._getOb(path) # For performance
for elem in path.split('/'): res = res._getOb(elem)
return res
def getLanguages(self):
'''Returns the supported languages. First one is the default.'''
return self.getProductConfig().languages
def getLanguageName(self, code):
'''Gets the language name (in this language) from a 2-chars language
p_code.'''
return languages.get(code)[2]
def getMessages(self):
'''Returns the list of messages to return to the user.'''
if hasattr(self.REQUEST, 'messages'):
# Empty the messages and return it
res = self.REQUEST.messages
del self.REQUEST.messages
else:
res = []
# Add portal_status_message key if present
if 'portal_status_message' in self.REQUEST:
res.append( ('info', self.REQUEST['portal_status_message']) )
return res
def getRootClasses(self):
'''Returns the list of root classes for this application.'''
@ -124,7 +156,7 @@ class ToolMixin(BaseMixin):
return {'fields': fields, 'nbOfColumns': nbOfColumns,
'fieldDicts': fieldDicts}
queryParamNames = ('type_name', 'search', 'sortKey', 'sortOrder',
queryParamNames = ('className', 'search', 'sortKey', 'sortOrder',
'filterKey', 'filterValue')
def getQueryInfo(self):
'''If we are showing search results, this method encodes in a string all
@ -160,8 +192,8 @@ class ToolMixin(BaseMixin):
return [importParams['headers'], elems]
def showPortlet(self, context):
if self.portal_membership.isAnonymousUser(): return False
if context.id == 'skyn': context = context.getParentNode()
if self.userIsAnon(): return False
if context.id == 'ui': context = context.getParentNode()
res = True
if not self.getRootClasses():
res = False
@ -170,24 +202,25 @@ class ToolMixin(BaseMixin):
if (self.id in context.absolute_url()): res = True
return res
def getObject(self, uid, appy=False):
def getObject(self, uid, appy=False, brain=False):
'''Allows to retrieve an object from its p_uid.'''
res = self.portal_catalog(UID=uid)
if res:
res = res[0].getObject()
if appy:
res = res.appy()
return res
res = self.getPhysicalRoot().catalog(UID=uid)
if not res: return
res = res[0]
if brain: return res
res = res.getObject()
if not appy: return res
return res.appy()
def executeQuery(self, contentType, searchName=None, startNumber=0,
def executeQuery(self, className, searchName=None, startNumber=0,
search=None, remember=False, brainsOnly=False,
maxResults=None, noSecurity=False, sortBy=None,
sortOrder='asc', filterKey=None, filterValue=None,
refObject=None, refField=None):
'''Executes a query on a given p_contentType (or several, separated
with commas) in portal_catalog. If p_searchName is specified, it
corresponds to:
1) a search defined on p_contentType: additional search criteria
'''Executes a query on instances of a given p_className (or several,
separated with commas) in the catalog. If p_searchName is specified,
it corresponds to:
1) a search defined on p_className: additional search criteria
will be added to the query, or;
2) "_advanced": in this case, additional search criteria will also
be added to the query, but those criteria come from the session
@ -224,16 +257,16 @@ class ToolMixin(BaseMixin):
If p_refObject and p_refField are given, the query is limited to the
objects that are referenced from p_refObject through p_refField.'''
# Is there one or several content types ?
if contentType.find(',') != -1:
portalTypes = contentType.split(',')
if className.find(',') != -1:
classNames = className.split(',')
else:
portalTypes = contentType
params = {'portal_type': portalTypes}
classNames = className
params = {'ClassName': classNames}
if not brainsOnly: params['batch'] = True
# Manage additional criteria from a search when relevant
if searchName:
# In this case, contentType must contain a single content type.
appyClass = self.getAppyClass(contentType)
# In this case, className must contain a single content type.
appyClass = self.getAppyClass(className)
if searchName != '_advanced':
search = ClassDescriptor.getSearch(appyClass, searchName)
else:
@ -273,7 +306,7 @@ class ToolMixin(BaseMixin):
# Determine what method to call on the portal catalog
if noSecurity: catalogMethod = 'unrestrictedSearchResults'
else: catalogMethod = 'searchResults'
exec 'brains = self.portal_catalog.%s(**params)' % catalogMethod
exec 'brains = self.getPath("/catalog").%s(**params)' % catalogMethod
if brainsOnly:
# Return brains only.
if not maxResults: return brains
@ -290,8 +323,8 @@ class ToolMixin(BaseMixin):
# time a page for an element is consulted.
if remember:
if not searchName:
# It is the global search for all objects pf p_contentType
searchName = contentType
# It is the global search for all objects pf p_className
searchName = className
uids = {}
i = -1
for obj in res.objects:
@ -339,15 +372,6 @@ class ToolMixin(BaseMixin):
return '<acronym title="%s">%s</acronym>' % \
(text, text[:width] + '...')
translationMapping = {'portal_path': ''}
def translateWithMapping(self, label):
'''Translates p_label in the application domain, with a default
translation mapping.'''
if not self.translationMapping['portal_path']:
self.translationMapping['portal_path'] = \
self.portal_url.getPortalPath()
return self.translate(label, mapping=self.translationMapping)
def getPublishedObject(self):
'''Gets the currently published object, if its meta_class is among
application classes.'''
@ -355,24 +379,24 @@ class ToolMixin(BaseMixin):
# If we are querying object, there is no published object (the truth is:
# the tool is the currently published object but we don't want to
# consider it this way).
if not req['ACTUAL_URL'].endswith('/skyn/view'): return
if not req['ACTUAL_URL'].endswith('/ui/view'): return
obj = self.REQUEST['PUBLISHED']
parent = obj.getParentNode()
if parent.id == 'skyn': obj = parent.getParentNode()
if parent.id == 'ui': obj = parent.getParentNode()
if obj.meta_type in self.getProductConfig().attributes: return obj
def getAppyClass(self, contentType, wrapper=False):
'''Gets the Appy Python class that is related to p_contentType.'''
# Retrieve first the Archetypes class corresponding to p_ContentType
portalType = self.portal_types.get(contentType)
if not portalType: return
atClassName = portalType.getProperty('content_meta_type')
appName = self.getProductConfig().PROJECTNAME
exec 'from Products.%s.%s import %s as atClass' % \
(appName, atClassName, atClassName)
# Get then the Appy Python class
if wrapper: return atClass.wrapperClass
else: return atClass.wrapperClass.__bases__[-1]
def getZopeClass(self, name):
'''Returns the Zope class whose name is p_name.'''
exec 'from Products.%s.%s import %s as C'% (self.getAppName(),name,name)
return C
def getAppyClass(self, zopeName, wrapper=False):
'''Gets the Appy class corresponding to the Zope class named p_name.
If p_wrapper is True, it returns the Appy wrapper. Else, it returns
the user-defined class.'''
zopeClass = self.getZopeClass(zopeName)
if wrapper: return zopeClass.wrapperClass
else: return zopeClass.wrapperClass.__bases__[-1]
def getCreateMeans(self, contentTypeOrAppyClass):
'''Gets the different ways objects of p_contentTypeOrAppyClass (which
@ -417,9 +441,9 @@ class ToolMixin(BaseMixin):
'''This method is called when the user wants to create objects from
external data.'''
rq = self.REQUEST
appyClass = self.getAppyClass(rq.get('type_name'))
appyClass = self.getAppyClass(rq.get('className'))
importPaths = rq.get('importPath').split('|')
appFolder = self.getAppFolder()
appFolder = self.getPath('/data')
for importPath in importPaths:
if not importPath: continue
objectId = os.path.basename(importPath)
@ -428,9 +452,9 @@ class ToolMixin(BaseMixin):
return self.goto(rq['HTTP_REFERER'])
def isAlreadyImported(self, contentType, importPath):
appFolder = self.getAppFolder()
data = self.getPath('/data')
objectId = os.path.basename(importPath)
if hasattr(appFolder.aq_base, objectId):
if hasattr(data.aq_base, objectId):
return True
else:
return False
@ -553,8 +577,8 @@ class ToolMixin(BaseMixin):
if refInfo: criteria['_ref'] = refInfo
rq.SESSION['searchCriteria'] = criteria
# Go to the screen that displays search results
backUrl = '%s/skyn/query?type_name=%s&&search=_advanced' % \
(self.absolute_url(), rq['type_name'])
backUrl = '%s/ui/query?className=%s&&search=_advanced' % \
(self.absolute_url(), rq['className'])
return self.goto(backUrl)
def getJavascriptMessages(self):
@ -614,8 +638,8 @@ class ToolMixin(BaseMixin):
'''This method creates the URL that allows to perform a (non-Ajax)
request for getting queried objects from a search named p_searchName
on p_contentType.'''
baseUrl = self.absolute_url() + '/skyn'
baseParams = 'type_name=%s' % contentType
baseUrl = self.absolute_url() + '/ui'
baseParams = 'className=%s' % contentType
rq = self.REQUEST
if rq.get('ref'): baseParams += '&ref=%s' % rq.get('ref')
# Manage start number
@ -663,7 +687,7 @@ class ToolMixin(BaseMixin):
res['backText'] = self.translate(label)
else:
fieldName, pageName = d2.split(':')
sourceObj = self.portal_catalog(UID=d1)[0].getObject()
sourceObj = self.getObject(d1)
label = '%s_%s' % (sourceObj.meta_type, fieldName)
res['backText'] = '%s : %s' % (sourceObj.Title(),
self.translate(label))
@ -739,9 +763,9 @@ class ToolMixin(BaseMixin):
except KeyError: pass
except IndexError: pass
if uid:
brain = self.portal_catalog(UID=uid)
brain = self.getObject(uid, brain=True)
if brain:
sibling = brain[0].getObject()
sibling = brain.getObject()
res[urlKey] = sibling.getUrl(nav=newNav % (index + 1),
page='main')
return res
@ -777,6 +801,9 @@ class ToolMixin(BaseMixin):
'''Gets the translated month name of month numbered p_monthNumber.'''
return self.translate(self.monthsIds[int(monthNumber)], domain='plone')
# --------------------------------------------------------------------------
# Authentication-related methods
# --------------------------------------------------------------------------
def performLogin(self):
'''Logs the user in.'''
rq = self.REQUEST
@ -788,11 +815,14 @@ class ToolMixin(BaseMixin):
msg = self.translate(u'You must enable cookies before you can ' \
'log in.', domain='plone')
return self.goto(urlBack, msg.encode('utf-8'))
# Perform the Zope-level authentication
self.acl_users.credentials_cookie_auth.login()
login = rq['login_name']
if self.portal_membership.isAnonymousUser():
login = rq.get('__ac_name', '')
password = rq.get('__ac_password', '')
cookieValue = base64.encodestring('%s:%s' % (login, password)).rstrip()
cookieValue = urllib.quote(cookieValue)
rq.RESPONSE.setCookie('__ac', cookieValue, path='/')
user = self.acl_users.validate(rq)
if self.userIsAnon():
rq.RESPONSE.expireCookie('__ac', path='/')
msg = self.translate(u'Login failed', domain='plone')
logMsg = 'Authentication failed (tried with login "%s")' % login
@ -803,7 +833,7 @@ class ToolMixin(BaseMixin):
msg = msg.encode('utf-8')
self.log(logMsg)
# Bring Managers to the config, leave others on the main page.
user = self.portal_membership.getAuthenticatedMember()
user = self.getUser()
if user.has_role('Manager'):
# Bring the user to the configuration
url = self.goto(self.absolute_url(), msg)
@ -814,28 +844,72 @@ class ToolMixin(BaseMixin):
def performLogout(self):
'''Logs out the current user when he clicks on "disconnect".'''
rq = self.REQUEST
userId = self.portal_membership.getAuthenticatedMember().getId()
userId = self.getUser().getId()
# Perform the logout in acl_users
try:
self.acl_users.logout(rq)
except:
pass
skinvar = self.portal_skins.getRequestVarname()
path = '/' + self.absolute_url(1)
if rq.has_key(skinvar) and not self.portal_skins.getCookiePersistence():
rq.RESPONSE.expireCookie(skinvar, path=path)
# Invalidate existing sessions, but only if they exist.
rq.RESPONSE.expireCookie('__ac', path='/')
# Invalidate existing sessions.
sdm = self.session_data_manager
session = sdm.getSessionData(create=0)
if session is not None:
session.invalidate()
from Products.CMFPlone import transaction_note
transaction_note('Logged out')
self.log('User "%s" has been logged out.' % userId)
# Remove user from variable "loggedUsers"
from appy.gen.plone25.installer import loggedUsers
if loggedUsers.has_key(userId): del loggedUsers[userId]
return self.goto(self.getParentNode().absolute_url())
return self.goto(self.getApp().absolute_url())
def validate(self, request, auth='', roles=_noroles):
'''This method performs authentication and authorization. It is used as
a replacement for Zope's AccessControl.User.BasicUserFolder.validate,
that allows to manage cookie-based authentication.'''
v = request['PUBLISHED'] # The published object
# v is the object (value) we're validating access to
# n is the name used to access the object
# a is the object the object was accessed through
# c is the physical container of the object
a, c, n, v = self._getobcontext(v, request)
# Try to get user name and password from basic authentication
login, password = self.identify(auth)
if not login:
# Try to get them from a cookie
cookie = request.get('__ac', None)
login = request.get('__ac_name', None)
if login and request.form.has_key('__ac_password'):
# The user just entered his credentials. The cookie has not been
# set yet (it will come in the upcoming HTTP response when the
# current request will be served).
login = request.get('__ac_name', '')
password = request.get('__ac_password', '')
elif cookie and (cookie != 'deleted'):
cookieValue = base64.decodestring(urllib.unquote(cookie))
login, password = cookieValue.split(':')
# Try to authenticate this user
user = self.authenticate(login, password, request)
emergency = self._emergency_user
if emergency and user is emergency:
# It is the emergency user.
return emergency.__of__(self)
elif user is None:
# Login and/or password incorrect. Try to authorize and return the
# anonymous user.
if self.authorize(self._nobody, a, c, n, v, roles):
return self._nobody.__of__(self)
else:
return # Anonymous can't acces this object
else:
# We found a user and his password was correct. Try to authorize him
# against the published object.
if self.authorize(user, a, c, n, v, roles):
return user.__of__(self)
# That didn't work. Try to authorize the anonymous user.
elif self.authorize(self._nobody, a, c, n, v, roles):
return self._nobody.__of__(self)
else:
return
# Patch BasicUserFolder with our version of m_validate above.
from AccessControl.User import BasicUserFolder
BasicUserFolder.validate = validate
def tempFile(self):
'''A temp file has been created in a temp folder. This method returns
@ -864,11 +938,16 @@ class ToolMixin(BaseMixin):
def getUserLine(self, user):
'''Returns a one-line user info as shown on every page.'''
res = [user.getId()]
name = user.getProperty('fullname')
if name: res.insert(0, name)
rolesToShow = [r for r in user.getRoles() \
if r not in ('Authenticated', 'Member')]
if rolesToShow:
res.append(', '.join([self.translate(r) for r in rolesToShow]))
return ' | '.join(res)
def generateUid(self, className):
'''Generates a UID for an instance of p_className.'''
name = className.replace('_', '')
randomNumber = str(random.random()).split('.')[1]
timestamp = ('%f' % time.time()).replace('.', '')
return '%s%s%s' % (name, timestamp, randomNumber)
# ------------------------------------------------------------------------------

View file

@ -15,8 +15,8 @@ from appy.gen.plone25.descriptors import ClassDescriptor
# ------------------------------------------------------------------------------
class BaseMixin:
'''Every Archetype class generated by appy.gen inherits from this class or
a subclass of it.'''
'''Every Zope class generated by appy.gen inherits from this class or a
subclass of it.'''
_appy_meta_type = 'Class'
def get_o(self):
@ -31,31 +31,36 @@ class BaseMixin:
initiator=None, initiatorField=None):
'''This method creates (if p_created is True) or updates an object.
p_values are manipulated versions of those from the HTTP request.
In the case of an object creation (p_created is True), p_self is a
temporary object created in the request by portal_factory, and this
method creates the corresponding final object. In the case of an
update, this method simply updates fields of p_self.'''
rq = self.REQUEST
In the case of an object creation from the web (p_created is True
and a REQUEST object is present), p_self is a temporary object
created in /temp_folder, and this method moves it at its "final"
place. In the case of an update, this method simply updates fields
of p_self.'''
rq = getattr(self, 'REQUEST', None)
obj = self
if created:
# portal_factory creates the final object from the temp object.
obj = self.portal_factory.doCreate(self, self.id)
if created and rq:
# Create the final object and put it at the right place.
tool = self.getTool()
id = tool.generateUid(obj.portal_type)
if not initiator:
folder = tool.getPath('/data')
else:
if initiator.isPrincipiaFolderish:
folder = initiator
else:
folder = initiator.getParentNode()
obj = createObject(folder, id, obj.portal_type, tool.getAppName())
previousData = None
if not created: previousData = self.rememberPreviousData()
# Perform the change on the object, unless self is a tool being created.
if (obj._appy_meta_type == 'Tool') and created:
# We do not process form data (=real update on the object) if the
# tool itself is being created.
pass
else:
if not created: previousData = obj.rememberPreviousData()
# Perform the change on the object
if rq:
# Store in the database the new value coming from the form
for appyType in self.getAppyTypes('edit', rq.get('page')):
value = getattr(values, appyType.name, None)
appyType.store(obj, value)
if created: obj.unmarkCreationFlag()
if previousData:
# Keep in history potential changes on historized fields
self.historizeData(previousData)
obj.historizeData(previousData)
# Manage potential link with an initiator object
if created and initiator: initiator.appy().link(initiatorField, obj)
@ -69,7 +74,7 @@ class BaseMixin:
appyObject = obj.appy()
if hasattr(appyObject, 'onEdit'):
msg = appyObject.onEdit(created)
obj.reindexObject()
obj.reindex()
return obj, msg
def delete(self):
@ -99,9 +104,11 @@ class BaseMixin:
def onCreate(self):
'''This method is called when a user wants to create a root object in
the application folder or an object through a reference field.'''
the "data" folder or an object through a reference field. A temporary
object is created in /temp_folder and the edit page to it is
returned.'''
rq = self.REQUEST
typeName = rq.get('type_name')
className = rq.get('className')
# Create the params to add to the URL we will redirect the user to
# create the object.
urlParams = {'mode':'edit', 'page':'main', 'nav':''}
@ -112,15 +119,12 @@ class BaseMixin:
splitted = rq.get('nav').split('.')
splitted[-1] = splitted[-2] = str(int(splitted[-1])+1)
urlParams['nav'] = '.'.join(splitted)
# Determine base URL
baseUrl = self.absolute_url()
if (self._appy_meta_type == 'Tool') and not urlParams['nav']:
# This is the creation of a root object in the app folder
baseUrl = self.getAppFolder().absolute_url()
objId = self.generateUniqueId(typeName)
editUrl = '%s/portal_factory/%s/%s/skyn/edit' % \
(baseUrl, typeName, objId)
return self.goto(self.getUrl(editUrl, **urlParams))
# Create a temp object in /temp_folder
tool = self.getTool()
id = tool.generateUid(className)
appName = tool.getAppName()
obj = createObject(tool.getPath('/temp_folder'), id, className, appName)
return self.goto(obj.getUrl(**urlParams))
def onCreateWithoutForm(self):
'''This method is called when a user wants to create a object from a
@ -175,10 +179,10 @@ class BaseMixin:
def onUpdate(self):
'''This method is executed when a user wants to update an object.
The object may be a temporary object created by portal_factory in
the request. In this case, the update consists in the creation of
the "final" object in the database. If the object is not a temporary
one, this method updates its fields in the database.'''
The object may be a temporary object created in /temp_folder.
In this case, the update consists in moving it to its "final" place.
If the object is not a temporary one, this method updates its
fields in the database.'''
rq = self.REQUEST
tool = self.getTool()
errorMessage = self.translate(
@ -244,7 +248,7 @@ class BaseMixin:
# object like a one-shot form and has already been deleted in method
# onEdit), redirect to the main site page.
if not getattr(obj.getParentNode().aq_base, obj.id, None):
obj.unindexObject()
obj.unindex()
return self.goto(tool.getSiteUrl(), msg)
# If the user can't access the object anymore, redirect him to the
# main site page.
@ -295,16 +299,42 @@ class BaseMixin:
return self.goto(obj.getUrl())
return obj.gotoEdit()
def reindex(self, indexes=None, unindex=False):
'''Reindexes this object the catalog. If names of indexes are specified
in p_indexes, recataloging is limited to those indexes. If p_unindex
is True, instead of cataloguing the object, it uncatalogs it.'''
url = self.absolute_url_path()
catalog = self.getPhysicalRoot().catalog
if unindex:
method = catalog.uncatalog_object
else:
method = catalog.catalog_object
if indexes:
return method(self, url)
else:
return method(self, url, idxs=indexes)
def unindex(self, indexes=None):
'''Undatalog this object.'''
url = self.absolute_url_path()
catalog = self.getPhysicalRoot().catalog
if indexes:
return catalog.catalog_object(self, url)
else:
return catalog.catalog_object(self, url, idxs=indexes)
def say(self, msg, type='info'):
'''Prints a p_msg in the user interface. p_logLevel may be "info",
"warning" or "error".'''
mType = type
rq = self.REQUEST
if not hasattr(rq, 'messages'):
messages = rq.messages = []
else:
messages = rq.messages
if mType == 'warning': mType = 'warn'
elif mType == 'error': mType = 'stop'
try:
self.plone_utils.addPortalMessage(msg, type=mType)
except UnicodeDecodeError:
self.plone_utils.addPortalMessage(msg.decode('utf-8'), type=mType)
messages.append( (mType, msg) )
def log(self, msg, type='info'):
'''Logs a p_msg in the log file. p_logLevel may be "info", "warning"
@ -315,29 +345,15 @@ class BaseMixin:
else: logMethod = logger.info
logMethod(msg)
def getState(self, name=True, initial=False):
'''Returns information about the current object state. If p_name is
True, the returned info is the state name. Else, it is the State
instance. If p_initial is True, instead of returning info about the
current state, it returns info about the workflow initial state.'''
wf = self.getWorkflow()
if initial or not hasattr(self.aq_base, 'workflow_history'):
# No workflow information is available (yet) on this object, or
# initial state is asked. In both cases, return info about this
# initial state.
res = 'active'
for elem in dir(wf):
attr = getattr(wf, elem)
if (attr.__class__.__name__ == 'State') and attr.initial:
res = elem
break
def do(self):
'''Performs some action from the user interface.'''
rq = self.REQUEST
action = rq['action']
if rq.get('objectUid', None):
obj = self.getTool().getObject(rq['objectUid'])
else:
# Return info about the current object state
key = self.workflow_history.keys()[0]
res = self.workflow_history[key][-1]['review_state']
# Return state name or state definition?
if name: return res
else: return getattr(wf, res)
obj = self
return obj.getMethod('on'+action)()
def rememberPreviousData(self):
'''This method is called before updating an object and remembers, for
@ -351,12 +367,12 @@ class BaseMixin:
def addHistoryEvent(self, action, **kw):
'''Adds an event in the object history.'''
userId = self.portal_membership.getAuthenticatedMember().getId()
userId = self.getUser().getId()
from DateTime import DateTime
event = {'action': action, 'actor': userId, 'time': DateTime(),
'comments': ''}
event.update(kw)
if 'review_state' not in event: event['review_state']=self.getState()
if 'review_state' not in event: event['review_state'] = self.State()
# Add the event to the history
histKey = self.workflow_history.keys()[0]
self.workflow_history[histKey] += (event,)
@ -423,7 +439,7 @@ class BaseMixin:
for field in self.getAppyTypes('edit', page):
if (field.type == 'String') and (field.format == 3):
self.REQUEST.set(field.name, '')
return self.skyn.edit(self)
return self.ui.edit(self)
def showField(self, name, layoutType='view'):
'''Must I show field named p_name on this p_layoutType ?'''
@ -657,7 +673,7 @@ class BaseMixin:
'''Returns information about the states that are related to p_phase.
If p_currentOnly is True, we return the current state, even if not
related to p_phase.'''
currentState = self.getState()
currentState = self.State()
if currentOnly:
return [StateDescr(currentState, 'current').get()]
res = []
@ -690,7 +706,7 @@ class BaseMixin:
'''
res = []
wf = self.getWorkflow()
currentState = self.getState(name=False)
currentState = self.State(name=False)
# Loop on every transition
for name in dir(wf):
transition = getattr(wf, name)
@ -855,7 +871,7 @@ class BaseMixin:
related data on the object.'''
wf = self.getWorkflow()
# Get the initial workflow state
initialState = self.getState(name=False)
initialState = self.State(name=False)
# Create a Transition instance representing the initial transition.
initialTransition = Transition((initialState, initialState))
initialTransition.trigger('_init_', self, wf, '')
@ -876,7 +892,7 @@ class BaseMixin:
'''Gets the i18n label for p_stateName, or for the current object state
if p_stateName is not given. Note that if p_stateName is given, it
can also represent the name of a transition.'''
stateName = stateName or self.getState()
stateName = stateName or self.State()
return '%s_%s' % (self.getWorkflow(name=True), stateName)
def refreshSecurity(self):
@ -885,15 +901,15 @@ class BaseMixin:
wf = self.getWorkflow()
try:
# Get the state definition of the object's current state.
state = getattr(wf, self.getState())
state = getattr(wf, self.State())
except AttributeError:
# The workflow information for this object does not correspond to
# its current workflow attribution. Add a new fake event
# representing passage of this object to the initial state of his
# currently attributed workflow.
stateName = self.getState(name=True, initial=True)
stateName = self.State(name=True, initial=True)
self.addHistoryEvent(None, review_state=stateName)
state = self.getState(name=False, initial=True)
state = self.State(name=False, initial=True)
self.log('Wrong workflow info for a "%s"; is not in state "%s".' % \
(self.meta_type, stateName))
# Update permission attributes on the object if required
@ -939,9 +955,11 @@ class BaseMixin:
'''Executes action with p_fieldName on this object.'''
appyType = self.getAppyType(actionName)
actionRes = appyType(self.appy())
if self.getParentNode().get(self.id):
parent = self.getParentNode()
parentAq = getattr(parent, 'aq_base', parent)
if not hasattr(parentAq, self.id):
# Else, it means that the action has led to self's deletion.
self.reindexObject()
self.reindex()
return appyType.result, actionRes
def onExecuteAppyAction(self):
@ -982,8 +1000,8 @@ class BaseMixin:
# the user.
return self.goto(msg)
def do(self, transitionName, comment='', doAction=True, doNotify=True,
doHistory=True, doSay=True):
def trigger(self, transitionName, comment='', doAction=True, doNotify=True,
doHistory=True, doSay=True):
'''Triggers transition named p_transitionName.'''
# Check that this transition exists.
wf = self.getWorkflow()
@ -998,12 +1016,12 @@ class BaseMixin:
transition.trigger(transitionName, self, wf, comment, doAction=doAction,
doNotify=doNotify, doHistory=doHistory, doSay=doSay)
def onDo(self):
def onTrigger(self):
'''This method is called whenever a user wants to trigger a workflow
transition on an object.'''
rq = self.REQUEST
self.do(rq['workflow_action'], comment=rq.get('comment', ''))
self.reindexObject()
self.trigger(rq['workflow_action'], comment=rq.get('comment', ''))
self.reindex()
return self.goto(self.getUrl(rq['HTTP_REFERER']))
def fieldValueSelected(self, fieldName, vocabValue, dbValue):
@ -1079,7 +1097,8 @@ class BaseMixin:
'''Returns a wrapper object allowing to manipulate p_self the Appy
way.'''
# Create the dict for storing Appy wrapper on the REQUEST if needed.
rq = self.REQUEST
rq = getattr(self, 'REQUEST', None)
if not rq: rq = Object()
if not hasattr(rq, 'appyWrappers'): rq.appyWrappers = {}
# Return the Appy wrapper from rq.appyWrappers if already there
uid = self.UID()
@ -1088,7 +1107,69 @@ class BaseMixin:
wrapper = self.wrapperClass(self)
rq.appyWrappers[uid] = wrapper
return wrapper
# --------------------------------------------------------------------------
# Standard methods for computing values of standard Appy indexes
# --------------------------------------------------------------------------
def UID(self):
'''Returns the unique identifier for this object.'''
return self._at_uid
def Title(self):
'''Returns the title for this object.'''
title = self.getAppyType('title')
if title: return title.getValue(self)
return self.id
def SortableTitle(self):
'''Returns the title as must be stored in index "SortableTitle".'''
return self.Title()
def SearchableText(self):
'''This method concatenates the content of every field with
searchable=True for indexing purposes.'''
res = []
for field in self.getAllAppyTypes():
if not field.searchable: continue
res.append(field.getIndexValue(self, forSearch=True))
return res
def Creator(self):
'''Who create this object?'''
return self.creator
def Created(self):
'''When was this object created ?'''
return self.created
def State(self, name=True, initial=False):
'''Returns information about the current object state. If p_name is
True, the returned info is the state name. Else, it is the State
instance. If p_initial is True, instead of returning info about the
current state, it returns info about the workflow initial state.'''
wf = self.getWorkflow()
if initial or not hasattr(self.aq_base, 'workflow_history'):
# No workflow information is available (yet) on this object, or
# initial state is asked. In both cases, return info about this
# initial state.
res = 'active'
for elem in dir(wf):
attr = getattr(wf, elem)
if (attr.__class__.__name__ == 'State') and attr.initial:
res = elem
break
else:
# Return info about the current object state
key = self.workflow_history.keys()[0]
res = self.workflow_history[key][-1]['review_state']
# Return state name or state definition?
if name: return res
else: return getattr(wf, res)
def ClassName(self):
'''Returns the name of the (Zope) class for self.'''
return self.portal_type
def _appy_showState(self, workflow, stateShow):
'''Must I show a state whose "show value" is p_stateShow?'''
if callable(stateShow):
@ -1129,11 +1210,6 @@ class BaseMixin:
# Update the permissions
for permission, creators in allCreators.iteritems():
updateRolesForPermission(permission, tuple(creators), folder)
# Beyond content-type-specific "add" permissions, creators must also
# have the main permission "Add portal content".
permission = 'Add portal content'
for creators in allCreators.itervalues():
updateRolesForPermission(permission, tuple(creators), folder)
def _appy_getPortalType(self, request):
'''Guess the portal_type of p_self from info about p_self and
@ -1162,7 +1238,7 @@ class BaseMixin:
param will not be included in the URL at all).'''
# Define the URL suffix
suffix = ''
if mode != 'raw': suffix = '/skyn/%s' % mode
if mode != 'raw': suffix = '/ui/%s' % mode
# Define base URL if omitted
if not base:
base = self.absolute_url() + suffix
@ -1171,7 +1247,7 @@ class BaseMixin:
if '?' in base: base = base[:base.index('?')]
base = base.strip('/')
for mode in ('view', 'edit'):
suffix = 'skyn/%s' % mode
suffix = 'ui/%s' % mode
if base.endswith(suffix):
base = base[:-len(suffix)].strip('/')
break
@ -1195,6 +1271,19 @@ class BaseMixin:
params = ''
return '%s%s' % (base, params)
def getUser(self):
'''Gets the Zope object representing the authenticated user.'''
from AccessControl import getSecurityManager
user = getSecurityManager().getUser()
if not user:
from AccessControl.User import nobody
return nobody
return user
def userIsAnon(self):
'''Is the currently logged user anonymous ?'''
return self.getUser().getUserName() == 'Anonymous User'
def getUserLanguage(self):
'''Gets the language (code) of the current user.'''
# Try first the "LANGUAGE" key from the request
@ -1291,12 +1380,11 @@ class BaseMixin:
layout = defaultPageLayouts[layoutType]
return layout.get()
def getPageTemplate(self, skyn, templateName):
'''Returns, in the skyn folder, the page template corresponding to
def getPageTemplate(self, ui, templateName):
'''Returns, in the ui folder, the page template corresponding to
p_templateName.'''
res = skyn
for name in templateName.split('/'):
res = res.get(name)
res = ui
for name in templateName.split('/'): res = getattr(res, name)
return res
def download(self):
@ -1316,15 +1404,6 @@ class BaseMixin:
response.setHeader('Expires', 'Thu, 11 Dec 1975 12:05:05 GMT')
return theFile.index_html(self.REQUEST, self.REQUEST.RESPONSE)
def SearchableText(self):
'''This method concatenates the content of every field with
searchable=True for indexing purposes.'''
res = []
for field in self.getAllAppyTypes():
if not field.searchable: continue
res.append(field.getIndexValue(self, forSearch=True))
return res
def allows(self, permission):
'''Has the logged user p_permission on p_self ?'''
# Get first the roles that have this permission on p_self.
@ -1332,8 +1411,10 @@ class BaseMixin:
if not hasattr(self.aq_base, zopeAttr): return
allowedRoles = getattr(self.aq_base, zopeAttr)
# Has the user one of those roles?
user = self.portal_membership.getAuthenticatedMember()
ids = [user.getId()] + user.getGroups()
user = self.getUser()
# XXX no groups at present
#ids = [user.getId()] + user.getGroups()
ids = [user.getId()]
userGlobalRoles = user.getRoles()
for role in allowedRoles:
# Has the user this role ? Check in the local roles first.
@ -1347,4 +1428,11 @@ class BaseMixin:
field p_name.'''
return 'tinyMCE.init({\nmode : "textareas",\ntheme : "simple",\n' \
'elements : "%s",\neditor_selector : "rich_%s"\n});'% (name,name)
def isTemporary(self):
'''Is this object temporary ?'''
parent = self.getParentNode()
if not parent: # Is propably being created through code
return False
return parent.getId() == 'temp_folder'
# ------------------------------------------------------------------------------