Rails 3, Custom Actions, and HTML request methods - ruby-on-rails

I don't really understand the pro's and con's of using "post" vs "get" vs "put" requests, on custom controller actions, and whether to use links or forms/buttons.
So let's say I have a simple to-do list with tasks, and a tasks controller, and I want a "complete" action where I find a specific task in the db and update it's status attribute from "incomplete" to "complete."
def complete
#task = Task.find(params[:id])
if #task.update_attributes(:status => "complete")
redirect_to tasks_url, :notice => "Completed!"
else
redirect_to tasks_url, :error => "Whoops."
end
end
What's the best practice way to define this route, which HTML request method should I use (post? put? get?), and should I use a plain link or a form? (and note: assume my user security model is all figured out with devise, and appropriate before filters, etc.)
And most of all, how would I articulate all this in a Rails 3 routes.rb file?
Note, the below code wasn't really working for me:
#routes.rb
resources :tasks do
members do
post 'complete'
end
end
so currently I'm using this instead:
#routes.rb
match 'tasks/:id/complete', 'tasks#complete', :as => "complete_task"
#view
= link_to "Complete", complete_task_path(:id => #task.id)
But this triggers a get request, and I feel like it should be a "put" or a "post." Or should it be a link at all? Should it be a form with hidden fields?

"link_to" method usually generates an anchor tag ie "<a></a>", ie a regular GET request
to do a POST request using link_to you should do the following
= link_to "Complete", complete_task_path(:id => #task.id), :method => :post
Remember if javascript is disabled in the browser, the above statement will fall back to a GET request instead of POST.

Related

Rails routing prob: on failing "create", re-renders the form (as it should) but not at the URL in my routes

RESTful resource, default type routes. Creating an event is supposed to work as follows:
def create
#event = current_user.events.build(params[:event])
if #event.save
redirect_to #event, :flash => { :success => "Event created!" }
else
render :action => "new" # new_event_path
end
end
When invalid data is entered, it does render the "new" view/form again, but it renders this view at the "localhost:3000/events" URL, where the "index" action/view should be on.
My event routes seem like they ought to be pretty predictable:
resources :events
I just updated to Capybara 2, began using DatabaseCleaner, and set transactional_fixtures to false in preparation for testing some JS-enabled functionality but can't think of any other way I might have stuffed this up.
Is there some simple thing I'm missing that could cause a weird routing muck up like this?
Ideas, anyone, on where to start troubleshooting it?
This is the correct behavior. What is happening is that it is using the POST method for that URL when issuing the create action. Using a GET at the URL would be the index action. Also note that rendering a different template does not change the URL (that would require a redirect).
Check out section 2.2 in the Rails Routing documentation:
http://guides.rubyonrails.org/routing.html
When you create event from submitting form you using post method to /events route. And when data becomes invalid rails render events/new for your /events(POST) request at /events address.
But you can
redirect_to action: "new", event: #event.attributes
and add to new action
#item = Item.new(params[:item].except('protected attributes','created_at', 'updated_at', 'id'))

How do you use a Rails 3 gem method to update a database model?

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

How do I maintain the same controller & action in a page URL upon re-rendering an action in Rails?

I am using AuthLogic to authenticate users in my rails app. That part is set up and workign properly.
I have the following route defined:
map.login '/account/login', :controller => :user_sessions, :action => :new
Calling rake routes returns what I expect:
login /account/login {:controller=>"user_sessions", :action=>"new"}
When someone submits a login, it calls UserSessionsController.create:
def create
#user_session = UserSession.new(params[:user_session])
if #user_session.save
flash[:notice] = "Login successful!"
redirect_back_or_default account_url
else
render :action => :new
end
end
If #user_session.save fails, the appropriate error messages appear on the screen. However, the browser URL also changes to "http://localhost:3000/user_session" instead of staying on "http://localhost:3000/account/login".
I assume the problem is what I am feeding to the render method. What should I be feeding it?
This is actually the intended behavior for this process. In a standard scaffolded RESTful controller, a validation error in the create and update actions will simply render the original template without redirecting. This results in what you are seeing – the new template will be displayed with the create action's URL in the URL bar. The reason for this is that in order to display information to the user about what errors occurred, the view must have access to the invalid model object, which is #user_session in your case.
You can use redirect_to instead of render if you want to force a redirect to the original URL, but this will cause you to lose information about the errors. You would need to manually persist the errors in the session, which would be messy. My advice is not to worry about the fact that the URL doesn't match that of the original as this is pretty standard in all Rails apps.
Just adding solution for Rails 4 (based on Shaun's answer here):
Add new route to routes file:
post '/carts/new' => 'carts#create', as: :create_post
Add url: create_post_path to form tag
Done.
After further digging, I found the solution in another StackOverflow question: Use custom route upon model validation failure
I simply modified my routes to add a new one for posing to '/account/login':
map.login '/account/login', :controller => :user_sessions, :action => :new, :conditions => {:method => :get}
map.login_post '/account/login', :controller => :user_sessions, :action => :create, :conditions => {:method => :post}
Then, I updated my view to utilize the new route:
<% form_for #user_session, :url => login_post_path do |f| %>
This works perfectly. A failed login gives the appropriate error messages and maintains the '/account/login' URL.

ruby-on-rails: passing params through a route & link_to

I have a name route:
map.up_vote 'up_vote', :controller => 'rep', :action => 'up_vote
But up_vote requires two arguments to be passed in, postID and posterID and I can't seem to figure how to do that in a partial, but in an integration test I have no issues.
Partial:
link_to 'Up Vote', up_vote_path, {:postID => session[:user_post_id], :postersID => session[:poster_id]}
Integration test:
post up_vote_path,{:postID => #latest.id,:postersID => users(:bob).id} (this works ok)
1) What is going on the in the partial?
2) What changes can I make to my tests to catch this?
A question: why are you passing your session variables in a link? You can get them directly from the session...
I don't know if there are any special reasons to put :user_post_id and :poster_id in the session but I recommend you two things:
1) Pass your variables in urls, sessions can be evil (try hitting back, refresh and forward on your browser)
2) Use resources in your URLs / controller actions logic.
Example (valid only if I got it right and you're voting an user's post):
routes:
map.resources :users do |user|
user.resources :posts do |post|
post.resource :vote
end
end
So you can have this url:
/users/:id/posts/:post_id/vote
And the link path:
link_to "Up", user_post_vote_path(#user, #post), :method => :create
I putting #user and #post instead of the integers because path methods accept them and you can build a shorter version with:
link_to "Up", [#user, #post, :vote] # or [:vote, #post, #user]
Implementing:
class VoteController ....
def create
# do your stuff here
end
end
This way it will be easier and RESTful.
Ryan Bates got a great episode on resources, it definately worths a look.
You want to pass your params in the ..._path
link_to "Up Vote", up_vote_path(:postID => session[:user_post_id], :postersID => session[:poster_id])
The integration test is written out differently than the link_to since your testing the act.
post "to" up_vote_path, "with these" {params}
Also since your doing a POST, you will want to add the appropriate :method option to the link_to

Rails redirect_to post method?

redirect_to :controller=>'groups',:action=>'invite'
but I got error because redirect_to send GET method I want to change this method to 'POST' there is no :method option in redirect_to what will I do ? Can I do this without redirect_to.
Edit:
I have this in groups/invite.html.erb
<%= link_to "Send invite", group_members_path(:group_member=>{:user_id=>friendship.friend.id, :group_id=>#group.id,:sender_id=>current_user.id,:status=>"requested"}), :method => :post %>
This link call create action in group_members controller,and after create action performed I want to show groups/invite.html.erb with group_id(I mean after click 'send invite' group_members will be created and then the current page will be shown) like this:
redirect_to :controller=>'groups',:action=>'invite',:group_id=>#group_member.group_id
After redirect_to request this with GET method, it calls show action in group and take invite as id and give this error
Couldn't find Group with ID=invite
My invite action in group
def invite
#friendships = current_user.friendships.find(:all,:conditions=>"status='accepted'")
#requested_friendships=current_user.requested_friendships.find(:all,:conditions=>"status='accepted'")
#group=Group.find(params[:group_id])
end
The solution is I have to redirect this with POST method but I couldn't find a way.
Ugly solution: I solved this problem which I don't prefer. I still wait if you have solution in fair way.
My solution is add route for invite to get rid of 'Couldn't find Group with ID=invite' error.
in routes.rb
map.connect "/invite",:controller=>'groups',:action=>'invite'
in create action
redirect_to "/invite?group_id=#{#group_member.group_id}"
I call this solution in may language 'amele yontemi' in english 'manual worker method' (I think).
The answer is that you cannot do a POST using a redirect_to.
This is because what redirect_to does is just send an HTTP 30x redirect header to the browser which in turn GETs the destination URL, and browsers do only GETs on redirects
It sounds like you are getting tripped up by how Rails routing works. This code:
redirect_to :controller=>'groups',:action=>'invite',:group_id=>#group_member.group_id
creates a URL that looks something like /groups/invite?group_id=1.
Without the mapping in your routes.rb, the Rails router maps this to the show action, not invite. The invite part of the URL is mapped to params[:id] and when it tries to find that record in the database, it fails and you get the message you found.
If you are using RESTful routes, you already have a map.resources line that looks like this:
map.resources :groups
You need to add a custom action for invite:
map.resources :groups, :member => { :invite => :get }
Then change your reference to params[:group_id] in the #invite method to use just params[:id].
I found a semi-workaround that I needed to make this happen in Rails 3. I made a route that would call the method in that controller that requires a post call. A line in "route.rb", such as:
match '/create', :to => "content#create"
It's probably ugly but desperate times call for desperate measures. Just thought I'd share.
The idea is to make a 'redirect' while under the hood you generate a form with method :post.
I was facing the same problem and extracted the solution into the gem repost, so it is doing all that work for you, so no need to create a separate view with the form, just use the provided by gem function redirect_post() on your controller.
class MyController < ActionController::Base
...
def some_action
redirect_post('url', params: {}, options: {})
end
...
end
Should be available on rubygems.

Resources