From eb52c1bb7d4f36e65d6ae27bc65aa254b214f1f4 Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Thu, 2 Sep 2010 16:16:08 +0200 Subject: [PATCH] Bugfix in new.py; added new user management. --- bin/new.py | 10 +- gen/__init__.py | 121 ++++++--- gen/generator.py | 12 +- gen/layout.py | 2 +- gen/plone25/descriptors.py | 60 +++-- gen/plone25/generator.py | 237 ++++++++++-------- gen/plone25/installer.py | 131 +++++----- gen/plone25/mixins/FlavourMixin.py | 27 +- gen/plone25/mixins/ToolMixin.py | 14 +- gen/plone25/mixins/UserMixin.py | 7 + gen/plone25/mixins/__init__.py | 71 ++---- gen/plone25/model.py | 33 ++- gen/plone25/skin/portlet.pt | 2 +- gen/plone25/skin/widgets/ref.pt | 8 +- gen/plone25/skin/widgets/string.pt | 20 +- gen/plone25/templates/FlavourTemplate.py | 1 - gen/plone25/templates/Install.py | 35 +-- gen/plone25/templates/PodTemplate.py | 4 +- gen/plone25/templates/Styles.css.dtml | 16 +- gen/plone25/templates/UserTemplate.py | 34 +++ gen/plone25/templates/__init__.py | 20 +- gen/plone25/templates/appyWrappers.py | 4 + gen/plone25/templates/config.py | 43 +++- gen/plone25/wrappers/UserWrapper.py | 93 +++++++ gen/plone25/wrappers/__init__.py | 12 +- gen/po.py | 1 + gen/utils.py | 17 ++ ...Iso639.2.txt => LanguageCodesIso639.2.txt} | 0 shared/data/__init__.py | 179 ++++++++++++- shared/utils.py | 25 +- 30 files changed, 842 insertions(+), 397 deletions(-) create mode 100644 gen/plone25/mixins/UserMixin.py create mode 100644 gen/plone25/templates/UserTemplate.py create mode 100644 gen/plone25/wrappers/UserWrapper.py rename shared/data/{CountryCodesIso639.2.txt => LanguageCodesIso639.2.txt} (100%) diff --git a/bin/new.py b/bin/new.py index c529302..c7466f9 100644 --- a/bin/new.py +++ b/bin/new.py @@ -5,7 +5,7 @@ # ------------------------------------------------------------------------------ import os, os.path, sys, shutil from optparse import OptionParser -from appy.shared.utils import cleanFolder +from appy.shared.utils import cleanFolder, copyFolder # ------------------------------------------------------------------------------ class NewError(Exception): pass @@ -103,7 +103,7 @@ class NewScript: os.system(cmd) else: # Copy thre product into the instance - shutil.copytree(folderName, destFolder) + copyFolder(folderName, destFolder) uglyChunks = ('pkg_resources', '.declare_namespace(') def findPythonPackageInEgg(self, currentFolder): @@ -199,7 +199,7 @@ class NewScript: # A Zope product. Copy its content in Products. innerFolder= self.getSubFolder(self.getSubFolder(eggMainFolder)) destFolder = j(productsFolder, os.path.basename(innerFolder)) - shutil.copytree(innerFolder, destFolder) + copyFolder(innerFolder, destFolder) else: # A standard Python package. Copy its content in lib/python. # Go into the subFolder that is not EGG-INFO. @@ -218,7 +218,7 @@ class NewScript: # libFolder. innerFolder = self.getSubFolder(eggFolder) destFolder = j(productsFolder,os.path.basename(innerFolder)) - shutil.copytree(innerFolder, destFolder) + copyFolder(innerFolder, destFolder) else: packageFolder = self.findPythonPackageInEgg(eggFolder) # Create the destination folder(s) in the instance, @@ -252,7 +252,7 @@ class NewScript: else: destFolder = libFolder destFolder = j(destFolder, os.path.basename(packageFolder)) - shutil.copytree(packageFolder, destFolder) + copyFolder(packageFolder, destFolder) self.patchPlone(productsFolder, libFolder) def manageArgs(self, args): diff --git a/gen/__init__.py b/gen/__init__.py index a78baa4..3de1eb1 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -5,8 +5,8 @@ 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 -from appy.shared.data import countries + FileWrapper, getClassName +from appy.shared.data import languages # Default Appy permissions ----------------------------------------------------- r, w, d = ('read', 'write', 'delete') @@ -14,7 +14,8 @@ digit = re.compile('[0-9]') alpha = re.compile('[a-zA-Z0-9]') letter = re.compile('[a-zA-Z]') nullValues = (None, '', ' ') -validatorTypes = (types.FunctionType, type(re.compile(''))) +validatorTypes = (types.FunctionType, types.UnboundMethodType, + type(re.compile(''))) emptyTuple = () # Descriptor classes used for refining descriptions of elements in types @@ -383,14 +384,7 @@ class Type: self.name = name # Determine ids of i18n labels for this field if not klass: prefix = appName - elif klass.__module__.endswith('.appyWrappers'): - prefix = appName + klass.__name__ - elif Tool in klass.__bases__: - prefix = appName + 'Tool' - elif Flavour in klass.__bases__: - prefix = appName + 'Flavour' - else: - prefix = klass.__module__.replace('.', '_') + '_' + klass.__name__ + else: prefix = getClassName(klass, appName) self.labelId = '%s_%s' % (prefix, name) self.descrId = self.labelId + '_descr' self.helpId = self.labelId + '_help' @@ -583,7 +577,7 @@ class Type: value = self.getStorableValue(value) if self.validator and (type(self.validator) in validatorTypes): obj = obj.appy() - if type(self.validator) == validatorTypes[0]: + if type(self.validator) != validatorTypes[-1]: # It is a custom function. Execute it. try: validValue = self.validator(obj, value) @@ -598,7 +592,7 @@ class Type: return str(e) except: return obj.translate('%s_valid' % self.labelId) - elif type(self.validator) == validatorTypes[1]: + else: # It is a regular expression if not self.validator.match(value): # If the regular expression is among the default ones, we @@ -739,7 +733,7 @@ class String(Type): # Maximum size is 34 chars if (len(v) < 8) or (len(v) > 34): return False # 2 first chars must be a valid country code - if not countries.exists(v[:2].lower()): return False + if not languages.exists(v[:2].lower()): return False # 2 next chars are a control code whose value must be between 0 and 96. try: code = int(v[2:4]) @@ -768,7 +762,7 @@ class String(Type): for c in value[:4]: if not letter.match(c): return False # 2 next chars must be a valid country code - if not countries.exists(value[4:6].lower()): return False + if not languages.exists(value[4:6].lower()): return False # Last chars represent some location within a country (a city, a # province...). They can only be letters or figures. for c in value[6:]: @@ -1356,24 +1350,71 @@ class Pod(Type): self.validable = False # Workflow-specific types ------------------------------------------------------ +class Role: + '''Represents a role.''' + ploneRoles = ('Manager', 'Member', 'Owner', 'Reviewer', 'Anonymous', + 'Authenticated') + ploneLocalRoles = ('Owner',) + ploneUngrantableRoles = ('Anonymous', 'Authenticated') + def __init__(self, name, local=False, grantable=True): + self.name = name + self.local = local # True if it can be used as local role only. + # It is a standard Plone role or an application-specific one? + self.plone = name in self.ploneRoles + if self.plone and (name in self.ploneLocalRoles): + self.local = True + self.grantable = grantable + if self.plone and (name in self.ploneUngrantableRoles): + self.grantable = False + # An ungrantable role is one that is, like the Anonymous or + # Authenticated roles, automatically attributed to a user. + class State: def __init__(self, permissions, initial=False, phase='main', show=True): - self.permissions = permissions #~{s_permissionName:[s_roleName]}~ This - # dict gives, for every permission managed by a workflow, the list of - # roles for which the permission is granted in this state. - # Standard permissions are 'read', 'write' and 'delete'. + self.usedRoles = {} + # The following dict ~{s_permissionName:[s_roleName|Role_role]}~ + # gives, for every permission managed by a workflow, the list of roles + # for which the permission is granted in this state. Standard + # permissions are 'read', 'write' and 'delete'. + self.permissions = permissions self.initial = initial self.phase = phase self.show = show - def getUsedRoles(self): - res = set() - for roleValue in self.permissions.itervalues(): - if isinstance(roleValue, basestring): - res.add(roleValue) - elif roleValue: - for role in roleValue: - res.add(role) - return list(res) + # Standardize the way roles are expressed within self.permissions + self.standardizeRoles() + + def getRole(self, role): + '''p_role can be the name of a role or a Role instance. If it is the + name of a role, this method returns self.usedRoles[role] if it + exists, or creates a Role instance, puts it in self.usedRoles and + returns it else. If it is a Role instance, the method stores it in + self.usedRoles if it not in it yet and returns it.''' + if isinstance(role, basestring): + if role in self.usedRoles: + return self.usedRoles[role] + else: + theRole = Role(role) + self.usedRoles[role] = theRole + return theRole + else: + if role.name not in self.usedRoles: + self.usedRoles[role.name] = role + return role + + def standardizeRoles(self): + '''This method converts, within self.permissions, every role to a + Role instance. Every used role is stored in self.usedRoles.''' + for permission, roles in self.permissions.items(): + if isinstance(roles, basestring) or isinstance(roles, Role): + self.permissions[permission] = [self.getRole(roles)] + elif roles: + rolesList = [] + for role in roles: + rolesList.append(self.getRole(role)) + self.permissions[permission] = rolesList + + def getUsedRoles(self): return self.usedRoles.values() + def getTransitions(self, transitions, selfIsFromState=True): '''Among p_transitions, returns those whose fromState is p_self (if p_selfIsFromState is True) or those whose toState is p_self (if @@ -1383,6 +1424,7 @@ class State: if self in t.getStates(selfIsFromState): res.append(t) return res + def getPermissions(self): '''If you get the permissions mapping through self.permissions, dict values may be of different types (a list of roles, a single role or @@ -1407,6 +1449,9 @@ class Transition: # transition at several places in the state-transition diagram. It may # be useful for "undo" transitions, for example. self.condition = condition + if isinstance(condition, basestring): + # The condition specifies the name of a role. + self.condition = Role(condition) self.action = action self.notify = notify # If not None, it is a method telling who must be # notified by email after the transition has been executed. @@ -1414,14 +1459,14 @@ class Transition: # the transition. It will only be possible by code. def getUsedRoles(self): - '''If self.condition is specifies a role.''' + '''self.condition can specify a role.''' res = [] - if isinstance(self.condition, basestring): - res = [self.condition] + if isinstance(self.condition, Role): + res.append(self.condition) return res def isSingle(self): - '''If this transitions is only define between 2 states, returns True. + '''If this transition is only defined between 2 states, returns True. Else, returns False.''' return isinstance(self.states[0], State) @@ -1517,13 +1562,16 @@ class Selection: return value # ------------------------------------------------------------------------------ -class Tool: +class Model: pass +class Tool(Model): '''If you want so define a custom tool class, she must inherit from this class.''' -class Flavour: +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.''' # ------------------------------------------------------------------------------ class Config: @@ -1544,6 +1592,11 @@ class Config: # For every language code that you specify in this list, appy.gen will # produce and maintain translation files. self.languages = ['en'] + # If languageSelector is True, on every page, a language selector will + # allow to switch between languages defined in self.languages. Else, + # the browser-defined language will be used for choosing the language + # of returned pages. + self.languageSelector = False # People having one of these roles will be able to create instances # of classes defined in your application. self.defaultCreators = ['Manager', 'Owner'] diff --git a/gen/generator.py b/gen/generator.py index 635e5a6..ebd53f2 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 +from appy.gen import Type, State, Config, Tool, Flavour, User from appy.gen.descriptors import * from appy.gen.utils import produceNiceMessage import appy.pod, appy.pod.renderer @@ -133,7 +133,8 @@ class Generator: # Default descriptor classes self.descriptorClasses = { 'class': ClassDescriptor, 'tool': ClassDescriptor, - 'flavour': ClassDescriptor, 'workflow': WorkflowDescriptor} + 'flavour': ClassDescriptor, '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,6 +144,7 @@ class Generator: self.classes = [] self.tool = None self.flavour = None + self.user = None self.workflows = [] self.initialize() self.config = Config.getDefault() @@ -235,6 +237,12 @@ class Generator: self.flavour = klass(moduleElem, attrs, self) else: self.flavour.update(moduleElem, attrs) + elif issubclass(moduleElem, User): + if not self.user: + klass = self.descriptorClasses['user'] + self.user = klass(moduleElem, attrs, self) + else: + self.user.update(moduleElem, attrs) else: descriptorClass = self.descriptorClasses['class'] descriptor = descriptorClass(moduleElem,attrs, self) diff --git a/gen/layout.py b/gen/layout.py index 818ae91..c7d9ecd 100644 --- a/gen/layout.py +++ b/gen/layout.py @@ -185,6 +185,6 @@ class Table(LayoutElement): # ------------------------------------------------------------------------------ defaultPageLayouts = { - 'view': Table('m;-s|-n!-w;-b|'), 'edit': Table('m;-s|-n!-w;-b|')} + 'view': Table('m;-s|-n!-w|-b|'), 'edit': Table('m;-w|-b|')} defaultFieldLayouts = {'view': 'l;f!', 'edit': 'lrv;f!'} # ------------------------------------------------------------------------------ diff --git a/gen/plone25/descriptors.py b/gen/plone25/descriptors.py index bffb61a..b1d1fc9 100644 --- a/gen/plone25/descriptors.py +++ b/gen/plone25/descriptors.py @@ -12,9 +12,9 @@ import appy.gen import appy.gen.descriptors from appy.gen.po import PoMessage from appy.gen import Date, String, State, Transition, Type, Search, \ - Selection, Import + Selection, Import, Role from appy.gen.utils import GroupDescr, PageDescr, produceNiceMessage, \ - sequenceTypes + sequenceTypes, getClassName TABS = 4 # Number of blanks in a Python indentation. # ------------------------------------------------------------------------------ @@ -110,13 +110,7 @@ class FieldDescriptor: # Update the list of referers self.generator.addReferer(self, relationship) # Add the widget label for the back reference - refClassName = ClassDescriptor.getClassName(self.appyType.klass) - if issubclass(self.appyType.klass, ModelClass): - refClassName = self.applicationName + self.appyType.klass.__name__ - elif issubclass(self.appyType.klass, appy.gen.Tool): - refClassName = '%sTool' % self.applicationName - elif issubclass(self.appyType.klass, appy.gen.Flavour): - refClassName = '%sFlavour' % self.applicationName + refClassName = getClassName(self.appyType.klass, self.applicationName) backLabel = "%s_%s" % (refClassName, self.appyType.back.attribute) poMsg = PoMessage(backLabel, '', self.appyType.back.attribute) poMsg.produceNiceDefault() @@ -240,7 +234,7 @@ class ClassDescriptor(appy.gen.descriptors.ClassDescriptor): # 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. - self.name = self.getClassName(klass) + self.name = getClassName(self.klass, generator.applicationName) self.predefined = False self.customized = False @@ -276,11 +270,6 @@ class ClassDescriptor(appy.gen.descriptors.ClassDescriptor): # Currently, we generate Archetypes fields for Refs only. self.schema += '\n' + fieldDef - @staticmethod - def getClassName(klass): - '''Generates the name of the corresponding Archetypes class.''' - return klass.__module__.replace('.', '_') + '_' + klass.__name__ - def isAbstract(self): '''Is self.klass abstract?''' res = False @@ -322,7 +311,14 @@ class ClassDescriptor(appy.gen.descriptors.ClassDescriptor): '''Gets the specific creators defined for this class.''' res = [] if self.klass.__dict__.has_key('creators') and self.klass.creators: - res += list(self.klass.creators) + for creator in self.klass.creators: + if isinstance(creator, Role): + if creator.local: + raise 'Local role "%s" cannot be used as a creator.' % \ + creator.name + res.append(creator) + else: + res.append(Role(creator)) return res def getCreateMean(self, type='Import'): @@ -379,7 +375,6 @@ 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.name = '%sTool' % generator.applicationName self.modelClass = self.klass self.predefined = True self.customized = False @@ -405,7 +400,6 @@ class FlavourClassDescriptor(ClassDescriptor): for the generated application.''' def __init__(self, klass, generator): ClassDescriptor.__init__(self,klass,klass._appy_attributes[:],generator) - self.name = '%sFlavour' % generator.applicationName self.attributesByClass = klass._appy_classes self.modelClass = self.klass self.predefined = True @@ -431,13 +425,37 @@ class PodTemplateClassDescriptor(ClassDescriptor): '''Represents a POD template.''' def __init__(self, klass, generator): ClassDescriptor.__init__(self,klass,klass._appy_attributes[:],generator) - self.name = '%sPodTemplate' % generator.applicationName 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.''' + 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=()): + res = ['User'] + 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 user + definition. We must then add the custom user elements in this + default User 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 WorkflowDescriptor(appy.gen.descriptors.WorkflowDescriptor): '''Represents a workflow.''' # How to map Appy permissions to Plone permissions ? @@ -497,11 +515,11 @@ class WorkflowDescriptor(appy.gen.descriptors.WorkflowDescriptor): permissionsMapping = {} for permission, roles in state.getPermissions().iteritems(): for plonePerm in self.getPlonePermissions(permission): - permissionsMapping[plonePerm] = roles + permissionsMapping[plonePerm] = [r.name for r in roles] # Add 'Review portal content' to anyone; this is not a security # problem because we limit the triggering of every transition # individually. - allRoles = self.generator.getAllUsedRoles() + allRoles = [r.name for r in self.generator.getAllUsedRoles()] if 'Manager' not in allRoles: allRoles.append('Manager') permissionsMapping['Review portal content'] = allRoles res[stateName] = (tNames, permissionsMapping) diff --git a/gen/plone25/generator.py b/gen/plone25/generator.py index e8c46c6..2e28124 100644 --- a/gen/plone25/generator.py +++ b/gen/plone25/generator.py @@ -7,10 +7,12 @@ import appy.gen from appy.gen import * from appy.gen.po import PoMessage, PoFile, PoParser from appy.gen.generator import Generator as AbstractGenerator -from model import ModelClass, PodTemplate, Flavour, Tool +from appy.gen.utils import getClassName +from model import ModelClass, PodTemplate, User, Flavour, Tool from descriptors import FieldDescriptor, ClassDescriptor, \ WorkflowDescriptor, ToolClassDescriptor, \ - FlavourClassDescriptor, PodTemplateClassDescriptor + FlavourClassDescriptor, PodTemplateClassDescriptor, \ + UserClassDescriptor # Common methods that need to be defined on every Archetype class -------------- COMMON_METHODS = ''' @@ -36,12 +38,14 @@ class Generator(AbstractGenerator): 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() self.skinsFolder = 'skins/%s' % self.applicationName @@ -54,7 +58,7 @@ class Generator(AbstractGenerator): {'toolName': self.toolName, 'flavourName': self.flavourName, 'portletName': self.portletName, 'queryName': self.queryName, 'toolInstanceName': self.toolInstanceName, - 'podTemplateName': self.podTemplateName, + 'podTemplateName': self.podTemplateName, 'userName': self.userName, 'commonMethods': commonMethods}) self.referers = {} @@ -149,18 +153,19 @@ class Generator(AbstractGenerator): msg('image_required', '', msg.IMAGE_REQUIRED), ] # Create a label for every role added by this application - for role in self.getAllUsedRoles(appOnly=True): - self.labels.append(msg('role_%s' % role,'', role, niceDefault=True)) + for role in self.getAllUsedRoles(): + self.labels.append(msg('role_%s' % role.name,'', role.name, + niceDefault=True)) # Create basic files (config.py, Install.py, etc) self.generateTool() self.generateConfig() self.generateInit() - self.generateInstall() self.generateWorkflows() self.generateWrappers() self.generateTests() if self.config.frontPage: self.generateFrontPage() + self.copyFile('Install.py', self.repls, destFolder='Extensions') self.copyFile('configure.zcml', self.repls) self.copyFile('import_steps.xml', self.repls, destFolder='profiles/default') @@ -227,41 +232,49 @@ class Generator(AbstractGenerator): if not poFile.generated: poFile.generate() - ploneRoles = ('Manager', 'Member', 'Owner', 'Reviewer', 'Anonymous') - def getAllUsedRoles(self, appOnly=False): + def getAllUsedRoles(self, plone=None, local=None, grantable=None): '''Produces a list of all the roles used within all workflows and - classes defined in this application. If p_appOnly is True, it - returns only roles which are specific to this application (ie it - removes predefined Plone roles like Member, Manager, etc.''' - res = [] + classes defined in this application. + + If p_plone is True, it keeps only Plone-standard roles; if p_plone + is False, it keeps only roles which are specific to this application; + if p_plone is None it has no effect (so it keeps both roles). + + If p_local is True, it keeps only local roles (ie, roles that can + only be granted locally); if p_local is False, it keeps only "global" + roles; if p_local is None it has no effect (so it keeps both roles). + + If p_grantable is True, it keeps only roles that the admin can + grant; if p_grantable is False, if keeps only ungrantable roles (ie + those that are implicitly granted by the system like role + "Authenticated"); if p_grantable is None it keeps both roles.''' + allRoles = {} # ~{s_roleName:Role_role}~ + # Gather roles from workflow states and transitions for wfDescr in self.workflows: - # Browse states and transitions for attr in dir(wfDescr.klass): attrValue = getattr(wfDescr.klass, attr) if isinstance(attrValue, State) or \ isinstance(attrValue, Transition): - res += attrValue.getUsedRoles() + for role in attrValue.getUsedRoles(): + if role.name not in allRoles: + allRoles[role.name] = role + # Gather roles from "creators" attributes from every class for cDescr in self.getClasses(include='all'): - res += cDescr.getCreators() - res = list(set(res)) - if appOnly: - for ploneRole in self.ploneRoles: - if ploneRole in res: - res.remove(ploneRole) + for role in cDescr.getCreators(): + if role.name not in allRoles: + allRoles[role.name] = role + res = allRoles.values() + # Filter the result according to parameters + for p in ('plone', 'local', 'grantable'): + if eval(p) != None: + 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.''' k = fieldDescr.appyType.klass - if issubclass(k, ModelClass): - refClassName = self.applicationName + k.__name__ - elif issubclass(k, appy.gen.Tool): - refClassName = '%sTool' % self.applicationName - elif issubclass(k, appy.gen.Flavour): - refClassName = '%sFlavour' % self.applicationName - else: - refClassName = ClassDescriptor.getClassName(k) + refClassName = getClassName(k, self.applicationName) if not self.referers.has_key(refClassName): self.referers[refClassName] = [] self.referers[refClassName].append( (fieldDescr, relationship)) @@ -277,6 +290,59 @@ class Generator(AbstractGenerator): return res def generateConfig(self): + repls = self.repls.copy() + # Compute imports + imports = ['import %s' % self.applicationName] + classDescrs = self.getClasses(include='custom') + for classDescr in (classDescrs + self.workflows): + theImport = 'import %s' % classDescr.klass.__module__ + if theImport not in imports: + imports.append(theImport) + repls['imports'] = '\n'.join(imports) + # Compute default add roles + repls['defaultAddRoles'] = ','.join( + ['"%s"' % r for r in self.config.defaultCreators]) + # Compute list of add permissions + addPermissions = '' + for classDescr in self.getClasses(include='allButTool'): + addPermissions += ' "%s":"%s: Add %s",\n' % (classDescr.name, + self.applicationName, classDescr.name) + repls['addPermissions'] = addPermissions + # Compute root classes + rootClasses = '' + for classDescr in self.classes: + if classDescr.isRoot(): + rootClasses += "'%s'," % classDescr.name + repls['rootClasses'] = rootClasses + # Compute list of class definitions + appClasses = [] + for classDescr in self.classes: + k = classDescr.klass + 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 + 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] + for blackClass in blackClasses: + catalogMap += "catalogMap['%s'] = {}\n" % blackClass + catalogMap += "catalogMap['%s']['black'] = " \ + "['portal_catalog']\n" % blackClass + repls['catalogMap'] = catalogMap + # Compute workflows + workflows = '' + for classDescr in self.getClasses(include='all'): + if hasattr(classDescr.klass, 'workflow'): + wfName = WorkflowDescriptor.getWorkflowName( + classDescr.klass.workflow) + workflows += '\n "%s":"%s",' % (classDescr.name, wfName) + repls['workflows'] = workflows # Compute workflow instances initialisation wfInit = '' for workflowDescr in self.workflows: @@ -295,24 +361,7 @@ class Generator(AbstractGenerator): for stateName in workflowDescr.getStateNames(ordered=True): wfInit += 'wf._states.append("%s")\n' % stateName wfInit += 'workflowInstances[%s] = wf\n' % className - # Compute imports - imports = ['import %s' % self.applicationName] - classDescrs = self.getClasses(include='custom') - for classDescr in (classDescrs + self.workflows): - theImport = 'import %s' % classDescr.klass.__module__ - if theImport not in imports: - imports.append(theImport) - # Compute root classes - rootClasses = '' - for classDescr in self.classes: - if classDescr.isRoot(): - rootClasses += "'%s'," % classDescr.name - # Compute list of add permissions - addPermissions = '' - for classDescr in self.classes: - addPermissions += ' "%s":"%s: Add %s",\n' % (classDescr.name, - self.applicationName, classDescr.name) - repls = self.repls.copy() + repls['workflowInstancesInit'] = wfInit # Compute the list of ordered attributes (foward and backward, inherited # included) for every Appy class. attributes = [] @@ -348,17 +397,22 @@ class Generator(AbstractGenerator): aDict += '"%s":attributes["%s"][%d],' % \ (attrNames[i], classDescr.name, i) attributesDict.append('"%s":{%s}' % (classDescr.name, aDict)) - # Compute list of used roles for registering them if needed - repls['roles'] = ','.join(['"%s"' % r for r in \ - self.getAllUsedRoles(appOnly=True)]) - repls['rootClasses'] = rootClasses - repls['workflowInstancesInit'] = wfInit - repls['imports'] = '\n'.join(imports) repls['attributes'] = ',\n '.join(attributes) repls['attributesDict'] = ',\n '.join(attributesDict) - repls['defaultAddRoles'] = ','.join( - ['"%s"' % r for r in self.config.defaultCreators]) - repls['addPermissions'] = addPermissions + # Compute list of used roles for registering them if needed + specificRoles = self.getAllUsedRoles(plone=False) + repls['roles'] = ','.join(['"%s"' % r.name for r in specificRoles]) + globalRoles = self.getAllUsedRoles(plone=False, local=False) + repls['gRoles'] = ','.join(['"%s"' % r.name for r in globalRoles]) + grantableRoles = self.getAllUsedRoles(local=False, grantable=True) + repls['grRoles'] = ','.join(['"%s"' % r.name for r in grantableRoles]) + # Generate configuration options + repls['showPortlet'] = self.config.showPortlet + repls['languages'] = ','.join('"%s"' % l for l in self.config.languages) + repls['languageSelector'] = self.config.languageSelector + repls['minimalistPlone'] = self.config.minimalistPlone + + repls['appFrontPage'] = bool(self.config.frontPage) self.copyFile('config.py', repls) def generateInit(self): @@ -376,50 +430,6 @@ class Generator(AbstractGenerator): repls['totalNumberOfTests'] = self.totalNumberOfTests self.copyFile('__init__.py', repls) - def generateInstall(self): - # Compute lists of class names - allClassNames = '"%s",' % self.flavourName - allClassNames += '"%s",' % self.podTemplateName - appClassNames = ','.join(['"%s"' % c.name for c in self.classes]) - allClassNames += appClassNames - # Compute imports - imports = [] - for classDescr in self.classes: - theImport = 'import %s' % classDescr.klass.__module__ - if theImport not in imports: - imports.append(theImport) - # Compute list of application classes - appClasses = [] - for classDescr in self.classes: - k = classDescr.klass - appClasses.append('%s.%s' % (k.__module__, k.__name__)) - # Compute classes whose instances must not be catalogued. - catalogMap = '' - blackClasses = [self.toolName, self.flavourName, self.podTemplateName] - for blackClass in blackClasses: - catalogMap += "catalogMap['%s'] = {}\n" % blackClass - catalogMap += "catalogMap['%s']['black'] = " \ - "['portal_catalog']\n" % blackClass - # Compute workflows - workflows = '' - for classDescr in self.getClasses(include='all'): - if hasattr(classDescr.klass, 'workflow'): - wfName = WorkflowDescriptor.getWorkflowName( - classDescr.klass.workflow) - workflows += '\n "%s":"%s",' % (classDescr.name, wfName) - # Generate the resulting file. - repls = self.repls.copy() - repls['allClassNames'] = allClassNames - repls['appClassNames'] = appClassNames - repls['catalogMap'] = catalogMap - repls['imports'] = '\n'.join(imports) - repls['appClasses'] = "[%s]" % ','.join(appClasses) - repls['minimalistPlone'] = self.config.minimalistPlone - repls['showPortlet'] = self.config.showPortlet - repls['appFrontPage'] = bool(self.config.frontPage) - repls['workflows'] = workflows - self.copyFile('Install.py', repls, destFolder='Extensions') - def generateWorkflows(self): '''Generates the file that contains one function by workflow. Those functions are called by Plone for registering the workflows.''' @@ -468,14 +478,18 @@ class Generator(AbstractGenerator): '''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 "custom", it includes descriptors for the - config-related classes for which the user has created a sub-class.''' + 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] + configClasses = [self.tool,self.flavour,self.podTemplate,self.user] if include == 'all': res += configClasses + elif include == 'allButTool': + res += configClasses[1:] elif include == 'custom': res += [c for c in configClasses if c.customized] return res @@ -564,6 +578,7 @@ class Generator(AbstractGenerator): 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') def generateTests(self): @@ -581,7 +596,8 @@ class Generator(AbstractGenerator): # We need a front page, but no specific one has been given. # So we will create a basic one that will simply display # some translated text. - self.labels.append(msg('front_page_text', '', msg.FRONT_PAGE_TEXT)) + self.labels.append(PoMessage('front_page_text', '', + PoMessage.FRONT_PAGE_TEXT)) repls['pageContent'] = '' else: @@ -605,6 +621,9 @@ class Generator(AbstractGenerator): Tool.flavours.klass = Flavour if self.flavour.customized: Tool.flavours.klass = self.flavour.klass + Tool.users.klass = User + if self.user.customized: + Tool.users.klass = self.user.klass self.tool.generateSchema() repls['fields'] = self.tool.schema repls['methods'] = self.tool.methods @@ -659,6 +678,16 @@ class Generator(AbstractGenerator): 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), + Msg('%s_edit_descr' % self.userName, '', ' ')] + repls = self.repls.copy() + 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) def generateClass(self, classDescr): '''Is called each time an Appy class is found in the application, for @@ -681,7 +710,7 @@ class Generator(AbstractGenerator): implements = [baseClass] for baseClass in classDescr.klass.__bases__: if self.determineAppyType(baseClass) == 'class': - bcName = ClassDescriptor.getClassName(baseClass) + bcName = getClassName(baseClass) parents.remove('ClassMixin') parents.append(bcName) implements.append(bcName) diff --git a/gen/plone25/installer.py b/gen/plone25/installer.py index a0467fb..f3fd47b 100644 --- a/gen/plone25/installer.py +++ b/gen/plone25/installer.py @@ -18,30 +18,27 @@ class ZCTextIndexInfo: class PloneInstaller: '''This Plone installer runs every time the generated Plone product is installed or uninstalled (in the Plone configuration interface).''' - def __init__(self, reinstall, productName, ploneSite, minimalistPlone, - appClasses, appClassNames, allClassNames, catalogMap, applicationRoles, - defaultAddRoles, workflows, appFrontPage, showPortlet, ploneStuff): + def __init__(self, reinstall, ploneSite, config): + # p_cfg is the configuration module of the Plone product. self.reinstall = reinstall # Is it a fresh install or a re-install? - self.productName = productName self.ploneSite = ploneSite - self.minimalistPlone = minimalistPlone # If True, lots of basic Plone - # stuff will be hidden. - self.appClasses = appClasses # The list of classes declared in the - # gen-application. - self.appClassNames = appClassNames # Names of those classes - self.allClassNames = allClassNames # Includes Flavour and PodTemplate - self.catalogMap = catalogMap # Indicates classes to be indexed or not - self.applicationRoles = applicationRoles # Roles defined in the app - self.defaultAddRoles = defaultAddRoles # The default roles that can add - # content - self.workflows = workflows # Dict whose keys are class names and whose - # values are workflow names (=the workflow - # used by the content type) - self.appFrontPage = appFrontPage # Does this app define a site-wide - # front page? - self.showPortlet = showPortlet # Must we show the application portlet? - self.ploneStuff = ploneStuff # A dict of some Plone functions or vars - self.attributes = ploneStuff['GLOBALS']['attributes'] + self.config = cfg = config + # Unwrap some useful variables from config + self.productName = cfg.PROJECTNAME + self.minimalistPlone = cfg.minimalistPlone + self.appClasses = cfg.appClasses + self.appClassNames = cfg.appClassNames + self.allClassNames = cfg.allClassNames + self.catalogMap = cfg.catalogMap + self.applicationRoles = cfg.applicationRoles # Roles defined in the app + self.defaultAddRoles = cfg.defaultAddRoles + self.workflows = cfg.workflows + self.appFrontPage = cfg.appFrontPage + self.showPortlet = cfg.showPortlet + self.languages = cfg.languages + self.languageSelector = cfg.languageSelector + self.attributes = cfg.attributes + # A buffer for logging purposes self.toLog = StringIO() self.typeAliases = {'sharing': '', 'gethtml': '', '(Default)': 'skynView', 'edit': 'skyn/edit', @@ -166,7 +163,7 @@ class PloneInstaller: site.manage_delObjects(['skyn']) # This way, if Appy has moved from one place to the other, the # directory view will always refer to the correct place. - addDirView = self.ploneStuff['manage_addDirectoryView'] + addDirView = self.config.manage_addDirectoryView addDirView(site, appy.getPath() + '/gen/plone25/skin', id='skyn') def installTypes(self): @@ -174,11 +171,9 @@ class PloneInstaller: gen-classes.''' site = self.ploneSite # Do Plone-based type registration - classes = self.ploneStuff['listTypes'](self.productName) - self.ploneStuff['installTypes'](site, self.toLog, classes, - self.productName) - self.ploneStuff['install_subskin'](site, self.toLog, - self.ploneStuff['GLOBALS']) + classes = self.config.listTypes(self.productName) + self.config.installTypes(site, self.toLog, classes, self.productName) + self.config.install_subskin(site, self.toLog, self.config.__dict__) # Set appy view/edit pages for every created type for className in self.allClassNames + ['%sTool' % self.productName]: # I did not put the app tool in self.allClassNames because it @@ -204,7 +199,7 @@ class PloneInstaller: factoryTool.manage_setPortalFactoryTypes(listOfTypeIds=factoryTypes) # Configure CatalogMultiplex: tell what types will be catalogued or not. - atTool = getattr(site, self.ploneStuff['ARCHETYPETOOLNAME']) + atTool = getattr(site, self.config.ARCHETYPETOOLNAME) for meta_type in self.catalogMap: submap = self.catalogMap[meta_type] current_catalogs = Set( @@ -294,7 +289,7 @@ class PloneInstaller: try: self.ploneSite.manage_addProduct[ self.productName].manage_addTool(self.toolName) - except self.ploneStuff['BadRequest']: + except self.config.BadRequest: # If an instance with the same name already exists, this error will # be unelegantly raised by Zope. pass @@ -357,7 +352,7 @@ class PloneInstaller: groups if needed.''' site = self.ploneSite data = list(site.__ac_roles__) - for role in self.applicationRoles: + for role in self.config.applicationRoles: if not role in data: data.append(role) # Add to portal_role_manager @@ -373,11 +368,13 @@ class PloneInstaller: pass except AttributeError: pass - # Create a specific group and grant him this role + # If it is a global role, create a specific group and grant him + # this role + if role not in self.config.applicationGlobalRoles: continue group = '%s_group' % role - if not site.portal_groups.getGroupById(group): - site.portal_groups.addGroup(group, title=group) - site.portal_groups.setRolesForGroup(group, [role]) + if site.portal_groups.getGroupById(group): continue # Already there + site.portal_groups.addGroup(group, title=group) + site.portal_groups.setRolesForGroup(group, [role]) site.__ac_roles__ = tuple(data) def installWorkflows(self): @@ -386,7 +383,7 @@ class PloneInstaller: for contentType, workflowName in self.workflows.iteritems(): # Register the workflow if needed if workflowName not in wfTool.listWorkflows(): - wfMethod = self.ploneStuff['ExternalMethod']('temp', 'temp', + wfMethod = self.config.ExternalMethod('temp', 'temp', self.productName + '.workflows', 'create_%s' % workflowName) workflow = wfMethod(self, workflowName) wfTool._setObject(workflowName, workflow) @@ -401,18 +398,14 @@ class PloneInstaller: cssName = self.productName + '.css' cssTitle = self.productName + ' CSS styles' cssInfo = {'id': cssName, 'title': cssTitle} + portalCss = self.ploneSite.portal_css try: - portalCss = self.ploneSite.portal_css - try: - portalCss.unregisterResource(cssInfo['id']) - except: - pass - defaults = {'id': '', 'media': 'all', 'enabled': True} - defaults.update(cssInfo) - portalCss.registerStylesheet(**defaults) + portalCss.unregisterResource(cssInfo['id']) except: - # No portal_css registry pass + defaults = {'id': '', 'media': 'all', 'enabled': True} + defaults.update(cssInfo) + portalCss.registerStylesheet(**defaults) def managePortlets(self): '''Shows or hides the application-specific portlet and configures other @@ -456,6 +449,22 @@ class PloneInstaller: if indexInfo: PloneInstaller.updateIndexes(self.ploneSite, indexInfo, self) + def manageLanguages(self): + '''Manages the languages supported by the application.''' + if self.languageSelector: + # We must install the PloneLanguageTool if not done yet + qi = self.ploneSite.portal_quickinstaller + if not qi.isProductInstalled('PloneLanguageTool'): + qi.installProduct('PloneLanguageTool') + languageTool = self.ploneSite.portal_languages + defLanguage = self.languages[0] + languageTool.manage_setLanguageSettings(defaultLanguage=defLanguage, + supportedLanguages=self.languages, setContentN=None, + setCookieN=True, setRequestN=True, setPathN=True, + setForcelanguageUrls=True, setAllowContentLanguageFallback=None, + setUseCombinedLanguageCodes=None, displayFlags=False, + startNeutral=False) + def finalizeInstallation(self): '''Performs some final installation steps.''' site = self.ploneSite @@ -493,6 +502,7 @@ class PloneInstaller: self.installStyleSheet() self.managePortlets() self.manageIndexes() + self.manageLanguages() self.finalizeInstallation() self.log("Installation of %s done." % self.productName) @@ -545,17 +555,16 @@ def traverseWrapper(self, path, response=None, validated_hook=None): class ZopeInstaller: '''This Zope installer runs every time Zope starts and encounters this generated Zope product.''' - def __init__(self, zopeContext, productName, toolClass, - defaultAddContentPermission, addContentPermissions, - logger, ploneStuff, classes): + def __init__(self, zopeContext, toolClass, config, classes): self.zopeContext = zopeContext - self.productName = productName self.toolClass = toolClass - self.defaultAddContentPermission = defaultAddContentPermission - self.addContentPermissions = addContentPermissions - self.logger = logger - self.ploneStuff = ploneStuff # A dict of some Plone functions or vars + self.config = cfg = config self.classes = classes + # Unwrap some useful config variables + self.productName = cfg.PROJECTNAME + self.logger = cfg.logger + self.defaultAddContentPermission = cfg.DEFAULT_ADD_CONTENT_PERMISSION + self.addContentPermissions = cfg.ADD_CONTENT_PERMISSIONS def completeAppyTypes(self): '''We complete here the initialisation process of every Appy type of @@ -574,23 +583,23 @@ class ZopeInstaller: def installApplication(self): '''Performs some application-wide installation steps.''' - register = self.ploneStuff['DirectoryView'].registerDirectory - register('skins', self.ploneStuff['product_globals']) + register = self.config.DirectoryView.registerDirectory + register('skins', self.config.__dict__) # Register the appy skin folder among DirectoryView'able folders register('skin', appy.getPath() + '/gen/plone25') def installTool(self): '''Installs the tool.''' - self.ploneStuff['ToolInit'](self.productName + ' Tools', + self.config.ToolInit(self.productName + ' Tools', tools = [self.toolClass], icon='tool.gif').initialize( self.zopeContext) def installTypes(self): '''Installs and configures the types defined in the application.''' - contentTypes, constructors, ftis = self.ploneStuff['process_types']( - self.ploneStuff['listTypes'](self.productName), self.productName) + contentTypes, constructors, ftis = self.config.process_types( + self.config.listTypes(self.productName), self.productName) - self.ploneStuff['cmfutils'].ContentInit(self.productName + ' Content', + self.config.cmfutils.ContentInit(self.productName + ' Content', content_types = contentTypes, permission = self.defaultAddContentPermission, extra_constructors = constructors, fti = ftis).initialize( @@ -611,14 +620,14 @@ class ZopeInstaller: global originalTraverse if not originalTraverse: # User tracking is not enabled yet. Do it now. - BaseRequest = self.ploneStuff['BaseRequest'] + BaseRequest = self.config.BaseRequest originalTraverse = BaseRequest.traverse BaseRequest.traverse = traverseWrapper def finalizeInstallation(self): '''Performs some final installation steps.''' # Apply customization policy if any - cp = self.ploneStuff['CustomizationPolicy'] + cp = self.config.CustomizationPolicy if cp and hasattr(cp, 'register'): cp.register(context) def install(self): diff --git a/gen/plone25/mixins/FlavourMixin.py b/gen/plone25/mixins/FlavourMixin.py index 3118a31..8f65177 100644 --- a/gen/plone25/mixins/FlavourMixin.py +++ b/gen/plone25/mixins/FlavourMixin.py @@ -18,33 +18,10 @@ POD_ERROR = 'An error occurred while generating the document. Please ' \ # ------------------------------------------------------------------------------ class FlavourMixin(AbstractMixin): _appy_meta_type = 'Flavour' - def getPortalType(self, metaTypeOrAppyType): + def getPortalType(self, metaTypeOrAppyClass): '''Returns the name of the portal_type that is based on p_metaTypeOrAppyType in this flavour.''' - res = metaTypeOrAppyType - isPredefined = False - isAppy = False - appName = self.getProductConfig().PROJECTNAME - if not isinstance(res, basestring): - res = ClassDescriptor.getClassName(res) - isAppy = True - if res.find('Extensions_appyWrappers') != -1: - isPredefined = True - elems = res.split('_') - res = '%s%s' % (elems[1], elems[4]) - elif isAppy and issubclass(metaTypeOrAppyType, appy.gen.Tool): - # This is the custom tool - isPredefined = True - res = '%sTool' % appName - elif isAppy and issubclass(metaTypeOrAppyType, appy.gen.Flavour): - # This is the custom Flavour - isPredefined = True - res = '%sFlavour' % appName - if not isPredefined: - number = self.appy().number - if number != 1: - res = '%s_%d' % (res, number) - return res + return self.getParentNode().getPortalType(metaTypeOrAppyClass) def registerPortalTypes(self): '''Registers, into portal_types, the portal types which are specific diff --git a/gen/plone25/mixins/ToolMixin.py b/gen/plone25/mixins/ToolMixin.py index b06c40f..64f9b65 100644 --- a/gen/plone25/mixins/ToolMixin.py +++ b/gen/plone25/mixins/ToolMixin.py @@ -1,8 +1,9 @@ # ------------------------------------------------------------------------------ import re, os, os.path, Cookie from appy.shared.utils import getOsTempFolder +import appy.gen from appy.gen import Type, Search, Selection -from appy.gen.utils import SomeObjects, sequenceTypes +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.wrappers import AbstractWrapper @@ -13,6 +14,17 @@ jsMessages = ('no_elem_selected', 'delete_confirm') # ------------------------------------------------------------------------------ class ToolMixin(AbstractMixin): _appy_meta_type = 'Tool' + def getPortalType(self, metaTypeOrAppyClass): + '''Returns the name of the portal_type that is based on + p_metaTypeOrAppyType in this flavour.''' + appName = self.getProductConfig().PROJECTNAME + if not isinstance(metaTypeOrAppyClass, basestring): + res = getClassName(metaTypeOrAppyClass, appName) + if res.find('Extensions_appyWrappers') != -1: + elems = res.split('_') + 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): diff --git a/gen/plone25/mixins/UserMixin.py b/gen/plone25/mixins/UserMixin.py new file mode 100644 index 0000000..cd119b8 --- /dev/null +++ b/gen/plone25/mixins/UserMixin.py @@ -0,0 +1,7 @@ +# ------------------------------------------------------------------------------ +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 c12868e..4302c2c 100644 --- a/gen/plone25/mixins/__init__.py +++ b/gen/plone25/mixins/__init__.py @@ -8,7 +8,7 @@ # ------------------------------------------------------------------------------ import os, os.path, types, mimetypes import appy.gen -from appy.gen import Type, String, Selection +from appy.gen import Type, String, Selection, Role from appy.gen.utils import * from appy.gen.layout import Table, defaultPageLayouts from appy.gen.plone25.descriptors import ClassDescriptor @@ -338,14 +338,30 @@ class AbstractMixin: 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) - i += 1 + # 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] @@ -413,18 +429,6 @@ class AbstractMixin: res = sortedObjectsUids.index(obj.UID()) return res - def getAppyRefPortalType(self, fieldName): - '''Gets the portal type of objects linked to me through Ref field named - p_fieldName.''' - appyType = self.getAppyType(fieldName) - tool = self.getTool() - if self._appy_meta_type == 'Flavour': - flavour = self.appy() - else: - portalTypeName = self._appy_getPortalType(self.REQUEST) - flavour = tool.getFlavour(portalTypeName) - return self._appy_getAtType(appyType.klass, flavour) - def getAppyType(self, name, asDict=False, className=None): '''Returns the Appy type named p_name. If no p_className is defined, the field is supposed to belong to self's class.''' @@ -696,10 +700,10 @@ class AbstractMixin: # Get the corresponding Appy transition transition = workflow._transitionsMapping[transitionName] user = self.portal_membership.getAuthenticatedMember() - if isinstance(transition.condition, basestring): + if isinstance(transition.condition, Role): # It is a role. Transition may be triggered if the user has this # role. - res = user.has_role(transition.condition, self) + res = user.has_role(transition.condition.name, self) elif type(transition.condition) == types.FunctionType: res = transition.condition(workflow, self.appy()) elif type(transition.condition) in (tuple, list): @@ -843,28 +847,6 @@ class AbstractMixin: rq.appyWrappers[uid] = wrapper return wrapper - def _appy_getAtType(self, appyClass, flavour=None): - '''Gets the name of the Archetypes class that corresponds to - p_appyClass (which is a Python class coming from the user - application). If p_flavour is specified, the method returns the name - of the specific Archetypes class in this flavour (ie suffixed with - the flavour number).''' - res = ClassDescriptor.getClassName(appyClass) - appName = self.getProductConfig().PROJECTNAME - if res.find('Extensions_appyWrappers') != -1: - # This is not a content type defined Maybe I am a tool or flavour - res = appName + appyClass.__name__ - elif issubclass(appyClass, appy.gen.Tool): - # This is the custom tool - res = '%sTool' % appName - elif issubclass(appyClass, appy.gen.Flavour): - # This is the custom Flavour - res = '%sFlavour' % appName - else: - if flavour and flavour.number != 1: - res += '_%d' % flavour.number - return res - def _appy_getRefsBack(self, fieldName, relName, ploneObjects=False, noListIfSingleObj=False): '''This method returns the list of objects linked to this one @@ -927,19 +909,14 @@ class AbstractMixin: if appyType.type != 'Ref': continue if appyType.isBack or appyType.link: continue # Indeed, no possibility to create objects with such Ref - refContentTypeName = self.getAppyRefPortalType(appyType.name) - refContentType = getattr(self.portal_types, refContentTypeName) - refMetaType = refContentType.content_meta_type - if refMetaType not in addPermissions: continue - # Indeed, there is no specific "add" permission is defined for tool - # and flavour, for example. - appyClass = refContentType.wrapperClass.__bases__[-1] + refType = self.getTool().getPortalType(appyType.klass) + if refType not in addPermissions: continue # Get roles that may add this content type - creators = getattr(appyClass, 'creators', None) + creators = getattr(appyType.klass, 'creators', None) if not creators: creators = self.getProductConfig().defaultAddRoles # Add those creators to the list of creators for this meta_type - addPermission = addPermissions[refMetaType] + addPermission = addPermissions[refType] if addPermission in allCreators: allCreators[addPermission] = allCreators[\ addPermission].union(creators) diff --git a/gen/plone25/model.py b/gen/plone25/model.py index ee970f1..8cb8bce 100644 --- a/gen/plone25/model.py +++ b/gen/plone25/model.py @@ -7,7 +7,7 @@ # ------------------------------------------------------------------------------ import copy, types -from appy.gen import Type, Integer, String, File, Ref, Boolean, Selection, Group +from appy.gen import * # ------------------------------------------------------------------------------ class ModelClass: @@ -70,6 +70,25 @@ class ModelClass: res += ' %s=%s\n' % (attrName, klass._appy_getTypeBody(appyType)) return res +class User(ModelClass): + # All methods defined below are fake. Real versions are in the wrapper. + title = String(show=False) + gm = {'group': 'main', 'multiplicity': (1,1)} + name = String(**gm) + firstName = String(**gm) + def showLogin(self): pass + def validateLogin(self): pass + login = String(show=showLogin, validator=validateLogin, **gm) + def showPassword(self): pass + def validatePassword(self): pass + password1 = String(format=String.PASSWORD, show=showPassword, + validator=validatePassword, **gm) + 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)) @@ -290,12 +309,18 @@ class Tool(ModelClass): # 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. - def validPythonWithUno(self, value): pass + 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', 'unoEnabledPython', 'openOfficePort', - 'numberOfResultsPerPage', 'listBoxesMaximumWidth'] + _appy_attributes = ['flavours', 'users', 'unoEnabledPython', + 'openOfficePort', 'numberOfResultsPerPage', + 'listBoxesMaximumWidth'] # ------------------------------------------------------------------------------ diff --git a/gen/plone25/skin/portlet.pt b/gen/plone25/skin/portlet.pt index 59e84c5..7d34374 100644 --- a/gen/plone25/skin/portlet.pt +++ b/gen/plone25/skin/portlet.pt @@ -174,7 +174,7 @@ tal:content="structure python: tool.translate('%s_page_%s' % (contextObj.meta_type, aPage))"> - + Object title, shown here if not specified somewhere else in appyType.shownInfo.   + use-macro="portal/skyn/widgets/ref/macros/objectTitle"/> Additional fields that must be shown @@ -217,7 +217,7 @@ innerRef python:True" condition="python: widget['name'] != 'title'"> -   + Actions @@ -248,7 +248,7 @@ requestValue python: request.get(rname, []); inRequest python: request.has_key(rname); allObjects python: contextObj.getSelectableAppyRefs(name); - refUids python: [o.UID() for o in here.getAppyRefs(name)['objects']]; + refUids python: [o.UID() for o in contextObj.getAppyRefs(name)['objects']]; isBeingCreated python: contextObj.isTemporary() or ('/portal_factory/' in contextObj.absolute_url())"> + size python: isMultiple and widget['height'] or 1">