From facbe7fa3d4f8b2d5d419306b08f129cb0dc20c8 Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Tue, 4 Aug 2009 14:39:43 +0200 Subject: [PATCH] Fixed bug https://bugs.launchpad.net/appy/+bug/408826, implemented blueprints https://blueprints.launchpad.net/appy/+spec/show-or-hide-application-portlet, https://blueprints.launchpad.net/appy/+spec/associate-a-workflow-to-custom-tool-or-flavour and https://blueprints.launchpad.net/appy/+spec/csv-parser --- gen/__init__.py | 3 + gen/generator.py | 1 + gen/plone25/generator.py | 18 ++- gen/plone25/installer.py | 21 ++-- gen/plone25/mixins/__init__.py | 4 + gen/plone25/templates/Install.py | 5 +- shared/utils.py | 189 +++++++++++++++++++++++++++++++ shared/xml_parser.py | 4 +- 8 files changed, 229 insertions(+), 16 deletions(-) diff --git a/gen/__init__.py b/gen/__init__.py index 33a2aab..3e70657 100755 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -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 # ------------------------------------------------------------------------------ diff --git a/gen/generator.py b/gen/generator.py index 3482b99..12cd3af 100755 --- a/gen/generator.py +++ b/gen/generator.py @@ -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)): diff --git a/gen/plone25/generator.py b/gen/plone25/generator.py index b24de50..20c3019 100755 --- a/gen/plone25/generator.py +++ b/gen/plone25/generator.py @@ -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) diff --git a/gen/plone25/installer.py b/gen/plone25/installer.py index b4e5219..ae47ea8 100644 --- a/gen/plone25/installer.py +++ b/gen/plone25/installer.py @@ -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() diff --git a/gen/plone25/mixins/__init__.py b/gen/plone25/mixins/__init__.py index 0887769..a81bb59 100644 --- a/gen/plone25/mixins/__init__.py +++ b/gen/plone25/mixins/__init__.py @@ -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 diff --git a/gen/plone25/templates/Install.py b/gen/plone25/templates/Install.py index 7408dd4..c1b97b4 100755 --- a/gen/plone25/templates/Install.py +++ b/gen/plone25/templates/Install.py @@ -16,13 +16,14 @@ appClasses = appClassNames = [] allClassNames = [] workflows = {} +showPortlet = # ------------------------------------------------------------------------------ def install(self, reinstall=False): '''Installation of product ""''' ploneInstaller = PloneInstaller(reinstall, "", self, , appClasses, appClassNames, allClassNames, catalogMap, applicationRoles, defaultAddRoles, workflows, - , globals()) + , showPortlet, globals()) return ploneInstaller.install() # ------------------------------------------------------------------------------ @@ -31,6 +32,6 @@ def uninstall(self, reinstall=False): ploneInstaller = PloneInstaller(reinstall, "", self, , appClasses, appClassNames, allClassNames, catalogMap, applicationRoles, defaultAddRoles, workflows, - , globals()) + , showPortlet, globals()) return ploneInstaller.uninstall() # ------------------------------------------------------------------------------ diff --git a/shared/utils.py b/shared/utils.py index 8413cba..21020b0 100755 --- a/shared/utils.py +++ b/shared/utils.py @@ -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 = '' + 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 +# ------------------------------------------------------------------------------ + diff --git a/shared/xml_parser.py b/shared/xml_parser.py index 1408af6..7d930e4 100755 --- a/shared/xml_parser.py +++ b/shared/xml_parser.py @@ -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 # ------------------------------------------------------------------------------