Gaetan Delannay 2009-08-04 14:39:43 +02:00
parent 4c29c7c484
commit facbe7fa3d
8 changed files with 229 additions and 16 deletions

View file

@ -456,4 +456,7 @@ class Config:
# frontPage = True will replace the Plone front page with a page
# whose content will come fron i18n label "front_page_text".
self.frontPage = False
# If you don't need the portlet that appy.gen has generated for your
# application, set the following parameter to False.
self.showPortlet = True
# ------------------------------------------------------------------------------

View file

@ -227,6 +227,7 @@ class Generator:
# Potentially, sub-modules exist
moduleFolder = os.path.dirname(moduleFile)
for elem in os.listdir(moduleFolder):
if elem.startswith('.'): continue
subModuleName, ext = os.path.splitext(elem)
if ((ext == '.py') and (subModuleName != '__init__')) or \
os.path.isdir(os.path.join(moduleFolder, subModuleName)):

View file

@ -354,13 +354,16 @@ class Generator(AbstractGenerator):
"['portal_catalog']\n" % blackClass
# Compute workflows
workflows = ''
for classDescr in self.classes:
allClasses = self.classes[:]
if self.customToolDescr:
allClasses.append(self.customToolDescr)
if self.customFlavourDescr:
allClasses.append(self.customFlavourDescr)
for classDescr in allClasses:
if hasattr(classDescr.klass, 'workflow'):
wfName = WorkflowDescriptor.getWorkflowName(
classDescr.klass.workflow)
className = ArchetypesClassDescriptor.getClassName(
classDescr.klass)
workflows += '\n "%s":"%s",' % (className, wfName)
workflows += '\n "%s":"%s",' % (classDescr.name, wfName)
# Generate the resulting file.
repls = self.repls.copy()
repls['allClassNames'] = allClassNames
@ -369,6 +372,7 @@ class Generator(AbstractGenerator):
repls['imports'] = '\n'.join(imports)
repls['appClasses'] = "[%s]" % ','.join(appClasses)
repls['minimalistPlone'] = self.config.minimalistPlone
repls['showPortlet'] = self.config.showPortlet
repls['appFrontPage'] = self.config.frontPage == True
repls['workflows'] = workflows
self.copyFile('Install.py', repls, destFolder='Extensions')
@ -515,6 +519,12 @@ class Generator(AbstractGenerator):
# Implicitly, the title will be added by Archetypes. So I need
# to define a property for it.
wrapperDef += self.generateWrapperProperty('title', String())
# For custom tool, flavour and pod template, add a call to a method
# that allows to custom element to update the basic element.
if isinstance(c, CustomToolClassDescriptor) or \
isinstance(c, CustomFlavourClassDescriptor):
wrapperDef += " if hasattr(%s, 'update'): %s.update(%s.__bases__[1])" % \
(parentClass, parentClass, parentWrapper)
wrappers.append(wrapperDef)
repls = self.repls.copy()
repls['imports'] = '\n'.join(imports)

View file

@ -13,7 +13,7 @@ class PloneInstaller:
installed or uninstalled (in the Plone configuration interface).'''
def __init__(self, reinstall, productName, ploneSite, minimalistPlone,
appClasses, appClassNames, allClassNames, catalogMap, applicationRoles,
defaultAddRoles, workflows, appFrontPage, ploneStuff):
defaultAddRoles, workflows, appFrontPage, showPortlet, ploneStuff):
self.reinstall = reinstall # Is it a fresh install or a re-install?
self.productName = productName
self.ploneSite = ploneSite
@ -32,6 +32,7 @@ class PloneInstaller:
# used by the content type)
self.appFrontPage = appFrontPage # Does this app define a site-wide
# front page?
self.showPortlet = showPortlet # Must we show the application portlet?
self.ploneStuff = ploneStuff # A dict of some Plone functions or vars
self.toLog = StringIO()
self.typeAliases = {'sharing': '', 'gethtml': '',
@ -257,8 +258,8 @@ class PloneInstaller:
nvProps.manage_changeProperties(**{'idsNotToList': current})
# Remove workflow for the tool
wfTool = self.ploneSite.portal_workflow
wfTool.setChainForPortalTypes([self.toolName], '')
#wfTool = self.ploneSite.portal_workflow
#wfTool.setChainForPortalTypes([self.toolName], '')
# Create the default flavour
self.tool = getattr(self.ploneSite, self.toolInstanceName)
@ -350,17 +351,19 @@ class PloneInstaller:
# No portal_css registry
pass
def installPortlet(self):
'''Adds the application-specific portlet and configure other Plone
portlets if relevant.'''
def managePortlets(self):
'''Shows or hides the application-specific portlet and configures other
Plone portlets if relevant.'''
portletName= 'here/%s_portlet/macros/portlet' % self.productName.lower()
site = self.ploneSite
# This is the name of the application-specific portlet
leftPortlets = site.getProperty('left_slots')
if not leftPortlets: leftPortlets = []
else: leftPortlets = list(leftPortlets)
if portletName not in leftPortlets:
if self.showPortlet and (portletName not in leftPortlets):
leftPortlets.insert(0, portletName)
if not self.showPortlet and (portletName in leftPortlets):
leftPortlets.remove(portletName)
# Remove some basic Plone portlets that make less sense when building
# web applications.
portletsToRemove = ["here/portlet_navigation/macros/portlet",
@ -402,7 +405,7 @@ class PloneInstaller:
self.installRolesAndGroups()
self.installWorkflows()
self.installStyleSheet()
self.installPortlet()
self.managePortlets()
self.finalizeInstallation()
self.log("Installation of %s done." % self.productName)
return self.toLog.getvalue()

View file

@ -533,6 +533,10 @@ class AbstractMixin:
self.appyWrapper = self.wrapperClass(self)
return self.appyWrapper
def appy(self):
'''Nice alias to the previous method.'''
return self._appy_getWrapper(force=True)
def _appy_getSourceClass(self, fieldName, baseClass):
'''We know that p_fieldName was defined on Python class p_baseClass or
one of its parents. This method returns the exact class (p_baseClass

View file

@ -16,13 +16,14 @@ appClasses = <!appClasses!>
appClassNames = [<!appClassNames!>]
allClassNames = [<!allClassNames!>]
workflows = {<!workflows!>}
showPortlet = <!showPortlet!>
# ------------------------------------------------------------------------------
def install(self, reinstall=False):
'''Installation of product "<!applicationName!>"'''
ploneInstaller = PloneInstaller(reinstall, "<!applicationName!>", self,
<!minimalistPlone!>, appClasses, appClassNames, allClassNames,
catalogMap, applicationRoles, defaultAddRoles, workflows,
<!appFrontPage!>, globals())
<!appFrontPage!>, showPortlet, globals())
return ploneInstaller.install()
# ------------------------------------------------------------------------------
@ -31,6 +32,6 @@ def uninstall(self, reinstall=False):
ploneInstaller = PloneInstaller(reinstall, "<!applicationName!>", self,
<!minimalistPlone!>, appClasses, appClassNames, allClassNames,
catalogMap, applicationRoles, defaultAddRoles, workflows,
<!appFrontPage!>, globals())
<!appFrontPage!>, showPortlet, globals())
return ploneInstaller.uninstall()
# ------------------------------------------------------------------------------

View file

@ -57,4 +57,193 @@ def getOsTempFolder():
else:
raise "Sorry, I can't find a temp folder on your machine."
return res
# ------------------------------------------------------------------------------
WRONG_LINE = 'Line number %d in file %s does not have the right number of ' \
'fields.'
class CsvObject:
'''Used for producing objects from CSV parsing.'''
def __repr__(self):
res = '<CsvObject '
for attrName, attrValue in self.__dict__.iteritems():
res += attrName + '=' + str(attrValue) + ' '
res = res.strip() + '>'
return res
class CsvParser:
'''This class reads a CSV file and creates a list of Python objects from it.
The first line of the CSV file must declare the format of the following
lines, which are 'data' lines. For example, if the first line of the file
is
id,roles*,password
Then subsequent lines in the CSV need to conform to this syntax. Field
separator will be the comma. Result of method 'parse' will be a list of
Python objects, each one having attributes id, roles and password.
Attributes declared with a star (like 'roles') are lists. An empty value
will produce an empty list in the resulting object; several values need
to be separated with the '+' sign. Here are some examples of valid 'data'
lines for the first line above:
gdy,,
gdy,MeetingManager,abc
gdy,MeetingManager+MeetingMember,abc
In the first (and subsequent) line(s), you may choose among the following
separators: , : ; |
'''
separators = [',', ':', ';', '|']
typeLetters = {'i': int, 'f': float, 's': str, 'b': bool}
def __init__(self, fileName, references={}, klass=None):
self.fileName = fileName
self.res = [] # The resulting list of Python objects.
self.sep = None
self.attributes = None # The list of attributes corresponding to
# CSV columns.
self.attributesFlags = None # Here we now if every attribute is a list
# (True) of not (False).
self.attributesTypes = None # Here we now the type of the attribute (if
# the attribute is a list it denotes the type of every item in the
# list): string, integer, float, boolean.
self.references = references
self.klass = klass # If a klass is given here, instead of creating
# CsvObject instances we will create instances of this class. But be
# careful: we will not call the constructor of this class. We will
# simply create instances of CsvObject and dynamically change the class
# of created instances to this class.
def identifySeparator(self, line):
'''What is the separator used in this file?'''
maxLength = 0
res = None
for sep in self.separators:
newLength = len(line.split(sep))
if newLength > maxLength:
maxLength = newLength
res = sep
self.sep = res
def identifyAttributes(self, line):
self.attributes = line.split(self.sep)
self.attributesFlags = [False] * len(self.attributes)
self.attributesTypes = [str] * len(self.attributes)
i = -1
for attr in self.attributes:
i += 1
# Is this attribute mono- or multi-valued?
if attr.endswith('*'):
self.attributesFlags[i] = True
attrNoFlag = attr.strip('*')
attrInfo = attrNoFlag.split('-')
# What is the type of value(s) for this attribute ?
if (len(attrInfo) == 2) and (attrInfo[1] in self.typeLetters):
self.attributesTypes[i] = self.typeLetters[attrInfo[1]]
# Remove trailing stars
self.attributes = [a.strip('*').split('-')[0] for a in self.attributes]
def resolveReference(self, attrName, refId):
'''Finds, in self.reference, the object having p_refId.'''
refObjects, refAttrName = self.references[attrName]
res = None
for refObject in refObjects:
if getattr(refObject, refAttrName) == refId:
res = refObject
break
return res
def convertValue(self, value, basicType):
'''Converts the atomic p_value which is a string into some other atomic
Python type specified in p_basicType (int, float, ...).'''
if (basicType != str) and (basicType != unicode):
try:
exec 'res = %s' % str(value)
except SyntaxError, se:
res = None
else:
try:
exec 'res = """%s"""' % str(value)
except SyntaxError, se:
try:
exec "res = '''%s'''" % str(value)
except SyntaxError, se:
res = None
return res
def parse(self):
'''Parses the CSV file named self.fileName and creates a list of
corresponding Python objects (CsvObject instances). Among object
fields, some may be references. If it is the case, you may specify
in p_references a dict of referred objects. The parser will then
replace string values of some fields (which are supposed to be ids
of referred objects) with corresponding objects in p_references.
How does this work? p_references must be a dictionary:
- keys correspond to field names of the current object;
- values are 2-tuples:
* 1st value is the list of available referred objects;
* 2nd value is the name of the attribute on those objects that
stores their ID.
'''
# The first pass parses the file and creates the Python object
f = file(self.fileName)
firstLine = True
lineNb = 0
for line in f:
lineNb += 1
line = line.strip()
if not line: continue
if firstLine:
# The first line declares the structure of the following 'data'
# lines.
self.identifySeparator(line)
self.identifyAttributes(line)
firstLine = False
else:
# Add an object corresponding to this line.
lineObject = CsvObject()
if self.klass:
lineObject.__class__ = self.klass
i = -1
# Do we get the right number of field values on this line ?
attrValues = line.split(self.sep)
if len(attrValues) != len(self.attributes):
raise WRONG_LINE % (lineNb, self.fileName)
for attrValue in line.split(self.sep):
i += 1
theValue = attrValue
vType = self.attributesTypes[i]
if self.attributesFlags[i]:
# The attribute is multi-valued
if not attrValue:
theValue = []
elif '+' in theValue:
theValue = [self.convertValue(v, vType) \
for v in attrValue.split('+')]
else:
theValue = [self.convertValue(theValue, vType)]
else:
# The attribute is mono-valued
theValue = self.convertValue(theValue, vType)
setattr(lineObject, self.attributes[i], theValue)
self.res.append(lineObject)
f.close()
# The second pass resolves the p_references if any
for attrName, refInfo in self.references.iteritems():
if attrName in self.attributes:
# Replace ID with real object from p_references
for obj in self.res:
attrValue = getattr(obj, attrName)
if isinstance(attrValue, list) or \
isinstance(attrValue, tuple):
# Multiple values to resolve
newValue = []
for v in attrValue:
newValue.append(self.resolveReference(attrName,v))
else:
# Only one value to resolve
newValue = self.resolveReference(attrName, attrValue)
setattr(obj, attrName, newValue)
return self.res
# ------------------------------------------------------------------------------

View file

@ -23,7 +23,7 @@ from xml.sax.xmlreader import InputSource
# ------------------------------------------------------------------------------
class XmlElement:
'''Representgs an XML tag.'''
'''Represents an XML tag.'''
def __init__(self, elem, attrs=None, nsUri=None):
'''An XmlElement instance may represent:
- an already parsed tag (in this case, p_elem may be prefixed with a
@ -95,6 +95,7 @@ class XmlParser(ContentHandler, ErrorHandler):
self.env.parser = self
self.caller = caller # The class calling this parser
self.parser = xml.sax.make_parser() # Fast, standard expat parser
self.res = None # The result of parsing.
def setDocumentLocator(self, locator):
self.locator = locator
return self.env
@ -128,4 +129,5 @@ class XmlParser(ContentHandler, ErrorHandler):
else:
inputSource.setByteStream(xmlContent)
self.parser.parse(inputSource)
return self.res
# ------------------------------------------------------------------------------