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.
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:
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.
So, make a copy of:
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.jsEdit 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:- AdminUserRegistration: Administrator functionality to register a new user
- AdminUserProfile: Administrator functionality to modify an existing user’s profile
- UserRegistration: User self-registration
- 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
- 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"
- Ensure the delegate reference is passed as a function parameter called preferenceDelegate:
], function(AbstractView, validatorsManager, uiUtils, userDelegate, eventManager, constants, conf, router, roleDelegate, preferenceDelegate)
- 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();
- 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.htmlThe 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
- Add the PreferencesDelegate.js file as per AdminUserRegistration above
- Add the preferenceDelegate function as per AdminUserRegistration above
- Add the data.preferences line as per AdminUserRegistration above
- 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; …
- 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));
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
- Add the PreferencesDelegate.js file as per AdminUserRegistration above
- Add the preferenceDelegate function as per AdminUserRegistration above
- 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; …
- 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:
Add a new UI block for the preferences. I added this below the fieldset defining the user profile properties:
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:
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.
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.
- Add the PreferencesDelegate reference as per Step 1 of the Admin side
- Add the preferenceDelegate function reference as per Step 2 of the Admin side
- 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:
Add the same block as per the AdminUserProfile. I added this after (i.e. below) the {{/if}} section that wraps the Address Details block:
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: