Making a custom folderish type act as the member folder
This tutorial explains how to create your own portal_membership tool in order to override the creation of the member folder. Be warned: this tutorial is not for the faint hearted, but for advanced plone programmers.
Introduction
An overview of this document, its history and it's requirements
History
==================
In the early January of the year 2005 I decided that I had the need to use a special self written folder for the memberarea of each member. I started trying to utilize preCreateMemberArea.py in the skins dir of my product, but that didn't work out as expected, since a call of getAuthenticatedMember() within that script leads to an endless loop. To discover that I had to debug for about 2 days discovering that my steps were utterly useless.
Overview
==================
The only true way to override the generation of the memberarea of a user is to write your custom membership_tool. Quite naturally this is a realy tricky thing, since you can trash portal_membership which in turn prevents a lot of things from wroking. It can even lead to the destruction of anything member related (depending on what is malfunctioning).
So anyone who hasn't got **advanced plone programming skills** *keep your hands off* this tutorial. You could take out your whole plone site when doing something wrong, probably resulting in heavy data loss.
This tutorial is provided as is - no response is taken when something fails or goes wrong.
I have tested the steps to my best knowledge and they work on my plone site.
Anyway **never, ever** try this out in a production invironment.
Motivation
----------
The idea to create your own MembershipTool might sound a bit like overkill at first, but it's indeed not. It just boils down to deriving your custom tool from the MembershipTool of CMFMember that already provides all functionality need by a MembershipTool. Still there is one major drawback caused by the usage of CMFMember: Whenever you upgrade (migrate) your CMFMember product, you'll have to install your custom MembershipTool anew afterwards. This is caused by the way the migration (upgrade) of CMFMember works, because during this process the MembershipTool is normally replaced by the one the new CMFMember product carries along.
If you just utilize the code of this tutorial, you'll only have to restart your zope instace in order to replace the CMFMember's MembershipTool by your custom one. This is probably not a perfect solution, since whenever you restart the MembershipTool gets replaced, but for development purposes it's quite comfortable. This is related to the fact, that reloading your product and utilizing the quickinstaller / external method to reinstall your product, replaces the MembershipTool too.
Writting your own custom MembershipTool does not only allow you to override the creation of the member's area, but you could override any function or extend it with new functions.
Taking the story even one step further you can provide for any Plone Tool your custom one.
Requirements
==================
* Plone 2.0.x
* CMFMember 1.0 beta5
* **advanced plone programming skills**
A custom MembershipTool
shows how a custom MembershipTool should look like and what pitfalls you should avoid when placing code within it.
Create your own custom folderish type
======================================
I won't show you that, since this is a trivial thing for someone with advanced programming skill.
Just create your own folderish content type, ZPTs and what ever else you need for it.
There is nothing special needed for the registration, or anything else. Just do it like you would do for a normal product.
For the further document i'll call this custom folderish type MyFolder (to abbreviate things).
Creating a custom MembershipTool
================================
The code presented in this chapter should alltogether go into one file. It actually defines a class MembershipTool that is derived from the MembershipTool class of CMFMember. By defining createMemberarea(), CMFMember.MembershipTool.MembershipTool.createMemberarea() is overridden.
create a file MembershipTool.py in the root of your product.
Now place following imports into it::
from Globals import InitializeClass
from AccessControl import ClassSecurityInfo
from Products.CMFCore.utils import getToolByName
from Products.CMFMember.MembershipTool import MembershipTool as BaseTool
from Products.CMFPlone.PloneUtilities import translate
from Products.CMFPlone.PloneUtilities import _createObjectByType
from Products.Archetypes.utils import shasattr
from Products.CMFCore.CMFCorePermissions import View
The first two imports are quite usual for any content type. The third one imports an import utility that we'll need later on. The forth line imports the MembershipTool of CMFMember with a different name than MembershipTool since we're going to call our class like that. The fifth import is for internationalization since it's always a good idea to internationalize your code when you start writting it. Trying to internationalize it later can be cumbersome. The sixth line imports a function that is quite special, since it not only creates objects by it's type name, but it does so without performing some security checks. This is necessary so that the folder is created regardless if it's an allowed content type within the Members folder. Similair functions, but with additional security checks, are invokeFactory and
fti.constructInstance . The seventh line imports shasattr which is a saver form of hasattr, since the one in python is broken at the moment (for details see Archetypes/utils.py).
After the imports we start to define our class::
class MembershipTool( BaseTool ):
""" Replace the membership tool in order to change the process of
creating the member folder
"""
meta_type='<YOURPRODUCTNAME> Membership Tool'
security = ClassSecurityInfo()
plone_tool = 1
The first function added to this class is a helper function that eases the changing of the owner of a content type and
setting of a title and a description. Since this will most likely happen more than once when creating a custom member area, this is is stuffed into a helper function. So the helper function should look like::
def _changeFolderForMember(self, folder, user, folder_title, folder_description):
""" change ownership, title and description for a folder
"""
# Grant Ownership and Owner role to Member
folder.changeOwnership(user)
folder.__ac_local_roles__ = None
folder.manage_setLocalRoles(user.getId(), ['Owner'])
# set title and description (edit invokes reindexObject)
folder.edit(title=folder_title, description=folder_description)
The next step is to add a function to our MembershipTool class that overrides the one of CMFMember for creating the Membership area (most of the following code was taken from CMFPlone/MembershipTool.py)::
def createMemberarea(self, member_id):
""" creates a costum member folder
"""
# do not create member_area for groups
if shasattr(self, 'portal_groups') and member_id in self.portal_groups.listGroupIds():
return
catalog = getToolByName(self, 'portal_catalog')
membership = getToolByName(self, 'portal_membership')
members = self.getMembersFolder()
if not member_id:
# member_id is optional (see CMFCore.interfaces.portal_membership:
# Create a member area for 'member_id' or authenticated user.)
member = membership.getAuthenticatedMember()
member_id = member.getId()
if hasattr(members, 'aq_explicit'):
members=members.aq_explicit
if members is None:
# no members area
# XXX exception?
return
if shasattr(members, member_id):
# has already this member
# XXX exception
return
_createObjectByType('MyFolder', members, id=member_id)
# get the user object from acl_users
acl_users = self.__getPUS()
user = acl_users.getUser(member_id)
if user is not None:
user= user.__of__(acl_users)
else:
user= getSecurityManager().getUser()
# check that we do not do something wrong
if user.getId() != member_id:
raise NotImplementedError, \
'cannot get user for member area creation'
In order to avoid showing off too much code at once and for better understanding, i'll explain the above code a bit.
The first if statement simply prevents the creation of home areas for groups.
The following three lines try to acquire necessary tools for the creation and assignment of a folder for the authenticated user.
Since the member_id parameter is not part of the very basic MembershipTool, we can't count on it being set. In case it's not set, well fix that by acquiring the authenticated user (not member!! getAuthenticatedMember() anywhere before the first _createObjectType() within createMembershiparea would leed to loops.)
Some checks if the Member folder exists and the member's area of the authenticated user doesn't exist already.
The _createObjectType() creates the folder for the user in the Member folder. Exchange MyFolder with the name of your custom folderish type.
Then fetch the user object.
When this part of the code has run, most of the things is already done. Missing is now only the renaming of the folderish content type and the changing of the ownership. Since internationalization is always a good idea, the translation of strings for the folder titles and descriptions follows::
## get some translations
member_folder_title = translate(
'plone', 'title_member_folder',
{'member': member_id}, self,
default = "%s's Home" % member_id)
member_folder_description = translate(
'plone', 'description_member_folder',
{'member': member_id}, self,
default = 'Home page area that contains the items created ' \
'and collected by %s' % member_id)
member_folder_index_html_title = translate(
'plone', 'title_member_folder_index_html',
{'member': member_id}, self,
default = "Home page for %s" % member_id)
personal_folder_title = translate(
'plone', 'title_member_personal_folder',
{'member': member_id}, self,
default = "Personal Items for %s" % member_id)
personal_folder_description = translate(
'plone', 'description_member_personal_folder',
{'member': member_id}, self,
default = 'contains personal workarea items for %s' % member_id)
The following lines rename the folder and it's description using the acquired translations.::
## Modify member folder
member_folder = self.getHomeFolder(member_id)
# Grant Ownership and Owner role to Member
member_folder.changeOwnership(user)
member_folder.__ac_local_roles__ = None
member_folder.manage_setLocalRoles(member_id, ['Owner'])
# set title and description (edit invokes reindexObject)
member_folder.edit(title=member_folder_title,
description=member_folder_description)
member_folder.reindexObject()
Note the member_folder.reindexObject() is needed since the folder's title changed.
To complete a standard member's home area you'll also have to create the .personal folder.::
## Create personal folder for personal items
_createObjectByType('Folder', member_folder, id=self.personal_id)
personal = getattr(member_folder, self.personal_id)
_changeFolderForMember(personal, user, personal_folder_title,
personal_folder_description)
# Don't add .personal folders to catalog
catalog.unindexObject(personal)
MembershipTool.__doc__ = BaseTool.__doc__
InitializeClass(MembershipTool)
The only difference of the .personal folder to the home area folder is that it's index is removed.
The MembershipTool.__doc__ = BaseTool.__doc__ does ????
The InitializeClass() call is again normal as usual.
You can place any code within the createMemberarea(), but keep in mind that it's normally only executed once per member at his/her initial login. Calls to getAuthenticatedMember should under any circumstance be avoided, since that would lead to an endless loop (thx to wrapuser).
Registering the custom MembershipTool
Shows how a custom MembershipTool is registered.
__init__.py
===========
following code needs to be added::
import MembershipTool
tools = (MembershipTool.MembershipTool,)
utils.ToolInit(PROJECTNAME + ' Tool',
tools=tools,
product_name=PROJECTNAME,
icon="member_control_icon.png",
).initialize(context)
Substitute member_control_icon.png with the name of your icon for the Membershiptool. Install that icon in the root directory of your product (e.g. product's folder is called FooBar, so the icon path is Products/FooBar/member_control_icon.png)
code that actually only needs to be run once:
=============================================
Following code actually only needs to be run once. Running it more often should not hurt, as long as you didn't enter any customizing Membership role mappings.
Anyway be aware, that when you upgrade CMFMember, your custom MembershipTool will get deleted and you'll have to install it anew.
Perhaps it would be nice to override the migrationtool of CMFMember too in order to accomplish the above mentioned functionality (But this is another story for some other tutorial).
So here is the code::
def _migrateTool(portal, toolid, name, attrs):
orig=getToolByName(portal, toolid)
portal.manage_delObjects(toolid)
portal.manage_addProduct[PROJECTNAME].manage_addTool(name)
tool = getToolByName(portal, toolid)
for attr in attrs:
setattr(tool, attr, aq_base(getattr(aq_base(orig), attr)))
return aq_base(orig)
def installMembershipTool(self, out):
portal = getToolByName(self, 'portal_url').getPortalObject()
if getToolByName(self, 'portal_membership', None) is not None:
_migrateTool(portal, 'portal_membership',
MembershipTool.meta_type,
['_actions'])
out.write('Migrated membership tool to AutoIMember Membership Tool')
else:
portal.manage_addProduct[PROJECTNAME].manage_addTool(MembershipTool.meta_type, None)
out.write('Added AutoIMember Membership Tool')
PROJECTNAME should be substituted by your projectname / packagename (or imported properly).
Calling installMembershipTool() should register the custom MembershipTool within your plone site.
One possibility is to call it from within your package's Extensions/Install.py. For development this is ok, but for a prduction site it would be best to create another tool that is responsible for migrating the membershiptool. This follows the idea of the cmfmember_control.

