recycle_bin 1.1.0 → 1.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 +4 -4
- data/CHANGELOG.md +57 -0
- data/README.md +228 -13
- data/app/controllers/recycle_bin/trash_controller.rb +424 -21
- data/app/helpers/recycle_bin/application_helper.rb +175 -0
- data/app/views/{layouts/recycle_bin/application.html.erb → recycle_bin/layouts/recycle_bin.html.erb} +850 -609
- data/app/views/recycle_bin/shared/_error_messages.html.erb +10 -0
- data/app/views/recycle_bin/shared/_flash_messages.html.erb +6 -0
- data/app/views/recycle_bin/trash/_action_history.html.erb +75 -0
- data/app/views/recycle_bin/trash/_associations.html.erb +50 -0
- data/app/views/recycle_bin/trash/_filters.html.erb +561 -0
- data/app/views/recycle_bin/trash/_item.html.erb +267 -0
- data/app/views/recycle_bin/trash/_pagination.html.erb +75 -0
- data/app/views/recycle_bin/trash/_stats.html.erb +50 -0
- data/app/views/recycle_bin/trash/dashboard.html.erb +618 -0
- data/app/views/recycle_bin/trash/index.html.erb +247 -278
- data/app/views/recycle_bin/trash/show.html.erb +60 -215
- data/config/routes.rb +9 -2
- data/docs/index.html +928 -0
- data/docs/logo.svg +71 -0
- data/lib/recycle_bin/version.rb +1 -1
- data/lib/recycle_bin.rb +111 -1
- metadata +18 -8
- data/app/views/layouts/recycle_bin/recycle_bin/application.html.erb +0 -266
- data/app/views/layouts/recycle_bin/recycle_bin/trash/index.html.erb +0 -133
- data/app/views/layouts/recycle_bin/recycle_bin/trash/show.html.erb +0 -175
- data/recycle_bin.gemspec +0 -47
|
@@ -2,9 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
module RecycleBin
|
|
4
4
|
class TrashController < ApplicationController
|
|
5
|
+
layout 'recycle_bin/layouts/recycle_bin'
|
|
5
6
|
before_action :load_deleted_items_with_pagination, only: [:index]
|
|
7
|
+
before_action :load_dashboard_data, only: [:dashboard]
|
|
6
8
|
before_action :find_item, only: %i[show restore destroy]
|
|
7
9
|
|
|
10
|
+
# Dashboard view - Overview with stats and recent activity
|
|
11
|
+
def dashboard
|
|
12
|
+
respond_to do |format|
|
|
13
|
+
format.html { render :dashboard }
|
|
14
|
+
format.csv { export_to_csv(@all_deleted_items) }
|
|
15
|
+
format.json { export_to_json(@all_deleted_items) }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# All Items view - Detailed list with filtering
|
|
8
20
|
def index
|
|
9
21
|
# Apply filters to the relation before pagination
|
|
10
22
|
@filtered_items = filter_items_relation(@all_deleted_items_relation)
|
|
@@ -31,6 +43,13 @@ module RecycleBin
|
|
|
31
43
|
|
|
32
44
|
# Sort by deletion time (most recent first) - only for current page to maintain performance
|
|
33
45
|
@deleted_items.sort_by!(&:deleted_at).reverse!
|
|
46
|
+
|
|
47
|
+
# Handle export requests
|
|
48
|
+
respond_to do |format|
|
|
49
|
+
format.html
|
|
50
|
+
format.csv { export_to_csv(@filtered_items) }
|
|
51
|
+
format.json { export_to_json(@filtered_items) }
|
|
52
|
+
end
|
|
34
53
|
end
|
|
35
54
|
|
|
36
55
|
def show
|
|
@@ -46,7 +65,7 @@ module RecycleBin
|
|
|
46
65
|
redirect_to root_path, alert: "Failed to restore #{@item.class.name}."
|
|
47
66
|
end
|
|
48
67
|
rescue => e
|
|
49
|
-
|
|
68
|
+
log_error('Error restoring item', e, item: @item)
|
|
50
69
|
redirect_to root_path, alert: "Failed to restore #{@item.class.name}."
|
|
51
70
|
end
|
|
52
71
|
|
|
@@ -58,42 +77,86 @@ module RecycleBin
|
|
|
58
77
|
redirect_to root_path, alert: "Failed to delete #{model_name}."
|
|
59
78
|
end
|
|
60
79
|
rescue => e
|
|
61
|
-
|
|
80
|
+
log_error('Error deleting item', e, item: @item)
|
|
62
81
|
redirect_to root_path, alert: "Failed to delete #{model_name}."
|
|
63
82
|
end
|
|
64
83
|
|
|
65
84
|
def bulk_restore
|
|
66
85
|
restored_count = 0
|
|
86
|
+
failed_count = 0
|
|
67
87
|
selected_items = parse_bulk_selection
|
|
68
88
|
|
|
89
|
+
if selected_items.empty?
|
|
90
|
+
redirect_to trash_index_path, alert: 'No items selected for restoration.'
|
|
91
|
+
return
|
|
92
|
+
end
|
|
93
|
+
|
|
69
94
|
selected_items.each do |model_class, id|
|
|
70
95
|
next unless model_class && id
|
|
71
96
|
|
|
72
97
|
model = safe_constantize_model(model_class)
|
|
73
|
-
|
|
98
|
+
unless model.respond_to?(:with_deleted)
|
|
99
|
+
Rails.logger.warn "Model #{model_class} does not support soft delete"
|
|
100
|
+
failed_count += 1
|
|
101
|
+
next
|
|
102
|
+
end
|
|
74
103
|
|
|
75
104
|
item = model.with_deleted.find_by(id: id)
|
|
76
|
-
|
|
105
|
+
if item&.restore
|
|
106
|
+
restored_count += 1
|
|
107
|
+
Rails.logger.info "Successfully restored #{model_class}##{id}"
|
|
108
|
+
else
|
|
109
|
+
failed_count += 1
|
|
110
|
+
Rails.logger.warn "Failed to restore #{model_class}##{id}"
|
|
111
|
+
end
|
|
77
112
|
end
|
|
78
113
|
|
|
79
|
-
|
|
114
|
+
message = "#{restored_count} items restored successfully!"
|
|
115
|
+
message += " (#{failed_count} failed)" if failed_count.positive?
|
|
116
|
+
|
|
117
|
+
redirect_to trash_index_path, notice: message
|
|
118
|
+
rescue => e
|
|
119
|
+
log_error('Bulk restore failed', e)
|
|
120
|
+
redirect_to trash_index_path, alert: "Bulk restore failed: #{e.message}"
|
|
80
121
|
end
|
|
81
122
|
|
|
82
123
|
def bulk_destroy
|
|
83
124
|
destroyed_count = 0
|
|
125
|
+
failed_count = 0
|
|
84
126
|
selected_items = parse_bulk_selection
|
|
85
127
|
|
|
128
|
+
if selected_items.empty?
|
|
129
|
+
redirect_to trash_index_path, alert: 'No items selected for deletion.'
|
|
130
|
+
return
|
|
131
|
+
end
|
|
132
|
+
|
|
86
133
|
selected_items.each do |model_class, id|
|
|
87
134
|
next unless model_class && id
|
|
88
135
|
|
|
89
136
|
model = safe_constantize_model(model_class)
|
|
90
|
-
|
|
137
|
+
unless model.respond_to?(:with_deleted)
|
|
138
|
+
Rails.logger.warn "Model #{model_class} does not support soft delete"
|
|
139
|
+
failed_count += 1
|
|
140
|
+
next
|
|
141
|
+
end
|
|
91
142
|
|
|
92
143
|
item = model.with_deleted.find_by(id: id)
|
|
93
|
-
|
|
144
|
+
if item&.destroy!
|
|
145
|
+
destroyed_count += 1
|
|
146
|
+
Rails.logger.info "Successfully destroyed #{model_class}##{id}"
|
|
147
|
+
else
|
|
148
|
+
failed_count += 1
|
|
149
|
+
Rails.logger.warn "Failed to destroy #{model_class}##{id}"
|
|
150
|
+
end
|
|
94
151
|
end
|
|
95
152
|
|
|
96
|
-
|
|
153
|
+
message = "#{destroyed_count} items permanently deleted."
|
|
154
|
+
message += " (#{failed_count} failed)" if failed_count.positive?
|
|
155
|
+
|
|
156
|
+
redirect_to trash_index_path, notice: message
|
|
157
|
+
rescue => e
|
|
158
|
+
log_error('Bulk destroy failed', e)
|
|
159
|
+
redirect_to trash_index_path, alert: "Bulk destroy failed: #{e.message}"
|
|
97
160
|
end
|
|
98
161
|
|
|
99
162
|
private
|
|
@@ -103,6 +166,62 @@ module RecycleBin
|
|
|
103
166
|
@all_deleted_items_relation = build_deleted_items_relation
|
|
104
167
|
end
|
|
105
168
|
|
|
169
|
+
def load_dashboard_data
|
|
170
|
+
# Get all deleted items for dashboard
|
|
171
|
+
@all_deleted_items = RecycleBin.all_deleted_items(limit: 1000).to_a
|
|
172
|
+
@total_count = @all_deleted_items.count
|
|
173
|
+
@model_types = @all_deleted_items.map { |item| item.class.name }.uniq.sort
|
|
174
|
+
|
|
175
|
+
# Recent activity (last 10 items)
|
|
176
|
+
@recent_items = @all_deleted_items.sort_by(&:deleted_at).reverse.first(10)
|
|
177
|
+
|
|
178
|
+
# Model breakdown for chart
|
|
179
|
+
model_counts = @all_deleted_items.group_by { |item| item.class.name }
|
|
180
|
+
model_data = model_counts.map do |model_name, items|
|
|
181
|
+
count = items.count
|
|
182
|
+
percentage = @total_count.positive? ? (count.to_f / @total_count * 100) : 0
|
|
183
|
+
{
|
|
184
|
+
name: model_name,
|
|
185
|
+
count: count,
|
|
186
|
+
percentage: percentage
|
|
187
|
+
}
|
|
188
|
+
end
|
|
189
|
+
@model_breakdown = model_data.sort_by { |data| -data[:count] }
|
|
190
|
+
|
|
191
|
+
# Weekly stats (last 7 days)
|
|
192
|
+
@weekly_stats = if @all_deleted_items.any?
|
|
193
|
+
calculate_weekly_stats(@all_deleted_items)
|
|
194
|
+
else
|
|
195
|
+
[]
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def calculate_weekly_stats(items)
|
|
200
|
+
# Get last 7 days
|
|
201
|
+
days = 7.times.map { |i| (Date.current - i.days) }.reverse
|
|
202
|
+
|
|
203
|
+
# Group items by date
|
|
204
|
+
items_by_date = items.group_by { |item| item.deleted_at.to_date }
|
|
205
|
+
|
|
206
|
+
# Calculate stats for each day
|
|
207
|
+
max_count = 0
|
|
208
|
+
day_stats = days.map do |date|
|
|
209
|
+
count = items_by_date[date]&.count || 0
|
|
210
|
+
max_count = [max_count, count].max
|
|
211
|
+
{
|
|
212
|
+
day: date.strftime('%a'),
|
|
213
|
+
count: count,
|
|
214
|
+
date: date
|
|
215
|
+
}
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Calculate percentages
|
|
219
|
+
day_stats.map do |stat|
|
|
220
|
+
stat[:percentage] = max_count.positive? ? (stat[:count].to_f / max_count * 100) : 0
|
|
221
|
+
stat
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
106
225
|
def build_deleted_items_relation
|
|
107
226
|
relations = []
|
|
108
227
|
|
|
@@ -114,13 +233,12 @@ module RecycleBin
|
|
|
114
233
|
relations << relation
|
|
115
234
|
end
|
|
116
235
|
rescue => e
|
|
117
|
-
|
|
236
|
+
log_debug("Skipping model #{model_name}: #{e.message}")
|
|
118
237
|
next
|
|
119
238
|
end
|
|
120
239
|
|
|
121
240
|
# If we have relations, combine them; otherwise return empty relation
|
|
122
241
|
if relations.any?
|
|
123
|
-
# For now, we'll work with arrays since UNION queries are complex across different models
|
|
124
242
|
# Convert relations to arrays and combine
|
|
125
243
|
combined_items = []
|
|
126
244
|
relations.each do |relation|
|
|
@@ -137,12 +255,81 @@ module RecycleBin
|
|
|
137
255
|
def filter_items_relation(items_collection)
|
|
138
256
|
filtered_items = items_collection.items
|
|
139
257
|
|
|
258
|
+
# Apply search filter first
|
|
259
|
+
filtered_items = filter_by_search(filtered_items) if params[:search].present?
|
|
260
|
+
|
|
261
|
+
# Apply other filters
|
|
140
262
|
filtered_items = filter_by_type(filtered_items) if params[:type].present?
|
|
141
263
|
filtered_items = filter_by_time(filtered_items) if params[:time].present?
|
|
264
|
+
filtered_items = filter_by_user(filtered_items) if params[:deleted_by].present?
|
|
265
|
+
filtered_items = filter_by_size(filtered_items) if params[:size].present?
|
|
266
|
+
filtered_items = filter_by_date_range(filtered_items) if params[:date_from].present? || params[:date_to].present?
|
|
142
267
|
|
|
143
268
|
DeletedItemsCollection.new(filtered_items)
|
|
144
269
|
end
|
|
145
270
|
|
|
271
|
+
def filter_by_search(items)
|
|
272
|
+
search_term = params[:search].downcase.strip
|
|
273
|
+
return items if search_term.blank?
|
|
274
|
+
|
|
275
|
+
items.select do |item|
|
|
276
|
+
# Priority 1: Exact matches in primary title fields (highest relevance)
|
|
277
|
+
primary_fields = %w[title name subject]
|
|
278
|
+
exact_title_match = primary_fields.any? do |field|
|
|
279
|
+
if item.respond_to?(field)
|
|
280
|
+
field_value = item.send(field)
|
|
281
|
+
field_value && field_value.to_s.downcase.strip == search_term
|
|
282
|
+
else
|
|
283
|
+
false
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Priority 2: Starts with matches in primary fields
|
|
288
|
+
starts_with_match = !exact_title_match && primary_fields.any? do |field|
|
|
289
|
+
if item.respond_to?(field)
|
|
290
|
+
field_value = item.send(field)
|
|
291
|
+
field_value && field_value.to_s.downcase.strip.start_with?(search_term)
|
|
292
|
+
else
|
|
293
|
+
false
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Priority 3: Contains matches in primary fields (only if search term is 3+ characters)
|
|
298
|
+
primary_contains_match = !exact_title_match && !starts_with_match && search_term.length >= 3 && primary_fields.any? do |field|
|
|
299
|
+
if item.respond_to?(field)
|
|
300
|
+
field_value = item.send(field)
|
|
301
|
+
field_value && field_value.to_s.downcase.include?(search_term)
|
|
302
|
+
else
|
|
303
|
+
false
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Priority 4: ID exact match
|
|
308
|
+
id_match = !exact_title_match && !starts_with_match && !primary_contains_match && item.id.to_s == search_term
|
|
309
|
+
|
|
310
|
+
# Priority 5: Model class name match (only if search term is 3+ characters)
|
|
311
|
+
class_match = !exact_title_match && !starts_with_match && !primary_contains_match && !id_match &&
|
|
312
|
+
search_term.length >= 3 && item.class.name.downcase.include?(search_term)
|
|
313
|
+
|
|
314
|
+
# Only search secondary fields for longer search terms (4+ characters) and as last resort
|
|
315
|
+
secondary_match = false
|
|
316
|
+
if !exact_title_match && !starts_with_match && !primary_contains_match && !id_match && !class_match && search_term.length >= 4
|
|
317
|
+
secondary_fields = %w[email] # Removed content, body, description to reduce false matches
|
|
318
|
+
secondary_match = secondary_fields.any? do |field|
|
|
319
|
+
if item.respond_to?(field)
|
|
320
|
+
field_value = item.send(field)
|
|
321
|
+
field_value && !field_value.to_s.strip.empty? && field_value.to_s.downcase.include?(search_term)
|
|
322
|
+
else
|
|
323
|
+
false
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Return true if any priority level matches
|
|
329
|
+
exact_title_match || starts_with_match || primary_contains_match || id_match || class_match || secondary_match
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
146
333
|
def filter_by_type(items)
|
|
147
334
|
target_class_name = params[:type]
|
|
148
335
|
items.select { |item| item.class.name == target_class_name }
|
|
@@ -153,12 +340,77 @@ module RecycleBin
|
|
|
153
340
|
when 'today' then 1.day.ago
|
|
154
341
|
when 'week' then 1.week.ago
|
|
155
342
|
when 'month' then 1.month.ago
|
|
343
|
+
when 'year' then 1.year.ago
|
|
156
344
|
else return items
|
|
157
345
|
end
|
|
158
346
|
|
|
159
347
|
items.select { |item| item.deleted_at >= cutoff_time }
|
|
160
348
|
end
|
|
161
349
|
|
|
350
|
+
def filter_by_user(items)
|
|
351
|
+
user_id = params[:deleted_by]
|
|
352
|
+
return items if user_id.blank?
|
|
353
|
+
|
|
354
|
+
items.select do |item|
|
|
355
|
+
# Check if item has user association or deleted_by field
|
|
356
|
+
if item.respond_to?(:user_id)
|
|
357
|
+
item.user_id.to_s == user_id.to_s
|
|
358
|
+
elsif item.respond_to?(:deleted_by)
|
|
359
|
+
item.deleted_by.to_s == user_id.to_s
|
|
360
|
+
elsif item.respond_to?(:created_by)
|
|
361
|
+
item.created_by.to_s == user_id.to_s
|
|
362
|
+
else
|
|
363
|
+
false
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def filter_by_size(items)
|
|
369
|
+
size_filter = params[:size]
|
|
370
|
+
return items if size_filter.blank?
|
|
371
|
+
|
|
372
|
+
items.select do |item|
|
|
373
|
+
item_size = calculate_item_memory_size(item)
|
|
374
|
+
case size_filter
|
|
375
|
+
when 'small'
|
|
376
|
+
item_size < 1.kilobyte
|
|
377
|
+
when 'medium'
|
|
378
|
+
item_size >= 1.kilobyte && item_size < 100.kilobytes
|
|
379
|
+
when 'large'
|
|
380
|
+
item_size >= 100.kilobytes
|
|
381
|
+
else
|
|
382
|
+
true
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def filter_by_date_range(items)
|
|
388
|
+
date_from = parse_date_param(params[:date_from])
|
|
389
|
+
date_to = parse_date_param(params[:date_to])
|
|
390
|
+
|
|
391
|
+
return items if date_from.nil? && date_to.nil?
|
|
392
|
+
|
|
393
|
+
items.select do |item|
|
|
394
|
+
item_date = item.deleted_at
|
|
395
|
+
next false unless item_date
|
|
396
|
+
|
|
397
|
+
from_condition = date_from.nil? || item_date >= date_from
|
|
398
|
+
to_condition = date_to.nil? || item_date <= date_to
|
|
399
|
+
|
|
400
|
+
from_condition && to_condition
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def parse_date_param(date_string)
|
|
405
|
+
return nil if date_string.blank?
|
|
406
|
+
|
|
407
|
+
begin
|
|
408
|
+
Date.parse(date_string)
|
|
409
|
+
rescue
|
|
410
|
+
nil
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
162
414
|
def get_all_model_types
|
|
163
415
|
all_items = build_deleted_items_relation.items
|
|
164
416
|
all_items.map { |item| item.class.name }.uniq.sort
|
|
@@ -175,43 +427,64 @@ module RecycleBin
|
|
|
175
427
|
@item = model_class.with_deleted.find(params[:id])
|
|
176
428
|
rescue ActiveRecord::RecordNotFound
|
|
177
429
|
redirect_to trash_index_path, alert: 'Item not found.'
|
|
430
|
+
rescue => e
|
|
431
|
+
log_error('Error finding item', e, model_type: params[:model_type], id: params[:id])
|
|
432
|
+
redirect_to trash_index_path, alert: 'An error occurred while finding the item.'
|
|
178
433
|
end
|
|
179
434
|
|
|
180
435
|
def load_associations(item)
|
|
436
|
+
return {} unless item.respond_to?(:class)
|
|
437
|
+
|
|
181
438
|
associations = {}
|
|
182
439
|
|
|
183
|
-
|
|
184
|
-
|
|
440
|
+
begin
|
|
441
|
+
item.class.reflect_on_all_associations.each do |association|
|
|
442
|
+
next if association.macro == :belongs_to
|
|
185
443
|
|
|
186
|
-
|
|
187
|
-
|
|
444
|
+
related_items = safely_load_association(item, association)
|
|
445
|
+
associations[association.name] = related_items if related_items.present?
|
|
446
|
+
end
|
|
447
|
+
rescue => e
|
|
448
|
+
log_error('Error loading associations', e, item: item)
|
|
449
|
+
associations = {}
|
|
188
450
|
end
|
|
189
451
|
|
|
190
452
|
associations
|
|
191
453
|
end
|
|
192
454
|
|
|
193
455
|
def safely_load_association(item, association)
|
|
456
|
+
return nil unless item.respond_to?(association.name)
|
|
457
|
+
|
|
194
458
|
related_items = item.send(association.name)
|
|
195
459
|
limit_association_items(related_items)
|
|
196
460
|
rescue => e
|
|
197
|
-
|
|
461
|
+
log_debug("Skipping association #{association.name}: #{e.message}")
|
|
198
462
|
nil
|
|
199
463
|
end
|
|
200
464
|
|
|
201
465
|
def limit_association_items(related_items)
|
|
466
|
+
return [] if related_items.blank?
|
|
202
467
|
return Array(related_items) unless related_items.respond_to?(:limit)
|
|
203
468
|
|
|
204
|
-
|
|
205
|
-
related_items.
|
|
206
|
-
|
|
207
|
-
|
|
469
|
+
begin
|
|
470
|
+
if related_items.respond_to?(:first)
|
|
471
|
+
related_items.is_a?(Array) ? related_items.first(10) : related_items
|
|
472
|
+
else
|
|
473
|
+
related_items.limit(10)
|
|
474
|
+
end
|
|
475
|
+
rescue => e
|
|
476
|
+
log_debug("Error limiting association items: #{e.message}")
|
|
477
|
+
Array(related_items).first(10)
|
|
208
478
|
end
|
|
209
479
|
end
|
|
210
480
|
|
|
211
481
|
def calculate_item_memory_size(item)
|
|
482
|
+
return 0 unless item.respond_to?(:attributes)
|
|
483
|
+
|
|
212
484
|
# Simple calculation of item memory footprint
|
|
213
485
|
item.attributes.to_s.bytesize
|
|
214
|
-
rescue
|
|
486
|
+
rescue => e
|
|
487
|
+
log_debug("Error calculating memory size: #{e.message}")
|
|
215
488
|
0
|
|
216
489
|
end
|
|
217
490
|
|
|
@@ -220,24 +493,38 @@ module RecycleBin
|
|
|
220
493
|
return [] unless selected_items.is_a?(Array)
|
|
221
494
|
|
|
222
495
|
parse_item_strings(selected_items)
|
|
496
|
+
rescue => e
|
|
497
|
+
log_error('Error parsing bulk selection', e)
|
|
498
|
+
[]
|
|
223
499
|
end
|
|
224
500
|
|
|
225
501
|
def extract_selected_items
|
|
226
502
|
selected_items = params[:selected_items]
|
|
227
503
|
|
|
504
|
+
return [] if selected_items.blank?
|
|
505
|
+
|
|
228
506
|
return JSON.parse(selected_items) if selected_items.is_a?(String)
|
|
229
507
|
|
|
230
508
|
selected_items
|
|
231
|
-
rescue JSON::ParserError
|
|
509
|
+
rescue JSON::ParserError => e
|
|
510
|
+
log_error('Invalid JSON in selected items', e)
|
|
511
|
+
[]
|
|
512
|
+
rescue => e
|
|
513
|
+
log_error('Error extracting selected items', e)
|
|
232
514
|
[]
|
|
233
515
|
end
|
|
234
516
|
|
|
235
517
|
def parse_item_strings(selected_items)
|
|
518
|
+
return [] unless selected_items.is_a?(Array)
|
|
519
|
+
|
|
236
520
|
parsed_items = selected_items.filter_map do |item_string|
|
|
237
521
|
parse_single_item(item_string)
|
|
238
522
|
end
|
|
239
523
|
|
|
240
524
|
parsed_items.reject { |model_class, id| model_class.blank? || id.zero? }
|
|
525
|
+
rescue => e
|
|
526
|
+
log_error('Error parsing item strings', e)
|
|
527
|
+
[]
|
|
241
528
|
end
|
|
242
529
|
|
|
243
530
|
def parse_single_item(item_string)
|
|
@@ -247,6 +534,64 @@ module RecycleBin
|
|
|
247
534
|
return nil if model_class.blank? || id.blank?
|
|
248
535
|
|
|
249
536
|
[model_class, id.to_i]
|
|
537
|
+
rescue => e
|
|
538
|
+
log_debug("Error parsing single item '#{item_string}': #{e.message}")
|
|
539
|
+
nil
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
# Export functionality
|
|
543
|
+
def export_to_csv(items)
|
|
544
|
+
csv_data = generate_csv_data(items)
|
|
545
|
+
send_data csv_data,
|
|
546
|
+
filename: "recycle_bin_export_#{Time.current.strftime('%Y%m%d_%H%M%S')}.csv",
|
|
547
|
+
type: 'text/csv'
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def export_to_json(items)
|
|
551
|
+
json_data = generate_json_data(items)
|
|
552
|
+
send_data json_data,
|
|
553
|
+
filename: "recycle_bin_export_#{Time.current.strftime('%Y%m%d_%H%M%S')}.json",
|
|
554
|
+
type: 'application/json'
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def generate_csv_data(items)
|
|
558
|
+
require 'csv'
|
|
559
|
+
|
|
560
|
+
CSV.generate(headers: true) do |csv|
|
|
561
|
+
# Headers
|
|
562
|
+
csv << ['ID', 'Model Type', 'Title', 'Deleted At', 'Size (bytes)', 'Attributes']
|
|
563
|
+
|
|
564
|
+
# Data rows
|
|
565
|
+
items.each do |item|
|
|
566
|
+
csv << [
|
|
567
|
+
item.id,
|
|
568
|
+
item.class.name,
|
|
569
|
+
item.recyclable_title,
|
|
570
|
+
item.deleted_at&.strftime('%Y-%m-%d %H:%M:%S'),
|
|
571
|
+
calculate_item_memory_size(item),
|
|
572
|
+
item.attributes.except('deleted_at').to_json
|
|
573
|
+
]
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
def generate_json_data(items)
|
|
579
|
+
data = {
|
|
580
|
+
export_date: Time.current.iso8601,
|
|
581
|
+
total_items: items.count,
|
|
582
|
+
items: items.map do |item|
|
|
583
|
+
{
|
|
584
|
+
id: item.id,
|
|
585
|
+
model_type: item.class.name,
|
|
586
|
+
title: item.recyclable_title,
|
|
587
|
+
deleted_at: item.deleted_at&.iso8601,
|
|
588
|
+
size_bytes: calculate_item_memory_size(item),
|
|
589
|
+
attributes: item.attributes.except('deleted_at')
|
|
590
|
+
}
|
|
591
|
+
end
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
JSON.pretty_generate(data)
|
|
250
595
|
end
|
|
251
596
|
|
|
252
597
|
def trash_index_path
|
|
@@ -254,6 +599,60 @@ module RecycleBin
|
|
|
254
599
|
rescue StandardError
|
|
255
600
|
root_path
|
|
256
601
|
end
|
|
602
|
+
|
|
603
|
+
def safe_constantize_model(model_name)
|
|
604
|
+
return nil if model_name.blank?
|
|
605
|
+
|
|
606
|
+
model_name.constantize
|
|
607
|
+
rescue NameError => e
|
|
608
|
+
log_warn("Failed to constantize model #{model_name}: #{e.message}")
|
|
609
|
+
nil
|
|
610
|
+
rescue => e
|
|
611
|
+
log_error("Unexpected error constantizing model #{model_name}", e)
|
|
612
|
+
nil
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
# Enhanced logging methods
|
|
616
|
+
def log_error(message, error, context = {})
|
|
617
|
+
error_context = {
|
|
618
|
+
message: message,
|
|
619
|
+
error: error.message,
|
|
620
|
+
backtrace: error.backtrace&.first(5),
|
|
621
|
+
controller: controller_name,
|
|
622
|
+
action: action_name,
|
|
623
|
+
params: params.except(:controller, :action, :password, :password_confirmation),
|
|
624
|
+
**context
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
Rails.logger.error("RecycleBin Error: #{JSON.pretty_generate(error_context)}")
|
|
628
|
+
|
|
629
|
+
# Log to external service if configured
|
|
630
|
+
return unless RecycleBin.config.respond_to?(:error_logging_service)
|
|
631
|
+
|
|
632
|
+
RecycleBin.config.error_logging_service.call(error_context)
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
def log_warn(message, context = {})
|
|
636
|
+
warn_context = {
|
|
637
|
+
message: message,
|
|
638
|
+
controller: controller_name,
|
|
639
|
+
action: action_name,
|
|
640
|
+
**context
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
Rails.logger.warn("RecycleBin Warning: #{JSON.pretty_generate(warn_context)}")
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
def log_debug(message, context = {})
|
|
647
|
+
debug_context = {
|
|
648
|
+
message: message,
|
|
649
|
+
controller: controller_name,
|
|
650
|
+
action: action_name,
|
|
651
|
+
**context
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
Rails.logger.debug("RecycleBin Debug: #{JSON.pretty_generate(debug_context)}")
|
|
655
|
+
end
|
|
257
656
|
end
|
|
258
657
|
|
|
259
658
|
# Helper class to work with combined deleted items from different models
|
|
@@ -292,6 +691,10 @@ module RecycleBin
|
|
|
292
691
|
DeletedItemsCollection.new(@items.select(&block))
|
|
293
692
|
end
|
|
294
693
|
|
|
694
|
+
def reject(&block)
|
|
695
|
+
DeletedItemsCollection.new(@items.reject(&block))
|
|
696
|
+
end
|
|
697
|
+
|
|
295
698
|
def empty?
|
|
296
699
|
@items.empty?
|
|
297
700
|
end
|