2009-06-29 07:06:01 -05:00
|
|
|
# ------------------------------------------------------------------------------
|
2011-12-05 03:52:18 -06:00
|
|
|
import os, os.path, re, sys, parser, symbol, token, types
|
2013-10-20 11:12:39 -05:00
|
|
|
import appy, appy.pod.renderer
|
2009-06-29 07:06:01 -05:00
|
|
|
from appy.shared.utils import FolderDeleter
|
2011-12-05 08:11:29 -06:00
|
|
|
import appy.gen as gen
|
2012-11-06 04:32:39 -06:00
|
|
|
import po
|
2011-12-05 03:52:18 -06:00
|
|
|
from descriptors import *
|
2012-11-06 04:32:39 -06:00
|
|
|
from utils import getClassName
|
2012-03-26 12:09:45 -05:00
|
|
|
from model import ModelClass, User, Group, Tool, Translation, Page
|
2009-06-29 07:06:01 -05:00
|
|
|
|
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
class GeneratorError(Exception): pass
|
|
|
|
|
|
|
|
# I need the following classes to parse Python classes and find in which
|
|
|
|
# order the attributes are defined. --------------------------------------------
|
|
|
|
class AstMatcher:
|
|
|
|
'''Allows to find a given pattern within an ast (part).'''
|
|
|
|
def _match(pattern, node):
|
|
|
|
res = None
|
|
|
|
if pattern[0] == node[0]:
|
|
|
|
# This level matches
|
|
|
|
if len(pattern) == 1:
|
|
|
|
return node
|
|
|
|
else:
|
|
|
|
if type(node[1]) == tuple:
|
|
|
|
return AstMatcher._match(pattern[1:], node[1])
|
|
|
|
return res
|
|
|
|
_match = staticmethod(_match)
|
|
|
|
def match(pattern, node):
|
|
|
|
res = []
|
|
|
|
for subNode in node[1:]:
|
|
|
|
# Do I find the pattern among the subnodes ?
|
|
|
|
occurrence = AstMatcher._match(pattern, subNode)
|
|
|
|
if occurrence:
|
|
|
|
res.append(occurrence)
|
|
|
|
return res
|
|
|
|
match = staticmethod(match)
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
class AstClass:
|
|
|
|
'''Python class.'''
|
|
|
|
def __init__(self, node):
|
|
|
|
# Link to the Python ast node
|
|
|
|
self.node = node
|
|
|
|
self.name = node[2][1]
|
|
|
|
self.attributes = [] # We are only interested in parsing static
|
|
|
|
# attributes to now their order
|
|
|
|
if sys.version_info[:2] >= (2,5):
|
|
|
|
self.statementPattern = (
|
|
|
|
symbol.stmt, symbol.simple_stmt, symbol.small_stmt,
|
|
|
|
symbol.expr_stmt, symbol.testlist, symbol.test, symbol.or_test,
|
|
|
|
symbol.and_test, symbol.not_test, symbol.comparison, symbol.expr,
|
|
|
|
symbol.xor_expr, symbol.and_expr, symbol.shift_expr,
|
|
|
|
symbol.arith_expr, symbol.term, symbol.factor, symbol.power)
|
|
|
|
else:
|
|
|
|
self.statementPattern = (
|
|
|
|
symbol.stmt, symbol.simple_stmt, symbol.small_stmt,
|
|
|
|
symbol.expr_stmt, symbol.testlist, symbol.test, symbol.and_test,
|
|
|
|
symbol.not_test, symbol.comparison, symbol.expr, symbol.xor_expr,
|
|
|
|
symbol.and_expr, symbol.shift_expr, symbol.arith_expr,
|
|
|
|
symbol.term, symbol.factor, symbol.power)
|
|
|
|
for subNode in node[1:]:
|
|
|
|
if subNode[0] == symbol.suite:
|
|
|
|
# We are in the class body
|
|
|
|
self.getStaticAttributes(subNode)
|
|
|
|
|
|
|
|
def getStaticAttributes(self, classBody):
|
|
|
|
statements = AstMatcher.match(self.statementPattern, classBody)
|
|
|
|
for statement in statements:
|
|
|
|
if len(statement) == 2 and statement[1][0] == symbol.atom and \
|
|
|
|
statement[1][1][0] == token.NAME:
|
|
|
|
attrName = statement[1][1][1]
|
|
|
|
self.attributes.append(attrName)
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return '<class %s has attrs %s>' % (self.name, str(self.attributes))
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
class Ast:
|
|
|
|
'''Python AST.'''
|
|
|
|
classPattern = (symbol.stmt, symbol.compound_stmt, symbol.classdef)
|
2012-05-31 10:29:06 -05:00
|
|
|
utf8prologue = '# -*- coding: utf-8 -*-'
|
2009-06-29 07:06:01 -05:00
|
|
|
def __init__(self, pyFile):
|
|
|
|
f = file(pyFile)
|
|
|
|
fContent = f.read()
|
|
|
|
f.close()
|
2012-05-31 10:29:06 -05:00
|
|
|
# For some unknown reason, when an UTF-8 encoding is declared, parsing
|
|
|
|
# does not work.
|
|
|
|
if fContent.startswith(self.utf8prologue):
|
|
|
|
fContent = fContent[len(self.utf8prologue):]
|
2009-06-29 07:06:01 -05:00
|
|
|
fContent = fContent.replace('\r', '')
|
|
|
|
ast = parser.suite(fContent).totuple()
|
|
|
|
# Get all the classes defined within this module.
|
|
|
|
self.classes = {}
|
|
|
|
classNodes = AstMatcher.match(self.classPattern, ast)
|
|
|
|
for node in classNodes:
|
|
|
|
astClass = AstClass(node)
|
|
|
|
self.classes[astClass.name] = astClass
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
CODE_HEADER = '''# -*- coding: utf-8 -*-
|
|
|
|
#
|
|
|
|
# GNU General Public License (GPL)
|
|
|
|
#
|
|
|
|
# This program is free software; you can redistribute it and/or
|
|
|
|
# modify it under the terms of the GNU General Public License
|
|
|
|
# as published by the Free Software Foundation; either version 2
|
|
|
|
# of the License, or (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with this program; if not, write to the Free Software
|
|
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
|
|
# 02110-1301, USA.
|
|
|
|
#
|
|
|
|
'''
|
|
|
|
class Generator:
|
|
|
|
'''Abstract base class for building a generator.'''
|
2012-01-18 07:27:24 -06:00
|
|
|
def __init__(self, application, options):
|
2009-06-29 07:06:01 -05:00
|
|
|
self.application = application
|
|
|
|
# Determine application name
|
|
|
|
self.applicationName = os.path.basename(application)
|
|
|
|
# Determine output folder (where to store the generated product)
|
2012-02-02 10:30:54 -06:00
|
|
|
self.outputFolder = os.path.join(application, 'zope',
|
|
|
|
self.applicationName)
|
2009-06-29 07:06:01 -05:00
|
|
|
self.options = options
|
|
|
|
# Determine templates folder
|
2011-12-05 03:52:18 -06:00
|
|
|
genFolder = os.path.dirname(__file__)
|
|
|
|
self.templatesFolder = os.path.join(genFolder, 'templates')
|
2009-06-29 07:06:01 -05:00
|
|
|
# Default descriptor classes
|
2010-08-05 11:23:17 -05:00
|
|
|
self.descriptorClasses = {
|
|
|
|
'class': ClassDescriptor, 'tool': ClassDescriptor,
|
2010-10-14 07:43:56 -05:00
|
|
|
'user': ClassDescriptor, 'workflow': WorkflowDescriptor}
|
2009-06-29 07:06:01 -05:00
|
|
|
# The following dict contains a series of replacements that need to be
|
|
|
|
# applied to file templates to generate files.
|
|
|
|
self.repls = {'applicationName': self.applicationName,
|
|
|
|
'applicationPath': os.path.dirname(self.application),
|
|
|
|
'codeHeader': CODE_HEADER}
|
|
|
|
# List of Appy classes and workflows found in the application
|
|
|
|
self.classes = []
|
2010-08-05 11:23:17 -05:00
|
|
|
self.tool = None
|
2010-09-02 09:16:08 -05:00
|
|
|
self.user = None
|
2009-06-29 07:06:01 -05:00
|
|
|
self.workflows = []
|
|
|
|
self.initialize()
|
2013-07-24 08:53:19 -05:00
|
|
|
self.config = gen.Config
|
2009-11-11 13:22:13 -06:00
|
|
|
self.modulesWithTests = set()
|
2009-12-03 09:45:05 -06:00
|
|
|
self.totalNumberOfTests = 0
|
2009-06-29 07:06:01 -05:00
|
|
|
|
2013-07-24 08:53:19 -05:00
|
|
|
def determineGenType(self, klass):
|
|
|
|
'''If p_klass is:
|
|
|
|
* a gen-class, this method returns "class";
|
|
|
|
* a gen-workflow, this method it "workflow";
|
|
|
|
* none of it, this method returns None.
|
|
|
|
|
|
|
|
If p_klass declares at least one static attribute that is a
|
|
|
|
appy.fields.Field, it will be considered a gen-class. If p_klass
|
|
|
|
declares at least one static attribute that is a appy.gen.State,
|
|
|
|
it will be considered a gen-workflow.'''
|
|
|
|
for attr in klass.__dict__.itervalues():
|
|
|
|
if isinstance(attr, gen.Field): return 'class'
|
|
|
|
elif isinstance(attr, gen.State): return 'workflow'
|
2009-06-29 07:06:01 -05:00
|
|
|
|
2009-11-11 13:22:13 -06:00
|
|
|
def containsTests(self, moduleOrClass):
|
2009-12-03 09:45:05 -06:00
|
|
|
'''Returns True if p_moduleOrClass contains doctests. This method also
|
|
|
|
counts tests and updates self.totalNumberOfTests.'''
|
|
|
|
res = False
|
|
|
|
docString = moduleOrClass.__doc__
|
|
|
|
if docString and (docString.find('>>>') != -1):
|
|
|
|
self.totalNumberOfTests += 1
|
|
|
|
res = True
|
|
|
|
# Count also docstring in methods
|
|
|
|
if type(moduleOrClass) == types.ClassType:
|
|
|
|
for name, elem in moduleOrClass.__dict__.iteritems():
|
|
|
|
if type(elem) in (staticmethod, classmethod):
|
|
|
|
elem = elem.__get__(name)
|
2011-02-17 11:13:42 -06:00
|
|
|
if callable(elem) and (type(elem) != types.ClassType) and \
|
|
|
|
hasattr(elem, '__doc__') and elem.__doc__ and \
|
2009-12-03 09:45:05 -06:00
|
|
|
(elem.__doc__.find('>>>') != -1):
|
|
|
|
res = True
|
|
|
|
self.totalNumberOfTests += 1
|
|
|
|
return res
|
2009-11-11 13:22:13 -06:00
|
|
|
|
2012-12-13 03:45:25 -06:00
|
|
|
def walkModule(self, moduleName, module):
|
|
|
|
'''Visits a given module of the application.'''
|
|
|
|
# Create the AST for this module. Producing an AST allows us to retrieve
|
|
|
|
# class attributes in the order of their definition, which is not
|
|
|
|
# possible by introspecting dict-based class objects.
|
|
|
|
moduleFile = module.__file__
|
|
|
|
if moduleFile.endswith('.pyc'):
|
|
|
|
moduleFile = moduleFile[:-1]
|
|
|
|
astClasses = Ast(moduleFile).classes
|
|
|
|
# Check if tests are present in this module
|
|
|
|
if self.containsTests(module):
|
|
|
|
self.modulesWithTests.add(module.__name__)
|
2009-06-29 07:06:01 -05:00
|
|
|
classType = type(Generator)
|
|
|
|
# Find all classes in this module
|
2012-12-13 03:45:25 -06:00
|
|
|
for name in module.__dict__.keys():
|
|
|
|
exec 'moduleElem = module.%s' % name
|
2013-07-24 08:53:19 -05:00
|
|
|
# Ignore non-classes module elements or classes that were imported
|
|
|
|
# from other modules.
|
|
|
|
if (type(moduleElem) != classType) or \
|
|
|
|
(moduleElem.__module__ != module.__name__): continue
|
|
|
|
# Ignore classes that are not gen-classes or gen-workflows.
|
|
|
|
genType = self.determineGenType(moduleElem)
|
|
|
|
if not genType: continue
|
|
|
|
# Produce a list of static class attributes (in the order
|
|
|
|
# of their definition).
|
|
|
|
attrs = astClasses[moduleElem.__name__].attributes
|
|
|
|
# Collect non-parsable attrs = back references added
|
|
|
|
# programmatically
|
|
|
|
moreAttrs = []
|
|
|
|
for eName, eValue in moduleElem.__dict__.iteritems():
|
|
|
|
if isinstance(eValue, gen.Field) and (eName not in attrs):
|
|
|
|
moreAttrs.append(eName)
|
|
|
|
# Sort them in alphabetical order: else, order would be random
|
|
|
|
moreAttrs.sort()
|
|
|
|
if moreAttrs: attrs += moreAttrs
|
|
|
|
# Add attributes added as back references
|
|
|
|
if genType == 'class':
|
|
|
|
# Determine the class type (standard, tool, user...)
|
|
|
|
if issubclass(moduleElem, gen.Tool):
|
|
|
|
if not self.tool:
|
|
|
|
klass = self.descriptorClasses['tool']
|
|
|
|
self.tool = klass(moduleElem, attrs, self)
|
2012-12-13 03:45:25 -06:00
|
|
|
else:
|
2013-07-24 08:53:19 -05:00
|
|
|
self.tool.update(moduleElem, attrs)
|
|
|
|
elif issubclass(moduleElem, gen.User):
|
|
|
|
if not self.user:
|
|
|
|
klass = self.descriptorClasses['user']
|
|
|
|
self.user = klass(moduleElem, attrs, self)
|
|
|
|
else:
|
|
|
|
self.user.update(moduleElem, attrs)
|
|
|
|
else:
|
|
|
|
descriptorClass = self.descriptorClasses['class']
|
|
|
|
descriptor = descriptorClass(moduleElem,attrs, self)
|
|
|
|
self.classes.append(descriptor)
|
|
|
|
# Manage classes containing tests
|
|
|
|
if self.containsTests(moduleElem):
|
|
|
|
self.modulesWithTests.add(module.__name__)
|
|
|
|
elif genType == 'workflow':
|
|
|
|
descriptorClass = self.descriptorClasses['workflow']
|
|
|
|
descriptor = descriptorClass(moduleElem, attrs, self)
|
|
|
|
self.workflows.append(descriptor)
|
|
|
|
if self.containsTests(moduleElem):
|
|
|
|
self.modulesWithTests.add(module.__name__)
|
2009-06-29 07:06:01 -05:00
|
|
|
|
|
|
|
def walkApplication(self):
|
|
|
|
'''This method walks into the application and creates the corresponding
|
|
|
|
meta-classes in self.classes, self.workflows, etc.'''
|
|
|
|
# Where is the application located ?
|
|
|
|
containingFolder = os.path.dirname(self.application)
|
|
|
|
sys.path.append(containingFolder)
|
|
|
|
# What is the name of the application ?
|
|
|
|
appName = os.path.basename(self.application)
|
2013-07-24 08:53:19 -05:00
|
|
|
# Get the app-specific config if any
|
|
|
|
exec 'import %s as appModule' % appName
|
|
|
|
if hasattr (appModule, 'Config'):
|
|
|
|
self.config = appModule.Config
|
|
|
|
if not issubclass(self.config, gen.Config):
|
|
|
|
raise Exception('Your Config class must subclass ' \
|
|
|
|
'appy.gen.Config.')
|
2012-12-13 03:45:25 -06:00
|
|
|
# Collect modules (only a the first level) in this application. Import
|
|
|
|
# them all, to be sure class definitions are complete (ie, back
|
|
|
|
# references are set from one class to the other). Moreover, potential
|
|
|
|
# syntax or import errors will raise an exception and abort the
|
|
|
|
# generation process before we do any undoable action.
|
|
|
|
modules = []
|
|
|
|
for fileName in os.listdir(self.application):
|
|
|
|
# Ignore non Python files
|
|
|
|
if not fileName.endswith('.py'): continue
|
|
|
|
moduleName = '%s.%s' % (appName, os.path.splitext(fileName)[0])
|
|
|
|
exec 'import %s' % moduleName
|
|
|
|
modules.append(eval(moduleName))
|
|
|
|
# Parse imported modules
|
|
|
|
for module in modules:
|
|
|
|
self.walkModule(moduleName, module)
|
2009-06-29 07:06:01 -05:00
|
|
|
sys.path.pop()
|
|
|
|
|
|
|
|
def generateClass(self, classDescr):
|
|
|
|
'''This method is called whenever a Python class declaring Appy type
|
|
|
|
definition(s) is encountered within the application.'''
|
|
|
|
|
|
|
|
def generateWorkflow(self, workflowDescr):
|
|
|
|
'''This method is called whenever a Python class declaring states and
|
|
|
|
transitions is encountered within the application.'''
|
|
|
|
|
|
|
|
def initialize(self):
|
|
|
|
'''Called before the old product is removed (if any), in __init__.'''
|
|
|
|
|
|
|
|
def finalize(self):
|
|
|
|
'''Called at the end of the generation process.'''
|
|
|
|
|
|
|
|
def copyFile(self, fileName, replacements, destName=None, destFolder=None,
|
|
|
|
isPod=False):
|
|
|
|
'''This method will copy p_fileName from self.templatesFolder to
|
|
|
|
self.outputFolder (or in a subFolder if p_destFolder is given)
|
|
|
|
after having replaced all p_replacements. If p_isPod is True,
|
|
|
|
p_fileName is a POD template and the copied file is the result of
|
|
|
|
applying p_fileName with context p_replacements.'''
|
|
|
|
# Get the path of the template file to copy
|
|
|
|
templatePath = os.path.join(self.templatesFolder, fileName)
|
|
|
|
# Get (or create if needed) the path of the result file
|
|
|
|
destFile = fileName
|
|
|
|
if destName: destFile = destName
|
|
|
|
if destFolder: destFile = '%s/%s' % (destFolder, destFile)
|
|
|
|
absDestFolder = self.outputFolder
|
|
|
|
if destFolder:
|
|
|
|
absDestFolder = os.path.join(self.outputFolder, destFolder)
|
|
|
|
if not os.path.exists(absDestFolder):
|
|
|
|
os.makedirs(absDestFolder)
|
|
|
|
resultPath = os.path.join(self.outputFolder, destFile)
|
|
|
|
if os.path.exists(resultPath): os.remove(resultPath)
|
|
|
|
if not isPod:
|
|
|
|
# Copy the template file to result file after having performed some
|
|
|
|
# replacements
|
|
|
|
f = file(templatePath)
|
|
|
|
fileContent = f.read()
|
|
|
|
f.close()
|
|
|
|
if not fileName.endswith('.png'):
|
|
|
|
for rKey, rValue in replacements.iteritems():
|
|
|
|
fileContent = fileContent.replace(
|
|
|
|
'<!%s!>' % rKey, str(rValue))
|
|
|
|
f = file(resultPath, 'w')
|
|
|
|
f.write(fileContent)
|
|
|
|
f.close()
|
|
|
|
else:
|
|
|
|
# Call the POD renderer to produce the result
|
|
|
|
rendererParams = {'template': templatePath,
|
|
|
|
'context': replacements,
|
|
|
|
'result': resultPath}
|
|
|
|
renderer = appy.pod.renderer.Renderer(**rendererParams)
|
|
|
|
renderer.run()
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
self.walkApplication()
|
2010-08-05 11:23:17 -05:00
|
|
|
for descriptor in self.classes: self.generateClass(descriptor)
|
|
|
|
for descriptor in self.workflows: self.generateWorkflow(descriptor)
|
2009-06-29 07:06:01 -05:00
|
|
|
self.finalize()
|
2009-12-03 09:45:05 -06:00
|
|
|
msg = ''
|
|
|
|
if self.totalNumberOfTests:
|
|
|
|
msg = ' (number of tests found: %d)' % self.totalNumberOfTests
|
2013-05-29 17:46:11 -05:00
|
|
|
print('Done%s.' % msg)
|
2011-12-05 03:52:18 -06:00
|
|
|
|
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
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
|
2012-03-26 12:09:45 -05:00
|
|
|
# Create Tool, User, Group, Translation and Page instances.
|
2011-12-05 03:52:18 -06:00
|
|
|
self.tool = ToolClassDescriptor(Tool, self)
|
|
|
|
self.user = UserClassDescriptor(User, self)
|
|
|
|
self.group = GroupClassDescriptor(Group, self)
|
|
|
|
self.translation = TranslationClassDescriptor(Translation, self)
|
2012-03-26 12:09:45 -05:00
|
|
|
self.page = PageClassDescriptor(Page, self)
|
2011-12-05 03:52:18 -06:00
|
|
|
# i18n labels to generate
|
2012-11-06 04:32:39 -06:00
|
|
|
self.labels = po.PoMessages()
|
2011-12-05 03:52:18 -06:00
|
|
|
|
2012-11-06 04:32:39 -06:00
|
|
|
def i18n(self, id, default, nice=True):
|
|
|
|
'''Shorthand for adding a new message into self.labels.'''
|
|
|
|
self.labels.append(id, default, nice=nice)
|
|
|
|
|
2011-12-05 03:52:18 -06:00
|
|
|
versionRex = re.compile('(.*?\s+build)\s+(\d+)')
|
|
|
|
def initialize(self):
|
|
|
|
# Determine version number
|
2012-01-18 07:27:24 -06:00
|
|
|
self.version = '0.1.0 build 1'
|
2011-12-05 03:52:18 -06:00
|
|
|
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:
|
2012-11-06 04:32:39 -06:00
|
|
|
poParser = po.PoParser(os.path.join(i18nFolder, fileName))
|
2011-12-05 03:52:18 -06:00
|
|
|
self.i18nFiles[fileName] = poParser.parse()
|
|
|
|
|
|
|
|
def finalize(self):
|
2012-11-06 04:32:39 -06:00
|
|
|
# Add a label for the application name
|
|
|
|
self.i18n(self.applicationName, self.applicationName)
|
2013-10-20 11:12:39 -05:00
|
|
|
# Add a i18n message for every role.
|
|
|
|
for role in self.getUsedRoles(appy=False):
|
2012-11-06 04:32:39 -06:00
|
|
|
self.i18n('role_%s' % role.name, role.name)
|
2011-12-05 03:52:18 -06:00
|
|
|
# Create basic files (config.py, etc)
|
|
|
|
self.generateTool()
|
|
|
|
self.generateInit()
|
|
|
|
self.generateTests()
|
|
|
|
# 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()
|
|
|
|
# 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)
|
2012-11-06 04:32:39 -06:00
|
|
|
potFile = po.PoFile(fullName)
|
2011-12-05 03:52:18 -06:00
|
|
|
self.i18nFiles[potFileName] = potFile
|
2013-10-20 11:12:39 -05:00
|
|
|
# Update the pot file with (a) standard Appy labels and (b) the list of
|
|
|
|
# generated application labels.
|
|
|
|
appyPotFileName = os.path.join(appy.getPath(), 'gen', 'tr', 'Appy.pot')
|
|
|
|
appyLabels = po.PoParser(appyPotFileName).parse().messages
|
|
|
|
removedLabels = potFile.update(appyLabels + self.labels.get(),
|
|
|
|
self.options.i18nClean, keepExistingOrder=False)
|
|
|
|
potFile.generate()
|
2011-12-05 03:52:18 -06:00
|
|
|
if removedLabels:
|
2013-05-29 17:46:11 -05:00
|
|
|
print('Warning: %d messages were removed from translation ' \
|
|
|
|
'files: %s' % (len(removedLabels), str(removedLabels)))
|
2011-12-05 03:52:18 -06:00
|
|
|
# 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)
|
2012-11-06 04:32:39 -06:00
|
|
|
poFile = po.PoFile(fullName)
|
2011-12-05 03:52:18 -06:00
|
|
|
self.i18nFiles[poFileName] = poFile
|
2013-10-20 11:12:39 -05:00
|
|
|
# If we have default Appy messages translated for this language,
|
|
|
|
# get it. Else, use appyLabels from the pot file as default empty
|
|
|
|
# labels.
|
|
|
|
appyPoFileName = os.path.join(appy.getPath(), 'gen', 'tr',
|
2013-10-20 16:13:16 -05:00
|
|
|
'%s.po' % language)
|
2013-10-20 11:12:39 -05:00
|
|
|
if os.path.exists(appyPoFileName):
|
|
|
|
baseLabels = po.PoParser(appyPoFileName).parse().messages
|
|
|
|
else:
|
|
|
|
baseLabels = appyLabels
|
|
|
|
poFile.update(baseLabels + self.labels.get(),
|
|
|
|
self.options.i18nClean, keepExistingOrder=False)
|
2011-12-05 03:52:18 -06:00
|
|
|
poFile.generate()
|
|
|
|
# Generate corresponding fields on the Translation class
|
2013-10-20 11:12:39 -05:00
|
|
|
page = '1'
|
2011-12-05 03:52:18 -06:00
|
|
|
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.
|
2013-10-20 11:12:39 -05:00
|
|
|
page = str(int(page)+1)
|
2011-12-05 03:52:18 -06:00
|
|
|
self.generateWrappers()
|
|
|
|
self.generateConfig()
|
|
|
|
|
2013-10-20 11:12:39 -05:00
|
|
|
def getUsedRoles(self, appy=None, local=None, grantable=None):
|
2011-12-05 03:52:18 -06:00
|
|
|
'''Produces a list of all the roles used within all workflows and
|
|
|
|
classes defined in this application.
|
|
|
|
|
2013-10-20 11:12:39 -05:00
|
|
|
If p_appy is True, it keeps only Appy standard roles; if p_appy
|
2011-12-05 03:52:18 -06:00
|
|
|
is False, it keeps only roles which are specific to this application;
|
2013-10-20 11:12:39 -05:00
|
|
|
if p_appy is None it has no effect (so it keeps both roles).
|
2011-12-05 03:52:18 -06:00
|
|
|
|
|
|
|
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)
|
2011-12-05 08:11:29 -06:00
|
|
|
if isinstance(attrValue, gen.State) or \
|
|
|
|
isinstance(attrValue, gen.Transition):
|
2011-12-05 03:52:18 -06:00
|
|
|
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'):
|
2013-09-22 09:33:32 -05:00
|
|
|
creators = cDescr.getCreators()
|
|
|
|
if not creators: continue
|
|
|
|
for role in creators:
|
2011-12-05 03:52:18 -06:00
|
|
|
if role.name not in allRoles:
|
|
|
|
allRoles[role.name] = role
|
|
|
|
res = allRoles.values()
|
|
|
|
# Filter the result according to parameters
|
2013-10-20 11:12:39 -05:00
|
|
|
for p in ('appy', 'local', 'grantable'):
|
2011-12-05 03:52:18 -06:00
|
|
|
if eval(p) != None:
|
|
|
|
res = [r for r in res if eval('r.%s == %s' % (p, p))]
|
|
|
|
return res
|
|
|
|
|
|
|
|
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
|
2012-03-26 12:09:45 -05:00
|
|
|
classes (tool, user, group, translation, page)
|
2011-12-05 03:52:18 -06:00
|
|
|
* "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[:]
|
2012-03-26 12:09:45 -05:00
|
|
|
configClasses = [self.tool, self.user, self.group, self.translation,
|
|
|
|
self.page]
|
2011-12-05 03:52:18 -06:00
|
|
|
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 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 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])
|
2014-03-03 11:54:21 -06:00
|
|
|
allShortClassNames = ['"%s":"%s"' % (c.name.split('_')[-1], c.name) \
|
|
|
|
for c in classesAll]
|
|
|
|
repls['allShortClassNames'] = ','.join(allShortClassNames)
|
2011-12-05 03:52:18 -06:00
|
|
|
# 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')
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
# Add the 'state' attribute
|
|
|
|
names.append('state')
|
2011-12-05 03:52:18 -06:00
|
|
|
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
|
2013-10-20 11:12:39 -05:00
|
|
|
specificRoles = self.getUsedRoles(appy=False)
|
2011-12-05 03:52:18 -06:00
|
|
|
repls['roles'] = ','.join(['"%s"' % r.name for r in specificRoles])
|
2013-10-20 11:12:39 -05:00
|
|
|
globalRoles = self.getUsedRoles(appy=False, local=False)
|
2011-12-05 03:52:18 -06:00
|
|
|
repls['gRoles'] = ','.join(['"%s"' % r.name for r in globalRoles])
|
2013-10-20 11:12:39 -05:00
|
|
|
grantableRoles = self.getUsedRoles(local=False, grantable=True)
|
2011-12-05 03:52:18 -06:00
|
|
|
repls['grRoles'] = ','.join(['"%s"' % r.name for r in grantableRoles])
|
2011-12-15 15:56:53 -06:00
|
|
|
self.copyFile('config.pyt', repls, destName='config.py')
|
2011-12-05 03:52:18 -06:00
|
|
|
|
|
|
|
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
|
2011-12-15 15:56:53 -06:00
|
|
|
self.copyFile('__init__.pyt', repls, destName='__init__.py')
|
2011-12-05 03:52:18 -06:00
|
|
|
|
|
|
|
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()
|
2011-12-15 15:56:53 -06:00
|
|
|
self.copyFile('wrappers.pyt', repls, destName='wrappers.py')
|
2011-12-05 03:52:18 -06:00
|
|
|
|
|
|
|
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)
|
2011-12-15 15:56:53 -06:00
|
|
|
self.copyFile('testAll.pyt', repls, destName='testAll.py',
|
|
|
|
destFolder='tests')
|
2011-12-05 03:52:18 -06:00
|
|
|
|
|
|
|
def generateTool(self):
|
2011-12-05 08:11:29 -06:00
|
|
|
'''Generates the tool that corresponds to this application.'''
|
2011-12-05 03:52:18 -06:00
|
|
|
# Create Tool-related i18n-related messages
|
2012-11-06 04:32:39 -06:00
|
|
|
self.i18n(self.tool.name, po.CONFIG % self.applicationName, nice=False)
|
2011-12-05 03:52:18 -06:00
|
|
|
# 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
|
|
|
|
|
2012-03-26 12:09:45 -05:00
|
|
|
# Generate the Tool-related classes (User, Group, Translation, Page)
|
|
|
|
for klass in (self.user, self.group, self.translation, self.page):
|
2011-12-05 03:52:18 -06:00
|
|
|
klassType = klass.name[len(self.applicationName):]
|
|
|
|
klass.generateSchema()
|
2012-11-06 04:32:39 -06:00
|
|
|
self.i18n(klass.name, klassType, nice=False)
|
|
|
|
self.i18n('%s_plural' % klass.name, klass.name+'s', nice=False)
|
2013-10-11 07:09:52 -05:00
|
|
|
self.generateSearches(klass)
|
2011-12-05 03:52:18 -06:00
|
|
|
repls = self.repls.copy()
|
2012-03-26 12:09:45 -05:00
|
|
|
if klass.isFolder():
|
|
|
|
parents = 'BaseMixin, Folder'
|
|
|
|
icon = 'folder.gif'
|
|
|
|
else:
|
|
|
|
parents = 'BaseMixin, SimpleItem'
|
|
|
|
icon = 'object.gif'
|
2011-12-05 03:52:18 -06:00
|
|
|
repls.update({'methods': klass.methods, 'genClassName': klass.name,
|
2012-03-26 12:09:45 -05:00
|
|
|
'baseMixin':'BaseMixin', 'parents': parents,
|
|
|
|
'classDoc': 'Standard Appy class', 'icon': icon})
|
2011-12-15 15:56:53 -06:00
|
|
|
self.copyFile('Class.pyt', repls, destName='%s.py' % klass.name)
|
2011-12-05 03:52:18 -06:00
|
|
|
|
2013-09-18 05:06:07 -05:00
|
|
|
# Before generating the Tool class, finalize it with search-related and
|
|
|
|
# import-related fields.
|
2011-12-05 03:52:18 -06:00
|
|
|
for classDescr in self.getClasses(include='allButTool'):
|
2012-11-06 04:32:39 -06:00
|
|
|
if not classDescr.isRoot(): continue
|
|
|
|
importMean = classDescr.getCreateMean('Import')
|
|
|
|
if importMean:
|
|
|
|
self.tool.addImportRelatedFields(classDescr)
|
2011-12-05 03:52:18 -06:00
|
|
|
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})
|
2011-12-15 15:56:53 -06:00
|
|
|
self.copyFile('Class.pyt', repls, destName='%s.py' % self.tool.name)
|
2011-12-05 03:52:18 -06:00
|
|
|
|
2013-10-11 07:09:52 -05:00
|
|
|
def generateSearches(self, classDescr):
|
|
|
|
'''Generates i18n labels for searches defined on p_classDescr.'''
|
|
|
|
for search in classDescr.getSearches(classDescr.klass):
|
|
|
|
label = '%s_search_%s' % (classDescr.name, search.name)
|
|
|
|
self.i18n(label, search.name)
|
|
|
|
self.i18n('%s_descr' % label, ' ', nice=False)
|
|
|
|
# Generate labels for groups of searches
|
|
|
|
if search.group and not search.group.label:
|
|
|
|
search.group.generateLabels(self.labels, classDescr, set(),
|
|
|
|
content='searches')
|
|
|
|
|
2011-12-05 03:52:18 -06:00
|
|
|
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
|
2013-05-29 17:46:11 -05:00
|
|
|
print('Generating %s.%s (gen-class)...' % (k.__module__, k.__name__))
|
2011-12-05 03:52:18 -06:00
|
|
|
# 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
|
2012-11-06 04:32:39 -06:00
|
|
|
classDoc = k.__doc__ or 'Appy class.'
|
2011-12-05 03:52:18 -06:00
|
|
|
repls = self.repls.copy()
|
|
|
|
classDescr.generateSchema()
|
|
|
|
repls.update({
|
2012-11-06 04:32:39 -06:00
|
|
|
'parents': parents, 'className': k.__name__,
|
2011-12-05 03:52:18 -06:00
|
|
|
'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)
|
2012-11-06 04:32:39 -06:00
|
|
|
self.i18n(classDescr.name, k.__name__)
|
|
|
|
self.i18n('%s_plural' % classDescr.name, k.__name__+'s')
|
2011-12-05 03:52:18 -06:00
|
|
|
# Create i18n labels for searches
|
2013-10-11 07:09:52 -05:00
|
|
|
self.generateSearches(classDescr)
|
2011-12-05 08:11:29 -06:00
|
|
|
# Generate the resulting Zope class.
|
2011-12-15 15:56:53 -06:00
|
|
|
self.copyFile('Class.pyt', repls, destName=fileName)
|
2011-12-05 03:52:18 -06:00
|
|
|
|
|
|
|
def generateWorkflow(self, wfDescr):
|
|
|
|
'''This method creates the i18n labels related to the workflow described
|
|
|
|
by p_wfDescr.'''
|
|
|
|
k = wfDescr.klass
|
2013-05-29 17:46:11 -05:00
|
|
|
print('Generating %s.%s (gen-workflow)...' % (k.__module__, k.__name__))
|
2011-12-05 03:52:18 -06:00
|
|
|
# Identify workflow name
|
|
|
|
wfName = WorkflowDescriptor.getWorkflowName(wfDescr.klass)
|
|
|
|
# Add i18n messages for states
|
|
|
|
for name in dir(wfDescr.klass):
|
2011-12-05 08:11:29 -06:00
|
|
|
if not isinstance(getattr(wfDescr.klass, name), gen.State): continue
|
2012-11-06 04:32:39 -06:00
|
|
|
self.i18n('%s_%s' % (wfName, name), name)
|
2011-12-05 03:52:18 -06:00
|
|
|
# Add i18n messages for transitions
|
|
|
|
for name in dir(wfDescr.klass):
|
|
|
|
transition = getattr(wfDescr.klass, name)
|
2011-12-05 08:11:29 -06:00
|
|
|
if not isinstance(transition, gen.Transition): continue
|
2012-12-19 03:01:31 -06:00
|
|
|
self.i18n('%s_%s' % (wfName, name), name)
|
2012-08-14 09:05:02 -05:00
|
|
|
if transition.show and transition.confirm:
|
2011-12-05 03:52:18 -06:00
|
|
|
# We need to generate a label for the message that will be shown
|
|
|
|
# in the confirm popup.
|
2012-11-06 04:32:39 -06:00
|
|
|
self.i18n('%s_%s_confirm'%(wfName, name),po.CONFIRM, nice=False)
|
2011-12-05 03:52:18 -06:00
|
|
|
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.
|
2012-11-06 04:32:39 -06:00
|
|
|
self.i18n('%s_%s_mail_subject' % (wfName, name),
|
|
|
|
po.EMAIL_SUBJECT, nice=False)
|
|
|
|
self.i18n('%s_%s_mail_body' % (wfName, name),
|
|
|
|
po.EMAIL_BODY, nice=False)
|
2009-06-29 07:06:01 -05:00
|
|
|
# ------------------------------------------------------------------------------
|