I have the following resources setup:
resources :sites do
resources :documents
# more nested here
end
resources :documents do
resources :notes, :except => [:show, :new, :edit]
end
I want the notes controller to have the document context. The problem is, the document controller itself depends on the site context. So the /document urls that are created from the above all throw a 500 error. I could adjust the controller code to handle this, but i wonder if there's a way to not create the /document urls, just: /document/#id/notes
For any one else that may have this problem You can restrict the routes you don't want by using :except just the way #agmcleod has for the :notes. So to restrict the document urls would either be:
resources :sites do
resources :documents, :except => [:index, :show, :new, :create, :edit, :update, :destroy]
# more nested here
end
or
resources :documents, :except => [:index, :show, :new, :create, :edit, :update, :destroy] do
resource :notes, :except => [:show, :new, :edit]
end
You can remove any of the actions as necessary.
Related
Assume a standard has_many :through relationship among three models
class Person < ActiveRecord::Base
has_many :memberships, :dependent => :destroy
has_many :clubs, :through => :memberships
end
class Club < ActiveRecord::Base
has_many :memberships, :dependent => :destroy
has_many :persons, :through => :memberships
end
class Membership < ActiveRecord::Base
belongs_to :person
belongs_to :club
end
In an API driven application, you would expect to expose URIs that:
list the clubs that person x belongs to
list the persons that are members of club y
(...and the usual collection of CRUD methods...)
My first thought is to implement a pair of nested routes that map to the MembersController, something like:
GET /clubs/:club_id/memberships => members_controller#index
GET /persons/:person_id/memberships => members_controller#index
... but here it gets a bit weird.
Both routes map to the same members_controller method (index). That's no problem -- I can look in the params hash to see if a :club_id or a :person_id is given, and apply appropriate scoping on the members_controller table.
But I'm not certain we want to expose Member objects to the end user at all. A more intuitive pair of routes (at least from the user's perspective) might be:
GET /clubs/:club_id/persons
GET /persons/:person_id/clubs
... which would return a list of persons and a list of clubs (respectively).
But if you do it this way, what controller and action would you map these routes to? Is there any convention in Rails that offers guidance? Or is this strayed far enough off the track that I should just implement it any way I see fit?
I've ended up implementing routes and controllers that accomplish the following:
It's fully RESTful.
It's fully symmetrical: /persons/:person_id/clubs/:club_id/memberships is identical to /clubs/:club_id/persons/:person_id/memberships
It's very DRY.
Ordinary Users never see a Membership :id. Instead, :person_id and :club_id serve as a compound key that refer to a specific membership.
It does allow direct access to Membership objects by :id, but that's reserved for Administrators.
Here are some of the routes it recognizes:
/persons/:person_id/clubs/:club_id/memberships - a specific membership association
/clubs/:club_id/persons/:person_id/memberships - equivalent
/persons/:person_id/clubs - all the clubs that a specific person belongs to
/clubs/:club_id/persons - all the persons that belong to a specific club
/memberships/:id - access to a specific membership (accessible only to admin)
Note that in the following examples, I have NOT included the Devise and CanCan constructs for authentication and authorization. They're easy to add.
Here is the routing file:
# file: /config/routes.rb
Clubbing::Application.routes.draw do
resources :persons, :except => [:new, :edit] do
resources :clubs, :only => :index
end
resources :clubs, :except => [:new, :edit] do
resources :persons, :only => :index
end
resources :memberships, :except => [:new, :edit]
# there may be clever ways to specify these routes using #resources and
# #collections and #member, but this ultimately is more straightforward
match "/persons/:person_id/clubs/:club_id/memberships" => "memberships#create", :via => :post
match "/persons/:person_id/clubs/:club_id/memberships" => "memberships#show", :via => :get
match "/persons/:person_id/clubs/:club_id/memberships" => "memberships#update", :via => :put
match "/persons/:person_id/clubs/:club_id/memberships" => "memberships#destroy", :via => :delete
match "/clubs/:club_id/persons/:person_id/memberships" => "memberships#create", :via => :post
match "/clubs/:club_id/persons/:person_id/memberships" => "memberships#show", :via => :get
match "/clubs/:club_id/persons/:person_id/memberships" => "memberships#update", :via => :put
match "/clubs/:club_id/persons/:person_id/memberships" => "memberships#destroy", :via => :delete
The controllers were surprisingly simple. For PersonsController and ClubsController, the only non-standard thing was in the :index methods, where we look for the presence of :club_id or :person_id in the parameters and scope accordingly:
# file: /app/controllers/persons_controller.rb
class PersonsController < ApplicationController
respond_to :json
before_filter :locate_collection, :only => :index
before_filter :locate_resource, :except => [:index, :create]
def index
respond_with #persons
end
def create
#person = Person.create(params[:person])
respond_with #person
end
def show
respond_with #person
end
def update
if #person.update_attributes(params[:person])
end
respond_with #person
end
def destroy
#person.destroy
respond_with #person
end
private
def locate_collection
if (params.has_key?("club_id"))
#persons = Club.find(params[:club_id]).persons
else
#persons = Person.all
end
end
def locate_resource
#person = Person.find(params[:id])
end
end
# file: /app/controllers/clubs_controller.rb
class ClubsController < ApplicationController
respond_to :json
before_filter :locate_collection, :only => :index
before_filter :locate_resource, :except => [:index, :create]
def index
respond_with #clubs
end
def create
#club = Club.create(params[:club])
respond_with #club
end
def show
respond_with #club
end
def update
if #club.update_attributes(params[:club])
end
respond_with #club
end
def destroy
#club.destroy
respond_with #club
end
private
def locate_collection
if (params.has_key?("person_id"))
#clubs = Person.find(params[:person_id]).clubs
else
#clubs = Club.all
end
end
def locate_resource
#club = Club.find(params[:id])
end
end
The MembershipsController is only slightly more complicated: it detects :person_id and/or :club_id in the parameters hash and applies scoping accordingly. If both :person_id and :club_id are present, we can assume it refers to a unique membership object:
# file: /app/controllers/memberships_controller.rb
class MembershipsController < ApplicationController
respond_to :json
before_filter :scope_collection, :only => [:index]
before_filter :scope_resource, :except => [:index, :create]
def index
respond_with #memberships
end
def create
#membership = scope_collection.create(params[:membership])
respond_with #membership
end
def show
respond_with #membership
end
def update
if #membership.update_attributes(params[:membership])
end
respond_with #membership
end
def destroy
#membership.destroy
respond_with #membership
end
private
# apply :person_id and/or :club_id scoping if present in params hash
def scope_collection
#memberships = scope_by_parameters
end
def scope_resource
#membership = scope_by_parameters.first
end
def scope_by_parameters
scope_by_param_id(scope_by_param_id(Membership.scoped, :person_id), :club_id)
end
def scope_by_param_id(relation, scope_name)
(id = params[scope_name]) ? relation.where(scope_name => id) : relation
end
end
addendum
(Not an answer -- just an observation). Most of this question can be re-framed as "how does REST handle compound keys?"
The REST philosophy is clear when you have a single ID to locate a resource such as /customers/2141. It's less clear what do do when a resource is uniquely defined by a compound key: in the example above, /clubs/:club_id and /persons/:person_id form a compound key that uniquely identifies a Membership.
I have not read Roy Fielding's thoughts on the matter. So that's the next step.
Without regard to how you'd do this in Rails, the key to doing this RESTfully is that you can have hyperlinks. What is a list of memberships from a person's perspective but a list of links to clubs they are members of? (Well, with some extra data for each.) What is a list of memberships to a club other than a list of links to people who are members (again, probably with some extra data)?
Given that, the memberships from a person's perspective is really a view on the Membership table (and similarly from the club's perspective; at this level of abstraction there's no difference). The tricky bit is that when you change the memberships of a person, you've got to push the changes through the view back into the underlying table. That would still be RESTful; having a RESTful resource does not mean that the resource never changes when not given a direct instruction to change. (That would prohibit all sorts of useful things, such as shared resources!) Indeed, the real meaning of REST in this area is that the client shouldn't assume that it can cache everything safely, not unless the hosting service explicitly says it can (via suitable metadata/HTTP headers).
In rails2, I was able to have code like this:
link_to(user.company.name, user.company)
which would map to:
/companies/id
but in rails 3, this same line of code throws a error stating:
undefined method `user_companies_path'
The obvious fix is to do something like:
link_to(user.company.name, company_path(user.company))
But I was wondering if anyone could explain the reason behind the change? The logic seemed a lot cleaner.
EDIT: Adding samples of my routes
In rails2, my routes looked like:
map.resources :users, :except => :edit, :member => { :details => :get }
map.resources :companies, :except => :edit, :member => { :details => :get }
In rails3, my routes are:
resources :users, :except => :edit do
member do
get :details
end
end
resources :companies, :except => :edit do
member do
get :details
end
end
The short answer is that the Rails 3 routing API bases your application on resources which is why these RESTful routes are being used, and also means that it does things like support constraints.
In Rails 2, you'd do:
resources :cars do
resource :models
member do
post :year
end
collection do
get :details
end
end
In Rails 3, you'd do:
map.resources :cars, :member => {:year => :post}, :collection => {:details => :get} do |cars|
cars.resource :model
end
You also have the :as key available which means you can then use named route helpers anywhere that url_for is available (i.e. controllers, mailers etc.)
I have routes like this:
namespace :admin do
resources :users, :only => :index do
resources :skills, :only => :index
end
end
resources :skills
In this case I got:
admin_user_skills GET /admin/users/:user_id/skills(.:format)
{:action=>"index", :controller=>"admin/skills"}
How to change nested route in order to point to SkillsController instead of Admin::SkillsController? I'd like to have this:
admin_user_skills GET /admin/users/:user_id/skills(.:format)
{:action=>"index", :controller=>"skills"}
Interesting thing - if we have no Admin::SkillsController, it will use SkillsController automatically, but only in development.
Using namespace in routes implies to have special directory for "namespaced" controllers, admin in your case. But if you use scope instead you have what you need:
scope '/admin' do
resources :users, :only => :index do
resources :skills, :only => :index
end
end
I have a resource database where resources can belong to different locations. Users and groups (self-referential user table) can have different roles on different locations. Groups can be inside other groups. Authorization works well for single users using 'if_attribute' to check if the location_id is among the location_ids that the user is allowed to show, edit, etc.:
has_permission_on :locations do
to [:show, :new, :create, :edit, :update, :destroy]
if_attribute :id => is_in { (user.permissions.where(:role => "admin").collect {|i| Location.find_by_id(i.location_id).subtree_ids}.flatten.uniq )}
end
Since the groups can be "nested" inside each other, I have figured that I'll have to use a recursive method to find all the "legal" location ids. I tried this:
has_permission_on :locations do
to [:show, :new, :create, :edit, :update, :destroy]
if_attribute :id => is_in { (user.permissions.where(:role => "admin").collect {|i| Location.find_by_id(i.location_id).subtree_ids} + find_group_location_ids(user)).flatten.uniq }
end
with the method defined outside the 'authorization do'-routine:
def find_group_location_ids(user)
location_ids = []
nested_group_location_ids(user)
def nested_group_location_ids(user)
user.group_memberships.each do |gm|
location_ids = location_ids + gm.user.location.id
nested_group_location_ids(gm.user)
end
end
return location_ids
end
The problem is that the method call doesn't find the method. I get this error:
NoMethodError (undefined method `find_group_location_ids' for (Authorization::Engine::AttributeValidator:0x7fb63448e2b0)
I have tried to place the method definition on a lot of different places, but with no luck.
How can I use if_attribute to see if an id is inside an array from a recursive method?
I got som help from steffenb at the declarative_authorization group at google groups (he is the author of declarative_authorization). The solution was to move the method to the user model:
def find_group_location_ids
location_ids = []
nested_group_location_ids(self)
def nested_group_location_ids(user)
user.group_memberships.each do |gm|
location_ids = location_ids + gm.user.location.id
nested_group_location_ids(gm.user)
end
end
return location_ids
end
and to call it in this way from autorization_rules.rb:
has_permission_on :locations do
to [:show, :new, :create, :edit, :update, :destroy]
if_attribute :id => is_in { (user.memberships.where(:role_id => "admin").collect {|i| Location.find_by_id(i.location_id).subtree_ids} + user.find_group_location_ids).flatten.uniq }
end
I want to add more methods to my rest.
Here is my routes.rb file:
resources :boards, :except => [:new, :create] do
get 'customize', :on => :member
get 'change_template', :on => :member
get 'all_walls', :on => :member
end
I am getting them in the following format:
change_template_board GET /boards/:id/change_template(.:format) {:action=>"change_template", :controller=>"boards"}
But I want them in this format:
/boards/:board_id/change_template/:id(.:format)
How can I do that?
I'm copying the first answer from this question. In your routes.rb, you can add a new route dooit to resource fifi by adding this to your routes.rb file:
resources :fifi do
member do
get :dooit
end
end
This will create the route dooit_fifi along with the standard fifi, fifi_index, new_fifi, and edit_fifi routes.
If you want to restrict the routes created, you can do something like this:
resources :fifi, only: [:show, :create, :destroy] do
member do
get :dooit
end
end
which will produce only the routes dooit_fifi, fifi, and fifi_index.