Member manipulation

Description

How to programmatically create, read, edit and delete site members.

Introduction

In Plone, there are two loosely-coupled subsystems relating to members:

Authentication and permission information

(acl_users under site root), managed by the PAS. In a default installation, this corresponds to Zope user objects. PAS is pluggable, though, so it may also be authenticating against an LDAP server, Plone content objects, or other sources.

Member profile information

accessible through the portal_membership tool. These represent Plone members. PAS authenticates, and the Plone member object provides metadata about the member.

Getting the logged-in member

Anonymous and logged-in members are exposed via the IPortalState context helper.

Example (browserview: use self.context since self is not acquisition-wrapped):

from zope.component import getMultiAdapter

portal_state = getMultiAdapter(
        (self.context, self.request), name="plone_portal_state")
if portal_state.anonymous():
    # Return target URL for the site anonymous visitors
    return self.product.getHomepageLink()
else:
    # Return edit URL for the site members
    return product.absolute_url()

or from a template:

<div tal:define="username context/portal_membership/getAuthenticatedMember/getUserName">
    ...
</div>

Getting any member

To get a member by username (you must have Manager role):

mt = getToolByName(self.context, 'portal_membership')
member = mt.getMemberById(username)

To get all usernames:

mt = getToolByName(self.context, 'portal_membership')
memberIds = mt.listMemberIds()

Getting member information

Once you have access to the member object, you can grab basic information about it.

Get the user’s name:

member.getName()

Reseting user password without emailing them

Exporting and importing member passwords

You can also get at the hash of the user’s password (only the hash is available, and only for standard Plone user objects) (in this example we’re in Plone add-on context, since self is acquisition-wrapped):

uf = getToolByName(self, 'acl_users')
passwordhash_map = uf.source_users._user_passwords
userpasswordhash = passwordhash_map.get(member.id, '')

Note that this is a private data structure. Depending on the Plone version and add-ons in use, it may not be available.

You can use this hash directly when importing your user data, for example as follows (can be executed from a debug prompt.):

# The file 'exported.txt' contains lines with: "memberid hash"
lines = open('exported.txt').readlines()
changes = []
c = 0
members = mt.listMembers()
for l in lines:
    memberid, passwordhash_exported = l.split(' ')
    passwordhash_exported = passwordhash_exported.strip()
    member = mt.getMemberById(memberid)
    if not member:
        print 'missing', memberid
        continue
    passwordhash = passwordhash_map.get(memberid)
    if passwordhash != passwordhash_exported:
        print 'changed', memberid, passwordhash, passwordhash_exported
        c += 1
        changes.append((memberid, passwordhash_exported))

uf.source_users._user_passwords.update(changes)

Also, take a look at a script for exporting Plone 3.0’s memberdata and passwords:

Iterating all site users

Example:

buffer = ""

# Returns list of site usernames
mt = getToolByName(self, 'portal_membership')
users = mt.listMemberIds()
# alternative: get member objects
# members = mt.listMembers()

for user in users:
   print "Got username:" + user

Note

Zope users, such as admin, are not included in this list.

Getting all Members for a given Role

In this example we use the portal_membership tool. We assume that a role called Agent exists and that we already have the context:

from Products.CMFCore.utils import getToolByName

membership_tool = getToolByName(self, 'portal_membership')
agents = [member for member in membership_tool.listMembers()
            if member.has_role('Agent')]

Groups

Groups are stored as PloneGroup objects. PloneGroup is a subclass of PloneUser. Groups are managed by the portal_groups tool.

Creating a group

Example:

groups_tool = getToolByName(context, 'portal_groups')

group_id = "companies"
if not group_id in groups_tool.getGroupIds():
    groups_tool.addGroup(group_id)

For more information, see:

Add local roles to a group

Example:

from AccessControl.interfaces import IRoleManager
if IRoleManager.providedBy(context):
    context.manage_addLocalRoles(groupid, ['Manager',])

Note

This is an example of code in a view, where context is available.

Update properties for a group

The editGroup method modifies the title and description in the source_groups plugin, and subsequently calls setGroupProperties(kw) which sets the properties on the mutable_properties plugin.

Example:

portal_groups.editGroup(groupid, **properties)
portal_groups.editGroup(groupid, roles = ['Manager',])
portal_groups.editGroup(groupid, title = u'my group title')

Getting available groups

Getting all groups on the site is possible through acl_users and the source_groups plugin, which provides the functionality to manipulate Plone groups.

Example to get only ids:

acl_users = getToolByName(self, 'acl_users')
# Iterable returning id strings:
groups = acl_users.source_groups.getGroupIds()

Example to get full group information:

acl_users = getToolByName(self, 'acl_users')
group_list = acl_users.source_groups.getGroups()

for group in group_list:
    # group is PloneGroup object
    yield (group.getName(), group.title)

List users within all groups

Example to get the email addresses of all users on a site, by group:

acl_users = getToolByName(context, 'acl_users')
groups_tool = getToolByName(context, 'portal_groups')
groups = acl_users.source_groups.getGroupIds()
for group_id in groups:
    group = groups_tool.getGroupById(group_id)
    if group is None:
        continue
    members = group.getGroupMembers()
    member_emails = [m.getProperty('email') for m in members]
    ...

Adding a user to a group

Example:

# Add user to group "companies"
portal_groups = getToolByName(self, 'portal_groups')
portal_groups.addPrincipalToGroup(member.getUserName(), "companies")

Removing a user from a group

Example:

portal_groups.removePrincipalFromGroup(member.getUserName(), "companies")

Getting groups for a certain user

Below is an example of getting groups for the logged-in user (Plone 3 and earlier):

mt = getToolByName(self.context, 'portal_membership')
mt.getAuthenticatedMember().getGroups()

In Plone 4 you have to use:

groups_tool = getToolByName(self, 'portal_groups')
groups_tool.getGroupsByUserId('admin')

Checking whether a user exists

Example:

mt = getToolByName(self, 'portal_membership')
return mt.getMemberById(id) is None

See also:

Creating users

Use the portal_registration tool. Example (browserview):

def createCompany(request, site, username, title, email, passwd=None):
    """
    Utility function which performs the actual creation, role and permission magic.

    @param username: Unicode string

    @param title: Fullname of user, unicode string

    @return: Created company content item or None if the creation fails
    """

    # If we use custom member properties they must be initialized
    # before regtool is called
    prepareMemberProperties(site)

    # portal_registration manages new user creation
    regtool = getToolByName(site, 'portal_registration')

    # Default password to the username
    # ... don't do this on the production server!
    if passwd == None:
        passwd = username

    # We allow only lowercase
    username = username.lower()

    # Username must be ASCII string
    # or Plone will choke when the user tries to log in
    try:
        username = str(username)
    except UnicodeEncodeError:
        IStatusMessage(request).addStatusMessage(_(u"Username must contain only characters a-z"), "error")
        return None

    # This is the minimum required information
    # to create a working member
    properties = {
        'username': username,
        # Full name must always be utf-8 encoded
        'fullname': title.encode("utf-8"),
        'email': email
        }

    try:
        # addMember() returns MemberData object
        member = regtool.addMember(username, passwd, properties=properties)
    except ValueError, e:
        # Give user visual feedback what went wrong
        IStatusMessage(request).addStatusMessage(_(u"Could not create the user:") + unicode(e), "error")
        return None

Batch member creation

Email login

  • In Plone 4 and up, it is a default feature.

Custom member creation form: complex example

Below is an example of a Grok form which the administrator can use to create new users. New users will receive special properties and a folder for which they have ownership access. The password is set to be the same as the username. The user is added to a group named “companies”.

Example company.py:

""" Add companies.

    Create user account + associated "home folder" content type
    for a company user.
    User accounts have a special role.

    Note: As of this writing, in 2010-04, we need the
    plone.app.directives trunk version which
    contains an unreleased validation decorator.
"""

# Core Zope 2 + Zope 3 + Plone
from zope.interface import Interface
from zope import schema
from five import grok
from Products.CMFCore.interfaces import ISiteRoot
from Products.CMFCore.utils import getToolByName
from Products.CMFCore import permissions
from Products.statusmessages.interfaces import IStatusMessage

# Form and validation
from z3c.form import field
import z3c.form.button
from plone.directives import form
from collective.z3cform.grok.grok import PloneFormWrapper
import plone.autoform.form

# Products.validation uses some ugly ZService magic which I can't quite comprehend
from Products.validation import validation

# Our translation catalog
from zope.i18nmessageid import MessageFactory
OurMessageFactory = MessageFactory('OurProduct')
OurMessageFactory = _

# If we're building an addon, we may already have one, for example:
# from isleofback.app import appMessageFactory as _

grok.templatedir("templates")

class ICompanyCreationFormSchema(form.Schema):
    """ Define fields used on the form """

    username = schema.TextLine(title=u"Username")

    company_name = schema.TextLine(title=u"Company name")

    email = schema.TextLine(title=u"Email")


class CompanyCreationForm(plone.autoform.form.AutoExtensibleForm, form.Form):
    """ Form action controller.

    form.DisplayForm will automatically expose the form
    as a view, no wrapping view creation needed.
    """

    # Form label
    name = _(u"Create Company")

    # Which schema is used by AutoExtensibleForm
    schema = ICompanyCreationFormSchema

    # The form does not care about the context object
    # and should not try to extract field value
    # defaults out of it
    ignoreContext = True

    # This form is available at the site root only
    grok.context(ISiteRoot)

    # z3c.form has a function decorator
    # which turns the function to a form button action handler

    @z3c.form.button.buttonAndHandler(_('Create Company'), name='create')
    def createCompanyAction(self, action):
        """ Button action handler to create company.
        """

        data, errors = self.extractData()
        if errors:
            self.status = self.formErrorsMessage
            return

        obj = createCompany(self.request, self.context, data["username"], data["company_name"], data["email"])
        if obj is not None:
            # mark as finished only if we get the new object
            IStatusMessage(self.request).addStatusMessage(_(u"Company created"), "info")


class CompanyCreationView(PloneFormWrapper):
    """ View which exposes form as URL """

    form = CompanyCreationForm

    # Set up security barrier -
    # non-priviledged users can't access this form
    grok.require("cmf.ManagePortal")

    # Use http://yourhost/@@create_company URL to access this form
    grok.name("create_company")

    # This view is available at the site root only
    grok.context(ISiteRoot)

    # Which template is used to decorate the form
    # -> forms.pt in template directory
    grok.template("form")


@form.validator(field=ICompanyCreationFormSchema['email'])
def validateEmail(value):
    """ Use old Products.validation validators to perform the validation.
    """
    validator_function = validation.validatorFor('isEmail')
    if not validator_function(value):
        raise schema.ValidationError(u"Entered email address is not good:" + value)


def prepareMemberProperties(site):
    """ Adjust site for custom member properties """

    # Need to use ancient Z2 property sheet API here...
    portal_memberdata = getToolByName(site, "portal_memberdata")

    # When new member is created, its MemberData
    # is populated with the values from portal_memberdata property sheet,
    # so value="" will be the default value for users' home_folder_uid
    # member property
    if not portal_memberdata.hasProperty("home_folder_uid"):
        portal_memberdata.manage_addProperty(id="home_folder_uid", value="", type="string")


    # Create a group "companies" where newly created members will be added
    acl_users = getToolByName(site, 'acl_users')
    gt = getToolByName(site, 'portal_groups')

    group_id = "companies"
    if not group_id in gt.getGroupIds():
        gt.addGroup(group_id, [], [], {'title': 'Companies'})

def createCompany(request, site, username, title, email, passwd=None):
    """
    Utility function which performs the actual creation, role and permission magic.

    @param username: Unicode string

    @param title: Fullname of user, unicode string

    @return: Created company content item or None if the creation fails
    """

    # If we use custom member properties
    # they must be intiialized before regtool is called
    prepareMemberProperties(site)

    # portal_registrations manages new user creation
    regtool = getToolByName(site, 'portal_registration')

    # Default password to the username
    # ... don't do this on the production server!
    if passwd == None:
        passwd = username

    # Only lowercase allowed
    username = username.lower()

    # Username must be ASCII string
    # or Plone will choke when the user tries to log in
    try:
        username = str(username)
    except UnicodeEncodeError:
        IStatusMessage(request).addStatusMessage(_(u"Username must contain only characters a-z"), "error")
        return None

    # This is minimum required information set
    # to create a working member
    properties = {
        'username': username,
        # Full name must be always as utf-8 encoded
        'fullname': title.encode("utf-8"),
        'email': email
        }

    try:
        # addMember() returns MemberData object
        member = regtool.addMember(username, passwd, properties=properties)
    except ValueError, e:
        # Give user visual feedback what went wrong
        IStatusMessage(request).addStatusMessage(_(u"Could not create the user:") + unicode(e), "error")
        return None

    # Add user to group "companies"
    gt = getToolByName(site, 'portal_groups')
    gt.addPrincipalToGroup(member.getUserName(), "companies")

    return createMatchingHomeFolder(request, site, member)

def createMatchingHomeFolder(request, site, member, target_folder="yritykset", target_type="IsleofbackCompany", language="fi"):
    """ Creates a folder, sets its ownership for the member and stores the folder UID in the member data.

    @param member: MemberData object

    @param target_folder: Under which folder a new content item is created

    @param language: Initial two language code of the item
    """

    parent_folder = site.restrictedTraverse(target_folder)

    # Cannot add custom memberdata properties unless explicitly declared

    id = member.getUserName()

    parent_folder.invokeFactory(target_type, id)

    home_folder = parent_folder[id]
    name = member.getProperty("fullname")

    home_folder.setTitle(name)
    home_folder.setLanguage(language)

    email = member.getProperty("email")
    home_folder.setEmail(email)

    # Unset the Archetypes object creation flag
    home_folder.processForm()

    # Store UID of the created folder in memberdata so we can
    # look it up later to e.g. generate the link to the member folder
    member.setMemberProperties({"home_folder_uid": home_folder.UID()})

    # Get the user handle from member data object
    user = member.getUser()
    username = user.getUserName()

    home_folder.manage_setLocalRoles(username, ["Owner",])
    home_folder.reindexObjectSecurity()

    return home_folder