appy.gen: refactoring due to De-Plonization.
This commit is contained in:
parent
6733f4c7dc
commit
d934f49a99
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
603
gen/generator.py
603
gen/generator.py
|
@ -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)
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
|
@ -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
|
|
@ -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))
|
|
||||||
# ------------------------------------------------------------------------------
|
|
|
@ -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)
|
|
||||||
# ------------------------------------------------------------------------------
|
|
|
@ -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)
|
|
||||||
# ------------------------------------------------------------------------------
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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.'''
|
|
@ -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!>'
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
|
@ -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>
|
||||||
|
|
|
@ -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'):
|
||||||
|
|
Loading…
Reference in a new issue