diff --git a/r2/r2/controllers/api.py b/r2/r2/controllers/api.py index 63cb398b1..5c0abf6f9 100644 --- a/r2/r2/controllers/api.py +++ b/r2/r2/controllers/api.py @@ -1403,10 +1403,15 @@ class ApiController(RedditController): jquery.refresh() @require_oauth2_scope("report") - @noresponse(VUser(), VModhash(), - thing = VByName('id')) + @validatedForm( + VUser(), + VModhash(), + thing=VByName('thing_id'), + reason=VLength('reason', max_length=100, empty_error=None), + other_reason=VLength('other_reason', max_length=100, empty_error=None), + ) @api_doc(api_section.links_and_comments) - def POST_report(self, thing): + def POST_report(self, form, jquery, thing, reason, other_reason): """Report a link or comment. Reporting a thing brings it to the attention of the subreddit's @@ -1417,6 +1422,12 @@ class ApiController(RedditController): if not thing or thing._deleted: return + if (form.has_errors("reason", errors.TOO_LONG) or + form.has_errors("other_reason", errors.TOO_LONG)): + return + + reason = other_reason if reason == "other" else reason + # if it is a message that is being reported, ban it. # every user is admin over their own personal inbox if isinstance(thing, Message): @@ -1434,12 +1445,21 @@ class ApiController(RedditController): hooks.get_hook("thing.report").call(thing=thing) sr = getattr(thing, 'subreddit_slow', None) - if (c.user._spam or + if not (c.user._spam or c.user.ignorereports or (sr and sr.is_banned(c.user))): + Report.new(c.user, thing, reason) + admintools.report(thing) + + if isinstance(thing, Link): + button = jquery(".id-%s .report-button" % thing._fullname) + elif isinstance(thing, Comment): + button = jquery(".id-%s .entry:first .report-button" % thing._fullname) + else: return - Report.new(c.user, thing) - admintools.report(thing) + + button.text(_("reported")) + form.fadeOut() @require_oauth2_scope("privatemessages") @noresponse(VUser(), VModhash(), diff --git a/r2/r2/lib/js.py b/r2/r2/lib/js.py index 03e30d90d..f413dd85b 100644 --- a/r2/r2/lib/js.py +++ b/r2/r2/lib/js.py @@ -470,6 +470,7 @@ module["reddit"] = LocalizedModule("reddit.js", "multi.js", "filter.js", "recommender.js", + "report.js", "saved.js", PermissionsDataSource({ "moderator": ModeratorPermissionSet, diff --git a/r2/r2/lib/jsontemplates.py b/r2/r2/lib/jsontemplates.py index 4f977d227..a2325e1d9 100644 --- a/r2/r2/lib/jsontemplates.py +++ b/r2/r2/lib/jsontemplates.py @@ -27,7 +27,7 @@ from wrapped import Wrapped, StringTemplate, CacheStub, CachedVariable, Template from mako.template import Template from r2.config.extensions import get_api_subtype from r2.lib.filters import spaceCompress, safemarkdown -from r2.models import Account +from r2.models import Account, Report from r2.models.subreddit import SubSR from r2.models.token import OAuth2Scope, extra_oauth2_scope import time, pytz @@ -171,10 +171,13 @@ class ThingJsonTemplate(JsonTemplate): return None return distinguished - if attr in ["num_reports", "banned_by", "approved_by"]: + if attr in ["num_reports", "report_reasons", "banned_by", "approved_by"]: if c.user_is_loggedin and thing.subreddit.is_moderator(c.user): if attr == "num_reports": return thing.reported + elif attr == "report_reasons": + return Report.get_reasons(thing) + ban_info = getattr(thing, "ban_info", {}) if attr == "banned_by": banner = (ban_info.get("banner") @@ -423,6 +426,7 @@ class LinkJsonTemplate(ThingJsonTemplate): media_embed="media_embed", num_comments="num_comments", num_reports="num_reports", + report_reasons="report_reasons", over_18="over_18", permalink="permalink", saved="saved", @@ -513,6 +517,7 @@ class CommentJsonTemplate(ThingJsonTemplate): likes="likes", link_id="link_id", num_reports="num_reports", + report_reasons="report_reasons", parent_id="parent_id", replies="child", saved="saved", diff --git a/r2/r2/lib/pages/pages.py b/r2/r2/lib/pages/pages.py index 2d0c40e22..ba10d91a6 100644 --- a/r2/r2/lib/pages/pages.py +++ b/r2/r2/lib/pages/pages.py @@ -329,8 +329,9 @@ class Reddit(Templated): clone_template=True, thing_type="comment", ) + report_form = ReportForm() self._content = PaneStack([ShareLink(), content, - gold_comment, gold_link]) + gold_comment, gold_link, report_form]) else: self._content = content @@ -2602,6 +2603,10 @@ class Gilding(Templated): pass +class ReportForm(Templated): + pass + + class Password(Templated): """Form encountered when 'recover password' is clicked in the LoginFormWide.""" def __init__(self, success=False): diff --git a/r2/r2/lib/pages/things.py b/r2/r2/lib/pages/things.py index 4676a38e9..46899dd0c 100644 --- a/r2/r2/lib/pages/things.py +++ b/r2/r2/lib/pages/things.py @@ -23,7 +23,7 @@ from r2.lib.db.thing import NotFound from r2.lib.menus import Styled from r2.lib.wrapped import Wrapped -from r2.models import LinkListing, Link, PromotedLink +from r2.models import LinkListing, Link, PromotedLink, Report from r2.models import make_wrapper, IDBuilder, Thing from r2.lib.utils import tup from r2.lib.strings import Score @@ -132,6 +132,7 @@ class LinkButtons(PrintableButtons): ignore_reports = thing.ignore_reports, show_delete = show_delete, show_report = show_report and c.user_is_loggedin, + report_reasons = Report.get_reasons(thing), show_distinguish = show_distinguish, show_marknsfw = show_marknsfw, show_unmarknsfw = show_unmarknsfw, @@ -173,6 +174,7 @@ class CommentButtons(PrintableButtons): parent_permalink = thing.parent_permalink, can_reply = thing.can_reply, show_report = show_report, + report_reasons = Report.get_reasons(thing), show_distinguish = show_distinguish, show_delete = show_delete, show_givegold=show_givegold, diff --git a/r2/r2/models/report.py b/r2/r2/models/report.py index c121cc5c5..ac3364119 100644 --- a/r2/r2/models/report.py +++ b/r2/r2/models/report.py @@ -39,7 +39,7 @@ class Report(MultiRelation('report', _field = 'reported' @classmethod - def new(cls, user, thing): + def new(cls, user, thing, reason=None): from r2.lib.db import queries # check if this report exists already! @@ -53,7 +53,11 @@ class Report(MultiRelation('report', g.log.debug("Ignoring duplicate report %s" % oldreport) return oldreport - r = Report(user, thing, '0') + kw = {} + if reason: + kw['reason'] = reason + + r = Report(user, thing, '0', **kw) if not thing._loaded: thing._load() @@ -84,7 +88,7 @@ class Report(MultiRelation('report', @classmethod def for_thing(cls, thing): rel = cls.rel(Account, thing.__class__) - rels = rel._query(rel.c._thing2_id == thing._id) + rels = rel._query(rel.c._thing2_id == thing._id, data=True) return list(rels) @@ -118,3 +122,20 @@ class Report(MultiRelation('report', queries.clear_reports(to_clear, rels) + + @classmethod + def get_reasons(cls, wrapped, max_reasons=20): + if wrapped.can_ban and wrapped.reported > 0: + reports = cls.for_thing(wrapped.lookups[0]) + reasons = set() + for report in reports: + if len(reasons) >= max_reasons: + break + + reason = getattr(report, 'reason', None) + if reason: + reasons.add(reason) + + return list(reasons) + else: + return [] diff --git a/r2/r2/public/static/css/reddit.less b/r2/r2/public/static/css/reddit.less index 0884a16a5..fba4b0e0b 100644 --- a/r2/r2/public/static/css/reddit.less +++ b/r2/r2/public/static/css/reddit.less @@ -10054,3 +10054,54 @@ body.with-listing-chooser { } } } + +.report-form { + display: none; + background-color: #f6e69f; + border: thin solid #d8bb3c; + max-width: 300px; + padding: 5px; + margin: 5px 0; + font-size: larger; + + input { + margin: 5px 0; + + &[type="radio"] { + margin: 2px 0.5em 0 0; + } + + &[name="other_reason"] { + width: 95%; + } + + &:disabled { + background: #dddddd; + } + } +} + +.reported-stamp.has-reasons { + cursor: pointer; +} + +ul.report-reasons { + width: 80%; + background-color: #f6e69f; + border: thin solid black; + display: none; + + li { + &.report-reason { + padding: 1px 10px; + display: block; + overflow: hidden; + text-overflow: ellipsis; + } + + &.report-reason-title { + padding: 1px 10px; + font-weight: bold; + } + } +} diff --git a/r2/r2/public/static/js/report.js b/r2/r2/public/static/js/report.js new file mode 100644 index 000000000..44a7f2cb9 --- /dev/null +++ b/r2/r2/public/static/js/report.js @@ -0,0 +1,88 @@ +r.report = { + "init": function() { + $('div.content').on( + 'click', + '.report-thing, button.cancel-report-thing', + $.proxy(this, 'toggleReportForm') + ); + + $('div.content').on( + 'click', + 'button.submit-report', + $.proxy(this, 'submitReport') + ); + + $('div.content').on( + 'change', + '.report-form input[type="radio"]', + $.proxy(this, 'enableReportForm') + ); + + $('div.content').on( + 'click', + '.reported-stamp.has-reasons', + $.proxy(function(event) { + $(event.target).parent().find('.report-reasons').toggle() + }, this) + ); + }, + + toggleReportForm: function(event) { + var element = event.target; + var $thing = $(element).thing(); + var $thingForm = $thing.find("> .entry .report-form"); + + event.stopPropagation(); + event.preventDefault(); + + if ($thingForm.length > 0) { + if ($thingForm.is(":visible")) { + $thingForm.hide(); + } else { + $thingForm.show(); + } + } else { + var $form = $(".report-form.clonable"); + var $clonedForm = $form.clone(); + var $insertionPoint = $thing.find("> .entry .buttons"); + var thingFullname = $thing.thing_id(); + + $clonedForm.removeClass("clonable"); + $clonedForm.attr("id", "report-thing-" + thingFullname); + $clonedForm.find("input[name='thing_id']").val(thingFullname); + $clonedForm.insertAfter($insertionPoint); + $clonedForm.show(); + } + }, + + submitReport: function(event) { + var $reportForm = $(event.target).parent() + return post_pseudo_form($reportForm, "report"); + }, + + enableReportForm: function(event) { + var $thing = $(event.target).thing(); + var $reportForm = $thing.find("> .entry .report-form"); + var $submitButton = $reportForm.find('button.submit-report'); + var $enabledRadio = $reportForm.find('input[type="radio"]:checked'); + var isOther = $enabledRadio.val() == 'other'; + var $otherInput = $reportForm.find('input[name="other_reason"]'); + + event.stopPropagation(); + event.preventDefault(); + + $submitButton.removeAttr("disabled"); + + if (isOther) { + $otherInput.removeAttr("disabled"); + } else { + $otherInput.attr("disabled", "disabled"); + } + + return false; + } +} + +$(function() { + r.report.init(); +}); diff --git a/r2/r2/templates/printablebuttons.html b/r2/r2/templates/printablebuttons.html index eacf0e3c1..ca7316774 100644 --- a/r2/r2/templates/printablebuttons.html +++ b/r2/r2/templates/printablebuttons.html @@ -20,7 +20,7 @@ ## reddit Inc. All Rights Reserved. ############################################################################### -<%namespace file="utils.html" import="plain_link, pretty_button, data" /> +<%namespace file="utils.html" import="plain_link, pretty_button, data, error_field" /> <%! from r2.lib.strings import strings @@ -52,8 +52,14 @@ %endif %endif %elif thing.show_report: -