activr 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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