[gen] More work ZPT->PX.

This commit is contained in:
Gaetan Delannay 2013-08-21 12:35:30 +02:00
parent 2e9a832463
commit 34e3a3083e
31 changed files with 3287 additions and 3067 deletions

View file

@ -1,16 +1,16 @@
# ------------------------------------------------------------------------------
import os, os.path, sys, re, time, random, types, base64, urllib
import os, os.path, sys, re, time, random, types
from appy import Object
import appy.gen
from appy.gen import Search, String, Page, ldap
from appy.gen.utils import SomeObjects, getClassName, GroupDescr, SearchDescr
from appy.gen import Search, UiSearch, String, Page, ldap
from appy.gen.layout import ColumnLayout
from appy.gen import utils as gutils
from appy.gen.mixins import BaseMixin
from appy.gen.wrappers import AbstractWrapper
from appy.gen.descriptors import ClassDescriptor
from appy.gen.mail import sendMail
from appy.shared import mimeTypes
from appy.shared.utils import getOsTempFolder, sequenceTypes, normalizeString, \
splitList
from appy.shared import utils as sutils
from appy.shared.data import languages
try:
from AccessControl.ZopeSecurityPolicy import _noroles
@ -32,7 +32,7 @@ class ToolMixin(BaseMixin):
appName = self.getProductConfig().PROJECTNAME
res = metaTypeOrAppyClass
if not isinstance(metaTypeOrAppyClass, basestring):
res = getClassName(metaTypeOrAppyClass, appName)
res = gutils.getClassName(metaTypeOrAppyClass, appName)
if res.find('_wrappers') != -1:
elems = res.split('_')
res = '%s%s' % (elems[1], elems[4])
@ -42,31 +42,31 @@ class ToolMixin(BaseMixin):
def home(self):
'''Returns the content of px ToolWrapper.pxHome.'''
tool = self.appy()
return tool.pxHome({'self': tool})
return tool.pxHome({'obj': None, 'tool': tool})
def query(self):
'''Returns the content of px ToolWrapper.pxQuery.'''
tool = self.appy()
return tool.pxQuery({'self': tool})
return tool.pxQuery({'obj': None, 'tool': tool})
def search(self):
'''Returns the content of px ToolWrapper.pxSearch.'''
tool = self.appy()
return tool.pxSearch({'self': tool})
return tool.pxSearch({'obj': None, 'tool': tool})
def getHomePage(self):
'''Return the home page when a user hits the app.'''
# If the app defines a method "getHomePage", call it.
appyTool = self.appy()
tool = self.appy()
try:
url = appyTool.getHomePage()
url = tool.getHomePage()
except AttributeError:
# Bring Managers to the config, lead others to home.pt.
user = self.getUser()
if user.has_role('Manager'):
url = self.goto(self.absolute_url())
else:
url = self.goto(self.getApp().ui.home.absolute_url())
url = self.goto('%s/home' % self.getApp().config.absolute_url())
return url
def getHomeObject(self):
@ -77,15 +77,12 @@ class ToolMixin(BaseMixin):
portlet menu will nevertheless appear: the user will not have the
feeling of being lost.'''
# If the app defines a method "getHomeObject", call it.
appyTool = self.appy()
try:
obj = appyTool.getHomeObject()
if obj: return obj.o
return self.appy().getHomeObject()
except AttributeError:
# For managers, the home object is the config. For others, there is
# no default home object.
user = self.getUser()
if user.has_role('Manager'): return self
if self.getUser().has_role('Manager'): return self.appy()
def getCatalog(self):
'''Returns the catalog object.'''
@ -220,31 +217,29 @@ class ToolMixin(BaseMixin):
'''Returns the list of root classes for this application.'''
return self.getProductConfig().rootClasses
def _appy_getAllFields(self, contentType):
'''Returns the (translated) names of fields of p_contentType.'''
def _appy_getAllFields(self, className):
'''Returns the (translated) names of fields of p_className.'''
res = []
for appyType in self.getAllAppyTypes(className=contentType):
res.append((appyType.name, self.translate(appyType.labelId)))
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, contentType):
def _appy_getSearchableFields(self, className):
'''Returns the (translated) names of fields that may be searched on
objects of type p_contentType (=indexed fields).'''
objects of type p_className (=indexed fields).'''
res = []
for appyType in self.getAllAppyTypes(className=contentType):
if appyType.indexed:
res.append((appyType.name, self.translate(appyType.labelId)))
for field in self.getAllAppyTypes(className=className):
if field.indexed:
res.append((field.name, self.translate(field.labelId)))
return res
def getSearchInfo(self, contentType, refInfo=None):
'''Returns, as a dict:
- the list of searchable fields (= some fields among all indexed
fields);
def getSearchInfo(self, className, refInfo=None):
'''Returns, as an object:
- the list of searchable fields (some among all indexed fields);
- the number of columns for layouting those fields.'''
fields = []
fieldDicts = []
if refInfo:
# The search is triggered from a Ref field.
refObject, fieldName = self.getRefInfo(refInfo)
@ -253,16 +248,13 @@ class ToolMixin(BaseMixin):
nbOfColumns = refField.queryNbCols
else:
# The search is triggered from an app-wide search.
at = self.appy()
fieldNames = getattr(at, 'searchFieldsFor%s' % contentType,())
nbOfColumns = getattr(at, 'numberOfSearchColumnsFor%s' %contentType)
tool = self.appy()
fieldNames = getattr(tool, 'searchFieldsFor%s' % className,())
nbOfColumns = getattr(tool, 'numberOfSearchColumnsFor%s' %className)
for name in fieldNames:
appyType = self.getAppyType(name,asDict=False,className=contentType)
appyDict = self.getAppyType(name, asDict=True,className=contentType)
fields.append(appyType)
fieldDicts.append(appyDict)
return {'fields': fields, 'nbOfColumns': nbOfColumns,
'fieldDicts': fieldDicts}
field = self.getAppyType(name, className=className)
fields.append(field)
return Object(fields=fields, nbOfColumns=nbOfColumns)
queryParamNames = ('className', 'search', 'sortKey', 'sortOrder',
'filterKey', 'filterValue')
@ -284,16 +276,16 @@ class ToolMixin(BaseMixin):
if hasattr(klass, 'resultMode'): return klass.resultMode
return 'list' # The default mode
def getImportElements(self, contentType):
def getImportElements(self, className):
'''Returns the list of elements that can be imported from p_path for
p_contentType.'''
appyClass = self.getAppyClass(contentType)
p_className.'''
appyClass = self.getAppyClass(className)
importParams = self.getCreateMeans(appyClass)['import']
onElement = importParams['onElement'].__get__('')
sortMethod = importParams['sort']
if sortMethod: sortMethod = sortMethod.__get__('')
elems = []
importType = self.getAppyType('importPathFor%s' % contentType)
importType = self.getAppyType('importPathFor%s' % className)
importPath = importType.getValue(self)
for elem in os.listdir(importPath):
elemFullPath = os.path.join(importPath, elem)
@ -339,17 +331,19 @@ class ToolMixin(BaseMixin):
def getAllowedValue(self):
'''Gets, for the currently logged user, the value for index
"Allowed".'''
tool = self.appy()
user = self.getUser()
rq = tool.request
# Get the user roles
res = user.getRoles()
res = rq.userRoles
# Add role "Anonymous"
if 'Anonymous' not in res: res.append('Anonymous')
# Add the user id if not anonymous
userId = user.getId()
if userId: res.append('user:%s' % userId)
userId = user.login
if userId != 'anon': res.append('user:%s' % userId)
# Add group ids
try:
res += ['user:%s' % g for g in user.groups.keys()]
res += ['user:%s' % g for g in rq.zopeUser.groups.keys()]
except AttributeError, ae:
pass # The Zope admin does not have this attribute.
return res
@ -434,7 +428,8 @@ class ToolMixin(BaseMixin):
if refField: maxResults = refField.maxPerPage
else: maxResults = self.appy().numberOfResultsPerPage
elif maxResults == 'NO_LIMIT': maxResults = None
res = SomeObjects(brains, maxResults, startNumber,noSecurity=noSecurity)
res = gutils.SomeObjects(brains, maxResults, startNumber,
noSecurity=noSecurity)
res.brainsToObjects()
# In some cases (p_remember=True), we need to keep some information
# about the query results in the current user's session, allowing him
@ -454,15 +449,14 @@ class ToolMixin(BaseMixin):
self.REQUEST.SESSION['search_%s' % searchName] = uids
return res.__dict__
def getResultColumnsLayouts(self, contentType, refInfo):
def getResultColumnsLayouts(self, className, refInfo):
'''Returns the column layouts for displaying objects of
p_contentType.'''
p_className.'''
if refInfo[0]:
res = refInfo[0].getAppyType(refInfo[1]).shownInfo
return refInfo[0].getAppyType(refInfo[1]).shownInfo
else:
toolFieldName = 'resultColumnsFor%s' % contentType
res = getattr(self.appy(), toolFieldName)
return res
toolFieldName = 'resultColumnsFor%s' % className
return getattr(self.appy(), toolFieldName)
def truncateValue(self, value, width=15):
'''Truncates the p_value according to p_width.'''
@ -487,10 +481,11 @@ class ToolMixin(BaseMixin):
def splitList(self, l, sub):
'''Returns a list made of the same elements as p_l, but grouped into
sub-lists of p_sub elements.'''
return splitList(l, sub)
return sutils.splitList(l, sub)
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
return "'%s'" % s
@ -500,21 +495,6 @@ class ToolMixin(BaseMixin):
if url.endswith('/view'): return 'view'
if url.endswith('/edit') or url.endswith('/do'): return 'edit'
def getPublishedObject(self, layoutType):
'''Gets the currently published object, if its meta_class is among
application classes.'''
# In some situations (ie, we are querying objects), the published object
# according to Zope is the tool, but we don't want to consider it that
# way.
if layoutType not in ('edit', 'view'): return
obj = self.REQUEST['PUBLISHED']
# If URL is a /do, published object is the "do" method.
if type(obj) == types.MethodType: obj = obj.im_self
else:
parent = obj.getParentNode()
if parent.id == 'ui': obj = parent.getParentNode()
if obj.meta_type in self.getProductConfig().attributes: return obj
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)
@ -735,7 +715,7 @@ class ToolMixin(BaseMixin):
rq = self.REQUEST
self.storeSearchCriteria()
# Go to the screen that displays search results
backUrl = '%s/ui/query?className=%s&&search=customSearch' % \
backUrl = '%s/query?className=%s&&search=customSearch' % \
(self.absolute_url(), rq['className'])
return self.goto(backUrl)
@ -747,15 +727,31 @@ class ToolMixin(BaseMixin):
res += 'var %s = "%s";\n' % (msg, self.translate(msg))
return res
def getColumnsSpecifiers(self, className, columnLayouts, dir):
'''Extracts and returns, from a list of p_columnLayouts, info required
for displaying columns of field values for instances of p_className,
either in a result screen or for a Ref field.'''
res = []
for info in columnLayouts:
fieldName, width, align = ColumnLayout(info).get()
align = self.flipLanguageDirection(align, dir)
field = self.getAppyType(fieldName, className)
if not field:
self.log('Field "%s", used in a column specifier, was not ' \
'found.' % fieldName, type='warning')
else:
res.append(Object(field=field, width=width, align=align))
return res
def getRefInfo(self, refInfo=None):
'''When a search is restricted to objects referenced through a Ref
field, this method returns information about this reference: the
source content type and the Ref field (Appy type). If p_refInfo is
not given, we search it among search criteria in the session.'''
source class and the Ref field. If p_refInfo is not given, we search
it among search criteria in the session.'''
if not refInfo and (self.REQUEST.get('search', None) == 'customSearch'):
criteria = self.REQUEST.SESSION.get('searchCriteria', None)
if criteria and criteria.has_key('_ref'): refInfo = criteria['_ref']
if not refInfo: return (None, None)
if not refInfo: return None, None
objectUid, fieldName = refInfo.split(':')
obj = self.getObject(objectUid)
return obj, fieldName
@ -771,7 +767,7 @@ class ToolMixin(BaseMixin):
res = []
default = None # Also retrieve the default one here.
groups = {} # The already encountered groups
page = Page('main') # A dummy page required by class GroupDescr
page = Page('main') # A dummy page required by class UiGroup
# Get the searches statically defined on the class
searches = ClassDescriptor.getSearches(appyClass, tool=self.appy())
# Get the dynamically computed searches
@ -779,22 +775,21 @@ class ToolMixin(BaseMixin):
searches += appyClass.getDynamicSearches(self.appy())
for search in searches:
# Create the search descriptor
sDescr = SearchDescr(search, className, self).get()
uiSearch = UiSearch(search, className, self)
if not search.group:
# Insert the search at the highest level, not in any group.
res.append(sDescr)
res.append(uiSearch)
else:
gDescr = search.group.insertInto(res, groups, page, className,
forSearch=True)
GroupDescr.addWidget(gDescr, sDescr)
uiGroup = search.group.insertInto(res, groups, page, className,
forSearch=True)
uiGroup.addField(uiSearch)
# Is this search the default search?
if search.default: default = sDescr
return Object(searches=res, default=default).__dict__
if search.default: default = uiSearch
return Object(searches=res, default=default)
def getSearch(self, className, name, descr=False):
'''Gets the Search instance (or a SearchDescr instance if p_descr is
True) corresponding to the search named p_name, on class
p_className.'''
def getSearch(self, className, name, ui=False):
'''Gets the Search instance (or a UiSearch instance if p_ui is True)
corresponding to the search named p_name, on class p_className.'''
if name == 'customSearch':
# It is a custom search whose parameters are in the session.
fields = self.REQUEST.SESSION['searchCriteria']
@ -812,8 +807,8 @@ class ToolMixin(BaseMixin):
else:
# It is the search for every instance of p_className
res = Search('allSearch')
# Return a SearchDescr if required.
if descr: res = SearchDescr(res, className, self).get()
# Return a UiSearch if required.
if ui: res = UiSearch(res, className, self)
return res
def advancedSearchEnabledFor(self, className):
@ -829,7 +824,7 @@ 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() + '/ui'
baseUrl = self.absolute_url()
baseParams = 'className=%s' % contentType
rq = self.REQUEST
if rq.get('ref'): baseParams += '&ref=%s' % rq.get('ref')
@ -856,13 +851,14 @@ class ToolMixin(BaseMixin):
return startNumber
def getNavigationInfo(self):
'''Extracts navigation information from request/nav and returns a dict
with the info that a page can use for displaying object
'''Extracts navigation information from request/nav and returns an
object with the info that a page can use for displaying object
navigation.'''
res = {}
t,d1,d2,currentNumber,totalNumber = self.REQUEST.get('nav').split('.')
res['currentNumber'] = int(currentNumber)
res['totalNumber'] = int(totalNumber)
res = Object()
rq = self.REQUEST
t, d1, d2, currentNumber, totalNumber = rq.get('nav').split('.')
res.currentNumber = int(currentNumber)
res.totalNumber = int(totalNumber)
# Compute the label of the search, or ref field
if t == 'search':
searchName = d2
@ -875,29 +871,28 @@ class ToolMixin(BaseMixin):
else:
# This is a named, predefined search.
label = '%s_search_%s' % (d1.split(':')[0], searchName)
res['backText'] = self.translate(label)
res.backText = self.translate(label)
# If it is a dynamic search this label does not exist.
if ('_' in res['backText']): res['backText'] = ''
if ('_' in res.backText): res.backText = ''
else:
fieldName, pageName = d2.split(':')
sourceObj = self.getObject(d1)
label = '%s_%s' % (sourceObj.meta_type, fieldName)
res['backText'] = '%s - %s' % (sourceObj.Title(),
self.translate(label))
res.backText = '%s - %s' % (sourceObj.Title(),self.translate(label))
newNav = '%s.%s.%s.%%d.%s' % (t, d1, d2, totalNumber)
# Among, first, previous, next and last, which one do I need?
previousNeeded = False # Previous ?
previousIndex = res['currentNumber'] - 2
if (previousIndex > -1) and (res['totalNumber'] > previousIndex):
previousIndex = res.currentNumber - 2
if (previousIndex > -1) and (res.totalNumber > previousIndex):
previousNeeded = True
nextNeeded = False # Next ?
nextIndex = res['currentNumber']
if nextIndex < res['totalNumber']: nextNeeded = True
nextIndex = res.currentNumber
if nextIndex < res.totalNumber: nextNeeded = True
firstNeeded = False # First ?
firstIndex = 0
if previousIndex > 0: firstNeeded = True
lastNeeded = False # Last ?
lastIndex = res['totalNumber'] - 1
lastIndex = res.totalNumber - 1
if (nextIndex < lastIndex): lastNeeded = True
# Get the list of available UIDs surrounding the current object
if t == 'ref': # Manage navigation from a reference
@ -908,16 +903,16 @@ class ToolMixin(BaseMixin):
# Display the reference widget at the page where the current object
# lies.
startNumberKey = '%s%s_startNumber' % (masterObj.UID(), fieldName)
startNumber = self.computeStartNumberFrom(res['currentNumber']-1,
res['totalNumber'], batchSize)
res['sourceUrl'] = masterObj.getUrl(**{startNumberKey:startNumber,
'page':pageName, 'nav':''})
startNumber = self.computeStartNumberFrom(res.currentNumber-1,
res.totalNumber, batchSize)
res.sourceUrl = masterObj.getUrl(**{startNumberKey:startNumber,
'page':pageName, 'nav':''})
else: # Manage navigation from a search
contentType = d1
searchName = keySuffix = d2
batchSize = self.appy().numberOfResultsPerPage
if not searchName: keySuffix = contentType
s = self.REQUEST.SESSION
s = rq.SESSION
searchKey = 'search_%s' % keySuffix
if s.has_key(searchKey): uids = s[searchKey]
else: uids = {}
@ -928,7 +923,7 @@ class ToolMixin(BaseMixin):
# I do not have this UID in session. I will need to
# retrigger the query by querying all objects surrounding
# this one.
newStartNumber = (res['currentNumber']-1) - (batchSize / 2)
newStartNumber = (res.currentNumber-1) - (batchSize / 2)
if newStartNumber < 0: newStartNumber = 0
self.executeQuery(contentType, searchName=searchName,
startNumber=newStartNumber, remember=True)
@ -938,15 +933,15 @@ class ToolMixin(BaseMixin):
if not uids.has_key(0): firstNeeded = False
if not uids.has_key(lastIndex): lastNeeded = False
# Compute URL of source object
startNumber = self.computeStartNumberFrom(res['currentNumber']-1,
res['totalNumber'], batchSize)
res['sourceUrl'] = self.getQueryUrl(contentType, searchName,
startNumber=startNumber)
startNumber = self.computeStartNumberFrom(res.currentNumber-1,
res.totalNumber, batchSize)
res.sourceUrl = self.getQueryUrl(contentType, searchName,
startNumber=startNumber)
# Compute URLs
for urlType in ('previous', 'next', 'first', 'last'):
exec 'needIt = %sNeeded' % urlType
urlKey = '%sUrl' % urlType
res[urlKey] = None
setattr(res, urlKey, None)
if needIt:
exec 'index = %sIndex' % urlType
uid = None
@ -959,37 +954,38 @@ class ToolMixin(BaseMixin):
brain = self.getObject(uid, brain=True)
if brain:
sibling = brain.getObject()
res[urlKey] = sibling.getUrl(nav=newNav % (index + 1),
page=self.REQUEST.get('page', 'main'))
setattr(res, urlKey, sibling.getUrl(\
nav=newNav % (index + 1),
page=rq.get('page', 'main')))
return res
def getGroupedSearchFields(self, searchInfo):
'''This method transforms p_searchInfo['fieldDicts'], which is a "flat"
'''This method transforms p_searchInfo.fields, which is a "flat"
list of fields, into a list of lists, where every sub-list having
length p_searchInfo['nbOfColumns']. For every field, scolspan
length p_searchInfo.nbOfColumns. For every field, scolspan
(=colspan "for search") is taken into account.'''
res = []
row = []
rowLength = 0
for field in searchInfo['fieldDicts']:
for field in searchInfo.fields:
# Can I insert this field in the current row?
remaining = searchInfo['nbOfColumns'] - rowLength
if field['scolspan'] <= remaining:
remaining = searchInfo.nbOfColumns - rowLength
if field.scolspan <= remaining:
# Yes.
row.append(field)
rowLength += field['scolspan']
rowLength += field.scolspan
else:
# We must put the field on a new line. Complete the current one
# if not complete.
while rowLength < searchInfo['nbOfColumns']:
while rowLength < searchInfo.nbOfColumns:
row.append(None)
rowLength += 1
res.append(row)
row = [field]
rowLength = field['scolspan']
rowLength = field.scolspan
# Complete the last unfinished line if required.
if row:
while rowLength < searchInfo['nbOfColumns']:
while rowLength < searchInfo.nbOfColumns:
row.append(None)
rowLength += 1
res.append(row)
@ -998,11 +994,6 @@ class ToolMixin(BaseMixin):
# --------------------------------------------------------------------------
# Authentication-related methods
# --------------------------------------------------------------------------
def _updateCookie(self, login, password):
cookieValue = base64.encodestring('%s:%s' % (login, password)).rstrip()
cookieValue = urllib.quote(cookieValue)
self.REQUEST.RESPONSE.setCookie('__ac', cookieValue, path='/')
def _encryptPassword(self, password):
'''Returns the encrypted version of clear p_password.'''
return self.acl_users._encryptPassword(password)
@ -1011,7 +1002,7 @@ class ToolMixin(BaseMixin):
'''Performs the Zope-level authentication. Returns True if
authentication succeeds.'''
user = self.acl_users.validate(request)
return not self.userIsAnon()
return user.getUserName() != 'Anonymous User'
def _ldapAuthenticate(self, login, password):
'''Performs a LDAP-based authentication. Returns True if authentication
@ -1032,16 +1023,16 @@ class ToolMixin(BaseMixin):
if jsEnabled and not cookiesEnabled:
msg = self.translate('enable_cookies')
return self.goto(urlBack, msg)
# Extract the login and password
# Extract the login and password, and create an authentication cookie
login = rq.get('__ac_name', '')
password = rq.get('__ac_password', '')
password = rq.get('__ac_password', '')
gutils.writeCookie(login, password, rq)
# Perform the Zope-level authentication
self._updateCookie(login, password)
if self._zopeAuthenticate(rq) or self._ldapAuthenticate(login,password):
msg = self.translate('login_ok')
logMsg = 'User "%s" logged in.' % login
else:
rq.RESPONSE.expireCookie('__ac', path='/')
rq.RESPONSE.expireCookie('_appy_', path='/')
msg = self.translate('login_ko')
logMsg = 'Authentication failed with login "%s".' % login
self.log(logMsg)
@ -1050,9 +1041,9 @@ class ToolMixin(BaseMixin):
def performLogout(self):
'''Logs out the current user when he clicks on "disconnect".'''
rq = self.REQUEST
userId = self.getUser().getId()
userId = self.getUser().login
# Perform the logout in acl_users
rq.RESPONSE.expireCookie('__ac', path='/')
rq.RESPONSE.expireCookie('_appy_', path='/')
# Invalidate session.
try:
sdm = self.session_data_manager
@ -1083,18 +1074,12 @@ class ToolMixin(BaseMixin):
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))
if ':' in cookieValue:
login, password = cookieValue.split(':')
login, password = gutils.readCookie(request)
if not login:
# Maybe the user just entered his credentials. The cookie could
# have been set in the response, but is not in the request.
login = request.get('__ac_name', None)
password = request.get('__ac_password', None)
# Try to authenticate this user
user = self.authenticate(login, password, request)
emergency = self._emergency_user
@ -1123,11 +1108,82 @@ class ToolMixin(BaseMixin):
from AccessControl.User import BasicUserFolder
BasicUserFolder.validate = validate
def getUser(self):
'''Gets the User instance (Appy wrapper) corresponding to the current
user.'''
tool = self.appy()
rq = tool.request
# Try first to return the user that can be cached on the request.
if hasattr(rq, 'user'): return rq.user
# Get the user login from the authentication cookie.
login, password = gutils.readCookie(rq)
if not login: # It is the anonymous user or the system.
# If we have a real request object, it is the anonymous user.
login = (rq.__class__.__name__ == 'Object') and 'system' or 'anon'
# Get the User object from a query in the catalog.
user = tool.search1('User', noSecurity=True, login=login)
rq.user = user
# Precompute some values or this usser for performance reasons
rq.userRoles = user.getRoles()
rq.zopeUser = user.getZopeUser()
return user
#from AccessControl import getSecurityManager
#user = getSecurityManager().getUser()
#if not user:
# from AccessControl.User import nobody
# return nobody
#return user
def getUserLine(self):
'''Returns a info about the currently logged user as a 2-tuple: first
elem is the one-line user info as shown on every page; second line is
the URL to edit user info.'''
user = self.getUser()
userRoles = self.appy().request.userRoles
info = [user.title]
rolesToShow = [r for r in userRoles if r != 'Authenticated']
if rolesToShow:
info.append(', '.join([self.translate('role_%s' % r) \
for r in rolesToShow]))
# Edit URL for the user.
url = None
if user.o.mayEdit():
url = user.o.getUrl(mode='edit', page='main', nav='')
return (' | '.join(info), url)
def getUserName(self, login=None, normalized=False):
'''Gets the user name corresponding to p_login (or the currently logged
login if None), or the p_login itself if the user does not exist
anymore. If p_normalized is True, special chars in the first and last
names are normalized.'''
tool = self.appy()
if not login: login = tool.user.login
# Manage the special case of an anonymous user.
if login == 'Anonymous User':
name = self.translate('anonymous')
if normalized: name = sutils.normalizeString(name)
return name
# Manage the case of a "real" user.
user = tool.search1('User', noSecurity=True, login=login)
if not user: return login
firstName = user.firstName
name = user.name
res = ''
if firstName:
if normalized: firstName = sutils.normalizeString(firstName)
res += firstName
if name:
if normalized: name = sutils.normalizeString(name)
if res: res += ' ' + name
else: res = name
if not res: res = login
return res
def tempFile(self):
'''A temp file has been created in a temp folder. This method returns
this file to the browser.'''
rq = self.REQUEST
baseFolder = os.path.join(getOsTempFolder(), self.getAppName())
baseFolder = os.path.join(sutils.getOsTempFolder(), self.getAppName())
baseFolder = os.path.join(baseFolder, rq.SESSION.id)
fileName = os.path.join(baseFolder, rq.get('name', ''))
if os.path.exists(fileName):
@ -1146,52 +1202,6 @@ class ToolMixin(BaseMixin):
if ',' in contentType: return ()
return [f.__dict__ for f in self.getAllAppyTypes(contentType) \
if (f.type == 'Pod') and (f.show == 'result')]
def getUserLine(self):
'''Returns a info about the currently logged user as a 2-tuple: first
elem is the one-line user info as shown on every page; second line is
the URL to edit user info.'''
appyUser = self.appy().appyUser
info = [appyUser.title]
rolesToShow = [r for r in appyUser.roles \
if r not in ('Authenticated', 'Member')]
if rolesToShow:
info.append(', '.join([self.translate('role_%s'%r) \
for r in rolesToShow]))
# Edit URL for the appy user.
url = None
if appyUser.o.mayEdit():
url = appyUser.o.getUrl(mode='edit', page='main', nav='')
return (' | '.join(info), url)
def getUserName(self, login=None, normalized=False):
'''Gets the user name corresponding to p_login (or the currently logged
login if None), or the p_login itself if the user does not exist
anymore. If p_normalized is True, special chars in the first and last
names are normalized.'''
tool = self.appy()
if not login: login = tool.user.getId()
# Manage the special case of an anonymous user.
if login == 'Anonymous User':
name = self.translate('anonymous')
if normalized: name = normalizeString(name)
return name
# Manage the case of a "real" user.
user = tool.search1('User', noSecurity=True, login=login)
if not user: return login
firstName = user.firstName
name = user.name
res = ''
if firstName:
if normalized: firstName = normalizeString(firstName)
res += firstName
if name:
if normalized: name = normalizeString(name)
if res: res += ' ' + name
else: res = name
if not res: res = login
return res
def formatDate(self, aDate, withHour=True):
'''Returns aDate formatted as specified by tool.dateFormat.
If p_withHour is True, hour is appended, with a format specified
@ -1216,7 +1226,7 @@ class ToolMixin(BaseMixin):
htmlMessage = '<a href="%s"><img src="%s/ui/home.gif"/></a>' \
'You are not allowed to access this page.' % \
(siteUrl, siteUrl)
userId = self.appy().user.getId() or 'system|anon'
userId = self.appy().user.login
textMessage = 'Unauthorized for %s @%s.' % \
(userId, self.REQUEST.get('PATH_INFO'))
else:
@ -1256,7 +1266,7 @@ class ToolMixin(BaseMixin):
return self.goto(backUrl, msg)
# Create a temporary file whose name is the user login and whose
# content is a generated token.
f = file(os.path.join(getOsTempFolder(), login), 'w')
f = file(os.path.join(sutils.getOsTempFolder(), login), 'w')
token = String().generatePassword()
f.write(token)
f.close()
@ -1277,7 +1287,7 @@ class ToolMixin(BaseMixin):
# Check if such token exists in temp folder
res = None
siteUrl = self.getSiteUrl()
tokenFile = os.path.join(getOsTempFolder(), login)
tokenFile = os.path.join(sutils.getOsTempFolder(), login)
if os.path.exists(tokenFile):
f = file(tokenFile)
storedToken = f.read()

View file

@ -7,7 +7,7 @@ import os, os.path, sys, types, urllib, cgi
from appy import Object
import appy.gen as gen
from appy.gen.utils import *
from appy.gen.layout import Table, defaultPageLayouts, ColumnLayout
from appy.gen.layout import Table, defaultPageLayouts
from appy.gen.descriptors import WorkflowDescriptor, ClassDescriptor
from appy.shared.utils import sequenceTypes,normalizeText,Traceback,getMimeType
from appy.shared.data import rtlLanguages
@ -169,7 +169,7 @@ class BaseMixin:
self.workflow_history[key] = tuple(history)
appy = self.appy()
self.log('Data change event deleted by %s for %s (UID=%s).' % \
(appy.user.getId(), appy.klass.__name__, appy.uid))
(appy.user.login, appy.klass.__name__, appy.uid))
self.goto(self.getUrl(rq['HTTP_REFERER']))
def onUnlink(self):
@ -213,13 +213,19 @@ class BaseMixin:
def view(self):
'''Returns the view PX.'''
appySelf = self.appy()
return appySelf.pxView({'self': appySelf})
obj = self.appy()
return obj.pxView({'obj': obj, 'tool': obj.tool})
def edit(self):
'''Returns the edit PX.'''
appySelf = self.appy()
return appySelf.pxEdit({'self': appySelf})
obj = self.appy()
return obj.pxEdit({'obj': obj, 'tool': obj.tool})
def ajax(self):
'''Called via an Ajax request to render some PX whose name is in the
request.'''
obj = self.appy()
return obj.pxAjax({'obj': obj, 'tool': obj.tool})
def setLock(self, user, page):
'''A p_user edits a given p_page on this object: we will set a lock, to
@ -232,7 +238,7 @@ class BaseMixin:
# Raise an error is the page is already locked by someone else. If the
# page is already locked by the same user, we don't mind: he could have
# used back/forward buttons of its browser...
userId = user.getId()
userId = user.login
if (page in self.locks) and (userId != self.locks[page][0]):
from AccessControl import Unauthorized
raise Unauthorized('This page is locked.')
@ -245,7 +251,7 @@ class BaseMixin:
mind and consider the page as unlocked. If the page is locked, this
method returns the tuple (userId, lockDate).'''
if hasattr(self.aq_base, 'locks') and (page in self.locks):
if (user.getId() != self.locks[page][0]): return self.locks[page]
if (user.login != self.locks[page][0]): return self.locks[page]
def removeLock(self, page, force=False):
'''Removes the lock on the current page. This happens:
@ -257,7 +263,7 @@ class BaseMixin:
# Raise an error if the user that saves changes is not the one that
# has locked the page (excepted if p_force is True)
if not force:
userId = self.getUser().getId()
userId = self.getTool().getUser().login
if self.locks[page][0] != userId:
from AccessControl import Unauthorized
raise Unauthorized('This page was locked by someone else.')
@ -270,7 +276,7 @@ class BaseMixin:
view.pt for this page. In this case, we consider that the user has
left the edit page in an unexpected way and we remove the lock.'''
if hasattr(self.aq_base, 'locks') and (page in self.locks) and \
(user.getId() == self.locks[page][0]):
(user.login == self.locks[page][0]):
del self.locks[page]
def onUnlock(self):
@ -420,11 +426,11 @@ class BaseMixin:
# previous pages may have changed). Moreover, previous and next
# pages may not be available in "edit" mode, so we return the edit
# or view pages depending on page.show.
phaseInfo = self.getAppyPhases(currentOnly=True, layoutType='edit')
pageName, pageInfo = self.getPreviousPage(phaseInfo, rq['page'])
phaseObj = self.getAppyPhases(currentOnly=True, layoutType='edit')
pageName, pageInfo = phaseObj.getPreviousPage(rq['page'])
if pageName:
# Return to the edit or view page?
if pageInfo['showOnEdit']:
if pageInfo.showOnEdit:
rq.set('page', pageName)
# I do not use gotoEdit here because I really need to
# redirect the user to the edit page. Indeed, the object
@ -440,11 +446,11 @@ class BaseMixin:
# We remember page name, because the next method may set a new
# current page if the current one is not visible anymore.
pageName = rq['page']
phaseInfo = self.getAppyPhases(currentOnly=True, layoutType='edit')
pageName, pageInfo = self.getNextPage(phaseInfo, pageName)
phaseObj = self.getAppyPhases(currentOnly=True, layoutType='edit')
pageName, pageInfo = phaseObj.getNextPage(pageName)
if pageName:
# Return to the edit or view page?
if pageInfo['showOnEdit']:
if pageInfo.showOnEdit:
# Same remark as above (click on "previous").
return self.goto(obj.getUrl(mode='edit', page=pageName))
else:
@ -543,7 +549,8 @@ class BaseMixin:
def addHistoryEvent(self, action, **kw):
'''Adds an event in the object history.'''
userId = self.getUser().getId()
user = self.getTool().getUser()
userId = user and user.login or 'system'
from DateTime import DateTime
event = {'action': action, 'actor': userId, 'time': DateTime(),
'comments': ''}
@ -603,13 +610,13 @@ class BaseMixin:
def gotoEdit(self):
'''Brings the user to the edit page for this object. This method takes
care of not carrying any password value. Unlike m_goto above, there
is no HTTP redirect here: we execute directly macro "edit" and we
is no HTTP redirect here: we execute directly PX "edit" and we
return the result.'''
page = self.REQUEST.get('page', 'main')
for field in self.getAppyTypes('edit', page):
if (field.type == 'String') and (field.format in (3,4)):
self.REQUEST.set(field.name, '')
return self.ui.edit(self)
return self.edit()
def showField(self, name, layoutType='view'):
'''Must I show field named p_name on this p_layoutType ?'''
@ -755,7 +762,7 @@ class BaseMixin:
res.update(parent)
return res
def getAppyType(self, name, asDict=False, className=None):
def getAppyType(self, name, className=None):
'''Returns the Appy type named p_name. If no p_className is defined, the
field is supposed to belong to self's class.'''
isInnerType = '*' in name # An inner type lies within a List type.
@ -770,7 +777,6 @@ class BaseMixin:
klass = self.getTool().getAppyClass(className, wrapper=True)
res = getattr(klass, name, None)
if res and isInnerType: res = res.getField(subName)
if res and asDict: return res.__dict__
return res
def getAllAppyTypes(self, className=None):
@ -782,40 +788,39 @@ class BaseMixin:
klass = self.getTool().getAppyClass(className, wrapper=True)
return klass.__fields__
def getGroupedAppyTypes(self, layoutType, pageName, cssJs=None):
'''Returns the fields sorted by group. For every field, the appyType
(dict version) is given. If a dict is given in p_cssJs, we will add
it in the css and js files required by the fields.'''
def getGroupedFields(self, layoutType, pageName, cssJs=None):
'''Returns the fields sorted by group. If a dict is given in p_cssJs,
we will add it in the css and js files required by the fields.'''
res = []
groups = {} # The already encountered groups
groups = {} # The already encountered groups.
# If a dict is given in p_cssJs, we must fill it with the CSS and JS
# files required for every returned appyType.
# files required for every returned field.
collectCssJs = isinstance(cssJs, dict)
css = js = None
# If param "refresh" is there, we must reload the Python class
refresh = ('refresh' in self.REQUEST)
if refresh:
klass = self.getClass(reloaded=True)
for appyType in self.getAllAppyTypes():
if refresh: appyType = appyType.reload(klass, self)
if appyType.page.name != pageName: continue
if not appyType.isShowable(self, layoutType): continue
for field in self.getAllAppyTypes():
if refresh: field = field.reload(klass, self)
if field.page.name != pageName: continue
if not field.isShowable(self, layoutType): continue
if collectCssJs:
if css == None: css = []
appyType.getCss(layoutType, css)
field.getCss(layoutType, css)
if js == None: js = []
appyType.getJs(layoutType, js)
if not appyType.group:
res.append(appyType.__dict__)
field.getJs(layoutType, js)
if not field.group:
res.append(field)
else:
# Insert the GroupDescr instance corresponding to
# appyType.group at the right place
groupDescr = appyType.group.insertInto(res, groups,
appyType.page, self.meta_type)
GroupDescr.addWidget(groupDescr, appyType.__dict__)
# Insert the UiGroup instance corresponding to field.group at
# the right place.
uiGroup = field.group.insertInto(res, groups, field.page,
self.meta_type)
uiGroup.addField(field)
if collectCssJs:
cssJs['css'] = css
cssJs['js'] = js
cssJs['css'] = css or ()
cssJs['js'] = js or ()
return res
def getAppyTypes(self, layoutType, pageName):
@ -852,23 +857,6 @@ class BaseMixin:
return klass.styles[elem]
return elem
def getColumnsSpecifiers(self, columnLayouts, dir):
'''Extracts and returns, from a list of p_columnLayouts, the information
that is necessary for displaying a column in a result screen or for
a Ref field.'''
res = []
tool = self.getTool()
for info in columnLayouts:
fieldName, width, align = ColumnLayout(info).get()
align = tool.flipLanguageDirection(align, dir)
field = self.getAppyType(fieldName, asDict=True)
if not field:
self.log('Field "%s", used in a column specifier, was not ' \
'found.' % fieldName, type='warning')
else:
res.append({'field':field, 'width':width, 'align': align})
return res
def getAppyTransitions(self, includeFake=True, includeNotShowable=False):
'''This method returns info about transitions that one can trigger from
the user interface.
@ -921,21 +909,21 @@ class BaseMixin:
# Get the list of phases
res = [] # Ordered list of phases
phases = {} # Dict of phases
for appyType in self.getAllAppyTypes():
typePhase = appyType.page.phase
if typePhase not in phases:
phase = PhaseDescr(typePhase, self)
res.append(phase.__dict__)
phases[typePhase] = phase
for field in self.getAllAppyTypes():
fieldPhase = field.page.phase
if fieldPhase not in phases:
phase = gen.Phase(fieldPhase, self)
res.append(phase)
phases[fieldPhase] = phase
else:
phase = phases[typePhase]
phase.addPage(appyType, self, layoutType)
if (appyType.type == 'Ref') and appyType.navigable:
phase.addPageLinks(appyType, self)
phase = phases[fieldPhase]
phase.addPage(field, self, layoutType)
if (field.type == 'Ref') and field.navigable:
phase.addPageLinks(field, self)
# Remove phases that have no visible page
for i in range(len(res)-1, -1, -1):
if not res[i]['pages']:
del phases[res[i]['name']]
if not res[i].pages:
del phases[res[i].name]
del res[i]
# Compute next/previous phases of every phase
for ph in phases.itervalues():
@ -948,16 +936,16 @@ class BaseMixin:
if not page:
if layoutType == 'edit': page = self.getDefaultEditPage()
else: page = self.getDefaultViewPage()
for phaseInfo in res:
if page in phaseInfo['pages']:
return phaseInfo
for phase in res:
if page in phase.pages:
return phase
# If I am here, it means that the page as defined in the request,
# or the default page, is not existing nor visible in any phase.
# In this case I find the first visible page among all phases.
viewAttr = 'showOn%s' % layoutType.capitalize()
for phase in res:
for page in phase['pages']:
if phase['pagesInfo'][page][viewAttr]:
for page in phase.pages:
if getattr(phase.pagesInfo[page], viewAttr):
rq.set('page', page)
pageFound = True
break
@ -965,9 +953,9 @@ class BaseMixin:
else:
# Return an empty list if we have a single, link-free page within
# a single phase.
if (len(res) == 1) and (len(res[0]['pages']) == 1) and \
not res[0]['pagesInfo'][res[0]['pages'][0]].get('links'):
return None
if (len(res) == 1) and (len(res[0].pages) == 1) and \
not res[0].pagesInfo[res[0].pages[0]].links:
return
return res
def getSupTitle(self, navInfo=''):
@ -983,87 +971,6 @@ class BaseMixin:
if hasattr(appyObj, 'getSubTitle'): return appyObj.getSubTitle()
return ''
def getPreviousPage(self, phase, page):
'''Returns the page that precedes p_page which is in p_phase.'''
try:
pageIndex = phase['pages'].index(page)
except ValueError:
# The current page is probably not visible anymore. Return the
# first available page in current phase.
res = phase['pages'][0]
return res, phase['pagesInfo'][res]
if pageIndex > 0:
# We stay on the same phase, previous page
res = phase['pages'][pageIndex-1]
resInfo = phase['pagesInfo'][res]
return res, resInfo
else:
if phase['previousPhase']:
# We go to the last page of previous phase
previousPhase = phase['previousPhase']
res = previousPhase['pages'][-1]
resInfo = previousPhase['pagesInfo'][res]
return res, resInfo
else:
return None, None
def getNextPage(self, phase, page):
'''Returns the page that follows p_page which is in p_phase.'''
try:
pageIndex = phase['pages'].index(page)
except ValueError:
# The current page is probably not visible anymore. Return the
# first available page in current phase.
res = phase['pages'][0]
return res, phase['pagesInfo'][res]
if pageIndex < len(phase['pages'])-1:
# We stay on the same phase, next page
res = phase['pages'][pageIndex+1]
resInfo = phase['pagesInfo'][res]
return res, resInfo
else:
if phase['nextPhase']:
# We go to the first page of next phase
nextPhase = phase['nextPhase']
res = nextPhase['pages'][0]
resInfo = nextPhase['pagesInfo'][res]
return res, resInfo
else:
return None, None
def changeRefOrder(self, fieldName, objectUid, newIndex, isDelta):
'''This method changes the position of object with uid p_objectUid in
reference field p_fieldName to p_newIndex i p_isDelta is False, or
to actualIndex+p_newIndex if p_isDelta is True.'''
refs = getattr(self.aq_base, fieldName, None)
oldIndex = refs.index(objectUid)
refs.remove(objectUid)
if isDelta:
newIndex = oldIndex + newIndex
else:
pass # To implement later on
refs.insert(newIndex, objectUid)
def onChangeRefOrder(self):
'''This method is called when the user wants to change order of an
item in a reference field.'''
rq = self.REQUEST
# Move the item up (-1), down (+1) ?
move = -1 # Move up
if rq['move'] == 'down':
move = 1 # Down
isDelta = True
self.changeRefOrder(rq['fieldName'], rq['refObjectUid'], move, isDelta)
def onSortReference(self):
'''This method is called when the user wants to sort the content of a
reference field.'''
rq = self.REQUEST
fieldName = rq.get('fieldName')
sortKey = rq.get('sortKey')
reverse = rq.get('reverse') == 'True'
self.appy().sort(fieldName, sortKey=sortKey, reverse=reverse)
def notifyWorkflowCreated(self):
'''This method is called every time an object is created, be it temp or
not. The objective here is to initialise workflow-related data on
@ -1589,9 +1496,9 @@ class BaseMixin:
getUrlDefaults = {'page':True, 'nav':True}
def getUrl(self, base=None, mode='view', **kwargs):
'''Returns a Appy URL.
'''Returns an URL for this object.
* If p_base is None, it will be the base URL for this object
(ie, self.absolute_url()).
(ie, Zope self.absolute_url()).
* p_mode can be "edit", "view" or "raw" (a non-param, base URL)
* p_kwargs can store additional parameters to add to the URL.
In this dict, every value that is a string will be added to the
@ -1600,7 +1507,7 @@ class BaseMixin:
param will not be included in the URL at all).'''
# Define the URL suffix
suffix = ''
if mode != 'raw': suffix = '/ui/%s' % mode
if mode != 'raw': suffix = '/%s' % mode
# Define base URL if omitted
if not base:
base = self.absolute_url() + suffix
@ -1609,9 +1516,8 @@ class BaseMixin:
if '?' in base: base = base[:base.index('?')]
base = base.strip('/')
for mode in ('view', 'edit'):
suffix = 'ui/%s' % mode
if base.endswith(suffix):
base = base[:-len(suffix)].strip('/')
if base.endswith(mode):
base = base[:-len(mode)].strip('/')
break
return base
# Manage default args
@ -1633,15 +1539,6 @@ 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 getTool(self):
'''Returns the application tool.'''
return self.getPhysicalRoot().config
@ -1659,7 +1556,8 @@ class BaseMixin:
data folder) it returns None.'''
parent = self.getParentNode()
# Not-Managers can't navigate back to the tool
if (parent.id == 'config') and not self.getUser().has_role('Manager'):
if (parent.id == 'config') and \
not self.getTool().getUser().has_role('Manager'):
return False
if parent.meta_type not in ('Folder', 'Temporary Folder'): return parent
@ -1678,7 +1576,7 @@ class BaseMixin:
return res
def index_html(self):
'''Redirects to /ui.'''
'''Redirects to /view.'''
rq = self.REQUEST
if rq.has_key('do'):
# The user wants to call a method on this object and get its result
@ -1688,10 +1586,6 @@ class BaseMixin:
# The user wants to consult the view page for this object
return rq.RESPONSE.redirect(self.getUrl())
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.'''
if not hasattr(self, 'REQUEST'): return 'en'
@ -1746,15 +1640,13 @@ class BaseMixin:
if not domain: domain = cfg.PROJECTNAME
# Get the label name, and the field-specific mapping if any.
if field:
# p_field is the dict version of a appy type or group
if field['type'] != 'group':
fieldMapping = field['mapping'][label]
if field.type != 'group':
fieldMapping = field.mapping[label]
if fieldMapping:
if callable(fieldMapping):
appyField = self.getAppyType(field['name'])
fieldMapping=appyField.callMethod(self,fieldMapping)
fieldMapping = field.callMethod(self, fieldMapping)
mapping.update(fieldMapping)
label = field['%sId' % label]
label = getattr(field, '%sId' % label)
# We will get the translation from a Translation object.
# In what language must we get the translation?
if not language: language = self.getUserLanguage()
@ -1855,11 +1747,11 @@ class BaseMixin:
def allows(self, permission, raiseError=False):
'''Has the logged user p_permission on p_self ?'''
hasPermission = self.getUser().has_permission(permission, self)
if not hasPermission and raiseError:
res = self.getTool().getUser().has_permission(permission, self)
if not res and raiseError:
from AccessControl import Unauthorized
raise Unauthorized
return hasPermission
return res
def getEditorInit(self, name):
'''Gets the Javascript init code for displaying a rich editor for