how to post to form from within an action - ruby-on-rails

I have a link that goes to an action, so if someone clicks:
localhost/cart/checkout?pid=123
It goes to the CartController checkout action which then displays a form.
But in some circumstances (depending on when I load the Product with id 123) I may not need to display the form, I can just load the data and then post to the form's action.
How can I programatically post to where my form was going to post with data.
class CartController < ApplicationController
def checkout
pid = params[:pid]
product = Product.find(pid)
if product....
# no need to display view, just post to handleCheckout
end
end
# checkout form posts to this action
def handleCheckout
end
end

I have not done something like this before but I have some idea so please note that none of the is tested.
If your handleCheckout action is meant to be used as a Get request then you can redirect to this action with the params. like:
class CartController < ApplicationController
def checkout
pid = params[:pid]
product = Product.find(pid)
if product....
redirect_to action: "handleCheckout", params: params
# Not sure whether you will get it as 'params' or params[:params] in handleCheckout action
end
end
# checkout form posts to this action
def handleCheckout
end
end
And if handleCheckout is meant to be used as post Then above method might not work since redirect_to will create a new http Get request to that action. so you may try something like this:
def checkout
pid = params[:pid]
product = Product.find(pid)
if product....
handleCheckout
# params since is a global hash and above method has access to it
end
end
# checkout form posts to this action
def handleCheckout
# your other code
redirect_to 'some_action' and return
# in above line you have to return with a render or redirect
# Otherwise it will render 'checkout' template with render and redirect or
# it will throw double render error if you have a simple render or redirect without explicit return
end
As I mentioned, I have not tried any of above code. There might be errors. I hope it helps.

Related

Rails, change URL for a POST action within a controller

I want the URL to be changed when a call to an action within the controller is made. My current scenario:
Controller:
class EventController < ApplicationController
def index
if city.blank?
failure
end
...
end
def failure
...
render 'failure'
end
end
Routes:
get '/event', to: 'event#index'
post '/event/failure' => 'event#failure'
But this code keeps the url as /events. The desired result is /events/failure
I've views for payment 'index' and 'failure'. I'm using rails ~ 5.0.0.
For the URL to change you will want to do a redirect, something like this :
class EventController < ApplicationController
def index
if city.blank?
redirect_to action: 'failure'
end
...
end
def failure
...
render 'failure'
end
end
But, redirection is not possible for POST requests as given in HTTP/1.1
You might want to consider changing your strategy.

(Rails) How to get 'id' out of edit url

I have a model called studies.
After action redirect redirect_to edit_study_path(#new_study),
URL: http://localhost:3000/studies/2/edit.
Is there anyway to customize an url after passing id ?
For example, http://localhost:3000/study
(still going to the edit path, and still with the :id in the params)
I guess what you want is to edit the current study?
In this case, it's possible, using ressource instead of ressources in the routes.
Let's have an example:
#in routes.rb
resources :studies
resource :study
Both of them will by default link to the StudiesController and call the same actions (eg. edit in your case) but in two different routes
get "/studies/:id/edit" => "studies#edit"
get "/study/edit" => "studies#edit"
in your edit action, you should then setup to handle correctly the parameters:
def edit
#study = params[:id].nil? ? current_study : Study.find(params[:id])
end
Note you need a current_study method somewhere, and store the current_study in cookies/sessions to make it works.
Example:
# In application_controller.rb
def current_study
#current_study ||= Study.find_by(id: session[:current_study_id]) #using find_by doesn't raise exception if doesn't exists
end
def current_study= x
#current_study = x
session[:current_study_id] = x.id
end
#... And back to study controller
def create
#...
#Eg. setup current_study and go to edit after creation
if study.save
self.current_study = study
redirect_to study_edit_path #easy peesy
end
end
Happy coding,
Yacine.

How to display Devise account details on a profile page

Using Devise I would like to display User account information such as profile name, first & last name etc on another page called profile page within my rails application.
I have created a controller called profiles with a view called profile/show
In the controller have added the below code
def show
#user = User.find_by_profile_name(params[:id])
if #user
render action: :show
else
render file: 'public/404', status: 404, formats: [:html]
end
end
end
In the view profiles/show I have the following code
<%= #user.profile_name %>
and the route is get 'profiles/show'.
My issue is when I do all of the above the profile name of the user still does not display on the profile page? There are no errors that come up it just doesn't display. I am not sure what code I am missing. I have checked the console and the user does have a profile name save to that ID and this is also in the devise account settings. So I am not sure how to get this information to display?
In Rails you would usually set it up like follow to take leverage of convention over configuration:
# config/routes.rb
resources :users, only: [:show, :index]
# app/models/user.rb
class User < ActiveRecord::Base
# ...
def self.find_by_uid!(uid)
User.find_by!("profile_name = :p OR id = :p", p: uid)
end
end
# app/controllers/users_controller.rb
class UsersController
# GET /users/:id
def show
#user = User.find_by_uid!(params[:id])
# Rails does the magic.
end
# GET /users
def index
#users = User.all
end
end
<%- # app/views/users/show.html.erb -%>
<h1><%= #user.profile_name %></h1>
The only special part here is that in the user model we create a class method which will query by id or profile_name. The reason that this is important is that it lets you use link_to(#user) and redirect_to(#user) as expected.
Which is also why we use resources :users. When the route name and the model line up the Rails polymorphic route handlers are able to do their job. If you want to use /profiles thats fine but never /profiles/show - including the action in the route defeats the whole purpose of REST.
The show action will render users/show.html.erb by default. So you rarely need to explicitly render in your controller.
render action: :foo
is only used when you want to render a template with the same name as another action, its usually used as follows:
def create
#something = Something.new
if #something.save
redirect_to(#something)
else
render action: :new # renders views/something/new.html.erb
end
end
If you want to explicitly render a template you would do render :foo or render "foo/bar".
And when you use find or find_by! it will raise an exception if the record is not found which by default will render the static 404 template. Reproducing this error handling in your actions is not very desirable since it violates the DRY pinciple.

Can you do a before create action on a rails controller

I have a create action but I only want the create action to create IF 2 items in the form that is trying to be created are true. Is that possible to do a if statement before completing the create action or is that making my controller too fat?
I have a form that I want to submit to my db, but based on some of the questions in the form, I want the form to bring up another page (A payment page) BEFORE it puts the form info into the db. Only after the charge is successful will the controller put the info in the db if the charge goes thru.
So I want to know is it DRY or even possible to put logic that will preceed the create action in the controller?
--Additional Information--
So Im still trying to get this right...
In my form controller I have
def intermediary
if #model.who_pays == true
#amount = #model.how_much_to_pay
redirect_to charge_path, :notice => "Please make payment before proceeding"
else
redirect_to model_path #to create object
end
end
and then in my actual form I had
Model.new
but i'm changing it to
<%= form_for [#model, url: [intermediary_path]] do |f| %>
is this logic correct?
Without the extra form, this should be handled in your model with validations:
#app/controllers/your_controller.rb
class YourController < ApplicationController
def new
#model = Model.new
end
def create
#model = Model.new model_params
#model.save
end
end
In your model...
#app/models/model.rb
class Model < ActiveRecord::Base
validate :true_questions, on: :create
private
def true_questions
errors.add(:option1, "Must be true") unless option1
errors.add(:option2, "Must be true") unless option2
end
end
With the form, you'll have several choices:
An "intermediary" action with sessions / cookies
JS / Ajax
As mentioned in the comments, this is a question of "state" -- IE how you're able to keep the data integrity throughout the process.
You should check out how stripe handle this - they send the user to an authentication page (where they receive a token), are redirected back to "confirm" page, from which they're able to proceed.
-
Intermediary
If you have an intermediary action, you'll have to change your flow a little:
#config/routes.rb
resources :controller do
get :intermediary, on: :collection
end
#app/controllers/your_controller.rb
class YourController < ApplicationController
def intermediary
# perform validations in the controller
# set instance or session vars if necessary
# render payment page
end
def create
# returned from payment system - input into DB
end
end
This would work quite well, although it means having custom routes and another action.
-
Ajax
A different approach would be to use either Ajax / JS.
This would be achieved by sending data to your app (probably to your "create" action) with certain switches defined. These would give you the ability to perform the required validations before invoking the payment view.
The payment view would be where the Ajax would come in:
#app/assets/javascripts/application.js
$(document).on("submit", "#____", function(){
$.ajax({
url: "controller",
method: "POST",
data: .......
success: function(data){
// show payment page
}
});
});
#app/controllers/your_controller.rb
class YourController < ApplicationController
def create
if params[:x]
# perform validation
# return payment form
else
# capture payment data
# save to db
end
end
end

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.

Resources