I have a number of classes like the below:
class Event < ActiveRecord::Base
include PreventUpdate
has_many :participants, conditions: ...
has_many :venues, conditions: ...
has_many :companies, conditions: ...
end
I have a module that handles prevent_update logic (obj is in past, obj is banned) for the classes, and as part of that it queries the including class for its has_many associations in order to write before_add hooks to those associations.
module PreventUpdate
extend ::ActiveSupport::Concern
included do
self.reflect_on_all_associations(:has_many).each do |assoc|
has_many assoc.name, before_add: :prevent_update
end
end
def prevent_update
#core prevent update logic
end
end
The only problem is that the dynamically added and original has_many statements overwrite each other. Which overwrites which depends on where in the including class the module is included.
Is there any way that the dynamically added and original declarations can "accumulate", i.e. the module's declaration can simply add on to the original without overwriting?
This is untested, but you should be able to just do:
included do
self.reflect_on_all_associations(:has_many).each do |assoc|
assoc.options.merge!(:before_add => :prevent_update)
end
end
This would require that the concern include come after the has_many declarations. If you want to do the include before them, you could add:
module ClassMethods
def modify_associations
self.reflect_on_all_associations(:has_many).each do |assoc|
assoc.options.merge!(:before_add => :prevent_update)
end
end
end
and then:
# ...
has_many :companies, conditions: ...
modify_associations
EDIT
This should work:
included do
self.reflect_on_all_associations(:has_many).each do |assoc|
has_many assoc.name, assoc.options.merge(:before_add => :prevent_update)
end
end
Related
I wrote setter and getter for the virtual attribute stock
For the getter it will aggregate the HAM_MANY relation records.
For the setter, it will create a new record and save the record to the right child table.
How could I DRY the two methods in the two models?
How could I avoid have two identical code in different model files? Thanks
Model Flight
has_many :stocks, foreign_key: "flight_sku_fare_id", class_name: "FlightSkuFareStock", dependent: :destroy
def stock
stocks.sum(:amount)
end
def stock=(stock_value)
self.save
stock_delta = stock_value - self.stock
if stock_value >=0 and (stock_delta!=0)
self.stocks.create(amount: stock_delta)
end
end
Model Room
has_many :stocks, foreign_key: "room_sku_id", class_name: "RoomSkuStock", dependent: :destroy
def stock
stocks.sum(:amount)
end
def stock=(stock_value)
self.save
stock_delta = stock_value - self.stock
if stock_value >=0 and (stock_delta!=0)
self.stocks.create(amount: stock_delta)
end
end
You can look into active_support/concern.
app/models/concerns/stock_concern.rb
require 'active_support/concern'
module StockConcern
extend ActiveSupport::Concern
def stock
stocks.sum(:amount)
end
def stock=(stock_value)
self.save
stock_delta = stock_value - self.stock
if stock_value >=0 and (stock_delta!=0)
self.stocks.create(amount: stock_delta)
end
end
end
And in your models,
app/models/flights.rb
class Flight < ActiveRecord::Base
include StockConcern
## other methods
end
app/models/rooms.rb
class Room < ActiveRecord::Base
include StockConcern
## other methods
end
You might have to tweak it a little bit to make it work perfectly.
I have implemented a tagging system for the models Unit, Group and Event, and currently, each one have their own instance of the methods add_tags and self.tagged_with.
def add_tags(options=[])
transaction do
options.each do |tag|
self.tags << Tag.find_by(name: tag)
end
end
end
and
def self.tagged_with(tags=[])
units = Unit.all
tags.each do |tag|
units = units & Tag.find_by_name(tag).units
end
units
end
end
I want to move these into a module and include them in the model, but as you can see, the tagged_with method is not polymorphic, as I don't know how I would refer the parenting class (Unit, Group etc.) and called methods like "all" on them. Any advice?
Tag model:
Class Tag < ActiveRecord::Base
has_and_belongs_to_many: units, :join_table => :unit_taggings
has_and_belongs_to_many: groups, :join_table => :group_taggings
has_and_belongs_to_many: events, :join_table => :event_taggings
end
You could call self.class to get the current class, like this:
def self.tagged_with(tags=[])
klass = self.class
units = klass.all
tags.each do |tag|
units = units & Tag.find_by_name(tag).units
end
units
end
end
self.class should return Unit or any class, calling any method on a class object (self.class.tagged_with) is the same as Unit.tagged_with
I would recommend that you use Concerns, take a look here
EDIT Answer to your comment
Using concerns you could do something like this, each class have that methods you mentioned before, but you dont have to rewrite all that code on every class (or file):
# app/models/concerns/taggable.rb
module Taggable
extend ActiveSupport::Concern
module ClassMethods
def self.tagged_with(tags=[])
klass = self.class
units = klass.all
tags.each do |tag|
units = units & Tag.find_by_name(tag).units
end
units
end
end
end
end
# app/models/unit.rb
class Unit
include Taggable
...
end
# app/models/group.rb
class Group
include Taggable
...
end
# app/models/event.rb
class Event
include Taggable
...
end
I would do it like so:
#table: taggings, fields: tag_id, taggable type (string), taggable id
class Tagging
belongs_to :tag
belongs_to :taggable, :polymorphic => true
Now make a module in lib - let's call it "ActsAsTaggable"*
module ActsAsTaggable
def self.included(base)
base.extend(ClassMethods)
base.class_eval do
#common associations, callbacks, validations etc go here
has_many :taggings, :as => :taggable, :dependent => :destroy
has_many :tags, :through => :taggings
end
end
#instance methods can be defined in the normal way
#class methods go in here
module ClassMethods
end
end
Now you can do this in any class you want to make taggable
include ActsAsTaggable
there is already a gem (or plugin perhaps) called ActsAsTaggable, which basically works in this way. But it's nicer to see the explanation rather than just get told to use the gem.
EDIT: here's the code you need to set up the association at the Tag end: note the source option.
class Tag
has_many :taggings
has_many :taggables, :through => :taggings, :source => :taggable
I’ve got what’s becoming a complex model, and am trying to DRY it out. In the case of my has_many options, instead of having them repeat, I’d like to simply load them from a method on the class.
class ExampleClass < ActiveRecord::Base
has_many :related_things, get_association_hash(arg1)
has_many :other_things, get_association_hash(arg2)
def get_association_hash(arg)
{ :class_name => 'SomeClass', :conditions => ['table.column = ?', arg] }
end
end
Unfortunately, this results in undefined method ‘get_association_hash’ for #<Class:0x007f9ae9efe6c0> when loading the class.
(As a sanity check, that method is fine if I just call it by itself, without including it in the has_many. Also, the actual class is considerably larger and so DRY is more helpful than in this small example.)
I do note that the error message mentions Class, and not my derived ExampleClass, so perhaps it has to do with how has_many is loaded, and where I define my method?
has_many is just a class method so this:
has_many :related_things, get_association_hash(arg1)
is just a method call like any other and the receiver in that context is your ExampleClass. That means that get_association_hash needs to be a class method. You'll also have to define it before your has_many calls or you won't be able to call it where you want to:
class ExampleClass < ActiveRecord::Base
def self.get_association_hash(arg)
{ :class_name => 'SomeClass', :conditions => ['table.column = ?', arg] }
end
has_many :related_things, get_association_hash(arg1)
has_many :other_things, get_association_hash(arg2)
end
That might be a bit ugly and make a mess of the usual definition order. If that's the case, then you can push your get_association_hash method into a module and then include that module at the top of your class:
module Pancakes
def self.included(base)
# There are various different ways to do this, use whichever one you like best
base.class_exec do
def self.get_association_hash(arg)
# ...
end
end
end
end
class ExampleClass < ActiveRecord::Base
include Pancakes
has_many :related_things, get_association_hash(arg1)
has_many :other_things, get_association_hash(arg2)
end
You'd probably call your module something more sensible than Pancakes, that's just my default name for things (because foo gets boring after awhile and I prefer Fargo over tradition).
I have the following three models
LegacyRole:
class LegacyRole < LegacyModel
has_many :permissions_roles
has_many :permissions, :through => :permissions_roles
end
LegacyPermissionsRole:
class LegacyPermissionsRole < LegacyModel
belongs_to :role
belongs_to :permission
end
and LegacyPermission:
class LegacyPermission < LegacyModel
has_many :permissions_roles
has_many :roles, :through => :permissions_roles
end
And in order for these to all work, and connect the legacy database and whatnot, I have the following class LegacyModel which is possibly trying to be too clever...
require 'active_record'
class LegacyModel < ActiveRecord::Base
self.abstract_class = true
establish_connection "legacy_#{::Rails.env}"
def self.inherited(subclass)
tabeleized_name = subclass.name.tableize
raise "Legacy models must be prefixed with 'Legacy'" unless tabeleized_name.start_with?('legacy_')
logger.info "***********LOAD***********"
logger.info "Loaded legacy model: #{subclass.name} using table: #{tabeleized_name.gsub('legacy_', '')}"
super
subclass.set_table_name tabeleized_name.gsub('legacy_','')
end
# these methods do much the same thing, can probably abstract some of this out
def self.belongs_to(association_id, options = {})
new_association = association_id.to_s.insert(0, 'legacy_').to_sym
old_association = association_id
logger.info "Legacy model has belongs_to association: '#{association_id}'"
association_id = association_id.to_s.insert(0, 'legacy_').to_sym
logger.info "Converting association to: '#{association_id}'"
unless options.has_key?(:foreign_key)
# our foreign_key is missing
options[:foreign_key] = old_association.to_s.foreign_key
logger.info("Foreign_key was missing, is now: #{options[:foreign_key]}")
end
super
alias_method old_association, new_association
end
def self.has_many(association_id, options = {})
new_association = association_id.to_s.insert(0, 'legacy_').to_sym
old_association = association_id
logger.info "Legacy model has_many to association: '#{association_id}'"
association_id = association_id.to_s.insert(0, 'legacy_').to_sym
logger.info "Converting association to: '#{association_id}'"
logger.debug("Association options are: #{options.inspect}")
if options.has_key?(:through)
options[:through] = options[:through].to_s.insert(0, 'legacy_')
logger.info("Through mutated, is now: #{options[:through]}")
end
super
alias_method old_association, new_association
end
end
Whenever I try to access permissions on an instance of LegacyRole, I get the following Active Record error:
ActiveRecord::HasManyThroughAssociationNotFoundError: Could not find the association "legacy_permissions_roles" in model LegacyRole
I've stepped through all this as best I can and I really can't figure out why this is occurring, obviously with this being a bit more complicated than standard by the LegacyModel class I really don't know how to diagnose this further... I'm now at the point with it where I can't see the forest for the trees and feel it might just be something really simple that I've missed out!
Edit:
Here is the log output from the models loading
****************************
Loaded legacy model: LegacyPermission using table: permissions
Legacy model has_many association: 'permissions_roles'
Converting association to: 'legacy_permissions_roles'
Association options are: {}
Legacy model has_many association: 'roles'
Converting association to: 'legacy_roles'
Association options are: {:through=>:permissions_roles}
Changed :through to: 'legacy_permissions_roles'
****************************
Loaded legacy model: LegacyPermissionsRole using table: permissions_roles
Legacy model has belongs_to association: 'role'
Converting association to: 'legacy_role'
Legacy model has belongs_to association: 'permission'
Converting association to: 'legacy_permission'
Foreign_key was missing, is now: 'permission_id'
****************************
Loaded legacy model: LegacyRole using table: roles
Legacy model has_many association: 'permissions_roles'
Converting association to: 'legacy_permissions_roles'
Association options are: {}
Legacy model has_many association: 'permissions'
Converting association to: 'legacy_permissions'
Association options are: {:through=>:permissions_roles}
Changed :through to: 'legacy_permissions_roles'
Perhaps you want
class LegacyRole < LegacyModel
has_many :permissions_roles
has_many :permissions, :through => :legacy_permissions_roles # note the `legacy` prefix
end
Or was this a typo in your post?
Is there a way to override one of the methods provided by an ActiveRecord association?
Say for example I have the following typical polymorphic has_many :through association:
class Story < ActiveRecord::Base
has_many :taggings, :as => :taggable
has_many :tags, :through => :taggings, :order => :name
end
class Tag < ActiveRecord::Base
has_many :taggings, :dependent => :destroy
has_many :stories, :through => :taggings, :source => :taggable, :source_type => "Story"
end
As you probably know this adds a whole slew of associated methods to the Story model like tags, tags<<, tags=, tags.empty?, etc.
How do I go about overriding one of these methods? Specifically the tags<< method. It's pretty easy to override a normal class methods but I can't seem to find any information on how to override association methods. Doing something like
def tags<< *new_tags
#do stuff
end
produces a syntax error when it's called so it's obviously not that simple.
You can use block with has_many to extend your association with methods. See comment "Use a block to extend your associations" here.
Overriding existing methods also works, don't know whether it is a good idea however.
has_many :tags, :through => :taggings, :order => :name do
def << (value)
"overriden" #your code here
super value
end
end
If you want to access the model itself in Rails 3.2 you should use proxy_association.owner
Example:
class Author < ActiveRecord::Base
has_many :books do
def << (book)
proxy_association.owner.add_book(book)
end
end
def add_book (book)
# do your thing here.
end
end
See documentation
I think you wanted def tags.<<(*new_tags) for the signature, which should work, or the following which is equivalent and a bit cleaner if you need to override multiple methods.
class << tags
def <<(*new_tags)
# rawr!
end
end
You would have to define the tags method to return an object which has a << method.
You could do it like this, but I really wouldn't recommend it. You'd be much better off just adding a method to your model that does what you want than trying to replace something ActiveRecord uses.
This essentially runs the default tags method adds a << method to the resulting object and returns that object. This may be a bit resource intensive because it creates a new method every time you run it
def tags_with_append
collection = tags_without_append
def collection.<< (*arguments)
...
end
collection
end
# defines the method 'tags' by aliasing 'tags_with_append'
alias_method_chain :tags, :append
The method I use is to extend the association. You can see the way I handle 'quantity' attributes here: https://gist.github.com/1399762
It basically allows you to just do
has_many : tags, :through => : taggings, extend => QuantityAssociation
Without knowing exactly what your hoping to achieve by overriding the methods its difficult to know if you could do the same.
This may not be helpful in your case but could be useful for others looking into this.
Association Callbacks:
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
Example from the docs:
class Project
has_and_belongs_to_many :developers, :after_add => :evaluate_velocity
def evaluate_velocity(developer)
...
end
end
Also see Association Extensions:
class Account < ActiveRecord::Base
has_many :people do
def find_or_create_by_name(name)
first_name, last_name = name.split(" ", 2)
find_or_create_by_first_name_and_last_name(first_name, last_name)
end
end
end
person = Account.first.people.find_or_create_by_name("David Heinemeier Hansson")
person.first_name # => "David"
person.last_name # => "Heinemeier Hansson"
Rails guides documents about overriding the added methods directly.
OP's issue with overriding << probably is the only exception to this, for which follow the top answer. But it wouldn't work for has_one's = assignment method or getter methods.