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
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
|