trackoid 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +3 -0
- data/README.rdoc +130 -0
- data/VERSION +1 -1
- data/lib/trackoid/aggregates.rb +6 -2
- data/lib/trackoid/core_ext/range.rb +53 -0
- data/lib/trackoid/core_ext/time.rb +46 -0
- data/lib/trackoid/core_ext.rb +3 -0
- data/lib/trackoid/errors.rb +11 -1
- data/lib/trackoid/reader_extender.rb +64 -0
- data/lib/trackoid/readers.rb +36 -24
- data/lib/trackoid/tracker.rb +44 -22
- data/lib/trackoid/tracker_aggregates.rb +1 -0
- data/lib/trackoid/tracking.rb +5 -2
- data/lib/trackoid.rb +3 -0
- data/spec/aggregates_spec.rb +16 -7
- data/spec/ext/range_spec.rb +110 -0
- data/spec/ext/time_spec.rb +142 -0
- data/spec/reader_extender_spec.rb +34 -0
- data/spec/spec_helper.rb +1 -1
- data/spec/timezone_spec.rb +249 -0
- data/spec/trackoid_spec.rb +23 -13
- data/trackoid.gemspec +15 -2
- metadata +16 -3
data/.rspec
ADDED
data/README.rdoc
CHANGED
@@ -2,6 +2,15 @@
|
|
2
2
|
|
3
3
|
Trackoid is an analytics tracking system made specifically for MongoDB using Mongoid as ORM.
|
4
4
|
|
5
|
+
= *** IMPORTANT ***
|
6
|
+
|
7
|
+
Trackoid Version 0.3.0 changes the internal representation of tracking data. So <b>YOU WILL NOT SEE PREVIOUS DATA</b> when you update.
|
8
|
+
|
9
|
+
Hopefully, due to the magic of MongoDB, data is <b>NOT LOST</b>. In fact it's never lost unless you delete it. :-) Just it's not visible right away.
|
10
|
+
|
11
|
+
See <b>Changes for TZ support</b> below for an explanation of changes. If you absolutely, desperately, dead or alive, need a migration, leave me a message and we can arrange a migration script.
|
12
|
+
|
13
|
+
|
5
14
|
= Requirements
|
6
15
|
|
7
16
|
Trackoid requires Mongoid, which obviously in turn requires MongoDB. Although you can only use Trackoid in Rails projects using Mongoid, it can easily be ported to MongoMapper or other ORM. You can also port it to work directly using MongoDB.
|
@@ -186,8 +195,129 @@ A year full of statistical data takes only 2.8Kb, if you store integers. If your
|
|
186
195
|
For comparison, this README is already 8.5Kb in size...
|
187
196
|
|
188
197
|
|
198
|
+
= Changes for TZ support
|
199
|
+
|
200
|
+
Well, this is the time (no pun intended) to add TZ support to Trackoid.
|
201
|
+
|
202
|
+
The problem is that "today" is not the same "today" for everyone, so unless you live in UTC or don't care about time zones, you probably should stay in 0.2.0 version and live long and prosper...
|
203
|
+
|
204
|
+
But... Okay, given the fact that "today" is not the same "today" for everyone, this is the brand new Trackoid, with TZ support.
|
205
|
+
|
206
|
+
== What has changed?
|
207
|
+
|
208
|
+
In the surface, almost nothing, but internally there has been a major rewrite of the tracking code (the 'inc', 'set' methods) and the readers ('today', 'yesterday', etc). This is due to the changes I've made to the MongoDB structure of the tracking data.
|
209
|
+
|
210
|
+
<b>YOU WILL NEED TO MIGRATE THE EXISTING DATA IF YOU WANT TO KEEP IT</b>
|
211
|
+
|
212
|
+
This is very important, so I will repeat:
|
213
|
+
|
214
|
+
<b>YOU WILL NEED TO MIGRATE THE EXISTING DATA IF YOU WANT TO KEEP IT</b>
|
215
|
+
|
216
|
+
The internal JSON structure of a tracking field was like that.
|
217
|
+
|
218
|
+
{
|
219
|
+
... some other fields in the model...,
|
220
|
+
"tracking_data" : {
|
221
|
+
"2011" : {
|
222
|
+
"01" : {
|
223
|
+
"01" : 10,
|
224
|
+
"02" : 20,
|
225
|
+
"03" : 30,
|
226
|
+
...
|
227
|
+
},
|
228
|
+
"02" : {
|
229
|
+
"01" : 10,
|
230
|
+
"02" : 20,
|
231
|
+
"03" : 30,
|
232
|
+
...
|
233
|
+
}
|
234
|
+
}
|
235
|
+
}
|
236
|
+
}
|
237
|
+
|
238
|
+
That is, years, months and days numbers created a nested hash in which the final data (leaves) was the amount tracked. You see? There was no trace of hours... That's the problem.
|
239
|
+
|
240
|
+
This is the new, TZ aware version of the internal JSON structure:
|
241
|
+
|
242
|
+
{
|
243
|
+
... some other fields in the model...,
|
244
|
+
"tracking_data" : {
|
245
|
+
"14975" : {
|
246
|
+
"00" : 10,
|
247
|
+
"01" : 20,
|
248
|
+
"02" : 30,
|
249
|
+
...
|
250
|
+
"22" : 88,
|
251
|
+
"23" : 99
|
252
|
+
},
|
253
|
+
"14976" : {
|
254
|
+
"00" : 10,
|
255
|
+
"01" : 20,
|
256
|
+
"02" : 30,
|
257
|
+
...
|
258
|
+
"22" : 88,
|
259
|
+
"23" : 99
|
260
|
+
}
|
261
|
+
}
|
262
|
+
}
|
263
|
+
|
264
|
+
So, instead of a nested array with keys like year/month/day, I now use the timestamp of the date. Well, a cooked timestamp. "14975" is the numbers of days since the epoch, which is the number of seconds elapsed since midnight Coordinated Universal Time (UTC) of January 1, 1970, and blah, blah, blah... You know what's this all about (http://en.wikipedia.org/wiki/Unix_time)
|
265
|
+
|
266
|
+
The exact formula is like this (Ruby):
|
267
|
+
|
268
|
+
date_index = Time.now.utc.to_i / 60 / 60 / 24
|
269
|
+
|
270
|
+
The contents of every "day record" is another hash with 24 keys, one for each hour. This MUST be a hash, not an array (which might be more natural) sice Trackoid uses "upserts" operations to be atomic. Reading the array, modifying it and saving it back is not an option. The exact MongoDB operation to support this is as follows:
|
271
|
+
|
272
|
+
db.update(
|
273
|
+
{ search_criteria },
|
274
|
+
{ "$inc" => {"track_data.14976.10" => 1} },
|
275
|
+
{ :upsert => true, :safe => false }
|
276
|
+
)
|
277
|
+
|
278
|
+
== What "today" is it?
|
279
|
+
|
280
|
+
All dates are saved in UTC. That means Trackoid returns a whole 24 hour block for "today" only where the TZ is exactly UTC/GMT (no offset). If you live in a country where there is an offset into UTC, Trackoid must read a whole block and some hours from the block before or after to build "your today".
|
281
|
+
|
282
|
+
Example: I live in GMT+0200 (Daylight saving in effect, or summer time), then if I request data for "today" as of 2011-04-14, Trackoid must first read the block for 15078 (UTC index for 2011-04-14), shift up 2 hours and then fill the missing 2 hours from the day before (15078). The entire block will be like this:
|
283
|
+
|
284
|
+
"tracking_data" : {
|
285
|
+
"15078" : {
|
286
|
+
"22" : 88, # Last two hours from 2011-04-13 UTC
|
287
|
+
"23" : 99
|
288
|
+
},
|
289
|
+
"15079" : {
|
290
|
+
"00" : 10,
|
291
|
+
"01" : 20,
|
292
|
+
"02" : 30,
|
293
|
+
""
|
294
|
+
}
|
295
|
+
|
296
|
+
This is a more graphical representation:
|
297
|
+
|
298
|
+
Hours 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
299
|
+
------ -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
|
300
|
+
GMT+2: 00 00 00 XX 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
|
301
|
+
UTC: ---> 00 XX 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
|
302
|
+
Shift up ---> 2 hours.
|
303
|
+
|
304
|
+
|
305
|
+
For timezones with a negative offset from UTC (Like PDT/PST) the process is reversed: UTC values are shifted down and holes filled with the following day.
|
306
|
+
|
307
|
+
|
308
|
+
== How should I tell Trackoid how TZ to use?
|
309
|
+
|
310
|
+
Piece of cake: Use the reader methods "today", "yesterday", "last_days(N)" and Trackoid will use the effective Time Zone of your Rails/Ruby application.
|
311
|
+
|
312
|
+
Trackoid will correctly translate dates for you (hopefully) if you pass a date to any of those methods.
|
313
|
+
|
314
|
+
|
315
|
+
|
189
316
|
= Revision History
|
190
317
|
|
318
|
+
0.3.0 - Biggest change ever. Read <b>Changes for TZ support</b> above.
|
319
|
+
<b>YOU WILL NEED A MIGRATION FOR EXISTING DATA</b>
|
320
|
+
|
191
321
|
0.2.0 - Added 'reset' and 'erase' methods to tracker fields:
|
192
322
|
* Reset does the same as "set" but also sets aggregate fields.
|
193
323
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.3.0
|
data/lib/trackoid/aggregates.rb
CHANGED
@@ -2,6 +2,9 @@
|
|
2
2
|
module Mongoid #:nodoc:
|
3
3
|
module Tracking #:nodoc:
|
4
4
|
module Aggregates
|
5
|
+
|
6
|
+
DEPRECATED_TOKENS = ['hour', 'hours']
|
7
|
+
|
5
8
|
# This module includes aggregate data extensions to Trackoid instances
|
6
9
|
def self.included(base)
|
7
10
|
base.class_eval do
|
@@ -55,8 +58,8 @@ module Mongoid #:nodoc:
|
|
55
58
|
# But you are encouraged to use Trackoid methods whenever possible.
|
56
59
|
#
|
57
60
|
def aggregate(name, &block)
|
58
|
-
raise Errors::AggregationAlreadyDefined.new(self.name, name) if
|
59
|
-
|
61
|
+
raise Errors::AggregationAlreadyDefined.new(self.name, name) if aggregate_fields.has_key? name
|
62
|
+
raise Errors::AggregationNameDeprecated.new(name) if DEPRECATED_TOKENS.include? name.to_s
|
60
63
|
|
61
64
|
define_aggregate_model if aggregate_klass.nil?
|
62
65
|
has_many_related internal_accessor_name(name), :class_name => aggregate_klass.to_s
|
@@ -83,6 +86,7 @@ module Mongoid #:nodoc:
|
|
83
86
|
# Defines the aggregation model. It checks for class name conflicts
|
84
87
|
def define_aggregate_model
|
85
88
|
raise Errors::ClassAlreadyDefined.new(internal_aggregates_name) if foreign_class_defined?
|
89
|
+
|
86
90
|
parent = self
|
87
91
|
define_klass do
|
88
92
|
include Mongoid::Document
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
class Range
|
3
|
+
# Adds some enumerable capabilities to Time ranges
|
4
|
+
# (Normally they raise a "Can't iterate time ranges")
|
5
|
+
#
|
6
|
+
# It works by assuming days while iterating the time range, but you can
|
7
|
+
# pass an optional parameter
|
8
|
+
|
9
|
+
HOURS = 3600
|
10
|
+
DAYS = 24*HOURS
|
11
|
+
DEFAULT_TIME_GRANULARITY = DAYS
|
12
|
+
|
13
|
+
# Map / Collect over a Time range.
|
14
|
+
# A better implementation would be redefining 'succ' on Time. However,
|
15
|
+
# the ruby source code (At least 1.9.2-p0) hardcodes a check for Type,
|
16
|
+
# so it would not work even if we provide our own 'succ' for Time.
|
17
|
+
def collect(step = DEFAULT_TIME_GRANULARITY)
|
18
|
+
return super() unless first.is_a?(Time)
|
19
|
+
|
20
|
+
return collect(step) {|c| c} unless block_given?
|
21
|
+
|
22
|
+
# Pretty much a standard implementation of Map/Collect here
|
23
|
+
ary, current, op = [], first, (exclude_end? ? :< : :<=)
|
24
|
+
while current.send(op, last)
|
25
|
+
ary << yield(current)
|
26
|
+
current = current + step
|
27
|
+
end
|
28
|
+
ary
|
29
|
+
end
|
30
|
+
alias :map :collect
|
31
|
+
|
32
|
+
# Diff returns the number of elements in the Range, much like 'count'.
|
33
|
+
# Again, redefining 'succ' would be a better idea (see above).
|
34
|
+
# However, I think redefining 'succ' makes this O(n) while this is O(1)
|
35
|
+
def diff(granularity = DEFAULT_TIME_GRANULARITY)
|
36
|
+
if first.is_a?(Time)
|
37
|
+
@diff ||= (last - first) / granularity + (exclude_end? ? 0 : 1)
|
38
|
+
@diff.to_i
|
39
|
+
else
|
40
|
+
@diff ||= count
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Helper methods for non default parameters
|
45
|
+
def hour_diff
|
46
|
+
diff(HOURS)
|
47
|
+
end
|
48
|
+
|
49
|
+
def hour_collect(&block)
|
50
|
+
collect(HOURS, &block)
|
51
|
+
end
|
52
|
+
alias :hour_map :hour_collect
|
53
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
class Time
|
3
|
+
# Functions to construct the MongoDB field key for trackers
|
4
|
+
#
|
5
|
+
# to_i_timestamp returns the computed UTC timestamp regardless of the
|
6
|
+
# timezone.
|
7
|
+
#
|
8
|
+
# Examples:
|
9
|
+
# 2011-01-01 00:00:00 UTC ===> 14975
|
10
|
+
# 2011-01-01 23:59:59 UTC ===> 14975
|
11
|
+
# 2011-01-02 00:00:00 UTC ===> 14976
|
12
|
+
#
|
13
|
+
# to_i_hour returns the hour for the date, again regardless of TZ
|
14
|
+
#
|
15
|
+
# 2011-01-01 00:00:00 UTC ===> 0
|
16
|
+
# 2011-01-01 23:59:59 UTC ===> 23
|
17
|
+
#
|
18
|
+
ONEHOUR = 60 * 60
|
19
|
+
ONEDAY = 24 * ONEHOUR
|
20
|
+
|
21
|
+
def to_i_timestamp
|
22
|
+
self.dup.utc.to_i / ONEDAY
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_i_hour
|
26
|
+
self.dup.utc.hour
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns an integer to use as MongoDB key
|
30
|
+
def to_key
|
31
|
+
"#{to_i_timestamp}.#{to_i_hour}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.from_key(ts, h)
|
35
|
+
Time.at(ts.to_i * ONEDAY + h.to_i * ONEHOUR)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns a range to be enumerated using hours for the whole day
|
39
|
+
def whole_day
|
40
|
+
# We could have used 'beginning_of_day' from ActiveSupport, but don't
|
41
|
+
# want to introduce a dependency (I've tried to avoid ActiveSupport
|
42
|
+
# although you will be using it since it's introduced by Mongoid)
|
43
|
+
midnight = Time.send(utc? ? :utc : :local, year, month, day)
|
44
|
+
midnight...(midnight + ::Range::DAYS)
|
45
|
+
end
|
46
|
+
end
|
data/lib/trackoid/errors.rb
CHANGED
@@ -13,10 +13,20 @@ module Mongoid #:nodoc
|
|
13
13
|
|
14
14
|
class AggregationAlreadyDefined < RuntimeError
|
15
15
|
def initialize(klass, token)
|
16
|
+
@klass = klass
|
17
|
+
@token = token
|
18
|
+
end
|
19
|
+
def message
|
20
|
+
"Aggregation '#{@token}' already defined for model #{@klass}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class AggregationNameDeprecated < RuntimeError
|
25
|
+
def initialize(token)
|
16
26
|
@token = token
|
17
27
|
end
|
18
28
|
def message
|
19
|
-
"
|
29
|
+
"Ussing aggregation name '#{@klass}' is deprecated. Please select another name."
|
20
30
|
end
|
21
31
|
end
|
22
32
|
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Mongoid #:nodoc:
|
3
|
+
module Tracking
|
4
|
+
|
5
|
+
# ReaderExtender is used in cases where we need to return an integer
|
6
|
+
# (class Numeric) while extending their contents. It would allow to
|
7
|
+
# perform advanced calculations in some situations:
|
8
|
+
#
|
9
|
+
# Example:
|
10
|
+
#
|
11
|
+
# a = visits.today # Would return a number, but "extended" so that
|
12
|
+
# # we can make a.hourly to get a detailed, hourly
|
13
|
+
# # array of the visits.
|
14
|
+
#
|
15
|
+
# b = visits.yesterday
|
16
|
+
# c = a + b # Here, in c, normally we would have a FixNum with
|
17
|
+
# # the sum of a plus b, but if we extend the sum
|
18
|
+
# # operation, we can additionaly sum the hourly
|
19
|
+
# # array and return a new ReaderExtender c.
|
20
|
+
#
|
21
|
+
class ReaderExtender
|
22
|
+
def initialize(number, hours)
|
23
|
+
@number = number
|
24
|
+
@hours = hours
|
25
|
+
end
|
26
|
+
|
27
|
+
def hourly
|
28
|
+
@hours
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
@number.to_s
|
33
|
+
end
|
34
|
+
|
35
|
+
def ==(other)
|
36
|
+
@number == other
|
37
|
+
end
|
38
|
+
|
39
|
+
def <=>(other)
|
40
|
+
@number <=> other
|
41
|
+
end
|
42
|
+
|
43
|
+
def +(other)
|
44
|
+
return @number + other unless other.is_a?(ReaderExtender)
|
45
|
+
|
46
|
+
@number = @number + other
|
47
|
+
@hours = @hours.zip(other.hourly).map!(&:sum)
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
# Solution proposed by Yehuda Katz in the following Stack Overflow:
|
52
|
+
# http://stackoverflow.com/questions/1095789/sub-classing-fixnum-in-ruby
|
53
|
+
#
|
54
|
+
# Basically we override our methods while proxying all missing methods
|
55
|
+
# to the underliying FixNum
|
56
|
+
#
|
57
|
+
def method_missing(name, *args, &blk)
|
58
|
+
ret = @number.send(name, *args, &blk)
|
59
|
+
ret.is_a?(Numeric) ? ReaderExtender.new(ret, @hours) : ret
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
data/lib/trackoid/readers.rb
CHANGED
@@ -6,11 +6,11 @@ module Mongoid #:nodoc:
|
|
6
6
|
|
7
7
|
# Access methods
|
8
8
|
def today
|
9
|
-
|
9
|
+
whole_data_for(Time.now)
|
10
10
|
end
|
11
11
|
|
12
12
|
def yesterday
|
13
|
-
|
13
|
+
whole_data_for(Time.now - 1.day)
|
14
14
|
end
|
15
15
|
|
16
16
|
def first_value
|
@@ -20,17 +20,16 @@ module Mongoid #:nodoc:
|
|
20
20
|
def last_value
|
21
21
|
data_for(last_date)
|
22
22
|
end
|
23
|
-
|
23
|
+
|
24
24
|
def last_days(how_much = 7)
|
25
25
|
return [today] unless how_much > 0
|
26
|
-
|
27
|
-
(
|
28
|
-
values
|
26
|
+
now, hmd = Time.now, (how_much - 1)
|
27
|
+
on( now.ago(hmd.days)..now )
|
29
28
|
end
|
30
29
|
|
31
30
|
def on(date)
|
32
|
-
return date.collect {|d|
|
33
|
-
|
31
|
+
return date.collect {|d| whole_data_for(d)} if date.is_a?(Range)
|
32
|
+
whole_data_for(date)
|
34
33
|
end
|
35
34
|
|
36
35
|
def all_values
|
@@ -39,25 +38,38 @@ module Mongoid #:nodoc:
|
|
39
38
|
|
40
39
|
# Utility methods
|
41
40
|
def first_date
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
Date.new(_y.to_i, _m.to_i, _d.to_i)
|
49
|
-
end
|
50
|
-
|
41
|
+
date_cleanup
|
42
|
+
return nil unless _ts = @data.keys.min
|
43
|
+
return nil unless _h = @data[_ts].keys.min
|
44
|
+
Time.from_key(_ts, _h)
|
45
|
+
end
|
46
|
+
|
51
47
|
def last_date
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
return nil unless _m = @data[_y].keys.max
|
57
|
-
return nil unless _d = @data[_y][_m].keys.max
|
58
|
-
Date.new(_y.to_i, _m.to_i, _d.to_i)
|
48
|
+
date_cleanup
|
49
|
+
return nil unless _ts = @data.keys.max
|
50
|
+
return nil unless _h = @data[_ts].keys.max
|
51
|
+
Time.from_key(_ts, _h)
|
59
52
|
end
|
60
53
|
|
54
|
+
# We need the cleanup method only for methods who rely on date indexes
|
55
|
+
# to be valid (well formed) like first/last_date. This is because
|
56
|
+
# Mongo update operations cleans up the last key, which in our case
|
57
|
+
# left the array in an inconsistent state.
|
58
|
+
#
|
59
|
+
# Example:
|
60
|
+
# Before update:
|
61
|
+
#
|
62
|
+
# { :visits_data => {"14803" => {"22" => 1} } }
|
63
|
+
#
|
64
|
+
# After updating with: {"$unset"=>{"visits_data.14803.22"=>1}
|
65
|
+
#
|
66
|
+
# { :visits_data => {"14803" => {} } }
|
67
|
+
#
|
68
|
+
# We can NOT retrieve the first date with visits_data.keys.min
|
69
|
+
#
|
70
|
+
def date_cleanup
|
71
|
+
@data.reject! {|k,v| v.count == 0}
|
72
|
+
end
|
61
73
|
end
|
62
74
|
end
|
63
75
|
end
|
data/lib/trackoid/tracker.rb
CHANGED
@@ -34,7 +34,7 @@ module Mongoid #:nodoc:
|
|
34
34
|
end
|
35
35
|
|
36
36
|
# Update methods
|
37
|
-
def add(how_much = 1, date =
|
37
|
+
def add(how_much = 1, date = Time.now)
|
38
38
|
raise Errors::ModelNotSaved, "Can't update a new record. Save first!" if @owner.new_record?
|
39
39
|
return if how_much == 0
|
40
40
|
|
@@ -64,15 +64,15 @@ module Mongoid #:nodoc:
|
|
64
64
|
end
|
65
65
|
end
|
66
66
|
|
67
|
-
def inc(date =
|
67
|
+
def inc(date = Time.now)
|
68
68
|
add(1, date)
|
69
69
|
end
|
70
70
|
|
71
|
-
def dec(date =
|
71
|
+
def dec(date = Time.now)
|
72
72
|
add(-1, date)
|
73
73
|
end
|
74
74
|
|
75
|
-
def set(how_much, date =
|
75
|
+
def set(how_much, date = Time.now)
|
76
76
|
raise Errors::ModelNotSaved, "Can't update a new record" if @owner.new_record?
|
77
77
|
update_data(how_much, date)
|
78
78
|
@owner.collection.update(
|
@@ -92,7 +92,7 @@ module Mongoid #:nodoc:
|
|
92
92
|
end
|
93
93
|
end
|
94
94
|
|
95
|
-
def reset(how_much, date =
|
95
|
+
def reset(how_much, date = Time.now)
|
96
96
|
return erase(date) if how_much.nil?
|
97
97
|
|
98
98
|
# First, we use the default "set" for the tracking field
|
@@ -111,11 +111,10 @@ module Mongoid #:nodoc:
|
|
111
111
|
end
|
112
112
|
end
|
113
113
|
|
114
|
-
def erase(date =
|
114
|
+
def erase(date = Time.now)
|
115
115
|
raise Errors::ModelNotSaved, "Can't update a new record" if @owner.new_record?
|
116
116
|
|
117
|
-
|
118
|
-
update_data(nil, date)
|
117
|
+
remove_data(date)
|
119
118
|
@owner.collection.update(
|
120
119
|
@owner._selector,
|
121
120
|
{ "$unset" => update_hash(1, date) },
|
@@ -137,35 +136,58 @@ module Mongoid #:nodoc:
|
|
137
136
|
|
138
137
|
private
|
139
138
|
def data_for(date)
|
140
|
-
|
141
|
-
|
142
|
-
|
139
|
+
unless date.nil?
|
140
|
+
date = normalize_date(date)
|
141
|
+
@data.try(:[], date.to_i_timestamp.to_s).try(:[], date.to_i_hour.to_s) || 0
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def whole_data_for(date)
|
146
|
+
unless date.nil?
|
147
|
+
date = normalize_date(date)
|
148
|
+
h = date.whole_day.hour_collect {|d| data_for(d)}
|
149
|
+
ReaderExtender.new(h.sum, h)
|
150
|
+
end
|
143
151
|
end
|
144
152
|
|
145
153
|
def update_data(value, date)
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
154
|
+
unless date.nil?
|
155
|
+
return remove_data(date) unless value
|
156
|
+
date = normalize_date(date)
|
157
|
+
dk, hk = date.to_i_timestamp.to_s, date.to_i_hour.to_s
|
158
|
+
unless ts = @data[dk]
|
159
|
+
ts = (@data[dk] = {})
|
160
|
+
end
|
161
|
+
ts[hk] = value
|
162
|
+
end
|
152
163
|
end
|
153
164
|
|
154
|
-
def
|
155
|
-
|
156
|
-
|
165
|
+
def remove_data(date)
|
166
|
+
unless date.nil?
|
167
|
+
date = normalize_date(date)
|
168
|
+
dk, hk = date.to_i_timestamp.to_s, date.to_i_hour.to_s
|
169
|
+
if ts = @data[dk]
|
170
|
+
ts.delete(hk)
|
171
|
+
unless ts.count > 0
|
172
|
+
@data.delete(dk)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
157
177
|
|
158
178
|
def update_hash(num, date)
|
159
179
|
date = normalize_date(date)
|
160
180
|
{
|
161
|
-
"#{@for_data}.#{
|
181
|
+
"#{@for_data}.#{date.to_key}" => num
|
162
182
|
}
|
163
183
|
end
|
164
184
|
|
165
185
|
def normalize_date(date)
|
166
186
|
case date
|
167
187
|
when String
|
168
|
-
|
188
|
+
Time.parse(date)
|
189
|
+
when Date
|
190
|
+
date.to_time
|
169
191
|
else
|
170
192
|
date
|
171
193
|
end
|
data/lib/trackoid/tracking.rb
CHANGED
@@ -40,8 +40,11 @@ module Mongoid #:nodoc:
|
|
40
40
|
# an index for the internal tracking field.
|
41
41
|
def set_tracking_field(name)
|
42
42
|
field internal_track_name(name), :type => Hash # , :default => {}
|
43
|
-
|
44
|
-
index
|
43
|
+
|
44
|
+
# DONT make an index for this field. MongoDB indexes have limited
|
45
|
+
# size and seems that this is not a good target for indexing.
|
46
|
+
# index internal_track_name(name)
|
47
|
+
|
45
48
|
tracked_fields << name
|
46
49
|
end
|
47
50
|
|
data/lib/trackoid.rb
CHANGED
data/spec/aggregates_spec.rb
CHANGED
@@ -84,6 +84,16 @@ describe Mongoid::Tracking::Aggregates do
|
|
84
84
|
}.should raise_error Mongoid::Errors::AggregationAlreadyDefined
|
85
85
|
end
|
86
86
|
|
87
|
+
it "should raise error if we try to use 'hours' as aggregate" do
|
88
|
+
lambda {
|
89
|
+
class TestModel
|
90
|
+
aggregate :hours do
|
91
|
+
"(none)"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
}.should raise_error Mongoid::Errors::AggregationNameDeprecated
|
95
|
+
end
|
96
|
+
|
87
97
|
it "should have Mongoid accessors defined" do
|
88
98
|
tm = TestModel.create(:name => "Dummy")
|
89
99
|
tm.send(tm.class.send(:internal_accessor_name, "browsers")).class.should == Mongoid::Criteria
|
@@ -311,7 +321,6 @@ describe Mongoid::Tracking::Aggregates do
|
|
311
321
|
|
312
322
|
it "should delete the values when using nil" do
|
313
323
|
@mock.visits.reset(nil, "2010-07-14")
|
314
|
-
|
315
324
|
@mock.visits.on("2010-07-14").should == 0
|
316
325
|
@mock.visits.browsers.all_values.should =~ [
|
317
326
|
["mozilla", [1]],
|
@@ -401,17 +410,17 @@ describe Mongoid::Tracking::Aggregates do
|
|
401
410
|
|
402
411
|
it "should return the correct first_date for every aggregate" do
|
403
412
|
@mock.visits.browsers.first_date.should =~ [
|
404
|
-
["mozilla",
|
405
|
-
["google",
|
406
|
-
["internet",
|
413
|
+
["mozilla", Time.parse("2010-07-11")],
|
414
|
+
["google", Time.parse("2010-07-12")],
|
415
|
+
["internet", Time.parse("2010-07-13")]
|
407
416
|
]
|
408
417
|
end
|
409
418
|
|
410
419
|
it "should return the correct last_date for every aggregate" do
|
411
420
|
@mock.visits.browsers.last_date.should =~ [
|
412
|
-
["mozilla",
|
413
|
-
["google",
|
414
|
-
["internet",
|
421
|
+
["mozilla", Time.parse("2010-07-14")],
|
422
|
+
["google", Time.parse("2010-07-15")],
|
423
|
+
["internet", Time.parse("2010-07-16")]
|
415
424
|
]
|
416
425
|
end
|
417
426
|
|