Rails Deep Dive: Loccasions, Making Events

Share this article

Our last post flushed out the Events model and created a very basic home page. Hopefully, by the end of this post, we will be able to add, modify, and delete events from our user home page.

CRUDdy Events

Unless you’ve just been unfrozen from a decades long, icy slumber, you know what CRUDifying a model entails. In Rails land, more often than not, a model is transformed into a RESTful resource. (Note: If you don’t have a great grasp on REST, I would HIGHLY recommend the O’Reilly book RESTful Web Services, I won’t be covering REST in detail in this series.) For the Event model, we’ll need HTTP endpoints that allow us to add (POST), modify (PUT), retrieve (GET), and delete (Um, DELETE) events.

UPDATE: An alert reader pointed out that I have forgotten to add the event routes to the routes.rb file.  So, add this to config/routes.rb

resources :events

Which will generate all the RESTful routes needed for events.

Creating Events

Since an Event is such a simple object, we can place the form for creating them right on our user home page. With the agility of a cat, I jump back into MockupBuilder and change our events page to add a form at the bottom.

Add Event

Time to write a test to fill in that form. I have added a spec/acceptance/add_events_spec.rb with

require 'spec_helper'
feature 'Add Events', %q{
  As a registered user
  I want to add Events
} do
  background do
    login_user Factory(:user)
  end
  scenario "Add Basic Event" do
    fill_in "Name", :with => "New Event"
    fill_in "Description", :with => "This is my new event"
    click_button "Create Event"
    page.should have_content("New Event")
    page.should have_content("This is my new event")
    page.should have_selector("ul > li")
  end
end

Running this spec results in complaints about ElementNotFound because we haven’t created our form yet. So, let’s add a form to our app/views/events/index.html.haml (add to the end of the file)

= form_for Event.new do |f|
  = f.label :name
  = f.text_field :name
  = f.label :description
  = f.text_field :description
  = f.submit

Since we want this form “inline” we need to override the base styles. In the app/assets/stylesheets/events.css.scss file add

.new_event label, input[type='text'] {
  display:inline;
}
input#event_description {
  width: 500px;
}

(Note: Rails adds the new_event class to the form). Now, the spec complains about there being no create action on EventsController. Okey dokey, we can add that (in app/controlles/events_controller.rb)

def create
  event = current_user.events.build(params[:event])
  event.save
end

Now the spec says we don’t have a events/create template, which is true. But, we don’t want that template, we want the create action to just render the home page. So, for now, let’s redirect to the events index page. I added

page.current_path.should == events_path

to our spec and

redirect_to events_path

to the EventsController#create method. The spec complains about the description text not being on the page. Oops, looks like I neglected that when I created the list of events. Change the app/views/events/index.html.haml to

%h2 Your Events

map.sixteen_columns

%ul#events
  - for event in @events
    %li
      %span.event_name= event.name
      %span.event_description= event.description
= form_for Event.new do |f|
  = f.label :name
  = f.text_field :name
  = f.label :description
  = f.text_field :description
  = f.submit

and the spec passes.

At points like this, I like to fire up the server (rails s) and look around. The first thing I notice is, when I am signed in and on the home page, there is no clear navigation to the user events page. Also, when I am on the user events page, the list of events looks like crap. I am not too hung up on the latter issue, but I’d like to get some visual clues in there to make them a bit nicer for now. Finally, the space where our map is going to go is a big void, but I am not going to deal with that until we get to creating Occasions.

Clean up the Signed In Navigation

Dealing with the first issue from above, when a user is signed in there should be a link to get to the users events. I am going to call it “My Events”. First, though, we need to write a test to make sure it’s there. In fact, we can just add it to the “Successful Sign In” scenario in spec/acceptance/sign_in_spec.rb

current_path.should == user_root_path # This line already exists
page.should have_selector("a", :text => "My Events", :href => user_root_path)

My Events Link Spec

Making this spec pass is simply a matter of adding this “My Events” link to the sign_in conditional in the app/views/layout/application.html.haml file. (Note: just add the lines commented with #Add this line)

-if user_signed_in?
      Hullo #{current_user.name}
      | <br>
      = link_to "My Events", user_root_path   #Add this line
      |                                       #Add this line
      = link_to "Sign Out", destroy_user_session_path, :method => :delete
    - else
      = link_to "Sign In", new_user_session_path

and the spec passes. If you want to fire up the server and look at our new sign-in area, go for it.

Adding More CRUD to Events

We can create and retrieve Events, but we can’t edit or delete events. Let’s add the ability to do that now. My initial thoughts with editing are to just load the “selected” event into the same form we are using to create events. I doubt it will stay this way, but it gives us a quick way to test the update capabilities. Using this workflow, the edit action of our EventsController will fetch the selected Event from the database and hydrate a @event instance variable. The edit view will then render that Event information into the form. A significant takeaway, then, is that our user events page is also going to be our edit view. Also, the user will need a way to “select” a particular event. For now, we’ll make the Event name in the list of events a hyperlink that fires the event view. Let’s write some more tests to flush this out. I have created a spec/acceptance/edit_events_spec.rb file with:

require 'spec_helper'

feature 'Select Event', %q{
  As a registered user
  I want to select an Event
} do
  background do
    @user = Factory(:user)
    @event = Factory(:event, :user => @user )
    login_user @user
  end

scenario "Select Event" do
    page.should have_selector("a", :text=> @event.name)
    click_link @event.name
    page.should have_selector("li.selected", :text=> @event.name)
    page.should have_selector("input[name='event[name]']", :value => @event.name)
    page.should have_selector("input[name='event[description]']", :value => @event.description)
  end
end

Of course, the spec fails because there is no link with “Test Event” (remember, that’s our factory Event object name) on the page. Open app/views/events/index.html.haml and change:

%span.event_name= event.name

to

%span.event_name
  = link_to event.name, edit_event_path(event)

The spec complains The action 'edit' could not be found for EventsController, which makes sense. So, add an edit method to the EventsController.

def edit
end

And now, the spec complains about the edit template missing. As a quick tangent, Rails tells you where it looked, and you can see that Devise has modified our views search path…pretty cool.

Where’s the edit_template

As I previously mentioned, we aren’t going to have a separate edit template, but rather, we are going to use our existing events index template and just load the selected event into an instance variable.

def edit
  @events = current_user.events
  @event = @events.find(params[:id])
  render 'index'
end

The next issue is that there is no li with a class selected, so open up the events index template and change it to:

%h2 Your Events

map.sixteen_columns

%ul#events
  - for event in @events
    %li{:class => @event == event ? :selected : nil}  #FIRST
      %span.event_name
        = link_to event.name, edit_event_path(event)
      %span.event_description= event.description
= form_for @event || Event.new do |f|         #SECOND
  = f.label :name
  = f.text_field :name
  = f.label :description
  = f.text_field :description
  = f.submit

In the interest of time, I’ve made all the changes to make our spec pass. First, the events loop checks to see if the current event matches our @event instance variable and adds the selected CSS class name to the list item when it does. Second, we have the form_for test for the existance of the @event instance variable, and fall back to a plain Event.new if it’s not there.

The spec now passes. We can select an event, which loads it into the form. Test HO! (By “HO!” I mean, add this feature to the same edit_event_spec.rb file)

feature 'Edit Event', %q{
  As a registered user
  I want to edit a selected Event
} do
  background do
    @user = Factory(:user)
    @event = Factory(:event, :user => @user )
    login_user @user
    click_link @event.name
  end

  scenario "Edit Event" do
    fill_in "Name", :with=> "Edited Event"
    click_button "Update Event"
    page.should have_selector("a", :text => "Edited Event")
  end
end

You can see that we select our event in the background block (sniff, sniff, I smell helper….), followed by the scenario of changing the Event’s name. This spec fails because there is no update action on EventsController. Just like the edit action, we need to add our new action to the controller. However, before we do that, I want to point out something cool that Rails just did for us, free of charge. Notice in the “Edit Event” scenario, we look for an “Update Event” button. However, we haven’t put any code in the view to differentiate between a create form and an update form. Rails and form_for do this for us, making the form do the right thing based on the object passed into it. Some of the little things Rails does, like this, makes me want to give it a great big hug.
Adding the update action follows the same steps as adding the edit action. Add the empty update method to EventsController, watch the spec complain about the missing update template, then add the guts to the update method, redirect to the index view, and rock out. The redirect is a slight difference, because after an update we just want to go back to the events page to refresh our updated event in the events list.

def update
  event = current_user.events.find(params[:id])
  event.update_attributes(params[:event])
  event.save
  redirect_to events_path
end

Specs pass and we can create and update Events. Progress is fun.

MUST DESTROY EVENTS

Once Loccasions can destroy events, we’ll be done with the manipulation of events. First, we’ll need something for the user to indicate that an event is a goner. A “Delete” button sounds like a good start, as does writing a test for said button. Here’s the spec:

require 'spec_helper'

feature "Delete Event", %q{
  As a registered user,
  I want to delete an event
} do
  background do
    Capybara.current_driver = :selenium   #FIRST
    @user = Factory(:user)
    @event = Factory(:event, :user => @user, :name=>"Dead Event Walking")
    login_user @user
  end

  after do                      #afterFIRST
    Capybara.use_default_driver
  end

  scenario "Delete Event" do
    page.should have_content("Dead Event Walking")
    page.should have_selector("form[action='/events/#{@event.id}'] input[value='delete']") #SECOND
    # auto confirm the dialog
    page.execute_script('window.confirm = function() {return true;}')   #FIRST
    click_button "X"
    page.should_not have_content("Dead Event Walking")
  end
end

I’ve done that thing where I jump ahead a bit with the “Delete Event” spec, so I’ll try to explain what is happening. FIRST, I am switching the test_driver for Capybara to Selenium, telling the page to just auto confirm all dialogs, and then switching back to the default test driver. SECOND, you might be wondering why I am testing for the existing of a form with those strange attributes. Due to Rails RESTful nature, the destroy route for a resource requires the use of the HTTP DELETE method. The only way to perform an HTTP request that does not use GET is to use a form. However, support for the HTTP DELETE method amongst browsers is spotty and HTML5 support is up in the air, so we need a convention. A current convention, and what Rails will do for you, is to create a POST form including a hidden input called _method that has the value of the HTTP verb we want to use. The Rails routing middleware then checks for that parameter and routes the request appropriately. That’s why I wrote this spec that way, even if it goes a bit deeper than a normal accepetance test might. Again, this test will likely change down the road.
The spec, of course, will complain about the view not having a form with those attributes. Here’s our new index view:

%h2 Your Events

map.sixteen_columns

%ul#events
  - for event in @events
    %li{:class => @event == event ? :selected : nil}
      %span.del_form
        =button_to "X", event, :confirm => "Are you sure?", :method => :delete
      %span.event_name
        = link_to event.name, edit_event_path(event)
      %span.event_description= event.description
      %div.clear
= form_for @event || Event.new do |f|
  = f.label :name
  = f.text_field :name
  = f.label :description
  = f.text_field :description
  = f.submit

If you ran rails s now, signed in, and went to the user events page, you could see (provided an Event exists) the delete button. Viewing the source of that page shows the delete form:

<span class='del_form'>
  <form method="post" action="/events/4e67812841574e0462000002"  class="button_to">
    <div>
      <input name="_method" type="hidden" value="delete" />
      <input data-confirm="Are you sure?" type="submit" value="X" />
      <input name="authenticity_token" type="hidden" value="..elided.." />
    </div>
  </form>
</span>

Rails is giving us all kinds of help here: the hidden _method input, the data-confirm attribute for our confirmation box, and an authenticity_token to help avoid cross-site scripting attacks. And what did YOU get Rails? Nothing, eh?
Run the spec, and we get the familiar complaint about EventsController missing an action, destroy in this case. At this point, you should know what’s coming. Add the blank method, watch it fail, add the destroy and redirect logic, watch it pass, and, finally, feel good about yourself. Once you’ve added the destroy method:

def destroy
  event = current_user.events.find(params[:id])
  event.destroy
  redirect_to events_path
end

all specs will pass. You may have been a bit startled by the browser popping up when you ran the specs, eh? That’s Selenium, and it’s awesome. (But, we’ll probably get rid of it later…) So, if you fire up the server, you should be able to add, modify, and destroy events. Next time, we’ll add Occasions and, maybe (DUN DUN DUUUUUN) the map. Oh, and don’t forget to:

git add .
git commit -am "CRUDed events"
git push origin adding_events
git checkout master
git merge adding_events
git push origin master
Glenn GoodrichGlenn Goodrich
View Author

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.

loccasions
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week