appy.gen: every field can now define a dict of i18n mappings that will be used for translating their label and/or description and/or help message(s).

This commit is contained in:
Gaetan Delannay 2011-03-04 10:30:45 +01:00
parent fd9dd569db
commit 66a02c453e
4 changed files with 85 additions and 36 deletions

View file

@ -20,6 +20,7 @@ nullValues = (None, '', ' ')
validatorTypes = (types.FunctionType, types.UnboundMethodType,
type(re.compile('')))
emptyTuple = ()
labelTypes = ('label', 'descr', 'help')
# Descriptor classes used for refining descriptions of elements in types
# (pages, groups,...) ----------------------------------------------------------
@ -356,7 +357,7 @@ class Type:
editDefault, show, page, group, layouts, move, indexed,
searchable, specificReadPermission, specificWritePermission,
width, height, colspan, master, masterValue, focus,
historized, sync):
historized, sync, mapping):
# 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.
@ -443,6 +444,9 @@ class Type:
# self.sync below determines if the field representations will be
# retrieved in a synchronous way by the browser or not (Ajax).
self.sync = self.formatSync(sync)
# Mapping is a dict of contexts that, if specified, are given when
# translating the label, descr or help related to this field.
self.mapping = self.formatMapping(mapping)
self.id = id(self)
self.type = self.__class__.__name__
self.pythonType = None # The True corresponding Python type
@ -571,6 +575,26 @@ class Type:
sync[layoutType] = False
return sync
def formatMapping(self, mapping):
'''Creates a dict of mappings, one entry by label type (label, descr,
help).'''
if isinstance(mapping, dict):
# Is it a dict like {'label':..., 'descr':...}, or is it directly a
# dict with a mapping?
for k, v in mapping.iteritems():
if (k not in labelTypes) or isinstance(v, basestring):
# It is already a mapping
return {'label':mapping, 'descr':mapping, 'help':mapping}
# If we are here, we have {'label':..., 'descr':...}. Complete
# it if necessary.
for labelType in labelTypes:
if labelType not in mapping:
mapping[labelType] = None # No mapping for this value.
return mapping
else:
# Mapping is a method that must be applied to any i18n message.
return {'label':mapping, 'descr':mapping, 'help':mapping}
def formatLayouts(self, layouts):
'''Standardizes the given p_layouts. .'''
# First, get the layouts as a dictionary, if p_layouts is None or
@ -696,10 +720,12 @@ class Type:
# Return self.default, of self.default() if it is a method
if callable(self.default):
try:
return self.callMethod(obj, self.default,
raiseOnError=True)
return self.callMethod(obj, self.default)
except Exception, e:
# Already logged.
# Already logged. Here I do not raise the exception,
# because it can be raised as the result of reindexing
# the object in situations that are not foreseen by
# method in self.default.
return None
else:
return self.default
@ -850,7 +876,7 @@ class Type:
res.validator = None
return res
def callMethod(self, obj, method, raiseOnError=False):
def callMethod(self, obj, method, raiseOnError=True):
'''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
field, a method used for showing or not a field...). Normally, those
@ -881,12 +907,12 @@ class Integer(Type):
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=6, height=None,
colspan=1, master=None, masterValue=None, focus=False,
historized=False):
historized=False, mapping=None):
Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, layouts, move, indexed,
searchable, specificReadPermission,
specificWritePermission, width, height, colspan, master,
masterValue, focus, historized, True)
masterValue, focus, historized, True, mapping)
self.pythonType = long
def validateValue(self, obj, value):
@ -910,7 +936,8 @@ class Float(Type):
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=6, height=None,
colspan=1, master=None, masterValue=None, focus=False,
historized=False, precision=None, sep=(',', '.')):
historized=False, mapping=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.
@ -929,7 +956,7 @@ class Float(Type):
editDefault, show, page, group, layouts, move, indexed,
False, specificReadPermission, specificWritePermission,
width, height, colspan, master, masterValue, focus,
historized, True)
historized, True, mapping)
self.pythonType = float
def getFormattedValue(self, obj, value):
@ -1078,7 +1105,7 @@ class String(Type):
indexed=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
colspan=1, master=None, masterValue=None, focus=False,
historized=False, transform='none'):
historized=False, mapping=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
@ -1090,7 +1117,7 @@ class String(Type):
editDefault, show, page, group, layouts, move, indexed,
searchable, specificReadPermission,
specificWritePermission, width, height, colspan, master,
masterValue, focus, historized, True)
masterValue, focus, historized, True, mapping)
self.isSelect = self.isSelection()
# Default width and height vary according to String format
if width == None:
@ -1312,12 +1339,12 @@ class Boolean(Type):
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
colspan=1, master=None, masterValue=None, focus=False,
historized=False):
historized=False, mapping=None):
Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, layouts, move, indexed,
searchable, specificReadPermission,
specificWritePermission, width, height, colspan, master,
masterValue, focus, historized, True)
masterValue, focus, historized, True, mapping)
self.pythonType = bool
def getDefaultLayouts(self):
@ -1355,7 +1382,7 @@ class Date(Type):
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
colspan=1, master=None, masterValue=None, focus=False,
historized=False):
historized=False, mapping=None):
self.format = format
self.calendar = calendar
self.startYear = startYear
@ -1364,7 +1391,7 @@ class Date(Type):
editDefault, show, page, group, layouts, move, indexed,
searchable, specificReadPermission,
specificWritePermission, width, height, colspan, master,
masterValue, focus, historized, True)
masterValue, focus, historized, True, mapping)
def getCss(self, layoutType):
if (layoutType == 'edit') and self.calendar:
@ -1419,13 +1446,13 @@ class File(Type):
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
colspan=1, master=None, masterValue=None, focus=False,
historized=False, isImage=False):
historized=False, mapping=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, colspan, master, masterValue, focus,
historized, True)
historized, True, mapping)
@staticmethod
def getFileObject(filePath, fileName=None, zope=False):
@ -1569,8 +1596,8 @@ class Ref(Type):
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=5,
colspan=1, master=None, masterValue=None, focus=False,
historized=False, queryable=False, queryFields=None,
queryNbCols=1):
historized=False, mapping=None, queryable=False,
queryFields=None, queryNbCols=1):
self.klass = klass
self.attribute = attribute
# May the user add new objects through this ref ?
@ -1620,7 +1647,7 @@ class Ref(Type):
editDefault, show, page, group, layouts, move, indexed,
False, specificReadPermission, specificWritePermission,
width, height, colspan, master, masterValue, focus,
historized, sync)
historized, sync, mapping)
self.validable = self.link
def getDefaultLayouts(self): return {'view': Table('l-f'), 'edit': 'lrv-f'}
@ -1787,7 +1814,7 @@ class Computed(Type):
specificWritePermission=False, width=None, height=None,
colspan=1, method=None, plainText=True, master=None,
masterValue=None, focus=False, historized=False, sync=True,
context={}):
mapping=None, context={}):
# The Python method used for computing the field value
self.method = method
# Does field computation produce plain text or XHTML?
@ -1804,7 +1831,7 @@ class Computed(Type):
False, show, page, group, layouts, move, indexed, False,
specificReadPermission, specificWritePermission, width,
height, colspan, master, masterValue, focus, historized,
sync)
sync, mapping)
self.validable = False
def callMacro(self, obj, macroPath):
@ -1853,7 +1880,8 @@ class Action(Type):
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
colspan=1, action=None, result='computation', confirm=False,
master=None, masterValue=None, focus=False, historized=False):
master=None, masterValue=None, focus=False, historized=False,
mapping=None):
# Can be a single method or a list/tuple of methods
self.action = action
# For the 'result' param:
@ -1874,7 +1902,7 @@ class Action(Type):
False, show, page, group, layouts, move, indexed, False,
specificReadPermission, specificWritePermission, width,
height, colspan, master, masterValue, focus, historized,
False)
False, mapping)
self.validable = False
def getDefaultLayouts(self): return {'view': 'l-f', 'edit': 'lrv-f'}
@ -1922,12 +1950,12 @@ class Info(Type):
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
colspan=1, master=None, masterValue=None, focus=False,
historized=False):
historized=False, mapping=None):
Type.__init__(self, None, (0,1), index, default, optional,
False, show, page, group, layouts, move, indexed, False,
specificReadPermission, specificWritePermission, width,
height, colspan, master, masterValue, focus, historized,
False)
False, mapping)
self.validable = False
class Pod(Type):
@ -1943,8 +1971,9 @@ class Pod(Type):
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
colspan=1, master=None, masterValue=None, focus=False,
historized=False, template=None, context=None, action=None,
askAction=False, stylesMapping={}, freezeFormat='pdf'):
historized=False, mapping=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
@ -1964,7 +1993,7 @@ class Pod(Type):
False, show, page, group, layouts, move, indexed,
searchable, specificReadPermission,
specificWritePermission, width, height, colspan, master,
masterValue, focus, historized, False)
masterValue, focus, historized, False, mapping)
self.validable = False
def isFrozen(self, obj):

View file

@ -1175,8 +1175,15 @@ class BaseMixin:
return res
def translate(self, label, mapping={}, domain=None, default=None,
language=None, format='html'):
'''Translates a given p_label into p_domain with p_mapping.'''
language=None, format='html', field=None, className=None):
'''Translates a given p_label into p_domain with p_mapping.
If p_field is given, p_label does not correspond to a full label
name, but to a label type linked to p_field: "label", "descr"
or "help". Indeed, in this case, a specific i18n mapping may be
available on the field, so we must merge this mapping into
p_mapping. If p_className is not given, we consider p_self being an
instance of the class where p_field is defined.'''
cfg = self.getProductConfig()
if not domain: domain = cfg.PROJECTNAME
if domain != cfg.PROJECTNAME:
@ -1190,6 +1197,19 @@ class BaseMixin:
# TranslationService
res = label
else:
# Get the label name, and the field-specific mapping if any.
if field:
# Maybe we do not have the field itself, but only its name
if isinstance(field, basestring):
field = self.getAppyType(field, className=className)
# Include field-specific mapping if any.
fieldMapping = field.mapping[label]
if fieldMapping:
if callable(fieldMapping):
fieldMapping = field.callMethod(self, fieldMapping)
mapping.update(fieldMapping)
# Get the label
label = getattr(field, label+'Id')
# We will get the translation from a Translation object.
# In what language must we get the translation?
if not language: language = self.getUserLanguage()

View file

@ -120,9 +120,9 @@
maxReached python:(multiplicity[1] != None) and (len(objs) >= multiplicity[1]);
showPlusIcon python:not appyType['isBack'] and appyType['add'] and not maxReached and member.has_permission(addPermission, folder) and canWrite;
atMostOneRef python: (multiplicity[1] == 1) and (len(objs)<=1);
label python: tool.translate(appyType['labelId']);
label python: contextObj.translate('label', field=appyType['name']);
addConfirmMsg python: tool.translate('%s_addConfirm' % appyType['labelId']);
description python: tool.translate(appyType['descrId']);
description python: contextObj.translate('descr', field=appyType['name']);
navBaseCall python: 'askRefField(\'%s\',\'%s\',\'%s\',\'%s\',**v**)' % (ajaxHookId, contextObj.absolute_url(), fieldName, innerRef)">
<tal:comment replace="nothing">This macro displays the Reference widget on a "consult" page.

View file

@ -186,17 +186,17 @@
<tal:label metal:define-macro="label" condition="widget/hasLabel">
<label tal:attributes="for widget/name"
tal:condition="python: not ((widget['type'] == 'Action') or ((widget['type'] == 'Ref') and (widget['add'])))"
tal:content="structure python: contextObj.translate(widget['labelId'])"></label>
tal:content="structure python: contextObj.translate('label', field=widget['name'])"></label>
</tal:label>
<tal:comment replace="nothing">Displays a field description.</tal:comment>
<tal:description metal:define-macro="description" condition="widget/hasDescr">
<span class="discreet" tal:content="structure python: contextObj.translate(widget['descrId'])"></span>
<span class="discreet" tal:content="structure python: contextObj.translate('descr', field=widget['name'])"></span>
</tal:description>
<tal:comment replace="nothing">Displays a field help.</tal:comment>
<tal:help metal:define-macro="help">
<acronym tal:attributes="title python: contextObj.translate(widget['helpId'])">
<acronym tal:attributes="title python: contextObj.translate('help', field=widget['name'])">
<img tal:attributes="src string: $portal_url/skyn/help.png"/>
</acronym>
</tal:help>