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])
}
}
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.
Leave a Reply