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.
@@ -0,0 +1,92 @@
1
+ module Activr
2
+
3
+ #
4
+ # Async hooks module
5
+ #
6
+ # The async hooks module permits to plug any job system to run some part if Activr code asynchronously.
7
+ #
8
+ # Possible hooks are:
9
+ # - :route_activity - An activity must me routed by the Dispatcher
10
+ # - :timeline_handle - An activity must be handled by a timeline
11
+ #
12
+ # A hook class:
13
+ # - must implement a `#enqueue` method, used to enqueue the async job
14
+ # - must call `Activr::Async.<hook_name>` method in the async job
15
+ #
16
+ # Hook classes to use are specified thanks to the `config.async` hash.
17
+ #
18
+ # When Resque is detected inside a Rails application then defaults hooks are provided out of the box (see the {Activr::Async::Resque} module).
19
+ #
20
+ # @example The default :route_activity hook handler when Resque is detected in a Rails application:
21
+ #
22
+ # # config
23
+ # Activr.configure do |config|
24
+ # config.async[:route_activity] ||= Activr::Async::Resque::RouteActivity
25
+ # end
26
+ #
27
+ # class Activr::Async::Resque::RouteActivity
28
+ # @queue = 'activr_route_activity'
29
+ #
30
+ # class << self
31
+ # def enqueue(activity)
32
+ # ::Resque.enqueue(self, activity.to_hash)
33
+ # end
34
+ #
35
+ # def perform(activity_hash)
36
+ # # unserialize argument
37
+ # activity_hash = Activr::Activity.unserialize_hash(activity_hash)
38
+ # activity = Activr::Activity.from_hash(activity_hash)
39
+ #
40
+ # # call hook
41
+ # Activr::Async.route_activity(activity)
42
+ # end
43
+ # end # class << self
44
+ # end # class RouteActivity
45
+ #
46
+ module Async
47
+
48
+ autoload :Resque, 'activr/async/resque'
49
+
50
+ class << self
51
+ # Run hook
52
+ #
53
+ # If an async class is defined for that hook name then it is used to process
54
+ # the hook asynchronously, else the hooked code is executed immediately.
55
+ #
56
+ # @param name [Symbol] Hook name to run
57
+ # @param args [Array] Hook parameters
58
+ def hook(name, *args)
59
+ if Activr.config.async[name] && (ENV['ACTIVR_FORCE_SYNC'] != 'true')
60
+ # async
61
+ Activr.config.async[name].enqueue(*args)
62
+ else
63
+ # sync
64
+ self.__send__(name, *args)
65
+ end
66
+ end
67
+
68
+
69
+ #
70
+ # Hooks
71
+ #
72
+
73
+ # Hook: route an activity
74
+ #
75
+ # @param activity [Activity] Activity to route
76
+ def route_activity(activity)
77
+ Activr.dispatcher.route(activity)
78
+ end
79
+
80
+ # Hook: timeline handles an activity thanks to given route
81
+ #
82
+ # @param timeline [Timeline] Timeline that handles the activity
83
+ # @param activity [Activity] Activity to handle
84
+ # @param route [Timeline::Route] The route causing that activity handling
85
+ def timeline_handle(timeline, activity, route)
86
+ timeline.handle_activity(activity, route)
87
+ end
88
+ end # class << self
89
+
90
+ end # module Async
91
+
92
+ end # module Activr
@@ -0,0 +1,58 @@
1
+ require 'resque'
2
+
3
+ #
4
+ # The defaults hook classes when Resque is detected inside a Rails application
5
+ #
6
+ module Activr::Async::Resque
7
+
8
+ # Class to handle :route_activity hook thanks to a Resque job
9
+ class RouteActivity
10
+ @queue = 'activr_route_activity'
11
+
12
+ class << self
13
+ # Enqueue job
14
+ def enqueue(activity)
15
+ ::Resque.enqueue(self, activity.to_hash)
16
+ end
17
+
18
+ # Perform job
19
+ def perform(activity_hash)
20
+ # unserialize argument
21
+ activity_hash = Activr::Activity.unserialize_hash(activity_hash)
22
+ activity = Activr::Activity.from_hash(activity_hash)
23
+
24
+ # call hook
25
+ Activr::Async.route_activity(activity)
26
+ end
27
+ end # class << self
28
+ end # class RouteActivity
29
+
30
+ # Class to handle :timeline_handle hook thanks to a Resque job
31
+ class TimelineHandle
32
+ @queue = 'activr_timeline_handle'
33
+
34
+ class << self
35
+ # Enqueue job
36
+ def enqueue(timeline, activity, route)
37
+ ::Resque.enqueue(self, timeline.kind, timeline.recipient_id, activity.to_hash, route.kind)
38
+ end
39
+
40
+ # Perform job
41
+ def perform(timeline_kind, recipient_id, activity_hash, route_kind)
42
+ # unserialize arguments
43
+ recipient_id = Activr.storage.unserialize_id_if_necessary(recipient_id)
44
+ activity_hash = Activr::Activity.unserialize_hash(activity_hash)
45
+
46
+ timeline_klass = Activr.registry.class_for_timeline(timeline_kind)
47
+
48
+ timeline = timeline_klass.new(recipient_id)
49
+ activity = Activr::Activity.from_hash(activity_hash)
50
+ route = timeline_klass.route_for_kind(route_kind)
51
+
52
+ # call hook
53
+ Activr::Async.timeline_handle(timeline, activity, route)
54
+ end
55
+ end # class << self
56
+ end # class TimelineHandle
57
+
58
+ end # module Activr::Async::Resque
@@ -0,0 +1,40 @@
1
+ module Activr
2
+
3
+ #
4
+ # That module gives configuration behaviour to includer
5
+ #
6
+ # @see ActiveSupport::Configurable
7
+ #
8
+ module Configuration
9
+
10
+ extend ActiveSupport::Concern
11
+
12
+ include ActiveSupport::Configurable
13
+
14
+ included do
15
+ # default config
16
+ config.app_path = Dir.pwd
17
+ config.skip_dup_period = nil
18
+
19
+ config.mongodb = {
20
+ :uri => 'mongodb://127.0.0.1/activr',
21
+ :col_prefix => nil,
22
+ :activities_col => nil,
23
+ :timelines_col => nil,
24
+ }
25
+
26
+ config.async = { }
27
+
28
+ # compiles reader methods so we don't have to go through method_missing
29
+ config.compile_methods!
30
+
31
+ # fetch config from fwissr
32
+ config.app_path = Fwissr['/activr/app_path'] unless Fwissr['/activr/app_path'].blank?
33
+ config.skip_dup_period = Fwissr['/activr/skip_dup_period'] unless Fwissr['/activr/skip_dup_period'].blank?
34
+ config.mongodb.merge!(Fwissr['/activr/mongodb'].symbolize_keys) unless Fwissr['/activr/mongodb'].blank?
35
+ config.async.merge!(Fwissr['/activr/async'].symbolize_keys) unless Fwissr['/activr/async'].blank?
36
+ end
37
+
38
+ end # module Configuration
39
+
40
+ end # module Activr
@@ -0,0 +1,70 @@
1
+ module Activr
2
+
3
+ #
4
+ # The dispatcher is the component that is in charge of routing activities to timelines.
5
+ #
6
+ # The dispatcher singleton is accessible with {Activr.dispatcher}
7
+ #
8
+ class Dispatcher
9
+
10
+ # Route an activity
11
+ #
12
+ # @param activity [Activity] Activity to route
13
+ # @return [Integer] The number of resolved recipients that will handle activity
14
+ def route(activity)
15
+ raise "Activity must be stored before routing: #{activity.inspect}" if !activity.stored?
16
+
17
+ result = 0
18
+
19
+ activity.run_callbacks(:route) do
20
+ # iterate on all timelines
21
+ Activr.registry.timelines.values.each do |timeline_class|
22
+ # check if timeline refuses that activity
23
+ next unless timeline_class.should_route_activity?(activity)
24
+
25
+ # store activity in timelines
26
+ self.recipients_for_timeline(timeline_class, activity).each do |recipient, route|
27
+ result += 1
28
+
29
+ timeline = timeline_class.new(recipient)
30
+ if timeline.should_handle_activity?(activity, route)
31
+ Activr::Async.hook(:timeline_handle, timeline, activity, route)
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ result
38
+ end
39
+
40
+ # Find recipients for given activity in given timeline
41
+ #
42
+ # @api private
43
+ #
44
+ # @param timeline_class [Class] Timeline class
45
+ # @param activity [Activity] Activity instance
46
+ # @return [Hash{Object=>Timeline::Route}] Recipients with corresponding Routes
47
+ def recipients_for_timeline(timeline_class, activity)
48
+ result = { }
49
+
50
+ routes = timeline_class.routes_for_activity(activity.class)
51
+ routes.each do |route|
52
+ route.resolve(activity).each do |recipient|
53
+ recipient_id = timeline_class.recipient_id(recipient)
54
+
55
+ # keep only one route per recipient
56
+ if result[recipient_id].nil?
57
+ result[recipient_id] = { :rcpt => recipient, :route => route }
58
+ end
59
+ end
60
+ end
61
+
62
+ result.inject({ }) do |memo, (recipient_id, infos)|
63
+ memo[infos[:rcpt]] = infos[:route]
64
+ memo
65
+ end
66
+ end
67
+
68
+ end # class Dispatcher
69
+
70
+ end # module Activr
@@ -0,0 +1,103 @@
1
+ module Activr
2
+
3
+ #
4
+ # An Entity represents one of your application model involved in activities
5
+ #
6
+ class Entity
7
+
8
+ autoload :ModelMixin, 'activr/entity/model_mixin'
9
+
10
+ # @return [Symbol] entity name
11
+ attr_reader :name
12
+
13
+ # @return [Hash] entity options
14
+ attr_reader :options
15
+
16
+ # @return [Activity] activity owning that entity
17
+ attr_reader :activity
18
+
19
+ # @return [Class] entity model class
20
+ attr_reader :model_class
21
+
22
+ # @return [Objecy] entity model id
23
+ attr_reader :model_id
24
+
25
+
26
+ # @param name [String] Entity name
27
+ # @param value [Object] Model instance or model id
28
+ # @param options [Hash] Options hash
29
+ # @option (see Activity.entity)
30
+ # @option options [Activity] :activity Entity belongs to that activity
31
+ def initialize(name, value, options = { })
32
+ @name = name
33
+ @options = options.dup
34
+
35
+ @activity = @options.delete(:activity)
36
+ @model_class = @options.delete(:class)
37
+
38
+ if Activr.storage.valid_id?(value)
39
+ @model_id = value
40
+
41
+ raise "Missing :class option for #{name} / #{value}: #{options.inspect}" if @model_class.nil?
42
+ raise "Model class MUST implement #find method" unless @model_class.respond_to?(:find)
43
+ else
44
+ @model = value
45
+
46
+ if (@model_class && (@model_class != @model.class))
47
+ raise "Model class mismatch: #{@model_class} != #{@model.class}"
48
+ end
49
+
50
+ @model_class ||= @model.class
51
+ @model_id = @model.id
52
+ end
53
+ end
54
+
55
+ # Get model instance
56
+ #
57
+ # @return [Object] Model instance
58
+ def model
59
+ @model ||= self.model_class.find(self.model_id)
60
+ end
61
+
62
+ # Humanize entity
63
+ #
64
+ # @param options [hash] Options
65
+ # @option options [true,false] :html Generate HTML ?
66
+ # @return [String] Humanized sentence
67
+ def humanize(options = { })
68
+ result = nil
69
+ htmlized = false
70
+
71
+ humanize_meth = @options[:humanize]
72
+ if humanize_meth.nil? && (self.model.respond_to?(:humanize))
73
+ humanize_meth = :humanize
74
+ end
75
+
76
+ if humanize_meth
77
+ case self.model.method(humanize_meth).arity
78
+ when 1
79
+ result = self.model.__send__(humanize_meth, options)
80
+ htmlized = true
81
+ else
82
+ result = self.model.__send__(humanize_meth)
83
+ end
84
+ end
85
+
86
+ if result.nil? && @options[:default]
87
+ result = @options[:default]
88
+ end
89
+
90
+ if !result.nil? && options[:html] && !htmlized && Activr::RailsCtx.view_context
91
+ # let Rails sanitize and htmlize the entity
92
+ result = Activr::RailsCtx.view_context.sanitize(result)
93
+ result = Activr::RailsCtx.view_context.link_to(result, self.model)
94
+ end
95
+
96
+ result ||= ""
97
+
98
+ result
99
+ end
100
+
101
+ end # class Entity
102
+
103
+ end # module Activr
@@ -0,0 +1,117 @@
1
+ #
2
+ # Including that module in your model class adds these methods: {#activities}, {#activities_count} and
3
+ # {#delete_activities!}.
4
+ #
5
+ # If you plan to call either {#activities} or {#activities_count} methods then set the `:feed_index => true`
6
+ # entity setting to ensure that an index is correctly setup when running the `rake activr:create_indexes` task.
7
+ #
8
+ # If you plan to call {#delete_activities!} method then you should set the `:deletable => true` entity setting
9
+ # to ensure that a deletion index is correctly setup when running the `rake activr:create_indexes` task.
10
+ #
11
+ # @example Model:
12
+ # class User
13
+ #
14
+ # # inject sugar methods
15
+ # include Activr::Entity::ModelMixin
16
+ #
17
+ # activr_entity :feed_index => true, :deletable => true
18
+ #
19
+ # include Mongoid::Document
20
+ #
21
+ # field :_id, :type => String
22
+ # field :first_name, :type => String
23
+ # field :last_name, :type => String
24
+ #
25
+ # def fullname
26
+ # "#{self.first_name} #{self.last_name}"
27
+ # end
28
+ #
29
+ # after_destroy :delete_activities!
30
+ #
31
+ # end
32
+ #
33
+ # @example Usage:
34
+ # user = User.find('john')
35
+ #
36
+ # puts "#{user.fullname} has #{user.activities_count} activites. Here are the 10 most recent:"
37
+ #
38
+ # user.activities(10).each do |activity|
39
+ # puts activity.humanize
40
+ # end
41
+ #
42
+ module Activr::Entity::ModelMixin
43
+
44
+ extend ActiveSupport::Concern
45
+
46
+ included do
47
+ # Entity settings
48
+ class_attribute :activr_entity_settings, :instance_writer => false
49
+ self.activr_entity_settings = { :feed_index => false, :deletable => false, :name => nil }
50
+
51
+ # Register model
52
+ Activr.registry.add_model(self)
53
+ end
54
+
55
+ # Class methods for the {ModelMixin} mixin
56
+ module ClassMethods
57
+
58
+ # Get entity name to use for activity feed queries
59
+ #
60
+ # @api private
61
+ #
62
+ # @return [Symbol]
63
+ def activr_entity_feed_actual_name
64
+ self.activr_entity_settings[:name] || Activr::Utils.kind_for_class(self).to_sym
65
+ end
66
+
67
+
68
+ #
69
+ # Class interface
70
+ #
71
+
72
+ # Set a custom entity name to use in entity activity feed queries
73
+ #
74
+ # @note By default, the entity name is inferred from the model class name
75
+ # @todo Add documentation in README for that
76
+ #
77
+ # @param settings [Hash] Entity settings
78
+ # @option settings [Boolean] :deletable Entity is deletable ? (default: `false`)
79
+ # @option settings [String] :name Custom entity name to use in entity activity feed queries (default is inferred from model class name)
80
+ # @option settings [Boolean] :feed_index Ensure entity activity feed index ? (default: `false`)
81
+ def activr_entity(settings)
82
+ self.activr_entity_settings = self.activr_entity_settings.merge(settings)
83
+ end
84
+
85
+ end # module ClassMethods
86
+
87
+ # sugar
88
+ def activr_entity_feed_actual_name
89
+ self.class.activr_entity_feed_actual_name
90
+ end
91
+
92
+ # Fetch activities
93
+ #
94
+ # @param limit [Integer] Max number of activities to fetch
95
+ # @param options (see Storage#find_activities)
96
+ # @return [Array<Activity>] A list of activities
97
+ def activities(limit, options = { })
98
+ Activr.activities(limit, options.merge(self.activr_entity_feed_actual_name => self.id))
99
+ end
100
+
101
+ # Get total number of activities
102
+ #
103
+ # @return [Integer] The total number of activities
104
+ def activities_count
105
+ Activr.activities_count(self.activr_entity_feed_actual_name => self.id)
106
+ end
107
+
108
+ # Delete all activities and timeline entries that reference that entity
109
+ def delete_activities!
110
+ # delete activities
111
+ Activr.storage.delete_activities_for_entity_model(self)
112
+
113
+ # delete timeline entries
114
+ Activr.storage.delete_timeline_entries_for_entity_model(self)
115
+ end
116
+
117
+ end # module Activr::Entity::ModelMixin