Add listing for promotions suspected of fraud

This commit is contained in:
David Wick
2014-12-03 15:26:52 -08:00
parent 0baff564e5
commit 705b49f073
14 changed files with 278 additions and 120 deletions

View File

@@ -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',

View File

@@ -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(
"<p>" + websafe(text) + "</p>")
@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(),

View File

@@ -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")])

View File

@@ -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({

View File

@@ -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')

View File

@@ -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

View File

@@ -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)

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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();
});

View File

@@ -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();
});

View File

@@ -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.
###############################################################################
<form id="fraud-action-form" class="action-form fraud-action-form rounded" data-form-action="review_fraud">
<input type="hidden" name="thing_id" value="thing-fullname">
<span class="reason-prompt">
${_('is this fraud?')}
</span>
<ol>
<li>
<label>
<input type="radio" name="fraud" value="True">${_("yes")}
</label>
</li>
<li>
<label>
<input type="radio" name="fraud" value="False">${_("no")}
</label>
</li>
</ol>
<button type="submit" class="btn submit-action-thing" disabled>
${_("submit")}
</button>
<button type="button" class="btn cancel-action-thing">
${_("cancel")}
</button>
<span class="status"></span>
</form>

View File

@@ -56,7 +56,7 @@
%if thing.show_report:
<li class="report-button">
<a href="javascript:void(0)" class="report-thing">
<a href="javascript:void(0)" class="action-thing" data-action-form="#report-action-form">
${_("report")}
</a>
</li>
@@ -292,6 +292,13 @@
${ynbutton(_("accept"), _("accepted"), "promote")}
</li>
%endif
%if thing.is_awaiting_fraud_review:
<li class="fraud-button">
<a href="javascript:void(0)" class="action-thing" data-action-form="#fraud-action-form">
${_("fraud")}
</a>
</li>
%endif
%endif
%if thing.user_is_sponsor or thing.is_author:
<li>

View File

@@ -22,7 +22,7 @@
<%namespace file="utils.html" import="error_field" />
<form id="report-thing-fullname" class="report-form clonable rounded">
<form id="report-action-form" class="action-form report-action-form rounded" data-form-action="report">
<input type="hidden" name="thing_id" value="thing-fullname">
<span class="reason-prompt">
${_('why are you reporting this?')}
@@ -30,42 +30,41 @@
<ol>
<li>
<label>
<input type="radio" name="reason" value="spam">${_("spam")}</input>
<input type="radio" name="reason" value="spam">${_("spam")}
</label>
</li>
<li>
<label>
<input type="radio" name="reason" value="vote manipulation">${_("vote manipulation")}</input>
<input type="radio" name="reason" value="vote manipulation">${_("vote manipulation")}
</label>
</li>
<li>
<label>
<input type="radio" name="reason" value="personal information">${_("personal information")}</input>
<input type="radio" name="reason" value="personal information">${_("personal information")}
</label>
</li>
<li>
<label>
<input type="radio" name="reason" value="sexualizing minors">${_("sexualizing minors")}</input>
<input type="radio" name="reason" value="sexualizing minors">${_("sexualizing minors")}
</label>
</li>
<li>
<label>
<input type="radio" name="reason" value="breaking reddit">${_("breaking reddit")}</input>
<input type="radio" name="reason" value="breaking reddit">${_("breaking reddit")}
</label>
</li>
<li>
<label>
<input type="radio" name="reason" value="other">
${_("other (max %(num)s characters):") % dict(num=100)}
</input>
${_("other (max %(num)s characters):") % dict(num=100)}
</label>
<input name="other_reason" value="" maxlength="100" type="text" disabled="disabled"></input>
<input name="other_reason" value="" maxlength="100" type="text" disabled>
</li>
</ol>
<button type="submit" class="btn submit-report" disabled="disabled">
<button type="submit" class="btn submit-action-thing" disabled>
${_("submit")}
</button>
<button type="button" class="btn cancel-report-thing">
<button type="button" class="btn cancel-action-thing">
${_("cancel")}
</button>
<span class="status"></span>