I have a Parent model which has Children. If all the Children of a certain Parent are deleted, I'd like to automatically delete the Parent as well.
In a non-AJAX scenario, in the ChildrenController I would do:
#parent = #child.parent
#child.destroy
if #parent.children.empty?
redirect_to :action => :destroy,
:controller => :parents,
:id => #parent.id
end
But this is impossible when the request is XHR. The redirect causes a GET request.
The only way I can think of to do this with AJAX is add logic to the response RJS, causing it to create a link_to_remote element, "click" it, and then remove it. It seems ugly. Is there a better way?
Clarification
When I use the term redirect, I do not mean an HTTP redirect. What I mean is that instead of returning the RJS associated with destroying Child, I want to perform destroy on Parent and return the RJS associated with destroying Parent.
I would listen to what nathanvda says, but you can do it via ruby syntax (and you don't need erb scriptlets in rjs):
if #parent.children.empty?
page.redirect_to(url_for :action => :destroy,
:controller => :parents,
:id => #parent.id)
else
.. do your normall stuff here ..
end
A better approach to destroying the parent through a redirect is doing it in an after_hook. Not only you don't have to tell your user's browser to make another request, you also don't need to keep track of everywhere in the code where you delete children so you don't end up with hanging parents.
class Parent < ActiveRecord::Base
# also worth getting the dependent destroy, so you don't have hanging children
has_many :chilren, :dependent => :destroy
end
class Child < ActiveRecord::Base
after_destroy { parent.destroy if parent.children.empty? }
end
Then you can just handle however you prefer what to show the user when that happens, like redirecting the user to '/parents'.
I would guess you could set the window.location.href in your rjs, something like
<% if #parent.children.empty? %>
window.location.href='<%= url_for :action => :destroy,
:controller => :parents,
:id => #parent.id %>'
<% else %>
.. do your normall stuff here ..
<% end %>
assuming you render javascript. Not sure if it is completely correct, but hope you get the idea.
[EDIT: added controller code]
TO make it clearer, your controller would look as follows
#parent = #child.parent
#child.destroy
if #parent.children.empty?
render :redirect
end
Related
I have a Review model which is nested resource of Publication model. Review model have accept_nested_attributes_for review_comments. I wonder how could I show delete path to delete review_commment?
<% #review.review_comments.each do |review_comment| %>
<%= link_to "delete", ???, method: :delete %>
<% end %>
review.rb
has_many :review_comments, :dependent => :destroy
accepts_nested_attributes_for :review_comments, :allow_destroy => :true
review_comment.rb
belongs_to :review
publication.rb
has_many :reviews
routes.rb
resources :publications do
resources :reviews
end
resources :review_comments
UPDATE
def create
#review_comment = ReviewComment.new(params[:review_comment])
if #review_comment.save
redirect_to #review_comment, notice: 'Review comment was successfully created.'
....
end
def destroy
#review_comment = ReviewComment.find(params[:id])
#review_comment.destroy
redirect_to :back, notice: "Deleted"
end
UPDATE
review_comments GET /review_comments(.:format) review_comments#index
POST /review_comments(.:format) review_comments#create
new_review_comment GET /review_comments/new(.:format) review_comments#new
edit_review_comment GET /review_comments/:id/edit(.:format) review_comments#edit
review_comment GET /review_comments/:id(.:format) review_comments#show
PUT /review_comments/:id(.:format) review_comments#update
DELETE /review_comments/:id(.:format) review_comments#destroy
link_to with a method anything other than GET is actually a bad idea, as links can be right clicked and opened in a new tab/window, and because this just copies the url (and not the method) it will break for non-get links.
Also, links are clicked on by web page indexing spiders, and even though the links in question are probably only available to logged in users (and therefore not spiders) it's still bad practise.
It's better to use button_to instead, which makes rails generate a mini-form to produce the same result.
From a practical point of view buttons are better (for the above reasons) but they're also better from a conceptual point of view: generally speaking, links should "take you somewhere", whereas buttons should "do something". It's better to keep these two basic functionalities seperate.
Something like this,
button_to t('general.delete'), :review_comment_path(review_comment), :method => :delete, :confirm => t('review_comment.confirm_delete'), :title => t('review_comment.delete_question')
For your routes:
<%= link_to "delete", review_comment, method: :delete %>
class ReviewCommentsController < ApplicationController
def destroy
#review_comment = ReviewComment.find(params[:id])
#review_comment.destroy
redirect_to review_comments_path # Or another path
end
end
I'm not sure where to start with this problem. I have images with nested routes, example:
localhost:3000/users/1/images/3
localhost:3000/users/1/images/4
When I go into the first image (id = 3), it works. But when I go to the second image (id = 4) I get a routing error
No route matches {:action=>"show", :controller=>"images", :user_id=>1, :id=>nil}
I have other images too (from previous upload), now when I upload other images, I get nil for my id.
When I check my database, it shows that the data is in there, and it does have the correct row with id 4.
Has this happened to anyone? Is there some kind of rake command that I can use? This is baffled and I don't even know where to start showing my code to see where the error is coming from
EDIT:
user_image GET /users/:user_id/images/:id(.:format) images#show
routes.rb:
App::Application.routes.draw do
resources :users do
resources :images do
resources :comments, :defaults => { :commentable => 'image' }
end
end
end
EDIT:
I figured out the issue.. Apparently it was a path that was in my show.html.erb view where I'm trying to get the "next" image from my database and that there's no id to get
<%= link_to "Next", user_style_path(#image.user_id, #image.next_img) if user_style_path(#image.user_id, #image.next_img) %>
Where in my model I have:
def previous_img
self.class.first(:conditions => ["created_at < ?", created_at], :order => "created_at desc")
end
def next_img
self.class.first(:conditions => ["created_at > ?", created_at], :order => "created_at asc")
end
How do I reverse back to the images ONLY associated with the user, so that it'll just recycle the images?
Edit: OK, you can get out of that by making the methods on the model class methods:
def self._next(img)
first(:conditions => ["created_at > ?", img.created_at], :order => "created_at asc") || first
end
and then scoping them to the user in the view:
<%= link_to "Next", user_style_path(#image.user_id, #user.images._next(#image)) unless #user.images == [#image] %>
Rails will combine the scopes for you. Note that I'm assuming you have a #user or a current_user or something available, and also that the class method is #_next not #next, because "next" is a reserved keyword.
The way #_next is structured, if it can't actually find the next image (the first created newer than the current), that clause will be nil, and it will return the first image of the collection instead. The link will be active unless there's only one image in the collection at first I had thought to make it active unless the collection is empty, but I guess it can't be empty or you wouldn't be on the page in the first place.
Your id is listed as null, which indicates to me that you may not have first identified your Image object in the show action of your ImageController before going to your show view.
Put
#image = #user.images.find(params[:id])
in your show controller action and see what happens. If not already defined, #user should be
#user = User.find(params[:user_id])
If that doesn't work, then it could be a routing problem. Ensure that you have the correct nested resources in your config/routes.rb:
resources :users do
resources :images
end
Otherwise, check your associations: your User class should have a has_many relation to images, and your Image class should have a belongs_to relation to users.
I've got two models: Book and ReadingList. A ReadingList has_and_belongs_to_many Books. On the BooksController#show page, I'd like to have a select list that shows all the reading lists, with a button to add the current book to the selected reading list.
Presumably this should go to the ReadingListController#update action, but I can't specify this as the form's URL, because I won't know which ReadingList to send to at the time the form is created. I could hack it with JavaScript, but I'd rather not rely on that.
Would it be better to have a custom action in the BooksController that accepts a reading list id to add the book to, or can I work the routes so this request ends up getting to the ReadingListController#update action?
I suggest that you have a resource which is a ReadingListEntry that represents a book in a reading list. Then you can simply POST to that resource to add it. There doesn't actually need to be a model behind it, you can manipulate the reading list directly.
Obviously this is something that could easily be achieved by using Ajax to submit the form, but in the case where JavaScript is disabled / unavailable, your best option is to have a custom action in the BooksController that adds it to the required reading list.
You could combine both by having the form pointing to the action in the BooksController, but having an onsubmit handler that posts to the ReadingList controller via Ajax.
I would create a custom action and route such that you can provide a book_id and list_id and form the relation.
Assuming you're using restful routes
resources :books do
post '/lists/:list_id/subscribe' => 'lists#subscribe', :as => :subscribe
end
def subscribe
#list = List.find params[:list_id]
#book = Book.find params[:book_id]
#list << #book
end
Now you can use button_to with or without ajax.
Perhaps a has_many :through relationship would be better? I like Anthony's idea of a ReadingListEntry resource - perhaps put a model behind this giving you:
# models/book.rb
has_many :reading_list_entries
has_many :reading_lists, :through => :reading_list_entries
I think here you are changing the Book, not the ReadingList. Therefore you should PUT to the BooksController#update resource with a new list_id attribute.
# in views/books/show.html.erb
<%= form_for #book, :url => book_path(#book) do |f| =>
<%= f.select :list, ReadingList.all.map { |l| [l.name, l.id] } =>
<%= submit_tag "Change" =>
<% end %>
# in controllers/books_controller.rb
# params[:book][:list_id] => 123
def update
#book = Book.find(params[:id])
#book.update_attributes(params[:book])
end
# config/routes.rb
resources :books
resources :lists do
resources :books
end
If you wanted a Book to belong to more than one ReadingList you'd need a has_and_belongs_to_many relationship instead
I am using the Thumb_Up gem for ruby on rails.
https://github.com/brady8/thumbs_up
I want users to be able to vote on posts.
However, I am unable to figure out how I can allow a user to click a button next to each post and add a vote to the database.
I can get this to happen in the rails console through doing the following:
u=User.first
m=Micropost.first
u.vote_for(m)
However, how can I get this to happen when a button is clicked in view. I am assuming I would have to use ajax, but how would I know the url I need to post to to make this action occur?
Any help would be greatly appreciated.
Update:
Thanks so much for the help! I am still having a problem with the code below.
Here is my routes.rb
resources :microposts do
post :vote, :on => :member
end
View:
<%= link_to('vote for this post!', vote_micropost_path(#micropost), :method => :post) %>
Controller:
def vote
#micropost = Micropost.find(params[:id])
current_user.vote_for #micropost
# This assumes you'll only call it via AJAX.
# If your ajax call doesn't return "ok", then you know something went wrong
render :text => 'ok', :layout => false
end
However, I'm still getting this error:
No route matches {:controller=>"microposts", :id=>#, :action=>"vote"}
Would anyone know why the routes aren't matching correctly?
I am assuming Rails 3. Rails 2's routes would look a little different.
First you would need to define a route in your config/routes.rb file. You could do this many ways. If you already have a route for microposts, you could simply add a "vote" action:
resources :microposts do
post :vote, :on => :member
end
(For clarity, the "post" above refers to the HTTP POST method and has nothing to do with your Micropost class.) If you use that route, you would then need to create a "vote" method in your Microposts controller to catch it. Something like
def vote
#post = Micropost.find(params[:id])
current_user.vote_for #post
# This assumes you'll only call it via AJAX.
# If your ajax call doesn't return "ok", then you know something went wrong
render :text => 'ok', :layout => false
end
Then in your view's AJAX POST call (assuming the example route I gave), you would get the url with:
vote_micropost_path(#micropost)
It would look like /microposts/56/vote
I have frequently run into the situation where I want to update many records at once - like GMail does with setting many messages "read" or "unread".
Rails encourages this with the 'update' method on an ActiveRecord class - Comment.update(keys, values)
Example - http://snippets.dzone.com/posts/show/7495
This is great functionality, but hard to map to a restful route. In a sense, I'd like to see a :put action on a collection. In routes, we might add something like
map.resources :comments, :collection => { :update_many => :put }
And then in the form, you'd do this...
<% form_for #comments do |f| %>
...
This doesn't work on many levels. If you do this: :collection => { :update_many => :put }, rails will submit a post to the index action (CommentsController#index), I want it to go to the 'update_many' action. Instead, you can do a :collection => { :update_many => :post }. This will at least go to the correct action in the controller.
And, instead of <% form for #comments ... you have to do the following:
<% form_for :comments, :url => { :controller => :comments, :action => :update_many } do |f| %>
It will work OK this way
Still not perfect - feels a little like we're not doing it the 'Rails way'. It also seems like :post, and :delete would also make sense on a collection controller.
I'm posting here to see if there's anything I missed on setting this up. Any other thoughts on how to restfully do a collection level :post, :put, :delete?
I've run into a few situations like you describe. The first couple of times I've implemented form almost identical to the one you suggest.
About the third time I hit this problem I realized that every item I'm updating has a common belongs_to relationship with something else. Usually a user. That's exactly the epiphany you need to make sense of this RESTfully. It will also help you clean clean up the form/controller.
Don't think of it as updating a bunch of messages, think of it as updating one user.
Here's some example code I've used in the past to highlight the difference. Assuming that we we want bulk operations on messages that belong to the current_user...
As of rails 2.3 we can add
accepts_nested_attributes_for :messages
to the user model. Ensure messages_attributes is part of attr_accessible, or is not attr_protected.
Then create the route:
map.resources :users, :member => {:bulk_message_update, :method => :put}
Then add the action to the controller. With AJAX capabilities ;)
def bulk_message_update
#user = User.find(params[:id])
#user.update_attributes(params[:user])
if #user.save
respond_to do |format|
format.html {redirect}
format.js {render :update do |page|
...
}
end
else
....
end
Then your form will look like this:
<% form_for current_user, bulk_message_update_user_url(current_user),
:html => {:method => :put} do |f| %>
<% f.fields_for :messages do |message| %>
form for each message
<% end %>
<%= sumbit_tag %>
<% end %>
I often add collection-based update_multiple and destroy_multiple actions to an otherwise RESTful controller.
Check out this Railscast on Updating Through Checkboxes. It should give you a good idea how to approach it, come back and add to this question if you run into troubles!