active-audit 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e1fca837bb3e2d692f664310ebe1beffad3484bd
4
+ data.tar.gz: a7f74a56a41b4443b00de87699df32bef7278d48
5
+ SHA512:
6
+ metadata.gz: 5cc11c9ece3877bac607d8083b56bf40e649a929c91768abe22174cdf32527b93c60bc4c917d6e529f79e704957cbd0fc509b27061c3608dba2cc82938fbe8e6
7
+ data.tar.gz: 9d089b1ac422adf173792fa6a4ae1c326c9bc62cbf1d951bdda6bb9ea9ae92bd811add470032b166530606dd12718316b2f53870ff5c91d2d7f52d3e287af24b
@@ -0,0 +1,34 @@
1
+ require 'active_support'
2
+ require 'active_audit/audit_repository'
3
+
4
+ module ActiveAudit
5
+ extend ActiveSupport::Autoload
6
+
7
+ eager_autoload { autoload :Base }
8
+ autoload :StorageAdapters
9
+
10
+ class << self
11
+
12
+ attr_accessor :storage_adapter, :current_user_method, :ignored_attributes, :job_queue, :delayed_auditing, :default_user, :extract_user_profile
13
+
14
+ def configure
15
+ @current_user_method = :current_user
16
+ @ignored_attributes = %w(created_at updated_at)
17
+ @job_queue = :audits
18
+ @delayed_auditing = false
19
+ @extract_user_profile = lambda { |user| { id: user.id } }
20
+ self.eager_load!
21
+ yield(self) if block_given?
22
+ AuditRepository.storage_adapter = storage_adapter if storage_adapter
23
+ end
24
+
25
+ def session
26
+ Thread.current[:auditing_store] ||= {}
27
+ end
28
+
29
+ def add_hint comment
30
+ self.session[:comment] = comment
31
+ end
32
+ end
33
+
34
+ end
@@ -0,0 +1,83 @@
1
+ require 'rails/observers/active_model/active_model'
2
+ require 'active_model'
3
+ require 'virtus'
4
+
5
+ module ActiveAudit
6
+ class Audit
7
+ include Virtus.model
8
+
9
+ extend ActiveModel::Callbacks
10
+ define_model_callbacks :create, :save
11
+
12
+ include ActiveModel::Observing
13
+
14
+ attribute :item_id, Integer
15
+ attribute :event, String
16
+ attribute :type, String
17
+ attribute :changes, Hash
18
+ attribute :user, Hash, default: lambda {|o,a| ActiveAudit.default_user }
19
+ attribute :comment, String
20
+ attribute :recorded_at, Time, default: lambda { |o,a| Time.now.utc }
21
+
22
+ before_save do
23
+ self.recorded_at = Time.at(self.recorded_at) if self.recorded_at.is_a? Integer
24
+ self.user = ActiveAudit.extract_user_profile.call(self.user) unless self.user.nil? || self.user.is_a?(Hash)
25
+ end
26
+
27
+ def initialize *args
28
+ attributes = args.extract_options!
29
+ if attributes.empty?
30
+ if args.count == 2
31
+ initialize_from_record(*args)
32
+ else
33
+ raise ArgumentError, "You need to supply at least one attribute"
34
+ end
35
+ else
36
+ super attributes
37
+ end
38
+ end
39
+
40
+ private def initialize_from_record event, record
41
+ if event == 'update'
42
+ if record.respond_to?(:aasm) && record.aasm.current_event
43
+ event = record.aasm.current_event.to_s.sub(/!$/, '')
44
+ end
45
+ self.changes = record.previous_changes.select { |key, value| record.class.auditing_options[:only].include? key }
46
+ self.changes.merge!(record.association_previous_changes.select { |key, value| record.class.auditing_options[:associations].include? key })
47
+ self.changes = self.changes.map do |key, values|
48
+ if values.any? { |v| v.is_a?(Time) }
49
+ [key, values.map { |x| x && x.utc.iso8601 }]
50
+ else
51
+ [key, values]
52
+ end
53
+ end.to_h
54
+ end
55
+ self.event = event
56
+ self.type = record.class.auditing_options[:type]
57
+ self.item_id = record.id
58
+ set_default_attributes
59
+ end
60
+
61
+ def changed?
62
+ ['create', 'destroy'].include?(self.event) || changes.present?
63
+ end
64
+
65
+ def save options={}
66
+ self.run_callbacks :save do
67
+ yield if changed?
68
+ end
69
+ end
70
+
71
+ def self.create(attributes, options={})
72
+ object = self.new(attributes)
73
+ object.run_callbacks :create do
74
+ object.save(options)
75
+ object
76
+ end
77
+ end
78
+
79
+ def serialize
80
+ self.attributes.select {|k,v| v.present?}.merge(recorded_at: self.recorded_at.to_i, type: self.type)
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,10 @@
1
+ module ActiveAudit
2
+ class AuditPusher < ActiveJob::Base
3
+ queue_as ActiveAudit.job_queue
4
+
5
+ def perform *args
6
+ AuditRepository.create(args[0])
7
+ end
8
+
9
+ end
10
+ end
@@ -0,0 +1,38 @@
1
+ module ActiveAudit
2
+ class AuditRepository
3
+ class << self
4
+ def storage_adapter=(repo_name)
5
+ @@storage_adapter = load_adapter(repo_name)
6
+ end
7
+
8
+ def storage_adapter
9
+ @@storage_adapter ||= ActiveAudit::StorageAdapters::TestAdapter
10
+ end
11
+
12
+ def find_by_record record, options={}
13
+ storage_adapter.find_by_record(record, options)
14
+ end
15
+
16
+ def create attributes
17
+ save Audit.new(attributes)
18
+ end
19
+
20
+ def save audit
21
+ audit.save do
22
+ storage_adapter.save audit
23
+ end
24
+ end
25
+
26
+ private
27
+ def load_adapter name
28
+ config = \
29
+ begin
30
+ Rails.application.config_for(:active_audit)
31
+ rescue RuntimeError
32
+ {}
33
+ end
34
+ return "ActiveAudit::StorageAdapters::#{name.to_s.camelize}Adapter".constantize.new config
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,82 @@
1
+ require 'active_audit/errors'
2
+ require 'active_audit/audit'
3
+ require 'active_audit/audit_pusher'
4
+ require 'active_audit/dirty_association'
5
+ require 'active_audit/sweeper'
6
+
7
+ module ActiveAudit
8
+ module Base
9
+ extend ActiveSupport::Concern
10
+ include DirtyAssociation
11
+
12
+ class_methods do
13
+
14
+ def auditing_options
15
+ @auditing_options ||= {
16
+ type: self.to_s.underscore,
17
+ only: self.attribute_names - ActiveAudit.ignored_attributes
18
+ }
19
+ end
20
+
21
+ def audit *args
22
+ options = args.extract_options!.dup
23
+ options.assert_valid_keys(:type, :except, :associations, :unless)
24
+ options[:type] = options[:type].to_s if options[:type]
25
+ except = options[:except] ? options[:except].map(&:to_s) : []
26
+ only = if args.present? then args.map(&:to_s) else auditing_options[:only] end
27
+ options[:only] = only - except
28
+ if options[:associations]
29
+ options[:associations] = options[:associations].map(&:to_s)
30
+ stain *options[:associations]
31
+ end
32
+ auditing_options.update options
33
+ end
34
+
35
+ end
36
+
37
+ included do
38
+ if self < ActiveRecord::Base
39
+ after_commit on: [:create] { write_audit('create') }
40
+ after_commit on: [:update] { write_audit('update') }
41
+ after_commit on: [:destroy] { write_audit('destroy') }
42
+ elsif defined?(Mongoid::Document) && self < Mongoid::Document
43
+ after_create { write_audit('create') }
44
+ after_update { write_audit('update') }
45
+ after_destroy { write_audit('destroy') }
46
+ else
47
+ raise Errors::UnsupportedModel, "can audit ActiveRecord and Mongoid models only"
48
+ end
49
+ end
50
+
51
+ def audits options={}
52
+ AuditRepository.find_by_record self, options
53
+ end
54
+
55
+ private
56
+ def audited?
57
+ if condition = self.class.auditing_options[:unless]
58
+ case condition
59
+ when Symbol, String
60
+ self.public_send condition
61
+ when Proc
62
+ condition.call self
63
+ end
64
+ end
65
+ return true
66
+ end
67
+
68
+ def write_audit event
69
+ if audited?
70
+ audit = Audit.new event, self
71
+ if audit.changed?
72
+ if ActiveAudit.delayed_auditing
73
+ AuditPusher.perform_later audit.serialize
74
+ else
75
+ AuditRepository.save(audit)
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ end
82
+ end
@@ -0,0 +1,95 @@
1
+ require 'active_support/hash_with_indifferent_access'
2
+
3
+ module ActiveAudit
4
+ module DirtyAssociation
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ def stain *association_names
9
+ association_names.each do |association_name|
10
+ if reflection = _reflect_on_association(association_name)
11
+ if reflection.collection? && through_refl = reflection.try(:through_reflection)
12
+ through_id = reflection.foreign_key
13
+ through_name = through_refl.name
14
+ define_association_method_suffix association_name
15
+ ActiveRecord::Associations::Builder::CollectionAssociation.define_callback(self, :after_add, through_name, before_add: lambda do |model, relation|
16
+ puts "addubng"
17
+ puts relation.attributes
18
+ DirtyAssociation.record_add association_name, model, relation, through_name, through_id
19
+ end)
20
+ ActiveRecord::Associations::Builder::CollectionAssociation.define_callback(self, :before_remove, through_name, before_remove: lambda do |model, relation|
21
+ DirtyAssociation.record_remove association_name, model, relation, through_name, through_id
22
+ end)
23
+ else
24
+ raise ArgumentError, "'#{association_name}' is not a many-to-many association."
25
+ end
26
+ else
27
+ raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
28
+ end
29
+ end
30
+ end
31
+
32
+
33
+ private
34
+ def define_association_method_suffix association_name
35
+ %w(_changed? _change _was _previously_changed? _previous_change).each do |suffix|
36
+ self.send :define_method, "#{association_name}#{suffix}" do
37
+ send "association#{suffix}", association_name
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ attr_reader :association_previous_changes
44
+
45
+ def association_changes
46
+ @association_changes ||= ActiveSupport::HashWithIndifferentAccess.new
47
+ end
48
+
49
+ def association_changed? association_name
50
+ !!association_changes[association_name]
51
+ end
52
+
53
+ def association_change association_name
54
+ association_changes[association_name]
55
+ end
56
+
57
+ def association_was association_name
58
+ association_changes[association_name].try '[]', 0
59
+ end
60
+
61
+ def association_previously_changed? association_name
62
+ !!association_previous_changes[association_name]
63
+ end
64
+
65
+ def association_previous_change association_name
66
+ association_previous_changes[association_name]
67
+ end
68
+
69
+ def changes_applied
70
+ super
71
+ @association_previous_changes = association_changes
72
+ @association_changes = ActiveSupport::HashWithIndifferentAccess.new
73
+ end
74
+
75
+ module_function
76
+ def init_association_change association_name, model, through, attribute
77
+ old_attributes = model.send(through).map {|r| r.send attribute }
78
+ model.association_changes[association_name] = [old_attributes, old_attributes.dup]
79
+ end
80
+
81
+ def record_add association_name, model, relation, through, attribute
82
+ unless model.association_changes[association_name]
83
+ init_association_change association_name, model, through, attribute
84
+ end
85
+ model.association_changes[association_name][1].push relation.send(attribute)
86
+ end
87
+
88
+ def record_remove association_name, model, relation, through, attribute
89
+ unless model.association_changes[association_name]
90
+ init_association_change association_name, model, through, attribute
91
+ end
92
+ model.association_changes[association_name][1].delete relation.send(attribute)
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveAudit
2
+ module Errors
3
+ class UnsupportedModel < StandardError; end
4
+ end
5
+ end
@@ -0,0 +1,28 @@
1
+ module ActiveAudit
2
+ module StorageAdapters
3
+ class ActiveRecordAdapter
4
+
5
+ def initialize options={}
6
+ @connection = ::ActiveRecord::Base.connection
7
+ end
8
+
9
+ def find_by_record record, options={}
10
+ result = @connection.exec_query "SELECT * FROM audits WHERE item_id=$1 AND type=$2 ORDER BY recorded_at DESC;", nil, [[nil, record.id], [nil,record.class.auditing_options[:type]]]
11
+ result.map { |attributes| ActiveAudit::Audit.new attributes }
12
+ end
13
+
14
+ def save audit
15
+ @connection.exec_query('INSERT INTO audits("item_id", "event", "type", "changes", "user_id", "user", "comment", "recorded_at") VALUES ($1,$2,$3,$4,$5,$6,$7,$8);', nil, [
16
+ [nil, audit.item_id],
17
+ [nil, audit.event],
18
+ [nil, audit.type],
19
+ [nil, audit.changes.to_json],
20
+ [nil, audit.user[:id]],
21
+ [nil, audit.user.to_json],
22
+ [nil, audit.comment],
23
+ [nil, audit.recorded_at]
24
+ ])
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,66 @@
1
+ require 'elasticsearch/persistence'
2
+
3
+ module ActiveAudit
4
+ module StorageAdapters
5
+ class ElasticsearchAdapter
6
+ include ::Elasticsearch::Persistence::Repository
7
+
8
+ def initialize options={}
9
+ index options['index'] || :auditing
10
+ client ::Elasticsearch::Client.new url: options['url'] || 'localhost:9200'
11
+ klass ActiveAudit::Audit
12
+ end
13
+
14
+ type :_default_
15
+
16
+ settings do
17
+ mappings _all: { enabled: false }, date_detection: false, dynamic_templates: [ {
18
+ strings: {
19
+ mapping: {
20
+ index: "not_analyzed",
21
+ type: "string"
22
+ },
23
+ match: "*",
24
+ match_mapping_type: "string"
25
+ }
26
+ } ] do
27
+ indexes :item_id, type: "long"
28
+ indexes :event, type: "string", index: "not_analyzed"
29
+ indexes :changes, type: "object", dynamic: true
30
+ indexes :user, type: "object", properties: {
31
+ id: { type: "long" },
32
+ name: {type: "string", index: "not_analyzed"}
33
+ }
34
+ indexes :comment, type: "string", index: "not_analyzed"
35
+ indexes :recorded_at, type: "date"
36
+ end
37
+ end
38
+
39
+ def find_by_record record, options={}
40
+ self.type "#{record.class.auditing_options[:type]}_events"
41
+ query = {
42
+ query: { filtered: { filter: { bool: { must: [{ term: { item_id: record.id }}]}}}},
43
+ sort: { recorded_at: { order: "desc" }}
44
+ }
45
+ if options[:from] && options[:to]
46
+ query[:query][:filtered][:filter][:bool][:must].push range: { recorded_at: { gte: options[:from], lt: options[:to] }}
47
+ elsif options[:from]
48
+ query[:query][:filtered][:filter][:bool][:must].push range: { recorded_at: { gte: options[:from] }}
49
+ elsif options[:to]
50
+ query[:query][:filtered][:filter][:bool][:must].push range: { recorded_at: { lt: options[:to] }}
51
+ end
52
+ if options[:comment]
53
+ query[:query][:filtered][:filter][:bool][:must].push wildcard: { comment: "*#{options[:comment]}*" }
54
+ end
55
+ search(query).to_a
56
+ end
57
+
58
+ define_method :gateway_save, self.new.gateway.method(:save).to_proc
59
+
60
+ def save audit
61
+ self.send :gateway_save, audit, type: "#{audit.type}_events"
62
+ end
63
+
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,22 @@
1
+ require 'mongo'
2
+
3
+ module ActiveAudit
4
+ module StorageAdapters
5
+ class MongoAdapter
6
+
7
+ def initialize options={}
8
+ client = ::Mongo::Client.new options['hosts'], options['options'].symbolize_keys
9
+ @collection = client[:audits]
10
+ end
11
+
12
+ def find_by_record record, options={}
13
+ result = @collection.find(type: record.class.auditing_options[:type], item_id: record.id)
14
+ result.map { |audit| ActiveAudit::Audit.new audit.symbolize_keys }
15
+ end
16
+
17
+ def save audit
18
+ @collection.insert_one(audit.attributes)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ module ActiveAudit
2
+ module StorageAdapters
3
+ class TestAdapter
4
+
5
+ class << self
6
+ def find_by_record record, options={}
7
+ raise NotImplementedError.new "can not retrieve audits with `TestAdapter`."
8
+ end
9
+
10
+ def save audit
11
+ Rails.logger.info "audit -> #{audit.attributes}"
12
+ end
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,10 @@
1
+ module ActiveAudit
2
+ module StorageAdapters
3
+ extend ActiveSupport::Autoload
4
+
5
+ autoload :ActiveRecordAdapter
6
+ autoload :ElasticsearchAdapter
7
+ autoload :MongoAdapter
8
+ autoload :TestAdapter
9
+ end
10
+ end
@@ -0,0 +1,55 @@
1
+ require "rails/observers/activerecord/active_record"
2
+ require "rails/observers/action_controller/caching"
3
+
4
+ module ActiveAudit
5
+ class Sweeper < ActionController::Caching::Sweeper
6
+ observe ActiveAudit::Audit
7
+
8
+ def around controller
9
+ begin
10
+ self.controller = controller
11
+ self.user = current_user
12
+ yield
13
+ ensure
14
+ self.controller = nil
15
+ self.user = nil
16
+ end
17
+ end
18
+
19
+ def before_create audit
20
+ audit.user = self.user if self.user
21
+ audit.comment = controller.params[:comment] if controller.respond_to?(:params, true)
22
+ audit.comment ||= ActiveAudit.session[:comment]
23
+ end
24
+
25
+ def current_user
26
+ current_user = controller.send(ActiveAudit.current_user_method) if controller.respond_to?(ActiveAudit.current_user_method, true)
27
+ end
28
+
29
+ def controller
30
+ ActiveAudit.session[:current_controller]
31
+ end
32
+
33
+ def controller=(value)
34
+ ActiveAudit.session[:current_controller] = value
35
+ end
36
+
37
+ def user
38
+ ActiveAudit.session[:current_user]
39
+ end
40
+
41
+ def user=(value)
42
+ ActiveAudit.session[:current_user] = value
43
+ end
44
+
45
+ end
46
+
47
+ ActiveSupport.on_load(:action_controller) do
48
+ if defined?(ActionController::Base)
49
+ ActionController::Base.around_action ActiveAudit::Sweeper.instance
50
+ end
51
+ if defined?(ActionController::API)
52
+ ActionController::API.around_action ActiveAudit::Sweeper.instance
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,34 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/active_record'
3
+
4
+ module ActiveAudit
5
+ module Generators
6
+ class InstallGenerator < Rails::Generators::Base
7
+ include ActiveRecord::Generators::Migration
8
+
9
+ desc "Create initializer and config files for active audit base on the adapter you define. In case of 'active_record' adapter a migration will be generated as well."
10
+
11
+ argument :adapter, required: true, type: :string, desc: "The name of the storage adapter you want to use.",
12
+ :banner => "adapter_name"
13
+
14
+ source_root File.expand_path("../templates", __FILE__)
15
+
16
+ def copy_initializer_file
17
+ template "initializer.rb", "config/initializers/active_audit.rb"
18
+ end
19
+
20
+ def copy_config_file
21
+ unless adapter == 'active_record'
22
+ copy_file "#{adapter}_config.yml", "config/active_audit.yml"
23
+ end
24
+ end
25
+
26
+ def generate_migration
27
+ if adapter == 'active_record'
28
+ migration_template 'migration.rb', 'db/migrate/create_audits.rb'
29
+ end
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,12 @@
1
+ development:
2
+ host: 'http://localhost:9200'
3
+ index: auditing
4
+ test:
5
+ url: 'http://localhost:9200'
6
+ index: auditing
7
+ staging:
8
+ url: 'http://localhost:9200'
9
+ index: auditing
10
+ production:
11
+ url: <%= ENV["AUDIT_STORE_HOST"] %>
12
+ index: <%= ENV["AUDIT_STORE_INDEX"] %>
@@ -0,0 +1,10 @@
1
+ development:
2
+ hosts:
3
+ - localhost:27017
4
+ options:
5
+ database: audits
6
+ production:
7
+ hosts:
8
+ - <%= ENV["AUDITING_MONGO_HOST"].split(',').join '\n\t\t- ' %>
9
+ options:
10
+ database: <%= ENV["AUDITING_MONGO_DATABASE_NAME"] %>
@@ -0,0 +1,9 @@
1
+ ActiveAudit.configure do |config|
2
+ config.storage_adapter = :<%= adapter.to_sym %>
3
+ #config.current_user_method = :current_user
4
+ #config.ignored_attributes = %w(created_at updated_at)
5
+ #config.job_queue = :audits
6
+ #config.delayed_auditing = false
7
+ #config.default_user = { id: 1 }
8
+ #config.extract_user_profile = lambda { |user| { id: user.id } }
9
+ end
@@ -0,0 +1,22 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :audits, :force => true do |t|
4
+ t.column :item_id, :integer
5
+ t.column :type, :string
6
+ t.column :event, :string
7
+ t.column :changes, :json
8
+ t.column :user_id, :integer
9
+ t.column :user, :json
10
+ t.column :comment, :string
11
+ t.column :recorded_at, :datetime
12
+ end
13
+
14
+ add_index :audits, [:type, :item_id], :name => 'item_index'
15
+ add_index :audits, :user_id, :name => 'user_index'
16
+ add_index :audits, :recorded_at
17
+ end
18
+
19
+ def self.down
20
+ drop_table :audits
21
+ end
22
+ end
@@ -0,0 +1,10 @@
1
+ development:
2
+ hosts:
3
+ - localhost:27017
4
+ options:
5
+ database: audits
6
+ production:
7
+ hosts:
8
+ - <%= ENV["AUDITING_MONGO_HOST"].split(',').join '\n\t\t- ' %>
9
+ options:
10
+ database: <%= ENV["AUDITING_MONGO_DATABASE_NAME"] %>
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active-audit
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Tariq Abughofa
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-07-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails-observers
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.1.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.1.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: virtus
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ description: ORM extension to track model changes. It also support keeping record
42
+ of who made the changes and why.
43
+ email: mhdtariqabughofa@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - lib/active-audit.rb
49
+ - lib/active_audit/audit.rb
50
+ - lib/active_audit/audit_pusher.rb
51
+ - lib/active_audit/audit_repository.rb
52
+ - lib/active_audit/base.rb
53
+ - lib/active_audit/dirty_association.rb
54
+ - lib/active_audit/errors.rb
55
+ - lib/active_audit/storage_adapters.rb
56
+ - lib/active_audit/storage_adapters/active_record_adapter.rb
57
+ - lib/active_audit/storage_adapters/elasticsearch_adapter.rb
58
+ - lib/active_audit/storage_adapters/mongo_adapter.rb
59
+ - lib/active_audit/storage_adapters/test_adapter.rb
60
+ - lib/active_audit/sweeper.rb
61
+ - lib/generators/active_audit/install_generator.rb
62
+ - lib/generators/active_audit/templates/adapter_config/elastic_search_config.yml
63
+ - lib/generators/active_audit/templates/adapter_config/mongo_config.yml
64
+ - lib/generators/active_audit/templates/initializer.rb
65
+ - lib/generators/active_audit/templates/migration.rb
66
+ - lib/generators/active_audit/templates/mongo_config.yml
67
+ homepage: http://rubygems.org/gems/active-audit
68
+ licenses:
69
+ - MIT
70
+ metadata: {}
71
+ post_install_message:
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ requirements: []
86
+ rubyforge_project:
87
+ rubygems_version: 2.4.8
88
+ signing_key:
89
+ specification_version: 4
90
+ summary: ORM extension to track model changes.
91
+ test_files: []