Suppose I have a polymorphic structure like this.
map.resources :bar, :has_many => :foo
map.resources :baz, :has_many => :foo
map.resources :qux, :has_many => :foo
class Foo
belongs_to :parent, :polymorphic => true
end
class FooController < AC
before_filter :find_parent
...
private
def find_parent
# ugly way:
#parent = if params[:bar_id]
Bar.find(params[:bar_id])
elsif params[:baz_id]
Baz.find(params[:baz_id])
elsif params[:qux_id]
Qux.find(params[:qux_id])
end
end
end
That's pretty ugly. Whenever we add a new thing that it might belong to, we need to add it un-DRYly to that before_filter.
It gets worse, too. Suppose that Foos are really polymorphic things that could show up anywhere, like comments or tags. And suppose that you have the following routes:
map.resources :bar, :has_many => :foo do |bar|
bar.resources :baz, :has_many => :foo
end
map.resources :qux, :has_many => :foo do |qux|
qux.resources :baz, :has_many => :foo
end
... now we have to worry about whether to check for bar_id or baz_id first.
For more complex resources, it's possible that this won't even be enough to be sure you get the parent's id.
What would be ideal is if we could do something like this:
def get_parent
# fetch the parameter that immediately preceeded :id
#parent = if x = params.before(:id)
# polymorphic find
x.key.to_s[0..-4].classify.constantize.find x.value
end
end
After all, our routes already encode the parent by virtue of the order of parameters in the URL. Why discard that information?
So: how can this be done?
You should be able to ask for foo.parent
You'll need to have something like this:
class Bar < ActiveRecord::Base
has_many :foos, :as => :parent
...
end
Related
I am converting a User object to json via:
user.to_json :methods => :new_cookies
the new_cookies method is:
cookies.all :include => :fortune, :conditions => {:opened => false}
This embed the cookies inside the user json object, but I want fortune to be embedded inside the cookie object as well. I passed inside :include => :fortune but that doesn't that work.
Is this possible?
Models:
class User < ActiveRecord::Base
has_many :cookies
has_many :fortunes, :through => :cookies
def new_cookies
cookies.all :include => :fortune, :conditions => {:opened => false}
end
end
class Cookie < ActiveRecord::Base
belongs_to :user
belongs_to :fortune
end
class Fortune < ActiveRecord::Base
serialize :rstatuses
serialize :genders
has_many :cookies
has_many :users, :through => :cookies
end
I am not sure that the :includes => :fortune option works as you expect (or perhaps at all) -- near the end of this section of the current Rails guides it mentions this option for finder methods other than .all.
I assume it works similarly to the new Active Relation query interface, e.g. Cookies.include(:fortunes).where(:opened => false) -- in this case, Rails "eager loads" the related records, meaning fortunes are fetched as part of the query for cookies. This is a performance enhancement, but doesn't otherwise change the behavior of Rails.
As I noted in the comments, I think as_json will do what you want -- it defines what is and is not part of the object when serialized using to_json. You specify methods that should be called in addition to (or to exclude) the methods of the data-backed object itself, for example, your new_cookies method.
In this example, I have added as_json to User (which gets Cookie) and also to Cookie in hopes that each cookie will nest its fortunes in its JSON. See below for an alternative.
class User < ActiveRecord::Base
has_many :cookies
has_many :fortunes, :through => :cookies
def as_json(option = {})
super(:methods => :new_cookies)
end
def new_cookies
cookies.all :conditions => {:opened => false}
end
end
class Cookie < ActiveRecord::Base
belongs_to :user
belongs_to :fortune
def as_json(options = {})
super(:methods => :cookie_fortune) # or perhaps just :fortune
end
def cookie_fortune
self.fortune
end
end
In a case where I was writing an API and didn't need to reflect the nested relationships between objects in the JSON, in the controller, I used something like
respond_to do |format|
format.html
format.json { render json: { :foo => #foo, :bar => #bar }}
end
to produce parallel nodes (objects) in the JSON.
I can implement reverse relationships, so if UserA adds UserB, then it shows UserA in B's profile, and visa versa.
But I cannot figure out how to let UserB remove UserA as a friend, if UserA added UserB.
I've tried so many different ways, but everytime I change something it moves the problem elsewhere! I can't tell if the fundamental issue is:
a. how the FriendshipsController destroy method is defined
b. whether I need another controller specifically just to handle
InverseFriendships destroy
c. if I need to customize the routes
d. if all the above are ok, but the code I have in my views (specifically
the _suggested_connections partial) is calling the wrong controller
and/or route
e. or none of the above.
Code snippets below:
class FriendshipsController < ApplicationController
def destroy
#friendship = current_user.friendships.find(params[:id])
#friendship.destroy
flash[:notice] = "Removed friendship."
redirect_to current_user
end
In the view
<% #user.inverse_friends.each do |inverse_friendship| %>
<li>
<%= inverse_friendship.name %>
<%= link_to "remove", #user.inverse_friendships, :method => :delete, :class => "btn-small btn-danger" %><br />
<%= image_tag inverse_friendship.avatar(:thumb) %>
My models:
class Friendship < ActiveRecord::Base
belongs_to :user
belongs_to :friend, class_name: 'User'
attr_accessible :friend_id, :user_id
end
class User < ActiveRecord::Base
has_many :friendships, dependent: :destroy
has_many :friends, through: :friendships
has_many :inverse_friendships, dependent: :destroy, class_name: "Friendship", foreign_key: "friend_id"
has_many :inverse_friends, through: :inverse_friendships, source: :user
And routes:
resources :friendships
authenticated :user do
root :to => 'home#index'
end
root :to => "home#index"
devise_for :users, :controllers => { :registrations => :registrations }
resources :users
Your main problem is a:
a. how the FriendshipsController destroy method is defined
You're looking for the friendship in the current_user.friendships, but it's not there. It's in inverse_friendships.
You'd need to either check both associations, or let the controller know which one you're looking for. The latter is probably preferable since although they are the same class, they are different resources. Something like this maybe:
# In routes, route inverse friendships to the same controller, but with a
# different path (I'm routing everything here, you may not need that.)
resources :friendships
resources :inverse_friendships, :controller => 'friendships'
# Then in your friendships controller, use the path to determine which
# collection you're working with:
#
def destroy
#friendship = collection.find(params[:id])
# ...
end
# the other collection methods would use the same collection, if you needed them,
# for example:
def create
#friendship = collection.build(params[:friendship])
# ..
end
protected
# simple case statement here, but you get the idea
def collection
case request.path
when /\/inverse_friendships/ then current_user.inverse_friendships
else current_user.friendships
end
end
Finally in your view you'd route to an inverse friendship like:
<%= link_to "remove", inverse_friendship_path(friendship), :method => :delete %>
A normal friendship could use the shorter form, or the full named route:
<%= link_to "remove", friendship, :method => :delete %>
OR
<%= link_to "remove", friendship_path(friendship), :method => :delete %>
EDIT: Searching both associations.
Of course if you wanted to keep it simple, and had no other use for inverse_friends being a separate resource, you could always just...
def destroy
id, cid = params[:id], current_user.id
# search both associations (two queries)
#friendship = current_user.friendships.find_by_id(id) ||
current_user.inverse_friendships.find(id)
# or query friendship looking for both types
#friendship = Friendship.
where("user_id = ? OR friend_id = ?", cid, cid).find(id)
# ...
end
I have the following route definition:
resources :documents do
collection do
post :filter
end
end
and the following model structure:
class Document < ActiveRecord::Base
belongs_to :documentable, :polymorphic => true
end
class User < ActiveRecord::Base
has_many :documents, :as => :documentable
end
and controller structure:
class DocumentsController < ApplicationController
def index
# not important
end
def filter
# not important
end
end
I can easily in a view say:
polymorphic_path([#user, Document])
to get the path /users/1/documents, but I want to be able to say:
filter_polymorphic_path([#user, Document])
to get the path /users/1/documents/filter, unfortunately, this doesn't work.
Anyone know how I can pull this off without adding the following to my routes, for each of my documentable models:
resources :users do
resources :documents do
collection do
post :filter
end
end
end
polymorphic_path([#user, Document], :action => 'filter') gives you /users/:user_id/documents/filter.
Also, polymorphic_path([#user, Document], :action => 'filter', :sort_order => 'this-order') gives you /users/:user_id/documents/filter?sort_order=this-order.
I ran into the same problem thinking you can replace the edit in edit_polymorphic_path to whatever method you want.
See: http://api.rubyonrails.org/classes/ActionDispatch/Routing/PolymorphicRoutes.html
This would do it, and it reads nicely.
polymorphic_path([:filter, #user, Document])
Or these
polymorphic_path([:filter, #user, :documents])
polymorphic_path([:filter, #user, Document.new])
And with a query param
polymorphic_path([:filter, #user, Document], :q => 'keyword')
And, in a view you can also do this:
= link_to "Documents", [[:filter, #user, :documents], :q => 'keyword']
I have a problem with 3 levels nesting of models in CanCan combined with Inherited Resources. I've read that we should nest everything up to 2 levels, but I had to put everything under account model and now I've tried doing this in CanCan:
load_and_authorize_resource :account
load_and_authorize_resource :project, :through => :account
load_and_authorize_resource :model, :through => :project
That gives me #account variable that has a value of #project, like it is overwriting that. #project is what is supposed to be and #model too. Is that fault of mine, CanCan's, Inherited Resources or just CanCan isn't supporting 3 levels nesting? Also, I do this in IR for the ModelsController.
belongs_to :account, :finder => :find_by_name! do
belongs_to :project, :finder => :find_by_name!
end
Another strange thing is when i remove the part load_and_ from CanCan's definition. It works then, but I've read that it can be dangerous not to use the load part.
Can I use only the authorize_resource or should I do something with CanCan?
Your authorizations have been correct as far as I can say.
The developer of the CanCan gem ryan posted how this should behave: https://github.com/ryanb/cancan/issues/127#issuecomment-364475
That means that your
load_and_authorize_resource :account
load_and_authorize_resource :project, :through => :account
load_and_authorize_resource :model, :through => :project
will end up in an block like this (here: create action. For other actions should the last authorize! and the #model change):
#account = Account.find(params[:account_id])
authorize! :read, #account
#project = #account.projects.find(params[:project_id])
authorize! :read, #project
#model = #project.models.build
authorize! :new, #model
I hope that this answer can help developers looking for nested cancan authorization :-) .
source: https://github.com/ryanb/cancan/issues/127#issuecomment-364475
ps: wrong behavior for /accounts/1/projects/2/models/new:
load_and_authorize_resource :project
load_and_authorize_resource :model, :through => :project
This is kind of a security issue, because this will do
#project = Project.find(params[:project_id])
[...]
, and does not check if the current account is allowed to read the linked account '1'.
And it does not check, if the project '2' is really a project of account '1'.
I have this Rails model: (Parameters removed for clarity)
class Folder < ActiveRecord::Base
belongs_to :parent, :class_name => :folder
has_many :children, :class_name => :folder
end
I want this model to be used like a file system folder. How do I have to configure the routes and the controller to make this possible?
1) As for the model: check out acts_as_tree
2) As for the routes: do something like
map.folder '/folders/*path', :controller => 'folders', :action => 'show'
and in the FoldersController,
def show
# params[:path] contains an array of folder names
#folder = Folder.root
params[:path].each |name|
#folder = #folder.children.find_by_name(name) or raise ActiveRecord::RecordNotFound
end
# there you go, #folder contains the folder identified by the path
end