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.
@@ -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