Eradicated Flavour and PodTemplate classes (for the latter, use Pod fields instead); Added a code analyser; Groups can now be slaves in master/slaves relationships; Refs have more params (show a confirmation popup before adding an object, add an object without creation form); Code for Refs has been refactored to comply with the new way to organize Types; Added a WebDAV client library.

This commit is contained in:
Gaetan Delannay 2010-10-14 14:43:56 +02:00
parent 9f4db88bdf
commit 990e16c6e7
47 changed files with 1006 additions and 1297 deletions

View file

@ -1,7 +0,0 @@
# ------------------------------------------------------------------------------
from appy.gen.plone25.mixins import AbstractMixin
# ------------------------------------------------------------------------------
class ClassMixin(AbstractMixin):
_appy_meta_type = 'Class'
# ------------------------------------------------------------------------------

View file

@ -1,263 +0,0 @@
# ------------------------------------------------------------------------------
import os, os.path, time, types
from StringIO import StringIO
from appy.shared import mimeTypes
from appy.shared.utils import getOsTempFolder
import appy.pod
from appy.pod.renderer import Renderer
import appy.gen
from appy.gen import Type
from appy.gen.plone25.mixins import AbstractMixin
from appy.gen.plone25.descriptors import ClassDescriptor
# Errors -----------------------------------------------------------------------
DELETE_TEMP_DOC_ERROR = 'A temporary document could not be removed. %s.'
POD_ERROR = 'An error occurred while generating the document. Please ' \
'contact the system administrator.'
# ------------------------------------------------------------------------------
class FlavourMixin(AbstractMixin):
_appy_meta_type = 'Flavour'
def getPortalType(self, metaTypeOrAppyClass):
'''Returns the name of the portal_type that is based on
p_metaTypeOrAppyType in this flavour.'''
return self.getParentNode().getPortalType(metaTypeOrAppyClass)
def registerPortalTypes(self):
'''Registers, into portal_types, the portal types which are specific
to this flavour.'''
i = -1
registeredFactoryTypes = self.portal_factory.getFactoryTypes().keys()
factoryTypesToRegister = []
appName = self.getProductConfig().PROJECTNAME
for metaTypeName in self.allMetaTypes:
i += 1
portalTypeName = '%s_%d' % (metaTypeName, self.number)
# If the portal type corresponding to the meta type is
# registered in portal_factory (in the model:
# use_portal_factory=True), we must also register the new
# portal_type we are currently creating.
if metaTypeName in registeredFactoryTypes:
factoryTypesToRegister.append(portalTypeName)
if not hasattr(self.portal_types, portalTypeName) and \
hasattr(self.portal_types, metaTypeName):
# Indeed abstract meta_types have no associated portal_type
typeInfoName = "%s: %s (%s)" % (appName, metaTypeName,
metaTypeName)
self.portal_types.manage_addTypeInformation(
getattr(self.portal_types, metaTypeName).meta_type,
id=portalTypeName, typeinfo_name=typeInfoName)
# Set the human readable title explicitly
portalType = getattr(self.portal_types, portalTypeName)
portalType.title = portalTypeName
# Associate a workflow for this new portal type.
pf = self.portal_workflow
workflowChain = pf.getChainForPortalType(metaTypeName)
pf.setChainForPortalTypes([portalTypeName],workflowChain)
# Copy actions from the base portal type
basePortalType = getattr(self.portal_types, metaTypeName)
portalType._actions = tuple(basePortalType._cloneActions())
# Copy aliases from the base portal type
portalType.setMethodAliases(basePortalType.getMethodAliases())
# Update the factory tool with the list of types to register
self.portal_factory.manage_setPortalFactoryTypes(
listOfTypeIds=factoryTypesToRegister+registeredFactoryTypes)
def getClassFolder(self, className):
'''Return the folder related to p_className.'''
return getattr(self, className)
def getAvailablePodTemplates(self, obj, phase='main'):
'''Returns the POD templates which are available for generating a
document from p_obj.'''
appySelf = self.appy()
fieldName = 'podTemplatesFor%s' % obj.meta_type
res = []
podTemplates = getattr(appySelf, fieldName, [])
if not isinstance(podTemplates, list):
podTemplates = [podTemplates]
res = [r.o for r in podTemplates if r.podPhase == phase]
hasParents = True
klass = obj.__class__
while hasParents:
parent = klass.__bases__[-1]
if hasattr(parent, 'wrapperClass'):
fieldName = 'podTemplatesFor%s' % parent.meta_type
podTemplates = getattr(appySelf, fieldName, [])
if not isinstance(podTemplates, list):
podTemplates = [podTemplates]
res += [r.o for r in podTemplates if r.podPhase == phase]
klass = parent
else:
hasParents = False
return res
def getMaxShownTemplates(self, obj):
attrName = 'getPodMaxShownTemplatesFor%s' % obj.meta_type
return getattr(self, attrName)()
def getPodInfo(self, ploneObj, fieldName):
'''Returns POD-related information about Pod field p_fieldName defined
on class whose p_ploneObj is an instance of.'''
res = {}
appyClass = self.getParentNode().getAppyClass(ploneObj.meta_type)
appyFlavour = self.appy()
n = appyFlavour.getAttributeName('formats', appyClass, fieldName)
res['formats'] = getattr(appyFlavour, n)
n = appyFlavour.getAttributeName('podTemplate', appyClass, fieldName)
res['template'] = getattr(appyFlavour, n)
appyType = ploneObj.getAppyType(fieldName)
res['title'] = self.translate(appyType.labelId)
res['context'] = appyType.context
res['action'] = appyType.action
return res
def generateDocument(self):
'''Generates the document:
- from a PodTemplate instance if it is a class-wide pod template;
- from field-related info on the flavour if it is a Pod field.
UID of object that is the template target is given in the request.'''
rq = self.REQUEST
appyTool = self.getParentNode().appy()
# Get the object
objectUid = rq.get('objectUid')
obj = self.uid_catalog(UID=objectUid)[0].getObject()
appyObj = obj.appy()
# Get information about the document to render. Information comes from
# a PodTemplate instance or from the flavour itself, depending on
# whether we generate a doc from a class-wide template or from a pod
# field.
templateUid = rq.get('templateUid', None)
specificPodContext = None
if templateUid:
podTemplate = self.uid_catalog(UID=templateUid)[0].getObject()
appyPt = podTemplate.appy()
format = podTemplate.getPodFormat()
template = appyPt.podTemplate.content
podTitle = podTemplate.Title()
doAction = False
else:
fieldName = rq.get('fieldName')
format = rq.get('podFormat')
podInfo = self.getPodInfo(obj, fieldName)
template = podInfo['template'].content
podTitle = podInfo['title']
if podInfo['context']:
if type(podInfo['context']) == types.FunctionType:
specificPodContext = podInfo['context'](appyObj)
else:
specificPodContext = podInfo['context']
doAction = rq.get('askAction') == 'True'
# Temporary file where to generate the result
tempFileName = '%s/%s_%f.%s' % (
getOsTempFolder(), obj.UID(), time.time(), format)
# Define parameters to pass to the appy.pod renderer
currentUser = self.portal_membership.getAuthenticatedMember()
podContext = {'tool': appyTool, 'flavour': self.appy(),
'user': currentUser, 'self': appyObj,
'now': self.getProductConfig().DateTime(),
'projectFolder': appyTool.getDiskFolder(),
}
if specificPodContext:
podContext.update(specificPodContext)
if templateUid:
podContext['podTemplate'] = appyPt
rendererParams = {'template': StringIO(template),
'context': podContext,
'result': tempFileName}
if appyTool.unoEnabledPython:
rendererParams['pythonWithUnoPath'] = appyTool.unoEnabledPython
if appyTool.openOfficePort:
rendererParams['ooPort'] = appyTool.openOfficePort
# Launch the renderer
try:
renderer = Renderer(**rendererParams)
renderer.run()
except appy.pod.PodError, pe:
if not os.path.exists(tempFileName):
# In some (most?) cases, when OO returns an error, the result is
# nevertheless generated.
appyTool.log(str(pe), type='error')
appyTool.say(POD_ERROR)
return self.goto(rq.get('HTTP_REFERER'))
# Open the temp file on the filesystem
f = file(tempFileName, 'rb')
res = f.read()
# Identify the filename to return
fileName = u'%s-%s' % (obj.Title().decode('utf-8'), podTitle)
fileName = appyTool.normalize(fileName)
response = obj.REQUEST.RESPONSE
response.setHeader('Content-Type', mimeTypes[format])
response.setHeader('Content-Disposition', 'inline;filename="%s.%s"'\
% (fileName, format))
f.close()
# Execute the related action if relevant
if doAction and podInfo['action']:
podInfo['action'](appyObj, podContext)
# Returns the doc and removes the temp file
try:
os.remove(tempFileName)
except OSError, oe:
appyTool.log(DELETE_TEMP_DOC_ERROR % str(oe), type='warning')
except IOError, ie:
appyTool.log(DELETE_TEMP_DOC_ERROR % str(ie), type='warning')
return res
def getAttr(self, name):
'''Gets on this flavour attribute named p_attrName. Useful because we
can't use getattr directly in Zope Page Templates.'''
return getattr(self.appy(), name, None)
def _appy_getAllFields(self, contentType):
'''Returns the (translated) names of fields of p_contentType.'''
res = []
for appyType in self.getProductConfig().attributes[contentType]:
if appyType.name != 'title': # Will be included by default.
label = '%s_%s' % (contentType, appyType.name)
res.append((appyType.name, self.translate(label)))
# Add object state
res.append(('workflowState', self.translate('workflow_state')))
return res
def _appy_getSearchableFields(self, contentType):
'''Returns the (translated) names of fields that may be searched on
objects of type p_contentType (=indexed fields).'''
res = []
for appyType in self.getProductConfig().attributes[contentType]:
if appyType.indexed:
res.append((appyType.name, self.translate(appyType.labelId)))
return res
def getSearchableFields(self, contentType):
'''Returns, among the list of all searchable fields (see method above),
the list of fields that the user has configured in the flavour as
being effectively used in the search screen.'''
res = []
fieldNames = getattr(self.appy(), 'searchFieldsFor%s' % contentType, ())
for name in fieldNames:
appyType = self.getAppyType(name, asDict=True,className=contentType)
res.append(appyType)
return res
def getImportElements(self, contentType):
'''Returns the list of elements that can be imported from p_path for
p_contentType.'''
tool = self.getParentNode()
appyClass = tool.getAppyClass(contentType)
importParams = tool.getCreateMeans(appyClass)['import']
onElement = importParams['onElement'].__get__('')
sortMethod = importParams['sort']
if sortMethod: sortMethod = sortMethod.__get__('')
elems = []
importPath = getattr(self, 'importPathFor%s' % contentType)
for elem in os.listdir(importPath):
elemFullPath = os.path.join(importPath, elem)
elemInfo = onElement(elemFullPath)
if elemInfo:
elemInfo.insert(0, elemFullPath) # To the result, I add the full
# path of the elem, which will not be shown.
elems.append(elemInfo)
if sortMethod:
elems = sortMethod(elems)
return [importParams['headers'], elems]
# ------------------------------------------------------------------------------

View file

@ -1,7 +0,0 @@
# ------------------------------------------------------------------------------
from appy.gen.plone25.mixins import AbstractMixin
# ------------------------------------------------------------------------------
class PodTemplateMixin(AbstractMixin):
_appy_meta_type = 'PodTemplate'
# ------------------------------------------------------------------------------

View file

@ -1,22 +1,28 @@
# ------------------------------------------------------------------------------
import re, os, os.path, Cookie
import re, os, os.path, time, Cookie, StringIO, types
from appy.shared import mimeTypes
from appy.shared.utils import getOsTempFolder
import appy.pod
from appy.pod.renderer import Renderer
import appy.gen
from appy.gen import Type, Search, Selection
from appy.gen.utils import SomeObjects, sequenceTypes, getClassName
from appy.gen.plone25.mixins import AbstractMixin
from appy.gen.plone25.mixins.FlavourMixin import FlavourMixin
from appy.gen.plone25.mixins import BaseMixin
from appy.gen.plone25.wrappers import AbstractWrapper
from appy.gen.plone25.descriptors import ClassDescriptor
# Errors -----------------------------------------------------------------------
DELETE_TEMP_DOC_ERROR = 'A temporary document could not be removed. %s.'
POD_ERROR = 'An error occurred while generating the document. Please ' \
'contact the system administrator.'
jsMessages = ('no_elem_selected', 'delete_confirm')
# ------------------------------------------------------------------------------
class ToolMixin(AbstractMixin):
class ToolMixin(BaseMixin):
_appy_meta_type = 'Tool'
def getPortalType(self, metaTypeOrAppyClass):
'''Returns the name of the portal_type that is based on
p_metaTypeOrAppyType in this flavour.'''
p_metaTypeOrAppyType.'''
appName = self.getProductConfig().PROJECTNAME
if not isinstance(metaTypeOrAppyClass, basestring):
res = getClassName(metaTypeOrAppyClass, appName)
@ -25,52 +31,100 @@ class ToolMixin(AbstractMixin):
res = '%s%s' % (elems[1], elems[4])
return res
def getFlavour(self, contextObjOrPortalType, appy=False):
'''Gets the flavour that corresponds to p_contextObjOrPortalType.'''
if isinstance(contextObjOrPortalType, basestring):
portalTypeName = contextObjOrPortalType
else:
# It is the contextObj, not a portal type name
portalTypeName = contextObjOrPortalType.portal_type
res = None
def getPodInfo(self, ploneObj, fieldName):
'''Returns POD-related information about Pod field p_fieldName defined
on class whose p_ploneObj is an instance of.'''
res = {}
appyClass = self.getAppyClass(ploneObj.meta_type)
appyTool = self.appy()
flavourNumber = None
nameElems = portalTypeName.split('_')
if len(nameElems) > 1:
try:
flavourNumber = int(nameElems[-1])
except ValueError:
pass
appName = self.getProductConfig().PROJECTNAME
if flavourNumber != None:
for flavour in appyTool.flavours:
if flavourNumber == flavour.number:
res = flavour
elif portalTypeName == ('%sFlavour' % appName):
# Current object is the Flavour itself. In this cas we simply
# return the wrapped contextObj. Here we are sure that
# contextObjOrPortalType is an object, not a portal type.
res = contextObjOrPortalType.appy()
if not res and appyTool.flavours:
res = appyTool.flavours[0]
# If appy=False, return the Plone object and not the Appy wrapper
# (this way, we avoid Zope security/access-related problems while
# using this object in Zope Page Templates)
if res and not appy:
res = res.o
n = appyTool.getAttributeName('formats', appyClass, fieldName)
res['formats'] = getattr(appyTool, n)
n = appyTool.getAttributeName('podTemplate', appyClass, fieldName)
res['template'] = getattr(appyTool, n)
appyType = ploneObj.getAppyType(fieldName)
res['title'] = self.translate(appyType.labelId)
res['context'] = appyType.context
res['action'] = appyType.action
return res
def getFlavoursInfo(self):
'''Returns information about flavours.'''
res = []
def generateDocument(self):
'''Generates the document from field-related info. UID of object that
is the template target is given in the request.'''
rq = self.REQUEST
appyTool = self.appy()
for flavour in appyTool.flavours:
if isinstance(flavour.o, FlavourMixin):
# This is a bug: sometimes other objects are associated as
# flavours.
res.append({'title': flavour.title, 'number':flavour.number})
# Get the object
objectUid = rq.get('objectUid')
obj = self.uid_catalog(UID=objectUid)[0].getObject()
appyObj = obj.appy()
# Get information about the document to render.
specificPodContext = None
fieldName = rq.get('fieldName')
format = rq.get('podFormat')
podInfo = self.getPodInfo(obj, fieldName)
template = podInfo['template'].content
podTitle = podInfo['title']
if podInfo['context']:
if type(podInfo['context']) == types.FunctionType:
specificPodContext = podInfo['context'](appyObj)
else:
specificPodContext = podInfo['context']
doAction = rq.get('askAction') == 'True'
# Temporary file where to generate the result
tempFileName = '%s/%s_%f.%s' % (
getOsTempFolder(), obj.UID(), time.time(), format)
# Define parameters to pass to the appy.pod renderer
currentUser = self.portal_membership.getAuthenticatedMember()
podContext = {'tool': appyTool, 'user': currentUser, 'self': appyObj,
'now': self.getProductConfig().DateTime(),
'projectFolder': appyTool.getDiskFolder(),
}
if specificPodContext:
podContext.update(specificPodContext)
rendererParams = {'template': StringIO.StringIO(template),
'context': podContext, 'result': tempFileName}
if appyTool.unoEnabledPython:
rendererParams['pythonWithUnoPath'] = appyTool.unoEnabledPython
if appyTool.openOfficePort:
rendererParams['ooPort'] = appyTool.openOfficePort
# Launch the renderer
try:
renderer = Renderer(**rendererParams)
renderer.run()
except appy.pod.PodError, pe:
if not os.path.exists(tempFileName):
# In some (most?) cases, when OO returns an error, the result is
# nevertheless generated.
appyTool.log(str(pe), type='error')
appyTool.say(POD_ERROR)
return self.goto(rq.get('HTTP_REFERER'))
# Open the temp file on the filesystem
f = file(tempFileName, 'rb')
res = f.read()
# Identify the filename to return
fileName = u'%s-%s' % (obj.Title().decode('utf-8'), podTitle)
fileName = appyTool.normalize(fileName)
response = obj.REQUEST.RESPONSE
response.setHeader('Content-Type', mimeTypes[format])
response.setHeader('Content-Disposition', 'inline;filename="%s.%s"'\
% (fileName, format))
f.close()
# Execute the related action if relevant
if doAction and podInfo['action']:
podInfo['action'](appyObj, podContext)
# Returns the doc and removes the temp file
try:
os.remove(tempFileName)
except OSError, oe:
appyTool.log(DELETE_TEMP_DOC_ERROR % str(oe), type='warning')
except IOError, ie:
appyTool.log(DELETE_TEMP_DOC_ERROR % str(ie), type='warning')
return res
def getAttr(self, name):
'''Gets attribute named p_attrName. Useful because we can't use getattr
directly in Zope Page Templates.'''
return getattr(self.appy(), name, None)
def getAppName(self):
'''Returns the name of this application.'''
return self.getProductConfig().PROJECTNAME
@ -86,6 +140,58 @@ class ToolMixin(AbstractMixin):
'''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.'''
res = []
for appyType in self.getProductConfig().attributes[contentType]:
if appyType.name != 'title': # Will be included by default.
label = '%s_%s' % (contentType, appyType.name)
res.append((appyType.name, self.translate(label)))
# Add object state
res.append(('workflowState', self.translate('workflow_state')))
return res
def _appy_getSearchableFields(self, contentType):
'''Returns the (translated) names of fields that may be searched on
objects of type p_contentType (=indexed fields).'''
res = []
for appyType in self.getProductConfig().attributes[contentType]:
if appyType.indexed:
res.append((appyType.name, self.translate(appyType.labelId)))
return res
def getSearchableFields(self, contentType):
'''Returns, among the list of all searchable fields (see method above),
the list of fields that the user has configured as being effectively
used in the search screen.'''
res = []
fieldNames = getattr(self.appy(), 'searchFieldsFor%s' % contentType, ())
for name in fieldNames:
appyType = self.getAppyType(name, asDict=True,className=contentType)
res.append(appyType)
return res
def getImportElements(self, contentType):
'''Returns the list of elements that can be imported from p_path for
p_contentType.'''
appyClass = self.getAppyClass(contentType)
importParams = self.getCreateMeans(appyClass)['import']
onElement = importParams['onElement'].__get__('')
sortMethod = importParams['sort']
if sortMethod: sortMethod = sortMethod.__get__('')
elems = []
importPath = getattr(self, 'importPathFor%s' % contentType)
for elem in os.listdir(importPath):
elemFullPath = os.path.join(importPath, elem)
elemInfo = onElement(elemFullPath)
if elemInfo:
elemInfo.insert(0, elemFullPath) # To the result, I add the full
# path of the elem, which will not be shown.
elems.append(elemInfo)
if sortMethod:
elems = sortMethod(elems)
return [importParams['headers'], elems]
def showPortlet(self, context):
if self.portal_membership.isAnonymousUser(): return False
if context.id == 'skyn': context = context.getParentNode()
@ -106,15 +212,13 @@ class ToolMixin(AbstractMixin):
res = res.appy()
return res
def executeQuery(self, contentType, flavourNumber=1, searchName=None,
startNumber=0, search=None, remember=False,
brainsOnly=False, maxResults=None, noSecurity=False,
sortBy=None, sortOrder='asc',
filterKey=None, filterValue=None):
def executeQuery(self, contentType, searchName=None, startNumber=0,
search=None, remember=False, brainsOnly=False,
maxResults=None, noSecurity=False, sortBy=None,
sortOrder='asc', filterKey=None, filterValue=None):
'''Executes a query on a given p_contentType (or several, separated
with commas) in Plone's portal_catalog. Portal types are from the
flavour numbered p_flavourNumber. If p_searchName is specified, it
corresponds to:
with commas) in Plone's portal_catalog. If p_searchName is specified,
it corresponds to:
1) a search defined on p_contentType: additional search criteria
will be added to the query, or;
2) "_advanced": in this case, additional search criteria will also
@ -150,11 +254,7 @@ class ToolMixin(AbstractMixin):
p_filterValue.'''
# Is there one or several content types ?
if contentType.find(',') != -1:
# Several content types are specified
portalTypes = contentType.split(',')
if flavourNumber != 1:
portalTypes = ['%s_%d' % (pt, flavourNumber) \
for pt in portalTypes]
else:
portalTypes = contentType
params = {'portal_type': portalTypes}
@ -164,8 +264,7 @@ class ToolMixin(AbstractMixin):
# In this case, contentType must contain a single content type.
appyClass = self.getAppyClass(contentType)
if searchName != '_advanced':
search = ClassDescriptor.getSearch(
appyClass, searchName)
search = ClassDescriptor.getSearch(appyClass, searchName)
else:
fields = self.REQUEST.SESSION['searchCriteria']
search = Search('customSearch', **fields)
@ -220,22 +319,17 @@ class ToolMixin(AbstractMixin):
for obj in res.objects:
i += 1
uids[startNumber+i] = obj.UID()
s['search_%s_%s' % (flavourNumber, searchName)] = uids
s['search_%s' % searchName] = uids
return res.__dict__
def getResultColumnsNames(self, contentType):
contentTypes = contentType.strip(',').split(',')
resSet = None # Temporary set for computing intersections.
res = [] # Final, sorted result.
flavour = None
fieldNames = None
appyTool = self.appy()
for cType in contentTypes:
# Get the flavour tied to those content types
if not flavour:
flavour = self.getFlavour(cType, appy=True)
if flavour.number != 1:
cType = cType.rsplit('_', 1)[0]
fieldNames = getattr(flavour, 'resultColumnsFor%s' % cType)
fieldNames = getattr(appyTool, 'resultColumnsFor%s' % cType)
if not resSet:
resSet = set(fieldNames)
else:
@ -483,9 +577,9 @@ class ToolMixin(AbstractMixin):
attrValue = oper.join(attrValue)
criteria[attrName[2:]] = attrValue
rq.SESSION['searchCriteria'] = criteria
# Goto the screen that displays search results
backUrl = '%s/query?type_name=%s&flavourNumber=%d&search=_advanced' % \
(os.path.dirname(rq['URL']), rq['type_name'], rq['flavourNumber'])
# Go to the screen that displays search results
backUrl = '%s/query?type_name=%s&&search=_advanced' % \
(os.path.dirname(rq['URL']), rq['type_name'])
return self.goto(backUrl)
def getJavascriptMessages(self):
@ -535,13 +629,12 @@ class ToolMixin(AbstractMixin):
if cookieValue: return cookieValue.value
return default
def getQueryUrl(self, contentType, flavourNumber, searchName,
startNumber=None):
def getQueryUrl(self, contentType, searchName, startNumber=None):
'''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 from flavour numbered p_flavourNumber.'''
on p_contentType.'''
baseUrl = self.getAppFolder().absolute_url() + '/skyn'
baseParams= 'type_name=%s&flavourNumber=%s' %(contentType,flavourNumber)
baseParams = 'type_name=%s' % contentType
# Manage start number
rq = self.REQUEST
if startNumber != None:
@ -609,11 +702,10 @@ class ToolMixin(AbstractMixin):
if (nextIndex < lastIndex): lastNeeded = True
# Get the list of available UIDs surrounding the current object
if t == 'ref': # Manage navigation from a reference
# In the case of a reference, we retrieve ALL surrounding objects.
masterObj = self.getObject(d1)
batchSize = masterObj.getAppyType(fieldName).maxPerPage
uids = getattr(masterObj, '_appy_%s' % fieldName)
# In the case of a reference, we retrieve ALL surrounding objects.
# Display the reference widget at the page where the current object
# lies.
startNumberKey = '%s%s_startNumber' % (masterObj.UID(), fieldName)
@ -622,13 +714,12 @@ class ToolMixin(AbstractMixin):
res['sourceUrl'] = masterObj.getUrl(**{startNumberKey:startNumber,
'page':pageName, 'nav':''})
else: # Manage navigation from a search
contentType, flavourNumber = d1.split(':')
flavourNumber = int(flavourNumber)
contentType = d1
searchName = keySuffix = d2
batchSize = self.appy().numberOfResultsPerPage
if not searchName: keySuffix = contentType
s = self.REQUEST.SESSION
searchKey = 'search_%s_%s' % (flavourNumber, keySuffix)
searchKey = 'search_%s' % keySuffix
if s.has_key(searchKey): uids = s[searchKey]
else: uids = {}
# In the case of a search, we retrieve only a part of all
@ -640,9 +731,8 @@ class ToolMixin(AbstractMixin):
# this one.
newStartNumber = (res['currentNumber']-1) - (batchSize / 2)
if newStartNumber < 0: newStartNumber = 0
self.executeQuery(contentType, flavourNumber,
searchName=searchName, startNumber=newStartNumber,
remember=True)
self.executeQuery(contentType, searchName=searchName,
startNumber=newStartNumber, remember=True)
uids = s[searchKey]
# For the moment, for first and last, we get them only if we have
# them in session.
@ -650,9 +740,9 @@ class ToolMixin(AbstractMixin):
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, flavourNumber,
searchName, startNumber=startNumber)
res['totalNumber'], batchSize)
res['sourceUrl'] = self.getQueryUrl(contentType, searchName,
startNumber=startNumber)
# Compute URLs
for urlType in ('previous', 'next', 'first', 'last'):
exec 'needIt = %sNeeded' % urlType

View file

@ -1,7 +0,0 @@
# ------------------------------------------------------------------------------
from appy.gen.plone25.mixins import AbstractMixin
# ------------------------------------------------------------------------------
class UserMixin(AbstractMixin):
_appy_meta_type = 'UserMixin'
# ------------------------------------------------------------------------------

View file

@ -1,9 +1,6 @@
'''This package contains mixin classes that are mixed in with generated classes:
- mixins/ClassMixin is mixed in with Standard Archetypes classes;
- mixins/ToolMixin is mixed in with the generated application Tool class;
- mixins/FlavourMixin is mixed in with the generated application Flavour
class.
The AbstractMixin defined hereafter is the base class of any mixin.'''
- mixins/BaseMixin is mixed in with Standard Archetypes classes;
- mixins/ToolMixin is mixed in with the generated application Tool class.'''
# ------------------------------------------------------------------------------
import os, os.path, types, mimetypes
@ -15,10 +12,10 @@ from appy.gen.plone25.descriptors import ClassDescriptor
from appy.gen.plone25.utils import updateRolesForPermission
# ------------------------------------------------------------------------------
class AbstractMixin:
'''Every Archetype class generated by appy.gen inherits from a mixin that
inherits from this class. It contains basic functions allowing to
minimize the amount of generated code.'''
class BaseMixin:
'''Every Archetype class generated by appy.gen inherits from this class or
a subclass of it.'''
_appy_meta_type = 'Class'
def createOrUpdate(self, created, values):
'''This method creates (if p_created is True) or updates an object.
@ -52,17 +49,21 @@ class AbstractMixin:
# Keep in history potential changes on historized fields
self.historizeData(previousData)
# Manage references
obj._appy_manageRefs(created)
# Manage potential link with an initiator object
if created and rq.get('nav', None):
# Get the initiator
splitted = rq['nav'].split('.')
if splitted[0] == 'search': return # Not an initiator but a search.
initiator = self.uid_catalog(UID=splitted[1])[0].getObject()
fieldName = splitted[2].split(':')[0]
initiator.appy().link(fieldName, obj)
# Call the custom "onEdit" if available
if obj.wrapperClass:
# Get the wrapper first
appyObject = obj.appy()
# Call the custom "onEdit" if available
if hasattr(appyObject, 'onEdit'):
appyObject.onEdit(created)
# Manage "add" permissions
if hasattr(appyObject, 'onEdit'): appyObject.onEdit(created)
# Manage "add" permissions and reindex the object
obj._appy_managePermissions()
# Reindex object
obj.reindexObject()
return obj
@ -95,6 +96,12 @@ class AbstractMixin:
(baseUrl, typeName, objId)
return self.goto(self.getUrl(editUrl, **urlParams))
def onCreateWithoutForm(self):
'''This method is called when a user wants to create a object from a
reference field, automatically (without displaying a form).'''
rq = self.REQUEST
self.appy().create(rq['fieldName'])
def intraFieldValidation(self, errors, values):
'''This method performs field-specific validation for every field from
the page that is being created or edited. For every field whose
@ -120,7 +127,7 @@ class AbstractMixin:
obj = self.appy()
if not hasattr(obj, 'validate'): return
obj.validate(values, errors)
# This custom "validate" method may have added fields in the given
# Those custom validation methods may have added fields in the given
# p_errors object. Within this object, for every error message that is
# not a string, we replace it with the standard validation error for the
# corresponding field.
@ -241,21 +248,19 @@ class AbstractMixin:
res = {}
for appyType in self.getAllAppyTypes():
if appyType.historized:
res[appyType.name] = (getattr(self, appyType.name),
appyType.labelId)
res[appyType.name] = appyType.getValue(self)
return res
def addDataChange(self, changes, labels=False):
def addDataChange(self, changes):
'''This method allows to add "manually" a data change into the objet's
history. Indeed, data changes are "automatically" recorded only when
a HTTP form is uploaded, not if, in the code, a setter is called on
a field. The method is also called by the method historizeData below,
that performs "automatic" recording when a HTTP form is uploaded.'''
# Add to the p_changes dict the field labels if they are not present
if not labels:
for fieldName in changes.iterkeys():
appyType = self.getAppyType(fieldName)
changes[fieldName] = (changes[fieldName], appyType.labelId)
# Add to the p_changes dict the field labels
for fieldName in changes.iterkeys():
appyType = self.getAppyType(fieldName)
changes[fieldName] = (changes[fieldName], appyType.labelId)
# Create the event to record in the history
DateTime = self.getProductConfig().DateTime
state = self.portal_workflow.getInfoFor(self, 'review_state')
@ -273,14 +278,18 @@ class AbstractMixin:
historized fields, while p_self already contains the (potentially)
modified values.'''
# Remove from previousData all values that were not changed
for fieldName in previousData.keys():
prev = previousData[fieldName][0]
curr = getattr(self, fieldName)
for field in previousData.keys():
prev = previousData[field]
appyType = self.getAppyType(field)
curr = appyType.getValue(self)
if (prev == curr) or ((prev == None) and (curr == '')) or \
((prev == '') and (curr == None)):
del previousData[fieldName]
del previousData[field]
if (appyType.type == 'Ref') and (field in previousData):
titles = [r.title for r in previousData[field]]
previousData[field] = ','.join(titles)
if previousData:
self.addDataChange(previousData, labels=True)
self.addDataChange(previousData)
def goto(self, url, addParams=False):
'''Brings the user to some p_url after an action has been executed.'''
@ -308,80 +317,14 @@ class AbstractMixin:
field named p_name.'''
return self.getAppyType(name).getFormattedValue(self, value)
def _appy_getRefs(self, fieldName, ploneObjects=False,
noListIfSingleObj=False, startNumber=None):
'''p_fieldName is the name of a Ref field. This method returns an
ordered list containing the objects linked to p_self through this
field. If p_ploneObjects is True, the method returns the "true"
Plone objects instead of the Appy wrappers.
If p_startNumber is None, this method returns all referred objects.
If p_startNumber is a number, this method will return x objects,
starting at p_startNumber, x being appyType.maxPerPage.'''
appyType = self.getAppyType(fieldName)
sortedUids = self._appy_getSortedField(fieldName)
batchNeeded = startNumber != None
exec 'refUids= self.getRaw%s%s()' % (fieldName[0].upper(),fieldName[1:])
# There may be too much UIDs in sortedUids because these fields
# are not updated when objects are deleted. So we do it now. TODO: do
# such cleaning on object deletion?
toDelete = []
for uid in sortedUids:
if uid not in refUids:
toDelete.append(uid)
for uid in toDelete:
sortedUids.remove(uid)
# Prepare the result
res = SomeObjects()
res.totalNumber = res.batchSize = len(sortedUids)
if batchNeeded:
res.batchSize = appyType.maxPerPage
if startNumber != None:
res.startNumber = startNumber
# Get the needed referred objects
i = res.startNumber
# Is it possible and more efficient to perform a single query in
# uid_catalog and get the result in the order of specified uids?
toUnlink = []
while i < (res.startNumber + res.batchSize):
if i >= res.totalNumber: break
refUid = sortedUids[i]
refObject = self.uid_catalog(UID=refUid)[0].getObject()
i += 1
tool = self.getTool()
if refObject.meta_type != tool.getPortalType(appyType.klass):
toUnlink.append(refObject)
continue
if not ploneObjects:
refObject = refObject.appy()
res.objects.append(refObject)
# Unlink dummy linked objects
if toUnlink:
suffix = '%s%s' % (fieldName[0].upper(), fieldName[1:])
exec 'linkedObjects = self.get%s()' % suffix
for dummyObject in toUnlink:
linkedObjects.remove(dummyObject)
self.getProductConfig().logger.warn('DB error: Ref %s.%s ' \
'contains a %s instance "%s". It was removed.' % \
(self.meta_type, fieldName, dummyObject.meta_type,
dummyObject.getId()))
exec 'self.set%s(linkedObjects)' % suffix
if res.objects and noListIfSingleObj:
if appyType.multiplicity[1] == 1:
res.objects = res.objects[0]
return res
def getAppyRefs(self, name, startNumber=None):
'''Gets the objects linked to me through Ref field named p_name.
If p_startNumber is None, this method returns all referred objects.
If p_startNumber is a number, this method will return x objects,
starting at p_startNumber, x being appyType.maxPerPage.'''
If p_startNumber is a number, this method will return
appyType.maxPerPage objects, starting at p_startNumber.'''
appyType = self.getAppyType(name)
if not appyType.isBack:
return self._appy_getRefs(name, ploneObjects=True,
startNumber=startNumber).__dict__
else:
# Note that pagination is not yet implemented for backward refs.
return SomeObjects(self.getBRefs(appyType.relationship)).__dict__
return appyType.getValue(self, type='zobjects', someObjects=True,
startNumber=startNumber).__dict__
def getSelectableAppyRefs(self, name):
'''p_name is the name of a Ref field. This method returns the list of
@ -780,10 +723,6 @@ class AbstractMixin:
self.reindexObject()
return self.goto(urlBack)
def getFlavour(self):
'''Returns the flavour corresponding to this object.'''
return self.getTool().getFlavour(self.portal_type)
def fieldValueSelected(self, fieldName, vocabValue, dbValue):
'''When displaying a selection box (ie a String with a validator being a
list), must the _vocabValue appear as selected?'''
@ -853,32 +792,6 @@ class AbstractMixin:
rq.appyWrappers[uid] = wrapper
return wrapper
def _appy_getRefsBack(self, fieldName, relName, ploneObjects=False,
noListIfSingleObj=False):
'''This method returns the list of objects linked to this one
through the BackRef corresponding to the Archetypes
relationship named p_relName.'''
# Preamble: must I return a list or a single element?
maxOne = False
if noListIfSingleObj:
# I must get the referred appyType to know its maximum multiplicity.
appyType = self.getAppyType(fieldName)
if appyType.multiplicity[1] == 1:
maxOne = True
# Get the referred objects through the Archetypes relationship.
objs = self.getBRefs(relName)
if maxOne:
res = None
if objs:
res = objs[0]
if res and not ploneObjects:
res = res.appy()
else:
res = objs
if not ploneObjects:
res = [o.appy() for o in objs]
return res
def _appy_showState(self, workflow, stateShow):
'''Must I show a state whose "show value" is p_stateShow?'''
if callable(stateShow):
@ -955,57 +868,6 @@ class AbstractMixin:
exec 'self.%s = pList()' % sortedFieldName
return getattr(self, sortedFieldName)
def _appy_manageRefs(self, created):
'''Every time an object is created or updated, this method updates
the Reference fields accordingly.'''
self._appy_manageRefsFromRequest()
rq = self.REQUEST
# If the creation was initiated by another object, update the ref.
if created and rq.get('nav', None):
# Get the initiator
splitted = rq['nav'].split('.')
if splitted[0] == 'search': return # Not an initiator but a search.
initiator = self.uid_catalog.searchResults(
UID=splitted[1])[0].getObject()
fieldName = splitted[2].split(':')[1]
initiator.appy().link(fieldName, self)
def _appy_manageRefsFromRequest(self):
'''Appy manages itself some Ref fields (with link=True). So here we must
update the Ref fields.'''
fieldsInRequest = [] # Fields present in the request
for requestKey in self.REQUEST.keys():
if requestKey.startswith('appy_ref_'):
fieldName = requestKey[9:]
# Security check
if not self.getAppyType(fieldName).isShowable(self, 'edit'):
continue
fieldsInRequest.append(fieldName)
fieldValue = self.REQUEST[requestKey]
sortedRefField = self._appy_getSortedField(fieldName)
del sortedRefField[:]
if not fieldValue: fieldValue = []
if isinstance(fieldValue, basestring):
fieldValue = [fieldValue]
refObjects = []
for uid in fieldValue:
obj = self.uid_catalog(UID=uid)[0].getObject()
refObjects.append(obj)
sortedRefField.append(uid)
exec 'self.set%s%s(refObjects)' % (fieldName[0].upper(),
fieldName[1:])
# Manage Ref fields that are not present in the request
currentPage = self.REQUEST.get('page', 'main')
for appyType in self.getAllAppyTypes():
if (appyType.type == 'Ref') and not appyType.isBack and \
(appyType.page == currentPage) and \
(appyType.name not in fieldsInRequest):
# If this field is visible, it was not present in the request:
# it means that we must remove any Ref from it.
if appyType.isShowable(self, 'edit'):
exec 'self.set%s%s([])' % (appyType.name[0].upper(),
appyType.name[1:])
getUrlDefaults = {'page':True, 'nav':True}
def getUrl(self, base=None, mode='view', **kwargs):
'''Returns a Appy URL.
@ -1039,12 +901,14 @@ class AbstractMixin:
params = ''
return '%s%s' % (base, params)
def translate(self, label, mapping={}, domain=None, default=None):
def translate(self, label, mapping={}, domain=None, default=None,
language=None):
'''Translates a given p_label into p_domain with p_mapping.'''
cfg = self.getProductConfig()
if not domain: domain = cfg.PROJECTNAME
return self.translation_service.utranslate(
domain, label, mapping, self, default=default)
return self.Control_Panel.TranslationService.utranslate(
domain, label, mapping, self, default=default,
target_language=language)
def getPageLayout(self, layoutType):
'''Returns the layout corresponding to p_layoutType for p_self.'''