I have my app setup where users can write reviews for a movie. What I'd like to do is limit the user to create only one review per movie. I've managed to accomplish this in my reviews controller as so:
class ReviewsController < ApplicationController
before_action :has_reviewed, only [:new]
....
def has_reviewed?
if Review.where(user_id: current_user.id, movie_id: #movie.id).any?
redirect_to movie_reviews_path
flash[:notice] = "You've already written a review for this movie."
end
end
end
Where I'm now having trouble is translating this same logic into my index view template with the helper methods of Devise and CanCanCan at my disposal.
<% if user_signed_in? && ... %> # current_user has already created a review for this movie
<%= link_to "Edit Review", edit_movie_review_path(#movie, review) %>
<% else %>
<%= link_to "Write a Review", new_movie_review_path %>
<% end %>
Also: Is there any way to improve the lookup in my has_reviewed? method? I feel like there's a better way to write it but can't determine the most appropriate fix.
Why not use a validation:
#app/models/review.rb
class Review < ActiveRecord::Base
validates :movie_id, uniqueness: { scope: :user_id, message: "You've reviewed this movie!" }
end
This is considering your review model belongs_to :movie
You could also use an ActiveRecord callback:
#app/models/review.rb
class Review < ActiveRecord::Base
before_create :has_review?
belongs_to :user, inverse_of: :reviews
belongs_to :movie
def has_review?
return if Review.exists?(user: user, movie_id: movie_id)
end
end
#app/models/user.rb
class User < ActiveRecord::Base
has_many :reviews, inverse_of: :user
end
Is there any way to improve the lookup in my has_reviewed? method?
def has_reviewed?
redirect_to album_reviews_path, notice: "You've already written a review for this album." if current_user.reviews.exists?(movie: #movie)
end
Why not make a has_reviewed? method on your User class?
e.g.
def has_reviewed?(reviewable)
# query in here
end
Then you should be able use that just fine in your controller and your views.
You will want to do this for both new and create. Otherwise a savvy user would be able to run a post that would get past your new action.
I would put the link_to in either a helper or a presenter object. It would generally look like this.
def create_or_edit_review_path(movie, current_user)
return '' if current_user.blank?
if current_user.review.present?
#Generate review edit link
else
#generate new link
end
end
After that in all of your views it would just be
<%= create_or_edit_review_path(#movie, current_user) %>
Then in your controller for both new and create you could do either a before action or just redirect on each.
before_action :enforce_single_review, only: [:create, :new]
def enforce_single_review
if current_user.review.present?
redirect_to review_path(current_user.review)
end
end
Here's what I came up with:
I created an instance method to retrieve a user's movie review using the find_by method on the Review model:
class User < ActiveRecord::Base
....
def movie_review(album)
Review.find_by(user_id: self, album_id: album)
end
end
This method also comes in handy when setting up my callback:
class ReviewsController < ApplicationController
before_action :limit_review, only: [:new, :create]
....
private
def limit_review
user_review = current_user.movie_review(#movie)
if user_review.present?
redirect_to edit_movie_review_path(#movie, user_review)
end
end
end
Created a helper method for showing the appropriate link to edit or create a review. Big thanks to Austio and his suggestion:
module ReviewsHelper
def create_or_edit_review_path(movie)
user_review = current_user.movie_review(movie) if user_signed_in?
if user_signed_in? && user_review.present?
link_to "Edit review", edit_movie_review_path(movie, user_review)
else
link_to "Write a review", new_movie_review_path
end
end
end
And at last this is how I call the helper in my view template(s):
....
<%= create_or_edit_review_path(#album) %>
Related
I'm making simple CRUD and the current goal is to add data. However, I found that I can't add any data, and the terminal log also shows "[Webpacker] Everything's up-to-date. Nothing to do", which means there is no error message.
According to my design in the controller, the new data must have failed, so I stopped at new.html.erb. I'm guessing it has something to do with the model's relationship.
This is model User
class User < ApplicationRecord
has_many :reviews
has_many :recipes, through: :reviews
end
This is the model Recipe
class Recipe < ApplicationRecord
has_many :reviews
belongs_to :user
end
This is model Review
class Review < ApplicationRecord
belongs_to :user
belongs_to :recipe
end
This is the RecipeController
class RecipesController < ApplicationController
def index
#recipes = Recipe.all
end
def new
#recipe = Recipe.new
end
def create
#recipe = Recipe.new(recipe_params)
if #recipe.save
redirect_to recipes_path, notice: "Successful!"
else
render :new
end
end
private
def recipe_params
params.require(:recipe).permit(:title, :money)
end
end
this is the web page
<h1>Add New One</h1>
<%= form_for(#recipe) do |r| %>
<%= r.label :title, "Title" %>
<%= r.text_field :title%>
<%= r.label :money, "Budget" %>
<%= r.text_field :money %>
<%= r.submit %>
<% end %>
<%= link_to "Back to list", recipes_path %>
You should first add a callback to ensure that only signed in users can create recipes (unless you actually want to let anomynous users create/update/delete recipies).
For example with Devise you would use its authenticate_user! helper which will bail and redirect to the sign in path if the user is not authenticated:
class RecipesController < ApplicationController
before_action :authenticate_user!, only: [:new, :create]
# ...
end
If you're reinventing the authentication wheel you should create a similiar method which is used to prevent access.
You would then initialize the resource off the current user:
class RecipesController < ApplicationController
before_action :authenticate_user!, except: [:show, :index]
def create
#recipe = current_user.recipes.new(recipe_params)
if #recipe.save
redirect_to recipes_path, notice: "Successful!"
else
render :new
end
end
end
Here I am assuming that you have a current_user method which will retrieve the user based on an id stored the session.
Since you have an indirect assocation this will create a row in the reviews table with the users id and the recipe id as the record in the recipies table.
You also want to display the validation errors in the form so that the user gets feedback.
You are probably right that it is a validation error related to the belongs_to relationship. You should display validation errors for your form as described here https://guides.rubyonrails.org/getting_started.html#validations-and-displaying-error-messages
Im new to rails and am having difficulty creating a profile by a devise user
here is the ProfileController:
class ProfilesController < ApplicationController
before_action :set_profile, only: [:show, :edit, :update, :destroy]
before_action :authenticate_user!
def index
#profiles = Profile.all
end
def new
#profile = current_user.build_profile
end
def create
#profile = current_user.build_profiles(profile_params)
redirect_to profiles_path
end
def show
end
def edit
end
def update
#profile.update(profile_params)
redirect_to(profiles_path(#profile))
end
def destroy
#profile.destroy
redirect_to profiles_path
end
private
def profile_params
params.require(:profile).permit(:university, :degree)
end
def set_profile
#profile = Profile.find(params[:id])
end
end
When i run the rails server i can submit the form but nothing is getting stored in the model 'Profile'
here is the index.html.erb where the data should appear:
<h2>profiles</h2>
<% #profiles.each do |profile| %>
<%= link_to profile do %>
<%= profile.university %>
<% end %>
<%= profile.degree %>
<% end %>
user.rb file:
has_one :profile
and the profile.rb file:
belongs_to :user
nothing seems to be getting saved to the profile model and nothing is getting displayed on the index.html.erb. Also i have created a migration to store user_id in the profile model.
Thank for your help
By far the best way to create a profile for a user is to build it when the User object is created:
#app/models/user.rb
class User < ActiveRecord::Base
has_one :profile
before_create :build_profile
accepts_nested_attributes_for :profile
end
#app/models/profile.rb
class Profile < ActiveRecord::Base
belongs_to :user
end
This will build a blank profile each time a new user is created. It means that each user will only ever have one profile, which they'll be able to populate & edit.
In regards your issue, there are several points:
Don't list "profiles" with an index, list users & pull their profile data
If managing a profile, nest it under the associative user model
Here's how to do that:
# config/routes.rb
resources :users, only: :index
resource :profile, only: [:show, :update]
#app/controllers/profiles_controller.rb
class ProfilesController < ApplicationController
def show
end
def update
redirect_to :show if current_user.update profile_params
end
private
def profile_params
params.require(:user).permit(profile_attributes: [:name])
end
end
#app/views/profiles/show.html.erb
<%= form_for current_user, url: profile_path do |f| %>
<%= f.fields_for :profile do |p| %>
<%= p.text_field :name %>
<% end %>
<%= f.submit %>
<% end %>
Update
My post above is exactly what we do.
The way it works is very simple -- when a User is created (IE they have gone to the trouble of filling out their details), the Rails backend automatically creates a blank Profile object.
This does several things:
Always makes sure you have a Profile for each user (you don't have to go to the bother of making them "create" a profile).
Gives you the ability to validate only inputted data on a created Profile (not having to guess whether it's already been done).
--
If you're getting undefined method build_profile, it means your associations are incorrect.
All singular associations have build_[association] as defined instance methods. The code I provided fires build_profile for a has_one association. The only time it would be "undefined" would be if the association was plural
--
Update
This suggests a routing error.
Considering it appears at root, I think the problem is here:
#app/views/layouts/application.html.erb
<%= link_to "Profile", profile_path %>
You don't have edit_profile_path -- it should just be profile_path
It needs a save call for the profile which you build in crate method.
e.g.
def create
#user = User.new(user_params)
if #user.save
redirect_to root_url
else
render :new
end
end
if condition checks is data save correct or not. And change example's User model to your Profile model
BTW, just careful with the plural, I think it should use 'build_profile' instead 'build_profiles'
I have been trying to build an app offering discounted vacation trips such that:
(1) a user(travel agent) can compose a trip by combining hotels (hotel chains) and cities
(2) a user(regular user) can review hotels and cities, s/he has already visited.
(3) another user can evaluate how good the deal is with respect to the country and hotel the travel agent will have him/her stay.
The models look like this
class User < ActiveRecord::Base
has_many :trips
has_many :reviews
end
class Trip < ActiveRecord::Base
belongs_to :user
belongs_to :hotel
belongs_to :city
end
class Hotel < ActiveRecord::Base
belongs_to :city
has_many :reviews, as: :reviewable
end
class City < ActiveRecord::Base
has_many :hotels
has_many :reviews, as: :reviewabel
end
class Review < ActiveRecord::Base
belongs_to :user
belongs_to :reviewable, polymorphic: true
end
The problem is I can figure out how to create the controllers for Hotel and City because they are only created in the context of a makeshift trip. I checked the rails casts on nested forms and the use of accepts_nested_attributes_for but I can't seem to get it right.
Note: the reason why I separated the hotels and the cities is to be able to retrieve the reviews independently. Such that Say I enjoyed my stay at the Four Seasons in Toronto but not the one in NY. - because of the cities/hotels (=> accommodating the case where I didn’t enjoy it because the hotel was crap and the one where I didn’t because the city was)
Note 2: I understand it doesn’t make much sense to seperate hotels and cities in this example - I made a mistake in self-appointing the tutorial. But the problem has been haunting me, what if it was an delivery order instead with entree/meal/dinner instead of hotels and cities, or restaurant chains and neighborhoods.
Any help is appreciated. Thank you
Edit
Edited after Settheline’s comment.
I mean the create actions for cities and hotels only exist in the context of a Trip create action.
Trip has 2 attributes: title & description: It’s only then that I “log” the itinerary. Here’s what my controllers look like to give you a better idea
class TripsController < ApplicationController
before_action :signed_in_user
def show
#trip = Trip.find(params[:id])
end
def index
#trips = current_user.Trip.all
end
def new
#trip = Trip.new
end
def create
# #trip = Trip.new(trip_params)
#trip = current_user.trips.build(trip_params)
if #trip.save
flash[:success] = "Your trip was successfully published!"
redirect_to #trip
else
render 'new'
end
end
def edit
end
def update
if #trip.update_attributes(trip_params)
flash[:success] = "Trip was updated"
redirect_to #trip
else
render 'edit'
end
end
def destroy
Trip.find(params[:id]).destroy
flash[:success] = "trip was deleted. Thank you"
redirect_to #user #root_url
end
private
def trip_params
params.require(:trip).permit(:title, :description)
end
end
class CitiesController < ApplicationController
before_action :signed_in_user
def create
#city = City.new(city_params)
if #city.save
# flash[:success] = ""
else
render 'new'
end
end
# def destroy
# City.find(params[:id]).destroy
# flash[:success] = “City was deleted."
# redirect_to root_url
# end
private
def city_params
params.require(:city).permit(:name, :province, :country)
end
end
class HotelsController < ApplicationController
before_action :signed_in_user
def create
#similar to city
end
def destroy
#similar to city
end
private
def hotel_params
params.require(:hotel).permit(:name, :address,
:management_contact,
:city_id)
end
end
And here’s the problem:
I want to have/add create forms within the trip one in
sample_app/app/views/trips/new.html.erb
<% provide(:title, 'New Trip') %>
<h1>New Trip</h1>
<div class="row">
<div class="span6 offset3">
<%= form_for(#trip) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<%= f.text_field :title, placeholder: "Type in a title" %>
<%= f.text_field :description, placeholder: "Any additional info." %>
<%= f.submit "Publish", class: "btn btn-large btn-primary" %>
<% end %>
</div>
</div>
accepts_nested_attributes_for allows you to save attributes on associations. Although you do have associations in your models it doesn't necessarily mean that you need to use accepts_nested_attributes_for. It depends on how the code flows through your controllers.
Simple Example
For example, you would probably want to allow your users to view their trips and reviews. First you'll need a method to get the current user:
users_controller.rb
class ApplicationController < ActionController::Base
def current_user
#current_user ||= User.find(session[:user_id])
end
end
This method will be inherited by all of your controllers and allow them to get the current user. (There are many solutions out there for getting the current user, this is definitely not a good solution but it is OK for demonstration purposes).
Trips & Reviews
Now you can create some controllers for the current user to view their trips and reviews:
trips_controller.rb
class TripsController < ApplicationController
def index
#trips = current_user.trips.all
end
end
reviews_controller.rb
class ReviewsController < ApplicationController
def index
#reviews = current_user.reviews.all
end
end
Now you have some controller actions displaying the trips/reviews for the current user. I think this example demonstrates how you can create your controllers and that accepts_nested_attributes_for is not necessarily required.
I have the following association setup
class Donation < ActiveRecord::Base
belongs_to :campaign
end
class Campaign < ActiveRecord::Base
has_many :donations
end
in my donation controller i have campaign_id as a strong parameter. Now when creating a donation i would like to have the campaign_id available to save in that form, but better off in the controller somewhere im guessing as less open to being edited. To get from the campaign to the donation form i click a button
<%= link_to 'Donate', new_donation_path, class: 'big-button green' %>
as we are already using
def show
#campaign = Campaign.find(params[:id])
end
How can i get that id into my donations form?
Thanks
It sounds like you're looking for Nested Resources.
Basically, your routes.rb might look something like:
resources :campaigns do
resources :donations
end
Your controller might look something like:
class DonationsController < ApplicationController
before_action :find_campaign
def new
#donation = #campaign.donations.build
end
def create
#donation = #campaign.donations.create(params[:donation])
# ...
end
protected
def find_campaign
#campaign ||= Campaign.find(params[:campaign_id])
end
end
And your example link, something like...
<%= link_to 'Donate', new_campaign_donation_path(#campaign), class: 'big-button green' %>
I have a simple Model with Movies and a model for Users. Whats the best way to save which movies has been seen by a user? Should i create a "seen_movie" model? And a Controller for it? How should the action to toggle the status been called? I thought about something like this:
Model:
class SeenMovie < ActiveRecord::Base
belongs_to :user
belongs_to :movie
def self.has_seen?(user)
#has_seen = SeenMovie.find_by_user(user)
return #has_seen
end
end
Controller:
class SeenMoviesController < ApplicationController
before_filter :require_login
def update # any other name? how to call this action?
if has_seen?(current_user)
render text: "delete record"
else
render text: "create record"
end
end
private
def seen_movie_params
params.require(:rating).permit(:movie_id)
end
end
I am not sure if this works and if it is a good way. For me it doesn't looks like a good way to toggle a simple state...
Info: Im very new in Rails (came from PHP)
Here's how I would handle it:
In your model:
class User < AR::Base
has_many :seen_movies
has_many :movies, through: :seen_movies
def has_seen?(movie)
movies.where(id: movie.id).first.present?
end
end
class Movie < AR::Base
has_many :seen_movies
has_many :users, through: :seen_movies
end
In your controller:
class SeenMoviesController < ApplicationController
before_filter :require_login
def update
#movie = Movie.find(params[:movie_id])
if current_user.has_seen?(#movie)
render text: "delete record"
else
render text: "create record"
end
end
private
def seen_movie_params
params.require(:rating).permit(:movie_id)
end
end
In your views:
<% #movies.each do |movie| %>
<%= link_to "Seen it!", seen_movie_path(movie_id: movie.id), method: :put %>
<% end %>