Friday, July 31, 2009

Add Document Ratings To Your Notes Client Designs

Way back when I was actively developing stuff on a daily basis, I wrote about a quickly cobbled together idea I had to include a document rating system in a Notes client application (might want to read that first). Document Rating is a familiar "Web 2.0" design pattern that can actually be quite useful. Document ratings allow users to democratically decide on good content and by using this technique the best stuff will bubble up to the top (at least theoretically). In the original post, I wanted to explore the concept of using editable view columns with icons in a little more detail while showing off some cool functionality in the client. With the frequent use of this design pattern in modern websites, I think it is safe to assume that it is a pattern that is around to stay. Thus, I wanted to make the solution a little more robust. As with most of the things I put here on Interface Matters, I've removed a lot of the extraneous code that you really should use in production so that you can focus on the core of the technique, but the sample database below should be enough to get you started.

Before we begin going through the technique, I suggest you read through the Rate Content design pattern page on UI That page should give you a little more detail on what I am shooting for. For our purposes, let's focus on the following mechanisms:

* Voting mechanism.
* Display the average rating an item has received.
* Display explanatory comments from users rating an item.

I've left off the three mechanisms below, but they seem like a perfect fit for dashboard-like functionality.

* Show the highest rated items.
* Favor quality items.
* Related items.

In the original post, I wasn't taking into account security on the document that is being rated. Thus, this technique really wouldn't work for most real world situations. To overcome this, I decided to use the concept of stub documents to capture each user's rating of a particular document. This offers several advantages. First, I can easily perform a lookup to see if the user has already rated a given document or not. Second, it allows me to extend the original functionality by allowing for additional information such as comments. Finally, this idea respects the security of the main document, as the average rating is calculated by an agent so that the user never has to have edit access to it. Thus, the flow of the process goes something like this:

1. User clicks a star to define their rating.

2. If desired, pop up a dialog box to capture their comments about why they rated the document as they did.

3. Write a stub document that captures a unique key value that corresponds to main document and user, their rating and comments.

4. Run an agent (that executes with proper authority) to update the main document with the new average rating. This agent will loop through all of its rating documents to determine the average.

At the risk of making this post too long, I am going to break this down step-by-step:

Step 1: Add columns to your view. I created a separate column for each star, since I want to know explicitly which one was clicked. Set the column to be editable and set it to "Display values as icons".

Step 2: The column value checks the current average rating of the document and based on the position of the column determines if it should show the filled or unfilled star.

@If(num_Rating >= 1; "star_red"; "star_open") and so on...

Step 3: Add the code to the InViewEdit event. I've included a bunch of comments to the code so you can follow what is happening. It's here that we first check to see if the user has rated the document previously and decide to allow them to edit that rating or not. I chose to implement this as a flag that you can enable or disable in the code itself for easy demonstration, but you could make this and some of the other options part of the db configuration. If the user hasn't voted yet, the InViewEdit code creates the stub doc, asks them for a comment (if appropriate) and kicks off the update agent.

(Please note I added some line breaks into the code samples. If you want to copy and paste, do so from the sample database)

Sub Inviewedit(Source As Notesuiview, Requesttype As Integer, Colprogname As Variant,
Columnvalue As Variant, Continue As Variant)

Dim workspace As New NotesUIWorkspace
Dim session As New NotesSession
Dim db As NotesDatabase
Dim doc As NotesDocument
Dim caret As String
Dim flag As Boolean

'Set AllowChanges to 0 to keep users from changing their rating. If set to 1, then can modify their initial value
Const AllowChanges = 1

'Get the CaretNoteID - exit if it does not point at a document
caret = Source.CaretNoteID
If caret = "0" Then Exit Sub

'Get the current database and document
Set db = Source.View.Parent
Set doc = db.GetDocumentByID(caret)

Dim ratingsView As NotesView
Dim ratingsDoc As NotesDocument
Dim DocUNID As String
Dim key As String

'We need to check to make sure the user hasn't already rated this document
Set ratingsView = db.GetView("viewRatingsByKey")
DocUNID = doc.UniversalID
key = DocUNID & "_" & session.CommonUserName
Set ratingsDoc = ratingsView.GetDocumentByKey(key)

If Not (ratingsDoc Is Nothing) Then
If AllowChanges = 1 Then
flag = workspace.DialogBox("dlgRatingsCommentDialog", True, True, True, True, False, False,
"Enter A Comment About Your Rating"
, ratingsDoc, True, True, False)
If flag = True Then
Call ratingsDoc.Save(True, False)
Call RunTheAgent(db, doc) 'Call the agent to update the main document with the new rating value
End If
Messagebox "'ve already rated this document"
End If
Exit Sub
End If

'If we got this far, the user hasn't voted yet, so we'll take their entry. Here we are creating the new stub document.
'This document uses a combination of the main document's UNID and the user name for the key value. This
'stub document could capture as much information as you need...just add the appropriate fields. If you don't
'ever plan to show the underlying content to the end users, you don't need an actual backend form.
Dim newRatingsDoc As NotesDocument
Dim item As NotesItem
Dim readersItem As NotesItem
Dim newValues( 1 To 2 ) As String
newValues( 1 ) = session.UserName
newValues( 2 ) = "[Admin]"

Set newRatingsDoc = db.CreateDocument
newRatingsDoc.Form = "frmRatingsDoc"
newRatingsDoc.txt_TargetUNID = DocUNID
newRatingsDoc.txt_RatingComment = ""
newRatingsDoc.txt_Key = DocUNID & "_" & session.CommonUserName
Set item = New NotesItem(newRatingsDoc, "nam_UserRated", session.UserName, NAMES)

'To keep ratings private, uncomment the line below. But if you do this, probably no use asking for comments.
'Set readersitem = New NotesItem(newRatingsDoc, "read_Users", newValues, READERS)

Select Case Colprogname(0)
Case "Star1"
newRatingsDoc.num_Rating = 1
Case "Star2"
newRatingsDoc.num_Rating = 2
Case "Star3"
newRatingsDoc.num_Rating = 3
Case "Star4"
newRatingsDoc.num_Rating = 4
Case "Star5"
newRatingsDoc.num_Rating = 5
End Select

'Modify the code for comments as necessary to suit your needs

'Now lets see if the user wants to add a comment
Call ratingsView.Refresh

'Option 1 - Use this if you want to pull up the comment dialog but don't require a comment.
'Call workspace.DialogBox("dlgRatingsCommentDialog", True, True, True, False, False, False,
"Enter A Comment About Your Rating", newRatingsDoc, True, True, False)

'Call newRatingsDoc.Save(True, False)

'Option 2 - Use this if you want to require comments before a rating can be saved.
flag = workspace.DialogBox("dlgRatingsCommentDialog", True, True, True, False, False, False,
"Enter A Comment About Your Rating"
, newRatingsDoc, True, True, False)
If flag = True Then
Call newRatingsDoc.Save(True, False)
End If

'Call the agent to update the main document with the new rating value
Call RunTheAgent(db, doc)

End Sub

Sub RunTheAgent(db As NotesDatabase, doc As NotesDocument)

'Call the agent to update the main document with the new rating value
Dim agent As NotesAgent

Set agent = db.GetAgent("agtCalculateRatings")
Call agent.RunOnServer(doc.NoteID)

End Sub

This LotusScript was converted to HTML using the ls2html routine,
provided by Julian Robichaux at

Step 4: Add the agent that updates the main document with the average rating. This agent is called by the InViewEdit code as described above. This agent will take the main document, find all of the stub rating documents based on the UNID of the main doc and then loop through the collection, simply adding the values of the ratings and then dividing by the number of ratings to get the average. This average value is then written back to the main document. Make sure that this agent is set to run with an id that has access to all of the main documents, not as the current user.

Sub Initialize

Dim session As New NotesSession
Dim db As NotesDatabase
Dim agent As NotesAgent
Dim ratingsView As NotesView
Dim mainDoc As NotesDocument
Dim ratingsDoc As NotesDocument
Dim ratingsCollection As NotesDocumentCollection

Set db = session.CurrentDatabase
Set agent = session.CurrentAgent
Set ratingsView = db.GetView("viewRatingsByUNID")
Set mainDoc = db.GetDocumentByID(agent.ParameterDocID)
Set ratingsCollection = ratingsView.GetAllDocumentsByKey(mainDoc.UniversalID)

If (ratingsCollection.Count = 0) Then
'Whoops...something went wrong and you'd do some good error trapping here
Exit Sub
End If

Dim ratingsCounter As Long
Dim numberOfRatings As Long
Dim averageRating As Long

ratingsCounter = 0
numberOfRatings = ratingsCollection.Count
Set ratingsDoc = ratingsCollection.GetFirstDocument

'Let's add up the total of all ratings first
Do While Not (ratingsDoc Is Nothing)
ratingsCounter = ratingsCounter + ratingsDoc.num_Rating(0)
Set ratingsDoc = ratingsCollection.GetNextDocument(ratingsDoc)

'Now, we average the ratings by taking the total and dividing by the number of votes
(assuming all have the same weight)

averageRating = ratingsCounter / numberOfRatings

'And now we can set this on the main document and be on our way
mainDoc.num_Rating = averageRating
Call mainDoc.Save(True, False)

End Sub

This LotusScript was converted to HTML using the ls2html routine,
provided by Julian Robichaux at

There are a lot of ways in which you could flush this technique out even more. For example, I added an embedded view to the main document form that is used to show the stub document ratings. This way, when you open a particular content document, you can see in detail who rated the content and why (via their comments). This is a nice way to provide users with real meaning behind the values. I'd be interested in hearing about other ways you might use this functionality as well.

The sample database is available below. You've likely seen this before, as it contains a variety of sample design patterns, some of which I haven't talked about yet. You can switch to the "User Ratings" view by using the selection box in the lower right corner of the main screen. Yes...this database is a bit ugly. I was playing with some different theme ideas and didn't want to make another boring looking Notes app. ;-)

Have fun, let me know what you think and please share with the others if you find a use for this in your company's applications.


Chris Toohey said...

Outstanding work, sir. Great blend of UI functionality and backend data manipulation.

Richard Moy said...


Good job. Are you demoing this in the conference?

Roy Rumaner said...

As always Chris, your code and screen designs continue to excel.

This is a really cool example of what can be accomplished with some serious thought and a lot of patience.

Thank you for sharing the code - I think I might be able to make use of it in an application that I am going to doing shortly.