implement report abuse for Marketplace detail page(bug 740283)
This commit is contained in:
Родитель
ef1f3b71d8
Коммит
dbaabe5f01
|
@ -19,10 +19,8 @@
|
|||
<div id="recaptcha_image"></div>
|
||||
<p>
|
||||
<input type="text" name="recaptcha_response_field"
|
||||
id="recaptcha_response_field" size="30" />
|
||||
id="recaptcha_response_field" size="30">
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</li>
|
||||
{{ form.recaptcha.errors }}
|
||||
|
||||
|
|
|
@ -1193,6 +1193,7 @@ RECAPTCHA_PUBLIC_KEY = ''
|
|||
RECAPTCHA_PRIVATE_KEY = ''
|
||||
RECAPTCHA_URL = ('https://www.google.com/recaptcha/api/challenge?k=%s' %
|
||||
RECAPTCHA_PUBLIC_KEY)
|
||||
RECAPTCHA_AJAX_URL = 'http://www.google.com/recaptcha/api/js/recaptcha_ajax.js'
|
||||
|
||||
# Send Django signals asynchronously on a background thread.
|
||||
ASYNC_SIGNALS = True
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
@import 'lib';
|
||||
|
||||
.recaptcha-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#abuse {
|
||||
ol {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
}
|
||||
#recap-container {
|
||||
margin-top: 15px;
|
||||
}
|
||||
#label {
|
||||
display: block;
|
||||
font-weight: normal;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
#recap-container {
|
||||
.border-radius(5px);
|
||||
background: @yellow;
|
||||
border: 1px solid darken(@yellow, 60%);
|
||||
padding: 1em;
|
||||
margin-bottom: 1em;
|
||||
position: relative;
|
||||
label {
|
||||
color: @gold;
|
||||
font-weight: bold;
|
||||
}
|
||||
input {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
#recaptcha_help {
|
||||
color: @gray;
|
||||
float: right;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
#recaptcha_widget_div {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: @4col) {
|
||||
#recaptcha_image {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
|
@ -164,6 +164,9 @@ p.req {
|
|||
display: inline-block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.errorlist {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
@bg: #dee3e6;
|
||||
@pale-bg: #eff1f3;
|
||||
@yellow: #ffe;
|
||||
@gold: darken(@yellow, 75%);
|
||||
|
||||
@btn-b: #44A5E1;
|
||||
@btn-b-dark: #267CC2;
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
(function() {
|
||||
z.page.on('fragmentloaded', function() {
|
||||
var $abuse = $('#abuse');
|
||||
if ($abuse.find('form legend a').length) {
|
||||
var $ol = $abuse.find('ol');
|
||||
$ol.hide();
|
||||
}
|
||||
var RecaptchaOptions = {theme: 'custom'};
|
||||
z.page.on('click', '#recaptcha_different', _pd(function() {
|
||||
Recaptcha.reload();
|
||||
})).on('click', '#recaptcha_audio', _pd(function() {
|
||||
Recaptcha.switch_type('audio');
|
||||
}));
|
||||
});
|
||||
})();
|
|
@ -11,6 +11,7 @@
|
|||
if (e.metaKey || e.ctrlKey || e.button !== 0) return;
|
||||
if (!href || href.substr(0,4) == 'http' ||
|
||||
href.substr(0,7) === 'mailto:' ||
|
||||
href.substr(0,11) === 'javascript:' ||
|
||||
href.substr(0,1) === '#' ||
|
||||
href.indexOf('/developers/') !== -1 ||
|
||||
href.indexOf('/statistics/') !== -1 ||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
z.page.on('fragmentloaded', function() {
|
||||
if ($('#recaptcha_div').length) {
|
||||
var recaptcha = $('body').data('recaptcha');
|
||||
if (recaptcha) {
|
||||
Recaptcha.create(recaptcha, 'recaptcha_div', {
|
||||
tabindex: 1,
|
||||
theme: 'red',
|
||||
callback: Recaptcha.focus_response_field
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -83,6 +83,7 @@ CSS = {
|
|||
'css/mkt/detail.less',
|
||||
'css/mkt/ratings.less',
|
||||
'css/mkt/device.less',
|
||||
'css/mkt/abuse.less',
|
||||
'css/mkt/slider.less',
|
||||
'css/mkt/promo-grid.less',
|
||||
'css/mkt/overlay.less',
|
||||
|
@ -189,6 +190,7 @@ JS = {
|
|||
'js/common/keys.js',
|
||||
'js/mkt/capabilities.js',
|
||||
'js/mkt/fragments.js',
|
||||
'js/mkt/recaptcha.js',
|
||||
'js/mkt/overlay.js',
|
||||
'js/mkt/slider.js',
|
||||
'js/mkt/login.js',
|
||||
|
@ -197,6 +199,7 @@ JS = {
|
|||
'js/mkt/buttons.js',
|
||||
'js/mkt/search.js',
|
||||
'js/mkt/apps.js',
|
||||
'js/mkt/abuse.js',
|
||||
'js/zamboni/outgoing_links.js',
|
||||
'js/common/upload-image.js',
|
||||
'js/impala/serializers.js',
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
{% extends 'mkt/base.html' %}
|
||||
|
||||
{% set title = _('Report Abuse') %}
|
||||
{% block title %}
|
||||
{# L10n: {0} is the name of the app. #}
|
||||
{{ mkt_page_title(_('Report abuse for {0}')|f(product.name)) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ mkt_breadcrumbs(product, [(None, title)]) }}
|
||||
<section id="abuse" class="friendly">
|
||||
<div>
|
||||
<h2>{{ title }}</h2>
|
||||
<p>
|
||||
<label id="label" for="{{ abuse_form.text.auto_id }}">
|
||||
{% trans policies_url='https://developer.mozilla.org/en/Apps/'
|
||||
'Marketplace_Review' %}
|
||||
If you suspect this app violates
|
||||
<a href="{{ policies_url }}">our policies</a> or has security or
|
||||
privacy issues, please use the form below to describe your
|
||||
concerns. Please do not use this form for any other reason.
|
||||
{% endtrans %}
|
||||
</label>
|
||||
</p>
|
||||
<form method="post">
|
||||
{{ csrf() }}
|
||||
{{ grid_field(abuse_form.text, req=False, label='') }}
|
||||
{% if abuse_form.has_recaptcha %}
|
||||
{% from 'includes/forms.html' import required %}
|
||||
<div id="recap-container">
|
||||
<label for="recaptcha_response_field">
|
||||
{{ _('Are you human?') }} {{ required() }}
|
||||
<a href="#" id="recaptcha_help">{{ _("What's this?") }}</a>
|
||||
</label>
|
||||
<div class="recaptcha-container">
|
||||
{% trans %}
|
||||
<p>
|
||||
Please enter <strong>both words</strong> below,
|
||||
<strong>separated by a space</strong>.
|
||||
</p>
|
||||
<p>
|
||||
If this is hard to read, you can
|
||||
<a href="#" id="recaptcha_different">try different words</a> or
|
||||
<a href="#" id="recaptcha_audio">listen to something</a> instead.
|
||||
</p>
|
||||
{% endtrans %}
|
||||
<div id="recaptcha_div"></div>
|
||||
</div>
|
||||
</div>
|
||||
{{ abuse_form.recaptcha.errors }}
|
||||
{% endif %}
|
||||
<p class="form-footer">
|
||||
<button type="submit">{{ _('Send Report') }}</button> {{ _('or') }}
|
||||
<a href="{{ product.get_detail_url() }}">{{ _('Cancel') }}</a>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -228,6 +228,22 @@
|
|||
<p>
|
||||
{{ product.created|timelabel }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li class="published">
|
||||
<h3>{{ _('Published') }}</h3>
|
||||
<p>
|
||||
<time isotime="{{ product.created|isotime }}"
|
||||
title="{{ product.created|isotime }}">
|
||||
{{ product.created|timesince }}
|
||||
</time>
|
||||
</p>
|
||||
</li>
|
||||
{% if abuse_form %}
|
||||
<li class="abuse">
|
||||
<h3>{{ _('Report Abuse') }}</h3>
|
||||
<p><a href="{{ product.get_detail_url('abuse') }}">
|
||||
{{ _('Report') }}</a></p>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import mail
|
||||
from django.utils.html import strip_tags
|
||||
|
||||
import mock
|
||||
|
@ -10,6 +12,7 @@ from pyquery import PyQuery as pq
|
|||
from tower import strip_whitespace
|
||||
import waffle
|
||||
|
||||
from abuse.models import AbuseReport
|
||||
import amo
|
||||
from amo.helpers import external_url, numberfmt
|
||||
import amo.tests
|
||||
|
@ -626,22 +629,52 @@ class TestPrivacy(amo.tests.TestCase):
|
|||
eq_(get_clean(doc('ol li:first')), '<li>papparapara2</li>')
|
||||
|
||||
|
||||
class TestReportAbuse(amo.tests.TestCase):
|
||||
|
||||
@mock.patch.object(settings, 'RECAPTCHA_PRIVATE_KEY', 'something')
|
||||
class TestReportAbuse(DetailBase):
|
||||
fixtures = ['base/users', 'webapps/337141-steamcube']
|
||||
|
||||
def setUp(self):
|
||||
self.webapp = Webapp.objects.get(id=337141)
|
||||
super(TestReportAbuse, self).setUp()
|
||||
self.url = self.webapp.get_detail_url('abuse')
|
||||
|
||||
def test_get(self):
|
||||
# TODO: Uncomment Report Abuse gets ported to mkt.
|
||||
raise SkipTest
|
||||
r = self.client.get(self.url)
|
||||
eq_(r.status_code, 200)
|
||||
def log_in(self):
|
||||
assert self.client.login(username='regular@mozilla.com',
|
||||
password='password')
|
||||
|
||||
def test_submit(self):
|
||||
# TODO: Uncomment Report Abuse gets ported to mkt.
|
||||
raise SkipTest
|
||||
self.client.login(username='regular@mozilla.com', password='password')
|
||||
r = self.client.post(self.url, {'text': 'this is some rauncy ish'})
|
||||
self.assertRedirects(r, self.webapp.get_detail_url())
|
||||
def test_recaptcha_shown_for_anonymous(self):
|
||||
eq_(self.get_pq()('#recap-container').length, 1)
|
||||
|
||||
def test_no_recaptcha_for_authenticated(self):
|
||||
self.log_in()
|
||||
eq_(self.get_pq()('#recap-container').length, 0)
|
||||
|
||||
@mock.patch('captcha.fields.ReCaptchaField.clean', new=mock.Mock)
|
||||
def test_abuse_anonymous(self):
|
||||
self.client.post(self.url, {'text': 'spammy'})
|
||||
eq_(len(mail.outbox), 1)
|
||||
assert 'spammy' in mail.outbox[0].body
|
||||
report = AbuseReport.objects.get(addon=337141)
|
||||
eq_(report.message, 'spammy')
|
||||
eq_(report.reporter, None)
|
||||
|
||||
def test_abuse_anonymous_fails(self):
|
||||
r = self.client.post(self.url, {'text': 'spammy'})
|
||||
assert 'recaptcha' in r.context['abuse_form'].errors
|
||||
|
||||
def test_abuse_authenticated(self):
|
||||
self.log_in()
|
||||
self.client.post(self.url, {'text': 'spammy'})
|
||||
eq_(len(mail.outbox), 1)
|
||||
assert 'spammy' in mail.outbox[0].body
|
||||
report = AbuseReport.objects.get(addon=337141)
|
||||
eq_(report.message, 'spammy')
|
||||
eq_(report.reporter.email, 'regular@mozilla.com')
|
||||
|
||||
def test_abuse_name(self):
|
||||
self.webapp.name = 'Bmrk.ru Социальные закладки'
|
||||
self.webapp.save()
|
||||
self.log_in()
|
||||
self.client.post(self.url, {'text': 'spammy'})
|
||||
assert 'spammy' in mail.outbox[0].body
|
||||
assert AbuseReport.objects.get(addon=self.webapp)
|
||||
|
|
|
@ -7,10 +7,12 @@ from . import views
|
|||
|
||||
urlpatterns = patterns('',
|
||||
url('^$', views.detail, name='detail'),
|
||||
url('^abuse$', views.abuse, name='detail.abuse'),
|
||||
url('^abuse/recaptcha$', views.abuse_recaptcha,
|
||||
name='detail.abuse.recaptcha'),
|
||||
url('^record$', views.record, name='detail.record'),
|
||||
url('^privacy$', views.privacy, name='detail.privacy'),
|
||||
|
||||
# Submission.
|
||||
('^purchase/', include('mkt.purchase.urls')),
|
||||
|
||||
# Statistics.
|
||||
|
|
|
@ -2,15 +2,23 @@ import jingo
|
|||
from tower import ugettext as _
|
||||
|
||||
from django import http
|
||||
from django.shortcuts import redirect
|
||||
|
||||
from commonware.response.decorators import xframe_sameorigin
|
||||
from session_csrf import anonymous_csrf_exempt
|
||||
from tower import ugettext as _
|
||||
|
||||
from abuse.models import send_abuse_report
|
||||
from access import acl
|
||||
from addons.decorators import addon_view_factory
|
||||
from amo.decorators import json_view, login_required, post_required, write
|
||||
from amo.forms import AbuseForm
|
||||
from amo.utils import memoize_get
|
||||
from lib.metrics import send_request
|
||||
from lib.crypto.receipt import cef, SigningError
|
||||
|
||||
from mkt.ratings.models import Rating
|
||||
from mkt.site import messages
|
||||
from mkt.webapps.models import create_receipt, Installed, Webapp
|
||||
|
||||
addon_view = addon_view_factory(qs=Webapp.objects.valid)
|
||||
|
@ -20,15 +28,19 @@ addon_all_view = addon_view_factory(qs=Webapp.objects.all)
|
|||
@addon_all_view
|
||||
def detail(request, addon):
|
||||
"""Product details page."""
|
||||
|
||||
ratings = Rating.objects.latest().filter(addon=addon).order_by('-created')
|
||||
positive_ratings = ratings.filter(score=1)[:5]
|
||||
negative_ratings = ratings.filter(score=-1)[:5]
|
||||
return jingo.render(request, 'detail/app.html', {
|
||||
ctx = {
|
||||
'product': addon,
|
||||
'ratings': ratings,
|
||||
'positive_ratings': positive_ratings,
|
||||
'negative_ratings': negative_ratings,
|
||||
})
|
||||
}
|
||||
if addon.is_public():
|
||||
ctx['abuse_form'] = AbuseForm(request=request)
|
||||
return jingo.render(request, 'detail/app.html', ctx)
|
||||
|
||||
|
||||
@addon_all_view
|
||||
|
@ -42,6 +54,32 @@ def privacy(request, addon):
|
|||
return jingo.render(request, 'detail/privacy.html', {'product': addon})
|
||||
|
||||
|
||||
@anonymous_csrf_exempt
|
||||
@addon_view
|
||||
def abuse(request, addon):
|
||||
form = AbuseForm(request.POST or None, request=request)
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
send_abuse_report(request, addon, form.cleaned_data['text'])
|
||||
messages.success(request, _('Abuse reported.'))
|
||||
return redirect(addon.get_url_path())
|
||||
else:
|
||||
return jingo.render(request, 'detail/abuse.html',
|
||||
{'product': addon, 'abuse_form': form})
|
||||
|
||||
|
||||
@anonymous_csrf_exempt
|
||||
@addon_view
|
||||
def abuse_recaptcha(request, addon):
|
||||
form = AbuseForm(request.POST or None, request=request)
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
send_abuse_report(request, addon, form.cleaned_data['text'])
|
||||
messages.success(request, _('Abuse reported.'))
|
||||
return redirect(addon.get_url_path())
|
||||
else:
|
||||
return jingo.render(request, 'detail/abuse_recaptcha.html',
|
||||
{'product': addon, 'abuse_form': form})
|
||||
|
||||
|
||||
@json_view
|
||||
@addon_all_view
|
||||
@login_required
|
||||
|
|
|
@ -29,6 +29,7 @@ MKT_SUPPORT_EMAIL = 'marketplace-developer-support@mozilla.org'
|
|||
MARKETPLACE_EMAIL = 'amo-marketplace@mozilla.org'
|
||||
ABUSE_EMAIL = 'Mozilla Marketplace <marketplace-abuse@mozilla.org>'
|
||||
NOBODY_EMAIL = 'Mozilla Marketplace <nobody@mozilla.org>'
|
||||
DEFAULT_FROM_EMAIL = 'Mozilla Marketplace <nobody@mozilla.org>'
|
||||
|
||||
ROOT_URLCONF = 'mkt.urls'
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
{% macro simple_label(field, label, opt, req, tooltip, hint) %}
|
||||
<label class="{{ 'choice' if choice }}" for="{{ field.auto_id }}">
|
||||
{{ label or field.label }}
|
||||
{{ label }}
|
||||
</label>
|
||||
|
||||
{% if field.field.required and req != False %}{{ required() -}}{% endif %}
|
||||
|
@ -24,7 +24,12 @@
|
|||
{% if choice %}{{ field.as_widget() }}{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{{ simple_label(field, label, opt, req, tooltip, hint) }}
|
||||
{% if not label %}
|
||||
{% set label = field.label %}
|
||||
{% endif %}
|
||||
{% if label %}
|
||||
{{ simple_label(field, label, opt, req, tooltip, hint) }}
|
||||
{% endif %}
|
||||
|
||||
{% if grid %}
|
||||
</div>
|
||||
|
|
|
@ -39,6 +39,7 @@
|
|||
data-media-url="{{ MEDIA_URL }}"
|
||||
data-login-url="{{ url('users.browserid_login') }}"
|
||||
data-collect-timings="{{ url('mkt.timing.record') }}:{{ collect_timings_percent }}"
|
||||
data-recaptcha="{{ settings.RECAPTCHA_PUBLIC_KEY }}"
|
||||
{% block bodyattrs %}{% endblock %}>
|
||||
|
||||
{% block siteheader %}
|
||||
|
@ -159,6 +160,7 @@
|
|||
{% if not logged %}
|
||||
{# We need Persona for only non-authenticated users. #}
|
||||
<script async defer src="https://browserid.org/include.js"></script>
|
||||
<script type="text/javascript" src="{{ settings.RECAPTCHA_AJAX_URL }}"></script>
|
||||
{% endif %}
|
||||
{{ js('mkt/consumer') }}
|
||||
{% endblock %}
|
||||
|
|
Загрузка…
Ссылка в новой задаче