Hacking ActiveRecord: add global named scope - ruby-on-rails

I am trying to have a pack of very generic named scopes for ActiveRecord models like this one:
module Scopes
def self.included(base)
base.class_eval do
named_scope :not_older_than, lambda {|interval|
{:conditions => ["#{table_name}.created_at >= ?", interval.ago]
}
end
end
end
ActiveRecord::Base.send(:include, Scopes)
class User < ActiveRecord::Base
end
If the named scope should be general, we need to specify *table_name* to prevent naming problems if their is joins that came from other chained named scope.
The problem is that we can't get table_name because it is called on ActiveRecord::Base rather then on User.
User.not_older_than(1.week)
NoMethodError: undefined method `abstract_class?' for Object:Class
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/base.rb:2207:in `class_of_active_record_descendant'
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/base.rb:1462:in `base_class'
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/base.rb:1138:in `reset_table_name'
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/base.rb:1134:in `table_name'
from /home/bogdan/makabu/railsware/startwire/repository/lib/core_ext/active_record/base.rb:15:in `included'
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/named_scope.rb:92:in `call'
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/named_scope.rb:92:in `named_scope'
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/named_scope.rb:97:in `call'
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/named_scope.rb:97:in `not_older_than'
How can I get actual table_name at Scopes module?

Try using the #scoped method inside a class method of ActiveRecord::Base. This should work:
module Scopes
def self.included(base)
base.class_eval do
def self.not_older_than(interval)
scoped(:conditions => ["#{table_name}.created_at > ?", interval.ago])
end
end
end
end
ActiveRecord::Base.send(:include, Scopes)

Rails 5, ApplicationRecord (Hope it helps others)
# app/models/concerns/not_older_than.rb
module NotOlderThan
extend ActiveSupport::Concern
included do
scope :not_older_than, -> (time, table = self.table_name){
where("#{table}.created_at >= ?", time.ago)
}
end
end
# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
include NotOlderThan
end
# app/models/user.rb
class User < ApplicationRecord
# Code
end
# Usage
User.not_older_than(1.week)
In Rails 5, all models are inherited from ApplicationRecord by default. If you wan to apply this scope for only particular set of models, add include statements only to those model classes. This works for join queries and chained scopes as well.

Additional useful scopes below :
module Scopes
def self.included(base)
base.class_eval do
def self.created(date_start, date_end = nil)
if date_start && date_end
scoped(:conditions => ["#{table_name}.created_at >= ? AND #{table_name}.created_at <= ?", date_start, date_end])
elsif date_start
scoped(:conditions => ["#{table_name}.created_at >= ?", date_start])
end
end
def self.updated(date_start, date_end = nil)
if date_start && date_end
scoped(:conditions => ["#{table_name}.updated_at >= ? AND #{table_name}.updated_at <= ?", date_start, date_end])
elsif date_start
scoped(:conditions => ["#{table_name}.updated_at >= ?", date_start])
end
end
end
end
end
ActiveRecord::Base.send(:include, Scopes)

Here is an updated, Rails4 compatible solution.
I am told defining global scopes like this can lead to conflicts, caveat emptor and all that, but sometimes you just need a simple scope on all your models, right?
Define a module.
# in /app/models/concerns/global_scopes.rb
module GlobalScopes
def self.included(base)
base.class_eval do
def self.in_daterange(start_date, end_date)
all.where(created_at: start_date.to_date.beginning_of_day..end_date.to_date.end_of_day)
end
end
end
end
Have the module included in ActiveRecord::Base.
# in /config/initializers/activerecord.rb
ActiveRecord::Base.send(:include, GlobalScopes)
That's it! Notice that in Rails4 you do not have to mess with :scoped, but instead you use :all and chain your query to it.

Related

Rails, issue with auto loading

So im having this weird issue for some reason i dont know why it is happening.
So, i have this extension in conifg/initializers/has_friendly_token.rb
ActiveRecord::Base.define_singleton_method :has_friendly_token do
extend FriendlyId
friendly_id :token
before_validation :set_unique_token, if: :set_token?
define_method :set_token? do
new_record? || saved_change_to_token?
end
define_method :set_unique_token do
table_name = self.model_name.route_key
scope = persisted? ? self.class.where("#{table_name}.id != ?", id) : self.class.default_scoped
while token.nil? || scope.where(token: token).exists?
self.token = SecureRandom.base58(24)
end
end
end
And I'm using it in two different models
class Business < ApplicationRecord
include Colorful, Channels, Rails.application.routes.url_helpers
has_friendly_token
end
and
class Conversation < ApplicationRecord
has_friendly_token
include AASM, Colorful, Rails.application.routes.url_helpers
end
the thing is, whenver i try to to run i get this error
`method_missing': undefined local variable or method `has_friendly_token' for Business (call 'Business.connection' to establish a
connection):Class (NameError)
Did you mean? has_secure_token
but it works in conversation. Any ideas why this happenes?
I can't see a good reason to use an initializer to monkeypatch ActiveRecord::Base here in the first place. Instead create a module and extend your ApplicationRecord class with it as this is simply a "macro" method.
module FriendlyToken
def has_friendly_token
extend FriendlyId
friendly_id :token
before_validation :set_unique_token, if: :set_token?
define_method :set_token? do
new_record? || saved_change_to_token?
end
define_method :set_unique_token do
table_name = self.model_name.route_key
scope = persisted? ? self.class.where("#{table_name}.id != ?", id) : self.class.default_scoped
while token.nil? || scope.where(token: token).exists?
self.token = SecureRandom.base58(24)
end
end
end
end
class ApplicationRecord < ActiveRecord::Base
extend FriendlyToken
end
Unlike a monkeypatch its easy to test this code and it will be picked up by documentation tools like rdoc or yard as well as coverage tools.
You could also just simply write it as a concern since your macro method does not take any arguments anyways:
module FriendlyToken
extend ActiveSupport::Concern
included do
extend FriendlyId
friendly_id :token
before_validation :set_unique_token, if: :set_token?
end
def set_token?
new_record? || saved_change_to_token?
end
def set_unique_token?
table_name = self.model_name.route_key
scope = persisted? ? self.class.where("#{table_name}.id != ?", id) : self.class.default_scoped
while token.nil? || scope.where(token: token).exists?
self.token = SecureRandom.base58(24)
end
end
end
class Business < ApplicationRecord
include Colorful,
Channels,
Rails.application.routes.url_helpers,
FriendlyToken
end
The advantage here is easier debugging since the methods are not defined dynamically.
See 4 Ways To Avoid Monkey Patching.

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'

Rails after_remove not getting triggered with destroy

I have venues and venue_managers, with a HABTM relationship, and I am rolling my own counter cache via after_add and after_remove callbacks. Those callbacks on the venue_managers are getting added to the venue model via a mixin. I am using am mixin because there are other models that need this same logic.
If there are 2 venue_managers and I re-assign venue_managers to just have 1, with venue.venue_managers = a_user, then the after_remove callback gets triggered and all is well. If I have 2 venue_managers and I destroy the last one with venue.venue_managers.last.destroy, the after_remove callback is NOT triggered and the counter cache does not update, which is my problem.
class Venue < ActiveRecord::Base
include HABTMCounterCache
has_and_belongs_to_many :venue_managers, :class_name => 'User', :join_table => :venues_venue_managers
end
module HABTMCounterCache
extend ::ActiveSupport::Concern
module ClassMethods
def count_cache_associations
klass = self.name.downcase
self.send("after_add_for_#{klass}_managers") << lambda do |obj, manager|
obj.update_counter_cache(manager)
end
self.send("after_remove_for_#{klass}_managers") << lambda do |obj, manager|
obj.update_counter_cache(manager)
end
end
end
def update_counter_cache(noop)
klass = self.class.name.downcase
self.assign_attributes(:"#{klass}_managers_count" => self.send("#{klass}_managers").count)
end
end

Can I define a query in Rails model?

Here's what I have:
module EventDependencyProperties
def start_date
shows.order('show_date ASC').first.show_date
end
def end_date
shows.order('show_date DESC').first.show_date
end
def is_future_show?
end_date >= Date.today
end
end
class Event < ActiveRecord::Base
include EventDependencyProperties
has_many :shows
has_and_belongs_to_many :users
end
class Show < ActiveRecord::Base
belongs_to :event
end
I have bits of code elsewhere using the is_future_show? method. What I would like to do is have a method in the module mixin to return "future shows" using a query that has the same criteria as the is_future_show? method. How would I go about achieving this? I'm very much a newbie to Rails but tainted by knowledge of other languages and frameworks.
Cheers,
Dany.
You can put the query into a scope:
class Show < ActiveRecord::Base
scope :future, lambda { where("show_date > ?", Date.today) }
end
Call it like this:
my_event.shows.future
Edit: Ah I see. To return all events with a show in the future:
Event.joins(:shows).where("shows.show_date > ?", Date.today)
agains this can be scoped:
class Event
scope :future, lambda { joins(:shows).where("shows.show_date > ?", Date.today) }
end
On a side note, I'm not sure about the setup of your models, especially the use of the mixin. Here's what I do:
class Show < ActiveRecord::Base
belongs_to :event
# use default_scope so shows are ordered by date by default
default_scope order("show_date ASC")
end
class Event < ActiveRecord::Base
has_many :shows
has_and_belongs_to_many :users
scope :future, lambda { joins(:shows).where("shows.show_date > ?", Date.today) }
def start_date
shows.first.show_date
end
def end_date
shows.last.show_date
end
def ends_in_future?
end_date > Date.today
end
end
also it would be cleaner if the show_date column for the Show model was just called date (so you could just write show.date rather that show.show_date).

Rails: How to effectively use self.inherited

I have an AbstractRecord model from which some of the concrete models (which have their own tables) descend. Following is the inheritance.
AbstractRecord < ActiveRecord::Base
Blog < AbstractRecord
Post < AbstractRecord
....
....
In order for Rails to look for the proper tables if there is inheritance, API docs say to define a class method abstract_class? that returns true so that rails won't look for its table. In my case, in order for rails to look for blogs table (instead of abstract_records table, which is typically the case as in STI) I defined method abstract_class? in AbstractRecord that returns true. All the queries seems to work fine. But I see whenever I instantiate Blog, rails shows it as Blog (abstract) in the console as its parent class returns true. In order to avoid this, I could again define abstract_class? that returns false in Blog class.
But I was thinking instead of defining abstract_class? in all the child models, if I could somehow make use of self.inherited and define that method in AbstractClass itself. I tried to use several approaches (following) none seems to work.
class AbstractRecord < ActiveRecord::Base
def self.abstract_class?
true
end
def self.inherited(subclass)
super
subclass.instance_eval do
define_method(:abstract_class?) { false }
end
end
end
class AbstractRecord < ActiveRecord::Base
def self.abstract_class?
true
end
def self.inherited(subclass)
super
subclass.class_eval do
define_method(:abstract_class?) { false }
end
end
end
class AbstractRecord < ActiveRecord::Base
def self.abstract_class?
true
end
def self.inherited(subclass)
super
subclass.instance_eval do
def abstract_class?
false
end
end
end
end
class AbstractRecord < ActiveRecord::Base
def self.abstract_class?
true
end
def self.inherited(subclass)
super
subclass.class_eval do
def abstract_class?
false
end
end
end
end
Any advise on what I am doing wrong is appreciated?
Try this:
def self.inherited(subclass)
super
def subclass.abstract_class?
false
end
end
Or:
def self.inherited(subclass)
super
subclass.class_eval do
def self.abstract_class?
# You lost the 'self' part, so you had just defined an instant method for the subclass
false
end
end
end
class Account < Foo::Bar::Base
end
module Foo
module Bar
class Base < ::ActiveRecord::Base
def self.abstract_class?
true
end
end
end
end
That works fine for me. It results in table name "accounts" as it have to be.

Resources