activr 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/Rakefile
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'rake'
|
2
|
+
|
3
|
+
$:.unshift File.expand_path(File.dirname(__FILE__) + "/lib")
|
4
|
+
require 'activr'
|
5
|
+
|
6
|
+
task :default => :list
|
7
|
+
task :list do
|
8
|
+
system 'rake -T'
|
9
|
+
end
|
10
|
+
|
11
|
+
##############################################################################
|
12
|
+
# SYNTAX CHECKING
|
13
|
+
##############################################################################
|
14
|
+
desc 'Check code syntax'
|
15
|
+
task :check_syntax do
|
16
|
+
`find . -name "*.rb" |xargs -n1 ruby -c |grep -v "Syntax OK"`
|
17
|
+
puts "* Done"
|
18
|
+
end
|
19
|
+
|
20
|
+
##############################################################################
|
21
|
+
# Stats
|
22
|
+
##############################################################################
|
23
|
+
desc 'Show some stats about the code'
|
24
|
+
task :stats do
|
25
|
+
line_count = proc do |path|
|
26
|
+
Dir[path].collect { |f| File.open(f).readlines.reject { |l| l =~ /(^\s*(\#|\/\*))|^\s*$/ }.size }.inject(0){ |sum,n| sum += n }
|
27
|
+
end
|
28
|
+
comment_count = proc do |path|
|
29
|
+
Dir[path].collect { |f| File.open(f).readlines.select { |l| l =~ /^\s*\#/ }.size }.inject(0) { |sum,n| sum += n }
|
30
|
+
end
|
31
|
+
lib = line_count['lib/**/*.rb']
|
32
|
+
comment = comment_count['lib/**/*.rb']
|
33
|
+
ext = line_count['ext/**/*.{c,h}']
|
34
|
+
spec = line_count['spec/**/*.rb']
|
35
|
+
|
36
|
+
comment_ratio = '%1.2f' % (comment.to_f / lib.to_f)
|
37
|
+
spec_ratio = '%1.2f' % (spec.to_f / lib.to_f)
|
38
|
+
|
39
|
+
puts '/======================\\'
|
40
|
+
puts '| Part LOC |'
|
41
|
+
puts '|======================|'
|
42
|
+
puts "| lib #{lib.to_s.ljust(5)}|"
|
43
|
+
puts "| lib comments #{comment.to_s.ljust(5)}|"
|
44
|
+
puts "| ext #{ext.to_s.ljust(5)}|"
|
45
|
+
puts "| spec #{spec.to_s.ljust(5)}|"
|
46
|
+
puts '| ratios: |'
|
47
|
+
puts "| lib/comment #{comment_ratio.to_s.ljust(5)}|"
|
48
|
+
puts "| lib/spec #{spec_ratio.to_s.ljust(5)}|"
|
49
|
+
puts '\======================/'
|
50
|
+
end
|
data/lib/activr.rb
ADDED
@@ -0,0 +1,206 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
require 'mustache'
|
4
|
+
require 'fwissr'
|
5
|
+
|
6
|
+
require 'logger'
|
7
|
+
|
8
|
+
# active support
|
9
|
+
require 'active_support/callbacks'
|
10
|
+
require 'active_support/core_ext/class'
|
11
|
+
require 'active_support/core_ext/string/inflections'
|
12
|
+
require 'active_support/core_ext/object/blank'
|
13
|
+
require 'active_support/concern'
|
14
|
+
require 'active_support/configurable'
|
15
|
+
|
16
|
+
# active model
|
17
|
+
require 'active_model/callbacks'
|
18
|
+
|
19
|
+
# activr
|
20
|
+
require 'activr/version'
|
21
|
+
require 'activr/utils'
|
22
|
+
require 'activr/configuration'
|
23
|
+
require 'activr/storage'
|
24
|
+
require 'activr/registry'
|
25
|
+
require 'activr/entity'
|
26
|
+
require 'activr/activity'
|
27
|
+
require 'activr/timeline'
|
28
|
+
require 'activr/dispatcher'
|
29
|
+
require 'activr/async'
|
30
|
+
require 'activr/rails_ctx'
|
31
|
+
require 'activr/railtie' if defined?(::Rails)
|
32
|
+
|
33
|
+
|
34
|
+
#
|
35
|
+
# Manage activity feeds
|
36
|
+
#
|
37
|
+
module Activr
|
38
|
+
|
39
|
+
# Access configuration with `Activr.config`
|
40
|
+
include Activr::Configuration
|
41
|
+
|
42
|
+
class << self
|
43
|
+
|
44
|
+
attr_writer :logger
|
45
|
+
|
46
|
+
|
47
|
+
# Configuration sugar
|
48
|
+
#
|
49
|
+
# @example
|
50
|
+
# Activr.configure do |config|
|
51
|
+
# config.app_path = File.join(File.dirname(__FILE__), "app")
|
52
|
+
# config.mongodb[:uri] = "mongodb://#{rspec_mongo_host}:#{rspec_mongo_port}/#{rspec_mongo_db}"
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# @yield [Configuration] Configuration singleton
|
56
|
+
def configure
|
57
|
+
yield self.config
|
58
|
+
end
|
59
|
+
|
60
|
+
# Setup registry
|
61
|
+
def setup
|
62
|
+
self.registry.setup
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return [Logger] A logger instance
|
66
|
+
def logger
|
67
|
+
@logger ||= begin
|
68
|
+
result = Logger.new(STDOUT)
|
69
|
+
result.formatter = proc do |severity, datetime, progname, msg|
|
70
|
+
"[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity} [activr] #{msg}\n"
|
71
|
+
end
|
72
|
+
result
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Path to activities classes directory
|
77
|
+
#
|
78
|
+
# @return [String] Directory path
|
79
|
+
def activities_path
|
80
|
+
File.join(Activr.config.app_path, "activities")
|
81
|
+
end
|
82
|
+
|
83
|
+
# Path to timelines classes directory
|
84
|
+
#
|
85
|
+
# @return [String] Directory path
|
86
|
+
def timelines_path
|
87
|
+
File.join(Activr.config.app_path, "timelines")
|
88
|
+
end
|
89
|
+
|
90
|
+
# {Registry} singleton
|
91
|
+
#
|
92
|
+
# @return [Registry] {Registry} instance
|
93
|
+
def registry
|
94
|
+
@registy ||= Activr::Registry.new
|
95
|
+
end
|
96
|
+
|
97
|
+
# {Storage} singleton
|
98
|
+
#
|
99
|
+
# @return [Storage] {Storage} instance
|
100
|
+
def storage
|
101
|
+
@storage ||= Activr::Storage.new
|
102
|
+
end
|
103
|
+
|
104
|
+
# {Dispatcher} singleton
|
105
|
+
#
|
106
|
+
# @return [Dispatcher] {Dispatcher} instance
|
107
|
+
def dispatcher
|
108
|
+
@dispatcher ||= Activr::Dispatcher.new
|
109
|
+
end
|
110
|
+
|
111
|
+
# Dispatch an activity
|
112
|
+
#
|
113
|
+
# @param activity [Activity] Activity instance to dispatch
|
114
|
+
# @param options [Hash] Options hash
|
115
|
+
# @option options [Integer] :skip_dup_period Activity is skipped if a duplicate one is found in that period of time, in seconds (default: `Activr.config.skip_dup_period`)
|
116
|
+
# @return [Activity] The activity
|
117
|
+
def dispatch!(activity, options = { })
|
118
|
+
# default options
|
119
|
+
options = {
|
120
|
+
:skip_dup_period => Activr.config.skip_dup_period,
|
121
|
+
}.merge(options)
|
122
|
+
|
123
|
+
# check for duplicates
|
124
|
+
skip_it = options[:skip_dup_period] && (options[:skip_dup_period] > 0) &&
|
125
|
+
(Activr.storage.count_duplicate_activities(activity, Time.now - options[:skip_dup_period]) > 0)
|
126
|
+
|
127
|
+
if !skip_it
|
128
|
+
if !activity.stored?
|
129
|
+
# store activity in main collection
|
130
|
+
activity.store!
|
131
|
+
end
|
132
|
+
|
133
|
+
# check if storing failed
|
134
|
+
if activity.stored?
|
135
|
+
Activr::Async.hook(:route_activity, activity)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
activity
|
140
|
+
end
|
141
|
+
|
142
|
+
# Normalize query options
|
143
|
+
#
|
144
|
+
# @api private
|
145
|
+
#
|
146
|
+
# @param options [Hash] Options to normalize
|
147
|
+
# @return [Hash] Normalized options
|
148
|
+
def normalize_query_options(options)
|
149
|
+
result = { }
|
150
|
+
|
151
|
+
options.each do |key, value|
|
152
|
+
key = key.to_sym
|
153
|
+
|
154
|
+
if Activr.registry.entities_names.include?(key)
|
155
|
+
# extract entities from options
|
156
|
+
result[:entities] ||= { }
|
157
|
+
result[:entities][key] = value
|
158
|
+
else
|
159
|
+
result[key] = value
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
result
|
164
|
+
end
|
165
|
+
|
166
|
+
# (see Storage#find_activities)
|
167
|
+
def activities(limit, options = { })
|
168
|
+
options = self.normalize_query_options(options)
|
169
|
+
|
170
|
+
Activr.storage.find_activities(limit, options)
|
171
|
+
end
|
172
|
+
|
173
|
+
# (see Storage#count_activities)
|
174
|
+
def activities_count(options = { })
|
175
|
+
options = self.normalize_query_options(options)
|
176
|
+
|
177
|
+
Activr.storage.count_activities(options)
|
178
|
+
end
|
179
|
+
|
180
|
+
# Get a timeline instance
|
181
|
+
#
|
182
|
+
# @param timeline_class [Class] Timeline class
|
183
|
+
# @param recipient [String|Object] Recipient instance or recipient id
|
184
|
+
# @return [Timeline] Timeline instance
|
185
|
+
def timeline(timeline_class, recipient)
|
186
|
+
timeline_class.new(recipient)
|
187
|
+
end
|
188
|
+
|
189
|
+
# Render a sentence
|
190
|
+
#
|
191
|
+
# @param text [String] Sentence to render
|
192
|
+
# @param bindings [Hash] Sentence bindings
|
193
|
+
# @return [String] Rendered sentence
|
194
|
+
def sentence(text, bindings = { })
|
195
|
+
# render
|
196
|
+
result = Activr::Utils.render_mustache(text, bindings)
|
197
|
+
|
198
|
+
# strip whitespaces
|
199
|
+
result.strip!
|
200
|
+
|
201
|
+
result
|
202
|
+
end
|
203
|
+
|
204
|
+
end # class << self
|
205
|
+
|
206
|
+
end # module Activr
|
@@ -0,0 +1,446 @@
|
|
1
|
+
module Activr
|
2
|
+
|
3
|
+
#
|
4
|
+
# An activity is an event that is (most of the time) performed by a user in your application.
|
5
|
+
#
|
6
|
+
# When defining an activity you specify allowed entities and a humanization template.
|
7
|
+
#
|
8
|
+
# When instanciated, an activity contains:
|
9
|
+
# - Concrete `entities` instances
|
10
|
+
# - A timestamp (the `at` field)
|
11
|
+
# - User-defined `meta` data
|
12
|
+
#
|
13
|
+
# By default, entities are mandatory and the exception {MissingEntityError} is raised when trying to store an activity
|
14
|
+
# with a missing entity.
|
15
|
+
#
|
16
|
+
# When an activity is stored in database, its `_id` field is filled.
|
17
|
+
#
|
18
|
+
# Model callbacks:
|
19
|
+
# - `before_store`, `around_store` and `after_store` are called when activity is stored in database
|
20
|
+
# - `before_route`, `around_route` and `after_route` are called when activity is routed by the dispatcher
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# class AddPictureActivity < Activr::Activity
|
24
|
+
#
|
25
|
+
# entity :actor, :class => User, :humanize => :fullname
|
26
|
+
# entity :picture, :humanize => :title
|
27
|
+
# entity :album, :humanize => :name
|
28
|
+
#
|
29
|
+
# humanize "{{{actor}}} added picture {{{picture}}} to the album {{{album}}}"
|
30
|
+
#
|
31
|
+
# before_store :set_bar_meta
|
32
|
+
#
|
33
|
+
# def set_bar_meta
|
34
|
+
# self[:bar] = 'baz'
|
35
|
+
# true
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# activity = AddPictureActivity.new(:actor => user, :picture => picture, :album => album, :foo => 'bar')
|
41
|
+
#
|
42
|
+
# activity.humanize
|
43
|
+
# # => John WILLIAMS added picture My Face to the album My Selfies
|
44
|
+
#
|
45
|
+
# activity[:foo]
|
46
|
+
# => 'bar'
|
47
|
+
#
|
48
|
+
# activity.store!
|
49
|
+
#
|
50
|
+
# activity._id
|
51
|
+
# => BSON::ObjectId('529cca3d61796d296e020000')
|
52
|
+
#
|
53
|
+
# activity[:bar]
|
54
|
+
# => 'baz'
|
55
|
+
#
|
56
|
+
class Activity
|
57
|
+
|
58
|
+
extend ActiveModel::Callbacks
|
59
|
+
|
60
|
+
# Callbacks when an activity is stored, and routed to timelines
|
61
|
+
define_model_callbacks :store, :route
|
62
|
+
|
63
|
+
|
64
|
+
# Exception: a mandatory entity is missing
|
65
|
+
class MissingEntityError < StandardError; end
|
66
|
+
|
67
|
+
|
68
|
+
# Allowed entities
|
69
|
+
class_attribute :allowed_entities, :instance_writer => false
|
70
|
+
self.allowed_entities = { }
|
71
|
+
|
72
|
+
# Humanization template
|
73
|
+
class_attribute :humanize_tpl, :instance_writer => false
|
74
|
+
self.humanize_tpl = nil
|
75
|
+
|
76
|
+
|
77
|
+
class << self
|
78
|
+
|
79
|
+
# Get activity class kind
|
80
|
+
#
|
81
|
+
# @example
|
82
|
+
# AddPictureActivity.kind
|
83
|
+
# => 'add_picture'
|
84
|
+
#
|
85
|
+
# @note Kind is inferred from class name, unless `#set_kind` method is used to force a custom value
|
86
|
+
#
|
87
|
+
# @return [String] Activity kind
|
88
|
+
def kind
|
89
|
+
@kind ||= @forced_kind || Activr::Utils.kind_for_class(self, 'activity')
|
90
|
+
end
|
91
|
+
|
92
|
+
# Instanciate an activity from a hash
|
93
|
+
#
|
94
|
+
# @note Correct activity subclass is resolved thanks to `kind` field
|
95
|
+
#
|
96
|
+
# @param data_hash [Hash] Activity fields
|
97
|
+
# @return [Activity] Subclass instance
|
98
|
+
def from_hash(data_hash)
|
99
|
+
activity_kind = data_hash['kind'] || data_hash[:kind]
|
100
|
+
raise "No kind found in activity hash: #{data_hash.inspect}" unless activity_kind
|
101
|
+
|
102
|
+
klass = Activr.registry.class_for_activity(activity_kind)
|
103
|
+
klass.new(data_hash)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Unserialize an activity hash
|
107
|
+
#
|
108
|
+
# That method fixes issues remaining after an activity hash has been unserialized partially:
|
109
|
+
#
|
110
|
+
# - the `at` field is converted from String to Time
|
111
|
+
# - the `_id` field is converted from `{ '$oid' => [String] }` format to correct `ObjectId` class (`BSON::ObjectId` or `Moped::BSON::ObjectId`)
|
112
|
+
#
|
113
|
+
# @param data_hash [Hash] Activity fields
|
114
|
+
# @return [Hash] Unserialized activity fields
|
115
|
+
def unserialize_hash(data_hash)
|
116
|
+
result = { }
|
117
|
+
|
118
|
+
data_hash.each do |key, val|
|
119
|
+
result[key] = if Activr.storage.serialized_id?(val)
|
120
|
+
Activr.storage.unserialize_id(val)
|
121
|
+
elsif (key == 'at') && val.is_a?(String)
|
122
|
+
Time.parse(val)
|
123
|
+
else
|
124
|
+
val
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
result
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
#
|
133
|
+
# Class interface
|
134
|
+
#
|
135
|
+
|
136
|
+
# Define an allowed entity for that activity class
|
137
|
+
#
|
138
|
+
# @example That method creates several instance methods, for example with `entity :album`:
|
139
|
+
#
|
140
|
+
# # Get the entity model instance
|
141
|
+
# def album
|
142
|
+
# # ...
|
143
|
+
# end
|
144
|
+
#
|
145
|
+
# # Set the entity model instance
|
146
|
+
# def album=(value)
|
147
|
+
# # ...
|
148
|
+
# end
|
149
|
+
#
|
150
|
+
# # Get the entity id
|
151
|
+
# def album_id
|
152
|
+
# # ...
|
153
|
+
# end
|
154
|
+
#
|
155
|
+
# # Get the Activr::Entity instance
|
156
|
+
# def album_entity
|
157
|
+
# # ...
|
158
|
+
# end
|
159
|
+
#
|
160
|
+
# @note By convention the entity that correspond to a user performing an action should be named `:actor`
|
161
|
+
#
|
162
|
+
# @param name [String,Symbol] Entity name
|
163
|
+
# @param options [Hash] Entity options
|
164
|
+
# @option options [Class] :class Entity model class
|
165
|
+
# @option options [Symbol] :humanize A method name to call on entity model instance to humanize it
|
166
|
+
# @option options [String] :default Default humanization value
|
167
|
+
# @option options [true, false] :optional Is it an optional entity ?
|
168
|
+
def entity(name, options = { })
|
169
|
+
name = name.to_sym
|
170
|
+
raise "Entity already defined: #{name}" unless self.allowed_entities[name].nil?
|
171
|
+
|
172
|
+
if options[:class].nil?
|
173
|
+
options = options.dup
|
174
|
+
options[:class] = name.to_s.camelize.constantize
|
175
|
+
end
|
176
|
+
|
177
|
+
# NOTE: always use a setter on a class_attribute (cf. http://apidock.com/rails/Class/class_attribute)
|
178
|
+
self.allowed_entities = self.allowed_entities.merge(name => options)
|
179
|
+
|
180
|
+
# create entity methods
|
181
|
+
class_eval <<-EOS, __FILE__, __LINE__
|
182
|
+
# eg: actor
|
183
|
+
def #{name}
|
184
|
+
@entities[:#{name}] && @entities[:#{name}].model
|
185
|
+
end
|
186
|
+
|
187
|
+
# eg: actor = ...
|
188
|
+
def #{name}=(value)
|
189
|
+
@entities.delete(:#{name})
|
190
|
+
|
191
|
+
if (value != nil)
|
192
|
+
@entities[:#{name}] = Activr::Entity.new(:#{name}, value, self.allowed_entities[:#{name}])
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# eg: actor_id
|
197
|
+
def #{name}_id
|
198
|
+
@entities[:#{name}] && @entities[:#{name}].model_id
|
199
|
+
end
|
200
|
+
|
201
|
+
# eg: actor_entity
|
202
|
+
def #{name}_entity
|
203
|
+
@entities[:#{name}]
|
204
|
+
end
|
205
|
+
EOS
|
206
|
+
|
207
|
+
if (name == :actor) && options[:class] &&
|
208
|
+
(options[:class] < Activr::Entity::ModelMixin) &&
|
209
|
+
options[:class].activr_entity_settings[:name].nil?
|
210
|
+
# sugar so that we don't have to explicitly call `activr_entity` on model class
|
211
|
+
options[:class].activr_entity_settings = options[:class].activr_entity_settings.merge(:name => :actor)
|
212
|
+
end
|
213
|
+
|
214
|
+
# register used entity
|
215
|
+
Activr.registry.add_entity(name, options, self)
|
216
|
+
end
|
217
|
+
|
218
|
+
# Define a humanization template for that activity class
|
219
|
+
#
|
220
|
+
# @param tpl [String] Mustache template
|
221
|
+
def humanize(tpl)
|
222
|
+
raise "Humanize already defined: #{self.humanize_tpl}" unless self.humanize_tpl.blank?
|
223
|
+
|
224
|
+
self.humanize_tpl = tpl
|
225
|
+
end
|
226
|
+
|
227
|
+
# Set activity kind
|
228
|
+
#
|
229
|
+
# @note Default kind is inferred from class name
|
230
|
+
#
|
231
|
+
# @param forced_kind [String] Activity kind
|
232
|
+
def set_kind(forced_kind)
|
233
|
+
@forced_kind = forced_kind.to_s
|
234
|
+
end
|
235
|
+
|
236
|
+
end # class << self
|
237
|
+
|
238
|
+
# @return [Object] activity id
|
239
|
+
attr_accessor :_id
|
240
|
+
|
241
|
+
# @return [Time] activity timestamp
|
242
|
+
attr_accessor :at
|
243
|
+
|
244
|
+
# @return [Hash{Symbol=>Entity}] activity entities
|
245
|
+
attr_reader :entities
|
246
|
+
|
247
|
+
# @return [Hash{Symbol=>Object}] activity meta hash (symbolized)
|
248
|
+
attr_reader :meta
|
249
|
+
|
250
|
+
# @param data_hash [Hash] Activity fields
|
251
|
+
def initialize(data_hash = { })
|
252
|
+
@_id = nil
|
253
|
+
@at = nil
|
254
|
+
|
255
|
+
@entities = { }
|
256
|
+
@meta = { }
|
257
|
+
|
258
|
+
data_hash.each do |data_name, data_value|
|
259
|
+
data_name = data_name.to_sym
|
260
|
+
|
261
|
+
if (self.allowed_entities[data_name] != nil)
|
262
|
+
# entity
|
263
|
+
@entities[data_name] = Activr::Entity.new(data_name, data_value, self.allowed_entities[data_name].merge(:activity => self))
|
264
|
+
elsif (data_name == :_id)
|
265
|
+
# activity id
|
266
|
+
@_id = data_value
|
267
|
+
elsif (data_name == :at)
|
268
|
+
# timestamp
|
269
|
+
raise "Wrong :at class: #{data_value.inspect}" unless data_value.is_a?(Time)
|
270
|
+
@at = data_value
|
271
|
+
elsif self.respond_to?("#{data_name}=")
|
272
|
+
# ivar
|
273
|
+
self.send("#{data_name}=", data_value)
|
274
|
+
elsif (data_name == :kind)
|
275
|
+
# ignore it
|
276
|
+
elsif (data_name == :meta)
|
277
|
+
# meta
|
278
|
+
@meta.merge!(data_value.symbolize_keys)
|
279
|
+
else
|
280
|
+
# sugar for meta data
|
281
|
+
self[data_name] = data_value
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
# default timestamp
|
286
|
+
@at ||= Time.now.utc
|
287
|
+
end
|
288
|
+
|
289
|
+
# Get a meta
|
290
|
+
#
|
291
|
+
# @example
|
292
|
+
# activity[:foo]
|
293
|
+
# # => 'bar'
|
294
|
+
#
|
295
|
+
# @param key [Symbol] Meta name
|
296
|
+
# @return [Object] Meta value
|
297
|
+
def [](key)
|
298
|
+
@meta[key.to_sym]
|
299
|
+
end
|
300
|
+
|
301
|
+
# Set a meta
|
302
|
+
#
|
303
|
+
# @example
|
304
|
+
# activity[:foo] = 'bar'
|
305
|
+
#
|
306
|
+
# @param key [Symbol] Meta name
|
307
|
+
# @param value [Object] Meta value
|
308
|
+
def []=(key, value)
|
309
|
+
@meta[key.to_sym] = value
|
310
|
+
end
|
311
|
+
|
312
|
+
# Serialize activity to a hash
|
313
|
+
#
|
314
|
+
# @note All keys are stringified (ie. there is no Symbol)
|
315
|
+
#
|
316
|
+
# @return [Hash] Activity hash
|
317
|
+
def to_hash
|
318
|
+
result = { }
|
319
|
+
|
320
|
+
# id
|
321
|
+
result['_id'] = @_id if @_id
|
322
|
+
|
323
|
+
# timestamp
|
324
|
+
result['at'] = @at
|
325
|
+
|
326
|
+
# kind
|
327
|
+
result['kind'] = kind.to_s
|
328
|
+
|
329
|
+
# entities
|
330
|
+
@entities.each do |entity_name, entity|
|
331
|
+
result[entity_name.to_s] = entity.model_id
|
332
|
+
end
|
333
|
+
|
334
|
+
# meta
|
335
|
+
result['meta'] = @meta.stringify_keys unless @meta.blank?
|
336
|
+
|
337
|
+
result
|
338
|
+
end
|
339
|
+
|
340
|
+
# Activity kind
|
341
|
+
#
|
342
|
+
# @example
|
343
|
+
# AddPictureActivity.new(...).kind
|
344
|
+
# => 'add_picture'
|
345
|
+
#
|
346
|
+
# @note Kind is inferred from Class name
|
347
|
+
#
|
348
|
+
# @return [String] Activity kind
|
349
|
+
def kind
|
350
|
+
self.class.kind
|
351
|
+
end
|
352
|
+
|
353
|
+
# Bindings for humanization sentence
|
354
|
+
#
|
355
|
+
# For each entity, returned hash contains:
|
356
|
+
# :<entity_name> => <entity humanization>
|
357
|
+
# :<entity_name>_model => <entity model instance>
|
358
|
+
#
|
359
|
+
# All `meta` are merged in returned hash too.
|
360
|
+
#
|
361
|
+
# @param options [Hash] Humanization options
|
362
|
+
# @return [Hash] Humanization bindings
|
363
|
+
def humanization_bindings(options = { })
|
364
|
+
result = { }
|
365
|
+
|
366
|
+
@entities.each do |entity_name, entity|
|
367
|
+
result[entity_name] = entity.humanize(options.merge(:activity => self))
|
368
|
+
result["#{entity_name}_model".to_sym] = entity.model
|
369
|
+
end
|
370
|
+
|
371
|
+
result.merge(@meta)
|
372
|
+
end
|
373
|
+
|
374
|
+
# Humanize that activity
|
375
|
+
#
|
376
|
+
# @param options [Hash] Options hash
|
377
|
+
# @option options [true, false] :html Output HTML (default: `false`)
|
378
|
+
# @return [String] Humanized activity
|
379
|
+
def humanize(options = { })
|
380
|
+
raise "No humanize_tpl defined" if self.humanize_tpl.blank?
|
381
|
+
|
382
|
+
Activr.sentence(self.humanize_tpl, self.humanization_bindings(options))
|
383
|
+
end
|
384
|
+
|
385
|
+
# Check if activity is valid
|
386
|
+
#
|
387
|
+
# @raise [MissingEntityError] if a mandatory entity is missing
|
388
|
+
# @api private
|
389
|
+
def check!
|
390
|
+
# check mandatory entities
|
391
|
+
self.allowed_entities.each do |entity_name, entity_options|
|
392
|
+
if !entity_options[:optional] && @entities[entity_name].blank?
|
393
|
+
raise Activr::Activity::MissingEntityError, "Missing '#{entity_name}' entity in this '#{self.kind}' activity: #{self.inspect}"
|
394
|
+
end
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
# Check if activity is stored in database
|
399
|
+
#
|
400
|
+
# @return [true, false]
|
401
|
+
def stored?
|
402
|
+
!@_id.nil?
|
403
|
+
end
|
404
|
+
|
405
|
+
# Store activity in database
|
406
|
+
#
|
407
|
+
# @raise [MissingEntityError] if a mandatory entity is missing
|
408
|
+
#
|
409
|
+
# @note SIDE EFFECT: The `_id` field is set
|
410
|
+
def store!
|
411
|
+
run_callbacks(:store) do
|
412
|
+
# check validity
|
413
|
+
self.check!
|
414
|
+
|
415
|
+
# store
|
416
|
+
@_id = Activr.storage.insert_activity(self)
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
# Sugar so that we can try to fetch an entity defined for another activity (yes, I hate myself for that...)
|
421
|
+
#
|
422
|
+
# @api private
|
423
|
+
def method_missing(sym, *args, &blk)
|
424
|
+
# match: actor_entity | actor_id | actor
|
425
|
+
match_data = sym.to_s.match(/(.+)_(entity|id)$/)
|
426
|
+
entity_name = match_data ? match_data[1].to_sym : sym
|
427
|
+
|
428
|
+
if Activr.registry.entities_names.include?(entity_name)
|
429
|
+
# ok, don't worry...
|
430
|
+
# define an instance method so that future calls on that method do not rely on method_missing
|
431
|
+
self.instance_eval <<-RUBY
|
432
|
+
def #{sym}(*args, &blk)
|
433
|
+
nil
|
434
|
+
end
|
435
|
+
RUBY
|
436
|
+
|
437
|
+
self.__send__(sym, *args, &blk)
|
438
|
+
else
|
439
|
+
# super Michel !
|
440
|
+
super
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
end # class Activity
|
445
|
+
|
446
|
+
end # module Activr
|