diff --git a/bin/generate.py b/bin/generate.py index f425f32..72375e4 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -9,7 +9,7 @@ import appy.version # ------------------------------------------------------------------------------ ERROR_CODE = 1 -VALID_PRODUCT_TYPES = ('plone25', 'odt') +VALID_PRODUCT_TYPES = ('zope', 'odt') APP_NOT_FOUND = 'Application not found at %s.' WRONG_NG_OF_ARGS = 'Wrong number of arguments.' WRONG_OUTPUT_FOLDER = 'Output folder not found. Please create it first.' @@ -39,39 +39,29 @@ S_OPTION = 'Sorts all i18n labels. If you use this option, among the ' \ class GeneratorScript: '''usage: %prog [options] app productType outputFolder - "app" is the path to your Appy application, which may be a - Python module (= a file than ends with .py) or a Python - package (= a folder containing a file named __init__.py). - Your app may reside anywhere (but it needs to be - accessible by the underlying application server, ie Zope), - excepted within the generated product. Typically, if you - generate a Plone product, it may reside within - /lib/python, but not within the - generated product (typically stored in - /Products). - "productType" is the kind of product you want to generate - (currently, only "plone25" and 'odt' are supported; - in the near future, the "plone25" target will also produce - Plone 3-compliant code that will still work with - Plone 2.5). - "outputFolder" is the folder where the product will be generated. - For example, if you specify /my/output/folder for your - application /home/gde/MyApp.py, this script will create - a folder /my/output/folder/MyApp and put in it the - generated product. + "app" is the path to your Appy application, which must be a + Python package (= a folder containing a file named + __init__.py). Your app may reside anywhere, but needs to + be accessible by Zope. Typically, it may be or symlinked + in /lib/python, but not within the + generated product, stored or symlinked in + /Products. - Example: generating a Plone product - ----------------------------------- - In your Zope instance named myZopeInstance, create a folder - "myZopeInstance/lib/python/MyApp". Create into it your Appy application - (we suppose here that it is a Python package, containing a __init__.py - file and other files). Then, chdir into this folder and type - "python /gen/generator.py . plone25 ../../../Products" and the - product will be generated in myZopeInstance/Products/MyApp. - "python" must refer to a Python interpreter that knows package appy.''' + "productType" is the kind of product you want to generate. "zope" is + the only available production-ready target. + "odt" is experimental. + + "outputFolder" is the folder where the Zope product will be generated. + For example, if you develop your application in + /home/gdy/MyProject/MyProject, you typically specify + "/home/gdy/MyProject/zope" as outputFolder. + ''' def generateProduct(self, options, application, productType, outputFolder): - exec 'from appy.gen.%s.generator import Generator' % productType + if productType == 'odt': + exec 'from appy.gen.odt.generator import Generator' + else: + from appy.gen.generator import ZopeGenerator as Generator Generator(application, outputFolder, options).run() def manageArgs(self, parser, options, args): diff --git a/gen/__init__.py b/gen/__init__.py index 35c3448..5c88e7b 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -2712,13 +2712,6 @@ class Config: # People having one of these roles will be able to create instances # of classes defined in your application. self.defaultCreators = ['Manager', 'Owner'] - # If you want to replace the default front page with a page coming from - # your application, use the following parameter. Setting - # frontPage = True will replace the Plone front page with a page - # whose content will come from i18n label "front_page_text". - self.frontPage = False - # You can choose the Plone or Appy main template - self.frontPageTemplate = 'plone' # or "appy" # 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 diff --git a/gen/descriptors.py b/gen/descriptors.py index 550f329..3459b05 100644 --- a/gen/descriptors.py +++ b/gen/descriptors.py @@ -1,5 +1,14 @@ +'''Descriptor classes defined in this file are "intermediary" classes that + gather, from the user application, information about found gen- or workflow- + classes.''' + # ------------------------------------------------------------------------------ +import types, copy from appy.gen import State, Transition, Type +from po import PoMessage +from model import ModelClass, toolFieldPrefixes +from utils import produceNiceMessage, getClassName +TABS = 4 # Number of blanks in a Python indentation. # ------------------------------------------------------------------------------ class Descriptor: # Abstract @@ -16,6 +25,32 @@ class Descriptor: # Abstract class ClassDescriptor(Descriptor): '''This class gives information about an Appy class.''' + + def __init__(self, klass, orderedAttributes, generator): + appy.gen.descriptors.ClassDescriptor.__init__(self, klass, + orderedAttributes, generator) + self.methods = '' # Needed method definitions will be generated here + # We remember here encountered pages and groups defined in the Appy + # type. Indeed, after having parsed all application classes, we will + # need to generate i18n labels for every child class of the class + # that declared pages and groups. + self.labelsToPropagate = [] #~[PoMessage]~ Some labels (like page, + # group or action names) need to be propagated in children classes + # (because they contain the class name). But at this time we don't know + # yet every sub-class. So we store those labels here; the Generator + # will propagate them later. + self.toolFieldsToPropagate = [] # For this class, some fields have + # been defined on the Tool class. Those fields need to be defined + # for child classes of this class as well, but at this time we don't + # know yet every sub-class. So we store field definitions here; the + # Generator will propagate them later. + 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 getOrderedAppyAttributes(self, condition=None): '''Returns the appy types for all attributes of this class and parent class(es). If a p_condition is specified, ony Appy types matching @@ -87,6 +122,139 @@ class ClassDescriptor(Descriptor): self.pages.append(appyType.page.name) return self.pages + def getParents(self, allClasses): + parentWrapper = 'AbstractWrapper' + parentClass = '%s.%s' % (self.klass.__module__, self.klass.__name__) + if self.klass.__bases__: + baseClassName = self.klass.__bases__[0].__name__ + for k in allClasses: + if self.klass.__name__ == baseClassName: + parentWrapper = '%s_Wrapper' % k.name + return (parentWrapper, parentClass) + + def generateSchema(self, configClass=False): + '''Generates i18n and other related stuff for this class. If this class + is in the configuration (tool, user, etc) we must avoid having + attributes that rely on the configuration (ie attributes that are + optional, with editDefault=True, etc).''' + for attrName in self.orderedAttributes: + try: + attrValue = getattr(self.klass, attrName) + except AttributeError: + attrValue = getattr(self.modelClass, attrName) + if isinstance(attrValue, Type): + if configClass: + attrValue = copy.copy(attrValue) + attrValue.optional = False + attrValue.editDefault = False + FieldDescriptor(attrName, attrValue, self).generate() + + def isAbstract(self): + '''Is self.klass abstract?''' + res = False + if self.klass.__dict__.has_key('abstract'): + res = self.klass.__dict__['abstract'] + return res + + def isRoot(self): + '''Is self.klass root? A root class represents some kind of major + concept into the application. For example, creating instances + of such classes will be easy from the user interface.''' + res = False + if self.klass.__dict__.has_key('root'): + res = self.klass.__dict__['root'] + return res + + def isFolder(self, klass=None): + '''Must self.klass be a folder? If klass is not None, this method tests + it on p_klass instead of self.klass.''' + res = False + theClass = self.klass + if klass: + theClass = klass + if theClass.__dict__.has_key('folder'): + res = theClass.__dict__['folder'] + else: + if theClass.__bases__: + res = self.isFolder(theClass.__bases__[0]) + return res + + def getCreators(self): + '''Gets the specific creators defined for this class.''' + res = [] + if self.klass.__dict__.has_key('creators') and self.klass.creators: + for creator in self.klass.creators: + if isinstance(creator, Role): + if creator.local: + raise 'Local role "%s" cannot be used as a creator.' % \ + creator.name + res.append(creator) + else: + res.append(Role(creator)) + return res + + def getCreateMean(self, type='Import'): + '''Returns the mean for this class that corresponds to p_type, or + None if the class does not support this create mean.''' + if not self.klass.__dict__.has_key('create'): return None + else: + means = self.klass.create + if not means: return None + if not isinstance(means, tuple) and not isinstance(means, list): + means = [means] + for mean in means: + exec 'found = isinstance(mean, %s)' % type + if found: return mean + return None + + @staticmethod + def getSearches(klass): + '''Returns the list of searches that are defined on this class.''' + res = [] + if klass.__dict__.has_key('search'): + searches = klass.__dict__['search'] + if isinstance(searches, basestring): res.append(Search(searches)) + elif isinstance(searches, Search): res.append(searches) + else: + # It must be a list of searches. + for search in searches: + if isinstance(search, basestring):res.append(Search(search)) + else: res.append(search) + return res + + @staticmethod + def getSearch(klass, searchName): + '''Gets the search named p_searchName.''' + for search in ClassDescriptor.getSearches(klass): + if search.name == searchName: + return search + return None + + def addIndexMethod(self, field): + '''For indexed p_field, this method generates a method that allows to + get the value of the field as must be copied into the corresponding + index.''' + m = self.methods + spaces = TABS + n = field.fieldName + m += '\n' + ' '*spaces + 'def get%s%s(self):\n' % (n[0].upper(), n[1:]) + spaces += TABS + m += ' '*spaces + "'''Gets indexable value of field \"%s\".'''\n" % n + m += ' '*spaces + 'return self.getAppyType("%s").getIndexValue(' \ + 'self)\n' % n + self.methods = m + + def addField(self, fieldName, fieldType): + '''Adds a new field to the Tool.''' + exec "self.modelClass.%s = fieldType" % fieldName + if fieldName in self.modelClass._appy_attributes: + print 'Warning, field "%s" is already existing on class "%s"' % \ + (fieldName, self.modelClass.__name__) + return + self.modelClass._appy_attributes.append(fieldName) + self.orderedAttributes.append(fieldName) + +# ------------------------------------------------------------------------------ class WorkflowDescriptor(Descriptor): '''This class gives information about an Appy workflow.''' @staticmethod @@ -94,4 +262,428 @@ class WorkflowDescriptor(Descriptor): '''Returns the name of this workflow.''' res = klass.__module__.replace('.', '_') + '_' + klass.__name__ return res.lower() + +# ------------------------------------------------------------------------------ +class FieldDescriptor: + '''This class gathers information about a specific typed attribute defined + in a gen-class.''' + + singleValuedTypes = ('Integer', 'Float', 'Boolean', 'Date', 'File') + # Although Appy allows to specify a multiplicity[0]>1 for those types, it is + # not supported by Archetypes. So we will always generate single-valued type + # definitions for them. + specialParams = ('title', 'description') + + def __init__(self, fieldName, appyType, classDescriptor): + self.appyType = appyType + self.classDescr = classDescriptor + self.generator = classDescriptor.generator + self.applicationName = classDescriptor.generator.applicationName + self.fieldName = fieldName + self.fieldParams = {'name': fieldName} + self.widgetParams = {} + self.fieldType = None + self.widgetType = None + + def __repr__(self): + return '' % (self.fieldName, self.classDescr) + + def getToolFieldMessage(self, fieldName): + '''Some attributes generated on the Tool class need a specific + default message, returned by this method.''' + res = fieldName + for prefix in toolFieldPrefixes: + fullPrefix = prefix + 'For' + if fieldName.startswith(fullPrefix): + messageId = 'MSG_%s' % prefix + res = getattr(PoMessage, messageId) + if res.find('%s') != -1: + # I must complete the message with the field name. + res = res % fieldName.split('_')[-1] + break + return res + + def produceMessage(self, msgId, isLabel=True): + '''Gets the default label, description or help (depending on p_msgType) + for i18n message p_msgId.''' + default = ' ' + produceNice = False + if isLabel: + produceNice = True + default = self.fieldName + # Some attributes need a specific predefined message + if isinstance(self.classDescr, ToolClassDescriptor): + default = self.getToolFieldMessage(self.fieldName) + if default != self.fieldName: produceNice = False + msg = PoMessage(msgId, '', default) + if produceNice: + msg.produceNiceDefault() + return msg + + def walkString(self): + '''How to generate an Appy String?''' + if self.appyType.isSelect and \ + (type(self.appyType.validator) in (list, tuple)): + # Generate i18n messages for every possible value if the list + # of values is fixed. + for value in self.appyType.validator: + msgLabel = '%s_%s_list_%s' % (self.classDescr.name, + self.fieldName, value) + poMsg = PoMessage(msgLabel, '', value) + poMsg.produceNiceDefault() + self.generator.labels.append(poMsg) + + def walkAction(self): + '''Generates the i18n-related label.''' + if self.appyType.confirm: + label = '%s_%s_confirm' % (self.classDescr.name, self.fieldName) + msg = PoMessage(label, '', PoMessage.CONFIRM) + self.generator.labels.append(msg) + + def walkRef(self): + '''How to generate a Ref?''' + # Update the list of referers + self.generator.addReferer(self) + # Add the widget label for the back reference + back = self.appyType.back + refClassName = getClassName(self.appyType.klass, self.applicationName) + if back.hasLabel: + backLabel = "%s_%s" % (refClassName, self.appyType.back.attribute) + poMsg = PoMessage(backLabel, '', self.appyType.back.attribute) + poMsg.produceNiceDefault() + self.generator.labels.append(poMsg) + # Add the label for the confirm message if relevant + if self.appyType.addConfirm: + label = '%s_%s_addConfirm' % (self.classDescr.name, self.fieldName) + msg = PoMessage(label, '', PoMessage.CONFIRM) + self.generator.labels.append(msg) + + def walkPod(self): + # Add i18n-specific messages + if self.appyType.askAction: + label = '%s_%s_askaction' % (self.classDescr.name, self.fieldName) + msg = PoMessage(label, '', PoMessage.POD_ASKACTION) + self.generator.labels.append(msg) + self.classDescr.labelsToPropagate.append(msg) + # Add the POD-related fields on the Tool + self.generator.tool.addPodRelatedFields(self) + + def walkList(self): + # Add i18n-specific messages + for name, field in self.appyType.fields: + label = '%s_%s_%s' % (self.classDescr.name, self.fieldName, name) + msg = PoMessage(label, '', name) + msg.produceNiceDefault() + self.generator.labels.append(msg) + + def walkAppyType(self): + '''Walks into the Appy type definition and gathers data about the + i18n labels.''' + # Manage things common to all Appy types + # - optional ? + if self.appyType.optional: + self.generator.tool.addOptionalField(self) + # - edit default value ? + if self.appyType.editDefault: + self.generator.tool.addDefaultField(self) + # - put an index on this field? + if self.appyType.indexed and \ + (self.fieldName not in ('title', 'description')): + self.classDescr.addIndexMethod(self) + # i18n labels + messages = self.generator.labels + if not self.appyType.label: + # Create labels for generating them in i18n files, only if required. + i18nPrefix = "%s_%s" % (self.classDescr.name, self.fieldName) + if self.appyType.hasLabel: + messages.append(self.produceMessage(i18nPrefix)) + if self.appyType.hasDescr: + descrId = i18nPrefix + '_descr' + messages.append(self.produceMessage(descrId,isLabel=False)) + if self.appyType.hasHelp: + helpId = i18nPrefix + '_help' + messages.append(self.produceMessage(helpId, isLabel=False)) + # 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) + # Create i18n messages linked to groups + group = self.appyType.group + if group and not group.label: + group.generateLabels(messages, self.classDescr, set()) + # Manage things which are specific to String types + if self.appyType.type == 'String': self.walkString() + # Manage things which are specific to Actions + elif self.appyType.type == 'Action': self.walkAction() + # Manage things which are specific to Ref types + elif self.appyType.type == 'Ref': self.walkRef() + # Manage things which are specific to Pod types + elif self.appyType.type == 'Pod': self.walkPod() + # Manage things which are specific to List types + elif self.appyType.type == 'List': self.walkList() + + def generate(self): + '''Generates the i18n labels for this type.''' + self.walkAppyType() + +# ------------------------------------------------------------------------------ +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.modelClass = self.klass + self.predefined = True + self.customized = False + + def getParents(self, allClasses=()): + res = ['Tool'] + if self.customized: + res.append('%s.%s' % (self.klass.__module__, self.klass.__name__)) + return res + + def update(self, klass, attributes): + '''This method is called by the generator when he finds a custom tool + definition. We must then add the custom tool elements in this default + Tool descriptor.''' + self.orderedAttributes += attributes + self.klass = klass + self.customized = True + + def isFolder(self, klass=None): return True + def isRoot(self): return False + def generateSchema(self): + ClassDescriptor.generateSchema(self, configClass=True) + + def addOptionalField(self, fieldDescr): + className = fieldDescr.classDescr.name + fieldName = 'optionalFieldsFor%s' % className + fieldType = getattr(self.modelClass, fieldName, None) + if not fieldType: + fieldType = String(multiplicity=(0,None)) + fieldType.validator = [] + self.addField(fieldName, fieldType) + fieldType.validator.append(fieldDescr.fieldName) + fieldType.page.name = 'data' + fieldType.group = Group(fieldDescr.classDescr.klass.__name__) + + def addDefaultField(self, fieldDescr): + className = fieldDescr.classDescr.name + fieldName = 'defaultValueFor%s_%s' % (className, fieldDescr.fieldName) + fieldType = fieldDescr.appyType.clone() + self.addField(fieldName, fieldType) + fieldType.page.name = 'data' + fieldType.group = Group(fieldDescr.classDescr.klass.__name__) + + def addPodRelatedFields(self, fieldDescr): + '''Adds the fields needed in the Tool for configuring a Pod field.''' + className = fieldDescr.classDescr.name + # On what page and group to display those fields ? + pg = {'page': 'documentGeneration', + 'group': Group(fieldDescr.classDescr.klass.__name__, ['50%']*2)} + # Add the field that will store the pod template. + fieldName = 'podTemplateFor%s_%s' % (className, fieldDescr.fieldName) + fieldType = File(**pg) + self.addField(fieldName, fieldType) + # Add the field that will store the output format(s) + fieldName = 'formatsFor%s_%s' % (className, fieldDescr.fieldName) + fieldType = String(validator=Selection('getPodOutputFormats'), + multiplicity=(1,None), default=('odt',), **pg) + self.addField(fieldName, fieldType) + + def addQueryResultColumns(self, classDescr): + '''Adds, for class p_classDescr, the attribute in the tool that allows + to select what default columns will be shown on query results.''' + className = classDescr.name + fieldName = 'resultColumnsFor%s' % className + fieldType = String(multiplicity=(0,None), validator=Selection( + '_appy_getAllFields*%s' % className), page='userInterface', + group=classDescr.klass.__name__) + self.addField(fieldName, fieldType) + + def addSearchRelatedFields(self, classDescr): + '''Adds, for class p_classDescr, attributes related to the search + functionality for class p_classDescr.''' + className = classDescr.name + # Field that defines if advanced search is enabled for class + # p_classDescr or not. + fieldName = 'enableAdvancedSearchFor%s' % className + fieldType = Boolean(default=True, page='userInterface', + group=classDescr.klass.__name__) + 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) + # Field that allows to select, among all indexed fields, what fields + # must really be used in the search screen. + fieldName = 'searchFieldsFor%s' % className + defaultValue = [a[0] for a in classDescr.getOrderedAppyAttributes( + condition='attrValue.indexed')] + fieldType = String(multiplicity=(0,None), validator=Selection( + '_appy_getSearchableFields*%s' % className), default=defaultValue, + page='userInterface', group=classDescr.klass.__name__) + self.addField(fieldName, fieldType) + + def addImportRelatedFields(self, classDescr): + '''Adds, for class p_classDescr, attributes related to the import + functionality for class p_classDescr.''' + className = classDescr.name + # Field that defines the path of the files to import. + fieldName = 'importPathFor%s' % className + defValue = classDescr.getCreateMean('Import').path + fieldType = String(page='data', multiplicity=(1,1), default=defValue, + group=classDescr.klass.__name__) + self.addField(fieldName, fieldType) + + def addWorkflowFields(self, classDescr): + '''Adds, for a given p_classDescr, the workflow-related fields.''' + className = classDescr.name + groupName = classDescr.klass.__name__ + # Adds a field allowing to show/hide completely any workflow-related + # information for a given class. + defaultValue = False + if classDescr.isRoot() or issubclass(classDescr.klass, ModelClass): + defaultValue = True + fieldName = 'showWorkflowFor%s' % className + fieldType = Boolean(default=defaultValue, page='userInterface', + group=groupName) + 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) + # 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 + # we are is sufficient. If this boolean is False, we simply show the + # current state. + defaultValue = False + if len(classDescr.getPhases()) > 1: + defaultValue = True + fieldName = 'showAllStatesInPhaseFor%s' % className + fieldType = Boolean(default=defaultValue, page='userInterface', + group=groupName) + self.addField(fieldName, fieldType) + +class UserClassDescriptor(ClassDescriptor): + '''Represents an Archetypes-compliant class that corresponds to the User + for the generated 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=()): + res = ['User'] + if self.customized: + res.append('%s.%s' % (self.klass.__module__, self.klass.__name__)) + return res + def update(self, klass, attributes): + '''This method is called by the generator when he finds a custom user + definition. We must then add the custom user elements in this + default User descriptor.''' + self.orderedAttributes += attributes + self.klass = klass + self.customized = True + def isFolder(self, klass=None): return False + def generateSchema(self): + ClassDescriptor.generateSchema(self, configClass=True) + +class GroupClassDescriptor(ClassDescriptor): + '''Represents the class that corresponds to the Group for the generated + 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=()): + res = ['Group'] + if self.customized: + res.append('%s.%s' % (self.klass.__module__, self.klass.__name__)) + return res + def update(self, klass, attributes): + '''This method is called by the generator when he finds a custom group + definition. We must then add the custom group elements in this + default Group descriptor. + + NOTE: currently, it is not possible to define a custom Group + class.''' + self.orderedAttributes += attributes + self.klass = klass + self.customized = 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.label, plainText=False, + page=page, show=self.modelClass.show, 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.show} + appName = self.generator.applicationName + # Scan all messages corresponding to p_messageId from all translation + # files. We will define field length from the longer found message + # 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) or \ + not i18nFiles[fileName].messagesDict.has_key(messageId): + # In this case this is not one of our Appy-managed translation + # files. + 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)) # ------------------------------------------------------------------------------ diff --git a/gen/generator.py b/gen/generator.py index 48b9dad..fd7c58d 100644 --- a/gen/generator.py +++ b/gen/generator.py @@ -1,10 +1,12 @@ # ------------------------------------------------------------------------------ -import os, os.path, sys, parser, symbol, token, types -from appy.gen import Type, State, Config, Tool, User -from appy.gen.descriptors import * -from appy.gen.utils import produceNiceMessage +import os, os.path, re, sys, parser, symbol, token, types import appy.pod, appy.pod.renderer from appy.shared.utils import FolderDeleter +#from appy.gen import * +from po import PoMessage, PoFile, PoParser +from descriptors import * +from utils import produceNiceMessage, getClassName +from model import ModelClass, User, Group, Tool, Translation # ------------------------------------------------------------------------------ class GeneratorError(Exception): pass @@ -91,8 +93,6 @@ class Ast: self.classes[astClass.name] = astClass # ------------------------------------------------------------------------------ -WARN_NO_TEMPLATE = 'Warning: the code generator should have a folder "%s" ' \ - 'containing all code templates.' CODE_HEADER = '''# -*- coding: utf-8 -*- # # GNU General Public License (GPL) @@ -119,17 +119,12 @@ class Generator: self.application = application # Determine application name self.applicationName = os.path.basename(application) - if application.endswith('.py'): - self.applicationName = self.applicationName[:-3] # Determine output folder (where to store the generated product) - self.outputFolder = '%s/%s' % (outputFolder, self.applicationName) + self.outputFolder = outputFolder self.options = options # Determine templates folder - exec 'import %s as genModule' % self.__class__.__module__ - self.templatesFolder = os.path.join(os.path.dirname(genModule.__file__), - 'templates') - if not os.path.exists(self.templatesFolder): - print WARN_NO_TEMPLATE % self.templatesFolder + genFolder = os.path.dirname(__file__) + self.templatesFolder = os.path.join(genFolder, 'templates') # Default descriptor classes self.descriptorClasses = { 'class': ClassDescriptor, 'tool': ClassDescriptor, @@ -273,8 +268,6 @@ class Generator: sys.path.append(containingFolder) # What is the name of the application ? appName = os.path.basename(self.application) - if os.path.isfile(self.application): - appName = os.path.splitext(appName)[0] self.walkModule(appName) sys.path.pop() @@ -342,4 +335,582 @@ class Generator: if self.totalNumberOfTests: msg = ' (number of tests found: %d)' % self.totalNumberOfTests print 'Done%s.' % msg + +# ------------------------------------------------------------------------------ +class ZopeGenerator(Generator): + '''This generator generates a Zope-compliant product from a given Appy + application.''' + poExtensions = ('.po', '.pot') + + def __init__(self, *args, **kwargs): + Tool._appy_clean() + Generator.__init__(self, *args, **kwargs) + # Set our own Descriptor classes + self.descriptorClasses['class'] = ClassDescriptor + # Create our own Tool, User, Group and Translation instances + self.tool = ToolClassDescriptor(Tool, self) + self.user = UserClassDescriptor(User, self) + self.group = GroupClassDescriptor(Group, self) + self.translation = TranslationClassDescriptor(Translation, self) + # i18n labels to generate + self.labels = [] # i18n labels + self.referers = {} + + versionRex = re.compile('(.*?\s+build)\s+(\d+)') + def initialize(self): + # Determine version number + self.version = '0.1 build 1' + versionTxt = os.path.join(self.outputFolder, 'version.txt') + if os.path.exists(versionTxt): + f = file(versionTxt) + oldVersion = f.read().strip() + f.close() + res = self.versionRex.search(oldVersion) + self.version = res.group(1) + ' ' + str(int(res.group(2))+1) + # Existing i18n files + self.i18nFiles = {} #~{p_fileName: PoFile}~ + # Retrieve existing i18n files if any + i18nFolder = os.path.join(self.application, 'tr') + if os.path.exists(i18nFolder): + for fileName in os.listdir(i18nFolder): + name, ext = os.path.splitext(fileName) + if ext in self.poExtensions: + poParser = PoParser(os.path.join(i18nFolder, fileName)) + self.i18nFiles[fileName] = poParser.parse() + + def finalize(self): + # Some useful aliases + msg = PoMessage + app = self.applicationName + # Some global i18n messages + poMsg = msg(app, '', app); poMsg.produceNiceDefault() + self.labels += [poMsg, + msg('workflow_state', '', msg.WORKFLOW_STATE), + msg('appy_title', '', msg.APPY_TITLE), + msg('data_change', '', msg.DATA_CHANGE), + msg('modified_field', '', msg.MODIFIED_FIELD), + msg('previous_value', '', msg.PREVIOUS_VALUE), + msg('phase', '', msg.PHASE), + msg('root_type', '', msg.ROOT_TYPE), + msg('workflow_comment', '', msg.WORKFLOW_COMMENT), + msg('choose_a_value', '', msg.CHOOSE_A_VALUE), + msg('choose_a_doc', '', msg.CHOOSE_A_DOC), + msg('min_ref_violated', '', msg.MIN_REF_VIOLATED), + msg('max_ref_violated', '', msg.MAX_REF_VIOLATED), + msg('no_ref', '', msg.REF_NO), + msg('add_ref', '', msg.REF_ADD), + msg('ref_actions', '', msg.REF_ACTIONS), + msg('action_ok', '', msg.ACTION_OK), + msg('action_ko', '', msg.ACTION_KO), + msg('move_up', '', msg.REF_MOVE_UP), + msg('move_down', '', msg.REF_MOVE_DOWN), + msg('query_create', '', msg.QUERY_CREATE), + msg('query_import', '', msg.QUERY_IMPORT), + msg('query_no_result', '', msg.QUERY_NO_RESULT), + msg('query_consult_all', '', msg.QUERY_CONSULT_ALL), + msg('import_title', '', msg.IMPORT_TITLE), + msg('import_show_hide', '', msg.IMPORT_SHOW_HIDE), + msg('import_already', '', msg.IMPORT_ALREADY), + msg('import_many', '', msg.IMPORT_MANY), + msg('import_done', '', msg.IMPORT_DONE), + msg('search_title', '', msg.SEARCH_TITLE), + msg('search_button', '', msg.SEARCH_BUTTON), + msg('search_objects', '', msg.SEARCH_OBJECTS), + msg('search_results', '', msg.SEARCH_RESULTS), + msg('search_results_descr', '', ' '), + msg('search_new', '', msg.SEARCH_NEW), + msg('search_from', '', msg.SEARCH_FROM), + msg('search_to', '', msg.SEARCH_TO), + msg('search_or', '', msg.SEARCH_OR), + msg('search_and', '', msg.SEARCH_AND), + msg('ref_invalid_index', '', msg.REF_INVALID_INDEX), + msg('bad_long', '', msg.BAD_LONG), + msg('bad_float', '', msg.BAD_FLOAT), + msg('bad_date', '', msg.BAD_DATE), + msg('bad_email', '', msg.BAD_EMAIL), + msg('bad_url', '', msg.BAD_URL), + msg('bad_alphanumeric', '', msg.BAD_ALPHANUMERIC), + msg('bad_select_value', '', msg.BAD_SELECT_VALUE), + msg('select_delesect', '', msg.SELECT_DESELECT), + msg('no_elem_selected', '', msg.NO_SELECTION), + msg('delete_confirm', '', msg.DELETE_CONFIRM), + msg('delete_done', '', msg.DELETE_DONE), + msg('goto_first', '', msg.GOTO_FIRST), + msg('goto_previous', '', msg.GOTO_PREVIOUS), + msg('goto_next', '', msg.GOTO_NEXT), + msg('goto_last', '', msg.GOTO_LAST), + msg('goto_source', '', msg.GOTO_SOURCE), + msg('whatever', '', msg.WHATEVER), + msg('yes', '', msg.YES), + msg('no', '', msg.NO), + msg('field_required', '', msg.FIELD_REQUIRED), + msg('field_invalid', '', msg.FIELD_INVALID), + msg('file_required', '', msg.FILE_REQUIRED), + msg('image_required', '', msg.IMAGE_REQUIRED), + msg('odt', '', msg.FORMAT_ODT), + msg('pdf', '', msg.FORMAT_PDF), + msg('doc', '', msg.FORMAT_DOC), + msg('rtf', '', msg.FORMAT_RTF), + msg('front_page_text', '', msg.FRONT_PAGE_TEXT), + ] + # Create a label for every role added by this application + for role in self.getAllUsedRoles(): + self.labels.append(msg('role_%s' % role.name,'', role.name, + niceDefault=True)) + # Create basic files (config.py, etc) + self.generateTool() + self.generateInit() + self.generateTests() + self.generateConfigureZcml() + # Create version.txt + f = open(os.path.join(self.outputFolder, 'version.txt'), 'w') + f.write(self.version) + f.close() + # Make folder "tests" a Python package + initFile = '%s/tests/__init__.py' % self.outputFolder + if not os.path.isfile(initFile): + f = open(initFile, 'w') + f.write('') + f.close() + # Decline i18n labels into versions for child classes + for classDescr in self.classes: + for poMsg in classDescr.labelsToPropagate: + for childDescr in classDescr.getChildren(): + childMsg = poMsg.clone(classDescr.name, childDescr.name) + if childMsg not in self.labels: + self.labels.append(childMsg) + # Generate i18n pot file + potFileName = '%s.pot' % self.applicationName + if self.i18nFiles.has_key(potFileName): + potFile = self.i18nFiles[potFileName] + else: + fullName = os.path.join(self.application, 'tr', 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: + # I must generate (or update) a po file for the language(s) + # specified in the configuration. + poFileName = potFile.getPoFileName(language) + if self.i18nFiles.has_key(poFileName): + poFile = self.i18nFiles[poFileName] + else: + fullName = os.path.join(self.application, 'tr', poFileName) + poFile = PoFile(fullName) + self.i18nFiles[poFileName] = poFile + 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) + 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 + classes defined in this application. + + If p_plone is True, it keeps only Plone-standard roles; if p_plone + is False, it keeps only roles which are specific to this application; + if p_plone is None it has no effect (so it keeps both roles). + + If p_local is True, it keeps only local roles (ie, roles that can + only be granted locally); if p_local is False, it keeps only "global" + roles; if p_local is None it has no effect (so it keeps both roles). + + If p_grantable is True, it keeps only roles that the admin can + grant; if p_grantable is False, if keeps only ungrantable roles (ie + those that are implicitly granted by the system like role + "Authenticated"); if p_grantable is None it keeps both roles.''' + allRoles = {} # ~{s_roleName:Role_role}~ + # Gather roles from workflow states and transitions + for wfDescr in self.workflows: + for attr in dir(wfDescr.klass): + attrValue = getattr(wfDescr.klass, attr) + if isinstance(attrValue, State) or \ + isinstance(attrValue, Transition): + for role in attrValue.getUsedRoles(): + if role.name not in allRoles: + allRoles[role.name] = role + # Gather roles from "creators" attributes from every class + for cDescr in self.getClasses(include='all'): + for role in cDescr.getCreators(): + if role.name not in allRoles: + allRoles[role.name] = role + res = allRoles.values() + # Filter the result according to parameters + for p in ('plone', 'local', 'grantable'): + if eval(p) != None: + res = [r for r in res if eval('r.%s == %s' % (p, p))] + return res + + def addReferer(self, fieldDescr): + '''p_fieldDescr is a Ref type definition.''' + k = fieldDescr.appyType.klass + refClassName = getClassName(k, self.applicationName) + if not self.referers.has_key(refClassName): + self.referers[refClassName] = [] + self.referers[refClassName].append(fieldDescr) + + def getAppyTypePath(self, name, appyType, klass, isBack=False): + '''Gets the path to the p_appyType when a direct reference to an + appyType must be generated in a Python file.''' + if issubclass(klass, ModelClass): + res = 'wrappers.%s.%s' % (klass.__name__, name) + else: + res = '%s.%s.%s' % (klass.__module__, klass.__name__, name) + if isBack: res += '.back' + return res + + 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, group, 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 + res = self.classes[:] + configClasses = [self.tool, self.user, self.group, 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 generateConfigureZcml(self): + '''Generates file configure.zcml.''' + repls = self.repls.copy() + # Note every class as "deprecated". + depr = '' + for klass in self.getClasses(include='all'): + depr += '\n' % \ + (klass.name, klass.name) + repls['deprecated'] = depr + self.copyFile('configure.zcml', repls) + + 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] + for classDescr in (classesWithCustom + self.workflows): + theImport = 'import %s' % classDescr.klass.__module__ + if theImport not in imports: + imports.append(theImport) + repls['imports'] = '\n'.join(imports) + # Compute default add roles + repls['defaultAddRoles'] = ','.join( + ['"%s"' % r for r in self.config.defaultCreators]) + # Compute list of add permissions + addPermissions = '' + for classDescr in classesAll: + addPermissions += ' "%s":"%s: Add %s",\n' % (classDescr.name, + self.applicationName, classDescr.name) + repls['addPermissions'] = addPermissions + # Compute root classes + repls['rootClasses'] = ','.join(["'%s'" % c.name \ + for c in classesButTool if c.isRoot()]) + # Compute list of class definitions + repls['appClasses'] = ','.join(['%s.%s' % (c.klass.__module__, \ + c.klass.__name__) for c in classes]) + # Compute lists of class names + repls['appClassNames'] = ','.join(['"%s"' % c.name \ + for c in classes]) + repls['allClassNames'] = ','.join(['"%s"' % c.name \ + for c in classesButTool]) + # Compute the list of ordered attributes (forward and backward, + # inherited included) for every Appy class. + attributes = [] + for classDescr in classesAll: + titleFound = False + names = [] + for name, appyType, klass in classDescr.getOrderedAppyAttributes(): + names.append(name) + if name == 'title': titleFound = True + # Add the "title" mandatory field if not found + if not titleFound: names.insert(0, 'title') + # Any backward attributes to append? + if classDescr.name in self.referers: + for field in self.referers[classDescr.name]: + names.append(field.appyType.back.attribute) + qNames = ['"%s"' % name for name in names] + attributes.append('"%s":[%s]' % (classDescr.name, ','.join(qNames))) + repls['attributes'] = ',\n '.join(attributes) + # Compute list of used roles for registering them if needed + specificRoles = self.getAllUsedRoles(plone=False) + repls['roles'] = ','.join(['"%s"' % r.name for r in specificRoles]) + globalRoles = self.getAllUsedRoles(plone=False, local=False) + repls['gRoles'] = ','.join(['"%s"' % r.name for r in globalRoles]) + grantableRoles = self.getAllUsedRoles(local=False, grantable=True) + repls['grRoles'] = ','.join(['"%s"' % r.name for r in grantableRoles]) + # Generate configuration options + repls['languages'] = ','.join('"%s"' % l for l in self.config.languages) + repls['languageSelector'] = self.config.languageSelector + repls['sourceLanguage'] = self.config.sourceLanguage + self.copyFile('config.py', repls) + + def generateInit(self): + # Compute imports + imports = [] + classNames = [] + for c in self.getClasses(include='all'): + importDef = ' import %s' % c.name + if importDef not in imports: + imports.append(importDef) + classNames.append("%s.%s" % (c.name, c.name)) + repls = self.repls.copy() + repls['imports'] = '\n'.join(imports) + repls['classes'] = ','.join(classNames) + repls['totalNumberOfTests'] = self.totalNumberOfTests + self.copyFile('__init__.py', repls) + + def getClassesInOrder(self, allClasses): + '''When generating wrappers, classes mut be dumped in order (else, it + generates forward references in the Python file, that does not + compile).''' + res = [] # Appy class descriptors + resClasses = [] # Corresponding real Python classes + for classDescr in allClasses: + klass = classDescr.klass + if not klass.__bases__ or \ + (klass.__bases__[0].__name__ == 'ModelClass'): + # This is a root class. We dump it at the begin of the file. + res.insert(0, classDescr) + resClasses.insert(0, klass) + else: + # If a child of this class is already present, we must insert + # this klass before it. + lowestChildIndex = sys.maxint + for resClass in resClasses: + if klass in resClass.__bases__: + lowestChildIndex = min(lowestChildIndex, + resClasses.index(resClass)) + if lowestChildIndex != sys.maxint: + res.insert(lowestChildIndex, classDescr) + resClasses.insert(lowestChildIndex, klass) + else: + res.append(classDescr) + resClasses.append(klass) + return res + + def generateWrappers(self): + # We must generate imports and wrapper definitions + imports = [] + wrappers = [] + allClasses = self.getClasses(include='all') + for c in self.getClassesInOrder(allClasses): + if not c.predefined or c.customized: + moduleImport = 'import %s' % c.klass.__module__ + if moduleImport not in imports: + imports.append(moduleImport) + # Determine parent wrapper and class + parentClasses = c.getParents(allClasses) + wrapperDef = 'class %s_Wrapper(%s):\n' % \ + (c.name, ','.join(parentClasses)) + wrapperDef += ' security = ClassSecurityInfo()\n' + if c.customized: + # For custom tool, add a call to a method that allows to + # customize elements from the base class. + wrapperDef += " if hasattr(%s, 'update'):\n " \ + "%s.update(%s)\n" % (parentClasses[1], parentClasses[1], + parentClasses[0]) + # For custom tool, add security declaration that will allow to + # call their methods from ZPTs. + for parentClass in parentClasses: + wrapperDef += " for elem in dir(%s):\n " \ + "if not elem.startswith('_'): security.declarePublic" \ + "(elem)\n" % (parentClass) + # Register the class in Zope. + wrapperDef += 'InitializeClass(%s_Wrapper)\n' % c.name + wrappers.append(wrapperDef) + repls = self.repls.copy() + repls['imports'] = '\n'.join(imports) + repls['wrappers'] = '\n'.join(wrappers) + for klass in self.getClasses(include='predefined'): + modelClass = klass.modelClass + repls['%s' % modelClass.__name__] = modelClass._appy_getBody() + self.copyFile('wrappers.py', repls) + + def generateTests(self): + '''Generates the file needed for executing tests.''' + repls = self.repls.copy() + modules = self.modulesWithTests + repls['imports'] = '\n'.join(['import %s' % m for m in modules]) + repls['modulesWithTests'] = ','.join(modules) + self.copyFile('testAll.py', repls, destFolder='tests') + + def generateTool(self): + '''Generates the Plone tool that corresponds to this application.''' + Msg = PoMessage + # Create Tool-related i18n-related messages + msg = Msg(self.tool.name, '', Msg.CONFIG % self.applicationName) + self.labels.append(msg) + + # Tune the Ref field between Tool->User and Group->User + Tool.users.klass = User + if self.user.customized: + Tool.users.klass = self.user.klass + Group.users.klass = self.user.klass + + # Generate the Tool-related classes (User, Group, Translation) + for klass in (self.user, self.group, self.translation): + klassType = klass.name[len(self.applicationName):] + klass.generateSchema() + self.labels += [ Msg(klass.name, '', klassType), + Msg('%s_plural' % klass.name,'', klass.name+'s')] + repls = self.repls.copy() + repls.update({'methods': klass.methods, 'genClassName': klass.name, + 'baseMixin':'BaseMixin', 'parents': 'BaseMixin, SimpleItem', + 'classDoc': 'Standard Appy class', 'icon':'object.gif'}) + 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. + for classDescr in self.getClasses(include='allButTool'): + for fieldName, fieldType in classDescr.toolFieldsToPropagate: + for childDescr in classDescr.getChildren(): + childFieldName = fieldName % childDescr.name + fieldType.group = childDescr.klass.__name__ + self.tool.addField(childFieldName, fieldType) + if classDescr.isRoot(): + # We must be able to configure query results from the tool. + self.tool.addQueryResultColumns(classDescr) + # Add the search-related fields. + self.tool.addSearchRelatedFields(classDescr) + importMean = classDescr.getCreateMean('Import') + if importMean: + self.tool.addImportRelatedFields(classDescr) + self.tool.addWorkflowFields(self.user) + self.tool.generateSchema() + + # Generate the Tool class + repls = self.repls.copy() + repls.update({'methods': self.tool.methods, + 'genClassName': self.tool.name, 'baseMixin':'ToolMixin', + 'parents': 'ToolMixin, Folder', 'icon': 'folder.gif', + 'classDoc': 'Tool class for %s' % 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 + generating the corresponding Archetype class.''' + k = classDescr.klass + print 'Generating %s.%s (gen-class)...' % (k.__module__, k.__name__) + if not classDescr.isAbstract(): + self.tool.addWorkflowFields(classDescr) + # Determine base Zope class + isFolder = classDescr.isFolder() + baseClass = isFolder and 'Folder' or 'SimpleItem' + icon = isFolder and 'folder.gif' or 'object.gif' + parents = 'BaseMixin, %s' % baseClass + classDoc = classDescr.klass.__doc__ or 'Appy class.' + repls = self.repls.copy() + classDescr.generateSchema() + repls.update({ + 'parents': parents, 'className': classDescr.klass.__name__, + 'genClassName': classDescr.name, 'baseMixin':'BaseMixin', + 'classDoc': classDoc, 'applicationName': self.applicationName, + 'methods': classDescr.methods, 'icon':icon}) + fileName = '%s.py' % classDescr.name + # Create i18n labels (class name and plural form) + poMsg = PoMessage(classDescr.name, '', classDescr.klass.__name__) + poMsg.produceNiceDefault() + self.labels.append(poMsg) + poMsgPl = PoMessage('%s_plural' % classDescr.name, '', + classDescr.klass.__name__+'s') + poMsgPl.produceNiceDefault() + self.labels.append(poMsgPl) + # Create i18n labels for searches + for search in classDescr.getSearches(classDescr.klass): + searchLabel = '%s_search_%s' % (classDescr.name, search.name) + labels = [searchLabel, '%s_descr' % searchLabel] + if search.group: + grpLabel = '%s_searchgroup_%s' % (classDescr.name, search.group) + labels += [grpLabel, '%s_descr' % grpLabel] + for label in labels: + default = ' ' + if label == searchLabel: default = search.name + poMsg = PoMessage(label, '', default) + poMsg.produceNiceDefault() + if poMsg not in self.labels: + self.labels.append(poMsg) + # Generate the resulting Archetypes class. + self.copyFile('Class.py', repls, destName=fileName) + + def generateWorkflow(self, wfDescr): + '''This method creates the i18n labels related to the workflow described + by p_wfDescr.''' + k = wfDescr.klass + print 'Generating %s.%s (gen-workflow)...' % (k.__module__, k.__name__) + # Identify workflow name + wfName = WorkflowDescriptor.getWorkflowName(wfDescr.klass) + # Add i18n messages for states + for name in dir(wfDescr.klass): + if not isinstance(getattr(wfDescr.klass, name), State): continue + poMsg = PoMessage('%s_%s' % (wfName, name), '', name) + poMsg.produceNiceDefault() + self.labels.append(poMsg) + # Add i18n messages for transitions + for name in dir(wfDescr.klass): + transition = getattr(wfDescr.klass, name) + if not isinstance(transition, Transition): continue + poMsg = PoMessage('%s_%s' % (wfName, name), '', name) + poMsg.produceNiceDefault() + self.labels.append(poMsg) + if transition.confirm: + # We need to generate a label for the message that will be shown + # in the confirm popup. + label = '%s_%s_confirm' % (wfName, name) + poMsg = PoMessage(label, '', PoMessage.CONFIRM) + self.labels.append(poMsg) + if transition.notify: + # Appy will send a mail when this transition is triggered. + # So we need 2 i18n labels: one for the mail subject and one for + # the mail body. + subjectLabel = '%s_%s_mail_subject' % (wfName, name) + poMsg = PoMessage(subjectLabel, '', PoMessage.EMAIL_SUBJECT) + self.labels.append(poMsg) + bodyLabel = '%s_%s_mail_body' % (wfName, name) + poMsg = PoMessage(bodyLabel, '', PoMessage.EMAIL_BODY) + self.labels.append(poMsg) # ------------------------------------------------------------------------------ diff --git a/gen/plone25/installer.py b/gen/installer.py similarity index 100% rename from gen/plone25/installer.py rename to gen/installer.py diff --git a/gen/plone25/migrator.py b/gen/migrator.py similarity index 100% rename from gen/plone25/migrator.py rename to gen/migrator.py diff --git a/gen/plone25/mixins/TestMixin.py b/gen/mixins/TestMixin.py similarity index 100% rename from gen/plone25/mixins/TestMixin.py rename to gen/mixins/TestMixin.py diff --git a/gen/plone25/mixins/ToolMixin.py b/gen/mixins/ToolMixin.py similarity index 99% rename from gen/plone25/mixins/ToolMixin.py rename to gen/mixins/ToolMixin.py index 7dbc5cc..5d6f73e 100644 --- a/gen/plone25/mixins/ToolMixin.py +++ b/gen/mixins/ToolMixin.py @@ -27,7 +27,7 @@ class ToolMixin(BaseMixin): res = metaTypeOrAppyClass if not isinstance(metaTypeOrAppyClass, basestring): res = getClassName(metaTypeOrAppyClass, appName) - if res.find('Extensions_appyWrappers') != -1: + if res.find('_wrappers') != -1: elems = res.split('_') res = '%s%s' % (elems[1], elems[4]) if res in ('User', 'Group', 'Translation'): res = appName + res diff --git a/gen/plone25/mixins/__init__.py b/gen/mixins/__init__.py similarity index 99% rename from gen/plone25/mixins/__init__.py rename to gen/mixins/__init__.py index 5bbc0b5..e1528b2 100644 --- a/gen/plone25/mixins/__init__.py +++ b/gen/mixins/__init__.py @@ -561,9 +561,8 @@ class BaseMixin: exec 'res = %s.%s' % (moduleName, klass.__name__) # More manipulations may have occurred in m_update if hasattr(res, 'update'): - parentName = res.__bases__[-1].__name__ - moduleName = 'Products.%s.Extensions.appyWrappers' % \ - self.getTool().getAppName() + parentName= res.__bases__[-1].__name__ + moduleName= 'Products.%s.wrappers' % self.getTool().getAppName() exec 'import %s' % moduleName exec 'parent = %s.%s' % (moduleName, parentName) res.update(parent) @@ -1091,13 +1090,13 @@ class BaseMixin: # Create the dict for storing Appy wrapper on the REQUEST if needed. rq = getattr(self, 'REQUEST', None) if not rq: rq = Object() - if not hasattr(rq, 'appyWrappers'): rq.appyWrappers = {} - # Return the Appy wrapper from rq.appyWrappers if already there + if not hasattr(rq, 'wrappers'): rq.wrappers = {} + # Return the Appy wrapper from rq.wrappers if already there uid = self.UID() - if uid in rq.appyWrappers: return rq.appyWrappers[uid] - # Create the Appy wrapper, cache it in rq.appyWrappers and return it + if uid in rq.wrappers: return rq.wrappers[uid] + # Create the Appy wrapper, cache it in rq.wrappers and return it wrapper = self.wrapperClass(self) - rq.appyWrappers[uid] = wrapper + rq.wrappers[uid] = wrapper return wrapper # -------------------------------------------------------------------------- diff --git a/gen/plone25/model.py b/gen/model.py similarity index 99% rename from gen/plone25/model.py rename to gen/model.py index 206c6ae..a42045c 100644 --- a/gen/plone25/model.py +++ b/gen/model.py @@ -65,7 +65,7 @@ class ModelClass: layouts = appyType.getInputLayouts() # For the Translation class that has potentially thousands of # attributes, the most used layout is cached in a global var in - # named "tfw" in appyWrappers.py. + # named "tfw" in wrappers.py. if (klass.__name__ == 'Translation') and \ (layouts == '{"edit":"f","cell":"f","view":"f",}'): value = 'tfw' @@ -96,7 +96,7 @@ class ModelClass: @classmethod def _appy_getBody(klass): '''This method returns the code declaration of this class. We will dump - this in appyWrappers.py in the resulting product.''' + this in wrappers.py in the Zope product.''' className = klass.__name__ # Determine the name of the class and its wrapper. Because so much # attributes can be generated on a TranslationWrapper, shortcutting it diff --git a/gen/plone25/notifier.py b/gen/notifier.py similarity index 100% rename from gen/plone25/notifier.py rename to gen/notifier.py diff --git a/gen/plone25/descriptors.py b/gen/plone25/descriptors.py deleted file mode 100644 index 50bbea7..0000000 --- a/gen/plone25/descriptors.py +++ /dev/null @@ -1,599 +0,0 @@ -'''Descriptor classes defined in this file are "intermediary" classes that - gather, from the user application, information about concepts (like Archetype - classes or DC workflow definitions) that will eventually be dumped into the - generated application. Typically they have methods named "generate..." that - produce generated code.''' - -# ------------------------------------------------------------------------------ -import types, copy -from model import ModelClass, toolFieldPrefixes -import appy.gen -import appy.gen.descriptors -from appy.gen.po import PoMessage -from appy.gen import * -from appy.gen.utils import produceNiceMessage, getClassName -TABS = 4 # Number of blanks in a Python indentation. - -# ------------------------------------------------------------------------------ -class FieldDescriptor: - '''This class allows to gather information needed to generate an Archetypes - definition (field + widget) from an Appy type. An Appy type is used for - defining the type of attributes defined in the user application.''' - - singleValuedTypes = ('Integer', 'Float', 'Boolean', 'Date', 'File') - # Although Appy allows to specify a multiplicity[0]>1 for those types, it is - # not supported by Archetypes. So we will always generate single-valued type - # definitions for them. - specialParams = ('title', 'description') - - def __init__(self, fieldName, appyType, classDescriptor): - self.appyType = appyType - self.classDescr = classDescriptor - self.generator = classDescriptor.generator - self.applicationName = classDescriptor.generator.applicationName - self.fieldName = fieldName - self.fieldParams = {'name': fieldName} - self.widgetParams = {} - self.fieldType = None - self.widgetType = None - - def __repr__(self): - return '' % (self.fieldName, self.classDescr) - - def getToolFieldMessage(self, fieldName): - '''Some attributes generated on the Tool class need a specific - default message, returned by this method.''' - res = fieldName - for prefix in toolFieldPrefixes: - fullPrefix = prefix + 'For' - if fieldName.startswith(fullPrefix): - messageId = 'MSG_%s' % prefix - res = getattr(PoMessage, messageId) - if res.find('%s') != -1: - # I must complete the message with the field name. - res = res % fieldName.split('_')[-1] - break - return res - - def produceMessage(self, msgId, isLabel=True): - '''Gets the default label, description or help (depending on p_msgType) - for i18n message p_msgId.''' - default = ' ' - produceNice = False - if isLabel: - produceNice = True - default = self.fieldName - # Some attributes need a specific predefined message - if isinstance(self.classDescr, ToolClassDescriptor): - default = self.getToolFieldMessage(self.fieldName) - if default != self.fieldName: produceNice = False - msg = PoMessage(msgId, '', default) - if produceNice: - msg.produceNiceDefault() - return msg - - def walkString(self): - '''How to generate an Appy String?''' - if self.appyType.isSelect and \ - (type(self.appyType.validator) in (list, tuple)): - # Generate i18n messages for every possible value if the list - # of values is fixed. - for value in self.appyType.validator: - msgLabel = '%s_%s_list_%s' % (self.classDescr.name, - self.fieldName, value) - poMsg = PoMessage(msgLabel, '', value) - poMsg.produceNiceDefault() - self.generator.labels.append(poMsg) - - def walkAction(self): - '''Generates the i18n-related label.''' - if self.appyType.confirm: - label = '%s_%s_confirm' % (self.classDescr.name, self.fieldName) - msg = PoMessage(label, '', PoMessage.CONFIRM) - self.generator.labels.append(msg) - - def walkRef(self): - '''How to generate a Ref?''' - # Update the list of referers - self.generator.addReferer(self) - # Add the widget label for the back reference - back = self.appyType.back - refClassName = getClassName(self.appyType.klass, self.applicationName) - if back.hasLabel: - backLabel = "%s_%s" % (refClassName, self.appyType.back.attribute) - poMsg = PoMessage(backLabel, '', self.appyType.back.attribute) - poMsg.produceNiceDefault() - self.generator.labels.append(poMsg) - # Add the label for the confirm message if relevant - if self.appyType.addConfirm: - label = '%s_%s_addConfirm' % (self.classDescr.name, self.fieldName) - msg = PoMessage(label, '', PoMessage.CONFIRM) - self.generator.labels.append(msg) - - def walkPod(self): - # Add i18n-specific messages - if self.appyType.askAction: - label = '%s_%s_askaction' % (self.classDescr.name, self.fieldName) - msg = PoMessage(label, '', PoMessage.POD_ASKACTION) - self.generator.labels.append(msg) - self.classDescr.labelsToPropagate.append(msg) - # Add the POD-related fields on the Tool - self.generator.tool.addPodRelatedFields(self) - - def walkList(self): - # Add i18n-specific messages - for name, field in self.appyType.fields: - label = '%s_%s_%s' % (self.classDescr.name, self.fieldName, name) - msg = PoMessage(label, '', name) - msg.produceNiceDefault() - self.generator.labels.append(msg) - - def walkAppyType(self): - '''Walks into the Appy type definition and gathers data about the - i18n labels.''' - # Manage things common to all Appy types - # - optional ? - if self.appyType.optional: - self.generator.tool.addOptionalField(self) - # - edit default value ? - if self.appyType.editDefault: - self.generator.tool.addDefaultField(self) - # - put an index on this field? - if self.appyType.indexed and \ - (self.fieldName not in ('title', 'description')): - self.classDescr.addIndexMethod(self) - # i18n labels - messages = self.generator.labels - if not self.appyType.label: - # Create labels for generating them in i18n files, only if required. - i18nPrefix = "%s_%s" % (self.classDescr.name, self.fieldName) - if self.appyType.hasLabel: - messages.append(self.produceMessage(i18nPrefix)) - if self.appyType.hasDescr: - descrId = i18nPrefix + '_descr' - messages.append(self.produceMessage(descrId,isLabel=False)) - if self.appyType.hasHelp: - helpId = i18nPrefix + '_help' - messages.append(self.produceMessage(helpId, isLabel=False)) - # 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) - # Create i18n messages linked to groups - group = self.appyType.group - if group and not group.label: - group.generateLabels(messages, self.classDescr, set()) - # Manage things which are specific to String types - if self.appyType.type == 'String': self.walkString() - # Manage things which are specific to Actions - elif self.appyType.type == 'Action': self.walkAction() - # Manage things which are specific to Ref types - elif self.appyType.type == 'Ref': self.walkRef() - # Manage things which are specific to Pod types - elif self.appyType.type == 'Pod': self.walkPod() - # Manage things which are specific to List types - elif self.appyType.type == 'List': self.walkList() - - def generate(self): - '''Generates the i18n labels for this type.''' - self.walkAppyType() - -class ClassDescriptor(appy.gen.descriptors.ClassDescriptor): - '''Represents an Archetypes-compliant class.''' - def __init__(self, klass, orderedAttributes, generator): - appy.gen.descriptors.ClassDescriptor.__init__(self, klass, - orderedAttributes, generator) - self.methods = '' # Needed method definitions will be generated here - # We remember here encountered pages and groups defined in the Appy - # type. Indeed, after having parsed all application classes, we will - # need to generate i18n labels for every child class of the class - # that declared pages and groups. - self.labelsToPropagate = [] #~[PoMessage]~ Some labels (like page, - # group or action names) need to be propagated in children classes - # (because they contain the class name). But at this time we don't know - # yet every sub-class. So we store those labels here; the Generator - # will propagate them later. - self.toolFieldsToPropagate = [] # For this class, some fields have - # been defined on the Tool class. Those fields need to be defined - # for child classes of this class as well, but at this time we don't - # know yet every sub-class. So we store field definitions here; the - # Generator will propagate them later. - 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' - parentClass = '%s.%s' % (self.klass.__module__, self.klass.__name__) - if self.klass.__bases__: - baseClassName = self.klass.__bases__[0].__name__ - for k in allClasses: - if self.klass.__name__ == baseClassName: - parentWrapper = '%s_Wrapper' % k.name - return (parentWrapper, parentClass) - - def generateSchema(self, configClass=False): - '''Generates i18n and other related stuff for this class. If this class - is in the configuration (tool, user, etc) we must avoid having - attributes that rely on the configuration (ie attributes that are - optional, with editDefault=True, etc).''' - for attrName in self.orderedAttributes: - try: - attrValue = getattr(self.klass, attrName) - except AttributeError: - attrValue = getattr(self.modelClass, attrName) - if isinstance(attrValue, Type): - if configClass: - attrValue = copy.copy(attrValue) - attrValue.optional = False - attrValue.editDefault = False - FieldDescriptor(attrName, attrValue, self).generate() - - def isAbstract(self): - '''Is self.klass abstract?''' - res = False - if self.klass.__dict__.has_key('abstract'): - res = self.klass.__dict__['abstract'] - return res - - def isRoot(self): - '''Is self.klass root? A root class represents some kind of major - concept into the application. For example, creating instances - of such classes will be easy from the user interface.''' - res = False - if self.klass.__dict__.has_key('root'): - res = self.klass.__dict__['root'] - return res - - def isFolder(self, klass=None): - '''Must self.klass be a folder? If klass is not None, this method tests - it on p_klass instead of self.klass.''' - res = False - theClass = self.klass - if klass: - theClass = klass - if theClass.__dict__.has_key('folder'): - res = theClass.__dict__['folder'] - else: - if theClass.__bases__: - res = self.isFolder(theClass.__bases__[0]) - return res - - def getCreators(self): - '''Gets the specific creators defined for this class.''' - res = [] - if self.klass.__dict__.has_key('creators') and self.klass.creators: - for creator in self.klass.creators: - if isinstance(creator, Role): - if creator.local: - raise 'Local role "%s" cannot be used as a creator.' % \ - creator.name - res.append(creator) - else: - res.append(Role(creator)) - return res - - def getCreateMean(self, type='Import'): - '''Returns the mean for this class that corresponds to p_type, or - None if the class does not support this create mean.''' - if not self.klass.__dict__.has_key('create'): return None - else: - means = self.klass.create - if not means: return None - if not isinstance(means, tuple) and not isinstance(means, list): - means = [means] - for mean in means: - exec 'found = isinstance(mean, %s)' % type - if found: return mean - return None - - @staticmethod - def getSearches(klass): - '''Returns the list of searches that are defined on this class.''' - res = [] - if klass.__dict__.has_key('search'): - searches = klass.__dict__['search'] - if isinstance(searches, basestring): res.append(Search(searches)) - elif isinstance(searches, Search): res.append(searches) - else: - # It must be a list of searches. - for search in searches: - if isinstance(search, basestring):res.append(Search(search)) - else: res.append(search) - return res - - @staticmethod - def getSearch(klass, searchName): - '''Gets the search named p_searchName.''' - for search in ClassDescriptor.getSearches(klass): - if search.name == searchName: - return search - return None - - def addIndexMethod(self, field): - '''For indexed p_field, this method generates a method that allows to - get the value of the field as must be copied into the corresponding - index.''' - m = self.methods - spaces = TABS - n = field.fieldName - m += '\n' + ' '*spaces + 'def get%s%s(self):\n' % (n[0].upper(), n[1:]) - spaces += TABS - m += ' '*spaces + "'''Gets indexable value of field \"%s\".'''\n" % n - m += ' '*spaces + 'return self.getAppyType("%s").getIndexValue(' \ - 'self)\n' % n - self.methods = m - - def addField(self, fieldName, fieldType): - '''Adds a new field to the Tool.''' - exec "self.modelClass.%s = fieldType" % fieldName - if fieldName in self.modelClass._appy_attributes: - print 'Warning, field "%s" is already existing on class "%s"' % \ - (fieldName, self.modelClass.__name__) - return - 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.modelClass = self.klass - self.predefined = True - self.customized = False - - def getParents(self, allClasses=()): - res = ['Tool'] - if self.customized: - res.append('%s.%s' % (self.klass.__module__, self.klass.__name__)) - return res - - def update(self, klass, attributes): - '''This method is called by the generator when he finds a custom tool - definition. We must then add the custom tool elements in this default - Tool descriptor.''' - self.orderedAttributes += attributes - self.klass = klass - self.customized = True - - def isFolder(self, klass=None): return True - def isRoot(self): return False - def generateSchema(self): - ClassDescriptor.generateSchema(self, configClass=True) - - def addOptionalField(self, fieldDescr): - className = fieldDescr.classDescr.name - fieldName = 'optionalFieldsFor%s' % className - fieldType = getattr(self.modelClass, fieldName, None) - if not fieldType: - fieldType = String(multiplicity=(0,None)) - fieldType.validator = [] - self.addField(fieldName, fieldType) - fieldType.validator.append(fieldDescr.fieldName) - fieldType.page.name = 'data' - fieldType.group = Group(fieldDescr.classDescr.klass.__name__) - - def addDefaultField(self, fieldDescr): - className = fieldDescr.classDescr.name - fieldName = 'defaultValueFor%s_%s' % (className, fieldDescr.fieldName) - fieldType = fieldDescr.appyType.clone() - self.addField(fieldName, fieldType) - fieldType.page.name = 'data' - fieldType.group = Group(fieldDescr.classDescr.klass.__name__) - - def addPodRelatedFields(self, fieldDescr): - '''Adds the fields needed in the Tool for configuring a Pod field.''' - className = fieldDescr.classDescr.name - # On what page and group to display those fields ? - pg = {'page': 'documentGeneration', - 'group': Group(fieldDescr.classDescr.klass.__name__, ['50%']*2)} - # Add the field that will store the pod template. - fieldName = 'podTemplateFor%s_%s' % (className, fieldDescr.fieldName) - fieldType = File(**pg) - self.addField(fieldName, fieldType) - # Add the field that will store the output format(s) - fieldName = 'formatsFor%s_%s' % (className, fieldDescr.fieldName) - fieldType = String(validator=Selection('getPodOutputFormats'), - multiplicity=(1,None), default=('odt',), **pg) - self.addField(fieldName, fieldType) - - def addQueryResultColumns(self, classDescr): - '''Adds, for class p_classDescr, the attribute in the tool that allows - to select what default columns will be shown on query results.''' - className = classDescr.name - fieldName = 'resultColumnsFor%s' % className - fieldType = String(multiplicity=(0,None), validator=Selection( - '_appy_getAllFields*%s' % className), page='userInterface', - group=classDescr.klass.__name__) - self.addField(fieldName, fieldType) - - def addSearchRelatedFields(self, classDescr): - '''Adds, for class p_classDescr, attributes related to the search - functionality for class p_classDescr.''' - className = classDescr.name - # Field that defines if advanced search is enabled for class - # p_classDescr or not. - fieldName = 'enableAdvancedSearchFor%s' % className - fieldType = Boolean(default=True, page='userInterface', - group=classDescr.klass.__name__) - 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) - # Field that allows to select, among all indexed fields, what fields - # must really be used in the search screen. - fieldName = 'searchFieldsFor%s' % className - defaultValue = [a[0] for a in classDescr.getOrderedAppyAttributes( - condition='attrValue.indexed')] - fieldType = String(multiplicity=(0,None), validator=Selection( - '_appy_getSearchableFields*%s' % className), default=defaultValue, - page='userInterface', group=classDescr.klass.__name__) - self.addField(fieldName, fieldType) - - def addImportRelatedFields(self, classDescr): - '''Adds, for class p_classDescr, attributes related to the import - functionality for class p_classDescr.''' - className = classDescr.name - # Field that defines the path of the files to import. - fieldName = 'importPathFor%s' % className - defValue = classDescr.getCreateMean('Import').path - fieldType = String(page='data', multiplicity=(1,1), default=defValue, - group=classDescr.klass.__name__) - self.addField(fieldName, fieldType) - - def addWorkflowFields(self, classDescr): - '''Adds, for a given p_classDescr, the workflow-related fields.''' - className = classDescr.name - groupName = classDescr.klass.__name__ - # Adds a field allowing to show/hide completely any workflow-related - # information for a given class. - defaultValue = False - if classDescr.isRoot() or issubclass(classDescr.klass, ModelClass): - defaultValue = True - fieldName = 'showWorkflowFor%s' % className - fieldType = Boolean(default=defaultValue, page='userInterface', - group=groupName) - 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) - # 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 - # we are is sufficient. If this boolean is False, we simply show the - # current state. - defaultValue = False - if len(classDescr.getPhases()) > 1: - defaultValue = True - fieldName = 'showAllStatesInPhaseFor%s' % className - fieldType = Boolean(default=defaultValue, page='userInterface', - group=groupName) - self.addField(fieldName, fieldType) - -class UserClassDescriptor(ClassDescriptor): - '''Represents an Archetypes-compliant class that corresponds to the User - for the generated 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=()): - res = ['User'] - if self.customized: - res.append('%s.%s' % (self.klass.__module__, self.klass.__name__)) - return res - def update(self, klass, attributes): - '''This method is called by the generator when he finds a custom user - definition. We must then add the custom user elements in this - default User descriptor.''' - self.orderedAttributes += attributes - self.klass = klass - self.customized = True - def isFolder(self, klass=None): return False - def generateSchema(self): - ClassDescriptor.generateSchema(self, configClass=True) - -class GroupClassDescriptor(ClassDescriptor): - '''Represents the class that corresponds to the Group for the generated - 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=()): - res = ['Group'] - if self.customized: - res.append('%s.%s' % (self.klass.__module__, self.klass.__name__)) - return res - def update(self, klass, attributes): - '''This method is called by the generator when he finds a custom group - definition. We must then add the custom group elements in this - default Group descriptor. - - NOTE: currently, it is not possible to define a custom Group - class.''' - self.orderedAttributes += attributes - self.klass = klass - self.customized = 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.label, plainText=False, - page=page, show=self.modelClass.show, 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.show} - appName = self.generator.applicationName - # Scan all messages corresponding to p_messageId from all translation - # files. We will define field length from the longer found message - # 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) or \ - not i18nFiles[fileName].messagesDict.has_key(messageId): - # In this case this is not one of our Appy-managed translation - # files. - 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)) -# ------------------------------------------------------------------------------ diff --git a/gen/plone25/generator.py b/gen/plone25/generator.py deleted file mode 100644 index 34e53c7..0000000 --- a/gen/plone25/generator.py +++ /dev/null @@ -1,608 +0,0 @@ -'''This file contains the main Generator class used for generating a Zope - product.''' - -# ------------------------------------------------------------------------------ -import os, os.path, re, sys -import appy.gen -from appy.gen import * -from appy.gen.po import PoMessage, PoFile, PoParser -from appy.gen.generator import Generator as AbstractGenerator -from appy.gen.utils import getClassName -from appy.gen.descriptors import WorkflowDescriptor -from descriptors import * -from model import ModelClass, User, Group, Tool, Translation - -# ------------------------------------------------------------------------------ -class Generator(AbstractGenerator): - '''This generator generates a Plone 2.5-compliant product from a given - appy application.''' - poExtensions = ('.po', '.pot') - - def __init__(self, *args, **kwargs): - Tool._appy_clean() - AbstractGenerator.__init__(self, *args, **kwargs) - # Set our own Descriptor classes - self.descriptorClasses['class'] = ClassDescriptor - # Create our own Tool, User, Group and Translation instances - self.tool = ToolClassDescriptor(Tool, self) - self.user = UserClassDescriptor(User, self) - self.group = GroupClassDescriptor(Group, self) - self.translation = TranslationClassDescriptor(Translation, self) - # i18n labels to generate - self.labels = [] # i18n labels - self.toolInstanceName = 'portal_%s' % self.applicationName.lower() - # The following dict, pre-filled in the abstract generator, contains a - # series of replacements that need to be applied to file templates to - # generate files. - self.repls.update({'toolInstanceName': self.toolInstanceName}) - self.referers = {} - - versionRex = re.compile('(.*?\s+build)\s+(\d+)') - def initialize(self): - # Determine version number of the Plone product - self.version = '0.1 build 1' - versionTxt = os.path.join(self.outputFolder, 'version.txt') - if os.path.exists(versionTxt): - f = file(versionTxt) - oldVersion = f.read().strip() - f.close() - res = self.versionRex.search(oldVersion) - self.version = res.group(1) + ' ' + str(int(res.group(2))+1) - # Existing i18n files - self.i18nFiles = {} #~{p_fileName: PoFile}~ - # Retrieve existing i18n files if any - i18nFolder = os.path.join(self.outputFolder, 'i18n') - if os.path.exists(i18nFolder): - for fileName in os.listdir(i18nFolder): - name, ext = os.path.splitext(fileName) - if ext in self.poExtensions: - poParser = PoParser(os.path.join(i18nFolder, fileName)) - self.i18nFiles[fileName] = poParser.parse() - - def finalize(self): - # Some useful aliases - msg = PoMessage - app = self.applicationName - # Some global i18n messages - poMsg = msg(app, '', app); poMsg.produceNiceDefault() - self.labels += [poMsg, - msg('workflow_state', '', msg.WORKFLOW_STATE), - msg('appy_title', '', msg.APPY_TITLE), - msg('data_change', '', msg.DATA_CHANGE), - msg('modified_field', '', msg.MODIFIED_FIELD), - msg('previous_value', '', msg.PREVIOUS_VALUE), - msg('phase', '', msg.PHASE), - msg('root_type', '', msg.ROOT_TYPE), - msg('workflow_comment', '', msg.WORKFLOW_COMMENT), - msg('choose_a_value', '', msg.CHOOSE_A_VALUE), - msg('choose_a_doc', '', msg.CHOOSE_A_DOC), - msg('min_ref_violated', '', msg.MIN_REF_VIOLATED), - msg('max_ref_violated', '', msg.MAX_REF_VIOLATED), - msg('no_ref', '', msg.REF_NO), - msg('add_ref', '', msg.REF_ADD), - msg('ref_actions', '', msg.REF_ACTIONS), - msg('action_ok', '', msg.ACTION_OK), - msg('action_ko', '', msg.ACTION_KO), - msg('move_up', '', msg.REF_MOVE_UP), - msg('move_down', '', msg.REF_MOVE_DOWN), - msg('query_create', '', msg.QUERY_CREATE), - msg('query_import', '', msg.QUERY_IMPORT), - msg('query_no_result', '', msg.QUERY_NO_RESULT), - msg('query_consult_all', '', msg.QUERY_CONSULT_ALL), - msg('import_title', '', msg.IMPORT_TITLE), - msg('import_show_hide', '', msg.IMPORT_SHOW_HIDE), - msg('import_already', '', msg.IMPORT_ALREADY), - msg('import_many', '', msg.IMPORT_MANY), - msg('import_done', '', msg.IMPORT_DONE), - msg('search_title', '', msg.SEARCH_TITLE), - msg('search_button', '', msg.SEARCH_BUTTON), - msg('search_objects', '', msg.SEARCH_OBJECTS), - msg('search_results', '', msg.SEARCH_RESULTS), - msg('search_results_descr', '', ' '), - msg('search_new', '', msg.SEARCH_NEW), - msg('search_from', '', msg.SEARCH_FROM), - msg('search_to', '', msg.SEARCH_TO), - msg('search_or', '', msg.SEARCH_OR), - msg('search_and', '', msg.SEARCH_AND), - msg('ref_invalid_index', '', msg.REF_INVALID_INDEX), - msg('bad_long', '', msg.BAD_LONG), - msg('bad_float', '', msg.BAD_FLOAT), - msg('bad_date', '', msg.BAD_DATE), - msg('bad_email', '', msg.BAD_EMAIL), - msg('bad_url', '', msg.BAD_URL), - msg('bad_alphanumeric', '', msg.BAD_ALPHANUMERIC), - msg('bad_select_value', '', msg.BAD_SELECT_VALUE), - msg('select_delesect', '', msg.SELECT_DESELECT), - msg('no_elem_selected', '', msg.NO_SELECTION), - msg('delete_confirm', '', msg.DELETE_CONFIRM), - msg('delete_done', '', msg.DELETE_DONE), - msg('goto_first', '', msg.GOTO_FIRST), - msg('goto_previous', '', msg.GOTO_PREVIOUS), - msg('goto_next', '', msg.GOTO_NEXT), - msg('goto_last', '', msg.GOTO_LAST), - msg('goto_source', '', msg.GOTO_SOURCE), - msg('whatever', '', msg.WHATEVER), - msg('yes', '', msg.YES), - msg('no', '', msg.NO), - msg('field_required', '', msg.FIELD_REQUIRED), - msg('field_invalid', '', msg.FIELD_INVALID), - msg('file_required', '', msg.FILE_REQUIRED), - msg('image_required', '', msg.IMAGE_REQUIRED), - msg('odt', '', msg.FORMAT_ODT), - msg('pdf', '', msg.FORMAT_PDF), - msg('doc', '', msg.FORMAT_DOC), - msg('rtf', '', msg.FORMAT_RTF), - msg('front_page_text', '', msg.FRONT_PAGE_TEXT), - ] - # Create a label for every role added by this application - for role in self.getAllUsedRoles(): - self.labels.append(msg('role_%s' % role.name,'', role.name, - niceDefault=True)) - # Create basic files (config.py, Install.py, etc) - self.generateTool() - self.generateInit() - self.generateTests() - self.generateConfigureZcml() - self.copyFile('import_steps.xml', self.repls, - destFolder='profiles/default') - self.copyFile('ProfileInit.py', self.repls, destFolder='profiles', - destName='__init__.py') - # Create version.txt - f = open(os.path.join(self.outputFolder, 'version.txt'), 'w') - f.write(self.version) - f.close() - # Make Extensions and tests Python packages - for moduleFolder in ('Extensions', 'tests'): - initFile = '%s/%s/__init__.py' % (self.outputFolder, moduleFolder) - if not os.path.isfile(initFile): - f = open(initFile, 'w') - f.write('') - f.close() - # Decline i18n labels into versions for child classes - for classDescr in self.classes: - for poMsg in classDescr.labelsToPropagate: - for childDescr in classDescr.getChildren(): - childMsg = poMsg.clone(classDescr.name, childDescr.name) - if childMsg not in self.labels: - self.labels.append(childMsg) - # Generate i18n pot file - potFileName = '%s.pot' % self.applicationName - if self.i18nFiles.has_key(potFileName): - potFile = self.i18nFiles[potFileName] - else: - 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: - # I must generate (or update) a po file for the language(s) - # specified in the configuration. - poFileName = potFile.getPoFileName(language) - if self.i18nFiles.has_key(poFileName): - poFile = self.i18nFiles[poFileName] - else: - fullName = os.path.join(self.outputFolder, - 'i18n/%s' % poFileName) - poFile = PoFile(fullName) - self.i18nFiles[poFileName] = poFile - 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 - classes defined in this application. - - If p_plone is True, it keeps only Plone-standard roles; if p_plone - is False, it keeps only roles which are specific to this application; - if p_plone is None it has no effect (so it keeps both roles). - - If p_local is True, it keeps only local roles (ie, roles that can - only be granted locally); if p_local is False, it keeps only "global" - roles; if p_local is None it has no effect (so it keeps both roles). - - If p_grantable is True, it keeps only roles that the admin can - grant; if p_grantable is False, if keeps only ungrantable roles (ie - those that are implicitly granted by the system like role - "Authenticated"); if p_grantable is None it keeps both roles.''' - allRoles = {} # ~{s_roleName:Role_role}~ - # Gather roles from workflow states and transitions - for wfDescr in self.workflows: - for attr in dir(wfDescr.klass): - attrValue = getattr(wfDescr.klass, attr) - if isinstance(attrValue, State) or \ - isinstance(attrValue, Transition): - for role in attrValue.getUsedRoles(): - if role.name not in allRoles: - allRoles[role.name] = role - # Gather roles from "creators" attributes from every class - for cDescr in self.getClasses(include='all'): - for role in cDescr.getCreators(): - if role.name not in allRoles: - allRoles[role.name] = role - res = allRoles.values() - # Filter the result according to parameters - for p in ('plone', 'local', 'grantable'): - if eval(p) != None: - res = [r for r in res if eval('r.%s == %s' % (p, p))] - return res - - def addReferer(self, fieldDescr): - '''p_fieldDescr is a Ref type definition.''' - k = fieldDescr.appyType.klass - refClassName = getClassName(k, self.applicationName) - if not self.referers.has_key(refClassName): - self.referers[refClassName] = [] - self.referers[refClassName].append(fieldDescr) - - def getAppyTypePath(self, name, appyType, klass, isBack=False): - '''Gets the path to the p_appyType when a direct reference to an - appyType must be generated in a Python file.''' - if issubclass(klass, ModelClass): - res = 'wraps.%s.%s' % (klass.__name__, name) - else: - res = '%s.%s.%s' % (klass.__module__, klass.__name__, name) - if isBack: res += '.back' - return res - - 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, group, 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 - res = self.classes[:] - configClasses = [self.tool, self.user, self.group, 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 generateConfigureZcml(self): - '''Generates file configure.zcml.''' - repls = self.repls.copy() - # Note every class as "deprecated". - depr = '' - for klass in self.getClasses(include='all'): - depr += '\n' % \ - (klass.name, klass.name) - repls['deprecated'] = depr - self.copyFile('configure.zcml', repls) - - 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] - for classDescr in (classesWithCustom + self.workflows): - theImport = 'import %s' % classDescr.klass.__module__ - if theImport not in imports: - imports.append(theImport) - repls['imports'] = '\n'.join(imports) - # Compute default add roles - repls['defaultAddRoles'] = ','.join( - ['"%s"' % r for r in self.config.defaultCreators]) - # Compute list of add permissions - addPermissions = '' - for classDescr in classesAll: - addPermissions += ' "%s":"%s: Add %s",\n' % (classDescr.name, - self.applicationName, classDescr.name) - repls['addPermissions'] = addPermissions - # Compute root classes - repls['rootClasses'] = ','.join(["'%s'" % c.name \ - for c in classesButTool if c.isRoot()]) - # Compute list of class definitions - repls['appClasses'] = ','.join(['%s.%s' % (c.klass.__module__, \ - c.klass.__name__) for c in classes]) - # Compute lists of class names - repls['appClassNames'] = ','.join(['"%s"' % c.name \ - for c in classes]) - repls['allClassNames'] = ','.join(['"%s"' % c.name \ - for c in classesButTool]) - # Compute the list of ordered attributes (forward and backward, - # inherited included) for every Appy class. - attributes = [] - for classDescr in classesAll: - titleFound = False - names = [] - for name, appyType, klass in classDescr.getOrderedAppyAttributes(): - names.append(name) - if name == 'title': titleFound = True - # Add the "title" mandatory field if not found - if not titleFound: names.insert(0, 'title') - # Any backward attributes to append? - if classDescr.name in self.referers: - for field in self.referers[classDescr.name]: - names.append(field.appyType.back.attribute) - qNames = ['"%s"' % name for name in names] - attributes.append('"%s":[%s]' % (classDescr.name, ','.join(qNames))) - repls['attributes'] = ',\n '.join(attributes) - # Compute list of used roles for registering them if needed - specificRoles = self.getAllUsedRoles(plone=False) - repls['roles'] = ','.join(['"%s"' % r.name for r in specificRoles]) - globalRoles = self.getAllUsedRoles(plone=False, local=False) - repls['gRoles'] = ','.join(['"%s"' % r.name for r in globalRoles]) - grantableRoles = self.getAllUsedRoles(local=False, grantable=True) - repls['grRoles'] = ','.join(['"%s"' % r.name for r in grantableRoles]) - # Generate configuration options - repls['languages'] = ','.join('"%s"' % l for l in self.config.languages) - repls['languageSelector'] = self.config.languageSelector - repls['appFrontPage'] = bool(self.config.frontPage) - repls['sourceLanguage'] = self.config.sourceLanguage - self.copyFile('config.py', repls) - - def generateInit(self): - # Compute imports - imports = [] - classNames = [] - for c in self.getClasses(include='all'): - importDef = ' import %s' % c.name - if importDef not in imports: - imports.append(importDef) - classNames.append("%s.%s" % (c.name, c.name)) - repls = self.repls.copy() - repls['imports'] = '\n'.join(imports) - repls['classes'] = ','.join(classNames) - repls['totalNumberOfTests'] = self.totalNumberOfTests - self.copyFile('__init__.py', repls) - - def getClassesInOrder(self, allClasses): - '''When generating wrappers, classes mut be dumped in order (else, it - generates forward references in the Python file, that does not - compile).''' - res = [] # Appy class descriptors - resClasses = [] # Corresponding real Python classes - for classDescr in allClasses: - klass = classDescr.klass - if not klass.__bases__ or \ - (klass.__bases__[0].__name__ == 'ModelClass'): - # This is a root class. We dump it at the begin of the file. - res.insert(0, classDescr) - resClasses.insert(0, klass) - else: - # If a child of this class is already present, we must insert - # this klass before it. - lowestChildIndex = sys.maxint - for resClass in resClasses: - if klass in resClass.__bases__: - lowestChildIndex = min(lowestChildIndex, - resClasses.index(resClass)) - if lowestChildIndex != sys.maxint: - res.insert(lowestChildIndex, classDescr) - resClasses.insert(lowestChildIndex, klass) - else: - res.append(classDescr) - resClasses.append(klass) - return res - - def generateWrappers(self): - # We must generate imports and wrapper definitions - imports = [] - wrappers = [] - allClasses = self.getClasses(include='all') - for c in self.getClassesInOrder(allClasses): - if not c.predefined or c.customized: - moduleImport = 'import %s' % c.klass.__module__ - if moduleImport not in imports: - imports.append(moduleImport) - # Determine parent wrapper and class - parentClasses = c.getParents(allClasses) - wrapperDef = 'class %s_Wrapper(%s):\n' % \ - (c.name, ','.join(parentClasses)) - wrapperDef += ' security = ClassSecurityInfo()\n' - if c.customized: - # For custom tool, add a call to a method that allows to - # customize elements from the base class. - wrapperDef += " if hasattr(%s, 'update'):\n " \ - "%s.update(%s)\n" % (parentClasses[1], parentClasses[1], - parentClasses[0]) - # For custom tool, add security declaration that will allow to - # call their methods from ZPTs. - for parentClass in parentClasses: - wrapperDef += " for elem in dir(%s):\n " \ - "if not elem.startswith('_'): security.declarePublic" \ - "(elem)\n" % (parentClass) - # Register the class in Zope. - wrapperDef += 'InitializeClass(%s_Wrapper)\n' % c.name - wrappers.append(wrapperDef) - repls = self.repls.copy() - repls['imports'] = '\n'.join(imports) - repls['wrappers'] = '\n'.join(wrappers) - 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): - '''Generates the file needed for executing tests.''' - repls = self.repls.copy() - modules = self.modulesWithTests - repls['imports'] = '\n'.join(['import %s' % m for m in modules]) - repls['modulesWithTests'] = ','.join(modules) - self.copyFile('testAll.py', repls, destFolder='tests') - - def generateTool(self): - '''Generates the Plone tool that corresponds to this application.''' - Msg = PoMessage - # Create Tool-related i18n-related messages - msg = Msg(self.tool.name, '', Msg.CONFIG % self.applicationName) - self.labels.append(msg) - - # Tune the Ref field between Tool->User and Group->User - Tool.users.klass = User - if self.user.customized: - Tool.users.klass = self.user.klass - Group.users.klass = self.user.klass - - # Generate the Tool-related classes (User, Group, Translation) - for klass in (self.user, self.group, self.translation): - klassType = klass.name[len(self.applicationName):] - klass.generateSchema() - self.labels += [ Msg(klass.name, '', klassType), - Msg('%s_plural' % klass.name,'', klass.name+'s')] - repls = self.repls.copy() - repls.update({'methods': klass.methods, 'genClassName': klass.name, - 'baseMixin':'BaseMixin', 'parents': 'BaseMixin, SimpleItem', - 'classDoc': 'Standard Appy class', 'icon':'object.gif'}) - 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. - for classDescr in self.getClasses(include='allButTool'): - for fieldName, fieldType in classDescr.toolFieldsToPropagate: - for childDescr in classDescr.getChildren(): - childFieldName = fieldName % childDescr.name - fieldType.group = childDescr.klass.__name__ - self.tool.addField(childFieldName, fieldType) - if classDescr.isRoot(): - # We must be able to configure query results from the tool. - self.tool.addQueryResultColumns(classDescr) - # Add the search-related fields. - self.tool.addSearchRelatedFields(classDescr) - importMean = classDescr.getCreateMean('Import') - if importMean: - self.tool.addImportRelatedFields(classDescr) - self.tool.addWorkflowFields(self.user) - self.tool.generateSchema() - - # Generate the Tool class - repls = self.repls.copy() - repls.update({'methods': self.tool.methods, - 'genClassName': self.tool.name, 'baseMixin':'ToolMixin', - 'parents': 'ToolMixin, Folder', 'icon': 'folder.gif', - 'classDoc': 'Tool class for %s' % 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 - generating the corresponding Archetype class.''' - k = classDescr.klass - print 'Generating %s.%s (gen-class)...' % (k.__module__, k.__name__) - if not classDescr.isAbstract(): - self.tool.addWorkflowFields(classDescr) - # Determine base Zope class - isFolder = classDescr.isFolder() - baseClass = isFolder and 'Folder' or 'SimpleItem' - icon = isFolder and 'folder.gif' or 'object.gif' - parents = 'BaseMixin, %s' % baseClass - classDoc = classDescr.klass.__doc__ or 'Appy class.' - repls = self.repls.copy() - classDescr.generateSchema() - repls.update({ - 'parents': parents, 'className': classDescr.klass.__name__, - 'genClassName': classDescr.name, 'baseMixin':'BaseMixin', - 'classDoc': classDoc, 'applicationName': self.applicationName, - 'methods': classDescr.methods, 'icon':icon}) - fileName = '%s.py' % classDescr.name - # Create i18n labels (class name and plural form) - poMsg = PoMessage(classDescr.name, '', classDescr.klass.__name__) - poMsg.produceNiceDefault() - self.labels.append(poMsg) - poMsgPl = PoMessage('%s_plural' % classDescr.name, '', - classDescr.klass.__name__+'s') - poMsgPl.produceNiceDefault() - self.labels.append(poMsgPl) - # Create i18n labels for searches - for search in classDescr.getSearches(classDescr.klass): - searchLabel = '%s_search_%s' % (classDescr.name, search.name) - labels = [searchLabel, '%s_descr' % searchLabel] - if search.group: - grpLabel = '%s_searchgroup_%s' % (classDescr.name, search.group) - labels += [grpLabel, '%s_descr' % grpLabel] - for label in labels: - default = ' ' - if label == searchLabel: default = search.name - poMsg = PoMessage(label, '', default) - poMsg.produceNiceDefault() - if poMsg not in self.labels: - self.labels.append(poMsg) - # Generate the resulting Archetypes class. - self.copyFile('Class.py', repls, destName=fileName) - - def generateWorkflow(self, wfDescr): - '''This method creates the i18n labels related to the workflow described - by p_wfDescr.''' - k = wfDescr.klass - print 'Generating %s.%s (gen-workflow)...' % (k.__module__, k.__name__) - # Identify workflow name - wfName = WorkflowDescriptor.getWorkflowName(wfDescr.klass) - # Add i18n messages for states - for name in dir(wfDescr.klass): - if not isinstance(getattr(wfDescr.klass, name), State): continue - poMsg = PoMessage('%s_%s' % (wfName, name), '', name) - poMsg.produceNiceDefault() - self.labels.append(poMsg) - # Add i18n messages for transitions - for name in dir(wfDescr.klass): - transition = getattr(wfDescr.klass, name) - if not isinstance(transition, Transition): continue - poMsg = PoMessage('%s_%s' % (wfName, name), '', name) - poMsg.produceNiceDefault() - self.labels.append(poMsg) - if transition.confirm: - # We need to generate a label for the message that will be shown - # in the confirm popup. - label = '%s_%s_confirm' % (wfName, name) - poMsg = PoMessage(label, '', PoMessage.CONFIRM) - self.labels.append(poMsg) - if transition.notify: - # Appy will send a mail when this transition is triggered. - # So we need 2 i18n labels: one for the mail subject and one for - # the mail body. - subjectLabel = '%s_%s_mail_subject' % (wfName, name) - poMsg = PoMessage(subjectLabel, '', PoMessage.EMAIL_SUBJECT) - self.labels.append(poMsg) - bodyLabel = '%s_%s_mail_body' % (wfName, name) - poMsg = PoMessage(bodyLabel, '', PoMessage.EMAIL_BODY) - self.labels.append(poMsg) -# ------------------------------------------------------------------------------ diff --git a/gen/plone25/templates/ProfileInit.py b/gen/plone25/templates/ProfileInit.py deleted file mode 100644 index 5bd4d25..0000000 --- a/gen/plone25/templates/ProfileInit.py +++ /dev/null @@ -1,19 +0,0 @@ -# ------------------------------------------------------------------------------ -from Products.CMFCore.utils import getToolByName - -# ------------------------------------------------------------------------------ -def installProduct(context): - '''Installs the necessary products for Appy.''' - portal = context.getSite() - qi = getToolByName(portal, 'portal_quickinstaller') - if not qi.isProductInstalled('PloneLanguageTool'): - qi.installProduct('PloneLanguageTool') - if not qi.isProductInstalled(''): - qi.installProduct('') - return " installed." - -# ------------------------------------------------------------------------------ -def install_default(context): - # Installation function of default profile. - installProduct(context) -# ------------------------------------------------------------------------------ diff --git a/gen/plone25/templates/frontPage.pt b/gen/plone25/templates/frontPage.pt deleted file mode 100644 index ec7a5ed..0000000 --- a/gen/plone25/templates/frontPage.pt +++ /dev/null @@ -1,11 +0,0 @@ - - - - -
- -
- - diff --git a/gen/plone25/templates/frontPageAppy.pt b/gen/plone25/templates/frontPageAppy.pt deleted file mode 100644 index d0177d9..0000000 --- a/gen/plone25/templates/frontPageAppy.pt +++ /dev/null @@ -1,5 +0,0 @@ - - -
- -
diff --git a/gen/plone25/templates/import_steps.xml b/gen/plone25/templates/import_steps.xml deleted file mode 100644 index 522c39f..0000000 --- a/gen/plone25/templates/import_steps.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - Product : installation. - - diff --git a/gen/plone25/templates/Class.py b/gen/templates/Class.py similarity index 94% rename from gen/plone25/templates/Class.py rename to gen/templates/Class.py index 3ee79ac..2ec361c 100644 --- a/gen/plone25/templates/Class.py +++ b/gen/templates/Class.py @@ -6,7 +6,7 @@ from AccessControl import ClassSecurityInfo import Products..config as cfg from appy.gen.plone25.mixins import BaseMixin from appy.gen.plone25.mixins.ToolMixin import ToolMixin -from Extensions.appyWrappers import _Wrapper as Wrapper +from wrappers import _Wrapper as Wrapper def manage_add(self, id, title='', REQUEST=None): '''Creates instances of this class.''' diff --git a/gen/plone25/templates/Styles.css.dtml b/gen/templates/Styles.css.dtml similarity index 100% rename from gen/plone25/templates/Styles.css.dtml rename to gen/templates/Styles.css.dtml diff --git a/gen/plone25/templates/__init__.py b/gen/templates/__init__.py similarity index 100% rename from gen/plone25/templates/__init__.py rename to gen/templates/__init__.py diff --git a/gen/plone25/templates/config.py b/gen/templates/config.py similarity index 96% rename from gen/plone25/templates/config.py rename to gen/templates/config.py index e519a12..4d56cc9 100644 --- a/gen/plone25/templates/config.py +++ b/gen/templates/config.py @@ -1,7 +1,7 @@ import os, os.path, sys, copy import appy.gen -import Extensions.appyWrappers as wraps +import wrappers # The following imports are here for allowing mixin classes to access those @@ -47,6 +47,5 @@ grantableRoles = [] # Configuration options languages = [] languageSelector = -appFrontPage = sourceLanguage = '' # ------------------------------------------------------------------------------ diff --git a/gen/plone25/templates/configure.zcml b/gen/templates/configure.zcml similarity index 100% rename from gen/plone25/templates/configure.zcml rename to gen/templates/configure.zcml diff --git a/gen/plone25/templates/testAll.py b/gen/templates/testAll.py similarity index 100% rename from gen/plone25/templates/testAll.py rename to gen/templates/testAll.py diff --git a/gen/plone25/templates/appyWrappers.py b/gen/templates/wrappers.py similarity index 100% rename from gen/plone25/templates/appyWrappers.py rename to gen/templates/wrappers.py diff --git a/gen/ui/page.pt b/gen/ui/page.pt index 18d4093..88033e2 100644 --- a/gen/ui/page.pt +++ b/gen/ui/page.pt @@ -84,12 +84,13 @@ tal:attributes="class python:test(odd, 'even', 'odd')" valign="top"> + tal:content="python: tool.translate(contextObj.getWorkflowLabel(event['action']))"/> - - - + + + + No comment. + Display the previous values of the fields whose value were modified in this change. diff --git a/gen/utils.py b/gen/utils.py index 4613c10..94872fd 100644 --- a/gen/utils.py +++ b/gen/utils.py @@ -323,11 +323,11 @@ class FileWrapper: # ------------------------------------------------------------------------------ def getClassName(klass, appName=None): '''Generates, from appy-class p_klass, the name of the corresponding - Archetypes class. For some classes, name p_appName is required: it is + Zope class. For some classes, name p_appName is required: it is part of the class name.''' moduleName = klass.__module__ if (moduleName == 'appy.gen.plone25.model') or \ - moduleName.endswith('.appyWrappers'): + moduleName.endswith('.wrappers'): # This is a model (generation time or run time) res = appName + klass.__name__ elif klass.__bases__ and (klass.__bases__[-1].__module__ == 'appy.gen'): diff --git a/gen/plone25/wrappers/GroupWrapper.py b/gen/wrappers/GroupWrapper.py similarity index 100% rename from gen/plone25/wrappers/GroupWrapper.py rename to gen/wrappers/GroupWrapper.py diff --git a/gen/plone25/wrappers/ToolWrapper.py b/gen/wrappers/ToolWrapper.py similarity index 100% rename from gen/plone25/wrappers/ToolWrapper.py rename to gen/wrappers/ToolWrapper.py diff --git a/gen/plone25/wrappers/TranslationWrapper.py b/gen/wrappers/TranslationWrapper.py similarity index 100% rename from gen/plone25/wrappers/TranslationWrapper.py rename to gen/wrappers/TranslationWrapper.py diff --git a/gen/plone25/wrappers/UserWrapper.py b/gen/wrappers/UserWrapper.py similarity index 100% rename from gen/plone25/wrappers/UserWrapper.py rename to gen/wrappers/UserWrapper.py diff --git a/gen/plone25/wrappers/__init__.py b/gen/wrappers/__init__.py similarity index 100% rename from gen/plone25/wrappers/__init__.py rename to gen/wrappers/__init__.py