appy.gen: added, for every Type, param 'label' allowing to specify an existing i18n label (the one from another field for instance), thus avoiding to generate i18n labels for this Type; optimized generation of appyWrappers.py (more than twice less code).

This commit is contained in:
Gaetan Delannay 2011-09-06 21:46:57 +02:00
parent 813b47843c
commit 3c95ac083d
8 changed files with 162 additions and 103 deletions

View file

@ -361,7 +361,7 @@ class Type:
editDefault, show, page, group, layouts, move, indexed,
searchable, specificReadPermission, specificWritePermission,
width, height, maxChars, colspan, master, masterValue, focus,
historized, sync, mapping):
historized, sync, mapping, label):
# The validator restricts which values may be defined. It can be an
# interval (1,None), a list of string values ['choice1', 'choice2'],
# a regular expression, a custom function, a Selection instance, etc.
@ -465,10 +465,18 @@ class Type:
self.filterable = False
# Can this field have values that can be edited and validated?
self.validable = True
# The base label for translations is normally generated automatically.
# It is made of 2 parts: the prefix, based on class name, and the name,
# which is the field name by default. You can change this by specifying
# a value for param "label". If this value is a string, it will be
# understood as a new prefix. If it is a tuple, it will represent the
# prefix and another name. If you want to specify a new name only, and
# not a prefix, write (None, newName).
self.label = label
def init(self, name, klass, appName):
'''When the application server starts, this secondary constructor is
called for storing the names of the Appy field (p_name) and other
called for storing the name of the Appy field (p_name) and other
attributes that are based on the name of the Appy p_klass, and the
application name (p_appName).'''
self.name = name
@ -477,9 +485,18 @@ class Type:
self.id = id(self)
if self.slaves: self.master_css = 'appyMaster master_%s' % self.id
# Determine ids of i18n labels for this field
labelName = name
prefix = None
if self.label:
if isinstance(self.label, basestring): prefix = self.label
else: # It is a tuple (prefix, name)
if self.label[1]: labelName = self.label[1]
if self.label[0]: prefix = self.label[0]
if not prefix:
if not klass: prefix = appName
else: prefix = getClassName(klass, appName)
self.labelId = '%s_%s' % (prefix, name)
# Determine name to use for i18n
self.labelId = '%s_%s' % (prefix, labelName)
self.descrId = self.labelId + '_descr'
self.helpId = self.labelId + '_help'
# Determine read and write permissions for this field
@ -932,12 +949,13 @@ class Integer(Type):
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=6, height=None,
maxChars=13, colspan=1, master=None, masterValue=None,
focus=False, historized=False, mapping=None):
focus=False, historized=False, mapping=None, label=None):
Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, layouts, move, indexed,
searchable, specificReadPermission,
specificWritePermission, width, height, maxChars, colspan,
master, masterValue, focus, historized, True, mapping)
master, masterValue, focus, historized, True, mapping,
label)
self.pythonType = long
def validateValue(self, obj, value):
@ -961,8 +979,8 @@ class Float(Type):
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=6, height=None,
maxChars=13, colspan=1, master=None, masterValue=None,
focus=False, historized=False, mapping=None, precision=None,
sep=(',', '.')):
focus=False, historized=False, mapping=None, label=None,
precision=None, sep=(',', '.')):
# The precision is the number of decimal digits. This number is used
# for rendering the float, but the internal float representation is not
# rounded.
@ -981,7 +999,7 @@ class Float(Type):
editDefault, show, page, group, layouts, move, indexed,
False, specificReadPermission, specificWritePermission,
width, height, maxChars, colspan, master, masterValue,
focus, historized, True, mapping)
focus, historized, True, mapping, label)
self.pythonType = float
def getFormattedValue(self, obj, value):
@ -1130,7 +1148,8 @@ class String(Type):
indexed=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
maxChars=None, colspan=1, master=None, masterValue=None,
focus=False, historized=False, mapping=None, transform='none'):
focus=False, historized=False, mapping=None, label=None,
transform='none'):
self.format = format
# The following field has a direct impact on the text entered by the
# user. It applies a transformation on it, exactly as does the CSS
@ -1142,7 +1161,8 @@ class String(Type):
editDefault, show, page, group, layouts, move, indexed,
searchable, specificReadPermission,
specificWritePermission, width, height, maxChars, colspan,
master, masterValue, focus, historized, True, mapping)
master, masterValue, focus, historized, True, mapping,
label)
self.isSelect = self.isSelection()
# Default width, height and maxChars vary according to String format
if width == None:
@ -1374,12 +1394,13 @@ class Boolean(Type):
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
maxChars=None, colspan=1, master=None, masterValue=None,
focus=False, historized=False, mapping=None):
focus=False, historized=False, mapping=None, label=None):
Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, layouts, move, indexed,
searchable, specificReadPermission,
specificWritePermission, width, height, None, colspan,
master, masterValue, focus, historized, True, mapping)
master, masterValue, focus, historized, True, mapping,
label)
self.pythonType = bool
def getDefaultLayouts(self):
@ -1417,7 +1438,7 @@ class Date(Type):
indexed=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
maxChars=None, colspan=1, master=None, masterValue=None,
focus=False, historized=False, mapping=None):
focus=False, historized=False, mapping=None, label=None):
self.format = format
self.calendar = calendar
self.startYear = startYear
@ -1429,7 +1450,8 @@ class Date(Type):
editDefault, show, page, group, layouts, move, indexed,
searchable, specificReadPermission,
specificWritePermission, width, height, None, colspan,
master, masterValue, focus, historized, True, mapping)
master, masterValue, focus, historized, True, mapping,
label)
def getCss(self, layoutType):
if (layoutType == 'edit') and self.calendar:
@ -1490,13 +1512,14 @@ class File(Type):
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
maxChars=None, colspan=1, master=None, masterValue=None,
focus=False, historized=False, mapping=None, isImage=False):
focus=False, historized=False, mapping=None, label=None,
isImage=False):
self.isImage = isImage
Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, layouts, move, indexed,
False, specificReadPermission, specificWritePermission,
width, height, None, colspan, master, masterValue, focus,
historized, True, mapping)
historized, True, mapping, label)
@staticmethod
def getFileObject(filePath, fileName=None, zope=False):
@ -1640,8 +1663,8 @@ class Ref(Type):
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=5,
maxChars=None, colspan=1, master=None, masterValue=None,
focus=False, historized=False, mapping=None, queryable=False,
queryFields=None, queryNbCols=1):
focus=False, historized=False, mapping=None, label=None,
queryable=False, queryFields=None, queryNbCols=1):
self.klass = klass
self.attribute = attribute
# May the user add new objects through this ref ?
@ -1656,6 +1679,7 @@ class Ref(Type):
self.link = link
# May the user unlink existing objects?
self.unlink = unlink
self.back = None
if back:
# It is a forward reference
self.isBack = False
@ -1691,7 +1715,7 @@ class Ref(Type):
editDefault, show, page, group, layouts, move, indexed,
False, specificReadPermission, specificWritePermission,
width, height, None, colspan, master, masterValue, focus,
historized, sync, mapping)
historized, sync, mapping, label)
self.validable = self.link
def getDefaultLayouts(self): return {'view': Table('l-f'), 'edit': 'lrv-f'}
@ -1858,7 +1882,7 @@ class Computed(Type):
specificWritePermission=False, width=None, height=None,
maxChars=None, colspan=1, method=None, plainText=True,
master=None, masterValue=None, focus=False, historized=False,
sync=True, mapping=None, context={}):
sync=True, mapping=None, label=None, context={}):
# The Python method used for computing the field value
self.method = method
# Does field computation produce plain text or XHTML?
@ -1875,7 +1899,7 @@ class Computed(Type):
False, show, page, group, layouts, move, indexed, False,
specificReadPermission, specificWritePermission, width,
height, None, colspan, master, masterValue, focus,
historized, sync, mapping)
historized, sync, mapping, label)
self.validable = False
def callMacro(self, obj, macroPath):
@ -1925,7 +1949,7 @@ class Action(Type):
specificWritePermission=False, width=None, height=None,
maxChars=None, colspan=1, action=None, result='computation',
confirm=False, master=None, masterValue=None, focus=False,
historized=False, mapping=None):
historized=False, mapping=None, label=None):
# Can be a single method or a list/tuple of methods
self.action = action
# For the 'result' param:
@ -1946,7 +1970,7 @@ class Action(Type):
False, show, page, group, layouts, move, indexed, False,
specificReadPermission, specificWritePermission, width,
height, None, colspan, master, masterValue, focus,
historized, False, mapping)
historized, False, mapping, label)
self.validable = False
def getDefaultLayouts(self): return {'view': 'l-f', 'edit': 'lrv-f'}
@ -1994,12 +2018,12 @@ class Info(Type):
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
maxChars=None, colspan=1, master=None, masterValue=None,
focus=False, historized=False, mapping=None):
focus=False, historized=False, mapping=None, label=None):
Type.__init__(self, None, (0,1), index, default, optional,
False, show, page, group, layouts, move, indexed, False,
specificReadPermission, specificWritePermission, width,
height, None, colspan, master, masterValue, focus,
historized, False, mapping)
historized, False, mapping, label)
self.validable = False
class Pod(Type):
@ -2015,9 +2039,9 @@ class Pod(Type):
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
maxChars=None, colspan=1, master=None, masterValue=None,
focus=False, historized=False, mapping=None, template=None,
context=None, action=None, askAction=False, stylesMapping={},
freezeFormat='pdf'):
focus=False, historized=False, mapping=None, label=None,
template=None, context=None, action=None, askAction=False,
stylesMapping={}, freezeFormat='pdf'):
# The following param stores the path to a POD template
self.template = template
# The context is a dict containing a specific pod context, or a method
@ -2037,7 +2061,8 @@ class Pod(Type):
False, show, page, group, layouts, move, indexed,
searchable, specificReadPermission,
specificWritePermission, width, height, None, colspan,
master, masterValue, focus, historized, False, mapping)
master, masterValue, focus, historized, False, mapping,
label)
self.validable = False
def isFrozen(self, obj):

View file

@ -147,9 +147,10 @@ class FieldDescriptor:
(self.fieldName not in ('title', 'description')):
self.classDescr.addIndexMethod(self)
# i18n labels
i18nPrefix = "%s_%s" % (self.classDescr.name, self.fieldName)
# Create labels for generating them in i18n files.
messages = self.generator.labels
if not self.appyType.label:
# Create labels for generating them in i18n files, only if required.
i18nPrefix = "%s_%s" % (self.classDescr.name, self.fieldName)
if self.appyType.hasLabel:
messages.append(self.produceMessage(i18nPrefix))
if self.appyType.hasDescr:
@ -561,8 +562,8 @@ class TranslationClassDescriptor(ClassDescriptor):
def addLabelField(self, messageId, page):
'''Adds a Computed field that will display, in the source language, the
content of the text to translate.'''
field = Computed(method=self.modelClass.computeLabel, plainText=False,
page=page, show=self.modelClass.showField, layouts='f')
field = Computed(method=self.modelClass.label, plainText=False,
page=page, show=self.modelClass.show, layouts='f')
self.addField('%s_label' % messageId, field)
def addMessageField(self, messageId, page, i18nFiles):
@ -570,7 +571,7 @@ class TranslationClassDescriptor(ClassDescriptor):
class, on a given p_page. We need i18n files p_i18nFiles for
fine-tuning the String type to generate for this field (one-line?
several lines?...)'''
params = {'page':page, 'layouts':'f', 'show':self.modelClass.showField}
params = {'page':page, 'layouts':'f', 'show': self.modelClass.show}
appName = self.generator.applicationName
# Scan all messages corresponding to p_messageId from all translation
# files. We will define field length from the longer found message

View file

@ -590,9 +590,8 @@ class Generator(AbstractGenerator):
'''Generates the Plone tool that corresponds to this application.'''
Msg = PoMessage
# Create Tool-related i18n-related messages
self.labels += [
Msg(self.tool.name, '', Msg.CONFIG % self.applicationName),
Msg('%s_edit_descr' % self.tool.name, '', ' ')]
msg = Msg(self.tool.name, '', Msg.CONFIG % self.applicationName)
self.labels.append(msg)
# Tune the Ref field between Tool and User
Tool.users.klass = User
@ -604,7 +603,6 @@ class Generator(AbstractGenerator):
klassType = klass.name[len(self.applicationName):]
klass.generateSchema()
self.labels += [ Msg(klass.name, '', klassType),
Msg('%s_edit_descr' % klass.name, '', ' '),
Msg('%s_plural' % klass.name,'', klass.name+'s')]
repls = self.repls.copy()
repls.update({'fields': klass.schema, 'methods': klass.methods,
@ -702,12 +700,10 @@ class Generator(AbstractGenerator):
'implements': implements, 'baseSchema': baseSchema, 'static': '',
'register': register, 'toolInstanceName': self.toolInstanceName})
fileName = '%s.py' % classDescr.name
# Create i18n labels (class name, description and plural form)
# Create i18n labels (class name and plural form)
poMsg = PoMessage(classDescr.name, '', classDescr.klass.__name__)
poMsg.produceNiceDefault()
self.labels.append(poMsg)
poMsgDescr = PoMessage('%s_edit_descr' % classDescr.name, '', ' ')
self.labels.append(poMsgDescr)
poMsgPl = PoMessage('%s_plural' % classDescr.name, '',
classDescr.klass.__name__+'s')
poMsgPl.produceNiceDefault()

View file

@ -9,6 +9,33 @@
import types
from appy.gen import *
# Prototypical instances of every type -----------------------------------------
class Protos:
protos = {}
# List of attributes that can't be given to a Type constructor
notInit = ('id', 'type', 'pythonType', 'slaves', 'isSelect', 'hasLabel',
'hasDescr', 'hasHelp', 'master_css', 'required', 'filterable',
'validable', 'backd', 'isBack', 'sync', 'pageName')
@classmethod
def get(self, appyType):
'''Returns a prototype instance for p_appyType.'''
className = appyType.__class__.__name__
isString = (className == 'String')
if isString:
# For Strings, we create one prototype per format, because default
# values may change according to format.
className += str(appyType.format)
if className in self.protos: return self.protos[className]
# The prototype does not exist yet: create it
if isString:
proto = appyType.__class__(format=appyType.format)
# Now, we fake to be able to detect default values
proto.format = 0
else:
proto = appyType.__class__()
self.protos[className] = proto
return proto
# ------------------------------------------------------------------------------
class ModelClass:
'''This class is the abstract class of all predefined application classes
@ -17,60 +44,73 @@ class ModelClass:
in order to avoid name conflicts with user-defined parts of the
application model.'''
_appy_attributes = [] # We need to keep track of attributes order.
# When creating a new instance of a ModelClass, the following attributes
# must not be given in the constructor (they are computed attributes).
_appy_notinit = ('id', 'type', 'pythonType', 'slaves', 'isSelect',
'hasLabel', 'hasDescr', 'hasHelp', 'master_css',
'required', 'filterable', 'validable', 'backd', 'isBack',
'sync', 'pageName')
@classmethod
def _appy_getTypeBody(klass, appyType):
def _appy_getTypeBody(klass, appyType, wrapperName):
'''This method returns the code declaration for p_appyType.'''
typeArgs = ''
for attrName, attrValue in appyType.__dict__.iteritems():
if attrName in ModelClass._appy_notinit: continue
if attrName == 'layouts':
if klass.__name__ == 'Tool': continue
proto = Protos.get(appyType)
for name, value in appyType.__dict__.iteritems():
# Some attrs can't be given to the constructor
if name in Protos.notInit: continue
# If the given value corresponds to the default value, don't give it
if value == getattr(proto, name): continue
if name == 'layouts':
# For Tool attributes we do not copy layout info. Indeed, most
# fields added to the Tool are config-related attributes whose
# layouts must be standard.
attrValue = appyType.getInputLayouts()
elif isinstance(attrValue, basestring):
attrValue = '"%s"' % attrValue
elif isinstance(attrValue, Ref):
if not attrValue.isBack: continue
attrValue = klass._appy_getTypeBody(attrValue)
elif type(attrValue) == type(ModelClass):
moduleName = attrValue.__module__
if moduleName.startswith('appy.gen'):
attrValue = attrValue.__name__
if klass.__name__ == 'Tool': continue
layouts = appyType.getInputLayouts()
# For the Translation class that has potentially thousands of
# attributes, the most used layout is cached in a global var in
# named "tfw" in appyWrappers.py.
if (klass.__name__ == 'Translation') and \
(layouts == '{"edit":"f","cell":"f","view":"f",}'):
value = 'tfw'
else:
attrValue = '%s.%s' % (moduleName, attrValue.__name__)
elif isinstance(attrValue, Selection):
attrValue = 'Selection("%s")' % attrValue.methodName
elif isinstance(attrValue, Group):
attrValue = 'Group("%s")' % attrValue.name
elif isinstance(attrValue, Page):
attrValue = 'pages["%s"]' % attrValue.name
elif callable(attrValue):
attrValue = '%sWrapper.%s'% (klass.__name__, attrValue.__name__)
typeArgs += '%s=%s,' % (attrName, attrValue)
value = appyType.getInputLayouts()
elif isinstance(value, basestring):
value = '"%s"' % value
elif isinstance(value, Ref):
if not value.isBack: continue
value = klass._appy_getTypeBody(value, wrapperName)
elif type(value) == type(ModelClass):
moduleName = value.__module__
if moduleName.startswith('appy.gen'):
value = value.__name__
else:
value = '%s.%s' % (moduleName, value.__name__)
elif isinstance(value, Selection):
value = 'Selection("%s")' % value.methodName
elif isinstance(value, Group):
value = 'Group("%s")' % value.name
elif isinstance(value, Page):
value = 'pages["%s"]' % value.name
elif callable(value):
value = '%s.%s' % (wrapperName, value.__name__)
typeArgs += '%s=%s,' % (name, value)
return '%s(%s)' % (appyType.__class__.__name__, typeArgs)
@classmethod
def _appy_getBody(klass):
'''This method returns the code declaration of this class. We will dump
this in appyWrappers.py in the resulting product.'''
res = 'class %s(%sWrapper):\n' % (klass.__name__, klass.__name__)
if klass.__name__ == 'Tool':
res += ' folder=True\n'
className = klass.__name__
# Determine the name of the class and its wrapper. Because so much
# attributes can be generated on a TranslationWrapper, shortcutting it
# to 'TW' may reduce the generated file from several kilobytes.
if className == 'Translation': wrapperName = 'WT'
else: wrapperName = 'W%s' % className
res = 'class %s(%s):\n' % (className, wrapperName)
# Tool must be folderish
if className == 'Tool': res += ' folder=True\n'
# First, scan all attributes, determine all used pages and create a
# dict with it. It will prevent us from creating a new Page instance
# for every field.
pages = {}
for attrName in klass._appy_attributes:
exec 'appyType = klass.%s' % attrName
layouts = []
for name in klass._appy_attributes:
exec 'appyType = klass.%s' % name
if appyType.page.name not in pages:
pages[appyType.page.name] = appyType.page
res += ' pages = {'
@ -81,9 +121,10 @@ class ModelClass:
res += '"%s":Page("%s", show=%s),'% (page.name, page.name, pageShow)
res += '}\n'
# Secondly, dump every attribute
for attrName in klass._appy_attributes:
exec 'appyType = klass.%s' % attrName
res += ' %s=%s\n' % (attrName, klass._appy_getTypeBody(appyType))
for name in klass._appy_attributes:
exec 'appyType = klass.%s' % name
typeBody = klass._appy_getTypeBody(appyType, wrapperName)
res += ' %s=%s\n' % (name, typeBody)
return res
# The User class ---------------------------------------------------------------
@ -115,8 +156,8 @@ class Translation(ModelClass):
po = Action(action=getPoFile, page=Page('actions', show='view'),
result='filetmp')
title = String(show=False, indexed=True)
def computeLabel(self): pass
def showField(self, name): pass
def label(self): pass
def show(self, name): pass
# The Tool class ---------------------------------------------------------------
# Here are the prefixes of the fields generated on the Tool.

View file

@ -564,11 +564,6 @@
<b class="appyTitle" tal:content="contextObj/title_or_id"></b>
</td>
</tr>
<tr tal:define="descrLabel python: contextObj.translate('%s_edit_descr' % contextObj.portal_type)"
tal:condition="descrLabel/strip" align="left">
<tal:comment replace="nothing">Content type description</tal:comment>
<td colspan="2" class="discreet" tal:content="descrLabel"/>
</tr>
<tr align="left">
<td class="documentByLine" colspan="2">
<tal:comment replace="nothing">Creator and last modification date</tal:comment>

View file

@ -27,7 +27,7 @@ class <!genClassName!>(<!parents!>):
default_view = 'skyn/view'
suppl_views = ()
typeDescription = '<!genClassName!>'
typeDescMsgId = '<!genClassName!>_edit_descr'
typeDescMsgId = '<!genClassName!>'
i18nDomain = '<!applicationName!>'
wrapperClass = <!genClassName!>_Wrapper
schema = fullSchema

View file

@ -1,11 +1,12 @@
# ------------------------------------------------------------------------------
from appy.gen import *
from appy.gen.plone25.wrappers import AbstractWrapper
from appy.gen.plone25.wrappers.ToolWrapper import ToolWrapper
from appy.gen.plone25.wrappers.UserWrapper import UserWrapper
from appy.gen.plone25.wrappers.TranslationWrapper import TranslationWrapper
from appy.gen.plone25.wrappers.ToolWrapper import ToolWrapper as WTool
from appy.gen.plone25.wrappers.UserWrapper import UserWrapper as WUser
from appy.gen.plone25.wrappers.TranslationWrapper import TranslationWrapper as WT
from Globals import InitializeClass
from AccessControl import ClassSecurityInfo
tfw = {"edit":"f","cell":"f","view":"f"} # Layout for Translation fields
<!imports!>
<!User!>

View file

@ -6,7 +6,7 @@ from appy.shared.utils import getOsTempFolder
# ------------------------------------------------------------------------------
class TranslationWrapper(AbstractWrapper):
def computeLabel(self, field):
def label(self, field):
'''The label for a text to translate displays the text of the
corresponding message in the source translation.'''
tool = self.tool
@ -32,7 +32,7 @@ class TranslationWrapper(AbstractWrapper):
'<img src="help.png"/></acronym>%s</div>' % \
(fieldName, sourceMsg)
def showField(self, field):
def show(self, field):
'''We show a field (or its label) only if the corresponding source
message is not empty.'''
tool = self.tool