[gen] Added a mechanism for caching method calls that are performed several times for displaying a single ui page (ie: field.show methods).

This commit is contained in:
Gaetan Delannay 2013-06-10 00:13:29 +02:00
parent b12ea0a64d
commit 244826194b
4 changed files with 100 additions and 52 deletions

View file

@ -7,14 +7,13 @@ from appy.gen.layout import Table
from appy.gen.layout import defaultFieldLayouts from appy.gen.layout import defaultFieldLayouts
from appy.gen.mail import sendNotification from appy.gen.mail import sendNotification
from appy.gen.indexer import defaultIndexes, XhtmlTextExtractor from appy.gen.indexer import defaultIndexes, XhtmlTextExtractor
from appy.gen.utils import GroupDescr, Keywords, getClassName, SomeObjects from appy.gen import utils as gutils
import appy.pod import appy.pod
from appy.pod.renderer import Renderer from appy.pod.renderer import Renderer
from appy.shared.data import countries from appy.shared.data import countries
from appy.shared.xml_parser import XhtmlCleaner from appy.shared.xml_parser import XhtmlCleaner
from appy.shared.diff import HtmlDiff from appy.shared.diff import HtmlDiff
from appy.shared.utils import Traceback, getOsTempFolder, formatNumber, \ from appy.shared import utils as sutils
FileWrapper, sequenceTypes
# Default Appy permissions ----------------------------------------------------- # Default Appy permissions -----------------------------------------------------
r, w, d = ('read', 'write', 'delete') r, w, d = ('read', 'write', 'delete')
@ -30,7 +29,7 @@ labelTypes = ('label', 'descr', 'help')
def initMasterValue(v): def initMasterValue(v):
'''Standardizes p_v as a list of strings.''' '''Standardizes p_v as a list of strings.'''
if not isinstance(v, bool) and not v: res = [] if not isinstance(v, bool) and not v: res = []
elif type(v) not in sequenceTypes: res = [v] elif type(v) not in sutils.sequenceTypes: res = [v]
else: res = v else: res = v
return [str(v) for v in res] return [str(v) for v in res]
@ -249,8 +248,8 @@ class Group:
# First, create the corresponding GroupDescr if not already in # First, create the corresponding GroupDescr if not already in
# p_groupDescrs. # p_groupDescrs.
if self.name not in groupDescrs: if self.name not in groupDescrs:
groupDescr = groupDescrs[self.name] = \ groupDescr = groupDescrs[self.name] = gutils.GroupDescr(\
GroupDescr(self, page, metaType, forSearch=forSearch).get() self, page, metaType, forSearch=forSearch).get()
# Insert the group at the higher level (ie, directly in p_widgets) # Insert the group at the higher level (ie, directly in p_widgets)
# if the group is not itself in a group. # if the group is not itself in a group.
if not self.group: if not self.group:
@ -258,7 +257,7 @@ class Group:
else: else:
outerGroupDescr = self.group.insertInto(widgets, groupDescrs, outerGroupDescr = self.group.insertInto(widgets, groupDescrs,
page, metaType, forSearch=forSearch) page, metaType, forSearch=forSearch)
GroupDescr.addWidget(outerGroupDescr, groupDescr) gutils.GroupDescr.addWidget(outerGroupDescr, groupDescr)
else: else:
groupDescr = groupDescrs[self.name] groupDescr = groupDescrs[self.name]
return groupDescr return groupDescr
@ -348,12 +347,12 @@ class Search:
if (field and (field.getIndexType() == 'TextIndex')) or \ if (field and (field.getIndexType() == 'TextIndex')) or \
(fieldName == 'SearchableText'): (fieldName == 'SearchableText'):
# For TextIndex indexes. We must split p_fieldValue into keywords. # For TextIndex indexes. We must split p_fieldValue into keywords.
res = Keywords(fieldValue).get() res = gutils.Keywords(fieldValue).get()
elif isinstance(fieldValue, basestring) and fieldValue.endswith('*'): elif isinstance(fieldValue, basestring) and fieldValue.endswith('*'):
v = fieldValue[:-1] v = fieldValue[:-1]
# Warning: 'z' is higher than 'Z'! # Warning: 'z' is higher than 'Z'!
res = {'query':(v,v+'z'), 'range':'min:max'} res = {'query':(v,v+'z'), 'range':'min:max'}
elif type(fieldValue) in sequenceTypes: elif type(fieldValue) in sutils.sequenceTypes:
if fieldValue and isinstance(fieldValue[0], basestring): if fieldValue and isinstance(fieldValue[0], basestring):
# We have a list of string values (ie: we need to # We have a list of string values (ie: we need to
# search v1 or v2 or...) # search v1 or v2 or...)
@ -410,7 +409,7 @@ class Search:
def isShowable(self, klass, tool): def isShowable(self, klass, tool):
'''Is this Search instance (defined in p_klass) showable?''' '''Is this Search instance (defined in p_klass) showable?'''
if self.show.__class__.__name__ == 'staticmethod': if self.show.__class__.__name__ == 'staticmethod':
return self.show.__get__(klass)(tool) return gutils.callMethod(tool, self.show, klass=klass)
return self.show return self.show
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@ -540,7 +539,7 @@ class Type:
self.name = name self.name = name
# Determine prefix for this class # Determine prefix for this class
if not klass: prefix = appName if not klass: prefix = appName
else: prefix = getClassName(klass, appName) else: prefix = gutils.getClassName(klass, appName)
# Recompute the ID (and derived attributes) that may have changed if # Recompute the ID (and derived attributes) that may have changed if
# we are in debug mode (because we recreate new Type instances). # we are in debug mode (because we recreate new Type instances).
self.id = id(self) self.id = id(self)
@ -628,7 +627,7 @@ class Type:
else: else:
res = self.show res = self.show
# Take into account possible values 'view', 'edit', 'result'... # Take into account possible values 'view', 'edit', 'result'...
if type(res) in sequenceTypes: if type(res) in sutils.sequenceTypes:
for r in res: for r in res:
if r == layoutType: return True if r == layoutType: return True
return False return False
@ -645,7 +644,7 @@ class Type:
master, masterValue = masterData master, masterValue = masterData
reqValue = master.getRequestValue(obj.REQUEST) reqValue = master.getRequestValue(obj.REQUEST)
# reqValue can be a list or not # reqValue can be a list or not
if type(reqValue) not in sequenceTypes: if type(reqValue) not in sutils.sequenceTypes:
return reqValue in masterValue return reqValue in masterValue
else: else:
for m in masterValue: for m in masterValue:
@ -847,7 +846,7 @@ class Type:
if forSearch and (value != None): if forSearch and (value != None):
if isinstance(value, unicode): if isinstance(value, unicode):
res = value.encode('utf-8') res = value.encode('utf-8')
elif type(value) in sequenceTypes: elif type(value) in sutils.sequenceTypes:
res = [] res = []
for v in value: for v in value:
if isinstance(v, unicode): res.append(v.encode('utf-8')) if isinstance(v, unicode): res.append(v.encode('utf-8'))
@ -966,7 +965,7 @@ class Type:
p_self type definition on p_obj.''' p_self type definition on p_obj.'''
setattr(obj, self.name, value) setattr(obj, self.name, value)
def callMethod(self, obj, method, raiseOnError=True): def callMethod(self, obj, method, cache=True):
'''This method is used to call a p_method on p_obj. p_method is part of '''This method is used to call a p_method on p_obj. p_method is part of
this type definition (ie a default method, the method of a Computed this type definition (ie a default method, the method of a Computed
field, a method used for showing or not a field...). Normally, those field, a method used for showing or not a field...). Normally, those
@ -975,26 +974,22 @@ class Type:
p_method with no arg *or* with the field arg.''' p_method with no arg *or* with the field arg.'''
obj = obj.appy() obj = obj.appy()
try: try:
return method(obj) return gutils.callMethod(obj, method, cache=cache)
except TypeError, te: except TypeError, te:
# Try a version of the method that would accept self as an # Try a version of the method that would accept self as an
# additional parameter. # additional parameter. In this case, we do not try to cache the
tb = Traceback.get() # value (we do not call gutils.callMethod), because the value may
# be different depending on the parameter.
tb = sutils.Traceback.get()
try: try:
return method(obj, self) return method(obj, self)
except Exception, e: except Exception, e:
obj.log(tb, type='error') obj.log(tb, type='error')
if raiseOnError: # Raise the initial error.
# Raise the initial error. raise te
raise te
else:
return str(te)
except Exception, e: except Exception, e:
obj.log(Traceback.get(), type='error') obj.log(sutils.Traceback.get(), type='error')
if raiseOnError: raise e
raise e
else:
return str(e)
def process(self, obj): def process(self, obj):
'''This method is a general hook allowing a field to perform some '''This method is a general hook allowing a field to perform some
@ -1047,7 +1042,7 @@ class Float(Type):
self.precision = precision self.precision = precision
# The decimal separator can be a tuple if several are allowed, ie # The decimal separator can be a tuple if several are allowed, ie
# ('.', ',') # ('.', ',')
if type(sep) not in sequenceTypes: if type(sep) not in sutils.sequenceTypes:
self.sep = (sep,) self.sep = (sep,)
else: else:
self.sep = sep self.sep = sep
@ -1065,8 +1060,8 @@ class Float(Type):
self.pythonType = float self.pythonType = float
def getFormattedValue(self, obj, value, showChanges=False): def getFormattedValue(self, obj, value, showChanges=False):
return formatNumber(value, sep=self.sep[0], precision=self.precision, return sutils.formatNumber(value, sep=self.sep[0],
tsep=self.tsep) precision=self.precision, tsep=self.tsep)
def validateValue(self, obj, value): def validateValue(self, obj, value):
# Replace used separator with the Python separator '.' # Replace used separator with the Python separator '.'
@ -1487,7 +1482,7 @@ class String(Type):
value = value[:self.maxChars] value = value[:self.maxChars]
# Get a multivalued value if required. # Get a multivalued value if required.
if value and self.isMultiValued() and \ if value and self.isMultiValued() and \
(type(value) not in sequenceTypes): (type(value) not in sutils.sequenceTypes):
value = [value] value = [value]
return value return value
@ -1701,12 +1696,12 @@ class File(Type):
res.filename = fileName res.filename = fileName
res.content_type = mimetypes.guess_type(fileName)[0] res.content_type = mimetypes.guess_type(fileName)[0]
f.close() f.close()
if not zope: res = FileWrapper(res) if not zope: res = sutils.FileWrapper(res)
return res return res
def getValue(self, obj): def getValue(self, obj):
value = Type.getValue(self, obj) value = Type.getValue(self, obj)
if value: value = FileWrapper(value) if value: value = sutils.FileWrapper(value)
return value return value
def getFormattedValue(self, obj, value, showChanges=False): def getFormattedValue(self, obj, value, showChanges=False):
@ -1786,11 +1781,11 @@ class File(Type):
setattr(obj, self.name, existingValue) setattr(obj, self.name, existingValue)
elif isinstance(value, OFSImageFile): elif isinstance(value, OFSImageFile):
setattr(obj, self.name, value) setattr(obj, self.name, value)
elif isinstance(value, FileWrapper): elif isinstance(value, sutils.FileWrapper):
setattr(obj, self.name, value._zopeFile) setattr(obj, self.name, value._zopeFile)
elif isinstance(value, basestring): elif isinstance(value, basestring):
setattr(obj, self.name, File.getFileObject(value, zope=True)) setattr(obj, self.name, File.getFileObject(value, zope=True))
elif type(value) in sequenceTypes: elif type(value) in sutils.sequenceTypes:
# It should be a 2-tuple or 3-tuple # It should be a 2-tuple or 3-tuple
fileName = None fileName = None
mimeType = None mimeType = None
@ -1956,13 +1951,13 @@ class Ref(Type):
if defValue: if defValue:
# I must prefix call to function "type" with "__builtins__" # I must prefix call to function "type" with "__builtins__"
# because this name was overridden by a method parameter. # because this name was overridden by a method parameter.
if __builtins__['type'](defValue) in sequenceTypes: if __builtins__['type'](defValue) in sutils.sequenceTypes:
uids = [o.o.UID() for o in defValue] uids = [o.o.UID() for o in defValue]
else: else:
uids = [defValue.o.UID()] uids = [defValue.o.UID()]
# Prepare the result: an instance of SomeObjects, that will be unwrapped # Prepare the result: an instance of SomeObjects, that will be unwrapped
# if not required. # if not required.
res = SomeObjects() res = gutils.SomeObjects()
res.totalNumber = res.batchSize = len(uids) res.totalNumber = res.batchSize = len(uids)
batchNeeded = startNumber != None batchNeeded = startNumber != None
if batchNeeded: if batchNeeded:
@ -2040,7 +2035,7 @@ class Ref(Type):
'''This method links p_value (which can be a list of objects) to p_obj '''This method links p_value (which can be a list of objects) to p_obj
through this Ref field.''' through this Ref field.'''
# p_value can be a list of objects # p_value can be a list of objects
if type(value) in sequenceTypes: if type(value) in sutils.sequenceTypes:
for v in value: self.linkObject(obj, v, back=back) for v in value: self.linkObject(obj, v, back=back)
return return
# Gets the list of referred objects (=list of uids), or create it. # Gets the list of referred objects (=list of uids), or create it.
@ -2068,7 +2063,7 @@ class Ref(Type):
'''This method unlinks p_value (which can be a list of objects) from '''This method unlinks p_value (which can be a list of objects) from
p_obj through this Ref field.''' p_obj through this Ref field.'''
# p_value can be a list of objects # p_value can be a list of objects
if type(value) in sequenceTypes: if type(value) in sutils.sequenceTypes:
for v in value: self.unlinkObject(obj, v, back=back) for v in value: self.unlinkObject(obj, v, back=back)
return return
obj = obj.o obj = obj.o
@ -2093,7 +2088,7 @@ class Ref(Type):
# Standardize p_value into a list of Zope objects # Standardize p_value into a list of Zope objects
objects = value objects = value
if not objects: objects = [] if not objects: objects = []
if type(objects) not in sequenceTypes: objects = [objects] if type(objects) not in sutils.sequenceTypes: objects = [objects]
tool = obj.getTool() tool = obj.getTool()
for i in range(len(objects)): for i in range(len(objects)):
if isinstance(objects[i], basestring): if isinstance(objects[i], basestring):
@ -2234,7 +2229,7 @@ class Computed(Type):
return self.callMacro(obj, self.method) return self.callMacro(obj, self.method)
else: else:
# self.method is a method that will return the field value # self.method is a method that will return the field value
return self.callMethod(obj, self.method, raiseOnError=True) return self.callMethod(obj, self.method, cache=False)
def getFormattedValue(self, obj, value, showChanges=False): def getFormattedValue(self, obj, value, showChanges=False):
if not isinstance(value, basestring): return str(value) if not isinstance(value, basestring): return str(value)
@ -2278,12 +2273,12 @@ class Action(Type):
def getDefaultLayouts(self): return {'view': 'l-f', 'edit': 'lrv-f'} def getDefaultLayouts(self): return {'view': 'l-f', 'edit': 'lrv-f'}
def __call__(self, obj): def __call__(self, obj):
'''Calls the action on p_obj.''' '''Calls the action on p_obj.'''
if type(self.action) in sequenceTypes: if type(self.action) in sutils.sequenceTypes:
# There are multiple Python methods # There are multiple Python methods
res = [True, ''] res = [True, '']
for act in self.action: for act in self.action:
actRes = act(obj) actRes = act(obj)
if type(actRes) in sequenceTypes: if type(actRes) in sutils.sequenceTypes:
res[0] = res[0] and actRes[0] res[0] = res[0] and actRes[0]
if self.result.startswith('file'): if self.result.startswith('file'):
res[1] = res[1] + actRes[1] res[1] = res[1] + actRes[1]
@ -2294,7 +2289,7 @@ class Action(Type):
else: else:
# There is only one Python method # There is only one Python method
actRes = self.action(obj) actRes = self.action(obj)
if type(actRes) in sequenceTypes: if type(actRes) in sutils.sequenceTypes:
res = list(actRes) res = list(actRes)
else: else:
res = [actRes, ''] res = [actRes, '']
@ -2395,7 +2390,9 @@ class Pod(Type):
retrieved by calling pod to compute the result.''' retrieved by calling pod to compute the result.'''
rq = getattr(obj, 'REQUEST', None) rq = getattr(obj, 'REQUEST', None)
res = getattr(obj.aq_base, self.name, None) res = getattr(obj.aq_base, self.name, None)
if res and res.size: return FileWrapper(res) # Return the frozen file. if res and res.size:
# Return the frozen file.
return sutils.FileWrapper(res)
# If we are here, it means that we must call pod to compute the file. # If we are here, it means that we must call pod to compute the file.
# A Pod field differs from other field types because there can be # A Pod field differs from other field types because there can be
# several ways to produce the field value (ie: output file format can be # several ways to produce the field value (ie: output file format can be
@ -2419,7 +2416,7 @@ class Pod(Type):
specificContext = self.context specificContext = self.context
# Temporary file where to generate the result # Temporary file where to generate the result
tempFileName = '%s/%s_%f.%s' % ( tempFileName = '%s/%s_%f.%s' % (
getOsTempFolder(), obj.uid, time.time(), outputFormat) sutils.getOsTempFolder(), obj.uid, time.time(), outputFormat)
# Define parameters to give to the appy.pod renderer # Define parameters to give to the appy.pod renderer
podContext = {'tool': tool, 'user': obj.user, 'self': obj, 'field':self, podContext = {'tool': tool, 'user': obj.user, 'self': obj, 'field':self,
'now': obj.o.getProductConfig().DateTime(), 'now': obj.o.getProductConfig().DateTime(),
@ -2486,7 +2483,7 @@ class Pod(Type):
def store(self, obj, value): def store(self, obj, value):
'''Stores (=freezes) a document (in p_value) in the field.''' '''Stores (=freezes) a document (in p_value) in the field.'''
if isinstance(value, FileWrapper): if isinstance(value, sutils.FileWrapper):
value = value._zopeFile value = value._zopeFile
setattr(obj, self.name, value) setattr(obj, self.name, value)

View file

@ -164,7 +164,7 @@ class Calendar(Type):
'''Returns the list of other calendars whose events must also be shown '''Returns the list of other calendars whose events must also be shown
on this calendar.''' on this calendar.'''
if self.otherCalendars: if self.otherCalendars:
res = self.callMethod(obj, self.otherCalendars, preComputed) res = self.otherCalendars(obj.appy(), preComputed)
# Replace field names with field objects # Replace field names with field objects
for i in range(len(res)): for i in range(len(res)):
res[i][1] = res[i][0].getField(res[i][1]) res[i][1] = res[i][0].getField(res[i][1])

View file

@ -306,4 +306,55 @@ def updateRolesForPermission(permission, roles, obj):
existingRoles = perm.getRoles() existingRoles = perm.getRoles()
allRoles = set(existingRoles).union(roles) allRoles = set(existingRoles).union(roles)
obj.manage_permission(permission, tuple(allRoles), acquire=0) 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
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -13,7 +13,7 @@ class UserWrapper(AbstractWrapper):
if self.user.has_role('Manager'): return True if self.user.has_role('Manager'): return True
return ('view', 'result') return ('view', 'result')
def showName(self): def showName(tool):
'''Name and first name, by default, are always shown.''' '''Name and first name, by default, are always shown.'''
return True return True
@ -23,9 +23,9 @@ class UserWrapper(AbstractWrapper):
email = self.email email = self.email
return email and (email != self.login) return email and (email != self.login)
def showRoles(self): def showRoles(tool):
'''Only the admin can view or edit roles.''' '''Only the admin can view or edit roles.'''
return self.user.has_role('Manager') return tool.user.has_role('Manager')
def validateLogin(self, login): def validateLogin(self, login):
'''Is this p_login valid?''' '''Is this p_login valid?'''