effective_logging 1.4.1 → 1.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f4ffc97c7a82cc7200aa95e8c47c498816bbce9b
4
- data.tar.gz: bb2df38354a9a0cb055f91ab5a07bae5199ac8ea
3
+ metadata.gz: 1df4e571d32d8173adad1e00cc1df96abf6cab61
4
+ data.tar.gz: e17269c8ad802e902237ce2ac10339da99c04cf7
5
5
  SHA512:
6
- metadata.gz: 56fa1f182939c81552fdc31bccc3c0976a7a3052c92963ad6c74e6ebab551c2f455dc8294c21690607bde318707054d3d2b1a94603ea9f9af1c99d0cc7a69a23
7
- data.tar.gz: 7d8631e9a817b077e1d6615092c62b6a865750ab30934cfa59719306e0ba1e5fa7ceb7f8f8ee0915569c4ac45e9c84ee0adc786e285c3f07be0103111b241101
6
+ metadata.gz: 583ddd1c3f1cf4cba8a6e28b5163909b44944dc51350b1cee16a8fc22b3fdaf8bb935505621f27caf742ad321ef5fefb55ced9a1650af737ee80296fc5ea443f
7
+ data.tar.gz: 14d0148fd0d692dd55214a38fe513f32285b8003c5557042b42ec3ec3295e961355ca885e09b1bb933e1154ed2e84df8770e4f663899d02a8994c311c042a0b2
data/README.md CHANGED
@@ -89,6 +89,13 @@ log.error('record 2 failed to import', :associated => @record2)
89
89
  log.success('record 3 imported', :associated => @record3)
90
90
  ```
91
91
 
92
+ ### Model
93
+
94
+ (optional) Sometimes you'd like to work with the `Effective::Log` relationship directly. Add to your model:
95
+
96
+ ```ruby
97
+ has_many :logs, class_name: Effective::Log, as: :associated
98
+ ```
92
99
 
93
100
  ### Automatic Logging of E-mails
94
101
 
@@ -185,6 +192,46 @@ end
185
192
  Similarly, skip_log_page_views also accepts any options that a before_filter would accept.
186
193
 
187
194
 
195
+ ### Logging ActiveRecord changes
196
+
197
+ All changes made to an ActiveRecord object can be logged.
198
+
199
+ This logging includes the resource's base attributes, all autosaved associations (any accepts_nested_attributes has_manys) and the string representation of all belongs_tos.
200
+
201
+ It will recurse through all accepts_nested_attributes has_many's and handle any number of child objects.
202
+
203
+ Add to your model:
204
+
205
+ ```ruby
206
+ class Post < ActiveRecord::Base
207
+ log_changes
208
+ end
209
+ ```
210
+
211
+ And to your controller:
212
+
213
+ ```ruby
214
+ class ApplicationController < ActionController::Base
215
+ set_log_changes_user
216
+ end
217
+ ```
218
+
219
+ Then to see the log for this resource, on any view:
220
+
221
+ ```erb
222
+ <%= render_datatable(@post.log_changes_datatable) %>
223
+ ```
224
+
225
+ The `log_changes` mixin sets up `before_save`, `before_destroy` and `after_commit` hooks to record both an activity log, and an audit log of all changes.
226
+
227
+ Each changed attribute will have its before and after values logged to form an activity log.
228
+
229
+ And on each create / destroy / update, a full dump of all current attributes is saved, forming an audit log.
230
+
231
+ There is some initial support for passing `only`, `except`, and `additionally` to the mixin to customize what attributes are saved.
232
+
233
+ Define your model with `log_changes additionally: [:method1, :method2]` to also log the value of methods.
234
+
188
235
  ### Logging From JavaScript
189
236
 
190
237
  First, require the javascript in your application.js:
@@ -42,14 +42,14 @@ module Effective
42
42
 
43
43
  # This is the User show event
44
44
  def show
45
- @log = Effective::Log.where(:user_id => current_user.id).includes(:logs).find(params[:id])
46
- @log.next_log = Effective::Log.unscoped.where(:user_id => current_user.id).order(:id).where(:parent_id => @log.parent_id).where('id > ?', @log.id).first
47
- @log.prev_log = Effective::Log.unscoped.where(:user_id => current_user.id).order(:id).where(:parent_id => @log.parent_id).where('id < ?', @log.id).last
45
+ @log = Effective::Log.includes(:logs).find(params[:id])
46
+ @log.next_log = Effective::Log.unscoped.order(:id).where(parent_id: @log.parent_id).where('id > ?', @log.id).first
47
+ @log.prev_log = Effective::Log.unscoped.order(:id).where(parent_id: @log.parent_id).where('id < ?', @log.id).last
48
48
 
49
49
  @page_title = "Log ##{@log.to_param}"
50
50
 
51
51
  if @log.logs.present?
52
- @log.datatable = Effective::Datatables::Logs.new(:user_id => current_user.id, :log_id => @log.id) if defined?(EffectiveDatatables)
52
+ @log.datatable = Effective::Datatables::Logs.new(log_id: @log.id) if defined?(EffectiveDatatables)
53
53
  end
54
54
 
55
55
  EffectiveLogging.authorized?(self, :show, @log)
@@ -64,7 +64,7 @@ module EffectiveLoggingHelper
64
64
  value = log.details[key]
65
65
 
66
66
  if value.kind_of?(Hash)
67
- tableize_hash(value, :class => 'table', :style => 'width: 50%', :th => true)
67
+ tableize_hash(value, :class => 'table', :th => true)
68
68
  elsif value.kind_of?(Array)
69
69
  value.map { |value| effective_logging_simple_format(value) }.join.html_safe
70
70
  else
@@ -0,0 +1,68 @@
1
+ module ActsAsLoggable
2
+ extend ActiveSupport::Concern
3
+
4
+ module ActiveRecord
5
+ def log_changes(*options)
6
+ @acts_as_loggable_options = options.try(:first) || {}
7
+
8
+ unless @acts_as_loggable_options.kind_of?(Hash)
9
+ raise ArgumentError.new("invalid arguments passed to (effective_logging) log_changes. Example usage: log_changes except: [:created_at]")
10
+ end
11
+
12
+ include ::ActsAsLoggable
13
+ end
14
+ end
15
+
16
+ included do
17
+ has_many :logged_changes, -> { where(status: EffectiveLogging.log_changes_status) }, as: :associated, class_name: Effective::Log
18
+
19
+ before_save do
20
+ @acts_as_loggable_new_record = new_record?
21
+ EffectiveLogging::ActiveRecordLogger.new(self, log_changes_options).changed! unless @acts_as_loggable_new_record
22
+ true
23
+ end
24
+
25
+ before_destroy do
26
+ EffectiveLogging::ActiveRecordLogger.new(self, log_changes_options).destroyed!
27
+ true
28
+ end
29
+
30
+ after_commit do
31
+ if @acts_as_loggable_new_record
32
+ EffectiveLogging::ActiveRecordLogger.new(self, log_changes_options).created!
33
+ else
34
+ EffectiveLogging::ActiveRecordLogger.new(self, log_changes_options).updated!
35
+ end
36
+ true
37
+ end
38
+
39
+ # Parse Options
40
+ log_changes_options = {
41
+ only: Array(@acts_as_loggable_options[:only]).map { |attribute| attribute.to_s },
42
+ except: Array(@acts_as_loggable_options[:except]).map { |attribute| attribute.to_s },
43
+ additionally: Array(@acts_as_loggable_options[:additionally]).map { |attribute| attribute.to_s }
44
+ }
45
+
46
+ self.send(:define_method, :log_changes_options) { log_changes_options }
47
+ end
48
+
49
+ module ClassMethods
50
+ end
51
+
52
+ # Regular instance methods
53
+
54
+ def log_changes_datatable
55
+ if persisted?
56
+ @log_changes_datatable ||= (
57
+ Effective::Datatables::Logs.new(
58
+ associated_id: id,
59
+ associated_type: self.class.name,
60
+ status: false,
61
+ log_changes: true
62
+ )
63
+ )
64
+ end
65
+ end
66
+
67
+ end
68
+
@@ -15,12 +15,17 @@ if defined?(EffectiveDatatables)
15
15
  end
16
16
 
17
17
  unless attributes[:status] == false
18
- table_column :status, filter: { type: :select, values: EffectiveLogging.statuses }
18
+ table_column :status, filter: { type: :select, values: (EffectiveLogging.statuses + [EffectiveLogging.log_changes_status]) }
19
+ end
20
+
21
+ table_column :message do |log|
22
+ log.message.starts_with?("\t") ? log.message.gsub("\t", "&nbsp;&nbsp;") : log.message
19
23
  end
20
24
 
21
- table_column :message
22
25
  table_column :logs_count, visible: false
23
26
 
27
+ table_column :associated, filter: false, sortable: false, visible: false
28
+
24
29
  table_column :details, visible: false, sortable: false do |log|
25
30
  tableize_hash(log.details.except(:email), th: true, sub_th: false, width: '100%')
26
31
  end
@@ -47,6 +52,10 @@ if defined?(EffectiveDatatables)
47
52
  collection = collection.where(associated_id: attributes[:associated_id], associated_type: attributes[:associated_type])
48
53
  end
49
54
 
55
+ if attributes[:log_changes]
56
+ collection = collection.where(status: EffectiveLogging.log_changes_status)
57
+ end
58
+
50
59
  if attributes[:associated]
51
60
  collection = collection.where(associated: attributes[:associated])
52
61
  end
@@ -8,13 +8,13 @@ module Effective
8
8
  attr_accessor :prev_log
9
9
 
10
10
  # Self-Referencing relationship
11
- belongs_to :parent, :class_name => 'Effective::Log', :counter_cache => true
12
- has_many :logs, :dependent => :destroy, :class_name => 'Effective::Log', :foreign_key => :parent_id
11
+ belongs_to :parent, class_name: 'Effective::Log', counter_cache: true
12
+ has_many :logs, class_name: 'Effective::Log', foreign_key: :parent_id
13
13
 
14
14
  # The user this log item is referring to
15
15
  # An associated object, if we wanna add anything extra
16
16
  belongs_to :user
17
- belongs_to :associated, :polymorphic => true
17
+ belongs_to :associated, polymorphic: true
18
18
 
19
19
  serialize :details, Hash
20
20
 
@@ -26,17 +26,17 @@ module Effective
26
26
  # timestamps
27
27
  # end
28
28
 
29
- validates_presence_of :message, :status
30
- validates_inclusion_of :status, in: EffectiveLogging.statuses
29
+ validates :message, presence: true
30
+ validates :status, presence: true, inclusion: { in: (EffectiveLogging.statuses + [EffectiveLogging.log_changes_status]) }
31
31
 
32
- default_scope -> { order("#{EffectiveLogging.logs_table_name.to_s}.updated_at DESC") }
32
+ default_scope -> { order(updated_at: :desc) }
33
33
 
34
34
  def log(message, status = EffectiveLogging.statuses.first, options = {})
35
35
  EffectiveLogger.log(message, status, (options || {}).merge({:parent => self}))
36
36
  end
37
37
 
38
38
  def details
39
- ((self[:details] ||= {}) rescue {})
39
+ self[:details] || {}
40
40
  end
41
41
 
42
42
  # def next_log
@@ -1,10 +1,9 @@
1
1
  require 'haml-rails'
2
2
  require 'effective_logging/engine'
3
3
  require 'effective_logging/version'
4
- require 'effective_logging/log_page_views'
5
- require 'effective_logging/user_logger'
6
4
 
7
5
  module EffectiveLogging
6
+
8
7
  # The following are all valid config keys
9
8
  mattr_accessor :logs_table_name
10
9
  mattr_accessor :use_active_admin
@@ -39,4 +38,17 @@ module EffectiveLogging
39
38
  @statuses ||= (Array(@@additional_statuses).map { |status| status.to_s.downcase } | ['info', 'success', 'error'])
40
39
  end
41
40
 
41
+ def self.log_changes_status
42
+ 'logged change'.freeze
43
+ end
44
+
45
+ # This is set by the "set_log_changes_user" before_filter.
46
+ def self.log_changes_user=(user)
47
+ @log_changes_user = user
48
+ end
49
+
50
+ def self.log_changes_user
51
+ @log_changes_user
52
+ end
53
+
42
54
  end
@@ -0,0 +1,131 @@
1
+ module EffectiveLogging
2
+ class ActiveRecordLogger
3
+ attr_accessor :resource, :logger, :depth, :options
4
+
5
+ def initialize(resource, options = {})
6
+ raise ArgumentError.new('options must be a Hash') unless options.kind_of?(Hash)
7
+ raise ArgumentError.new('logger must respond to logged_changes') unless (options[:logger] || resource).respond_to?(:logged_changes)
8
+
9
+ @resource = resource
10
+ @logger = options.delete(:logger) || resource
11
+ @depth = options.delete(:depth) || 0
12
+ @options = options
13
+ end
14
+
15
+ # execute! is called when we recurse, otherwise the following methods are best called individually
16
+ def execute!
17
+ if resource.new_record?
18
+ created!
19
+ elsif resource.marked_for_destruction?
20
+ destroyed!
21
+ else
22
+ changed!
23
+ end
24
+ end
25
+
26
+ # before_destroy
27
+ def destroyed!
28
+ log('Deleted', applicable(attributes))
29
+ end
30
+
31
+ # after_commit
32
+ def created!
33
+ log('Created', applicable(attributes))
34
+ end
35
+
36
+ # after_commit
37
+ def updated!
38
+ log('Updated', applicable(attributes))
39
+ end
40
+
41
+ # before_save
42
+ def changed!
43
+ applicable(changes).each do |attribute, (before, after)|
44
+ if after.present?
45
+ log("#{attribute.titleize} changed from '#{before}' to '#{after}'", { attribute: attribute, before: before, after: after })
46
+ else
47
+ log("#{attribute.titleize} set to '#{before}'", { attribute: attribute, value: before })
48
+ end
49
+ end
50
+
51
+ # Log changes on all accepts_as_nested_parameters has_many associations
52
+ (resource.class.try(:reflect_on_all_autosave_associations) || []).each do |association|
53
+ child_name = association.name.to_s.singularize.titleize
54
+
55
+ resource.send(association.name).each_with_index do |child, index|
56
+ ActiveRecordLogger.new(child, options.merge(logger: logger, depth: (depth + 1), prefix: "#{child_name} ##{index+1}: ")).execute!
57
+ end
58
+ end
59
+ end
60
+
61
+ def attributes
62
+ attributes = { attributes: resource.attributes }
63
+
64
+ # Collect to_s representations of all belongs_to associations
65
+ (resource.class.try(:reflect_on_all_associations, :belongs_to) || []).each do |association|
66
+ attributes[association.name] = resource.send(association.name).to_s.presence || 'nil'
67
+ end
68
+
69
+ # Collect to_s representations for all has_one associations
70
+ (resource.class.try(:reflect_on_all_associations, :has_one) || []).each do |association|
71
+ attributes[association.name] = resource.send(association.name).to_s.presence || 'nil'
72
+ end
73
+
74
+ # Collects attributes for all accepts_as_nested_parameters has_many associations
75
+ (resource.class.try(:reflect_on_all_autosave_associations) || []).each do |association|
76
+ attributes[association.name] = {}
77
+
78
+ resource.send(association.name).each_with_index do |child, index|
79
+ attributes[association.name][index+1] = ActiveRecordLogger.new(child, options.merge(logger: logger)).attributes
80
+ end
81
+ end
82
+
83
+ attributes
84
+ end
85
+
86
+ def changes
87
+ changes = resource.changes
88
+
89
+ # Log to_s changes on all belongs_to associations
90
+ (resource.class.try(:reflect_on_all_associations, :belongs_to) || []).each do |association|
91
+ if (change = changes.delete(association.foreign_key)).present?
92
+ changes[association.name] = [association.klass.find_by_id(change.first), resource.send(association.name)]
93
+ end
94
+ end
95
+
96
+ changes
97
+ end
98
+
99
+ private
100
+
101
+ def log(message, details = {})
102
+ logger.logged_changes.build(
103
+ user: EffectiveLogging.log_changes_user,
104
+ status: EffectiveLogging.log_changes_status,
105
+ message: "#{"\t" * depth}#{options[:prefix]}#{message}",
106
+ details: details
107
+ ).tap { |log| log.save }
108
+ end
109
+
110
+ # TODO: Make this work better with nested objects
111
+ def applicable(attributes)
112
+ atts = if options[:only].present?
113
+ attributes.slice(*options[:only])
114
+ elsif options[:except].present?
115
+ attributes.except(*options[:except])
116
+ else
117
+ attributes
118
+ end
119
+
120
+ options[:additionally].each do |attribute|
121
+ value = (resource.send(attribute) rescue :effective_logging_nope)
122
+ next if attributes[attribute].present? || value == :effective_logging_nope
123
+
124
+ atts[attribute] = value
125
+ end
126
+
127
+ atts
128
+ end
129
+
130
+ end
131
+ end
@@ -2,6 +2,8 @@ module EffectiveLogging
2
2
  class Engine < ::Rails::Engine
3
3
  engine_name 'effective_logging'
4
4
 
5
+ config.autoload_paths += Dir["#{config.root}/lib/"]
6
+
5
7
  # Set up our default configuration options.
6
8
  initializer "effective_logging.defaults", :before => :load_config_initializers do |app|
7
9
  eval File.read("#{config.root}/lib/generators/templates/effective_logging.rb")
@@ -23,6 +25,20 @@ module EffectiveLogging
23
25
  end
24
26
  end
25
27
 
28
+ # Include acts_as_loggable concern and allow any ActiveRecord object to call it with log_changes()
29
+ initializer 'effective_logging.active_record' do |app|
30
+ ActiveSupport.on_load :active_record do
31
+ ActiveRecord::Base.extend(ActsAsLoggable::ActiveRecord)
32
+ end
33
+ end
34
+
35
+ # Register the log_page_views concern so that it can be called in ActionController or elsewhere
36
+ initializer 'effective_logging.log_changes_action_controller' do |app|
37
+ ActiveSupport.on_load :action_controller do
38
+ ActionController::Base.include(EffectiveLogging::LogChangesUser)
39
+ end
40
+ end
41
+
26
42
  # Register the log_page_views concern so that it can be called in ActionController or elsewhere
27
43
  initializer 'effective_logging.action_controller' do |app|
28
44
  ActiveSupport.on_load :action_controller do
@@ -0,0 +1,17 @@
1
+ module EffectiveLogging
2
+ module LogChangesUser
3
+
4
+ # Add me to your ApplicationController
5
+ # before_action :set_log_changes_user
6
+
7
+ def set_log_changes_user
8
+ if responds_to?(:current_user)
9
+ EffectiveLogging.log_changes_user = current_user
10
+ else
11
+ raise "(effective_logging) set_log_changes_user expects a current_user() method to be available"
12
+ end
13
+ end
14
+
15
+ end
16
+ end
17
+
@@ -1,3 +1,3 @@
1
1
  module EffectiveLogging
2
- VERSION = '1.4.1'.freeze
2
+ VERSION = '1.5.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: effective_logging
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.1
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Code and Effect
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-03-12 00:00:00.000000000 Z
11
+ date: 2016-06-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -96,6 +96,7 @@ files:
96
96
  - app/controllers/admin/logs_controller.rb
97
97
  - app/controllers/effective/logs_controller.rb
98
98
  - app/helpers/effective_logging_helper.rb
99
+ - app/models/concerns/acts_as_loggable.rb
99
100
  - app/models/effective/access_denied.rb
100
101
  - app/models/effective/datatables/logs.rb
101
102
  - app/models/effective/log.rb
@@ -111,8 +112,10 @@ files:
111
112
  - config/routes.rb
112
113
  - db/migrate/01_create_effective_logging.rb.erb
113
114
  - lib/effective_logging.rb
115
+ - lib/effective_logging/active_record_logger.rb
114
116
  - lib/effective_logging/email_logger.rb
115
117
  - lib/effective_logging/engine.rb
118
+ - lib/effective_logging/log_changes_user.rb
116
119
  - lib/effective_logging/log_page_views.rb
117
120
  - lib/effective_logging/user_logger.rb
118
121
  - lib/effective_logging/version.rb