recycle_bin 1.1.1 → 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 +40 -0
- data/README.md +136 -7
- data/app/controllers/recycle_bin/trash_controller.rb +418 -21
- data/app/helpers/recycle_bin/application_helper.rb +175 -0
- data/app/views/recycle_bin/layouts/recycle_bin.html.erb +33 -32
- data/app/views/recycle_bin/trash/_action_history.html.erb +13 -9
- data/app/views/recycle_bin/trash/_associations.html.erb +9 -5
- data/app/views/recycle_bin/trash/_filters.html.erb +558 -14
- data/app/views/recycle_bin/trash/_item.html.erb +262 -19
- data/app/views/recycle_bin/trash/_stats.html.erb +27 -9
- data/app/views/recycle_bin/trash/dashboard.html.erb +618 -0
- data/app/views/recycle_bin/trash/index.html.erb +246 -17
- data/config/routes.rb +9 -2
- data/lib/recycle_bin/version.rb +1 -1
- data/lib/recycle_bin.rb +111 -1
- metadata +7 -4
- data/recycle_bin.gemspec +0 -47
|
@@ -27,5 +27,180 @@ module RecycleBin
|
|
|
27
27
|
time.strftime('%B %d, %Y')
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
|
+
|
|
31
|
+
# Check if any filters are currently active
|
|
32
|
+
def any_filters_active?
|
|
33
|
+
%w[search type time deleted_by size date_from date_to].any? { |param| params[param].present? }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Generate human-readable label for time filter
|
|
37
|
+
def time_filter_label(time_value)
|
|
38
|
+
case time_value
|
|
39
|
+
when 'today'
|
|
40
|
+
'Today'
|
|
41
|
+
when 'week'
|
|
42
|
+
'This Week'
|
|
43
|
+
when 'month'
|
|
44
|
+
'This Month'
|
|
45
|
+
when 'year'
|
|
46
|
+
'This Year'
|
|
47
|
+
else
|
|
48
|
+
time_value.to_s.humanize
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Generate human-readable label for size filter
|
|
53
|
+
def size_filter_label(size_value)
|
|
54
|
+
case size_value
|
|
55
|
+
when 'small'
|
|
56
|
+
'Small (< 1KB)'
|
|
57
|
+
when 'medium'
|
|
58
|
+
'Medium (1KB - 100KB)'
|
|
59
|
+
when 'large'
|
|
60
|
+
'Large (> 100KB)'
|
|
61
|
+
else
|
|
62
|
+
size_value.to_s.humanize
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Classify item size for CSS styling
|
|
67
|
+
def size_class(item)
|
|
68
|
+
size_bytes = calculate_item_memory_size(item)
|
|
69
|
+
|
|
70
|
+
if size_bytes < 1.kilobyte
|
|
71
|
+
'small'
|
|
72
|
+
elsif size_bytes < 100.kilobytes
|
|
73
|
+
'medium'
|
|
74
|
+
else
|
|
75
|
+
'large'
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Calculate item memory size (helper method for views)
|
|
80
|
+
def calculate_item_memory_size(item)
|
|
81
|
+
return 0 unless item.respond_to?(:attributes)
|
|
82
|
+
|
|
83
|
+
# Simple calculation of item memory footprint
|
|
84
|
+
item.attributes.to_s.bytesize
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
Rails.logger.debug("Error calculating memory size: #{e.message}")
|
|
87
|
+
0
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Format file size in human-readable format
|
|
91
|
+
def human_file_size(bytes)
|
|
92
|
+
return '0 B' if bytes.nil? || bytes.zero?
|
|
93
|
+
|
|
94
|
+
units = %w[B KB MB GB TB]
|
|
95
|
+
size = bytes.to_f
|
|
96
|
+
unit_index = 0
|
|
97
|
+
|
|
98
|
+
while size >= 1024 && unit_index < units.length - 1
|
|
99
|
+
size /= 1024
|
|
100
|
+
unit_index += 1
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
"#{size.round(2)} #{units[unit_index]}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Highlight search terms in text
|
|
107
|
+
def highlight_search_terms(text, search_term)
|
|
108
|
+
return text if search_term.blank? || text.blank?
|
|
109
|
+
|
|
110
|
+
text.to_s.gsub(/(#{Regexp.escape(search_term)})/i, '<mark>\1</mark>').html_safe
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Generate search result summary
|
|
114
|
+
def search_result_summary(total_count, search_term)
|
|
115
|
+
if search_term.present?
|
|
116
|
+
"Found #{total_count} result#{total_count == 1 ? '' : 's'} for \"#{search_term}\""
|
|
117
|
+
else
|
|
118
|
+
"#{total_count} item#{total_count == 1 ? '' : 's'} in trash"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Check if current page has search results
|
|
123
|
+
def search_results?
|
|
124
|
+
@deleted_items&.any?
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Generate export filename with current filters
|
|
128
|
+
def export_filename_with_filters(base_name, format)
|
|
129
|
+
filters = []
|
|
130
|
+
filters << "search_#{params[:search]}" if params[:search].present?
|
|
131
|
+
filters << "type_#{params[:type]}" if params[:type].present?
|
|
132
|
+
filters << "time_#{params[:time]}" if params[:time].present?
|
|
133
|
+
filters << "user_#{params[:deleted_by]}" if params[:deleted_by].present?
|
|
134
|
+
filters << "size_#{params[:size]}" if params[:size].present?
|
|
135
|
+
|
|
136
|
+
if params[:date_from].present? || params[:date_to].present?
|
|
137
|
+
date_range = [params[:date_from], params[:date_to]].compact.join('_to_')
|
|
138
|
+
filters << "date_#{date_range}" if date_range.present?
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
timestamp = Time.current.strftime('%Y%m%d_%H%M%S')
|
|
142
|
+
filter_suffix = filters.any? ? "_#{filters.join('_')}" : ''
|
|
143
|
+
|
|
144
|
+
"#{base_name}#{filter_suffix}_#{timestamp}.#{format}"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Generate filter breadcrumb
|
|
148
|
+
def filter_breadcrumb
|
|
149
|
+
return unless any_filters_active?
|
|
150
|
+
|
|
151
|
+
breadcrumbs = []
|
|
152
|
+
|
|
153
|
+
breadcrumbs << "Search: \"#{params[:search]}\"" if params[:search].present?
|
|
154
|
+
|
|
155
|
+
breadcrumbs << "Type: #{params[:type]}" if params[:type].present?
|
|
156
|
+
|
|
157
|
+
breadcrumbs << "Time: #{time_filter_label(params[:time])}" if params[:time].present?
|
|
158
|
+
|
|
159
|
+
breadcrumbs << "User: #{params[:deleted_by]}" if params[:deleted_by].present?
|
|
160
|
+
|
|
161
|
+
breadcrumbs << "Size: #{size_filter_label(params[:size])}" if params[:size].present?
|
|
162
|
+
|
|
163
|
+
if params[:date_from].present? || params[:date_to].present?
|
|
164
|
+
date_range = [params[:date_from], params[:date_to]].compact.join(' to ')
|
|
165
|
+
breadcrumbs << "Date: #{date_range}"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
breadcrumbs.join(' • ')
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Check if advanced filters are visible
|
|
172
|
+
def advanced_filters_visible?
|
|
173
|
+
%w[deleted_by size date_from date_to].any? { |param| params[param].present? }
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Generate filter count badge
|
|
177
|
+
def active_filter_count
|
|
178
|
+
count = 0
|
|
179
|
+
count += 1 if params[:search].present?
|
|
180
|
+
count += 1 if params[:type].present?
|
|
181
|
+
count += 1 if params[:time].present?
|
|
182
|
+
count += 1 if params[:deleted_by].present?
|
|
183
|
+
count += 1 if params[:size].present?
|
|
184
|
+
count += 1 if params[:date_from].present? || params[:date_to].present?
|
|
185
|
+
count
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Check if item matches current search
|
|
189
|
+
def item_matches_search?(item, search_term)
|
|
190
|
+
return true if search_term.blank?
|
|
191
|
+
|
|
192
|
+
search_term = search_term.downcase
|
|
193
|
+
|
|
194
|
+
# Check common fields
|
|
195
|
+
%w[title name email content body description].any? do |field|
|
|
196
|
+
item.send(field).to_s.downcase.include?(search_term) if item.respond_to?(field) && item.send(field).present?
|
|
197
|
+
end ||
|
|
198
|
+
# Check ID
|
|
199
|
+
item.id.to_s.include?(search_term) ||
|
|
200
|
+
# Check model class name
|
|
201
|
+
item.class.name.downcase.include?(search_term) ||
|
|
202
|
+
# Check all attributes
|
|
203
|
+
item.attributes.values.any? { |val| val.to_s.downcase.include?(search_term) }
|
|
204
|
+
end
|
|
30
205
|
end
|
|
31
206
|
end
|
|
@@ -461,37 +461,37 @@
|
|
|
461
461
|
flex-direction: column;
|
|
462
462
|
gap: 1rem;
|
|
463
463
|
}
|
|
464
|
-
|
|
464
|
+
|
|
465
465
|
.nav {
|
|
466
466
|
width: 100%;
|
|
467
467
|
justify-content: center;
|
|
468
468
|
}
|
|
469
|
-
|
|
469
|
+
|
|
470
470
|
.container {
|
|
471
471
|
padding: 1rem;
|
|
472
472
|
}
|
|
473
|
-
|
|
473
|
+
|
|
474
474
|
.filters {
|
|
475
475
|
flex-direction: column;
|
|
476
476
|
align-items: flex-start;
|
|
477
477
|
gap: 1rem;
|
|
478
478
|
}
|
|
479
|
-
|
|
479
|
+
|
|
480
480
|
.table th,
|
|
481
481
|
.table td {
|
|
482
482
|
padding: 0.5rem;
|
|
483
483
|
}
|
|
484
|
-
|
|
484
|
+
|
|
485
485
|
.stats-grid {
|
|
486
486
|
grid-template-columns: repeat(2, 1fr);
|
|
487
487
|
}
|
|
488
|
-
|
|
488
|
+
|
|
489
489
|
.card-header {
|
|
490
490
|
flex-direction: column;
|
|
491
491
|
gap: 1rem;
|
|
492
492
|
align-items: flex-start;
|
|
493
493
|
}
|
|
494
|
-
|
|
494
|
+
|
|
495
495
|
.card-actions {
|
|
496
496
|
width: 100%;
|
|
497
497
|
justify-content: flex-start;
|
|
@@ -502,7 +502,7 @@
|
|
|
502
502
|
.table thead {
|
|
503
503
|
display: none;
|
|
504
504
|
}
|
|
505
|
-
|
|
505
|
+
|
|
506
506
|
.table tr {
|
|
507
507
|
display: block;
|
|
508
508
|
margin-bottom: 1rem;
|
|
@@ -510,14 +510,14 @@
|
|
|
510
510
|
border-radius: 8px;
|
|
511
511
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
512
512
|
}
|
|
513
|
-
|
|
513
|
+
|
|
514
514
|
.table td {
|
|
515
515
|
display: block;
|
|
516
516
|
padding: 0.5rem 1rem;
|
|
517
517
|
border: none;
|
|
518
518
|
border-bottom: 1px solid #f0f0f0;
|
|
519
519
|
}
|
|
520
|
-
|
|
520
|
+
|
|
521
521
|
.table td:before {
|
|
522
522
|
content: attr(data-label) ": ";
|
|
523
523
|
font-weight: bold;
|
|
@@ -681,18 +681,18 @@
|
|
|
681
681
|
<div class="header">
|
|
682
682
|
<div class="header-content">
|
|
683
683
|
<div class="logo">
|
|
684
|
-
<img src="https://raw.githubusercontent.com/R95-del/recycle_bin/main/docs/logo.svg"
|
|
685
|
-
alt="RecycleBin Logo"
|
|
684
|
+
<img src="https://raw.githubusercontent.com/R95-del/recycle_bin/main/docs/logo.svg"
|
|
685
|
+
alt="RecycleBin Logo"
|
|
686
686
|
class="logo-icon"
|
|
687
687
|
onerror="this.style.display='none'; this.nextElementSibling.style.display='inline';">
|
|
688
688
|
<span class="icon-fallback" style="display:none;">🗑️</span>
|
|
689
689
|
<span>RecycleBin</span>
|
|
690
690
|
</div>
|
|
691
|
-
|
|
691
|
+
|
|
692
692
|
<nav class="nav">
|
|
693
|
-
<%= link_to "Dashboard", recycle_bin.root_path,
|
|
694
|
-
class: ("active" if current_page?(recycle_bin.root_path)) %>
|
|
695
|
-
<%= link_to "All Items", recycle_bin.trash_index_path,
|
|
693
|
+
<%= link_to "Dashboard", recycle_bin.root_path,
|
|
694
|
+
class: ("active" if current_page?(recycle_bin.root_path) || current_page?(recycle_bin.dashboard_path)) %>
|
|
695
|
+
<%= link_to "All Items", recycle_bin.trash_index_path,
|
|
696
696
|
class: ("active" if current_page?(recycle_bin.trash_index_path)) %>
|
|
697
697
|
</nav>
|
|
698
698
|
</div>
|
|
@@ -717,26 +717,26 @@
|
|
|
717
717
|
// Handle custom method (e.g., PATCH, DELETE)
|
|
718
718
|
if (this.dataset.method) {
|
|
719
719
|
e.preventDefault();
|
|
720
|
-
|
|
720
|
+
|
|
721
721
|
const method = this.dataset.method.toUpperCase();
|
|
722
722
|
const url = this.href;
|
|
723
723
|
const form = document.createElement('form');
|
|
724
724
|
form.method = 'POST';
|
|
725
725
|
form.action = url;
|
|
726
|
-
|
|
726
|
+
|
|
727
727
|
const methodInput = document.createElement('input');
|
|
728
728
|
methodInput.type = 'hidden';
|
|
729
729
|
methodInput.name = '_method';
|
|
730
730
|
methodInput.value = method;
|
|
731
731
|
form.appendChild(methodInput);
|
|
732
|
-
|
|
732
|
+
|
|
733
733
|
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
|
|
734
734
|
const csrfInput = document.createElement('input');
|
|
735
735
|
csrfInput.type = 'hidden';
|
|
736
736
|
csrfInput.name = 'authenticity_token';
|
|
737
737
|
csrfInput.value = csrfToken;
|
|
738
738
|
form.appendChild(csrfInput);
|
|
739
|
-
|
|
739
|
+
|
|
740
740
|
document.body.appendChild(form);
|
|
741
741
|
form.submit();
|
|
742
742
|
}
|
|
@@ -745,7 +745,7 @@
|
|
|
745
745
|
|
|
746
746
|
// Bulk actions and other interactivity
|
|
747
747
|
updateBulkActions();
|
|
748
|
-
|
|
748
|
+
|
|
749
749
|
document.addEventListener('click', function(e) {
|
|
750
750
|
if (e.target.matches('[data-copy]')) {
|
|
751
751
|
const text = e.target.dataset.copy;
|
|
@@ -760,7 +760,7 @@
|
|
|
760
760
|
|
|
761
761
|
const autoRefreshCheckbox = document.getElementById('auto-refresh');
|
|
762
762
|
let autoRefreshInterval;
|
|
763
|
-
|
|
763
|
+
|
|
764
764
|
if (autoRefreshCheckbox) {
|
|
765
765
|
autoRefreshCheckbox.addEventListener('change', function() {
|
|
766
766
|
if (this.checked) {
|
|
@@ -777,11 +777,11 @@
|
|
|
777
777
|
function toggleSelectAll() {
|
|
778
778
|
const selectAllCheckbox = document.getElementById('select-all');
|
|
779
779
|
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
|
|
780
|
-
|
|
780
|
+
|
|
781
781
|
itemCheckboxes.forEach(checkbox => {
|
|
782
782
|
checkbox.checked = selectAllCheckbox.checked;
|
|
783
783
|
});
|
|
784
|
-
|
|
784
|
+
|
|
785
785
|
updateBulkActions();
|
|
786
786
|
}
|
|
787
787
|
|
|
@@ -790,14 +790,15 @@
|
|
|
790
790
|
const bulkActions = document.getElementById('bulk-actions');
|
|
791
791
|
const bulkCount = document.getElementById('bulk-count');
|
|
792
792
|
const selectAllCheckbox = document.getElementById('select-all');
|
|
793
|
-
|
|
793
|
+
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
|
|
794
|
+
|
|
794
795
|
if (checkedBoxes.length > 0) {
|
|
795
796
|
bulkActions.classList.add('show');
|
|
796
797
|
bulkCount.textContent = checkedBoxes.length + ' item' + (checkedBoxes.length > 1 ? 's' : '') + ' selected';
|
|
797
798
|
} else {
|
|
798
799
|
bulkActions.classList.remove('show');
|
|
799
800
|
}
|
|
800
|
-
|
|
801
|
+
|
|
801
802
|
if (checkedBoxes.length === itemCheckboxes.length && itemCheckboxes.length > 0) {
|
|
802
803
|
selectAllCheckbox.checked = true;
|
|
803
804
|
selectAllCheckbox.indeterminate = false;
|
|
@@ -812,31 +813,31 @@
|
|
|
812
813
|
|
|
813
814
|
function handleBulkAction(action) {
|
|
814
815
|
const checkedBoxes = document.querySelectorAll('.item-checkbox:checked');
|
|
815
|
-
|
|
816
|
+
|
|
816
817
|
if (checkedBoxes.length === 0) {
|
|
817
818
|
alert('Please select at least one item.');
|
|
818
819
|
return false;
|
|
819
820
|
}
|
|
820
|
-
|
|
821
|
+
|
|
821
822
|
const selectedItems = Array.from(checkedBoxes).map(cb => cb.value);
|
|
822
|
-
|
|
823
|
+
|
|
823
824
|
let message;
|
|
824
825
|
if (action === 'restore') {
|
|
825
826
|
message = 'Restore ' + checkedBoxes.length + ' selected item' + (checkedBoxes.length > 1 ? 's' : '') + '?';
|
|
826
827
|
} else {
|
|
827
828
|
message = 'Permanently delete ' + checkedBoxes.length + ' selected item' + (checkedBoxes.length > 1 ? 's' : '') + '? This cannot be undone!';
|
|
828
829
|
}
|
|
829
|
-
|
|
830
|
+
|
|
830
831
|
if (!confirm(message)) {
|
|
831
832
|
return false;
|
|
832
833
|
}
|
|
833
|
-
|
|
834
|
+
|
|
834
835
|
if (action === 'restore') {
|
|
835
836
|
document.getElementById('bulk-restore-items').value = JSON.stringify(selectedItems);
|
|
836
837
|
} else {
|
|
837
838
|
document.getElementById('bulk-destroy-items').value = JSON.stringify(selectedItems);
|
|
838
839
|
}
|
|
839
|
-
|
|
840
|
+
|
|
840
841
|
return true;
|
|
841
842
|
}
|
|
842
843
|
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
<tbody>
|
|
18
18
|
<% item.versions.each do |version| %>
|
|
19
19
|
<tr>
|
|
20
|
-
<td><span class="event-type"><%= version.event.humanize %></span></td>
|
|
20
|
+
<td><span class="event-type"><%= version.respond_to?(:event) ? version.event.humanize : 'Unknown' %></span></td>
|
|
21
21
|
<td>
|
|
22
|
-
<% if version.whodunnit %>
|
|
22
|
+
<% if version.respond_to?(:whodunnit) && version.whodunnit %>
|
|
23
23
|
<%= version.whodunnit %>
|
|
24
24
|
<% else %>
|
|
25
25
|
<span class="timestamp">System</span>
|
|
@@ -27,20 +27,24 @@
|
|
|
27
27
|
</td>
|
|
28
28
|
<td>
|
|
29
29
|
<span class="timestamp">
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
<% if version.respond_to?(:created_at) && version.created_at %>
|
|
31
|
+
<%= time_ago_in_words(version.created_at) %> ago •
|
|
32
|
+
<%= version.created_at.strftime('%B %d, %Y at %l:%M %p') %>
|
|
33
|
+
<% else %>
|
|
34
|
+
Unknown time
|
|
35
|
+
<% end %>
|
|
32
36
|
</span>
|
|
33
37
|
</td>
|
|
34
38
|
<td>
|
|
35
|
-
<% if version.changeset.any? %>
|
|
39
|
+
<% if version.respond_to?(:changeset) && version.changeset.any? %>
|
|
36
40
|
<details>
|
|
37
41
|
<summary class="btn btn-outline btn-sm">View Changes</summary>
|
|
38
42
|
<div class="changeset-details">
|
|
39
43
|
<% version.changeset.each do |key, changes| %>
|
|
40
44
|
<div class="change-item">
|
|
41
|
-
<strong><%= key.humanize %>:</strong>
|
|
42
|
-
<span class="change-from"><%= changes[0].nil? ? 'nil' : changes[0] %></span>
|
|
43
|
-
→
|
|
45
|
+
<strong><%= key.humanize %>:</strong>
|
|
46
|
+
<span class="change-from"><%= changes[0].nil? ? 'nil' : changes[0] %></span>
|
|
47
|
+
→
|
|
44
48
|
<span class="change-to"><%= changes[1].nil? ? 'nil' : changes[1] %></span>
|
|
45
49
|
</div>
|
|
46
50
|
<% end %>
|
|
@@ -68,4 +72,4 @@
|
|
|
68
72
|
</div>
|
|
69
73
|
<% end %>
|
|
70
74
|
</div>
|
|
71
|
-
</div>
|
|
75
|
+
</div>
|
|
@@ -15,13 +15,17 @@
|
|
|
15
15
|
<div class="association-item">
|
|
16
16
|
<div class="association-content">
|
|
17
17
|
<div class="card-title">
|
|
18
|
-
<%= item.class.name %> -
|
|
19
|
-
<%= item.respond_to?(:title) ? item.title :
|
|
20
|
-
item.respond_to?(:name) ? item.name :
|
|
18
|
+
<%= item.class.name %> -
|
|
19
|
+
<%= item.respond_to?(:title) ? item.title :
|
|
20
|
+
item.respond_to?(:name) ? item.name :
|
|
21
21
|
"ID: #{item.id}" %>
|
|
22
22
|
</div>
|
|
23
23
|
<div class="timestamp">
|
|
24
|
-
|
|
24
|
+
<% if item.respond_to?(:created_at) && item.created_at %>
|
|
25
|
+
Created <%= time_ago_in_words(item.created_at) %> ago
|
|
26
|
+
<% else %>
|
|
27
|
+
Created recently
|
|
28
|
+
<% end %>
|
|
25
29
|
<% if item.respond_to?(:deleted_at) && item.deleted_at %>
|
|
26
30
|
• <span class="deleted-status">Also deleted</span>
|
|
27
31
|
<% end %>
|
|
@@ -43,4 +47,4 @@
|
|
|
43
47
|
</div>
|
|
44
48
|
<% end %>
|
|
45
49
|
</div>
|
|
46
|
-
</div>
|
|
50
|
+
</div>
|