06 Sep, 2010

Published at 07:33AM

Tagged with golf and life

This post has 0 comments

Golfing at Isle of Palms, SC

I just got back from a week at Wild Dunes, SC. As with most vacations, it was great to get away from pretty much everything and clear my head, even if it was only for 7 or so days. While I was there, I had a goal to play 4 rounds of golf on different courses around the area. I’m happy to say I accomplished that goal.

I put the rounds in Golf Trac and since it allows you to share your rounds (bragging rights for some people), I figured I’d post them here for those interested:

It took me a day or two (or three) to get used to the wind, as that was my first “links” experience. It was amazing, too. Several holes were right alongside the ocean.

I played great (for me) my last round there, at Rivertowne, despite still being +9. My misses weren’t because of mishits, but rather the wind and trying to get up and down from a bunker. I was in disbelief that wind can affect a putt, but when it’s blowing 25-30 mph, it definitely does. Lesson learned.

22 Jul, 2010

Published at 06:30AM

Tagged with design, golftrac, and sketches

This post has 0 comments

Sketch to Reality: Golf Trac Signup Complete

The last major infrastructure piece of Golf Trac was payment integration. Since I was integrating with a hosted payment page I wanted to give users the ability to know that the signup process was complete. In theory they wouldn’t question it, because the idea is to make the external service feel like you never left, but on any step-by-step process (in this case, 3 steps), I believe it’s often worthwhile to have proper closure.

With that said, here was my thought for the screen you would see when your signup was officially done:

The green with the flag protruding out behind the login box may have been a nice touch, but it was a little too “flashy” and just added YATTP (Yet Another Thing To Process). So I blew up the checkbox and got rid of the eye candy. Also, highlighting that they have completed the final step is probably a good idea. Here’s what I came up with after 2-3 more sketches:

And it’s only fair to show you what the live implementation looks like, so here you go:

I’m pretty happy with it. Even though I’m integrating with an external application for payment processing, I must say, the experience is about as good as it gets, which was really important to me.

By the way, I use doane paper for all of my design sketching, and I can’t recommend them enough. Very high-quality stuff.

20 Jul, 2010

Published at 10:13PM

Tagged with business and golftrac

This post has 0 comments

Golf Trac Status Update

It is almost to the point of embarrassment how long I’ve let this project drag on. I first mentioned it a few years ago, but I didn’t really take it seriously until these past 12-16 months. And once I decided to take it seriously, a whole slew of hiccups came into play. Some were decisions, some where logistics, and some were technical challenges. But I’m happy to say I’m past all of that now, and it’s a couple of days away from launch.

Deciding to Charge

This was a really difficult decision. The original plan all along was to build this tool and release it for free. But that doesn’t scale, for obvious reasons. Servers aren’t free. It costs money to host things, and even more so when you get a lot of people using something. That’s not to say that will happen, but if it did, the expense would have been on me to keep it running. In today’s world, some people might expect that, but it didn’t sound all that appealing to me. Plus, this is my blood sweat and tears poured into this thing, it’s a little less rewarding to just hand it over for nothing.

But as you might expect, the main deterrent was dealing with credit cards. That was not in my comfort zone. Luckily, SaaS companies have options now. There are things like Spreedly and CheddarGetter that ease the pain that is recurring billing. I chose to go with Chargify – they seemed to have the most of what I was looking for and their support was/is outstanding. Plus, they have a great API so it was relatively easy to integrate.

Merchant Account & Business Crapola

Getting a merchant account for someone who has done it before is probably not a big deal. But when you first dive into all of the options and fine print, it’s a bit overwhelming. There are transaction fees, monthly minimums, credibility, and on and on. And with any merchant account comes a payment gateway. That’s another area where confusion is the norm. Luckily it’s all behind me now, but that was a rather large wall I had to bust through. That alone pushed the launch date back a couple of months.

Giving it a Shot

After a four month private beta period and about 400 passing tests, the application itself is stable and ready to go. And now that I have all of the logistics ironed out, it’s just a matter of flipping the switch. There are a few more things that I’m waiting to hear back on and then it will be live. I would say the end of this week, but who launches an application at the end of the week? Who knows, maybe me :-)

For those that don’t know, check it out at http://www.golftracapp.com.

Stay tuned! The public launch really is a day or two away.

07 Jun, 2010

Published at 07:22AM

Tagged with ajax, code, javascript, jquery, programming, rails, and tips

This post has 0 comments

Ajax convention using jQuery and Rails

Really, I guess this convention could apply to anything, but some frameworks are limited in the ways in which they deal with an Ajax request/response. So I’ll be using Rails.

Rails already has several different great ways to deal with Ajax. Although, when you just want to update a DOM element with a chunk of HTML, it feels like overkill to create a whole template for a single line of jQuery. And yes, I know this topic is up for debate. Some say you should always stick with the convention of using templates. I used to agree, but I’m not sure I do anymore. You can decide for yourself.

Here’s the convention:

1
2
<%= link_to 'Send Request', some_url, :id => 'content', :rel => 'updater' %>
<div id="x_content"></div>

You have a link with an ID and a rel attribute of “updater” (that indicates that this link is used to update content on the page somewhere). The element to update uses the ID of the link with an “x_” prefix. The “x” indicates that the attribute is not used for styling, but has functionality tied to it (so don’t change it!)

And here’s the jQuery plugin that makes it work. It’s straightforward:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$.fn.ajaxUpdater = function(options) {
  var options = $.extend({
    prefix: 'x'
  }, options || {})

  return this.each(function() {
    $(this).click(function(e) {
      e.preventDefault()

      var $dom_id = ['#', options.prefix, '_', $(this).attr('id')].join('')

      $.get(this.href, function(data) {
        $($dom_id).html(data)
      })
    })
  })
}

And to make use of it, standard jQuery:

1
$(function() { $('a[rel=updater]').ajaxUpdater() })

That will make the link request via Ajax and update the appropriate div with the response. Now, about the response. Like I said, in Rails there are several ways to do Ajax, most of which involve a template. But if you’re just needing to return a partial (read: chunk of HTML), is there really a need for a template? I’m not sure there is. Templates make obvious sense for visually representing content, but that’s not really what we’re doing here.

An example Rails action that would make this work:

1
2
3
4
5
6
7
8
def index
  @users = User.all

  respond_to do |format|
    format.html
    format.js { render :partial => "users", :object => @users }
  end
end

By rendering a partial directly within the action, it’s clear that there’s no template involved. And I, at least, combine 1) the fact that I’m in the format.js block with 2) the fact that I’m returning a chunk of HTML to conclude that the content is being injected into the page somewhere.

I’m experimenting with this approach in a current project to see how it feels. It does sort of break the MVC model given that there’s no template (or view), so you do lose that consistent “place to look” relating to the content display. But you can also see that this is a UJS approach, which means that there is still a template for the raw HTML view—this is only for Ajax requests.

What do you think?

04 Jun, 2010

Published at 12:14AM

Tagged with code, extensions, programming, rails, ruby, and tips

This post has 2 comments

Convert links to clickable links in Rails

When someone types in a URL in a comment or message, it’d be nice if it would turn itself into a clickable URL, wouldn’t it? It’s a simple idea, and conveniently, it has a simple solution. Here’s my approach.

Create a separate module and stick it in your lib/ directory. You know the drill. Then stick this code in it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
module RPH
  module LinkConverter
    def self.included(klass)
      klass.extend ClassMethods
    end

    module ClassMethods
      def convert_links_for(*columns)
        methods = []

        columns.each do |column|
          define_method "converted_link_#{column}" do
            html = self[column].to_s
            html.gsub!(/\swww\./, ' http://www.').to_s
            html.gsub!(/((https?:\/\/|www\.)([-\w\.]+)+(:\d+)?(\/([\w\/_\.]*(\?\S+)?)?)?)/, '<a href="\1">\1</a>')
            self[column] = html
          end

          methods << "converted_link_#{column}".to_sym
        end

        before_save *methods
      end
    end
  end
end

This allows you to pass several columns to the link converter, and before the content is saved, the links will be converted to clickable URLs. In your model, just include the module and specify the columns, like so:

1
2
3
4
class Comment < ActiveRecord::Base
  include RPH::LinkConverter
  convert_links_for :body
end

I’ve also created a gist, so fork and improve!

27 May, 2010

Published at 10:37PM

Tagged with creativity and design

This post has 0 comments

Instead, fight through creative block

I love designing things. I have a huge appreciation for all things designed, even if it’s a simple table-top fan. What I don’t appreciate is when people steal the ideas and creativity of others and claim it as their own. It’s not fair. Some people steal designs because that’s just what they do. But some steal designs without even realizing it, which brings me to my point.

Every designer deals with creative block. It’s that deflating feeling when your sketches are garbage, Photoshop is puking with the type of graphics you might see on a government site, and you simply cannot solve a single problem, creatively. It happens.

Naturally when I get into such a slump I seek inspiration. I usually turn to sites that showcase design in some way. Logo sites, CSS galleries, or simply those sites that never let me down. It’s always a joy to look at other designs and be reminded of what quality feels like. But I’ve come to realize there’s a time and place for that, and it’s not when you’re in a creative block.

When we’re stuck on something we can’t help but try and find a solution. That’s just what humans do. The good one’s think first, search second. But some just search. Searching to solve a programming problem often makes sense, but searching to solve a design problem does not. The worst thing that could happen to you when searching for a design solution is finding it. Because it wouldn’t be your solution. It’d be someone else’s that they’ve solved their problem with. They didn’t know about your problem, so how could it possibly fit? It couldn’t, and it’s delusional to think it could.

Now I know there are exceptions to the rule, there always are. But in general I think it’s best to fight through your creative block, even if that means putting down your pen and paper for a couple of days. It will come back on its own, I promise.

25 May, 2010

Published at 12:26AM

Tagged with code, navigation, plugin, programming, rails, and xss

This post has 2 comments

Rails 2.3.7 and navigation plugin

On Monday Rails 2.3.6 came out. And then today Rails 2.3.7 came out. I upgraded a few apps today and realized the new XSS safe-by-default stuff broke my navigation plugin.

I considered updating the master branch with the fixes, but I’m going to hold off on that for now, since it will break in Rails 2.3.5 and below. Not everyone wants to upgrade to the latest 2.3.7 release just yet, but I can’t imagine why not :-)

Anyway, here are the (simple) fixes required to get the plugin working with Rails 2.3.7.

Plugin load order

First up, make sure you have the official rails_xss plugin installed. Then you need to tell Rails to load this plugin first, so that the navigation plugin can make use of the new XSS methods. Here’s how to do that. In environment.rb:

1
config.plugins = [ :rails_xss, :all ]

Load the rails_xss plugin, then everything else.

Making the output html_safe

init.rb—Tell ActionView::Base that the navigation helper is safe. Change this:

1
2
3
ActionView::Base.class_eval do
  include RPH::Navigation::Helpers
end

to this:

1
2
3
4
ActionView::Base.class_eval do
  include RPH::Navigation::Helpers
  self.safe_helper :navigation
end

navigator.rb—Ensure that the links are safe. At the end of the block (around line 81) do this:

1
2
3
links = menu.inject([]) do |items, (item, opts)|
  # ...
end.join("\n").html_safe

That’s about it. Now the plugin should behave as it always has.

Oh, there’s one more thing. If you will be passing HTML to the :text option in the menu configuration, you’ll need to ensure that the link text is safe as well.

navigator.rb—Change this line (around line 109):

1
[text, path, attrs]

to this:

1
[text.html_safe, path, attrs]

That should do it. Now if you have :text => "About &amp; Tour" you’ll get the proper output. If you run into any other issues, let me know.

12 May, 2010

Published at 06:53PM

Tagged with defensio, site, and spam

This post has 1 comment

Spam Protection: Bringing Back CAPTCHA

A couple of months ago I posted about adding Defensio to help with the spam protection. I thought by using Defensio I’d be able to get rid of the annoying CAPTCHA, but it’s back.

Defensio did (er, does) a great job preventing spammy comments from getting through, but I got tired of dealing with the approval process on the questionable one’s. Even though it was only questioning about 5% of the comments, that was averaging about 10-15 per day that I had to explicitly mark as spam or approve. So, in hopes to avoid having to do that, I’m now running a double-whammy spam protection setup: Defensio and the CAPTCHA. We’ll see how it goes.

22 Apr, 2010

Published at 10:26PM

Tagged with creativity, design, inspiration, and video

This post has 0 comments

Ryan Singer, "Designing with Forces"

Design is hard. CSS and HTML aren’t hard, but design itself is hard. I’ve been working at it for a long time, reading books, practicing, drawing, etc. Studying, basically.

Before yesterday, I’ve never heard of Designing with Forces. The book, the phrase, or the concept. And that’s a shame, because it really gave me a new perspective on web design. And to be honest, in retrospect I was already doing some of this. But I didn’t have these concrete notions of forces impinging on a form, for example. It’s enlightening. I wish the book was available on the Kindle.

Content-wise, this is one of the best talks I’ve seen in a while (courtesy of Ryan Singer, a designer for 37signals). If you’re into design, you should check it out. I usually don’t embed videos, but for your convenience:


02 Mar, 2010

Published at 12:24PM

Tagged with caching, programming, rails, and tips

This post has 0 comments

Conditional page caching for logged in users

Along with the new design I’ve added some page caching here and there. I’ve yet to do the blog and archives sections, as there seems to be something quirky going on (if you’re aware of any page caching bugs in Rails 2.3.5, let me know :-).

As you may or may not know, this site has a minimal admin section so I can manage content. When I’m logged in, I have an “Admin” tab and a “Logout” link somewhere in the UI. So naturally, I can’t page cache when I’m logged in. Luckily, Rails provides the option to cache on an :if condition.

Here’s a little tip for those in a similar situation:

1
2
3
4
5
6
class ApplicationController < ActionController::Base
  protected
    def self.cache_if_not_logged_in(*actions)
      self.caches_page *( actions << { :if => Proc.new { |c| !c.send :logged_in? } } )
    end
end

Then, in any controller that is to behave this way, all you have to do is:

1
2
3
4
5
6
7
class PortfolioController < ApplicationController
  cache_if_not_logged_in :index

  def index
    # do what you do best
  end
end

I know this is rather simple (and to most of you, obvious), but I was once a beginner seeking useful ways to do simple things, so there you go. Chalk one up for the beginners.

02 Mar, 2010

Published at 01:07AM

Tagged with design and site

This post has 0 comments

So fresh and so clean (new design)

I caught the bug again. The “site redesign” bug that is. I didn’t sway too far from the last design, but nonetheless it was time for some much-needed housekeeping.

For the new readers (and those who don’t remember), here’s what the old homepage used to look like:

And now, the new one:

Some elements stayed (almost) the same; namely, the header. And obviously not much got added. Instead, I dug deep into my design experience and did one of the hardest things on a designers repertoire: took stuff out.

So, if you like plain, black and white sites, click the link to leave your feed reader and take a look.

24 Jan, 2010

Published at 11:17PM

Tagged with defensio, programming, site, and spam

This post has 4 comments

How to implement basic Defensio spam protection in Rails

Right off the bat I should say that, like most Ruby applications, there are a ton of different ways this could be achieved. But there didn’t seem to be too many examples floating around, so I thought I would explain how I’m doing it on this site.

For those who don’t know what Defensio is, let me explain. It’s a simple web service that essentially filters any content you give it in order to determine if it’s “spammy” or not. Basically, you give it some content and it spits back some attributes that you can use to base decisions on.

There are 3 key attributes that you’ll find very useful: allow, profanity-match, and spaminess.

  • allow – returns true/false depending on whether or not Defensio thinks this piece of content should be allowed on your site
  • profanity-match – returns true/false based on, well, whether or not the content has profanity in it
  • spaminess – returns a Float value between 0 and 1 indicating how “spammy” the content is (1 would be 100% spam).

Based on these attributes, you can setup some rules to handle the flow of your comments. Here’s essentially what my rules are:

  • Automatically approve the comment and allow it to go through if Defensio sets allow to true, profanity-match to false, and a spaminess value between 0 and 0.10
  • Put the comment in an approval queue if any one of the above things fail, and the spaminess is less than 0.75
  • Automatically reject the comment if the spaminess is greater than 0.75, regardless of what the allow and profanity-match values say

With that said, here’s basically how I have things setup. Firstly…

1
$> sudo gem install defensio

It’s pretty simple to interface with the Defensio API without the gem, but why not use it, right?

In my comment model I have a before_create :check_for_spam callback that hands the comment over to Defensio. I added a DefensioResponse class that I pass the returned attributes to. That’s also where I keep the rules/logic. Here are the noteworthy pieces of my comment model:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class Comment < ActiveRecord::Base
  has_one :defensio_response
  
  attr_accessor :defensio_signature
  
  before_create :check_for_spam
  after_create  :associate_with_defensio_response
    
  attr_protected :approved
  
protected
  def check_for_spam
    result = self.class.defensio.post_document(self.defensio_attributes)
    status, attributes = result.first, result.last
    
    return false unless status == 200
    
    defensio_response = DefensioResponse.init(attributes)
    self.defensio_signature = defensio_response.signature
    self.approved = defensio_response.approved?
    
    defensio_response.proceed?
  end
  
  def associate_with_defensio_response
    DefensioResponse.find_by_signature(self.defensio_signature).update_attribute(:comment_id, self.id)
  end
  
  def self.defensio
    @@defensio ||= Defensio.new(self.defensio_api_key)
  end
  
  def self.defensio_api_key
    YAML::load(File.open(File.join(Rails.root, 'config', 'defensio.yml')))["api_key"]
  end
  
  def defensio_attributes
    {
      "type" => "comment",
      "platform" => "rubyonrails",
      "content" => self.body,
      "author-email" => self.email,
      "author-name" => self.name,
      "author-url" => self.site
    }
  end
  
public
  def approve!
    self.update_attribute(:approved, true)
    self.class.defensio.put_document(signature, { :allow => true })
  end
  
  def mark_as_spam!
    self.update_attribute(:approved, false)
    self.class.defensio.put_document(signature, { :allow => false })
  end
  
  def signature
    self.defensio_response.signature
  end
end

When you post a document to Defensio, it returns an array of two values. The first is the status code and the second is the actual output of the response (as a hash of attributes).

You can see in the check_for_spam method that I’m setting the self.approved flag based on the logic I have in my defensio_response. Like I said, if a comment looks good according to my rules, then I don’t want to be bothered with approving it and let it pass through.

For the missing pieces, here are the internals of my DefensioResponse class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class DefensioResponse < ActiveRecord::Base
  belongs_to :comment
  
  validates_presence_of :allow, :spaminess, :signature
  
  AUTOMATIC_APPROVAL_RANGE = 0.0..0.1
  AUTOMATIC_DENIAL_VALUE = 0.75
  
private  
  def self.clean!(attrs)
    returning({}) do |cleansed_attributes|
      attrs.each do |key, value|
        cleansed_attributes.merge!(key.to_s.underscore => value)
      end
    end
  end
  
public
  def self.init(attributes)
    self.new(clean!(attributes))
  end
  
  def approved?
    self.allow? && 
      self.no_profanity? && 
        AUTOMATIC_APPROVAL_RANGE.include?(self.spaminess)
  end
  
  def no_profanity?
    !self.profanity_match?
  end
  
  def proceed?
    return false if self.spaminess > AUTOMATIC_DENIAL_VALUE
    self.save!
  end
end

All the clean!(attrs) method does is change the response values, which use dashes (i.e. “profanity-match”) to use underscores (i.e. “profanity_match”), since that’s what ActiveRecord prefers.

Also, as you can see the approved? method only returns true if all 3 of my requirements are true. That’s the only way a comment will automatically be accepted.

In my comment model, the final line in the check_for_spam method is a call to the defensio_response.proceed? method. Remember, in Rails, if you return false from a callback then the entire transaction is canceled. So, in the proceed? method, if the spaminess is simply too high, I’m just returning false and canceling everything (no comment or response get saved). But if it’s good to go, I’ll let the self.save! call determine if things should be committed, because really, I don’t want a comment without a defensio response.

Improving Over Time

From the code above, the only thing the defensio gem provides is the post_document and put_document methods, which reflect a RESTful API. You may understand why you would POST a document, but why would you PUT (read: update) one? It’s simple: to report false positives/negatives.

Defensio uses a learning algorithm, and like anything that learns, you have to teach it so it becomes smarter. If you look in my comment model you’ll notice two methods: approve! and mark_as_spam!. Remember, if a comment falls between 0.1 and 0.75 then it goes into the “needs my approval” queue.

If I approve the comment, it updates the flag in the comments table, but it also tells Defensio that “this is a good comment”. Likewise, if I mark one as spam, it updates the flag and tells Defensio that “this is a bad comment”.

That’s one reason why I’m saving a Defensio response with each comment, so I have the response signature to identify the comment later on. The signature is the only common piece of information between the content on Defensio and the comment on this site.

Results

So far, Defensio has been doing great. I’ve only been using it for a few days, and here’s what it has done so far:

Already has prevented 333 spam comments at 96.07% accuracy? I’ll take that.

Like I said, I didn’t find much in terms of examples of how others approached this, so hopefully someone will find this useful. If you’re having spam trouble, I’d take a close look at Defensio. They have a simple API and an ever-increasing level of accuracy. So far I’m very pleased.

19 Jan, 2010

Published at 11:30PM

Tagged with defensio, site, and spam

This post has 0 comments

Spam protection with Defensio

Those who have this comment feed have undoubtedly realized the amount of spam I’ve been getting lately. It seems that the “ruhuman” image was no longer effective… ahhh, the robots are learning!

Anyway, I’ve now upped the ante a bit and am using Defensio for my spam protection. That said, there will still probably be a few misses here and there while Defensio “learns” a bit more. Also, if you are submitting a comment and it rejects it, please shoot me an email: ryan at rpheath dot com.

Here’s to hoping my spam issues are coming to a close. Happy commenting!

13 Jan, 2010

Published at 10:54PM

Tagged with code, frameworks, programming, and webby

This post has 0 comments

Create custom rake tasks in Webby

I love Webby. I use it quite often these days (here are just two of the many examples: 1 and 2). It’s so fast and very easy to deploy.

Just recently I chose Webby for a documentation site. It has FAQs and help articles/tutorials. Basic stuff. For each entry, I wanted to have a structure something like this:

doc_project
 |_ content
 | |_ entries
 |   |_ 1.txt
 |   |_ 2.txt
 |_ images
   |_ 1
   |_ 2

(Sorry for the poor attempt at illustrating a directory structure)

Essentially, I wanted to keep all the *.txt files in a entries/ (or whatever) directory, and have a special nested directory in images/ that matched the name of the txt file. For documentation sites, I find that keeping a structure like that helps with screenshots and such. It more or less creates a namespace to prevent file clashing.

Since we programmers don’t like to do things manually, I took advantage of Webby’s support for custom rake tasks. So now I can run:

1
rake create:entry "This is the title of the new entry"

And it will handle this for me. Here’s the task:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
namespace :create do
  desc "Creates a new Doc entry"
  task :entry do |t|
    page, title, dir = Webby::Builder.new_page_info
    
    dir = File.join(dir, "entries")
    
    entry_count = Dir["#{dir}/*"].collect { |f| File.directory?(f) }.size
    new_page_name = "#{entry_count + 1}"
    
    page = File.join(dir, new_page_name)
    
    Dir.mkdir("#{Webby.site.content_dir}/images/#{new_page_name}")
    
    page = Webby::Builder.create(page, :from => "#{Webby.site.template_dir}/entries/entry.erb",
      :locals => { :title => title, :directory => dir })
    
    Webby.exec_editor(page)
  end
end

I have a pre-defined template in doc_project/templates/entries/entry.erb that my new files are based off of (notice the :from option in the call to Webby::Builder.create(...)).

I would break down the rake task, but I think it’s self-explanatory. The only piece that’s not obvious is the :locals option. And that just makes variables available in your templates (so I can have the title automatically filled in, for example).

Overall, Webby is awesome by itself and doesn’t need any additional support. But even so, now you know how to tell it to do a bit more.

10 Jan, 2010

Published at 11:48PM

Tagged with code, golftrac, navigation, plugin, programming, projects, and rails

This post has 9 comments

Multi-menu tabbed navigation in Rails

I just recently scrapped my old navigation plugin (in several applications) in favor of the new one I wrote. It has allowed me to clean that part of my code up tremendously. I know I’ve posted about it already, but I thought I’d show a real-world example of how I’m using it in Golf Trac.

First, here’s the situation. There are three main areas where navigation comes into play: 1) public navigation 2) sidebar navigation and 3) action-level sub navigation. Here’s what I’m referring to:

Not logged in (1):

Logged in (2 and 3):

The public and sidebar navigation doesn’t really change. But the tabs at the top right (sub navigation) change depending on where you are in the app, as they allow you to navigate to different actions within the same controller.

So here’s how it works. I define my navigation menus in an initializer and reference them by a key. This keeps my controllers and views clean. Also, the new plugin has native support for nested menus and action-level navigation, which is something the old one just wasn’t designed to support.

Here’s a snippet of the actual menu definitions used in Golf Trac.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# config/initializers/navigation.rb
 
RPH::Navigation::Builder.config do |navigation|
  navigation.define :public do |menu|
    menu.item WelcomeController, :path => :hello_path
    menu.item TourController, :text => 'About &amp; Tour'
    menu.item PlansController, :text => 'Plans &amp; Pricing'
  end
  
  navigation.define :sidebar do |menu|
    menu.item DashboardController
    menu.item RoundsController
    menu.item CoursesController
    menu.item FriendsController, 
      :text => Proc.new { |view| view.sidebar_friends_text }
    menu.item SettingsController
    menu.item AdminController, 
      :text => 'Administrator', :path => :administrator_path,
      :if => Proc.new { |view| view.logged_in_as_admin? }
  end
  
  # ...
 
end

As you can see, I’m defining a menu for each of the different situations. You can define as many as you want. It’s worth noting that the order that you list your menu items is the order they’ll be rendered.

So once your menus are defined, use <%= navigation :public %> to render them (:public being the key/name of a menu).

A nice side-effect about this is there’s no sloppy ERB tags to deal with. This is a Ruby file and allows for a natural use of Ruby blocks. That in itself is an improvement. Also, the plugin allows you to actually pass the controller itself as a menu item, rather than ensuring you have proper string matching. As expected, a RoundsController would provide a “Rounds” link that goes to the rounds_path. But as you can see, if you need to override the text or the path, that’s not a problem.

One interesting thing I’d like to point out is the “Friends” link in the sidebar navigation. Looking at the second screen shot above, see how it has that little number (in this case a “1”) next to the text? That indicates that you have 1 new friend request. But more importantly, that also means that the text for that item will be dynamic. Fortunately, that’s not a problem.

If you pass a Proc to the :text option, you will be handed an instance of the view. From there, you can call any helper your heart desires. Here’s what Golf Trac does:

1
2
3
4
5
6
7
8
def friend_requests_count
  return if (requests = current_user.friend_requests).size.zero?
  content_tag :span, requests.size
end
 
def sidebar_friends_text
  ["Friends", friend_requests_count].compact.join(" ")
end

So when you see this:

1
2
menu.item FriendsController, 
  :text => Proc.new { |view| view.sidebar_friends_text }

It’s just a simple helper call.

In a similar fashion, you can determine which menu items should be shown based on the result of some condition. In this case, I have an “Administrator” section that should only be shown if an administrator is logged in. So that’s what this does:

1
2
3
menu.item AdminController, 
  :text => 'Administrator', :path => :administrator_path,
  :if => Proc.new { |view| view.logged_in_as_admin? }

Notice the :if option, here. And that’s just another helper call to:

1
2
3
def logged_in_as_admin?
  !!current_user.administrator?
end

Simple stuff, but it makes a world of difference when you’re trying to hack together some navigation with different rules, conditions, text, paths, etc.

Now, that leaves one final question: how is the action-level sub navigation handled? It’s pretty simple, too. When you’re defining a menu, you can specify that it’s an :action_menu, which simply means that it will check against the current action rather than the current controller.

Note: if you want nested navigation, just pass a block to the menu.item call, defining the menu you want nested. In this case, we don’t want a nested menu, but still want action-level navigation. See the README for more.

So all I do is define the menus I want for each controller, using a key of that controller’s name. Here’s the sub navigation for the RoundsController, for example:

1
2
3
4
5
navigation.define :rounds, :action_menu => true do |menu|
  menu.item :index, :text => 'List', :path => :rounds_path
  menu.item :best, :path => :best_round_path
  menu.item :new, :path => :new_round_path
end

You can name the action-level menus anything you want, but by naming them after the controller in which they correspond, you can do some automatic rendering. For example, here’s what I’m doing:

1
2
3
4
5
def sub_navigation
  navigation controller_name.downcase.to_sym, :class => 'sub_navigation'
rescue RPH::Navigation::InvalidMenuIdentifier
  nil
end

If there’s no menu defined, nothing will happen. You can stick a call to the <%= sub_navigation %> helper in your layout, and it will work automatically. Nothing in your controllers, and no jumbled view code.

Nearly every Rails application has navigation. It’s something you have to deal with over and over again, too. Hopefully you have realized how something like this navigation plugin can greatly help you organize almost any form of navigation in your Rails app.

To install:

1
$ script/plugin install git://github.com/rpheath/navigation.git

Happy coding!

Ryan Heath | Site Management A Ruby on Rails production.

This site is a Formed Function. Formed Function LLC | @formedfunction | Get in Touch