recycle_bin 1.0.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/CHANGELOG.md +16 -0
- data/Gemfile +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +425 -0
- data/Rakefile +12 -0
- data/app/controllers/recycle_bin/application_controller.rb +102 -0
- data/app/controllers/recycle_bin/trash_controller.rb +213 -0
- data/app/helpers/recycle_bin/application_helper.rb +31 -0
- data/app/views/layouts/recycle_bin/application.html.erb +609 -0
- data/app/views/layouts/recycle_bin/recycle_bin/application.html.erb +266 -0
- data/app/views/layouts/recycle_bin/recycle_bin/trash/index.html.erb +133 -0
- data/app/views/layouts/recycle_bin/recycle_bin/trash/show.html.erb +175 -0
- data/app/views/recycle_bin/trash/index.html.erb +229 -0
- data/app/views/recycle_bin/trash/show.html.erb +288 -0
- data/config/routes.rb +19 -0
- data/lib/generators/recycle_bin/add_deleted_at/add_deleted_at_generator.rb +49 -0
- data/lib/generators/recycle_bin/add_deleted_at/templates/add_deleted_at_migration.rb.erb +6 -0
- data/lib/generators/recycle_bin/install/install_generator.rb +82 -0
- data/lib/generators/recycle_bin/install/templates/recycle_bin.rb +24 -0
- data/lib/recycle_bin/engine.rb +21 -0
- data/lib/recycle_bin/install/templates/recycle_bin.rb +24 -0
- data/lib/recycle_bin/soft_deletable.rb +101 -0
- data/lib/recycle_bin/version.rb +5 -0
- data/lib/recycle_bin.rb +121 -0
- data/recycle_bin.gemspec +47 -0
- data/sig/recycle_bin.rbs +4 -0
- metadata +144 -0
@@ -0,0 +1,213 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RecycleBin
|
4
|
+
class TrashController < ApplicationController
|
5
|
+
before_action :load_deleted_items, only: [:index]
|
6
|
+
before_action :find_item, only: %i[show restore destroy]
|
7
|
+
|
8
|
+
def index
|
9
|
+
# Apply filters
|
10
|
+
@deleted_items = filter_items(@deleted_items)
|
11
|
+
|
12
|
+
# Get model types for filter buttons
|
13
|
+
@model_types = @deleted_items.map { |item| item.class.name }.uniq.sort
|
14
|
+
|
15
|
+
# Apply pagination (since @deleted_items is an Array, we need to handle this differently)
|
16
|
+
items_per_page = RecycleBin.config.items_per_page || 25
|
17
|
+
@deleted_items = @deleted_items.first(items_per_page)
|
18
|
+
end
|
19
|
+
|
20
|
+
def show
|
21
|
+
@original_attributes = @item.attributes.except('deleted_at')
|
22
|
+
@associations = load_associations(@item)
|
23
|
+
end
|
24
|
+
|
25
|
+
def restore
|
26
|
+
if @item.restore
|
27
|
+
redirect_to root_path, notice: "#{@item.class.name} successfully restored!"
|
28
|
+
else
|
29
|
+
redirect_to root_path, alert: "Failed to restore #{@item.class.name}."
|
30
|
+
end
|
31
|
+
rescue => e
|
32
|
+
Rails.logger.error "Error restoring item: #{e.message}"
|
33
|
+
redirect_to root_path, alert: "Failed to restore #{@item.class.name}."
|
34
|
+
end
|
35
|
+
|
36
|
+
def destroy
|
37
|
+
model_name = @item.class.name
|
38
|
+
if @item.destroy!
|
39
|
+
redirect_to root_path, notice: "#{model_name} permanently deleted."
|
40
|
+
else
|
41
|
+
redirect_to root_path, alert: "Failed to delete #{model_name}."
|
42
|
+
end
|
43
|
+
rescue => e
|
44
|
+
Rails.logger.error "Error deleting item: #{e.message}"
|
45
|
+
redirect_to root_path, alert: "Failed to delete #{model_name}."
|
46
|
+
end
|
47
|
+
|
48
|
+
def bulk_restore
|
49
|
+
restored_count = 0
|
50
|
+
selected_items = parse_bulk_selection
|
51
|
+
|
52
|
+
selected_items.each do |model_class, id|
|
53
|
+
next unless model_class && id
|
54
|
+
|
55
|
+
model = safe_constantize_model(model_class)
|
56
|
+
next unless model.respond_to?(:with_deleted)
|
57
|
+
|
58
|
+
item = model.with_deleted.find_by(id: id)
|
59
|
+
restored_count += 1 if item&.restore
|
60
|
+
end
|
61
|
+
|
62
|
+
redirect_to trash_index_path, notice: "#{restored_count} items restored successfully!"
|
63
|
+
end
|
64
|
+
|
65
|
+
def bulk_destroy
|
66
|
+
destroyed_count = 0
|
67
|
+
selected_items = parse_bulk_selection
|
68
|
+
|
69
|
+
selected_items.each do |model_class, id|
|
70
|
+
next unless model_class && id
|
71
|
+
|
72
|
+
model = safe_constantize_model(model_class)
|
73
|
+
next unless model.respond_to?(:with_deleted)
|
74
|
+
|
75
|
+
item = model.with_deleted.find_by(id: id)
|
76
|
+
destroyed_count += 1 if item&.destroy!
|
77
|
+
end
|
78
|
+
|
79
|
+
redirect_to trash_index_path, notice: "#{destroyed_count} items permanently deleted."
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def load_deleted_items
|
85
|
+
@deleted_items = []
|
86
|
+
|
87
|
+
# Use the safer method from RecycleBin module
|
88
|
+
RecycleBin.models_with_soft_delete.each do |model_name|
|
89
|
+
model = model_name.constantize
|
90
|
+
if model.respond_to?(:deleted)
|
91
|
+
# Get up to 100 items per model to avoid memory issues
|
92
|
+
deleted_records = model.deleted.limit(100).to_a
|
93
|
+
@deleted_items.concat(deleted_records)
|
94
|
+
end
|
95
|
+
rescue => e
|
96
|
+
Rails.logger.debug "Skipping model #{model_name}: #{e.message}"
|
97
|
+
next
|
98
|
+
end
|
99
|
+
|
100
|
+
# Sort by deletion time (most recent first)
|
101
|
+
@deleted_items.sort_by!(&:deleted_at).reverse!
|
102
|
+
end
|
103
|
+
|
104
|
+
def filter_items(items)
|
105
|
+
items = filter_by_type(items) if params[:type].present?
|
106
|
+
items = filter_by_time(items) if params[:time].present?
|
107
|
+
items
|
108
|
+
end
|
109
|
+
|
110
|
+
def filter_by_type(items)
|
111
|
+
# RuboCop prefers this approach over direct class name comparison
|
112
|
+
# We need to compare against the string parameter from URL params
|
113
|
+
target_class_name = params[:type]
|
114
|
+
items.select { |item| item.class.name == target_class_name }
|
115
|
+
end
|
116
|
+
|
117
|
+
def filter_by_time(items)
|
118
|
+
cutoff_time = case params[:time]
|
119
|
+
when 'today' then 1.day.ago
|
120
|
+
when 'week' then 1.week.ago
|
121
|
+
when 'month' then 1.month.ago
|
122
|
+
else return items
|
123
|
+
end
|
124
|
+
|
125
|
+
items.select { |item| item.deleted_at >= cutoff_time }
|
126
|
+
end
|
127
|
+
|
128
|
+
def find_item
|
129
|
+
model_class = safe_constantize_model(params[:model_type])
|
130
|
+
|
131
|
+
unless model_class.respond_to?(:with_deleted)
|
132
|
+
redirect_to trash_index_path, alert: 'Invalid model type.'
|
133
|
+
return
|
134
|
+
end
|
135
|
+
|
136
|
+
@item = model_class.with_deleted.find(params[:id])
|
137
|
+
rescue ActiveRecord::RecordNotFound
|
138
|
+
redirect_to trash_index_path, alert: 'Item not found.'
|
139
|
+
end
|
140
|
+
|
141
|
+
def load_associations(item)
|
142
|
+
associations = {}
|
143
|
+
|
144
|
+
item.class.reflect_on_all_associations.each do |association|
|
145
|
+
next if association.macro == :belongs_to
|
146
|
+
|
147
|
+
related_items = safely_load_association(item, association)
|
148
|
+
associations[association.name] = related_items if related_items.present?
|
149
|
+
end
|
150
|
+
|
151
|
+
associations
|
152
|
+
end
|
153
|
+
|
154
|
+
def safely_load_association(item, association)
|
155
|
+
related_items = item.send(association.name)
|
156
|
+
limit_association_items(related_items)
|
157
|
+
rescue => e
|
158
|
+
Rails.logger.debug "Skipping association #{association.name}: #{e.message}"
|
159
|
+
nil
|
160
|
+
end
|
161
|
+
|
162
|
+
def limit_association_items(related_items)
|
163
|
+
return Array(related_items) unless related_items.respond_to?(:limit)
|
164
|
+
|
165
|
+
if related_items.respond_to?(:first)
|
166
|
+
related_items.is_a?(Array) ? related_items.first(10) : related_items
|
167
|
+
else
|
168
|
+
related_items.limit(10)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def parse_bulk_selection
|
173
|
+
selected_items = extract_selected_items
|
174
|
+
return [] unless selected_items.is_a?(Array)
|
175
|
+
|
176
|
+
parse_item_strings(selected_items)
|
177
|
+
end
|
178
|
+
|
179
|
+
def extract_selected_items
|
180
|
+
selected_items = params[:selected_items]
|
181
|
+
|
182
|
+
return JSON.parse(selected_items) if selected_items.is_a?(String)
|
183
|
+
|
184
|
+
selected_items
|
185
|
+
rescue JSON::ParserError
|
186
|
+
[]
|
187
|
+
end
|
188
|
+
|
189
|
+
def parse_item_strings(selected_items)
|
190
|
+
parsed_items = selected_items.filter_map do |item_string|
|
191
|
+
parse_single_item(item_string)
|
192
|
+
end
|
193
|
+
|
194
|
+
parsed_items.reject { |model_class, id| model_class.blank? || id.zero? }
|
195
|
+
end
|
196
|
+
|
197
|
+
def parse_single_item(item_string)
|
198
|
+
return nil unless item_string.is_a?(String)
|
199
|
+
|
200
|
+
model_class, id = item_string.split(':')
|
201
|
+
return nil if model_class.blank? || id.blank?
|
202
|
+
|
203
|
+
[model_class, id.to_i]
|
204
|
+
end
|
205
|
+
|
206
|
+
# Helper method to generate trash index path
|
207
|
+
def trash_index_path
|
208
|
+
recycle_bin.root_path
|
209
|
+
rescue StandardError
|
210
|
+
root_path
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RecycleBin
|
4
|
+
# Helper methods for RecycleBin views
|
5
|
+
module ApplicationHelper
|
6
|
+
def recyclable_icon(item)
|
7
|
+
icon_class = case item.class.name.downcase
|
8
|
+
when 'user'
|
9
|
+
'fa-user'
|
10
|
+
when 'post', 'article'
|
11
|
+
'fa-file-text'
|
12
|
+
when 'comment'
|
13
|
+
'fa-comment'
|
14
|
+
else
|
15
|
+
'fa-trash'
|
16
|
+
end
|
17
|
+
|
18
|
+
content_tag(:i, '', class: "fa #{icon_class}")
|
19
|
+
end
|
20
|
+
|
21
|
+
def time_ago_in_words_or_date(time)
|
22
|
+
return 'Never' if time.blank?
|
23
|
+
|
24
|
+
if time > 1.week.ago
|
25
|
+
"#{time_ago_in_words(time)} ago"
|
26
|
+
else
|
27
|
+
time.strftime('%B %d, %Y')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|