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.
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)
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.
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 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.