From 705b49f073faba30ab8057e141dc09c631a48740 Mon Sep 17 00:00:00 2001 From: David Wick Date: Wed, 3 Dec 2014 15:26:52 -0800 Subject: [PATCH] Add listing for promotions suspected of fraud --- r2/r2/config/routing.py | 5 +- r2/r2/controllers/promotecontroller.py | 20 +++- r2/r2/lib/db/queries.py | 17 ++++ r2/r2/lib/js.py | 2 +- r2/r2/lib/pages/pages.py | 9 ++ r2/r2/lib/pages/things.py | 8 +- r2/r2/lib/promote.py | 34 +++++-- r2/r2/models/link.py | 3 +- r2/r2/public/static/css/reddit.less | 10 +- r2/r2/public/static/js/action-forms.js | 125 +++++++++++++++++++++++++ r2/r2/public/static/js/report.js | 88 ----------------- r2/r2/templates/fraudform.html | 47 ++++++++++ r2/r2/templates/printablebuttons.html | 9 +- r2/r2/templates/reportform.html | 21 ++--- 14 files changed, 278 insertions(+), 120 deletions(-) create mode 100644 r2/r2/public/static/js/action-forms.js delete mode 100644 r2/r2/public/static/js/report.js create mode 100644 r2/r2/templates/fraudform.html diff --git a/r2/r2/config/routing.py b/r2/r2/config/routing.py index 890f7ec38..c7b8a287f 100644 --- a/r2/r2/config/routing.py +++ b/r2/r2/config/routing.py @@ -227,7 +227,7 @@ def make_map(): mc('/sponsor/promoted/:sort', controller='sponsorlisting', action='listing', requirements=dict(sort="future_promos|pending_promos|unpaid_promos|" "rejected_promos|live_promos|underdelivered|" - "reported|house|all")) + "reported|house|fraud|all")) mc('/sponsor', controller='sponsorlisting', action="listing", sort="all") mc('/sponsor/promoted/', controller='sponsorlisting', action="listing", @@ -371,7 +371,8 @@ def make_map(): "freebie|promote_note|update_pay|" "edit_campaign|delete_campaign|" "add_roadblock|rm_roadblock|check_inventory|" - "refund_campaign|terminate_campaign"))) + "refund_campaign|terminate_campaign|" + "review_fraud"))) mc('/api/:action', controller='apiminimal', requirements=dict(action="new_captcha")) mc('/api/:type', controller='api', diff --git a/r2/r2/controllers/promotecontroller.py b/r2/r2/controllers/promotecontroller.py index 23dcc0b75..e283f9d67 100644 --- a/r2/r2/controllers/promotecontroller.py +++ b/r2/r2/controllers/promotecontroller.py @@ -373,6 +373,7 @@ class SponsorListingController(PromoteListingController): 'underdelivered': N_('underdelivered promoted links'), 'reported': N_('reported promoted links'), 'house': N_('house promoted links'), + 'fraud': N_('fraud suspected promoted links'), }.items()) base_path = '/sponsor/promoted' @@ -382,7 +383,7 @@ class SponsorListingController(PromoteListingController): @property def menus(self): - if self.sort in {'underdelivered', 'reported', 'house'}: + if self.sort in {'underdelivered', 'reported', 'house', 'fraud'}: menus = [] else: menus = super(SponsorListingController, self).menus @@ -465,6 +466,8 @@ class SponsorListingController(PromoteListingController): return [Link._fullname_from_id36(to36(id)) for id in link_ids] elif self.sort == 'reported': return queries.get_reported_links(Subreddit.get_promote_srid()) + elif self.sort == 'fraud': + return queries.get_payment_flagged_links() elif self.sort == 'house': return self.get_house_link_names() elif self.sort == 'all': @@ -548,6 +551,21 @@ class PromoteApiController(ApiController): form.find(".notes").children(":last").after( "

" + websafe(text) + "

") + @validatedForm( + VSponsorAdmin(), + VModhash(), + thing = VByName("thing_id"), + is_fraud=VBoolean("fraud"), + ) + def POST_review_fraud(self, form, jquery, thing, is_fraud): + if not promote.is_promo(thing): + return + + promote.review_fraud(thing, is_fraud) + + button = jquery(".id-%s .fraud-button" % thing._fullname) + button.text(_("fraud" if is_fraud else "not fraud")) + form.fadeOut() @noresponse(VSponsorAdmin(), VModhash(), diff --git a/r2/r2/lib/db/queries.py b/r2/r2/lib/db/queries.py index 0409d2cb4..5de44e7d4 100644 --- a/r2/r2/lib/db/queries.py +++ b/r2/r2/lib/db/queries.py @@ -793,6 +793,23 @@ def get_all_accepted_links(): return _promoted_link_query(None, 'accepted') +@cached_query(UserQueryCache) +def get_payment_flagged_links(): + return FakeQuery(sort=[desc("_date")]) + + +def set_payment_flagged_link(link): + with CachedQueryMutator() as m: + q = get_payment_flagged_links() + m.insert(q, [link]) + + +def unset_payment_flagged_link(link): + with CachedQueryMutator() as m: + q = get_payment_flagged_links() + m.delete(q, [link]) + + @cached_query(UserQueryCache) def get_underdelivered_campaigns(): return FakeQuery(sort=[desc("_date")]) diff --git a/r2/r2/lib/js.py b/r2/r2/lib/js.py index ffec8a01f..effe05e4c 100644 --- a/r2/r2/lib/js.py +++ b/r2/r2/lib/js.py @@ -459,7 +459,7 @@ module["reddit"] = LocalizedModule("reddit.js", "multi.js", "filter.js", "recommender.js", - "report.js", + "action-forms.js", "saved.js", "messages.js", PermissionsDataSource({ diff --git a/r2/r2/lib/pages/pages.py b/r2/r2/lib/pages/pages.py index f9fc22f4e..72c26d0e9 100644 --- a/r2/r2/lib/pages/pages.py +++ b/r2/r2/lib/pages/pages.py @@ -338,6 +338,10 @@ class Reddit(Templated): panes = [ShareLink(), content, report_form] if self.show_sidebar: panes.extend([gold_comment, gold_link]) + + if c.user_is_sponsor: + panes.append(FraudForm()) + self._content = PaneStack(panes) else: self._content = content @@ -2732,6 +2736,10 @@ class ReportForm(Templated): pass +class FraudForm(Templated): + pass + + class Password(Templated): """Form encountered when 'recover password' is clicked in the LoginFormWide.""" def __init__(self, success=False): @@ -3728,6 +3736,7 @@ class PromotePage(Reddit): NavButton('underdelivered', '/sponsor/promoted/underdelivered'), NavButton('house ads', '/sponsor/promoted/house'), NavButton('reported links', '/sponsor/promoted/reported'), + NavButton('fraud', '/sponsor/promoted/fraud'), NavButton('lookup user', '/sponsor/lookup_user'), ] return NavMenu(buttons, type='flatlist') diff --git a/r2/r2/lib/pages/things.py b/r2/r2/lib/pages/things.py index 1f285ca79..08ef452dc 100644 --- a/r2/r2/lib/pages/things.py +++ b/r2/r2/lib/pages/things.py @@ -112,8 +112,12 @@ class LinkButtons(PrintableButtons): kw = dict(promo_url = promo_edit_url(thing), promote_status = getattr(thing, "promote_status", 0), user_is_sponsor = c.user_is_sponsor, - traffic_url = promo_traffic_url(thing), - is_author = thing.is_author) + traffic_url = promo_traffic_url(thing), + is_author = thing.is_author, + ) + + if c.user_is_sponsor: + kw["is_awaiting_fraud_review"] = is_awaiting_fraud_review(thing) PrintableButtons.__init__(self, 'linkbuttons', thing, # user existence and preferences diff --git a/r2/r2/lib/promote.py b/r2/r2/lib/promote.py index d6ae1599b..fb3703146 100644 --- a/r2/r2/lib/promote.py +++ b/r2/r2/lib/promote.py @@ -41,11 +41,7 @@ from r2.lib import ( hooks, ) from r2.lib.db.operators import not_ -from r2.lib.db.queries import ( - set_promote_status, - set_underdelivered_campaigns, - unset_underdelivered_campaigns, -) +from r2.lib.db import queries from r2.lib.cache import sgm from r2.lib.memoize import memoize from r2.lib.strings import strings @@ -137,6 +133,9 @@ def refund_url(link, campaign): # booleans +def is_awaiting_fraud_review(link): + return link.payment_flagged_reason and link.fraud == None + def is_promo(link): return (link and not link._deleted and link.promoted is not None and hasattr(link, "promote_status")) @@ -216,7 +215,7 @@ def add_trackers(items, sr): def update_promote_status(link, status): - set_promote_status(link, status) + queries.set_promote_status(link, status) hooks.get_hook('promote.edit_promotion').call(link=link) @@ -487,10 +486,25 @@ def accept_promotion(link): all_live_promo_srnames(_update=True) -def flag_payment(link, reason="Unknown reason."): - link.payment_flagged = reason +def flag_payment(link, reason): + # already determined to be fraud. + if link.payment_flagged_reason and link.fraud: + return + + link.payment_flagged_reason = reason link._commit() PromotionLog.add(link, "payment flagged: %s" % reason) + queries.set_payment_flagged_link(link) + + +def review_fraud(link, is_fraud): + link.fraud = is_fraud + link._commit() + PromotionLog.add(link, "marked as fraud" if is_fraud else "resolved as not fraud") + queries.unset_payment_flagged_link(link) + + if is_fraud: + hooks.get_hook("promote.fraud_identified").call(link=link, sponsor=c.user) def reject_promotion(link, reason=None): @@ -724,7 +738,7 @@ def finalize_completed_campaigns(daysago=1): underdelivered_campaigns.append(camp) if underdelivered_campaigns: - set_underdelivered_campaigns(underdelivered_campaigns) + queries.set_underdelivered_campaigns(underdelivered_campaigns) def get_refund_amount(camp, billable): @@ -758,7 +772,7 @@ def refund_campaign(link, camp, billable_amount, billable_impressions): PromotionLog.add(link, text) camp.refund_amount = refund_amount camp._commit() - unset_underdelivered_campaigns(camp) + queries.unset_underdelivered_campaigns(camp) emailer.refunded_promo(link) diff --git a/r2/r2/models/link.py b/r2/r2/models/link.py index fea42bec0..fc1dd3593 100644 --- a/r2/r2/models/link.py +++ b/r2/r2/models/link.py @@ -82,7 +82,8 @@ class Link(Thing, Printable): media_autoplay=False, domain_override=None, promoted=None, - payment_flagged=None, + payment_flagged_reason=None, + fraud=None, managed_promo=False, pending=False, disable_comments=False, diff --git a/r2/r2/public/static/css/reddit.less b/r2/r2/public/static/css/reddit.less index 819a9244d..0ac61ff2e 100644 --- a/r2/r2/public/static/css/reddit.less +++ b/r2/r2/public/static/css/reddit.less @@ -10414,7 +10414,7 @@ body.with-listing-chooser { } } -.report-form { +.action-form { display: none; background-color: #f6e69f; border: thin solid #d8bb3c; @@ -10424,13 +10424,13 @@ body.with-listing-chooser { font-size: larger; input { - margin: 5px 0; &[type="radio"] { margin: 2px 0.5em 0 0; } - &[name="other_reason"] { + &[type="text"] { + margin-top: 5px; width: 95%; } @@ -10438,6 +10438,10 @@ body.with-listing-chooser { background: #dddddd; } } + + ol { + margin-bottom: 5px; + } } .reported-stamp.has-reasons { diff --git a/r2/r2/public/static/js/action-forms.js b/r2/r2/public/static/js/action-forms.js new file mode 100644 index 000000000..238de043c --- /dev/null +++ b/r2/r2/public/static/js/action-forms.js @@ -0,0 +1,125 @@ +r.actionForm = { + init: function() { + $('div.content').on( + 'click', + '.action-thing, .cancel-action-thing', + this.toggleActionForm.bind(this) + ); + + $('div.content').on( + 'submit', + '.action-form', + this.submitAction.bind(this) + ); + }, + + toggleActionForm: function(e) { + var el = e.target; + var $el = $(el); + var $thing = $el.thing(); + var $thingForm = $thing.find('> .entry .action-form'); + var formSelector = $el.data('action-form'); + + e.stopPropagation(); + e.preventDefault(); + + if ($thingForm.length > 0) { + $thingForm.toggle(); + } else { + var $form = $(formSelector); + var $clonedForm = $form.clone(); + var $insertionPoint = $thing.find('> .entry .buttons'); + var thingFullname = $thing.thing_id(); + + $clonedForm.attr('id', 'action-thing-' + thingFullname); + $clonedForm.find('input[name="thing_id"]').val(thingFullname); + $clonedForm.insertAfter($insertionPoint); + $clonedForm.show(); + } + }, + + submitAction: function(e) { + var $actionForm = $(e.target).thing().find('.action-form'); + var action = $actionForm.data('form-action'); + + return post_pseudo_form($actionForm, action); + } + +}; + +r.fraud = { + + init: function() { + $('div.content').on( + 'change', + '.fraud-action-form input', + this.validate.bind(this) + ); + }, + + validate: function(e) { + var $el = $(e.target); + var $form = $el.parents('form'); + var $submit = $form.find('[type="submit"]'); + var $refund = $form.find('input[name=refund]'); + var fraud = $form.find('input[name=fraud]:checked').val(); + var allowRefund = fraud === 'True'; + + if (allowRefund) { + $refund.removeAttr('disabled').focus(); + } else { + $refund.prop('checked', false).attr('disabled', 'disabled'); + } + + if (!!fraud) { + $submit.removeAttr('disabled'); + } else { + $submit.attr('disabled', 'disabled'); + } + } +}; + +r.report = { + + init: function() { + $('div.content').on( + 'change', + '.report-action-form input', + this.validate.bind(this) + ); + + $('div.content').on( + 'click', + '.reported-stamp.has-reasons', + this.toggleReasons.bind(this) + ); + }, + + toggleReasons: function(e) { + $(e.target).parent().find('.report-reasons').toggle(); + }, + + validate: function(e) { + var $thing = $(e.target).thing(); + var $form = $thing.find('> .entry .report-action-form'); + var $submit = $form.find('[type="submit"]'); + var $reason = $form.find('[name=reason]:checked'); + var $other = $form.find('[name="other_reason"]'); + var isOther = $reason.val() === 'other'; + + $submit.removeAttr('disabled'); + + if (isOther) { + $other.removeAttr('disabled').focus(); + } else { + $other.attr('disabled', 'disabled'); + } + } + +}; + +$(function () { + r.actionForm.init(); + r.fraud.init(); + r.report.init(); +}); diff --git a/r2/r2/public/static/js/report.js b/r2/r2/public/static/js/report.js deleted file mode 100644 index 21ab9d059..000000000 --- a/r2/r2/public/static/js/report.js +++ /dev/null @@ -1,88 +0,0 @@ -r.report = { - "init": function() { - $('div.content').on( - 'click', - '.report-thing, button.cancel-report-thing', - $.proxy(this, 'toggleReportForm') - ); - - $('div.content').on( - 'submit', - 'form.report-form', - $.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).thing().find(".report-form") - 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").focus(); - } else { - $otherInput.attr("disabled", "disabled"); - } - - return false; - } -} - -$(function() { - r.report.init(); -}); diff --git a/r2/r2/templates/fraudform.html b/r2/r2/templates/fraudform.html new file mode 100644 index 000000000..42a60f0a5 --- /dev/null +++ b/r2/r2/templates/fraudform.html @@ -0,0 +1,47 @@ +## 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. +############################################################################### + +
+ + + ${_('is this fraud?')} + +
    +
  1. + +
  2. +
  3. + +
  4. +
+ + + +
diff --git a/r2/r2/templates/printablebuttons.html b/r2/r2/templates/printablebuttons.html index 431b0759d..37880f69e 100644 --- a/r2/r2/templates/printablebuttons.html +++ b/r2/r2/templates/printablebuttons.html @@ -56,7 +56,7 @@ %if thing.show_report:
  • - + ${_("report")}
  • @@ -292,6 +292,13 @@ ${ynbutton(_("accept"), _("accepted"), "promote")} %endif + %if thing.is_awaiting_fraud_review: +
  • + + ${_("fraud")} + +
  • + %endif %endif %if thing.user_is_sponsor or thing.is_author:
  • diff --git a/r2/r2/templates/reportform.html b/r2/r2/templates/reportform.html index eb68594eb..b244dd773 100644 --- a/r2/r2/templates/reportform.html +++ b/r2/r2/templates/reportform.html @@ -22,7 +22,7 @@ <%namespace file="utils.html" import="error_field" /> -
    + ${_('why are you reporting this?')} @@ -30,42 +30,41 @@
    1. - +
    - -