From a321257e55e908f9b6793cee4f42720ab0e9fb03 Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Mon, 28 Nov 2011 22:50:01 +0100 Subject: [PATCH] appy.gen: Ploneless version. --- gen/__init__.py | 8 +- gen/plone25/descriptors.py | 27 +++++ gen/plone25/generator.py | 61 +++++------ gen/plone25/installer.py | 87 ++++++++++------ gen/plone25/mixins/ToolMixin.py | 19 +++- gen/plone25/mixins/__init__.py | 76 ++++++++------ gen/plone25/model.py | 39 ++++++-- gen/plone25/templates/Class.py | 16 +-- gen/plone25/templates/appyWrappers.py | 3 + gen/plone25/templates/config.py | 1 + gen/plone25/wrappers/GroupWrapper.py | 74 ++++++++++++++ gen/plone25/wrappers/UserWrapper.py | 139 ++++++++++++++++++-------- gen/plone25/wrappers/__init__.py | 2 + 13 files changed, 393 insertions(+), 159 deletions(-) create mode 100644 gen/plone25/wrappers/GroupWrapper.py diff --git a/gen/__init__.py b/gen/__init__.py index ed0b3cf..2ae76b3 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -2566,9 +2566,9 @@ class Transition: comments=comment) # Update permissions-to-roles attributes targetState.updatePermissions(wf, obj) - # Refresh catalog-related security if required - if not obj.isTemporary(): - obj.reindex(indexes=('allowedRolesAndUsers', 'State')) + # Reindex the object if required. Not only security-related indexes + # (Allowed, State) need to be updated here. + if not obj.isTemporary(): obj.reindex() # Execute the related action if needed msg = '' if doAction and self.action: msg = self.executeAction(obj, wf) @@ -2718,7 +2718,7 @@ class Config: # If you want to replace the default front page with a page coming from # your application, use the following parameter. Setting # frontPage = True will replace the Plone front page with a page - # whose content will come fron i18n label "front_page_text". + # whose content will come from i18n label "front_page_text". self.frontPage = False # You can choose the Plone or Appy main template self.frontPageTemplate = 'plone' # or "appy" diff --git a/gen/plone25/descriptors.py b/gen/plone25/descriptors.py index 6b14ad7..50bbea7 100644 --- a/gen/plone25/descriptors.py +++ b/gen/plone25/descriptors.py @@ -515,6 +515,33 @@ class UserClassDescriptor(ClassDescriptor): def generateSchema(self): ClassDescriptor.generateSchema(self, configClass=True) +class GroupClassDescriptor(ClassDescriptor): + '''Represents the class that corresponds to the Group 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 = ['Group'] + 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 group + definition. We must then add the custom group elements in this + default Group descriptor. + + NOTE: currently, it is not possible to define a custom Group + class.''' + self.orderedAttributes += attributes + self.klass = klass + self.customized = True + def isFolder(self, klass=None): return False + def generateSchema(self): + ClassDescriptor.generateSchema(self, configClass=True) + class TranslationClassDescriptor(ClassDescriptor): '''Represents the set of translation ids for a gen-application.''' diff --git a/gen/plone25/generator.py b/gen/plone25/generator.py index bef3d11..33e9008 100644 --- a/gen/plone25/generator.py +++ b/gen/plone25/generator.py @@ -9,9 +9,8 @@ from appy.gen.po import PoMessage, PoFile, PoParser from appy.gen.generator import Generator as AbstractGenerator from appy.gen.utils import getClassName from appy.gen.descriptors import WorkflowDescriptor -from descriptors import ClassDescriptor, ToolClassDescriptor, \ - UserClassDescriptor, TranslationClassDescriptor -from model import ModelClass, User, Tool, Translation +from descriptors import * +from model import ModelClass, User, Group, Tool, Translation # ------------------------------------------------------------------------------ class Generator(AbstractGenerator): @@ -24,9 +23,10 @@ class Generator(AbstractGenerator): AbstractGenerator.__init__(self, *args, **kwargs) # Set our own Descriptor classes self.descriptorClasses['class'] = ClassDescriptor - # Create our own Tool, User and Translation instances + # Create our own Tool, User, Group and Translation instances self.tool = ToolClassDescriptor(Tool, self) self.user = UserClassDescriptor(User, self) + self.group = GroupClassDescriptor(Group, self) self.translation = TranslationClassDescriptor(Translation, self) # i18n labels to generate self.labels = [] # i18n labels @@ -132,6 +132,7 @@ class Generator(AbstractGenerator): msg('pdf', '', msg.FORMAT_PDF), msg('doc', '', msg.FORMAT_DOC), msg('rtf', '', msg.FORMAT_RTF), + msg('front_page_text', '', msg.FRONT_PAGE_TEXT), ] # Create a label for every role added by this application for role in self.getAllUsedRoles(): @@ -283,6 +284,27 @@ class Generator(AbstractGenerator): if isBack: res += '.back' return res + def getClasses(self, include=None): + '''Returns the descriptors for all the classes in the generated + gen-application. If p_include is: + * "all" it includes the descriptors for the config-related + classes (tool, user, group, translation) + * "allButTool" it includes the same descriptors, the tool excepted + * "custom" it includes descriptors for the config-related classes + for which the user has created a sub-class.''' + if not include: return self.classes + res = self.classes[:] + configClasses = [self.tool, self.user, self.group, self.translation] + if include == 'all': + res += configClasses + elif include == 'allButTool': + res += configClasses[1:] + elif include == 'custom': + res += [c for c in configClasses if c.customized] + elif include == 'predefined': + res = configClasses + return res + def generateConfigureZcml(self): '''Generates file configure.zcml.''' repls = self.repls.copy() @@ -375,27 +397,6 @@ class Generator(AbstractGenerator): repls['totalNumberOfTests'] = self.totalNumberOfTests self.copyFile('__init__.py', repls) - def getClasses(self, include=None): - '''Returns the descriptors for all the classes in the generated - gen-application. If p_include is: - * "all" it includes the descriptors for the config-related - classes (tool, user, translation) - * "allButTool" it includes the same descriptors, the tool excepted - * "custom" it includes descriptors for the config-related classes - for which the user has created a sub-class.''' - if not include: return self.classes - res = self.classes[:] - configClasses = [self.tool, self.user, self.translation] - if include == 'all': - res += configClasses - elif include == 'allButTool': - res += configClasses[1:] - elif include == 'custom': - res += [c for c in configClasses if c.customized] - elif include == 'predefined': - res = configClasses - return res - def getClassesInOrder(self, allClasses): '''When generating wrappers, classes mut be dumped in order (else, it generates forward references in the Python file, that does not @@ -478,13 +479,14 @@ class Generator(AbstractGenerator): msg = Msg(self.tool.name, '', Msg.CONFIG % self.applicationName) self.labels.append(msg) - # Tune the Ref field between Tool and User + # Tune the Ref field between Tool->User and Group->User Tool.users.klass = User if self.user.customized: Tool.users.klass = self.user.klass + Group.users.klass = self.user.klass - # Generate the Tool-related classes (User, Translation) - for klass in (self.user, self.translation): + # Generate the Tool-related classes (User, Group, Translation) + for klass in (self.user, self.group, self.translation): klassType = klass.name[len(self.applicationName):] klass.generateSchema() self.labels += [ Msg(klass.name, '', klassType), @@ -492,8 +494,7 @@ class Generator(AbstractGenerator): repls = self.repls.copy() repls.update({'methods': klass.methods, 'genClassName': klass.name, 'baseMixin':'BaseMixin', 'parents': 'BaseMixin, SimpleItem', - 'classDoc': 'User class for %s' % self.applicationName, - 'icon':'object.gif'}) + 'classDoc': 'Standard Appy class', 'icon':'object.gif'}) self.copyFile('Class.py', repls, destName='%s.py' % klass.name) # Before generating the Tool class, finalize it with query result diff --git a/gen/plone25/installer.py b/gen/plone25/installer.py index ee4b1e6..e599210 100644 --- a/gen/plone25/installer.py +++ b/gen/plone25/installer.py @@ -85,28 +85,10 @@ class PloneInstaller: site.invokeFactory(self.appyFolderType, self.productName, title=self.productName) getattr(site.portal_types, self.appyFolderType).global_allow = 0 - # Manager has been granted Add permissions for all root classes. - # This may not be desired, so remove this. - appFolder = getattr(site, self.productName) - for className in self.config.rootClasses: - permission = self.getAddPermission(className) - appFolder.manage_permission(permission, (), acquire=0) + else: appFolder = getattr(site, self.productName) - # All roles defined as creators should be able to create the - # corresponding root content types in this folder. - i = -1 - allCreators = set() - for klass in self.appClasses: - i += 1 - if not klass.__dict__.has_key('root') or not klass.__dict__['root']: - continue # It is not a root class - creators = getattr(klass, 'creators', None) - if not creators: creators = self.defaultAddRoles - allCreators = allCreators.union(creators) - className = self.appClassNames[i] - permission = self.getAddPermission(className) - updateRolesForPermission(permission, tuple(creators), appFolder) + # Beyond content-type-specific "add" permissions, creators must also # have the main permission "Add portal content". permission = 'Add portal content' @@ -273,7 +255,8 @@ class ZopeInstaller: indexInfo = {'State': 'FieldIndex', 'UID': 'FieldIndex', 'Title': 'TextIndex', 'SortableTitle': 'FieldIndex', 'SearchableText': 'FieldIndex', 'Creator': 'FieldIndex', - 'Created': 'DateIndex', 'ClassName': 'FieldIndex'} + 'Created': 'DateIndex', 'ClassName': 'FieldIndex', + 'Allowed': 'KeywordIndex'} tool = self.app.config for className in self.config.attributes.iterkeys(): wrapperClass = tool.getAppyClass(className, wrapper=True) @@ -284,22 +267,49 @@ class ZopeInstaller: indexInfo[indexName] = appyType.getIndexType() self.installIndexes(indexInfo) + def getAddPermission(self, className): + '''What is the name of the permission allowing to create instances of + class whose name is p_className?''' + return self.productName + ': Add ' + className + def installBaseObjects(self): '''Creates the tool and the root data folder if they do not exist.''' # Create or update the base folder for storing data zopeContent = self.app.objectIds() - if 'data' not in zopeContent: self.app.manage_addFolder('data') + + if 'data' not in zopeContent: + self.app.manage_addFolder('data') + data = self.app.data + # Manager has been granted Add permissions for all root classes. + # This may not be desired, so remove this. + for className in self.config.rootClasses: + permission = self.getAddPermission(className) + data.manage_permission(permission, (), acquire=0) + # All roles defined as creators should be able to create the + # corresponding root classes in this folder. + i = -1 + for klass in self.config.appClasses: + i += 1 + if not klass.__dict__.has_key('root') or \ + not klass.__dict__['root']: + continue # It is not a root class + creators = getattr(klass, 'creators', None) + if not creators: creators = self.config.defaultAddRoles + className = self.config.appClassNames[i] + permission = self.getAddPermission(className) + updateRolesForPermission(permission, tuple(creators), data) + if 'config' not in zopeContent: toolName = '%sTool' % self.productName createObject(self.app, 'config', toolName,self.productName,wf=False) - # Remove some default objects created by Zope but not useful + # Remove some default objects created by Zope but not useful to Appy for name in ('standard_html_footer', 'standard_html_header',\ 'standard_template.pt'): if name in zopeContent: self.app.manage_delObjects([name]) def installTool(self): '''Updates the tool (now that the catalog is created) and updates its - inner objects (translations, documents).''' + inner objects (users, groups, translations, documents).''' tool = self.app.config tool.createOrUpdate(True, None) tool.refreshSecurity() @@ -307,10 +317,24 @@ class ZopeInstaller: # Create the admin user if no user exists. if not self.app.acl_users.getUsers(): - appyTool.create('users', name='min', firstName='ad', - login='admin', password1='admin', - password2='admin', roles=['Manager']) + self.app.acl_users._doAddUser('admin', 'admin', ['Manager'], ()) appyTool.log('Admin user "admin" created.') + + # Create group "admins" if it does not exist + if not appyTool.count('Group', login='admins'): + appyTool.create('groups', login='admins', title='Administrators', + roles=['Manager']) + appyTool.log('Group "admins" created.') + + # Create a group for every global role defined in the application + for role in self.config.applicationGlobalRoles: + relatedGroup = '%s_group' % role + if appyTool.count('Group', login=relatedGroup): continue + appyTool.create('groups', login=relatedGroup, title=relatedGroup, + roles=[role]) + appyTool.log('Group "%s", related to global role "%s", was ' \ + 'created.' % (relatedGroup, role)) + # Create POD templates within the tool if required for contentType in self.config.attributes.iterkeys(): appyClass = tool.getAppyClass(contentType) @@ -426,9 +450,16 @@ class ZopeInstaller: continue # Back refs are initialised within fw refs appyType.init(name, baseClass, appName) + def installRoles(self): + '''Installs the application-specific roles if not already done.''' + roles = list(self.app.__ac_roles__) + for role in self.config.applicationRoles: + if role not in roles: roles.append(role) + self.app.__ac_roles__ = tuple(roles) + def install(self): self.logger.info('is being installed...') - # Create the "admin" user if no user is present in the database + self.installRoles() self.installAppyTypes() self.installZopeClasses() self.enableUserTracking() diff --git a/gen/plone25/mixins/ToolMixin.py b/gen/plone25/mixins/ToolMixin.py index 0189f81..6ee8356 100644 --- a/gen/plone25/mixins/ToolMixin.py +++ b/gen/plone25/mixins/ToolMixin.py @@ -30,6 +30,7 @@ class ToolMixin(BaseMixin): if res.find('Extensions_appyWrappers') != -1: elems = res.split('_') res = '%s%s' % (elems[1], elems[4]) + if res in ('User', 'Group', 'Translation'): res = appName + res return res def getCatalog(self): @@ -212,6 +213,17 @@ class ToolMixin(BaseMixin): if not appy: return res return res.appy() + def getAllowedValue(self): + '''Gets, for the currently logged user, the value for index + "Allowed".''' + user = self.getUser() + res = ['user:%s' % user.getId(), 'Anonymous'] + user.getRoles() + try: + res += ['user:%s' % g for g in user.groups.keys()] + except AttributeError, ae: + pass # The Zope admin does not have this attribute. + return res + def executeQuery(self, className, searchName=None, startNumber=0, search=None, remember=False, brainsOnly=False, maxResults=None, noSecurity=False, sortBy=None, @@ -303,10 +315,9 @@ class ToolMixin(BaseMixin): if refObject: refField = refObject.getAppyType(refField) params['UID'] = getattr(refObject, refField.name).data - # Determine what method to call on the portal catalog - if noSecurity: catalogMethod = 'unrestrictedSearchResults' - else: catalogMethod = 'searchResults' - exec 'brains = self.getPath("/catalog").%s(**params)' % catalogMethod + # Use index "Allowed" if noSecurity is False + if not noSecurity: params['Allowed'] = self.getAllowedValue() + brains = self.getPath("/catalog")(**params) if brainsOnly: # Return brains only. if not maxResults: return brains diff --git a/gen/plone25/mixins/__init__.py b/gen/plone25/mixins/__init__.py index 6760b0c..5bbc0b5 100644 --- a/gen/plone25/mixins/__init__.py +++ b/gen/plone25/mixins/__init__.py @@ -87,6 +87,8 @@ class BaseMixin: if field.type != 'Ref': continue for obj in field.getValue(self): field.back.unlinkObject(obj, self, back=True) + # Uncatalog the object + self.reindex(unindex=True) # Delete the object self.getParentNode().manage_delObjects([self.id]) @@ -306,22 +308,12 @@ class BaseMixin: url = self.absolute_url_path() catalog = self.getPhysicalRoot().catalog if unindex: - method = catalog.uncatalog_object + catalog.uncatalog_object(url) else: - method = catalog.catalog_object - if indexes: - return method(self, url) - else: - return method(self, url, idxs=indexes) - - def unindex(self, indexes=None): - '''Undatalog this object.''' - url = self.absolute_url_path() - catalog = self.getPhysicalRoot().catalog - if indexes: - return catalog.catalog_object(self, url) - else: - return catalog.catalog_object(self, url, idxs=indexes) + if indexes: + catalog.catalog_object(self, url, idxs=indexes) + else: + catalog.catalog_object(self, url) def say(self, msg, type='info'): '''Prints a p_msg in the user interface. p_logLevel may be "info", @@ -620,7 +612,7 @@ class BaseMixin: # Insert the GroupDescr instance corresponding to # appyType.group at the right place groupDescr = appyType.group.insertInto(res, groups, - appyType.page, self.meta_type) + appyType.page, self.meta_type) GroupDescr.addWidget(groupDescr, appyType.__dict__) return res @@ -1170,6 +1162,23 @@ class BaseMixin: '''Returns the name of the (Zope) class for self.''' return self.portal_type + def Allowed(self): + '''Returns the list of roles and users that are allowed to view this + object. This index value will be used within catalog queries for + filtering objects the user is allowed to see.''' + res = set() + # Get, from the workflow, roles having permission 'View'. + for role in self.getProductConfig().rolesForPermissionOn('View', self): + res.add(role) + # Add users having, locally, this role on this object. + localRoles = getattr(self, '__ac_local_roles__', None) + if not localRoles: return list(res) + for id, roles in localRoles.iteritems(): + for role in roles: + if role in res: + res.add('user:%s' % id) + return list(res) + def _appy_showState(self, workflow, stateShow): '''Must I show a state whose "show value" is p_stateShow?''' if callable(stateShow): @@ -1280,6 +1289,24 @@ class BaseMixin: return nobody return user + def getTool(self): + '''Returns the application tool.''' + return self.getPhysicalRoot().config + + def getProductConfig(self): + '''Returns a reference to the config module.''' + return self.__class__.config + + def index_html(self): + """Redirects to /ui. Transfers the status message if any.""" + rq = self.REQUEST + msg = rq.get('portal_status_message', '') + if msg: + url = self.getUrl(portal_status_message=msg) + else: + url = self.getUrl() + return rq.RESPONSE.redirect(url) + def userIsAnon(self): '''Is the currently logged user anonymous ?''' return self.getUser().getUserName() == 'Anonymous User' @@ -1406,22 +1433,7 @@ class BaseMixin: def allows(self, permission): '''Has the logged user p_permission on p_self ?''' - # Get first the roles that have this permission on p_self. - zopeAttr = Permission.getZopeAttrName(permission) - if not hasattr(self.aq_base, zopeAttr): return - allowedRoles = getattr(self.aq_base, zopeAttr) - # Has the user one of those roles? - user = self.getUser() - # XXX no groups at present - #ids = [user.getId()] + user.getGroups() - ids = [user.getId()] - userGlobalRoles = user.getRoles() - for role in allowedRoles: - # Has the user this role ? Check in the local roles first. - for id, roles in self.__ac_local_roles__.iteritems(): - if (role in roles) and (id in ids): return True - # Check then in the global roles. - if role in userGlobalRoles: return True + return self.getUser().has_permission(permission, self) def getEditorInit(self, name): '''Gets the Javascrit init code for displaying a rich editor for diff --git a/gen/plone25/model.py b/gen/plone25/model.py index 4acd667..206c6ae 100644 --- a/gen/plone25/model.py +++ b/gen/plone25/model.py @@ -8,6 +8,7 @@ # ------------------------------------------------------------------------------ import types from appy.gen import * +Grp=Group # Avoid name clash between appy.gen.Group and class Group below # Prototypical instances of every type ----------------------------------------- class Protos: @@ -83,8 +84,8 @@ class ModelClass: value = '%s.%s' % (moduleName, value.__name__) elif isinstance(value, Selection): value = 'Selection("%s")' % value.methodName - elif isinstance(value, Group): - value = 'Group("%s")' % value.name + elif isinstance(value, Grp): + value = 'Grp("%s")' % value.name elif isinstance(value, Page): value = 'pages["%s"]' % value.name elif callable(value): @@ -149,6 +150,23 @@ class User(ModelClass): gm['multiplicity'] = (0, None) roles = String(validator=Selection('getGrantableRoles'), indexed=True, **gm) +# The Group class -------------------------------------------------------------- +class Group(ModelClass): + # In a ModelClass we need to declare attributes in the following list. + _appy_attributes = ['title', 'login', 'roles', 'users'] + # All methods defined below are fake. Real versions are in the wrapper. + m = {'group': 'main', 'width': 25, 'indexed': True} + title = String(multiplicity=(1,1), **m) + def showLogin(self): pass + def validateLogin(self): pass + login = String(show=showLogin, validator=validateLogin, + multiplicity=(1,1), **m) + roles = String(validator=Selection('getGrantableRoles'), + multiplicity=(0,None), **m) + users = Ref(User, multiplicity=(0,None), add=False, link=True, + back=Ref(attribute='groups', show=True), + showHeaders=True, shownInfo=('title', 'login')) + # The Translation class -------------------------------------------------------- class Translation(ModelClass): _appy_attributes = ['po', 'title'] @@ -166,7 +184,7 @@ toolFieldPrefixes = ('defaultValue', 'podTemplate', 'formats', 'resultColumns', 'enableAdvancedSearch', 'numberOfSearchColumns', 'searchFields', 'optionalFields', 'showWorkflow', 'showWorkflowCommentField', 'showAllStatesInPhase') -defaultToolFields = ('users', 'translations', 'enableNotifications', +defaultToolFields = ('users', 'groups', 'translations', 'enableNotifications', 'unoEnabledPython', 'openOfficePort', 'numberOfResultsPerPage', 'listBoxesMaximumWidth', 'appyVersion', 'refreshSecurity') @@ -187,13 +205,20 @@ class Tool(ModelClass): refreshSecurity = Action(action=refreshSecurity, confirm=True) # Ref(User) will maybe be transformed into Ref(CustomUserClass). users = Ref(User, multiplicity=(0,None), add=True, link=False, - back=Ref(attribute='toTool', show=False), page='users', - queryable=True, queryFields=('login',), showHeaders=True, - shownInfo=('login', 'title', 'roles')) + back=Ref(attribute='toTool', show=False), + page=Page('users', show='view'), + queryable=True, queryFields=('title', 'login'), + showHeaders=True, shownInfo=('title', 'login', 'roles')) + groups = Ref(Group, multiplicity=(0,None), add=True, link=False, + back=Ref(attribute='toTool2', show=False), + page=Page('groups', show='view'), + queryable=True, queryFields=('title', 'login'), + showHeaders=True, shownInfo=('title', 'login', 'roles')) translations = Ref(Translation, multiplicity=(0,None),add=False,link=False, back=Ref(attribute='trToTool', show=False), show='view', page=Page('translations', show='view')) - enableNotifications = Boolean(default=True, page='notifications') + enableNotifications = Boolean(default=True, + page=Page('notifications', show=False)) @classmethod def _appy_clean(klass): diff --git a/gen/plone25/templates/Class.py b/gen/plone25/templates/Class.py index 3511dc9..3ee79ac 100644 --- a/gen/plone25/templates/Class.py +++ b/gen/plone25/templates/Class.py @@ -23,21 +23,11 @@ class (): global_allow = 1 icon = "ui/" wrapperClass = Wrapper - for elem in dir(): - if not elem.startswith('__'): security.declarePublic(elem) - def getTool(self): return self.getPhysicalRoot().config - def getProductConfig(self): return cfg - def index_html(self): - """Redirects to /ui. Transfers the status message if any.""" - rq = self.REQUEST - msg = rq.get('portal_status_message', '') - if msg: - url = self.getUrl(portal_status_message=msg) - else: - url = self.getUrl() - return rq.RESPONSE.redirect(url) + config = cfg def do(self): '''BaseMixin.do can't be traversed by Zope if this class is the tool. So here, we redefine this method.''' return BaseMixin.do(self) + for elem in dir(): + if not elem.startswith('__'): security.declarePublic(elem) diff --git a/gen/plone25/templates/appyWrappers.py b/gen/plone25/templates/appyWrappers.py index dd0fc62..9149b7f 100644 --- a/gen/plone25/templates/appyWrappers.py +++ b/gen/plone25/templates/appyWrappers.py @@ -1,8 +1,10 @@ # ------------------------------------------------------------------------------ from appy.gen import * +Grp = Group # Avoid name clashes with the Group class below and appy.gen.Group from appy.gen.plone25.wrappers import AbstractWrapper from appy.gen.plone25.wrappers.ToolWrapper import ToolWrapper as WTool from appy.gen.plone25.wrappers.UserWrapper import UserWrapper as WUser +from appy.gen.plone25.wrappers.GroupWrapper import GroupWrapper as WGroup from appy.gen.plone25.wrappers.TranslationWrapper import TranslationWrapper as WT from Globals import InitializeClass from AccessControl import ClassSecurityInfo @@ -10,6 +12,7 @@ tfw = {"edit":"f","cell":"f","view":"f"} # Layout for Translation fields + diff --git a/gen/plone25/templates/config.py b/gen/plone25/templates/config.py index 0188e61..e519a12 100644 --- a/gen/plone25/templates/config.py +++ b/gen/plone25/templates/config.py @@ -14,6 +14,7 @@ from ZPublisher.HTTPRequest import BaseRequest from OFS.Image import File from ZPublisher.HTTPRequest import FileUpload from AccessControl import getSecurityManager +from AccessControl.PermissionRole import rolesForPermissionOn from DateTime import DateTime from Products.ExternalMethod.ExternalMethod import ExternalMethod from Products.Transience.Transience import TransientObjectContainer diff --git a/gen/plone25/wrappers/GroupWrapper.py b/gen/plone25/wrappers/GroupWrapper.py new file mode 100644 index 0000000..32bd3f8 --- /dev/null +++ b/gen/plone25/wrappers/GroupWrapper.py @@ -0,0 +1,74 @@ +# ------------------------------------------------------------------------------ +from appy.gen.plone25.wrappers import AbstractWrapper + +# ------------------------------------------------------------------------------ +class GroupWrapper(AbstractWrapper): + + def showLogin(self): + '''When must we show the login field?''' + if self.o.isTemporary(): return 'edit' + return 'view' + + def validateLogin(self, login): + '''Is this p_login valid?''' + return True + + def getGrantableRoles(self): + '''Returns the list of roles that the admin can grant to a user.''' + res = [] + for role in self.o.getProductConfig().grantableRoles: + res.append( (role, self.translate('role_%s' % role)) ) + return res + + def validate(self, new, errors): + '''Inter-field validation.''' + return self._callCustom('validate', new, errors) + + def confirm(self, new): + '''Use this method for remembering the previous list of users for this + group.''' + obj = self.o + if hasattr(obj.aq_base, '_oldUsers'): del obj.aq_base._oldUsers + obj._oldUsers = self.users + + def addUser(self, user): + '''Adds a p_user to this group.''' + # Update the Ref field. + self.link('users', user) + # Update the group-related info on the Zope user. + zopeUser = user.getZopeUser() + zopeUser.groups[self.login] = self.roles + + def removeUser(self, user): + '''Removes a p_user from this group.''' + self.unlink('users', user) + # Update the group-related info on the Zope user. + zopeUser = user.getZopeUser() + del zopeUser.groups[self.login] + + def onEdit(self, created): + # Create or update, on every Zope user of this group, group-related + # information. + # 1. Remove reference to this group for users that were removed from it + newUsers = self.users + # The list of previously existing users does not exist when editing a + # group from Python. For updating self.users, it is recommended to use + # methods m_addUser and m_removeUser above. + oldUsers = getattr(self.o.aq_base, '_oldUsers', ()) + for user in oldUsers: + if user not in newUsers: + del user.getZopeUser().groups[self.login] + self.log('User "%s" removed from group "%s".' % \ + (user.login, self.login)) + # 2. Add reference to this group for users that were added to it + for user in newUsers: + zopeUser = user.getZopeUser() + # We refresh group-related info on the Zope user even if the user + # was already in the group. + zopeUser.groups[self.login] = self.roles + if user not in oldUsers: + self.log('User "%s" added to group "%s".' % \ + (user.login, self.login)) + if hasattr(self.o.aq_base, '_oldUsers'): del self.o._oldUsers + return self._callCustom('onEdit', created) +# ------------------------------------------------------------------------------ diff --git a/gen/plone25/wrappers/UserWrapper.py b/gen/plone25/wrappers/UserWrapper.py index a7ef83d..1d6c310 100644 --- a/gen/plone25/wrappers/UserWrapper.py +++ b/gen/plone25/wrappers/UserWrapper.py @@ -12,24 +12,18 @@ class UserWrapper(AbstractWrapper): def validateLogin(self, login): '''Is this p_login valid?''' # The login can't be the id of the whole site or "admin" - if (login == self.o.portal_url.getPortalObject().getId()) or \ - (login == 'admin'): - return self.translate(u'This username is reserved. Please choose ' \ - 'a different name.', domain='plone') - # Check that the login does not already exist and check some - # Plone-specific rules. - pr = self.o.portal_registration - if not pr.isMemberIdAllowed(login): - return self.translate(u'The login name you selected is already ' \ - 'in use or is not valid. Please choose another.', domain='plone') + if login == 'admin': + return self.translate('This username is reserved.') + # Check that no user or group already uses this login. + if self.count('User', login=login) or self.count('Group', login=login): + return self.translate('This login is already in use.') return True def validatePassword(self, password): '''Is this p_password valid?''' # Password must be at least 5 chars length if len(password) < 5: - return self.translate(u'Passwords must contain at least 5 letters.', - domain='plone') + return self.translate('Passwords must contain at least 5 letters.') return True def showPassword(self): @@ -49,7 +43,7 @@ class UserWrapper(AbstractWrapper): page = self.request.get('page', 'main') if page == 'main': if hasattr(new, 'password1') and (new.password1 != new.password2): - msg = self.translate(u'Passwords do not match.', domain='plone') + msg = self.translate('Passwords do not match.') errors.password1 = msg errors.password2 = msg return self._callCustom('validate', new, errors) @@ -61,41 +55,104 @@ class UserWrapper(AbstractWrapper): if created: # Create the corresponding Zope user aclUsers._doAddUser(login, self.password1, self.roles, ()) + zopeUser = aclUsers.getUser(login) # Remove our own password copies self.password1 = self.password2 = '' - # Perform updates on the corresponding Plone user - zopeUser = aclUsers.getUserById(login) - # This object must be owned by its Plone user + from persistent.mapping import PersistentMapping + # The following dict will store, for every group, global roles + # granted to it. + zopeUser.groups = PersistentMapping() + else: + # Updates roles at the Zope level. + zopeUser = aclUsers.getUserById(login) + zopeUser.roles = self.roles + # "self" must be owned by its Zope user if 'Owner' not in self.o.get_local_roles_for_userid(login): self.o.manage_addLocalRoles(login, ('Owner',)) - # Change group membership according to self.roles. Indeed, instead of - # granting roles directly to the user, we will add the user to a - # Appy-created group having this role. - userRoles = self.roles - #userGroups = zopeUser.getGroups() - # for role in self.o.getProductConfig().grantableRoles: - # # Retrieve the group corresponding to this role - # groupName = '%s_group' % role - # if role == 'Manager': groupName = 'Administrators' - # elif role == 'Reviewer': groupName = 'Reviewers' - # group = self.o.portal_groups.getGroupById(groupName) - # # Add or remove the user from this group according to its role(s). - # if role in userRoles: - # # Add the user if not already present in the group - # if groupName not in userGroups: - # group.addMember(self.login) - # else: - # # Remove the user if it was in the corresponding group - # if groupName in userGroups: - # group.removeMember(self.login) return self._callCustom('onEdit', created) + def getZopeUser(self): + '''Gets the Zope user corresponding to this user.''' + return self.o.acl_users.getUser(self.login) + def onDelete(self): - '''Before deleting myself, I must delete the corresponding Plone - user.''' - # Delete the corresponding Plone user - self.o.acl_users._doDelUser(self.login) - self.log('Plone user "%s" deleted.' % self.login) + '''Before deleting myself, I must delete the corresponding Zope user.''' + self.o.acl_users._doDelUsers([self.login]) + self.log('User "%s" deleted.' % self.login) # Call a custom "onDelete" if any. return self._callCustom('onDelete') + +# ------------------------------------------------------------------------------ +try: + from AccessControl.PermissionRole import _what_not_even_god_should_do, \ + rolesForPermissionOn + from Acquisition import aq_base +except ImportError: + pass # For those using Appy without Zope + +class ZopeUserPatches: + '''This class is a fake one that defines Appy variants of some of Zope's + AccessControl.User methods. The idea is to implement the notion of group + of users.''' + + def getRoles(self): + '''Returns the global roles that this user (or any of its groups) + possesses.''' + res = list(self.roles) + # Add group global roles + if not hasattr(aq_base(self), 'groups'): return res + for roles in self.groups.itervalues(): + for role in roles: + if role not in res: res.append(role) + return res + + def getRolesInContext(self, object): + '''Return the list of global and local (to p_object) roles granted to + this user (or to any of its groups).''' + object = getattr(object, 'aq_inner', object) + # Start with user global roles + res = self.getRoles() + # Add local roles + localRoles = getattr(object, '__ac_local_roles__', None) + if not localRoles: return res + userId = self.getId() + groups = getattr(self, 'groups', ()) + for id, roles in localRoles.iteritems(): + if (id != userId) and (id not in groups): continue + for role in roles: res.add(role) + return res + + def allowed(self, object, object_roles=None): + '''Checks whether the user has access to p_object. The user (or one of + its groups) must have one of the roles in p_object_roles.''' + if object_roles is _what_not_even_god_should_do: return 0 + # If "Anonymous" is among p_object_roles, grant access. + if (object_roles is None) or ('Anonymous' in object_roles): return 1 + # If "Authenticated" is among p_object_roles, grant access if the user + # is not anonymous. + if 'Authenticated' in object_roles and \ + (self.getUserName() != 'Anonymous User'): + if self._check_context(object): return 1 + # Try first to grant access based on global user roles + for role in self.getRoles(): + if role not in object_roles: continue + if self._check_context(object): return 1 + return + # Try then to grant access based on local roles + innerObject = getattr(object, 'aq_inner', object) + localRoles = getattr(innerObject, '__ac_local_roles__', None) + if not localRoles: return + userId = self.getId() + groups = getattr(self, 'groups', ()) + for id, roles in localRoles.iteritems(): + if (id != userId) and (id not in groups): continue + for role in roles: + if role not in object_roles: continue + if self._check_context(object): return 1 + return + + from AccessControl.User import SimpleUser + SimpleUser.getRoles = getRoles + SimpleUser.getRolesInContext = getRolesInContext + SimpleUser.allowed = allowed # ------------------------------------------------------------------------------ diff --git a/gen/plone25/wrappers/__init__.py b/gen/plone25/wrappers/__init__.py index 35e32e9..8c604f9 100644 --- a/gen/plone25/wrappers/__init__.py +++ b/gen/plone25/wrappers/__init__.py @@ -54,6 +54,8 @@ class AbstractWrapper(object): return o.workflow_history[key] elif name == 'user': return self.o.getUser() + elif name == 'appyUser': + return self.search('User', login=self.o.getUser().getId())[0] elif name == 'fields': return self.o.getAllAppyTypes() # Now, let's try to return a real attribute. res = object.__getattribute__(self, name)