Reputation: 533
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?
Upvotes: 0
Views: 1027
Reputation: 102368
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 %>
Upvotes: 1
Reputation: 1374
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)
Upvotes: 1