How cause association callback in rails models - ruby-on-rails

How to track in the models this command
=> order = Order.create
=> order.items << Item.first // this command
if i have such models:
class Order < ApplicationRecord
has_many :order_items
has_many :items, through: :order_items
end
class Item < ApplicationRecord
has_many :order_items
has_many :orders, through: :order_items
end
class OrderItem < ApplicationRecord
belongs_to :order
belongs_to :item
end
I try use after_add for example, but I did not succeed.
For example my task:
In controller`s (OrderController) method create:
def create
#order = Order.create(order_params)
#order.items << Item.find(params[:id])
end
And i have that models Order or Item track this (when i add item to order) and print me message in console (for example)

Have a look at the Rails Guides about Association Callbacks. There is, for example, an after_add callback.
# in your Order model
has_many :items, after_add: :track_item_added
private
def track_item_added(item)
# your tracking code, for example
Rails.logger.debug("Item ##{item.id} added to order ##{id}")
end

Related

Why 'include' position in Rails model affets HABTM behavior?

I spent an hour debugging a very strange rails behavior.
Given:
app/models/user.rb
class User < ApplicationRecord
...
has_many :images
has_many :videos
...
has_many :tags
...
end
app/models/image.rb
class Image < ApplicationRecord
...
belongs_to :user
...
has_and_belongs_to_many :tags
...
include TagsFunctions
...
end
app/models/video.rb
class Video < ApplicationRecord
...
include TagsFunctions
...
belongs_to :user
...
has_and_belongs_to_many :tags
...
end
app/models/tag.rb
class Tag < ApplicationRecord
belongs_to :user
validates :text, uniqueness: {scope: :user}, presence: true
before_create :set_code
def set_code
return if self[:code].present?
loop do
self[:code] = [*'A'..'Z'].sample(8).join
break if Tag.find_by(code: self[:code]).nil?
end
end
end
app/models/concerns/tags_functions.rb
module TagsFunctions
extend ActiveSupport::Concern
# hack for new models
included do
attr_accessor :tags_after_creation
after_create -> { self.tags_string = tags_after_creation if tags_after_creation.present? }
end
def tags_string
tags.pluck(:text).join(',')
end
def tags_string=(value)
unless user
#tags_after_creation = value
return
end
#tags_after_creation = ''
self.tags = []
value.to_s.split(',').map(&:strip).each do |tag_text|
tag = user.tags.find_or_create_by(text: tag_text)
self.tags << tag
end
end
end
If I execute such code:
user = User.first
tags_string = 'test'
image = user.images.create(tags_string: tags_string)
video = user.videos.create(tags_string: tags_string)
It will give 1 item in image.tags, but 2 duplicate items in video.tags
But if we change code this way:
user = User.first
tags_string = 'test'
image = Image.create(user: user, tags_string: tags_string)
video = Video.create(user: user, tags_string: tags_string)
everything works fine, 1 tag for an image and 1 tag for a video
And even more...
If we move include TagsFunctions below has_and_belongs_to_many :tags, in a video.rb file, both code examples work fine.
I thought I know rails pretty well, but this behavior is really unclear for me.
Rails version: 5.1.1
It seems like you may want to check these 2 lines
tag = user.tags.find_or_create_by(text: tag_text)
self.tags << tag
What seems to be happening here is a tag is being create for the user but also the actual video record. But it is hard to know without seeing if there is anything in the tag model. It may be good to avoid associating the tag association with the user in the tags function.
I think what you have is an X and Y problem since the domain can be modeled better in the first place:
# rails g model tag name:string:uniq
class Tag < ApplicationRecord
has_many :taggings
has_many :tagged_items, through: :taggings, source: :resource
has_many :videos, through: :taggings, source: :resource, source_type: 'Video'
has_many :images, through: :taggings, source: :resource, source_type: 'Image'
end
# rails g model tagging tag:belongs_to tagger:belongs_to resource:belongs_to:polymorphic
class Tagging < ApplicationRecord
belongs_to :tag
belongs_to :tagger, class_name: 'User'
belongs_to :resource, polymorpic: true
end
class User < ApplicationRecord
has_many :taggings, foreign_key: 'tagger_id'
has_many :tagged_items, through: :taggings, source: :resource
has_many :tagged_videos, through: :taggings, source: :resource, source_type: 'Video'
has_many :tagged_images, through: :taggings, source: :resource, source_type: 'Image'
end
module Taggable
extend ActiveSupport::Concern
included do
has_many :taggings, as: :resource
has_many :tags, through: :taggings
end
# example
# #video.tag!('#amazeballs', '#cooking', tagger: current_user)
def tag!(*names, tagger:)
names.each do |name|
tag = Tag.find_or_create_by(name: name)
taggnings.create(tag: tag, tagger: tagger)
end
end
end
This creates a normalized tags table that we can use for lookup instead of comparing string values. has_and_belongs_to_many is only really useful in the simplest of cases where you are just joining two tables and never will need to query the join table directly (in other words using HABTM is a misstake 90% of the time).
Using callbacks to "hack into" HABTM just makes it worse.
You can use a bit of metaprogramming to cut the duplication when setting up different taggable classes.
class Video < ApplicationRecord
include Taggable
end
class Image< ApplicationRecord
include Taggable
end

Cart validations for maximum items

I have 2 models, cart and line_item:
cart.rb & line_item.rb
class Cart < ActiveRecord::Base
has_many :line_items, dependent: :destroy
belongs_to :user
class LineItem < ActiveRecord::Base
belongs_to :cart
belongs_to :user
application_controller.rb
def current_cart
Cart.find(session[:cart_id])
rescue ActiveRecord::RecordNotFound
cart = current_user.cart.create
session[:cart_id] = cart.id
cart
end
How can I add validations to my cart so that user can only add 5 items maximum into their cart? At the moment I have this code but it is not working?
def maximum_items_not_more_than_5
if line_items.count > 5
errors.add(:line_items, "must be less than 5")
end
end
Here is a way, I would try :
class LineItem < ActiveRecord::Base
belongs_to :cart, validate: true # enables validation
Then inside the Cart model, write your own custom validation like :
class Cart < ActiveRecord::Base
has_many :line_items, dependent: :destroy
validate :maximum_items_not_more_than_5 # using custom validation
private
def maximum_items_not_more_than_5
if line_items.count > 5
errors.add(:base, "must be less than 5")
end
end
Why is line_item belonging to user?? Surely it would be item:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :carts
end
#app/models/cart.rb
class Cart < ActiveRecord::Base
belongs_to :user
has_many :line_items, inverse_of: :cart
has_many :items, through: :line_items
validate :max_line_items
private
def max_line_items
errors.add(:tags, "Too many items in your cart!!!!!!") if line_items.size > 5
end
end
#app/models/line_item.rb
class LineItem < ActiveRecord::Base
belongs_to :cart, inverse_of: :line_items
belongs_to :item #-> surely you want to specify which item is in the cart?
end
#app/models/item.rb
class Item < ActiveRecord::Base
has_many :line_items
has_many :carts, through: :line_items
end
Validation
This is certainly in the realms of validation, specifically a custom method:
#app/models/model.rb
class Model < ActiveRecord::Base
validate :method
private
def method
## has access to all the instance attributes
end
end
I also put inverse_of into the mix.
You can see how this works here: https://stackoverflow.com/a/20283759/1143732
Specifically, it allows you to call parent / child objects from a particular model, thus allowing you to call validations & methods residing in those files.
In your case, it may be prudent to add validations to the line_item model -- specifying individual quantities or something. You can call the validations in this model directly from your cart model by setting the correct inverse_of

How to use build method with a has_many :through association

My Models:
class Vip < ActiveRecord::Base
belongs_to :organization
has_many :events
has_many :organizations, :through => :events
end
class Organization < ActiveRecord::Base
belongs_to :user
has_many :events
has_many :vips, :through => :events
end
class Event < ActiveRecord::Base
belongs_to :organization
belongs_to :vip
end
My vips Controller:
def create
#organization = Organization.find(params[:organization_id])
#vip = #organization.vips.build(vip_params)
if #vip.save
redirect_to organization_path(#organization)
else
render 'new'
end
end
def vip_params
params.require(:vip).permit(:name, :about, :organization_id)
end
Before I started using the has_many :through associations, the build method would automatically add the foreign key to the new vip. So my vips table would have the organization_id column populated. Since using the has_many associations, the organization_id column is being left NULL on 'vip#create'.
Is there a reason that build wouldn't work the same way anymore with my new associations?

validate destruction of has_many association?

Models:
class Factory < ActiveRecord::Base
has_many :factory_workers
has_many :workers, through: :factory_workers
end
class FactoryWorkers < ActiveRecord::Base
belongs_to :factory
belongs_to :worker
before_destroy :union_approves?
private
def union_approves?
errors.add(:proletariat, "is never destroyed!")
false
end
end
class Worker < ActiveRecord::Base
has_many :factory_workers
has_many :factorys, through: :factory_workers
end
If I attempt to update a Factory's list of Workers via Factory, and that leads to the destruction of some FactoryWorker associations, I hope that the before_destroy hook is called, but this does not seem to be the case.
Example
Factory.create(name: 'communist paradise', worker_ids: [1, 2])
Factory.find_by(name: 'commnist paradise').update(worker_ids: [1])
# before_destroy hook is not called, proletariat must riot!
How can I ensure the before_destory hook is called when updating a record's associations?
Found what I needed: ActiveRecord provides a before_remove method on associations, so I just need to rejigger as follows:
class Factory < ActiveRecord::Base
has_and_belongs_to_many :workers, before_remove: :union_approves?
private
def union_approves?
...
end
end

How to get related records across a join table in Rails?

In my Rails application I have people which can have many projects and vice versa:
# app/models/person.rb
class Person < ActiveRecord::Base
has_many :people_projects
has_many :projects, :through => :people_projects
end
# app/models/people_project.rb
class PeopleProject < ActiveRecord::Base
belongs_to :person
belongs_to :project
end
# app/models/project.rb
class Project < ActiveRecord::Base
has_many :people_projects
has_many :people, :through => :people_projects
def self.search(person_id)
if person_id
where("person_id = ?", person_id) # not working because no person_id column in projects table
else
scoped
end
end
end
How can I filter the projects by person_id in the index view of my ProjectsController, e.g. by using a URL like this: http://localhost:3000/projects?person_id=164
I can't get my head around this. Please help! Thanks...
Your association definition is not complete for Person and Project models. You also need has_many :people_projects defined.
# app/models/person.rb
class Person < ActiveRecord::Base
has_many :people_projects # <-- This line
has_many :projects, :through => :people_projects
end
# app/models/project.rb
class Project < ActiveRecord::Base
has_many :people_projects # <-- This line
has_many :people, :through => :people_projects
end
# app/models/people_project.rb
# This is defined correctly
class PeopleProject < ActiveRecord::Base
belongs_to :person
belongs_to :project
end
Please reference The has_many :through Association for further details.
With this definition, you will be able to get all the projects of the current user using current_user.projects, just like you've already done in your ProjectsController#index.
Update:
You could use either joins or includes in your search method and apply the where condition. Something like follows:
# app/models/project.rb
class Project < ActiveRecord::Base
has_many :people_projects
has_many :people, :through => :people_projects
def self.search(person_id)
if person_id
includes([:people_projects, :people]).where("people.id = ?", person_id)
else
scoped
end
end
end
You will not have a person_id in the projects table because its a has_many<>has_many relationship.
Simply #person.projects will perform a join btw person_projects & projects tables and returns the appropriate projects.
*I assume,current_user returns a Person object.*
Also, complete your Model definitions. Each of them should list their relation to PeopleProjects
class Person < ActiveRecord::Base
has_many :people_projects
has_many :projects, :through => :people_projects
end
class Project < ActiveRecord::Base
has_many :people_projects
has_many :people, :through => :people_projects
end

Resources