Saba Learning 5.x has a portlet for In-Progress activities, which I also have enhanced as described in this article to show the certification structure. However, there is no Transcript portlet for a quick list of your completed courses, with the possibility to re-launch content with just one click. Quickly accessible online content, even for completed courses, is a major commodity for reference and for just a refresh, so why "burying" this functionality when we can make it available on the front page of each student?
Among the widgets, a <wdk:table> displays data in a table format. The <wdktags:attachTo> indicates where, in the XML model, to go and find the data to display (i.e. which is the root node of the XML document generated by the command), and the <wdktags:nodeRef> tags retrieve the value for the indicated node.
We also need XML node values in the <xsp:logic> section, as we need to decide whether the portlet should display the Launch or the Details link. Some simple Java code can be embedded in the model page (do not abuse of it, business logic should be always implemented within Java classes). Based on the values of the HasContentModules node (true or false), MaxAttempts and LearnerAttempts, either link is displayed.
The WDK tag for defining a link widget is <wdk:frameworkLink>, which contains a reference to the model page to invoke when clicking on it, in the <mainPage> tag, and one or more parameters to pass to that page, as defined in the <field> tag.
That's everything you need for creating a Transcript portlet. Hope you find it useful!
The Transcript portlet in three steps
Building a brand new Transcript portlet is as easy as an ABC... or should I say as an MCM?- The Manager, responsible for defining the controller page that displays the information in the portlet.
- The Command, responsible for retrieving information from the database and presenting it in XML format.
- The Model (and View and Controller), which ultimately renders the XML information in a portlet on the screen.
If you have followed my past tutorials on how to create a custom portlet, you should be already familiar with these steps. In this post, I will spend more time describing the business logic for implementing the following requirements for the Transcript portlet:
- The portlet should display a list of completed courses, from the most recent to the oldest.
- For each entry, Title, Completion Date and an Action link should be displayed.
- The Action link should be:
- Launch: If there are content modules defined for the offering; by clicking on this link, the user is able to open the first existing content.
- View Details: If there are no content modules defined; the user will be redirected to the details page of the offering.
- The number of completed courses displayed by the portlet should be limited to a specific value entered as a parameter in the portlet definition.
The Transcript Portlet
The Portlet Manager
The portlet manager class is TranscriptPortletManager. The overridden init() method registers the transcriptPortlet.rdf controller page and the NumberOfTranscripts parameter. That's it, job done!
public class TranscriptPortletManager
extends AbstractPortletPageManager
implements PortletPageManager
{
@Override
protected void init() throws SabaException
{
registerPage("showDefaultDisplay",
"/custom/portlets/transcriptPortlet.rdf");
ParameterDetail recordNo =
new ParameterDetail("NumberOfTranscripts",
TypeInfo.createStringType());
registerParameter(recordNo);
}
}
The Command
The purpose of the portlet command TranscriptPortletCommand is to build an XML representation of the information needed by the Model page for displaying transcript entries within the portlet. The information we need is summarised in the following XML structure, with one or more <Transcript> nodes for as many completed courses exists for the given user. All those *Id values for registration, offering, course, subscription, context, order and order item are required (don't ask why...) by the content launch and the offering details display pages, so I am making sure that I bring with me these pieces of information in my model.
<Result>
<Transcript>
<Id></Id>
<RegistrationId></RegistrationId>
<OfferingId></OfferingId>
<CourseId></CourseId
<SubscriptionId></SubscriptionId>
<ContextId></ContextId>
<OrderId></OrderId>
<OrderItemId></OrderItemId>
<Title></Title>
<CompletionDate></CompletionDate>
<HasContentModules></HasContentModules>
</Transcript>
</Result>
The Java code is a bit long here, but highly readable in its flow. Briefly, what it does is:
- Add the input parameter NumberOfTranscripts in the constructor.
- Read the value of the parameter in the doExecute() method and start visiting (aka building) the XML document.
- Retrieve a list of transcript using the Saba finder LearningFinder.kLearnerTranscriptFinder. This finder returns a collection of ArrayList objects, containing (most of) all the required information.
- The TranscriptEntry class represents the Java object of a transcript record, to be serialised in to the XML document. I have implemented attributes as public fields instead of having setters and getters for simplicity of use. Aah how much I miss C#-like properties in Java...
- The getTranscripts() method eventually sorts results by date using a custom comparator dateComparator and returns a subset of the result collection up to the indicated number of records to display.
public class TranscriptPortletCommand
extends SabaWebCommand
{
public TranscriptPortletCommand()
{
addInParam("NumberOfTranscripts", String.class, "...");
}
public void doExecute(HttpServletRequest request,
IXMLVisitor visitor)
IXMLVisitor visitor)
{
String recordStr = (String)getArg("NumberOfTranscripts");
int recordNum = 10; // default to 10
try {
recordNum = Integer.parseInt(recordStr);
} catch (NumberFormatException ex) {
...
} finally {
visitTranscript(visitor, recordNum);
}
}
private void visitTranscript(IXMLVisitor visitor,
int recordNum)
int recordNum)
{
visitor.beginVisit(null, "Result", null, null, null);
List<TranscriptEntry> results = getTranscripts(recordNum);
for (TranscriptEntry entry : results) {
visitor.beginVisit(null, "Transcript", null, null, null);
visitor.visit(null, "Id", entry.Id);
... // repeat for all the other attributes
visitor.endVisit(null, "Transcript");
}
visitor.endVisit(null, "Result");
}
private List<TranscriptEntry> getTranscripts(int recordNum)
{
List<TranscriptEntry> results =
new ArrayList<TranscriptEntry>();
new ArrayList<TranscriptEntry>();
addCompletedCourses(locator, results);
Collections.sort(results, dateComparator);
return results.size() > recordNum ?
results.subList(0, recordNum) :
results;
results.subList(0, recordNum) :
results;
}
private void addCompletedCourses(
List<TranscriptEntry> results)
List<TranscriptEntry> results)
{
ServiceLocator locator = getServiceLocator();
FinderManager finder = (FinderManager)
locator.getManager(Delegates.kFinder);
locator.getManager(Delegates.kFinder);
DriverData driverData = finder.getDefaultDriverData(
LearningFinder.kLearnerTranscriptFinder);
LearningFinder.kLearnerTranscriptFinder);
driverData.addCondition("learnerId",
FinderOperator.kEqual,
locator.getSabaPrincipal().getID());
FinderOperator.kEqual,
locator.getSabaPrincipal().getID());
FinderCollection collection = finder.findAll(
LearningFinder.kLearnerTranscriptFinder, driverData);
LearningFinder.kLearnerTranscriptFinder, driverData);
Iterator iter = collection.iterator();
while (iter.hasNext()) {
ArrayList list = (ArrayList)iter.next();
TranscriptEntry entry = new TranscriptEntry();
entry.Id = list.get(0).toString();
... // repeat for all the other attributes
results.add(entry);
}
}
private class TranscriptEntry
{
public String Id;
public String RegistrationId;
public String OfferingId;
public String CourseId;
public String SubscriptionId;
public String ContextId;
public String OrderId;
public String OrderItemId;
public String Title;
public Date CompletionDate;
public boolean HasContentModules;
}
private Comparator<TranscriptEntry> dateComparator =
new Comparator<TranscriptEntry>()
new Comparator<TranscriptEntry>()
{
public int compare(TranscriptEntry entry1,
TranscriptEntry entry2)
TranscriptEntry entry2)
{
// sort from most recent first
return entry2.CompletionDate
.compareTo(entry1.CompletionDate);
}
};
}
The Model
The transcriptPortlet.xml page represents the Model page. Along with the Controller (.rdf extension) and the View (.xsl extension), it is a key element of the Saba WDK for displaying information in a browser.
First of all, we need an instance of the TranscriptPortletCommand object, to use for executing the command via the <wdktags:execute> tag. It is important that this WDK tag is nested inside the <wdk:model> tag, as the output of the command object is an XML document representing the model.
<xsp:logic>
TranscriptPortletCommand command = new TranscriptPortletCommand();
</xsp:logic>
<wdk:model>
<wdktags:execute commandObj="command">
<param name="NumberOfTranscripts"
expr="NumberOfTranscripts" type="String" mode="in" />
expr="NumberOfTranscripts" type="String" mode="in" />
</wdktags:execute>
</wdk:model>
Among the widgets, a <wdk:table> displays data in a table format. The <wdktags:attachTo> indicates where, in the XML model, to go and find the data to display (i.e. which is the root node of the XML document generated by the command), and the <wdktags:nodeRef> tags retrieve the value for the indicated node.
We also need XML node values in the <xsp:logic> section, as we need to decide whether the portlet should display the Launch or the Details link. Some simple Java code can be embedded in the model page (do not abuse of it, business logic should be always implemented within Java classes). Based on the values of the HasContentModules node (true or false), MaxAttempts and LearnerAttempts, either link is displayed.
The WDK tag for defining a link widget is <wdk:frameworkLink>, which contains a reference to the model page to invoke when clicking on it, in the <mainPage> tag, and one or more parameters to pass to that page, as defined in the <field> tag.
<wdk:widgets>
<wdk:table name="transcript">
<wdktags:attachTo path="Result"/>
<row path="Transcript">
<column>
<wdktags:nodeRef path="Title"/>
</column>
<wdktags:nodeRef path="Title"/>
</column>
<column>
<wdktags:nodeRef path="CompletionDate"/></column>
<wdktags:nodeRef path="CompletionDate"/></column>
<column>
<xsp:logic>
String hasContentModules = WDKDomUtils
.getNodeTextValue(wdkwidgetNode, "HasContentModules", wdkWidgetMaster.getXPathCache());
.getNodeTextValue(wdkwidgetNode, "HasContentModules", wdkWidgetMaster.getXPathCache());
String maxAttemptsStr = WDKDomUtils
.getNodeTextValue(wdkwidgetNode, "MaxAttempts",
wdkWidgetMaster.getXPathCache());
.getNodeTextValue(wdkwidgetNode, "MaxAttempts",
wdkWidgetMaster.getXPathCache());
String learnerAttemptsStr = WDKDomUtils
.getNodeTextValue(wdkwidgetNode, "LearnerAttempts",
wdkWidgetMaster.getXPathCache());
.getNodeTextValue(wdkwidgetNode, "LearnerAttempts",
wdkWidgetMaster.getXPathCache());
int maxAttempts = Integer.parseInt(maxAttemptsStr);
int learnerAttempts =
Integer.parseInt(learnerAttemptsStr);
Integer.parseInt(learnerAttemptsStr);
boolean showLaunchLink =
hasContentModules.equals("true") &&
(maxAttempts == 0 ||
maxAttempts > learnerAttempts);
hasContentModules.equals("true") &&
(maxAttempts == 0 ||
maxAttempts > learnerAttempts);
if (showLaunchLink)
{
</xsp:logic>
<wdk:frameworkLink name="wbtLaunch">
<mainPage>learningOfferingDetails.xml</mainPage>
<field>
<name>offeringId</name>
<value>
<wdktags:nodeRef path="OfferingId"/>
</value>
</field>
...
...
</wdk:frameworkLink>
<xsp:logic>
}
else
{
</xsp:logic>
<wdk:frameworkLink name="offeringDetails">
...
...
</wdk:frameworkLink>
<xsp:logic>
}
</xsp:logic>
</row>
</wdk:table>
</wdk:widgets>
</row>
</wdk:table>
</wdk:widgets>
That's everything you need for creating a Transcript portlet. Hope you find it useful!