mongoid-historicals 0.1.0

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/README.md ADDED
@@ -0,0 +1,5 @@
1
+ Mongoid Historicals
2
+ ===================
3
+
4
+ Record a snapshot of current values to reference later as historical values. Useful for showing changes in values over time.
5
+
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'rake/testtask'
2
+
3
+ task :default => :test
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs.push "lib"
7
+ t.pattern = "test/*_test.rb"
8
+ end
@@ -0,0 +1,10 @@
1
+ module Mongoid
2
+ module Historicals
3
+ class Record
4
+ include Mongoid::Document
5
+ include Mongoid::Timestamps::Created
6
+
7
+ field :'_label', type: String
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ module Mongoid
2
+ module Historicals
3
+
4
+ VERSION = "0.1.0"
5
+
6
+ end
7
+ end
@@ -0,0 +1,103 @@
1
+ require 'mongoid'
2
+ require_relative 'historicals/version'
3
+ require_relative 'historicals/record'
4
+
5
+ module Mongoid
6
+ module Historicals
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ embeds_many :historicals,
11
+ class_name: "Mongoid::Historicals::Record"
12
+
13
+ class_attribute :historical_attributes
14
+ class_attribute :historical_options
15
+ end
16
+
17
+ # Save the current values for historicals by label
18
+ #
19
+ def record!(label = nil)
20
+ label ||= Time.now
21
+ record = historical(label) || self.historicals.build(:'_label' => labelize(label))
22
+
23
+ self.class.historical_attributes.each do |attr|
24
+ record[attr] = self.send(attr)
25
+ end
26
+
27
+ record.save!
28
+ destroy_old_historicals!
29
+ record
30
+ end
31
+
32
+ def historical(label)
33
+ self.historicals.where(:'_label' => labelize(label)).first
34
+ end
35
+
36
+ def historical_difference(attr, label, options = {})
37
+ opts = {
38
+ default: 0
39
+ }.merge(options)
40
+
41
+ record = historical(labelize(label))
42
+
43
+ begin
44
+ self[attr] - record[attr]
45
+ rescue # Pokemon exception handling, but actually seems appropriate here
46
+ opts[:default]
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def labelize(label)
53
+ if label.is_a?(String) || label.is_a?(Symbol)
54
+ label.to_s
55
+ elsif label.respond_to?(:to_datetime)
56
+ dt = label.to_datetime
57
+
58
+ case self.class.historical_options[:frequency]
59
+ when :monthly
60
+ dt.strftime("%Y/%m")
61
+ when :weekly
62
+ dt.strftime("%G-%V")
63
+ else # default to `:daily`
64
+ dt.strftime("%Y/%m/%d")
65
+ end
66
+ else
67
+ raise("`label` must be a String, Symbol or respond to :to_datetime")
68
+ end
69
+ end
70
+
71
+ def destroy_old_historicals!
72
+ if self.class.historical_options[:max]
73
+ records = self.historicals.asc(:created_at)
74
+
75
+ if (num_to_delete = records.size - self.class.historical_options[:max]) > 0
76
+ records[0, num_to_delete].each do |r|
77
+ self.historicals.delete(r)
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ module ClassMethods
84
+
85
+ # This model should record historical values for the specified
86
+ # attributes.
87
+ #
88
+ # Options:
89
+ #
90
+ # <tt>max</tt>: The maximum number of entries to store (default: none)
91
+ # <tt>frequency</tt>: :monthly, :weekly, or :daily (default: :daily)
92
+ #
93
+ def historicals(*attrs)
94
+ options = attrs.extract_options!
95
+ options[:max] ||= nil
96
+ options[:frequency] ||= :daily
97
+
98
+ self.historical_options = options
99
+ self.historical_attributes = attrs
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,6 @@
1
+ test:
2
+ sessions:
3
+ default:
4
+ database: mongoid_historicals_test
5
+ hosts:
6
+ - localhost:27017
@@ -0,0 +1,9 @@
1
+ class Player
2
+ include Mongoid::Document
3
+ include Mongoid::Historicals
4
+
5
+ field :name, type: String
6
+ field :score, type: Float
7
+
8
+ historicals :score
9
+ end
@@ -0,0 +1,173 @@
1
+ require_relative 'test_helper'
2
+
3
+ describe Mongoid::Historicals do
4
+ describe ".historical_attributes" do
5
+ it "should include specified attributes" do
6
+ Player.historical_attributes.must_include :score
7
+ end
8
+
9
+ it "should not include other attributes" do
10
+ Player.historical_attributes.wont_include :name
11
+ end
12
+ end
13
+
14
+ describe ".historical_options" do
15
+ it "should have default `max` of nil" do
16
+ Player.historical_options[:max].must_equal nil
17
+ end
18
+
19
+ it "should have a default `frequency` of :daily" do
20
+ Player.historical_options[:frequency].must_equal :daily
21
+ end
22
+ end
23
+
24
+ describe "#record!" do
25
+ before do
26
+ @player = Player.create!(name: "Lytol", score: 95.0)
27
+ @record = @player.record!('test')
28
+ end
29
+
30
+ it "should add a record to historicals" do
31
+ @player.historicals.wont_equal []
32
+ end
33
+
34
+ it "should label with specified label" do
35
+ @record._label.must_equal 'test'
36
+ end
37
+
38
+ it "should timestamp the record" do
39
+ @record.created_at.wont_equal nil
40
+ end
41
+
42
+ it "should have existing values for specified attrbutes in record" do
43
+ @record.score.must_equal 95.0
44
+ end
45
+
46
+ it "should not have values for unspecified attributes in record" do
47
+ @record.wont_respond_to :name
48
+ end
49
+
50
+ describe "when labelled record already exists" do
51
+ it "should overwrite existing record" do
52
+ @player.update_attribute(:score, 90.0)
53
+ record = @player.record!('test')
54
+ @player.historicals.size.must_equal 1
55
+ @player.historicals.must_include record
56
+ end
57
+ end
58
+
59
+ describe "when `max` records is exceeded" do
60
+ before do
61
+ @player.class.historical_options[:max] = 5
62
+ 5.times do |i|
63
+ @player.record!(i.to_s)
64
+ end
65
+ end
66
+
67
+ after do
68
+ @player.class.historical_options[:max] = nil
69
+ end
70
+
71
+ it "should delete the oldest records" do
72
+ oldest_record = @player.historicals.desc(:created_at).last
73
+ @player.record!('test')
74
+ @player.historicals.wont_include oldest_record
75
+ end
76
+ end
77
+
78
+ describe "when labelled with DateTime" do
79
+ before do
80
+ @player.update_attribute(:score, 54.0)
81
+ @player.record!(5.days.ago)
82
+ @player.update_attribute(:score, 65.0)
83
+ end
84
+
85
+ it "should be retrievable with DateTime" do
86
+ record = @player.historical(5.days.ago)
87
+ record.score.must_equal 54.0
88
+ end
89
+ end
90
+ end
91
+
92
+ describe "#historical" do
93
+ before do
94
+ @player = Player.create!(name: "Lytol", score: 95.0)
95
+ @player.record!('test')
96
+ end
97
+
98
+ describe "when record exists" do
99
+ it "should return record" do
100
+ @record = @player.historical('test')
101
+ @record.must_be_instance_of(Mongoid::Historicals::Record)
102
+ @record._label.must_equal 'test'
103
+ end
104
+ end
105
+
106
+ describe "when record does not exist" do
107
+ it "should return nil" do
108
+ @record = @player.historical('unknown-label')
109
+ @record.must_equal nil
110
+ end
111
+ end
112
+ end
113
+
114
+ describe "#historicals" do
115
+ before do
116
+ @player = Player.create!(name: "Lytol", score: 95.0)
117
+ end
118
+
119
+ describe "before any recording" do
120
+ it "should be empty" do
121
+ @player.historicals.must_equal []
122
+ end
123
+ end
124
+ end
125
+
126
+ describe "#historical_difference" do
127
+ before do
128
+ @player = Player.create!(name: "Lytol", score: 90.5)
129
+ @player.record!('test')
130
+ @player.update_attribute(:score, 92.0)
131
+ end
132
+
133
+ it "should return difference from specified label" do
134
+ @player.historical_difference(:score, 'test').must_equal 1.5
135
+ end
136
+
137
+ describe "when there is no historical value" do
138
+ before do
139
+ @player = Player.create!(name: "Lytol", score: nil)
140
+ @player.record!('test')
141
+ @player.update_attribute(:score, 92.0)
142
+ end
143
+
144
+ it "should return 0" do
145
+ @player.historical_difference(:score, 'test').must_equal 0
146
+ end
147
+
148
+ describe "and a default is provided" do
149
+ it "should return specified `default`" do
150
+ @player.historical_difference(:score, 'test', default: 'none').must_equal 'none'
151
+ end
152
+ end
153
+ end
154
+
155
+ describe "when there is no labeled record" do
156
+ before do
157
+ @player = Player.create!(name: "Lytol", score: 90.0)
158
+ @player.record!('test')
159
+ @player.update_attribute(:score, 92.0)
160
+ end
161
+
162
+ it "should return 0" do
163
+ @player.historical_difference(:score, 'invalid').must_equal 0
164
+ end
165
+
166
+ describe "and a default is provided" do
167
+ it "should return specified `default`" do
168
+ @player.historical_difference(:score, 'invalid', default: 'none').must_equal 'none'
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,7 @@
1
+ require 'mongoid'
2
+ require 'minitest/autorun'
3
+
4
+ require_relative '../lib/mongoid/historicals'
5
+ require_relative 'example/player'
6
+
7
+ Mongoid.load!(File.join(File.dirname(__FILE__), "example/mongoid.yml"), :test)
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mongoid-historicals
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Brian Smith
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-17 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: mongoid
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: minitest
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: 2.6.2
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: 2.6.2
62
+ description: ''
63
+ email: bsmith@swig505.com
64
+ executables: []
65
+ extensions: []
66
+ extra_rdoc_files: []
67
+ files:
68
+ - Rakefile
69
+ - lib/mongoid/historicals/record.rb
70
+ - lib/mongoid/historicals/version.rb
71
+ - lib/mongoid/historicals.rb
72
+ - test/example/mongoid.yml
73
+ - test/example/player.rb
74
+ - test/historicals_test.rb
75
+ - test/test_helper.rb
76
+ - README.md
77
+ homepage: http://github.com/Lytol/mongoid-historicals
78
+ licenses: []
79
+ post_install_message:
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ! '>='
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ! '>='
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubyforge_project:
97
+ rubygems_version: 1.8.23
98
+ signing_key:
99
+ specification_version: 3
100
+ summary: ''
101
+ test_files: []