allow people to submit app ratings (bug 755567)
This commit is contained in:
Родитель
931f1781df
Коммит
a605dd61e5
|
@ -7,6 +7,7 @@ from tower import ugettext_lazy as _lazy
|
|||
from quieter_formset.formset import BaseModelFormSet
|
||||
|
||||
import amo
|
||||
from amo.utils import raise_required
|
||||
import reviews
|
||||
from .models import ReviewFlag, Review
|
||||
|
||||
|
@ -15,6 +16,13 @@ class ReviewReplyForm(forms.Form):
|
|||
title = forms.CharField(required=False)
|
||||
body = forms.CharField(widget=forms.Textarea(attrs={'rows': 3}))
|
||||
|
||||
def clean_body(self):
|
||||
body = self.cleaned_data.get('body', '')
|
||||
# Whitespace is not a review!
|
||||
if not body.strip():
|
||||
raise_required()
|
||||
return body
|
||||
|
||||
|
||||
class ReviewForm(ReviewReplyForm):
|
||||
rating = forms.ChoiceField(zip(range(1, 6), range(1, 6)))
|
||||
|
|
|
@ -280,9 +280,10 @@ class TestCreate(ReviewTest):
|
|||
eq_(r.status_code, 403)
|
||||
|
||||
def test_no_body(self):
|
||||
r = self.client.post(self.add, {'body': ''})
|
||||
self.assertFormError(r, 'form', 'body', 'This field is required.')
|
||||
eq_(len(mail.outbox), 0)
|
||||
for body in ('', ' \t \n '):
|
||||
r = self.client.post(self.add, {'body': body})
|
||||
self.assertFormError(r, 'form', 'body', 'This field is required.')
|
||||
eq_(len(mail.outbox), 0)
|
||||
|
||||
def test_no_rating(self):
|
||||
r = self.client.post(self.add, {'body': 'no rating'})
|
||||
|
|
|
@ -77,10 +77,7 @@
|
|||
}
|
||||
|
||||
&.good {
|
||||
|
||||
background-color: #86DE32;
|
||||
.gradient-two-color(#A2E456, #86DE32);
|
||||
|
||||
.gradient-two-color(darken(#A2E456, 20%), darken(#86DE32, 20%));
|
||||
}
|
||||
|
||||
&.arrow {
|
||||
|
|
|
@ -1,5 +1,29 @@
|
|||
@import 'lib';
|
||||
|
||||
.friendly {
|
||||
p, ul {
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
}
|
||||
.req,
|
||||
.errorlist {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.char-count, .html-support, .errorlist {
|
||||
margin: 5px 0;
|
||||
}
|
||||
.char-count {
|
||||
float: right;
|
||||
b {
|
||||
color: @dark-gray;
|
||||
}
|
||||
&.error b {
|
||||
color: @maroon;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button, input, select, textarea {
|
||||
font: 100% @open-stack;
|
||||
margin: 0;
|
||||
|
@ -247,7 +271,7 @@ form {
|
|||
|
||||
.form-footer {
|
||||
button {
|
||||
margin-right: 5px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
a {
|
||||
display: inline-block;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@import 'lib';
|
||||
|
||||
.reviews {
|
||||
ul {
|
||||
padding: 0;
|
||||
|
@ -8,6 +10,22 @@
|
|||
}
|
||||
h3 {
|
||||
text-align: center;
|
||||
span {
|
||||
background: url(../../img/icons/thumbs.png) no-repeat;
|
||||
display: inline-block;
|
||||
line-height: 16px;
|
||||
padding-left: 30px;
|
||||
}
|
||||
&.upvotes {
|
||||
color: #00b960;
|
||||
}
|
||||
&.downvotes {
|
||||
color: #d93a40;
|
||||
span {
|
||||
background-position: 100% -86px;
|
||||
padding: 0 30px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.review {
|
||||
list-style-type: none;
|
||||
|
@ -15,4 +33,72 @@
|
|||
font-size: 120%;
|
||||
}
|
||||
}
|
||||
}
|
||||
#submit-review {
|
||||
margin: 10px 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
#submit-rating {
|
||||
form {
|
||||
margin-top: 25px;
|
||||
.simple-field {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
select {
|
||||
display: none;
|
||||
}
|
||||
.barometer {
|
||||
font-size: 120%;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.form-footer {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.barometer {
|
||||
span {
|
||||
display: inline-block;
|
||||
.width(1.5);
|
||||
&:before {
|
||||
background: url(../../img/icons/thumbs.png) no-repeat;
|
||||
content: "";
|
||||
display: block;
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
height: 20px;
|
||||
width: 15px;
|
||||
}
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
&.voted {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
.upvotes {
|
||||
color: #00b960;
|
||||
&:before {
|
||||
background-position: 0 -42px;
|
||||
}
|
||||
&:hover, &.voted {
|
||||
&:before {
|
||||
background-position: 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.downvotes {
|
||||
color: #d93a40;
|
||||
&:before {
|
||||
background-position: 100% 100%;
|
||||
}
|
||||
&:hover, &.voted {
|
||||
&:before {
|
||||
background-position: 100% -82px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
(function() {
|
||||
z.page.on('fragmentloaded', function() {
|
||||
initCharCount();
|
||||
|
||||
// Hijack <select> with Thumbs Up and Thumbs Down.
|
||||
if ($('#submit-rating').length) {
|
||||
var $up = $('#thumbs-up'),
|
||||
$down = $('#thumbs-down'),
|
||||
$score = $('#id_score'),
|
||||
$review = $('#id_body');
|
||||
|
||||
function upRate() {
|
||||
$down.removeClass('voted');
|
||||
$up.toggleClass('voted');
|
||||
}
|
||||
function downRate() {
|
||||
$up.removeClass('voted');
|
||||
$down.toggleClass('voted');
|
||||
}
|
||||
|
||||
$up.on('click', function() {
|
||||
upRate();
|
||||
$score.val($score.val() == '1' ? '' : '1');
|
||||
if (!$review.val()) {
|
||||
$review.focus();
|
||||
}
|
||||
});
|
||||
$down.on('click', function() {
|
||||
downRate();
|
||||
$score.val($score.val() == '-1' ? '' : '-1');
|
||||
if (!$review.val()) {
|
||||
$review.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize thumbs when POST'ed.
|
||||
if ($score.val() == '-1') {
|
||||
downRate();
|
||||
} else if ($score.val() == '1') {
|
||||
upRate();
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
|
@ -33,6 +33,32 @@ $.fn.exists = function(callback, args) {
|
|||
};
|
||||
|
||||
|
||||
// Initializes character counters for textareas.
|
||||
function initCharCount() {
|
||||
var countChars = function(el, cc) {
|
||||
var $el = $(el),
|
||||
val = $el.val(),
|
||||
max = parseInt(cc.attr('data-maxlength'), 10),
|
||||
left = max - val.length;
|
||||
// L10n: {0} is the number of characters left.
|
||||
cc.html(format(ngettext('<b>{0}</b> character left.',
|
||||
'<b>{0}</b> characters left.', left), [left]))
|
||||
.toggleClass('error', left < 0);
|
||||
};
|
||||
$('.char-count').each(function() {
|
||||
var $cc = $(this),
|
||||
$form = $(this).closest('form'),
|
||||
$el;
|
||||
if ($cc.attr('data-for-startswith') !== undefined) {
|
||||
$el = $('textarea[id^="' + $cc.attr('data-for-startswith') + '"]:visible', $form);
|
||||
} else {
|
||||
$el = $('textarea#' + $cc.attr('data-for'), $form);
|
||||
}
|
||||
$el.bind('keyup blur', function() { countChars(this, $cc) }).trigger('blur');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
$('html').ajaxSuccess(function(event, xhr, ajaxSettings) {
|
||||
$(window).trigger('resize'); // Redraw what needs to be redrawn.
|
||||
});
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE ratings CHANGE COLUMN score score tinyint(1);
|
|
@ -212,6 +212,9 @@ JS = {
|
|||
'js/mkt/detail.js',
|
||||
'js/mkt/lightbox.js',
|
||||
|
||||
# Ratings.
|
||||
'js/mkt/ratings.js',
|
||||
|
||||
# Stick.
|
||||
'js/lib/stick.js',
|
||||
),
|
||||
|
|
|
@ -235,21 +235,43 @@
|
|||
|
||||
{% if waffle.switch('ratings') %}
|
||||
<section class="reviews c" id="reviews">
|
||||
{% if product.can_review(request.amo_user if
|
||||
request.user.is_authenticated() else None) %}
|
||||
<p id="submit-review">
|
||||
<a href="{{ product.get_ratings_url('add') }}" class="button good">
|
||||
{{ _('Review This App') }}</a></p>
|
||||
{% endif %}
|
||||
<div class="thumbs-up">
|
||||
<h3>Positive Reviews <b>(100)</b></h3>
|
||||
<ul>
|
||||
{% for x in range(5) %}
|
||||
{% include 'ratings/rating.html' %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<h3 class="upvotes">
|
||||
<span>Positive Reviews <b>({{ product._rating_counts.positive }})</b></span>
|
||||
</h3>
|
||||
{% if positive_ratings %}
|
||||
<ul>
|
||||
{% for rating in positive_ratings %}
|
||||
{% include 'ratings/rating.html' %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
{{ no_results() }}
|
||||
{% endif %}
|
||||
<p><a href="{{ product.get_ratings_url() }}" class="button">
|
||||
{{ _('Read More') }}</a></p>
|
||||
</div>
|
||||
<div class="thumbs-down">
|
||||
<h3>Negative Reviews <b>(95)</b></h3>
|
||||
<ul>
|
||||
{% for x in range(5) %}
|
||||
{% include 'ratings/rating.html' %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<h3 class="downvotes">
|
||||
<span>Negative Reviews <b>({{ product._rating_counts.negative }})</b></span>
|
||||
</h3>
|
||||
{% if negative_ratings %}
|
||||
<ul>
|
||||
{% for rating in negative_ratings %}
|
||||
{% include 'ratings/rating.html' %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
{{ no_results() }}
|
||||
{% endif %}
|
||||
<p><a href="{{ product.get_ratings_url() }}" class="button">
|
||||
{{ _('Read More') }}</a></p>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
|
|
@ -4,23 +4,31 @@ from tower import ugettext as _
|
|||
from django import http
|
||||
|
||||
from access import acl
|
||||
from addons.models import Addon
|
||||
from addons.decorators import addon_view_factory
|
||||
from amo.decorators import json_view, login_required, post_required, write
|
||||
from amo.utils import memoize_get
|
||||
from lib.metrics import send_request
|
||||
from lib.crypto.receipt import cef, SigningError
|
||||
from mkt.webapps.models import create_receipt, Installed
|
||||
|
||||
addon_view = addon_view_factory(qs=Addon.objects.valid)
|
||||
addon_enabled_view = addon_view_factory(qs=Addon.objects.enabled)
|
||||
addon_all_view = addon_view_factory(qs=Addon.objects.all)
|
||||
from mkt.ratings.models import Rating
|
||||
from mkt.webapps.models import create_receipt, Installed, Webapp
|
||||
|
||||
addon_view = addon_view_factory(qs=Webapp.objects.valid)
|
||||
addon_all_view = addon_view_factory(qs=Webapp.objects.all)
|
||||
|
||||
|
||||
@addon_all_view
|
||||
def detail(request, addon):
|
||||
"""Product details page."""
|
||||
return jingo.render(request, 'detail/app.html', {'product': addon})
|
||||
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', {
|
||||
'product': addon,
|
||||
'ratings': ratings,
|
||||
'positive_ratings': positive_ratings,
|
||||
'negative_ratings': negative_ratings,
|
||||
})
|
||||
|
||||
|
||||
@addon_all_view
|
||||
|
|
|
@ -127,6 +127,7 @@
|
|||
"pk": 1865,
|
||||
"model": "addons.addon",
|
||||
"fields": {
|
||||
"app_slug": "a1865",
|
||||
"slug": "a1865",
|
||||
"dev_agreement": true,
|
||||
"eula": null,
|
||||
|
@ -239,7 +240,6 @@
|
|||
"score": 4,
|
||||
"editorreview": false,
|
||||
"created": "2010-04-21 12:36:48",
|
||||
"title": 1043704,
|
||||
"modified": "2010-06-15 15:21:14",
|
||||
"flag": false,
|
||||
"addon": 1865,
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
from django import forms
|
||||
from django.forms.models import modelformset_factory
|
||||
|
||||
import happyforms
|
||||
from quieter_formset.formset import BaseModelFormSet
|
||||
from tower import ugettext_lazy as _lazy
|
||||
|
||||
import amo
|
||||
from amo.utils import raise_required
|
||||
import reviews
|
||||
|
||||
from .models import RatingFlag, Rating
|
||||
|
||||
|
||||
class RatingReplyForm(forms.Form):
|
||||
body = forms.CharField(max_length=150,
|
||||
widget=forms.Textarea(attrs={'rows': 2}))
|
||||
|
||||
def clean_body(self):
|
||||
body = self.cleaned_data.get('body', '')
|
||||
# Whitespace is not a review!
|
||||
if not body.strip():
|
||||
raise_required()
|
||||
return body
|
||||
|
||||
|
||||
class RatingForm(RatingReplyForm):
|
||||
score = forms.ChoiceField(choices=([1, _lazy('Thumbs Up')],
|
||||
[-1, _lazy('Thumbs Down')]))
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
super(RatingForm, self).__init__(*args, **kw)
|
||||
# Default to a blank value.
|
||||
if self.fields['score'].choices[0][0]:
|
||||
self.fields['score'].choices.insert(0, ('', ''))
|
||||
|
||||
|
||||
class RatingFlagForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = RatingFlag
|
||||
fields = ('flag', 'note', 'rating', 'user')
|
||||
|
||||
def clean(self):
|
||||
data = super(RatingFlagForm, self).clean()
|
||||
if data.get('note', '').strip():
|
||||
data['flag'] = RatingFlag.OTHER
|
||||
return data
|
||||
|
||||
|
||||
class BaseRatingFlagFormSet(BaseModelFormSet):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.form = ModerateRatingFlagForm
|
||||
super(BaseRatingFlagFormSet, self).__init__(*args, **kwargs)
|
||||
|
||||
def save(self):
|
||||
for form in self.forms:
|
||||
if form.cleaned_data:
|
||||
action = int(form.cleaned_data['action'])
|
||||
|
||||
is_flagged = form.instance.reviewflag_set.count() > 0
|
||||
|
||||
if action != reviews.REVIEW_MODERATE_SKIP: # Delete flags.
|
||||
for flag in form.instance.reviewflag_set.all():
|
||||
flag.delete()
|
||||
|
||||
rating = form.instance
|
||||
addon = rating.addon
|
||||
if action in (reviews.REVIEW_MODERATE_KEEP,
|
||||
reviews.REVIEW_MODERATE_KEEP):
|
||||
if action == reviews.REVIEW_MODERATE_DELETE:
|
||||
rating_addon, rating_id = rating.addon, rating.id
|
||||
log_action = amo.LOG.DELETE_REVIEW
|
||||
rating.delete()
|
||||
elif action == reviews.REVIEW_MODERATE_KEEP:
|
||||
rating_addon, rating_id = rating.addon, rating
|
||||
rating.update(editorreview=False)
|
||||
log_action = amo.LOG.APPROVE_REVIEW
|
||||
|
||||
amo.log(log_action, rating_addon, rating_id,
|
||||
details=dict(body=unicode(rating.body),
|
||||
addon_id=addon.id,
|
||||
addon_title=unicode(addon.name),
|
||||
is_flagged=is_flagged))
|
||||
|
||||
|
||||
class ModerateRatingFlagForm(happyforms.ModelForm):
|
||||
|
||||
action_choices = [
|
||||
(reviews.REVIEW_MODERATE_KEEP, _lazy('Keep review; remove flags')),
|
||||
(reviews.REVIEW_MODERATE_SKIP, _lazy('Skip for now')),
|
||||
(reviews.REVIEW_MODERATE_DELETE, _lazy('Delete review'))
|
||||
]
|
||||
action = forms.ChoiceField(choices=action_choices, required=False,
|
||||
initial=0, widget=forms.RadioSelect())
|
||||
|
||||
class Meta:
|
||||
model = Rating
|
||||
fields = ('action',)
|
||||
|
||||
|
||||
RatingFlagFormSet = modelformset_factory(Rating, extra=0,
|
||||
form=ModerateRatingFlagForm,
|
||||
formset=BaseRatingFlagFormSet)
|
|
@ -29,7 +29,7 @@ class Rating(amo.models.ModelBase):
|
|||
reply_to = models.ForeignKey('self', null=True, unique=True,
|
||||
related_name='replies', db_column='reply_to')
|
||||
|
||||
score = models.PositiveSmallIntegerField(null=True)
|
||||
score = models.IntegerField(null=True)
|
||||
body = TranslatedField(require_locale=False)
|
||||
ip_address = models.IPAddressField(default='0.0.0.0')
|
||||
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
{% from 'includes/forms.html' import required_note %}
|
||||
|
||||
{% extends 'detail/protected_app.html' %}
|
||||
|
||||
{% set title = _('Add a review for {0}')|f(product.name) %}
|
||||
{% block title %}{{ mkt_page_title(title) }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ mkt_breadcrumbs(product, [(product.get_ratings_url(), _('Reviews')),
|
||||
(None, _('Add'))]) }}
|
||||
<section id="submit-rating" class="friendly">
|
||||
<h1>{{ title }}</h1>
|
||||
<p>
|
||||
{% trans %}
|
||||
Please do not post bug reports in reviews. We do not make your email
|
||||
address available to app developers and they may need to contact
|
||||
you to help resolve your issue.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
{% if product.support_email or product.support_url %}
|
||||
<p>
|
||||
{% trans support=product.get_detail_url() + '#support' %}
|
||||
See the <a href="{{ support }}">support section</a> to find out
|
||||
where to get assistance for this app.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<p>
|
||||
{% trans %}
|
||||
Please keep reviews clean, avoid the use of improper language, and do
|
||||
not post any personal information.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
<form method="post">
|
||||
{{ csrf() }}
|
||||
{{ form_field(form.score, label=_('How would you rate this?'), tag='p') }}
|
||||
<div class="barometer c">
|
||||
<span id="thumbs-up" class="upvotes">{{ _('Good') }}</span>
|
||||
<span id="thumbs-down" class="downvotes">{{ _('Bad') }}</span>
|
||||
</div>
|
||||
{{ form_field(form.body, label=_('Your review'), hint=True,
|
||||
cc_for=form.body.auto_id,
|
||||
cc_maxlength=form.body.field.max_length, tag='p') }}
|
||||
{{ required_note() }}
|
||||
<p class="form-footer">
|
||||
<button type="submit">{{ _('Submit') }}</button> {{ _('or') }}
|
||||
<a href="{{ product.get_detail_url() }}">{{ _('Cancel') }}</a>
|
||||
</p>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -1,6 +1,11 @@
|
|||
<li class="review positive">
|
||||
<p>It Stinks!</p>
|
||||
<li class="review {{ 'positive' if rating.score == 1 else 'negative' }}">
|
||||
<p>{{ rating.body }}</p>
|
||||
<div class="byline">
|
||||
by <a href="#">Jay Sherman</a> on <span>January 26, 1994</span>
|
||||
{% trans user_url=rating.user.get_url_path(),
|
||||
user_name=rating.user.name,
|
||||
timestamp=rating.created|timelabel %}
|
||||
by <a href="{{ user_url }}">{{ user_name }}</a> {{ timestamp }}
|
||||
{% endtrans %}
|
||||
</div>
|
||||
<a href="{{ rating.get_url_path() }}" title="Permalink">Permalink</a>
|
||||
</li>
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
from nose.tools import eq_
|
||||
from pyquery import PyQuery as pq
|
||||
|
||||
import amo.tests
|
||||
import waffle
|
||||
|
||||
from users.models import UserProfile
|
||||
|
||||
from mkt.developers.models import ActivityLog
|
||||
from mkt.ratings.models import Rating
|
||||
from mkt.webapps.models import Webapp
|
||||
|
||||
|
||||
class ReviewTest(amo.tests.TestCase):
|
||||
fixtures = ['base/admin', 'base/apps', 'ratings/dev-reply']
|
||||
|
||||
def setUp(self):
|
||||
self.webapp = self.get_webapp()
|
||||
|
||||
def get_webapp(self):
|
||||
return Webapp.objects.get(id=1865)
|
||||
|
||||
def log_in_dev(self):
|
||||
self.client.login(username='trev@adblockplus.org', password='password')
|
||||
|
||||
def log_in_admin(self):
|
||||
self.client.login(username='jbalogh@mozilla.com', password='password')
|
||||
|
||||
def make_it_my_review(self, review_id=218468):
|
||||
r = Rating.objects.get(id=review_id)
|
||||
r.user = UserProfile.objects.get(username='jbalogh')
|
||||
r.save()
|
||||
|
||||
def enable_waffle(self):
|
||||
waffle.models.Switch.objects.create(name='ratings', active=True)
|
||||
|
||||
|
||||
class TestCreate(ReviewTest):
|
||||
|
||||
def setUp(self):
|
||||
super(TestCreate, self).setUp()
|
||||
self.add = self.webapp.get_ratings_url('add')
|
||||
self.user = UserProfile.objects.get(email='root_x@ukr.net')
|
||||
assert self.client.login(username=self.user.email, password='password')
|
||||
self.detail = self.webapp.get_detail_url()
|
||||
|
||||
def test_add_logged(self):
|
||||
r = self.client.get(self.add)
|
||||
eq_(r.status_code, 200)
|
||||
self.assertTemplateUsed(r, 'ratings/add.html')
|
||||
|
||||
def test_add_admin(self):
|
||||
self.log_in_admin()
|
||||
r = self.client.get(self.add)
|
||||
eq_(r.status_code, 200)
|
||||
|
||||
def test_add_dev(self):
|
||||
self.log_in_dev()
|
||||
r = self.client.get(self.add)
|
||||
eq_(r.status_code, 403)
|
||||
|
||||
def test_no_body(self):
|
||||
for body in ('', ' \t \n '):
|
||||
r = self.client.post(self.add, {'body': body})
|
||||
self.assertFormError(r, 'form', 'body', 'This field is required.')
|
||||
|
||||
def test_no_rating(self):
|
||||
r = self.client.post(self.add, {'body': 'no rating'})
|
||||
self.assertFormError(r, 'form', 'score', 'This field is required.')
|
||||
|
||||
def test_review_success(self):
|
||||
qs = Rating.objects.filter(addon=1865)
|
||||
old_cnt = qs.count()
|
||||
log_count = ActivityLog.objects.count()
|
||||
|
||||
r = self.client.post(self.add, {'body': 'xx', 'score': 1})
|
||||
self.assertRedirects(r, self.webapp.get_detail_url() + '#reviews',
|
||||
status_code=302)
|
||||
eq_(qs.count(), old_cnt + 1)
|
||||
eq_(ActivityLog.objects.count(), log_count + 1,
|
||||
'Expected ADD_REVIEW entry')
|
||||
eq_(self.get_webapp()._rating_counts, {'positive': 1, 'negative': 0})
|
||||
|
||||
def test_can_review_purchased(self):
|
||||
self.webapp.addonpurchase_set.create(user=self.user)
|
||||
self.webapp.update(premium_type=amo.ADDON_PREMIUM)
|
||||
data = {'body': 'x', 'score': 1}
|
||||
eq_(self.client.get(self.add, data).status_code, 200)
|
||||
r = self.client.post(self.add, data)
|
||||
self.assertRedirects(r, self.detail + '#reviews')
|
||||
|
||||
def test_not_review_purchased(self):
|
||||
self.webapp.update(premium_type=amo.ADDON_PREMIUM)
|
||||
data = {'body': 'x', 'score': 1}
|
||||
eq_(self.client.get(self.add, data).status_code, 403)
|
||||
eq_(self.client.post(self.add, data).status_code, 403)
|
||||
|
||||
def test_add_link_visitor(self):
|
||||
# Ensure non-logged user can see Add Review links on detail page
|
||||
# but not on Reviews listing page.
|
||||
self.enable_waffle()
|
||||
self.client.logout()
|
||||
r = self.client.get(self.detail)
|
||||
eq_(pq(r.content)('#submit-review').length, 1)
|
||||
|
||||
def test_add_link_logged(self):
|
||||
"""Ensure logged user can see Add Review links."""
|
||||
self.enable_waffle()
|
||||
r = self.client.get(self.detail)
|
||||
eq_(pq(r.content)('#submit-review').length, 1)
|
||||
|
||||
def test_add_link_dev(self):
|
||||
# Ensure developer cannot see Add Review links.
|
||||
self.enable_waffle()
|
||||
self.log_in_dev()
|
||||
r = self.client.get(self.detail)
|
||||
eq_(pq(r.content)('#submit-review').length, 0)
|
||||
|
||||
def test_premium_no_add_review_link_visitor(self):
|
||||
# Check for no review link for premium apps for non-logged user.
|
||||
self.enable_waffle()
|
||||
self.client.logout()
|
||||
self.webapp.update(premium_type=amo.ADDON_PREMIUM)
|
||||
r = self.client.get(self.detail)
|
||||
eq_(pq(r.content)('#submit-review').length, 0)
|
||||
|
||||
def test_premium_no_add_review_link_logged(self):
|
||||
self.enable_waffle()
|
||||
# Check for no review link for premium apps for logged users.
|
||||
self.webapp.update(premium_type=amo.ADDON_PREMIUM)
|
||||
r = self.client.get(self.detail)
|
||||
eq_(pq(r.content)('#submit-review').length, 0)
|
||||
|
||||
def test_premium_add_review_link_dev(self):
|
||||
# Check for no review link for premium apps for app owners.
|
||||
self.enable_waffle()
|
||||
self.log_in_dev()
|
||||
self.webapp.addonpurchase_set.create(user=self.user)
|
||||
self.webapp.update(premium_type=amo.ADDON_PREMIUM)
|
||||
r = self.client.get(self.detail)
|
||||
eq_(pq(r.content)('#submit-review').length, 0)
|
||||
|
||||
def test_premium_no_add_review_link(self):
|
||||
# Check for review link for non-purchased premium apps.
|
||||
self.enable_waffle()
|
||||
self.webapp.update(premium_type=amo.ADDON_PREMIUM)
|
||||
r = self.client.get(self.detail)
|
||||
eq_(pq(r.content)('#submit-review').length, 0)
|
||||
|
||||
def test_premium_add_review_link(self):
|
||||
# Check for review link for owners of purchased premium apps.
|
||||
self.enable_waffle()
|
||||
self.webapp.addonpurchase_set.create(user=self.user)
|
||||
self.webapp.update(premium_type=amo.ADDON_PREMIUM)
|
||||
r = self.client.get(self.detail)
|
||||
eq_(pq(r.content)('#submit-review').length, 1)
|
||||
|
||||
def test_no_reviews_premium_no_add_review_link(self):
|
||||
# Ensure no 'Review this App' link for non-purchased premium apps.
|
||||
self.enable_waffle()
|
||||
Rating.objects.all().delete()
|
||||
self.webapp.update(premium_type=amo.ADDON_PREMIUM)
|
||||
r = self.client.get(self.detail)
|
||||
eq_(pq(r.content)('#submit-review').length, 0)
|
||||
|
||||
def test_no_reviews_premium_add_review_link(self):
|
||||
# Ensure 'Review this App' link exists for purchased premium apps.
|
||||
self.enable_waffle()
|
||||
Rating.objects.all().delete()
|
||||
self.webapp.addonpurchase_set.create(user=self.user)
|
||||
self.webapp.update(premium_type=amo.ADDON_PREMIUM)
|
||||
r = self.client.get(self.detail)
|
||||
eq_(pq(r.content)('#submit-review').length, 1)
|
||||
|
||||
def test_add_logged_out(self):
|
||||
self.client.logout()
|
||||
r = self.client.get(self.add)
|
||||
self.assertLoginRedirects(r, self.add, 302)
|
|
@ -1,19 +1,34 @@
|
|||
from django import http
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
|
||||
import commonware.log
|
||||
import jingo
|
||||
from tower import ugettext as _
|
||||
|
||||
import amo
|
||||
import amo.log
|
||||
from addons.decorators import addon_view_factory, has_purchased
|
||||
from addons.models import Addon
|
||||
from amo.decorators import json_view, login_required, post_required
|
||||
from reviews.models import Review
|
||||
from reviews.helpers import user_can_delete_review
|
||||
|
||||
from mkt.site import messages
|
||||
from mkt.ratings.models import Rating
|
||||
|
||||
from . import forms
|
||||
|
||||
|
||||
log = commonware.log.getLogger('mkt.ratings')
|
||||
addon_view = addon_view_factory(qs=Addon.objects.valid)
|
||||
|
||||
|
||||
def _review_details(request, addon, form):
|
||||
d = dict(addon_id=addon.id, user_id=request.user.id,
|
||||
ip_address=request.META.get('REMOTE_ADDR', ''))
|
||||
d.update(**form.cleaned_data)
|
||||
return d
|
||||
|
||||
|
||||
@addon_view
|
||||
def review_list(request, addon, review_id=None, user_id=None):
|
||||
return http.HttpResponse()
|
||||
|
@ -31,7 +46,7 @@ def flag(request, addon, review_id):
|
|||
@post_required
|
||||
@login_required(redirect=False)
|
||||
def delete(request, addon, review_id):
|
||||
review = get_object_or_404(Review.objects, pk=review_id, addon=addon)
|
||||
review = get_object_or_404(Rating.objects, pk=review_id, addon=addon)
|
||||
if not user_can_delete_review(request, review):
|
||||
return http.HttpResponseForbidden()
|
||||
return http.HttpResponse()
|
||||
|
@ -49,4 +64,20 @@ def edit(request, addon, review_id):
|
|||
@login_required
|
||||
@has_purchased
|
||||
def add(request, addon):
|
||||
return http.HttpResponse()
|
||||
if addon.has_author(request.user):
|
||||
# Don't let app owners review their own apps.
|
||||
return http.HttpResponseForbidden()
|
||||
|
||||
data = request.POST or None
|
||||
form = forms.RatingForm(data)
|
||||
if data and form.is_valid():
|
||||
rating = Rating.objects.create(**_review_details(request, addon, form))
|
||||
amo.log(amo.LOG.ADD_REVIEW, addon, rating)
|
||||
log.debug('New rating: %s' % rating.id)
|
||||
messages.success(request, _('Your review was successfully added!'))
|
||||
return redirect(addon.get_detail_url() + '#reviews')
|
||||
# TODO: When rating list is done uncomment this (bug 755954).
|
||||
#return redirect(addon.get_ratings_url('list'))
|
||||
|
||||
return jingo.render(request, 'ratings/add.html',
|
||||
{'product': addon, 'form': form})
|
||||
|
|
|
@ -77,7 +77,7 @@ def product_as_dict(request, product, purchased=None):
|
|||
'author_url': author_url,
|
||||
'iconUrl': product.get_icon_url(64)
|
||||
}
|
||||
if product.is_premium():
|
||||
if product.is_premium() and product.premium:
|
||||
ret.update({
|
||||
'price': product.premium.get_price() or '0',
|
||||
'priceLocale': product.premium.get_price_locale(),
|
||||
|
@ -134,7 +134,8 @@ def mkt_breadcrumbs(context, product=None, items=None, crumb_size=40,
|
|||
else:
|
||||
# The Product is the end of the trail.
|
||||
url_ = None
|
||||
crumbs.append((url_, product.name))
|
||||
crumbs += [(reverse('browse.apps'), _('Apps')),
|
||||
(url_, product.name)]
|
||||
if items:
|
||||
crumbs.extend(items)
|
||||
|
||||
|
@ -149,10 +150,10 @@ def mkt_breadcrumbs(context, product=None, items=None, crumb_size=40,
|
|||
|
||||
@register.function
|
||||
def form_field(field, label=None, tag='div', req=None, opt=False, hint=False,
|
||||
some_html=False, cc_startswith=None, cc_maxlength=None,
|
||||
grid=False, **attrs):
|
||||
some_html=False, cc_startswith=None, cc_for=None,
|
||||
cc_maxlength=None, grid=False, **attrs):
|
||||
c = dict(field=field, label=label, tag=tag, req=req, opt=opt, hint=hint,
|
||||
some_html=some_html, cc_startswith=cc_startswith,
|
||||
some_html=some_html, cc_startswith=cc_startswith, cc_for=cc_for,
|
||||
cc_maxlength=cc_maxlength, grid=grid, attrs=attrs)
|
||||
t = env.get_template('site/helpers/simple_field.html').render(**c)
|
||||
return jinja2.Markup(t)
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
{% if hint and tooltip %}
|
||||
<span class="hint">{{ field.help_text }}</span>
|
||||
{% endif %}
|
||||
{{ field.errors }}
|
||||
|
||||
{% if some_html %}
|
||||
{{ some_html_tip() }}
|
||||
{% endif %}
|
||||
|
@ -51,9 +51,14 @@
|
|||
<div class="char-count" data-maxlength="{{ cc_maxlength }}"
|
||||
{% if cc_startswith %}
|
||||
data-for-startswith="{{ cc_startswith }}"
|
||||
{% endif %}
|
||||
{% if cc_for %}
|
||||
data-for="{{ cc_for }}"
|
||||
{% endif %}></div>
|
||||
{% endif %}
|
||||
|
||||
{{ field.errors }}
|
||||
|
||||
{% if grid %}</div>{% endif %}
|
||||
{% if tag %}
|
||||
</{{ tag }}>
|
||||
|
|
|
@ -117,6 +117,14 @@ class Webapp(Addon):
|
|||
apps_dict[adt.addon_id]._device_types = []
|
||||
apps_dict[adt.addon_id]._device_types.append(adt.device_type)
|
||||
|
||||
# TODO: This may need to be its own column.
|
||||
for app in apps:
|
||||
#if not hasattr(apps_dict[adt.addon_id], '_rating_counts'):
|
||||
scores = dict(app._ratings.values_list('score')
|
||||
.annotate(models.Count('id')))
|
||||
app._rating_counts = {'positive': scores.get(1, 0),
|
||||
'negative': scores.get(-1, 0)}
|
||||
|
||||
def get_url_path(self, more=False, add_prefix=True):
|
||||
# We won't have to do this when Marketplace absorbs all apps views,
|
||||
# but for now pretend you didn't see this.
|
||||
|
@ -147,8 +155,8 @@ class Webapp(Addon):
|
|||
return reverse(view_name % (prefix, action),
|
||||
args=[self.app_slug] + args)
|
||||
|
||||
def get_ratings_url(self, action, args=None, add_prefix=True):
|
||||
"""Reverse URLs for 'ratings.add', 'ratings.detail', etc."""
|
||||
def get_ratings_url(self, action='list', args=None, add_prefix=True):
|
||||
"""Reverse URLs for 'ratings.list', 'ratings.add', etc."""
|
||||
return reverse(('ratings.%s' % action),
|
||||
args=[self.app_slug] + (args or []),
|
||||
add_prefix=add_prefix)
|
||||
|
|
Загрузка…
Ссылка в новой задаче