A user manager for our site boils down to writing an editor for the PrincipalFolder we used as the basis of our IAuthenticatorPlugin.  Such an editor should be able to add or remove users, or change the details associated with the principal, for example, change the roles associated with a given principal.

The code for this may be found in users.py.

Let us start with the interface for the editor component we are building.  We know that the PrincipalFolder implements the IInternalPrincipalContainer interface, which is a container for items which conform to IInternalPrincipal.

We may envisage our editor as implementing ILayout, being the general layout of this site, and defining a content area and header area which we can fill in with the guts of our editor.  The editor we might define as a simple list followed by a search box:

class IUsers(Interface):
    """ A user management interface
    """
    users    = List(title=u'Accounts', required=True,
                    value_type=Object(title=u"User", schema=IAccount))
    search   = TextLine(title=u'Search:', required=False, default=u'')

    def nItems(self):
        ''' Returns the number of items in the complete search result '''

    def fromItem(self, setPos=None):
        ''' Optionally sets and returns the position from which to display items '''

We see that the list in the interface uses a value type of IAccount, which we define below:

class IAccount(Interface):
    login       = TextLine(title=u'Login: ', 
                           description=u'A login/user Name')
    title       = TextLine(title=u"Title: ", 
                           description=u"Provides a title for the principal.", 
                           required=False)
    password    = Password(title=u"Password: ", 
                           description=u"The password for the principal.", 
                           required=False)
    roles       = HSet(title=u'Roles: ',
                        value_type=Choice(title=u'Role', vocabulary=u'gfn.AccountRoles',
                            description=u'The kind role the user plays',
                            default='gfn.Visitor'), default=set())

The only non-standard or odd thing about this is the HSet type.  This type does not appear in zope.schema, although it otherwise behaves like a schema.Set:

class HSet(Set):
    ''' Marker class for a horizontal set
    '''

Now, when rendering a schema.Set element, zope.formlib does a queryMultiAdapter((Set, SimpleVocabulary, IBrowserRequest), IInputWidget) to determine the type of widget to render.  Knowing this, we can override default behaviour and define our own widget rather simply by implementing an adapter:

from zope.formlib.itemswidgets import MultiCheckBoxWidget
from zope.formlib.interfaces import IInputWidget
from zope.publisher.browser import IBrowserRequest
from zope.schema.vocabulary import SimpleVocabulary

class HSetVocabularyWidget(grok.MultiAdapter):
    '''  Left to it's own devices, this a set displays a dropdown selection box with
           multi-select capability (control-LMB to choose multiple values.)  Rather than this,
           we would like the widget for Set fields to be multiple check boxes, one for each
           value in the set.  This MultiAdapter overrides the original widget with the new one.
    '''
    grok.adapts(HSet, SimpleVocabulary, IBrowserRequest)
    grok.provides(IInputWidget)

    def __new__(cls, context, vocab, request):
            w=MultiCheckBoxWidget(context,vocab,request)
            w.orientation = 'horizontal'
            return w

This lets us change the way a schema.Set is rendered, and possibly even add a bit of styling to it.

We define our Account model, which implements an IAccount, by declaring the attributes found in IAccount, and giving them values.  We also provide a few helper methods for populating the IAccount.roles appropriately for the given principal:

class Account(grok.Model):
    grok.implements(IAccount)
    login = u''
    password = u''
    title = u''
    roles = set('gfn.Visitor')

    def rolesFromAccount(self):
        ''' Populate the managed roles for this principal from self.roles
        '''
        roleMgr = IPrincipalRoleManager(grok.getSite())
        if self.login == 'admin':
            self.roles.add('gfn.Administrator')
        for rid, _setting in roleMgr.getRolesForPrincipal('gfn.'+self.login):
            roleMgr.unsetRoleForPrincipal(rid, 'gfn.'+self.login)
        for role in self.roles:
            roleMgr.assignRoleToPrincipal(role, 'gfn.'+self.login)

    def accountFromRoles(self, login):
        ''' Populate self.roles by querying the role manager
        '''
        roleMgr = IPrincipalRoleManager(grok.getSite())
        for rid, setting in roleMgr.getRolesForPrincipal('gfn.'+login):
            if setting.getName() == 'Allow':
                self.roles.add(rid)

    def __init__(self, user=None):
        self.user = user
        if user:
            self.login = user.login
            self.password = user.password
            self.title = user.title
            self.roles = set()
            self.accountFromRoles(self.login)

Of course, we don't start with an IAccount, instead we have an IInternalPrincipal.  So to make an IAccount from an IInternalPrincipal, we need an adapter:

class InternalPrincipalAccount(grok.Adapter):
    grok.context(IInternalPrincipal)
    grok.implements(IAccount)

    def __new__(cls, principal):
        return Account(principal)

The Users() model, which implements the IUsers interface, initialises itself from a IInternalPrincipalContainer, keeping a reference to the container and working directly on that.  Users() also keeps a list of Account objects which mirror the container items, but only for a subset of the container items as specified by BATCH_SIZE (10 items).

The do_search() method populates this short list of items, which are the only ones displayed in the editor.

class Users(grok.Model):
    """ The list of users matching the _search filter is cached in _accounts.  We limit
        the number of cached users to BATCH_SIZE, also the number of users actually
        displayed at any time.  The page offset is counted from <pos>.
        The principals folder for the site looks somewhat like a dict, and is passed
        as an argument when instantiating this object.  We keep a reference in
        <principals>.
        The users property setter takes a look at the cached list and compares the new
        list.  From this it determines the set of deleted, inserted or changed items,
        allowing us to update several principals details at once.
    """
    grok.implements(ILayout, IUsers)
    title      = u'User Management'
    principals = {}
    pos        = 0
    _accounts  = []
    _search    = None

    def __init__(self, principals):
        self.principals = principals
        self.do_search()

    def do_search(self):
        p = self.principals
        gen = p.search({'search':self.search or ''}, start=self.pos, batch_size=BATCH_SIZE)
        self._accounts = [IAccount(p[p.principalInfo(i).login]) for i in gen]

The 'search' attribute is implemented in the Users() class as a property, which mirrors the _search attribute:

    @property
    def search(self):
        return self._search
    @search.setter
    def search(self, value):
        if self._search != value:
            self._search = value
            self.do_search()

This approach makes it easy to detect when the search specification has changed.

The users attribute is also implemented as a property.  The read property simply returns the self._accounts attribute, but the users.setter is where the magic is.  The incoming list of accounts is compared  to the self._accounts, and we use set arithmetic to determine the set of added, deleted or altered records.  For the deleted set, we then delete the corresponding container items.  For the new elements we add appropriate InternalPrincipal items to the container.  For the changed items, we alter the record in the container directly.

    @property
    def users(self):
        return self._accounts
    @users.setter
    def users(self, values):
        vdict = {account.login:account for account in values}
        a = set([account.login for account in self._accounts])
        v = set([account.login for account in values])
        deleted = a.difference(v)
        added = v.difference(a)
        updated = a.intersection(v)
        p = self.principals

        for user in deleted:
            del p[user]
        for user in added:
            account = vdict[user]
            if account.login:
                account = vdict[user]
                p[user] = InternalPrincipal(login=account.login, title=account.title, password=account.password)
                account.rolesFromAccount()

        for user in updated:
            u = p[user]
            account = vdict[user]
            if account.login and u.login != account.login: u.login=account.login
            u.title=account.title or ''
            if account.password: u.password=account.password
            account.rolesFromAccount()
        self.do_search()

When all the changes have been made, we repeat the last search to update our internal list of 10 (BATCH_SIZE) elements.

nItems is expected to hold the total length of our internal list, and fromItem is the offset into our result set from which we start displaying items.  nItems is again a property, but fromItems() is a method.

    @property
    def nItems(self):
        ''' Returns the number of items in the complete search result '''
        return len(list(self.principals.search({'search':self._search or ''})))

    def fromItem(self, setPos=None):
        ''' Optionally sets and returns the position from which to display items '''
        if setPos: self.pos = setPos
        return self.pos

To make a Users() instance from a IInternalPrincipalContainer, we need an adapter:

class PrincipalUsers(grok.Adapter):
    """ Adapts our PrincipalFolder to a user management interface
    """
    grok.context(IInternalPrincipalContainer)
    grok.implements(IUsers)

    def __new__(cls, principals):
        return Users(principals)

This adapter means that even though we have traversed to an intance of IInternalPrincipalContainer, our content viewlet which is based upon a Users model will work just fine.  When we redirect to the editor, we determine the new context for our editor by using context=IUser(oldContext). Isn't ZCA magic just great?

...and to render the editor in the content area, we need a viewlet:

class EditPrincipals(grok.Viewlet):
    """ Renders the user management interface within the Content Area
    """
    grok.context(Users)
    grok.require(gfn.Administering)
    grok.viewletmanager(Content)

with a page template to match:

<div tal:content="structure context/@@editprincipalform" />

The above template simply renders the EditPrincipalForm form inside the content area.

Now for the display.  We use grok.formlib to generate an entirely automatic form for the IUsers interface. Since the IUsers.users field is a schema.List of schema.Object, we use the formlib.ListSequenceWidget as a custom widget which holds formlib.ObjectWidget items.  The way to do this is to use a formlib.widget.CustomWidgetFactory:

from zope.formlib.widget import CustomWidgetFactory
from zope.formlib.objectwidget import ObjectWidget
from zope.formlib.sequencewidget import ListSequenceWidget

def AccountWidget():
    '''  An ObjectWidget produces a 'sub-form' for an object, in this case an Account.  If our
           data field happens to be a list of accounts, we can wrap the account widget in a
           list sequence widget.  The CustomWidgetFactory produces widgets by calling the
           appropriate factory with the configured arguments.
    '''
# Show accounts in the list as object widgets (sub-forms)
    ow = CustomWidgetFactory(ObjectWidget, Account)    
    return CustomWidgetFactory(ListSequenceWidget, subwidget=ow)

Now the new AccountWidget can be set inside our automatic formlib form as follows:

class EditPrincipalForm(grok.EditForm):
    ''' A form that allows creation, editing and deletion of principals. '''
    grok.context(Users)          # view available as URL: 'appname/editprincipal'

    grok.require(gfn.Administering)    # Permission requirement
    form_fields = grok.Fields(IUsers)  # Present a search form
    form_fields['users'].custom_widget = AccountWidget()  # The current list of principals
...

We then add a bunch of actions to the form, rendered as buttons. These navigate through the virtual list of items, search the container or apply changes to the list:

    @grok.action(u"Search")
    def search(self, **data):
        self.context.search = data['search']

    @grok.action(u"Apply")
    def apply(self, **data):
        self.applyData(self.context, **data)

    @grok.action(u"First Page")
    def firstPage(self, **data):
        self.context.fromItem(0)

    @grok.action(u"Next Page")
    def nextPage(self, **data):
        if self.context.fromItem() + BATCH_SIZE < self.context.nItems():
            self.context.fromItem(self.context.fromItem()+BATCH_SIZE)

    @grok.action(u"Prev Page")
    def prevPage(self, **data):
        if self.context.fromItem() - BATCH_SIZE >= 0:
            self.context.fromItem(self.context.fromItem()-BATCH_SIZE)
        else:
            self.context.fromItem(0)

    @grok.action(u"Last Page")
    def lastPage(self, **data):
        n = self.context.fromItem() / BATCH_SIZE
        if n % BATCH_SIZE == 0: n -= 1
        if n < 0: n = 0
        self.context.fromItem(n * BATCH_SIZE)

To make the form look good, we use a bit of css, as defined in the update() method of the form:

    def update(self):
        rc.tabular.need()

The tabularform.css included by the rc.tabular.need() looks like this:

form { width:100% }
form > table {
    width: 100%;
    -webkit-border-top-left-radius: 20px;
    -khtml-border-top-left-radius: 20px;
    -moz-border-top-left-radius: 20px;
    -ms-border-top-left-radius: 20px;
    -o-border-top-left-radius: 20px;
    border-top-left-radius: 20px;
}

form > table fieldset {color:blue; border:none}
form > table fieldset > legend {display:none}

form > table.form-fields {border:1px solid blue}
form > table.form-fields td.fieldname{display:none}
form > table.form-fields tr:first-child td.label span{display:none}
form > table.form-fields li:nth-child(even) {background:#cecefe}
form > table.form-fields tr:nth-child(even) {background:#cecefe}
form > table.form-fields div.row {float:left}
form > table.form-fields div.row input {border:none; border-bottom:1px dashed black; background:lightgreen}
form > table.form-fields div.row div {float:left}
form > table.form-fields div.row label {margin-left:5px; margin-right:2px}
form > table.listing input {background:none}

 The form renders automaticallly as:

 The rest of the editor is just plugging in the navigation buttons and so forth.

class BackButtonMenuEntry(MenuItem):
    '''  A menu item for articles with parent articles. IOW NoobsArticle
    '''
    grok.context(Users)
    title = u'Back to Main'
    link = u'..'
    mclass = 'nav buttons'


class UsersButtonMenuEntry(MenuItem):
    '''  A menu item for articles with parent articles. IOW NoobsArticle
    '''
    grok.context(ISiteRoot)
    grok.require(gfn.Administering)
    grok.order(-4)
    title = u'Manage Users'
    link = u'/users'
    mclass = 'nav buttons'

 In order to link the /users URL to the site, we add the following to the grok4noobs.Grok4Noobs class:

    grok.traversable('users')
    def users(self):
        sm = self.getSiteManager()
        if 'users' in sm:
            return IUsers(sm['users'])

 

Grok 4 Noobs

Building a user management interface