allow people to submit app ratings (bug 755567)

This commit is contained in:
Chris Van 2012-05-16 21:44:46 -07:00
Родитель 931f1781df
Коммит a605dd61e5
21 изменённых файлов: 648 добавлений и 44 удалений

Просмотреть файл

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

44
media/js/mkt/ratings.js Normal file
Просмотреть файл

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

105
mkt/ratings/forms.py Normal file
Просмотреть файл

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