Sunday, April 13, 2008

Writing a Simple Issue Tracker in Grails, Part 1

My project for the weekend was to write a simple issue tracking webapp in Grails. I could have used something like Trac but that's overkill for my needs. I just wanted something simple where my users could report issues and request new features. I also wanted to add a few personal touches, which I'll show you along the way.

I'm going to assuming that you're not absolutely new to Grails and you've already got it installed and played around with it. If that's not the case, I suggest checking out Scott Davis's Mastering Grails series of articles. He's goes into far more detail than I do, so check them out.

Let's start by creating our project:
grails create-app simpleissue

First up is to define our domain models. We're going to keep it simple with just three models: Project, Component, and Issue. A Project has one or more Components, such as ui, documentation, etc. A Component is associated with a single Project and has zero or more Issues associated with it. The Issue object is associated with a Component and captures a bunch of information.

So let's lay down the code:
Project.groovy

class Project {
// relationships
static hasMany = [components: Component]

// fields
String name

String toString() {
return name
}

// constraints
static constraints = {
name()
components()
}
}

Component.groovy

class Component {
// relationships
static belongsTo = Project
static hasMany = [issues: Issue]

// fields
Project project
String name

// override for nice display
String toString() {
return "${project} - ${name}"
}

// constraints
static def constraints = {
name()
project()
issues()
}
}

Issue.groovy

class Issue {
// relationships
static belongsTo = Component

// fields
Component component
String type
String submitter
String description
String status = "New"
Integer bounty
Date dateCreated
Date lastUpdated

// constraints
static constraints = {
component()
type(inList: ["Defect", "Feature"])
submitter()
description(size: 0..5000)
status(inList: ["New", "Accepted", "Closed", "Won't Fix"])
bounty(range:0..12)
}
}


Most of the code is a pretty straightforward translation of our written description of the domain. You may, however, notice a few peculiar constraints. I've used a fair number of 'empty' constraints such as:

static def constraints = {
name()
project()
issues()
}

By default, Grails treats all fields in the domain class as required. I didn't want to change that, but I wanted to affect the order that the fields show up in a particular order in the web forms. By specifying the constraint, even if it is empty, it'll show up in that order in our forms. Of course, we could have customized the field order by hand directly in the view GSP code.

I also make use of the inList constraint to limit the fields to a specific set of values. Our views will be generated with an HTML select drop down containing the list of values we've specified.

Finally, we specify our issue description as being size:0..5000. This will ensure that there is plenty of space in the database for the description text. If we hadn't specified this, the description would have been generated as a varchar(255).

With our domain classes in place, we can create our controllers and views to test things out:

grails generate-all Project
grails generate-all Component
grails generate-all Issue
grails run-app

Fire up your browser and test things out by visiting http://localhost:8080/simpleissue:
and our issue creation form:

Looks pretty decent for 5 minutes of work. Poke around and test creating a project, component, and a few issues.

Customizing the Look

Now let's clean things up a bit and add some polish. The first thing I want to do is have the index page show the list of issues. We could copy and paste the code from the Issue List view or we can simply add a redirect to the top of our web-app/index.gsp file:

<% response.sendRedirect('issue/list') %>


The next thing I want to do is clean up the issue creation form. A few of the values, such as status, dateCreated, lastUpdated don't need to be specified in the form. We can go into grails-app/views/issue/create.gsp and remove those fields.

You may have noticed an odd field in the Issue domain class: bounty. You might have expected to see a field for priority on the issue. Instead, I chose to add a "beer bounty" field where the issue submitter could pledge a certain number of beers that I could redeem upon completion of the issue. This is, in my opinion, far superior to simply assigning low, medium, high priorities to issues.

As a final customization, I want to convert the number of beers into little beer mug icons to make it easy to see the important issues to fix. We'll do this by first copying the repeat example tag from the Dynamic Tag Libraries page of the documentation:
grails create-tag-lib Misc
This will create a grails-app/taglib/MiscTagLib.groovy file which we can add:

class MiscTagLib {
def repeat = {attrs, body ->
def i = Integer.valueOf(attrs["times"])
def current = 0
i.times {
out << body(++current)
}
}
}


And we'll call it in our grails-app/views/issue/list.gsp:

<g:repeat times="${issue.bounty}">
<img src="${createLinkTo(dir:'images', file:'beer.gif')}" alt="${issue.bounty} beers"/>
</g:repeat>


Here's a look at the final output:
In part 2, we're going to add in some security to prevent arbitrary user's from editing and deleting issues. We'll also add in searching/filtering support with the Searchable plugin.

Cheers.

13 comments:

Anonymous said...

Great! Just what I am looking for. I already supposed it has to be very easy to write an issue tracker in grails.
Please continue the tutorial series.

Just some ideas for improvements:
- support for i18n
- support for subprojects
- search form (and saving search criterias)

Best regards
Marc Pompl

Robby O'Connor said...

nice job!

Anonymous said...

Nice one!
You did some minor :) mistakes though (the second one is actually a typo):
- it's 'grails create-app ...' and not 'grails create-project'
- it's 'grails create-taglib Misc'

Josh Reed said...

@Anonymous,

Good catch on the first one, it is grails create-app. I fixed it in the text.

On Grails 1.0.1, it's grails create-tag-lib. Maybe they changed it in later versions?

Unknown said...

Greate article.... As our organization is look for issue tracker , Grails might be the best fit that.

Thanks and keep posting.

Sen

Anonymous said...

Hi Josh,
I like your blog. I've got a similar series going on Grails at http://jorgeonprogramming.blogspot.com/2008/03/my-first-grails-app-part-1-introduction.html.

Your issue tracker app seems more popular than my car service tracker app, :-).

Would you mind sharing what you use to export your code to HTML for blog posts? Your code samples look great!

Josh Reed said...

Hi Jorge,

I'm using syntaxhighlighter. I followed the instructions at:
http://developertips.blogspot.com/2007/08/syntaxhighlighter-on-blogger.html

Thanks for checking out the blog. The issue tracker was something I needed for a project and I figured it would be a shoe-in for the developers.

Cheers,
Josh

Josh Reed said...

Just another note, Jorge. You have to make sure you escape your < and > otherwise Blogger will eat them.

Cheers,
Josh

Frank Rocco said...

Nice Josh,
I currently have a c# version, and would like to see Part 2 of your grails issue tracker.

Uudashr said...

Cool josh. Why don't grails people create a real project for issue tracker.

ramana said...

well all the examples i have seen are using inList with static data to populate. Can any body give how to populate from database.
thanks
ramana
ramanakallil@rediffmail.com

Anonymous said...

Nice one!
I what concerns to relations One-to-Many I have a problem... The grails application it isn't capable of associate many components to a project (for example). In the views there is no way , like a multiple combo box to do that.. It appears a link something like "Add a component" which takes you to the "create.gsp" of component. And even after you create a bunch of components they don't show up in the Project create view ... How did you manage that ?

cheers,
diogo

Anonymous said...
This comment has been removed by a blog administrator.