Passing Params With Rails Render AKA Lesson on Rails Render vs Redirect_to

Problem: User is directed to the purchase page of the website with a discount code
url/discount_code=trial5&product=greatest-product
While purchasing user forgets to add password, hit submit button
Rails(with Devise gem) sends user to url/users but now discount is gone!

This bug … well…bugged me for a morning. I hope this post helps others with the same problem.

This is how my RegistrationsController which inherits from Devise looked

  def new
    @user = User.new
    @purchase = Purchase.new
    super
  end

  def create
    @user = User.new(user_params)
    if @user.save
       redirect_to desired_path
    else
      @purchase = Purchase.new
      return render 'new'
    end
  end
 

I figured what I needed was to send the discount params to render ‘new’. How could I could that. First let me tell you the difference from redirect_to and render.

Render
We can give render the name of an action to cause the corresponding view template to be rendered.

For instance, if you’re in an action named update, Rails will be attempting to display an update.html.erb view template. If you wanted it to display the edit form, associated with the edit action, then render can override the template selection.

When render :edit is executed it only causes the edit.html.erb view template to be displayed. The actual edit action in the controller will not be executed.

Redirect_to

Redirect_to creates a new http request. When a user submits data it comes in as a POST request. If we successfully process that data we likely next display them the data they just created. We could display the article using render in the same POST that sent us the data. But it is not the Rails way. If the user hit the back arrow on the browser it will prompt the User to submit form data again?! when you successfully store data you want to respond with an HTML redirect. That will force the browser to start a new request.
That’s why in the create method in the registrations controller I want to redirect if @user is successfully saved. In this example, the application saves user if new user buys a product.

The Solution

After this brief lesson on the difference between render and redirect. The solution to persist discount when the new.html.erb page is rendered by loading the discount object before rendering the action.

  def new
    @user = User.new
    @purchase = Purchase.new
    super
  end

  def create
    @user = User.new(user_params)
    if @user.save
       redirect_to desired_path
    else
      @purchase = Purchase.new
      @discount = Discount.find_by(code: params[:purchase][:discount_code])
      return render 'new'
    end
  end
 

The params[:purchase][:discount_code] was being passed to the render new action but what I just needed was to load define @discount with the Discount object so my new.html.erb knew what to do with it.

Debugging is a skill that comes with time and good understanding of the framework and tools you are using to develop software. Slowly but surely, we are all on our way there.

Redirect User to a Specific Page After Sign In With Devise

Devise is a great gem that allows you to authenticate users and manage sessions in your application. At times you may want to overwrite the devise code a bit to perform a task your application needs. I had to do the same the other day and the solution might help you too if you happen to be wanting to have the user visit the current page after logging in.

Context:

User is on purchase page but is not logged in. If purchase attempt is made with an email already in the database, user is prompted to log in and soon after he/she needs to be redirected to that specific purchase page where he/she can complete the transaction.

After some research I used some of the code from the How to in the plataformatec/devise github page.

In the ApplicationController I wrote


after_filter :store_location

def store_location
  # store last url - this is needed for post-login redirect to whatever the user last visited.
    if (request.fullpath != "/users/sign_in" &&
        request.fullpath != "/users/sign_up" &&
        request.fullpath != "/users/password" &&
        request.fullpath != "/users/sign_out" &&
        !request.xhr?) # don't store ajax calls

      session[:previous_url] = new_purchase_path(params["product”]) if params["product"]
   
   end
 end

  def after_sign_in_path_for(resource)
    session[:previous_url] || root_path
  end

The code above is storing the session[:previous_url] only if params[“product”] exists.

Although this worked it was not the most elegant solution I could find. As far as I could tell devise stores previous url as session[:user_return_to] for a visitor tries to access a page that it does not have access unless user logs in.

Having this in mind I went to my registrations controller and I set session[:user_return_to] to the path of a purchase page.


class RegistrationsController < Devise::RegistrationsController

def new
    @user = User.new
    @purchase = Purchase.new
    session[:user_return_to] = request.fullpath
    super
  end

def create
    @user = User.new(user_params)
    unless @user.save
     @purchase = Purchase.new
      return render 'new'
    end
    sign_in @user
    session[:just_signed_up] = true
    session.delete(:user_return_to)
 end


In this Registration Controller session[:user_return_to] is set to request.fullpath in the method new. Request.fullpath by the way returns the string full path of the last url requested.
Just so the session[:user_return_to] is not saved to more areas than it should, I make sure it gets deleted in the create method. The session.delete(:user_return_to) is erasing the value of key “user_return_to”.