How to DRY up this code - ruby-on-rails

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

Related

Using ActiveRecord:Relation for specific classes, not all relations across ActiveRecord

I'm building a gem in which part of its purpose is to extend associations on a target Class. Although I can easily extend all associations by using something like :
ActiveRecord::Relation.send(:include, MyGem::ActiveRecord::RelationMethods)
This is too broad, and for a Rails App that may use this Gem, I don't want to extend associations for all Classes.
For better granularity, I want to provide the equivalent of :
class User < ActiveRecord::Base
has_many :messages, :extend => MyGem::ActiveRecord::RelationMethods
has_many :comments, :extend => MyGem::ActiveRecord::RelationMethods
end
By using
class User < ActiveRecord::Base
acts_as_my_fancy_gem
has_many :messages
has_many :comments
end
The problem I have is trying to conditionally extend associations within the Gem, when acts_as_my_fancy_gem is added to a class. This is the bare bones of it.
module MyGem
extend ActiveSupport::Concern
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def acts_as_my_fancy_gem
include MyGem::InstanceMethods
end
end
module InstanceMethods
...
end
end
I've looked into reflections, but at this point can find a clear path, and have simply taking stabs in the dark to experiment.
UPDATE:
Currently, I can achieve this with each association by providing a class method like
class User < ActiveRecord::Base
has_many :messages
has_many :comments
fancy_extend :messages
end
module MyGem
extend ActiveSupport::Concern
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def acts_as_my_fancy_gem
include MyGem::InstanceMethods
end
def fancy_extend *associations
class_eval do
associations.each do |association|
reflections[association].options[:extend] = MyGem::ActiveRecord::RelationMethods
end
end
end
end
module InstanceMethods
...
end
end
Adding this approach into the act_as_my_fancy method (which is where I would like to have it) gives me :
# NoMethodError: undefined method `options' for nil:NilClass
Is this rail4? I did not find the :extend option documented. It looks like rails 4 uses blocks to do that nowadays.
It could be as simple as this:
module Fancy
def has_many(name, scope = nil, options = {})
super(name, scope, options) do
def doit
"i did"
end
end
end
end
# in your model
extend Fancy
YourModel.your_relation.doit # => 'i did'

Add associations from mixin without overwriting existing associations

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

Loading has_many options hash via a method

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).

How to model has_many with polymorphism?

I've run into a situation that I am not quite sure how to model.
EDIT: The code below now represent a working solution. I am still interested in nicer looking solutions, though.
Suppose I have a User class, and a user has many services. However, these services are quite different, for example a MailService and a BackupService, so single table inheritance won't do. Instead, I am thinking of using polymorphic associations together with an abstract base class:
class User < ActiveRecord::Base
has_many :services
end
class Service < ActiveRecord::Base
validates_presence_of :user_id, :implementation_id, :implementation_type
validates_uniqueness_of :user_id, :scope => :implementation_type
belongs_to :user
belongs_to :implementation, :polymorphic => true, :dependent => :destroy
delegate :common_service_method, :name, :to => :implementation
end
#Base class for service implementations
class ServiceImplementation < ActiveRecord::Base
validates_presence_of :user_id, :on => :create
#Virtual attribute, allows us to create service implementations in one step
attr_accessor :user_id
has_one :service, :as => :implementation
after_create :create_service_record
#Tell Rails this class does not use a table.
def self.abstract_class?
true
end
#Name of the service.
def name
self.class.name
end
#Returns the user this service
#implementation belongs to.
def user
unless service.nil?
service.user
else #Service not yet created
#my_user ||= User.find(user_id) rescue nil
end
end
#Sets the user this
#implementation belongs to.
def user=(usr)
#my_user = usr
user_id = usr.id
end
protected
#Sets up a service object after object creation.
def create_service_record
service = Service.new(:user_id => user_id)
service.implementation = self
service.save!
end
end
class MailService < ServiceImplementation
#validations, etc...
def common_service_method
puts "MailService implementation of common service method"
end
end
#Example usage
MailService.create(..., :user => user)
BackupService.create(...., :user => user)
user.services.each do |s|
puts "#{user.name} is using #{s.name}"
end #Daniel is using MailService, Daniel is using BackupService
Notice that I want the Service instance to be implictly created when I create a new service.
So, is this the best solution? Or even a good one? How have you solved this kind of problem?
I don't think your current solution will work. If ServiceImplementation is abstract, what will the associated classes point to? How does the other end of the has_one work, if ServiceImplementation doesn't have a pk persisted to the database? Maybe I'm missing something.
EDIT: Whoops, my original didn't work either. But the idea is still there. Instead of a module, go ahead and use Service with STI instead of polymorphism, and extend it with individual implementations. I think you're stuck with STI and a bunch of unused columns across different implementations, or rethinking the services relationship in general. The delegation solution you have might work as a separate ActiveRecord, but I don't see how it works as abstract if it has to have a has_one relationship.
EDIT: So instead of your original abstract solution, why not persist the delgates? You'd have to have separate tables for MailServiceDelegate and BackupServiceDelegate -- not sure how to get around that if you want to avoid all the null columns with pure STI. You can use a module with the delgate classes to capture the common relationships and validations, etc. Sorry it took me a couple of passes to catch up with your problem:
class User < ActiveRecord::Base
has_many :services
end
class Service < ActiveRecord::Base
validates_presence_of :user_id
belongs_to :user
belongs_to :service_delegate, :polymorphic => true
delegate :common_service_method, :name, :to => :service_delegate
end
class MailServiceDelegate < ActiveRecord::Base
include ServiceDelegate
def name
# implement
end
def common_service_method
# implement
end
end
class BackupServiceDelegate < ActiveRecord::Base
include ServiceDelegate
def name
# implement
end
def common_service_method
# implement
end
end
module ServiceDelegate
def self.included(base)
base.has_one :service, :as => service_delegate
end
def name
raise "Not Implemented"
end
def common_service_method
raise "Not Implemented"
end
end
I think following will work
in user.rb
has_many :mail_service, :class_name => 'Service'
has_many :backup_service, :class_name => 'Service'
in service.rb
belongs_to :mail_user, :class_name => 'User', :foreign_key => 'user_id', :conditions=> is_mail=true
belongs_to :backup_user, :class_name => 'User', :foreign_key => 'user_id', :conditions=> is_mail=false

Rails: Overriding ActiveRecord association method

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.

Resources