trackoid 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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