Tuesday 22 September 2015

Data Driven ‘preferences list’ in OpenIDM

My colleague over at idmonsters has done a great job of describing how to add ‘marketing’ preferences to a user’s profile in OpenIDM.  This requirement is fairly typical as it allows user’s (and administrators) to control things like contact preferences as mandated by law.  The method presented at http://idmonsters.blogspot.co.uk/2015/09/implementing-user-marketing-preferences.html uses config files to determine the set of preferences that should be available for management on a user’s profile.  I decided to take this a step further and make this data-driven.  The list of preferences should be populated via REST call.  This goes beyond a fairly standard set of ‘contact preferences’ and allows more flexibility to ask the user for, say, brand preferences, seating preferences, colour choices, disability needs, channel preferences etc.  i.e. the sorts of ‘choices’ where the available list might change based on the changing drivers of the business.

In the approach presented here I used the generic repository object capability of OpenIDM to store the list of preferences.  This is an often overlooked feature that allows any data to be stored in the OpenIDM repository and accessed via a REST call.  These generic objects are not ‘managed objects’ and therefore can’t participate directly in synchronisation mappings.  They also don’t have a user interface in order to define the behaviour, nor a user interface to manage data – it is all done through REST.  You could of course populate generic objects as part of a Master Data Management strategy that sees the master of this data being elsewhere, or maybe the generic object is the master…anyway, I digress…in this scenario the generic object provides a handy way to store and retrieve simple data sets (the ‘preferences’) over REST.

Prepare Generic Repository Object

Populate Data

So, let’s begin by populating the data.  Fire up your favourite REST client and formulate a ‘PUT’ request.  I typically use Postman, but CURL etc will work just as well.  The parameters we’ll use are:
Method: PUT
Headers:
  • X-OpenIDM-Username: <user> (e.g. openidm-admin) 
  • X-OpenIDM-Password: <password> (e.g. openidm-admin) 
  • Content-Type: application/json 
  • If-None-Match: *
Data:
{"name":"London", "type":"location"}
URL: http://openam.example.com/openidm/repo/custom/preferences/london

Just look at the URL for a moment.  The /repo/custom element tells OpenIDM that we’re planning on storing a generic repository object.  The name of the object is the next element i.e. ‘preferences’.  Then the last element is the _id of the item we’re going to store in this object.  (I find it helps to think of the ‘object’ as a table and the items as rows).
The ‘Data’ will be the values stored as the item in the object.  In this case it is essentially two columns: name and type.

Let’s add some more data using the same Method and Headers, but with the following Data and URLs:

Data:
{"name":"Manchester", "type":"location"}
URL: http://openam.example.com/openidm/repo/custom/preferences/manchester

Data:
{"name":"Glasgow", "type":"location"}
URL: http://openam.example.com/openidm/repo/custom/preferences/glasgow

So far we’ve added three preferences of type ‘location’.  You may not need to include the ‘type’ in your model, or you may need more complex Data.  But for now we’ll add more preferences with a different type:

Data:
{"name":"Bus", "type":"transport"}
URL: http://openam.example.com/openidm/repo/custom/preferences/bus

Data:
{"name":"Underground", "type":"transport"}
URL: http://openam.example.com/openidm/repo/custom/preferences/underground

Data:
{"name":"Taxi", "type":"transport"}
URL: http://openam.example.com/openidm/repo/custom/preferences/taxi

When it comes to the User Interface, we’ll display all of these preferences in a single list, but you should be able to extrapolate what’s going on here to provide multiple/hierarchical lists if you desire.

Configure Access to REST endpoints

By default, OpenIDM restricts access to the REST API for generic repository objects to administrators only.  So, having added the data we now need to configure OpenIDM to allow any logged in user to retrieve this list from the REST API as they’ll be calling this when they access the User Interface.
In ‘access.js’ find the section controlling access to the ‘repo’ endpoint and permit the ‘openidm-authorized’ (for user self management) and ‘openidm-reg’ (for user self registration) roles to access it.  Of course, in production, you may want to be more granular in your permissions.  It should now look like:

        // Disallow command action on repo
        {
            "pattern"   : "repo",
            "roles"     : "openidm-admin,openidm-authorized,openidm-reg",
            "methods"   : "*", // default to all methods allowed
            "actions"   : "*", // default to all actions allowed
            "customAuthz" : "disallowCommandAction()"
        },
        {
            "pattern"   : "repo/*",
            "roles"     : "openidm-admin,openidm-authorized,openidm-reg",
            "methods"   : "*", // default to all methods allowed
            "actions"   : "*", // default to all actions allowed
            "customAuthz" : "disallowCommandAction()"
        },

Configure Profile functionality

Configure permissions to allow users to update their Preferences profile attribute 

Also, by default, OpenIDM restricts the list of profile properties that a user can modify themselves.  An admin can get/set any old property put users are restricted to the list defined in access.js.  So whilst we’re in the file let’s allow users to get/set the ‘preferences’ property of their profile:
var allowedPropertiesForManagedUser =   "preferences,userName,password,mail,givenName,sn,telephoneNumber," +
                                        "postalAddress,address2,city,stateProvince,postalCode,country,siteImage," +
                                        "passPhrase,securityAnswer,securityQuestion";
If we were purely using the REST API to manage/retrieve the information about a user, then we could stop here.  But this article is really about configuring the OpenIDM native UI in order to manage these preferences.

Retrieve Preferences from REST endpoint

Now we need to create a PreferencesDelegate.js file.  This will be used by the UI to call the REST API of the ‘preferences’ generic object in order to retrieve the data to show.  The easiest way to do this is make a copy of the ‘RolesDelegate.js’ and edit that.  (I’m going to make all my UI changes in the ‘default’ branch of the structure rather than make use of the ‘extension’ branch.  The recommendation is to use the extension branch to avoid files being overwritten on upgrade but in my case I have made a copy of each of the files I changed in case I need to revert).
So, make a copy of: 

ui/default/enduser/public/org/forgerock/openidm/ui/user/delegates/RoleDelegate.js 
as
ui/default/enduser/public/org/forgerock/openidm/ui/user/delegates/PreferencesDelegate.js 
Edit PreferencesDelegate.js so that the path to the REST endpoint is the ‘preferences’ repository object rather than the ‘roles’ managed object.  There will be four lines to change that should now look like:
define("org/forgerock/openidm/ui/user/delegates/PreferencesDelegate", [
var obj = new AbstractDelegate(constants.host + "/openidm/repo/custom/preferences");
obj.getAllPreferences = function () {
r._id = "repo/custom/preferences/" + r._id;

User Interface Modification 

Now we’ll modify the UI. There are 4 logical areas to change, with each area needing a changes to both an html template as well as javascript files:

  1. AdminUserRegistration: Administrator functionality to register a new user 
  2. AdminUserProfile: Administrator functionality to modify an existing user’s profile 
  3. UserRegistration: User self-registration 
  4. UserProfile: User modification of their own profile 

 We’ll take each one step by step, although you may not need to implement all items depending on your needs.

AdminUserRegistration

JS file:
ui/default/enduser/public/org/forgerock/openidm/ui/admin/users/AdminUserRegistrationView.js

  1. Include the PreferencesDelegate.js file we created earlier.  Add this after the RoleDelegate reference:
    "org/forgerock/openidm/ui/user/delegates/RoleDelegate",
    "org/forgerock/openidm/ui/user/delegates/PreferencesDelegate"
  2. Ensure the delegate reference is passed as a function parameter called preferenceDelegate:
    ], function(AbstractView, validatorsManager, uiUtils, userDelegate, eventManager, constants, conf, router, roleDelegate, preferenceDelegate)
    
  3. In the formSubmit event ensure that any selected preferences are saved. Add this after the ‘data.roles = …’ line:
    data.preferences = this.$el.find("input[name=preferences]:checked").map(function(){return $(this).val();}).get();
    
  4. In the render event, call the preferences REST API (using the delegate) and add it to the ‘data’ object that will be used to populate the UI. Make the event look like this:
            …
            render: function() {
               $.when(
                roleDelegate.getAllRoles(),
                preferenceDelegate.getAllPreferences()
               ).then(_.bind(function (roles, preferences) {
    
                    var managedRoleMap = _.chain(roles.result)
                                          .map(function (r) { return [r._id, r.name || r._id]; })
                                          .object()
                                          .value();
    
                    this.data.roles = _.extend({}, conf.globalData.userRoles, managedRoleMap);
    
                    var preferenceMap = _.chain(preferences.result)
                                          .map(function (r) { return [r._id, r.name || r._id]; })
                                          .object()
                                          .value();
    
                    this.data.preferences = preferenceMap;
    
                    this.parentRender(function() {
                    …
Note that I’m populating preferences solely from the REST endpoint.  If you look at the ‘roles’ block in this file you’ll see that this is merging ‘managed roles’ with roles configured in a configuration file.  Maybe you’ll want some preferences to be managed by configuration, if so you can replicate the ‘extend’ method in the roles functionality.  E.g:
this.data.preferences = _.extend({}, conf.globalData.userPreferences, preferenceMap);

HTML Template:
ui/default/enduser/public/templates/admin/AdminUserRegistrationTemplate.html
The template specifies the layout of the registration page and is easy enough to work out. You need to decide where to put the preferences list…I chose to locate it ‘after’ (i.e. below) the Password validation rules section. So, add the following block:
            <div class="group-field-block" >
                <label class="light align-right">{{t "Preferences"}}</label>
                {{checkbox preferences "preferences"}}
            </div>

Note the {{t “Preferences”}} item.  This controls the label value in the UI.  If you want to make this globalised then follow the format of the other blocks and update the translation.json file.  I’ll leave that as an exercise for the reader! This block uses the ‘checkbox’ helper function to display the supplied object (the 2nd parameter: preferences), using the name and _id of the items with the object, as checkboxes.  The HTML element it creates is given the ‘name’ of the 3rd parameter (“preferences”).  Note that the JS file relies on the HTML element name in order to work out where to populate that data and which data to submit when the profile is saved. When logged in as an administrator, the ‘Add User’ screen should now look like this:
Now if you add a user, selecting preferences, you’ll be able to see the data stored by retrieving the user in a REST call.
e.g.
http://openam.example.com/openidm/managed/user?_queryId=query-all
might return something similar to this:
        {
            "mail": "jim@example.com",
            "sn": "Bean",
            "userName": "jim",
            "stateProvince": "",
            "preferences": [
                "repo/custom/preferences/bus",
                "repo/custom/preferences/london",
                "repo/custom/preferences/taxi"
            ]
        }


AdminUserProfile

Now let’s edit the Administrator’s functionality to manage a user so that it also includes the preferences functionality.

JS file:
ui/default/enduser/public/org/forgerock/openidm/ui/admin/users/AdminUserProfileView.js

  1. Add the PreferencesDelegate.js file as per AdminUserRegistration above
  2. Add the preferenceDelegate function as per AdminUserRegistration above
  3. Add the data.preferences line as per AdminUserRegistration above
  4. Modify the render event as follows:
    render: function(userName, callback) {
                userName = userName[0].toString();
    
                $.when(
                    userDelegate.getForUserName(userName),
                    roleDelegate.getAllRoles(),
                    preferenceDelegate.getAllPreferences()
                ).then(
                    _.bind(function(user, roles, preferences) {
    
                        var managedRoleMap = _.chain(roles.result)
                            .map(function (r) { return [r._id, r.name || r._id]; })
                            .object()
                            .value();
    
                        var preferenceMap = _.chain(preferences.result)
                            .map(function (r) { return [r._id, r.name || r._id]; })
                            .object()
                            .value();
    
                        this.editedUser = user;
                        this.data.user = user;
                        this.data.roles = _.extend({}, conf.globalData.userRoles, managedRoleMap);
                        this.data.preferences = preferenceMap; 
                     this.data.profileName = user.givenName + ' ' + user.sn;
       …
    
  5. Modify the reloadData event to ensure the UI is able to highlight the preferences already selected against the user’s profile. Add this after the similar roles block:
        _.each(this.editedUser.preferences, _.bind(function(v) {
          this.$el.find("input[name=preferences][value='"+v+"']").prop('checked', true);
        }, this));
    

HTML file:
ui/default/enduser/public/templates/admin/AdminUserProfileTemplate.html

As per the AdminUserRegistration template add a new UI block for the preferences. In this case I created a whole new section after (i.e. below) the Country and State items:
<div class="clearfix">
  <div class="group-field-block col2">
    <label class="light align-right">{{t "Preferences"}}</label>
    {{checkbox preferences "preferences"}}
  </div>
</div>

An Administrator managing a user’s profile should now have a screen that looks like this:


UserRegistration

This allows the user to self-register their own profile.
JS file:
ui/default/enduser/public/org/forgerock/openidm/ui/user/UserRegistrationView.js
  1. Add the PreferencesDelegate.js file as per AdminUserRegistration above
  2. Add the preferenceDelegate function as per AdminUserRegistration above
  3. Modfiy the formSubmit event by adding the data.preferences line:
    … 
    var data = form2js(this.$el.attr("id")), element;
    data.preferences = this.$el.find("input[name=preferences]:checked").map(function(){return $(this).val();}).get();
    delete data.terms;
    …
    
  4. Modify the render event as follows:
            render: function(args, callback) {
    
            $.when(preferenceDelegate.getAllPreferences()
            ).then(
              _.bind(function(preferences){
    
                conf.setProperty("gotoURL", null);
    
                var preferenceMap = _.chain(preferences.result)
                            .map(function (r) { return [r._id, r.name || r._id]; })
                            .object()
                            .value();
                this.data.preferences = preferenceMap;
    
                this.parentRender(_.bind(function() {
    
                    …
                    }, this));
                }, this));
              }, this));
            },
    
HTML file:
ui/default/enduser/public/templates/user/UserRegistrationTemplate.html

Add a new UI block for the preferences.  I added this below the fieldset defining the user profile properties:
<fieldset class="fieldset col0">
    <div class="group-field-block">
        <label for="marketing" class="light align-right">Preferences</label>
        <div class="float-left separate-message">
             {{checkbox preferences "preferences"}}
        </div>
     </div>
</fieldset>

Now, having enabled user self-registration (https://backstage.forgerock.com/#!/docs/openidm/3.1.0/integrators-guide#ui-self-registration) the user should see a screen like this:
The user can now self-register, including preferences information.


UserProfile

To allow the user to modify their own profile preferences once they have been registered carry out the following changes.

This requires two JS files updating along with the HTML template
JS file:
ui/default/enduser/public/org/forgerock/commons/ui/user/profile/UserProfileView.js
This is a fairly simple change replicating the formSubmit event handling of the Admin side:

                …
                this.data = form2js(this.el, '.', false);
                this.data.preferences = this.$el.find("input[name=preferences]:checked").map(function(){return $(this).val();}).get();
                
                // buttons will be included in this structure, so remove those.
                                    …
JS file:
ui/default/enduser/public/org/forgerock/openidm/ui/user/profile/UserProfileView.js

This looks like fairly complex set of changes in order to introduce the PreferencesDelegate and call it at the appropriate point in order to populate the preferences data element, as well as select the items in the list that the user has already added to their profile. But, as it turns out, we end up making this file look like the AdminUserProfile to call the getAllPreferences function.

  1. Add the PreferencesDelegate reference as per Step 1 of the Admin side
  2. Add the preferenceDelegate function reference as per Step 2 of the Admin side
  3. Change the obj.render assignment as follows:
    obj.render = function(args, callback) {
         $.when(preferenceDelegate.getAllPreferences()
         ).then(
           _.bind(function(preferences){
                var preferenceMap = _.chain(preferences.result)
                            .map(function (r) { return [r._id, r.name || r._id]; })
                            .object()
                            .value();
                obj.data.preferences = preferenceMap;
    
            if(conf.globalData.userComponent && conf.globalData.userComponent === "repo/internal/user"){
                obj.data.adminUser = true;
            } else {
                obj.data.adminUser = false;
            }
    
            this.parentRender(function() {
                var self = this,
              …
                    });
    
                    this.reloadData();
    
                   _.each(conf.loggedUser.preferences, _.bind(function(v) {
                       this.$el.find("input[name=preferences][value='"+v+"']").prop('checked', true);
                   }, this));
    
                    if(callback) {
                        callback();
                    }
    
                }, this));
            });
           },this));
        };
    
HTML Template:
ui/default/enduser/public/templates/user/UserProfileTemplate.html

Add the same block as per the AdminUserProfile.  I added this after (i.e. below) the {{/if}} section that wraps the Address Details block:
        <div class="clearfix">
            <div class="group-field-block col2" >
                <label class="light align-right">{{t "Preferences"}}</label>
                {{checkbox preferences "preferences"}}
            </div>
        </div>
When the user edits their profile they should get a screen that looks like this:

Summary

You’re done!  You now have a list preferences populated from a REST endpoint (which happens to be an OpenIDM generic repository object) that are selectable by users and admins alike.

No comments:

Post a Comment