lib/trackoid/aggregates.rb in trackoid-0.1.1 vs lib/trackoid/aggregates.rb in trackoid-0.1.2

- old
+ new

@@ -1,68 +1,141 @@ +# encoding: utf-8 module Mongoid #:nodoc: - module Tracking - + module Tracking #:nodoc: module Aggregates # This module includes aggregate data extensions to Trackoid instances def self.included(base) base.class_eval do extend ClassMethods - include InstanceMethods class_inheritable_accessor :aggregate_fields, :aggregate_klass - self.aggregate_fields = [] + self.aggregate_fields = {} self.aggregate_klass = nil -# delegate :aggregate_fields, :aggregate_klass, :to => "self.class" + delegate :aggregate_fields, + :aggregate_klass, + :aggregated?, + :to => "self.class" end end module ClassMethods - + # Defines an aggregate token to an already tracked model. It defines + # a new mongoid model named after the original model. + # + # Example: + # + # <tt>class Page</tt> + # <tt> include Mongoid::Document</tt> + # <tt> include Mongoid::Document</tt> + # <tt> track :visits</tt> + # <tt> aggregate :browsers do |b|</tt> + # <tt> b.split(" ").first</tt> + # <tt> end</tt> + # <tt>end</tt> + # + # A new model is defined as <tt>class PageAggregates</tt> + # + # This model has the following structure: + # + # <tt>belongs_to :page</tt> + # <tt>field :ns, :type => String</tt> + # <tt>field :key, :type => String</tt> + # <tt>index [:ns, :key], :unique => true</tt> + # <tt>track :[original_parent_tracking_data]</tt> + # <tt>track :...</tt> + # + # :ns is the "namespace". It's the name you put along the + # "aggregate :browsers" in the original model definition. + # + # :key is your aggregation key. This is the value you are required to + # return in the "aggregate" block. + # + # With the above structure, you can always query aggregates directly + # using Mongoid this way: + # + # <tt>TestModelAggregates.where(:ns => "browsers", :key => "explorer").first</tt> + # + # But you are encouraged to use Trackoid methods whenever possible. + # def aggregate(name, &block) + raise Errors::AggregationAlreadyDefined.new(self.name, name) if + aggregate_fields.has_key? name + define_aggregate_model if aggregate_klass.nil? - has_many name.to_sym, :class_name => aggregate_klass.to_s + has_many_related internal_accessor_name(name), :class_name => aggregate_klass.to_s add_aggregate_field(name, block) + create_aggregation_accessors(name) end + # Return true if this model has aggregated data. + def aggregated? + !aggregate_klass.nil? + end + protected # Returns the internal representation of the aggregates class name def internal_aggregates_name str = self.to_s.underscore + "_aggregates" str.camelize end + def internal_accessor_name(name) + (name.to_s + "_accessor").to_sym + end + + # Defines the aggregation model. It checks for class name conflicts def define_aggregate_model raise Errors::ClassAlreadyDefined.new(internal_aggregates_name) if foreign_class_defined + parent_name = self.name.underscore define_klass do include Mongoid::Document include Mongoid::Tracking - field :name, :type => String, :default => "Dummy Text" -# belongs_to : + + # Make the relation to the original class + belongs_to_related parent_name.to_sym, :class_name => parent_name.camelize + + # Internal fields to track aggregation token and keys + field :ns, :type => String + field :key, :type => String + index [[:ns, Mongo::ASCENDING], [:key, Mongo::ASCENDING]], :unique => true, :background => true + + # Include parent tracking data. + parent_name.camelize.constantize.tracked_fields.each {|track_field| track track_field } end self.aggregate_klass = internal_aggregates_name.constantize end - + + # Returns true if there is a class defined with the same name as our + # aggregate class. def foreign_class_defined Object.const_defined?(internal_aggregates_name.to_sym) end + # Adds the aggregate field to the array of aggregated fields. def add_aggregate_field(name, block) - aggregate_fields << { name => block } + aggregate_fields[name] = block end + # Defines the aggregation external class. This class is named after + # the original class model but with "Aggregates" appended. + # Example: TestModel ==> TestModelAggregates def define_klass(&block) - # klass = Class.new Object, &block - klass = Object.const_set internal_aggregates_name, Class.new + klass = Object.const_set(internal_aggregates_name, Class.new) klass.class_eval(&block) end - end - - module InstanceMethods - def aggregated? - !self.class.aggregate_klass.nil? + def create_aggregation_accessors(name) + # Aggregation accessors in the model acts like a named scope + define_method(name) do |*args| + TrackerAggregates.new(self, name, args) + end + + define_method("#{name}=") do + raise NoMethodError + end end + end end @@ -77,12 +150,13 @@ # aggregate :referers do # ["domain.com"] # end # # - # self.visits.inc("Google engine") - # - # + # self.visits(agg).inc + # self.visits.today + # self.visits.browsers.today + # # users # { _id: 32334333, name:"pepe", visits_data:{} } # # users_aggregates # { _id: 11221223, data_for: 32334333, ns: "browsers", key: nil, visits_data:{} }