Human Readable Dates in XPages

If I display a date/time in a view I can enable the option “show ‘today’ if appropriate”. This will replace todays (or yesterdays) date with the text “Today” (or “Yesterday”). I wanted to have the same functionality in an Xpage-based application I’m creating, but couldn’t find the XPage equivalent.

After some searching on the web I found an article written by Matt White (Human Readable dates in XPages) in which he kind-of did the same: the SSJS library he created allows you to convert dates to a “time since” string (e.g. “2 hours ago”, “6 days ago”). Since what I wanted is also another way of displaying dates I decided to extend his class (and rename it while I’m at it). You’ll find the resulting class this (compressed) text file: xpDates.zip.

Usage instructions

Download the attachment and paste the code in an SSJS script library. Add the library as a resource to the Xpage(s) you’ll want to use the new date format on.

There are 2 ways to convert dates (example for a document with a “modifiedDate” date/time field)

1. use SSJS to compute and convert a value:

var myDateValue = document1.getItemValueDate("modifiedDate");

var d = new ReadableDate();
d.toReadable(myDateValue);

or:

var d = new ReadableDate();
d.timeSince(myDateValue);

The first method will give you dates formatted like “Today 9:10”, “Yesterday 3:15”, “24 July 2:10”, the second will display the time since the specified date.

2. Bind a control to the date/time value/ field, set the display type to “String” and add a custom converter:

  • type= xp:customConverter
  • getAsObject: just enter “value” (javascript, without quotes)
  • getAsString: enter “ReadableDateConverter.getAsString()” (javascript, without quotes)

The ReadableDate class currently has 3 configuration options:

  • includeTime : will include the time when displaying a date.
  • includeSeconds : includes seconds when displaying the time.
  • includeYearIfCurrent : if true the year is always added, if false the year is only added if not the current year.

Since my app has a Dutch audience, I’ve added Dutch language strings to the class.

Beware of what getColumnValue is returning

When working with view columns that might contain multiple values, remember that the return value of the getColumnValue(“column”) method from the NotesXspViewEntry object is different depending on the number of values in the column of a specific view entry:

  • If the view entry contains a single value, a string is returned.
  • If the view entry contains multiple values, it returns a Vector.
If you always want to have a Vector as the return type, you’ll have to convert it:
var values = rowData.getColumnValue("MultiValueColumn");
if ( (typeof values).toLowerCase() == "string" ) {
 var vTmp = new java.util.Vector();
 vTmp.add(values);
 values = vTmp;
}
Converting the return type is also a possible workaround when working with nested repeat controls: if you set the Iteration to yourRowData.getColumnValue(“MultiValueColumn”) the repeat control won’t work with rows containing a single value.

Using OpenLog for logging/ debugging XPages

It probably won’t make your admins happy if you constantly use print(“this”) in you Server-Side JavaScript code to print debug or error messages to the server’s console. Luckily there is a good alternative: use OpenNTF’s OpenLog project to log everything to a central database. The only problem is that the database doesn’t contain a library for Xpages (yet).

After some digging I found out that the TaskJam app from Elguji Software does have a SSJS library to be able to log messages from to the OpenLog database. To use it:

  • Download the OpenLog database and copy it to your server.
  • Download TaskJam.
  • Copy the OpenLogXPages script library from the TaskJam template to your XPage application.
  • Open your copy of the OpenLogXPages script library and update the variable “logDbPath” to reflect the path and location of your OpenLog database.
  • The OpenLogXPages script library contains code specifically for TaskJam that tries to retrieve the location of the log database from a view called vwControlPanel: disable that code.
  • On two places in the script library a variable called location is set to session.getURL(); if not specified. This is used in the log entries to show were the message came from. Since that property doesn’t always return the correct location, I”ve changed it to view.getPageName();
  • Add the OpenLogXpages script library to every page you want to debug/ log information. You can do this by adding a “resource” to basic/ resources property of your XPage or Custom Control (clientSide = false, location = /OpenLogXPages.jss). I’ve added the library to a Custom Control that is loaded on every XPage.

You’re now able to log events or error messages in your SSJS code by using:

log.logEvent( "Hello world" );
log.logError( e.toString(), null, e );

Since you’ve already downloaded TaskJam: don’t forget to have a look at it. It contains some good examples of how to use XPages.

Clientside onLoad events on Xpages

I wanted to set the focus (client side) on an input field in my Xpage. This required:
  • adding a function to the onLoad event
  • referencing the (generated) id of my editable field: in the Xpage the element is named “userName”.

As I found out, there are a couple of ways to add clientside javascript to your Xpage (the dojo.addOnLoad function I’m using defers execution of the code until all HTML is loaded, so it doesn’t matter where you place your code):

  • Add the code directly to the XPage source:
    <script type="text/javascript">
    dojo.addOnLoad( function() {
     alert("added directly in source");
    } );
    </script>
  • Add a Computed Field to your Xpage, set its Content type to “HTML” and enter a value that evaluates to a string, including the <script> tags:
    "<script type="text/javascript">" +
    "dojo.addOnLoad( function() {" +
    " alert("computed field");"
    "} );"
    "</script>"

    The value if the computed field is computed on the server, so if I want to get the generated ID of the Editable field, I have to use the getClientId() function:

    "<script type="text/javascript">" +
    "dojo.addOnLoad( function() {" +
    " alert("The id of the element is: " + getClientId("userName") + "");" +
    "} );" +
    "</script>"
  • Add an “Output script” core control to the Xpage and edit its value. This control is rendered in the browser with <script></script> tags (if “Output script” isn’t available in the menu you have to enable it in the Designer preferences through File – Preferences – Domino Designer – Palette – Core Controls):
    dojo.addOnLoad( function() {
        alert("Output script control");
    } );

    The value of the Output script is passed to the browser as clientside Javascript, but is first evaluated by the server. So if I wanted to reference a clientside document ID, I’d have to use the “#{id:userName}” syntax:

    dojo.addOnLoad( function() {
        var id = "#{id:userName}";
        alert("The id of the element is: " + id);
    } );

Since the first method didn’t allow me to reference the generated ID of the Editable field and the second method required me to escape and wrap everything in quotes I went with the third method. The final code in the Output script element is now:

dojo.addOnLoad( function() {
 var objFocus = dojo.byId("#{id:userName}");
 if (objFocus) {
   if (!objFocus.disabled &amp;&amp; objFocus != "hidden") {
     objFocus.focus();
   }
 }
 } );

To set the focus only if a new document is being edited, I’ve added the following formula to the “rendered” property of the Output script control:

currentDocument.isEditable() &amp;&amp; currentDocument.isNewNote();

XPages “Change document mode” not working (and why)

I just tried to add a “Change Document Mode” action on an XPage to be able to switch between read- and edit mode. The XPage (containing the document) opened fine in read mode, but after clicking the “Edit”  button, nothing happened. At first I thought it was trying to put the wrong data source in edit mode or that it had something to do with using multiple custom controls. A simple test with a new Xpage showed that this wasn’t the case. I then tried other things: changing the view control that showed all documents to open the document directly in edit mode, adding an ?action=editDocument parameter to the URL: all with no success.

Then it occurred to me: is the user allowed to edit the document at all? He wasn’t… Back in the days the Domino server used to tell you “you’re not allowed to perform that operation”, but that doesn’t seem to happen anymore: the document just stays in read mode.

The solution was to show the “edit” button only if the user is actually allowed to edit the document. To do that I wrote a function for the Visible property of the “edit” button that checks if the user is allowed to edit a document and hides it if he isn’t:

  • It first checks the access level to the parent database: editors or higher are allowed to edit, readers or lower are not.
  • If the user is an author it checks a field called docAuthors (which I use to store document authors in): if that field contains the current user’s username or one of its roles the user is allowed to edit.
Here’s the function:
function isAuthor( doc:NotesDocument ) {

    var level = doc.getParentDatabase().getCurrentAccessLevel();
        
    if (level >= 4) {
        return true;        //editor or higher
    } else if (level < 3) {
        return false;        //reader or lower
    } else {    //author
        
        var authors = doc.getItemValue("docAuthors");        //field containing all document authors
        if (authors === null) { return false; }        //no authors field present
        
        var userName = @UserName().toLowerCase();
        var roles = context.getUser().getRoles();  

        for (var i=0; i<authors.length; i++) {
            if (authors[i].substring(0,1) == "[") {        //role
                for (var j=0; j<roles.length; j++) {
                    if (authors[i].toLowerCase() == roles[j].toLowerCase()) {        //on of user's roles is in authors field
                        return true;
                    }
                }
                
            } else if (authors[i].toLowerCase() === userName ) {    //username matches one of the values in the authors field
                return true;
            }
        }
    }  

    return false;
} 

UPDATE 1 Serdar Basegmez came up with a Java version for this function. Since he’s from Turkey it will probably even work for users with a dotless-i in their name 🙂

 @SuppressWarnings("unchecked")
public static boolean isAuthor(Document doc, String authorField) {
               
  try {
    Database db=doc.getParentDatabase();
    Session session=db.getParent();
                       
    int level = db.getCurrentAccessLevel();
                             
    if (level >= 4) {
      return true;        //editor or higher
    } else if (level < 3) {
      return false;        //reader or lower
    } else {    //author
   
      Vector<String> authors=doc.getItemValue(authorField);
 
      if (null==authors || authors.isEmpty()) { return false; }        //no authors field present
                         
      Vector<String> checkList=session.evaluate("@UserNamesList");
                           
      for(int i=0; i<checkList.size(); i++) {
        String checkStr=checkList.get(i);
        checkList.set(i, checkStr.toLowerCase(Locale.ENGLISH));
      }
     
      for (int i=0; i<authors.size(); i++) {
        String checkStr=authors.get(i);
        if(checkList.contains(checkStr.toLowerCase(Locale.ENGLISH))) {
          return true;
        }
      }
    }
  } catch (NotesException e) {
    // Nothing to do
  }
         
  return false;
}

UPDATE 2: Philipp Bauer mentioned that you can also check if the current user is allowed to edit a document with the built-in isDocEditable() function:

import com.ibm.domino.xsp.module.nsf.NotesContext;
import lotus.domino.NotesException;

public static boolean isAuthor(Document doc) throws NotesException {
   NotesContext localNotesContext = NotesContext.getCurrent();
   return localNotesContext.isDocEditable(doc);
}