Managing Content through Tagging in Grails: Part 2

by Jon Dickinson | May 2009 | Java Open Source Web Development

In the previous part of the article by Jon Dickinson, we saw how to construct a domain model to allow files and messages to be tagged. In this part, we will see how to customize our home page and work with the templates and tags.

Customizing the home page in Grails

With tagging in place, we can enhance the application to allow users to create their own home page. The aim is to allow users to specify the tags they are interested in, so any content with these tags will be displayed on their home page. This will allow us to break the home page up into two sections:

  • A Most Recent section, containing the last five file uploads and messages
  • A Your Data section, containing all the files and messages that are tagged according to the user's preferences

Introducing templates

Taking this approach means that files and messages will be displayed in many different places on the site, instead of just the home page. By the end of this article, messages and files will be rendered in the context of:

  • A Most Recent section
  • A Your Data section

In the future, we will probably render messages and files in the following contexts as well:

  • Show all files and messages
  • Show files and messages by tags
  • Show files and messages by search results

Ideally we want to encapsulate the rendering of a file and a message so they look the same all over the site, and we don't need to duplicate our presentation logic. Grails provides a mechanism to handle this, through GSP, called templates.

A template is a GSP file, just the same as our view GSP files, but is differentiated from a view by prefixing the file name with an underscore. We are going to create two templates—one template for messages, which will be called _message.gsp and the other for files, which will be called _file.gsp.

The templates will be responsible for rendering a single message and a single file.

Templates can be created anywhere under the views folder. The location that they are created in affects the way they are executed. To execute a template we use the grails render tag. Assume that we create our message template under the views/message folder. To render this template from a view in the same folder, we would call the following:

<g:render template="message" />

However, if we need to render a message from another controller view, say the home page, which exists under views/home, we would need to call it like so:

<g:render template="/message/message" />

Passing data to a template

The two examples of executing a template above would only be capable of rendering static information. We have not supplied any data to the template to render. There are three ways of passing data into a template:

  • Send a map of the data into the template to be rendered
  • Provide an object for the template to render
  • Provide a collection of objects for the template to render

Render a map

This mechanism is the same as when a controller provides a model for a view to render. The keys of the map will be the variable names that the values of the map are bound to within the template. Calling the render tag given below:

<g:render template="message" model="[message: myMessage]" />

would bind the myMessage object into a message variable in the template scope and the template could perform the following:

<div class="messagetitle">
<g:message code="${message.title}" encodeAs="HTML"/>
</div>

Render an object

A single object can be rendered by using the bean attribute:

<g:render template="message" bean="${message}" />

The bean is bound into the template scope with the default variable named it:

<div class="messagetitle">
<g:message code="${it.title}" encodeAs="HTML"/>
</div>

Render a collection

A collection of objects can be rendered by using the collection and var attributes:

<g:render template="message" var="message" collection="${messages}" />

When using a collection, the render tag will iterate over the items in the collection and execute the template for each item, binding the current item into the variable name supplied by the var attribute.

<div class="messagetitle">
<g:message code="${message.title}" encodeAs="HTML"/>
</div>

Be careful to pass in the actual collection by using ${}. If just the name of the variable is passed through, then the characters in the collection variable name provided will be iterated over, rather than the items in the collection. For example, if we use the following code, the messages collection will be iterated over:

<g:render template="message" var="message" collection="${messages}" />

However, if we forget to reference the messages object and just pass through the name of the object, we will end up iterating over the string "messages":

<g:render template="message" var="message" collection="messages" />

Template namespace

Grails 1.1 has introduced a template namespace to make rendering of templates even easier. This option only works if the GSP file that renders the template is in the same folder as the template itself. Consider the first example we saw when rendering a template and passing a Map of parameters to be rendered:

<g:render template="message" model="[message: myMessage]" />

Using the template namespace, this code would be simplified as follows:

<tmpl:message message="${myMessage}"/>

As we can see, this is a much simpler syntax. Do remember though that this option is only available when the GSP is in the same folder as the template.

Create the message and file templates

Now, we must extract the presentation logic on the home page, views/home/index.gsp, to a message and file template. This will make the home page much simpler and allow us to easily create other views that can render messages and files.

Create two new template files:

  • /views/message/_message.gsp
  • /views/file/_file.gsp

Taking the code from the index page, we can fill in _message.gsp as follows:

<div class="amessage">
<div class="messagetitle">
<g:message code="message.title"
args="${[message.title]}" encodeAs="HTML"/>
</div>
<div class="tagcontainer">
<g:message code="tags.display"
args="${[message.tagsAsString]}" />
</div>
<div class="messagetitlesupplimentary">
<g:message code="message.user"
args="${[message.user.firstName, message.user.
lastName]}"/>
</div>
<div class="messagebody">
<g:message code="message.detail"
args="${[message.detail]}" encodeAs="HTML"/>
</div>
</div>

Likewise, the <div> that contains a file panel should be moved over to the new _file.gsp. This means the main content of our home page (views/home/index.gsp) becomes much simpler:

<div class="panel">
<h2>Messages</h2>
<g:render template="/message/message"
collection="${messages}" var="message"/>
</div>
<div class="panel">
<h2>Files</h2>
<g:render template="/file/file" collection="${files}" var="file"/>
</div>

User tags

The next step is to allow users to register their interest in tags. Once we have captured this information then we can start to personalize the home page. This is going to be surprisingly simple, although it sounds like a lot! We just need to:

  • Create a relationship between Users and Tags
  • Create a controller to handle user profiles
  • Create a form that will allow users to specify the tags in which they are interested

User to tag relationship

Creating a relationship between users and tags is very simple. Users will select a number of tags that they want to watch, but users themselves are not 'tagged', so the User class cannot extend the Taggable class. Otherwise users would be returned when performing a polymorphic query on Taggable for all objects with a certain tag.

Besides allowing a user to have a number of tags, it is also necessary to be able to add tags to a user by specifying a space delimited string. We must also be able to return the list of tags as a space delimited string.

The updates to the user class are:

package app
import tagging.Tagger
class User {
def tagService
static hasMany = [watchedTags: Tagger]
...
def overrideTags( String tags ) {
watchedTags?.each { tag -> tag.delete() }
watchedTags = []
watchedTags.addAll( tagService.createTagRelationships( tags ))
}
def getTagsAsString() {
return ((watchedTags)?:[]).join(' ')
}
}

User ProfileController

The ProfileController is responsible for loading the current user for the MyTags form, and then saving the tags that have been entered about the user. Create a new controller class called ProfileController.groovy under the grails-app/controller/app folder, and add the following code to it:

package app
class ProfileController {
def userService
def myTags = {
return ['user': userService.getAuthenticatedUser() ]
}
def saveTags = {
User.get(params.id)?.overrideTags( params.tags )
redirect( controller:'home' )
}
}

The myTags action uses userService to retrieve the details of the user making the request and returns this to the myTags view. Remember, if no view is specified, Grails will default to the view with the same name of the action.

The saveTags action overrides the existing user tags with the newly submitted tags

The myTags form

The last step is to create the form view that will allow users to specify the tags they would like to watch. We will create a GSP view to match the myTags action in ProfileController. Create the folder grails-app/views/profile and then create a new file myTags.gsp and give it the following markup:

<%@ page contentType="text/html;charset=UTF-8" %>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="layout" content="main"/>
<title>My Tags</title>
</head>
<body>
<g:form action="saveTags">
<g:hiddenField name="id" value="${user.id}"/>
<fieldset>
<dl>
<dt>My Tags</dt>
<dd><g:textField name="tags" value="${user.tagsAsString}"
size="35" class="bigfield"/></dd>
</dl>
</fieldset>
<g:submitButton name="Save" value="Save"/>
|
<g:link controller="home">Cancel</g:link>
</g:form>
</body>
</html>

This view will be rendered by the myTags action on the ProfileController and is provided with a User instance. The form submits the tags to the saveTags action on the ProfileController. The user id is put in a hidden field so we know which user to add the tags to when the form is submitted, and any existing tags for the user are rendered in the text field via the tagsAsString property.

Add a link to the myTags action in the header navigation from our layout in main.gsp:

<div id="header">
<jsec:isLoggedIn>
<div id="profileActions">
<span class="signout">
<g:link controller="profile" action="myTags">My Tags</g:link>
<g:link controller="auth" action="signOut">Sign out</g:link>
</span>
</div>
</jsec:isLoggedIn>
<h1><g:link controller="home">Teamwork</g:link></h1>
</div>

Now restart the application, log in as the default user and you will be able to specify which tags you are interested in.

Grails 1.1 Web Application Development

Personalizing the home page

Now that we have a way of allowing users to register interest in specific tags, we can update the home page to have a more personal feel. We are going to modify the homepage to display only the five most recent posts and files, as well as all of the tagged content that the user is interested in.

Content service

To help retrieve this information, we are going to introduce a ContentService class that can handle the necessary logic and call this from the HomeController. Under services/app, create a new class ContentService.groovy. First of all, we will implement the code to retrieve all the items tagged with one of the tags that the current user is interested in:

package app
import tagging.Taggable
import org.hibernate.FetchMode
class ContentService {
def userService
def allWatchedItems() {
def watchedTags = userService.authenticatedUser.tags
return watchedTags ?
Taggable.withTags( watchedTags, lastUpdatedSort ) : []
}
private lastUpdatedSort = {one, other ->
one.lastUpdated < other.lastUpdated? 1 :
(one.lastUpdated == other.lastUpdated) ? 0 : -1
}
}

The allWatchedItems method gets all of the instances of Tag that the user is interested in and then performs a polymorphic query on Taggable to get all of the items with one or more of these tags. When querying for the items, we also pass in a reference to a closure that can be used as a comparator on Groovy List objects for sorting. The return value from this closure is an integer that determines if the first object is less than, equal to or greater than the second object.

To allow the lastUpdatedSort closure to work, we need to add the lastUpdated property to the File domain class.

So the code for the File domain class is as shown below:

package app
import tagging.Taggable
class File extends Taggable {
static hasMany = [versions: FileVersion]
SortedSet versions
FileVersion currentVersion
Date lastUpdated
def newVersion(version) {
versions = (versions) ?: new TreeSet()
versions << currentVersion
currentVersion = version
}
def static withTag(String tagName) {
return Taggable.withTag(tagName, File)
}
}

We need to add a method to the User class to retrieve a list of Tag instances that the user is watching. At the moment the User class can only return a list of Tagger instances. Add the following method to the User class:

def getTags() {
return watchedTags.collect{ it.tag }
}

We have also not implemented the withTags method on Taggable, so let's take a look at it now:

def static withTags( checkTags, sorter ) {
return Taggable.withCriteria {
tags {
'in'('tag', checkTags)
}
}.sort( sorter )
}

This method performs a polymorphic query against Taggable to find all subclass instances that have a relationship to a Tag instance that is in the supplied list of tags to check. The results are then sorted using the sorter closure, which in our case, happens to be the lastUpatedSort closure from ContentService.

The next responsibility of the ContentService is to return the five most recent items posted to the application. It can be implemented by using the following code:

def fiveMostRecentItems() {
def messages = Message.list(sort: 'lastUpdated', order: 'desc',
fetch: [user: 'eager'], max: 5)
def files = File.createCriteria().listDistinct {
currentVersion {
order('dateCreated', 'desc')
fetchMode('user', FetchMode.EAGER)
}
fetchMode('tags', FetchMode.EAGER)
maxResults(5)
}
return mergeAndSortListsToSize(messages, files, lastUpdatedSort, 5)
}
private mergeAndSortListsToSize(list1, list2, sorter, size) {
def merged = list1
merged.addAll( list2 )
merged = merged.sort( sorter )
if( merged.size() >= size ) {
merged = merged[0..<size]
}
return merged
}

First, the five most recent messages are retrieved, followed by the five files where the latest version was updated most recently. Both of these results are then merged together into one list, ordered and then limited to five results by the mergeAndSortListsToSize method. Notice how we have been able to reuse the lastUpdatedSort closure here.

Update the HomeController

Now that the ContentService is implemented, we can use this code from the HomeController to display the required information:

package app
class HomeController {
def contentService
def index = {
return [
latestContent: contentService.fiveMostRecentItems(),
myContent: contentService.allWatchedItems()]
}
}

We can see that the five most recent items are made available to the home page via the latestContent variable, and the user's watched items are available in the myContent variable.

Update the home page

We now have the situation where we are returning collections of mixed types; messages and files instances are stored side-by-side in the same collection. We will implement a template to handle this so we can continue to reuse the individual file and message templates and keep the implementation of the home page clean. Create the following folder: grails-app/views/shared. Now create a new template GSP called _item.gsp. This will determine which template to use, based on the type of a particular object:

<%@ page import="app.Message" contentType="text/html;charset=UTF-8" %>
<%@ page import="app.File" contentType="text/html;charset=UTF-8" %>
<g:each in="${data}" var="item">
<g:if test="${item.class == File}">
<g:render template="/file/file" bean="${item}" var="file"/>
</g:if>
<g:if test="${item.class == Message}">
<g:render template="/message/message" bean="${item}" var="message"/>
</g:if>
</g:each>

We can use this template from our home page. Replace the main content div in views/home/index.gsp with the following:

<div id="mostrecent" class="panel">
<h2>Most Recent Updates</h2>
<g:render template="/shared/item" bean="${latestContent}" var="data"/>
</div>
<div id="yourdata" class="panel">
<h2>Items of Interest</h2>
<g:render template="/shared/item" bean="${myContent}" var="data"/>
</div>

Everything is in place for our personalized home page! Run the application and create some messages and files. Make sure to tag the files and then create some tags for our user as shown in the following screenshot.

Grails 1.1 Web Application Development

All Messages and Files

Our home page is starting to feel really useful now! It provides an initial overview for the users to quickly see what new information has been posted by their teammates and keep an eye on things that interest them. The only problem is that as more and more messages and files are posted, the old content can't be viewed any more. We need a couple of new pages to list all messages and files. Hopefully, by now, we are starting to see how trivial this is going to be. In fact, we can add the new pages without even restarting our application!

In MessageController, create a new action called list:

def list = {
def messages = Message.list(sort: 'lastUpdated', order: 'desc',
fetch: [user: 'eager'])
return [messages: messages]
}

In FileController create a new action called list:

def list = {
def files = File.withCriteria {
currentVersion {
order('dateCreated', 'desc')
fetchMode('user', FetchMode.EAGER)
}
}
return [files: files]
}

We will also need to import the Hibernate fetch mode for FileController:

import org.hibernate.FetchMode

Create a new view under views/message called list.gsp and give it the following markup:

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="layout" content="main"/>
<title>Messages</title>
</head>
<body>
<div class="singlepanel">
<h2>View All Message</h2>
<g:render template="message" collection="${messages}" var="message"/>
</div>
</body>
</html>

Then create a new view under views/file called list.gsp and give it the following markup:

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="layout" content="main"/>
<title>Files</title>
</head>
<body>
<div class="singlepanel">
<h2>View All Files</h2>
<g:render template="file" collection="${files}" var="file"/>
</div>
</body>
</html>

Update the layout (views/layouts/main.gsp) to link to the two new pages in the navigation:

<g:link controller="message" action="list" class="navigationitem">All Messages</g:link> |
<g:link controller="file" action="list" class="navigationitem">All Files</g:link>

Without restarting the application, go back to your web browser and you should be able to see two new links on the primary navigation: All Messages andAll Files. This is what you should see on the All Messages screen as shown in the following screenshot:

Grails 1.1 Web Application Development

This is what the All Files screen should look like:

Grails 1.1 Web Application Development

Summary

In this article, we have seen how to customize and update our homepage. Our home page started to become a bit complicated, but Grails templates came to the rescue allowing us to extract repeatable and reusable presentation logic into templates for rendering messages and files. Once the tagging structure was set up and the templates were in place, we moved on to allow users to customize their home page by specifying tags that they are interested in. Finally, while creating the pages to view All Messages and All Files, once again, we saw how trivial it is to create new pages and rework an applications structure in Grails.

About the Author :


Jon Dickinson

Jon Dickinson is the principal consultant and founder of Accolade Consulting Ltd. (http://www.accolade-consulting.co.uk) and can be contacted at jon@accolade-consulting.co.uk. He specializes in developing web applications on the Java platform to meet the goals of users in the simplest and least obtrusive way possible.

Books From Packt


Practical Plone 3: A Beginner's Guide to Building Powerful Websites
Practical Plone 3: A Beginner's Guide to Building Powerful Websites

WordPress Plugin Development: Beginner's Guide
WordPress Plugin Development: Beginner's Guide

Spring 2.5 Aspect Oriented Programming
Spring 2.5 Aspect Oriented Programming

Spring Web Flow 2 Web Development
Spring Web Flow 2 Web Development

Seam 2.x Web Development
Seam 2.x Web Development

Drools JBoss Rules 5.0 Developer's Guide
Drools JBoss Rules 5.0 Developer's Guide

Django 1.0 Website Development
Django 1.0 Website Development

Learning jQuery 1.3
Learning jQuery 1.3


No votes yet

Post new comment

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
4
i
m
d
D
f
Enter the code without spaces and pay attention to upper/lower case.
Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software