Module: Moderate::Reportable
- Extended by:
- ActiveSupport::Concern
- Included in:
- Actor
- Defined in:
- lib/moderate/models/concerns/reportable.rb
Overview
The contract a model opts into to become reportable — i.e. a piece of user-generated content (a comment, a listing, a profile) that another user (or a public DSA notice) can file a report against.
Plain Rails polymorphism answers “what row was reported?” via the ‘Moderate::Report#reportable` association. This concern answers the Trust & Safety questions that polymorphism *can’t*:
- which fields of the record may be reported (`reportable_fields`)
- who is *responsible* for the content (`reported_owner`) — the person a
decision's statement of reasons must reach (DSA Art. 17) and who a ban
would apply to
- what the moderation queue should *call* this item (`moderation_label`)
- what immutable evidence to snapshot at report time so it survives an edit
or delete (`moderation_snapshot`)
- what a moderator may *remove* without nuking the whole record
(`remove_reported_field!`)
- whether a given viewer is even allowed to report it (`report_visible_to?`)
WHY a concern and not just config: these answers are intrinsic to each model (only the Listing knows its title is reportable but its internal SKU isn’t), so they belong ON the model. Hosts override the handful of methods that need domain knowledge; everything else has a safe default.
Regulatory grounding for why a reportable contract has to exist at all:
- Apple App Store Review Guideline 1.2 requires a way to report UGC and a
mechanism to remove offending content:
https://developer.apple.com/app-store/review/guidelines/#user-generated-content
- Google Play UGC policy requires in-app reporting and ongoing moderation:
https://support.google.com/googleplay/android-developer/answer/9876937
- EU DSA Art. 16 (notice & action) presupposes that reported items can be
identified, snapshotted, and acted on:
https://eur-lex.europa.eu/eli/reg/2022/2065/oj
The documented include form is ‘include Moderate::Reportable` + `reportable_fields :a, :b`; the `has_reportable_content :a, :b` macro is exact sugar.
Instance Method Summary collapse
-
#flagged?(field = nil) ⇒ Boolean
Has this record been flagged and not yet resolved/dismissed? Optionally pass a field (‘listing.flagged?(:description)`) for field-level UI.
-
#moderation_admin_path(_routes) ⇒ Object
The admin/back-office path for this content, so the moderation queue can deep link a reviewer straight to the item under review.
-
#moderation_flags(field = nil) ⇒ Object
All auto-filter/manual flags against this record, optionally narrowed to a field.
-
#moderation_label ⇒ Object
The human-readable name the moderation queue shows for this item.
-
#moderation_return_path(_routes) ⇒ Object
Where to send a user *back to* after they file a report on this content (e.g. the content’s own page).
-
#moderation_snapshot(_field) ⇒ Object
The immutable evidence text to capture for ‘field` at report time, so the report survives the content being edited or deleted.
-
#moderation_subject_url(_routes) ⇒ Object
The public, canonical URL of this content — what a DSA Art.
-
#open_reports(field = nil) ⇒ Object
Open reports filed against this record, optionally narrowed to one field.
-
#pending_moderation_flags(field = nil) ⇒ Object
Pending flags are the “allowed through, awaiting review” state a host may want to surface near user-generated content.
-
#removable_reported_field?(_field) ⇒ Boolean
Companion query to ‘remove_reported_field!`: CAN this specific `field` be removed on this record? An admin UI uses it to decide whether to OFFER a “remove content” action at all.
-
#remove_reported_field!(_field) ⇒ Object
Remove the reported ‘field` as an enforcement action — WITHOUT destroying the whole record (a moderator removing one objectionable photo shouldn’t delete the whole listing).
-
#report_visible_to?(viewer, field:) ⇒ Boolean
Visibility/authorization gate for the report affordance: should ‘viewer` be offered a “report this” control for `field`? The default enforces two rules:.
-
#reportable_field_allowed?(field) ⇒ Boolean
Is ‘field` one a reporter is allowed to name? Two cases:.
-
#reported?(field = nil) ⇒ Boolean
Has this record received any open reports? This is the public predicate documented beside ‘reports` in the README.
-
#reported_owner ⇒ Object
WHO is responsible for this content — the account a decision’s statement of reasons reaches (DSA Art. 17) and the user a ban would apply to.
Instance Method Details
#flagged?(field = nil) ⇒ Boolean
Has this record been flagged and not yet resolved/dismissed? Optionally pass a field (‘listing.flagged?(:description)`) for field-level UI.
214 215 216 |
# File 'lib/moderate/models/concerns/reportable.rb', line 214 def flagged?(field = nil) pending_moderation_flags(field).exists? end |
#moderation_admin_path(_routes) ⇒ Object
The admin/back-office path for this content, so the moderation queue can deep link a reviewer straight to the item under review.
246 247 248 |
# File 'lib/moderate/models/concerns/reportable.rb', line 246 def moderation_admin_path(_routes) nil end |
#moderation_flags(field = nil) ⇒ Object
All auto-filter/manual flags against this record, optionally narrowed to a field. We keep this as a method instead of a ‘has_many :flags` association: `flags` is a common host-model word, while `flagged?` is the public DX.
198 199 200 201 202 203 204 |
# File 'lib/moderate/models/concerns/reportable.rb', line 198 def moderation_flags(field = nil) moderation_scope_by_field( Moderate::Flag.where(flaggable: self), :field, field ) end |
#moderation_label ⇒ Object
The human-readable name the moderation queue shows for this item. Defaults to Rails’ own ‘to_s` (usually “#<Comment id: 42>”); override for something an admin can act on at a glance, e.g. `def moderation_label = “Comment #id”`.
121 122 123 |
# File 'lib/moderate/models/concerns/reportable.rb', line 121 def moderation_label to_s end |
#moderation_return_path(_routes) ⇒ Object
Where to send a user *back to* after they file a report on this content (e.g. the content’s own page). Falls back to the app root when nil.
240 241 242 |
# File 'lib/moderate/models/concerns/reportable.rb', line 240 def moderation_return_path(_routes) nil end |
#moderation_snapshot(_field) ⇒ Object
The immutable evidence text to capture for ‘field` at report time, so the report survives the content being edited or deleted. The Report model calls this in its `before_validation :capture_snapshot` (DSA-grade evidence preservation). Defaults to nil; override to return the actual field text (e.g. `public_send(field)`) or a description of a non-text attachment.
NAMED ‘moderation_snapshot` per the gem’s public reportable contract; takes the field so a multi-field record can snapshot the specific thing reported.
133 134 135 |
# File 'lib/moderate/models/concerns/reportable.rb', line 133 def moderation_snapshot(_field) nil end |
#moderation_subject_url(_routes) ⇒ Object
The public, canonical URL of this content — what a DSA Art. 16 notice records as the precise location of the allegedly illegal content, and what a statement of reasons points the affected user to.
234 235 236 |
# File 'lib/moderate/models/concerns/reportable.rb', line 234 def moderation_subject_url(_routes) nil end |
#open_reports(field = nil) ⇒ Object
Open reports filed against this record, optionally narrowed to one field. Hosts can use this when they need the actual relation (queue previews, counters, “already reported” affordances) instead of just a boolean.
185 186 187 |
# File 'lib/moderate/models/concerns/reportable.rb', line 185 def open_reports(field = nil) moderation_scope_by_field(reports.open, :reported_field, field) end |
#pending_moderation_flags(field = nil) ⇒ Object
Pending flags are the “allowed through, awaiting review” state a host may want to surface near user-generated content.
208 209 210 |
# File 'lib/moderate/models/concerns/reportable.rb', line 208 def pending_moderation_flags(field = nil) moderation_flags(field).pending end |
#removable_reported_field?(_field) ⇒ Boolean
Companion query to ‘remove_reported_field!`: CAN this specific `field` be removed on this record? An admin UI uses it to decide whether to OFFER a “remove content” action at all. Without it, a host that only removes SOME fields (e.g. an avatar but not a display name) would render a remove button that always fails when the moderator clicks it on a non-removable field.
Defaults to false (mirrors the no-op ‘remove_reported_field!`). Override it alongside `remove_reported_field!` and have the latter reuse it, so the “can I?” answer and the “do it” action never drift apart.
158 159 160 |
# File 'lib/moderate/models/concerns/reportable.rb', line 158 def removable_reported_field?(_field) false end |
#remove_reported_field!(_field) ⇒ Object
Remove the reported ‘field` as an enforcement action — WITHOUT destroying the whole record (a moderator removing one objectionable photo shouldn’t delete the whole listing). Returns truthy if something was actually removed (so the decision service knows whether to fire the ‘content_removed` event).
Defaults to a no-op returning false: a model that hasn’t opted into field-level removal simply reports “nothing removed,” and the moderator falls back to other actions. Override to purge an attachment, blank a column, etc.
145 146 147 |
# File 'lib/moderate/models/concerns/reportable.rb', line 145 def remove_reported_field!(_field) false end |
#report_visible_to?(viewer, field:) ⇒ Boolean
Visibility/authorization gate for the report affordance: should ‘viewer` be offered a “report this” control for `field`? The default enforces two rules:
1. the field must be reportable (`reportable_field_allowed?`), and
2. you can't report YOUR OWN content — the affordance is hidden from the
content's owner, so an author never sees "Report" on their own post. We
compare the viewer to this content's `reported_owner` (the Reportable
contract's "who is responsible" answer).
The ‘moderate_report_link` helper renders nothing when this is false, and the report controller redirects. Hosts can override for richer rules. (A User with `has_reporting_and_blocking` overrides this in Moderate::Actor to compare ids directly, since a user IS its own owner.)
175 176 177 178 179 180 |
# File 'lib/moderate/models/concerns/reportable.rb', line 175 def report_visible_to?(viewer, field:) return false unless reportable_field_allowed?(field) return false if viewer.present? && moderation_owner_is?(viewer) true end |
#reportable_field_allowed?(field) ⇒ Boolean
Is ‘field` one a reporter is allowed to name? Two cases:
- A BLANK field is ALWAYS allowed — it means "report the whole record," which
is valid whether or not the model declared specific reportable fields. (A
user tapping "Report this comment" doesn't name a field; only the public DSA
notice / a field-targeted in-app flow does.) So a Comment that declares
`has_reportable_content :body` can still be reported as a whole with a nil field.
- A NAMED field must be in the whitelist. With no fields declared, the
whitelist is empty, so any named field is rejected (there's nothing to
target field-by-field on a bare-`has_reportable_content` record).
This is the authorization gate the Report model and the report controller both consult before accepting a ‘reported_field`.
96 97 98 99 100 101 |
# File 'lib/moderate/models/concerns/reportable.rb', line 96 def reportable_field_allowed?(field) field_s = field.to_s return true if field_s.empty? self.class.reportable_fields.include?(field_s) end |
#reported?(field = nil) ⇒ Boolean
Has this record received any open reports? This is the public predicate documented beside ‘reports` in the README.
191 192 193 |
# File 'lib/moderate/models/concerns/reportable.rb', line 191 def reported?(field = nil) open_reports(field).exists? end |
#reported_owner ⇒ Object
WHO is responsible for this content — the account a decision’s statement of reasons reaches (DSA Art. 17) and the user a ban would apply to.
NO default: a model that can be reported MUST tell the gem who’s behind it, because guessing wrong here means notifying or banning the wrong person. We raise a NotImplementedError naming the class so the omission is loud at the first report, not silent. (A ‘User` model with `has_reporting_and_blocking` is itself reportable and returns `self` — see Moderate::Actor.)
111 112 113 114 115 116 |
# File 'lib/moderate/models/concerns/reportable.rb', line 111 def reported_owner raise NotImplementedError, "#{self.class.name} is reportable but doesn't define #reported_owner. " \ "Return the user responsible for this content (the one a decision notifies " \ "and a ban applies to), e.g. `def reported_owner = user`." end |