Added a new system for layouting production-ready forms without any HTML coding, many performance improvements and more independence towards Archetypes.

This commit is contained in:
Gaetan Delannay 2010-08-05 18:23:17 +02:00
parent 309ea921fa
commit bfd2357f69
84 changed files with 4663 additions and 3549 deletions

View file

@ -3,40 +3,5 @@ from appy.gen.plone25.mixins import AbstractMixin
# ------------------------------------------------------------------------------
class ClassMixin(AbstractMixin):
_appy_meta_type = 'class'
def _appy_fieldIsUsed(self, portalTypeName, fieldName):
tool = self.getTool()
flavour = tool.getFlavour(portalTypeName)
optionalFieldsAccessor = 'getOptionalFieldsFor%s' % self.meta_type
exec 'usedFields = flavour.%s()' % optionalFieldsAccessor
res = False
if fieldName in usedFields:
res = True
return res
def _appy_getDefaultValueFor(self, portalTypeName, fieldName):
tool = self.getTool()
flavour = tool.getFlavour(portalTypeName)
fieldFound = False
klass = self.__class__
while not fieldFound:
metaType = klass.meta_type
defValueAccessor = 'getDefaultValueFor%s_%s' % (metaType, fieldName)
if not hasattr(flavour, defValueAccessor):
# The field belongs to a super-class.
klass = klass.__bases__[-1]
else:
fieldFound = True
exec 'res = flavour.%s()' % defValueAccessor
return res
def fieldIsUsed(self, fieldName):
'''Checks in the corresponding flavour if p_fieldName is used.'''
portalTypeName = self._appy_getPortalType(self.REQUEST)
return self._appy_fieldIsUsed(portalTypeName, fieldName)
def getDefaultValueFor(self, fieldName):
'''Gets in the flavour the default value for p_fieldName.'''
portalTypeName = self._appy_getPortalType(self.REQUEST)
return self._appy_getDefaultValueFor(portalTypeName,fieldName)
_appy_meta_type = 'Class'
# ------------------------------------------------------------------------------

View file

@ -8,7 +8,7 @@ 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 ArchetypesClassDescriptor
from appy.gen.plone25.descriptors import ClassDescriptor
# Errors -----------------------------------------------------------------------
DELETE_TEMP_DOC_ERROR = 'A temporary document could not be removed. %s.'
@ -17,7 +17,7 @@ POD_ERROR = 'An error occurred while generating the document. Please ' \
# ------------------------------------------------------------------------------
class FlavourMixin(AbstractMixin):
_appy_meta_type = 'flavour'
_appy_meta_type = 'Flavour'
def getPortalType(self, metaTypeOrAppyType):
'''Returns the name of the portal_type that is based on
p_metaTypeOrAppyType in this flavour.'''
@ -26,7 +26,7 @@ class FlavourMixin(AbstractMixin):
isAppy = False
appName = self.getProductConfig().PROJECTNAME
if not isinstance(res, basestring):
res = ArchetypesClassDescriptor.getClassName(res)
res = ClassDescriptor.getClassName(res)
isAppy = True
if res.find('Extensions_appyWrappers') != -1:
isPredefined = True
@ -41,8 +41,9 @@ class FlavourMixin(AbstractMixin):
isPredefined = True
res = '%sFlavour' % appName
if not isPredefined:
if self.getNumber() != 1:
res = '%s_%d' % (res, self.number)
number = self.appy().number
if number != 1:
res = '%s_%d' % (res, number)
return res
def registerPortalTypes(self):
@ -129,9 +130,9 @@ class FlavourMixin(AbstractMixin):
n = appyFlavour.getAttributeName('podTemplate', appyClass, fieldName)
res['template'] = getattr(appyFlavour, n)
appyType = ploneObj.getAppyType(fieldName)
res['title'] = self.translate(appyType['label'])
res['context'] = appyType['context']
res['action'] = appyType['action']
res['title'] = self.translate(appyType.labelId)
res['context'] = appyType.context
res['action'] = appyType.action
return res
def generateDocument(self):
@ -225,18 +226,18 @@ class FlavourMixin(AbstractMixin):
appyTool.log(DELETE_TEMP_DOC_ERROR % str(ie), type='warning')
return res
def getAttr(self, attrName):
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, attrName, None)
return getattr(self.appy(), name, None)
def _appy_getAllFields(self, contentType):
'''Returns the (translated) names of fields of p_contentType.'''
res = []
for attrName in self.getProductConfig().attributes[contentType]:
if attrName != 'title': # Will be included by default.
label = '%s_%s' % (contentType, attrName)
res.append((attrName, self.translate(label)))
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
@ -244,15 +245,10 @@ class FlavourMixin(AbstractMixin):
def _appy_getSearchableFields(self, contentType):
'''Returns the (translated) names of fields that may be searched on
objects of type p_contentType (=indexed fields).'''
tool = self.getParentNode()
appyClass = tool.getAppyClass(contentType)
attrNames = self.getProductConfig().attributes[contentType]
res = []
for attrName in attrNames:
attr = getattr(appyClass, attrName)
if isinstance(attr, Type) and attr.indexed:
label = '%s_%s' % (contentType, attrName)
res.append((attrName, self.translate(label)))
for appyType in self.getProductConfig().attributes[contentType]:
if appyType.indexed:
res.append((appyType.name, self.translate(appyType.labelId)))
return res
def getSearchableFields(self, contentType):
@ -260,11 +256,10 @@ class FlavourMixin(AbstractMixin):
the list of fields that the user has configured in the flavour as
being effectively used in the search screen.'''
res = []
appyClass = self.getAppyClass(contentType)
for attrName in getattr(self, 'searchFieldsFor%s' % contentType, ()):
attr = getattr(appyClass, attrName)
dAttr = self._appy_getTypeAsDict(attrName, attr, appyClass)
res.append((attrName, dAttr))
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):

View file

@ -3,5 +3,5 @@ from appy.gen.plone25.mixins import AbstractMixin
# ------------------------------------------------------------------------------
class PodTemplateMixin(AbstractMixin):
_appy_meta_type = 'podtemplate'
_appy_meta_type = 'PodTemplate'
# ------------------------------------------------------------------------------

View file

@ -1,40 +1,18 @@
# ------------------------------------------------------------------------------
import re, os, os.path, Cookie
from appy.shared.utils import getOsTempFolder
from appy.gen import Type, Search, Selection
from appy.gen.utils import FieldDescr, SomeObjects, sequenceTypes
from appy.gen.utils import SomeObjects, sequenceTypes
from appy.gen.plone25.mixins import AbstractMixin
from appy.gen.plone25.mixins.FlavourMixin import FlavourMixin
from appy.gen.plone25.wrappers import AbstractWrapper
from appy.gen.plone25.descriptors import ArchetypesClassDescriptor
from appy.gen.plone25.descriptors import ClassDescriptor
_PY = 'Please specify a file corresponding to a Python interpreter ' \
'(ie "/usr/bin/python").'
FILE_NOT_FOUND = 'Path "%s" was not found.'
VALUE_NOT_FILE = 'Path "%s" is not a file. ' + _PY
NO_PYTHON = "Name '%s' does not starts with 'python'. " + _PY
NOT_UNO_ENABLED_PYTHON = '"%s" is not a UNO-enabled Python interpreter. ' \
'To check if a Python interpreter is UNO-enabled, ' \
'launch it and type "import uno". If you have no ' \
'ImportError exception it is ok.'
jsMessages = ('no_elem_selected', 'delete_confirm')
# ------------------------------------------------------------------------------
class ToolMixin(AbstractMixin):
_appy_meta_type = 'tool'
def _appy_validateUnoEnabledPython(self, value):
'''This method represents the validator for field unoEnabledPython.
This field is present on the Tool only if POD is needed.'''
if value:
if not os.path.exists(value):
return FILE_NOT_FOUND % value
if not os.path.isfile(value):
return VALUE_NOT_FILE % value
if not os.path.basename(value).startswith('python'):
return NO_PYTHON % value
if os.system('%s -c "import uno"' % value):
return NOT_UNO_ENABLED_PYTHON % value
return None
_appy_meta_type = 'Tool'
def getFlavour(self, contextObjOrPortalType, appy=False):
'''Gets the flavour that corresponds to p_contextObjOrPortalType.'''
if isinstance(contextObjOrPortalType, basestring):
@ -102,8 +80,11 @@ class ToolMixin(AbstractMixin):
def getObject(self, uid, appy=False):
'''Allows to retrieve an object from its p_uid.'''
res = self.uid_catalog(UID=uid)
if res: return res[0].getObject()
return None
if res:
res = res[0].getObject()
if appy:
res = res.appy()
return res
def executeQuery(self, contentType, flavourNumber=1, searchName=None,
startNumber=0, search=None, remember=False,
@ -133,7 +114,7 @@ class ToolMixin(AbstractMixin):
useful for some usages like knowing the number of objects without
needing to get information about them). If no p_maxResults is
specified, the method returns maximum
self.getNumberOfResultsPerPage(). The method returns all objects if
self.numberOfResultsPerPage. The method returns all objects if
p_maxResults equals string "NO_LIMIT".
If p_noSecurity is True, it gets all the objects, even those that the
@ -163,7 +144,7 @@ class ToolMixin(AbstractMixin):
# In this case, contentType must contain a single content type.
appyClass = self.getAppyClass(contentType)
if searchName != '_advanced':
search = ArchetypesClassDescriptor.getSearch(
search = ClassDescriptor.getSearch(
appyClass, searchName)
else:
fields = self.REQUEST.SESSION['searchCriteria']
@ -201,7 +182,7 @@ class ToolMixin(AbstractMixin):
# Return brains only.
if not maxResults: return brains
else: return brains[:maxResults]
if not maxResults: maxResults = self.getNumberOfResultsPerPage()
if not maxResults: maxResults = self.appy().numberOfResultsPerPage
elif maxResults == 'NO_LIMIT': maxResults = None
res = SomeObjects(brains, maxResults, startNumber,noSecurity=noSecurity)
res.brainsToObjects()
@ -249,26 +230,31 @@ class ToolMixin(AbstractMixin):
def getResultColumns(self, anObject, contentType):
'''What columns must I show when displaying a list of root class
instances? Result is a list of tuples containing the name of the
column (=name of the field) and a FieldDescr instance.'''
column (=name of the field) and the corresponding appyType (dict
version).'''
res = []
for fieldName in self.getResultColumnsNames(contentType):
if fieldName == 'workflowState':
# We do not return a FieldDescr instance if the attributes is
# not a *real* attribute but the workfow state.
# We do not return a appyType if the attribute is not a *real*
# attribute, but the workfow state.
res.append(fieldName)
else:
# Create a FieldDescr instance
appyType = anObject.getAppyType(fieldName)
appyType = anObject.getAppyType(fieldName, asDict=True)
if not appyType:
res.append({'atField': None, 'name': fieldName})
res.append({'name': fieldName, '_wrong': True})
# The field name is wrong.
# We return it so we can show it in an error message.
else:
atField = anObject.schema.get(fieldName)
fieldDescr = FieldDescr(atField, appyType, None)
res.append(fieldDescr.get())
res.append(appyType)
return res
def truncateValue(self, value, appyType):
'''Truncates the p_value according to p_appyType width.'''
maxWidth = appyType['width']
if len(value) > maxWidth:
return value[:maxWidth] + '...'
return value
xhtmlToText = re.compile('<.*?>', re.S)
def getReferenceLabel(self, brain, appyType):
'''p_appyType is a Ref with link=True. I need to display, on an edit
@ -288,8 +274,8 @@ class ToolMixin(AbstractMixin):
elif isinstance(value, basestring):
value = value.decode('utf-8')
refAppyType = appyObj.o.getAppyType(fieldName)
if refAppyType and (refAppyType['type'] == 'String') and \
(refAppyType['format'] == 2):
if refAppyType and (refAppyType.type == 'String') and \
(refAppyType.format == 2):
value = self.xhtmlToText.sub(' ', value)
else:
value = str(value)
@ -312,13 +298,21 @@ class ToolMixin(AbstractMixin):
appName = self.getProductConfig().PROJECTNAME
return self.utranslate(label, self.translationMapping, domain=appName)
def getPublishedObject(self):
'''Gets the currently published object.'''
def getPublishedObject(self, rootClasses):
'''Gets the currently published object, if its meta_class is among
p_rootClasses or if it is the corresponding tool or flavour.'''
rq = self.REQUEST
obj = rq['PUBLISHED']
parent = obj.getParentNode()
if parent.id == 'skyn': return parent.getParentNode()
return rq['PUBLISHED']
if parent.id == 'skyn':
obj = parent.getParentNode()
if obj.meta_type in rootClasses:
return obj
else:
appName = self.getAppName()
if obj.meta_type in ('%sTool' % appName, '%sFlavour' % appName):
return obj
return None
def getAppyClass(self, contentType):
'''Gets the Appy Python class that is related to p_contentType.'''
@ -359,6 +353,17 @@ class ToolMixin(AbstractMixin):
res[means.id] = means.__dict__
return res
def userMayAdd(self, rootClass):
'''For deciding if a user may add a new instance of a class, beyond the
permission-based check, we can have a custom method that proposes an
additional condition. This method checks if there is such a custom
method (must be named "mayCreate") define on p_rootClass, and calls
it if yes. If no, it returns True.'''
pythonClass = self.getAppyClass(rootClass)
if 'mayCreate' in pythonClass.__dict__:
return pythonClass.mayCreate(self.appy())
return True
def onImportObjects(self):
'''This method is called when the user wants to create objects from
external data.'''
@ -503,7 +508,7 @@ class ToolMixin(AbstractMixin):
appyClass = self.getAppyClass(contentType)
res = []
visitedGroups = {} # Names of already visited search groups
for search in ArchetypesClassDescriptor.getSearches(appyClass):
for search in ClassDescriptor.getSearches(appyClass):
# Determine first group label, we will need it.
groupLabel = ''
if search.group:
@ -610,7 +615,7 @@ class ToolMixin(AbstractMixin):
if t == 'ref': # Manage navigation from a reference
fieldName = d2
masterObj = self.getObject(d1)
batchSize = masterObj.getAppyType(fieldName)['maxPerPage']
batchSize = masterObj.getAppyType(fieldName).maxPerPage
uids = getattr(masterObj, '_appy_%s' % fieldName)
# In the case of a reference, we retrieve ALL surrounding objects.
@ -679,21 +684,18 @@ class ToolMixin(AbstractMixin):
into a list of lists, where every sub-list has length p_numberOfRows.
This method is typically used for rendering elements in a table of
p_numberOfRows rows.'''
if numberOfRows > 1:
res = []
row = []
for elem in data:
row.append(elem)
if len(row) == numberOfRows:
res.append(row)
row = []
# Complete the last unfinished line if required.
if row:
while len(row) < numberOfRows: row.append(None)
res = []
row = []
for elem in data:
row.append(elem)
if len(row) == numberOfRows:
res.append(row)
return res
else:
return data
row = []
# Complete the last unfinished line if required.
if row:
while len(row) < numberOfRows: row.append(None)
res.append(row)
return res
def truncate(self, value, numberOfChars):
'''Truncates string p_value to p_numberOfChars.'''
@ -708,22 +710,6 @@ class ToolMixin(AbstractMixin):
'''Gets the translated month name of month numbered p_monthNumber.'''
return self.translate(self.monthsIds[int(monthNumber)], domain='plone')
def getSelectValues(self, appyType):
'''Return the possible values (with their translation) of String type
p_appyType (dict version) which is a string whose validator limits
the possible values, either statically (validator is simply a list
of values) or dynamically (validator is a Selection instance).'''
validator = appyType['validator']
if isinstance(validator, Selection):
vocab = self._appy_getDynamicDisplayList(validator.methodName)
return vocab.items()
else:
res = []
for v in validator:
text = self.translate('%s_list_%s' % (appyType['label'], v))
res.append((v, self.truncate(text, 30)))
return res
def logout(self):
'''Logs out the current user when he clicks on "disconnect".'''
rq = self.REQUEST
@ -748,4 +734,20 @@ class ToolMixin(AbstractMixin):
from appy.gen.plone25.installer import loggedUsers
if loggedUsers.has_key(userId): del loggedUsers[userId]
return self.goto(self.getParentNode().absolute_url())
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(baseFolder, rq.SESSION.id)
fileName = os.path.join(baseFolder, rq.get('name', ''))
if os.path.exists(fileName):
f = file(fileName)
content = f.read()
f.close()
# Remove the temp file
os.remove(fileName)
return content
return 'File does not exist'
# ------------------------------------------------------------------------------

File diff suppressed because it is too large Load diff