Star rating in the Grails RichUI plugin

I’ve been working on a Grails application for rating the popularity of Grails plugins.  Rather than just use a simple form with radio buttons or a drop-down list, I thought I’d use the star rating component of the RichUI plugin.

The initial set-up is easy enough.  First, install the RichUI plugin.

grails install-plugin richui

then go into any GSP page where you want to use star rating and add

<resource:rating />

and after that it’s just a question of using the <g:render /> tag appropriately.

Mine looks like this:


<g:each in="${pluginList}" var="plugin">
   ...
   <g:render template="rate"
      model='[plugin: plugin, rating: "${plugin.rating}"]' />

along with some other stuff.

The documentation says to add a RatingController which computes the new rate.  I considered just adding the rating method to my PluginController, but eventually decided to keep it separate.  My RatingController looks like


class RatingController {
    def rate = {
        def rating = params.rating
        def plugin = Plugin.get( params.id )
        def average = (rating.toDouble() + 
            plugin.rating*plugin.totalVotes)/
                (plugin.totalVotes + 1)
        plugin.rating = average
        plugin.totalVotes += 1
        plugin.save()
        session.voted[plugin.name] = true
        render(template: "/plugin/rate", 
            model: [plugin: plugin, rating: average])
    }
}

That’s all pretty much taken from the sample, except for the session.voted[...] business.  I’ll come back to that in a moment.

The template I’m using is called _rate.gsp in the plugin view folder.  It consists of


<div id="plugin${plugin.id}">
    <% def dynamic = !session.voted[plugin.name] %>
    <richui:rating dynamic="${dynamic.toString()}" id="${plugin.id}" units="5"
        rating="${rating}" updateId="plugin${plugin.id}" controller="rating" action="rate"  />
    <p class="static">
        Rating ${java.text.NumberFormat.instance.format(plugin.rating)}
        based on ${plugin.totalVotes} vote<g:if test="${plugin.totalVotes != 1}">s</g:if>
    </p>
    <g:if test="${!dynamic}">
        <div style="color: green;" id="vote${plugin.id}">Thanks for voting!</div>
    </g:if>
</div>

And here we see some good stuff.  First, when you click on the star rating as it comes “out of the box”, it only updates the average value, which it optionally displays.  I wanted to show the number of total votes, too.  As it turns out, there’s an excellent blog post by Jan Sokol that deals with exactly this problem.  See the blog post for details, but essentially it involves changing update ID to that for a div wrapper, which allows you to update a whole section instead.

The <richui:rating> tag has an attribute called dynamic which determines whether you can vote or not.  If dynamic is false, you get a (naturally enough) static view.  If dynamic is true, you can mouseover the stars, highlighting them as you go, and then click to vote.

In most applications I’ve seen that use the star rating, you have to register and login in order to be able to rate anything.  I now believe that’s so they can render the rating tag as dynamic when you enter and then change it to static when you click.  The state is then kept in a user table, which remembers whether you’ve already voted or not.

My problem, though, is that I wanted to let anyone vote without having to register (a decision I’m currently reviewing).  So the question is, how do I keep a person from just voting over and over again?

I tried a couple of ideas, like a toggle or putting something in the page, but if someone browsed to a different page and came back they could vote again.  In the end, I found that I can use the session, even though nobody has logged in!

(That may be obvious to you, but even after teaching server-side Java courses for years now, I guess it never really hit me that a session exists even if you’re not logged in.  Whenever I visited a site with a shopping cart, I always had to log in.  It turns out that was only to buy the products, not to have a session at all. I guess in retrospect it seems obvious, but I never really thought about it until now.)

Anyway, that left me with the question of how to use the session for 90-some different plugins.  I decided to use a boolean array, where the index was the plugin name and the value was true if the person had voted and false otherwise.  As you can see from the RatingController code above, whenever anyone votes, I simply go

session.voted[plugin.name] = true

and I’m all set, because the rating template has

<% def dynamic = !session.voted[plugin.name] %>

and

<richui:rating dynamic="${dynamic.toString()}" ... />

in it.

The only remaining question was how to initialize that array for the session.  I decided that was a classic application of an interceptor, so in my controller for the plugins themselves, I have


def beforeInterceptor = {
    if (!session || !session.voted) {
        def voted = [:]
        def names = Plugin.list().collect { it.name }
        names.each { name ->
            voted[name] = false
        }
        session.voted = voted
   }
}

and there you have it.  Of course, it’s better if you test, and while I still struggle with that, I was able to come up with something effective in this case.  Here’s my PluginControllerTests class, which is, of course, an integration test.  (There’s no doubt a way to make it a unit test instead, but hey, at least it’s tested.)


class PluginControllerTests extends GroovyTestCase {
    def pc

    void setUp() {
        pc = new PluginController()
    }

    void testNoVotedArrayBeforeIntercepting() {
        assertNull pc.session?.voted
    }
   
    void testVotedArrayExistsAfterIntercepting() {
        pc.beforeInterceptor()
        assertNotNull pc.session
        assertNotNull pc.session.voted
    }

    void testVotedArrayHasAcegi() {
        pc.beforeInterceptor()
        assertFalse pc.session.voted['acegi']
   }

   void tearDown() {
        pc = null
    }
}

So it all works, at least so far.  Soon I’ll be able to deploy it, but now I’m still fighting with CSS styles.  Whoever thought doing layout with CSS was a good idea has some explaining to do.

Incidentally, anyone who has used WordPress for blogging knows the frustrations of trying to format code in it. I think there are plugins available to make it easier, but I’m not running WordPress myself — I’m letting them host my blog. The bottom line, therefore, is that sometimes it’s really hard to read code that is posted here. I think, when my app is finished, I’ll follow the recent trend and upload it to github. If I do that, I’ll be sure to mention it here.

3 responses to “Star rating in the Grails RichUI plugin”

  1. Cool post, maybe you dream fof writters?
    ___________________________________

    Sry, hehe))

  2. hi!
    thanks for your comments.
    Only a couple of comments. Although it is trivial
    you should mention that you need to add some attributes to the class plugin like:

    Float rating = 0
    Integer totalVotes = 0

    If you are working with legacy code -I mean if the DB already exists when you add this feature- you have to update the DB with some non-null values (update plugin set rating=0, total_votes=0) otherwise you are going to get an exception when calculating the average. In fact would be better to enclose the average calculation with a try / catch block.

    Your hints have been very helpfully for me.
    Thanks a lot!

    Luis

  3. Thanks for the post. I was looking for the logic to compute average rating (just didn’t want to use my own brain I guess) and found your blog.

    Can you tell me why you decided to create a separate controller for Rating? I am using an existing controller but wanted to know your reason.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.