diff --git a/doc/version.txt b/doc/version.txt index ee6cdce..b616048 100644 --- a/doc/version.txt +++ b/doc/version.txt @@ -1 +1 @@ -0.6.1 +0.6.2 diff --git a/gen/__init__.py b/gen/__init__.py index a1f9450..7b24fa8 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -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: ": Write|Read xxx". If, for example, I want + # of the form: ": 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 diff --git a/gen/descriptors.py b/gen/descriptors.py index 4c69dc1..6a67f10 100644 --- a/gen/descriptors.py +++ b/gen/descriptors.py @@ -70,12 +70,22 @@ class ClassDescriptor(Descriptor): return res def getPhases(self): - '''Gets the phases defined on fields of this class.''' - res = [] - for fieldName, appyType, klass in self.getOrderedAppyAttributes(): - if appyType.page.phase not in res: - res.append(appyType.page.phase) - return 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 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.''' diff --git a/gen/plone25/descriptors.py b/gen/plone25/descriptors.py index 1c39291..e49497a 100644 --- a/gen/plone25/descriptors.py +++ b/gen/plone25/descriptors.py @@ -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('
') + 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 ? diff --git a/gen/plone25/generator.py b/gen/plone25/generator.py index 898c11b..6b30792 100644 --- a/gen/plone25/generator.py +++ b/gen/plone25/generator.py @@ -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,10 +48,8 @@ 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, - 'commonMethods': commonMethods}) + {'toolInstanceName': self.toolInstanceName, + 'commonMethods': commonMethods}) self.referers = {} versionRex = re.compile('(.*?\s+build)\s+(\d+)') @@ -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')] - 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) + # 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.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 diff --git a/gen/plone25/installer.py b/gen/plone25/installer.py index 2096349..556cc8e 100644 --- a/gen/plone25/installer.py +++ b/gen/plone25/installer.py @@ -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() diff --git a/gen/plone25/mixins/ToolMixin.py b/gen/plone25/mixins/ToolMixin.py index ee0d8ff..5577c8b 100644 --- a/gen/plone25/mixins/ToolMixin.py +++ b/gen/plone25/mixins/ToolMixin.py @@ -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,8 +710,8 @@ 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'), - self.translate(label)) + 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? previousNeeded = False # Previous ? diff --git a/gen/plone25/mixins/__init__.py b/gen/plone25/mixins/__init__.py index d33ccaa..33293f6 100644 --- a/gen/plone25/mixins/__init__.py +++ b/gen/plone25/mixins/__init__.py @@ -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): - value = self.xhtmlToText.sub(' ', value) + 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 - 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 - res = label + 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 + 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): diff --git a/gen/plone25/model.py b/gen/plone25/model.py index 6568387..9630d3a 100644 --- a/gen/plone25/model.py +++ b/gen/plone25/model.py @@ -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: - attrValue = klass._appy_getTypeBody(attrValue) - else: - continue + if not attrValue.isBack: continue + attrValue = klass._appy_getTypeBody(attrValue) 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 = {} # ------------------------------------------------------------------------------ diff --git a/gen/plone25/skin/help.jpg b/gen/plone25/skin/help.jpg deleted file mode 100644 index 199ba41..0000000 Binary files a/gen/plone25/skin/help.jpg and /dev/null differ diff --git a/gen/plone25/templates/ArchetypesTemplate.py b/gen/plone25/templates/Class.py similarity index 82% rename from gen/plone25/templates/ArchetypesTemplate.py rename to gen/plone25/templates/Class.py index a3a468b..9775812 100644 --- a/gen/plone25/templates/ArchetypesTemplate.py +++ b/gen/plone25/templates/Class.py @@ -3,8 +3,10 @@ from AccessControl import ClassSecurityInfo from DateTime import DateTime from Products.Archetypes.atapi import * import Products..config -from Extensions.appyWrappers import _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 _Wrapper schema = Schema(( @@ -18,19 +20,20 @@ class (): archetype_name = '' meta_type = '' portal_type = '' - allowed_content_types = [] + allowed_content_types = () filter_content_types = 0 - global_allow = 1 + global_allow = immediate_view = 'skyn/view' default_view = 'skyn/view' suppl_views = () typeDescription = '' typeDescMsgId = '_edit_descr' i18nDomain = '' - schema = fullSchema wrapperClass = _Wrapper - for elem in dir(BaseMixin): + schema = fullSchema + for elem in dir(): if not elem.startswith('__'): security.declarePublic(elem) + diff --git a/gen/plone25/templates/Styles.css.dtml b/gen/plone25/templates/Styles.css.dtml index 5fdfedd..200e6c6 100644 --- a/gen/plone25/templates/Styles.css.dtml +++ b/gen/plone25/templates/Styles.css.dtml @@ -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 { diff --git a/gen/plone25/templates/ToolTemplate.py b/gen/plone25/templates/ToolTemplate.py deleted file mode 100644 index 9eafb8c..0000000 --- a/gen/plone25/templates/ToolTemplate.py +++ /dev/null @@ -1,49 +0,0 @@ - -from AccessControl import ClassSecurityInfo -from DateTime import DateTime -from Products.Archetypes.atapi import * -from Products.CMFCore.utils import UniqueObject -import Products..config -from appy.gen.plone25.mixins.ToolMixin import ToolMixin -from Extensions.appyWrappers import AbstractWrapper, - -schema = Schema(( -),) -fullSchema = OrderedBaseFolderSchema.copy() + schema.copy() - -class (ToolMixin, UniqueObject, OrderedBaseFolder): - '''Tool for .''' - security = ClassSecurityInfo() - __implements__ = (getattr(UniqueObject,'__implements__',()),) + (getattr(OrderedBaseFolder,'__implements__',()),) - - archetype_name = '' - meta_type = '' - portal_type = '' - allowed_content_types = () - filter_content_types = 0 - global_allow = 0 - #content_icon = '.gif' - immediate_view = 'skyn/view' - default_view = 'skyn/view' - suppl_views = () - typeDescription = "" - typeDescMsgId = '_edit_descr' - i18nDomain = '' - allMetaTypes = - 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, '') - self.setTitle('') - - -registerType(, '') diff --git a/gen/plone25/templates/UserTemplate.py b/gen/plone25/templates/UserTemplate.py deleted file mode 100644 index 4d98b5b..0000000 --- a/gen/plone25/templates/UserTemplate.py +++ /dev/null @@ -1,34 +0,0 @@ - -from AccessControl import ClassSecurityInfo -from Products.Archetypes.atapi import * -import Products..config -from appy.gen.plone25.mixins import BaseMixin -from Extensions.appyWrappers import - -schema = Schema(( -),) -fullSchema = BaseSchema.copy() + schema.copy() - -class User(BaseMixin, BaseContent): - '''User mixin.''' - security = ClassSecurityInfo() - __implements__ = (getattr(BaseContent,'__implements__',()),) - archetype_name = 'User' - meta_type = 'User' - portal_type = 'User' - allowed_content_types = [] - filter_content_types = 0 - global_allow = 1 - immediate_view = 'skyn/view' - default_view = 'skyn/view' - suppl_views = () - typeDescription = "User" - typeDescMsgId = 'User_edit_descr' - i18nDomain = '' - schema = fullSchema - wrapperClass = - for elem in dir(BaseMixin): - if not elem.startswith('__'): security.declarePublic(elem) - - -registerType(User, '') diff --git a/gen/plone25/templates/appyWrappers.py b/gen/plone25/templates/appyWrappers.py index 80f191c..9614566 100644 --- a/gen/plone25/templates/appyWrappers.py +++ b/gen/plone25/templates/appyWrappers.py @@ -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 -class User(UserWrapper): - '''This class represents a user.''' - -class Tool(ToolWrapper): - '''This class represents the tool for this application.''' - folder=True - + + + # ------------------------------------------------------------------------------ diff --git a/gen/plone25/templates/config.py b/gen/plone25/templates/config.py index 9fba27f..607d579 100644 --- a/gen/plone25/templates/config.py +++ b/gen/plone25/templates/config.py @@ -45,7 +45,7 @@ setDefaultRoles(DEFAULT_ADD_CONTENT_PERMISSION, tuple(defaultAddRoles)) # Applications classes, in various formats rootClasses = [] -appClasses = +appClasses = [] appClassNames = [] 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 = {} -# 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 = {} @@ -81,4 +81,5 @@ languages = [] languageSelector = minimalistPlone = appFrontPage = +sourceLanguage = '' # ------------------------------------------------------------------------------ diff --git a/gen/plone25/wrappers/ToolWrapper.py b/gen/plone25/wrappers/ToolWrapper.py index 37a11f5..3df8481 100644 --- a/gen/plone25/wrappers/ToolWrapper.py +++ b/gen/plone25/wrappers/ToolWrapper.py @@ -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] # ------------------------------------------------------------------------------ diff --git a/gen/plone25/wrappers/TranslationWrapper.py b/gen/plone25/wrappers/TranslationWrapper.py new file mode 100644 index 0000000..9ef7b98 --- /dev/null +++ b/gen/plone25/wrappers/TranslationWrapper.py @@ -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('<','<').replace('>','>') + sourceMsg = sourceMsg.replace('\n', '
') + return '
' \ + '%s
' % \ + (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', '
'), ('\n', '
'), ('"', '\\"') ) + 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') +# ------------------------------------------------------------------------------ diff --git a/gen/plone25/wrappers/UserWrapper.py b/gen/plone25/wrappers/UserWrapper.py index bdb6fb5..ce01475 100644 --- a/gen/plone25/wrappers/UserWrapper.py +++ b/gen/plone25/wrappers/UserWrapper.py @@ -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' diff --git a/gen/plone25/wrappers/__init__.py b/gen/plone25/wrappers/__init__.py index 22f980b..b80bbd6 100644 --- a/gen/plone25/wrappers/__init__.py +++ b/gen/plone25/wrappers/__init__.py @@ -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) diff --git a/gen/po.py b/gen/po.py index e65b149..4cde1b8 100644 --- a/gen/po.py +++ b/gen/po.py @@ -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('
', '\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): - res = copy.copy(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 diff --git a/shared/data/__init__.py b/shared/data/__init__.py index adaf629..bcfe24f 100644 --- a/shared/data/__init__.py +++ b/shared/data/__init__.py @@ -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 = ''