DRY Multiple same associations in rails Model - ruby

In my model, I have multiple has_one associations like
has_one :t1_for_self_order, -> { t1_for_self_order }, as: :source, dependent: :destroy, inverse_of: :source,
class_name: 'Spree::PriceMapping'
has_one :shipping_charges_for_t1, -> { shipping_charges_for_t1 }, as: :source, dependent: :destroy, inverse_of: :source,
class_name: 'Spree::PriceMapping'
has_one :t2_for_self_order, -> { t2_for_self_order }, as: :source, dependent: :destroy, inverse_of: :source,
class_name: 'Spree::PriceMapping'
has_one :shipping_charges_for_t2, -> { shipping_charges_for_t2 }, as: :source, dependent: :destroy, inverse_of: :source,
class_name: 'Spree::PriceMapping'
has_one :minimum_value_for_gifting, -> { minimum_value_for_gifting }, as: :source, dependent: :destroy, inverse_of: :source,
class_name: 'Spree::PriceMapping'
has_one :international_fulfillment_fees, -> { international_fulfillment_fees }, as: :source, dependent: :destroy, inverse_of: :source,
class_name: 'Spree::PriceMapping'
you notice that only the association name with the same scope name is different in all the associations but the rest of the content is same.
So I want to write a function that will remove all these repetitions.
I think it can be possible with Meta programming but I'm not sure how to do this
Moreover, I also have an array of nested attributes which we can use.
THRESHOLD_INTENT = ["t1_for_self_order", "shipping_charges_for_t1", "t2_for_self_order", "shipping_charges_for_t2",
"minimum_value_for_gifting", "international_fulfillment_fees", "menu_customization_fee",
"total_menu_customization_fee_cap", "video_message_fee", "total_video_message_fee_cap",
"swag_price", "box_types_with_price", "customization_box_types_with_price", "custom_note_fee",
"non_us_fees"]
I like to do it in this way
THRESHOLD_INTENT.each do |t_intent|
has_one t_intent.to_sym, as: :source, dependent: :destroy, inverse_of: :source, class_name: 'Spree::PriceMapping'
end
But how can i pas the scope in this like
-> { t1_for_self_order }

You can iterate over a list of association names and call has_one with all the parameters on each item. send(association) is needed to call an association scope as a method.
ASSOCIATIONS = [:t1_for_self_order, :shipping_charges_for_t1, :t2_for_self_order, :shipping_charges_for_t2, :minimum_value_for_gifting, :international_fulfillment_fees]
ASSOCIATIONS.each do |association|
has_one association, -> { send(association) }, as: :source, dependent: :destroy, inverse_of: :source, class_name: 'Spree::PriceMapping'
end

Try extracting the common attributes, something like
relationship_attributes = { as: :source, dependent: :destroy, inverse_of: :source, class_name: 'Spree::PriceMapping' }
has_one :t1_for_self_order, -> { t1_for_self_order }, relationship_attributes
has_one :shipping_charges_for_t1, -> { shipping_charges_for_t1 }, relationship_attributes
has_one :t2_for_self_order, -> { t2_for_self_order }, relationship_attributes
has_one :shipping_charges_for_t2, -> { shipping_charges_for_t2 }, relationship_attributes
has_one :minimum_value_for_gifting, -> { minimum_value_for_gifting }, relationship_attributes
has_one :international_fulfillment_fees, -> { international_fulfillment_fees }, relationship_attributes
I would not use metaprogramming, it can make it difficult to read

Related

Rails 5 passes wrong argument into has_many :through scope

Suppose we have the following (albeit complicated) setup:
class Company < ActiveRecord::Base
has_many :homes, inverse_of: :company
has_many :item_company_items, class_name: 'CompanyItem', inverse_of: :item_company, foreign_key: :item_company_id
has_many :home_company_items, class_name: 'CompanyItem', inverse_of: :home_company, foreign_key: :home_company_id
has_many :replacement_items, through: :item_company_items, source: :replacement_items, inverse_of: :responsible_company
end
class Home < ActiveRecord::Base
belongs_to :company, inverse_of: :homes
has_many :company_items, through: :company, source: :home_company_items, inverse_of: :homes
has_many :replacement_items, inverse_of: :home
end
class CompanyItem < ActiveRecord::Base
belongs_to :item, inverse_of: :company_items
belongs_to :item_company, class_name: 'Company', inverse_of: :item_company_items, foreign_key: :item_company_id
belongs_to :home_company, class_name: 'Company', inverse_of: :home_company_items, foreign_key: :home_company_id
has_many :homes, through: :home_company, source: :homes, inverse_of: :company_items
has_many :replacement_items, -> company_item {
where item_id: company_item.item_id
}, through: :homes, source: :replacement_items, inverse_of: :company_item
end
class Item < ActiveRecord::Base
has_many :replacement_items, inverse_of: :item
has_many :company_items, inverse_of: :item
end
class ReplacementItem < ActiveRecord::Base
belongs_to :item, inverse_of: :replacement_items
belongs_to :home, inverse_of: :replacement_items
has_one :company_item, -> rep { where item_id: rep.item_id }, through: :home, source: :company_items, inverse_of: :replacement_items
has_one :responsible_company, through: :company_item, source: :item_company, inverse_of: :replacement_items
end
So, in effect, Items have many CompanyItems which represent the Company that is responsible (CompanyItem#item_company) for ReplacementItems belonging to that Item when the ReplacementItem is for a Home belonging to the Company specified by the CompanyItem (CompanyItem#home_company).
Very circuitous, but it's exactly what we need.
Now, as we can see, on the ReplacementItem model, the has_one :company_item, through: :home relationship uses a scope to select the specific CompanyItem belonging to the home that matches the item_id. This makes sense sence just doing replacement_item.home.company_items would give us all of homes company items, when we really just need the one that matches the given ReplacementItem. Calling the ReplacementItem#company_item method works flawlessly and gives us exactly what we want.
Likewise, the CompanyItem model has has_many :replacement_items, through: :homes, using the exact same scope to get just the replacement items that match the given CompanyItem. In this instance, however, we get an error when calling CompanyItem#replacement_items:
NoMethodError: undefined method `item_id' for #<Company id: 2>
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activemodel/lib/active_model/attribute_methods.rb:434:in `method_missing'
bug.rb:59:in `block in <class:CompanyItem>'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/associations/association_scope.rb:161:in `instance_exec'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/associations/association_scope.rb:161:in `eval_scope'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/associations/association_scope.rb:139:in `block in add_constraints'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/associations/association_scope.rb:138:in `each'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/associations/association_scope.rb:138:in `add_constraints'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/associations/association_scope.rb:28:in `scope'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/associations/association_scope.rb:5:in `scope'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/associations/association.rb:97:in `association_scope'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/associations/association.rb:86:in `scope'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/associations/collection_association.rb:327:in `scope'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/associations/collection_proxy.rb:36:in `initialize'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/relation/delegation.rb:101:in `new'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/relation/delegation.rb:101:in `create'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/associations/collection_association.rb:46:in `reader'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/associations/builder/association.rb:111:in `replacement_items'
bug.rb:85:in `test_hmt_with_conditions'
This looks like a Rails bug to me. There's absolutely no way a Company object should be passed in to the has_many :replacement_items scope defined on the CompanyItem model. Before bugging the Rails team, though, I am hoping someone here has seen a similar problem or can point out the fault in my code.
See my gist here for a runnable, failing test: https://gist.github.com/cmbankester/64ef4eb125bf90dbe0274fab8e5427f1

Validate uniqueness nested model params

I have three models:
class User < ActiveRecord::Base
has_many :user_countries
accepts_nested_attributes_for :user_countries, reject_if: :all_blank
validate :user_countries
end
class UserCountry < ActiveRecord::Base
belongs_to :user
belongs_to :country
end
class Country < ActiveRecord::Base
has_many :user_countries
has_many :users, :through => :user_countries
end
In a the user creation form I can create user_countries too. How I can validate in server the uniqueness of country_id in user_countries. A user has many countries: France, United State... but not for example: France and France.
I've added this to user_countries.. but it don't work:
validates :user_id, uniqueness: {scope: :country_id, allow_blank: false}
Try this in the user_country model.
class UserCountry < ActiveRecord::Base
belongs_to :user
belongs_to :country
validates_uniqueness_of :user_id, scope: :country_id, allow_blank: false
end
Also, you need to create a migration like below to add unique indexes on database level.
add_index :user_countries, [ :user_id, :country_id ], :unique => true

ActiveRecord::AssociationTypeMismatch in TicketsController#new

I have two models and controllers, one for Users and one for Tickets.
Each user can have many tickets. Each Admin user (denoted by admin: true) can have many tickets assigned to them.
Here are my associations:
class Ticket < ActiveRecord::Base
belongs_to :sender, class_name: "User", foreign_key: "id", inverse_of: :sent_tickets
belongs_to :admin, class_name: "User", foreign_key: "id", inverse_of: :assigned_tickets
.
.
.
end
class User < ActiveRecord::Base
.
.
.
has_many :sent_tickets, class_name: "Ticket", foreign_key: "sender", inverse_of: :sender, dependent: :nullify
has_many :assigned_tickets, class_name: "Ticket", foreign_key: "assigned", inverse_of: :admin, dependent: :nullify
.
.
.
end
However, anytime I try to run #ticket = user.sent_tickets.build (after running user = User.first in the Rails IRB, I get ActiveRecord::AssociationTypeMismatch in TicketsController#new: User(#number) expected, got Fixnum(#number)
Can anyone help me with this?
Fixed it. I renamed sender and assigned to sender_id and assigned_id. Then I rewrote my associations as such:
class Ticket < ActiveRecord::Base
belongs_to :assigned, class_name: "User"
belongs_to :sender, class_name: "User"
.
.
.
end
class User < ActiveRecord::Base
.
.
.
has_many :help_requests, foreign_key: "sender_id", class_name: "Ticket", dependent: :nullify
has_many :sent_requests, through: :help_requests , source: :sender
has_many :tickets, foreign_key: "assigned_id", dependent: :nullify
has_many :assigned_tickets, through: :tickets, source: :assigned
.
.
.
end

Mongoid: Referencing Same Model More Than Once Through has_many

I'd like to be able to reference a model (a has_many relationship) more than once in the same model. For example, given the following models:
class MyModel
include Mongoid::Document
field :name, type: String
has_many :main_efforts, :class_name => 'Effort', as: :effortable, dependent: :delete, autosave: true
has_many :secondary_efforts, :class_name => 'Effort', as: :effortable, dependent: :delete, autosave: true
validates_presence_of :name
end
class Effort
include Mongoid::Document
field :name, type: String
belongs_to :effortable, polymorphic: true
validates_presence_of :name
end
As you can see, the Effort model is referenced twice. Originally, my Effort model wasn't polymorphic, but it seemed Mongoid was unable to determine which collection (main_efforts or secondary_efforts) the effort belonged to. As such, I made it polymorphic. After making it polymorphic, however, my main_efforts and secondary_efforts fields are always an empty array.
What is the proper way to reference a polymorphic model more than once in the same model (assuming a polymorphic model is necessary)?
Figured it out:
class MyModel
include Mongoid::Document
field :name, type: String
has_many :main_efforts, :class_name => 'Effort', dependent: :delete, autosave: true, :inverse_of => :main_effort
has_many :secondary_efforts, :class_name => 'Effort', dependent: :delete, autosave: true, :inverse_of => :secondary_effort
validates_presence_of :name
end
class Effort
include Mongoid::Document
field :name, type: String
belongs_to :main_effort, :class_name => 'Conop', :inverse_of => :main_efforts
belongs_to :secondary_effort, :class_name => 'Conop', :inverse_of => :secondary_efforts
validates_presence_of :name
end

How do I Query Polymorphic Comments when commentables can belong to another commentable?

So I'm trying to do a relatively advanced query on a polymorphic model. I have the following models:
class Project < ActiveRecord::Base
has_many :project_stakeholders, :dependent => :destroy
has_many :features, :dependent => :destroy
has_many :iterations, :dependent => :destroy
has_many :comments, :as => :commentable, :dependent => :destroy
end
class ProjectStakeholder < ActiveRecord::Base
belongs_to :project
has_many :comments, :as => :commentable, :dependent => :destroy
end
class Feature < ActiveRecord::Base
belongs_to :project
has_many :comments, :as => :commentable, :dependent => :destroy
end
class Iteration < ActiveRecord::Base
belongs_to :project
has_many :comments, :as => :commentable, :dependent => :destroy
end
class User < ActiveRecord::Base
has_many :comments, :dependent => :destroy
end
class Comment < ActiveRecord::Base
belongs_to :commentable, :polymorphic => true
belongs_to :created_by, :class_name => 'User'
end
I'm trying to find the current_user's comments for a particular project (project.comments, project.features.comments, project.iterations.comments, project.project_stakeholder.comments) and sort them in created_at descending order.
The best I have come up with is:
Class Project < ActiveRecord::Base
def all_comments_for_user(user)
Comment.where(:created_by_id => user.id).select { |c| c.commentable.attributes.has_key?('project_id') }.select { |c| c.commentable.project == self } | comments.where(:created_by_id => user)
end
end
But this doesn't address the descending create_at sequence.
Class Project < ActiveRecord::Base
def all_comments_for_user(user)
Comment.where(:created_by_id => user.id).order('created_at DESC').select { |c| c.commentable.attributes.has_key?('project_id') }.select { |c| c.commentable.project == self } | comments.where(:created_by_id => user)
end
end
Does this work ? I just added order('created_at DESC')

Resources