0

I am building a rails site that is has gyms and reviews. I would like users to be able to leave reviews for gyms. I have my tables set up as

class Gym < ActiveRecord::Base
  has_many :pictures, as: :imageable
  has_many :reviews
end

and

class Review < ActiveRecord::Base
  belongs_to :user
  belongs_to :gym
  validates :body, presence: true, length: { maximum: 1000 }
  validates :rating, presence: true
end

Right now the gym controller is static (can't CRUD gyms, as that's an admin thing) and just renders the pages w/ info. I am trying to add reviews, but I don't want to muddle associations. Here is my gym controller info

class GymsController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
  before_action :correct_user,   only: [:edit, :update]

  def index
    @q = Gym.ransack(params[:q])
    @gyms = @q.result
    @other_gyms = Gym.all
    if @gyms.to_a.count < 1
      flash[:warning] = "No gym matched #{params[:q][:name_or_phone_number_or_city_or_zip_code_cont]}"
    end
  end

  def new
    @gym = Gym.find(params[:id])
    @review = @gym.review.new
  end

  def create
    @gym = Gym.find(params[:id])
    @review = @gym.reviews.build(gym_params)
    if @review.save
      flash[:success] = 'Review Saved'
      redirect_to :back
    else
      render 'new'
    end
  end

  def show
    @gym = Gym.find(params[:id])
    @reviews = @gym.reviews
  end

  private
  def gym_params
  params.require(:gym).permit(:name, :description, :address, :address_2, :zip_code,
                              :phone_number, :website_url, :city, :state, :latitude, :longitude,
                              review_attributes: [:user_id, :rating, :body, :gym_id])
  end

  def logged_in_user
    unless logged_in?
      store_location
      flash[:danger] = 'Please log in'
      redirect_to login_url
    end
  end

  def correct_user
    @user = User.find(params[:id])
    redirect_to(root_url) unless current_user?(@user)
  end
end

my routes

resources :gyms, only: [:index, :show] do
  resources :reviews
end

and the gym/show link_to which points to gyms/:id/reviews

In gyms/new I have the review form

<%= form_for [@gym, @review] do |f| %>
   <%= f.label :rating, 'Select your rating' %>
   <div id='ratyRating'></div><br>

   <%= f.text_area :body, size: '100x10' %>

   <%= f.hidden_field :user_id, value: current_user.id %>

   <%= f.submit 'Post', class: 'btn btn-gen' %>
<% end %>

this does not work. and from the link_to button I get it directing to gyms/:id/reviews which is an index page. I feel like there is a much better way to do this. Does anyone see what I am doing wrong here?

3
  • I see a bunch of things going wrong here your correct_user filter gets the user id from params[:id]. flash[:warning] = "No gym matched #{params[:q][:name_or_phone_number_or_city_or_zip_code_cont]}" exposes your users to an injection vulnerability since you are echoing back the params. Commented Sep 21, 2016 at 17:58
  • thanks for catching that! I had no idea. could you elaborate more? Commented Sep 21, 2016 at 18:38
  • guides.rubyonrails.org/security.html#cross-site-scripting-xss Commented Sep 21, 2016 at 19:01

2 Answers 2

1

Start by running $ rake routes from the console. That will tell you that POST /gyms/:gym_id/reviews will be handled by ReviewsController not GymsController.

Which is exactly as it should be since each controller should only be responsible for CRUD'ing a single resource.

class ReviewsController < ApplicationController

  before_action :set_gym!

  # GET /gyms/:gym_id/reviews
  def index 
    @reviews = @gym.reviews
  end

  # POST /gyms/:gym_id/reviews
  def create
    @review = @gym.reviews.new(review_params) do |r|
      r.user = current_user
    end
    if @review.save
      redirect_to @gym, success: 'Review created!'
    else 
      render :new
    end
  end

  private
    def set_gym!
      @gym = Gym.find(params[:gym_id])
    end

    def review_params
      params.require(:review).permit(:body)
    end
end

Some things to take notice of here - don't pass the user id via the form. It makes it way to easy to spoof. Instead get the current user from the session or a token.

Lets create a partial for the form:

<%= form_for [gym, review] do |f| %>
   <%= f.label :rating, 'Select your rating' %>
   <%= f.text_area :body, size: '100x10' %>
   <%= f.submit 'Post', class: 'btn btn-gen' %>
<% end %>

When then need a reviews/new.html.erb view that is rendered if the review is invalid:

<%= render partial: 'form', gym: @gym, review: @review %>

We can then also embed the form in gyms/show.html.erb:

<%= render partial: 'reviews/form', gym: @gym, review: @gym.reviews.new %>
Sign up to request clarification or add additional context in comments.

Comments

1

It looks like you are trying to create a review from the gyms controller. That would be a nested form, which would require a accepts_nested_attributes_for in your gym model:

class Gym < ActiveRecord::Base
  has_many :pictures, as: :imageable
  has_many :reviews
  accepts_nested_attributes_for :reviews
end

Your form needs to be reworked with fields_for:

<%= form_for @gym do |f| %>

       <%= f.fields_for :reviews do |reviews_form| %>

          <%= reviews_form.label :rating, 'Select your rating' %>
          <div id='ratyRating'></div><br>

          <%= reviews_form.text_area :body, size: '100x10' %>

          <%= reviews_form.hidden_field :user_id, value: current_user.id %>
       <% end %>

   <%= f.submit 'Post', class: 'btn btn-gen' %>
<% end %>

Your gym_params needs to look like this with reviews_attributes, not review_attributes

def gym_params
  params.require(:gym).permit(:name, :description, :address, :address_2, :zip_code,
                              :phone_number, :website_url, :city, :state, :latitude, :longitude,
                              reviews_attributes: [:user_id, :rating, :body, :gym_id])
end

Then in your new action, you are creating a new Gym instance and you are missing the plural of review in when you create a @review instance:

@gym = Gym.new
@review = @gym.reviews.build

Remember that a gym has_many reviews - so you are going to use the plural reviews when possible.

Not sure if i caught everything, but i would recommend checking out the Rails guide on Nested Forms, section 9.2 for a really good explanation. Nested forms can be tricky, and another simpler option would be to have a separate review form that's created in the reviews controller (see @max's answer)

2 Comments

Note that there is a pretty big difference here - nested_attributes is only really useful when you need the user to be able to create several things in the same request. In this case it would be the same user creating a gym and review at the same time - which is probably not what you want.
I agree with you that it's less useful, as I did mention in my last sentence. Oh well, doesn't hurt to learn a different way.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.