From 85a9223dced1ac12734a334400bb2a7aa8d10a28 Mon Sep 17 00:00:00 2001 From: Brian Simpson Date: Mon, 30 Jun 2014 13:15:21 -0400 Subject: [PATCH] Add report reasons for Links and Comments. --- r2/r2/controllers/api.py | 32 ++++++++-- r2/r2/lib/js.py | 1 + r2/r2/lib/jsontemplates.py | 9 ++- r2/r2/lib/pages/pages.py | 7 ++- r2/r2/lib/pages/things.py | 4 +- r2/r2/models/report.py | 27 +++++++- r2/r2/public/static/css/reddit.less | 51 ++++++++++++++++ r2/r2/public/static/js/report.js | 88 +++++++++++++++++++++++++++ r2/r2/templates/printablebuttons.html | 58 +++++++++++++++--- r2/r2/templates/reportform.html | 73 ++++++++++++++++++++++ 10 files changed, 328 insertions(+), 22 deletions(-) create mode 100644 r2/r2/public/static/js/report.js create mode 100644 r2/r2/templates/reportform.html 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: -
  • - ${ynbutton(_("report"), _("reported"), "report", "hide_thing")} +
  • + %if thing.style in ("linkbuttons", "commentbuttons"): + + ${_("report")} + + %else: + ${ynbutton(_("report"), _("reported"), "report", "hide_thing")} + %endif
  • %endif %if thing.show_marknsfw: @@ -174,6 +180,28 @@ +<%def name="reports_button()"> +
  • + ${strings.reports % thing.thing.reported} +
  • + + +<%def name="report_reasons()"> + + + <%def name="linkbuttons()"> %if thing.show_comments:
  • @@ -252,9 +280,7 @@ %endif %if thing.show_reports and not thing.show_spam: -
  • - ${strings.reports % thing.thing.reported} -
  • + ${reports_button()} %endif %if getattr(thing.thing, "use_big_modbuttons", False): @@ -262,6 +288,11 @@ %elif thing.ignore_reports and thing.can_ban: ${ignore_reports_toggle(thing.thing)} %endif + + %if thing.show_reports and not thing.show_spam and thing.report_reasons: + ${report_reasons()} + %endif + <%def name="commentbuttons()"> @@ -280,6 +311,7 @@ %endif %endif + %if c.profilepage:
  • ${self.bylink_button(_("context"), thing.permalink + "?context=3")} @@ -292,6 +324,7 @@ a_class="may-blank")}
  • %endif + %if not thing.profilepage: %if thing.parent_permalink:
  • @@ -304,24 +337,31 @@
  • %endif %endif + ${self.banbuttons()} ${self.distinguish()} ${self.give_gold()} + %if not thing.profilepage and thing.can_reply:
  • ${self.simple_button(_("reply {verb}"), "reply")}
  • %endif + %if thing.show_reports and not thing.show_spam: -
  • - ${strings.reports % thing.thing.reported} -
  • + ${reports_button()} %endif + %if getattr(thing.thing, "use_big_modbuttons", False): ${big_modbuttons(thing.thing)} %elif thing.ignore_reports and thing.can_ban: ${ignore_reports_toggle(thing.thing)} %endif + + %if thing.show_reports and not thing.show_spam and thing.report_reasons: + ${report_reasons()} + %endif + %endif diff --git a/r2/r2/templates/reportform.html b/r2/r2/templates/reportform.html new file mode 100644 index 000000000..3e84390d9 --- /dev/null +++ b/r2/r2/templates/reportform.html @@ -0,0 +1,73 @@ +## The contents of this file are subject to the Common Public Attribution +## License Version 1.0. (the "License"); you may not use this file except in +## compliance with the License. You may obtain a copy of the License at +## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public +## License Version 1.1, but Sections 14 and 15 have been added to cover use of +## software over a computer network and provide for limited attribution for the +## Original Developer. In addition, Exhibit A has been modified to be +## consistent with Exhibit B. +## +## Software distributed under the License is distributed on an "AS IS" basis, +## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for +## the specific language governing rights and limitations under the License. +## +## The Original Code is reddit. +## +## The Original Developer is the Initial Developer. The Initial Developer of +## the Original Code is reddit Inc. +## +## All portions of the code written by reddit are Copyright (c) 2006-2014 +## reddit Inc. All Rights Reserved. +############################################################################### + +<%namespace file="utils.html" import="error_field" /> + +
    + + + ${_('why are you reporting this?')} + +
      +
    1. + +
    2. +
    3. + +
    4. +
    5. + +
    6. +
    7. + +
    8. +
    9. + +
    10. +
    11. + + +
    12. +
    + + + + ${error_field("TOO_LONG", "reason", "span")} +