I have 3 models user, micropost, like
I followed Ruby on Rails Tutorial by Michael Hartl
https://rails-4-0.railstutorial.org/book
Now I am adding new features and was trying to add a like button and after some changes now my micropost delete button doesn't work.
#micropost_controller
def destroy
Micropost.find(params[:id]).destroy
redirect_to root_url
end
#_micropost.html.erb
<%= link_to "delete", micropost, method: :delete,
data: { confirm: "You sure?" },
title: micropost.content %>
Even though i am calling Micropost.find Active Record searches in likes table and gives the error as
ActiveRecord::StatementInvalid in MicropostsController#destroy
SQLite3::SQLException: no such column: likes.micropost_id: SELECT "likes".* FROM "likes" WHERE "likes"."micropost_id" = ?
Similar thing happens when i try to delete a user. So basically whichever destroy i am trying to call it is redirected to like model
PS: the problem started after i executed
ActiveRecord::Base.connection.execute("BEGIN TRANSACTION; END;")
as i was getting BusyException: database is locked: commit transaction
models
#like.rb
class Like < ApplicationRecord
belongs_to :microposts, optional: true
belongs_to :users, optional: true
validates :src_user_id, presence: true
validates :des_user_id, presence: true
validates :post_id, presence: true
end
#micropost.rb
class Micropost < ActiveRecord::Base
belongs_to :user
default_scope -> { order('created_at DESC') }
validates :content, presence: true, length: { maximum: 140 }
validates :user_id, presence: true
has_many :likes, dependent: :destroy
# Returns microposts from the users being followed by the given user.
def self.from_users_followed_by(user)
followed_user_ids = "SELECT followed_id FROM relationships
WHERE follower_id = :user_id"
where("user_id IN (#{followed_user_ids}) OR user_id = :user_id",
user_id: user.id)
end
def self.search(search)
where("content LIKE ?", "%#{search}%")
end
end
#user.rb
class User < ActiveRecord::Base
has_many :microposts, dependent: :destroy
has_many :relationships, foreign_key: "follower_id", dependent: :destroy
has_many :followed_users, through: :relationships, source: :followed
has_many :reverse_relationships, foreign_key: "followed_id",
class_name: "Relationship",
dependent: :destroy
has_many :followers, through: :reverse_relationships, source: :follower
has_many :likes, foreign_key: "src_user_id", dependent: :destroy
has_many :liked_by_users, through: :likes, source: :likes
scope :starts_with, -> (name) { where("name like ?", "#{Example User}%")}
before_save { self.email = email.downcase }
before_create :create_remember_token
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i
validates :email, presence: true, format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, presence: true, length: { minimum: 6 }, unless: Proc.new { |user| user.password.nil? }
validates :password_confirmation, presence: true, unless: Proc.new { |user| user.password.nil? }
validates :birthday, presence: true
validate :email_verification, on: :update
end
class User
has_many :microposts, dependent: :destroy
# You never have to specify foreign key for indirect assocations
has_many :likes, through: :microposts
end
class Micropost
belongs_to :user
has_many :likes, dependent: :destroy, foreign_key: "post_id"
end
class Like
# You must specify the foreign key if it cannot be derived from the name of the association
# `belongs_to :micropost` will use micropost_id
belongs_to :micropost, optional: true, foreign_key: "post_id"
belongs_to :user # you have to add a `likes.user_id` column
end
Although I would really question if you want to use the same table/model for user likes and micropost likes. It really just makes things a lot more complicated and you end up with a table with a large number of null values. If this is part of the book I really wonder what the author was thinking as its a really bad design.
You can just set it up as:
# app/models/users/like.rb
# table name is users_likes
module Users
class Like
belongs_to :src_user, class_name: 'User', optional: false
belongs_to :des_user, class_name: 'User', optional: false
end
end
class User
has_many :likes_as_src,
class_name: 'Users::Like',
foreign_key: :src_user
has_many :likes_as_des,
class_name: 'Users::Like',
foreign_key: :src_user
end
# app/models/users/like.rb
# table name is microposts_likes
module Microposts
class Like
belongs_to :user, optional: false
belongs_to :micropost, optional: false, foreign_key: 'post_id'
end
end
class User
has_many :micropost_likes, class_name: 'Microposts::Like'
has_many :liked_microposts, through: :micropost_likes, source: :micropost
end
That gives you two simple join tables with non-nullable foreign keys and good indices. It also makes the validations very straight forward.
Correct associations would be
#user.rb
has_many :microposts, dependent: :destroy
has_many :likes, through: :microposts, foreign_key: "post_id"
#micropost.rb
belongs_to :user
has_many :likes, dependent: :destroy, foreign_key: "post_id"
#like.rb
belongs_to :micropost, optional: true
After this, it works fine
Related
I am trying to implement likes into my rails app.
like model
class Like < ApplicationRecord
belongs_to :comment
belongs_to :post
belongs_to :user
validates :user_id, uniqueness: {scope: :post_id}
validates :user_id, uniqueness: {scope: :comment_id}
end
user model
class User < ApplicationRecord
has_secure_password
has_one :profile, dependent: :destroy
has_many :comments, dependent: :destroy
has_many :posts, dependent: :destroy
has_many :likes, dependent: :destroy
has_many :liked_comments, :through => :likes, :source => :comment, dependent: :destroy
has_many :liked_posts, :through => :likes, :source => :post, dependent: :destroy
has_one_attached :avatar
validates :username, presence: true, uniqueness: true
validates :email, presence: true, uniqueness: true
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :password, length: { minimum: 8 }
validates :password, format: { with: /\A[a-zA-Z0-9!##$%^&*()_]+\z/ }
# validates :password, confirmation: true
# validates :password_confirmation, presence: true
end
post & comment model
class Post < ApplicationRecord
belongs_to :category
belongs_to :user
has_many :comments, dependent: :destroy
has_many :likes, dependent: :destroy
has_many :liking_users, :through => :likes, :source => :user
has_many_attached :images
def liked?(user)
!!self.likes.find{|like| like.user_id == user.id}
end
end
class Comment < ApplicationRecord
belongs_to :post
belongs_to :user
has_many :likes, dependent: :destroy
has_many :liking_users, :through => :likes, :source => :user
has_many_attached :images
def liked?(user)
!!self.likes.find{|like| like.user_id == user.id}
end
end
The issue I'm having is when I run my seed data I get one of two errors NoMethodError: undefined method "marked_for_destruction?" for false:FalseClass if set up like so:
#like0 = Like.create!(
user: #admin0,
post: #post3 & #post5 & #post6 & #post8 & #post9,
comment: #comment1,
username: #admin0.username
)
or NotNullViolation: ERROR: null value in column "comment_id" violates not-null constraint if set up like so:
class Like < ApplicationRecord
belongs_to :comment, optional: true
belongs_to :post, optional: true
belongs_to :user
validates :user_id, uniqueness: {scope: :post_id}
validates :user_id, uniqueness: {scope: :comment_id}
end
#like0 = Like.create!(
user: #admin0,
post: #post3,
comment: #comment1,
username: #admin0.username
)
my question is how can I better set up the relationship between these 4 models so as to not get these errors?
I have a self join model captain on my user model. I'm having an issue using the captain in an has_one relationship with a team model, which already has a has_many relationship with the user model.
My Team Model
class Team < ActiveRecord::Base
validates :teamname, :teamcolor, presence: true
has_one :captain, :class_name => "User"
#, :through => :user
has_many :users
#after_save :set_default_captain
accepts_nested_attributes_for :users
accepts_nested_attributes_for :captain
end
My User Model
class User < ActiveRecord::Base
attr_accessor :remember_token
before_save {self.email = email.downcase }
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format:{with: VALID_EMAIL_REGEX},
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
has_one :profile, inverse_of: :user, dependent: :destroy
has_many :teammates, :class_name => "User", :foreign_key => "captain_id"
belongs_to :captain, :class => "User"
belongs_to :team
accepts_nested_attributes_for :team
end
I'm having the issue using #team.captain, as captain_id is in the User database table but it's retrieving the first user with the team_id equaling #team.id. Usinghas_one :captain, :through => :user` gives association error. Any advice is appreciated. Thanks.
Try to specify foreign_key for the association.
has_one :captain, :class_name => "User", :foreign_key => 'captain_id'
I have 2 models as below,
Updated based on suggestions
class User < ActiveRecord::Base
has_many :company_users, dependent: :destroy, inverse_of: :user
accepts_nested_attributes_for :company_users, allow_destroy: true
has_many :companies, through: :company_users
has_many :roles, through: :company_users
end
and
class CompanyUser < ActiveRecord::Base
belongs_to :company
belongs_to :role
belongs_to :user, inverse_of: :company_users
validates :user, uniqueness: {scope: [:company, :role]}
end
I find the uniqueness validation is working only on the update request. On create request validation is not functioning and it simply bypasses it.
I want to enable the same validation to reject if a user has same company & role assigned more than once.
If you want a ensure the uniqueness of user on unique pair of :company and :role, then you can try following. By default, the validations run for both create and update. You don't need :on => [ :create, :update ]. So it should be just:
validates :user, uniqueness: {scope: [:company, :role]}
Solved this issue with the below validation,
class User < ActiveRecord::Base
has_many :company_users, dependent: :destroy, inverse_of: :user
accepts_nested_attributes_for :company_users, allow_destroy: true
has_many :companies, through: :company_users
has_many :roles, through: :company_users
validate :company_users, :uniqueness_of_company_users
end
private
def uniqueness_of_company_users
errors.add(:company_users, 'error in role creation') if company_users.map{|x| "#{x.company_id} #{x.role_id}"}.uniq.size != company_users.size
end
This is the additional validation required to solve the issue
Thanks Rich Peck for this https://railscoding.wordpress.com/2015/04/27/uniqueness-gotcha/
From the docs
The :on option takes one of the values :create or :update
--
A validation is only run on create or update anyway, right?
find doesn't manipulate the db, destroy gets rid of the record & new just invokes a new instance of the object. You have literally zero other reasons to validate.
So, really, you should have:
validates :user, uniqueness: {scope: [:company_id, :role_id]}
This will look up against the values in company_id and role_id, which is probably going to be more efficient than calling the company and role objects themselves.
I could be wrong, but I really think if you used the above, it should work.
--
You may also wish to clean up your models:
class User < ActiveRecord::Base
has_many :company_users, dependent: :destroy, inverse_of: :user
accepts_nested_attributes_for :company_users, allow_destroy: true
has_many :companies, through: :company_users
has_many :roles, through: :company_users
end
class CompanyUser < ActiveRecord::Base
belongs_to :company
belongs_to :role
belongs_to :user, inverse_of: :company_users
validates :user, uniqueness: {scope: [:company_id, :role_id]}
end
How can add a cascade of deletes that will remove Profile, TodoList, and TodoItem rows for any User removed.
User Model:
class User < ActiveRecord::Base
has_one :profile
has_many :todo_lists
has_many :todo_items, through: :todo_lists, source: :todo_items
validates :username, presence: true
end
Profile Model:
class Profile < ActiveRecord::Base
belongs_to :user
validates :first_name, presence: true
validates :last_name, presence: true
validates :gender, inclusion: %w(male female)
validate :first_and_last
validate :male_Sue
def first_and_last
if (first_name.nil? and last_name.nil?)
errors.add(:base, "Specify a first or a last.")
end
end
def male_Sue
if (first_name == "Sue" and gender == "male")
errors.add(:base, "we are prevent male by name Sue.")
end
end
end
TodoList Model:
class TodoList < ActiveRecord::Base
belongs_to :user
has_many :todo_items, dependent: :destroy
default_scope { order :list_due_date }
end
TodoItem Model:
class TodoItem < ActiveRecord::Base
belongs_to :todo_list
default_scope {order :due_date }
end
Thanks, Michael.
I guess adding dependent: :destroy will do.
#user.rb
class User < ActiveRecord::Base
has_one :profile, dependent: :destroy
has_many :todo_lists, dependent: :destroy
has_many :todo_items, through: :todo_lists, source: :todo_items, dependent: :destroy
validates :username, presence: true
end
From the docs:
has_many, has_one and belongs_to associations support the :dependent option. This allows you to specify that associated records should be deleted when the owner is deleted
By using dependent: :destroy on your association in the User class, anytime you destroy a User, all associated objects to that instance gets destroyed as well.
You can check this documentation for more information.
I have a model Job, which has_many Applications, and has_many Questions. An Answer belongs to both an Application and a Question.
I'm trying to make a factory method an admin can use to create applications, without users having to write anything.
To do this, I wrote --
def self.make_by_admin(params)
app = Application.new
app.user_id = params[:user_id]
app.job_id = params[:job_id]
app.questions.each do |question|
app.answers.new(question_id: question.id, content: 'N/A')
end
app
end
But, I get the error
#<ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection: Cannot modify association 'Application#questions' because the source reflection class 'Question' is associated to 'Job' via :has_many.>
What's weird though is that I'm not actually modifying questions. All I want to do is to generate blank answers for each question.
How would I go about doing that?
Full Models
class Job < ActiveRecord::Base
default_scope { order('jobs.created_at DESC') }
has_many :bullets, dependent: :destroy, inverse_of: :job
has_many :roles, dependent: :destroy, inverse_of: :job
has_many :questions, dependent: :destroy, inverse_of: :job
has_many :job_city_relations, inverse_of: :job, dependent: :destroy
has_many :cities, through: :job_city_relations
has_many :job_industry_relations, inverse_of: :job, dependent: :destroy
has_many :industries, through: :job_industry_relations
has_many :applications, inverse_of: :job, dependent: :destroy
has_many :users, through: :applications
validates :cities,
:job_title,
:job_summary,
:qualifications,
:industries,
:bullets,
:roles,
:questions,
presence: true
accepts_nested_attributes_for :bullets,
reject_if: :all_blank,
allow_destroy: true
accepts_nested_attributes_for :roles,
reject_if: :all_blank,
allow_destroy: true
accepts_nested_attributes_for :questions,
reject_if: :all_blank,
allow_destroy: true
scope :with_cities, ->(city) do
job = Job
.includes(:industries)
.includes(:cities)
.includes(:questions)
.includes(:bullets)
.includes(:roles)
job = job.where(cities: { id: city }) if city
job
end
scope :with_search, ->(search) do
job = Job.includes(:industries)
.includes(:cities)
.includes(:bullets)
.includes(:roles)
.includes(:questions)
if search
job = job.where('jobs.job_title LIKE ?', "%#{search}%")
end
job
end
scope :with_info, -> do
Job.includes(:industries)
.includes(:cities)
.includes(:bullets)
.includes(:roles)
.includes(:questions)
end
def self.build
job = Job.new
2.times {
job.bullets.build
job.roles.build
}
job.questions.build
job
end
def potentials
good_fits = User.includes(:source, :heat, :applications, common_app: [:cities, :industries])
.where('cities.id IN (?)', self.city_ids)
.where('industries.id IN (?)', self.industry_ids)
.where('users.id NOT IN (?)', self.users.map(&:id))
end
end
class Application < ActiveRecord::Base
STATUS_OPTIONS = ["Application Complete",
"Materials Submitted",
"Pending Interview",
"Second Interview"]
belongs_to :job, counter_cache: true
belongs_to :user, counter_cache: true
has_many :questions, through: :job
has_many :answers, inverse_of: :application, dependent: :destroy
validates :job_id, presence: true
validates :user_id, presence: true
validates :answers, presence: true
accepts_nested_attributes_for :answers,
allow_destroy: true, reject_if: :all_blank
scope :with_dependents, -> do
Application
.includes(:job)
.includes(:questions)
.includes(:answers)
end
scope :for_job, ->(job_id) do
Application
.includes(user: [:source, :heat, common_app: [:cities, :industries]])
.includes(questions: :answer)
.where('applications.job_id = ?', job_id)
end
def self.build(job, appl = Application.new)
job.questions.each do |question|
appl.answers.build(question_id: question.id)
end
appl
end
def self.make_by_admin(params)
app = Application.new
app.user_id = params[:user_id]
app.job_id = params[:job_id]
app.questions.each do |question|
app.answers.new(question_id: question.id, content: 'N/A')
end
fail
app
end
end
class Question < ActiveRecord::Base
belongs_to :job
has_one :answer
end
class Answer < ActiveRecord::Base
belongs_to :question
belongs_to :application
end
I ended up writing
def self.make_by_admin(params)
app = Application.new
app.user_id = params[:user_id]
app.job_id = params[:job_id]
app.questions.pluck(:id).each do |id|
app.answers.new(question_id: id, content: 'N/A')
end
app
end