452 lines
20 KiB
Python
452 lines
20 KiB
Python
# ------------------------------------------------------------------------------
|
|
import re, os, os.path
|
|
from appy.shared.utils import normalizeText
|
|
from appy.px import Px
|
|
|
|
# Function for creating a Zope object ------------------------------------------
|
|
def createObject(folder, id, className, appName, wf=True, noSecurity=False):
|
|
'''Creates, in p_folder, object with some p_id. Object will be an instance
|
|
of p_className from application p_appName. In a very special case (the
|
|
creation of the config object), computing workflow-related info is not
|
|
possible at this time. This is why this function can be called with
|
|
p_wf=False.'''
|
|
exec 'from Products.%s.%s import %s as ZopeClass' % (appName, className,
|
|
className)
|
|
if not noSecurity:
|
|
# Check that the user can create objects of className
|
|
if folder.meta_type.endswith('Folder'): # Folder or temp folder.
|
|
tool = folder.config
|
|
else:
|
|
tool = folder.getTool()
|
|
user = tool.getUser()
|
|
userRoles = user.getRoles()
|
|
allowedRoles=ZopeClass.wrapperClass.getCreators(tool.getProductConfig())
|
|
allowed = False
|
|
for role in userRoles:
|
|
if role in allowedRoles:
|
|
allowed = True
|
|
break
|
|
if not allowed:
|
|
from AccessControl import Unauthorized
|
|
raise Unauthorized("User can't create instances of %s" % \
|
|
ZopeClass.__name__)
|
|
obj = ZopeClass(id)
|
|
folder._objects = folder._objects + \
|
|
({'id':id, 'meta_type':className},)
|
|
folder._setOb(id, obj)
|
|
obj = folder._getOb(id) # Important. Else, obj is not really in the folder.
|
|
obj.portal_type = className
|
|
obj.id = id
|
|
obj._at_uid = id
|
|
user = obj.getUser()
|
|
if not user.getId():
|
|
if user.name == 'System Processes':
|
|
userId = 'admin' # This is what happens when Zope is starting.
|
|
else:
|
|
userId = None # Anonymous.
|
|
else:
|
|
userId = user.getId()
|
|
obj.creator = userId or 'Anonymous User'
|
|
from DateTime import DateTime
|
|
obj.created = DateTime()
|
|
obj.modified = obj.created
|
|
obj.__ac_local_roles__ = { userId: ['Owner'] } # userId can be None (anon).
|
|
if wf: obj.notifyWorkflowCreated()
|
|
return obj
|
|
|
|
# Classes used by edit/view PXs for accessing information ----------------------
|
|
class Descr:
|
|
'''Abstract class for description classes.'''
|
|
def get(self): return self.__dict__
|
|
|
|
class GroupDescr(Descr):
|
|
'''Intermediary, on-the-fly-generated data structure that groups all fields
|
|
sharing the same appy.gen.Group instance, that some logged user can
|
|
see.'''
|
|
# PX that renders a group of fields
|
|
pxView = Px('''<p>pxGroupedFields</p>''')
|
|
|
|
# PX that renders a group of searches
|
|
pxViewSearches = Px('''
|
|
<x var="expanded=req.get(field.labelId, 'collapsed') == 'expanded'">
|
|
<!-- Group name, prefixed by the expand/collapse icon -->
|
|
<div class="portletGroup">
|
|
<img class="clickable" style="margin-right: 3px" align=":dleft"
|
|
id=":'%s_img' % field.labelId"
|
|
src=":expanded and url('collapse.gif') or url('expand.gif')"
|
|
onclick=":'toggleCookie(%s)' % q(field.labelId)"/>
|
|
<x if="not field.translated">:_(field.labelId)</x>
|
|
<x if="field.translated">:field.translated</x>
|
|
</div>
|
|
<!-- Group content -->
|
|
<div var="display=expanded and 'display:block' or 'display:none'"
|
|
id=":field.labelId" style=":'padding-left: 10px; %s' % display">
|
|
<x for="searches in field.widgets">
|
|
<x for="elem in searches">
|
|
<!-- An inner group within this group -->
|
|
<x if="elem['type'] == 'group'"
|
|
var2="field=elem">:field.pxViewSearches</x>
|
|
<!-- A search -->
|
|
<x if="elem['type'] != 'group'" var2="search=elem">:search.pxView</x>
|
|
</x>
|
|
</x>
|
|
</div>
|
|
</x>''')
|
|
|
|
def __init__(self, group, page, metaType, forSearch=False):
|
|
self.type = 'group'
|
|
# All p_group attributes become self attributes.
|
|
for name, value in group.__dict__.iteritems():
|
|
if not name.startswith('_'):
|
|
setattr(self, name, value)
|
|
self.columnsWidths = [col.width for col in group.columns]
|
|
self.columnsAligns = [col.align for col in group.columns]
|
|
# Names of i18n labels
|
|
labelName = self.name
|
|
prefix = metaType
|
|
if group.label:
|
|
if isinstance(group.label, basestring): prefix = group.label
|
|
else: # It is a tuple (metaType, name)
|
|
if group.label[1]: labelName = group.label[1]
|
|
if group.label[0]: prefix = group.label[0]
|
|
if forSearch: gp = 'searchgroup'
|
|
else: gp = 'group'
|
|
self.labelId = '%s_%s_%s' % (prefix, gp, labelName)
|
|
self.descrId = self.labelId + '_descr'
|
|
self.helpId = self.labelId + '_help'
|
|
# The name of the page where the group lies
|
|
self.page = page.name
|
|
# The widgets belonging to the group that the current user may see.
|
|
# They will be stored by m_addWidget below as a list of lists because
|
|
# they will be rendered as a table.
|
|
self.widgets = [[]]
|
|
# PX to user for rendering this group.
|
|
self.px = forSearch and self.pxViewSearches or self.pxView
|
|
|
|
@staticmethod
|
|
def addWidget(groupDict, newWidget):
|
|
'''Adds p_newWidget into p_groupDict['widgets']. We try first to add
|
|
p_newWidget into the last widget row. If it is not possible, we
|
|
create a new row.
|
|
|
|
This method is a static method taking p_groupDict as first param
|
|
instead of being an instance method because at this time the object
|
|
has already been converted to a dict (for being maniputated within
|
|
ZPTs).'''
|
|
# Get the last row
|
|
widgetRow = groupDict['widgets'][-1]
|
|
numberOfColumns = len(groupDict['columnsWidths'])
|
|
# Computes the number of columns already filled by widgetRow
|
|
rowColumns = 0
|
|
for widget in widgetRow: rowColumns += widget['colspan']
|
|
freeColumns = numberOfColumns - rowColumns
|
|
if freeColumns >= newWidget['colspan']:
|
|
# We can add the widget in the last row.
|
|
widgetRow.append(newWidget)
|
|
else:
|
|
if freeColumns:
|
|
# Terminate the current row by appending empty cells
|
|
for i in range(freeColumns): widgetRow.append('')
|
|
# Create a new row
|
|
newRow = [newWidget]
|
|
groupDict['widgets'].append(newRow)
|
|
|
|
class PhaseDescr(Descr):
|
|
'''Describes a phase.'''
|
|
|
|
pxPhase = Px('''
|
|
<tr var="singlePage=len(phase['pages']) == 1">
|
|
<td var="label='%s_phase_%s' % (zobj.meta_type, phase['name'])">
|
|
|
|
<!-- The title of the phase -->
|
|
<div class="portletGroup"
|
|
if="not singlePhase and not singlePage">::_(label)</div>
|
|
|
|
<!-- The page(s) within the phase -->
|
|
<x for="aPage in phase['pages']">
|
|
<!-- First line: page name and icons -->
|
|
<div if="not (singlePhase and singlePage)"
|
|
class=":aPage==page and 'portletCurrent portletPage' or \
|
|
'portletPage'">
|
|
<a href=":zobj.getUrl(page=aPage)">::_('%s_page_%s' % \
|
|
(zobj.meta_type, aPage))</a>
|
|
<x var="locked=zobj.isLocked(user, aPage);
|
|
editable=mayEdit and phase['pagesInfo'][aPage]['showOnEdit']">
|
|
<a if="editable and not locked"
|
|
href="zobj.getUrl(mode='edit', page=aPage)">
|
|
<img src=":url('edit')" title=":_('object_edit')"/></a>
|
|
<a if="editable and locked">
|
|
<img style="cursor: help"
|
|
var="lockDate=tool.formatDate(locked[1]);
|
|
lockMap={'user':ztool.getUserName(locked[0]), \
|
|
'date':lockDate};
|
|
lockMsg=_('page_locked', mapping=lockMap)"
|
|
src=":url('locked')" title=":lockMsg"/></a>
|
|
<a if="editable and locked and user.has_role('Manager')">
|
|
<img class="clickable" title=":_('page_unlock')" src=":url('unlock')"
|
|
onclick=":'onUnlockPage(%s,%s)' % \
|
|
(q(zobj.UID()), q(aPage))"/></a>
|
|
</x>
|
|
</div>
|
|
<!-- Next lines: links -->
|
|
<x var="links=phase['pagesInfo'][aPage].get('links')" if="links">
|
|
<div for="link in links">
|
|
<a href=":link['url']">:link['title']</a>
|
|
</div>
|
|
</x>
|
|
</x>
|
|
</td>
|
|
</tr>''')
|
|
|
|
def __init__(self, name, obj):
|
|
self.name = name
|
|
self.obj = obj
|
|
# The list of names of pages in this phase
|
|
self.pages = []
|
|
# The list of hidden pages in this phase
|
|
self.hiddenPages = []
|
|
# The dict below stores infor about every page listed in self.pages.
|
|
self.pagesInfo = {}
|
|
self.totalNbOfPhases = None
|
|
# The following attributes allows to browse, from a given page, to the
|
|
# last page of the previous phase and to the first page of the following
|
|
# phase if allowed by phase state.
|
|
self.previousPhase = None
|
|
self.nextPhase = None
|
|
self.px = self.pxPhase
|
|
|
|
def addPageLinks(self, appyType, obj):
|
|
'''If p_appyType is a navigable Ref, we must add, within self.pagesInfo,
|
|
those links.'''
|
|
if appyType.page.name in self.hiddenPages: return
|
|
infos = []
|
|
for obj in appyType.getValue(obj, type="zobjects"):
|
|
infos.append({'title': obj.title, 'url':obj.absolute_url()})
|
|
self.pagesInfo[appyType.page.name]['links'] = infos
|
|
|
|
def addPage(self, appyType, obj, layoutType):
|
|
'''Adds page-related information in the phase.'''
|
|
# If the page is already there, we have nothing more to do.
|
|
if (appyType.page.name in self.pages) or \
|
|
(appyType.page.name in self.hiddenPages): return
|
|
# Add the page only if it must be shown.
|
|
isShowableOnView = appyType.page.isShowable(obj, 'view')
|
|
isShowableOnEdit = appyType.page.isShowable(obj, 'edit')
|
|
if isShowableOnView or isShowableOnEdit:
|
|
# The page must be added.
|
|
self.pages.append(appyType.page.name)
|
|
# Create the dict about page information and add it in self.pageInfo
|
|
pageInfo = {'page': appyType.page,
|
|
'showOnView': isShowableOnView,
|
|
'showOnEdit': isShowableOnEdit}
|
|
pageInfo.update(appyType.page.getInfo(obj, layoutType))
|
|
self.pagesInfo[appyType.page.name] = pageInfo
|
|
else:
|
|
self.hiddenPages.append(appyType.page.name)
|
|
|
|
def computeNextPrevious(self, allPhases):
|
|
'''This method also fills fields "previousPhase" and "nextPhase"
|
|
if relevant, based on list of p_allPhases.'''
|
|
# Identify previous and next phases
|
|
for phaseInfo in allPhases:
|
|
if phaseInfo['name'] == self.name:
|
|
i = allPhases.index(phaseInfo)
|
|
if i > 0:
|
|
self.previousPhase = allPhases[i-1]
|
|
if i < (len(allPhases)-1):
|
|
self.nextPhase = allPhases[i+1]
|
|
|
|
class SearchDescr(Descr):
|
|
'''Describes a Search.'''
|
|
# PX for rendering a search.
|
|
pxView = Px('''
|
|
<div class="portletSearch">
|
|
<a href=":'%s?className=%s&search=%s' % \
|
|
(queryUrl, rootClass, search['name'])"
|
|
class=":search['name'] == currentSearch and 'portletCurrent' or ''"
|
|
title=":search['translatedDescr']">:search['translated']</a>
|
|
</div>''')
|
|
|
|
def __init__(self, search, className, tool):
|
|
self.search = search
|
|
self.name = search.name
|
|
self.type = 'search'
|
|
self.colspan = search.colspan
|
|
if search.translated:
|
|
self.translated = search.translated
|
|
self.translatedDescr = search.translatedDescr
|
|
else:
|
|
# The label may be specific in some special cases.
|
|
labelDescr = ''
|
|
if search.name == 'allSearch':
|
|
label = '%s_plural' % className
|
|
elif search.name == 'customSearch':
|
|
label = 'search_results'
|
|
else:
|
|
label = '%s_search_%s' % (className, search.name)
|
|
labelDescr = label + '_descr'
|
|
self.translated = tool.translate(label)
|
|
if labelDescr:
|
|
self.translatedDescr = tool.translate(labelDescr)
|
|
else:
|
|
self.translatedDescr = ''
|
|
self.px = self.pxView
|
|
|
|
# ------------------------------------------------------------------------------
|
|
upperLetter = re.compile('[A-Z]')
|
|
def produceNiceMessage(msg):
|
|
'''Transforms p_msg into a nice msg.'''
|
|
res = ''
|
|
if msg:
|
|
res = msg[0].upper()
|
|
for c in msg[1:]:
|
|
if c == '_':
|
|
res += ' '
|
|
elif upperLetter.match(c):
|
|
res += ' ' + c.lower()
|
|
else:
|
|
res += c
|
|
return res
|
|
|
|
# ------------------------------------------------------------------------------
|
|
class SomeObjects:
|
|
'''Represents a bunch of objects retrieved from a reference or a query in
|
|
the catalog.'''
|
|
def __init__(self, objects=None, batchSize=None, startNumber=0,
|
|
noSecurity=False):
|
|
self.objects = objects or [] # The objects
|
|
self.totalNumber = len(self.objects) # self.objects may only represent a
|
|
# part of all available objects.
|
|
self.batchSize = batchSize or self.totalNumber # The max length of
|
|
# self.objects.
|
|
self.startNumber = startNumber # The index of first object in
|
|
# self.objects in the whole list.
|
|
self.noSecurity = noSecurity
|
|
def brainsToObjects(self):
|
|
'''self.objects has been populated from brains from the catalog,
|
|
not from True objects. This method turns them (or some of them
|
|
depending on batchSize and startNumber) into real objects.
|
|
If self.noSecurity is True, it gets the objects even if the logged
|
|
user does not have the right to get them.'''
|
|
start = self.startNumber
|
|
brains = self.objects[start:start + self.batchSize]
|
|
if self.noSecurity: getMethod = '_unrestrictedGetObject'
|
|
else: getMethod = 'getObject'
|
|
self.objects = [getattr(b, getMethod)() for b in brains]
|
|
|
|
# ------------------------------------------------------------------------------
|
|
class Keywords:
|
|
'''This class allows to handle keywords that a user enters and that will be
|
|
used as basis for performing requests in a TextIndex/XhtmlIndex.'''
|
|
|
|
toRemove = '?-+*()'
|
|
def __init__(self, keywords, operator='AND'):
|
|
# Clean the p_keywords that the user has entered.
|
|
words = normalizeText(keywords)
|
|
if words == '*': words = ''
|
|
for c in self.toRemove: words = words.replace(c, ' ')
|
|
self.keywords = words.split()
|
|
# Store the operator to apply to the keywords (AND or OR)
|
|
self.operator = operator
|
|
|
|
def merge(self, other, append=False):
|
|
'''Merges our keywords with those from p_other. If p_append is True,
|
|
p_other keywords are appended at the end; else, keywords are appended
|
|
at the begin.'''
|
|
for word in other.keywords:
|
|
if word not in self.keywords:
|
|
if append:
|
|
self.keywords.append(word)
|
|
else:
|
|
self.keywords.insert(0, word)
|
|
|
|
def get(self):
|
|
'''Returns the keywords as needed by the TextIndex.'''
|
|
if self.keywords:
|
|
op = ' %s ' % self.operator
|
|
return op.join(self.keywords)+'*'
|
|
return ''
|
|
|
|
# ------------------------------------------------------------------------------
|
|
def getClassName(klass, appName=None):
|
|
'''Generates, from appy-class p_klass, the name of the corresponding
|
|
Zope class. For some classes, name p_appName is required: it is
|
|
part of the class name.'''
|
|
moduleName = klass.__module__
|
|
if (moduleName == 'appy.gen.model') or moduleName.endswith('.wrappers'):
|
|
# This is a model (generation time or run time)
|
|
res = appName + klass.__name__
|
|
elif klass.__bases__ and (klass.__bases__[-1].__module__ == 'appy.gen'):
|
|
# This is a customized class (inherits from appy.gen.Tool, User,...)
|
|
res = appName + klass.__bases__[-1].__name__
|
|
else: # This is a standard class
|
|
res = klass.__module__.replace('.', '_') + '_' + klass.__name__
|
|
return res
|
|
|
|
# ------------------------------------------------------------------------------
|
|
def updateRolesForPermission(permission, roles, obj):
|
|
'''Adds roles from list p_roles to the list of roles that are granted
|
|
p_permission on p_obj.'''
|
|
from AccessControl.Permission import Permission
|
|
# Find existing roles that were granted p_permission on p_obj
|
|
existingRoles = ()
|
|
for p in obj.ac_inherited_permissions(1):
|
|
name, value = p[:2]
|
|
if name == permission:
|
|
perm = Permission(name, value, obj)
|
|
existingRoles = perm.getRoles()
|
|
allRoles = set(existingRoles).union(roles)
|
|
obj.manage_permission(permission, tuple(allRoles), acquire=0)
|
|
|
|
# ------------------------------------------------------------------------------
|
|
def callMethod(obj, method, klass=None, cache=True):
|
|
'''This function is used to call a p_method on some Appy p_obj. m_method
|
|
can be an instance method on p_obj; it can also be a static method. In
|
|
this latter case, p_obj is the tool and the static method, defined in
|
|
p_klass, will be called with the tool as unique arg.
|
|
|
|
A method cache is implemented on the request object (available at
|
|
p_obj.request). So while handling a single request from the ui, every
|
|
method is called only once. Some method calls must not be cached (ie,
|
|
values of Computed fields). In this case, p_cache will be False.'''
|
|
rq = obj.request
|
|
# Create the method cache if it does not exist on the request
|
|
if not hasattr(rq, 'methodCache'): rq.methodCache = {}
|
|
# If m_method is a static method or an instance method, unwrap the true
|
|
# Python function object behind it.
|
|
methodType = method.__class__.__name__
|
|
if methodType == 'staticmethod':
|
|
method = method.__get__(klass)
|
|
elif methodType == 'instancemethod':
|
|
method = method.im_func
|
|
# Call the method if cache is not needed.
|
|
if not cache: return method(obj)
|
|
# If first arg of method is named "tool" instead of the traditional "self",
|
|
# we cheat and will call the method with the tool as first arg. This will
|
|
# allow to consider this method as if it was a static method on the tool.
|
|
# Every method call, even on different instances, will be cached in a unique
|
|
# key.
|
|
cheat = False
|
|
if not klass and (method.func_code.co_varnames[0] == 'tool'):
|
|
prefix = obj.klass.__name__
|
|
obj = obj.tool
|
|
cheat = True
|
|
# Build the key of this method call in the cache.
|
|
# First part of the key: the p_obj's uid (if p_method is an instance method)
|
|
# or p_className (if p_method is a static method).
|
|
if not cheat:
|
|
if klass:
|
|
prefix = klass.__name__
|
|
else:
|
|
prefix = obj.uid
|
|
# Second part of the key: p_method name
|
|
key = '%s:%s' % (prefix, method.func_name)
|
|
# Return the cached value if present in the method cache.
|
|
if key in rq.methodCache:
|
|
return rq.methodCache[key]
|
|
# No cached value: call the method, cache the result and return it
|
|
res = method(obj)
|
|
rq.methodCache[key] = res
|
|
return res
|
|
# ------------------------------------------------------------------------------
|