trackoid 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.1.1
@@ -0,0 +1,109 @@
1
+ module Mongoid #:nodoc:
2
+ module Tracking
3
+
4
+ module Aggregates
5
+ # This module includes aggregate data extensions to Trackoid instances
6
+ def self.included(base)
7
+ base.class_eval do
8
+ extend ClassMethods
9
+ include InstanceMethods
10
+
11
+ class_inheritable_accessor :aggregate_fields, :aggregate_klass
12
+ self.aggregate_fields = []
13
+ self.aggregate_klass = nil
14
+ # delegate :aggregate_fields, :aggregate_klass, :to => "self.class"
15
+ end
16
+ end
17
+
18
+ module ClassMethods
19
+
20
+ def aggregate(name, &block)
21
+ define_aggregate_model if aggregate_klass.nil?
22
+ has_many name.to_sym, :class_name => aggregate_klass.to_s
23
+ add_aggregate_field(name, block)
24
+ end
25
+
26
+ protected
27
+ # Returns the internal representation of the aggregates class name
28
+ def internal_aggregates_name
29
+ str = self.to_s.underscore + "_aggregates"
30
+ str.camelize
31
+ end
32
+
33
+ def define_aggregate_model
34
+ raise Errors::ClassAlreadyDefined.new(internal_aggregates_name) if foreign_class_defined
35
+ define_klass do
36
+ include Mongoid::Document
37
+ include Mongoid::Tracking
38
+ field :name, :type => String, :default => "Dummy Text"
39
+ # belongs_to :
40
+ end
41
+ self.aggregate_klass = internal_aggregates_name.constantize
42
+ end
43
+
44
+ def foreign_class_defined
45
+ Object.const_defined?(internal_aggregates_name.to_sym)
46
+ end
47
+
48
+ def add_aggregate_field(name, block)
49
+ aggregate_fields << { name => block }
50
+ end
51
+
52
+ def define_klass(&block)
53
+ # klass = Class.new Object, &block
54
+ klass = Object.const_set internal_aggregates_name, Class.new
55
+ klass.class_eval(&block)
56
+ end
57
+
58
+ end
59
+
60
+ module InstanceMethods
61
+ def aggregated?
62
+ !self.class.aggregate_klass.nil?
63
+ end
64
+ end
65
+
66
+ end
67
+
68
+
69
+ # class Aggregate
70
+ # include Mongoid::Document
71
+ # end
72
+ #
73
+ # track :visits
74
+ # aggregate :browsers do
75
+ # ["google"]
76
+ # end
77
+ # aggregate :referers do
78
+ # ["domain.com"]
79
+ # end
80
+ #
81
+ #
82
+ # self.visits.inc("Google engine")
83
+ #
84
+ #
85
+ # users
86
+ # { _id: 32334333, name:"pepe", visits_data:{} }
87
+ #
88
+ # users_aggregates
89
+ # { _id: 11221223, data_for: 32334333, ns: "browsers", key: nil, visits_data:{} }
90
+ # { _id: 11223223, data_for: 32334333, ns: "browsers", key: "google", visits_data:{} }
91
+ # { _id: 11224432, data_for: 32334333, ns: "browsers", key: "firefox", visits_data:{} }
92
+ #
93
+ #
94
+ # class UsersAggregate
95
+ # include Mongoid::Document
96
+ # include Mongoid::Tracking
97
+ #
98
+ # belongs_to :users
99
+ # field :ns
100
+ # field :key
101
+ #
102
+ # track :visits
103
+ # track :uniques
104
+ # end
105
+ #
106
+ #
107
+
108
+ end
109
+ end
@@ -0,0 +1,19 @@
1
+ # encoding: utf-8
2
+ module Mongoid #:nodoc
3
+ module Errors #:nodoc
4
+
5
+ class ClassAlreadyDefined < RuntimeError
6
+ def initialize(klass)
7
+ @klass = klass
8
+ end
9
+ def message
10
+ "#{@klass} already defined, can't aggregate!"
11
+ end
12
+ end
13
+
14
+ class ModelNotSaved < RuntimeError; end
15
+
16
+ class NotMongoid < RuntimeError; end
17
+
18
+ end
19
+ end
@@ -5,12 +5,12 @@ module Mongoid #:nodoc:
5
5
  class Tracker
6
6
  def initialize(owner, field)
7
7
  @owner, @for = owner, field
8
- @data = @owner.read_attribute(@for)
8
+ @data = @owner.read_attribute(@for) || {}
9
9
  end
10
10
 
11
11
  # Update methods
12
12
  def add(how_much = 1, date = DateTime.now)
13
- raise "Can not update a recently created object" if @owner.new_record?
13
+ raise Errors::ModelNotSaved, "Can't update a new record" if @owner.new_record?
14
14
 
15
15
  update_data(data_for(date) + how_much, date)
16
16
  @owner.collection.update( @owner._selector,
@@ -27,7 +27,7 @@ module Mongoid #:nodoc:
27
27
  end
28
28
 
29
29
  def set(how_much, date = DateTime.now)
30
- raise "Can not update a recently created object" if @owner.new_record?
30
+ raise Errors::ModelNotSaved, "Can't update a new record" if @owner.new_record?
31
31
 
32
32
  update_data(how_much, date)
33
33
  @owner.collection.update( @owner._selector,
@@ -1,15 +1,20 @@
1
1
  # encoding: utf-8
2
- require 'trackoid/tracker'
3
-
4
2
  module Mongoid #:nodoc:
5
- module Tracking
3
+ module Tracking #:nodoc:
4
+
6
5
  # Include this module to add analytics tracking into a +root level+ document.
7
6
  # Use "track :field" to add a field named :field and an associated mongoid
8
7
  # field named after :field
9
8
  def self.included(base)
10
9
  base.class_eval do
11
- raise "Must be included in a Mongoid::Document" unless self.ancestors.include? Mongoid::Document
10
+ raise Errors::NotMongoid, "Must be included in a Mongoid::Document" unless self.ancestors.include? Mongoid::Document
11
+
12
+ include Aggregates
12
13
  extend ClassMethods
14
+
15
+ class_inheritable_accessor :tracked_fields
16
+ self.tracked_fields = []
17
+ delegate :tracked_fields, :internal_track_name, :to => "self.class"
13
18
  end
14
19
  end
15
20
 
@@ -20,18 +25,36 @@ module Mongoid #:nodoc:
20
25
  # field. This is necessary so that Mongoid does not "dirty" the field
21
26
  # potentially overwriting the original data.
22
27
  def track(name)
23
- name_sym = "#{name}_data".to_sym
24
- field name_sym, :type => Hash, :default => {}
25
-
26
- # Shoul we make an index for this field?
27
- # index name_sym
28
+ set_tracking_field(name)
29
+ create_tracking_accessors(name)
30
+ end
31
+
32
+ protected
33
+ # Returns the internal representation of the tracked field name
34
+ def internal_track_name(name)
35
+ "#{name}_data".to_sym
36
+ end
28
37
 
38
+ # Configures the internal fields for tracking. Additionally also creates
39
+ # an index for the internal tracking field.
40
+ def set_tracking_field(name)
41
+ field internal_track_name(name), :type => Hash, :default => {}
42
+ # Shoul we make an index for this field?
43
+ index internal_track_name(name)
44
+ tracked_fields << internal_track_name(name)
45
+ end
46
+
47
+ # Creates the tracking field accessor and also disables the original
48
+ # ones from Mongoid. Hidding here the original accessors for the
49
+ # Mongoid fields ensures they doesn't get dirty, so Mongoid does not
50
+ # overwrite old data.
51
+ def create_tracking_accessors(name)
29
52
  define_method("#{name}") do
30
- Tracker.new(self, name_sym)
53
+ Tracker.new(self, "#{name}_data".to_sym)
31
54
  end
32
55
 
33
56
  # Should we just "undef" this methods?
34
- # They override the just defined ones from Mongoid
57
+ # They override the ones defined from Mongoid
35
58
  define_method("#{name}_data") do
36
59
  raise NoMethodError
37
60
  end
@@ -39,8 +62,8 @@ module Mongoid #:nodoc:
39
62
  define_method("#{name}_data=") do
40
63
  raise NoMethodError
41
64
  end
42
-
43
65
  end
66
+
44
67
  end
45
68
 
46
69
  end
data/lib/trackoid.rb CHANGED
@@ -2,4 +2,16 @@ require 'rubygems'
2
2
 
3
3
  gem "mongoid", ">= 1.9.0"
4
4
 
5
- require 'trackoid/tracking'
5
+ require 'trackoid/errors'
6
+ require 'trackoid/tracker'
7
+ require 'trackoid/aggregates'
8
+ require 'trackoid/tracking'
9
+
10
+ module Mongoid #:nodoc:
11
+ module Tracking
12
+
13
+ VERSION = File.read(File.expand_path("../VERSION", File.dirname(__FILE__)))
14
+
15
+ end
16
+ end
17
+
@@ -0,0 +1,118 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ class TestModel
4
+ include Mongoid::Document
5
+ include Mongoid::Tracking
6
+
7
+ field :name # Dummy field
8
+ track :visits
9
+
10
+ aggregate :browsers do
11
+ "Mozilla"
12
+ end
13
+ end
14
+
15
+ class SecondTestModel
16
+ include Mongoid::Document
17
+ include Mongoid::Tracking
18
+
19
+ field :name # Dummy field
20
+ track :visits
21
+
22
+ aggregate :browsers do
23
+ "Chrome"
24
+ end
25
+ end
26
+
27
+ describe Mongoid::Tracking::Aggregates do
28
+
29
+ before(:all) do
30
+ @mock = TestModel.new(:name => "TestInstance")
31
+ end
32
+
33
+ it "should define a class model named after the original model" do
34
+ defined?(TestModelAggregates).should_not be_nil
35
+ end
36
+
37
+ it "should define a class model named after the original second model" do
38
+ defined?(SecondTestModelAggregates).should_not be_nil
39
+ end
40
+
41
+ it "should create a has_many relationship in the original model" do
42
+ @mock.class.method_defined?(:browsers).should be_true
43
+ end
44
+
45
+ it "should have the aggregates klass in a instance var" do
46
+ @mock.aggregate_klass == TestModelAggregates
47
+ end
48
+
49
+ it "should create an array in the class with all aggregate fields" do
50
+ @mock.class.aggregate_fields.map(&:keys).flatten.should == [ :browsers ]
51
+ end
52
+
53
+ it "should create an array in the class with all aggregate fields even when monkey patching" do
54
+ class TestModel
55
+ aggregate :referers do
56
+ "(none)"
57
+ end
58
+ end
59
+ @mock.class.aggregate_fields.map(&:keys).flatten.should == [ :browsers, :referers ]
60
+ end
61
+
62
+ it "should indicate this is an aggregated traking object with aggregated?" do
63
+ @mock.aggregated?.should be_true
64
+ end
65
+
66
+ it "should raise error if already defined class with the same aggregated klass name" do
67
+ lambda {
68
+ class MockTestAggregates
69
+ def dummy; true; end
70
+ end
71
+ class MockTest
72
+ include Mongoid::Document
73
+ include Mongoid::Tracking
74
+ track :something
75
+ aggregate :other_something do
76
+ "other"
77
+ end
78
+ end
79
+ }.should raise_error Mongoid::Errors::ClassAlreadyDefined
80
+ end
81
+
82
+ it "should NOT raise error if the already defined class is our aggregated model" do
83
+ lambda {
84
+ class MockTest2
85
+ include Mongoid::Document
86
+ include Mongoid::Tracking
87
+ track :something
88
+ end
89
+ class MockTest2
90
+ include Mongoid::Document
91
+ include Mongoid::Tracking
92
+ track :something_else
93
+ aggregate :other_something do
94
+ "other"
95
+ end
96
+ end
97
+ }.should_not raise_error Mongoid::Errors::ClassAlreadyDefined
98
+ end
99
+
100
+ it "should raise error although the already defined class includes tracking" do
101
+ lambda {
102
+ class MockTest3Aggregates
103
+ include Mongoid::Document
104
+ include Mongoid::Tracking
105
+ track :something
106
+ end
107
+ class MockTest3
108
+ include Mongoid::Document
109
+ include Mongoid::Tracking
110
+ track :something_else
111
+ aggregate :other_something do
112
+ "other"
113
+ end
114
+ end
115
+ }.should raise_error Mongoid::Errors::ClassAlreadyDefined
116
+ end
117
+
118
+ end
@@ -9,20 +9,30 @@ class Test
9
9
  end
10
10
 
11
11
  describe Mongoid::Tracking do
12
+
13
+ before(:all) do
14
+ @trackoid_version = File.read(File.expand_path("../VERSION", File.dirname(__FILE__)))
15
+ end
16
+
17
+ it "should expose the same version as the VERSION file" do
18
+ Mongoid::Tracking::VERSION.should == @trackoid_version
19
+ end
12
20
 
13
21
  it "should raise error when used in a class not of class Mongoid::Document" do
14
22
  lambda {
15
23
  class NotMongoidClass
16
24
  include Mongoid::Tracking
25
+ track :something
17
26
  end
18
- }.should raise_error
27
+ }.should raise_error Mongoid::Errors::NotMongoid
19
28
  end
20
29
 
21
- it "should not not raise when used in a class of class Mongoid::Document" do
30
+ it "should not raise error when used in a class of class Mongoid::Document" do
22
31
  lambda {
23
32
  class MongoidedDocument
24
33
  include Mongoid::Document
25
34
  include Mongoid::Tracking
35
+ track :something
26
36
  end
27
37
  }.should_not raise_error
28
38
  end
@@ -46,8 +56,19 @@ describe Mongoid::Tracking do
46
56
  @mock.visits.class.should == Mongoid::Tracking::Tracker
47
57
  end
48
58
 
59
+ it "should create an array in the class with all tracking fields" do
60
+ @mock.class.tracked_fields.should == [ :visits_data ]
61
+ end
62
+
63
+ it "should create an array in the class with all tracking fields even when monkey patching" do
64
+ class Test
65
+ track :something_else
66
+ end
67
+ @mock.class.tracked_fields.should == [ :visits_data, :something_else_data ]
68
+ end
69
+
49
70
  it "should not update stats when new record" do
50
- lambda { @mock.inc }.should raise_error
71
+ lambda { @mock.visits.inc }.should raise_error Mongoid::Errors::ModelNotSaved
51
72
  end
52
73
 
53
74
  it "shold create an empty hash as the internal representation" do
@@ -66,6 +87,10 @@ describe Mongoid::Tracking do
66
87
  @mock.visits.last_days(0).should == [@mock.visits.today]
67
88
  end
68
89
 
90
+ it "should not be aggregated" do
91
+ @mock.aggregated?.should be_false
92
+ end
93
+
69
94
  end
70
95
 
71
96
  describe "when using a model in the database" do
@@ -168,4 +193,26 @@ describe Mongoid::Tracking do
168
193
 
169
194
  end
170
195
 
196
+
197
+
198
+ context "regression test for github issues" do
199
+
200
+ it "should not raise undefined method [] for nil:NilClass for objects already saved" do
201
+ class TestModel
202
+ include Mongoid::Document
203
+ include Mongoid::Tracking
204
+ field :name
205
+ end
206
+ TestModel.delete_all
207
+ TestModel.create(:name => "dummy")
208
+
209
+ class TestModel
210
+ track :something
211
+ end
212
+ tm = TestModel.first
213
+ tm.something.today.should == 0
214
+ end
215
+
216
+ end
217
+
171
218
  end
data/trackoid.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{trackoid}
8
- s.version = "0.1.0"
8
+ s.version = "0.1.1"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Jose Miguel Perez"]
12
- s.date = %q{2010-05-30}
12
+ s.date = %q{2010-06-04}
13
13
  s.description = %q{Trackoid uses an embeddable approach to track analytics data using the poweful features of MongoDB for scalability}
14
14
  s.email = %q{josemiguel@perezruiz.com}
15
15
  s.extra_rdoc_files = [
@@ -24,8 +24,11 @@ Gem::Specification.new do |s|
24
24
  "Rakefile",
25
25
  "VERSION",
26
26
  "lib/trackoid.rb",
27
+ "lib/trackoid/aggregates.rb",
28
+ "lib/trackoid/errors.rb",
27
29
  "lib/trackoid/tracker.rb",
28
30
  "lib/trackoid/tracking.rb",
31
+ "spec/aggregates_spec.rb",
29
32
  "spec/spec.opts",
30
33
  "spec/spec_helper.rb",
31
34
  "spec/trackoid_spec.rb",
@@ -37,7 +40,8 @@ Gem::Specification.new do |s|
37
40
  s.rubygems_version = %q{1.3.7}
38
41
  s.summary = %q{Trackoid is an easy scalable analytics tracker using MongoDB and Mongoid}
39
42
  s.test_files = [
40
- "spec/spec_helper.rb",
43
+ "spec/aggregates_spec.rb",
44
+ "spec/spec_helper.rb",
41
45
  "spec/trackoid_spec.rb"
42
46
  ]
43
47
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trackoid
3
3
  version: !ruby/object:Gem::Version
4
- hash: 27
4
+ hash: 25
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 1
9
- - 0
10
- version: 0.1.0
9
+ - 1
10
+ version: 0.1.1
11
11
  platform: ruby
12
12
  authors:
13
13
  - Jose Miguel Perez
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-05-30 00:00:00 +02:00
18
+ date: 2010-06-04 00:00:00 +02:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -51,8 +51,11 @@ files:
51
51
  - Rakefile
52
52
  - VERSION
53
53
  - lib/trackoid.rb
54
+ - lib/trackoid/aggregates.rb
55
+ - lib/trackoid/errors.rb
54
56
  - lib/trackoid/tracker.rb
55
57
  - lib/trackoid/tracking.rb
58
+ - spec/aggregates_spec.rb
56
59
  - spec/spec.opts
57
60
  - spec/spec_helper.rb
58
61
  - spec/trackoid_spec.rb
@@ -92,5 +95,6 @@ signing_key:
92
95
  specification_version: 3
93
96
  summary: Trackoid is an easy scalable analytics tracker using MongoDB and Mongoid
94
97
  test_files:
98
+ - spec/aggregates_spec.rb
95
99
  - spec/spec_helper.rb
96
100
  - spec/trackoid_spec.rb