Grails 1.1 Web Application Development

«»

SHARE & COMMENT :

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..

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.

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 and
All Files. This is what you should see on the All Messages screen as shown in the
following screenshot:

This is what the All Files screen should look like:

Summary

In this chapter, we have seen how to construct a domain model to allow files and
messages to be tagged. We used inheritance to enable tagging for the Message
and File domain classes and saw how GORM supports persistence of inheritance
structures to the database. Creating an inheritance structure in our domain classes
allowed us to make use of polymorphic queries.

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.

In the next chapter, we will see how Grails supports AJAX and look at some of the
Rich Internet Application (RIA) plug-ins that are available.

«»

Comments

comments

Pages: 1 2 3 4

About Krishna Srinivasan

He is Founder and Chief Editor of JavaBeat. He has more than 8+ years of experience on developing Web applications. He writes about Spring, DOJO, JSF, Hibernate and many other emerging technologies in this blog.

Speak Your Mind

*

Close
Please support the site
By clicking any of these buttons you help our site to get better