Calling custom action without re-rendering page with Rails link_to - ruby-on-rails

In _follow.html.slim I am trying to make a link to "Add Friend" with this:
= link_to "Add Friend", :controller => "relationships", :action => "req"
I want it to call the method req in the relationships controller while staying on the same page. It currently isn't even calling the method and is returning this error:
No route matches {:controller=>"relationships", :action=>"req", :name=>"Nathan Glass", :age=>"21"}
I'm following this tutorial http://francik.name/rails2010/week10.html and he doesn't define a route for this action. If this error is correct I guess my confusion is why I need a route for this. Otherwise, what is my problem here? Thanks!
class RelationshipsController < ApplicationController
def req
puts "req called"*10
# is setting #current_user since the current_user method already returns #current_user?
#current_user = current_user
#friend = User.find_by_name(params[:name])
unless #friend.nil?
if Relationship.request(#current_user, #friend)
flash[:notice] = "Friendship with #{#friend.name} requested"
else
flash[:error] = "Friendship with #{#friend.name} cannot be requested"
end
end
# render somewhere
end
end

First, you always need to define a route for an action. If you don't, rails doesn't know that your action exists (even if you specify the controller and the action names in your link_to).
For that, you can simply do, in your config/routes.rb file:
get 'relationships/req'
Now, your req action has a path, relationships_req_path (responding to HTTP GET requests).
Then, if you want to call a controller action while staying on the same page, you can do:
link_to "Add as friend", relationships_req_path, remote: true
The remote: true modifies the link behavior(it will works like an ajax call) and renders the relationships/req.js.erb file by default (which can contain nothing). This file allows use to dynamically add/modify content on the current page.

Related

How to add routes for a new template?

I am new in Ruby and Rails and little bit confused about rendering and adding routes for a new template.
I have following link_to tag
<td colspan="3">
<%= link_to 'Show Current State', simulation, :action => :current_state, :class => 'btn btn-primary'%>
</td>
Where simulation is the name of controller and action is name of the method in SimulationController.
I added this in my routes.rb
resources :simulations, except: [:edit]
resources :simulations do
collection do
get 'current_state'
post 'current_state'
end
end
In my SimulationController class I added a new method i.e.
def current_state
byebug
end
My problem? routes is not re-directing to current_state method. Instead, it is redirecting to http://localhost:3000/simulations/{someID}
This redirection is calling show action.
def show
...
end
How can I make this work out and make <%= #simulation.dat %> line accessible in new.html.erb. Location of new.html.erb is in following path
views/simulations/index.html.js
views/similations/show.html.js
views/simulations/new.html.erb
This could be a basic question but I am new to rails 4. Thanks in advance.
Edit-1
Def of get_state method in controller
def get_state
#simulation = current_user.simulations.find(params[:id])
return not_found if #simulation.nil?
.....
/// How to send `#simulation` into `state.html.erb` formally as `new.html.erb`
end
You have too many misses in your code.
First, You don't need 2 resources :simulations, just merge them into one:
resources :simulations, except: :edit do
member do
get 'current_state', action: 'get_state'
post 'current_state', action: 'change_state'
end
end
Note that the original collection block is changed to a member block.
The difference between a collection block and a member block is that you need to provide an resource id for each routes in the member block, while no resource id is required for those in the collection block.
Also note that I added action: 'xxx' in each route, so you have to add these 2 actions in your SimulationsController, one for GET requests, and the other for POST requests.
UPDATE
In both of these actions, add render 'new' at the end.
END OF UPDATE
Run rake routes in your console (or bundle exec rake routes if you have multiple versions of rails installed), and you will see all the routes along with there url helper methods listed, like this:
Prefix Verb URI Pattern Controller#Action
current_state_simulations GET /simulations/:id/current_state simulations#get_state
current_state_simulations POST /simulations/:id/current_state simulations#change_state
...
According to the Prefix column, the link in the view should be
<%= link_to 'Show Current State', current_state_simulations_path(simulation), :class => 'btn btn-primary'%>
Or in short
<%= link_to 'Show Current State', [:current_state, simulation], :class => 'btn btn-primary'%>
UPDATE FOR Edit-1
Don't return in actions, because return doesn't stop rendering.
Instead, use raise ActionController::RoutingError.new('Not Found') to redirect users to the 404 page.
You can define an instance method in ApplicationController:
class ApplicationController < ActionController::Base
private
def not_found!
raise ActionController::RoutingError.new('Not Found')
end
end
And modify your SimulationsController:
def get_state
#simulation = current_user.simulations.find(params[:id])
not_found! unless #simulation
# ...
render 'new'
end
Best Practice
For dynamic page web applications, don't render views for non-GET requests!
Why? Because if a user POSTs some data to your web app, and then refreshes his/her browser, that request gets POSTed again, and your database got tainted. Same for PATCH, PUT and DELETE requests.
You can redirect the user to a GET path if the non-GET request succeeds, or to a 400 page if the non-GET request fails.

Ruby on Rails controller design

When I look at examples of Rails controllers, I usually see something like this:
class WidgetController < ActionController::Base
def new
#widget = Widget.new
end
def create
#widget = Widget.new(params[:id])
if #widget.save
redirect_to #widget
else
render 'new'
end
end
end
This works, but there's a couple problems:
Routes
If I add widgets to my routes.rb file:
Example::Application.routes.draw do
resources :widgets
end
GET /widgets/new will route to new and POST /widgets will route to create.
If the user enters incorrect information on the new widget page and submits it, their browser will display a URL with /widgets, but the new template will be rendered. If the user bookmarks the page and returns later or refreshes the page, the index action will be called instead of the new action, which isn't what the user expects. If there's no index action or if the user doesn't have permission to view it, the response will be a 404.
Duplication of code
As a contrived example, let's say I had some tricky logic in my new method:
def new
#widget = Widget.new
do_something_tricky()
end
Using the current approach, I'd duplicate that logic in new and create. I could call new from create, but then I'd have to modify new to check if #widget is defined:
def new
#widget ||= Widget.new
do_something_tricky()
end
Plus, this feels wrong because it reduces the orthogonality of the controller actions.
What to do?
So what's the Rails way of resolving this problem? Should I redirect to new instead of rendering the new template? Should I call new inside of create? Should I just live with it? Is there a better way?
I don't think this is a problem in "the rails way" and there is no builtin functionality to allow this without getting your hands dirty. What does a user expects when bookmarking a form they just submitted and had errors? Users don't know better, and they shouldn't bookmark a failed form.
I think redirecting to new_widget_path is the cleanest solution. Yet, you should keep the errors and display them on the form. For this I recommend you keep the params in session (which I expect to be smaller than a serialized Widget object).
def new
#widget = widget_from_session || Widget.new
end
def widget_from_session
Widget.new(session.delete(:widget_params)) if session[:widget_params].present?
end
private :widget_from_session
# Before the redirect
session[:widget_params] = params
The code is self explanatory, Widget.new will only be called when widget_from_session returns nil, this is when session[:widget_params] is present. Calling delete on a hash will return de deleted value and delete it from the original hash.
UPDATE Option 2
What about submitting the form using ajax? Your controller could benefit from:
respond_to :html, :json
...
def create
#widget = Widget.new params[:widget]
#widget
respond_with #widget, location: nil
end
Based on the response code (which is set by Rails: 201 Created or 422 Unprocessable Entity), you could show the errors (available in the body of the response when validations fail) or redirect the user to #widget
This is how StackOverflow does it: https://stackoverflow.com/questions/ask. They submit the form asynchronously.
In general, I think the Rails way of solving the problem would be to put the tricky method onto the model or as a helper method, so the controller stays "thin" and you don't have to make sure to add custom behavior to both #new and #create.
EDIT: For further reading, I'd recommend the "Rails AntiPatterns" book, as they go through a lot of these common design issues and give potential solutions.
you put do_something_tricky() in its own method and call it inside the create action (but only when you're rendering the new template, ie when validation fails).
As for the bookmark issue, I don't know a good way to prevent that but to modify the routes and set the create action to the new action but using POST
get '/users/new' => 'users#new'
post '/users/new' => 'users#create'
UPDATE: using resources
resources :platos, except: :create do
post '/new' => 'plates#create', on: :collection, as: :create
end
then you can use create_platos_path in your forms
You don't need to write same function in two action , use before_filter instead.
If you want to have "widget_new_url" after incorrect submission then in your form add url of new widget path something like :url => widget_new_path .
Rails takes the url from Form .
I have this problem before, so I use edit action instead.
Here is my code.
Routes:
resources :wines do
collection do
get :create_wine, as: :create_wine
end
end
Controller:
def create_wine
#wine = Wine.find_uncomplete_or_create_without_validation(current_user)
redirect_to edit_wine_path(#wine)
end
def edit
#wine = Wine.find(params[:id])
end
def update
#wine = Wine.find(params[:id])
if #wine.update_attributes(params[:wine])
redirect_to #wine, notice: "#{#wine.name} updated"
else
render :edit
end
end
Model:
def self.find_uncomplete_or_create_without_validation(user)
wine = user.wines.uncomplete.first || self.create_without_validation(user)
end
def self.create_without_validation(user)
wine = user.wines.build
wine.save(validate: false)
wine
end
View:
= simple_form_for #wine, html: { class: 'form-horizontal' } do |f|
= f.input :complete, as: :hidden, input_html: { value: 'true' }
What I did is create a new action 'create_wine' with get action.
If user request 'create_wine', it will create a new wine without validation and redirect to edit action with a update form for attributes and a hidden field for compele .
If user has create before but gave up saving the wine it will return the last uncompleted wine.
Which means whether use save it or not, the url will be the same to /wines/:id.
Not really good for RESTful design, but solve my problem. If there is any better solution please let me know.

Custom Rest actions

I am trying to create a friendship and I created a custom action called accept. however i can't reach it. Everytime I call it i get the action show could not be found.
Here my route.rb file
resources :friendships do
collection do
delete 'cancel'
get 'accept'
end
end
Here how i call it
<%= link_to 'Accept', accept_friendships_path(:friend_id => f) %>
accept_friendships was taken from rake routes commands. And here how i define my accept controller
#Accept friendships
def accept
if #customer.requested_friends.include?(#friend)
Friendship.accept(#customer, #friend)
flash[:notice] = "Friendship Accepted"
else
flash[:notice] = "No Friendship request"
end
redirect_to root_url
end
Here the error
Unknown action
The action 'show' could not be found for FriendshipsController
Maybe I am wrong, but why you want "accept" to be a collection? I guess you want it to be a member, since you are passing friend_id. If you change it to member and make the path accept_friendship_path(#friendship) [ note singular form of friendship ], you might have better luck. Beside an addition argument your case does not differ from example on Ruby on Rails Guides, that is why it is worth to try it

What to do if more than one view needs to link to a destroy action?

I'm not sure what to do here. I have two scaffolds: Groups and Users. In two different Group views I'm listing group users and calling the Destroy method of the users_controller.
Since a 2nd view is now calling the destroy action, I need some way of detecting what view called the Destroy action because I need a different redirect and custom flash notice for each of the two group views.
Is there a simple way of solving this, or would the solution be something like making a copy of the Destroy method and mapping a new route for it?
-thanks!
edit: maybe this is a stupid idea, but I was thinking... For the two involved views, what if I stored their view names in the session when the views are generated (as a flag for the Destroy action to know which view to redirect to and what custom flash notice to send back)?
Pass the parameter with the links and check those parameters in your action.
Like ,
link_to "Delete", :controller => "groups", :action => "destroy", :pass_par => "view1"
link_to "Delete", :controller => "groups", :action => "destroy", :pass_par => "view2"
Controller:
def destroy
if params[:pass_par] == "view1"
redirect_to view1
else
redirect_to view2
end
end
The destroy method is not very long, so yes, go ahead and copy it.
If it looks like this:
# DELETE /users/1
def destroy
#user = User.find(params[:id])
#user.destroy
redirect_to users_url
end
It should be not repeating yourself at all, and it will make your code simpler to read in the end.

link_to_unless_current fails when processing forms with error messages in it with restfull routes

does anyone know how to prevent the failing mechanism of link_to_unless_current?
f.e.: I have my page navigation with
link_to_unless_current "new task", new_task_path
When I click on the link, i come to the new taks path form... And no link is created -> ok.
Then I put incorrect values in the form and submit.
The TasksController processes the "create" action, the validation for the ActiveRecord-model fails because of the incorrect data and the controller renders the "new" action (and includes the error messages for the model).
class TasksController < ApplicationController
def create
#task = Task.new(params[:task])
if #task.save
flash[:notice] = 'task was successfully created.'
redirect_to(tasks_url)
else
render :action => "new"
end
end
end
But here the link gets created!
-> Because of the difference between the urls:
link path = new_task_path
but
posted path = tasks_path with :method => :post
Does anybody know how to cleanly solve this problem?
Thanks
Having a quick look at the source for link_to_unless_current...
...it makes use of current_path? such that you should be able to do something like this:
In a helper...
def current_page_in?(*pages)
pages.select {|page| current_page?(page)}.compact.any?
end
... and then in your view, you can just supply an array of either named_routes or hashes like Shadwell's answer above.
<%= link_to_unless(current_page_in?(new_thing_path, things_path), "add a thing") %>
You get the idea...
UPDATED
Had a think about this... and it'd be great if you could just use it like you'd hoped that the original method worked. Here we compare the supplied named route (or controller + action hash) with the current page AND its referrer.
def current_page_or_referrer_in(options)
url_string = CGI.unescapeHTML(url_for(options))
request = #controller.request
# We ignore any extra parameters in the request_uri if the
# submitted url doesn't have any either. This lets the function
# work with things like ?order=asc
if url_string.index("?")
request_uri = request.request_uri
referrer_uri = request.referrer
else
request_uri = request.request_uri.split('?').first
referrer_uri = request.referrer.split('?').first
end
#referrer_uri always has full path (protocol, host, port) so we need to be sure to compare apples w apples
if url_string =~ /^\w+:\/\//
["#{request.protocol}#{request.host_with_port}#{request_uri}", referrer_uri].include?(url_string)
else
referrer_uri = referrer_uri.gsub(request.protocol, '').gsub(request.host_with_port, '')
[request_uri, referrer_uri].include?(url_string)
end
end
The beauty is that it now lets you just do this (from your example):
<%= link_to_unless(current_page_or_referrer_in(new_task_path), "Add a task") %>
It'll then display if you're on new_task_path OR a page to which it has been sent (such as the create page
You can do it with link_to_unless instead of link_to_unless_current:
link_to_unless(controller_name == 'tasks' &&
(action_name == 'new' || action_name == 'create'),
new_task_path)

Resources