feat: require internal reasons on reports & shadow bans#2079
feat: require internal reasons on reports & shadow bans#2079NeonGamerBot-QK wants to merge 6 commits intohackclub:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Implements the “internal reason required” workflow for resolving project reports and shadow-banning projects, adding new fields to persist those reasons and updating admin/report review UIs to collect them (fixes #1942).
Changes:
- Add
resolution_reasontoproject_reportsandinternal_shadow_ban_reasontoprojects, and wire them into controllers/jobs. - Update admin report resolution UI and Slack token report review flow to collect a resolution reason.
- Require both user-visible and internal reasons when shadow-banning a project, and display the internal reason in admin.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| test/models/project_test.rb | Schema annotation update for new project field. |
| test/fixtures/projects.yml | Schema annotation update for new project field. |
| db/schema.rb | Reflect new columns in schema. |
| db/migrate/20260407212052_add_internal_shadow_ban_reason_to_projects.rb | Adds internal_shadow_ban_reason column. |
| db/migrate/20260407205924_add_resolution_reason_to_project_reports.rb | Adds resolution_reason column. |
| config/routes.rb | Routes report review token GETs to a new show action and adds POST endpoints. |
| app/views/report_reviews/show.html.erb | New UI to enter a resolution reason for token-based review/dismiss. |
| app/views/admin/reports/show.html.erb | Add resolution reason input + display of saved resolution reason. |
| app/views/admin/reports/index.html.erb | Remove quick-action buttons (review/dismiss) from index. |
| app/views/admin/projects/show.html.erb | Add internal reason display and require reasons in shadow-ban modal. |
| app/models/project/report.rb | Add validation requiring resolution_reason on reviewed/dismissed reports. |
| app/models/project.rb | Require shadow-ban reasons when shadow_banned?; extend shadow-ban methods. |
| app/jobs/process_demo_broken_reports_job.rb | Populate resolution_reason for auto-resolved reports. |
| app/controllers/report_reviews_controller.rb | Add show action and persist resolution_reason on review/dismiss. |
| app/controllers/admin/reports_controller.rb | Persist resolution_reason and surface validation errors. |
| app/controllers/admin/projects_controller.rb | Pass internal reason into shadow_ban! and set auto-resolution reason for pending reports. |
Comments suppressed due to low confidence (1)
app/controllers/report_reviews_controller.rb:62
process_tokenis called with symbols (:reviewed/:dismissed), butaction_textcomparesnew_statusto the integer1, so it will always choose the "dismissed" branch. Use a symbol/string comparison (or derive the text from the enum) so the notice matches the action taken.
if report.update(status: new_status, resolution_reason: params[:resolution_reason]) && @token.update(used_at: Time.current)
PaperTrail::Version.create!(
item_type: "Project::Report",
item_id: report.id,
event: "update",
whodunnit: current_user.id.to_s,
object_changes: {
status: [ old_status, @token.report.status ]
}
)
action_text = new_status == 1 ? "reviewed" : "dismissed"
redirect_to root_path, notice: "Report has been #{action_text}"
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| validates :resolution_reason, presence: { message: "is required when reviewing or dismissing a report" }, | ||
| if: -> { reviewed? || dismissed? } | ||
|
|
There was a problem hiding this comment.
Issue #1942 requires internal reasons to be 5+ words, but the new validation only checks presence. Consider enforcing a minimum word count (server-side) for resolution_reason when reviewed? || dismissed? so short inputs like "ok" are rejected consistently (including for non-browser clients).
| validates :shadow_banned_reason, presence: { message: "is required when shadow banning a project" }, | ||
| if: :shadow_banned? | ||
| validates :internal_shadow_ban_reason, presence: { message: "is required when shadow banning a project" }, | ||
| if: :shadow_banned? |
There was a problem hiding this comment.
Issue #1942 requires internal shadow-ban reasons to be 5+ words, but internal_shadow_ban_reason (and shadow_banned_reason, if it’s also intended to be constrained) are only validated for presence. Add a server-side minimum word-count (or length) validation so the requirement can’t be bypassed via non-HTML clients.
| @@ -95,11 +96,12 @@ def shadow_ban | |||
| end | |||
| end | |||
|
|
|||
| @project.shadow_ban!(reason: reason) | |||
| @project.shadow_ban!(reason: reason, internal_reason: internal_reason) | |||
| end | |||
There was a problem hiding this comment.
shadow_ban! now enforces required reasons via model validations, but this action calls the bang method inside a transaction without handling ActiveRecord::RecordInvalid. If the request is missing either reason (e.g., JS disabled or a manual POST), this will raise and likely 500 the admin page. Consider rescuing validation errors and redirecting back with @project.errors.full_messages.to_sentence (similar to the reports controller).
| form.addEventListener("submit", function(e) { | ||
| const reason = textarea.value.trim(); | ||
| if (!reason) { | ||
| e.preventDefault(); | ||
| textarea.focus(); | ||
| textarea.classList.add("report-resolution-textarea--error"); | ||
| return false; | ||
| } | ||
| hidden.value = reason; | ||
| }); |
There was a problem hiding this comment.
The new client-side check only validates that resolution_reason is non-empty, but the feature requirement is 5+ words. Even with server-side validation added, consider updating this submit handler to enforce the same word-count requirement so admins get immediate feedback instead of a redirect error.
| <%= f.label :resolution_reason, "Resolution Reason", class: "report-review-label" %> | ||
| <%= f.text_area :resolution_reason, rows: 4, required: true, placeholder: "Explain why this report is being #{@action_type == 'review' ? 'reviewed' : 'dismissed'}…", class: "form-control" %> | ||
| </div> |
There was a problem hiding this comment.
This form marks resolution_reason as required, but the feature request is 5+ words. Consider adding a small client-side hint/validation (and rely on server-side enforcement) so users don’t submit a too-short reason and get bounced by the model validation.
| before_action :find_token, only: [ :show, :review, :dismiss ] | ||
|
|
||
| def show | ||
| @action_type = params[:action_type] | ||
| @report = @token.report | ||
| end |
There was a problem hiding this comment.
Report::ReviewToken actions are only review/dismiss, but find_token currently compares @token.action to action_name. With the updated routes, the GET pages hit #show, so action_name == "show" and tokens will always be rejected (and the redirect doesn’t return, so it may also fall through and attempt to render). Consider allowing #show by validating params[:action_type] against the token action (and using return redirect_to ... to halt the filter).
Uh oh!
There was an error while loading. Please reload this page.