rails-image-post-solution 0.1.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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +8 -0
  3. data/.ruby-version +1 -0
  4. data/CHANGELOG.md +42 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +264 -0
  7. data/Rakefile +8 -0
  8. data/app/controllers/admin/frozen_posts_controller.rb +79 -0
  9. data/app/controllers/admin/image_reports_controller.rb +81 -0
  10. data/app/controllers/admin/users_controller.rb +100 -0
  11. data/app/controllers/rails_image_post_solution/admin/image_reports_controller.rb +92 -0
  12. data/app/controllers/rails_image_post_solution/image_reports_controller.rb +87 -0
  13. data/app/jobs/rails_image_post_solution/image_moderation_job.rb +97 -0
  14. data/app/models/rails_image_post_solution/image_report.rb +57 -0
  15. data/app/services/rails_image_post_solution/openai_vision_service.rb +128 -0
  16. data/app/views/admin/frozen_posts/index.html.erb +128 -0
  17. data/app/views/admin/image_reports/index.html.erb +94 -0
  18. data/app/views/admin/image_reports/show.html.erb +138 -0
  19. data/app/views/admin/users/index.html.erb +93 -0
  20. data/app/views/admin/users/show.html.erb +198 -0
  21. data/app/views/rails_image_post_solution/admin/image_reports/index.html.erb +100 -0
  22. data/app/views/rails_image_post_solution/admin/image_reports/show.html.erb +154 -0
  23. data/app/views/shared/_image_report_button.html.erb +26 -0
  24. data/app/views/shared/_image_report_modal.html.erb +45 -0
  25. data/config/locales/en.yml +260 -0
  26. data/config/locales/ja.yml +259 -0
  27. data/config/routes.rb +16 -0
  28. data/db/migrate/20250101000001_create_rails_image_post_solution_image_reports.rb +20 -0
  29. data/db/migrate/20250101000002_add_ai_moderation_fields_to_image_reports.rb +12 -0
  30. data/lib/generators/rails_image_post_solution/install/README +27 -0
  31. data/lib/generators/rails_image_post_solution/install/install_generator.rb +47 -0
  32. data/lib/generators/rails_image_post_solution/install/templates/add_ai_moderation_fields.rb.erb +12 -0
  33. data/lib/generators/rails_image_post_solution/install/templates/create_image_reports.rb.erb +19 -0
  34. data/lib/generators/rails_image_post_solution/install/templates/rails_image_post_solution.rb +15 -0
  35. data/lib/rails_image_post_solution/engine.rb +22 -0
  36. data/lib/rails_image_post_solution/version.rb +5 -0
  37. data/lib/rails_image_post_solution.rb +28 -0
  38. metadata +150 -0
@@ -0,0 +1,154 @@
1
+ <div class="admin-container">
2
+ <h1 class="admin-header"><%= t('rails_image_post_solution.admin.show.title') %></h1>
3
+
4
+ <div class="report-detail">
5
+ <!-- Image Display -->
6
+ <div class="detail-section">
7
+ <h3><%= t('rails_image_post_solution.admin.show.reported_image') %></h3>
8
+ <% if @attachment.present? %>
9
+ <div class="reported-image-container">
10
+ <%= image_tag @attachment.blob, class: "reported-image-large", style: "max-width: 800px; max-height: 600px;" %>
11
+ </div>
12
+ <% else %>
13
+ <p class="text-muted"><%= t('rails_image_post_solution.admin.show.image_deleted') %></p>
14
+ <% end %>
15
+ </div>
16
+
17
+ <!-- Report Information -->
18
+ <div class="detail-section">
19
+ <h3><%= t('rails_image_post_solution.admin.show.report_info') %></h3>
20
+ <table class="detail-table">
21
+ <tr>
22
+ <th><%= t('rails_image_post_solution.admin.show.status') %></th>
23
+ <td>
24
+ <span class="status-badge status-<%= @report.status %>">
25
+ <%= t("rails_image_post_solution.image_report.statuses.#{@report.status}") %>
26
+ </span>
27
+ </td>
28
+ </tr>
29
+ <tr>
30
+ <th><%= t('rails_image_post_solution.admin.show.reporter') %></th>
31
+ <td>
32
+ <% if @report.user %>
33
+ <%= @report.user.try(:display_name) || @report.user.try(:name) || "Unknown" %>
34
+ <% else %>
35
+ <span class="text-muted"><%= t('rails_image_post_solution.admin.show.system_ai') %></span>
36
+ <% end %>
37
+ </td>
38
+ </tr>
39
+ <tr>
40
+ <th><%= t('rails_image_post_solution.admin.show.reason') %></th>
41
+ <td><%= simple_format(@report.reason) || t('rails_image_post_solution.admin.show.no_reason') %></td>
42
+ </tr>
43
+ <tr>
44
+ <th><%= t('rails_image_post_solution.admin.show.reported_at') %></th>
45
+ <td><%= l(@report.created_at, format: :long) %></td>
46
+ </tr>
47
+ <% if @report.reviewed_by %>
48
+ <tr>
49
+ <th><%= t('rails_image_post_solution.admin.show.reviewer') %></th>
50
+ <td><%= @report.reviewed_by.try(:display_name) || @report.reviewed_by.try(:name) || "Unknown" %></td>
51
+ </tr>
52
+ <tr>
53
+ <th><%= t('rails_image_post_solution.admin.show.reviewed_at') %></th>
54
+ <td><%= l(@report.reviewed_at, format: :long) %></td>
55
+ </tr>
56
+ <% end %>
57
+ <% if @report.ai_flagged %>
58
+ <tr>
59
+ <th><%= t('rails_image_post_solution.admin.show.ai_detection') %></th>
60
+ <td>
61
+ <span class="badge badge-warning"><%= t('rails_image_post_solution.admin.show.ai_detected') %></span>
62
+ <% if @report.ai_confidence %>
63
+ <%= t('rails_image_post_solution.admin.show.confidence') %>: <%= (@report.ai_confidence * 100).round(1) %>%
64
+ <% end %>
65
+ </td>
66
+ </tr>
67
+ <% end %>
68
+ </table>
69
+ </div>
70
+
71
+ <!-- Image Poster Information -->
72
+ <% if @reported_user %>
73
+ <div class="detail-section">
74
+ <h3><%= t('rails_image_post_solution.admin.show.image_poster') %></h3>
75
+ <table class="detail-table">
76
+ <tr>
77
+ <th><%= t('rails_image_post_solution.admin.show.username') %></th>
78
+ <td><%= @reported_user.try(:display_name) || @reported_user.try(:name) || "Unknown" %></td>
79
+ </tr>
80
+ <% if @reported_user.respond_to?(:banned?) %>
81
+ <tr>
82
+ <th><%= t('rails_image_post_solution.admin.show.user_status') %></th>
83
+ <td>
84
+ <% if @reported_user.banned? %>
85
+ <span class="user-status-badge banned"><%= t('rails_image_post_solution.admin.show.banned') %></span>
86
+ <% elsif @reported_user.respond_to?(:suspended?) && @reported_user.suspended? %>
87
+ <span class="user-status-badge suspended"><%= t('rails_image_post_solution.admin.show.suspended') %></span>
88
+ <% else %>
89
+ <span class="user-status-badge active"><%= t('rails_image_post_solution.admin.show.active') %></span>
90
+ <% end %>
91
+ </td>
92
+ </tr>
93
+ <% end %>
94
+ </table>
95
+ </div>
96
+ <% end %>
97
+
98
+ <!-- Other Reports for Same Image -->
99
+ <div class="detail-section">
100
+ <h3><%= t('rails_image_post_solution.admin.show.all_reports_title', count: @all_reports.count) %></h3>
101
+ <% if @all_reports.count > 1 %>
102
+ <table class="admin-table">
103
+ <thead>
104
+ <tr>
105
+ <th><%= t('rails_image_post_solution.admin.index.table.reporter') %></th>
106
+ <th><%= t('rails_image_post_solution.admin.index.table.reason') %></th>
107
+ <th><%= t('rails_image_post_solution.admin.index.table.status') %></th>
108
+ <th><%= t('rails_image_post_solution.admin.index.table.reported_at') %></th>
109
+ </tr>
110
+ </thead>
111
+ <tbody>
112
+ <% @all_reports.each do |r| %>
113
+ <tr class="<%= 'current-report' if r.id == @report.id %>">
114
+ <td>
115
+ <% if r.user %>
116
+ <%= r.user.try(:display_name) || r.user.try(:name) || "Unknown" %>
117
+ <% else %>
118
+ <span class="text-muted"><%= t('rails_image_post_solution.admin.index.system') %></span>
119
+ <% end %>
120
+ <% if r.id == @report.id %>
121
+ <span class="badge-current"><%= t('rails_image_post_solution.admin.show.current') %></span>
122
+ <% end %>
123
+ </td>
124
+ <td><%= truncate(r.reason, length: 50) || t('rails_image_post_solution.admin.index.no_reason') %></td>
125
+ <td>
126
+ <span class="status-badge status-<%= r.status %>">
127
+ <%= t("rails_image_post_solution.image_report.statuses.#{r.status}") %>
128
+ </span>
129
+ </td>
130
+ <td><%= l(r.created_at, format: :long) %></td>
131
+ </tr>
132
+ <% end %>
133
+ </tbody>
134
+ </table>
135
+ <% else %>
136
+ <p class="text-muted"><%= t('rails_image_post_solution.admin.show.single_report') %></p>
137
+ <% end %>
138
+ </div>
139
+
140
+ <!-- Actions -->
141
+ <div class="detail-actions">
142
+ <% if @report.status == RailsImagePostSolution::ImageReport::STATUSES[:pending] %>
143
+ <%= button_to t('rails_image_post_solution.admin.show.confirm_button'), rails_image_post_solution.confirm_admin_image_report_path(@report), method: :patch, class: "btn btn-danger", data: { turbo_confirm: t('rails_image_post_solution.admin.show.confirm_action') } %>
144
+ <%= button_to t('rails_image_post_solution.admin.show.dismiss_button'), rails_image_post_solution.dismiss_admin_image_report_path(@report), method: :patch, class: "btn btn-success", data: { turbo_confirm: t('rails_image_post_solution.admin.show.dismiss_action') } %>
145
+ <% else %>
146
+ <p class="text-muted"><%= t('rails_image_post_solution.admin.show.already_reviewed') %></p>
147
+ <% end %>
148
+ </div>
149
+ </div>
150
+
151
+ <div class="admin-actions">
152
+ <%= link_to t('rails_image_post_solution.admin.show.back_to_list'), rails_image_post_solution.admin_image_reports_path, class: "btn btn-default" %>
153
+ </div>
154
+ </div>
@@ -0,0 +1,26 @@
1
+ <%# Image report button partial %>
2
+ <%# Usage: render 'shared/image_report_button', image: @image %>
3
+ <%
4
+ is_reported = ImageReport.image_reported?(image.id)
5
+ report_count = ImageReport.report_count(image.id)
6
+ user_reported = logged_in? && ImageReport.exists?(active_storage_attachment_id: image.id, user_id: current_user.id)
7
+ %>
8
+
9
+ <div class="image-report-info">
10
+ <% if report_count > 0 %>
11
+ <span class="report-count"><%= t('app.image_report.report_count', count: report_count) %></span>
12
+ <% end %>
13
+
14
+ <% if logged_in? %>
15
+ <% if user_reported %>
16
+ <span class="reported-badge"><%= t('app.image_report.reported_badge') %></span>
17
+ <% else %>
18
+ <button type="button"
19
+ class="btn-report"
20
+ data-attachment-id="<%= image.id %>"
21
+ data-action="click->image-report#openModal">
22
+ <%= t('app.image_report.button') %>
23
+ </button>
24
+ <% end %>
25
+ <% end %>
26
+ </div>
@@ -0,0 +1,45 @@
1
+ <% if logged_in? %>
2
+ <!-- Image report modal -->
3
+ <div class="image-report-modal"
4
+ data-image-report-target="modal"
5
+ data-action="click->image-report#closeModal"
6
+ data-alert-select-category="<%= t('app.image_report.alert_select_category') %>"
7
+ data-alert-enter-details="<%= t('app.image_report.alert_enter_details') %>">
8
+ <div class="modal-content-report" data-action="click->image-report#closeModal:stop">
9
+ <div class="modal-header">
10
+ <h2><%= t('app.image_report.modal_title') %></h2>
11
+ <button type="button" class="modal-close" data-action="click->image-report#closeModal">&times;</button>
12
+ </div>
13
+
14
+ <%= form_with url: image_reports_path, method: :post, data: { image_report_target: "form", turbo: false, action: "submit->image-report#submitReport" } do |f| %>
15
+ <%= f.hidden_field :attachment_id, data: { image_report_target: "attachmentId" } %>
16
+
17
+ <div class="modal-body">
18
+ <h3><%= t('app.image_report.select_reason') %></h3>
19
+ <div class="report-categories">
20
+ <% ImageReport::REASON_CATEGORIES.each do |key| %>
21
+ <button type="button"
22
+ class="category-btn"
23
+ data-category="<%= key %>"
24
+ data-image-report-target="category"
25
+ data-action="click->image-report#selectCategory">
26
+ <%= t("app.image_report.categories.#{key}") %>
27
+ </button>
28
+ <% end %>
29
+ </div>
30
+
31
+ <div class="report-details">
32
+ <label for="report_details"><%= t('app.image_report.details_label') %></label>
33
+ <%= f.text_area :reason, id: "report_details", data: { image_report_target: "details" }, placeholder: t('app.image_report.details_placeholder'), rows: 4 %>
34
+ <p class="help-text"><%= t('app.image_report.details_help') %></p>
35
+ </div>
36
+ </div>
37
+
38
+ <div class="modal-footer">
39
+ <button type="button" class="btn btn-secondary" data-action="click->image-report#closeModal"><%= t('app.common.cancel') %></button>
40
+ <%= f.submit t('app.image_report.submit'), class: "btn btn-danger" %>
41
+ </div>
42
+ <% end %>
43
+ </div>
44
+ </div>
45
+ <% end %>
@@ -0,0 +1,260 @@
1
+ en:
2
+ rails_image_post_solution:
3
+ image_report:
4
+ categories:
5
+ r18: "R18 (Adult Content)"
6
+ r18g: "R18G (Grotesque)"
7
+ copyright: "Copyright Infringement"
8
+ spam: "Spam"
9
+ harassment: "Harassment"
10
+ other: "Other"
11
+ statuses:
12
+ pending: "Pending"
13
+ confirmed: "Inappropriate"
14
+ dismissed: "Dismissed"
15
+ reviewed: "Reviewed"
16
+ flash:
17
+ already_reported: "You have already reported this image"
18
+ report_received: "Report received"
19
+ image_not_found: "Image not found"
20
+ login_required: "Login required"
21
+ admin:
22
+ report_confirmed: "Report confirmed as inappropriate"
23
+ report_dismissed: "Report dismissed (no issues found)"
24
+ report_not_found: "Report not found"
25
+ admin_access_only: "Admin access only"
26
+ login_required: "Login required"
27
+ admin:
28
+ index:
29
+ title: "Image Report Management"
30
+ stats:
31
+ total: "Total Reports"
32
+ pending: "Pending"
33
+ confirmed: "Inappropriate"
34
+ dismissed: "Dismissed"
35
+ reviewed: "Reviewed"
36
+ filters:
37
+ all: "All"
38
+ pending: "Pending"
39
+ confirmed: "Inappropriate"
40
+ dismissed: "Dismissed"
41
+ reviewed: "Reviewed"
42
+ table:
43
+ image: "Image"
44
+ reporter: "Reporter"
45
+ reason: "Reason"
46
+ status: "Status"
47
+ reported_at: "Reported At"
48
+ reviewer: "Reviewer"
49
+ actions: "Actions"
50
+ deleted: "Deleted"
51
+ system: "System"
52
+ no_reason: "No reason"
53
+ no_reports: "No reports found"
54
+ back: "Back"
55
+ detail: "Details"
56
+ show:
57
+ title: "Report Details"
58
+ reported_image: "Reported Image"
59
+ image_deleted: "Image has been deleted"
60
+ report_info: "Report Information"
61
+ status: "Status"
62
+ reporter: "Reporter"
63
+ reason: "Reason"
64
+ reported_at: "Reported At"
65
+ reviewer: "Reviewer"
66
+ reviewed_at: "Reviewed At"
67
+ system_ai: "System (AI Auto-detection)"
68
+ no_reason: "No reason"
69
+ ai_detection: "AI Detection"
70
+ ai_detected: "AI Detected"
71
+ confidence: "Confidence"
72
+ image_poster: "Image Poster"
73
+ username: "Username"
74
+ user_status: "Status"
75
+ banned: "Banned"
76
+ suspended: "Suspended"
77
+ active: "Active"
78
+ all_reports_title: "All Reports for This Image (%{count} reports)"
79
+ single_report: "This is the only report for this image"
80
+ current: "(Current)"
81
+ already_reviewed: "This report has already been reviewed"
82
+ confirm_button: "Mark as Inappropriate"
83
+ dismiss_button: "Dismiss (No Issues)"
84
+ confirm_action: "Are you sure you want to mark this image as inappropriate?"
85
+ dismiss_action: "Are you sure you want to dismiss this report?"
86
+ back_to_list: "Back to Report List"
87
+ user_actions: "Actions for User"
88
+ view_user_admin: "Go to User Management Page"
89
+
90
+ # Admin namespace (outside rails_image_post_solution)
91
+ admin:
92
+ flash:
93
+ user_suspended: "%{name} has been suspended for %{days} days"
94
+ user_unsuspended: "%{name} suspension has been lifted"
95
+ user_banned: "%{name} has been banned"
96
+ user_unbanned: "%{name} ban has been lifted"
97
+ user_not_found: "User not found"
98
+ report_confirmed: "Report confirmed as inappropriate"
99
+ report_dismissed: "Report dismissed (no issues)"
100
+ report_not_found: "Report not found"
101
+ admin_access_only: "Admin access only"
102
+ access_denied: "Access denied"
103
+ stage_unfrozen: "Stage has been unfrozen"
104
+ comment_unfrozen: "Comment has been unfrozen"
105
+ stage_permanently_frozen: "Stage has been permanently frozen"
106
+ comment_permanently_frozen: "Comment has been permanently frozen"
107
+ permanent_freeze_by_admin: "Permanently frozen by administrator"
108
+ stage_deleted: "Stage has been deleted"
109
+ comment_deleted: "Comment has been deleted"
110
+ users:
111
+ index:
112
+ title: "User Management"
113
+ stats:
114
+ total: "Total Users"
115
+ active: "Active"
116
+ suspended: "Suspended"
117
+ banned: "Banned"
118
+ admin: "Administrators"
119
+ filters:
120
+ all: "All"
121
+ active: "Active"
122
+ suspended: "Suspended"
123
+ banned: "Banned"
124
+ admin: "Administrators"
125
+ table:
126
+ username: "Username"
127
+ email: "Email"
128
+ status: "Status"
129
+ role: "Role"
130
+ registered_at: "Registered"
131
+ post_count: "Posts"
132
+ actions: "Actions"
133
+ status_badges:
134
+ banned: "Banned"
135
+ suspended: "Suspended"
136
+ active: "Active"
137
+ role_badges:
138
+ admin: "Administrator"
139
+ user: "User"
140
+ no_users: "No users found"
141
+ view_reports: "Report Management"
142
+ back: "Back"
143
+ detail: "Details"
144
+ show:
145
+ title: "User Details: %{name}"
146
+ basic_info: "Basic Information"
147
+ username: "Username"
148
+ nickname: "Nickname"
149
+ email: "Email"
150
+ status: "Status"
151
+ ban_date: "Ban Date"
152
+ ban_reason: "Reason"
153
+ suspension_expiry: "Suspension Expiry"
154
+ suspension_reason: "Reason"
155
+ role: "Role"
156
+ registered_date: "Registered"
157
+ language: "Language Setting"
158
+ activity_stats: "Activity Statistics"
159
+ stage_count: "Stage Posts"
160
+ comment_count: "Comments"
161
+ reports_made_count: "Reports Made"
162
+ reports_received_count: "Reports Received"
163
+ recent_stages: "Recent Stage Posts (Latest 10)"
164
+ recent_comments: "Recent Comments (Latest 10)"
165
+ report_history: "Report History (Received Reports)"
166
+ reporter: "Reporter"
167
+ reason: "Reason"
168
+ report_status: "Status"
169
+ date: "Date"
170
+ no_posts: "No posts"
171
+ no_comments: "No comments"
172
+ no_reports: "No reports"
173
+ admin_actions: "Admin Actions"
174
+ suspend_duration: "Suspension Duration (days)"
175
+ suspend_reason: "Reason"
176
+ suspend_reason_placeholder: "Enter suspension reason"
177
+ suspend_button: "Suspend User"
178
+ suspend_confirm: "Are you sure you want to suspend this user?"
179
+ ban_reason: "Reason"
180
+ ban_reason_placeholder: "Enter ban reason"
181
+ ban_button: "Ban User"
182
+ ban_confirm: "Are you sure you want to permanently ban this user? Please proceed with caution."
183
+ unsuspend_button: "Lift Suspension"
184
+ unsuspend_confirm: "Are you sure you want to lift this user's suspension?"
185
+ unban_button: "Lift Ban"
186
+ unban_confirm: "Are you sure you want to lift this user's ban?"
187
+ back_to_list: "Back to User List"
188
+ view_public_profile: "Public Profile"
189
+ count_suffix: " items"
190
+ frozen_posts:
191
+ index:
192
+ title: "Frozen Posts Management"
193
+ stats:
194
+ total: "Total Frozen"
195
+ temporary: "Temporary Freeze"
196
+ permanent: "Permanent Freeze"
197
+ filters:
198
+ all: "All"
199
+ temporary: "Temporary Freeze"
200
+ permanent: "Permanent Freeze"
201
+ table:
202
+ type: "Type"
203
+ content: "Content"
204
+ poster: "Poster"
205
+ freeze_reason: "Freeze Reason"
206
+ status: "Status"
207
+ frozen_at: "Frozen At"
208
+ actions: "Actions"
209
+ post_types:
210
+ stage: "Stage"
211
+ comment: "Comment"
212
+ comment_prefix: "Comment"
213
+ status_badges:
214
+ temporary: "Temporary Freeze"
215
+ permanent: "Permanent Freeze"
216
+ actions:
217
+ permanent_freeze: "Permanent Freeze"
218
+ unfreeze: "Unfreeze"
219
+ delete: "Delete"
220
+ confirm:
221
+ permanent_freeze: "Are you sure you want to permanently freeze?"
222
+ unfreeze: "Are you sure you want to unfreeze?"
223
+ delete: "Are you sure you want to delete?"
224
+ no_frozen_posts: "No frozen posts"
225
+ back: "Back"
226
+ image_reports:
227
+ flash:
228
+ report_confirmed: "Report confirmed as inappropriate"
229
+ report_dismissed: "Report dismissed (no issues)"
230
+ report_not_found: "Report not found"
231
+
232
+ # App namespace for shared views
233
+ app:
234
+ common:
235
+ cancel: "Cancel"
236
+ image_report:
237
+ modal_title: "Report Image"
238
+ select_reason: "Select Reason"
239
+ categories:
240
+ r18: "R18 (Adult Content)"
241
+ r18g: "R18G (Grotesque)"
242
+ copyright: "Copyright Infringement"
243
+ spam: "Spam"
244
+ harassment: "Harassment"
245
+ other: "Other"
246
+ details_label: "Details (optional)"
247
+ details_placeholder: "Please provide additional details..."
248
+ details_help: "More details help us review the report."
249
+ submit: "Submit Report"
250
+ alert_select_category: "Please select a category"
251
+ alert_enter_details: "Please enter details"
252
+ button: "Report"
253
+ reported_badge: "Reported"
254
+ report_count:
255
+ one: "%{count} report"
256
+ other: "%{count} reports"
257
+
258
+ languages:
259
+ en: "English"
260
+ ja: "Japanese"