Tuesday, 24 September 2013

Quick Search By Location in a Portlet

Looking for a simple portlet for quick search in the catalog by location? Look no further, Saba Guru has created this portlet for you! The portlet is a simplified version of the Catalog Search, offering a list of locations and the next 5 offerings available for the selected location. In this first implementation of the portlet, I'll present the list of locations in a drop-down list. In the next article, locations will be displayed on a map. Because the search in the catalog is specific to offerings available by location, WBT (Web Based Training) and any other type of offerings without a location assigned are automatically excluded by the result.

A list of Locations

Locations will be displayed in a drop-down list, at least initially, and displayed in the portlet as in the screenshot below.

List of Locations in the portlet

By selecting a location in the list, the portlet refreshes and then displays the search result, that is a table of the next five offerings available in the selected location.


For each offering, it is possible to register to it by clicking on the Register link under the Actions column.

Step by step, this is the way I have implemented the Location drop-down list in Saba:


1) Any portlet always starts with a manager. I am assigning the name QuickSearchByLocationPortletPageManager to the manager class. The fully qualified name of the manager class, i.e. including also the package name, will be entered in Saba when configuring the portlet on the portal.


public class QuickSearchByLocationPortletPageManager
   extends AbstractPortletPageManager
   implements PortletPageManager
{
   @Override
   protected void init()
   {
      registerPage("showDefaultDisplay"
         "/custom/portlets/quickSearchByLocation.rdf");
   }
}

2) The manager refers to the quickSearchByLocation.rdf page for the layout of the portlet on screen. This is the controller page, we also have the model quickSearchByLocation.xml and the view quickSearchByLocation.xsl.
In the model we define the logic for retrieving the list of locations, which is encapsulated in a portlet command. A portlet command is simply an extension of a regular SabaWebCommand that generates the result (the list of locations) and returns it to the model page as an XML document.

public class QuickSearchByLocationPortletCommand
   extends SabaWebCommand
{
   public void doExecute(HttpServletRequest request, IXMLVisitor visitor)
   {
      visitor.beginVisit(null, "Result"nullnullnull);
      visitLocations(visitor);
      visitor.endVisit(null, "Result");
   }

QuickSearchByLocationPortletCommand is the portlet command class that builds a model document with the list of the available locations, in alphabetical order. For each location, ID and Name attributes are retrieved and passed to the model page using the following XML format:

<Result>
   <Locations>
      <Location>
         <Id></Id>
         <Name></Name>
      </Location>
   </Locations>
</Result>

The structure of the <Locations> tag is built by the visitLocations() method.

protected void visitLocations(IXMLVisitor visitor)
{
   visitor.beginVisit(null, "Locations"nullnullnull);

   List<ArrayList<String>> locations = getLocations();

   for (ArrayList<String> loc : locations) {
      visitor.beginVisit(null, "Location"nullnullnull);
      visitor.visit(null, "Id", loc.get(0));
      visitor.visit(null, "Name", loc.get(1));
      visitor.endVisit(null, "Location");
   }

   visitor.endVisit(null, "Locations");
}

The getLocations() method retrieves a list of ArrayList containing Id and Name at position 0 and 1 respectively for each location, and with this information builds the XML tree aforementioned.

3) With the XML model ready, it is now the turn of the model page to map it to a widget for display inside the portlet. A drop-down list control is rendered in Saba using the <wdk:list> widget with type = select. I call this widget locationId, as this will be the name of the parameter passed to the portlet command again when searching for offerings in the indicated location, as we will see later.

<wdk:list name="locationId">
   <type>select</type>
   <wdktags:attachTo path="Result/Locations"/>
   <wdktags:repeat name="options" path="Location">
      <option>
         <value>
            <wdktags:nodeRef source="options" path="Id"/>
                      </value>
         <text>
            <wdktags:nodeRef source="options" path="Name"/>
         </text>
      </option>
   </wdktags:repeat>
       <event>
      <type>action</type>
      <action>
         <type>submit</type>
         <href>
            /platform/presentation/portal/portalDriver.rdf
         </href>
      </action>
   </event>
</wdk:list>

The list is "attached to" the Result/Locations path using the <wdktags:attachTo> tag. This means that single locations are listed under this parent node in the XML model, and each location is identified by the Location tag, as defined in the <wdktags:repeat> tag. This tag, as the name implies, repeats each occurrence of the defined path and displays multiple options for the drop-down list. The value of the option is assigned as the content of the Id tag in the XML model, and the text as the Name tag.
When an option is selected, the event defined in the <event> tag is triggered, which basically is a refresh of the portal driver page that contains the portlet.

4) When the portal page is refreshed, the locationId parameter identifying the selected location is passed to the portlet command for performing the search in the catalog.

<xsp:logic>
   QuickSearchByLocationPortletCommand command =
      new QuickSearchByLocationPortletCommand(); 
</xsp:logic>
<wdk:model>
   <wdktags:execute commandObj="command">
      <param name="locationId" expr="locationId" type="String" 
         mode="in" /> 
   </wdktags:execute>
</wdk:model>

An instance of the QuickSearchByLocationPortletCommand object is created and then used to execute the command as specified in the <wdktags:execute> tag, by passing the locationId parameter.

Performing the search in the catalog by location

Continuing along the step by step implementation, it is now the turn of performing the search in the catalog for offerings in the selected location.

5) We are now back in the portlet command. First of all we need to register the input parameter, and we do so in the constructor of the command class.

public QuickSearchByLocationPortletCommand()
{
   addInParam("locationId", String.class, "Id of the Location");
}

6) We then retrieve the value of this parameter in the doExecute() method and then pass it to the Saba API that performs the search in the catalog.

protected void visitOfferings(IXMLVisitor visitor,
   String locationId)
{
   visitor.beginVisit(null, "Offerings"nullnullnull);
   Iterator<OfferingResult> offerings = getOfferings(locationId);
   while (offerings.hasNext()) {
      OfferingResult result = offerings.next();
 
      visitor.beginVisit(null, "Offering"nullnullnull);
 
      visitor.visit(null, "Id",
         result.getPrimaryKey().toString());
      visitor.visit(null, "OfferingId",
         result.getPartNo());
      visitor.visit(null, "Title",
         result.getOffTemp().getDisplayName());
      visitor.visit(null, "Delivery"
         result.getDelivery().getDisplayName());
      visitor.visit(null, "StartDate",       
         result.getStartDate());
 
      visitor.endVisit(null, "Offering");
   }

   visitor.endVisit(null, "Offerings");
}

The visitOfferings() method is basically building the XML model with the list of offerings to display in the portlet, for the selected location. The XML model has the following format, with the <Offering> tag repeated for each occurrence. The <Offerings> node is contained within the <Result> root node, as we have seen previously for the <Locations> node.

<Offerings>
   <Offering>
      <Id></Id>
      <OfferingId></OfferingId>
      <Title></Title>
      <Delivery></Delivery>
      <StartDate></StartDate>
   </Offering>
</Offerings>

7) The final touch is with displaying the offering list in the portlet. I decided to use the <wdk:table> widget for displaying items in a table format. The full description of the syntax of this widget can be found in the Saba Application Developer Guide, so for now I will describe only the significant parts that concern this portlet.

<wdk:table name="finderResults">
   <wdktags:attachTo path="Result/Offerings"/>
   <head>
      <column>Title</column>
      <column>Offering ID</column>
      <column>Delivery Type</column>
      <column>Start Date</column>
      <column>Actions</column>
   </head>
   <row path="Offering">
      <column><wdktags:nodeRef path="Title"/></column>
      <column><wdktags:nodeRef path="OfferingId"/></column>
      <column><wdktags:nodeRef path="Delivery"/></column>
      <column><wdktags:nodeRef path="StartDate"/></column>
      <column>
         <wdk:link name="register"></wdk:link>
      </column>
   </row>
</wdk:table>

The widget is attached to the Result/Offerings path in the XML model, where all the offerings can be found. For each row identified by the Offering node, the following columns are displayed:
  1. Title
  2. Offering ID
  3. Delivery Type
  4. Start Date
The Actions column contains a link to the registration page for the offering (link not detailed in the code above). For simplicity of visualization of the code snipped above, I hard-coded the name of the columns in the <column> tag within <head>. In a real scenario, no hard-coded labels should be used, but a reference to the label bundle should be used instead. This will facilitate internationalization of the portlet as well.
The value of the each column, as in the model, is retrieved from the XML node using the <wdktags:nodeRef> tag for each <column> within <row>.

Wrap up

Lot of Java and XML code in this post, but hope it's been useful to figure out the necessary steps for creating a fully functional portlet that interacts with the database. I saved you the use of the Saba API for actually retrieving information (locations and offerings) from the database, as this is not specific to a portlet implementation. Feel free to contact me if you want to discuss these details further.

In the next article, I will expand the functionality of this portlet by introducing a visualization of locations based on Google Maps rather than in a drop-down list. The opportunity is good to show how to integrate Saba resources (locations in this case) with external web sites and display everything within a portlet!

Happy search!

Wednesday, 28 August 2013

Summer Refresh of Saba Mobile Apps

It has been a busy summer for the Saba Mobile team. The full set of Saba mobile apps for Android and iOS, which includes Saba Cloud, Saba Meeting and Saba Anywhere, received new important updates.

The apps are available in the Google Play and Apple iTunes app stores.



Saba has refreshed the user experience of their apps to empower users to collaborate, share and consume their learning content anytime and anywhere on their mobile devices.

Here is a quick summary & highlights of some of the exciting enhancements:

  • Online & offline consumption of content on mobile
  • Ability to consume standards-compliant content on mobile devices: AICC, SCORM & Tin Can
  • Added support for multi-SCO and multi-module learning content
  • Ability to consume SCORM and file-based content online without having to download it first
  • Ability to play externally hosted content on mobile devices
  • Rock Star endorsements added to the activity stream
  • Ability to seamlessly launch Saba virtual classroom from Saba Learning App to either attend virtual sessions or launch session recordings


More information about this important release on the official Saba Blog.

Wednesday, 21 August 2013

Saba surveys in a portlet

Saba provides several portlets with the out of the box installation of the Learning Management System, but one missing and topping the list of requests from the customer community is a portlet for displaying surveys. If you have ever published a survey, you know how little accessible surveys are to end users: click on My Learning tab, then Evaluations & Surveys menu and again on Surveys, then eventually you see a list of your surveys. Not great, if what you need is to draw the attention of your student to a public survey. What's better page than the student's dashboard page then!

In my previous article, I've already addressed a specific case of integrating a third party survey tool in a portlet for publishing quick and anonymous surveys. In this article I'll present an easy technique for publishing the Saba built-in surveys in a portlet and make them accessible to end users in just a click!

Overview of Surveys

Saba provides a framework for obtaining feedback from learners in the form of surveys. A survey is a questionnaire with one or more questions that is used to gather feedback from specified respondents. Surveys can be assigned to both internal people (i.e. employees) and external people (i.e. clients). Designated respondents can always choose to accept or decline a survey.

The stages and roles involved in creation and delivery of a survey are:

  • Creation: People Administrators create a survey, attach a questionnaire, define the distribution list, and publish it. Survey is sent to participants in the distribution list for approval and assessment.
  • Progression: Raters accept the survey request and complete the survey or decline the survey request. A participant can also choose to remain anonymous, but answer the survey.
  • Completion: Saba automatically accumulates the assessment results and sends a survey closure notification to the owner after the expiry of the due date.

The Survey Management page displays a list of all surveys and their statuses. A survey can have the following statuses:

  • Draft: A survey is created and is saved for later edits, not published.
  • In Progress: A survey is published and sent to respondents.
  • Completed: A survey is closed.

Once a survey is published, it is available in the My Learning > Evaluations & Surveys tab of the Home module to learners designated as respondents for the survey. The respondents can choose to accept or decline the survey, and, if the former, then eventually take and complete it by answering all questions in the survey.

The Survey Portlet

The purpose of the survey portlet is to list In Progress surveys to nominated users directly in their home dashboard. Draft and completed surveys won't be displayed. A portlet should be used for providing a shortcut to a more structured and complete area of the Saba system, and it should not be used as a replacement for an existing functionality. For this reason, I decided to create a very specific version of the portlet with some reduced functionality (as compared to the regular page from where users can access surveys), but with the added benefit of fitting within the limited space of a portlet and offering a one-click access to published surveys.

The starting point of any new portlet, as we learned in my previous article, is to create a Portlet Page Manager. This is a Java class that extends the AbstractPortletPageManager interface and implements the PortletPageManager abstract class. By doing this, we have all the wiring ready, and what is left to do is simply to override the init method and specify the Saba WDK page that is responsible for displaying the actual portlet on the screen.

public class SabaSurveyPortletPageManager
   extends AbstractPortletPageManager
   implements PortletPageManager
{
   @Override
   protected void init() throws SabaException
   {
      registerPage("showDefaultDisplay",
         "/custom/portlets/surveysForRater.rdf");
   }
}

The fully qualified name of the SabaSurveyPortletPageManager class will be used for the PageManagerClass parameter when creating a new portlet in Saba.



The name of the controller page is /custom/portlets/surveysForRater.rdf. This is not coincidental. The regular Saba page that is used for displaying surveys to end users is /common/survey/surveysForRater.rdf. All I have done has been to simply make a copy of that page into the /custom/portlets folder of the alternative source code and then modify it to fit the purpose of the survey portlet.

If you look at the standard Surveys page, you will notice that the page displays survey information that we can hide in our portlet, to save space and reduce the clutter on the screen.
Specifically, we do not need the Current and Completed tabs, as we are only displaying In Progress surveys. Also, in the survey list, we probably can get along without Description, Created On and Created By. We also do not want the user to modify, print or export the list of surveys, so we can get rid of these links on the top right corner of the survey table.

This is the original page with all the mentioned columns and links:


And this is the layout of the portlet that we want to obtain:


Let's go in order then and remove the tabs on top that allow to switch between Current and Completed surveys. When doing this kind of page "surgery", I typically look first at the view page (the page with the .xsl extension) and see where that control is displayed:
Within the surveysForRater.xsl page I can easily spot these lines of a table row:

<tr>
   <td colspan="2">
      <xsl:apply-templates select="$wdkWidget[@name='surveyStatusMultiSelect']"/>
   </td>
</tr>

That's your widget. Copy the surveyStatusMultiSelect name, we need it later, and delete all the above lines from your view page. Job done!

Now let's find this control in the model page (the page with the .xml extension) and remove it from there as well. Search for "surveyStatusMultiSelect" in the surveysForRater.xml page until you find this line:

<wdk:multisection name="surveyStatusMultiSelect">

That's your multi-section widget. Feel free to remove the entire tag, including its content. And again job done!

Now the columns. To get rid of extra columns in the survey table, we have to look for a the FinderResult widget identified by the <wdk:finderResult> tag, within the <wdk:widgets> section.

<wdk:widgets>
  <wdk:finderResult name="surveyResults">

This is the widget that displays the list of surveys assigned to a user in a table format. There is a special instruction that you can use for removing an existing column, which is applied to the widget by entering as many <removeDisplayColumn> tags as needed. To remove the Description column, for example, just add the following XML tag to any place within the FinderResult widget (I typically add these instructions after the list of columns to display and before the definition of the sorting order, but the actual position is not really relevant).

<removeDisplayColumn>
  <name>description</name>
</removeDisplayColumn>

Repeat the same instruction also for the Created On and Created By columns and, congratulations, job done!

Lastly, we want to remove the links to print, export and modify the table. There are specific instructions for that, as documented in the Saba Application Developer's Guide, that you can apply by specifying the following simple tags:

<print>false</print>
<export>false</export>
<modifyTable>false</modifyTable>

Once again, job done, all changes have been applied to the model page.

The final touch

Are we done then? Well, yes and no. Publish these pages and launch a survey. You will see a pop-up like this one opening, containing all sections and questions of the survey.



When you submit (or exit) this pop-up, Saba refreshes the launching page (i.e. the page from where you clicked on the Launch link) to reflect any changes introduced by completing the survey (very likely, if you complete the survey, its status moves to Completed and therefore it disappears from the list of In Progress surveys in the portlet). If you launch the survey from within the portlet and then close the survey pop-up window, however, the launching page reloads but a wrong page is displayed. Actually, it's not a wrong page but it is the standard survey page that you typically find in My Learning > Evaluations & Surveys > Surveys. So what's happening is that the pop-up window doesn't know whether the survey has been launched from the standard page or from the portlet. Reason for that is that... ehm... unfortunately, the address of the launching page has been hard-coded in the view page (bad bad practice Saba, come on!) so we have to modify it to make it work for the portlet and have the dashboard page reloaded (and hence our survey portlet with it) rather than the standard survey page.

Look for this JavaScript code in the view page, that performs the refresh of the launching page:

function submitForm()
{
wdkFrameworkSubmit("/common/survey/surveysForRater.rdf");
}

As you can see, the full name of the /common/survey/surveysForRater.rdf page is specified, which, as we know, is the standard page for displaying surveys to users.
We want to replace it with our portlet page, but we cannot just specify the full name of the /custom/portlets/surveysForRater.rdf page here, otherwise we would reload only the portlet page outside of the dashboard portal. We do need to specify the portal page that, once refreshed, will reload all portlets in turn.

function submitForm()
{
  wdkFrameworkSubmit("/platform/presentation/portal/portalDriver.rdf");
}

That works, but yes, it still stinks of hard-coded page name in a view page, which is against the MVC principles (page navigation should only be responsibility of the controller page, and never of the view). A more elegant way to resolve this problem is to introduce a link reference in the controller page and have the view point to that reference rather than the full survey page name. But this is another story...

Wednesday, 14 August 2013

An Enhanced Survey Monkey portlet

In my previous post, I presented a simple usage of the Mashup portlet in Saba for displaying a Survey Monkey survey and collect anonymous responses. Although immediate to configure, I am not entirely happy with the result, as on completion of the survey, Survey Monkey displays a promotional page to the user. Also, once the response is collected, the portlet is no longer relevant to the user and it would be better hidden automatically.

So my new objective that I want to address in this post is to hide the portlet automatically and remove it from the user's dashboard as soon as he/she provides the response. In order to do that, I need to... invent a new bespoke portlet! C'est plus facile!

A new Survey Monkey portlet

Adding a brand new portlet to Saba is not complicated at all, as explained in this article already. But before getting into the technical details, let's set a few requirements for the new portlet, that will guide us in the development of the necessary code.
  1. I want to specify the URL of the survey without bothering with the HTML code for the iframe that eventually displays the survey in my portlet (as instead seen for the Mashup article in my previous article).
  2. I still want control on the size of the area where the survey is displayed within the portlet, so I need a way to enter the width and height values of this window.
  3. I want to inform users, with a user-friendly message, possibly supporting text formatting, on what to do when they complete the survey.
  4. This will trigger an action that hides the portlet from the user's dashboard only. The portlet will remain visible to all other users who have not yet completed the survey.
In order to accomplish all these requirements, we need a new portlet that supports 3 parameters for entering the survey URL, width and height and user message.
Eventually, as a System Administrator, I want to be able to enter this data in the portlet parameters as in the following screenshot:


The code now. I will call my new portlet "Survey Monkey" (no surprise here!) and define the portlet manager as follows:

public class SurveyMonkeyPortletPageManager
extends AbstractPortletPageManager
implements PortletPageManager
{

   @Override
   protected void init()
   throws SabaException
   {
      registerPage("showDefaultDisplay",

         "/custom/portlets/surveyMonkeyPortlet.rdf");

The surveyMonkeyPortlet.rdf page is the controller for the Saba WDK pages that represent the portlet itself. More about this and the model and view pages later in this article.

Where are the parameters? Within the init method, simply add these additional lines of code to register the three parameters. Unfortunately, parameter names cannot contains spaces, so play with upper and lower case letters (aka Pascal case) for making these names readable.

ParameterDetail url = new ParameterDetail("SurveyURL",
   TypeInfo.createStringType());
registerParameter(url);


ParameterDetail par = new ParameterDetail("IframeParams",
   TypeInfo.createStringType());
registerParameter(par);


ParameterDetail msg = new ParameterDetail("MessageXHTML",
   TypeInfo.createStringType());
registerParameter(msg);


That's all we need for the Java portlet manager. As we know, the portlet manager references the controller page (RDF page) that is responsible for orchestrating the communication flow between the model (XML page) and the view (XSL page). The controller references the other two pages with these simple WDK tags:

<wdk:model rdf:resource="surveyMonkeyPortlet.xml"/>
<wdk:view rdf:resource="surveyMonkeyPortlet.xsl"/>


The workflows

This is the process that I want to implement as a user experience for a System Administrator in Saba:
  1. The new Survey Monkey portlet is created in the system, using the SurveyMonkeyPortletPageManager class as portlet manager.
  2. Externally, a survey exists or is created in Survey Monkey.
  3. The survey's URL is obtained and entered in the SurveyURL parameter of the portlet, along with the expected width and height of the area where the survey is displayed within the portlet. The accepted format is: width="nn" height="mm" where nn and mm are whole numbers. Unit indicators like % (percentage of the portlet size) and px (pixels) can also be used, as in, for example: width="100%" height="300px"
  4. The portlet is assigned to "mysaba" portal for making it available to end users in their dashboard.
  5. The survey is then launched and visible to end users, who can complete it at their next login to the Saba system. Instructions on what to do on completion of the survey are indicated within the portlet (see separate process for the end user). Once the survey is closed, which happens when either all participants have responded, or when the survey expires, the System Administrator can remove the portlet from the portal and hide it from end users.

For the end user, the process starts when the portlet is assigned to the portal and made visible in the Dashboard upon login:
  1. The user completes the survey and follows the instructions post completion. Instructions are entered by the System Administrator in XHTML format (i.e. with support for full text formatting including bold, italics, links, etc.) in the MessageXHTML parameter of the portlet.
  2. A "Hide" button exists in the portlet for automatically removing the portlet from the user's dashboard. This does not effect any other users.
This step is necessary because there is no way in Saba to monitor the completion of the survey in Survey Monkey. The responses provided in the survey are sent to the Survey Monkey server from the page embedded in the portlet, with no interaction with the Saba system. Therefore, there is no possibility for the Saba system to know when the user actually completes the survey. The only option available is to let the user mark their survey complete by clicking on the Hide button explicitly.


The workflows for these two processes are implemented in the Saba WDK pages with the help of a special portlet command class that facilitates the communication between the model page and the database. This page is not strictly necessary, as all the business logic can be contained in the model page, but it is a commodity for encapsulating repeating code into one single specialised class, as we will see later.

The Model

The structure of the model is very straightforward:
  • The header <wdk:head> contains the input parameters received by the portlet manager
  • The form <wdk:form> contains the XML model as obtained from the portlet command
  • The widgets section <wdk:widgets> contains the Hide button
Just a quick word about the form section, which is internally made of two subsection for the logic part (i.e. the Java code to invoke the portlet command) and the actual model.
Java code can be injected in the model page by using the <xsp:logic> tag.

<xsp:logic>
SurveyMonkeyPortletCommand command = 
   new SurveyMonkeyPortletCommand();
</xsp:logic>


This simple line of code creates a new instance of the SurveyMonkeyPortletCommand class, to be used by the <wdktags:execute> tag for invoking the command class.

<wdktags:execute commandObj="command">
   <param name="SurveyURL" expr="SurveyURL" type="String" mode="in"/>
   <param name="IframeParams" expr="IframeParams" type="String" mode="in"/>
   <param name="MessageXHTML" expr="
MessageXHTML" type="String" mode="in"/>
   <param name="actionKey" expr="actionKey" type="String" mode="in"/>
</wdktags:execute>


The command accepts the 3 portlet parameters, plus a fourth actionKey that I'll explain later. After execution, the command returns an XML model that is then transferred to the view for visualization to the end user.

The last section of the model identifies all widgets, i.e. visual controls, to display on the page. We only have one button to show to the user, to inform Saba the the survey has been completed. This is the Hide button that also triggers the action for automatically hiding the portlet from the user's dashboard without the user explicitly removes the portlet from the dashboard page. The tag for defining a button widget is <wdk:link> with type = button. On click, the portlet reloads and executes the command again, but this time, because of the actionKey parameter mentioned before, the outcome is different, as explained later for the command class.

The XML tags for defining the Hide button are as follows:

<wdk:widgets>
   <wdk:link name="hideButton">
      <id>hideButton</id>

      <label>Hide</label>
      <type>button</type>
      <do>Completed</do>
    </wdk:link>
</wdk:widgets>

The <do> tag is a shortcut for passing the actionKey parameter to the portlet.

The View

The view page offers two visual sections to the end user within the portlet:
  1. A frame for displaying the survey.
  2. An section, that I decided to put just beneath the survey - but can be moved anywhere, for the instruction message to the user on what to do on completion of the survey. This section also contains the Hide button.
The frame for displaying the survey is actually an inline frame, whose properties are defined as attributes of the <iframe> tag by assigning the pertinent value as received from the model page. The XSLT code below defines the src attribute. The other attributes can be defined in the same way.

<iframe id="iframeSurveyMonkey">
   <xsl:attribute name="src">
      <xsl:value-of select="params/surveyUrl" />
   </xsl:attribute>


The section below the survey, with the message, instead is contained within a <div> tag. The message itself can contain HTML tags for formatting the text, hence the XSLT code for rendering this message on screen relies on the <xsl:copy-of> tag that performs a copy of the message including all existing tags. Adding the Hide button widget relies instead on the ordinary <xsl:apply-templates> tag for selecting the widget matching the indicated name.

<div class="sbListText">
   <xsl:for-each select="params/message/*">
      <xsl:copy-of select="."/>
   </xsl:for-each>


   <xsl:apply-templates select="$wdkWidget[@name='hideButton']"/>
</div>


The Command

To complete the description of this portlet, the last section is dedicated to the portlet command. As said before, the command is instantiated and executed by the model page, which passes several parameters to the command class. Specifically, according to the value of the actionKey parameter, two completely different actions are performed:
  1. actionKey does not have any value, i.e. it's an empty string: the command class parses the URL, width, height and message parameters and builds the XML model response. The portlet displays the survey and the user message.
  2. actionKey is equals to Completed: the command class hides the portlet by leveraging the services of the PortalClientManager class, which is part of the Saba API, and then returns an empty model to the portlet (at this point the model is irrelevant as the portlet has been removed from the user's dashboard).
A few snippets of the command's class source code.
The base class from which all commands should extend their definition is SabaWebCommand. The input parameters are to be defined in the constructor through the addInParam method.

public class SurveyMonkeyPortletCommand
extends SabaWebCommand
{
   public SurveyMonkeyPortletCommand()
   {
      addInParam("SurveyURL", String.class, 

         "The URL of the survey");
      ...

The public method invoked by the model page through the <wdktags:execute> tag is doExecute. The signature of this method is as follows, and the way to obtain the value of the input paramenter is through the getArt method.

public void doExecute(HttpServletRequest request, 
    IXMLVisitor visitor)
{
   String surveyURL = (String)getArg("SurveyURL");

   ...

Inside this method, according to the value of the actionKey parameter, two different actions are performed, as described. The visitParams method is used for building the XML document that is returned to the model page.

if (actionKey.equals("Completed")) {
   hidePortlet("Survey Monkey");
   visitParams(visitor, "", "", "", null);
}
else {
   Map<String, String> params = getParams(iframeParams);
   Node message = getContent(messageXHTML);
   visitParams(visitor, surveyURL, params, message);
}


Conclusion

We put it all together and the end result is something similar to the following screenshot: a fully integrated bespoke portlet for Survey Monkey in Saba.


This was the opportunity to describe an end-to-end development of a custom portlet that integrates with an external system and interacts with the user for collecting information and communicating with the back-end using a portlet command.

Happy Monkey Regards!
Stefano






Thursday, 1 August 2013

A Survey Monkey portlet

Ever thought about submitting a quick anonymous survey to all your students in Saba and make it available in a portlet on the person's dashboard?
The Saba's own survey tool is not quite fit for the purpose, not last because it requires a content administrator in Saba to create the survey itself.
What we really want, instead, is a very simple and immediate survey that even your CEO can create and submit in minutes.

Tools like Survey Monkey (www.surveymonkey.com) suits perfectly the need and the good news is that you can integrate a survey in Saba and display it within a portlet in just minutes!

The Mashup portlet

All the wiring of the Survey Monkey survey with Saba is done in a mashup portlet. Mashup portlets display data retrieved from an external site using JavaScript and HTML.
Basically, a mashup portlet is a container of HTML and JavaScript code that can be embedded in a Saba portal. The page manager class is com.saba.client.portal.MashupPortletPageManager.
With this in mind, adding a new mashup portlet to an existing portal is as simple as an ABC (and therefore I won't bore you with the details).
For reference, the next picture shows the details of my Survey Monkey mashup portlet as configured in my environment.


Not in the picture, there are two extra parameters that can be configured for defining the content and layout of the mashup portlet:
  • JSFunc: This parameter defines the JavaScript expression that is executed on page load, i.e. when the portal completes loading the mashup portlet. Please note, the Saba 5.5 System Administration Guide reports an example of a JavaScript function specified for this parameter. Unfortunately, this is not correct. The JSFunc parameter accepts immediate JavaScript commands and not functions. The good news is that... we don't need to configure anything here, the Survey Monkey survey will open automatically inside the portlet container, we don't need to specify any JavaScript action occurring after the portlet loads.
  • XHTML: This is where the HTML code goes, which defines the content and layout of the portlet. This must be a properly formatted XHTML code, so all tags should have an opening and a respecting closing tag (i.e. <p> and </p>). Also, attributes should be quoted (<div width=100> is not acceptable, it has to be <div width="100">). We will use this parameter to embed the Survey Monkey survey in our mashup portlet.

Putting a Survey Monkey in a portlet

Probably you have already used Survey Monkey in the past, so you already know how to create a survey in there. What you need to do now is simply obtaining the survey web link and use it for defining the survey to display in the portlet.

The survey web link comes in the form http://www.surveymonkey/s/XXXXXXX, where XXXXXXX is a 7-character code the uniquely identifies the survey, as in the picture below.


All we need to do now is to embed this survey within the mashup portlet. We do this by using an iframe configured as follows:

<iframe src="http://www.surveymonkey.com/s/6S9Y5Y5" width="100%" height="300px"></iframe>

The src attribute clearly references the survey web link. The iframe width is set to 100% so it can take all the horizontal extent of portlet. The height attribute instead is fixed to the minimum height you want to set for your survey showing inside the portlet's frame. 300 pixel is a reasonable size, but make your own tries to figure out which height best fits your survey.

Just add that piece of HTML to the XHTML property of the mashup portlet, save, add the portlet to a portal and voilĂ  job done!


There's margin for improvement, though. Once the survey is answered, Survey Monkey displays a promotional page instead. You may not want your users to see this page. Unfortunately, portlets cannot be assigned and removed on a user-by-user basis. The only workaround viable at the moment is that the user themselves removes the portlet from their dashboard, or for the administrator to wait until the survey closes and then remove the portlet from the portal.

An enhanced version of the Survey Monkey portlet will be presented in the next article, with better control on exactly this scenario when a user has completed the survey.

Regards,
Stefano

Monday, 29 July 2013

A better In-Progress portlet


The In-Progress Learning Activity portlet that is available with Saba 5.x offers a compact view of your recent registrations to offerings and plans, but it lacks grouping courses by certification or curriculum when a registration is made for an offering of a course that is part of a certification or curriculum. Basically, the list of In-Progress activities displayed in the portlet is flat and sorted by date or alphabetically, as in the following picture.



But what if our training framework is strongly based on certifications for regulatory compliance, for example, and listing courses without a structure is simply not good enough? Or simply courses that form a more complex module have titles like a generic “Etiquette” that taken outside of its context (i.e. the certification) mean little to the student? What we want, in short, it’s something like the portlet in the next picture, a structured visualisation of course registrations.




A custom brand new portlet

How to turn the standard In-Progress portlet into the structured one, then? The answer is, guess what, to customise the original portlet and introduce grouping by retrieving the full structure of certification / path / module and courses.

The layout of a portlet is controlled by a manager (a Java class) and a set of Saba pages. The manager class for the In-Progress portlet is called InProgressLearningActivitiesPortletManager, and you can find it in the com.saba.client.learning.blendedenrollment package of the Saba’s Java Application Archive (JAR).

Customising the manager class for the new portlet layout is very simple, as the full portlet framework is offered “for free” by the Saba API. What we need to do in our new Java class, which for convenience I’m going to call InProgressLearningActivitiesPortletManager2, is to extend the abstract manager AbstractPortletPageManager and implement the interface PortletPageManager. Then, simply define the init() method and register the Saba controller page (the one with .rdf extension) that is used to display the new portlet.

public class InProgressLearningActivitiesPortletManager2
   extends AbstractPortletPageManager
   implements PortletPageManager
{
   protected void init()
      throws SabaException
   {
      registerPage("showDefaultDisplay",
         "/learning/portlet/inProgressLearningActivities2.rdf");
   }
}

In the example above, the new controller page registered by the manager class is called inProgressLearningActivities2.rdf, and, as you can guess, it’s a copy of the inProgressLearningActivities.rdf page available out of the box in the system, with the necessary changes to reference the new portlet structure, as described in the next section.


Creating the structure

The controller page, in a pure MVC (Model – View – Controller) pattern, defines which page contains the model (i.e. the logic to retrieve data from the database and the list of widgets used for displaying it on the screen), which page is the view (i.e. the layout of all defined widgets to display), and the model object (i.e. the Java class with the actual business logic).

This is an example of the inProgressLearningActivities2.rdf used for defining the new custom In-Progress portlet. Particularly relevant are the <wdk:model> tag that references the new model page with .xml extension, the <wdk:view> tag that references the new view page with .xsl extension, and the <wdk:modelObject> tag that references the fully qualified name of the model Java class.

<?xml version="1.0" encoding="UTF-8"?>
<?cocoon-process type="portlet"?>
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:wdk="http://www.saba.com/XML/WDK">
   <rdf:Description id="41078" pageCategory="/learning/portlet">
      <rdf:type resource="http://www.saba.com/XML/WDK/Control"/>
      <wdk:version>1.0</wdk:version>
      <wdk:model rdf:resource="inProgressLearningActivities2.xml"/>
      <wdk:view rdf:resource="inProgressLearningActivities2.xsl"/>
      <wdk:widgets rdf:resource="/wdk/xsl/widget/wdk_widgets.xsl"/>
      <wdk:modelObject>com.saba.client.learning.blendedenrollment. InProgressLearningActivitiesModel2</wdk:modelObject>
      <wdk:links> ... </wdk:links>
   </rdf:Description>
</rdf:RDF>

Basically, we have a new set of Saba pages that defines the layout of the In-Progress portlet v2, a new portlet manager and a new model class. Where and how do we define the structure, then? That’s pretty simple: the structure of courses grouped by certification or curriculum (together: by education plan) is defined in the model class, and the layout of how they show up on the screen within the portlet is defined in the Table widget used by the Saba model and view pages to represent data in a tabular format.

Starting from the former point (the Table widget), its definition is in the model .xml page. The trick here is to receive some piece of information from the model class that tells me that the specific course being displayed in the portlet belongs to a certification or a curriculum. If so, then indent its visualisation on the right.

<wdk:table name="simpleTable">
   <row path="InProgressLearningItems/LearningItem">
      <column>
         <xsp:logic>
         // If the course is part of a certification or curriculum, indent the title in the display
         if (WDKDomUtils.getNodeTextValue(wdkwidgetNode, "isInEducationPlan", wdkWidgetMaster.getXPathCache()).equals("true")) {
         </xsp:logic>
            <span class="indent">&#xa0;</span>
         <xsp:logic>
         }
         </xsp:logic>

You can read the code above in this way:

  1. Define the Table widget that displays course registrations in the portlet.
  2. For each row, the first column of the table contains a conditional logic depending on the value of the isInEducationPlan variable; this variable is defined in the model class (more later) and assumes value “true” if the course is part of a certification or curriculum, otherwise it is “false”.
  3. If true, add a <span> tag to the output for indenting the title of the course as it visually shows as belonging to a certification or curriculum. MVC-purely speaking, the HTML layout should be defined in the view .xsl page, but limitations in the Saba’s implementation of the MVC pattern allow for HTML code to be specified in the model page instead (purists are horrifying here now…).



The portlet model

Now that we have defined where to visualise the certification and course structure, we need to prepare this structure and assign the correct value to the isInEducationPlan variable. This variable is the key for building the structured layout; as said, when its value is “true”, which happens only for courses that belong to a certification, the portlet will display the course title indented under the certification itself. When “false”, the record to display is a certification itself (hence, no indenting is required) or is a loose course not part of any education plan.

Adding the isInEducationPlan variable to the XML output that the model class transfers to the model page is a matter of “visiting” a new <isInEducationPlan> tag for each processed record. If the record is a course and it belongs to an education plan (i.e. either a certification or a curriculum), the value of isInEducationPlan is true, else it is false. It sounds complicated, and… it is :-). But let’s try to make it simple by first looking at the implementation code below. A few pointers: a model class should extend the abstract class AbstractModelObject and implement the doExecute() method. This method is invoked by the model page by passing the HTTP servlet request, which represents the context where the page operates, and an XML visitor, which is used to build the output information returned to the model page. Adding a new XML tag to the output is simply a matter of visiting a new tag and writing its value, by using the visit() method of the visitor object.

public class InProgressLearningActivitiesModel2
   extends AbstractModelObject
{
   public void doExecute(HttpServletRequest req, IXMLVisitor visitor)
   {
      BlendedEnrollmentViewUtil util = new BlendedEnrollmentViewUtil();
      Collection items = util.getBlendedViewItems();
      Iterator itemIter = items.iterator();
 
      while (itemIter.hasNext()) {
         BlendedAbstractDetail itemDetail = (BlendedAbstractDetail)
         itemIter.next();
 
         // item is a course and belongs to an education plan
         if (itemDetail instanceof BlendedEnrollmentDetail &&
            isInEducationPlan(itemDetail)) {
            visitor.visit(null, "isInEducationPlan", "true");
         }
         else {
            visitor.visit(null, "isInEducationPlan", "false");
         }
      }
   }
}

Read the code above as follows:

  1. The BlendedEnrollmentViewUtil class provides a utility for retrieving In-Progress registrations for a given user as a collection of BlendedAbstractDetail objects.
  2. The code iterates over the collection and if the current item is an instance of the BlendedEnrollmentDetail object, i.e. it’s a registration to an offering for a course AND the course is part of an education plan, it creates an isInEducationPlan tag and assigns the value “true”.
  3. Otherwise, for any other item type and for courses that are not part of an education plan, the isInEducationPlan tag contains the value “false”.



Adding the new portlet to the platform

To add the new portlet to the system, simply follow these steps:

  1. In System Administration > General Configuration > Portlets, add a new portlet. The important parameter to set is PageManagerClass, which should be the fully qualified name (i.e. including the package name) of the personalised portlet manager Java class.
  2. From the Portals page, add the newly created portlet to the “mysaba” portal (this is the default portal for learners that is displayed after log in).



All done, enjoy it!

Lijepi pozdrav,
Stefano