implement category browse pages (bug 735578)

This commit is contained in:
Chris Van 2012-04-20 15:34:10 -07:00
Родитель dc19764061
Коммит 79168daf03
37 изменённых файлов: 757 добавлений и 277 удалений

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

@ -459,7 +459,8 @@ class ESTestCase(TestCase):
Application)
for model in models:
model.objects.all().delete()
for index in settings.ES_INDEXES.values():
cls.es.delete_index_if_exists(index)
super(ESTestCase, cls).tearDownClass()
@classmethod

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

@ -7,6 +7,7 @@
.gradient-two-color(rgba(176,186,192, 0.33), rgba(187,195,199, 0.33));
color: @text;
margin-bottom: 24px;
.light-text-shadow;
padding-top: 3px;
width: 100%;
ol {
@ -27,14 +28,14 @@
padding: 5px 0;
&.home {
&:before {
background: url(../../img/mkt/icons/home_crumb.png) no-repeat;
background: url(../../img/mkt/icons/home_crumb.png) 0 2px no-repeat;
content: "";
display: inline-block;
opacity: .8;
position: relative;
text-decoration: none;
top: 1px;
height: 14px;
height: 16px;
width: 25px;
}
&:hover {

26
media/css/mkt/browse.less Normal file
Просмотреть файл

@ -0,0 +1,26 @@
@import 'lib';
#breadcrumbs.dark {
.box-shadow(inset 0 1px 2px rgba(0,0,0,.15));
.grain;
background-color: darken(#404f5a,3%);
border-top: darken(#404f5a, 5%) 1px solid;
border-bottom: darken(@bg, 30%) 1px solid;
margin-bottom: 0;
color: @white;
text-shadow: 0 1px 0 rgba(0,0,0,.5);
li + li:before {
color: @note-gray;
}
a {
color: @link-bright;
&.home:before {
background-position: 0 -14px;
opacity: .9;
}
}
}
#browse-featured {
padding-top: 24px;
}

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

@ -173,7 +173,6 @@ h1 .num {
h1 {
font-size: 32px;
font-weight: 100;
line-height: 27px;
margin: 0 0 0 -1px;
}
.authors {

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

@ -67,129 +67,3 @@
background-position: 150% 58px;
}
}
.featured, .popular {
&.slider {
height: 282px;
}
}
.featured {
background: #60747F;
.grain;
color: @white;
&.slider {
border-bottom: 1px solid #666;
}
a {
color: lighten(@text, 60%);
&:hover {
color: @white;
}
}
}
.popular {
h2 {
padding-top: 10px;
}
a {
color: @text;
&:hover {
color: darken(@text, 15%);
}
}
}
.popular, .categories {
h2 {
padding-top: 10px;
}
}
.categories {
background: @pale-bg;
.controls {
.next-page, .prev-page {
height: 162px;
top: 12px;
}
}
a {
color: @medium-gray;
&:hover {
color: @dark-gray;
}
}
.promo-slider {
li, li a, img {
height: 120px;
width: 120px;
}
li {
h3 {
margin: 0;
text-align: center;
width: 100%;
}
a {
.box-shadow(~'0 1px 2px 0 rgba(0,0,0,.3), 0 2px 1px 0 rgba(0,0,0,0.1), 0 0 0 1px rgba(0,0,0,.05), 0 0 12px 0 rgba(0,0,0,.05), 0 0 12px 0 rgba(0,0,0,.05) inset');
.transition-duration(0.1s);
.transition-property(~'background-color,'
'-moz-box-shadow, -webkit-box-shadow,'
'box-shadow, line-height');
background-color: #D9DDE0;
display: block;
line-height: 120px;
margin: 0 auto;
text-align: center;
&:active {
background-color: rgba(0,0,0,0.1);
.box-shadow(~'0 2px 0 0 rgba(0,0,0,.2) inset, 0 12px 24px 6px rgba(0,0,0,.2) inset, 0 2px 2px 2px rgba(0,0,0,.2) inset');
line-height: 124px;
}
&:hover {
.box-shadow(~'0 1px 2px 0 rgba(0,0,0,.3), 0 4px 1px 0 rgba(0,0,0,.1), 0 0 0 1px rgba(0,0,0,.05), 0 0 12px 0 rgba(0,0,0,.05), 0 0 48px 0 @white inset, 0 -3px 0 0 rgba(0,0,0,.15) inset');
line-height: 118px;
}
}
}
img {
background: url(../../img/mkt/glyphs/rocket.png) 50% 50% no-repeat;
}
}
.promo-slider ul a:after {
bottom: 5px;
right: 5px;
}
}
.arrow.button {
.border-box;
.width(3);
color: lighten(@text, 60%);
font-size: 18px;
line-height: 35px;
position: relative;
text-align: left;
&:after {
background: url(../../img/mkt/arrows/plain-lrg-go.png) 0 0 no-repeat;
content: "";
display: block;
height: 19px;
opacity: .7;
position: absolute;
top: 8px;
right: 15px;
width: 18px;
}
&:hover {
color: @white;
&:after {
opacity: 1;
}
}
}
.html-rtl .arrow.button {
text-align: right;
}

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

@ -22,6 +22,7 @@
// Colors
@link: #2d87ca;
@link-bright: #5be;
@faded-link: fadeOut(@link, 50%);
@shadow-blue: #98B2C9;
@border-blue: #C9DDF2;
@ -64,6 +65,10 @@
background: url(../../img/mkt/arrows/plain.png) no-repeat;
}
.light-text-shadow() {
text-shadow: 0 1px 0 rgba(255,255,255,.5);
}
.border-radius(@radius) {
-webkit-border-radius: @radius;
-moz-border-radius: @radius;

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

@ -0,0 +1,69 @@
@import 'lib';
.promo-grid, .slider {
h3 {
font-size: 18px;
line-height: 22px;
}
}
.promo-grid {
ul {
list-style: none;
padding: 0;
}
li {
.border-box;
.width(4.5);
float: left;
margin-bottom: 42px;
margin-right: 42px;
&:nth-of-type(3n) {
margin-right: 0;
}
}
a {
.light-text-shadow;
background: fadeOut(@light-gray, 50%);
display: block;
height: 100px;
padding: 10px 10px 10px 90px;
position: relative;
text-decoration: none;
&:hover {
background: @faint-gray;
.box-shadow(0 1px 2px fadeOut(@black, 50%));
}
&:active {
background: @light-gray;
.box-shadow(inset 0 2px 0 0 rgba(0,0,0,.2),
inset 0 12px 24px 6px rgba(0,0,0,.2),
inset 0 0 2px 2px rgba(0,0,0,.2));
}
}
img {
background: @white;
.box-shadow(0 1px 2px fadeOut(@medium-gray, 25%));
position: absolute;
top: 10px;
left: 10px;
}
.author {
.ellipsis;
display: block;
margin: 5px 0;
font-style: normal;
}
.price {
font-size: 15px;
line-height: 17px;
font-weight: bold;
margin: 0;
}
}
.html-rtl .promo-grid {
a {
padding: 10px 90px 10px 10px;
}
}

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

@ -7,7 +7,7 @@
}
#sorter {
margin-bottom: 1em;
margin: 10px 0 15px;
ul {
padding: 0;
list-style: none;
@ -288,7 +288,7 @@
}
h2 {
line-height: 50px;
margin-bottom: 16px;
margin-bottom: 10px;
min-height: 29px;
padding: 0;
text-transform: inherit;

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

@ -6,6 +6,7 @@
overflow-x: hidden;
overflow-y: hidden;
padding: 0;
min-height: 20px;
max-width: 100%;
-webkit-overflow-scrolling: touch;
.content {
@ -28,7 +29,7 @@
opacity: 0;
visibility: hidden;
width: 48px;
height: 240px;
height: 220px;
left: 0;
background: fadeOut(@white, 80%);
z-index: 1;
@ -79,7 +80,7 @@
}
.promo-slider {
height: 200px;
height: 245px;
ul {
white-space: nowrap;
padding: 0;
@ -124,6 +125,7 @@
h3 {
.ellipsis;
float: left;
font-size: 16px;
max-width: 190px;
}
p {
@ -132,3 +134,136 @@
}
}
}
#home-featured, #home-popular {
.controls {
.next-page, .prev-page {
height: 240px;
}
}
.promo-slider {
height: 282px;
}
}
.featured {
background: #60747F;
.grain;
color: @white;
&.slider {
border-bottom: 1px solid #666;
}
a {
color: lighten(@text, 60%);
&:hover {
color: @white;
}
}
}
.popular {
h2 {
padding-top: 10px;
}
a {
color: @text;
&:hover {
color: darken(@text, 15%);
}
}
}
.popular, .categories {
h2 {
padding-top: 10px;
}
}
.categories {
background: @pale-bg;
.controls {
.next-page, .prev-page {
height: 162px;
top: 12px;
}
}
a {
color: @medium-gray;
&:hover {
color: @dark-gray;
}
}
.promo-slider {
height: 175px;
li, li a, img {
height: 120px;
width: 120px;
}
li {
h3 {
margin: 0;
text-align: center;
width: 100%;
}
a {
.box-shadow(~'0 1px 2px 0 rgba(0,0,0,.3), 0 2px 1px 0 rgba(0,0,0,0.1), 0 0 0 1px rgba(0,0,0,.05), 0 0 12px 0 rgba(0,0,0,.05), 0 0 12px 0 rgba(0,0,0,.05) inset');
.transition-duration(0.1s);
.transition-property(~'background-color,'
'-moz-box-shadow, -webkit-box-shadow,'
'box-shadow, line-height');
background-color: #D9DDE0;
display: block;
line-height: 120px;
margin: 0 auto;
text-align: center;
&:hover {
.box-shadow(~'0 1px 2px 0 rgba(0,0,0,.3), 0 4px 1px 0 rgba(0,0,0,.1), 0 0 0 1px rgba(0,0,0,.05), 0 0 12px 0 rgba(0,0,0,.05), 0 0 48px 0 @white inset, 0 -3px 0 0 rgba(0,0,0,.15) inset');
line-height: 118px;
}
&:active {
background-color: rgba(0,0,0,0.1);
.box-shadow(~'0 2px 0 0 rgba(0,0,0,.2) inset, 0 12px 24px 6px rgba(0,0,0,.2) inset, 0 2px 2px 2px rgba(0,0,0,.2) inset');
line-height: 124px;
}
}
}
img {
background: url(../../img/mkt/glyphs/rocket.png) 50% 50% no-repeat;
}
}
.promo-slider ul a:after {
bottom: 5px;
right: 5px;
}
}
.arrow.button {
.border-box;
.width(3);
color: lighten(@text, 60%);
font-size: 18px;
line-height: 35px;
position: relative;
text-align: left;
&:after {
background: url(../../img/mkt/arrows/plain-lrg-go.png) 0 0 no-repeat;
content: "";
display: block;
height: 19px;
opacity: .7;
position: absolute;
top: 8px;
right: 15px;
width: 18px;
}
&:hover {
color: @white;
&:after {
opacity: 1;
}
}
}
.html-rtl .arrow.button {
text-align: right;
}

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

@ -33,8 +33,29 @@ h1, h2, h3, h4 {
font-weight: 300;
}
h1 {
font-size: 36px;
line-height: 130%;
letter-spacing: -1px;
}
h2 {
font-size: 28px;
line-height: 120%;
letter-spacing: -.5px;
}
.full h2 {
line-height: 38px;
}
h3 {
font-size: 24px;
letter-spacing: -.25px;
}
.listing h3 {
font-size: 18px;
}
b, strong {

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

@ -15,8 +15,7 @@ function registerAddonAutocomplete(node) {
select: function(event, ui) {
$(node).val(ui.item.name).attr('data-id', ui.item.id);
var current = template(
'<a href="{url}" target="_blank" ' +
'class="collectionitem"><img src="{icon}">{name}</a>');
'<a href="{url}" target="_blank"><img src="{icon}"> {name}</a>');
$td.find('.current-webapp').show().html(current({
url: ui.item.url,
icon: ui.item.icon,
@ -29,12 +28,12 @@ function registerAddonAutocomplete(node) {
}
}).data('autocomplete')._renderItem = function(ul, item) {
var html = format('<a>{0}<b>ID: {1}</b></a>', [item.name, item.id]);
return $('<li>').data('item.autocomplete', item).append(html).appendTo(ul);
};
return $('<li>').data('item.autocomplete', item).append(html).appendTo(ul);
};
}
function newAddonSlot(id) {
var $tbody = $("#" + id + "-webapps")
var $tbody = $("#" + id + "-webapps");
var $form = $tbody.next().children("tr").clone();
var $input = $form.find('input.placeholder');
registerAddonAutocomplete($input);
@ -44,9 +43,9 @@ function newAddonSlot(id) {
}
$(document).ready(function(){
$("#home-webapps, #featured-webapps").delegate(
$("#home-webapps, #category-webapps").delegate(
'.remove', 'click', _pd(function() {$(this).closest('tr').remove();}));
$('#home-add').click(_pd(function() { newAddonSlot("home"); }));
$('#featured-add').click(_pd(function() { newAddonSlot("featured"); }));
});
$('#category-add').click(_pd(function() { newAddonSlot("category"); }));
});

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

@ -81,6 +81,7 @@ z.page.on('fragmentloaded', function() {
}
function initSliders() {
$('.promo-grid h3').lineclamp(2);
$('.slider').each(function() {
var currentPage,
$this = $(this),

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

@ -75,19 +75,28 @@ $.fn.lineclamp = function(lines) {
lh = $this.css('line-height');
if (lh.substr(-2) == 'px') {
lh = parseFloat(lh.replace('px', ''));
$this.css({'max-height': Math.ceil(lh) * lines,
'overflow': 'hidden',
'text-overflow': 'ellipsis'});
var maxHeight = Math.ceil(lh) * lines,
truncated;
if ((this.scrollHeight - maxHeight) > 2) {
$this.css({'height': maxHeight + 2, 'overflow': 'hidden',
'text-overflow': 'ellipsis'});
// Add an ellipsis.
$this.truncate({dir: 'v'});
} else {
$this.css({'max-height': maxHeight, 'overflow': 'hidden',
'text-overflow': 'ellipsis'});
}
}
});
};
$.fn.linefit = function() {
$.fn.linefit = function(lines) {
// This function shrinks text to fit on one line.
var min_font_size = 7;
lines = lines || 1;
return this.each(function() {
var $this = $(this),
fs = parseFloat($this.css('font-size').replace('px', '')),
max_height = Math.ceil(parseFloat($this.css('line-height').replace('px', ''))),
max_height = Math.ceil(parseFloat($this.css('line-height').replace('px', ''))) * lines,
height = $this.height();
while (height > max_height && fs > min_font_size) {
// Repeatedly shrink the text by 0.5px until all the text fits.

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

@ -0,0 +1,15 @@
from bandwagon.models import Collection
def run():
# Rename collection for homepage-featured apps.
home = Collection.objects.get(author__username='mozilla',
slug='webapps_home')
home.slug = 'featured_apps_home'
home.save()
# Rename collection for category-featured apps.
cat = Collection.objects.get(author__username='mozilla',
slug='webapps_featured')
cat.slug = 'featured_apps_category'
cat.save()

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

@ -80,6 +80,7 @@ CSS = {
'css/mkt/buttons.less',
'css/mkt/detail.less',
'css/mkt/slider.less',
'css/mkt/promo-grid.less',
'css/mkt/overlay.less',
'css/mkt/search.less',
'css/mkt/paginator.less',
@ -93,6 +94,7 @@ CSS = {
'css/devreg/l10n.less',
'css/impala/lightbox.less',
'css/mkt/lightbox.less',
'css/mkt/browse.less',
),
'mkt/in-app-payments': (
'css/mkt/reset.less',

31
mkt/browse/helpers.py Normal file
Просмотреть файл

@ -0,0 +1,31 @@
import caching.base as caching
from jingo import env, register
import jinja2
import amo
from addons.models import Category
@register.function
def category_slider():
return caching.cached(lambda: _categories(), 'category-slider-apps')
def _categories():
categories = Category.objects.filter(type=amo.ADDON_WEBAPP, weight__gte=0)
t = env.get_template('browse/helpers/category_slider.html')
return jinja2.Markup(t.render(categories=categories))
@register.filter
@jinja2.contextfilter
def promo_grid(context, products):
t = env.get_template('browse/helpers/promo_grid.html')
return jinja2.Markup(t.render(products=products))
@register.filter
@jinja2.contextfilter
def promo_slider(context, products, feature=False):
t = env.get_template('browse/helpers/promo_slider.html')
return jinja2.Markup(t.render(products=products, feature=feature))

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

@ -0,0 +1,21 @@
<section id="categories" class="categories full">
<div><h2>{{ _('All categories') }}</h2></div>
</section>
<section class="categories slider full">
<div class="promo-slider">
<div class="controls">
<a href="#" class="prev-page"></a>
<a href="#" class="next-page"></a>
</div>
<ul class="content">
{% for category in categories %}
<li>
<a href="{{ category.get_url_path() }}">
<img src="">
<h3>{{ category.name }}</h3>
</a>
</li>
{% endfor %}
</ul>
</div>
</section>

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

@ -0,0 +1,18 @@
{% if products %}
<div class="promo-grid">
<ul class="content">
{% for product in products %}
<li>
<a href="{{ product.get_url_path() }}">
<img src="{{ product.get_icon_url(64) }}">
<h3>{{ product.name }}</h3>
{% if product.listed_authors %}
<em class="author">{{ product.listed_authors[0].name }}</em>
{% endif %}
<p class="price">{{ product|price_label }}</p>
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}

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

@ -0,0 +1,27 @@
{% if products %}
<div class="promo-slider">
<div class="controls">
<a href="#" class="prev-page"></a>
<a href="#" class="next-page"></a>
</div>
<ul class="content">
{% for product in products %}
{% if product.previews.all() %}
<li>
<a href="{{ product.get_url_path() }}">
{% if feature %}
<img src="{{ product.previews.order_by('-created')[0].image_url }}">
{% else %}
<img src="{{ product.previews.order_by('-created')[0].thumbnail_url }}">
{% endif %}
<h3>{{ product.name }}</h3>
<p>
{{ product|price_label }}
</p>
</a>
</li>
{% endif %}
{% endfor %}
</ul>
</div>
{% endif %}

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

@ -0,0 +1,37 @@
{% extends 'mkt/base.html' %}
{% if category %}
{% set title = category.name %}
{% set heading = title %}
{% set crumbs = [(url('browse.apps'), _('Apps')),
(None, title)] %}
{% else %}
{% set title = _('Apps') %}
{% set heading = title %}
{% set crumbs = [(None, title)] %}
{% endif %}
{% block title %}{{ mkt_page_title(title) }}{% endblock %}
{% block content %}
{{ mkt_breadcrumbs(product, crumbs, cls='dark') }}
<section id="browse-featured" class="featured full">
<div>
<h2>{{ title if category else _('Featured') }}</h2>
</div>
</section>
<section class="featured full slider">
{{ featured|promo_slider(feature=True) }}
</section>
<section class="popular full">
<div>
<h2><a href="{{ url('browse.apps')|urlparams(category=category.slug or None,
sort='popular') }}">
{{ _('By popularity') }}</a></h2>
</div>
</section>
<section class="popular grid full">
{{ popular|promo_grid }}
</section>
{{ category_slider() }}
{% endblock %}

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

@ -1,11 +1,16 @@
from nose import SkipTest
from nose.tools import eq_
from pyquery import PyQuery as pq
import waffle
import amo
import amo.tests
from amo.urlresolvers import reverse
from amo.utils import urlparams
from addons.models import AddonCategory, Category
from bandwagon.models import Collection, CollectionAddon
from users.models import UserProfile
from mkt.webapps.models import Webapp
@ -21,18 +26,120 @@ class BrowseBase(amo.tests.ESTestCase):
self.webapp.save()
self.refresh()
def get_pks(self, key, url, data={}):
r = self.client.get(url, data)
eq_(r.status_code, 200)
return sorted(x.id for x in r.context[key])
class TestIndex(BrowseBase):
def make_featured(self, webapp, group):
a, yay = UserProfile.objects.get_or_create(username='mozilla')
c, yay = Collection.objects.get_or_create(author=a,
slug='featured_apps_%s' % group, type=amo.COLLECTION_FEATURED)
CollectionAddon.objects.create(collection=c, addon=webapp)
def setup_featured(self):
waffle.models.Switch.objects.get_or_create(name='unleash-consumer',
active=True)
amo.tests.addon_factory()
# Category featured.
a = amo.tests.app_factory()
self.make_featured(webapp=a, group='category')
AddonCategory.objects.create(addon=a, category=self.cat)
b = amo.tests.app_factory()
self.make_featured(webapp=b, group='category')
AddonCategory.objects.create(addon=b, category=self.cat)
# Home featured.
c = amo.tests.app_factory()
self.make_featured(webapp=c, group='home')
AddonCategory.objects.create(addon=c, category=self.cat)
return a, b, c
def setup_popular(self):
# TODO: Figure out why ES flakes out on every other test run!
# (I'm starting to think the "elastic" in elasticsearch is symbolic
# of how our problems keep bouncing back. I thought elastic had more
# potential. Maybe it's too young? I play with an elastic instrument;
# would you like to join my rubber band? [P.S. If you can help in any
# way, pun-wise or code-wise, please don't hesitate to do so.] In the
# meantime, SkipTest is the rubber band to our elastic problems.)
raise SkipTest
waffle.models.Switch.objects.get_or_create(name='unleash-consumer',
active=True)
amo.tests.addon_factory()
# Popular without a category.
a = amo.tests.app_factory()
self.refresh()
# Popular for this category.
b = amo.tests.app_factory()
AddonCategory.objects.create(addon=b, category=self.cat)
b.save()
# Popular and category featured and home featured.
self.make_featured(webapp=self.webapp, group='category')
self.make_featured(webapp=self.webapp, group='home')
self.webapp.save()
# Something's really up.
self.refresh()
return a, b
def _test_popular(self, url, pks):
r = self.client.get(self.url)
eq_(r.status_code, 200)
results = r.context['popular']
# Test correct apps.
eq_(sorted(r.id for r in results), sorted(pks))
# Test sort order.
expected = sorted(results, key=lambda x: x.weekly_downloads,
reverse=True)
eq_(list(results), expected)
class TestIndexLanding(BrowseBase):
def setUp(self):
super(TestIndex, self).setUp()
super(TestIndexLanding, self).setUp()
self.url = reverse('browse.apps')
def test_good_cat(self):
r = self.client.get(self.url)
eq_(r.status_code, 200)
self.assertTemplateUsed(r, 'browse/landing.html')
def test_featured(self):
a, b, c = self.setup_featured()
# Check that these apps are featured on the category landing page.
eq_(self.get_pks('featured', self.url), sorted([a.id, b.id]))
def test_popular(self):
a, b = self.setup_popular()
# Check that these apps are shown on the category landing page.
self._test_popular(self.url, [self.webapp.id, a.id, b.id])
class TestIndexSearch(BrowseBase):
def setUp(self):
super(TestIndexSearch, self).setUp()
self.url = reverse('browse.apps') + '?sort=downloads'
def test_page(self):
r = self.client.get(self.url)
eq_(r.status_code, 200)
self.assertTemplateUsed(r, 'search/results.html')
eq_(pq(r.content)('#page h1').text(), 'Apps')
eq_(pq(r.content)('#page h1').text(), 'By Popularity')
def test_good_sort_option(self):
for sort in ('downloads', 'rating', 'price', 'created'):
@ -46,30 +153,78 @@ class TestIndex(BrowseBase):
def test_sorter(self):
r = self.client.get(self.url)
li = pq(r.content)('#sorter li:eq(0)')
eq_(li.attr('class'), None)
eq_(li.filter('.selected').length, 1)
eq_(li.find('a').attr('href'),
urlparams(reverse('browse.apps'), sort='downloads'))
class TestCategories(BrowseBase):
class TestCategoryLanding(BrowseBase):
def setUp(self):
super(TestCategories, self).setUp()
super(TestCategoryLanding, self).setUp()
self.url = reverse('browse.apps', args=[self.cat.slug])
def get_new_cat(self):
return Category.objects.create(name='Slap Tickling', slug='booping',
type=amo.ADDON_WEBAPP)
def test_good_cat(self):
r = self.client.get(self.url)
eq_(r.status_code, 200)
self.assertTemplateUsed(r, 'browse/landing.html')
def test_bad_cat(self):
r = self.client.get(reverse('browse.apps', args=['xxx']))
eq_(r.status_code, 404)
def test_featured(self):
a, b, c = self.setup_featured()
# Check that these apps are featured for this category.
eq_(self.get_pks('featured', self.url), sorted([a.id, b.id]))
# Check that these apps are not featured for another category.
new_cat_url = reverse('browse.apps', args=[self.get_new_cat().slug])
eq_(self.get_pks('featured', new_cat_url), [])
def test_popular(self):
a, b = self.setup_popular()
# Check that these apps are shown for this category.
self._test_popular(self.url, [self.webapp.id, b.id])
# Check that these apps are not shown for another category.
new_cat_url = reverse('browse.apps', args=[self.get_new_cat().slug])
eq_(self.get_pks('popular', new_cat_url), [])
def test_search_category(self):
# Ensure category got set in the search form.
r = self.client.get(self.url)
eq_(pq(r.content)('#search input[name=cat]').val(), str(self.cat.id))
class TestCategorySearch(BrowseBase):
def setUp(self):
super(TestCategorySearch, self).setUp()
self.url = reverse('browse.apps',
args=[self.cat.slug]) + '?sort=downloads'
def test_good_cat(self):
r = self.client.get(self.url)
eq_(r.status_code, 200)
self.assertTemplateUsed(r, 'search/results.html')
def test_bad_cat(self):
r = self.client.get(reverse('browse.apps', args=['xxx']))
r = self.client.get(reverse('browse.apps', args=['xxx']),
{'sort': 'downloads'})
eq_(r.status_code, 404)
def test_non_indexed_cat(self):
new_cat = Category.objects.create(name='Slap Tickling', slug='booping',
type=amo.ADDON_WEBAPP)
r = self.client.get(reverse('browse.apps', args=[new_cat.slug]))
r = self.client.get(reverse('browse.apps', args=[new_cat.slug]),
{'sort': 'downloads'})
# If the category has no indexed apps, we redirect to main search page.
self.assertRedirects(r, reverse('search.search'))
@ -84,7 +239,7 @@ class TestCategories(BrowseBase):
def test_sorter(self):
r = self.client.get(self.url)
li = pq(r.content)('#sorter li:eq(0)')
eq_(li.attr('class'), 'selected')
eq_(li.filter('.selected').length, 1)
eq_(li.find('a').attr('href'),
urlparams(reverse('search.search'), cat=self.cat.id,
sort='downloads'))

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

@ -4,6 +4,5 @@ from . import views
urlpatterns = patterns('',
url('^(?P<category>[^ /]+)?$', views.categories_apps,
name='browse.apps'),
url('^(?P<category>[^ /]+)?$', views.browse_apps, name='browse.apps'),
)

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

@ -4,14 +4,34 @@ from django.shortcuts import get_object_or_404, redirect
import amo
from addons.models import Category
from mkt.search.views import _app_search
from mkt.webapps.models import Webapp
def categories_apps(request, category=None):
def _landing(request, category=None):
featured = Webapp.featured('category')
popular = Webapp.popular()
if category:
category = get_object_or_404(
Category.objects.filter(type=amo.ADDON_WEBAPP, weight__gte=0),
slug=category)
featured = featured.filter(category=category)[:6]
popular = popular.filter(category=category.id)
return jingo.render(request, 'browse/landing.html', {
'category': category,
'featured': featured[:6],
'popular': popular[:6]
})
def _search(request, category=None):
ctx = {'browse': True}
if category is not None:
qs = Category.objects.filter(type=amo.ADDON_WEBAPP)
qs = Category.objects.filter(type=amo.ADDON_WEBAPP, weight__gte=0)
ctx['category'] = get_object_or_404(qs, slug=category)
# Do a search filtered by this category and sort by Weekly Downloads.
@ -29,3 +49,10 @@ def categories_apps(request, category=None):
return redirect(ctx['redirect'])
return jingo.render(request, 'search/results.html', ctx)
def browse_apps(request, category=None):
if request.GET.get('sort'):
return _search(request, category)
else:
return _landing(request, category)

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

@ -16,41 +16,23 @@
target="_blank">{{ _('Learn more') }}</a>
</div>
</section>
<section class="featured full">
<section id="home-featured" class="featured full">
<div>
<h2><a href="{{ url('browse.apps')|urlparams(sort='featured') }}">{{ _('Featured') }}</a></h2>
<h2><a href="{{ url('browse.apps')|urlparams(sort='featured') }}">
{{ _('Featured') }}</a></h2>
</div>
</section>
<section class="featured full slider">
{{ featured|promo_slider(feature=True) }}
</section>
<section class="popular full">
<section id="home-popular" class="popular full">
<div>
<h2><a href="{{ url('browse.apps')|urlparams(sort='popular') }}">{{ _('Popular') }}</a></h2>
<h2><a href="{{ url('browse.apps')|urlparams(sort='popular') }}">
{{ _('Popular') }}</a></h2>
</div>
</section>
<section class="popular slider full">
{{ popular|promo_slider }}
</section>
<section id="categories" class="categories full">
<div><h2>{{ _('All categories') }}</h2></div>
</section>
<section class="categories slider full">
<div class="promo-slider">
<div class="controls">
<a href="#" class="prev-page"></a>
<a href="#" class="next-page"></a>
</div>
<ul class="content">
{% for category in categories %}
<li>
<a href="{{ category.get_url_path() }}">
<img src="">
<h3>{{ category.name }}</h3>
</a>
</li>
{% endfor %}
</ul>
</div>
</section>
{{ category_slider() }}
{% endblock %}

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

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

@ -0,0 +1,30 @@
from nose.tools import eq_
import waffle
from amo.urlresolvers import reverse
from mkt.browse.tests.test_views import BrowseBase
class TestHome(BrowseBase):
def setUp(self):
super(TestHome, self).setUp()
waffle.models.Switch.objects.create(name='unleash-consumer',
active=True)
self.url = reverse('home')
def test_good_cat(self):
r = self.client.get(self.url)
eq_(r.status_code, 200)
self.assertTemplateUsed(r, 'home/home.html')
def test_featured(self):
a, b, c = self.setup_featured()
# Check that these apps are featured.
eq_(self.get_pks('featured', self.url), [c.id])
def test_popular(self):
a, b = self.setup_popular()
# Check that these apps are shown.
self._test_popular(self.url, [self.webapp.id, a.id, b.id])

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

@ -1,10 +1,6 @@
import jingo
import waffle
import amo
from addons.models import Category
from bandwagon.models import Collection
from mkt.developers.views import home as devhub_home
from mkt.webapps.models import Webapp
@ -13,19 +9,9 @@ def home(request):
"""The home page."""
if not waffle.switch_is_active('unleash-consumer'):
return devhub_home
try:
featured = Collection.objects.get(author__username='mozilla',
slug='webapps_home', type=amo.COLLECTION_FEATURED)
except Collection.DoesNotExist:
featured = []
if featured:
featured = featured.addons.filter(status=amo.STATUS_PUBLIC,
disabled_by_user=False)[:30]
popular = (Webapp.objects.order_by('-weekly_downloads')
.filter(status=amo.STATUS_PUBLIC, disabled_by_user=False))[:6]
categories = Category.objects.filter(type=amo.ADDON_WEBAPP)
featured = Webapp.featured('home')[:6]
popular = Webapp.popular()[:6]
return jingo.render(request, 'home/home.html', {
'featured': featured,
'popular': popular,
'categories': categories,
'popular': popular
})

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

@ -20,5 +20,5 @@ def search_results(context, products, field=None, src=None, dl_src=None):
@register.function
def SimpleSearchForm(request):
return forms.SimpleSearchForm(request.GET)
def SimpleSearchForm(data):
return forms.SimpleSearchForm(data)

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

@ -35,7 +35,7 @@
{% set heading = title %}
{% set title = '%s | %s' % (title, _('Apps')) %}
{% set crumbs = [(url('browse.apps'), _('Apps')),
(None, title)] %}
(None, heading)] %}
{% else %}
{% set title = 'Apps' %}
{% set heading = title %}

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

@ -110,7 +110,7 @@ def promo_slider(context, products, feature=False):
@register.function
@jinja2.contextfunction
def mkt_breadcrumbs(context, product=None, items=None, crumb_size=40,
add_default=True):
add_default=True, cls=None):
"""
Wrapper function for ``breadcrumbs``.
@ -142,7 +142,7 @@ def mkt_breadcrumbs(context, product=None, items=None, crumb_size=40,
crumbs = [(url_, truncate(label, crumb_size)) for (url_, label) in crumbs]
t = env.get_template('site/helpers/breadcrumbs.html').render(
breadcrumbs=crumbs)
breadcrumbs=crumbs, cls=cls)
return jinja2.Markup(t)

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

@ -1,5 +1,5 @@
{% if breadcrumbs %}
<section id="breadcrumbs" class="full">
<section id="breadcrumbs" class="full{{ ' ' + cls if cls }}">
<div>
<nav>
<ol>

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

@ -1,25 +0,0 @@
<div class="promo-slider">
<div class="controls">
<a href="#" class="prev-page"></a>
<a href="#" class="next-page"></a>
</div>
<ul class="content">
{% for product in products %}
{% if product.all_previews|length %}
<li>
<a href="{{ product.get_url_path() }}">
{% if feature %}
<img src="{{ product.previews.order_by('-created')[0].image_url }}">
{% else %}
<img src="{{ product.previews.order_by('-created')[0].thumbnail_url }}">
{% endif %}
<h3>{{ product.name }}</h3>
<p>
{{ product|price_label }}
</p>
</a>
</li>
{% endif %}
{% endfor %}
</ul>
</div>

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

@ -4,7 +4,8 @@
<nav>
<a href="#" class="menu-button"><b></b></a>
{% block search %}
{% set search_form = SimpleSearchForm(request) %}
{% set data = {'cat': category.id} if category else request.GET %}
{% set search_form = SimpleSearchForm(data) %}
<form id="search" action="{{ url('search.search') }}">
<input id="search-q" type="text" name="q" autocomplete="off" title=""
placeholder="{{ _('Search') }}"

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

@ -21,6 +21,7 @@ from amo.urlresolvers import reverse
from amo.utils import memoize
from addons import query
from addons.models import Addon, update_name_table, update_search_index
from bandwagon.models import Collection
from files.models import FileUpload, Platform
from lib.crypto.receipt import sign
from versions.models import Version
@ -41,19 +42,18 @@ class WebappManager(amo.models.ManagerBase):
def reviewed(self):
return self.filter(status__in=amo.REVIEWED_STATUSES)
def listed(self):
return self.reviewed().filter(_current_version__isnull=False,
disabled_by_user=False)
def visible(self):
return self.filter(status=amo.STATUS_PUBLIC, disabled_by_user=False)
def top_free(self, listed=True):
qs = self.listed() if listed else self
qs = self.visible() if listed else self
return (qs.filter(premium_type__in=amo.ADDON_FREES)
.exclude(addonpremium__price__price__isnull=False)
.order_by('-weekly_downloads')
.with_index(addons='downloads_type_idx'))
def top_paid(self, listed=True):
qs = self.listed() if listed else self
qs = self.visible() if listed else self
return (qs.filter(premium_type__in=amo.ADDON_PREMIUMS,
addonpremium__price__price__gt=0)
.order_by('-weekly_downloads')
@ -189,7 +189,7 @@ class Webapp(Addon):
def authors_other_addons(self, app=None):
"""Return other apps by the same author."""
return (self.__class__.objects.listed()
return (self.__class__.objects.visible()
.filter(type=amo.ADDON_WEBAPP)
.exclude(id=self.id).distinct()
.filter(addonuser__listed=True,
@ -204,6 +204,33 @@ class Webapp(Addon):
def is_pending(self):
return self.status == amo.STATUS_PENDING
@classmethod
def featured_collection(cls, group):
try:
featured = Collection.objects.get(author__username='mozilla',
slug='featured_apps_%s' % group,
type=amo.COLLECTION_FEATURED)
except Collection.DoesNotExist:
featured = None
return featured
@classmethod
def featured(cls, group):
featured = cls.featured_collection(group)
if featured:
return (featured.addons.filter(status=amo.STATUS_PUBLIC,
disabled_by_user=False)
.order_by('-weekly_downloads'))
else:
return cls.objects.none()
@classmethod
def popular(cls):
"""Elastically grab the most popular apps."""
return (cls.search().filter(status=amo.STATUS_PUBLIC,
is_disabled=False)
.order_by('-weekly_downloads'))
# Pull all translated_fields from Addon over to Webapp.
Webapp._meta.translated_fields = Addon._meta.translated_fields

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

@ -128,7 +128,7 @@ class TestWebappManager(test_utils.TestCase):
def setUp(self):
self.reviewed_eq = (lambda f=[]:
eq_(list(Webapp.objects.reviewed()), f))
self.listed_eq = (lambda f=[]: eq_(list(Webapp.objects.listed()), f))
self.listed_eq = (lambda f=[]: eq_(list(Webapp.objects.visible()), f))
def test_reviewed(self):
for status in amo.REVIEWED_STATUSES:

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

@ -9,19 +9,31 @@
{% set title = 'Feature Manager' %}
{% block title %}{{ mkt_page_title(title) }}{% endblock %}
{% macro appsform(id, addons) %}
{% macro appsform(id, apps) %}
<table>
<thead>
<th>App</th>
<th class="js-hidden">Delete</th>
</thead>
<tbody id="{{ id }}-webapps">
{% for info in addons %}
<tr><td><div class="current-webapp js-hidden" style="display: block;"><a class="collectionitem" target="_blank" href="{{ info[1].url }}"><img src="{{ info[1].icon_url }}">{{ info[1].name }}</a></div><input type="hidden" name="{{info[0]}}-{{id}}-webapp" value="{{ info[1].id }}"><a class="remove">×</a>
{% for idx, app in apps %}
<tr><td>
<div class="current-webapp js-hidden" style="display: block;">
<a target="_blank" href="{{ app.get_url_path() }}">
<img src="{{ app.icon_url }}"> {{ app.name }}</a>
</div>
<input type="hidden" name="{{ idx }}-{{ id }}-webapp" value="{{ app.id }}">
<a class="remove">×</a>
</td></tr>
{% endfor %}
</tbody>
<tfoot class="hidden">
<tr><td><div class="current-webapp js-hidden" style="display: block;"></div><input placeholder="{{ _('Enter the name of the webapp to include') }}" class="placeholder addon-ac" data-src="{{ url('search.apps_ajax') }}" /><input type="hidden"><a class="remove">×</a></td></tr>
<tr><td>
<div class="current-webapp js-hidden" style="display: block;"></div>
<input placeholder="{{ _('Enter the name of the webapp to include') }}"
class="placeholder addon-ac large" data-src="{{ url('search.apps_ajax') }}">
<input type="hidden"><a class="remove">×</a>
</td></tr>
</tfoot>
</table>
{% endmacro %}
@ -31,17 +43,17 @@
<form method="post">
{{ csrf() }}
<h3>Home</h3>
{{ appsform("home", home_addons) }}
<p><a href="#" id="home-add">Add an app to Home</a></p>
<h3>Home Featured</h3>
{{ appsform('home', home_featured) }}
<p><a href="#" id="home-add">Add an app</a></p>
<p>
<button type="submit" name="home_submit">Save Changes</button> or <a href="">Cancel</a>
</p>
<h3>Featured</h3>
{{ appsform("featured", featured_addons) }}
<p><a href="#" id="featured-add">Add an app to Featured</a></p>
<h3>Category Featured</h3>
{{ appsform('category', category_featured) }}
<p><a href="#" id="category-add">Add an app</a></p>
<p>
<button type="submit" name="featured_submit">Save Changes</button> or <a href="">Cancel</a>
<button type="submit" name="category_submit">Save Changes</button> or <a href="">Cancel</a>
</p>
</form>
{% endblock %}

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

@ -1,46 +1,41 @@
import jingo
from django.shortcuts import redirect
from django.db import transaction
from tower import ugettext as _
import amo
from amo import messages
from amo.decorators import write
from amo.urlresolvers import reverse
from bandwagon.models import CollectionAddon, Collection
from users.models import UserProfile
from bandwagon.models import CollectionAddon
from mkt.webapps.models import Webapp
@transaction.commit_on_success
@write
def featured_apps_admin(request):
author = UserProfile.objects.get(username="mozilla")
home_collection = Collection.objects.get(author=author,
slug="webapps_home",
type=amo.COLLECTION_FEATURED)
featured_collection = Collection.objects.get(author=author,
slug="webapps_featured",
type=amo.COLLECTION_FEATURED)
home_collection = Webapp.featured_collection('home')
category_collection = Webapp.featured_collection('category')
if request.POST:
if 'home_submit' in request.POST:
coll = home_collection
rowid = 'home'
elif 'featured_submit' in request.POST:
coll = featured_collection
rowid = 'featured'
elif 'category_submit' in request.POST:
coll = category_collection
rowid = 'category'
existing = set(coll.addons.values_list('id', flat=True))
requested = set(int(request.POST[k]) for k in sorted(request.POST.keys())
requested = set(int(request.POST[k])
for k in sorted(request.POST.keys())
if k.endswith(rowid + '-webapp'))
CollectionAddon.objects.filter(collection=coll, addon__in=(existing - requested)).delete()
CollectionAddon.objects.filter(collection=coll,
addon__in=(existing - requested)).delete()
for id in requested - existing:
CollectionAddon.objects.create(collection=coll, addon=Webapp.objects.get(id=id))
messages.success(request, _('Changes successfully saved.'))
CollectionAddon.objects.create(collection=coll, addon_id=id)
messages.success(request, 'Changes successfully saved.')
return redirect(reverse('admin.featured_apps'))
def get(collection):
return (c.addon for c in
CollectionAddon.objects.filter(collection=collection))
return jingo.render(request, 'zadmin/featuredapp.html',
{"home_addons": enumerate(get(home_collection)),
"featured_addons": enumerate(get(featured_collection))})
return jingo.render(request, 'zadmin/featuredapp.html', {
'home_featured': enumerate(home_collection.addons.all()),
'category_featured': enumerate(category_collection.addons.all())
})