polymorphic_path for custom collection route - ruby-on-rails

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']

Related

Deleting a 'friend' that added you using self-referential association

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

Rails has_and_belongs_to_many association

so I have two models:
class User < ActiveRecord::Base
has_and_belongs_to_many :followed_courses, :class_name => "Course"
end
class Course < ActiveRecord::Base
has_and_belongs_to_many :followers, :class_name => "User"
end
in User.rb, I also have:
def following_course?(course)
followed_courses.include?(course)
end
def follow_course!(course)
followed_courses<<course
end
def unfollow_course!(course)
followed_courses.delete(course)
end
I don't have a courses_users model, just a join table(courses_users). I guess I'll have to follow/unfollow a course in CoursesController. Do I create a new action in the controller?
Right now, I have a follow form in the courses/show page when the course is not followed
= form_for #course, :url => { :action => "follow" }, :remote => true do |f|
%div= f.hidden_field :id
.actions= f.submit "Follow"
And I have in CoursesController:
def follow
#course = Course.find(params[:id])
current_user.follow_course!(#course)
respond_to do |format|
format.html { redirect_to #course }
format.js
end
end
But looks like it was never activated. Do I need to modify the route so the action will be activated? How to modify it? Or is there a better way to do this? Can I replace the form with just a link? Thanks in advance!
This is a follow-up of a related question rails polymorphic model implementation
THe routing system invokes CoursesController#follow? If not you have to write in the routes.rb file the following line:
map.resources :courses, :member => {:follow => :post}
#You'll have the map.resources :courses, just add the second argument.
After that the routing system could redirect to that action, and it will you give the follow_course_url(#course) helper

Whats wrong with this form routing?

I have a users model and a book model. Users can read books (as a reader) which creates an entry in the Readings model:
id | reader_id | book_id
Users also have a list of books that they have read. These are stored in the Red (I use Red because the present and past tense of the word 'read' are the same) model which looks the same as the Reading model above.
Now when a user is reading a book, I would like to display a button which represents finishing the book.
The finish action is in the ReadingsController and looks like this:
def finish
#book = current_user.readings.find(params[:id]).book
current_user.stop_reading!(#book)
current_user.make_red! #book
redirect_to :back
end
As you can probably tell, this takes in the id of a record in the readings table, destroys it and makes a new record in the table for recording books red.
The form helper for the "Finish Reading" button currently looks like this:
<%= form_for :reading, current_user.readings.find_by_book_id(book.id), :url => { :controller => :readings, :action => "finish" }, :method => :delete do |f| %>
<div class="actions"><%= f.submit button_text %></div>
<% end %>
But for some reason this renders a form with the wrong id because "9781440506604" is not the id of a record in the readings table, it's the id of a record in the books table (the ISBN-13 of a book to be precise).
<form accept-charset="UTF-8" action="/readings/9781440506604/finish" method="post">
</form>
What is it I'm doing wrong?
EDIT to add reading.rb
class Reading < ActiveRecord::Base
attr_accessible :book_id
# one person reading a new book may cause feed_item creations in multiple users feeds
has_many :feed_items, :as => :event
has_many :comments, :as => :parent, :dependent => :destroy
scope :from_users_followed_by, lambda { |user| followed_by(user) }
# need to pass the class name here because there is no Reader model
belongs_to :reader, :class_name => "User"
belongs_to :book
validates :reader_id, :presence => true
validates :book_id, :presence => true
def self.followed_by(user)
...
end
end
# and user.rb
class User < ActiveRecord::Base
attr_accessible :name, :email, :password, :password_confirmation, :avatar, :remember_me, :avatar_url
has_many :readings, :dependent => :destroy,
:foreign_key => "reader_id"
has_many :reads, :through => :readings, :source => :book
has_many :reds, :foreign_key => "reader_id",
:dependent => :destroy
has_many :red, :through => :reds, :source => :book
def reading? book
self.readings.find_by_book_id(book)
end
def read! book
self.readings.create!(:book_id => book.id)
end
def stop_reading! book
self.readings.find_by_book_id(book).destroy
end
def red? book
self.reds.find_by_book_id(book)
end
def make_red! book
unless red? book
self.reds.create!(:book_id => book.id)
end
end
end
By the way I tried making a user who is reading book 1 and doing user.readings.find_by_book_id(1) in the console and it returns a record from the readings table.
as requested
# routes.rb
resources :readings, :only => [:create, :destroy, :show] do
member do
post :create_comment
delete :finish
end
end
Looks like you have got to_param method in your Reading model
try to call id clearly:
current_user.readings.find_by_book_id(book.id).id
UPD
remove :only => [:create, :destroy, :show] from your routes
use this <%= form_for :reading, current_user.readings.find_by_book_id(book.id), :url => { :controller => :readings, :action => "finish", :id => current_user.readings.find_by_book_id(book.id).id }, :html => {:method => :delete} do |f| %>
I'm not particularly knowledgeable about rails 3 (still using rails 2), but shouldn't you be passing more information to the :url param?
This doesn't seem to mention anything about the ID you want to post to:
:url => { :controller => :readings, :action => "finish" }
Shouldn't it be something closer to this:
:url => { :controller => :readings, :action => "finish", :id => reading_id }
(Assuming reading_id to be substituted for the actual ID)

Route alias, is it possible?

I have a Vehicle model:
Routes:
map.resources :vehicles, :has_many => :suppliers
Everything works great, but Vehicle has a boolean attribute is_truck. I want to make an Alias so I can get the same resources filtering only trucks, I tried with:
Routes:
map.trucks '/trucks', :controller => :vehicles, :action => :index, :is_truck => true
map.trucks '/trucks/by_supplier/:supplier', :controller => :vehicles, :action => :index, :is_truck => true
The first one works well, but when I search within a Form the second doesn't work and searches all suppliers.
Controller:
class VehiclesController
def index
if params[:supplier]
#vehicles = Vehicle.all :conditions => { :is_truck => params[:is_truck] }
else
#vehicles = Vehicle.all
end
end
...
end
Search Form:
<% form_for :truck, :url => {:controller => :trucks, :action => :index}, :html => {:method => :get} do |f| %>
<% f.text_field :search %>
<% f.submit 'Search Trucks' %>
<% end %>
Is it possible to map.resources as an alias ?
I found a cleaner way to do it, but Search is still broken under a specific supplier:
# Show all vehicles
map.connect '/vehicles/supplier/:supplier', :controller => :vehicles, :action => :index
map.resources :vehicles
# Only show trucks
map.connect '/trucks/supplier/:supplier', :controller => :vehicles, :action => :index, :is_truck => true
map.resources :vehicles, :as => 'trucks', :requirements => { :is_truck => true }
Resource: http://api.rubyonrails.org/classes/ActionController/Resources.html
Just amend your routes in the following way:
map.resources :vehicles, :has_many => :suppliers,
:collection => { :trucks => :get }
And check rake routes for the routes this generates. It will allow you to list vehicles which are trucks:
trucks_vehicles GET /vehicles/trucks(.:format)
{:controller=>"vehicles", :action=>"trucks"}
So you now just need to add a new action called "trucks" which works similar to "index". Forms should keep track on it's own (via form fields) if you create a truck or another vehicle. Don't try to trick around with rails routing, which usually would mean your app design is flawed which will get you into trouble later.
You may take a look at STI (single table inheritance: one table stores multiple classes of vehicles). Another way would be to create a trucks controller which inherits from the vehicles controllers and overwrites just some methods like so:
class TrucksController < VehiclesController
def new
#is_truck = true
super
end
...
end
or
class TrucksController < VehiclesController
before_filter :this_is_a_truck
...
private
def this_is_a_truck
#is_truck = true
super
end
end
Update: Here's another one (given you have a is_truck column):
class TrucksController < VehiclesController
around_filter :with_truck_scope
...
private
# Scope every active record access with an is_truck condition
# you may want to put this directly into the model to get rid of the .send
# method and directly access "Vehicle.with_truck_scope &block" here
def with_truck_scope(&block)
Vehicle.send :with_scope, :find => { :conditions => "is_truck = 1" },
:create => { :is_truck => 1 }, &block
end
end
But I recommend really first to try out going with the :collection and :member parameters of Rails' routing.
are you processing it in your controller?
smth like:
class VehiclesController
def index
if params[:supplier]
#vehicles = Vehicle.all :conditions => { :supplier_id => params[:supplier] }
else
#vehicles = Vehicle.all
end
end
end
Not sure if it's possible to make an alias but at least you can try to swap your routes:
map.trucks '/trucks/by_supplier/:supplier', :controller => :vehicles, :action => :index, :is_truck => true
map.trucks '/trucks', :controller => :vehicles, :action => :index, :is_truck => true
Since routes works like 'first added - has higher priority' your solution could fail since map.trucks '/trucks' catch '/trucks/by_supplier/:supplier' as well. Also I'd recommed to refactor it a bit:
map.with_options :controller => :vehicles, :action => :index, :is_truck => true do |v|
v.trucks '/trucks/by_supplier/:supplier'
v.trucks '/trucks'
end
I recommend you to use another resource, just adding:
map.resources :vehicles, :as => :trucks, :has_many => :suppliers
Then handle it in your controller with something like:
def index
conds = {}
conds = { ... } if request.uri =~ /trucks/ # you can be more specific about the regexp if you need to
#vehicles = Vehicle.all :conditions => conds
end
What do you think about it?

How can I handle a folder-like model structure?

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

Resources