240 likes | 314 Views
Learn about routes, URL helpers, RESTful resource access, validations, and filters in Ruby on Rails programming. Understand how URLs map to controllers and actions, generate RESTful routes, and use model validations effectively.
E N D
Routes & REST & URL-helpers & validations & filters CS 98-10/CS 198-10 Web 2.0 Programming Using Ruby on Rails Armando Fox
Administrivia • Office hours switched (again...sorry) • Tuesdays 1:30-2:30 in 413 Soda • Monday: Project brainstorming • Weds: Project groups informally chosen-up • By following Monday: start planning your app (user stories, testing, deployment....) • Technical material: Routes & REST (today), more on controllers & views
Outline... • Routing basics: how do.... • URLs get mapped to a controller and action? • link_to, redirect_to etc. generate their URLs? • REST and RESTful routes • what is RESTful resource access? • how Rails 2.0 routing supports REST • URL helpers • generating URL’s (for your pages) that will map to specific controller actions...RESTfully • Further reading • “Rails Way” book has good content on this • “Agile” book has routes, but not 2.0 routes and REST • Model validations, callbacks, controller filters
$APP_ROOT/config/routes.rb • Ruby code (that makes use of high-level methods!) to declare “rules” for mapping incoming URLs to controllers/actions • actually each rule has 2 purposes: • map incoming URL to ctrler/action/params • generate URL to match ctrler/action/params • e.g. when using link_to, redirect_to, etc. • What’s in a rule? • A URL template • Keywords stating what to do
Simple example • In routes.rb: map.connect 'professors/:dept',:controller => 'professors', :action => 'list' • In one of your views: <%= link_to "List professors in EECS",:controller => 'professors', :action => 'list',:dept => 'eecs', :hired_since => 2005 %> • matching is determined by keywords • link_to uses underlying function url_for, which consults routing rules to build the URL: http://www.yourapp.com/professors/eecs?hired_since=2005
Simple example cont. • In routes.rb: map.connect 'professors/:dept',:controller => 'professors', :action => 'list' • Now if someone visits this URL: http://www.yourapp.com/professors/eecs • Matching is determined by position • How about: http://www.yourapp.com/professors/eecs?glub=1&hired_since=2006 • How about: http://www.yourapp.com/professors
Default routes • URL is compared to routing rules, one at a time, until match found • then “wildcard” pieces of URL get put into params[] • If no match, default route (last one in routes.rb) is used • typically something like:map.connect ':controller/:action/:id' • e.g., catches things like professors/edit/35 • Warning! Can lead to dangerous behaviors • Use the root route to map the “empty” URL (e.g. http://www.myapp.com): map.root :controller=>'main', :action=>'index'
More on Routes • Ordering of routes matters; more specific ones should come earlier so they’ll match first map.connect 'users/:action/:id' map.connect ':controller/:action/:id' • Many, many apps will never need to use more than the “conventional” predefined routes • If you want to, you should definitely read more about routes offline
REST is CRUD • REST Idea: each HTTP interaction should specify, on its own, a CRUD operation and which object to do it on. • GET used for read operations; POST for writes (create, update, delete) • Also guards against spidering/bots! • Rails 2.0: routes, scaffolds and URL helpers are now all RESTful by default • result: syntax of link_to, etc. has changed • Get them by sayingmap.resources :model
The DELETE & PUT hack • REST says: use HTTP method DELETE to request deletion; PUT to request Update • But: Web browsers only have GET and POST • “Solution”: use POST, but include extra field _method with value DELETE or PUT • routing takes care of parsing this out to disambiguate dispatching • done with JavaScript in link_to (but you shouldn’t be using link_to for this...why?) • or use button_to which creates a self-contained form for a single button
How url_for has changed Excerpted from REST Cheat Sheet which I’m trying to get a site license for (Peepcode) Note use of either _path or _url suffix
That whole thing withrespond_to do |format| • Let’s make it easier to read by: • put the if...elseoutside the do block • change variable name format to wants
Easier-to-read version • respond_to accepts a block, and yields to it passing the wants object • wants’s instance methods named for possible MIME output types (HTML, XML, etc.) • Each of those methods takes a block that specifies what to do for each format • Based on parsing HTTP headers from request • Many, many MIME types predefined (add more in environment.rb) • A useful one when we do AJAX: js (runs .rjs template if available)
What about redirect_to? • RESTful routing strikes again! • url_for and friends now assume RESTful routes by default • Hint: consider url_for(@student) • Or redirect_to(edit_student_path(@student)), etc.
Nested routes • Consider a Course that belongs_to :professor (and as well, Professor has_many :courses) • In particular, it makes no sense to have a course without a professor • in our app, I mean • When invoking CRUD methods on a course, we’d like to be able to specify which professor it belongs_to
Enter nested RESTful routes map.resources :professors do |p| p.resources :courses end —or— map.resources :professors, :has_many=>:courses • Now you can say course_path(:professor_id=>3, :id=>20) and get a RESTful URI for course ID 20 that belongs to professor ID 3. • Note! The route builder doesn’t check if the belongs_to relationship keys match what’s in the database!
In Courses controller... • Need to set up the @professor that owns the course in each of the methods • Otherwise the URL-builder methods won’t know what the parent object ID is • Ugly (we’ll learn a better way with filters): def index @professor = Professor.find(params[:professor_id]) @courses = @professor.courses.find(:all) ...etc... end def update ...if update of Course fails... redirect_to([@professor,@course]) end
What about views for nested models? # example: views/courses/edit.html.erb <% form_for([@professor,@course]) do |f| %> <%= f.text :description %> <%= f.text :ccn %> <%= f.submit "Save Changes" %> <% end %> <%= link_to "Show", [@professor,@course]) %> or...link_to "Show", course_path(@course) if makes sense <%= link_to "Back", professor_courses_path(@professor) %>
In your index (list) view... <% @courses.each do |c| %> <tr> <td> c.name </td> <td><%= link_to "Edit",edit_course_path([@professor,c]) %> </td> <td><%= link_to "Show",course_path([@professor,c])%></td> <td> <%= button_to "Destroy", course_path([@professor,c]),:method=>:delete %></td> </tr> <% end %> • Similarly for other views
Worth understanding... • Routing and REST caused lots of changes in 2.0, but ultimately they will make life better • Best tutorial we’ve found (thx Arthur!): http://www.akitaonrails.com/2007/12/12/rolling-with-rails-2-0-the-first-full-tutorial (Linked from course home page)
Controller predicates: verify • A declarative way to assert various preconditions on calling controller methods • You can check selectively (:only, :except) for: • HTTP request type (GET, POST, Ajax XHR) • Presence of a key in the flash or the session • Presence of a key in params[] • And if the check fails, you can... • redirect_to somewhere else • add_to_flash a helpful message • Example: verify :method => :post, :only => 'dangerous_action', :redirect_to => {:action => 'index'},:add_to_flash => "Dangerous action requires Post"
More General Filters • Code blocks that can go before, after or around controller actions; return Boolean before_filter :filter_method_name before_filter { |controller| ... } before_filter ClassName • options include :only,:except, etc. • multiple filters allowed; calls provided to prepend or append to filter chain • subclasses inherit filters but can use skip_filter methods to selectively disable them • If any before-filter returns false, chain halted & controller action method won’t be invoked • so filter should redirect_to, render, or otherwise deal with the request • Simple useful example: a before-filter for nested routes! before_filter :load_professor def load_professor @professor = Professor.find(params[:professor_id]) end
A General Pattern:“Do It Declaratively” • More and more ways to specify what should be done rather than how to do it • Should always be asking yourself this question • Especially when you find yourself (re)writing common code in multiple places!