From 93619dafe1e8ada6fb0eee37f0b81ef034749902 Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Mon, 26 Sep 2011 21:19:34 +0200 Subject: [PATCH] appy.gen: reworked management of Ref fields, that do not use Archetypes and reference catalog anymore. appy.gen: added a mechanism for migrating from one Appy version to another, automatically, when reinstalling an Appy application. --- bin/generate.py | 2 + gen/__init__.py | 66 ++++++++-------------- gen/plone25/descriptors.py | 47 ++-------------- gen/plone25/generator.py | 29 +++++----- gen/plone25/installer.py | 67 ++++++----------------- gen/plone25/migrator.py | 63 +++++++++++++++++++++ gen/plone25/mixins/ToolMixin.py | 4 +- gen/plone25/mixins/__init__.py | 39 ++++++------- gen/plone25/model.py | 3 +- gen/plone25/templates/Class.py | 7 +-- gen/plone25/utils.py | 84 ---------------------------- gen/plone25/wrappers/__init__.py | 94 ++++++++++++++++---------------- gen/utils.py | 15 +++++ 13 files changed, 209 insertions(+), 311 deletions(-) create mode 100644 gen/plone25/migrator.py delete mode 100644 gen/plone25/utils.py diff --git a/bin/generate.py b/bin/generate.py index 0347af2..f425f32 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -5,6 +5,7 @@ import sys, os.path from optparse import OptionParser from appy.gen.generator import GeneratorError from appy.shared.utils import LinesCounter +import appy.version # ------------------------------------------------------------------------------ ERROR_CODE = 1 @@ -103,6 +104,7 @@ class GeneratorScript: (options, args) = optParser.parse_args() try: self.manageArgs(optParser, options, args) + print 'Appy version:', appy.version.verbose print 'Generating %s product in %s...' % (args[1], args[2]) self.generateProduct(options, *args) # Give the user some statistics about its code diff --git a/gen/__init__.py b/gen/__init__.py index 4607632..2cf0558 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -528,7 +528,6 @@ class Type: # We must initialise the corresponding back reference self.back.klass = klass self.back.init(self.back.attribute, self.klass, appName) - self.back.relationship = '%s_%s_rel' % (prefix, name) def reload(self, klass, obj): '''In debug mode, we want to reload layouts without restarting Zope. @@ -1735,18 +1734,16 @@ class Ref(Type): if (layoutType == 'edit') and self.add: return False if self.isBack: if layoutType == 'edit': return False - else: - return obj.getBRefs(self.relationship) + else: return getattr(obj, self.name, None) return res def getValue(self, obj, type='objects', noListIfSingleObj=False, startNumber=None, someObjects=False): - '''Returns the objects linked to p_obj through Ref field "self". + '''Returns the objects linked to p_obj through this Ref field. - 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. @@ -1756,23 +1753,7 @@ class Ref(Type): If p_someObjects is True, it returns an instance of SomeObjects instead of returning a list of references.''' - if self.isBack: - getRefs = obj.reference_catalog.getBackReferences - uids = [r.sourceUID for r in getRefs(obj, self.relationship)] - else: - 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) + uids = getattr(obj, self.name, []) if not uids: # Maybe is there a default value? defValue = Type.getValue(self, obj) @@ -1783,8 +1764,8 @@ class Ref(Type): uids = [o.o.UID() for o in defValue] else: uids = [defValue.o.UID()] - # Prepare the result: an instance of SomeObjects, that, in this case, - # represent a subset of all referred objects + # Prepare the result: an instance of SomeObjects, that will be unwrapped + # if not required. res = SomeObjects() res.totalNumber = res.batchSize = len(uids) batchNeeded = startNumber != None @@ -1792,10 +1773,8 @@ class Ref(Type): res.batchSize = self.maxPerPage if startNumber != None: res.startNumber = startNumber - # Get the needed referred objects + # Get the objects given their uids i = res.startNumber - # Is it possible and more efficient to perform a single query in - # portal_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 @@ -1852,22 +1831,23 @@ class Ref(Type): * a Appy object; * a list of Appy or Zope objects. ''' - # Standardize the way p_value is expressed - refs = value - if not refs: refs = [] - if type(refs) not in sequenceTypes: refs = [refs] - for i in range(len(refs)): - if isinstance(refs[i], basestring): - # Get the Zope object from the UID - refs[i] = obj.portal_catalog(UID=refs[i])[0].getObject() - else: - refs[i] = refs[i].o # Now we are sure to have a Zope object. - # Update the field storing on p_obj the ordered list of UIDs - sortedRefs = obj._appy_getSortedField(self.name) - del sortedRefs[:] - for ref in refs: sortedRefs.append(ref.UID()) - # Update the Archetypes Ref field. - exec 'obj.set%s%s(refs)' % (self.name[0].upper(), self.name[1:]) + # Standardize p_value into a list of uids + uids = value + if not uids: uids = [] + if type(uids) not in sequenceTypes: uids = [uids] + for i in range(len(uids)): + if not isinstance(uids[i], basestring): + # Get the UID from the Zope or Appy object + uids[i] = uids[i].o.UID() + # Update the list of referred uids. + refs = getattr(obj, self.name, None) + if refs == None: + refs = obj.getProductConfig().PersistentList(uids) + setattr(obj, self.name, refs) + else: + # Empty the list and fill it with uids + del refs[:] + for uid in uids: refs.append(uid) def clone(self, forTool=True): '''Produces a clone of myself.''' diff --git a/gen/plone25/descriptors.py b/gen/plone25/descriptors.py index 764b362..bbcdf7a 100644 --- a/gen/plone25/descriptors.py +++ b/gen/plone25/descriptors.py @@ -7,7 +7,6 @@ # ------------------------------------------------------------------------------ import types, copy from model import ModelClass, toolFieldPrefixes -from utils import stringify import appy.gen import appy.gen.descriptors from appy.gen.po import PoMessage @@ -95,14 +94,8 @@ class FieldDescriptor: def walkRef(self): '''How to generate a Ref?''' - relationship = '%s_%s_rel' % (self.classDescr.name, self.fieldName) - self.fieldType = 'ReferenceField' - self.widgetType = 'ReferenceWidget' - self.fieldParams['relationship'] = relationship - if self.appyType.isMultiValued(): - self.fieldParams['multiValued'] = True # Update the list of referers - self.generator.addReferer(self, relationship) + self.generator.addReferer(self) # Add the widget label for the back reference refClassName = getClassName(self.appyType.klass, self.applicationName) backLabel = "%s_%s" % (refClassName, self.appyType.back.attribute) @@ -185,35 +178,12 @@ class FieldDescriptor: def generate(self): '''Generates the i18n labels for this type.''' self.walkAppyType() - if self.appyType.type != 'Ref': return - res = '' - s = stringify - spaces = TABS - # Generate field name - res += ' '*spaces + self.fieldType + '(\n' - # Generate field parameters - spaces += TABS - for fParamName, fParamValue in self.fieldParams.iteritems(): - res += ' '*spaces + fParamName + '=' + s(fParamValue) + ',\n' - # Generate widget - res += ' '*spaces + 'widget=%s(\n' % self.widgetType - spaces += TABS - for wParamName, wParamValue in self.widgetParams.iteritems(): - res += ' '*spaces + wParamName + '=' + s(wParamValue) + ',\n' - # End of widget definition - spaces -= TABS - res += ' '*spaces + ')\n' - # End of field definition - spaces -= TABS - res += ' '*spaces + '),\n' - return res class ClassDescriptor(appy.gen.descriptors.ClassDescriptor): '''Represents an Archetypes-compliant class.''' def __init__(self, klass, orderedAttributes, generator): appy.gen.descriptors.ClassDescriptor.__init__(self, klass, orderedAttributes, generator) - self.schema = '' # The archetypes schema will be generated here self.methods = '' # Needed method definitions will be generated here # We remember here encountered pages and groups defined in the Appy # type. Indeed, after having parsed all application classes, we will @@ -247,11 +217,10 @@ class ClassDescriptor(appy.gen.descriptors.ClassDescriptor): return (parentWrapper, parentClass) 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, user, etc) we must avoid having attributes that rely on - the configuration (ie attributes that are optional, with - editDefault=True, etc).''' + '''Generates i18n and other related stuff for this class. If this class + is in the configuration (tool, user, etc) we must avoid having + attributes that rely on the configuration (ie attributes that are + optional, with editDefault=True, etc).''' for attrName in self.orderedAttributes: try: attrValue = getattr(self.klass, attrName) @@ -262,11 +231,7 @@ class ClassDescriptor(appy.gen.descriptors.ClassDescriptor): attrValue = copy.copy(attrValue) attrValue.optional = False attrValue.editDefault = False - field = FieldDescriptor(attrName, attrValue, self) - fieldDef = field.generate() - if fieldDef: - # Currently, we generate Archetypes fields for Refs only. - self.schema += '\n' + fieldDef + FieldDescriptor(attrName, attrValue, self).generate() def isAbstract(self): '''Is self.klass abstract?''' diff --git a/gen/plone25/generator.py b/gen/plone25/generator.py index 398e6c4..d016207 100644 --- a/gen/plone25/generator.py +++ b/gen/plone25/generator.py @@ -286,14 +286,13 @@ class Generator(AbstractGenerator): res = [r for r in res if eval('r.%s == %s' % (p, p))] return res - def addReferer(self, fieldDescr, relationship): - '''p_fieldDescr is a Ref type definition. We will create in config.py a - dict that lists all back references, by type.''' + def addReferer(self, fieldDescr): + '''p_fieldDescr is a Ref type definition.''' k = fieldDescr.appyType.klass refClassName = getClassName(k, self.applicationName) if not self.referers.has_key(refClassName): self.referers[refClassName] = [] - self.referers[refClassName].append( (fieldDescr, relationship)) + self.referers[refClassName].append(fieldDescr) def getAppyTypePath(self, name, appyType, klass, isBack=False): '''Gets the path to the p_appyType when a direct reference to an @@ -363,7 +362,7 @@ class Generator(AbstractGenerator): if not titleFound: names.insert(0, 'title') # Any backward attributes to append? if classDescr.name in self.referers: - for field, rel in self.referers[classDescr.name]: + for field in self.referers[classDescr.name]: names.append(field.appyType.back.attribute) qNames = ['"%s"' % name for name in names] attributes.append('"%s":[%s]' % (classDescr.name, ','.join(qNames))) @@ -536,10 +535,10 @@ class Generator(AbstractGenerator): self.labels += [ Msg(klass.name, '', klassType), Msg('%s_plural' % klass.name,'', klass.name+'s')] repls = self.repls.copy() - repls.update({'fields': klass.schema, 'methods': klass.methods, - 'genClassName': klass.name, 'imports': '','baseMixin':'BaseMixin', - 'baseSchema': 'BaseSchema', 'global_allow': 1, - 'parents': 'BaseMixin, BaseContent', 'static': '', + repls.update({'methods': klass.methods, 'genClassName': klass.name, + 'imports': '','baseMixin':'BaseMixin', 'baseSchema': 'BaseSchema', + 'global_allow': 1, 'parents': 'BaseMixin, BaseContent', + 'static': '', 'classDoc': 'User class for %s' % self.applicationName, 'implements': "(getattr(BaseContent,'__implements__',()),)", 'register': "registerType(%s, '%s')" % (klass.name, @@ -567,7 +566,7 @@ class Generator(AbstractGenerator): # Generate the Tool class repls = self.repls.copy() - repls.update({'fields': self.tool.schema, 'methods': self.tool.methods, + repls.update({'methods': self.tool.methods, 'genClassName': self.tool.name, 'imports':'', 'baseMixin':'ToolMixin', 'baseSchema': 'OrderedBaseFolderSchema', 'global_allow': 0, 'parents': 'ToolMixin, UniqueObject, OrderedBaseFolder', @@ -584,7 +583,7 @@ class Generator(AbstractGenerator): def generateClass(self, classDescr): '''Is called each time an Appy class is found in the application, for - generating the corresponding Archetype class and schema.''' + generating the corresponding Archetype class.''' k = classDescr.klass print 'Generating %s.%s (gen-class)...' % (k.__module__, k.__name__) if not classDescr.isAbstract(): @@ -625,9 +624,9 @@ class Generator(AbstractGenerator): 'className': classDescr.klass.__name__, 'global_allow': 1, 'genClassName': classDescr.name, 'baseMixin':'BaseMixin', 'classDoc': classDoc, 'applicationName': self.applicationName, - 'fields': classDescr.schema, 'methods': classDescr.methods, - 'implements': implements, 'baseSchema': baseSchema, 'static': '', - 'register': register, 'toolInstanceName': self.toolInstanceName}) + 'methods': classDescr.methods, 'implements': implements, + 'baseSchema': baseSchema, 'static': '', 'register': register, + 'toolInstanceName': self.toolInstanceName}) fileName = '%s.py' % classDescr.name # Create i18n labels (class name and plural form) poMsg = PoMessage(classDescr.name, '', classDescr.klass.__name__) @@ -651,7 +650,7 @@ class Generator(AbstractGenerator): poMsg.produceNiceDefault() if poMsg not in self.labels: self.labels.append(poMsg) - # Generate the resulting Archetypes class and schema. + # Generate the resulting Archetypes class. self.copyFile('Class.py', repls, destName=fileName) def generateWorkflow(self, wfDescr): diff --git a/gen/plone25/installer.py b/gen/plone25/installer.py index ae62069..0b61a7f 100644 --- a/gen/plone25/installer.py +++ b/gen/plone25/installer.py @@ -6,12 +6,14 @@ import os, os.path, time from StringIO import StringIO from sets import Set import appy +import appy.version from appy.gen import Type, Ref, String from appy.gen.po import PoParser -from appy.gen.utils import produceNiceMessage -from appy.gen.plone25.utils import updateRolesForPermission +from appy.gen.utils import produceNiceMessage, updateRolesForPermission from appy.shared.data import languages +from migrator import Migrator +# ------------------------------------------------------------------------------ class ZCTextIndexInfo: '''Silly class used for storing information about a ZCTextIndex.''' lexicon_id = "plone_lexicon" @@ -246,7 +248,6 @@ class PloneInstaller: self.tool.createOrUpdate(False, None) else: self.tool.createOrUpdate(True, None) - self.updatePodTemplates() def installTranslations(self): '''Creates or updates the translation objects within the tool.''' @@ -304,20 +305,6 @@ class PloneInstaller: site.portal_groups.setRolesForGroup(group, [role]) site.__ac_roles__ = tuple(data) - def installStyleSheet(self): - '''Registers In Plone the stylesheet linked to this application.''' - cssName = self.productName + '.css' - cssTitle = self.productName + ' CSS styles' - cssInfo = {'id': cssName, 'title': cssTitle} - portalCss = self.ploneSite.portal_css - try: - portalCss.unregisterResource(cssInfo['id']) - except: - pass - defaults = {'id': '', 'media': 'all', 'enabled': True} - defaults.update(cssInfo) - portalCss.registerStylesheet(**defaults) - def manageIndexes(self): '''For every indexed field, this method installs and updates the corresponding index if it does not exist yet.''' @@ -350,54 +337,34 @@ class PloneInstaller: site = self.ploneSite # Do not allow an anonymous user to register himself as new user site.manage_permission('Add portal member', ('Manager',), acquire=0) - # Call custom installer if any - if hasattr(self.appyTool, 'install'): - self.tool.executeAppyAction('install', reindex=False) # Replace Plone front-page with an application-specific page if needed if self.appFrontPage: frontPageName = self.productName + 'FrontPage' site.manage_changeProperties(default_page=frontPageName) + # Store the used Appy version (used for detecting new versions) + self.appyTool.appyVersion = appy.version.short + self.info('Appy version is %s.' % self.appyTool.appyVersion) + # Call custom installer if any + if hasattr(self.appyTool, 'install'): + self.tool.executeAppyAction('install', reindex=False) def info(self, msg): return self.appyTool.log(msg) def install(self): + # Begin with a migration if required. + self.installTool() + if self.reinstall: Migrator(self).run() self.installRootFolder() self.installTypes() - self.installTool() + self.manageLanguages() + self.manageIndexes() + self.updatePodTemplates() self.installTranslations() self.installRolesAndGroups() - self.installStyleSheet() - self.manageIndexes() - self.manageLanguages() self.finalizeInstallation() self.appyTool.log("Installation done.") - def uninstallTool(self): - site = self.ploneSite - # Unmention tool in the search form - portalProperties = getattr(site, 'portal_properties', None) - if portalProperties is not None: - siteProperties = getattr(portalProperties, 'site_properties', None) - if siteProperties is not None and \ - siteProperties.hasProperty('types_not_searched'): - current = list(siteProperties.getProperty('types_not_searched')) - if self.toolName in current: - current.remove(self.toolName) - siteProperties.manage_changeProperties( - **{'types_not_searched' : current}) - - # Unmention tool in the navigation - if portalProperties is not None: - nvProps = getattr(portalProperties, 'navtree_properties', None) - if nvProps is not None and nvProps.hasProperty('idsNotToList'): - current = list(nvProps.getProperty('idsNotToList')) - if self.toolInstanceName in current: - current.remove(self.toolInstanceName) - nvProps.manage_changeProperties(**{'idsNotToList': current}) - - def uninstall(self): - self.uninstallTool() - return 'Done.' + def uninstall(self): return 'Done.' # Stuff for tracking user activity --------------------------------------------- loggedUsers = {} diff --git a/gen/plone25/migrator.py b/gen/plone25/migrator.py new file mode 100644 index 0000000..a3db7b1 --- /dev/null +++ b/gen/plone25/migrator.py @@ -0,0 +1,63 @@ +# ------------------------------------------------------------------------------ +import time + +# ------------------------------------------------------------------------------ +class Migrator: + '''This class is responsible for performing migrations, when, on + installation, we've detected a new Appy version.''' + def __init__(self, installer): + self.installer = installer + + def migrateTo_0_7_1(self): + '''Appy 0.7.1 has its own management of Ref fields and does not use + Archetypes references and the reference catalog anymore. So we must + update data structures that store Ref info on instances.''' + ins = self.installer + ins.info('Migrating to Appy 0.7.1...') + allClassNames = [ins.tool.__class__.__name__] + ins.config.allClassNames + for className in allClassNames: + i = -1 + updated = 0 + ins.info('Analysing class "%s"...' % className) + refFields = None + for obj in ins.tool.executeQuery(className,\ + noSecurity=True)['objects']: + i += 1 + if i == 0: + # Get the Ref fields for objects of this class + refFields = [f for f in obj.getAllAppyTypes() \ + if (f.type == 'Ref') and not f.isBack] + if refFields: + refNames = ', '.join([rf.name for rf in refFields]) + ins.info(' Ref fields found: %s' % refNames) + else: + ins.info(' No Ref field found.') + break + isUpdated = False + for field in refFields: + # Attr for storing UIDs of referred objects has moved + # from _appy_[fieldName] to [fieldName]. + refs = getattr(obj, '_appy_%s' % field.name) + if refs: + isUpdated = True + setattr(obj, field.name, refs) + exec 'del obj._appy_%s' % field.name + # Set the back references + for refObject in field.getValue(obj): + refObject.link(field.back.name, obj, back=True) + if isUpdated: updated += 1 + if updated: + ins.info(' %d/%d object(s) updated.' % (updated, i+1)) + + def run(self): + i = self.installer + installedVersion = i.appyTool.appyVersion + startTime = time.time() + migrationRequired = False + if not installedVersion or (installedVersion <= '0.7.0'): + migrationRequired = True + self.migrateTo_0_7_1() + stopTime = time.time() + if migrationRequired: + i.info('Migration done in %d minute(s).'% ((stopTime-startTime)/60)) +# ------------------------------------------------------------------------------ diff --git a/gen/plone25/mixins/ToolMixin.py b/gen/plone25/mixins/ToolMixin.py index b79816a..98f1765 100644 --- a/gen/plone25/mixins/ToolMixin.py +++ b/gen/plone25/mixins/ToolMixin.py @@ -270,7 +270,7 @@ class ToolMixin(BaseMixin): # corresponding filter widget on the screen. if refObject: refField = refObject.getAppyType(refField) - params['UID'] = refObject._appy_getSortedField(refField.name).data + params['UID'] = getattr(refObject, refField.name).data # Determine what method to call on the portal catalog if noSecurity: catalogMethod = 'unrestrictedSearchResults' else: catalogMethod = 'searchResults' @@ -694,7 +694,7 @@ class ToolMixin(BaseMixin): # 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) + uids = getattr(masterObj, fieldName) # Display the reference widget at the page where the current object # lies. startNumberKey = '%s%s_startNumber' % (masterObj.UID(), fieldName) diff --git a/gen/plone25/mixins/__init__.py b/gen/plone25/mixins/__init__.py index 1800ed6..a45d843 100644 --- a/gen/plone25/mixins/__init__.py +++ b/gen/plone25/mixins/__init__.py @@ -11,7 +11,6 @@ from appy.gen.utils import * from appy.gen.layout import Table, defaultPageLayouts from appy.gen.descriptors import WorkflowDescriptor from appy.gen.plone25.descriptors import ClassDescriptor -from appy.gen.plone25.utils import updateRolesForPermission,checkTransitionGuard # ------------------------------------------------------------------------------ class BaseMixin: @@ -81,6 +80,11 @@ class BaseMixin: # Call a custom "onDelete" if it exists appyObj = self.appy() if hasattr(appyObj, 'onDelete'): appyObj.onDelete() + # Any people referencing me must forget me now + for field in self.getAllAppyTypes(): + if field.type != 'Ref': continue + for obj in field.getValue(self): + obj.unlink(field.back.name, self, back=True) # Delete the object self.getParentNode().manage_delObjects([self.id]) @@ -430,7 +434,6 @@ class BaseMixin: appyType = self.getAppyType(name) if not onlyIfSync or (onlyIfSync and appyType.sync[layoutType]): return appyType.getValue(self) - return None def getFormattedFieldValue(self, name, value): '''Gets a nice, string representation of p_value which is a value from @@ -442,9 +445,9 @@ class BaseMixin: If p_startNumber is None, this method returns all referred objects. If p_startNumber is a number, this method will return appyType.maxPerPage objects, starting at p_startNumber.''' - appyType = self.getAppyType(name) - return appyType.getValue(self, type='zobjects', someObjects=True, - startNumber=startNumber).__dict__ + field = self.getAppyType(name) + return field.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 @@ -494,9 +497,9 @@ class BaseMixin: def getAppyRefIndex(self, fieldName, obj): '''Gets the position of p_obj within Ref field named p_fieldName.''' - sortedObjectsUids = self._appy_getSortedField(fieldName) - res = sortedObjectsUids.index(obj.UID()) - return res + refs = getattr(self, fieldName, None) + if not refs: raise IndexError() + return refs.index(obj.UID()) def isDebug(self): '''Are we in debug mode ?''' @@ -782,14 +785,14 @@ class BaseMixin: '''This method changes the position of object with uid p_objectUid in reference field p_fieldName to p_newIndex i p_isDelta is False, or to actualIndex+p_newIndex if p_isDelta is True.''' - sortedObjectsUids = self._appy_getSortedField(fieldName) - oldIndex = sortedObjectsUids.index(objectUid) - sortedObjectsUids.remove(objectUid) + refs = getattr(self, fieldName, None) + oldIndex = refs.index(objectUid) + refs.remove(objectUid) if isDelta: newIndex = oldIndex + newIndex else: pass # To implement later on - sortedObjectsUids.insert(newIndex, objectUid) + refs.insert(newIndex, objectUid) def onChangeRefOrder(self): '''This method is called when the user wants to change order of an @@ -1074,7 +1077,7 @@ class BaseMixin: for appyType in self.getAllAppyTypes(): if appyType.type != 'Ref': continue if appyType.isBack or appyType.link: continue - # Indeed, no possibility to create objects with such Ref + # Indeed, no possibility to create objects with such Refs refType = self.getTool().getPortalType(appyType.klass) if refType not in addPermissions: continue # Get roles that may add this content type @@ -1111,16 +1114,6 @@ class BaseMixin: res = self.portal_type return res - def _appy_getSortedField(self, fieldName): - '''Gets, for reference field p_fieldName, the Appy persistent list - that contains the sorted list of referred object UIDs. If this list - does not exist, it is created.''' - sortedFieldName = '_appy_%s' % fieldName - if not hasattr(self.aq_base, sortedFieldName): - pList = self.getProductConfig().PersistentList - exec 'self.%s = pList()' % sortedFieldName - return getattr(self, sortedFieldName) - getUrlDefaults = {'page':True, 'nav':True} def getUrl(self, base=None, mode='view', **kwargs): '''Returns a Appy URL. diff --git a/gen/plone25/model.py b/gen/plone25/model.py index 678508a..3dc1e99 100644 --- a/gen/plone25/model.py +++ b/gen/plone25/model.py @@ -168,7 +168,7 @@ toolFieldPrefixes = ('defaultValue', 'podTemplate', 'formats', 'resultColumns', defaultToolFields = ('users', 'translations', 'enableNotifications', 'unoEnabledPython', 'openOfficePort', 'numberOfResultsPerPage', 'listBoxesMaximumWidth', - 'refreshSecurity') + 'appyVersion', 'refreshSecurity') class Tool(ModelClass): # In a ModelClass we need to declare attributes in the following list. @@ -181,6 +181,7 @@ class Tool(ModelClass): openOfficePort = Integer(default=2002, group="connectionToOpenOffice") numberOfResultsPerPage = Integer(default=30) listBoxesMaximumWidth = Integer(default=100) + appyVersion = String(show=False, layouts='f') def refreshSecurity(self): pass # Real method in the wrapper refreshSecurity = Action(action=refreshSecurity, confirm=True) # Ref(User) will maybe be transformed into Ref(CustomUserClass). diff --git a/gen/plone25/templates/Class.py b/gen/plone25/templates/Class.py index b32d554..0411c2d 100644 --- a/gen/plone25/templates/Class.py +++ b/gen/plone25/templates/Class.py @@ -8,11 +8,6 @@ from appy.gen.plone25.mixins import BaseMixin from appy.gen.plone25.mixins.ToolMixin import ToolMixin from Extensions.appyWrappers import _Wrapper - -schema = Schema(( -),) -fullSchema = .copy() + schema.copy() - class (): '''''' security = ClassSecurityInfo() @@ -30,7 +25,7 @@ class (): typeDescMsgId = '' i18nDomain = '' wrapperClass = _Wrapper - schema = fullSchema + schema = .copy() for elem in dir(): if not elem.startswith('__'): security.declarePublic(elem) diff --git a/gen/plone25/utils.py b/gen/plone25/utils.py deleted file mode 100644 index c4425bf..0000000 --- a/gen/plone25/utils.py +++ /dev/null @@ -1,84 +0,0 @@ -# ------------------------------------------------------------------------------ -def stringify(value): - '''Transforms p_value such that it can be dumped as a string into a - generated file.''' - if isinstance(value, tuple) or isinstance(value, list): - res = '(' - for v in value: - res += '%s,' % stringify(v) - res += ')' - elif value.__class__.__name__ == 'DateTime': - res = 'DateTime("%s")' % value.strftime('%Y/%m/%d %H:%M') - else: - res = str(value) - if isinstance(value, basestring): - if value.startswith('python:'): - res = value[7:] - else: - res = "'%s'" % value.replace("'", "\\'") - res = res.replace('\n', '\\n') - return res - -# ------------------------------------------------------------------------------ -def updateRolesForPermission(permission, roles, obj): - '''Adds roles from list p_roles to the list of roles that are granted - p_permission on p_obj.''' - from AccessControl.Permission import Permission - # Find existing roles that were granted p_permission on p_obj - existingRoles = () - for p in obj.ac_inherited_permissions(1): - name, value = p[:2] - if name == permission: - perm = Permission(name, value, obj) - existingRoles = perm.getRoles() - allRoles = set(existingRoles).union(roles) - obj.manage_permission(permission, tuple(allRoles), acquire=0) - -# ------------------------------------------------------------------------------ -def checkTransitionGuard(guard, sm, wf_def, ob): - '''This method is similar to DCWorkflow.Guard.check, but allows to retrieve - the truth value as a appy.gen.No instance, not simply "1" or "0".''' - from Products.DCWorkflow.Expression import StateChangeInfo,createExprContext - u_roles = None - if wf_def.manager_bypass: - # Possibly bypass. - u_roles = sm.getUser().getRolesInContext(ob) - if 'Manager' in u_roles: - return 1 - if guard.permissions: - for p in guard.permissions: - if _checkPermission(p, ob): - break - else: - return 0 - if guard.roles: - # Require at least one of the given roles. - if u_roles is None: - u_roles = sm.getUser().getRolesInContext(ob) - for role in guard.roles: - if role in u_roles: - break - else: - return 0 - if guard.groups: - # Require at least one of the specified groups. - u = sm.getUser() - b = aq_base( u ) - if hasattr( b, 'getGroupsInContext' ): - u_groups = u.getGroupsInContext( ob ) - elif hasattr( b, 'getGroups' ): - u_groups = u.getGroups() - else: - u_groups = () - for group in guard.groups: - if group in u_groups: - break - else: - return 0 - expr = guard.expr - if expr is not None: - econtext = createExprContext(StateChangeInfo(ob, wf_def)) - res = expr(econtext) - return res - return 1 -# ------------------------------------------------------------------------------ diff --git a/gen/plone25/wrappers/__init__.py b/gen/plone25/wrappers/__init__.py index 86732da..a1590e1 100644 --- a/gen/plone25/wrappers/__init__.py +++ b/gen/plone25/wrappers/__init__.py @@ -86,65 +86,67 @@ class AbstractWrapper(object): def getField(self, name): return self.o.getAppyType(name) - def link(self, fieldName, obj): + def link(self, fieldName, obj, back=False): '''This method links p_obj (which can be a list of objects) to this one - through reference field p_fieldName.''' - postfix = 'et%s%s' % (fieldName[0].upper(), fieldName[1:]) - # Update the Archetypes reference field - exec 'objs = self.o.g%s()' % postfix - if not objs: - objs = [] - elif type(objs) not in sequenceTypes: - objs = [objs] - # Add p_obj to the existing objects + through reference field p_fieldName. As this method is recursive and + can be called to update the corresponding back reference, param + p_back is there, but, you, as Appy developer, should never set it + to True.''' + # p_objs can be a list of objects if type(obj) in sequenceTypes: - for o in obj: objs.append(o.o) - else: - objs.append(obj.o) - exec 'self.o.s%s(objs)' % postfix - # Update the ordered list of references - sorted = self.o._appy_getSortedField(fieldName) - if type(obj) in sequenceTypes: - for o in obj: sorted.append(o.o.UID()) - else: - sorted.append(obj.o.UID()) + for o in obj: self.link(fieldName, o, back=back) + return + # Gets the list of referred objects (=list of uids), or create it. + selfO = self.o + refs = getattr(selfO, fieldName, None) + if refs == None: + refs = selfO.getProductConfig().PersistentList() + setattr(selfO, fieldName, refs) + # Insert p_obj into it. + uid = obj.o.UID() + if uid not in refs: + refs.append(uid) + # Update the back reference + if not back: + backName = selfO.getAppyType(fieldName).back.attribute + obj.appy().link(backName, self, back=True) - def unlink(self, fieldName, obj): + def unlink(self, fieldName, obj, back=False): '''This method unlinks p_obj (which can be a list of objects) from this - one through reference field p_fieldName.''' - postfix = 'et%s%s' % (fieldName[0].upper(), fieldName[1:]) - # Update the Archetypes reference field. - exec 'objs = self.o.g%s()' % postfix - if not objs: return - if type(objs) not in sequenceTypes: objs = [objs] - # Remove p_obj from existing objects + one through reference field p_fieldName. As this method is recursive + and can be called to update the corresponding back reference, param + p_back is there, but, you, as Appy developer, should never set it + to True.''' + # p_objs can be a list of objects if type(obj) in sequenceTypes: - for o in obj: - if o.o in objs: objs.remove(o.o) - else: - if obj.o in objs: objs.remove(obj.o) - exec 'self.o.s%s(objs)' % postfix - # Update the ordered list of references - sorted = self.o._appy_getSortedField(fieldName) - if type(obj) in sequenceTypes: - for o in obj: - if o.o.UID() in sorted: - sorted.remove(o.o.UID()) - else: - if obj.o.UID() in sorted: - sorted.remove(obj.o.UID()) + for o in obj: self.unlink(fieldName, o, back=back) + return + # Get the list of referred objects + selfO = self.o + refs = getattr(selfO, fieldName, None) + if not refs: return + # Unlink the object + uid = obj.o.UID() + if uid in refs: + refs.remove(uid) + # Update the back reference + if not back: + backName = selfO.getAppyType(fieldName).back.attribute + obj.appy().unlink(backName, self, back=True) def sort(self, fieldName, sortKey='title', reverse=False): '''Sorts referred elements linked to p_self via p_fieldName according to a given p_sortKey which must be an attribute set on referred objects ("title", by default).''' - sortedUids = getattr(self.o, '_appy_%s' % fieldName) - c = self.o.portal_catalog - sortedUids.sort(lambda x,y: \ + selfO = self.o + refs = getattr(selfO, fieldName, None) + if not refs: return + c = selfO.portal_catalog + refs.sort(lambda x,y: \ cmp(getattr(c(UID=x)[0].getObject().appy(), sortKey), getattr(c(UID=y)[0].getObject().appy(), sortKey))) if reverse: - sortedUids.reverse() + refs.reverse() def create(self, fieldNameOrClass, **kwargs): '''If p_fieldNameOfClass is the name of a field, this method allows to diff --git a/gen/utils.py b/gen/utils.py index 63d7884..bb5e413 100644 --- a/gen/utils.py +++ b/gen/utils.py @@ -314,4 +314,19 @@ def getClassName(klass, appName=None): else: # This is a standard class res = klass.__module__.replace('.', '_') + '_' + klass.__name__ return res + +# ------------------------------------------------------------------------------ +def updateRolesForPermission(permission, roles, obj): + '''Adds roles from list p_roles to the list of roles that are granted + p_permission on p_obj.''' + from AccessControl.Permission import Permission + # Find existing roles that were granted p_permission on p_obj + existingRoles = () + for p in obj.ac_inherited_permissions(1): + name, value = p[:2] + if name == permission: + perm = Permission(name, value, obj) + existingRoles = perm.getRoles() + allRoles = set(existingRoles).union(roles) + obj.manage_permission(permission, tuple(allRoles), acquire=0) # ------------------------------------------------------------------------------