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.
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…
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:
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.
Glenn works for Skookum Digital Works by day and manages the SitePoint Ruby channel at night. He likes to pretend he has a secret identity, but can't come up with a good superhero name. He's settling for "Roob", for now.