Loccasions: Hiring a Foreman, Inheriting Resources, & Occasions

Glenn Goodrich
Ruby Editor
Tweet
This entry is part 10 of 15 in the series Loccasions

Loccasions

In this post, I want to finally get the Occasions MVC sequence done. This is the seventh post in the series, and I thought we’d be farther by now. Those responsible for our less-than-expected progress have been sacked. First, however, let’s make firing up the development environment a bit easier. Maybe that will kickstart our productivity…

Hiring a Foreman

Every time I want to hack on Loccasions, I have to fire up guard, a web server (rails s, for now), and mongodb along with my vim session. Without fail, I forget to fire up mongodb, so guard blows up all over the place. It’s an annoying time-waster and also puts me in a bad mindset at the start of my hack session. I would like to clean this up a bit, so I am bringing in Foreman . Foreman is a “manager for Procfile-based applications”, which Google will tell you means you can create a Procfile (we’ll put ours in the root of the app) and list out the processes we want Foreman to start up.

That’s sounds positively smashing to me, so I add gem foreman, "~> 0.24.0" to the :development and :test groups in my Gemfile, quick bundle install and foreman is officially on the payrole.

I have three processes I want to run in development: mongod, guard, and rails s, so my Procfile looks like:

web: rails s
test: guard
db: mongod --dbpath=/Users/ggoodrich/db/data

Now, I can type foreman start in my application directory and Forman will start these three processes.

No hard hat here

I like to imagine a scruffy, hard-hat wearing dude screaming at the processes (“ALL RIGHT, Database! Get off your lazy shard and prepare for data!”) Although, being honest, I think a better name for Formean would have been Procadile. I can already see the logo…maybe I need to get a non-programming hobby…

OK, maybe the logo would be better than this...

Occasions

We’re finally to a point where we can design how we’ll add Occasions. Occasions, as you may remember, belong to an Event. An Occasion is an individual occurrence of that Event. So, if your Event is “Selling Girl Scout Cookies”, then an Occasion for that event might be “February 2nd, 2010″ with the lat/long of 35.223/-85.443 (My Neighbor’s House), and a note of “2 boxes of Samoas”. Another Occasion for that Event could, then, have a date of February 10th, 2010, with the lat/long of (lat/long for my kid’s school), and a note that says “Mrs. Whatsherface bought 1 box of Thin Mints.”

Let’s write some unit tests around that idea. Put this in spec/models/occasion_spec.rb

require 'spec_helper'

describe 'Occasion' do
  before do
    @event = Factory.build(:event)
    @occasion = @event.occasions.build
  end

  it "should belong to an event" do
    @occasion.event.should_not be_nil
  end
  it "should have a time and date of occurrence" do
    dt = Time.now

    @occasion.occurred_at = dt
    @occasion.occurred_at.to_s.should == dt.to_s
  end

  it "should have a latitude and longitude" do
    @occasion.latitude = -85.000
    @occasion.longitude = 35.3232

    @occasion.latitude.should == -85.000
    @occasion.longitude.should == 35.3232
  end

  it "should have a note" do
    @occasion.note = "This thang went down"
    @occasion.note.should == "This thang went down"
  end

end

These tests fail, because we haven’t created an Occasion model and Event doesn’t have a occasions method. A quick rails g model Occasion occurred_at:datetime latitude:float longitude:float note:text -s will take care of that. (Note: the -s skips existing files, which is our spec file that we already created). We have to modify the generated model file to tell it that it lives in Events. Our app/models/occasion.rb file looks like: (I’ve gone ahead and added validations and accessors)

class Occasion
  include Mongoid::Document
  field :occurred_at, :type => Time
  field :latitude, :type => Float
  field :longitude, :type => Float
  field :note, :type => String
  embedded_in :event, :inverse_of => :occasions
  validates :occurred_at, :latitude, :longitude, :presence => true

  attr_accessible :occurred_at, :latitude, :longitude, :note
end

Also, open up models/event.rb and add embeds_many :occasions below the embedded_in :user line. I realized, looking at this file again, that I had neglected to defined which attributes on Event should be accessible. This is bad mojo, so I added attr_accessible :name, :description to the Event model.

Changing Our Spork Configuration

In the midst of writing the Occasion model spec, I added a new factory to create an Occasion in spec/factories.rb

  factory :occasion do
    latitude 35.1234
    longitude -80.1234
    occurred_at DateTime.now
    note "Test Occasion"
    event
  end

With my new factory, I changed the before block in the occasion spec to use it. This resulted in my specs blowing up all over the place with errors like:

Donde esta mi factory?

So, my new-fangled Spork/Guard super fantastic environment wasn’t reloading the factories. I frantically turned to Google and asked “WHAT NOW??!?” Google calmly replied, “Put this in the Spork.each_run block in your spec/spec_helper.rb file, my man.”

  # Reload our factories
  FactoryGirl.factories.clear
  Dir[Rails.root.join("spec/factories.rb")].each{|f| load f}

Guard knows to reload the RSpec environment when you mess with spec_helper.rb, so my tests were happy again. While we are in there, let’s add something to reload the routes too:

  # Reload routes
  Loccasions::Application.reload_routes!

Now that we have a model, we need a way to create them.

You Say Potatoe “Hurry up”, and I Say Potahtoe “Occasions Controller”

At this point, we should all be Olympic Gold Medalists at creating the vanilla Rails Controller for a resource. In this case, our resources are Occasions. Go ahead and try to get a working (and spec’d) controller for Occasions up and running. You can check what I did with this gist and see how it came out.

Inherited Resources

WHOA! What’s up with THAT gist? That doesn’t look like what we did for the events controller. You’re right, it doesn’t look like that. I tricked you. Jose Valim of Plataformatec (and Crafting Rails Applications) fame created the inherited_resources gem to address the fact that 95% of all RESTful controllers in Rails do the same stuff. Using Jose’s gem, we can have our OccasionsController inherit from InheritedResources::Base and we get the 7 ~~Deadly~~common controller actions for free. I heart this community. (BTW, now we be a good time to add gem "inherited_resources", "~> 1.3.0"
to your Gemfile and bundle install that baby.)

In this case, it’s not totally free, though, as we have to do some configuration to handle our “special” circumstances.
These circumstances relate mostly to our using MongoDB and the fact that Occasions are embedded within a document hierarchy (User ==> Events ==> Occasions). If you try to do something like Occasion.where(:event_id => @event.id) or whatever, you get the following error that scares the hell out of you the first time you see it:

Mongoid::Errors::InvalidCollection: Access to the collection for Occasion is not allowed since it is an embedded document, please access a collection from the root document.

Once you calm down, you realize that this makes total sense. Because we are using a document database, occasions are embedded within events and events are embedded within users. So, rather than use the regular ActiveModel class methods to access the collections, you have to walk down the document hierarchy. We need a user (current_user, which we are already using to scope events), and an event. Where do we get the event?

The route parameters have a :event_id entry so, if we were doing this ourselves, we’d grab that and query the current_user.events collection. This is a pretty common scenario, and the inheritedresources gem is crazy smart about common scenarios. Let’s take a look at this configuration in the app/controllers/occasionscontoller.rb file:

belongs_to :event
actions :all, :except => [:show, :index]

def begin_of_association_chain
  current_user
end

But wait! There’s more!! You see that action method call up there? That tells inherited_resources which actions we want (or don’t want, in this case) for our controller. Occasions will only ever been seen through an Event, so there is no point in creating the show and index actions (we will change our mind when we get to the Loccasions API) right now. The truly perceptive among you are now asking “But, what about redirects?”, which is a great question. A common idiom for Rails RESTful controllers is to redirect to the index or show page after resource creation. Again, we aren’t going to do that here, we want to go to the events#show action. The inherited_resources gem has a feature called “Smart redirects” that (from their github page:)

Redirects in create and update actions calculates in following order resourceurl, collectionurl, parenturl (which we are going to see later), rooturl. Redirect in destroy action calculate in following order collectionurl, parenturl, root_url.

In other words, it figures out what we want. I squealed like a little girl when I found that feature. (To be fair, though, I squeal a lot.)

Pretty straightforward, and we’ve reduced the amount of code we need to write. Occasions can be added to an event. I’ve written the spec/acceptance/add_occasions_spec.rb and delete_occasions_spec.rb. I am not currently going to worry about update, because I am having a problem seeing the use case. I am sure we’ll be back to update later, but right now I want to get to the map.

Update: Alert Reader Nicholas Henry points out in the comments below that you need to:

  • Amend events/show.html.haml with the Occasion form github
  • Add occasions/_occasion.html.haml github
  • Add the route for occasions github

Loccasions.map do { |its| about.time()}

Well, almost…the map will be the next post.

Loccasions

<< Rails Deep Dive: Loccasions, Making Events<< Loccasions: Pair ProgrammingLoccasions: Going Client-Side with Leaflet, Backbone, and Jasmine >>

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Scott Parrish

    I use guard-rails to run my rails server when developing. It detects when files in the areas mandating a server restart(config/ Gemfile, lib/…) and restarts the server automatically. There is also guard-process and guard-shell that can start and restart additional processes.

    That being said, I really like Foreman too, though personally I only use it for things that I’d use it for in production.

    • http://www.ruprict.net/ Glenn Goodrich

      Hey Scott,

      Yup, fair point about production and Foreman. Once I get to deployment (especially if I use Heroku) I’ll have to work some things out. Foreman suits my dev needs now, once I get to production concerns, things may change a bit.

      Thanks for the comment.

    • http://www.ruprict.net/ Glenn Goodrich

      BTW, this post mentions a strategy where you pass -f ./Procfile.production if you are in production. Not a bad strategy for managing the multiple environments.

  • http://blog.firsthand.ca Nicholas Henry

    Please to see you are keeping the series going, Glenn. Here’s something I came across in your original occasion_spec.rb:

    1) Occasion should belong to an event
    Failure/Error: @occasion = @event.occasions.build(Occasion.new)
    NoMethodError:
    undefined method `reject’ for #
    # ./spec/models/occassion_spec.rb:4:in `block (2 levels) in ‘

    The line #5: @occasion = @event.occasions.build(Occasion.new) is not valid. I realize that you switch this for a Factory eventually, but passing an instance of Occasion is incorrect, the #build method accepts a hash (or no param). In this case, the code should read:

    @event.occasions.build

    • http://blog.firsthand.ca Nicholas Henry

      Rather that should have been:

      @occasion = @event.occasions.build

  • http://blog.firsthand.ca Nicholas Henry

    I surprise you got a validation error when you created a new factory and it was loaded. Typically when a factory is not found/loaded you get a error like the following:

    Failure/Error: @occasion = Factory.create(:occasion)
    ArgumentError:
    Not registered: occasion
    # ./spec/models/occasion_spec.rb:3:in `block (2 levels) in ‘

  • http://blog.firsthand.ca Nicholas Henry

    The factory for occasion is missing the note attribute: causing the delete_occasions_spec to fail:

    note “Test Occasion”

    • http://www.ruprict.net/ Glenn Goodrich

      Fixed, thanks

  • http://blog.firsthand.ca Nicholas Henry

    For those following at home, after Glenn mentions:

    “I’ve written the spec/acceptance/add_occasions_spec.rb and delete_occasions_spec.rb.”

    You will need to do the following to complete the specs:

    * Amend events/show.html.haml with the Occasion form
    * Add occasions/_occasion.html.haml
    * Add the route for occasions

    Best to take a look at the source for these in the git repo: https://github.com/ruprict/loccasions

    • http://www.ruprict.net/ Glenn Goodrich

      Added update to post, complete with links to files on github.