Rails 5, I am trying to add Likes from scratch without a gem dependency. I am so close to having it down but am being completely stumped by what's happening when Comments get involved.
Storing and saving article.likes worked perfectly. Then I got the comment.likes to work. Now, when I like a Comment, they individually store a new Like (great!), except now Article is not properly saving any likes, and even weirder: article.likes.count gives a TOTAL sum of all it's comments' likes.. I am sure this is an easy fix but I am just totally missing it and I've tried everything. I've gone down some deep rabbit holes for this.
I think the problem lies in routing, or how the models relate.
Articles have many Comments, and both of them have many Likes.
Here are the models starting with like.rb:
class Like < ApplicationRecord
belongs_to :user
belongs_to :article
belongs_to :comment
# Make sure that one user can only have one like per post or comment
validates :user_id, uniqueness: { scope: [:article_id, :comment_id] }
end
article.rb
class Article < ApplicationRecord
belongs_to :user
...
# destroy associated comments on article deletion
has_many :comments, dependent: :destroy
has_many :likes, dependent: :destroy
end
comment.rb
class Comment < ApplicationRecord
belongs_to :article
belongs_to :user
...
has_many :likes, dependent: :destroy
end
routes.rb
...
resources :articles, :path => 'blog' do
resources :likes, only: [:create, :destroy]
resources :comments do
resources :likes, only: [:create, :destroy]
end
end
the meaty likes_controller.rb. Mind the #{}, EVERYTHING checks out the way it should, so why does the comment.likes.create save correctly, but the article.likes.create does not?? Help, please.
class LikesController < ApplicationController
before_action :get_signed_in_user
before_action :comment_or_article
def create
if #comment_like
like_resource(comment)
else
like_resource(article)
end
end
def destroy
if #comment_like
comment.likes.where(user: current_user).destroy_all
else
article.likes.where(user: current_user).destroy_all
end
flash[:success] = "Unliked! :("
redirect_to article_redirect(article)
end
private
def like_resource(obj)
if obj.likes.where(user: current_user.id).present?
flash[:error] = "You've already upvoted this! + #{like_params} + #{#comment_like} + #{obj}"
if #comment_like
if obj.likes.create(user: current_user, article: #article)
flash[:success] = "Upvoted Comment! + #{like_params} + #{#comment_like} + #{obj}"
else
flash[:error] = "An error prevented you from upvoting."
end
elsif obj.likes.create(user: current_user)
flash[:success] = "Upvoted Blog! + #{like_params} + #{#comment_like} + #{obj}"
else
flash[:error] = "An error prevented you from upvoting."
end
redirect_to article_path(article)
end
def get_signed_in_user
unless user_signed_in?
flash[:error] = "Sign in to upvote!"
redirect_back(fallback_location: articles_path)
end
end
# decide what we are liking
def comment_or_article
if comment
#comment_like = true
else
#comment_like = false
end
end
def article
#article = Article.find(params[:article_id])
end
def comment
unless params[:comment_id].nil?
#comment = article.comments.find(params[:comment_id])
end
end
def like_params
params.permit( :article_id, :comment_id).merge(user_id: current_user.id)
end
end
Finally the like buttons in my articles/show.html.erb:
<%= link_to "Like", article_likes_path(#article), method: :post %>
<%= "#{#article.likes.count}" %>
... # inside loop to show comments:
<%= link_to "Like", article_comment_likes_path(#article, comment), method: :post %>
<%= "#{comment.likes.count}" %>
TLDR:
Comment likes work fine, save fine, count individually fine.
Article likes do not save, but article.likes.count === article.comments.likes.count. Why?
I want article.likes to be completely unique, like it's own comments are.
Thank you in advance.
EDIT: took out belongs_to :comments in like.rb and refactored like_controller.rb main function to
private
def like_resource(obj)
if obj.likes.where(user: current_user.id).present?
flash[:error] = "You've already upvoted this!"
elsif obj.likes.create(user: current_user, article: #article)
flash[:success] = "Upvoted!"
else
flash[:error] = "An error prevented you from upvoting."
end
redirect_to article_path(article)
end
Always supplying the #article helps when liking a comment. An Article like would not need a comment_id to save.
Sorry for the long post, hopes this helps someone.
I just figured it out, actually.
class Like < ApplicationRecord
belongs_to :user
belongs_to :article
# belongs_to :comment
end
Commenting the above out, it allows the #article "like" to save without a comment being referenced. Article likes are saved properly. However, article.likes.count still increments whenever an article.comment is liked. This means article.likes is always >= article.comments.likes; which is completely fine.
I just changed the #article.likes to:
<%= "#{#article.likes.where(comment_id: nil).count}" %>
Filtering out all the strictly article.likes. The comment.likes still work perfectly.
Related
Making a site in with three main models: Users, Posts, and Gyms. Users should be able to post either from their own model (User.post), or, if they are the admin of a gym, from the Gym's model (Gym.post).
I'm using the same post controller and post form to post fro either the gym or the user, but the controller "Create" action can't distinguish between the two.
class PostsController < ApplicationController
before_action :logged_in_user, only: [:create, :destroy]
before_action :correct_user, only: :destroy
def create
if (gym.gym_admin == current_user.id)
#post = gym.posts.build(post_params)
if #post.save
flash[:success] = "Post!"
redirect_to "/gyms/#{gym.id}"
else
#feed_items = []
render 'static_pages/home'
end
else
#post = current_user.posts.build(post_params)
if #post.save
flash[:success] = "Post!"
redirect_to root_url
else
#feed_items = []
render 'static_pages/home'
end
end
end
def destroy
#post.destroy
flash[:notice] = "Post deleted"
redirect_to request.referrer || root_url
end
private
def post_params
params.require(:post).permit(:post_type, :title, :content, :picture, :body_parts,
:duration, :equipment, :calories, :protein,
:fat, :carbs, :ingredients, :tag_list,
:postable_id, :postable_type)
end
def correct_user
#post = current_user.posts.find_by(id: params[:postable_id])
redirect_to root_url if #post.nil?
end
def gym
#gym = Gym.find_by(params[:id])
end
end
And the Models:
class Post < ApplicationRecord
belongs_to :user
belongs_to :gym
belongs_to :postable, polymorphic: true
class User < ApplicationRecord
has_many :posts, as: :postable, dependent: :destroy
has_many :gyms
class Gym < ApplicationRecord
has_many :posts, as: :postable, dependent: :destroy
belongs_to :user
Rught now, this create action only creates posts from the gym's model; if I remove the first half of the conditional, it will only post from the User model.
Any help is greatly appreciated, thank you
I would be curious what gym.gym_admin (and consequently the whole line below 'def create') evaluates to since I don't see in referenced anywhere else.
My suspicion is that you would want to change
if (gym.gym_admin == current_user.id)
to
if (gym.gym_admin.id == current_user.id)
or
if (gym.gym_admin == current_user)
once that relationship is working correctly.
Also, could post be built independently of whether a user is a gym admin and send the post params the gym_id if applicable. Then accessed either through:
/gym/:id
#posts = Post.where('gym_id = ?', params[:id])
or
/user/:id
#posts = Post.where('user_id = ?', params[:id])
I fixed it my removing the conditional logic altogether; I just made two separate custom actions in the controller and called them from different links. ie. :actions => create_gym / create_user
I will have an option were comment model can be used for a user to post in a person profile page and community page. Currently I'm working on community and would like some direction as I'm confused.
Current error I get is ActiveModel::ForbiddenAttributesError for my CommentsController#Create. Would like help if possible or help me point in the direction of fixing my mistake.
questions in regards to what view people are seeing comment is /communities/show
Models
User
has_one :profile
has_many :communities
has_many :comments, dependent: :destroy
Community
extend FriendlyId
friendly_id :title, use: [:slugged, :finders]
has_many :comments, dependent: :destroy
belongs_to :user
Comment
belongs_to :user
belongs_to :community
Routes
resources :communities do
resources :comments
end
Controllers
Communities
def show
#community = Community.friendly.find(params[:id])
#current_user = User.find(session[:user_id])
#comment = Comment.new
end
Comments
before_filter :load_community
def create
#comment = #community.comments.build(params[:comment])
#comment.user_id = current_user.id
if #comment.save
redirect_to :back
else
redirect_to "/"
end
# #comment = Comment.new(comment_params)
# #comment.user_id = session[:user_id]
# if #comment.save && #comment.community_id
# flash[:notice] = "Comment has been posted"
# else
# flash[:alert] = #comment.errors.full_messages
# end
end
private
def load_community
#community = Community.friendly.find(params[:community_id])
end
def comment_params
params.require(:comment).permit(:text, :user_id, :community_id, :profile_id)
end
Views
/communities/show
<%= render "profiles/index" %>
<h4><%= #community.title.capitalize! %></h4>
<%= #community.bio %>
<%= render "comments/new" %>
/comments/_new
<%= form_for ([#community, #comment]) do |f| %>
<%= f.text_area :text, placeholder: "Enter New Comment Here ...", :cols => 50, :rows => 3, :class => 'text_field_message', :id => 'new_comment' %>
<%= f.submit :class => 'new_comment_button' %>
<% end %>
Thank you everyone who helps explain where I'm making my mistake and also sorry in advance if I may need to ask what you might be requesting from me. For further questions please ask.
UPDATE
What I see in my console is
Started POST "/communities/dang/comments" for 127.0.0.1 at 2015-10-23 18:38:47 -0400
Processing by CommentsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"us8KNTLUZUdao13GK4OQId0YoUqf+CeLFIGjydnyWtI=", "comment"=> {"text"=>"www"}, "commit"=>"Create Comment", "community_id"=>"dang"}
Community Load (0.1ms) SELECT "communities".* FROM "communities" WHERE "communities"."slug" = 'dang' ORDER BY "communities"."id" ASC LIMIT 1
Completed 500 Internal Server Error in 11ms
ActiveModel::ForbiddenAttributesError (ActiveModel::ForbiddenAttributesError):
app/controllers/comments_controller.rb:17:in `create'
Okay.
ActiveModel::ForbiddenAttributesError for my CommentsController#Create
This basically means you're not permitting the required attributes in your create method.
This is what you need:
#app/controllers/comments_controller.rb
class CommentsController < ApplicationController
def create
#comment = #community.comments.new comment_params
#comment.save
end
private
def comment_params
params.require(:comment).permit(:text).merge(user_id: current_user.id)
end
end
You should read up on strong params to better understand how this works.
Polymorphic
You also have another issue which can be solved with a polymorphic association:
Simply, this allows you to associate a model with any number of others.
In your instance, where you can comment on users and communities, this functionality will serve well:
#app/models/comment.rb
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :commentable, polymorphic: true
end
#app/models/user.rb
class User < ActiveRecord::Base
has_many :sent_comments, class_name: "Comment", foreign_key: :user_id
has_many :comments, as: :commentable
end
#app/models/community.rb
class Community < ActiveRecord::Base
has_many :comments, as: :commentable
end
This will allow you to perform the following:
#user = User.find params[:id]
#user.comments.new comment_params
def comment_params
params.require(:comment).permit(:text).merge(user_id: current_user.id) #-> current_user from Devise
end
This allows you to use #user.comments and #community.comments with a single association.
You'll have to migrate the commentable_id & commentable_type columns into your comments table, but after that the above code should work.
I'm working on a simple project management tool in Rails 4, and the part which gives me headaches has three main models: Projects, Users and Memberships.
Users can have many projects and projects can have many users. I implemented a has_many through membership relationship between Projects and Users in the following manner:
Project:
class Project < ActiveRecord::Base
has_many :memberships
has_many :users, through: :memberships
end
User:
class User < ActiveRecord::Base
has_many :memberships
has_many :projects, through: :memberships
end
Membership:
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :project
end
So far I haven’t seen it on Stack Overflow, but I created a seperate controller for the Memberships, with a :create and :destroy only.
So far, :create seems to work just fine.
The main problem lies in the destroy function of the Memberships.
The destroy function I implemented is:
def destroy
Membership.find(:id).destroy
redirect_to current_project || request.referer
end
rake routes says that the membership path exists, but the following tries give me:
I tried to use a link_to helper to delete the memberships:
<%= link_to "delete", membership, method: :delete %>
EDIT: error: undefined local variable or method `membership'
<%= link_to "delete", #membership, method: :delete %>
EDIT: error: Sorry something went wrong --> goes to /memberships
<%= link_to "delete, membership_path(#membership), method: :delete %>
EDIT: error No route matches {:action=>"destroy", :controller=>"memberships", :id=>nil} missing required keys: [:id]
which all give errors.
EDIT: on request also the projects_controller #show function
def show
#user = current_user
#project = current_user.projects.find(params[:id])
#members = #project.users
#projects = #user.projects
#membership = #project.memberships.build if logged_in?
#memberships = #project.memberships
end
How can I make sure a membership is removed with the associated id in #project.membership_ids? Should I include certain extra parameters?
resources :memberships, only: [:create, :destroy]
Update
Don't know how I missed this earlier, in your destroy action you have Membership.find(:id).destroy. It should utilize params and be more along the lines of this:
# MembershipsController
def destroy
#membership = Membership.find(params[:id])
if #membership.destroy
redirect_to current_project || request.referer
else
#
end
end
Your ProjectsController's Show action isn't defining #membership as a Membership object.
def show
#user = current_user
#project = #user.projects.find(params[:id])
#membership = Membership.find_by user: #user, project: #project
# build is used for nested attributes, not sure why you'd have this in a show action...
# #project.memberships.build if logged_in?
# the following are redundant.
#projects = #user.projects
#members = #project.users
#memberships = #project.memberships
end
In the view: <%= link_to "delete", #membership, method: :delete %>
If you want all memberships of a project or user to be destroyed upon parent deletion, make the following changes to your User & Project models:
class Project < ActiveRecord::Base
has_many :memberships, dependent: :destroy
has_many :users, through: :memberships
end
class User < ActiveRecord::Base
has_many :memberships, dependent: :destroy
has_many :projects, through: :memberships
end
The reason the your code did not work is because you did not specify the path to link_to. I do not believe you need to have #membership so I edited that out.
<%= link_to "delete", membership_path(#membership), method: :delete %>
So i'm relatively new to RoR, and am having some issues in trying to get my code back up and working. So previously I had users, and wikis that users could create. I've set up so that users can subscribe and get premium status to make wikis private. Now I'm in the process of making it so that Premium users can add standard users as collaborators to the wiki. I've decided to got about associating them through has_many :through relationships.
The issue I'm running into so that some of my buttons have started making errors that I don't understand. The one I'm stuck on right now is when showing the page that has a create new wiki button on it.
This is the error I am getting when I added the has_many through: relationship
No route matches {:action=>"new", :controller=>"wikis", :format=>nil, :user_id=>nil} missing required keys: [:user_id]
Here are the models:
collaborator.rb
class Collaborator < ActiveRecord::Base
belongs_to :wiki
belongs_to :user
end
user.rb
class User < ActiveRecord::Base
...
has_many :collaborators
has_many :wikis, :through => :collaborators
end
wiki.rb
class Wiki < ActiveRecord::Base
belongs_to :user
has_many :collaborators
has_many :users, :through => :collaborators
end
The important bits of the wiki_controller.rb
def new
#user = User.find(params[:user_id])
#wiki = Wiki.new
authorize #wiki
end
def create
#user = current_user
#wiki = #user.wikis.create(wiki_params)
authorize #wiki
if #wiki.save
flash[:notice] = "Wiki was saved"
redirect_to #wiki
else
flash[:error] = "There was an error saving the Wiki. Please try again"
render :new
end
end
And finally the show.html.erb file the button is located in.
<div class="center-align">
<%= link_to "New Wiki", new_user_wiki_path(#user, #wiki), class: 'btn grey darken-1' %>
</div>
If I'm missing any files or relevant info please let me know. This may be a simple stupid answer but I'm stuck for the life of me.
Thanks in advance.
Edit:
Here is the requested added info, first up the show info in the users_controllers.rb
def show
#wikis = policy_scope(Wiki)
end
the corresponding policy scope I'm using in the user_policy.rb
class UserPolicy < ApplicationPolicy
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
wikis = []
all_wikis = scope.all
all_wikis.each do |wiki|
if wiki.user == user || wiki.users.include?(user)
wikis << wiki
end
end
end
wikis
end
end
and the route.rb file
Rails.application.routes.draw do
devise_for :users
resources :users, only: [:update, :show] do
resources :wikis, shallow: true
end
resources :wikis, only: [:index]
resources :charges, only: [:new, :create]
delete '/downgrade', to: 'charges#downgrade'
authenticated do
root to: "users#show", as: :authenticated
end
root to: 'welcome#index'
end
Hope it helps
I found out the problem. I set up the migrate file wrong when originally creating the collaboration model.
Thanks for all of your help.
I'd like to create a numerical rating system in rails where users can rate a post from 1 - 10.
I've looked on Google but I only find outdated tutorials and star rating gems which simply don't do the job for me.
Perhaps someone can point me to a gem that can help me achieve this?
Ruby Toolbox lists several, although most are DOA. Mongoid_ratings seemed to be the most recently updated, although you may not want to go the Mongo route.
https://www.ruby-toolbox.com/categories/rails_ratings
I would suggest building from scratch. Heres a quick (and probably non-functional/non-secure) hack that might help get you started:
Routes
resources :articles do
resources :ratings
end
Models
class Article < ActiveRecord::Base
has_many :ratings, :dependent => :destroy
end
class Rating < ActiveRecord::Base
belongs_to :article
validates_presence_of :article
validates_inclusion_of :value, :in => 1..10
end
Controllers
class RatingsController < ApplicationController
before_filter :set_article
def create
#rating = #article.ratings.new :value => params[:value]
if #rating.save
redirect_to article_ratings_path(#article), :notice => "Rating successful."
else
redirect_to article_ratings_path(#article), :notice => "Something went wrong."
end
end
def update
#rating = Rating.find(params[:id])
#rating.update_attribute :value, params[:value]
end
private
def set_article
#article = Article.find(parms[:article_id])
end
end
In an article view somewhere:
form_for [#article,#rating] do |f|
f.select("rating", "value", (1..10))
f.submit "Rate this Article"
end
Have a look at the Letsrate gem: https://github.com/muratguzel/letsrate
Works great for me.