New translation system, that generates screens for updating translations through the web, within the configuration.

This commit is contained in:
Gaetan Delannay 2011-01-14 09:06:25 +01:00
parent f3604624de
commit ead9f7c2de
22 changed files with 525 additions and 278 deletions

View file

@ -1 +1 @@
0.6.1
0.6.2

View file

@ -70,7 +70,8 @@ class Page:
showAttr = 'show%s' % elem.capitalize()
# Get the value of the show attribute as identified above.
show = getattr(self, showAttr)
if callable(show): show = show(obj.appy())
if callable(show):
show = show(obj.appy())
# Show value can be 'view', for example. Thanks to p_layoutType,
# convert show value to a real final boolean value.
res = show
@ -407,7 +408,7 @@ class Type:
# permission (string) instead of assigning "True" to the following
# arg(s). A named permission will be global to your whole Zope site, so
# take care to the naming convention. Typically, a named permission is
# of the form: "<yourAppName>: Write|Read xxx". If, for example, I want
# of the form: "<yourAppName>: Write|Read ---". If, for example, I want
# to define, for my application "MedicalFolder" a specific permission
# for a bunch of fields that can only be modified by a doctor, I can
# define a permission "MedicalFolder: Write medical information" and
@ -531,7 +532,7 @@ class Type:
return False
# Evaluate self.show
if callable(self.show):
res = self.show(obj.appy())
res = self.callMethod(obj, self.show)
else:
res = self.show
# Take into account possible values 'view' and 'edit' for 'show' param.
@ -663,6 +664,15 @@ class Type:
default layouts. If None is returned, a global set of default layouts
will be used.'''
def getInputLayouts(self):
'''Gets, as a string, the layouts as could have been specified as input
value for the Type constructor.'''
res = '{'
for k, v in self.layouts.iteritems():
res += '"%s":"%s",' % (k, v['layoutString'])
res += '}'
return res
def computeDefaultLayouts(self):
'''This method gets the default layouts from an Appy type, or a copy
from the global default field layouts when they are not available.'''
@ -687,13 +697,12 @@ class Type:
# If there is no value, get the default value if any
if not self.editDefault:
# Return self.default, of self.default() if it is a method
if type(self.default) == types.FunctionType:
appyObj = obj.appy()
if callable(self.default):
try:
return self.default(appyObj)
return self.callMethod(obj, self.default,
raiseOnError=True)
except Exception, e:
appyObj.log('Exception while getting default value ' \
'of field "%s": %s.' % (self.name, str(e)))
# Already logged.
return None
else:
return self.default
@ -839,11 +848,35 @@ class Type:
res.specificReadPermission = False
res.specificWritePermission = False
res.multiplicity = (0, self.multiplicity[1])
if type(res.validator) == types.FunctionType:
if callable(res.validator):
# We will not be able to call this function from the tool.
res.validator = None
return res
def callMethod(self, obj, method, raiseOnError=False):
'''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
methods are called without any arg. But one may need, within the
method, to access the related field. This method tries to call
p_method with no arg *or* with the field arg.'''
obj = obj.appy()
try:
return method(obj)
except TypeError, te:
# Try a version of the method that would accept self as an
# additional parameter.
try:
return method(obj, self)
except Exception, e:
obj.log(Traceback.get(), type='error')
if raiseOnError: raise e
else: return str(e)
except Exception, e:
obj.log(Traceback.get(), type='error')
if raiseOnError: raise e
else: return str(e)
class Integer(Type):
def __init__(self, validator=None, multiplicity=(0,1), index=None,
default=None, optional=False, editDefault=False, show=True,
@ -1139,6 +1172,10 @@ class String(Type):
res = unicode(value)
except UnicodeDecodeError:
res = str(value)
# If value starts with a carriage return, add a space; else, it will
# be ignored.
if isinstance(res, basestring) and \
(res.startswith('\n') or res.startswith('\r\n')): res = ' ' + res
return res
def getIndexValue(self, obj, forSearch=False):
@ -1719,12 +1756,7 @@ class Computed(Type):
def getValue(self, obj):
'''Computes the value instead of getting it in the database.'''
if not self.method: return
obj = obj.appy()
try:
return self.method(obj)
except Exception, e:
obj.log(Traceback.get(), type='error')
return str(e)
return self.callMethod(obj, self.method, raiseOnError=False)
def getFormattedValue(self, obj, value):
if not isinstance(value, basestring): return str(value)
@ -2106,6 +2138,11 @@ class Config:
# If you don't need the portlet that appy.gen has generated for your
# application, set the following parameter to False.
self.showPortlet = True
# Number of translations for every page on a Translation object
self.translationsPerPage = 30
# Language that will be used as a basis for translating to other
# languages.
self.sourceLanguage = 'en'
# ------------------------------------------------------------------------------
# Special field "type" is mandatory for every class. If one class does not

View file

@ -70,12 +70,22 @@ class ClassDescriptor(Descriptor):
return res
def getPhases(self):
'''Gets the phases defined on fields of this class.'''
res = []
'''Lazy-gets the phases defined on fields of this class.'''
if not hasattr(self, 'phases') or (self.phases == None):
self.phases = []
for fieldName, appyType, klass in self.getOrderedAppyAttributes():
if appyType.page.phase not in res:
res.append(appyType.page.phase)
return res
if appyType.page.phase in self.phases: continue
self.phases.append(appyType.page.phase)
return self.phases
def getPages(self):
'''Lazy-gets the page names defined on fields of this class.'''
if not hasattr(self, 'pages') or (self.pages == None):
self.pages = []
for fieldName, appyType, klass in self.getOrderedAppyAttributes():
if appyType.page.name in self.pages: continue
self.pages.append(appyType.page.name)
return self.pages
class WorkflowDescriptor(Descriptor):
'''This class gives information about an Appy workflow.'''

View file

@ -146,8 +146,6 @@ class FieldDescriptor:
if self.appyType.indexed and \
(self.fieldName not in ('title', 'description')):
self.classDescr.addIndexMethod(self)
# - searchable ? TODO
#if self.appyType.searchable: self.fieldParams['searchable'] = True
# i18n labels
i18nPrefix = "%s_%s" % (self.classDescr.name, self.fieldName)
# Create labels for generating them in i18n files.
@ -160,17 +158,20 @@ class FieldDescriptor:
if self.appyType.hasHelp:
helpId = i18nPrefix + '_help'
messages.append(self.produceMessage(helpId, isLabel=False))
# Create i18n messages linked to pages and phases
messages = self.generator.labels
pageMsgId = '%s_page_%s' % (self.classDescr.name,
self.appyType.page.name)
phaseMsgId = '%s_phase_%s' % (self.classDescr.name,
self.appyType.page.phase)
pagePoMsg = PoMessage(pageMsgId, '',
produceNiceMessage(self.appyType.page.name))
phasePoMsg = PoMessage(phaseMsgId, '',
produceNiceMessage(self.appyType.page.phase))
for poMsg in (pagePoMsg, phasePoMsg):
# Create i18n messages linked to pages and phases, only if there is more
# than one page/phase for the class.
ppMsgs = []
if len(self.classDescr.getPhases()) > 1:
# Create the message for the name of the phase
phaseName = self.appyType.page.phase
msgId = '%s_phase_%s' % (self.classDescr.name, phaseName)
ppMsgs.append(PoMessage(msgId, '', produceNiceMessage(phaseName)))
if len(self.classDescr.getPages()) > 1:
# Create the message for the name of the page
pageName = self.appyType.page.name
msgId = '%s_page_%s' % (self.classDescr.name, pageName)
ppMsgs.append(PoMessage(msgId, '', produceNiceMessage(pageName)))
for poMsg in ppMsgs:
if poMsg not in messages:
messages.append(poMsg)
self.classDescr.labelsToPropagate.append(poMsg)
@ -237,6 +238,9 @@ class ClassDescriptor(appy.gen.descriptors.ClassDescriptor):
self.name = getClassName(self.klass, generator.applicationName)
self.predefined = False
self.customized = False
# Phase and page names will be calculated later, when first required.
self.phases = None
self.pages = None
def getParents(self, allClasses):
parentWrapper = 'AbstractWrapper'
@ -365,11 +369,16 @@ class ClassDescriptor(appy.gen.descriptors.ClassDescriptor):
'self)\n' % n
self.methods = m
def addField(self, fieldName, fieldType):
'''Adds a new field to the Tool.'''
exec "self.modelClass.%s = fieldType" % fieldName
self.modelClass._appy_attributes.append(fieldName)
self.orderedAttributes.append(fieldName)
class ToolClassDescriptor(ClassDescriptor):
'''Represents the POD-specific fields that must be added to the tool.'''
def __init__(self, klass, generator):
ClassDescriptor.__init__(self,klass,klass._appy_attributes[:],generator)
self.attributesByClass = klass._appy_classes
self.modelClass = self.klass
self.predefined = True
self.customized = False
@ -393,13 +402,6 @@ class ToolClassDescriptor(ClassDescriptor):
def generateSchema(self):
ClassDescriptor.generateSchema(self, configClass=True)
def addField(self, fieldName, fieldType, classDescr):
'''Adds a new field to the Tool.'''
exec "self.modelClass.%s = fieldType" % fieldName
self.modelClass._appy_attributes.append(fieldName)
self.orderedAttributes.append(fieldName)
self.modelClass._appy_classes[fieldName] = classDescr.name
def addOptionalField(self, fieldDescr):
className = fieldDescr.classDescr.name
fieldName = 'optionalFieldsFor%s' % className
@ -407,7 +409,7 @@ class ToolClassDescriptor(ClassDescriptor):
if not fieldType:
fieldType = String(multiplicity=(0,None))
fieldType.validator = []
self.addField(fieldName, fieldType, fieldDescr.classDescr)
self.addField(fieldName, fieldType)
fieldType.validator.append(fieldDescr.fieldName)
fieldType.page.name = 'data'
fieldType.group = Group(fieldDescr.classDescr.klass.__name__)
@ -416,7 +418,7 @@ class ToolClassDescriptor(ClassDescriptor):
className = fieldDescr.classDescr.name
fieldName = 'defaultValueFor%s_%s' % (className, fieldDescr.fieldName)
fieldType = fieldDescr.appyType.clone()
self.addField(fieldName, fieldType, fieldDescr.classDescr)
self.addField(fieldName, fieldType)
fieldType.page.name = 'data'
fieldType.group = Group(fieldDescr.classDescr.klass.__name__)
@ -429,12 +431,12 @@ class ToolClassDescriptor(ClassDescriptor):
# Add the field that will store the pod template.
fieldName = 'podTemplateFor%s_%s' % (className, fieldDescr.fieldName)
fieldType = File(**pg)
self.addField(fieldName, fieldType, fieldDescr.classDescr)
self.addField(fieldName, fieldType)
# Add the field that will store the output format(s)
fieldName = 'formatsFor%s_%s' % (className, fieldDescr.fieldName)
fieldType = String(validator=('odt', 'pdf', 'doc', 'rtf'),
multiplicity=(1,None), default=('odt',), **pg)
self.addField(fieldName, fieldType, fieldDescr.classDescr)
self.addField(fieldName, fieldType)
def addQueryResultColumns(self, classDescr):
'''Adds, for class p_classDescr, the attribute in the tool that allows
@ -444,7 +446,7 @@ class ToolClassDescriptor(ClassDescriptor):
fieldType = String(multiplicity=(0,None), validator=Selection(
'_appy_getAllFields*%s' % className), page='userInterface',
group=classDescr.klass.__name__)
self.addField(fieldName, fieldType, classDescr)
self.addField(fieldName, fieldType)
def addSearchRelatedFields(self, classDescr):
'''Adds, for class p_classDescr, attributes related to the search
@ -455,13 +457,13 @@ class ToolClassDescriptor(ClassDescriptor):
fieldName = 'enableAdvancedSearchFor%s' % className
fieldType = Boolean(default=True, page='userInterface',
group=classDescr.klass.__name__)
self.addField(fieldName, fieldType, classDescr)
self.addField(fieldName, fieldType)
# Field that defines how many columns are shown on the custom search
# screen.
fieldName = 'numberOfSearchColumnsFor%s' % className
fieldType = Integer(default=3, page='userInterface',
group=classDescr.klass.__name__)
self.addField(fieldName, fieldType, classDescr)
self.addField(fieldName, fieldType)
# Field that allows to select, among all indexed fields, what fields
# must really be used in the search screen.
fieldName = 'searchFieldsFor%s' % className
@ -470,7 +472,7 @@ class ToolClassDescriptor(ClassDescriptor):
fieldType = String(multiplicity=(0,None), validator=Selection(
'_appy_getSearchableFields*%s' % className), default=defaultValue,
page='userInterface', group=classDescr.klass.__name__)
self.addField(fieldName, fieldType, classDescr)
self.addField(fieldName, fieldType)
def addImportRelatedFields(self, classDescr):
'''Adds, for class p_classDescr, attributes related to the import
@ -481,7 +483,7 @@ class ToolClassDescriptor(ClassDescriptor):
defValue = classDescr.getCreateMean('Import').path
fieldType = String(page='data', multiplicity=(1,1), default=defValue,
group=classDescr.klass.__name__)
self.addField(fieldName, fieldType, classDescr)
self.addField(fieldName, fieldType)
def addWorkflowFields(self, classDescr):
'''Adds, for a given p_classDescr, the workflow-related fields.'''
@ -495,12 +497,12 @@ class ToolClassDescriptor(ClassDescriptor):
fieldName = 'showWorkflowFor%s' % className
fieldType = Boolean(default=defaultValue, page='userInterface',
group=groupName)
self.addField(fieldName, fieldType, classDescr)
self.addField(fieldName, fieldType)
# Adds the boolean field for showing or not the field "enter comments".
fieldName = 'showWorkflowCommentFieldFor%s' % className
fieldType = Boolean(default=defaultValue, page='userInterface',
group=groupName)
self.addField(fieldName, fieldType, classDescr)
self.addField(fieldName, fieldType)
# Adds the boolean field for showing all states in current state or not.
# If this boolean is True but the current phase counts only one state,
# we will not show the state at all: the fact of knowing in what phase
@ -512,7 +514,7 @@ class ToolClassDescriptor(ClassDescriptor):
fieldName = 'showAllStatesInPhaseFor%s' % className
fieldType = Boolean(default=defaultValue, page='userInterface',
group=groupName)
self.addField(fieldName, fieldType, classDescr)
self.addField(fieldName, fieldType)
class UserClassDescriptor(ClassDescriptor):
'''Represents an Archetypes-compliant class that corresponds to the User
@ -534,10 +536,61 @@ class UserClassDescriptor(ClassDescriptor):
self.orderedAttributes += attributes
self.klass = klass
self.customized = True
def isFolder(self, klass=None): return True
def isFolder(self, klass=None): return False
def generateSchema(self):
ClassDescriptor.generateSchema(self, configClass=True)
class TranslationClassDescriptor(ClassDescriptor):
'''Represents the set of translation ids for a gen-application.'''
def __init__(self, klass, generator):
ClassDescriptor.__init__(self,klass,klass._appy_attributes[:],generator)
self.modelClass = self.klass
self.predefined = True
self.customized = False
def getParents(self, allClasses=()): return ('Translation',)
def generateSchema(self):
ClassDescriptor.generateSchema(self, configClass=True)
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')
self.addField('%s_label' % messageId, field)
def addMessageField(self, messageId, page, i18nFiles):
'''Adds a message field corresponding to p_messageId to the Translation
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}
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
# content.
maxLine = 100 # We suppose a line is 100 characters long.
width = 0
height = 0
for fileName, poFile in i18nFiles.iteritems():
if not fileName.startswith('%s-' % appName): continue
msgContent = i18nFiles[fileName].messagesDict[messageId].msg
# Compute width
width = max(width, len(msgContent))
# Compute height (a "\n" counts for one line)
mHeight = int(len(msgContent)/maxLine) + msgContent.count('<br/>')
height = max(height, mHeight)
if height < 1:
# This is a one-line field.
params['width'] = width
else:
# This is a multi-line field, or a very-long-single-lined field
params['format'] = String.TEXT
params['height'] = height
self.addField(messageId, String(**params))
class WorkflowDescriptor(appy.gen.descriptors.WorkflowDescriptor):
'''Represents a workflow.'''
# How to map Appy permissions to Plone permissions ?

View file

@ -9,8 +9,9 @@ from appy.gen.po import PoMessage, PoFile, PoParser
from appy.gen.generator import Generator as AbstractGenerator
from appy.gen.utils import getClassName
from descriptors import ClassDescriptor, WorkflowDescriptor, \
ToolClassDescriptor, UserClassDescriptor
from model import ModelClass, User, Tool
ToolClassDescriptor, UserClassDescriptor, \
TranslationClassDescriptor
from model import ModelClass, User, Tool, Translation
# Common methods that need to be defined on every Archetype class --------------
COMMON_METHODS = '''
@ -32,16 +33,14 @@ class Generator(AbstractGenerator):
# Set our own Descriptor classes
self.descriptorClasses['class'] = ClassDescriptor
self.descriptorClasses['workflow'] = WorkflowDescriptor
# Create our own Tool and User instances
# Create our own Tool, User and Translation instances
self.tool = ToolClassDescriptor(Tool, self)
self.user = UserClassDescriptor(User, self)
self.translation = TranslationClassDescriptor(Translation, self)
# i18n labels to generate
self.labels = [] # i18n labels
self.toolName = '%sTool' % self.applicationName
self.toolInstanceName = 'portal_%s' % self.applicationName.lower()
self.userName = '%sUser' % self.applicationName
self.portletName = '%s_portlet' % self.applicationName.lower()
self.queryName = '%s_query' % self.applicationName.lower()
self.skinsFolder = 'skins/%s' % self.applicationName
# The following dict, pre-filled in the abstract generator, contains a
# series of replacements that need to be applied to file templates to
@ -49,9 +48,7 @@ class Generator(AbstractGenerator):
commonMethods = COMMON_METHODS % \
(self.toolInstanceName, self.applicationName)
self.repls.update(
{'toolName': self.toolName, 'portletName': self.portletName,
'queryName': self.queryName, 'userName': self.userName,
'toolInstanceName': self.toolInstanceName,
{'toolInstanceName': self.toolInstanceName,
'commonMethods': commonMethods})
self.referers = {}
@ -150,10 +147,8 @@ class Generator(AbstractGenerator):
niceDefault=True))
# Create basic files (config.py, Install.py, etc)
self.generateTool()
self.generateConfig()
self.generateInit()
self.generateWorkflows()
self.generateWrappers()
self.generateTests()
if self.config.frontPage:
self.generateFrontPage()
@ -198,11 +193,22 @@ class Generator(AbstractGenerator):
fullName = os.path.join(self.outputFolder, 'i18n/%s' % potFileName)
potFile = PoFile(fullName)
self.i18nFiles[potFileName] = potFile
# We update the POT file with our list of automatically managed labels.
removedLabels = potFile.update(self.labels, self.options.i18nClean,
not self.options.i18nSort)
if removedLabels:
print 'Warning: %d messages were removed from translation ' \
'files: %s' % (len(removedLabels), str(removedLabels))
# Before generating the POT file, we still need to add one label for
# every page for the Translation class. We've not done it yet because
# the number of pages depends on the total number of labels in the POT
# file.
pageLabels = []
nbOfPages = int(len(potFile.messages)/self.config.translationsPerPage)+1
for i in range(nbOfPages):
msgId = '%s_page_%d' % (self.translation.name, i+2)
pageLabels.append(msg(msgId, '', 'Page %d' % (i+2)))
potFile.update(pageLabels, keepExistingOrder=False)
potFile.generate()
# Generate i18n po files
for language in self.config.languages:
@ -219,10 +225,27 @@ class Generator(AbstractGenerator):
poFile.update(potFile.messages, self.options.i18nClean,
not self.options.i18nSort)
poFile.generate()
# Generate corresponding fields on the Translation class
page = 'main'
i = 0
for message in potFile.messages:
i += 1
# A computed field is used for displaying the text to translate.
self.translation.addLabelField(message.id, page)
# A String field will hold the translation in itself.
self.translation.addMessageField(message.id, page, self.i18nFiles)
if (i % self.config.translationsPerPage) == 0:
# A new page must be defined.
if page == 'main':
page = '2'
else:
page = str(int(page)+1)
# Generate i18n po files for other potential files
for poFile in self.i18nFiles.itervalues():
if not poFile.generated:
poFile.generate()
self.generateWrappers()
self.generateConfig()
def getAllUsedRoles(self, plone=None, local=None, grantable=None):
'''Produces a list of all the roles used within all workflows and
@ -283,10 +306,14 @@ class Generator(AbstractGenerator):
def generateConfig(self):
repls = self.repls.copy()
# Get some lists of classes
classes = self.getClasses()
classesWithCustom = self.getClasses(include='custom')
classesButTool = self.getClasses(include='allButTool')
classesAll = self.getClasses(include='all')
# Compute imports
imports = ['import %s' % self.applicationName]
classDescrs = self.getClasses(include='custom')
for classDescr in (classDescrs + self.workflows):
for classDescr in (classesWithCustom + self.workflows):
theImport = 'import %s' % classDescr.klass.__module__
if theImport not in imports:
imports.append(theImport)
@ -296,31 +323,24 @@ class Generator(AbstractGenerator):
['"%s"' % r for r in self.config.defaultCreators])
# Compute list of add permissions
addPermissions = ''
for classDescr in self.getClasses(include='allButTool'):
for classDescr in classesButTool:
addPermissions += ' "%s":"%s: Add %s",\n' % (classDescr.name,
self.applicationName, classDescr.name)
repls['addPermissions'] = addPermissions
# Compute root classes
rootClasses = ''
for classDescr in self.getClasses(include='allButTool'):
if classDescr.isRoot():
rootClasses += "'%s'," % classDescr.name
repls['rootClasses'] = rootClasses
repls['rootClasses'] = ','.join(["'%s'" % c.name \
for c in classesButTool if c.isRoot()])
# Compute list of class definitions
appClasses = []
for classDescr in self.classes:
k = classDescr.klass
appClasses.append('%s.%s' % (k.__module__, k.__name__))
repls['appClasses'] = "[%s]" % ','.join(appClasses)
repls['appClasses'] = ','.join(['%s.%s' % (c.klass.__module__, \
c.klass.__name__) for c in classes])
# Compute lists of class names
allClassNames = '"%s",' % self.userName
appClassNames = ','.join(['"%s"' % c.name for c in self.classes])
allClassNames += appClassNames
repls['allClassNames'] = allClassNames
repls['appClassNames'] = appClassNames
repls['appClassNames'] = ','.join(['"%s"' % c.name \
for c in classes])
repls['allClassNames'] = ','.join(['"%s"' % c.name \
for c in classesButTool])
# Compute classes whose instances must not be catalogued.
catalogMap = ''
blackClasses = [self.toolName]
blackClasses = [self.tool.name]
for blackClass in blackClasses:
catalogMap += "catalogMap['%s'] = {}\n" % blackClass
catalogMap += "catalogMap['%s']['black'] = " \
@ -328,7 +348,7 @@ class Generator(AbstractGenerator):
repls['catalogMap'] = catalogMap
# Compute workflows
workflows = ''
for classDescr in self.getClasses(include='all'):
for classDescr in classesAll:
if hasattr(classDescr.klass, 'workflow'):
wfName = WorkflowDescriptor.getWorkflowName(
classDescr.klass.workflow)
@ -357,7 +377,7 @@ class Generator(AbstractGenerator):
# inherited included) for every Appy class.
attributes = []
attributesDict = []
for classDescr in self.getClasses(include='all'):
for classDescr in classesAll:
titleFound = False
attrs = []
attrNames = []
@ -402,8 +422,8 @@ class Generator(AbstractGenerator):
repls['languages'] = ','.join('"%s"' % l for l in self.config.languages)
repls['languageSelector'] = self.config.languageSelector
repls['minimalistPlone'] = self.config.minimalistPlone
repls['appFrontPage'] = bool(self.config.frontPage)
repls['sourceLanguage'] = self.config.sourceLanguage
self.copyFile('config.py', repls)
def generateInit(self):
@ -470,21 +490,24 @@ class Generator(AbstractGenerator):
def getClasses(self, include=None):
'''Returns the descriptors for all the classes in the generated
gen-application. If p_include is "all", it includes the descriptors
for the config-related classes (tool, user, etc); if p_include is
"allButTool", it includes the same descriptors, the tool excepted;
if p_include is "custom", it includes descriptors for the
config-related classes for which the user has created a sub-class.'''
gen-application. If p_include is:
* "all" it includes the descriptors for the config-related
classes (tool, user, translation)
* "allButTool" it includes the same descriptors, the tool excepted
* "custom" it includes descriptors for the config-related classes
for which the user has created a sub-class.'''
if not include: return self.classes
else:
res = self.classes[:]
configClasses = [self.tool, self.user]
configClasses = [self.tool, self.user, self.translation]
if include == 'all':
res += configClasses
elif include == 'allButTool':
res += configClasses[1:]
elif include == 'custom':
res += [c for c in configClasses if c.customized]
elif include == 'predefined':
res = configClasses
return res
def getClassesInOrder(self, allClasses):
@ -569,8 +592,9 @@ class Generator(AbstractGenerator):
repls = self.repls.copy()
repls['imports'] = '\n'.join(imports)
repls['wrappers'] = '\n'.join(wrappers)
repls['toolBody'] = Tool._appy_getBody()
repls['userBody'] = User._appy_getBody()
for klass in self.getClasses(include='predefined'):
modelClass = klass.modelClass
repls['%s' % modelClass.__name__] = modelClass._appy_getBody()
self.copyFile('appyWrappers.py', repls, destFolder='Extensions')
def generateTests(self):
@ -608,24 +632,31 @@ class Generator(AbstractGenerator):
Msg = PoMessage
# Create Tool-related i18n-related messages
self.labels += [
Msg(self.toolName, '', Msg.CONFIG % self.applicationName),
Msg('%s_edit_descr' % self.toolName, '', ' ')]
Msg(self.tool.name, '', Msg.CONFIG % self.applicationName),
Msg('%s_edit_descr' % self.tool.name, '', ' ')]
# Tune the Ref field between Tool and User
Tool.users.klass = User
if self.user.customized:
Tool.users.klass = self.user.klass
# Generate the User class
self.user.generateSchema()
self.labels += [ Msg(self.userName, '', Msg.USER),
Msg('%s_edit_descr' % self.userName, '', ' '),
Msg('%s_plural' % self.userName, '',self.userName+'s')]
# Generate the Tool-related classes (User, Translation)
for klass in (self.user, self.translation):
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['fields'] = self.user.schema
repls['methods'] = self.user.methods
repls['wrapperClass'] = '%s_Wrapper' % self.user.name
self.copyFile('UserTemplate.py', repls,destName='%s.py' % self.userName)
repls.update({'fields': klass.schema, 'methods': klass.methods,
'genClassName': klass.name, 'imports': '','baseMixin':'BaseMixin',
'baseSchema': 'BaseSchema', 'global_allow': 1,
'parents': 'BaseMixin, BaseContent', 'static': '',
'classDoc': 'User class for %s' % self.applicationName,
'implements': "(getattr(BaseContent,'__implements__',()),)",
'register': "registerType(%s, '%s')" % (klass.name,
self.applicationName)})
self.copyFile('Class.py', repls, destName='%s.py' % klass.name)
# Before generating the Tool class, finalize it with query result
# columns, with fields to propagate, workflow-related fields.
@ -634,7 +665,7 @@ class Generator(AbstractGenerator):
for childDescr in classDescr.getChildren():
childFieldName = fieldName % childDescr.name
fieldType.group = childDescr.klass.__name__
self.tool.addField(childFieldName, fieldType, childDescr)
self.tool.addField(childFieldName, fieldType)
if classDescr.isRoot():
# We must be able to configure query results from the tool.
self.tool.addQueryResultColumns(classDescr)
@ -648,11 +679,22 @@ class Generator(AbstractGenerator):
# Generate the Tool class
repls = self.repls.copy()
repls['metaTypes'] = [c.name for c in self.classes]
repls['fields'] = self.tool.schema
repls['methods'] = self.tool.methods
repls['wrapperClass'] = '%s_Wrapper' % self.tool.name
self.copyFile('ToolTemplate.py', repls, destName='%s.py'% self.toolName)
repls.update({'fields': self.tool.schema, 'methods': self.tool.methods,
'genClassName': self.tool.name, 'imports':'', 'baseMixin':'ToolMixin',
'baseSchema': 'OrderedBaseFolderSchema', 'global_allow': 0,
'parents': 'ToolMixin, UniqueObject, OrderedBaseFolder',
'classDoc': 'Tool class for %s' % self.applicationName,
'implements': "(getattr(UniqueObject,'__implements__',()),) + " \
"(getattr(OrderedBaseFolder,'__implements__',()),)",
'register': "registerType(%s, '%s')" % (self.tool.name,
self.applicationName),
'static': "left_slots = ['here/portlet_prefs/macros/portlet']\n " \
"right_slots = []\n " \
"def __init__(self, id=None):\n " \
" OrderedBaseFolder.__init__(self, '%s')\n " \
" self.setTitle('%s')\n" % (self.toolInstanceName,
self.applicationName)})
self.copyFile('Class.py', repls, destName='%s.py' % self.tool.name)
def generateClass(self, classDescr):
'''Is called each time an Appy class is found in the application, for
@ -694,11 +736,11 @@ class Generator(AbstractGenerator):
classDescr.generateSchema()
repls.update({
'imports': '\n'.join(imports), 'parents': parents,
'className': classDescr.klass.__name__,
'genClassName': classDescr.name,
'className': classDescr.klass.__name__, 'global_allow': 1,
'genClassName': classDescr.name, 'baseMixin':'BaseMixin',
'classDoc': classDoc, 'applicationName': self.applicationName,
'fields': classDescr.schema, 'methods': classDescr.methods,
'implements': implements, 'baseSchema': baseSchema,
'implements': implements, 'baseSchema': baseSchema, 'static': '',
'register': register, 'toolInstanceName': self.toolInstanceName})
fileName = '%s.py' % classDescr.name
# Create i18n labels (class name, description and plural form)
@ -726,7 +768,7 @@ class Generator(AbstractGenerator):
if poMsg not in self.labels:
self.labels.append(poMsg)
# Generate the resulting Archetypes class and schema.
self.copyFile('ArchetypesTemplate.py', repls, destName=fileName)
self.copyFile('Class.py', repls, destName=fileName)
def generateWorkflow(self, wfDescr):
'''This method does not generate the workflow definition, which is done

View file

@ -7,8 +7,10 @@ from StringIO import StringIO
from sets import Set
import appy
from appy.gen import Type, Ref
from appy.gen.po import PoParser
from appy.gen.utils import produceNiceMessage
from appy.gen.plone25.utils import updateRolesForPermission
from appy.shared.data import languages
class ZCTextIndexInfo:
'''Silly class used for storing information about a ZCTextIndex.'''
@ -313,6 +315,38 @@ class PloneInstaller:
'%sID' % self.toolName, 'site_icon.gif', # Icon in control_panel
self.productName, None)
def installTranslations(self):
'''Creates or updates the translation objects within the tool.'''
translations = [t.o.id for t in self.appyTool.translations]
# We browse the languages supported by this application and check
# whether we need to create the corresponding Translation objects.
for language in self.languages:
if language in translations: continue
# We will create, in the tool, the translation object for this
# language. Determine first its title.
langId, langEn, langNat = languages.get(language)
if langEn != langNat:
title = '%s (%s)' % (langEn, langNat)
else:
title = langEn
self.appyTool.create('translations', id=language, title=title)
self.appyTool.log('Translation object created for "%s".' % language)
# Now, we synchronise every Translation object with the corresponding
# "po" file on disk.
appFolder = self.config.diskFolder
appName = self.config.PROJECTNAME
dn = os.path.dirname
jn = os.path.join
i18nFolder = jn(jn(jn(dn(dn(dn(appFolder))),'Products'),appName),'i18n')
for translation in self.appyTool.translations:
# Get the "po" file
poName = '%s-%s.po' % (appName, translation.id)
poFile = PoParser(jn(i18nFolder, poName)).parse()
for message in poFile.messages:
setattr(translation, message.id, message.getMessage())
self.appyTool.log('Translation "%s" updated from "%s".' % \
(translation.id, poName))
def installRolesAndGroups(self):
'''Registers roles used by workflows and classes defined in this
application if they are not registered yet. Creates the corresponding
@ -459,6 +493,7 @@ class PloneInstaller:
self.installRootFolder()
self.installTypes()
self.installTool()
self.installTranslations()
self.installRolesAndGroups()
self.installWorkflows()
self.installStyleSheet()

View file

@ -109,8 +109,8 @@ class ToolMixin(BaseMixin):
fileName = appyTool.normalize(fileName)
response = obj.REQUEST.RESPONSE
response.setHeader('Content-Type', mimeTypes[format])
response.setHeader('Content-Disposition', 'inline;filename="%s.%s"'\
% (fileName, format))
response.setHeader('Content-Disposition', 'inline;filename="%s.%s"' % \
(fileName, format))
f.close()
# Execute the related action if relevant
if doAction and podInfo['action']:
@ -710,7 +710,7 @@ class ToolMixin(BaseMixin):
fieldName, pageName = d2.split(':')
sourceObj = self.uid_catalog(UID=d1)[0].getObject()
label = '%s_%s' % (sourceObj.meta_type, fieldName)
res['backText'] = u'%s : %s' % (sourceObj.Title().decode('utf-8'),
res['backText'] = '%s : %s' % (sourceObj.Title(),
self.translate(label))
newNav = '%s.%s.%s.%%d.%s' % (t, d1, d2, totalNumber)
# Among, first, previous, next and last, which one do I need?

View file

@ -384,8 +384,11 @@ class BaseMixin:
refType = refObject.o.getAppyType(fieldName)
value = getattr(refObject, fieldName)
value = refType.getFormattedValue(refObject.o, value)
if (refType.type == 'String') and (refType.format == 2):
if refType.type == 'String':
if refType.format == 2:
value = self.xhtmlToText.sub(' ', value)
elif type(value) in sequenceTypes:
value = ', '.join(value)
prefix = ''
if res:
prefix = ' | '
@ -816,12 +819,13 @@ class BaseMixin:
self.plone_utils.addPortalMessage(msg)
return self.goto(self.getUrl(rq['HTTP_REFERER']))
elif resultType == 'file':
# msg does not contain a message, but a complete file to show as is.
# (or, if your prefer, the message must be shown directly to the
# user, not encapsulated in a Plone page).
res = self.getProductConfig().File(msg.name, msg.name, msg,
content_type=mimetypes.guess_type(msg.name)[0])
return res.index_html(rq, rq.RESPONSE)
# msg does not contain a message, but a file instance.
response = self.REQUEST.RESPONSE
response.setHeader('Content-Type',mimetypes.guess_type(msg.name)[0])
response.setHeader('Content-Disposition', 'inline;filename="%s"' %\
msg.name)
response.write(msg.read())
msg.close()
elif resultType == 'redirect':
# msg does not contain a message, but the URL where to redirect
# the user.
@ -1045,13 +1049,27 @@ class BaseMixin:
'''Translates a given p_label into p_domain with p_mapping.'''
cfg = self.getProductConfig()
if not domain: domain = cfg.PROJECTNAME
if domain != cfg.PROJECTNAME:
# We need to translate something that is in a standard Zope catalog
try:
res = self.Control_Panel.TranslationService.utranslate(
domain, label, mapping, self, default=default,
target_language=language)
except AttributeError:
# When run in test mode, Zope does not create the TranslationService
# When run in test mode, Zope does not create the
# TranslationService
res = label
else:
# We will get the translation from a Translation object.
# In what language must we get the translation?
if not language: language = self.REQUEST['LANGUAGE']
tool = self.getTool()
translation = getattr(self.getTool(), language).appy()
res = getattr(translation, label, '')
# Perform replacements if needed
for name, repl in mapping.iteritems():
res = res.replace('${%s}' % name, repl)
# At present, there is no fallback machanism.
return res
def getPageLayout(self, layoutType):

View file

@ -21,23 +21,26 @@ class ModelClass:
# must not be given in the constructor (they are computed attributes).
_appy_notinit = ('id', 'type', 'pythonType', 'slaves', 'isSelect',
'hasLabel', 'hasDescr', 'hasHelp', 'master_css',
'layouts', 'required', 'filterable', 'validable', 'backd',
'isBack', 'sync', 'pageName')
'required', 'filterable', 'validable', 'backd', 'isBack',
'sync', 'pageName')
@classmethod
def _appy_getTypeBody(klass, appyType):
'''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 isinstance(attrValue, basestring):
if attrName in ModelClass._appy_notinit: continue
if attrName == 'layouts':
if klass.__name__ == 'Tool': continue
# 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 attrValue.isBack:
if not attrValue.isBack: continue
attrValue = klass._appy_getTypeBody(attrValue)
else:
continue
elif type(attrValue) == type(ModelClass):
moduleName = attrValue.__module__
if moduleName.startswith('appy.gen'):
@ -49,8 +52,8 @@ class ModelClass:
elif isinstance(attrValue, Group):
attrValue = 'Group("%s")' % attrValue.name
elif isinstance(attrValue, Page):
attrValue = 'Page("%s")' % attrValue.name
elif type(attrValue) == types.FunctionType:
attrValue = 'pages["%s"]' % attrValue.name
elif callable(attrValue):
attrValue = '%sWrapper.%s'% (klass.__name__, attrValue.__name__)
typeArgs += '%s=%s,' % (attrName, attrValue)
return '%s(%s)' % (appyType.__class__.__name__, typeArgs)
@ -59,7 +62,25 @@ class ModelClass:
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 = ''
res = 'class %s(%sWrapper):\n' % (klass.__name__, klass.__name__)
if klass.__name__ == '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
if appyType.page.name not in pages:
pages[appyType.page.name] = appyType.page
res += ' pages = {'
for page in pages.itervalues():
# Determine page show
pageShow = page.show
if isinstance(pageShow, basestring): pageShow='"%s"' % pageShow
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))
@ -86,25 +107,38 @@ class User(ModelClass):
gm['multiplicity'] = (0, None)
roles = String(validator=Selection('getGrantableRoles'), indexed=True, **gm)
# The Tool class ---------------------------------------------------------------
# The Translation class --------------------------------------------------------
class Translation(ModelClass):
_appy_attributes = ['po', 'title']
# All methods defined below are fake. Real versions are in the wrapper.
def getPoFile(self): pass
po = Action(action=getPoFile, page=Page('actions', show='view'),
result='file')
title = String(show=False, indexed=True)
def computeLabel(self): pass
def showField(self, name): pass
# The Tool class ---------------------------------------------------------------
# Here are the prefixes of the fields generated on the Tool.
toolFieldPrefixes = ('defaultValue', 'podTemplate', 'formats', 'resultColumns',
'enableAdvancedSearch', 'numberOfSearchColumns',
'searchFields', 'optionalFields', 'showWorkflow',
'showWorkflowCommentField', 'showAllStatesInPhase')
defaultToolFields = ('users', 'enableNotifications', 'unoEnabledPython',
'openOfficePort', 'numberOfResultsPerPage',
'listBoxesMaximumWidth')
defaultToolFields = ('users', 'translations', 'enableNotifications',
'unoEnabledPython', 'openOfficePort',
'numberOfResultsPerPage', 'listBoxesMaximumWidth')
class Tool(ModelClass):
# The following dict allows us to remember the original classes related to
# the attributes we will add due to params in user attributes.
_appy_classes = {} # ~{s_attributeName: s_className}~
# In a ModelClass we need to declare attributes in the following list.
_appy_attributes = list(defaultToolFields)
# Tool attributes
def validPythonWithUno(self, value): pass # Real method in the wrapper
unoEnabledPython = String(group="connectionToOpenOffice",
validator=validPythonWithUno)
openOfficePort = Integer(default=2002, group="connectionToOpenOffice")
numberOfResultsPerPage = Integer(default=30)
listBoxesMaximumWidth = Integer(default=100)
# First arg of Ref field below is None because we don't know yet if it will
# link to the predefined User class or a custom class defined in the
# application.
@ -112,13 +146,10 @@ class Tool(ModelClass):
back=Ref(attribute='toTool'), page='users', queryable=True,
queryFields=('login',), showHeaders=True,
shownInfo=('login', 'title', 'roles'))
translations = Ref(Translation, multiplicity=(0,None), add=False,link=False,
back=Ref(attribute='trToTool', show=False), show='view',
page=Page('translations', show='view'))
enableNotifications = Boolean(default=True, page='notifications')
def validPythonWithUno(self, value): pass # Real method in the wrapper
unoEnabledPython = String(group="connectionToOpenOffice",
validator=validPythonWithUno)
openOfficePort = Integer(default=2002, group="connectionToOpenOffice")
numberOfResultsPerPage = Integer(default=30)
listBoxesMaximumWidth = Integer(default=100)
@classmethod
def _appy_clean(klass):
@ -130,5 +161,4 @@ class Tool(ModelClass):
for k in toClean:
exec 'del klass.%s' % k
klass._appy_attributes = list(defaultToolFields)
klass._appy_classes = {}
# ------------------------------------------------------------------------------

Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 B

View file

@ -3,8 +3,10 @@ from AccessControl import ClassSecurityInfo
from DateTime import DateTime
from Products.Archetypes.atapi import *
import Products.<!applicationName!>.config
from Extensions.appyWrappers import <!genClassName!>_Wrapper
from Products.CMFCore.utils import UniqueObject
from appy.gen.plone25.mixins import BaseMixin
from appy.gen.plone25.mixins.ToolMixin import ToolMixin
from Extensions.appyWrappers import <!genClassName!>_Wrapper
<!imports!>
schema = Schema((<!fields!>
@ -18,19 +20,20 @@ class <!genClassName!>(<!parents!>):
archetype_name = '<!genClassName!>'
meta_type = '<!genClassName!>'
portal_type = '<!genClassName!>'
allowed_content_types = []
allowed_content_types = ()
filter_content_types = 0
global_allow = 1
global_allow = <!global_allow!>
immediate_view = 'skyn/view'
default_view = 'skyn/view'
suppl_views = ()
typeDescription = '<!genClassName!>'
typeDescMsgId = '<!genClassName!>_edit_descr'
i18nDomain = '<!applicationName!>'
schema = fullSchema
wrapperClass = <!genClassName!>_Wrapper
for elem in dir(BaseMixin):
schema = fullSchema
for elem in dir(<!baseMixin!>):
if not elem.startswith('__'): security.declarePublic(elem)
<!static!>
<!commonMethods!>
<!methods!>
<!register!>

View file

@ -266,6 +266,13 @@ div.appyPopup {
border: 1px solid gray;
}
.translationLabel {
background-color: #EAEAEA;
border-bottom: 1px dashed grey;
margin-top: 1em;
margin-bottom: 0.5em;
}
/* Uncomment this if you want to hide breadcrumbs */
/*
#portal-breadcrumbs {

View file

@ -1,49 +0,0 @@
<!codeHeader!>
from AccessControl import ClassSecurityInfo
from DateTime import DateTime
from Products.Archetypes.atapi import *
from Products.CMFCore.utils import UniqueObject
import Products.<!applicationName!>.config
from appy.gen.plone25.mixins.ToolMixin import ToolMixin
from Extensions.appyWrappers import AbstractWrapper, <!wrapperClass!>
schema = Schema((<!fields!>
),)
fullSchema = OrderedBaseFolderSchema.copy() + schema.copy()
class <!toolName!>(ToolMixin, UniqueObject, OrderedBaseFolder):
'''Tool for <!applicationName!>.'''
security = ClassSecurityInfo()
__implements__ = (getattr(UniqueObject,'__implements__',()),) + (getattr(OrderedBaseFolder,'__implements__',()),)
archetype_name = '<!toolName!>'
meta_type = '<!toolName!>'
portal_type = '<!toolName!>'
allowed_content_types = ()
filter_content_types = 0
global_allow = 0
#content_icon = '<!toolName!>.gif'
immediate_view = 'skyn/view'
default_view = 'skyn/view'
suppl_views = ()
typeDescription = "<!toolName!>"
typeDescMsgId = '<!toolName!>_edit_descr'
i18nDomain = '<!applicationName!>'
allMetaTypes = <!metaTypes!>
wrapperClass = <!wrapperClass!>
schema = fullSchema
schema["id"].widget.visible = False
schema["title"].widget.visible = False
# When browsing into the tool, the 'configure' portlet should be displayed.
left_slots = ['here/portlet_prefs/macros/portlet']
right_slots = []
for elem in dir(ToolMixin):
if not elem.startswith('__'): security.declarePublic(elem)
# Tool constructor has no id argument, the id is fixed.
def __init__(self, id=None):
OrderedBaseFolder.__init__(self, '<!toolInstanceName!>')
self.setTitle('<!applicationName!>')
<!commonMethods!>
<!methods!>
registerType(<!toolName!>, '<!applicationName!>')

View file

@ -1,34 +0,0 @@
<!codeHeader!>
from AccessControl import ClassSecurityInfo
from Products.Archetypes.atapi import *
import Products.<!applicationName!>.config
from appy.gen.plone25.mixins import BaseMixin
from Extensions.appyWrappers import <!wrapperClass!>
schema = Schema((<!fields!>
),)
fullSchema = BaseSchema.copy() + schema.copy()
class <!applicationName!>User(BaseMixin, BaseContent):
'''User mixin.'''
security = ClassSecurityInfo()
__implements__ = (getattr(BaseContent,'__implements__',()),)
archetype_name = '<!applicationName!>User'
meta_type = '<!applicationName!>User'
portal_type = '<!applicationName!>User'
allowed_content_types = []
filter_content_types = 0
global_allow = 1
immediate_view = 'skyn/view'
default_view = 'skyn/view'
suppl_views = ()
typeDescription = "<!applicationName!>User"
typeDescMsgId = '<!applicationName!>User_edit_descr'
i18nDomain = '<!applicationName!>'
schema = fullSchema
wrapperClass = <!wrapperClass!>
for elem in dir(BaseMixin):
if not elem.startswith('__'): security.declarePublic(elem)
<!commonMethods!>
<!methods!>
registerType(<!applicationName!>User, '<!applicationName!>')

View file

@ -3,16 +3,13 @@ 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 Globals import InitializeClass
from AccessControl import ClassSecurityInfo
<!imports!>
class User(UserWrapper):
'''This class represents a user.'''
<!userBody!>
class Tool(ToolWrapper):
'''This class represents the tool for this application.'''
folder=True
<!toolBody!>
<!User!>
<!Translation!>
<!Tool!>
<!wrappers!>
# ------------------------------------------------------------------------------

View file

@ -45,7 +45,7 @@ setDefaultRoles(DEFAULT_ADD_CONTENT_PERMISSION, tuple(defaultAddRoles))
# Applications classes, in various formats
rootClasses = [<!rootClasses!>]
appClasses = <!appClasses!>
appClasses = [<!appClasses!>]
appClassNames = [<!appClassNames!>]
allClassNames = [<!allClassNames!>]
# List of classes that must be hidden from the catalog
@ -66,7 +66,7 @@ workflowInstances = {}
# In the following dict, we store, for every Appy class, the ordered list of
# appy types (included inherited ones).
attributes = {<!attributes!>}
# In the followinf dict, we store, for every Appy class, a dict of appy types
# In the following dict, we store, for every Appy class, a dict of appy types
# keyed by their names.
attributesDict = {<!attributesDict!>}
@ -81,4 +81,5 @@ languages = [<!languages!>]
languageSelector = <!languageSelector!>
minimalistPlone = <!minimalistPlone!>
appFrontPage = <!appFrontPage!>
sourceLanguage = '<!sourceLanguage!>'
# ------------------------------------------------------------------------------

View file

@ -105,4 +105,8 @@ class ToolWrapper(AbstractWrapper):
res = '%sFor%s' % (attributeType, fullClassName)
if attrName: res += '_%s' % attrName
return res
def getAvailableLanguages(self):
'''Returns the list of available languages for this application.'''
return [(t.id, t.title) for t in self.translations]
# ------------------------------------------------------------------------------

View file

@ -0,0 +1,76 @@
# ------------------------------------------------------------------------------
import os.path
from appy.gen.plone25.wrappers import AbstractWrapper
from appy.gen.po import PoFile, PoMessage
from appy.shared.utils import getOsTempFolder
# ------------------------------------------------------------------------------
class TranslationWrapper(AbstractWrapper):
def computeLabel(self, field):
'''The label for a text to translate displays the text of the
corresponding message in the source translation.'''
tool = self.tool
sourceLanguage = self.o.getProductConfig().sourceLanguage
sourceTranslation = getattr(tool.o, sourceLanguage).appy()
# p_field is the Computed field. We need to get the name of the
# corresponding field holding the translation message.
fieldName = field.name[:-6]
# If we are showing the source translation, we do not repeat the message
# in the label.
if self.id == sourceLanguage:
sourceMsg = ''
else:
sourceMsg = getattr(sourceTranslation,fieldName)
# When editing the value, we don't want HTML code to be interpreted.
# This way, the translator sees the HTML tags and can reproduce them in
# the translation.
if self.request['URL'].endswith('/skyn/edit'):
sourceMsg = sourceMsg.replace('<','&lt;').replace('>','&gt;')
sourceMsg = sourceMsg.replace('\n', '<br/>')
return '<div class="translationLabel"><acronym title="%s">' \
'<img src="help.png"/></acronym>%s</div>' % \
(fieldName, sourceMsg)
def showField(self, field):
'''We show a field (or its label) only if the corresponding source
message is not empty.'''
tool = self.tool
if field.type == 'Computed': name = field.name[:-6]
else: name = field.name
# Get the source message
sourceLanguage = self.o.getProductConfig().sourceLanguage
sourceTranslation = getattr(tool.o, sourceLanguage).appy()
sourceMsg = getattr(sourceTranslation, name)
if field.isEmptyValue(sourceMsg): return False
return True
poReplacements = ( ('\r\n', '<br/>'), ('\n', '<br/>'), ('"', '\\"') )
def getPoFile(self):
'''Computes and returns the PO file corresponding to this
translation.'''
tool = self.tool
fileName = os.path.join(getOsTempFolder(),
'%s-%s.po' % (tool.o.getAppName(), self.id))
poFile = PoFile(fileName)
for field in self.fields:
if (field.name == 'title') or (field.type != 'String'): continue
# Adds the PO message corresponding to this field
msg = field.getValue(self.o) or ''
for old, new in self.poReplacements:
msg = msg.replace(old, new)
poFile.addMessage(PoMessage(field.name, msg, ''))
poFile.generate()
return True, file(fileName)
def validate(self, new, errors):
# Call a custom "validate" if any.
self._callCustom('validate', new, errors)
def onEdit(self, created):
# Call a custom "onEdit" if any.
self._callCustom('onEdit', created)
def onDelete(self):
# Call a custom "onDelete" if any.
self._callCustom('onDelete')
# ------------------------------------------------------------------------------

View file

@ -4,17 +4,6 @@ from appy.gen.plone25.wrappers import AbstractWrapper
# ------------------------------------------------------------------------------
class UserWrapper(AbstractWrapper):
def _callCustom(self, methodName, *args, **kwargs):
'''This wrapper implements some methods like "validate" and "onEdit".
If the user has defined its own wrapper, its methods will not be
called. So this method allows, from the methods here, to call the
user versions.'''
if len(self.__class__.__bases__) > 1:
# There is a custom user class
customUser = self.__class__.__bases__[-1]
if customUser.__dict__.has_key(methodName):
customUser.__dict__[methodName](self, *args, **kwargs)
def showLogin(self):
'''When must we show the login field?'''
if self.o.isTemporary(): return 'edit'

View file

@ -33,6 +33,17 @@ class AbstractWrapper:
if other: return cmp(self.o, other.o)
else: return 1
def _callCustom(self, methodName, *args, **kwargs):
'''This wrapper implements some methods like "validate" and "onEdit".
If the user has defined its own wrapper, its methods will not be
called. So this method allows, from the methods here, to call the
user versions.'''
if len(self.__class__.__bases__) > 1:
# There is a custom user class
customUser = self.__class__.__bases__[-1]
if customUser.__dict__.has_key(methodName):
customUser.__dict__[methodName](self, *args, **kwargs)
def get_tool(self): return self.o.getTool().appy()
tool = property(get_tool)

View file

@ -42,7 +42,6 @@ class PoMessage:
'comment every time a transition is ' \
'triggered'
MSG_showAllStatesInPhase = 'Show all states in phase'
USER = 'User'
POD_ASKACTION = 'Trigger related action'
REF_NO = 'No object.'
REF_ADD = 'Add a new one'
@ -194,6 +193,10 @@ class PoMessage:
newId = '%s_%s' % (newPrefix, self.id.split('_', 1)[1])
return PoMessage(newId, self.msg, self.default, comments=self.comments)
def getMessage(self):
'''Returns self.msg, but with some replacements.'''
return self.msg.replace('<br/>', '\n').replace('\\"', '"')
class PoHeader:
def __init__(self, name, value):
self.name = name
@ -236,8 +239,11 @@ class PoFile:
self.generated = False # Is set to True during the generation process
# when this file has been generated.
def addMessage(self, newMsg):
def addMessage(self, newMsg, needsCopy=True):
if needsCopy:
res = copy.copy(newMsg)
else:
res = newMsg
self.messages.append(res)
self.messagesDict[res.id] = res
return res
@ -261,7 +267,9 @@ class PoFile:
i = len(self.messages)-1
while i >= 0:
oldId = self.messages[i].id
if not oldId.startswith('custom_') and (oldId not in newIds):
if not oldId.startswith('custom_') and \
not oldId.startswith('%sTranslation_page_'%self.domain) and \
(oldId not in newIds):
del self.messages[i]
del self.messagesDict[oldId]
removedIds.append(oldId)
@ -343,7 +351,7 @@ class PoParser:
def parse(self):
'''Parses all i18n messages in the file, stores it in
self.res.messages and returns it also.'''
self.res.messages and returns self.res.'''
f = file(self.res.fileName)
# Currently parsed values
msgDefault = msgFuzzy = msgId = msgStr = None

View file

@ -197,6 +197,15 @@ class Languages:
'''Is p_code a valid 2-digits language/country code?'''
return code in self.languageCodes
def get(self, code):
'''Returns information about the language whose code is p_code.'''
try:
iCode = self.languageCodes.index(code)
return self.languageCodes[iCode], self.languageNames[iCode], \
self.nativeNames[iCode]
except ValueError:
return None, None, None
def __repr__(self):
i = -1
res = ''