effective_trash 0.2.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +96 -0
- data/app/controllers/admin/trash_controller.rb +27 -0
- data/app/controllers/effective/trash_controller.rb +44 -0
- data/app/helpers/effective_trash_helper.rb +69 -0
- data/app/models/concerns/acts_as_trashable.rb +46 -0
- data/app/models/effective/access_denied.rb +17 -0
- data/app/models/effective/datatables/trash.rb +53 -0
- data/app/models/effective/trash.rb +39 -0
- data/app/views/admin/trash/_actions.html.haml +15 -0
- data/app/views/admin/trash/index.html.haml +3 -0
- data/app/views/admin/trash/show.html.haml +4 -0
- data/app/views/effective/trash/_trash.html.haml +30 -0
- data/app/views/effective/trash/index.html.haml +6 -0
- data/app/views/effective/trash/show.html.haml +3 -0
- data/config/effective_trash.rb +36 -0
- data/config/routes.rb +21 -0
- data/db/migrate/01_create_effective_trash.rb.erb +26 -0
- data/lib/effective_trash/active_record_logger.rb +160 -0
- data/lib/effective_trash/engine.rb +29 -0
- data/lib/effective_trash/set_current_user.rb +16 -0
- data/lib/effective_trash/version.rb +3 -0
- data/lib/effective_trash.rb +34 -0
- data/lib/generators/effective_trash/install_generator.rb +29 -0
- metadata +138 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 473e805a96b7c73933733dff9e8342b0f6e8eb3c
|
4
|
+
data.tar.gz: 4b4c0294ef97d0ed8545925cecb4810437c362f2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: dcfad56fc484fee2bf983b521011a3e54dbc28855fd0e500b7639b539022038642b65cf04320991f905ec40ec2765e79caf34cf6edd6b4367399d046dc172e52
|
7
|
+
data.tar.gz: 66704561125458f4a8c17ff18db6d8c06d5634dd7179160f345bcde95f6a1cb625aa83793272e7550bbf6eb319204483fb17b810cd5d4e0e9fda36fcb123f130
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2017 Code and Effect Inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
# Effective Trash
|
2
|
+
|
3
|
+
Trash and Restore any Active Record object.
|
4
|
+
|
5
|
+
## Getting Started
|
6
|
+
|
7
|
+
Add to your Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'effective_trash'
|
11
|
+
```
|
12
|
+
|
13
|
+
Run the bundle command to install it:
|
14
|
+
|
15
|
+
```console
|
16
|
+
bundle install
|
17
|
+
```
|
18
|
+
|
19
|
+
Then run the generator:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
rails generate effective_trash:install
|
23
|
+
```
|
24
|
+
|
25
|
+
The generator will install an initializer which describes all configuration options and creates a database migration.
|
26
|
+
|
27
|
+
If you want to tweak the table name (to use something other than the default 'trash'), manually adjust both the configuration file and the migration now.
|
28
|
+
|
29
|
+
Then migrate the database:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
rake db:migrate
|
33
|
+
```
|
34
|
+
|
35
|
+
## Usage
|
36
|
+
|
37
|
+
Add to your model:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
class Post < ActiveRecord::Base
|
41
|
+
acts_as_trashable
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
and to your contoller:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
class ApplicationController < ActionController::Base
|
49
|
+
before_action :set_effective_trash_current_user
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
## How it works
|
54
|
+
|
55
|
+
The `acts_as_trashable` mixin sets up `before_destroy` and serializes the resource's attributes to an `Effective::Trash` object.
|
56
|
+
|
57
|
+
It also serializes the attributes of any `belongs_to`, `has_one`, and `has_many` with `accepts_nested_attributes` related resources.
|
58
|
+
|
59
|
+
Restoring only works with the single base object right now.
|
60
|
+
|
61
|
+
## Routes
|
62
|
+
|
63
|
+
Visit `/trash`, or `/admin/trash` for an interface to view and restore Trash.
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
link_to 'Trash', effective_trash.trash_index_path # /trash
|
67
|
+
link_to 'Admin Trash', effective_trash.admin_trash_index_path # /admin/trash
|
68
|
+
```
|
69
|
+
|
70
|
+
## Permissions
|
71
|
+
|
72
|
+
Add the following permissions (using CanCan):
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
can :manage, Effective::Trash, user_id: user.id
|
76
|
+
|
77
|
+
# Admin
|
78
|
+
can :manage, Effective::Trash
|
79
|
+
can :admin, :effective_trash
|
80
|
+
```
|
81
|
+
|
82
|
+
The user must be permitted to to `:update` an `Effective::Trash` in order to restore the trashed item.
|
83
|
+
|
84
|
+
## License
|
85
|
+
|
86
|
+
MIT License. Copyright [Code and Effect Inc.](http://www.codeandeffect.com/)
|
87
|
+
|
88
|
+
## Contributing
|
89
|
+
|
90
|
+
1. Fork it
|
91
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
92
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
93
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
94
|
+
5. Bonus points for test coverage
|
95
|
+
6. Create new Pull Request
|
96
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# This copies the permissions of The Logs controller
|
2
|
+
|
3
|
+
module Admin
|
4
|
+
class TrashController < ApplicationController
|
5
|
+
respond_to?(:before_action) ? before_action(:authenticate_user!) : before_filter(:authenticate_user!) # Devise
|
6
|
+
|
7
|
+
layout (EffectiveTrash.layout.kind_of?(Hash) ? EffectiveTrash.layout[:admin_trash] : EffectiveTrash.layout)
|
8
|
+
|
9
|
+
helper EffectiveTrashHelper
|
10
|
+
|
11
|
+
def index
|
12
|
+
@datatable = Effective::Datatables::Trash.new()
|
13
|
+
@page_title = 'Trash'
|
14
|
+
|
15
|
+
EffectiveTrash.authorized?(self, :index, Effective::Trash)
|
16
|
+
EffectiveTrash.authorized?(self, :admin, :effective_trash)
|
17
|
+
end
|
18
|
+
|
19
|
+
def show
|
20
|
+
@trash = Effective::Trash.all.find(params[:id])
|
21
|
+
@page_title = "Trash item - #{@trash.to_s}"
|
22
|
+
|
23
|
+
EffectiveTrash.authorized?(self, :show, @trash)
|
24
|
+
EffectiveTrash.authorized?(self, :admin, :effective_trash)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Effective
|
2
|
+
class TrashController < ApplicationController
|
3
|
+
if respond_to?(:before_action) # Devise
|
4
|
+
before_action :authenticate_user!
|
5
|
+
else
|
6
|
+
before_filter :authenticate_user!
|
7
|
+
end
|
8
|
+
|
9
|
+
# This is the User index event
|
10
|
+
def index
|
11
|
+
@datatable = Effective::Datatables::Trash.new(user_id: current_user.id)
|
12
|
+
@page_title = 'Trash'
|
13
|
+
|
14
|
+
EffectiveTrash.authorized?(self, :index, Effective::Trash.new(user_id: current_user.id))
|
15
|
+
end
|
16
|
+
|
17
|
+
# This is the User show event
|
18
|
+
def show
|
19
|
+
@trash = Effective::Trash.where(user_id: current_user.id).find(params[:id])
|
20
|
+
@page_title = "Trash item - #{@trash.to_s}"
|
21
|
+
|
22
|
+
EffectiveTrash.authorized?(self, :show, @trash)
|
23
|
+
end
|
24
|
+
|
25
|
+
def restore
|
26
|
+
@trash = Effective::Trash.all.find(params[:id])
|
27
|
+
EffectiveTrash.authorized?(self, :update, @trash)
|
28
|
+
|
29
|
+
Effective::Trash.transaction do
|
30
|
+
begin
|
31
|
+
@trash.restore_trash!
|
32
|
+
@trash.destroy!
|
33
|
+
flash[:success] = "Successfully restored #{@trash}"
|
34
|
+
rescue => e
|
35
|
+
flash[:danger] = "Unable to restore: #{e.message}"
|
36
|
+
raise ActiveRecord::Rollback
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
redirect_back(fallback_location: effective_trash.trash_path)
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module EffectiveTrashHelper
|
2
|
+
ALLOWED_TAGS = ActionView::Base.sanitized_allowed_tags.to_a + ['table', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th']
|
3
|
+
ALLOWED_ATTRIBUTES = ActionView::Base.sanitized_allowed_attributes.to_a + ['colspan', 'rowspan']
|
4
|
+
|
5
|
+
def render_trash(trash)
|
6
|
+
render(partial: 'effective/trash/trash', locals: {trash: trash})
|
7
|
+
end
|
8
|
+
|
9
|
+
# Call me with :th => true, :sub_th => false
|
10
|
+
# Any other options are sent to the table tag
|
11
|
+
def tableize_hash(hash, options = {})
|
12
|
+
if hash.present? && hash.kind_of?(Hash)
|
13
|
+
content_tag(:table, options) do
|
14
|
+
hash.map do |k, v|
|
15
|
+
content_tag(:tr) do
|
16
|
+
content_tag((options[:th] ? :th : :td), k) +
|
17
|
+
content_tag(:td) do
|
18
|
+
if v.kind_of?(Hash)
|
19
|
+
tableize_hash(v, options.merge({:class => 'table table-bordered', :th => (options.key?(:sub_th) ? options[:sub_th] : options[:th])}))
|
20
|
+
elsif v.kind_of?(Array)
|
21
|
+
'[' + v.join(', ') + ']'
|
22
|
+
else
|
23
|
+
v.to_s
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end.join('').html_safe
|
28
|
+
end.html_safe
|
29
|
+
else
|
30
|
+
hash.to_s.html_safe
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def format_trash_details_value(trash, key)
|
35
|
+
value = trash.details[key]
|
36
|
+
|
37
|
+
if value.kind_of?(Hash)
|
38
|
+
tableize_hash(value, :class => 'table', :th => true)
|
39
|
+
elsif value.kind_of?(Array)
|
40
|
+
value.map { |value| effective_trash_simple_format(value) }.join.html_safe
|
41
|
+
else
|
42
|
+
value = value.to_s
|
43
|
+
|
44
|
+
open = value.index('<!DOCTYPE html') || value.index('<html')
|
45
|
+
close = value.rindex('</html>') if open.present?
|
46
|
+
return effective_trash_simple_format(value) unless (open.present? && close.present?)
|
47
|
+
|
48
|
+
before = value[0...open]
|
49
|
+
after = value[(close+7)..-1]
|
50
|
+
divide = before.sub!('<hr>', '').present?
|
51
|
+
|
52
|
+
[
|
53
|
+
h(before).gsub("\n", '<br>'),
|
54
|
+
(content_tag(:hr) if divide),
|
55
|
+
content_tag(:iframe, '',
|
56
|
+
src: '#',
|
57
|
+
style: 'frameborder: 0; border: 0; width: 100%; height: 100%;',
|
58
|
+
onload: "this.style.height=(this.contentDocument.body.scrollHeight + 30) + 'px';",
|
59
|
+
scrolling: 'no'),
|
60
|
+
h(after).gsub("\n", '<br>')
|
61
|
+
].compact.join.html_safe
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def effective_trash_simple_format(value)
|
66
|
+
simple_format(sanitize(value.to_s, :tags => ALLOWED_TAGS, :attributes => ALLOWED_ATTRIBUTES), {}, :sanitize => false)
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module ActsAsTrashable
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
module ActiveRecord
|
5
|
+
def acts_as_trashable(*options)
|
6
|
+
@acts_as_trashable_options = options.try(:first) || {}
|
7
|
+
|
8
|
+
unless @acts_as_trashable_options.kind_of?(Hash)
|
9
|
+
raise ArgumentError.new("invalid arguments passed to (effective_trash) acts_as_trashable. Expecting no options.")
|
10
|
+
end
|
11
|
+
|
12
|
+
include ::ActsAsTrashable
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
included do
|
17
|
+
has_one :trash, as: :trashed, class_name: Effective::Trash
|
18
|
+
|
19
|
+
before_destroy do
|
20
|
+
trash = Effective::Trash.new(
|
21
|
+
trashed: self,
|
22
|
+
user: EffectiveTrash.current_user,
|
23
|
+
trashed_to_s: to_s,
|
24
|
+
trashed_extra: (trashed_extra if respond_to?(:trashed_extra)),
|
25
|
+
|
26
|
+
details: EffectiveTrash::ActiveRecordLogger.new(self, acts_as_trashable_options).attributes
|
27
|
+
).save!
|
28
|
+
end
|
29
|
+
|
30
|
+
# Parse Options
|
31
|
+
acts_as_trashable_options = {
|
32
|
+
only: Array(@acts_as_trashable_options[:only]).map { |attribute| attribute.to_s },
|
33
|
+
except: Array(@acts_as_trashable_options[:except]).map { |attribute| attribute.to_s },
|
34
|
+
additionally: Array(@acts_as_trashable_options[:additionally]).map { |attribute| attribute.to_s }
|
35
|
+
}
|
36
|
+
|
37
|
+
self.send(:define_method, :acts_as_trashable_options) { acts_as_trashable_options }
|
38
|
+
end
|
39
|
+
|
40
|
+
module ClassMethods
|
41
|
+
end
|
42
|
+
|
43
|
+
# Regular instance methods
|
44
|
+
|
45
|
+
end
|
46
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
unless defined?(Effective::AccessDenied)
|
2
|
+
module Effective
|
3
|
+
class AccessDenied < StandardError
|
4
|
+
attr_reader :action, :subject
|
5
|
+
|
6
|
+
def initialize(message = nil, action = nil, subject = nil)
|
7
|
+
@message = message
|
8
|
+
@action = action
|
9
|
+
@subject = subject
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
@message || I18n.t(:'unauthorized.default', :default => 'Access Denied')
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
if defined?(EffectiveDatatables)
|
2
|
+
module Effective
|
3
|
+
module Datatables
|
4
|
+
class Trash < Effective::Datatable
|
5
|
+
include EffectiveTrashHelper
|
6
|
+
|
7
|
+
datatable do
|
8
|
+
default_order :created_at, :desc
|
9
|
+
|
10
|
+
table_column :created_at, label: 'Destroyed at'
|
11
|
+
table_column :id, visible: false
|
12
|
+
|
13
|
+
unless attributes[:user_id] || attributes[:user] || (attributes[:user] == false)
|
14
|
+
table_column :user, label: 'Destroyed by', visible: false
|
15
|
+
end
|
16
|
+
|
17
|
+
table_column :trashed_type, label: 'Type'
|
18
|
+
table_column :trashed_id, label: 'Original Id', visible: false
|
19
|
+
table_column :trashed_to_s, label: 'Item'
|
20
|
+
|
21
|
+
table_column :details, visible: true, sortable: false do |trash|
|
22
|
+
tableize_hash(trash.details, th: true, sub_th: false, width: '100%')
|
23
|
+
end
|
24
|
+
|
25
|
+
unless attributes[:actions] == false
|
26
|
+
actions_column partial: 'admin/trash/actions', partial_local: :trash
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# A nil attributes[:log_id] means give me all the top level log entries
|
31
|
+
# If we set a log_id then it's for sub trash
|
32
|
+
def collection
|
33
|
+
collection = Effective::Trash.all.includes(:user)
|
34
|
+
|
35
|
+
if attributes[:user_id].present?
|
36
|
+
collection = collection.where(user_id: attributes[:user_id])
|
37
|
+
end
|
38
|
+
|
39
|
+
if attributes[:user].present?
|
40
|
+
collection = collection.where(user: attributes[:user])
|
41
|
+
end
|
42
|
+
|
43
|
+
if attributes[:trashed_id] && attributes[:trashed_type]
|
44
|
+
collection = collection.where(trashed_id: attributes[:trashed_id], trashed_type: attributes[:trashed_type])
|
45
|
+
end
|
46
|
+
|
47
|
+
collection
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Effective
|
2
|
+
class Trash < ActiveRecord::Base
|
3
|
+
self.table_name = EffectiveTrash.trash_table_name.to_s
|
4
|
+
|
5
|
+
belongs_to :trashed, polymorphic: true # The original item type and id. Note that this object will never exist as it's deleted.
|
6
|
+
belongs_to :user, optional: true # The user that destroyed the original resource
|
7
|
+
|
8
|
+
# Attributes
|
9
|
+
# trashed_type :string
|
10
|
+
# trashed_id :integer
|
11
|
+
# trashed_to_s :string
|
12
|
+
# trashed_extra :string
|
13
|
+
|
14
|
+
# details :text
|
15
|
+
# timestamps
|
16
|
+
|
17
|
+
serialize :details, Hash
|
18
|
+
|
19
|
+
default_scope -> { order(updated_at: :desc) }
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
[trashed_type, trashed_id].join(' ').presence || 'New Trash item'
|
23
|
+
end
|
24
|
+
|
25
|
+
def details
|
26
|
+
self[:details] || {}
|
27
|
+
end
|
28
|
+
|
29
|
+
# So this is a Trash item
|
30
|
+
# When we delete ourselves, we restore this trash item first
|
31
|
+
def restore_trash!
|
32
|
+
raise 'no attributes to restore from' unless details.kind_of?(Hash) && details[:attributes].present?
|
33
|
+
trashed_type.constantize.new(details[:attributes]).save!
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
|
@@ -0,0 +1,15 @@
|
|
1
|
+
:ruby
|
2
|
+
show_path =
|
3
|
+
if datatables_active_admin_path?
|
4
|
+
admin_effective_trash_path(trash)
|
5
|
+
elsif datatables_admin_path?
|
6
|
+
effective_trash.admin_trash_path(trash)
|
7
|
+
else
|
8
|
+
effective_trash.trash_path(trash)
|
9
|
+
end
|
10
|
+
|
11
|
+
= link_to show_path, title: 'view' do
|
12
|
+
%span.glyphicon.glyphicon-eye-open
|
13
|
+
|
14
|
+
= link_to effective_trash.restore_trash_path(trash), title: 'Restore', data: {confirm: 'Restore this item?'} do
|
15
|
+
%span.glyphicon.glyphicon-retweet
|
@@ -0,0 +1,30 @@
|
|
1
|
+
.panel.panel-default
|
2
|
+
.panel-body
|
3
|
+
%p
|
4
|
+
%span.label.label-default= trash
|
5
|
+
= trash.trashed_to_s
|
6
|
+
|
7
|
+
- if trash.trashed_extra.present?
|
8
|
+
%p= trash.trashed_extra
|
9
|
+
|
10
|
+
.row
|
11
|
+
.col-sm-6
|
12
|
+
%strong Destroyed at:
|
13
|
+
= trash.created_at.strftime("%Y-%m-%d %H:%M:%S")
|
14
|
+
= '(' + time_ago_in_words(trash.created_at) + ' ago)'
|
15
|
+
|
16
|
+
.col-sm-6
|
17
|
+
- if trash.user.present?
|
18
|
+
%strong Destroyed by:
|
19
|
+
= trash.user.to_s
|
20
|
+
|
21
|
+
%br
|
22
|
+
|
23
|
+
- trash.details.each do |key, value|
|
24
|
+
- next unless value.present?
|
25
|
+
.row
|
26
|
+
.col-lg-12
|
27
|
+
%p
|
28
|
+
%strong= "#{key.to_s.titleize}:"
|
29
|
+
= format_trash_details_value(trash, key)
|
30
|
+
|
@@ -0,0 +1,36 @@
|
|
1
|
+
EffectiveTrash.setup do |config|
|
2
|
+
# Configure Database Table
|
3
|
+
config.trash_table_name = :trash
|
4
|
+
|
5
|
+
# Authorization Method
|
6
|
+
#
|
7
|
+
# This method is called by all controller actions with the appropriate action and resource
|
8
|
+
# If the method returns false, an Effective::AccessDenied Error will be raised (see README.md for complete info)
|
9
|
+
#
|
10
|
+
# Use via Proc (and with CanCan):
|
11
|
+
# config.authorization_method = Proc.new { |controller, action, resource| can?(action, resource) }
|
12
|
+
#
|
13
|
+
# Use via custom method:
|
14
|
+
# config.authorization_method = :my_authorization_method
|
15
|
+
#
|
16
|
+
# And then in your application_controller.rb:
|
17
|
+
#
|
18
|
+
# def my_authorization_method(action, resource)
|
19
|
+
# current_user.is?(:admin)
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# Or disable the check completely:
|
23
|
+
# config.authorization_method = false
|
24
|
+
config.authorization_method = Proc.new { |controller, action, resource| authorize!(action, resource) } # CanCanCan
|
25
|
+
|
26
|
+
# Admin Screens Layout Settings
|
27
|
+
config.layout = 'application' # All EffectiveTrash controllers will use this layout
|
28
|
+
|
29
|
+
# config.layout = {
|
30
|
+
# trash: 'application',
|
31
|
+
# admin_trash: 'admin',
|
32
|
+
# }
|
33
|
+
|
34
|
+
# Enable the /trash, /admin/trash and /trash/:id/restore routes. Doesn't affect acts_as_trashable itself.
|
35
|
+
config.routes_enabled = true
|
36
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
EffectiveTrash::Engine.routes.draw do
|
2
|
+
scope :module => 'effective' do
|
3
|
+
|
4
|
+
if EffectiveTrash.routes_enabled
|
5
|
+
resources :trash, only: [:index, :show] do
|
6
|
+
member { get :restore }
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
if EffectiveTrash.routes_enabled
|
12
|
+
namespace :admin do
|
13
|
+
resources :trash, only: [:index, :show]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
Rails.application.routes.draw do
|
20
|
+
mount EffectiveTrash::Engine => '/', :as => 'effective_trash'
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class CreateEffectiveTrash < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table <%= @trash_table_name %> do |t|
|
4
|
+
t.integer :user_id
|
5
|
+
|
6
|
+
t.string :trashed_type
|
7
|
+
t.integer :trashed_id
|
8
|
+
|
9
|
+
t.string :trashed_to_s
|
10
|
+
t.string :trashed_extra
|
11
|
+
|
12
|
+
t.text :details
|
13
|
+
|
14
|
+
t.timestamps
|
15
|
+
end
|
16
|
+
|
17
|
+
add_index <%= @trash_table_name %>, :user_id
|
18
|
+
add_index <%= @trash_table_name %>, [:trashed_type, :trashed_id]
|
19
|
+
add_index <%= @trash_table_name %>, :trashed_id
|
20
|
+
add_index <%= @trash_table_name %>, :trashed_extra
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.down
|
24
|
+
drop_table <%= @trash_table_name %>
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
module EffectiveTrash
|
2
|
+
class ActiveRecordLogger
|
3
|
+
attr_accessor :resource, :logger, :depth, :options
|
4
|
+
|
5
|
+
BLANK = "''"
|
6
|
+
|
7
|
+
def initialize(resource, options = {})
|
8
|
+
raise ArgumentError.new('options must be a Hash') unless options.kind_of?(Hash)
|
9
|
+
|
10
|
+
@resource = resource
|
11
|
+
@logger = options.delete(:logger) || resource
|
12
|
+
@depth = options.delete(:depth) || 0
|
13
|
+
@options = options
|
14
|
+
|
15
|
+
unless @logger.respond_to?(:logged_changes) || @logger.respond_to?(:trash)
|
16
|
+
raise ArgumentError.new('logger must respond to logged_changes or trash')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# execute! is called when we recurse, otherwise the following methods are best called individually
|
21
|
+
def execute!
|
22
|
+
if resource.new_record?
|
23
|
+
created!
|
24
|
+
elsif resource.marked_for_destruction?
|
25
|
+
destroyed!
|
26
|
+
else
|
27
|
+
changed!
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# before_destroy
|
32
|
+
def trashed!
|
33
|
+
log_trash((resource.to_s rescue ''), details: applicable(attributes))
|
34
|
+
end
|
35
|
+
|
36
|
+
# before_destroy
|
37
|
+
def destroyed!
|
38
|
+
log('Deleted', details: applicable(attributes))
|
39
|
+
end
|
40
|
+
|
41
|
+
# after_commit
|
42
|
+
def created!
|
43
|
+
log('Created', details: applicable(attributes))
|
44
|
+
end
|
45
|
+
|
46
|
+
# after_commit
|
47
|
+
def updated!
|
48
|
+
log('Updated', details: applicable(attributes))
|
49
|
+
end
|
50
|
+
|
51
|
+
# before_save
|
52
|
+
def changed!
|
53
|
+
applicable(changes).each do |attribute, (before, after)|
|
54
|
+
if resource.respond_to?(:log_changes_formatted_value)
|
55
|
+
before = resource.log_changes_formatted_value(attribute, before) || before
|
56
|
+
after = resource.log_changes_formatted_value(attribute, after) || after
|
57
|
+
end
|
58
|
+
|
59
|
+
attribute = if resource.respond_to?(:log_changes_formatted_attribute)
|
60
|
+
resource.log_changes_formatted_attribute(attribute)
|
61
|
+
end || attribute.titleize
|
62
|
+
|
63
|
+
if after.present?
|
64
|
+
log("#{attribute} changed from #{before.presence || BLANK} to #{after.presence || BLANK}", details: { attribute: attribute, before: before, after: after })
|
65
|
+
else
|
66
|
+
log("#{attribute} set to #{before || BLANK}", details: { attribute: attribute, value: before })
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Log changes on all accepts_as_nested_parameters has_many associations
|
71
|
+
(resource.class.try(:reflect_on_all_autosave_associations) || []).each do |association|
|
72
|
+
child_name = association.name.to_s.singularize.titleize
|
73
|
+
|
74
|
+
Array(resource.send(association.name)).each_with_index do |child, index|
|
75
|
+
ActiveRecordLogger.new(child, options.merge(logger: logger, depth: (depth + 1), prefix: "#{child_name} ##{index+1}: ")).execute!
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def attributes
|
81
|
+
attributes = { attributes: resource.attributes }
|
82
|
+
|
83
|
+
# Collect to_s representations of all belongs_to associations
|
84
|
+
(resource.class.try(:reflect_on_all_associations, :belongs_to) || []).each do |association|
|
85
|
+
attributes[association.name] = resource.send(association.name).to_s.presence || 'nil'
|
86
|
+
end
|
87
|
+
|
88
|
+
# Collect to_s representations for all has_one associations
|
89
|
+
(resource.class.try(:reflect_on_all_associations, :has_one) || []).each do |association|
|
90
|
+
next if association.name == :trash && resource.respond_to?(:acts_as_trashable_options) # We skip our own association
|
91
|
+
attributes[association.name] = resource.send(association.name).to_s.presence || 'nil'
|
92
|
+
end
|
93
|
+
|
94
|
+
# Collects attributes for all accepts_as_nested_parameters has_many associations
|
95
|
+
(resource.class.try(:reflect_on_all_autosave_associations) || []).each do |association|
|
96
|
+
attributes[association.name] = {}
|
97
|
+
|
98
|
+
Array(resource.send(association.name)).each_with_index do |child, index|
|
99
|
+
attributes[association.name][index+1] = ActiveRecordLogger.new(child, options.merge(logger: logger)).attributes
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
attributes
|
104
|
+
end
|
105
|
+
|
106
|
+
def changes
|
107
|
+
changes = resource.changes
|
108
|
+
|
109
|
+
# Log to_s changes on all belongs_to associations
|
110
|
+
(resource.class.try(:reflect_on_all_associations, :belongs_to) || []).each do |association|
|
111
|
+
if (change = changes.delete(association.foreign_key)).present?
|
112
|
+
changes[association.name] = [association.klass.find_by_id(change.first), resource.send(association.name)]
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
changes
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def log(message, details: {})
|
122
|
+
logger.logged_changes.build(
|
123
|
+
user: EffectiveTrash.current_user,
|
124
|
+
status: EffectiveTrash.log_changes_status,
|
125
|
+
message: "#{"\t" * depth}#{options[:prefix]}#{message}",
|
126
|
+
details: details
|
127
|
+
).tap { |log| log.save }
|
128
|
+
end
|
129
|
+
|
130
|
+
def log_trash(message, details: {})
|
131
|
+
logger.build_trash(
|
132
|
+
user: EffectiveTrash.current_user,
|
133
|
+
status: EffectiveTrash.trashable_status,
|
134
|
+
message: "#{"\t" * depth}#{options[:prefix]}#{message}",
|
135
|
+
details: details
|
136
|
+
).tap { |log| log.save }
|
137
|
+
end
|
138
|
+
|
139
|
+
# TODO: Make this work better with nested objects
|
140
|
+
def applicable(attributes)
|
141
|
+
atts = if options[:only].present?
|
142
|
+
attributes.slice(*options[:only])
|
143
|
+
elsif options[:except].present?
|
144
|
+
attributes.except(*options[:except])
|
145
|
+
else
|
146
|
+
attributes.except(:updated_at, :created_at)
|
147
|
+
end
|
148
|
+
|
149
|
+
(options[:additionally] || []).each do |attribute|
|
150
|
+
value = (resource.send(attribute) rescue :effective_trash_nope)
|
151
|
+
next if attributes[attribute].present? || value == :effective_trash_nope
|
152
|
+
|
153
|
+
atts[attribute] = value
|
154
|
+
end
|
155
|
+
|
156
|
+
atts
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module EffectiveTrash
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
engine_name 'effective_trash'
|
4
|
+
|
5
|
+
config.autoload_paths += Dir["#{config.root}/lib/"]
|
6
|
+
|
7
|
+
# Set up our default configuration options.
|
8
|
+
initializer "effective_trash.defaults", before: :load_config_initializers do |app|
|
9
|
+
eval File.read("#{config.root}/config/effective_trash.rb")
|
10
|
+
end
|
11
|
+
|
12
|
+
# Include acts_as_trashable concern and allow any ActiveRecord object to call it with acts_as_trashable()
|
13
|
+
initializer 'effective_trash.active_record' do |app|
|
14
|
+
ActiveSupport.on_load :active_record do
|
15
|
+
ActiveRecord::Base.extend(ActsAsTrashable::ActiveRecord)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Register the log_page_views concern so that it can be called in ActionController or elsewhere
|
20
|
+
initializer 'effective_trash.action_controller' do |app|
|
21
|
+
Rails.application.config.to_prepare do
|
22
|
+
ActiveSupport.on_load :action_controller do
|
23
|
+
ActionController::Base.include(EffectiveTrash::SetCurrentUser::ActionController)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module EffectiveTrash
|
2
|
+
module SetCurrentUser
|
3
|
+
module ActionController
|
4
|
+
|
5
|
+
def set_effective_trash_current_user
|
6
|
+
if respond_to?(:current_user)
|
7
|
+
EffectiveTrash.current_user = current_user
|
8
|
+
else
|
9
|
+
raise "(effective_trash) set_effective_trash_current_user expects a current_user() method to be available"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'haml-rails'
|
2
|
+
require 'effective_trash/engine'
|
3
|
+
require 'effective_trash/version'
|
4
|
+
|
5
|
+
module EffectiveTrash
|
6
|
+
|
7
|
+
# The following are all valid config keys
|
8
|
+
mattr_accessor :trash_table_name
|
9
|
+
|
10
|
+
mattr_accessor :authorization_method
|
11
|
+
mattr_accessor :layout
|
12
|
+
mattr_accessor :routes_enabled
|
13
|
+
|
14
|
+
def self.setup
|
15
|
+
yield self
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.authorized?(controller, action, resource)
|
19
|
+
if authorization_method.respond_to?(:call) || authorization_method.kind_of?(Symbol)
|
20
|
+
raise Effective::AccessDenied.new() unless (controller || self).instance_exec(controller, action, resource, &authorization_method)
|
21
|
+
end
|
22
|
+
true
|
23
|
+
end
|
24
|
+
|
25
|
+
# This is set by the "set_effective_trash_current_user" before_filter.
|
26
|
+
def self.current_user=(user)
|
27
|
+
@effective_trash_current_user = user
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.current_user
|
31
|
+
@effective_trash_current_user
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module EffectiveTrash
|
2
|
+
module Generators
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
4
|
+
include Rails::Generators::Migration
|
5
|
+
|
6
|
+
desc 'Creates an EffectiveTrash initializer in your application.'
|
7
|
+
|
8
|
+
source_root File.expand_path('../../templates', __FILE__)
|
9
|
+
|
10
|
+
def self.next_migration_number(dirname)
|
11
|
+
if not ActiveRecord::Base.timestamped_migrations
|
12
|
+
Time.new.utc.strftime("%Y%m%d%H%M%S")
|
13
|
+
else
|
14
|
+
"%.3d" % (current_migration_number(dirname) + 1)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def copy_initializer
|
19
|
+
template ('../' * 3) + 'config/effective_trash.rb', 'config/initializers/effective_trash.rb'
|
20
|
+
end
|
21
|
+
|
22
|
+
def create_migration_file
|
23
|
+
@trash_table_name = ':' + EffectiveTrash.trash_table_name.to_s
|
24
|
+
|
25
|
+
migration_template ('../' * 3) + 'db/migrate/01_create_effective_trash.rb.erb', 'db/migrate/create_effective_trash.rb'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: effective_trash
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Code and Effect
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-01-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 3.2.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 3.2.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: effective_datatables
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.0.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 2.0.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: coffee-rails
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: devise
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: haml-rails
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Trash and Restore any Active Record object.
|
84
|
+
email:
|
85
|
+
- info@codeandeffect.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- MIT-LICENSE
|
91
|
+
- README.md
|
92
|
+
- app/controllers/admin/trash_controller.rb
|
93
|
+
- app/controllers/effective/trash_controller.rb
|
94
|
+
- app/helpers/effective_trash_helper.rb
|
95
|
+
- app/models/concerns/acts_as_trashable.rb
|
96
|
+
- app/models/effective/access_denied.rb
|
97
|
+
- app/models/effective/datatables/trash.rb
|
98
|
+
- app/models/effective/trash.rb
|
99
|
+
- app/views/admin/trash/_actions.html.haml
|
100
|
+
- app/views/admin/trash/index.html.haml
|
101
|
+
- app/views/admin/trash/show.html.haml
|
102
|
+
- app/views/effective/trash/_trash.html.haml
|
103
|
+
- app/views/effective/trash/index.html.haml
|
104
|
+
- app/views/effective/trash/show.html.haml
|
105
|
+
- config/effective_trash.rb
|
106
|
+
- config/routes.rb
|
107
|
+
- db/migrate/01_create_effective_trash.rb.erb
|
108
|
+
- lib/effective_trash.rb
|
109
|
+
- lib/effective_trash/active_record_logger.rb
|
110
|
+
- lib/effective_trash/engine.rb
|
111
|
+
- lib/effective_trash/set_current_user.rb
|
112
|
+
- lib/effective_trash/version.rb
|
113
|
+
- lib/generators/effective_trash/install_generator.rb
|
114
|
+
homepage: https://github.com/code-and-effect/effective_trash
|
115
|
+
licenses:
|
116
|
+
- MIT
|
117
|
+
metadata: {}
|
118
|
+
post_install_message:
|
119
|
+
rdoc_options: []
|
120
|
+
require_paths:
|
121
|
+
- lib
|
122
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - ">="
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
requirements: []
|
133
|
+
rubyforge_project:
|
134
|
+
rubygems_version: 2.4.5.1
|
135
|
+
signing_key:
|
136
|
+
specification_version: 4
|
137
|
+
summary: Trash and Restore any Active Record object.
|
138
|
+
test_files: []
|