USING SESSION HASH FOR STORING VARIABLES ACROSS CONTROLLER ACTIONS

By: Saurav

2017-11-13 04:03:00 UTC

Man! I am so happy to solve this problem I had been facing for almost four days now.

Lets talk about the requirement:

The app I am building has Repair shops and Vehicle Listings. Each Repair shop/listing belongs to one user. Each user have roles and depending upon the roles they have privileges. Its a complex application but I would only talk about this specific requirement.

Each Dealers/Repair shops can have reviews. Now dealers will have reviews only on the vehicle listings (After a car sale, buyers publish a review. That review is for dealers and not for the listing itself but its submitted on a listing page. But for the case of Repair shops, users will submit review for that shop).

For handling this case, I thought its better to have reviews belong to the listing/Repairshop user.

Here is my review table generated through rails g migration and reference commands:

class AddUserToReviews < ActiveRecord::Migration
 def change
    	add_reference :reviews, :user, foreign_key: true
	end
end

class AddOwnerUserToReviews < ActiveRecord::Migration
  def change
  	add_column :reviews, :owner_id, :integer
  end
end


create_table "reviews", force: :cascade do |t|
    t.string   "comment"
    t.integer  "rating"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.integer  "user_id"
    t.integer  "owner_id"
end

For each review, I have a comment, a rating, a user_id (to store review user) and an owner_id (owner of that particular listing/repairshop).

Here is my new review form using hidden fields:

<%= bootstrap_form_for [@owner, @review] do |f| %>
  <% if @review.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@review.errors.count, "error") %> prohibited this review from being saved:</h2>

      <ul>
      <% @review.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <%= f.select :rating, [1,2,3,4,5], :label => "CHOOSE YOUR RATING (1 is poor, 5 is great)" %>
  <%= f.text_area :comment, :label => "YOUR COMMENTS" %>
  <%= f.hidden_field :repairshop_id, :value => params[:repairshop_id]  %>  
  
  <%= f.submit "SUBMIT", class: "btn btn-danger" %>    
  
<% end %>

The main problem was after the review has been added, it should redirect the user to the resource from which the action was called. Either it should be the listing or the repairshop. Keep in mind the review belong to a user and not a repairshop/listing/

First, I thought about using hidden params in the form, passing get params through the link (in repairshop) itself and adding it before new and create action. My main purpose was to persist a variable which stores the repairshop_id/listing_id and then use that to redirect back to the resource from which new review action was called.

<%= link_to 'Add A Review', new_user_review_path(@user, :repairshop_id => @repairshop.id), class: "btn btn-danger btn-xs", style: "border: #e20049" %


class ReviewsController < InheritedResources::Base

    before_action :authenticate_user!, only: [:new, :create, :destroy, :update] 

    before_action :set_parent, only:  [:new, :create] 
    before_action :set_child_and_parent, only:  [:destroy, :update, :update, :edit]


    def new
        @review = Review.new        
    end

    def edit

    end

    def create
        @review = Review.new(review_params)         
        @review.user_id = current_user.id
        @review.owner_id = @owner.id        
        

        respond_to do |format|
            if @review.save 
                format.html { redirect_to @parent, notice: 'Review was successfully created.' }
                format.json { render :show, status: :created, location:@parent }
            else                
                format.html { render :new }
                format.json { render json: @review.errors, status: :unprocessable_entity }
            end
        end

    end

    def destroy

        @review.destroy
        respond_to do |format|                      
            format.html { redirect_to @parent, notice: 'Review  was successfully destroyed.' }
            format.json { head :no_content }
        end

    end

    def update
        respond_to do |format|
            if @review.update(review_params)
                format.html { redirect_to @parent, notice: 'Review was updated.' }
                format.json { render :show, status: :ok, location: @parent}
            else
                format.html { render :edit }
                format.json { render json: @review.errors, status: :unprocessable_entity }
            end
        end
    end

    private

    def review_params
      params.require(:review).permit(:comment, :rating, :repairshop_id)
    end

    def set_child_and_parent
        @review = Review.find_by_id(params[:id])
        if params[:repairshop_id]
            @parent =  Repairshop.find_by_id(params[:repairshop_id])
        end

        if params[:listing_id]
            @parent = Listing.find_by_id(params[:listing_id])   
        end

        @owner = User.find_by_id(@parent.user_id)
    end

    def set_parent      
        if params[:repairshop_id]
            @parent =  Repairshop.find_by_id(params[:repairshop_id] )
        end

        if params[:listing_id]
            @parent = Listing.find_by_id(params[:listing_id] )  
        end

        @owner = User.find_by_id(params[:user_id])
    end


end

But I got these errors:

Piq24

Then I thought why not have some local variables in the class which we don't persist in the database but use in all the methods.

So I tried:

class ReviewsController < InheritedResources::Base
  
	before_action :authenticate_user!, only: [:new, :create, :destroy, :update]	

	before_action :set_parent, only:  [:new]
	before_action :set_child_and_parent, only:  [:destroy, :update, :edit]

	attr_accessor :parent_id, :parent_type, :owner_id, :parent, :var
	
	def new
		@review = Review.new		
		post_initialize(params[:parent_id].to_s, params[:parent_type].to_s)	
		@owner = User.find_by_id(self.owner_id)
	end

	def edit				
	end

	def create
		@review = Review.new(review_params)			
		@review.user_id = current_user.id
		@review.owner_id = self.owner_id			
		respond_to do |format|
			if @review.save	
				format.html { redirect_to Repairshop.find_by_id(self.parent_id), notice: 'Review was successfully created.' }
		        format.json { render :show, status: :created, location:parent }
			else				
				format.html { render :new }
		        format.json { render json: @review.errors, status: :unprocessable_entity }
			end
		end
	end
	def destroy

		@review.destroy
		respond_to do |format|						
		    format.html { redirect_to @parent, notice: 'Review  was successfully destroyed.' }
		    format.json { head :no_content }
		end
	end
	def update
		respond_to do |format|
	      	if @review.update(review_params)
		        format.html { redirect_to @parent, notice: 'Review was updated.' }
		        format.json { render :show, status: :ok, location: @parent}
	      	else
		        format.html { render :edit }
		        format.json { render json: @review.errors, status: :unprocessable_entity }
	    	end
    	end
	end

	def post_initialize(parent_id, parent_type)
		self.parent_id =  parent_id.to_s
		self.parent_type =  parent_type.to_s
		self.parent = parent_type.to_s.strip == "repairshop" ? Repairshop.find_by_id(self.parent_id) : Listing.find_by_id(self.parent_id)		
		self.owner_id =  self.parent.user_id		
		self.var = "hello world"
							
	end


  private

  def review_params		
      	params.require(:review).permit(:comment, :rating)
  end

   def set_child_and_parent    	
    	@review = Review.find_by_id(params[:id])		
   end

   def set_parent     			
	@owner = User.find_by_id(@parent.user_id)
   end

end

Turns out, rails make a new object instance for every method mapped with HTTP actions. So each time we call new, create etc, it creates a new object. So calling post_initialize(parent_id, parent_type) method in new and using it to set the local variables from params and then using it in create to redirect won't work.

What solved my problem was to use session params.


A session is just a place to store data during one request that you can read during later requests.

I decided to use session hash to store the resource id params I get from the new link in the resource it was called from and then use the session hash to retrieve the parent to redirect to after a review has been created.

class ReviewsController < InheritedResources::Base
  
	before_action :authenticate_user!, only: [:new, :create, :destroy, :update]		
	before_action :set_child_and_parent, only:  [:destroy, :update, :edit]

	def new
		@review = Review.new				
		session[:parent_id] = params[:parent_id]
		session[:parent_type] = params[:parent_type]
		@parent = get_parent(params[:parent_type], params[:parent_id])		
		@owner = User.find_by_id(@parent.user_id)		
	end
	
	def create		
		@review = Review.new(review_params)			
		@review.user_id = current_user.id
		@parent = get_parent(session[:parent_type], session[:parent_id])
		@owner = User.find_by_id(@parent.user_id)		
		@review.owner_id = @owner.id			 
						
		respond_to do |format|
			if @review.save	
				format.html { redirect_to @parent, notice: 'Review was successfully created.' }
		        format.json { render :show, status: :created, location:@review }
			else				
				format.html { render :new }
		        format.json { render json: @review.errors, status: :unprocessable_entity }
			end
		end
	end
	

private

    def review_params		
      	params.require(:review).permit(:comment, :rating)
    end

    def set_child_and_parent    	
    	@review = Review.find_by_id(params[:id])		
    end
   
    def get_parent(parent_type, parent_id)
	    if parent_type == "repairshop"
			parent = Repairshop.find_by_id(parent_id.to_i)
		elsif parent_type == "listing"
			parent = Listing.find_by_id(parent_id.to_i)
		else
			parent = root_path
		end

		return parent
    end

end

As seen below, finally the problem was solved!!

So, if you want to persist a variable throughout different controllers actions, use session hash and store what you want with a key.

Also, we don't need hidden fields in the review form to make it work.

<%= bootstrap_form_for [@owner, @review] do |f| %>
  <% if @review.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@review.errors.count, "error") %> prohibited this review from being saved:</h2>

      <ul>
      <% @review.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <%= f.select :rating, [1,2,3,4,5], :label => "CHOOSE YOUR RATING (1 is poor, 5 is great)" %>
  <%= f.text_area :comment, :label => "YOUR COMMENTS" %>    
  <%= f.submit "SUBMIT", class: "btn btn-danger" %>    
  
<% end %>

Owned & Maintained by Saurav Prakash

If you like what you see, you can help me cover server costs or buy me a cup of coffee though donation :)