From 990e16c6e7e907d85f629e5de9242f900ba0ca91 Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Thu, 14 Oct 2010 14:43:56 +0200 Subject: [PATCH] Eradicated Flavour and PodTemplate classes (for the latter, use Pod fields instead); Added a code analyser; Groups can now be slaves in master/slaves relationships; Refs have more params (show a confirmation popup before adding an object, add an object without creation form); Code for Refs has been refactored to comply with the new way to organize Types; Added a WebDAV client library. --- bin/generate.py | 3 + bin/publish.py | 5 +- gen/__init__.py | 158 +++++++++--- gen/generator.py | 14 +- gen/plone25/descriptors.py | 86 ++----- gen/plone25/generator.py | 177 +++++-------- gen/plone25/installer.py | 87 ++----- gen/plone25/mixins/ClassMixin.py | 7 - gen/plone25/mixins/FlavourMixin.py | 263 -------------------- gen/plone25/mixins/PodTemplateMixin.py | 7 - gen/plone25/mixins/ToolMixin.py | 260 ++++++++++++------- gen/plone25/mixins/UserMixin.py | 7 - gen/plone25/mixins/__init__.py | 234 ++++------------- gen/plone25/model.py | 129 ++++------ gen/plone25/skin/ajax.pt | 2 +- gen/plone25/skin/edit.pt | 1 - gen/plone25/skin/import.pt | 3 +- gen/plone25/skin/macros.pt | 11 +- gen/plone25/skin/page.pt | 101 ++++---- gen/plone25/skin/portlet.pt | 35 +-- gen/plone25/skin/query.pt | 6 +- gen/plone25/skin/search.pt | 6 +- gen/plone25/skin/view.pt | 3 +- gen/plone25/skin/widgets/action.pt | 5 +- gen/plone25/skin/widgets/pod.pt | 4 +- gen/plone25/skin/widgets/ref.pt | 50 ++-- gen/plone25/skin/widgets/show.pt | 7 +- gen/plone25/templates/ArchetypesTemplate.py | 4 +- gen/plone25/templates/FlavourTemplate.py | 36 --- gen/plone25/templates/PodTemplate.py | 34 --- gen/plone25/templates/Portlet.pt | 6 +- gen/plone25/templates/Styles.css.dtml | 26 +- gen/plone25/templates/ToolTemplate.py | 1 + gen/plone25/templates/UserTemplate.py | 8 +- gen/plone25/templates/appyWrappers.py | 9 - gen/plone25/templates/config.py | 2 +- gen/plone25/workflow.py | 5 +- gen/plone25/wrappers/FlavourWrapper.py | 86 ------- gen/plone25/wrappers/PodTemplateWrapper.py | 6 - gen/plone25/wrappers/ToolWrapper.py | 61 +++++ gen/plone25/wrappers/UserWrapper.py | 21 +- gen/plone25/wrappers/__init__.py | 30 +-- gen/po.py | 28 ++- gen/test/applications/AppyCar/__init__.py | 3 - gen/test/applications/ZopeComponent.py | 10 - shared/dav.py | 107 ++++++++ shared/utils.py | 149 +++++++++++ 47 files changed, 1006 insertions(+), 1297 deletions(-) delete mode 100644 gen/plone25/mixins/ClassMixin.py delete mode 100644 gen/plone25/mixins/FlavourMixin.py delete mode 100644 gen/plone25/mixins/PodTemplateMixin.py delete mode 100644 gen/plone25/mixins/UserMixin.py delete mode 100644 gen/plone25/templates/FlavourTemplate.py delete mode 100644 gen/plone25/templates/PodTemplate.py delete mode 100644 gen/plone25/wrappers/FlavourWrapper.py delete mode 100644 gen/plone25/wrappers/PodTemplateWrapper.py create mode 100644 shared/dav.py diff --git a/bin/generate.py b/bin/generate.py index b46d813..0347af2 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -4,6 +4,7 @@ import sys, os.path from optparse import OptionParser from appy.gen.generator import GeneratorError +from appy.shared.utils import LinesCounter # ------------------------------------------------------------------------------ ERROR_CODE = 1 @@ -104,6 +105,8 @@ class GeneratorScript: self.manageArgs(optParser, options, args) print 'Generating %s product in %s...' % (args[1], args[2]) self.generateProduct(options, *args) + # Give the user some statistics about its code + LinesCounter(args[0]).run() except GeneratorError, ge: sys.stderr.write(str(ge)) sys.stderr.write('\n') diff --git a/bin/publish.py b/bin/publish.py index e170667..44c09a9 100755 --- a/bin/publish.py +++ b/bin/publish.py @@ -1,8 +1,9 @@ #!/usr/bin/python2.4.4 # Imports ---------------------------------------------------------------------- import os, os.path, shutil, re, zipfile, sys, ftplib, time +import appy from appy.shared import appyPath -from appy.shared.utils import FolderDeleter +from appy.shared.utils import FolderDeleter, LinesCounter from appy.bin.clean import Cleaner from appy.gen.utils import produceNiceMessage @@ -432,6 +433,8 @@ class Publisher: def run(self): Cleaner().run(verbose=False) + # Perform a small analysis on the Appy code + LinesCounter(appy).run() print 'Generating site in %s...' % self.genFolder self.prepareGenFolder() self.createDocToc() diff --git a/gen/__init__.py b/gen/__init__.py index 6ff1c3d..fd1a03b 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -5,7 +5,7 @@ from appy.gen.layout import Table from appy.gen.layout import defaultFieldLayouts from appy.gen.po import PoMessage from appy.gen.utils import sequenceTypes, PageDescr, GroupDescr, Keywords, \ - FileWrapper, getClassName + FileWrapper, getClassName, SomeObjects from appy.shared.data import languages # Default Appy permissions ----------------------------------------------------- @@ -29,10 +29,10 @@ class Page: class Group: '''Used for describing a group of widgets within a page.''' - def __init__(self, name, columns=['100%'], wide=True, style='fieldset', + def __init__(self, name, columns=['100%'], wide=True, style='section2', hasLabel=True, hasDescr=False, hasHelp=False, hasHeaders=False, group=None, colspan=1, align='center', - valign='top'): + valign='top', css_class='', master=None, masterValue=None): self.name = name # In its simpler form, field "columns" below can hold a list or tuple # of column widths expressed as strings, that will be given as is in @@ -77,6 +77,26 @@ class Group: self.columns = self.columns[:1] # Header labels will be used as labels for the tabs. self.hasHeaders = True + self.css_class = css_class + self.master = None + self.masterValue = None + if self.master: + self._addMaster(self, master, masterValue) + + def _addMaster(self, master, masterValue): + '''Specifies this group being a slave of another field: we will add css + classes allowing to show/hide, in Javascript, its widget according + to master value.''' + self.master = master + self.masterValue = masterValue + classes = 'slave_%s' % self.master.id + if type(self.masterValue) not in sequenceTypes: + masterValues = [self.masterValue] + else: + masterValues = self.masterValue + for masterValue in masterValues: + classes += ' slaveValue_%s_%s' % (self.master.id, masterValue) + self.css_class += ' ' + classes def _setColumns(self): '''Standardizes field "columns" as a list of Column instances. Indeed, @@ -416,13 +436,12 @@ class Type: '''When displaying p_obj on a given p_layoutType, must we show this field?''' isEdit = layoutType == 'edit' - # Do not show field if it is optional and not selected in flavour + # Do not show field if it is optional and not selected in tool if self.optional: - tool = obj.getTool() - flavour = tool.getFlavour(obj, appy=True) - flavourAttrName = 'optionalFieldsFor%s' % obj.meta_type - flavourAttrValue = getattr(flavour, flavourAttrName, ()) - if self.name not in flavourAttrValue: + tool = obj.getTool().appy() + fieldName = 'optionalFieldsFor%s' % obj.meta_type + fieldValue = getattr(tool, fieldName, ()) + if self.name not in fieldValue: return False # Check if the user has the permission to view or edit the field user = obj.portal_membership.getAuthenticatedMember() @@ -568,11 +587,10 @@ class Type: return self.default(obj.appy()) else: return self.default - # If value is editable, get the default value from the flavour + # If value is editable, get the default value from the tool portalTypeName = obj._appy_getPortalType(obj.REQUEST) - tool = obj.getTool() - flavour = tool.getFlavour(portalTypeName, appy=True) - return getattr(flavour, 'defaultValueFor%s' % self.labelId) + tool = obj.getTool().appy() + return getattr(tool, 'defaultValueFor%s' % self.labelId) return value def getFormattedValue(self, obj, value): @@ -1188,17 +1206,24 @@ class File(Type): class Ref(Type): def __init__(self, klass=None, attribute=None, validator=None, multiplicity=(0,1), index=None, default=None, optional=False, - editDefault=False, add=False, link=True, unlink=False, - back=None, show=True, page='main', group=None, layouts=None, - showHeaders=False, shownInfo=(), select=None, maxPerPage=30, - move=0, indexed=False, searchable=False, - specificReadPermission=False, specificWritePermission=False, - width=None, height=None, colspan=1, master=None, - masterValue=None, focus=False, historized=False): + editDefault=False, add=False, addConfirm=False, noForm=False, + link=True, unlink=False, back=None, show=True, page='main', + group=None, layouts=None, showHeaders=False, shownInfo=(), + select=None, maxPerPage=30, move=0, indexed=False, + searchable=False, specificReadPermission=False, + specificWritePermission=False, width=None, height=None, + colspan=1, master=None, masterValue=None, focus=False, + historized=False): self.klass = klass self.attribute = attribute # May the user add new objects through this ref ? self.add = add + # When the user adds a new object, must a confirmation popup be shown? + self.addConfirm = addConfirm + # If noForm is True, when clicking to create an object through this ref, + # the object will be created automatically, and no creation form will + # be presented to the user. + self.noForm = noForm # May the user link existing objects through this ref? self.link = link # May the user unlink existing objects? @@ -1246,12 +1271,70 @@ class Ref(Type): return obj.getBRefs(self.relationship) return res - def getValue(self, obj): + def getValue(self, obj, type='objects', noListIfSingleObj=False, + startNumber=None, someObjects=False): + '''Returns the objects linked to p_obj through Ref field "self". + - If p_type is "objects", it returns the Appy wrappers; + - If p_type is "zobjects", it returns the Zope objects; + - If p_type is "uids", it returns UIDs of objects (= strings). + + + * If p_startNumber is None, it returns all referred objects. + * If p_startNumber is a number, it returns self.maxPerPage objects, + starting at p_startNumber. + + If p_noListIfSingleObj is True, it returns the single reference as + an object and not as a list. + + If p_someObjects is True, it returns an instance of SomeObjects + instead of returning a list of references.''' if self.isBack: - return obj._appy_getRefsBack(self.name, self.relationship, - noListIfSingleObj=True) + getRefs = obj.reference_catalog.getBackReferences + uids = [r.sourceUID for r in getRefs(obj, self.relationship)] else: - return obj._appy_getRefs(self.name, noListIfSingleObj=True).objects + uids = obj._appy_getSortedField(self.name) + batchNeeded = startNumber != None + exec 'refUids = obj.getRaw%s%s()' % (self.name[0].upper(), + self.name[1:]) + # There may be too much UIDs in sortedField because these fields + # are not updated when objects are deleted. So we do it now. + # TODO: do such cleaning on object deletion ? + toDelete = [] + for uid in uids: + if uid not in refUids: + toDelete.append(uid) + for uid in toDelete: + uids.remove(uid) + # Prepare the result: an instance of SomeObjects, that, in this case, + # represent a subset of all referred objects + res = SomeObjects() + res.totalNumber = res.batchSize = len(uids) + batchNeeded = startNumber != None + if batchNeeded: + res.batchSize = self.maxPerPage + if startNumber != None: + res.startNumber = startNumber + # Get the needed referred objects + i = res.startNumber + # Is it possible and more efficient to perform a single query in + # uid_catalog and get the result in the order of specified uids? + while i < (res.startNumber + res.batchSize): + if i >= res.totalNumber: break + # Retrieve every reference in the correct format according to p_type + if type == 'uids': + ref = uids[i] + else: + ref = obj.uid_catalog(UID=uids[i])[0].getObject() + if type == 'objects': + ref = ref.appy() + res.objects.append(ref) + i += 1 + # Manage parameter p_noListIfSingleObj + if res.objects and noListIfSingleObj: + if self.multiplicity[1] == 1: + res.objects = res.objects[0] + if someObjects: return res + return res.objects def getFormattedValue(self, obj, value): return value @@ -1281,6 +1364,24 @@ class Ref(Type): elif nbOfRefs > maxRef: return obj.translate('max_ref_violated') + def store(self, obj, value): + '''Stores on p_obj, the p_value, which can be None, an object UID or a + list of UIDs coming from the request. This method is only called for + Ref fields with link=True.''' + # Security check + if not self.isShowable(obj, 'edit'): return + # Standardize the way p_value is expressed + uids = value + if not value: uids = [] + if isinstance(value, basestring): uids = [value] + # Update the field storing on p_obj the ordered list of UIDs + sortedRefs = obj._appy_getSortedField(self.name) + del sortedRefs[:] + for uid in uids: sortedRefs.append(uid) + # Update the refs + refs = [obj.uid_catalog(UID=uid)[0].getObject() for uid in uids] + exec 'obj.set%s%s(refs)' % (self.name[0].upper(), self.name[1:]) + class Computed(Type): def __init__(self, validator=None, multiplicity=(0,1), index=None, default=None, optional=False, editDefault=False, show='view', @@ -1645,10 +1746,6 @@ class Model: pass class Tool(Model): '''If you want so define a custom tool class, she must inherit from this class.''' -class Flavour(Model): - '''A flavour represents a given group of configuration options. If you want - to define a custom flavour class, she must inherit from this class.''' - def __init__(self, name): self.name = name class User(Model): '''If you want to extend or modify the User class, subclass me.''' @@ -1692,11 +1789,6 @@ class Config: # If you don't need the portlet that appy.gen has generated for your # application, set the following parameter to False. self.showPortlet = True - # Default number of flavours. It will be used for generating i18n labels - # for classes in every flavour. Indeed, every flavour can name its - # concepts differently. For example, class Thing in flavour 2 may have - # i18n label "MyProject_Thing_2". - self.numberOfFlavours = 2 # ------------------------------------------------------------------------------ # Special field "type" is mandatory for every class. If one class does not diff --git a/gen/generator.py b/gen/generator.py index ebd53f2..a049b3c 100644 --- a/gen/generator.py +++ b/gen/generator.py @@ -1,6 +1,6 @@ # ------------------------------------------------------------------------------ import os, os.path, sys, parser, symbol, token, types -from appy.gen import Type, State, Config, Tool, Flavour, User +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 @@ -133,8 +133,7 @@ class Generator: # Default descriptor classes self.descriptorClasses = { 'class': ClassDescriptor, 'tool': ClassDescriptor, - 'flavour': ClassDescriptor, 'user': ClassDescriptor, - 'workflow': WorkflowDescriptor} + 'user': ClassDescriptor, 'workflow': WorkflowDescriptor} # The following dict contains a series of replacements that need to be # applied to file templates to generate files. self.repls = {'applicationName': self.applicationName, @@ -143,7 +142,6 @@ class Generator: # List of Appy classes and workflows found in the application self.classes = [] self.tool = None - self.flavour = None self.user = None self.workflows = [] self.initialize() @@ -224,19 +222,13 @@ class Generator: # of their definition). attrs = astClasses[moduleElem.__name__].attributes if appyType == 'class': - # Determine the class type (standard, tool, flavour...) + # Determine the class type (standard, tool, user...) if issubclass(moduleElem, Tool): if not self.tool: klass = self.descriptorClasses['tool'] self.tool = klass(moduleElem, attrs, self) else: self.tool.update(moduleElem, attrs) - elif issubclass(moduleElem, Flavour): - if not self.flavour: - klass = self.descriptorClasses['flavour'] - self.flavour = klass(moduleElem, attrs, self) - else: - self.flavour.update(moduleElem, attrs) elif issubclass(moduleElem, User): if not self.user: klass = self.descriptorClasses['user'] diff --git a/gen/plone25/descriptors.py b/gen/plone25/descriptors.py index b1d1fc9..dd8717d 100644 --- a/gen/plone25/descriptors.py +++ b/gen/plone25/descriptors.py @@ -6,7 +6,7 @@ # ------------------------------------------------------------------------------ import types, copy -from model import ModelClass, Flavour, flavourAttributePrefixes +from model import ModelClass, Tool, toolFieldPrefixes from utils import stringify import appy.gen import appy.gen.descriptors @@ -43,12 +43,13 @@ class FieldDescriptor: def __repr__(self): return '' % (self.fieldName, self.classDescr) - def getFlavourAttributeMessage(self, fieldName): - '''Some attributes generated on the Flavour class need a specific + 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 flavourAttributePrefixes: - if fieldName.startswith(prefix): + for prefix in toolFieldPrefixes: + fullPrefix = prefix + 'For' + if fieldName.startswith(fullPrefix): messageId = 'MSG_%s' % prefix res = getattr(PoMessage, messageId) if res.find('%s') != -1: @@ -66,8 +67,8 @@ class FieldDescriptor: produceNice = True default = self.fieldName # Some attributes need a specific predefined message - if isinstance(self.classDescr, FlavourClassDescriptor): - default = self.getFlavourAttributeMessage(self.fieldName) + if isinstance(self.classDescr, ToolClassDescriptor): + default = self.getToolFieldMessage(self.fieldName) if default != self.fieldName: produceNice = False msg = PoMessage(msgId, '', default) if produceNice: @@ -88,9 +89,7 @@ class FieldDescriptor: self.generator.labels.append(poMsg) def walkAction(self): - '''How to generate an action field ? We generate an Archetypes String - field.''' - # Add action-specific i18n messages + '''Generates the i18n-related labels.''' for suffix in ('ok', 'ko'): label = '%s_%s_action_%s' % (self.classDescr.name, self.fieldName, suffix) @@ -98,6 +97,10 @@ class FieldDescriptor: getattr(PoMessage, 'ACTION_%s' % suffix.upper())) self.generator.labels.append(msg) self.classDescr.labelsToPropagate.append(msg) + 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?''' @@ -115,6 +118,11 @@ class FieldDescriptor: 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 @@ -123,8 +131,8 @@ class FieldDescriptor: msg = PoMessage(label, '', PoMessage.POD_ASKACTION) self.generator.labels.append(msg) self.classDescr.labelsToPropagate.append(msg) - # Add the POD-related fields on the Flavour - Flavour._appy_addPodRelatedFields(self) + # Add the POD-related fields on the Tool + Tool._appy_addPodRelatedFields(self) notToValidateFields = ('Info', 'Computed', 'Action', 'Pod') def walkAppyType(self): @@ -133,10 +141,10 @@ class FieldDescriptor: # Manage things common to all Appy types # - optional ? if self.appyType.optional: - Flavour._appy_addOptionalField(self) + Tool._appy_addOptionalField(self) # - edit default value ? if self.appyType.editDefault: - Flavour._appy_addDefaultField(self) + Tool._appy_addDefaultField(self) # - put an index on this field? if self.appyType.indexed and \ (self.fieldName not in ('title', 'description')): @@ -229,8 +237,8 @@ class ClassDescriptor(appy.gen.descriptors.ClassDescriptor): # (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.flavourFieldsToPropagate = [] # For this class, some fields have - # been defined on the Flavour class. Those fields need to be defined + 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. @@ -251,7 +259,7 @@ class ClassDescriptor(appy.gen.descriptors.ClassDescriptor): def generateSchema(self, configClass=False): '''Generates the corresponding Archetypes schema in self.schema. If we are generating a schema for a class that is in the configuration - (tool, flavour, etc) we must avoid having attributes that rely on + (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: @@ -286,13 +294,6 @@ class ClassDescriptor(appy.gen.descriptors.ClassDescriptor): res = self.klass.__dict__['root'] return res - def isPod(self): - '''May this class be associated with POD templates?.''' - res = False - if self.klass.__dict__.has_key('pod') and self.klass.__dict__['pod']: - res = True - 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.''' @@ -375,6 +376,7 @@ 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.attributesByClass = klass._appy_classes self.modelClass = self.klass self.predefined = True self.customized = False @@ -395,42 +397,6 @@ class ToolClassDescriptor(ClassDescriptor): def generateSchema(self): ClassDescriptor.generateSchema(self, configClass=True) -class FlavourClassDescriptor(ClassDescriptor): - '''Represents an Archetypes-compliant class that corresponds to the Flavour - for the generated application.''' - def __init__(self, klass, generator): - ClassDescriptor.__init__(self,klass,klass._appy_attributes[:],generator) - self.attributesByClass = klass._appy_classes - self.modelClass = self.klass - self.predefined = True - self.customized = False - def getParents(self, allClasses=()): - res = ['Flavour'] - 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 flavour - definition. We must then add the custom flavour elements in this - default Flavour 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) - -class PodTemplateClassDescriptor(ClassDescriptor): - '''Represents a POD template.''' - 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 ['PodTemplate'] - def isRoot(self): return False - class UserClassDescriptor(ClassDescriptor): '''Represents an Archetypes-compliant class that corresponds to the User for the generated application.''' diff --git a/gen/plone25/generator.py b/gen/plone25/generator.py index e838af4..9cd2802 100644 --- a/gen/plone25/generator.py +++ b/gen/plone25/generator.py @@ -8,11 +8,8 @@ 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 model import ModelClass, PodTemplate, User, Flavour, Tool -from descriptors import FieldDescriptor, ClassDescriptor, \ - WorkflowDescriptor, ToolClassDescriptor, \ - FlavourClassDescriptor, PodTemplateClassDescriptor, \ - UserClassDescriptor +from model import ModelClass, User, Tool +from descriptors import * # Common methods that need to be defined on every Archetype class -------------- COMMON_METHODS = ''' @@ -29,22 +26,18 @@ class Generator(AbstractGenerator): poExtensions = ('.po', '.pot') def __init__(self, *args, **kwargs): - Flavour._appy_clean() + Tool._appy_clean() AbstractGenerator.__init__(self, *args, **kwargs) # Set our own Descriptor classes self.descriptorClasses['class'] = ClassDescriptor self.descriptorClasses['workflow'] = WorkflowDescriptor - # Create our own Tool, Flavour and PodTemplate instances + # Create our own Tool and User instances self.tool = ToolClassDescriptor(Tool, self) - self.flavour = FlavourClassDescriptor(Flavour, self) - self.podTemplate = PodTemplateClassDescriptor(PodTemplate, self) self.user = UserClassDescriptor(User, self) # i18n labels to generate self.labels = [] # i18n labels self.toolName = '%sTool' % self.applicationName - self.flavourName = '%sFlavour' % self.applicationName self.toolInstanceName = 'portal_%s' % self.applicationName.lower() - self.podTemplateName = '%sPodTemplate' % self.applicationName self.userName = '%sUser' % self.applicationName self.portletName = '%s_portlet' % self.applicationName.lower() self.queryName = '%s_query' % self.applicationName.lower() @@ -55,10 +48,9 @@ class Generator(AbstractGenerator): commonMethods = COMMON_METHODS % \ (self.toolInstanceName, self.applicationName) self.repls.update( - {'toolName': self.toolName, 'flavourName': self.flavourName, - 'portletName': self.portletName, 'queryName': self.queryName, + {'toolName': self.toolName, 'portletName': self.portletName, + 'queryName': self.queryName, 'userName': self.userName, 'toolInstanceName': self.toolInstanceName, - 'podTemplateName': self.podTemplateName, 'userName': self.userName, 'commonMethods': commonMethods}) self.referers = {} @@ -145,7 +137,6 @@ class Generator(AbstractGenerator): msg('goto_last', '', msg.GOTO_LAST), msg('goto_source', '', msg.GOTO_SOURCE), msg('whatever', '', msg.WHATEVER), - msg('confirm', '', msg.CONFIRM), msg('yes', '', msg.YES), msg('no', '', msg.NO), msg('field_required', '', msg.FIELD_REQUIRED), @@ -321,15 +312,14 @@ class Generator(AbstractGenerator): appClasses.append('%s.%s' % (k.__module__, k.__name__)) repls['appClasses'] = "[%s]" % ','.join(appClasses) # Compute lists of class names - allClassNames = '"%s",' % self.flavourName - allClassNames += '"%s",' % self.podTemplateName + allClassNames = '"%s",' % self.userName appClassNames = ','.join(['"%s"' % c.name for c in self.classes]) allClassNames += appClassNames repls['allClassNames'] = allClassNames repls['appClassNames'] = appClassNames # Compute classes whose instances must not be catalogued. catalogMap = '' - blackClasses = [self.toolName, self.flavourName, self.podTemplateName] + blackClasses = [self.toolName] for blackClass in blackClasses: catalogMap += "catalogMap['%s'] = {}\n" % blackClass catalogMap += "catalogMap['%s']['black'] = " \ @@ -464,28 +454,30 @@ class Generator(AbstractGenerator): repls['workflows'] = workflows self.copyFile('workflows.py', repls, destFolder='Extensions') - def generateWrapperProperty(self, name): + def generateWrapperProperty(self, name, type): '''Generates the getter for attribute p_name.''' res = ' def get_%s(self):\n ' % name if name == 'title': res += 'return self.o.Title()\n' else: - res += 'return self.o.getAppyType("%s").getValue(self.o)\n' % name + suffix = '' + if type == 'Ref': suffix = ', noListIfSingleObj=True' + res += 'return self.o.getAppyType("%s").getValue(self.o%s)\n' % \ + (name, suffix) res += ' %s = property(get_%s)\n\n' % (name, name) 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, flavour, etc); if - p_include is "allButTool", it includes the same descriptors, the - tool excepted; if p_include is "custom", it includes descriptors - for the config-related classes for which the user has created a - sub-class.''' + for the config-related classes (tool, user, etc); if p_include is + "allButTool", it includes the same descriptors, the tool excepted; + if p_include is "custom", it includes descriptors for the + config-related classes for which the user has created a sub-class.''' if not include: return self.classes else: res = self.classes[:] - configClasses = [self.tool,self.flavour,self.podTemplate,self.user] + configClasses = [self.tool, self.user] if include == 'all': res += configClasses elif include == 'allButTool': @@ -547,24 +539,25 @@ class Generator(AbstractGenerator): except AttributeError: attrValue = getattr(c.modelClass, attrName) if isinstance(attrValue, Type): - wrapperDef += self.generateWrapperProperty(attrName) + wrapperDef += self.generateWrapperProperty(attrName, + attrValue.type) # Generate properties for back references if self.referers.has_key(c.name): for refDescr, rel in self.referers[c.name]: attrName = refDescr.appyType.back.attribute - wrapperDef += self.generateWrapperProperty(attrName) + wrapperDef += self.generateWrapperProperty(attrName, 'Ref') if not titleFound: # Implicitly, the title will be added by Archetypes. So I need # to define a property for it. - wrapperDef += self.generateWrapperProperty('title') + wrapperDef += self.generateWrapperProperty('title', 'String') if c.customized: - # For custom tool and flavour, add a call to a method that - # allows to customize elements from the base class. + # 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 and flavour, add security declaration that - # will allow to call their methods from ZPTs. + # 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" \ @@ -576,8 +569,6 @@ class Generator(AbstractGenerator): repls['imports'] = '\n'.join(imports) repls['wrappers'] = '\n'.join(wrappers) repls['toolBody'] = Tool._appy_getBody() - repls['flavourBody'] = Flavour._appy_getBody() - repls['podTemplateBody'] = PodTemplate._appy_getBody() repls['userBody'] = User._appy_getBody() self.copyFile('appyWrappers.py', repls, destFolder='Extensions') @@ -613,72 +604,49 @@ class Generator(AbstractGenerator): def generateTool(self): '''Generates the Plone tool that corresponds to this application.''' - # Generate the tool class in itself and related i18n messages - t = self.toolName Msg = PoMessage - repls = self.repls.copy() - # Manage predefined fields - Tool.flavours.klass = Flavour - if self.flavour.customized: - Tool.flavours.klass = self.flavour.klass + # Create Tool-related i18n-related messages + self.labels += [ + Msg(self.toolName, '', Msg.CONFIG % self.applicationName), + Msg('%s_edit_descr' % self.toolName, '', ' ')] + + # Tune the Ref field between Tool and User Tool.users.klass = User if self.user.customized: Tool.users.klass = self.user.klass + + # Before generating the Tool class, finalize it with query result + # columns, with fields to propagate, workflow-related fields. + for classDescr in self.classes: + for fieldName, fieldType in classDescr.toolFieldsToPropagate: + for childDescr in classDescr.getChildren(): + childFieldName = fieldName % childDescr.name + fieldType.group = childDescr.klass.__name__ + Tool._appy_addField(childFieldName, fieldType, childDescr) + if classDescr.isRoot(): + # We must be able to configure query results from the tool. + Tool._appy_addQueryResultColumns(classDescr) + # Add the search-related fields. + Tool._appy_addSearchRelatedFields(classDescr) + importMean = classDescr.getCreateMean('Import') + if importMean: + Tool._appy_addImportRelatedFields(classDescr) + Tool._appy_addWorkflowFields(self.user) + # Complete self.tool.orderedAttributes from the attributes that we + # just added to the Tool model class. + for fieldName in Tool._appy_attributes: + if fieldName not in self.tool.orderedAttributes: + self.tool.orderedAttributes.append(fieldName) self.tool.generateSchema() + + # Generate the Tool class + repls = self.repls.copy() + repls['metaTypes'] = [c.name for c in self.classes] repls['fields'] = self.tool.schema repls['methods'] = self.tool.methods repls['wrapperClass'] = '%s_Wrapper' % self.tool.name self.copyFile('ToolTemplate.py', repls, destName='%s.py'% self.toolName) - repls = self.repls.copy() - # Create i18n-related messages - self.labels += [ - Msg(self.toolName, '', Msg.CONFIG % self.applicationName), - Msg('%s_edit_descr' % self.toolName, '', ' ')] - # Before generating the Flavour class, finalize it with query result - # columns, with fields to propagate, workflow-related fields. - for classDescr in self.classes: - for fieldName, fieldType in classDescr.flavourFieldsToPropagate: - for childDescr in classDescr.getChildren(): - childFieldName = fieldName % childDescr.name - fieldType.group = childDescr.klass.__name__ - Flavour._appy_addField(childFieldName,fieldType,childDescr) - if classDescr.isRoot(): - # We must be able to configure query results from the flavour. - Flavour._appy_addQueryResultColumns(classDescr) - # Add the search-related fields. - Flavour._appy_addSearchRelatedFields(classDescr) - importMean = classDescr.getCreateMean('Import') - if importMean: - Flavour._appy_addImportRelatedFields(classDescr) - Flavour._appy_addWorkflowFields(self.flavour) - Flavour._appy_addWorkflowFields(self.podTemplate) - Flavour._appy_addWorkflowFields(self.user) - # Complete self.flavour.orderedAttributes from the attributes that we - # just added to the Flavour model class. - for fieldName in Flavour._appy_attributes: - if fieldName not in self.flavour.orderedAttributes: - self.flavour.orderedAttributes.append(fieldName) - # Generate the flavour class and related i18n messages - self.flavour.generateSchema() - self.labels += [ Msg(self.flavourName, '', Msg.FLAVOUR), - Msg('%s_edit_descr' % self.flavourName, '', ' ')] - repls = self.repls.copy() - repls['fields'] = self.flavour.schema - repls['methods'] = self.flavour.methods - repls['wrapperClass'] = '%s_Wrapper' % self.flavour.name - repls['metaTypes'] = [c.name for c in self.classes] - self.copyFile('FlavourTemplate.py', repls, - destName='%s.py'% self.flavourName) - # Generate the PodTemplate class - self.podTemplate.generateSchema() - self.labels += [ Msg(self.podTemplateName, '', Msg.POD_TEMPLATE), - Msg('%s_edit_descr' % self.podTemplateName, '', ' ')] - repls = self.repls.copy() - repls['fields'] = self.podTemplate.schema - repls['methods'] = self.podTemplate.methods - repls['wrapperClass'] = '%s_Wrapper' % self.podTemplate.name - self.copyFile('PodTemplate.py', repls, - destName='%s.py' % self.podTemplateName) + # Generate the User class self.user.generateSchema() self.labels += [ Msg(self.userName, '', Msg.USER), @@ -687,32 +655,28 @@ class Generator(AbstractGenerator): repls['fields'] = self.user.schema repls['methods'] = self.user.methods repls['wrapperClass'] = '%s_Wrapper' % self.user.name - self.copyFile('UserTemplate.py', repls, - destName='%s.py' % self.userName) + self.copyFile('UserTemplate.py', repls,destName='%s.py' % self.userName) def generateClass(self, classDescr): '''Is called each time an Appy class is found in the application, for generating the corresponding Archetype class and schema.''' k = classDescr.klass print 'Generating %s.%s (gen-class)...' % (k.__module__, k.__name__) - # Add, for this class, the needed configuration attributes on Flavour - if classDescr.isPod(): - Flavour._appy_addPodField(classDescr) if not classDescr.isAbstract(): - Flavour._appy_addWorkflowFields(classDescr) + Tool._appy_addWorkflowFields(classDescr) # Determine base archetypes schema and class baseClass = 'BaseContent' baseSchema = 'BaseSchema' if classDescr.isFolder(): baseClass = 'OrderedBaseFolder' baseSchema = 'OrderedBaseFolderSchema' - parents = [baseClass, 'ClassMixin'] + parents = [baseClass, 'BaseMixin'] imports = [] implements = [baseClass] for baseClass in classDescr.klass.__bases__: if self.determineAppyType(baseClass) == 'class': bcName = getClassName(baseClass) - parents.remove('ClassMixin') + parents.remove('BaseMixin') parents.append(bcName) implements.append(bcName) imports.append('from %s import %s' % (bcName, bcName)) @@ -750,19 +714,6 @@ class Generator(AbstractGenerator): classDescr.klass.__name__+'s') poMsgPl.produceNiceDefault() self.labels.append(poMsgPl) - # Create i18n labels for flavoured variants - for i in range(2, self.config.numberOfFlavours+1): - poMsg = PoMessage('%s_%d' % (classDescr.name, i), '', - classDescr.klass.__name__) - poMsg.produceNiceDefault() - self.labels.append(poMsg) - poMsgDescr = PoMessage('%s_%d_edit_descr' % (classDescr.name, i), - '', ' ') - self.labels.append(poMsgDescr) - poMsgPl = PoMessage('%s_%d_plural' % (classDescr.name, i), '', - 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) diff --git a/gen/plone25/installer.py b/gen/plone25/installer.py index b8d0bd8..4f4e861 100644 --- a/gen/plone25/installer.py +++ b/gen/plone25/installer.py @@ -144,7 +144,6 @@ class PloneInstaller: appFolder = getattr(site, self.productName) for className in self.config.rootClasses: permission = self.getAddPermission(className) - print 'Permission is', permission appFolder.manage_permission(permission, (), acquire=0) else: appFolder = getattr(site, self.productName) @@ -221,78 +220,33 @@ class PloneInstaller: current_catalogs.remove(catalog) atTool.setCatalogsByType(meta_type, list(current_catalogs)) - def findPodFile(self, klass, podTemplateName): - '''Finds the file that corresponds to p_podTemplateName for p_klass.''' - res = None - exec 'import %s' % klass.__module__ - exec 'moduleFile = %s.__file__' % klass.__module__ - folderName = os.path.dirname(moduleFile) - fileName = os.path.join(folderName, '%s.odt' % podTemplateName) - if os.path.isfile(fileName): - res = fileName - return res - def updatePodTemplates(self): - '''Creates or updates the POD templates in flavours according to pod + '''Creates or updates the POD templates in the tool according to pod declarations in the application classes.''' - # Creates or updates the old-way class-related templates - i = -1 - for klass in self.appClasses: - i += 1 - if klass.__dict__.has_key('pod'): - pod = getattr(klass, 'pod') - if isinstance(pod, bool): - podTemplates = [klass.__name__] - else: - podTemplates = pod - for templateName in podTemplates: - fileName = self.findPodFile(klass, templateName) - if fileName: - # Create the corresponding PodTemplate in all flavours - for flavour in self.appyTool.flavours: - podId='%s_%s' % (self.appClassNames[i],templateName) - podAttr = 'podTemplatesFor%s'% self.appClassNames[i] - allPodTemplates = getattr(flavour, podAttr) - if allPodTemplates: - if isinstance(allPodTemplates, list): - allIds = [p.id for p in allPodTemplates] - else: - allIds = [allPodTemplates.id] - else: - allIds = [] - if podId not in allIds: - # Create a PodTemplate instance - f = file(fileName) - flavour.create(podAttr, id=podId, podTemplate=f, - title=produceNiceMessage(templateName)) - f.close() - # Creates the new-way templates for Pod fields if they do not exist. + # Creates the templates for Pod fields if they do not exist. for contentType, appyTypes in self.attributes.iteritems(): appyClass = self.tool.getAppyClass(contentType) if not appyClass: continue # May be an abstract class for appyType in appyTypes: - if appyType.type == 'Pod': - # For every flavour, find the attribute that stores the - # template, and store on it the default one specified in - # the appyType if no template is stored yet. - for flavour in self.appyTool.flavours: - attrName = flavour.getAttributeName( - 'podTemplate', appyClass, appyType.name) - fileObject = getattr(flavour, attrName) - if not fileObject or (fileObject.size == 0): - # There is no file. Put the one specified in the - # appyType. - fileName=os.path.join(self.appyTool.getDiskFolder(), - appyType.template) - if os.path.exists(fileName): - setattr(flavour, attrName, fileName) - else: - self.appyTool.log( - 'Template "%s" was not found!' % \ - fileName, type='error') + if appyType.type != 'Pod': continue + # Find the attribute that stores the template, and store on + # it the default one specified in the appyType if no + # template is stored yet. + attrName = self.appyTool.getAttributeName( + 'podTemplate', appyClass, appyType.name) + fileObject = getattr(self.appyTool, attrName) + if not fileObject or (fileObject.size == 0): + # There is no file. Put the one specified in the appyType. + fileName = os.path.join(self.appyTool.getDiskFolder(), + appyType.template) + if os.path.exists(fileName): + setattr(self.appyTool, attrName, fileName) + else: + self.appyTool.log('Template "%s" was not found!' % \ + fileName, type='error') def installTool(self): - '''Configures the application tool and flavours.''' + '''Configures the application tool.''' # Register the tool in Plone try: self.ploneSite.manage_addProduct[ @@ -333,9 +287,6 @@ class PloneInstaller: else: self.tool.createOrUpdate(True, None) - if not self.appyTool.flavours: - # Create the default flavour - self.appyTool.create('flavours', title=self.productName, number=1) self.updatePodTemplates() # Uncatalog tool diff --git a/gen/plone25/mixins/ClassMixin.py b/gen/plone25/mixins/ClassMixin.py deleted file mode 100644 index fcd871f..0000000 --- a/gen/plone25/mixins/ClassMixin.py +++ /dev/null @@ -1,7 +0,0 @@ -# ------------------------------------------------------------------------------ -from appy.gen.plone25.mixins import AbstractMixin - -# ------------------------------------------------------------------------------ -class ClassMixin(AbstractMixin): - _appy_meta_type = 'Class' -# ------------------------------------------------------------------------------ diff --git a/gen/plone25/mixins/FlavourMixin.py b/gen/plone25/mixins/FlavourMixin.py deleted file mode 100644 index 8f65177..0000000 --- a/gen/plone25/mixins/FlavourMixin.py +++ /dev/null @@ -1,263 +0,0 @@ -# ------------------------------------------------------------------------------ -import os, os.path, time, types -from StringIO import StringIO -from appy.shared import mimeTypes -from appy.shared.utils import getOsTempFolder -import appy.pod -from appy.pod.renderer import Renderer -import appy.gen -from appy.gen import Type -from appy.gen.plone25.mixins import AbstractMixin -from appy.gen.plone25.descriptors import ClassDescriptor - -# Errors ----------------------------------------------------------------------- -DELETE_TEMP_DOC_ERROR = 'A temporary document could not be removed. %s.' -POD_ERROR = 'An error occurred while generating the document. Please ' \ - 'contact the system administrator.' - -# ------------------------------------------------------------------------------ -class FlavourMixin(AbstractMixin): - _appy_meta_type = 'Flavour' - def getPortalType(self, metaTypeOrAppyClass): - '''Returns the name of the portal_type that is based on - p_metaTypeOrAppyType in this flavour.''' - return self.getParentNode().getPortalType(metaTypeOrAppyClass) - - def registerPortalTypes(self): - '''Registers, into portal_types, the portal types which are specific - to this flavour.''' - i = -1 - registeredFactoryTypes = self.portal_factory.getFactoryTypes().keys() - factoryTypesToRegister = [] - appName = self.getProductConfig().PROJECTNAME - for metaTypeName in self.allMetaTypes: - i += 1 - portalTypeName = '%s_%d' % (metaTypeName, self.number) - # If the portal type corresponding to the meta type is - # registered in portal_factory (in the model: - # use_portal_factory=True), we must also register the new - # portal_type we are currently creating. - if metaTypeName in registeredFactoryTypes: - factoryTypesToRegister.append(portalTypeName) - if not hasattr(self.portal_types, portalTypeName) and \ - hasattr(self.portal_types, metaTypeName): - # Indeed abstract meta_types have no associated portal_type - typeInfoName = "%s: %s (%s)" % (appName, metaTypeName, - metaTypeName) - self.portal_types.manage_addTypeInformation( - getattr(self.portal_types, metaTypeName).meta_type, - id=portalTypeName, typeinfo_name=typeInfoName) - # Set the human readable title explicitly - portalType = getattr(self.portal_types, portalTypeName) - portalType.title = portalTypeName - # Associate a workflow for this new portal type. - pf = self.portal_workflow - workflowChain = pf.getChainForPortalType(metaTypeName) - pf.setChainForPortalTypes([portalTypeName],workflowChain) - # Copy actions from the base portal type - basePortalType = getattr(self.portal_types, metaTypeName) - portalType._actions = tuple(basePortalType._cloneActions()) - # Copy aliases from the base portal type - portalType.setMethodAliases(basePortalType.getMethodAliases()) - # Update the factory tool with the list of types to register - self.portal_factory.manage_setPortalFactoryTypes( - listOfTypeIds=factoryTypesToRegister+registeredFactoryTypes) - - def getClassFolder(self, className): - '''Return the folder related to p_className.''' - return getattr(self, className) - - def getAvailablePodTemplates(self, obj, phase='main'): - '''Returns the POD templates which are available for generating a - document from p_obj.''' - appySelf = self.appy() - fieldName = 'podTemplatesFor%s' % obj.meta_type - res = [] - podTemplates = getattr(appySelf, fieldName, []) - if not isinstance(podTemplates, list): - podTemplates = [podTemplates] - res = [r.o for r in podTemplates if r.podPhase == phase] - hasParents = True - klass = obj.__class__ - while hasParents: - parent = klass.__bases__[-1] - if hasattr(parent, 'wrapperClass'): - fieldName = 'podTemplatesFor%s' % parent.meta_type - podTemplates = getattr(appySelf, fieldName, []) - if not isinstance(podTemplates, list): - podTemplates = [podTemplates] - res += [r.o for r in podTemplates if r.podPhase == phase] - klass = parent - else: - hasParents = False - return res - - def getMaxShownTemplates(self, obj): - attrName = 'getPodMaxShownTemplatesFor%s' % obj.meta_type - return getattr(self, attrName)() - - def getPodInfo(self, ploneObj, fieldName): - '''Returns POD-related information about Pod field p_fieldName defined - on class whose p_ploneObj is an instance of.''' - res = {} - appyClass = self.getParentNode().getAppyClass(ploneObj.meta_type) - appyFlavour = self.appy() - n = appyFlavour.getAttributeName('formats', appyClass, fieldName) - res['formats'] = getattr(appyFlavour, n) - n = appyFlavour.getAttributeName('podTemplate', appyClass, fieldName) - res['template'] = getattr(appyFlavour, n) - appyType = ploneObj.getAppyType(fieldName) - res['title'] = self.translate(appyType.labelId) - res['context'] = appyType.context - res['action'] = appyType.action - return res - - def generateDocument(self): - '''Generates the document: - - from a PodTemplate instance if it is a class-wide pod template; - - from field-related info on the flavour if it is a Pod field. - UID of object that is the template target is given in the request.''' - rq = self.REQUEST - appyTool = self.getParentNode().appy() - # Get the object - objectUid = rq.get('objectUid') - obj = self.uid_catalog(UID=objectUid)[0].getObject() - appyObj = obj.appy() - # Get information about the document to render. Information comes from - # a PodTemplate instance or from the flavour itself, depending on - # whether we generate a doc from a class-wide template or from a pod - # field. - templateUid = rq.get('templateUid', None) - specificPodContext = None - if templateUid: - podTemplate = self.uid_catalog(UID=templateUid)[0].getObject() - appyPt = podTemplate.appy() - format = podTemplate.getPodFormat() - template = appyPt.podTemplate.content - podTitle = podTemplate.Title() - doAction = False - else: - fieldName = rq.get('fieldName') - format = rq.get('podFormat') - podInfo = self.getPodInfo(obj, fieldName) - template = podInfo['template'].content - podTitle = podInfo['title'] - if podInfo['context']: - if type(podInfo['context']) == types.FunctionType: - specificPodContext = podInfo['context'](appyObj) - else: - specificPodContext = podInfo['context'] - doAction = rq.get('askAction') == 'True' - # Temporary file where to generate the result - tempFileName = '%s/%s_%f.%s' % ( - getOsTempFolder(), obj.UID(), time.time(), format) - # Define parameters to pass to the appy.pod renderer - currentUser = self.portal_membership.getAuthenticatedMember() - podContext = {'tool': appyTool, 'flavour': self.appy(), - 'user': currentUser, 'self': appyObj, - 'now': self.getProductConfig().DateTime(), - 'projectFolder': appyTool.getDiskFolder(), - } - if specificPodContext: - podContext.update(specificPodContext) - if templateUid: - podContext['podTemplate'] = appyPt - rendererParams = {'template': StringIO(template), - 'context': podContext, - 'result': tempFileName} - if appyTool.unoEnabledPython: - rendererParams['pythonWithUnoPath'] = appyTool.unoEnabledPython - if appyTool.openOfficePort: - rendererParams['ooPort'] = appyTool.openOfficePort - # Launch the renderer - try: - renderer = Renderer(**rendererParams) - renderer.run() - except appy.pod.PodError, pe: - if not os.path.exists(tempFileName): - # In some (most?) cases, when OO returns an error, the result is - # nevertheless generated. - appyTool.log(str(pe), type='error') - appyTool.say(POD_ERROR) - return self.goto(rq.get('HTTP_REFERER')) - # Open the temp file on the filesystem - f = file(tempFileName, 'rb') - res = f.read() - # Identify the filename to return - fileName = u'%s-%s' % (obj.Title().decode('utf-8'), podTitle) - fileName = appyTool.normalize(fileName) - response = obj.REQUEST.RESPONSE - response.setHeader('Content-Type', mimeTypes[format]) - response.setHeader('Content-Disposition', 'inline;filename="%s.%s"'\ - % (fileName, format)) - f.close() - # Execute the related action if relevant - if doAction and podInfo['action']: - podInfo['action'](appyObj, podContext) - # Returns the doc and removes the temp file - try: - os.remove(tempFileName) - except OSError, oe: - appyTool.log(DELETE_TEMP_DOC_ERROR % str(oe), type='warning') - except IOError, ie: - appyTool.log(DELETE_TEMP_DOC_ERROR % str(ie), type='warning') - return res - - def getAttr(self, name): - '''Gets on this flavour attribute named p_attrName. Useful because we - can't use getattr directly in Zope Page Templates.''' - return getattr(self.appy(), name, None) - - def _appy_getAllFields(self, contentType): - '''Returns the (translated) names of fields of p_contentType.''' - res = [] - for appyType in self.getProductConfig().attributes[contentType]: - if appyType.name != 'title': # Will be included by default. - label = '%s_%s' % (contentType, appyType.name) - res.append((appyType.name, self.translate(label))) - # Add object state - res.append(('workflowState', self.translate('workflow_state'))) - return res - - def _appy_getSearchableFields(self, contentType): - '''Returns the (translated) names of fields that may be searched on - objects of type p_contentType (=indexed fields).''' - res = [] - for appyType in self.getProductConfig().attributes[contentType]: - if appyType.indexed: - res.append((appyType.name, self.translate(appyType.labelId))) - return res - - def getSearchableFields(self, contentType): - '''Returns, among the list of all searchable fields (see method above), - the list of fields that the user has configured in the flavour as - being effectively used in the search screen.''' - res = [] - fieldNames = getattr(self.appy(), 'searchFieldsFor%s' % contentType, ()) - for name in fieldNames: - appyType = self.getAppyType(name, asDict=True,className=contentType) - res.append(appyType) - return res - - def getImportElements(self, contentType): - '''Returns the list of elements that can be imported from p_path for - p_contentType.''' - tool = self.getParentNode() - appyClass = tool.getAppyClass(contentType) - importParams = tool.getCreateMeans(appyClass)['import'] - onElement = importParams['onElement'].__get__('') - sortMethod = importParams['sort'] - if sortMethod: sortMethod = sortMethod.__get__('') - elems = [] - importPath = getattr(self, 'importPathFor%s' % contentType) - for elem in os.listdir(importPath): - elemFullPath = os.path.join(importPath, elem) - elemInfo = onElement(elemFullPath) - if elemInfo: - elemInfo.insert(0, elemFullPath) # To the result, I add the full - # path of the elem, which will not be shown. - elems.append(elemInfo) - if sortMethod: - elems = sortMethod(elems) - return [importParams['headers'], elems] -# ------------------------------------------------------------------------------ diff --git a/gen/plone25/mixins/PodTemplateMixin.py b/gen/plone25/mixins/PodTemplateMixin.py deleted file mode 100644 index abf2b51..0000000 --- a/gen/plone25/mixins/PodTemplateMixin.py +++ /dev/null @@ -1,7 +0,0 @@ -# ------------------------------------------------------------------------------ -from appy.gen.plone25.mixins import AbstractMixin - -# ------------------------------------------------------------------------------ -class PodTemplateMixin(AbstractMixin): - _appy_meta_type = 'PodTemplate' -# ------------------------------------------------------------------------------ diff --git a/gen/plone25/mixins/ToolMixin.py b/gen/plone25/mixins/ToolMixin.py index bdc5626..958d2a7 100644 --- a/gen/plone25/mixins/ToolMixin.py +++ b/gen/plone25/mixins/ToolMixin.py @@ -1,22 +1,28 @@ # ------------------------------------------------------------------------------ -import re, os, os.path, Cookie +import re, os, os.path, time, Cookie, StringIO, types +from appy.shared import mimeTypes from appy.shared.utils import getOsTempFolder +import appy.pod +from appy.pod.renderer import Renderer import appy.gen from appy.gen import Type, Search, Selection from appy.gen.utils import SomeObjects, sequenceTypes, getClassName -from appy.gen.plone25.mixins import AbstractMixin -from appy.gen.plone25.mixins.FlavourMixin import FlavourMixin +from appy.gen.plone25.mixins import BaseMixin from appy.gen.plone25.wrappers import AbstractWrapper from appy.gen.plone25.descriptors import ClassDescriptor +# Errors ----------------------------------------------------------------------- +DELETE_TEMP_DOC_ERROR = 'A temporary document could not be removed. %s.' +POD_ERROR = 'An error occurred while generating the document. Please ' \ + 'contact the system administrator.' jsMessages = ('no_elem_selected', 'delete_confirm') # ------------------------------------------------------------------------------ -class ToolMixin(AbstractMixin): +class ToolMixin(BaseMixin): _appy_meta_type = 'Tool' def getPortalType(self, metaTypeOrAppyClass): '''Returns the name of the portal_type that is based on - p_metaTypeOrAppyType in this flavour.''' + p_metaTypeOrAppyType.''' appName = self.getProductConfig().PROJECTNAME if not isinstance(metaTypeOrAppyClass, basestring): res = getClassName(metaTypeOrAppyClass, appName) @@ -25,52 +31,100 @@ class ToolMixin(AbstractMixin): res = '%s%s' % (elems[1], elems[4]) return res - def getFlavour(self, contextObjOrPortalType, appy=False): - '''Gets the flavour that corresponds to p_contextObjOrPortalType.''' - if isinstance(contextObjOrPortalType, basestring): - portalTypeName = contextObjOrPortalType - else: - # It is the contextObj, not a portal type name - portalTypeName = contextObjOrPortalType.portal_type - res = None + def getPodInfo(self, ploneObj, fieldName): + '''Returns POD-related information about Pod field p_fieldName defined + on class whose p_ploneObj is an instance of.''' + res = {} + appyClass = self.getAppyClass(ploneObj.meta_type) appyTool = self.appy() - flavourNumber = None - nameElems = portalTypeName.split('_') - if len(nameElems) > 1: - try: - flavourNumber = int(nameElems[-1]) - except ValueError: - pass - appName = self.getProductConfig().PROJECTNAME - if flavourNumber != None: - for flavour in appyTool.flavours: - if flavourNumber == flavour.number: - res = flavour - elif portalTypeName == ('%sFlavour' % appName): - # Current object is the Flavour itself. In this cas we simply - # return the wrapped contextObj. Here we are sure that - # contextObjOrPortalType is an object, not a portal type. - res = contextObjOrPortalType.appy() - if not res and appyTool.flavours: - res = appyTool.flavours[0] - # If appy=False, return the Plone object and not the Appy wrapper - # (this way, we avoid Zope security/access-related problems while - # using this object in Zope Page Templates) - if res and not appy: - res = res.o + n = appyTool.getAttributeName('formats', appyClass, fieldName) + res['formats'] = getattr(appyTool, n) + n = appyTool.getAttributeName('podTemplate', appyClass, fieldName) + res['template'] = getattr(appyTool, n) + appyType = ploneObj.getAppyType(fieldName) + res['title'] = self.translate(appyType.labelId) + res['context'] = appyType.context + res['action'] = appyType.action return res - def getFlavoursInfo(self): - '''Returns information about flavours.''' - res = [] + def generateDocument(self): + '''Generates the document from field-related info. UID of object that + is the template target is given in the request.''' + rq = self.REQUEST appyTool = self.appy() - for flavour in appyTool.flavours: - if isinstance(flavour.o, FlavourMixin): - # This is a bug: sometimes other objects are associated as - # flavours. - res.append({'title': flavour.title, 'number':flavour.number}) + # Get the object + objectUid = rq.get('objectUid') + obj = self.uid_catalog(UID=objectUid)[0].getObject() + appyObj = obj.appy() + # Get information about the document to render. + specificPodContext = None + fieldName = rq.get('fieldName') + format = rq.get('podFormat') + podInfo = self.getPodInfo(obj, fieldName) + template = podInfo['template'].content + podTitle = podInfo['title'] + if podInfo['context']: + if type(podInfo['context']) == types.FunctionType: + specificPodContext = podInfo['context'](appyObj) + else: + specificPodContext = podInfo['context'] + doAction = rq.get('askAction') == 'True' + # Temporary file where to generate the result + tempFileName = '%s/%s_%f.%s' % ( + getOsTempFolder(), obj.UID(), time.time(), format) + # Define parameters to pass to the appy.pod renderer + currentUser = self.portal_membership.getAuthenticatedMember() + podContext = {'tool': appyTool, 'user': currentUser, 'self': appyObj, + 'now': self.getProductConfig().DateTime(), + 'projectFolder': appyTool.getDiskFolder(), + } + if specificPodContext: + podContext.update(specificPodContext) + rendererParams = {'template': StringIO.StringIO(template), + 'context': podContext, 'result': tempFileName} + if appyTool.unoEnabledPython: + rendererParams['pythonWithUnoPath'] = appyTool.unoEnabledPython + if appyTool.openOfficePort: + rendererParams['ooPort'] = appyTool.openOfficePort + # Launch the renderer + try: + renderer = Renderer(**rendererParams) + renderer.run() + except appy.pod.PodError, pe: + if not os.path.exists(tempFileName): + # In some (most?) cases, when OO returns an error, the result is + # nevertheless generated. + appyTool.log(str(pe), type='error') + appyTool.say(POD_ERROR) + return self.goto(rq.get('HTTP_REFERER')) + # Open the temp file on the filesystem + f = file(tempFileName, 'rb') + res = f.read() + # Identify the filename to return + fileName = u'%s-%s' % (obj.Title().decode('utf-8'), podTitle) + fileName = appyTool.normalize(fileName) + response = obj.REQUEST.RESPONSE + response.setHeader('Content-Type', mimeTypes[format]) + response.setHeader('Content-Disposition', 'inline;filename="%s.%s"'\ + % (fileName, format)) + f.close() + # Execute the related action if relevant + if doAction and podInfo['action']: + podInfo['action'](appyObj, podContext) + # Returns the doc and removes the temp file + try: + os.remove(tempFileName) + except OSError, oe: + appyTool.log(DELETE_TEMP_DOC_ERROR % str(oe), type='warning') + except IOError, ie: + appyTool.log(DELETE_TEMP_DOC_ERROR % str(ie), type='warning') return res + def getAttr(self, name): + '''Gets attribute named p_attrName. Useful because we can't use getattr + directly in Zope Page Templates.''' + return getattr(self.appy(), name, None) + def getAppName(self): '''Returns the name of this application.''' return self.getProductConfig().PROJECTNAME @@ -86,6 +140,58 @@ class ToolMixin(AbstractMixin): '''Returns the list of root classes for this application.''' return self.getProductConfig().rootClasses + def _appy_getAllFields(self, contentType): + '''Returns the (translated) names of fields of p_contentType.''' + res = [] + for appyType in self.getProductConfig().attributes[contentType]: + if appyType.name != 'title': # Will be included by default. + label = '%s_%s' % (contentType, appyType.name) + res.append((appyType.name, self.translate(label))) + # Add object state + res.append(('workflowState', self.translate('workflow_state'))) + return res + + def _appy_getSearchableFields(self, contentType): + '''Returns the (translated) names of fields that may be searched on + objects of type p_contentType (=indexed fields).''' + res = [] + for appyType in self.getProductConfig().attributes[contentType]: + if appyType.indexed: + res.append((appyType.name, self.translate(appyType.labelId))) + return res + + def getSearchableFields(self, contentType): + '''Returns, among the list of all searchable fields (see method above), + the list of fields that the user has configured as being effectively + used in the search screen.''' + res = [] + fieldNames = getattr(self.appy(), 'searchFieldsFor%s' % contentType, ()) + for name in fieldNames: + appyType = self.getAppyType(name, asDict=True,className=contentType) + res.append(appyType) + return res + + def getImportElements(self, contentType): + '''Returns the list of elements that can be imported from p_path for + p_contentType.''' + appyClass = self.getAppyClass(contentType) + importParams = self.getCreateMeans(appyClass)['import'] + onElement = importParams['onElement'].__get__('') + sortMethod = importParams['sort'] + if sortMethod: sortMethod = sortMethod.__get__('') + elems = [] + importPath = getattr(self, 'importPathFor%s' % contentType) + for elem in os.listdir(importPath): + elemFullPath = os.path.join(importPath, elem) + elemInfo = onElement(elemFullPath) + if elemInfo: + elemInfo.insert(0, elemFullPath) # To the result, I add the full + # path of the elem, which will not be shown. + elems.append(elemInfo) + if sortMethod: + elems = sortMethod(elems) + return [importParams['headers'], elems] + def showPortlet(self, context): if self.portal_membership.isAnonymousUser(): return False if context.id == 'skyn': context = context.getParentNode() @@ -106,15 +212,13 @@ class ToolMixin(AbstractMixin): res = res.appy() return res - def executeQuery(self, contentType, flavourNumber=1, searchName=None, - startNumber=0, search=None, remember=False, - brainsOnly=False, maxResults=None, noSecurity=False, - sortBy=None, sortOrder='asc', - filterKey=None, filterValue=None): + def executeQuery(self, contentType, searchName=None, startNumber=0, + search=None, remember=False, brainsOnly=False, + maxResults=None, noSecurity=False, sortBy=None, + sortOrder='asc', filterKey=None, filterValue=None): '''Executes a query on a given p_contentType (or several, separated - with commas) in Plone's portal_catalog. Portal types are from the - flavour numbered p_flavourNumber. If p_searchName is specified, it - corresponds to: + with commas) in Plone's portal_catalog. If p_searchName is specified, + it corresponds to: 1) a search defined on p_contentType: additional search criteria will be added to the query, or; 2) "_advanced": in this case, additional search criteria will also @@ -150,11 +254,7 @@ class ToolMixin(AbstractMixin): p_filterValue.''' # Is there one or several content types ? if contentType.find(',') != -1: - # Several content types are specified portalTypes = contentType.split(',') - if flavourNumber != 1: - portalTypes = ['%s_%d' % (pt, flavourNumber) \ - for pt in portalTypes] else: portalTypes = contentType params = {'portal_type': portalTypes} @@ -164,8 +264,7 @@ class ToolMixin(AbstractMixin): # In this case, contentType must contain a single content type. appyClass = self.getAppyClass(contentType) if searchName != '_advanced': - search = ClassDescriptor.getSearch( - appyClass, searchName) + search = ClassDescriptor.getSearch(appyClass, searchName) else: fields = self.REQUEST.SESSION['searchCriteria'] search = Search('customSearch', **fields) @@ -220,22 +319,17 @@ class ToolMixin(AbstractMixin): for obj in res.objects: i += 1 uids[startNumber+i] = obj.UID() - s['search_%s_%s' % (flavourNumber, searchName)] = uids + s['search_%s' % searchName] = uids return res.__dict__ def getResultColumnsNames(self, contentType): contentTypes = contentType.strip(',').split(',') resSet = None # Temporary set for computing intersections. res = [] # Final, sorted result. - flavour = None fieldNames = None + appyTool = self.appy() for cType in contentTypes: - # Get the flavour tied to those content types - if not flavour: - flavour = self.getFlavour(cType, appy=True) - if flavour.number != 1: - cType = cType.rsplit('_', 1)[0] - fieldNames = getattr(flavour, 'resultColumnsFor%s' % cType) + fieldNames = getattr(appyTool, 'resultColumnsFor%s' % cType) if not resSet: resSet = set(fieldNames) else: @@ -483,9 +577,9 @@ class ToolMixin(AbstractMixin): attrValue = oper.join(attrValue) criteria[attrName[2:]] = attrValue rq.SESSION['searchCriteria'] = criteria - # Goto the screen that displays search results - backUrl = '%s/query?type_name=%s&flavourNumber=%d&search=_advanced' % \ - (os.path.dirname(rq['URL']), rq['type_name'], rq['flavourNumber']) + # Go to the screen that displays search results + backUrl = '%s/query?type_name=%s&&search=_advanced' % \ + (os.path.dirname(rq['URL']), rq['type_name']) return self.goto(backUrl) def getJavascriptMessages(self): @@ -535,13 +629,12 @@ class ToolMixin(AbstractMixin): if cookieValue: return cookieValue.value return default - def getQueryUrl(self, contentType, flavourNumber, searchName, - startNumber=None): + def getQueryUrl(self, contentType, searchName, startNumber=None): '''This method creates the URL that allows to perform a (non-Ajax) request for getting queried objects from a search named p_searchName - on p_contentType from flavour numbered p_flavourNumber.''' + on p_contentType.''' baseUrl = self.getAppFolder().absolute_url() + '/skyn' - baseParams= 'type_name=%s&flavourNumber=%s' %(contentType,flavourNumber) + baseParams = 'type_name=%s' % contentType # Manage start number rq = self.REQUEST if startNumber != None: @@ -609,11 +702,10 @@ class ToolMixin(AbstractMixin): if (nextIndex < lastIndex): lastNeeded = True # Get the list of available UIDs surrounding the current object if t == 'ref': # Manage navigation from a reference + # In the case of a reference, we retrieve ALL surrounding objects. masterObj = self.getObject(d1) batchSize = masterObj.getAppyType(fieldName).maxPerPage uids = getattr(masterObj, '_appy_%s' % fieldName) - # In the case of a reference, we retrieve ALL surrounding objects. - # Display the reference widget at the page where the current object # lies. startNumberKey = '%s%s_startNumber' % (masterObj.UID(), fieldName) @@ -622,13 +714,12 @@ class ToolMixin(AbstractMixin): res['sourceUrl'] = masterObj.getUrl(**{startNumberKey:startNumber, 'page':pageName, 'nav':''}) else: # Manage navigation from a search - contentType, flavourNumber = d1.split(':') - flavourNumber = int(flavourNumber) + contentType = d1 searchName = keySuffix = d2 batchSize = self.appy().numberOfResultsPerPage if not searchName: keySuffix = contentType s = self.REQUEST.SESSION - searchKey = 'search_%s_%s' % (flavourNumber, keySuffix) + searchKey = 'search_%s' % keySuffix if s.has_key(searchKey): uids = s[searchKey] else: uids = {} # In the case of a search, we retrieve only a part of all @@ -640,9 +731,8 @@ class ToolMixin(AbstractMixin): # this one. newStartNumber = (res['currentNumber']-1) - (batchSize / 2) if newStartNumber < 0: newStartNumber = 0 - self.executeQuery(contentType, flavourNumber, - searchName=searchName, startNumber=newStartNumber, - remember=True) + self.executeQuery(contentType, searchName=searchName, + startNumber=newStartNumber, remember=True) uids = s[searchKey] # For the moment, for first and last, we get them only if we have # them in session. @@ -650,9 +740,9 @@ class ToolMixin(AbstractMixin): if not uids.has_key(lastIndex): lastNeeded = False # Compute URL of source object startNumber = self.computeStartNumberFrom(res['currentNumber']-1, - res['totalNumber'], batchSize) - res['sourceUrl'] = self.getQueryUrl(contentType, flavourNumber, - searchName, startNumber=startNumber) + res['totalNumber'], batchSize) + res['sourceUrl'] = self.getQueryUrl(contentType, searchName, + startNumber=startNumber) # Compute URLs for urlType in ('previous', 'next', 'first', 'last'): exec 'needIt = %sNeeded' % urlType diff --git a/gen/plone25/mixins/UserMixin.py b/gen/plone25/mixins/UserMixin.py deleted file mode 100644 index cd119b8..0000000 --- a/gen/plone25/mixins/UserMixin.py +++ /dev/null @@ -1,7 +0,0 @@ -# ------------------------------------------------------------------------------ -from appy.gen.plone25.mixins import AbstractMixin - -# ------------------------------------------------------------------------------ -class UserMixin(AbstractMixin): - _appy_meta_type = 'UserMixin' -# ------------------------------------------------------------------------------ diff --git a/gen/plone25/mixins/__init__.py b/gen/plone25/mixins/__init__.py index c461f7e..afbfd01 100644 --- a/gen/plone25/mixins/__init__.py +++ b/gen/plone25/mixins/__init__.py @@ -1,9 +1,6 @@ '''This package contains mixin classes that are mixed in with generated classes: - - mixins/ClassMixin is mixed in with Standard Archetypes classes; - - mixins/ToolMixin is mixed in with the generated application Tool class; - - mixins/FlavourMixin is mixed in with the generated application Flavour - class. - The AbstractMixin defined hereafter is the base class of any mixin.''' + - mixins/BaseMixin is mixed in with Standard Archetypes classes; + - mixins/ToolMixin is mixed in with the generated application Tool class.''' # ------------------------------------------------------------------------------ import os, os.path, types, mimetypes @@ -15,10 +12,10 @@ from appy.gen.plone25.descriptors import ClassDescriptor from appy.gen.plone25.utils import updateRolesForPermission # ------------------------------------------------------------------------------ -class AbstractMixin: - '''Every Archetype class generated by appy.gen inherits from a mixin that - inherits from this class. It contains basic functions allowing to - minimize the amount of generated code.''' +class BaseMixin: + '''Every Archetype class generated by appy.gen inherits from this class or + a subclass of it.''' + _appy_meta_type = 'Class' def createOrUpdate(self, created, values): '''This method creates (if p_created is True) or updates an object. @@ -52,17 +49,21 @@ class AbstractMixin: # Keep in history potential changes on historized fields self.historizeData(previousData) - # Manage references - obj._appy_manageRefs(created) + # Manage potential link with an initiator object + if created and rq.get('nav', None): + # Get the initiator + splitted = rq['nav'].split('.') + if splitted[0] == 'search': return # Not an initiator but a search. + initiator = self.uid_catalog(UID=splitted[1])[0].getObject() + fieldName = splitted[2].split(':')[0] + initiator.appy().link(fieldName, obj) + + # Call the custom "onEdit" if available if obj.wrapperClass: - # Get the wrapper first appyObject = obj.appy() - # Call the custom "onEdit" if available - if hasattr(appyObject, 'onEdit'): - appyObject.onEdit(created) - # Manage "add" permissions + if hasattr(appyObject, 'onEdit'): appyObject.onEdit(created) + # Manage "add" permissions and reindex the object obj._appy_managePermissions() - # Reindex object obj.reindexObject() return obj @@ -95,6 +96,12 @@ class AbstractMixin: (baseUrl, typeName, objId) return self.goto(self.getUrl(editUrl, **urlParams)) + def onCreateWithoutForm(self): + '''This method is called when a user wants to create a object from a + reference field, automatically (without displaying a form).''' + rq = self.REQUEST + self.appy().create(rq['fieldName']) + def intraFieldValidation(self, errors, values): '''This method performs field-specific validation for every field from the page that is being created or edited. For every field whose @@ -120,7 +127,7 @@ class AbstractMixin: obj = self.appy() if not hasattr(obj, 'validate'): return obj.validate(values, errors) - # This custom "validate" method may have added fields in the given + # Those custom validation methods may have added fields in the given # p_errors object. Within this object, for every error message that is # not a string, we replace it with the standard validation error for the # corresponding field. @@ -241,21 +248,19 @@ class AbstractMixin: res = {} for appyType in self.getAllAppyTypes(): if appyType.historized: - res[appyType.name] = (getattr(self, appyType.name), - appyType.labelId) + res[appyType.name] = appyType.getValue(self) return res - def addDataChange(self, changes, labels=False): + def addDataChange(self, changes): '''This method allows to add "manually" a data change into the objet's history. Indeed, data changes are "automatically" recorded only when a HTTP form is uploaded, not if, in the code, a setter is called on a field. The method is also called by the method historizeData below, that performs "automatic" recording when a HTTP form is uploaded.''' - # Add to the p_changes dict the field labels if they are not present - if not labels: - for fieldName in changes.iterkeys(): - appyType = self.getAppyType(fieldName) - changes[fieldName] = (changes[fieldName], appyType.labelId) + # Add to the p_changes dict the field labels + for fieldName in changes.iterkeys(): + appyType = self.getAppyType(fieldName) + changes[fieldName] = (changes[fieldName], appyType.labelId) # Create the event to record in the history DateTime = self.getProductConfig().DateTime state = self.portal_workflow.getInfoFor(self, 'review_state') @@ -273,14 +278,18 @@ class AbstractMixin: historized fields, while p_self already contains the (potentially) modified values.''' # Remove from previousData all values that were not changed - for fieldName in previousData.keys(): - prev = previousData[fieldName][0] - curr = getattr(self, fieldName) + for field in previousData.keys(): + prev = previousData[field] + appyType = self.getAppyType(field) + curr = appyType.getValue(self) if (prev == curr) or ((prev == None) and (curr == '')) or \ ((prev == '') and (curr == None)): - del previousData[fieldName] + del previousData[field] + if (appyType.type == 'Ref') and (field in previousData): + titles = [r.title for r in previousData[field]] + previousData[field] = ','.join(titles) if previousData: - self.addDataChange(previousData, labels=True) + self.addDataChange(previousData) def goto(self, url, addParams=False): '''Brings the user to some p_url after an action has been executed.''' @@ -308,80 +317,14 @@ class AbstractMixin: field named p_name.''' return self.getAppyType(name).getFormattedValue(self, value) - def _appy_getRefs(self, fieldName, ploneObjects=False, - noListIfSingleObj=False, startNumber=None): - '''p_fieldName is the name of a Ref field. This method returns an - ordered list containing the objects linked to p_self through this - field. If p_ploneObjects is True, the method returns the "true" - Plone objects instead of the Appy wrappers. - If p_startNumber is None, this method returns all referred objects. - If p_startNumber is a number, this method will return x objects, - starting at p_startNumber, x being appyType.maxPerPage.''' - appyType = self.getAppyType(fieldName) - sortedUids = self._appy_getSortedField(fieldName) - batchNeeded = startNumber != None - exec 'refUids= self.getRaw%s%s()' % (fieldName[0].upper(),fieldName[1:]) - # There may be too much UIDs in sortedUids because these fields - # are not updated when objects are deleted. So we do it now. TODO: do - # such cleaning on object deletion? - toDelete = [] - for uid in sortedUids: - if uid not in refUids: - toDelete.append(uid) - for uid in toDelete: - sortedUids.remove(uid) - # Prepare the result - res = SomeObjects() - res.totalNumber = res.batchSize = len(sortedUids) - if batchNeeded: - res.batchSize = appyType.maxPerPage - if startNumber != None: - res.startNumber = startNumber - # Get the needed referred objects - i = res.startNumber - # Is it possible and more efficient to perform a single query in - # uid_catalog and get the result in the order of specified uids? - toUnlink = [] - while i < (res.startNumber + res.batchSize): - if i >= res.totalNumber: break - refUid = sortedUids[i] - refObject = self.uid_catalog(UID=refUid)[0].getObject() - i += 1 - tool = self.getTool() - if refObject.meta_type != tool.getPortalType(appyType.klass): - toUnlink.append(refObject) - continue - if not ploneObjects: - refObject = refObject.appy() - res.objects.append(refObject) - # Unlink dummy linked objects - if toUnlink: - suffix = '%s%s' % (fieldName[0].upper(), fieldName[1:]) - exec 'linkedObjects = self.get%s()' % suffix - for dummyObject in toUnlink: - linkedObjects.remove(dummyObject) - self.getProductConfig().logger.warn('DB error: Ref %s.%s ' \ - 'contains a %s instance "%s". It was removed.' % \ - (self.meta_type, fieldName, dummyObject.meta_type, - dummyObject.getId())) - exec 'self.set%s(linkedObjects)' % suffix - if res.objects and noListIfSingleObj: - if appyType.multiplicity[1] == 1: - res.objects = res.objects[0] - return res - def getAppyRefs(self, name, startNumber=None): '''Gets the objects linked to me through Ref field named p_name. If p_startNumber is None, this method returns all referred objects. - If p_startNumber is a number, this method will return x objects, - starting at p_startNumber, x being appyType.maxPerPage.''' + If p_startNumber is a number, this method will return + appyType.maxPerPage objects, starting at p_startNumber.''' appyType = self.getAppyType(name) - if not appyType.isBack: - return self._appy_getRefs(name, ploneObjects=True, - startNumber=startNumber).__dict__ - else: - # Note that pagination is not yet implemented for backward refs. - return SomeObjects(self.getBRefs(appyType.relationship)).__dict__ + return appyType.getValue(self, type='zobjects', someObjects=True, + startNumber=startNumber).__dict__ def getSelectableAppyRefs(self, name): '''p_name is the name of a Ref field. This method returns the list of @@ -780,10 +723,6 @@ class AbstractMixin: self.reindexObject() return self.goto(urlBack) - def getFlavour(self): - '''Returns the flavour corresponding to this object.''' - return self.getTool().getFlavour(self.portal_type) - def fieldValueSelected(self, fieldName, vocabValue, dbValue): '''When displaying a selection box (ie a String with a validator being a list), must the _vocabValue appear as selected?''' @@ -853,32 +792,6 @@ class AbstractMixin: rq.appyWrappers[uid] = wrapper return wrapper - def _appy_getRefsBack(self, fieldName, relName, ploneObjects=False, - noListIfSingleObj=False): - '''This method returns the list of objects linked to this one - through the BackRef corresponding to the Archetypes - relationship named p_relName.''' - # Preamble: must I return a list or a single element? - maxOne = False - if noListIfSingleObj: - # I must get the referred appyType to know its maximum multiplicity. - appyType = self.getAppyType(fieldName) - if appyType.multiplicity[1] == 1: - maxOne = True - # Get the referred objects through the Archetypes relationship. - objs = self.getBRefs(relName) - if maxOne: - res = None - if objs: - res = objs[0] - if res and not ploneObjects: - res = res.appy() - else: - res = objs - if not ploneObjects: - res = [o.appy() for o in objs] - return res - def _appy_showState(self, workflow, stateShow): '''Must I show a state whose "show value" is p_stateShow?''' if callable(stateShow): @@ -955,57 +868,6 @@ class AbstractMixin: exec 'self.%s = pList()' % sortedFieldName return getattr(self, sortedFieldName) - def _appy_manageRefs(self, created): - '''Every time an object is created or updated, this method updates - the Reference fields accordingly.''' - self._appy_manageRefsFromRequest() - rq = self.REQUEST - # If the creation was initiated by another object, update the ref. - if created and rq.get('nav', None): - # Get the initiator - splitted = rq['nav'].split('.') - if splitted[0] == 'search': return # Not an initiator but a search. - initiator = self.uid_catalog.searchResults( - UID=splitted[1])[0].getObject() - fieldName = splitted[2].split(':')[1] - initiator.appy().link(fieldName, self) - - def _appy_manageRefsFromRequest(self): - '''Appy manages itself some Ref fields (with link=True). So here we must - update the Ref fields.''' - fieldsInRequest = [] # Fields present in the request - for requestKey in self.REQUEST.keys(): - if requestKey.startswith('appy_ref_'): - fieldName = requestKey[9:] - # Security check - if not self.getAppyType(fieldName).isShowable(self, 'edit'): - continue - fieldsInRequest.append(fieldName) - fieldValue = self.REQUEST[requestKey] - sortedRefField = self._appy_getSortedField(fieldName) - del sortedRefField[:] - if not fieldValue: fieldValue = [] - if isinstance(fieldValue, basestring): - fieldValue = [fieldValue] - refObjects = [] - for uid in fieldValue: - obj = self.uid_catalog(UID=uid)[0].getObject() - refObjects.append(obj) - sortedRefField.append(uid) - exec 'self.set%s%s(refObjects)' % (fieldName[0].upper(), - fieldName[1:]) - # Manage Ref fields that are not present in the request - currentPage = self.REQUEST.get('page', 'main') - for appyType in self.getAllAppyTypes(): - if (appyType.type == 'Ref') and not appyType.isBack and \ - (appyType.page == currentPage) and \ - (appyType.name not in fieldsInRequest): - # If this field is visible, it was not present in the request: - # it means that we must remove any Ref from it. - if appyType.isShowable(self, 'edit'): - exec 'self.set%s%s([])' % (appyType.name[0].upper(), - appyType.name[1:]) - getUrlDefaults = {'page':True, 'nav':True} def getUrl(self, base=None, mode='view', **kwargs): '''Returns a Appy URL. @@ -1039,12 +901,14 @@ class AbstractMixin: params = '' return '%s%s' % (base, params) - def translate(self, label, mapping={}, domain=None, default=None): + def translate(self, label, mapping={}, domain=None, default=None, + language=None): '''Translates a given p_label into p_domain with p_mapping.''' cfg = self.getProductConfig() if not domain: domain = cfg.PROJECTNAME - return self.translation_service.utranslate( - domain, label, mapping, self, default=default) + return self.Control_Panel.TranslationService.utranslate( + domain, label, mapping, self, default=default, + target_language=language) def getPageLayout(self, layoutType): '''Returns the layout corresponding to p_layoutType for p_self.''' diff --git a/gen/plone25/model.py b/gen/plone25/model.py index e53169c..ae3c125 100644 --- a/gen/plone25/model.py +++ b/gen/plone25/model.py @@ -12,10 +12,10 @@ from appy.gen import * # ------------------------------------------------------------------------------ class ModelClass: '''This class is the abstract class of all predefined application classes - used in the Appy model: Tool, Flavour, PodTemplate, etc. All methods and - attributes of those classes are part of the Appy machinery and are - prefixed with _appy_ in order to avoid name conflicts with user-defined - parts of the application model.''' + used in the Appy model: Tool, User, etc. All methods and attributes of + those classes are part of the Appy machinery and are prefixed with _appy_ + in order to avoid name conflicts with user-defined parts of the + application model.''' _appy_attributes = [] # We need to keep track of attributes order. # When creating a new instance of a ModelClass, the following attributes # must not be given in the constructor (they are computed attributes). @@ -70,7 +70,11 @@ class ModelClass: res += ' %s=%s\n' % (attrName, klass._appy_getTypeBody(appyType)) return res +# The User class --------------------------------------------------------------- class User(ModelClass): + # In a ModelClass we need to declare attributes in the following list. + _appy_attributes = ['title', 'name', 'firstName', 'login', 'password1', + 'password2', 'roles'] # All methods defined below are fake. Real versions are in the wrapper. title = String(show=False) gm = {'group': 'main', 'multiplicity': (1,1)} @@ -86,47 +90,50 @@ class User(ModelClass): password2 = String(format=String.PASSWORD, show=showPassword, **gm) gm['multiplicity'] = (0, None) roles = String(validator=Selection('getGrantableRoles'), **gm) - _appy_attributes = ['title', 'name', 'firstName', 'login', - 'password1', 'password2', 'roles'] -class PodTemplate(ModelClass): - description = String(format=String.TEXT) - podTemplate = File(multiplicity=(1,1)) - podFormat = String(validator=['odt', 'pdf', 'rtf', 'doc'], - multiplicity=(1,1), default='odt') - podPhase = String(default='main') - _appy_attributes = ['description', 'podTemplate', 'podFormat', 'podPhase'] +# The Tool class --------------------------------------------------------------- -defaultFlavourAttrs = ('number', 'enableNotifications') -flavourAttributePrefixes = ('optionalFieldsFor', 'defaultValueFor', - 'podTemplatesFor', 'podMaxShownTemplatesFor', 'resultColumnsFor', - 'showWorkflowFor', 'showWorkflowCommentFieldFor', 'showAllStatesInPhaseFor') -# Attribute prefixes of the fields generated on the Flavour for configuring -# the application classes. +# Here are the prefixes of the fields generated on the Tool. +toolFieldPrefixes = ('defaultValue', 'podTemplate', 'formats', 'resultColumns', + 'enableAdvancedSearch', 'numberOfSearchColumns', + 'searchFields', 'optionalFields', 'showWorkflow', + 'showWorkflowCommentField', 'showAllStatesInPhase') +defaultToolFields = ('users', 'enableNotifications', 'unoEnabledPython', + 'openOfficePort', 'numberOfResultsPerPage', + 'listBoxesMaximumWidth') -class Flavour(ModelClass): - '''For every application, the Flavour may be different (it depends on the - fields declared as optional, etc). Instead of creating a new way to - generate the Archetypes Flavour class, we create a silly - FlavourStub instance and we will use the standard Archetypes - generator that generates classes from the application to generate the - flavour class.''' - number = Integer(default=1, show=False) - enableNotifications = Boolean(default=True, page='notifications') +class Tool(ModelClass): + # The following dict allows us to remember the original classes related to + # the attributes we will add due to params in user attributes. _appy_classes = {} # ~{s_attributeName: s_className}~ - # We need to remember the original classes related to the flavour attributes - _appy_attributes = list(defaultFlavourAttrs) + # In a ModelClass we need to declare attributes in the following list. + _appy_attributes = list(defaultToolFields) + + # Tool attributes + # First arg of Ref field below is None because we don't know yet if it will + # link to the predefined User class or a custom class defined in the + # application. + users = Ref(None, multiplicity=(0,None), add=True, link=False, + back=Ref(attribute='toTool'), page='users', + shownInfo=('login', 'title', 'roles'), showHeaders=True) + enableNotifications = Boolean(default=True, page='notifications') + def validPythonWithUno(self, value): pass # Real method in the wrapper + unoEnabledPython = String(group="connectionToOpenOffice", + validator=validPythonWithUno) + openOfficePort = Integer(default=2002, group="connectionToOpenOffice") + numberOfResultsPerPage = Integer(default=30) + listBoxesMaximumWidth = Integer(default=100) @classmethod def _appy_clean(klass): toClean = [] for k, v in klass.__dict__.iteritems(): if not k.startswith('__') and (not k.startswith('_appy_')): - if k not in defaultFlavourAttrs: + if k not in defaultToolFields: toClean.append(k) for k in toClean: exec 'del klass.%s' % k - klass._appy_attributes = list(defaultFlavourAttrs) + klass._appy_attributes = list(defaultToolFields) klass._appy_classes = {} @classmethod @@ -134,20 +141,20 @@ class Flavour(ModelClass): '''From a given p_appyType, produce a type definition suitable for storing the default value for this field.''' res = copy.copy(appyType) - # A fiekd in the flavour can't have parameters that would lead to the - # creation of new fields in the flavour. + # A field added to the tool can't have parameters that would lead to the + # creation of new fields in the tool. res.editDefault = False res.optional = False res.show = True res.group = copy.copy(appyType.group) res.phase = 'main' - # Set default layouts for all Flavour fields + # Set default layouts for all Tool fields res.layouts = res.formatLayouts(None) res.specificReadPermission = False res.specificWritePermission = False res.multiplicity = (0, appyType.multiplicity[1]) if type(res.validator) == types.FunctionType: - # We will not be able to call this function from the flavour. + # We will not be able to call this function from the tool. res.validator = None if isinstance(appyType, Ref): res.link = True @@ -155,7 +162,7 @@ class Flavour(ModelClass): res.back = copy.copy(appyType.back) res.back.attribute += 'DefaultValue' res.back.show = False - res.select = None # Not callable from flavour + res.select = None # Not callable from tool. return res @classmethod @@ -182,10 +189,7 @@ class Flavour(ModelClass): @classmethod def _appy_addPodRelatedFields(klass, fieldDescr): - '''Adds the fields needed in the Flavour for configuring a Pod field. - The following method, m_appy_addPodField, is the previous way to - manage gen-pod integration. For the moment, both approaches coexist. - In the future, only this one may subsist.''' + '''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', @@ -200,29 +204,9 @@ class Flavour(ModelClass): multiplicity=(1,None), default=('odt',), **pg) klass._appy_addField(fieldName, fieldType, fieldDescr.classDescr) - @classmethod - def _appy_addPodField(klass, classDescr): - '''Adds a POD field to the flavour and also an integer field that will - determine the maximum number of documents to show at once on consult - views. If this number is reached, a list is displayed.''' - # First, add the POD field that will hold PodTemplates. - fieldType = Ref(PodTemplate, multiplicity=(0,None), add=True, - link=False, back = Ref(attribute='flavour'), - page="documentGeneration", - group=classDescr.klass.__name__) - fieldName = 'podTemplatesFor%s' % classDescr.name - klass._appy_addField(fieldName, fieldType, classDescr) - # Then, add the integer field - fieldType = Integer(default=1, page='userInterface', - group=classDescr.klass.__name__) - fieldName = 'podMaxShownTemplatesFor%s' % classDescr.name - klass._appy_addField(fieldName, fieldType, classDescr) - classDescr.flavourFieldsToPropagate.append( - ('podMaxShownTemplatesFor%s', copy.copy(fieldType)) ) - @classmethod def _appy_addQueryResultColumns(klass, classDescr): - '''Adds, for class p_classDescr, the attribute in the flavour that + '''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 @@ -302,25 +286,4 @@ class Flavour(ModelClass): fieldType = Boolean(default=defaultValue, page='userInterface', group=groupName) klass._appy_addField(fieldName, fieldType, classDescr) - -class Tool(ModelClass): - flavours = Ref(None, multiplicity=(1,None), add=True, link=False, - back=Ref(attribute='tool')) - # First arg is None because we don't know yet if it will link - # to the predefined Flavour class or a custom class defined - # in the application. - users = Ref(None, multiplicity=(0,None), add=True, link=False, - back=Ref(attribute='toTool'), page='users', - shownInfo=('login', 'title', 'roles'), showHeaders=True) - # First arg is None because we don't know yet if it will link to the - # predefined User class or a custom class defined in the application. - def validPythonWithUno(self, value): pass # Real method in the wrapper - unoEnabledPython = String(group="connectionToOpenOffice", - validator=validPythonWithUno) - openOfficePort = Integer(default=2002, group="connectionToOpenOffice") - numberOfResultsPerPage = Integer(default=30) - listBoxesMaximumWidth = Integer(default=100) - _appy_attributes = ['flavours', 'users', 'unoEnabledPython', - 'openOfficePort', 'numberOfResultsPerPage', - 'listBoxesMaximumWidth'] # ------------------------------------------------------------------------------ diff --git a/gen/plone25/skin/ajax.pt b/gen/plone25/skin/ajax.pt index b305341..861d2dc 100644 --- a/gen/plone25/skin/ajax.pt +++ b/gen/plone25/skin/ajax.pt @@ -11,7 +11,7 @@ response request/RESPONSE; member context/portal_membership/getAuthenticatedMember; portal context/portal_url/getPortalObject; - portal_url context/portal_url/getPortalPath; + portal_url python: context.portal_url(); template python: contextObj.getPageTemplate(portal.skyn, page); dummy python: response.setHeader('Content-Type','text/html;;charset=utf-8'); dummy2 python: response.setHeader('Expires', 'Mon, 11 Dec 1975 12:05:05 GMT'); diff --git a/gen/plone25/skin/edit.pt b/gen/plone25/skin/edit.pt index d6acfa8..c509f47 100644 --- a/gen/plone25/skin/edit.pt +++ b/gen/plone25/skin/edit.pt @@ -4,7 +4,6 @@ layoutType python:'edit'; layout python: contextObj.getPageLayout(layoutType); tool contextObj/getTool; - flavour python: tool.getFlavour(contextObj); appFolder tool/getAppFolder; appName appFolder/getId; page request/page|python:'main'; diff --git a/gen/plone25/skin/import.pt b/gen/plone25/skin/import.pt index 0206a17..2295cb2 100644 --- a/gen/plone25/skin/import.pt +++ b/gen/plone25/skin/import.pt @@ -16,8 +16,7 @@ tal:define="appFolder context/getParentNode; contentType request/type_name; tool python: portal.get('portal_%s' % appFolder.id.lower()); - flavour python: tool.getFlavour(contentType); - importElems python: flavour.getImportElements(contentType); + importElems python: tool.getImportElements(contentType); global allAreImported python:True">
diff --git a/gen/plone25/skin/macros.pt b/gen/plone25/skin/macros.pt index 53f4cc9..15ae643 100644 --- a/gen/plone25/skin/macros.pt +++ b/gen/plone25/skin/macros.pt @@ -1,7 +1,6 @@ + navBaseCall python: 'askQueryResult(\'%s\',\'%s\',\'%s\',\'%s\',**v**)' % (ajaxHookId, tool.absolute_url(), contentType, searchName); + newSearchUrl python: '%s/skyn/search?type_name=%s&' % (tool.getAppFolder().absolute_url(), contentType);"> @@ -91,7 +90,7 @@ Mandatory column "Title"/"Name" Columns corresponding to other fields @@ -125,7 +124,7 @@ Edit the element - Global form for generating a document from a pod template.
+ tal:attributes="action python: tool.absolute_url() + '/generateDocument'"> - templateUid is given if class-wide pod, fieldName and podFormat are given if podField. - @@ -384,31 +398,6 @@ - - This macro lists the POD templates that are available. It is used by macro "header" below. - -
- - Display templates as links if a few number of templates must be shown - - - - - -   - Display templates as a list if a lot of templates must be shown - - -
- This macro displays an object's history. It is used by macro "header" below. @@ -460,7 +449,7 @@ - + @@ -482,13 +471,13 @@ This macro displays an object's state(s). It is used by macro "header" below. -
@@ -512,7 +501,7 @@ Input field allowing to enter a comment before triggering a transition - @@ -556,7 +545,7 @@ - - Object history @@ -631,7 +618,7 @@ masterValue.push(idField); } else { - if (idField[0] == '(') { + if ((idField[0] == '(') || (idField[0] == '[')) { // There are multiple values, split it var subValues = idField.substring(1, idField.length-1).split(','); for (var k=0; k < subValues.length; k++){ diff --git a/gen/plone25/skin/portlet.pt b/gen/plone25/skin/portlet.pt index 48ae628..4d60e56 100644 --- a/gen/plone25/skin/portlet.pt +++ b/gen/plone25/skin/portlet.pt @@ -8,13 +8,13 @@ contextObj python: tool.getPublishedObject()"> Portlet title, with link to tool.
- If there is only one flavour, clicking on the portlet + For the Manager, clicking on the portlet title allows to see all root objects in the database.
@@ -520,7 +509,7 @@ Buttons for triggering transitions -
+ Creator and last modification date Plus/minus icon for accessing history @@ -580,8 +569,6 @@ -
@@ -37,16 +37,14 @@ Create a section for every root class. - + Section title, with action icons
@@ -66,11 +64,11 @@ tal:attributes="onClick python: 'href: window.location=\'%s/skyn/import?type_name=%s\'' % (appFolder.absolute_url(), rootClass); src string: $portal_url/skyn/import.png; title python: tool.translate('query_import')"/> - Search objects of this type (todo: update flavourNumber) + Search objects of this type @@ -94,7 +92,7 @@
- @@ -105,7 +103,7 @@
- @@ -113,16 +111,6 @@ - All objects in flavour - - Greyed transparent zone that is deployed on the whole screen when a popup is displayed. @@ -133,8 +121,9 @@
-

- +

+ +
@@ -25,8 +23,8 @@
diff --git a/gen/plone25/skin/search.pt b/gen/plone25/skin/search.pt index c706271..a2b5320 100644 --- a/gen/plone25/skin/search.pt +++ b/gen/plone25/skin/search.pt @@ -16,8 +16,7 @@ tal:define="appFolder context/getParentNode; contentType request/type_name; tool python: portal.get('portal_%s' % appFolder.id.lower()); - flavour python: tool.getFlavour('Dummy_%s' % request['flavourNumber']); - searchableFields python: flavour.getSearchableFields(contentType)"> + searchableFields python: tool.getSearchableFields(contentType)"> Search title

— @@ -27,10 +26,9 @@ -

-
+ tal:define="numberOfColumns python: tool.getAttr('numberOfSearchColumnsFor%s' % contentType)"> Edit the element - + + Delete the element
diff --git a/gen/plone25/skin/view.pt b/gen/plone25/skin/view.pt index 30cd220..c42cefc 100644 --- a/gen/plone25/skin/view.pt +++ b/gen/plone25/skin/view.pt @@ -21,13 +21,12 @@ layoutType python:'view'; layout python: contextObj.getPageLayout(layoutType); tool contextObj/getTool; - flavour python: tool.getFlavour(contextObj); appFolder tool/getAppFolder; appName appFolder/getId; page request/page|python:'main'; phaseInfo python: contextObj.getAppyPhases(page=page); phase phaseInfo/name; - showWorkflow python: flavour.getAttr('showWorkflowFor' + contextObj.meta_type)"> + showWorkflow python: tool.getAttr('showWorkflowFor' + contextObj.meta_type)"> diff --git a/gen/plone25/skin/widgets/action.pt b/gen/plone25/skin/widgets/action.pt index 2f5769c..001a8aa 100644 --- a/gen/plone25/skin/widgets/action.pt +++ b/gen/plone25/skin/widgets/action.pt @@ -2,14 +2,15 @@ + onClick python: 'askConfirm(\'form\', \'%s\', "%s")' % (formId, labelConfirm)"/> The previous onClick is simply used to prevent Plone diff --git a/gen/plone25/skin/widgets/pod.pt b/gen/plone25/skin/widgets/pod.pt index f0c3536..8ae42d5 100644 --- a/gen/plone25/skin/widgets/pod.pt +++ b/gen/plone25/skin/widgets/pod.pt @@ -7,9 +7,9 @@ - diff --git a/gen/plone25/skin/widgets/ref.pt b/gen/plone25/skin/widgets/ref.pt index c3f0b09..7113514 100644 --- a/gen/plone25/skin/widgets/ref.pt +++ b/gen/plone25/skin/widgets/ref.pt @@ -10,7 +10,7 @@ from one object to the next/previous on skyn/view. @@ -40,13 +40,13 @@ + + tal:attributes="href python: obj.getUrl(mode='edit', page='main', nav=navInfo)"> - + onClick python: test(appyType['noForm'], noFormCall, formCall)"/> @@ -109,8 +113,7 @@ totalNumber refObjects/totalNumber; batchSize refObjects/batchSize; folder python: contextObj.isPrincipiaFolderish and contextObj or contextObj.getParentNode(); - flavour python:tool.getFlavour(contextObj); - linkedPortalType python:flavour.getPortalType(appyType['klass']); + linkedPortalType python: tool.getPortalType(appyType['klass']); addPermission python: '%s: Add %s' % (tool.getAppName(), linkedPortalType); canWrite python: not appyType['isBack'] and member.has_permission(appyType['writePermission'], contextObj); multiplicity appyType/multiplicity; @@ -118,6 +121,7 @@ showPlusIcon python:not appyType['isBack'] and appyType['add'] and not maxReached and member.has_permission(addPermission, folder) and canWrite; atMostOneRef python: (multiplicity[1] == 1) and (len(objs)<=1); label python: tool.translate(appyType['labelId']); + addConfirmMsg python: tool.translate('%s_addConfirm' % appyType['labelId']); description python: tool.translate(appyType['descrId']); navBaseCall python: 'askRefField(\'%s\',\'%s\',\'%s\',\'%s\',**v**)' % (ajaxHookId, contextObj.absolute_url(), fieldName, innerRef)"> @@ -240,13 +244,11 @@ A carriage return needed in some cases. -
Edit macro for an Ref. -
- -
+ + Cell macro for a Ref. diff --git a/gen/plone25/skin/widgets/show.pt b/gen/plone25/skin/widgets/show.pt index f6b47dc..209f373 100644 --- a/gen/plone25/skin/widgets/show.pt +++ b/gen/plone25/skin/widgets/show.pt @@ -90,7 +90,9 @@ - +
First row: the tabs.
@@ -137,7 +139,8 @@
+ align widget/align; + class widget/css_class"> Display the title of the group if it is not rendered a fieldset.
" - typeDescMsgId = '_edit_descr' - i18nDomain = '' - schema = fullSchema - allMetaTypes = - wrapperClass = - for elem in dir(FlavourMixin): - if not elem.startswith('__'): security.declarePublic(elem) - - -registerType(, '') diff --git a/gen/plone25/templates/PodTemplate.py b/gen/plone25/templates/PodTemplate.py deleted file mode 100644 index c0bf34b..0000000 --- a/gen/plone25/templates/PodTemplate.py +++ /dev/null @@ -1,34 +0,0 @@ - -from AccessControl import ClassSecurityInfo -from DateTime import DateTime -from Products.Archetypes.atapi import * -import Products..config -from appy.gen.plone25.mixins.PodTemplateMixin import PodTemplateMixin -from Extensions.appyWrappers import - -schema = Schema(( -),) -fullSchema = BaseSchema.copy() + schema.copy() - -class PodTemplate(BaseContent, PodTemplateMixin): - '''POD template.''' - security = ClassSecurityInfo() - __implements__ = (getattr(BaseContent,'__implements__',()),) - archetype_name = 'PodTemplate' - meta_type = 'PodTemplate' - portal_type = 'PodTemplate' - allowed_content_types = [] - filter_content_types = 0 - global_allow = 1 - immediate_view = 'skyn/view' - default_view = 'skyn/view' - suppl_views = () - typeDescription = "PodTemplate" - typeDescMsgId = 'PodTemplate_edit_descr' - wrapperClass = - schema = fullSchema - for elem in dir(PodTemplateMixin): - if not elem.startswith('__'): security.declarePublic(elem) - - -registerType(PodTemplate, '') diff --git a/gen/plone25/templates/Portlet.pt b/gen/plone25/templates/Portlet.pt index f94e215..e69e71f 100644 --- a/gen/plone25/templates/Portlet.pt +++ b/gen/plone25/templates/Portlet.pt @@ -3,15 +3,13 @@ i18n:domain="">
+ appFolder tool/getAppFolder" class="portlet">
diff --git a/gen/plone25/templates/Styles.css.dtml b/gen/plone25/templates/Styles.css.dtml index c4456f0..595c398 100644 --- a/gen/plone25/templates/Styles.css.dtml +++ b/gen/plone25/templates/Styles.css.dtml @@ -36,24 +36,22 @@ label { font-weight: bold; font-style: italic; line-height: 1.4em;} border-width: thin; text-align: center; padding: 0.1em 1em 0.1em 1.3em; + background-position: -1px 4px; } .appyChanges th { font-style: italic; background-color: transparent; - border-bottom: 1px dashed #8CACBB; - border-top: 0 none transparent; - border-left: 0 none transparent; - border-right: 0 none transparent; + border: 0 none transparent; padding: 0.1em 0.1em 0.1em 0.1em; } .appyChanges td { - padding: 0.1em 0.1em 0.1em 0.1em !important; + padding: 0.1em 0.2em 0.1em 0.2em !important; + border-top: 1px dashed #8CACBB !important; border-right: 0 none transparent !important; - border-top: 0 none transparent; - border-left: 0 none transparent; - border-right: 0 none transparent; + border-left: 0 none transparent !important; + border-bottom: 0 none transparent !important; } .appyHistory { @@ -70,6 +68,12 @@ label { font-weight: bold; font-style: italic; line-height: 1.4em;} background-repeat: no-repeat; background-position: -1px 7px; } +.stepDoneState { + background-color: #cde2a7; + background-image: url(&dtml-portal_url;/skyn/done.png); + background-repeat: no-repeat; + background-position: -1px 4px; +} .stepCurrent { background-color: #eef3f5; @@ -77,6 +81,12 @@ label { font-weight: bold; font-style: italic; line-height: 1.4em;} background-repeat: no-repeat; background-position: -1px 7px; } +.stepCurrentState { + background-color: #eef3f5; + background-image: url(&dtml-portal_url;/skyn/current.png); + background-repeat: no-repeat; + background-position: -1px 4px; +} .stepFuture { background-color: #ffffff; diff --git a/gen/plone25/templates/ToolTemplate.py b/gen/plone25/templates/ToolTemplate.py index 516d3f3..cc53ade 100644 --- a/gen/plone25/templates/ToolTemplate.py +++ b/gen/plone25/templates/ToolTemplate.py @@ -29,6 +29,7 @@ class (UniqueObject, OrderedBaseFolder, ToolMixin): typeDescription = "" typeDescMsgId = '_edit_descr' i18nDomain = '' + allMetaTypes = wrapperClass = schema = fullSchema schema["id"].widget.visible = False diff --git a/gen/plone25/templates/UserTemplate.py b/gen/plone25/templates/UserTemplate.py index 9186776..dd352bc 100644 --- a/gen/plone25/templates/UserTemplate.py +++ b/gen/plone25/templates/UserTemplate.py @@ -2,15 +2,15 @@ from AccessControl import ClassSecurityInfo from Products.Archetypes.atapi import * import Products..config -from appy.gen.plone25.mixins.UserMixin import UserMixin +from appy.gen.plone25.mixins import BaseMixin from Extensions.appyWrappers import schema = Schema(( ),) fullSchema = BaseSchema.copy() + schema.copy() -class User(BaseContent, UserMixin): - '''Configuration flavour class for .''' +class User(BaseContent, BaseMixin): + '''User mixin.''' security = ClassSecurityInfo() __implements__ = (getattr(BaseContent,'__implements__',()),) archetype_name = 'User' @@ -27,7 +27,7 @@ class User(BaseContent, UserMixin): i18nDomain = '' schema = fullSchema wrapperClass = - for elem in dir(UserMixin): + for elem in dir(BaseMixin): if not elem.startswith('__'): security.declarePublic(elem) diff --git a/gen/plone25/templates/appyWrappers.py b/gen/plone25/templates/appyWrappers.py index 46374c2..80f191c 100644 --- a/gen/plone25/templates/appyWrappers.py +++ b/gen/plone25/templates/appyWrappers.py @@ -2,23 +2,14 @@ from appy.gen import * from appy.gen.plone25.wrappers import AbstractWrapper from appy.gen.plone25.wrappers.ToolWrapper import ToolWrapper -from appy.gen.plone25.wrappers.FlavourWrapper import FlavourWrapper -from appy.gen.plone25.wrappers.PodTemplateWrapper import PodTemplateWrapper from appy.gen.plone25.wrappers.UserWrapper import UserWrapper from Globals import InitializeClass from AccessControl import ClassSecurityInfo -class PodTemplate(PodTemplateWrapper): - '''This class represents a POD template for this application.''' - class User(UserWrapper): '''This class represents a user.''' -class Flavour(FlavourWrapper): - '''This class represents the Appy class used for defining a flavour.''' - folder=True - class Tool(ToolWrapper): '''This class represents the tool for this application.''' folder=True diff --git a/gen/plone25/templates/config.py b/gen/plone25/templates/config.py index 50171f0..d5fd20d 100644 --- a/gen/plone25/templates/config.py +++ b/gen/plone25/templates/config.py @@ -42,7 +42,7 @@ ADD_CONTENT_PERMISSIONS = { } setDefaultRoles(DEFAULT_ADD_CONTENT_PERMISSION, tuple(defaultAddRoles)) -# Applications classes, in various formats and flavours +# Applications classes, in various formats rootClasses = [] appClasses = appClassNames = [] diff --git a/gen/plone25/workflow.py b/gen/plone25/workflow.py index cc67d9a..35bc18d 100644 --- a/gen/plone25/workflow.py +++ b/gen/plone25/workflow.py @@ -155,9 +155,8 @@ def do(transitionName, stateChange, logger): if hasattr(ploneObj, '_v_appy_do') and \ not ploneObj._v_appy_do['doNotify']: doNotify = False - elif not ploneObj.getTool().getFlavour( - ploneObj).getEnableNotifications(): - # We do not notify if the "notify" flag in the flavour is disabled. + elif not getattr(ploneObj.getTool().appy(), 'enableNotifications'): + # We do not notify if the "notify" flag in the tool is disabled. doNotify = False if doAction or doNotify: obj = ploneObj.appy() diff --git a/gen/plone25/wrappers/FlavourWrapper.py b/gen/plone25/wrappers/FlavourWrapper.py deleted file mode 100644 index aa76fd9..0000000 --- a/gen/plone25/wrappers/FlavourWrapper.py +++ /dev/null @@ -1,86 +0,0 @@ -# ------------------------------------------------------------------------------ -from appy.gen.plone25.wrappers import AbstractWrapper - -# ------------------------------------------------------------------------------ -class FlavourWrapper(AbstractWrapper): - def onEdit(self, created): - if created: - nbOfFlavours = len(self.tool.flavours) - if nbOfFlavours != 1: - self.number = nbOfFlavours - self.o.registerPortalTypes() - # Call the custom flavour "onEdit" method if it exists - if len(self.__class__.__bases__) > 1: - # There is a custom flavour - if customFlavour.__dict__.has_key('onEdit'): - customFlavour.__dict__['onEdit'](self, created) - - def getAttributeName(self, attributeType, klass, attrName=None): - '''Some names of Flavour attributes are not easy to guess. For example, - the attribute that stores, for a given flavour, the POD templates - for class A that is in package x.y is "flavour.podTemplatesForx_y_A". - Other example: the attribute that stores the editable default value - of field "f1" of class x.y.A is "flavour.defaultValueForx_y_A_f1". - This method generates the attribute name based on p_attributeType, - a p_klass from the application, and a p_attrName (given only if - needed, for example if p_attributeType is "defaultValue"). - p_attributeType may be: - - "defaultValue" - Stores the editable default value for a given p_attrName of a - given p_klass. - - "podTemplates" - Stores the POD templates that are defined for a given p_klass. - - "podMaxShownTemplates" - Stores the maximum number of POD templates shown at once. If the - number of available templates is higher, templates are shown in a - drop-down list. - - "podTemplate" - Stores the pod template for p_attrName. - - "formats" - Stores the output format(s) of a given pod template for - p_attrName. - - "resultColumns" - Stores the list of columns that must be shown when displaying - instances of the a given root p_klass. - - "enableAdvancedSearch" - Determines if the advanced search screen must be enabled for - p_klass. - - "numberOfSearchColumns" - Determines in how many columns the search screen for p_klass - is rendered. - - "searchFields" - Determines, among all indexed fields for p_klass, which one will - really be used in the search screen. - - "optionalFields" - Stores the list of optional attributes that are in use in the - current flavour for the given p_klass. - - "showWorkflow" - Stores the boolean field indicating if we must show workflow- - related information for p_klass or not. - - "showWorkflowCommentField" - Stores the boolean field indicating if we must show the field - allowing to enter a comment every time a transition is triggered. - - "showAllStatesInPhase" - Stores the boolean field indicating if we must show all states - linked to the current phase or not. If this field is False, we - simply show the current state, be it linked to the current phase - or not. - ''' - fullClassName = self.o.getPortalType(klass) - res = '%sFor%s' % (attributeType, fullClassName) - if attrName: res += '_%s' % attrName - return res -# ------------------------------------------------------------------------------ diff --git a/gen/plone25/wrappers/PodTemplateWrapper.py b/gen/plone25/wrappers/PodTemplateWrapper.py deleted file mode 100644 index 97a7e1a..0000000 --- a/gen/plone25/wrappers/PodTemplateWrapper.py +++ /dev/null @@ -1,6 +0,0 @@ -# ------------------------------------------------------------------------------ -from appy.gen.plone25.wrappers import AbstractWrapper - -# ------------------------------------------------------------------------------ -class PodTemplateWrapper(AbstractWrapper): pass -# ------------------------------------------------------------------------------ diff --git a/gen/plone25/wrappers/ToolWrapper.py b/gen/plone25/wrappers/ToolWrapper.py index 702fe32..37a11f5 100644 --- a/gen/plone25/wrappers/ToolWrapper.py +++ b/gen/plone25/wrappers/ToolWrapper.py @@ -44,4 +44,65 @@ class ToolWrapper(AbstractWrapper): def getDiskFolder(self): '''Returns the disk folder where the Appy application is stored.''' return self.o.getProductConfig().diskFolder + + def getAttributeName(self, attributeType, klass, attrName=None): + '''Some names of Tool attributes are not easy to guess. For example, + the attribute that stores the names of the columns to display in + query results for class A that is in package x.y is + "tool.resultColumnsForx_y_A". Other example: the attribute that + stores the editable default value of field "f1" of class x.y.A is + "tool.defaultValueForx_y_A_f1". This method generates the attribute + name based on p_attributeType, a p_klass from the application, and a + p_attrName (given only if needed, for example if p_attributeType is + "defaultValue"). p_attributeType may be: + + "defaultValue" + Stores the editable default value for a given p_attrName of a + given p_klass. + + "podTemplate" + Stores the pod template for p_attrName. + + "formats" + Stores the output format(s) of a given pod template for + p_attrName. + + "resultColumns" + Stores the list of columns that must be shown when displaying + instances of the a given root p_klass. + + "enableAdvancedSearch" + Determines if the advanced search screen must be enabled for + p_klass. + + "numberOfSearchColumns" + Determines in how many columns the search screen for p_klass + is rendered. + + "searchFields" + Determines, among all indexed fields for p_klass, which one will + really be used in the search screen. + + "optionalFields" + Stores the list of optional attributes that are in use in the + tool for the given p_klass. + + "showWorkflow" + Stores the boolean field indicating if we must show workflow- + related information for p_klass or not. + + "showWorkflowCommentField" + Stores the boolean field indicating if we must show the field + allowing to enter a comment every time a transition is triggered. + + "showAllStatesInPhase" + Stores the boolean field indicating if we must show all states + linked to the current phase or not. If this field is False, we + simply show the current state, be it linked to the current phase + or not. + ''' + fullClassName = self.o.getPortalType(klass) + res = '%sFor%s' % (attributeType, fullClassName) + if attrName: res += '_%s' % attrName + return res # ------------------------------------------------------------------------------ diff --git a/gen/plone25/wrappers/UserWrapper.py b/gen/plone25/wrappers/UserWrapper.py index 743f9c9..1a2ec6b 100644 --- a/gen/plone25/wrappers/UserWrapper.py +++ b/gen/plone25/wrappers/UserWrapper.py @@ -3,6 +3,18 @@ from appy.gen.plone25.wrappers import AbstractWrapper # ------------------------------------------------------------------------------ class UserWrapper(AbstractWrapper): + + def _callCustom(self, methodName, *args, **kwargs): + '''This wrapper implements some methods like "validate" and "onEdit". + If the user has defined its own wrapper, its methods will not be + called. So this method allows, from the methods here, to call the + user versions.''' + if len(self.__class__.__bases__) > 1: + # There is a custom user class + customUser = self.__class__.__bases__[-1] + if customUser.__dict__.has_key(methodName): + customUser.__dict__[methodName](self, *args, **kwargs) + def showLogin(self): '''When must we show the login field?''' if self.o.isTemporary(): return 'edit' @@ -51,6 +63,7 @@ class UserWrapper(AbstractWrapper): msg = self.translate(u'Passwords do not match.', domain='plone') errors.password1 = msg errors.password2 = msg + self._callCustom('validate', new, errors) def onEdit(self, created): self.title = self.firstName + ' ' + self.name @@ -86,11 +99,5 @@ class UserWrapper(AbstractWrapper): # Remove the user if it was in the corresponding group if groupName in userGroups: group.removeMember(self.login) - # Call the custom user "onEdit" method if it exists - # XXX This code does not work. - if len(self.__class__.__bases__) > 1: - customUser = self.__class__.__bases__[-1] - # There is a custom user class - if customUser.__dict__.has_key('onEdit'): - customUser.__dict__['onEdit'](self, created) + self._callCustom('onEdit', created) # ------------------------------------------------------------------------------ diff --git a/gen/plone25/wrappers/__init__.py b/gen/plone25/wrappers/__init__.py index 9492317..9930bfc 100644 --- a/gen/plone25/wrappers/__init__.py +++ b/gen/plone25/wrappers/__init__.py @@ -82,8 +82,6 @@ class AbstractWrapper: else: return 1 def get_tool(self): return self.o.getTool().appy() tool = property(get_tool) - def get_flavour(self): return self.o.getTool().getFlavour(self.o, appy=True) - flavour = property(get_flavour) def get_request(self): return self.o.REQUEST request = property(get_request) def get_session(self): return self.o.REQUEST.SESSION @@ -204,9 +202,9 @@ class AbstractWrapper: ploneObj.reindexObject() return appyObj - def translate(self, label, mapping={}, domain=None): + def translate(self, label, mapping={}, domain=None, language=None): '''Check documentation of self.o.translate.''' - return self.o.translate(label, mapping, domain) + return self.o.translate(label, mapping, domain, language=language) def do(self, transition, comment='', doAction=False, doNotify=False, doHistory=True): @@ -276,27 +274,25 @@ class AbstractWrapper: p_maxResults. If p_noSecurity is specified, you get all objects, even if the logged user does not have the permission to view it.''' # Find the content type corresponding to p_klass - flavour = self.flavour - contentType = flavour.o.getPortalType(klass) + contentType = self.tool.o.getPortalType(klass) # Create the Search object search = Search('customSearch', sortBy=sortBy, **fields) if not maxResults: maxResults = 'NO_LIMIT' # If I let maxResults=None, only a subset of the results will be # returned by method executeResult. - res = self.tool.o.executeQuery(contentType,flavour.number,search=search, - maxResults=maxResults, noSecurity=noSecurity) + res = self.tool.o.executeQuery(contentType, search=search, + maxResults=maxResults, noSecurity=noSecurity) return [o.appy() for o in res['objects']] def count(self, klass, noSecurity=False, **fields): '''Identical to m_search above, but returns the number of objects that match the search instead of returning the objects themselves. Use this method instead of writing len(self.search(...)).''' - flavour = self.flavour - contentType = flavour.o.getPortalType(klass) + contentType = self.tool.o.getPortalType(klass) search = Search('customSearch', **fields) - res = self.tool.o.executeQuery(contentType,flavour.number,search=search, - brainsOnly=True, noSecurity=noSecurity) + res = self.tool.o.executeQuery(contentType, search=search, + brainsOnly=True, noSecurity=noSecurity) if res: return res._len # It is a LazyMap instance else: return 0 @@ -325,14 +321,12 @@ class AbstractWrapper: "for obj in self.search(MyClass,...)" ''' - flavour = self.flavour - contentType = flavour.o.getPortalType(klass) + contentType = self.tool.o.getPortalType(klass) search = Search('customSearch', sortBy=sortBy, **fields) # Initialize the context variable "ctx" ctx = context - for brain in self.tool.o.executeQuery(contentType, flavour.number, \ - search=search, brainsOnly=True, maxResults=maxResults, - noSecurity=noSecurity): + for brain in self.tool.o.executeQuery(contentType, search=search, \ + brainsOnly=True, maxResults=maxResults, noSecurity=noSecurity): # Get the Appy object from the brain obj = brain.getObject().appy() exec expression @@ -379,5 +373,5 @@ class AbstractWrapper: p_data must be a dictionary whose keys are field names (strings) and whose values are the previous field values.''' - self.o.addDataChange(data, labels=False) + self.o.addDataChange(data) # ------------------------------------------------------------------------------ diff --git a/gen/po.py b/gen/po.py index 1962cc4..9be7602 100644 --- a/gen/po.py +++ b/gen/po.py @@ -26,20 +26,22 @@ fallbacks = {'en': 'en-us en-ca', class PoMessage: '''Represents a i18n message (po format).''' CONFIG = "Configuration panel for product '%s'" - FLAVOUR = "Configuration flavour" - # The following messages (starting with MSG_) correspond to flavour + # The following messages (starting with MSG_) correspond to tool # attributes added for every gen-class (warning: the message IDs correspond - # to MSG_). - MSG_optionalFieldsFor = 'Optional fields' - MSG_defaultValueFor = "Default value for field '%s'" - MSG_podTemplatesFor = "POD templates" - MSG_podMaxShownTemplatesFor = "Max shown POD templates" - MSG_resultColumnsFor = "Columns to display while showing query results" - MSG_showWorkflowFor = 'Show workflow-related information' - MSG_showWorkflowCommentFieldFor = 'Show field allowing to enter a ' \ - 'comment every time a transition is triggered' - MSG_showAllStatesInPhaseFor = 'Show all states in phase' - POD_TEMPLATE = 'POD template' + # to MSG_). + MSG_defaultValue = "Default value for field '%s'" + MSG_podTemplate = "POD template for field '%s'" + MSG_formats = "Output format(s) for field '%s'" + MSG_resultColumns = "Columns to display while showing query results" + MSG_enableAdvancedSearch = "Enable advanced search" + MSG_numberOfSearchColumns = "Number of search columns" + MSG_searchFields = "Search fields" + MSG_optionalFields = 'Optional fields' + MSG_showWorkflow = 'Show workflow-related information' + MSG_showWorkflowCommentField = 'Show field allowing to enter a ' \ + 'comment every time a transition is ' \ + 'triggered' + MSG_showAllStatesInPhase = 'Show all states in phase' USER = 'User' POD_ASKACTION = 'Trigger related action' DEFAULT_VALID_ERROR = 'Please fill or correct this.' diff --git a/gen/test/applications/AppyCar/__init__.py b/gen/test/applications/AppyCar/__init__.py index cc00fba..5f6b0c4 100644 --- a/gen/test/applications/AppyCar/__init__.py +++ b/gen/test/applications/AppyCar/__init__.py @@ -32,6 +32,3 @@ class StandardRadio(Radio): c = Config() c.languages = ('en', 'fr') - -class CarFlavour(Flavour): - explanation = String(group="userInterface") diff --git a/gen/test/applications/ZopeComponent.py b/gen/test/applications/ZopeComponent.py index 5ecfc81..4fb816a 100644 --- a/gen/test/applications/ZopeComponent.py +++ b/gen/test/applications/ZopeComponent.py @@ -9,16 +9,6 @@ class ZopeComponentTool(Tool): self.someUsefulConfigurationOption = 'My app is configured now!' install = Action(action=onInstall) -class ZopeComponentFlavour(Flavour): - anIntegerOption = Integer() - bunchesOfGeeks = Ref(BunchOfGeek, multiplicity=(0,None), add=True, - link=False, back=Ref(attribute='backToTool'), - shownInfo=('description',), page='data') - def onEdit(self, created): - if 'Escadron de la mort' not in [b.title for b in self.bunchesOfGeeks]: - self.create('bunchesOfGeeks', title='Escadron de la mort', - description='I want those guys everywhere!') - class ZopeComponentWorkflow: # Specific permissions wf = WritePermission('ZopeComponent.funeralDate') diff --git a/shared/dav.py b/shared/dav.py new file mode 100644 index 0000000..0eb4265 --- /dev/null +++ b/shared/dav.py @@ -0,0 +1,107 @@ +# ------------------------------------------------------------------------------ +import os, re, httplib, sys +from StringIO import StringIO +from mimetypes import guess_type +from base64 import encodestring + +# ------------------------------------------------------------------------------ +urlRex = re.compile(r'http://([^:/]+)(:[0-9]+)?(/.+)?', re.I) + +# ------------------------------------------------------------------------------ +class Resource: + '''Every instance of this class represents some web resource accessible + through WebDAV.''' + + def __init__(self, url, username=None, password=None): + self.username = username + self.password = password + self.url = url + + # Split the URL into its components + res = urlRex.match(url) + if res: + host, port, uri = res.group(1,2,3) + self.host = host + self.port = port and int(port[1:]) or 80 + self.uri = uri or '/' + else: raise 'Wrong URL: %s' % str(url) + + def updateHeaders(self, headers): + # Add credentials if present + if not (self.username and self.password): return + if headers.has_key('Authorization'): return + credentials = '%s:%s' % (self.username,self.password) + credentials = credentials.replace('\012','') + headers['Authorization'] = "Basic %s" % encodestring(credentials) + headers['User-Agent'] = 'WebDAV.client' + headers['Host'] = self.host + headers['Connection'] = 'close' + headers['Accept'] = '*/*' + return headers + + def sendRequest(self, method, uri, body=None, headers={}): + '''Sends a HTTP request with p_method, for p_uri.''' + conn = httplib.HTTP() + conn.connect(self.host, self.port) + conn.putrequest(method, uri) + # Add HTTP headers + self.updateHeaders(headers) + for n, v in headers.items(): conn.putheader(n, v) + conn.endheaders() + if body: conn.send(body) + ver, code, msg = conn.getreply() + data = conn.getfile().read() + conn.close() + return data + + def mkdir(self, name): + '''Creates a folder named p_name in this resource.''' + folderUri = self.uri + '/' + name + #body = '' \ + # '%s' \ + # '' % name + return self.sendRequest('MKCOL', folderUri) + + def delete(self, name): + '''Deletes a file or a folder (and all contained files if any) named + p_name within this resource.''' + toDeleteUri = self.uri + '/' + name + return self.sendRequest('DELETE', toDeleteUri) + + def add(self, content, type='fileName', name=''): + '''Adds a file in this resource. p_type can be: + - "fileName" In this case, p_content is the path to a file on disk + and p_name is ignored; + - "zope" In this case, p_content is an instance of + OFS.Image.File and the name of the file is given in + p_name. + ''' + if type == 'fileName': + # p_content is the name of a file on disk + f = file(content, 'rb') + body = f.read() + f.close() + fileName = os.path.basename(content) + fileType, encoding = guess_type(fileName) + elif type == 'zope': + # p_content is a "Zope" file, ie a OFS.Image.File instance + fileName = name + fileType = content.content_type + encoding = None + if isinstance(content.data, basestring): + # The file content is here, in one piece + body = content.data + else: + # There are several parts to this file. + body = '' + data = content.data + while data is not None: + body += data.data + data = data.next + fileUri = self.uri + '/' + fileName + headers = {} + if fileType: headers['Content-Type'] = fileType + if encoding: headers['Content-Encoding'] = encoding + headers['Content-Length'] = str(len(body)) + return self.sendRequest('PUT', fileUri, body, headers) +# ------------------------------------------------------------------------------ diff --git a/shared/utils.py b/shared/utils.py index b4b9de3..fa0646e 100644 --- a/shared/utils.py +++ b/shared/utils.py @@ -125,4 +125,153 @@ def normalizeString(s, usage='fileName'): # ------------------------------------------------------------------------------ typeLetters = {'b': bool, 'i': int, 'j': long, 'f':float, 's':str, 'u':unicode, 'l': list, 'd': dict} + +# ------------------------------------------------------------------------------ +class CodeAnalysis: + '''This class holds information about some code analysis (line counts) that + spans some folder hierarchy.''' + def __init__(self, name): + self.name = name # Let's give a name for the analysis + self.numberOfFiles = 0 # The total number of analysed files + self.emptyLines = 0 # The number of empty lines within those files + self.commentLines = 0 # The number of comment lines + # A code line is defined as anything that is not an empty or comment + # line. + self.codeLines = 0 + + def numberOfLines(self): + '''Computes the total number of lines within analysed files.''' + return self.emptyLines + self.commentLines + self.codeLines + + def analyseZptFile(self, theFile): + '''Analyses the ZPT file named p_fileName.''' + inDoc = False + for line in theFile: + stripped = line.strip() + # Manage a comment + if not inDoc and (line.find('') != -1: + inDoc = False + continue + # Manage an empty line + if not stripped: + self.emptyLines += 1 + else: + self.codeLines += 1 + + docSeps = ('"""', "'''") + def isPythonDoc(self, line, start, isStart=False): + '''Returns True if we find, in p_line, the start of a docstring (if + p_start is True) or the end of a docstring (if p_start is False). + p_isStart indicates if p_line is the start of the docstring.''' + if start: + res = line.startswith(self.docSeps[0]) or \ + line.startswith(self.docSeps[1]) + else: + sepOnly = (line == self.docSeps[0]) or (line == self.docSeps[1]) + if sepOnly: + # If the line contains the separator only, is this the start or + # the end of the docstring? + if isStart: res = False + else: res = True + else: + res = line.endswith(self.docSeps[0]) or \ + line.endswith(self.docSeps[1]) + return res + + def analysePythonFile(self, theFile): + '''Analyses the Python file named p_fileName.''' + # Are we in a docstring ? + inDoc = False + for line in theFile: + stripped = line.strip() + # Manage a line that is within a docstring + inDocStart = False + if not inDoc and self.isPythonDoc(stripped, start=True): + inDoc = True + inDocStart = True + if inDoc: + self.commentLines += 1 + if self.isPythonDoc(stripped, start=False, isStart=inDocStart): + inDoc = False + continue + # Manage an empty line + if not stripped: + self.emptyLines += 1 + continue + # Manage a comment line + if line.startswith('#'): + self.commentLines += 1 + continue + # If we are here, we have a code line. + self.codeLines += 1 + + def analyseFile(self, fileName): + '''Analyses file named p_fileName.''' + self.numberOfFiles += 1 + theFile = file(fileName) + if fileName.endswith('.py'): + self.analysePythonFile(theFile) + elif fileName.endswith('.pt'): + self.analyseZptFile(theFile) + theFile.close() + + def printReport(self): + '''Returns the analysis report as a string, only if there is at least + one analysed line.''' + lines = self.numberOfLines() + if not lines: return + commentRate = (self.commentLines / float(lines)) * 100.0 + blankRate = (self.emptyLines / float(lines)) * 100.0 + print '%s: %d files, %d lines (%.0f%% comments, %.0f%% blank)' % \ + (self.name, self.numberOfFiles, lines, commentRate, blankRate) + +class LinesCounter: + '''Counts and classifies the lines of code within a folder hierarchy.''' + def __init__(self, folderOrModule): + if isinstance(folderOrModule, basestring): + # It is the path of some folder + self.folder = folderOrModule + else: + # It is a Python module + self.folder = os.path.dirname(folderOrModule.__file__) + # These dicts will hold information about analysed files + self.python = {False: CodeAnalysis('Python'), + True: CodeAnalysis('Python (test)')} + self.zpt = {False: CodeAnalysis('ZPT'), + True: CodeAnalysis('ZPT (test)')} + # Are we currently analysing real or test code? + self.inTest = False + + def printReport(self): + '''Displays on stdout a small analysis report about self.folder.''' + for zone in (False, True): self.python[zone].printReport() + for zone in (False, True): self.zpt[zone].printReport() + + def run(self): + '''Let's start the analysis of self.folder.''' + # The test markers will allow us to know if we are analysing test code + # or real code within a given part of self.folder code hierarchy. + testMarker1 = '%stest%s' % (os.sep, os.sep) + testMarker2 = '%stest' % os.sep + j = os.path.join + for root, folders, files in os.walk(self.folder): + rootName = os.path.basename(root) + if rootName.startswith('.') or \ + (rootName in ('tmp', 'temp')): + continue + # Are we in real code or in test code ? + self.inTest = False + if root.endswith(testMarker2) or (root.find(testMarker1) != -1): + self.inTest = True + # Scan the files in this folder + for fileName in files: + if fileName.endswith('.py'): + self.python[self.inTest].analyseFile(j(root, fileName)) + elif fileName.endswith('.pt'): + self.zpt[self.inTest].analyseFile(j(root, fileName)) + self.printReport() # ------------------------------------------------------------------------------