wiser_trails 1.1.3

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.
Files changed (35) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +17 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +145 -0
  6. data/Rakefile +2 -0
  7. data/lib/generators/wiser_trails.rb +12 -0
  8. data/lib/generators/wiser_trails/migration/migration_generator.rb +17 -0
  9. data/lib/generators/wiser_trails/migration/templates/migration.rb +23 -0
  10. data/lib/generators/wiser_trails/model/model_generator.rb +17 -0
  11. data/lib/generators/wiser_trails/model/templates/model.rb +3 -0
  12. data/lib/wiser_trails.rb +69 -0
  13. data/lib/wiser_trails/actions/creation.rb +15 -0
  14. data/lib/wiser_trails/actions/destruction.rb +15 -0
  15. data/lib/wiser_trails/actions/update.rb +15 -0
  16. data/lib/wiser_trails/activity.rb +6 -0
  17. data/lib/wiser_trails/common.rb +334 -0
  18. data/lib/wiser_trails/config.rb +63 -0
  19. data/lib/wiser_trails/models/activist.rb +9 -0
  20. data/lib/wiser_trails/models/activity.rb +4 -0
  21. data/lib/wiser_trails/models/adapter.rb +5 -0
  22. data/lib/wiser_trails/models/trackable.rb +9 -0
  23. data/lib/wiser_trails/orm/active_record.rb +5 -0
  24. data/lib/wiser_trails/orm/active_record/activist.rb +48 -0
  25. data/lib/wiser_trails/orm/active_record/activity.rb +25 -0
  26. data/lib/wiser_trails/orm/active_record/adapter.rb +16 -0
  27. data/lib/wiser_trails/orm/active_record/trackable.rb +15 -0
  28. data/lib/wiser_trails/renderable.rb +118 -0
  29. data/lib/wiser_trails/roles/deactivatable.rb +42 -0
  30. data/lib/wiser_trails/roles/trail_it.rb +180 -0
  31. data/lib/wiser_trails/utility/store_controller.rb +37 -0
  32. data/lib/wiser_trails/utility/view_helpers.rb +26 -0
  33. data/lib/wiser_trails/version.rb +3 -0
  34. data/wiser_trails.gemspec +27 -0
  35. metadata +147 -0
@@ -0,0 +1,63 @@
1
+ require 'singleton'
2
+
3
+ module WiserTrails
4
+ # Class used to initialize configuration object.
5
+ class Config
6
+ include ::Singleton
7
+ attr_accessor :enabled
8
+
9
+ @@orm = :active_record
10
+
11
+ def initialize
12
+ # Indicates whether WiserTrails is enabled globally
13
+ @enabled = true
14
+ end
15
+
16
+ # Evaluates given block to provide DSL configuration.
17
+ # @example Initializer for Rails
18
+ # WiserTrails::Config.set do
19
+ # orm :mongo_mapper
20
+ # enabled false
21
+ # end
22
+ def self.set &block
23
+ b = Block.new
24
+ b.instance_eval &block
25
+ orm = b.instance_variable_get(:@orm)
26
+ @@orm = orm unless orm.nil?
27
+ enabled = b.instance_variable_get(:@en)
28
+ instance
29
+ instance.instance_variable_set(:@enabled, enabled) unless enabled.nil?
30
+ end
31
+
32
+ # Set the ORM for use by WiserTrails.
33
+ def self.orm(orm = nil)
34
+ @@orm = (orm ? orm.to_sym : false) || @@orm
35
+ end
36
+
37
+ # alias for {#orm}
38
+ # @see #orm
39
+ def self.orm=(orm = nil)
40
+ orm(orm)
41
+ end
42
+
43
+ # instance version of {Config#orm}
44
+ # @see Config#orm
45
+ def orm(orm=nil)
46
+ self.class.orm(orm)
47
+ end
48
+
49
+ # Provides simple DSL for the config block.
50
+ class Block
51
+ # @see Config#orm
52
+ def orm(orm = nil)
53
+ @orm = (orm ? orm.to_sym : false) || @orm
54
+ end
55
+
56
+ # Decides whether to enable WiserTrails.
57
+ # @param en [Boolean] Enabled?
58
+ def enabled(en = nil)
59
+ @en = (en.nil? ? @en : en)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,9 @@
1
+ module WiserTrails
2
+ # Provides helper methods for selecting activities from a user.
3
+ module Activist
4
+ # Delegates to configured ORM.
5
+ def self.included(base)
6
+ base.extend WiserTrails::inherit_orm("Activist")
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,4 @@
1
+ module WiserTrails
2
+ class Activity < inherit_orm("Activity")
3
+ end
4
+ end
@@ -0,0 +1,5 @@
1
+ module WiserTrails
2
+ # Loads database-specific routines for use by WiserTrails.
3
+ class Adapter < inherit_orm("Adapter")
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ module WiserTrails
2
+ # Provides association for activities bound to this object by *trackable*.
3
+ module Trackable
4
+ # Delegates to ORM.
5
+ def self.included(base)
6
+ base.extend WiserTrails::inherit_orm("Trackable")
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ require 'active_record'
2
+ require_relative 'active_record/activity.rb'
3
+ require_relative 'active_record/adapter.rb'
4
+ require_relative 'active_record/activist.rb'
5
+ require_relative 'active_record/trackable.rb'
@@ -0,0 +1,48 @@
1
+ module WiserTrails
2
+ module ORM
3
+ module ActiveRecord
4
+ # Module extending classes that serve as owners
5
+ module Activist
6
+ extend ActiveSupport::Concern
7
+
8
+ # Loads the {ClassMethods#activist} method for declaring the class
9
+ # as an activist.
10
+ def self.extended(base)
11
+ base.extend(ClassMethods)
12
+ end
13
+
14
+
15
+ # Module extending classes that serve as owners
16
+ module ClassMethods
17
+ # Adds ActiveRecord associations to model to simplify fetching
18
+ # so you can list activities performed by the owner.
19
+ # It is completely optional. Any model can be an owner to an activity
20
+ # even without being an explicit activist.
21
+ #
22
+ # == Usage:
23
+ # In model:
24
+ #
25
+ # class User < ActiveRecord::Base
26
+ # include WiserTrails::Model
27
+ # activist
28
+ # end
29
+ #
30
+ # In controller:
31
+ # User.first.activities
32
+ #
33
+ def activist
34
+ # Association of activities as their owner.
35
+ # @!method activities_as_owner
36
+ # @return [Array<Activity>] Activities which self is the owner of.
37
+ has_many :activities_as_owner, :class_name => "::WiserTrails::Activity", :as => :owner
38
+
39
+ # Association of activities as their recipient.
40
+ # @!method activities_as_recipient
41
+ # @return [Array<Activity>] Activities which self is the recipient of.
42
+ has_many :activities_as_account, :class_name => "::WiserTrails::Activity", :as => :account
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,25 @@
1
+ module WiserTrails
2
+ module ORM
3
+ module ActiveRecord
4
+ # The ActiveRecord model containing
5
+ # details about recorded activity.
6
+ class Activity < ::ActiveRecord::Base
7
+ self.table_name = "wiser_trails"
8
+ include Renderable
9
+
10
+ # Define polymorphic association to the parent
11
+ belongs_to :trackable, :polymorphic => true
12
+ # Define ownership to a resource responsible for this activity
13
+ belongs_to :owner, :polymorphic => true
14
+ # Define ownership to a resource targeted by this activity
15
+ belongs_to :account, :polymorphic => true
16
+ # Serialize parameters Hash
17
+ serialize :new_value, Hash
18
+
19
+ if ::ActiveRecord::VERSION::MAJOR < 4
20
+ attr_accessible :key, :owner, :new_value, :account, :trackable
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,16 @@
1
+ module WiserTrails
2
+ module ORM
3
+ # Support for ActiveRecord for WiserTrails. Used by default and supported
4
+ # officialy.
5
+ module ActiveRecord
6
+ # Provides ActiveRecord specific, database-related routines for use by
7
+ # WiserTrails.
8
+ class Adapter
9
+ # Creates the activity on `trackable` with `options`
10
+ def self.create_activity(trackable, options)
11
+ trackable.activities.create options
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ module WiserTrails
2
+ module ORM
3
+ module ActiveRecord
4
+ # Implements {WiserTrails::Trackable} for ActiveRecord
5
+ # @see WiserTrails::Trackable
6
+ module Trackable
7
+ # Creates an association for activities where self is the *trackable*
8
+ # object.
9
+ def self.extended(base)
10
+ base.has_many :activities, :class_name => "::WiserTrails::Activity", :as => :trackable
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,118 @@
1
+ module WiserTrails
2
+ # Provides logic for rendering activities. Handles both i18n strings
3
+ # support and smart partials rendering (different templates per activity key).
4
+ module Renderable
5
+ # Virtual attribute returning text description of the activity
6
+ # using the activity's key to translate using i18n.
7
+ def text(params = {})
8
+ # TODO: some helper for key transformation for two supported formats
9
+ k = key.split('.')
10
+ k.unshift('activity') if k.first != 'activity'
11
+ k = k.join('.')
12
+
13
+ I18n.t(k, parameters.merge(params) || {})
14
+ end
15
+
16
+ # Renders activity from views.
17
+ #
18
+ # @param [ActionView::Base] context
19
+ # @return [nil] nil
20
+ #
21
+ # Renders activity to the given ActionView context with included
22
+ # AV::Helpers::RenderingHelper (most commonly just ActionView::Base)
23
+ #
24
+ # The *preferred* *way* of rendering activities is
25
+ # to provide a template specifying how the rendering should be happening.
26
+ # However, one may choose using _I18n_ based approach when developing
27
+ # an application that supports plenty of languages.
28
+ #
29
+ # If partial view exists that matches the *key* attribute
30
+ # renders that partial with local variables set to contain both
31
+ # Activity and activity_parameters (hash with indifferent access)
32
+ #
33
+ # Otherwise, it outputs the I18n translation to the context
34
+ # @example Render a list of all activities from a view (erb)
35
+ # <ul>
36
+ # <% for activity in WiserTrails::Activity.all %>
37
+ # <li><%= render_activity(activity) %></li>
38
+ # <% end %>
39
+ # </ul>
40
+ #
41
+ # = Layouts
42
+ # You can supply a layout that will be used for activity partials
43
+ # with :layout param.
44
+ # Keep in mind that layouts for partials are also partials.
45
+ # @example Supply a layout
46
+ # # in views:
47
+ # # All examples look for a layout in app/views/layouts/_activity.erb
48
+ # render_activity @activity, :layout => "activity"
49
+ # render_activity @activity, :layout => "layouts/activity"
50
+ # render_activity @activity, :layout => :activity
51
+ #
52
+ # # app/views/layouts/_activity.erb
53
+ # <p><%= a.created_at %></p>
54
+ # <%= yield %>
55
+ #
56
+ # = Creating a template
57
+ # To use templates for formatting how the activity should render,
58
+ # create a template based on activity key, for example:
59
+ #
60
+ # Given a key _activity.article.create_, create directory tree
61
+ # _app/views/wiser_trails/article/_ and create the _create_ partial there
62
+ #
63
+ # Note that if a key consists of more than three parts splitted by commas, your
64
+ # directory structure will have to be deeper, for example:
65
+ # activity.article.comments.destroy => app/views/wiser_trails/articles/comments/_destroy.html.erb
66
+ #
67
+ # == Variables in templates
68
+ # From within a template there are two variables at your disposal:
69
+ # * activity (aliased as *a* for a shortcut)
70
+ # * params (aliased as *p*) [converted into a HashWithIndifferentAccess]
71
+ #
72
+ # @example Template for key: _activity.article.create_ (erb)
73
+ # <p>
74
+ # Article <strong><%= p[:name] %></strong>
75
+ # was written by <em><%= p["author"] %></em>
76
+ # <%= distance_of_time_in_words_to_now(a.created_at) %>
77
+ # </p>
78
+ def render(context, params = {})
79
+ partial_path = nil
80
+ if params.has_key? :display
81
+ # if i18n has been requested, let it render and bail
82
+ return context.render :text => self.text(params) if params[:display].to_sym == :"i18n"
83
+ partial_path = 'wiser_trails/'+params[:display].to_s
84
+ end
85
+
86
+ controller = WiserTrails.get_controller
87
+ if layout = params.delete(:layout)
88
+ layout = layout.to_s
89
+ layout = layout[0,8] == "layouts/" ? layout : "layouts/#{layout}"
90
+ end
91
+
92
+ locals = params.delete(:locals) || Hash.new
93
+
94
+ params_indifferent = self.parameters.with_indifferent_access
95
+ params_indifferent.merge!(params)
96
+
97
+ context.render params.merge(:partial => (partial_path || self.template_path(self.key)),
98
+ :layout => layout,
99
+ :locals => locals.merge(:a => self, :activity => self,
100
+ :controller => controller,
101
+ :current_user => controller.respond_to?(:current_user) ?
102
+ controller.current_user : nil ,
103
+ :p => params_indifferent, :params => params_indifferent))
104
+ end
105
+
106
+ protected
107
+ # Builds the path to template based on activity key
108
+ # TODO: verify that attribute `key` is splitted by commas
109
+ # and that the word before first comma is equal to
110
+ # "activity"
111
+ def template_path(key)
112
+ path = key.split(".")
113
+ path.delete_at(0) if path[0] == "activity"
114
+ path.unshift "wiser_trails"
115
+ path.join("/")
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,42 @@
1
+ module WiserTrails
2
+ # Enables per-class disabling of WiserTrails functionality.
3
+ module Deactivatable
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class_attribute :wiser_trails_enabled_for_model
8
+ set_wiser_trails_class_defaults
9
+ end
10
+
11
+ # Returns true if WiserTrails is enabled
12
+ # globally and for this class.
13
+ # @return [Boolean]
14
+ # @api private
15
+ # @since 0.5.0
16
+ # overrides the method from Common
17
+ def wiser_trails_enabled?
18
+ WiserTrails.enabled? && self.class.wiser_trails_enabled_for_model
19
+ end
20
+
21
+ # Provides global methods to disable or enable WiserTrails on a per-class
22
+ # basis.
23
+ module ClassMethods
24
+ # Switches wiser_trails off for this class
25
+ def wiser_trails_off
26
+ self.wiser_trails_enabled_for_model = false
27
+ end
28
+
29
+ # Switches wiser_trails on for this class
30
+ def wiser_trails_on
31
+ self.wiser_trails_enabled_for_model = true
32
+ end
33
+
34
+ # @since 1.0.0
35
+ # @api private
36
+ def set_wiser_trails_class_defaults
37
+ super
38
+ self.wiser_trails_enabled_for_model = true
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,180 @@
1
+ module WiserTrails
2
+ # Main module extending classes we want to keep track of.
3
+ module TrailIt
4
+ extend ActiveSupport::Concern
5
+ # A shortcut method for setting custom key, owner and parameters of {Activity}
6
+ # in one line. Accepts a hash with 3 keys:
7
+ # :key, :owner, :params. You can specify all of them or just the ones you want to overwrite.
8
+ #
9
+ # == Options
10
+ #
11
+ # [:key]
12
+ # See {Common#activity_key}
13
+ # [:owner]
14
+ # See {Common#activity_owner}
15
+ # [:params]
16
+ # See {Common#activity_new_value}
17
+ # [:recipient]
18
+ # Set the recipient for this activity. Useful for private notifications, which should only be visible to a certain user. See {Common#activity_account}.
19
+ # @example
20
+ #
21
+ # @article = Article.new
22
+ # @article.title = "New article"
23
+ # @article.activity :key => "my.custom.article.key", :owner => @article.author, :params => {:title => @article.title}
24
+ # @article.save
25
+ # @article.activities.last.key #=> "my.custom.article.key"
26
+ # @article.activities.last.parameters #=> {:title => "New article"}
27
+ #
28
+ # @param options [Hash] instance options to set on the tracked model
29
+ # @return [nil]
30
+ def activity(options = {})
31
+ rest = options.clone
32
+ self.activity_key = rest.delete(:key) if rest[:key]
33
+ self.activity_owner = rest.delete(:owner) if rest[:owner]
34
+ self.activity_new_value = rest.delete(:params) if rest[:params]
35
+ self.activity_account = rest.delete(:recipient) if rest[:recipient]
36
+ self.activity_custom_fields = rest if rest.count > 0
37
+ nil
38
+ end
39
+
40
+ # Module with basic +tracked+ method that enables tracking models.
41
+ module ClassMethods
42
+ # Adds required callbacks for creating and updating
43
+ # tracked models and adds +activities+ relation for listing
44
+ # associated activities.
45
+ #
46
+ # == Parameters:
47
+ # [:owner]
48
+ # Specify the owner of the {Activity} (person responsible for the action).
49
+ # It can be a Proc, Symbol or an ActiveRecord object:
50
+ # == Examples:
51
+ #
52
+ # tracked :owner => :author
53
+ # tracked :owner => proc {|o| o.author}
54
+ #
55
+ # Keep in mind that owner relation is polymorphic, so you can't just
56
+ # provide id number of the owner object.
57
+ # [:recipient]
58
+ # Specify the recipient of the {Activity}
59
+ # It can be a Proc, Symbol, or an ActiveRecord object
60
+ # == Examples:
61
+ #
62
+ # tracked :recipient => :author
63
+ # tracked :recipient => proc {|o| o.author}
64
+ #
65
+ # Keep in mind that recipient relation is polymorphic, so you can't just
66
+ # provide id number of the owner object.
67
+ # [:params]
68
+ # Accepts a Hash with custom parameters you want to pass to i18n.translate
69
+ # method. It is later used in {Renderable#text} method.
70
+ # == Example:
71
+ # class Article < ActiveRecord::Base
72
+ # include WiserTrails::Model
73
+ # tracked :params => {
74
+ # :title => :title,
75
+ # :author_name => "Michael",
76
+ # :category_name => proc {|controller, model_instance| model_instance.category.name},
77
+ # :summary => proc {|controller, model_instance| truncate(model.text, :length => 30)}
78
+ # }
79
+ # end
80
+ #
81
+ # Values in the :params hash can either be an *exact* *value*, a *Proc/Lambda* executed before saving the activity or a *Symbol*
82
+ # which is a an attribute or a method name executed on the tracked model's instance.
83
+ #
84
+ # Everything specified here has a lower priority than parameters
85
+ # specified directly in {#activity} method.
86
+ # So treat it as a place where you provide 'default' values or where you
87
+ # specify what data should be gathered for every activity.
88
+ # For more dynamic settings refer to {Activity} model documentation.
89
+ # [:skip_defaults]
90
+ # Disables recording of activities on create/update/destroy leaving that to programmer's choice. Check {WiserTrails::Common#create_activity}
91
+ # for a guide on how to manually record activities.
92
+ # [:only]
93
+ # Accepts a symbol or an array of symbols, of which any combination of the three is accepted:
94
+ # * _:create_
95
+ # * _:update_
96
+ # * _:destroy_
97
+ # Selecting one or more of these will make WiserTrails create activities
98
+ # automatically for the tracked model on selected actions.
99
+ #
100
+ # Resulting activities will have have keys assigned to, respectively:
101
+ # * _article.create_
102
+ # * _article.update_
103
+ # * _article.destroy_
104
+ # Since only three options are valid,
105
+ # see _:except_ option for a shorter version
106
+ # [:except]
107
+ # Accepts a symbol or an array of symbols with values like in _:only_, above.
108
+ # Values provided will be subtracted from all default actions:
109
+ # (create, update, destroy).
110
+ #
111
+ # So, passing _create_ would track and automatically create
112
+ # activities on _update_ and _destroy_ actions,
113
+ # but not on the _create_ action.
114
+ # [:on]
115
+ # Accepts a Hash with key being the *action* on which to execute *value* (proc)
116
+ # Currently supported only for CRUD actions which are enabled in _:only_
117
+ # or _:except_ options on this method.
118
+ #
119
+ # Key-value pairs in this option define callbacks that can decide
120
+ # whether to create an activity or not. Procs have two attributes for
121
+ # use: _model_ and _controller_. If the proc returns true, the activity
122
+ # will be created, if not, then activity will not be saved.
123
+ #
124
+ # == Example:
125
+ # # app/models/article.rb
126
+ # tracked :on => {:update => proc {|model, controller| model.published? }}
127
+ #
128
+ # In the example above, given a model Article with boolean column _published_.
129
+ # The activities with key _article.update_ will only be created
130
+ # if the published status is set to true on that article.
131
+ # @param opts [Hash] options
132
+ # @return [nil] options
133
+ def trail_it(opts = {})
134
+ options = opts.clone
135
+
136
+ all_options = [:create, :update, :destroy]
137
+
138
+ if !options.has_key?(:skip_defaults) && !options[:only] && !options[:except]
139
+ include Creation
140
+ include Destruction
141
+ include Update
142
+ end
143
+ options.delete(:skip_defaults)
144
+
145
+ if options[:except]
146
+ options[:only] = all_options - Array(options.delete(:except))
147
+ end
148
+
149
+ if options[:only]
150
+ Array(options[:only]).each do |opt|
151
+ if opt.eql?(:create)
152
+ include Creation
153
+ elsif opt.eql?(:destroy)
154
+ include Destruction
155
+ elsif opt.eql?(:update)
156
+ include Update
157
+ end
158
+ end
159
+ options.delete(:only)
160
+ end
161
+
162
+ if options[:owner]
163
+ self.activity_owner_global = options.delete(:owner)
164
+ end
165
+ if options[:account]
166
+ self.activity_account_global = options.delete(:account)
167
+ end
168
+ if options.has_key?(:on) and options[:on].is_a? Hash
169
+ self.activity_hooks = options.delete(:on).select {|_, v| v.is_a? Proc}.symbolize_keys
170
+ end
171
+
172
+ options.each do |k, v|
173
+ self.activity_custom_fields_global[k] = v
174
+ end
175
+
176
+ nil
177
+ end
178
+ end
179
+ end
180
+ end