240 likes | 308 Views
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
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!