appy.gen: refactoring due to De-Plonization.

This commit is contained in:
Gaetan Delannay 2011-12-05 10:52:18 +01:00
parent 6733f4c7dc
commit d934f49a99
31 changed files with 1220 additions and 1325 deletions

View file

@ -9,7 +9,7 @@ import appy.version
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
ERROR_CODE = 1 ERROR_CODE = 1
VALID_PRODUCT_TYPES = ('plone25', 'odt') VALID_PRODUCT_TYPES = ('zope', 'odt')
APP_NOT_FOUND = 'Application not found at %s.' APP_NOT_FOUND = 'Application not found at %s.'
WRONG_NG_OF_ARGS = 'Wrong number of arguments.' WRONG_NG_OF_ARGS = 'Wrong number of arguments.'
WRONG_OUTPUT_FOLDER = 'Output folder not found. Please create it first.' 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: class GeneratorScript:
'''usage: %prog [options] app productType outputFolder '''usage: %prog [options] app productType outputFolder
"app" is the path to your Appy application, which may be a "app" is the path to your Appy application, which must be a
Python module (= a file than ends with .py) or a Python Python package (= a folder containing a file named
package (= a folder containing a file named __init__.py). __init__.py). Your app may reside anywhere, but needs to
Your app may reside anywhere (but it needs to be be accessible by Zope. Typically, it may be or symlinked
accessible by the underlying application server, ie Zope), in <yourZopeInstance>/lib/python, but not within the
excepted within the generated product. Typically, if you generated product, stored or symlinked in
generate a Plone product, it may reside within <yourZopeInstance>/Products.
<yourZopeInstance>/lib/python, but not within the
generated product (typically stored in
<yourZopeInstance>/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.
Example: generating a Plone product "productType" is the kind of product you want to generate. "zope" is
----------------------------------- the only available production-ready target.
In your Zope instance named myZopeInstance, create a folder "odt" is experimental.
"myZopeInstance/lib/python/MyApp". Create into it your Appy application
(we suppose here that it is a Python package, containing a __init__.py "outputFolder" is the folder where the Zope product will be generated.
file and other files). Then, chdir into this folder and type For example, if you develop your application in
"python <appyPath>/gen/generator.py . plone25 ../../../Products" and the /home/gdy/MyProject/MyProject, you typically specify
product will be generated in myZopeInstance/Products/MyApp. "/home/gdy/MyProject/zope" as outputFolder.
"python" must refer to a Python interpreter that knows package appy.''' '''
def generateProduct(self, options, application, productType, 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() Generator(application, outputFolder, options).run()
def manageArgs(self, parser, options, args): def manageArgs(self, parser, options, args):

View file

@ -2712,13 +2712,6 @@ class Config:
# People having one of these roles will be able to create instances # People having one of these roles will be able to create instances
# of classes defined in your application. # of classes defined in your application.
self.defaultCreators = ['Manager', 'Owner'] 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 # Number of translations for every page on a Translation object
self.translationsPerPage = 30 self.translationsPerPage = 30
# Language that will be used as a basis for translating to other # Language that will be used as a basis for translating to other

View file

@ -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 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 class Descriptor: # Abstract
@ -16,6 +25,32 @@ class Descriptor: # Abstract
class ClassDescriptor(Descriptor): class ClassDescriptor(Descriptor):
'''This class gives information about an Appy class.''' '''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): def getOrderedAppyAttributes(self, condition=None):
'''Returns the appy types for all attributes of this class and parent '''Returns the appy types for all attributes of this class and parent
class(es). If a p_condition is specified, ony Appy types matching 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) self.pages.append(appyType.page.name)
return self.pages 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): class WorkflowDescriptor(Descriptor):
'''This class gives information about an Appy workflow.''' '''This class gives information about an Appy workflow.'''
@staticmethod @staticmethod
@ -94,4 +262,428 @@ class WorkflowDescriptor(Descriptor):
'''Returns the name of this workflow.''' '''Returns the name of this workflow.'''
res = klass.__module__.replace('.', '_') + '_' + klass.__name__ res = klass.__module__.replace('.', '_') + '_' + klass.__name__
return res.lower() 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 '<Field %s, %s>' % (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('<br/>')
height = max(height, mHeight)
if height < 1:
# This is a one-line field.
params['width'] = width
else:
# This is a multi-line field, or a very-long-single-lined field
params['format'] = String.TEXT
params['height'] = height
self.addField(messageId, String(**params))
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -1,10 +1,12 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import os, os.path, sys, parser, symbol, token, types import os, os.path, re, 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 appy.pod, appy.pod.renderer import appy.pod, appy.pod.renderer
from appy.shared.utils import FolderDeleter 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 class GeneratorError(Exception): pass
@ -91,8 +93,6 @@ class Ast:
self.classes[astClass.name] = astClass 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 -*- CODE_HEADER = '''# -*- coding: utf-8 -*-
# #
# GNU General Public License (GPL) # GNU General Public License (GPL)
@ -119,17 +119,12 @@ class Generator:
self.application = application self.application = application
# Determine application name # Determine application name
self.applicationName = os.path.basename(application) self.applicationName = os.path.basename(application)
if application.endswith('.py'):
self.applicationName = self.applicationName[:-3]
# Determine output folder (where to store the generated product) # Determine output folder (where to store the generated product)
self.outputFolder = '%s/%s' % (outputFolder, self.applicationName) self.outputFolder = outputFolder
self.options = options self.options = options
# Determine templates folder # Determine templates folder
exec 'import %s as genModule' % self.__class__.__module__ genFolder = os.path.dirname(__file__)
self.templatesFolder = os.path.join(os.path.dirname(genModule.__file__), self.templatesFolder = os.path.join(genFolder, 'templates')
'templates')
if not os.path.exists(self.templatesFolder):
print WARN_NO_TEMPLATE % self.templatesFolder
# Default descriptor classes # Default descriptor classes
self.descriptorClasses = { self.descriptorClasses = {
'class': ClassDescriptor, 'tool': ClassDescriptor, 'class': ClassDescriptor, 'tool': ClassDescriptor,
@ -273,8 +268,6 @@ class Generator:
sys.path.append(containingFolder) sys.path.append(containingFolder)
# What is the name of the application ? # What is the name of the application ?
appName = os.path.basename(self.application) appName = os.path.basename(self.application)
if os.path.isfile(self.application):
appName = os.path.splitext(appName)[0]
self.walkModule(appName) self.walkModule(appName)
sys.path.pop() sys.path.pop()
@ -342,4 +335,582 @@ class Generator:
if self.totalNumberOfTests: if self.totalNumberOfTests:
msg = ' (number of tests found: %d)' % self.totalNumberOfTests msg = ' (number of tests found: %d)' % self.totalNumberOfTests
print 'Done%s.' % msg 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 += '<five:deprecatedManageAddDelete class=".%s.%s"/>\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)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -27,7 +27,7 @@ class ToolMixin(BaseMixin):
res = metaTypeOrAppyClass res = metaTypeOrAppyClass
if not isinstance(metaTypeOrAppyClass, basestring): if not isinstance(metaTypeOrAppyClass, basestring):
res = getClassName(metaTypeOrAppyClass, appName) res = getClassName(metaTypeOrAppyClass, appName)
if res.find('Extensions_appyWrappers') != -1: if res.find('_wrappers') != -1:
elems = res.split('_') elems = res.split('_')
res = '%s%s' % (elems[1], elems[4]) res = '%s%s' % (elems[1], elems[4])
if res in ('User', 'Group', 'Translation'): res = appName + res if res in ('User', 'Group', 'Translation'): res = appName + res

View file

@ -561,9 +561,8 @@ class BaseMixin:
exec 'res = %s.%s' % (moduleName, klass.__name__) exec 'res = %s.%s' % (moduleName, klass.__name__)
# More manipulations may have occurred in m_update # More manipulations may have occurred in m_update
if hasattr(res, 'update'): if hasattr(res, 'update'):
parentName = res.__bases__[-1].__name__ parentName= res.__bases__[-1].__name__
moduleName = 'Products.%s.Extensions.appyWrappers' % \ moduleName= 'Products.%s.wrappers' % self.getTool().getAppName()
self.getTool().getAppName()
exec 'import %s' % moduleName exec 'import %s' % moduleName
exec 'parent = %s.%s' % (moduleName, parentName) exec 'parent = %s.%s' % (moduleName, parentName)
res.update(parent) res.update(parent)
@ -1091,13 +1090,13 @@ class BaseMixin:
# Create the dict for storing Appy wrapper on the REQUEST if needed. # Create the dict for storing Appy wrapper on the REQUEST if needed.
rq = getattr(self, 'REQUEST', None) rq = getattr(self, 'REQUEST', None)
if not rq: rq = Object() if not rq: rq = Object()
if not hasattr(rq, 'appyWrappers'): rq.appyWrappers = {} if not hasattr(rq, 'wrappers'): rq.wrappers = {}
# Return the Appy wrapper from rq.appyWrappers if already there # Return the Appy wrapper from rq.wrappers if already there
uid = self.UID() uid = self.UID()
if uid in rq.appyWrappers: return rq.appyWrappers[uid] if uid in rq.wrappers: return rq.wrappers[uid]
# Create the Appy wrapper, cache it in rq.appyWrappers and return it # Create the Appy wrapper, cache it in rq.wrappers and return it
wrapper = self.wrapperClass(self) wrapper = self.wrapperClass(self)
rq.appyWrappers[uid] = wrapper rq.wrappers[uid] = wrapper
return wrapper return wrapper
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------

View file

@ -65,7 +65,7 @@ class ModelClass:
layouts = appyType.getInputLayouts() layouts = appyType.getInputLayouts()
# For the Translation class that has potentially thousands of # For the Translation class that has potentially thousands of
# attributes, the most used layout is cached in a global var in # 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 \ if (klass.__name__ == 'Translation') and \
(layouts == '{"edit":"f","cell":"f","view":"f",}'): (layouts == '{"edit":"f","cell":"f","view":"f",}'):
value = 'tfw' value = 'tfw'
@ -96,7 +96,7 @@ class ModelClass:
@classmethod @classmethod
def _appy_getBody(klass): def _appy_getBody(klass):
'''This method returns the code declaration of this class. We will dump '''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__ className = klass.__name__
# Determine the name of the class and its wrapper. Because so much # Determine the name of the class and its wrapper. Because so much
# attributes can be generated on a TranslationWrapper, shortcutting it # attributes can be generated on a TranslationWrapper, shortcutting it

View file

@ -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 '<Field %s, %s>' % (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('<br/>')
height = max(height, mHeight)
if height < 1:
# This is a one-line field.
params['width'] = width
else:
# This is a multi-line field, or a very-long-single-lined field
params['format'] = String.TEXT
params['height'] = height
self.addField(messageId, String(**params))
# ------------------------------------------------------------------------------

View file

@ -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 += '<five:deprecatedManageAddDelete class=".%s.%s"/>\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)
# ------------------------------------------------------------------------------

View file

@ -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('<!applicationName!>'):
qi.installProduct('<!applicationName!>')
return "<!applicationName!> installed."
# ------------------------------------------------------------------------------
def install_default(context):
# Installation function of default profile.
installProduct(context)
# ------------------------------------------------------------------------------

View file

@ -1,11 +0,0 @@
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"
metal:use-macro="here/main_template/macros/master" i18n:domain="<!applicationName!>">
<metal:disable fill-slot="top_slot"
tal:define="dummy python:request.set('disable_border',1)" />
<body>
<div metal:fill-slot="main" tal:define="tool python: context.<!toolInstanceName!>">
<!pageContent!>
</div>
</body>
</html>

View file

@ -1,5 +0,0 @@
<tal:main define="tool python: context.<!toolInstanceName!>">
<html metal:use-macro="context/ui/template/macros/main">
<div metal:fill-slot="content"><!pageContent!></div>
</html>
</tal:main>

View file

@ -1,8 +0,0 @@
<?xml version="1.0"?>
<import-steps>
<import-step id="install-product-<!applicationName!>" version="1.0"
handler="Products.<!applicationName!>.profiles.install_default"
title="Product <!applicationName!>: installation.">
Product <!applicationName!>: installation.
</import-step>
</import-steps>

View file

@ -6,7 +6,7 @@ from AccessControl import ClassSecurityInfo
import Products.<!applicationName!>.config as cfg import Products.<!applicationName!>.config as cfg
from appy.gen.plone25.mixins import BaseMixin from appy.gen.plone25.mixins import BaseMixin
from appy.gen.plone25.mixins.ToolMixin import ToolMixin from appy.gen.plone25.mixins.ToolMixin import ToolMixin
from Extensions.appyWrappers import <!genClassName!>_Wrapper as Wrapper from wrappers import <!genClassName!>_Wrapper as Wrapper
def manage_add<!genClassName!>(self, id, title='', REQUEST=None): def manage_add<!genClassName!>(self, id, title='', REQUEST=None):
'''Creates instances of this class.''' '''Creates instances of this class.'''

View file

@ -1,7 +1,7 @@
<!codeHeader!> <!codeHeader!>
import os, os.path, sys, copy import os, os.path, sys, copy
import appy.gen import appy.gen
import Extensions.appyWrappers as wraps import wrappers
<!imports!> <!imports!>
# The following imports are here for allowing mixin classes to access those # The following imports are here for allowing mixin classes to access those
@ -47,6 +47,5 @@ grantableRoles = [<!grRoles!>]
# Configuration options # Configuration options
languages = [<!languages!>] languages = [<!languages!>]
languageSelector = <!languageSelector!> languageSelector = <!languageSelector!>
appFrontPage = <!appFrontPage!>
sourceLanguage = '<!sourceLanguage!>' sourceLanguage = '<!sourceLanguage!>'
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -84,12 +84,13 @@
tal:attributes="class python:test(odd, 'even', 'odd')" valign="top"> tal:attributes="class python:test(odd, 'even', 'odd')" valign="top">
<td tal:condition="isDataChange" tal:content="python: tool.translate('data_change')"></td> <td tal:condition="isDataChange" tal:content="python: tool.translate('data_change')"></td>
<td tal:condition="not: isDataChange" <td tal:condition="not: isDataChange"
tal:content="python: tool.translate(contextObj.getWorkflowLabel(event['action']))" tal:content="python: tool.translate(contextObj.getWorkflowLabel(event['action']))"/>
tal:attributes="class string:state-${state}"/>
<td tal:define="actorid python:event.get('actor');" tal:content="actorid"/> <td tal:define="actorid python:event.get('actor');" tal:content="actorid"/>
<td tal:content="python:contextObj.restrictedTraverse('@@plone').toLocalizedTime(event['time'],long_format=True)"/> <td tal:content="event/time"/>
<td tal:condition="not: isDataChange"><tal:comment condition="rhComments" tal:content="structure rhComments"/> <td tal:condition="not: isDataChange">
<tal:noComment condition="not: rhComments" i18n:translate="no_comments" i18n:domain="plone"/></td> <tal:comment condition="rhComments" tal:content="structure rhComments"/>
<span tal:condition="not: rhComments">No comment.</span>
</td>
<td tal:condition="isDataChange"> <td tal:condition="isDataChange">
<tal:comment replace="nothing"> <tal:comment replace="nothing">
Display the previous values of the fields whose value were modified in this change.</tal:comment> Display the previous values of the fields whose value were modified in this change.</tal:comment>

View file

@ -323,11 +323,11 @@ class FileWrapper:
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
def getClassName(klass, appName=None): def getClassName(klass, appName=None):
'''Generates, from appy-class p_klass, the name of the corresponding '''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.''' part of the class name.'''
moduleName = klass.__module__ moduleName = klass.__module__
if (moduleName == 'appy.gen.plone25.model') or \ if (moduleName == 'appy.gen.plone25.model') or \
moduleName.endswith('.appyWrappers'): moduleName.endswith('.wrappers'):
# This is a model (generation time or run time) # This is a model (generation time or run time)
res = appName + klass.__name__ res = appName + klass.__name__
elif klass.__bases__ and (klass.__bases__[-1].__module__ == 'appy.gen'): elif klass.__bases__ and (klass.__bases__[-1].__module__ == 'appy.gen'):