Skip to content

Commit

Permalink
Trying to add magic methods :(
Browse files Browse the repository at this point in the history
  • Loading branch information
Ricard Forniol committed May 27, 2011
1 parent 3ddeea4 commit a77b7c2
Show file tree
Hide file tree
Showing 9 changed files with 158 additions and 132 deletions.
85 changes: 26 additions & 59 deletions README.rdoc
@@ -1,34 +1,26 @@
= ActivityCounter

This plugin creates a table with cache counter for diferent status of models

It gets a source model(supose Event) and a cached model(supose Intiviation)

A user is invited to an event
Event => Invitation

How many invitations have/are (status)?
Event => invitations
Invitations has a status
accepted
rejected
maybe
pending
== Counter

class Event < ActiveRecord::Base
has_many :invitations, :dependent => :destroy

end

class Invitation < ActiveRecord::Base
# This method will create a namespace with is?, to?, from? for every status key.
# It will count all invitations ( :all ) rows. As we want :new items to be count
# as pending, we add a hash :new => :pending to do so.
belongs_to :event, :counter_cache => {:defaults => [:all, :new => :pending ], :pending => 1, :accepted => 2, :rejected => 3, :maybe => 4 }

end

There are two default counters:
Figure we have an Event model that has many invitations. We want to know how many Invitations have status: accepted, rejected, pending or maybe.

To do so we define a Source module and a Cached module we will load then into classes that match these roles.

Counter Module

This is the module were counting actions are defined. This method has the following fields:

- :source_class
- :source_id
:source_class and :source_id gives us the source model instance.

- :source_relation the name of the relation that will let us know
the collection of items.

- :name is the name of the counter. This is the status we want to count.
- :count is the number of items count.

== There are two default counters:
all: counts all elements
new: counts new before some "status" field is updated

Expand All @@ -38,46 +30,21 @@ There are two default counters:
be compared when is updated or the current status can be seen to decrease the counter before it's destroyed.

For this purpose three methods are created for every status:

*status.#{counter_name}.to?*
When the status change to counter status (increase)
*status.#{counter_name}.from?*
When the status moves from another counter status (decrease)
*status.#{counter_name}.is?*
When the row is being destroyed ask if this is the current status (decrease)

New invitation
invitation.status.pending.to? true (pending counter is increased)
invitation.status.pending.from? false

User updates his status to accepted
invitation.status.pending.to? false
invitation.status.pending.from? true (pending counter is decreased)
invitation.status.accepted.to? true (accepted counter is increased)

Invitation is destroyed
invitation.status.accepted.is? true (accepted counter is decreased)
*Example:
@invitation.status.current (current status)
@invitation.status.old (status before update)
@invitation.status.new (status after update)

Source class collection accessor method has method for every counter so we can access them.

@event.invitations.pending.count
@event.invitations.accepted.count



== Default :counter_cache's

:new
increase => it is being increased when a new item is created
decrease => when this item is updated, the counter will decrease it
:all
increase => it is being increased when a new item is created
decrease => it is being decreased when an item is destroyed

=== Custom :counter_cache's

When an item is being updated the counter will be increased/decreased depending on
if #{counter}.to? is true (increased) or if #{counter}.from? is true (decreased).

The updated row must have a "status" field containing the different stats of it.
If your model hasn't a field named "status" exactly, it can be customized by passing
:status_field => 'my_status_field_name' parameter to the belongs_to method
decrease => it is being decreased when an item is destroyed
2 changes: 1 addition & 1 deletion lib/activity_counter/active_record/associations.rb
Expand Up @@ -20,7 +20,7 @@ def self.add_multiple_counter_cache(reflection)
status_field_name = CounterNamespaces.status_field_for(reflection)
statuses = reflection.options[:counter_cache]

class_eval <<-MAGIC, __FILE__, __LINE__ + 1
instance_eval <<-MAGIC, __FILE__, __LINE__ + 1
def #{status_field_name}
@status ||= Status.new(self,#{reflection.name})
Expand Down
Expand Up @@ -22,18 +22,40 @@ def count_on(*options)
alias_method :activity_count_initialize, :initialize
def initialize(owner, reflection)
activity_count_initialize(owner, reflection)
reflection.reverseme(reflection)
if reflection.reverseme.last.options[:counter_cache].is_a?(Hash)
options = reflection.reverseme.last.options[:counter_cache].reject{ |key,value| key == :default }
end
puts "Class variable Owner: #{@owner}"
puts "Given Owner: #{owner.inspect}"
puts "Containing class: #{self.object_id}"
@counter_cache_options = reflection.reverseme.last.options[:counter_cache]
@counter_cache_options_without_default = @counter_cache_options.reject{ |key,value| key == :default }
define_counters_accessor
end
#alias_method :rails_method_missing, :method_missing
#def method_missing(method, *args)
# puts "missing method #{method}"
# if @counter_cache_options.reject{ |key,value| key == :default }.keys.include?(method.to_sym)
#
# send(method)
# else
# rails_method_missing(method, args.first)
# end
#end
private
def define_counter_accessor(reflection, accessor_name)
class_eval <<-MAGIC
def #{accessor_name}
Counter.create_or_retrieve(:source => @owner, :reflection => @reflection, :name => #{accessor_name.inspect})
end
MAGIC
def define_counters_accessor
@counter_cache_options_without_default.keys.each do |accessor_name|
eval <<-MAGIC
module MagicMethods
def #{accessor_name}
puts "Containing class: \#{self.object_id}"
puts "Owner: \#{@owner.inspect}"
Counter.create_or_retrieve(:source => @owner, :auto => @reflection, :name => #{accessor_name.inspect})
end
end
MAGIC
end
self.class.send :include, MagicMethods
@counter_cache_options_without_default.keys.each do |accessor_name|
puts "Is #{accessor_name} defined? #{(respond_to? accessor_name).inspect}"
end
end
end
end
Expand Down
33 changes: 33 additions & 0 deletions lib/activity_counter/model/cached.rb
@@ -0,0 +1,33 @@
module ActivityCounter
module Model
module Cached
module ClassMethods

## Add status_update methods and trigger them to their events
def configure_cached_class
self.instance_eval <<-HELLO
def update_status_counter_on_create
status.current.counter.increase
end
def update_status_counter_on_change
if status.changed?
status.old.counter.decrease
status.new.counter.increase
end
end
def update_status_counter_on_destroy
status.current.counter.decrease
end
HELLO

after_create :update_status_counter_on_create
after_update :update_status_counter_on_change
before_destroy :update_status_counter_on_destroy

end
end
end
end
end
87 changes: 41 additions & 46 deletions lib/activity_counter/model/counter.rb
Expand Up @@ -3,15 +3,13 @@ module Model
module Counter
module ClassMethods
def validate_counter
send :validates_uniqueness_of, :name, :scope => [:source_class, :source_id, :cached_relation], :on => :create
send :validates_presence_of, :source_class, :source_id, :cached_class, :name
send :validates_presence_of, :cached_relation, :if => :pluralized_class_is_relation_name
send :validates_uniqueness_of, :name, :scope => [:source_class, :source_id, :source_relation], :on => :create
send :validates_presence_of, :source_class, :source_id, :source_relation, :name
end

def create_or_retrieve(*options)
options = cleanup_params(options.first)
counter = self.where(options).first

(counter.blank? ? generate!(options) : counter)
end

Expand All @@ -21,41 +19,49 @@ def generate(*options)

def split_source(options)
@source = options.delete(:source)
{:source_class => @source.class.to_s, :source_id => @source[:id]}.merge(options)
p @source
{ :source_class => @source.class.to_s, :source_id => @source[:id] }.merge(options)
end

def find_reflection_name(options)
case
when options[:reverse] then
reverse = options.delete(:reverse)
options[:source_reflection] = reverse.reverseme.name
when options[:cached_class] then
cached_class = options.delete(:cached_class)

options[:source_relation] = options.delete(:reverse).reverseme.name
when options[:auto] then
auto = options.delete(:auto)
raise "unsuported relation #{auto.macro}" unless [:belongs_to, :has_many].include?(auto.macro)
(auto.macro == :belongs_to and options[:reverse] = auto) or options[:reflection] = auto
options = find_reflection_name(options)
when options[:reflection] then
reflection = options.delete(:reflection)
options[:source_reflection] = reflection.name
options[:source_relation] = options.delete(:reflection).name
end
options
end

# :source => object that has many items
# :reverse => reflection on the belongs to side
# :cached_class => class that belongs to other one
# :reflection => has_many reflection of the source side
# :name => counter name
###=====================================================###
## Parameters description ##
###-----------------------------------------------------###
## - :source => object that has many items ##
## - :name => counter name ##
###-----------------------------------------------------###
## Only one of them can be passed ##
## - :reverse => belongs to side reflection ##
## - :auto => it discovers the given reflection type ##
## - :reflection => has many side reflection ##
## - :source_relation => it's the has_many relation name ##
###=====================================================###
def cleanup_params(*options)
options = options.first
[:source, [:reverse, :reflection], :name].each do |option|
if option.is_a?(Array)
option.each{|new_options| validate_option(options, new_options)}
[:source, [:reverse, :auto, :reflection, :source_relation], :name].each do |expected|
if expected.is_a?(Array)
validate_one_is_present(expected, options)
else
validate_option(options, option)
validate_option(options, expected)
end
end

options = split_source(options)

options = find_reflection_name(options)
options
end
def generate!(*options)
Expand All @@ -64,43 +70,32 @@ def generate!(*options)
counter
end
private
def validate_one_is_present(expected, given_options)
found = expected.reject{|new_option| !given_options.keys.include?(new_option)}
if found.empty?
raise "Non of the following params found: #{expected.inspect}"
elsif found.size > 1
raise "Only one of the following can be present: #{found.inspect}"
end
end
def validate_option(options, option)
unless options.keys.include?(option)
raise "missing parameter #{option} at #{self.class.to_s}.generate method"
end
end

end
module InstanceMethods

# belongs_to relation
def belongs_to_relation_name
self[:cached_relation].blank? ? source_class.tableize : self[:cached_relation]
end
alias_method :cached_relation, :belongs_to_relation_name
def source
eval("#{source_class}.find(#{source_id})")
end
def cached_items
source.send(source_relation)
end
def increase
update_attribute(:count, self[:count]+1)
self.class.increment_counter('count', self[:id])
end
def decrease
self[:count] > 0 and update_attribute(:count, self[:count]-1)
end
def cached_items
# Not working with relations having custom names yet!!!
cached_class.tableize
source.send(cached_class.tableize)
end

def pluralized_class_is_relation_name
unless source_class.blank?
#puts "source_class: #{source_class}"
has_relation = eval(source_class).reflections.keys.include?(cached_relation)
if self[:cached_relation].blank? && !has_relation
errors[:cached_relation] << "No relation found named #{cached_relation} for #{self[:cached_class]}"
end
end
self.class.decrement_counter('count', self[:id])
end
end
end
Expand Down
9 changes: 9 additions & 0 deletions lib/activity_counter/model/source.rb
@@ -0,0 +1,9 @@
module ActivityCounter
module Model
module Source
module ClassMethods

end
end
end
end
2 changes: 1 addition & 1 deletion test/dummy/app/models/counter.rb
@@ -1,4 +1,4 @@
class Counter < ActiveRecord::Base
acts_as_activity_counter

end
8 changes: 4 additions & 4 deletions test/unit/counter_test.rb
Expand Up @@ -6,13 +6,13 @@ def setup
@user = User.create
@user2 = User.create
end
def new_counter(source, cached_class, counter_name, *options)
def new_counter(source, counter_name, *options)
options = options.first.blank? ? {} : options.first
counter_options = {:source => source, :cached_class => cached_class, :name => counter_name}.merge(options)
counter_options = {:source => source, :name => counter_name}.merge(options)
Counter.generate(counter_options)
end
def new_event_counter(user)
new_counter(user, 'Event', 'total', :cached_relation => 'organizer')
new_counter(user, 'total', :source_relation => 'events')
end
test "acts_as_counter method presence" do
assert User.public_methods.include?('acts_as_activity_counter')
Expand All @@ -24,7 +24,7 @@ def new_event_counter(user)
end

test "have all attributes set" do
counter = new_counter(@user, 'Event', nil, :cached_relation => 'organizer')
counter = new_counter(@user, nil, :source_relation => 'events')
assert(!counter.save)
assert_equal(counter.errors.keys, [:name])
counter.name = 'total'
Expand Down

0 comments on commit a77b7c2

Please sign in to comment.