activr 1.0.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.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +764 -0
- data/Rakefile +50 -0
- data/lib/activr.rb +206 -0
- data/lib/activr/activity.rb +446 -0
- data/lib/activr/async.rb +92 -0
- data/lib/activr/async/resque.rb +58 -0
- data/lib/activr/configuration.rb +40 -0
- data/lib/activr/dispatcher.rb +70 -0
- data/lib/activr/entity.rb +103 -0
- data/lib/activr/entity/model_mixin.rb +117 -0
- data/lib/activr/rails_ctx.rb +37 -0
- data/lib/activr/railtie.rb +73 -0
- data/lib/activr/railties/activr.rake +14 -0
- data/lib/activr/registry.rb +268 -0
- data/lib/activr/storage.rb +404 -0
- data/lib/activr/storage/mongo_driver.rb +645 -0
- data/lib/activr/timeline.rb +441 -0
- data/lib/activr/timeline/entry.rb +165 -0
- data/lib/activr/timeline/route.rb +161 -0
- data/lib/activr/utils.rb +74 -0
- data/lib/activr/version.rb +6 -0
- data/lib/generators/activr/activity_generator.rb +91 -0
- data/lib/generators/activr/templates/activity.rb +28 -0
- data/lib/generators/activr/templates/timeline.rb +42 -0
- data/lib/generators/activr/timeline_generator.rb +24 -0
- metadata +197 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
module Activr
|
2
|
+
|
3
|
+
#
|
4
|
+
# Rails Context holder
|
5
|
+
#
|
6
|
+
class RailsCtx
|
7
|
+
|
8
|
+
class_attribute :controller
|
9
|
+
self.controller = nil
|
10
|
+
|
11
|
+
class << self
|
12
|
+
|
13
|
+
# Get current Rails View context
|
14
|
+
#
|
15
|
+
# @return [ActionView::Base] Rails view instance
|
16
|
+
def view_context
|
17
|
+
@view_context ||= if defined?(::Rails)
|
18
|
+
rails_controller = self.controller || begin
|
19
|
+
fake_controller = ApplicationController.new
|
20
|
+
fake_controller.request = ActionController::TestRequest.new if defined?(ActionController::TestRequest)
|
21
|
+
fake_controller
|
22
|
+
end
|
23
|
+
|
24
|
+
rails_controller.view_context
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Clear memoization of current Rails view context
|
29
|
+
def clear_view_context!
|
30
|
+
@view_context = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
end # class << self
|
34
|
+
|
35
|
+
end # class RailsCtx
|
36
|
+
|
37
|
+
end # module Activr
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Activr
|
2
|
+
|
3
|
+
# Hook into Rails
|
4
|
+
class Railtie < ::Rails::Railtie
|
5
|
+
initializer "activr.set_conf", :after => 'mongoid.load-config' do |app|
|
6
|
+
Activr.configure do |config|
|
7
|
+
# setup app path
|
8
|
+
activr_dir = File.join(::Rails.root, 'app', 'activr')
|
9
|
+
if !File.exists?(activr_dir)
|
10
|
+
activr_dir = File.join(::Rails.root, 'app')
|
11
|
+
end
|
12
|
+
|
13
|
+
config.app_path = activr_dir
|
14
|
+
|
15
|
+
use_mongoid_conn = Fwissr['/activr/mongodb/uri'].blank? &&
|
16
|
+
(Fwissr['/activr/skip_mongoid_railtie'] != true) &&
|
17
|
+
(ENV['ACTIVR_SKIP_MONGOID_RAILTIE'] != 'true') &&
|
18
|
+
defined?(Mongoid)
|
19
|
+
|
20
|
+
if use_mongoid_conn
|
21
|
+
# get mongoid conf
|
22
|
+
if Mongoid::VERSION.start_with?("2.")
|
23
|
+
# Mongoid 2
|
24
|
+
config.mongodb[:uri] = "mongodb://#{Mongoid.master.connection.host}:#{Mongoid.master.connection.port}/#{Mongoid.master.name}"
|
25
|
+
elsif Mongoid.sessions[:default] && !Mongoid.sessions[:default][:uri].blank?
|
26
|
+
# Mongoid >= 3 with :uri setting
|
27
|
+
config.mongodb[:uri] = Mongoid.sessions[:default][:uri].dup
|
28
|
+
elsif Mongoid.sessions[:default] && !Mongoid.sessions[:default][:database].blank? && !Mongoid.sessions[:default][:hosts].blank?
|
29
|
+
# Mongoid >= 3 without :uri setting
|
30
|
+
config.mongodb[:uri] = "mongodb://#{Mongoid.sessions[:default][:hosts].first}/#{Mongoid.sessions[:default][:database]}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
initializer 'activr.autoload', :after => "activr.set_conf", :before => :set_autoload_paths do |app|
|
37
|
+
app.config.autoload_paths += [
|
38
|
+
File.join(Activr.config.app_path, 'activities'),
|
39
|
+
File.join(Activr.config.app_path, 'timelines'),
|
40
|
+
]
|
41
|
+
end
|
42
|
+
|
43
|
+
initializer "activr.setup_async_hooks" do |app|
|
44
|
+
if defined?(::Resque) && (ENV['ACTIVR_FORCE_SYNC'] != 'true')
|
45
|
+
Activr.configure do |config|
|
46
|
+
config.async[:route_activity] ||= Activr::Async::Resque::RouteActivity
|
47
|
+
config.async[:timeline_handle] ||= Activr::Async::Resque::TimelineHandle
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
initializer "activr.setup_action_controller" do |app|
|
53
|
+
ActiveSupport.on_load :action_controller do
|
54
|
+
self.class_eval do
|
55
|
+
before_filter do |controller|
|
56
|
+
Activr::RailsCtx.clear_view_context!
|
57
|
+
Activr::RailsCtx.controller = controller
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
rake_tasks do
|
64
|
+
load "activr/railties/activr.rake"
|
65
|
+
end
|
66
|
+
|
67
|
+
config.after_initialize do |app|
|
68
|
+
# setup registry
|
69
|
+
Activr.setup
|
70
|
+
end
|
71
|
+
end # class Railtie
|
72
|
+
|
73
|
+
end # module Activr
|
@@ -0,0 +1,14 @@
|
|
1
|
+
namespace :activr do
|
2
|
+
|
3
|
+
desc "Create the indexes"
|
4
|
+
task :create_indexes => :environment do
|
5
|
+
::Rails.application.eager_load!
|
6
|
+
|
7
|
+
Activr.setup
|
8
|
+
|
9
|
+
Activr.storage.create_indexes do |index_name|
|
10
|
+
puts "Created index: #{index_name}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
@@ -0,0 +1,268 @@
|
|
1
|
+
module Activr
|
2
|
+
|
3
|
+
#
|
4
|
+
# The registry holds all activities, entities, timelines and timeline entries classes defined in the application
|
5
|
+
#
|
6
|
+
# The registry singleton is accessible with {Activr.registry}
|
7
|
+
#
|
8
|
+
class Registry
|
9
|
+
|
10
|
+
# @return [Hash{Symbol=>Class}] model class associated to entity name
|
11
|
+
attr_reader :entity_classes
|
12
|
+
|
13
|
+
# @return [Hash{Class=>Array<Symbol>}] entity names for activity class
|
14
|
+
attr_reader :activity_entities
|
15
|
+
|
16
|
+
# Init
|
17
|
+
def initialize
|
18
|
+
@setup = false
|
19
|
+
|
20
|
+
@timelines = nil
|
21
|
+
@timeline_entries = nil
|
22
|
+
@activities = nil
|
23
|
+
@entities = nil
|
24
|
+
@models = nil
|
25
|
+
|
26
|
+
@entity_classes = { }
|
27
|
+
@activity_entities = { }
|
28
|
+
|
29
|
+
@timeline_entities_for_model = { }
|
30
|
+
@activity_entities_for_model = { }
|
31
|
+
end
|
32
|
+
|
33
|
+
# Setup registry
|
34
|
+
def setup
|
35
|
+
return if @setup
|
36
|
+
|
37
|
+
# eagger load all classes
|
38
|
+
self.activities
|
39
|
+
self.timelines
|
40
|
+
self.timeline_entries
|
41
|
+
|
42
|
+
@setup = true
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
#
|
47
|
+
# Classes
|
48
|
+
#
|
49
|
+
|
50
|
+
# Get all registered timelines
|
51
|
+
#
|
52
|
+
# @return [Hash{String=>Class}] A hash of `<timeline kind> => <timeline class>`
|
53
|
+
def timelines
|
54
|
+
@timelines ||= self.classes_from_path(Activr.timelines_path)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Get class for given timeline kind
|
58
|
+
#
|
59
|
+
# @param timeline_kind [String] Timeline kind
|
60
|
+
# @return [Class] Timeline class
|
61
|
+
def class_for_timeline(timeline_kind)
|
62
|
+
result = self.timelines[timeline_kind]
|
63
|
+
raise "No class defined for timeline kind: #{timeline_kind}" if result.blank?
|
64
|
+
result
|
65
|
+
end
|
66
|
+
|
67
|
+
# Get all registered timeline entries
|
68
|
+
#
|
69
|
+
# @return [Hash{String=>Hash{String=>Class}}] A hash of `<timeline kind> => { <route kind> => <timeline entry class>, ... }`
|
70
|
+
def timeline_entries
|
71
|
+
@timeline_entries ||= begin
|
72
|
+
result = { }
|
73
|
+
|
74
|
+
self.timelines.each do |(timeline_kind, timeline_class)|
|
75
|
+
dir_name = Activr::Utils.kind_for_class(timeline_class)
|
76
|
+
dir_path = File.join(Activr.timelines_path, dir_name)
|
77
|
+
|
78
|
+
if !File.directory?(dir_path)
|
79
|
+
dir_name = Activr::Utils.kind_for_class(timeline_class, 'timeline')
|
80
|
+
dir_path = File.join(Activr.timelines_path, dir_name)
|
81
|
+
end
|
82
|
+
|
83
|
+
if File.directory?(dir_path)
|
84
|
+
result[timeline_kind] = { }
|
85
|
+
|
86
|
+
Dir["#{dir_path}/*.rb"].sort.inject(result[timeline_kind]) do |memo, file_path|
|
87
|
+
base_name = File.basename(file_path, '.rb')
|
88
|
+
|
89
|
+
# skip base class
|
90
|
+
if (base_name != "base_timeline_entry")
|
91
|
+
klass = "#{timeline_class.name}::#{base_name.camelize}".constantize
|
92
|
+
|
93
|
+
route_kind = if (match_data = base_name.match(/(.+)_timeline_entry$/))
|
94
|
+
match_data[1]
|
95
|
+
else
|
96
|
+
base_name
|
97
|
+
end
|
98
|
+
|
99
|
+
route = timeline_class.routes.find do |timeline_route|
|
100
|
+
timeline_route.kind == route_kind
|
101
|
+
end
|
102
|
+
|
103
|
+
raise "Timeline entry class found for an unspecified timeline route: #{file_path} / routes: #{timeline_class.routes.inspect}" unless route
|
104
|
+
memo[route_kind] = klass
|
105
|
+
end
|
106
|
+
|
107
|
+
memo
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
result
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Get class for timeline entry corresponding to given route in given timeline
|
117
|
+
#
|
118
|
+
# @param timeline_kind [String] Timeline kind
|
119
|
+
# @param route_kind [String] Route kind
|
120
|
+
# @return [Class] Timeline entry class
|
121
|
+
def class_for_timeline_entry(timeline_kind, route_kind)
|
122
|
+
(self.timeline_entries[timeline_kind] && self.timeline_entries[timeline_kind][route_kind]) || Activr::Timeline::Entry
|
123
|
+
end
|
124
|
+
|
125
|
+
# Get all registered activities
|
126
|
+
#
|
127
|
+
# @return [Hash{String=>Class}] A hash of `<activity kind> => <activity class>`
|
128
|
+
def activities
|
129
|
+
@activities ||= self.classes_from_path(Activr.activities_path)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Get class for given activity
|
133
|
+
#
|
134
|
+
# @param activity_kind [String] Activity kind
|
135
|
+
# @return [Class] Activity class
|
136
|
+
def class_for_activity(activity_kind)
|
137
|
+
result = self.activities[activity_kind]
|
138
|
+
raise "No class defined for activity kind: #{activity_kind}" if result.blank?
|
139
|
+
result
|
140
|
+
end
|
141
|
+
|
142
|
+
# Get all registered entities
|
143
|
+
#
|
144
|
+
# @return [Hash{Symbol=>Array<Class>}] A hash of `:<entity name> => [ <activity class>, <activity class>, ... ]`
|
145
|
+
def entities
|
146
|
+
# loading activities triggers calls to #add_entity method
|
147
|
+
self.activities if @entities.blank?
|
148
|
+
|
149
|
+
@entities || { }
|
150
|
+
end
|
151
|
+
|
152
|
+
# Get all registered entities names
|
153
|
+
#
|
154
|
+
# @return [Array<Symbol>] List of entities names
|
155
|
+
def entities_names
|
156
|
+
@entities_names ||= self.entities.keys
|
157
|
+
end
|
158
|
+
|
159
|
+
# Register an entity
|
160
|
+
#
|
161
|
+
# @param entity_name [Symbol] Entity name
|
162
|
+
# @param entity_options [Hash] Entity options
|
163
|
+
# @param activity_klass [Class] Activity class that uses that entity
|
164
|
+
def add_entity(entity_name, entity_options, activity_klass)
|
165
|
+
entity_name = entity_name.to_sym
|
166
|
+
|
167
|
+
if @entity_classes[entity_name] && (@entity_classes[entity_name].name != entity_options[:class].name)
|
168
|
+
# otherwise this would break timeline entries deletion mecanism
|
169
|
+
raise "Entity name #{entity_name} already used with class #{@entity_classes[entity_name]}, can't redefine it with class #{entity_options[:class]}"
|
170
|
+
end
|
171
|
+
|
172
|
+
# class for entity
|
173
|
+
@entity_classes[entity_name] = entity_options[:class]
|
174
|
+
|
175
|
+
# entities for activity
|
176
|
+
@activity_entities[activity_klass] ||= [ ]
|
177
|
+
@activity_entities[activity_klass] << entity_name
|
178
|
+
|
179
|
+
# entities
|
180
|
+
@entities ||= { }
|
181
|
+
@entities[entity_name] ||= { }
|
182
|
+
|
183
|
+
if !@entities[entity_name][activity_klass].blank?
|
184
|
+
raise "Entity name #{entity_name} already used for activity: #{activity_klass}"
|
185
|
+
end
|
186
|
+
|
187
|
+
@entities[entity_name][activity_klass] = entity_options
|
188
|
+
end
|
189
|
+
|
190
|
+
# Get all models that included mixin {Activr::Entity::ModelMixin}
|
191
|
+
#
|
192
|
+
# @return [Array<Class>] List of model classes
|
193
|
+
def models
|
194
|
+
# loading activities triggers models loading
|
195
|
+
self.activities if @models.blank?
|
196
|
+
|
197
|
+
@models || [ ]
|
198
|
+
end
|
199
|
+
|
200
|
+
# Register a model
|
201
|
+
#
|
202
|
+
# @param model_class [Class] Model class
|
203
|
+
def add_model(model_class)
|
204
|
+
@models ||= [ ]
|
205
|
+
@models << model_class
|
206
|
+
end
|
207
|
+
|
208
|
+
# Get all entities names for given model class
|
209
|
+
def activity_entities_for_model(model_class)
|
210
|
+
@activity_entities_for_model[model_class] ||= begin
|
211
|
+
result = [ ]
|
212
|
+
|
213
|
+
@entity_classes.each do |entity_name, entity_class|
|
214
|
+
result << entity_name if (entity_class == model_class)
|
215
|
+
end
|
216
|
+
|
217
|
+
result
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
# Get all entities names by timelines that can have a reference to given model class
|
222
|
+
#
|
223
|
+
# @param model_class [Class] Model class
|
224
|
+
# @return [Hash{Class=>Array<Symbol>}] Lists of entities names indexed by timeline class
|
225
|
+
def timeline_entities_for_model(model_class)
|
226
|
+
@timeline_entities_for_model[model_class] ||= begin
|
227
|
+
result = { }
|
228
|
+
|
229
|
+
self.timelines.each do |timeline_kind, timeline_class|
|
230
|
+
result[timeline_class] = [ ]
|
231
|
+
|
232
|
+
timeline_class.routes.each do |route|
|
233
|
+
entities_ary = @activity_entities[route.activity_class]
|
234
|
+
(entities_ary || [ ]).each do |entity_name|
|
235
|
+
result[timeline_class] << entity_name if (@entity_classes[entity_name] == model_class)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
result[timeline_class].uniq!
|
240
|
+
end
|
241
|
+
|
242
|
+
result
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# Find all classes in given directory
|
247
|
+
#
|
248
|
+
# @api private
|
249
|
+
#
|
250
|
+
# @param dir_path [String] Directory path
|
251
|
+
# @return [Hash{String=>Class}] Hash of `<kind> => <Class>`
|
252
|
+
def classes_from_path(dir_path)
|
253
|
+
Dir["#{dir_path}/*.rb"].sort.inject({ }) do |memo, file_path|
|
254
|
+
klass = File.basename(file_path, '.rb').camelize.constantize
|
255
|
+
|
256
|
+
if !memo[klass.kind].nil?
|
257
|
+
raise "Kind #{klass.kind} already used by class #{memo[klass.kind]} so can't use it for class #{klass}"
|
258
|
+
end
|
259
|
+
|
260
|
+
memo[klass.kind] = klass
|
261
|
+
|
262
|
+
memo
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
end # class Registry
|
267
|
+
|
268
|
+
end # module Activr
|
@@ -0,0 +1,404 @@
|
|
1
|
+
module Activr
|
2
|
+
|
3
|
+
#
|
4
|
+
# The storage is the component that uses the database driver to serialize/unserialize activities and timeline entries.
|
5
|
+
#
|
6
|
+
# The storage singleton is accessible with {Activr.storage}
|
7
|
+
#
|
8
|
+
class Storage
|
9
|
+
|
10
|
+
autoload :MongoDriver, 'activr/storage/mongo_driver'
|
11
|
+
|
12
|
+
# @return [MongoDriver] database driver
|
13
|
+
attr_reader :driver
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@driver = Activr::Storage::MongoDriver.new
|
17
|
+
|
18
|
+
@hooks = { }
|
19
|
+
end
|
20
|
+
|
21
|
+
# Is it a valid document id
|
22
|
+
#
|
23
|
+
# @param doc_id [Object] Document id to check
|
24
|
+
# @return [true, false]
|
25
|
+
def valid_id?(doc_id)
|
26
|
+
self.driver.valid_id?(doc_id)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Is it a serialized document id
|
30
|
+
#
|
31
|
+
# @return [true,false]
|
32
|
+
def serialized_id?(doc_id)
|
33
|
+
self.driver.serialized_id?(doc_id)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Unserialize a document id
|
37
|
+
#
|
38
|
+
# @param doc_id [Object] Document id
|
39
|
+
# @return [Object] Unserialized document id
|
40
|
+
def unserialize_id(doc_id)
|
41
|
+
self.driver.unserialize_id(doc_id)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Unserialize given parameter only if it is a serialized document id
|
45
|
+
#
|
46
|
+
# @param doc_id [Object] Document id
|
47
|
+
# @return [Object] Unserialized or unmodified document id
|
48
|
+
def unserialize_id_if_necessary(doc_id)
|
49
|
+
self.serialized_id?(doc_id) ? self.unserialize_id(doc_id) : doc_id
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
#
|
54
|
+
# Activities
|
55
|
+
#
|
56
|
+
|
57
|
+
# Insert a new activity
|
58
|
+
#
|
59
|
+
# @param activity [Activity] Activity to insert
|
60
|
+
# @return [Object] The inserted activity id
|
61
|
+
def insert_activity(activity)
|
62
|
+
# serialize
|
63
|
+
activity_hash = activity.to_hash
|
64
|
+
|
65
|
+
# run hook
|
66
|
+
self.run_hook(:will_insert_activity, activity_hash)
|
67
|
+
|
68
|
+
# insert
|
69
|
+
self.driver.insert_activity(activity_hash)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Find an activity
|
73
|
+
#
|
74
|
+
# @param activity_id [Object] Activity id to find
|
75
|
+
# @return [Activity, Nil] An activity instance or `nil` if not found
|
76
|
+
def find_activity(activity_id)
|
77
|
+
activity_hash = self.driver.find_activity(activity_id)
|
78
|
+
if activity_hash
|
79
|
+
# run hook
|
80
|
+
self.run_hook(:did_find_activity, activity_hash)
|
81
|
+
|
82
|
+
# unserialize
|
83
|
+
Activr::Activity.from_hash(activity_hash)
|
84
|
+
else
|
85
|
+
nil
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Find latest activities
|
90
|
+
#
|
91
|
+
# @note If you use others selectors then 'limit' argument and 'skip' option then you have to setup corresponding indexes in database.
|
92
|
+
#
|
93
|
+
# @param limit [Integer] Max number of activities to find
|
94
|
+
# @param options [Hash] Options hash
|
95
|
+
# @option options [Integer] :skip Number of activities to skip (default: 0)
|
96
|
+
# @option options [Time] :before Find activities generated before that datetime (excluding)
|
97
|
+
# @option options [Time] :after Find activities generated after that datetime (excluding)
|
98
|
+
# @option options [Hash{Sym=>String}] :entities Filter by entities values (empty means 'all values')
|
99
|
+
# @option options [Array<Class>] :only Find only these activities
|
100
|
+
# @option options [Array<Class>] :except Skip these activities
|
101
|
+
# @return [Array<Activity>] An array of activities
|
102
|
+
def find_activities(limit, options = { })
|
103
|
+
# default options
|
104
|
+
options = {
|
105
|
+
:skip => 0,
|
106
|
+
:before => nil,
|
107
|
+
:after => nil,
|
108
|
+
:entities => { },
|
109
|
+
:only => [ ],
|
110
|
+
:except => [ ],
|
111
|
+
}.merge(options)
|
112
|
+
|
113
|
+
options[:only] = [ options[:only] ] if (options[:only] && !options[:only].is_a?(Array))
|
114
|
+
|
115
|
+
# find
|
116
|
+
result = self.driver.find_activities(limit, options).map do |activity_hash|
|
117
|
+
# run hook
|
118
|
+
self.run_hook(:did_find_activity, activity_hash)
|
119
|
+
|
120
|
+
# unserialize
|
121
|
+
Activr::Activity.from_hash(activity_hash)
|
122
|
+
end
|
123
|
+
|
124
|
+
result
|
125
|
+
end
|
126
|
+
|
127
|
+
# Count number of activities
|
128
|
+
#
|
129
|
+
# @note If you use one of options selectors then you have to setup corresponding indexes in database.
|
130
|
+
#
|
131
|
+
# @param options [Hash] Options hash
|
132
|
+
# @option options [Time] :before Find activities generated before that datetime (excluding)
|
133
|
+
# @option options [Time] :after Find activities generated after that datetime (excluding)
|
134
|
+
# @option options [Hash{Sym=>String}] :entities Filter by entities values (empty means 'all values')
|
135
|
+
# @option options [Array<Class>] :only Find only these activities
|
136
|
+
# @option options [Array<Class>] :except Skip these activities
|
137
|
+
# @return [Integer] Number of activities
|
138
|
+
def count_activities(options = { })
|
139
|
+
# default options
|
140
|
+
options = {
|
141
|
+
:before => nil,
|
142
|
+
:after => nil,
|
143
|
+
:entities => { },
|
144
|
+
:only => [ ],
|
145
|
+
:except => [ ],
|
146
|
+
}.merge(options)
|
147
|
+
|
148
|
+
options[:only] = [ options[:only] ] if (options[:only] && !options[:only].is_a?(Array))
|
149
|
+
|
150
|
+
# count
|
151
|
+
self.driver.count_activities(options)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Find number of duplicate activities
|
155
|
+
#
|
156
|
+
# @param activity [Activity] Activity to search
|
157
|
+
# @param after [Time] Search after that datetime
|
158
|
+
# @return [Integer] Number of activity duplicates
|
159
|
+
def count_duplicate_activities(activity, after)
|
160
|
+
entities = { }
|
161
|
+
|
162
|
+
activity.entities.each do |entity_name, entity|
|
163
|
+
entities[entity_name.to_sym] = entity.model_id
|
164
|
+
end
|
165
|
+
|
166
|
+
self.count_activities({
|
167
|
+
:only => activity.class,
|
168
|
+
:entities => entities,
|
169
|
+
:after => after,
|
170
|
+
})
|
171
|
+
end
|
172
|
+
|
173
|
+
# Delete activities referring to given entity model instance
|
174
|
+
#
|
175
|
+
# @param model [Object] Model instance
|
176
|
+
def delete_activities_for_entity_model(model)
|
177
|
+
Activr.registry.activity_entities_for_model(model.class).each do |entity_name|
|
178
|
+
self.driver.delete_activities(:entities => { entity_name => model.id })
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
|
183
|
+
#
|
184
|
+
# Timeline Entries
|
185
|
+
#
|
186
|
+
|
187
|
+
# Insert a new timeline entry
|
188
|
+
#
|
189
|
+
# @param timeline_entry [Timeline::Entry] Timeline entry to insert
|
190
|
+
# @return [Object] Inserted timeline entry id
|
191
|
+
def insert_timeline_entry(timeline_entry)
|
192
|
+
# serialize
|
193
|
+
timeline_entry_hash = timeline_entry.to_hash
|
194
|
+
|
195
|
+
# run hook
|
196
|
+
self.run_hook(:will_insert_timeline_entry, timeline_entry_hash, timeline_entry.timeline.class)
|
197
|
+
|
198
|
+
# insert
|
199
|
+
self.driver.insert_timeline_entry(timeline_entry.timeline.kind, timeline_entry_hash)
|
200
|
+
end
|
201
|
+
|
202
|
+
# Find a timeline entry
|
203
|
+
#
|
204
|
+
# @param timeline [Timeline] Timeline instance
|
205
|
+
# @param tl_entry_id [Object] Timeline entry id
|
206
|
+
# @return [Timeline::Entry, Nil] Found timeline entry
|
207
|
+
def find_timeline_entry(timeline, tl_entry_id)
|
208
|
+
timeline_entry_hash = self.driver.find_timeline_entry(timeline.kind, tl_entry_id)
|
209
|
+
if timeline_entry_hash
|
210
|
+
# run hook
|
211
|
+
self.run_hook(:did_find_timeline_entry, timeline_entry_hash, timeline.class)
|
212
|
+
|
213
|
+
# unserialize
|
214
|
+
Activr::Timeline::Entry.from_hash(timeline_entry_hash, timeline)
|
215
|
+
else
|
216
|
+
nil
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# Find timeline entries by descending timestamp
|
221
|
+
#
|
222
|
+
# @param timeline [Timeline] Timeline instance
|
223
|
+
# @param limit [Integer] Max number of entries to find
|
224
|
+
# @param options [Hash] Options hash
|
225
|
+
# @option options [Integer] :skip Number of entries to skip (default: 0)
|
226
|
+
# @option options [Array<Timeline::Route>] :only An array of routes to fetch
|
227
|
+
# @return [Array<Timeline::Entry>] An array of timeline entries
|
228
|
+
def find_timeline(timeline, limit, options = { })
|
229
|
+
options = {
|
230
|
+
:skip => 0,
|
231
|
+
:only => [ ],
|
232
|
+
}.merge(options)
|
233
|
+
|
234
|
+
options[:only] = [ options[:only] ] if (options[:only] && !options[:only].is_a?(Array))
|
235
|
+
|
236
|
+
result = self.driver.find_timeline_entries(timeline.kind, timeline.recipient_id, limit, options).map do |timeline_entry_hash|
|
237
|
+
# run hook
|
238
|
+
self.run_hook(:did_find_timeline_entry, timeline_entry_hash, timeline.class)
|
239
|
+
|
240
|
+
# unserialize
|
241
|
+
Activr::Timeline::Entry.from_hash(timeline_entry_hash, timeline)
|
242
|
+
end
|
243
|
+
|
244
|
+
result
|
245
|
+
end
|
246
|
+
|
247
|
+
# Count number of timeline entries
|
248
|
+
#
|
249
|
+
# @param timeline [Timeline] Timeline instance
|
250
|
+
# @param options [Hash] Options hash
|
251
|
+
# @option options [Array<Timeline::Route>] :only An array of routes to count
|
252
|
+
# @return [Integer] Number of timeline entries in given timeline
|
253
|
+
def count_timeline(timeline, options = { })
|
254
|
+
options = {
|
255
|
+
:only => [ ],
|
256
|
+
}.merge(options)
|
257
|
+
|
258
|
+
options[:only] = [ options[:only] ] if (options[:only] && !options[:only].is_a?(Array))
|
259
|
+
|
260
|
+
self.driver.count_timeline_entries(timeline.kind, timeline.recipient_id, options)
|
261
|
+
end
|
262
|
+
|
263
|
+
# Delete timeline entries
|
264
|
+
#
|
265
|
+
# @param timeline [Timeline] Timeline instance
|
266
|
+
# @param options [Hash] Options hash
|
267
|
+
# @option options [Time] :before Delete only timeline entries which timestamp is before that datetime (excluding)
|
268
|
+
# @option options [Hash{Sym=>String}] :entity Delete only timeline entries with these entities values
|
269
|
+
def delete_timeline(timeline, options = { })
|
270
|
+
# default options
|
271
|
+
options = {
|
272
|
+
:before => nil,
|
273
|
+
:entities => { },
|
274
|
+
}.merge(options)
|
275
|
+
|
276
|
+
self.driver.delete_timeline_entries(timeline.kind, timeline.recipient_id, options)
|
277
|
+
end
|
278
|
+
|
279
|
+
# Delete timeline entries referring to given entity model instance
|
280
|
+
#
|
281
|
+
# @param model [Object] Model instance
|
282
|
+
def delete_timeline_entries_for_entity_model(model)
|
283
|
+
Activr.registry.timeline_entities_for_model(model.class).each do |timeline_class, entities|
|
284
|
+
entities.each do |entity_name|
|
285
|
+
self.driver.delete_timeline_entries(timeline_class.kind, nil, :entities => { entity_name => model.id })
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
|
291
|
+
#
|
292
|
+
# Indexes
|
293
|
+
#
|
294
|
+
|
295
|
+
# Ensure all necessary indexes
|
296
|
+
#
|
297
|
+
# @yield [String] Created index name
|
298
|
+
def create_indexes
|
299
|
+
self.driver.create_indexes
|
300
|
+
end
|
301
|
+
|
302
|
+
|
303
|
+
#
|
304
|
+
# Hooks
|
305
|
+
#
|
306
|
+
|
307
|
+
# Hook: run just before inserting an activity document in the database
|
308
|
+
#
|
309
|
+
# @example Insert the 'foo' meta into all activities
|
310
|
+
#
|
311
|
+
# Activr.storage.will_insert_activity do |activity_hash|
|
312
|
+
# activity_hash['meta'] ||= { }
|
313
|
+
# activity_hash['meta']['foo'] = 'bar'
|
314
|
+
# end
|
315
|
+
#
|
316
|
+
def will_insert_activity(&block)
|
317
|
+
register_hook(:will_insert_activity, block)
|
318
|
+
end
|
319
|
+
|
320
|
+
# Hook: run just after fetching an activity document from the database
|
321
|
+
#
|
322
|
+
# @example Ignore the 'foo' meta
|
323
|
+
#
|
324
|
+
# Activr.storage.did_find_activity do |activity_hash|
|
325
|
+
# if activity_hash['meta']
|
326
|
+
# activity_hash['meta'].delete('foo')
|
327
|
+
# end
|
328
|
+
# end
|
329
|
+
#
|
330
|
+
def did_find_activity(&block)
|
331
|
+
register_hook(:did_find_activity, block)
|
332
|
+
end
|
333
|
+
|
334
|
+
# Hook: run just before inserting a timeline entry document in the database
|
335
|
+
#
|
336
|
+
# @example Insert the 'bar' field into all timeline entries documents
|
337
|
+
#
|
338
|
+
# Activr.storage.will_insert_timeline_entry do |timeline_entry_hash, timeline_class|
|
339
|
+
# timeline_entry_hash['bar'] = 'baz'
|
340
|
+
# end
|
341
|
+
#
|
342
|
+
def will_insert_timeline_entry(&block)
|
343
|
+
register_hook(:will_insert_timeline_entry, block)
|
344
|
+
end
|
345
|
+
|
346
|
+
# Hook: run just after fetching a timeline entry document from the database
|
347
|
+
#
|
348
|
+
# @example Ignore the 'bar' field
|
349
|
+
#
|
350
|
+
# Activr.storage.did_find_timeline_entry do |timeline_entry_hash, timeline_class|
|
351
|
+
# timeline_entry_hash.delete('bar')
|
352
|
+
# end
|
353
|
+
#
|
354
|
+
def did_find_timeline_entry(&block)
|
355
|
+
register_hook(:did_find_timeline_entry, block)
|
356
|
+
end
|
357
|
+
|
358
|
+
|
359
|
+
# Register a hook
|
360
|
+
#
|
361
|
+
# @api private
|
362
|
+
#
|
363
|
+
# @param name [Symbol] Hook name
|
364
|
+
# @param block [Proc] Hook code
|
365
|
+
def register_hook(name, block)
|
366
|
+
@hooks[name] ||= [ ]
|
367
|
+
@hooks[name] << block
|
368
|
+
end
|
369
|
+
|
370
|
+
# Get hooks
|
371
|
+
#
|
372
|
+
# @api private
|
373
|
+
# @note Returns all hooks if `name` is `nil`
|
374
|
+
#
|
375
|
+
# @param name [Symbol] Hook name
|
376
|
+
# @return [Array<Proc>] List of hooks
|
377
|
+
def hooks(name = nil)
|
378
|
+
name ? (@hooks[name] || [ ]) : @hooks
|
379
|
+
end
|
380
|
+
|
381
|
+
# Run a hook
|
382
|
+
#
|
383
|
+
# @api private
|
384
|
+
#
|
385
|
+
# @param name [Symbol] Hook name
|
386
|
+
# @param args [Array] Hook arguments
|
387
|
+
def run_hook(name, *args)
|
388
|
+
return if @hooks[name].blank?
|
389
|
+
|
390
|
+
@hooks[name].each do |hook|
|
391
|
+
args.any? ? hook.call(*args) : hook.call
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
# Reset all hooks
|
396
|
+
#
|
397
|
+
# @api private
|
398
|
+
def clear_hooks!
|
399
|
+
@hooks = { }
|
400
|
+
end
|
401
|
+
|
402
|
+
end # class Storage
|
403
|
+
|
404
|
+
end # module Activr
|