featured app admin tool (bug 754441)

This commit is contained in:
Allen Short 2012-06-06 03:18:48 -05:00
Родитель fab67d669d
Коммит 33e90a82c4
14 изменённых файлов: 351 добавлений и 99 удалений

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

@ -295,7 +295,7 @@ class BaseAjaxSearch(object):
"""
def __init__(self, request, excluded_ids=[]):
def __init__(self, request, excluded_ids=()):
self.request = request
self.excluded_ids = excluded_ids
self.src = getattr(self, 'src', None)
@ -369,6 +369,19 @@ class PersonaSuggestionsAjax(SearchSuggestionsAjax):
class WebappSuggestionsAjax(SearchSuggestionsAjax):
types = [amo.ADDON_WEBAPP]
fields = {'id': 'id',
'name': 'name'
}
def __init__(self, request, excluded_ids=(), category=None):
self.category = category
SearchSuggestionsAjax.__init__(self, request, excluded_ids)
def queryset(self):
res = SearchSuggestionsAjax.queryset(self)
if self.category:
res = res.filter(category__in=[self.category])
return res
@json_view

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

@ -5,7 +5,8 @@ function registerAddonAutocomplete(node) {
width: 300,
source: function(request, response) {
$.getJSON($(node).attr('data-src'), {
q: request.term
q: request.term,
category: $("#categories").val()
}, response);
},
focus: function(event, ui) {
@ -13,17 +14,8 @@ function registerAddonAutocomplete(node) {
return false;
},
select: function(event, ui) {
$(node).val(ui.item.name).attr('data-id', ui.item.id);
var current = template(
'<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,
name: ui.item.name
}));
$td.find('input[type=hidden]').val(ui.item.id);
node.val('');
node.hide();
updateAppsList($("#categories"),
ui.item.id);
return false;
}
}).data('autocomplete')._renderItem = function(ul, item) {
@ -33,19 +25,55 @@ function registerAddonAutocomplete(node) {
}
function newAddonSlot(id) {
var $tbody = $("#" + id + "-webapps");
var $tbody = $("#featured-webapps");
var $form = $tbody.next().children("tr").clone();
var $input = $form.find('input.placeholder');
registerAddonAutocomplete($input);
$form.find('input[type=hidden]').attr(
"name", $tbody.children().length + "-" + id +"-webapp");
$tbody.append($form);
}
$(document).ready(function(){
$("#home-webapps, #category-webapps").delegate(
'.remove', 'click', _pd(function() {$(this).closest('tr').remove();}));
function showAppsList(cat) {
return appslistXHR('GET', {
category: cat.val()
})};
$('#home-add').click(_pd(function() { newAddonSlot("home"); }));
$('#category-add').click(_pd(function() { newAddonSlot("category"); }));
function updateAppsList(cat, newItem) {
return appslistXHR('POST', {
category: cat.val(),
add: newItem
})};
function deleteFromAppsList(cat, oldItem) {
return appslistXHR('POST', {
category: cat.val(),
delete: oldItem
})};
function appslistXHR(verb, data) {
var appslist = $("#featured-webapps");
var q = $.ajax({type: verb, url: appslist.data("src"), data: data});
q.then(function (data) {
appslist.html(data);
});
return q;
};
$(document).ready(function(){
$("#featured-webapps").delegate(
'.remove', 'click', _pd(function() {
deleteFromAppsList($("#categories"),
$(this).data("id"));
}));
var categories = $("#categories");
var appslist = $("#featured-webapps");
var p = $.ajax({type: 'GET',
url: categories.data("src")});
p.then(function(data) {
categories.html(data);
showAppsList(categories);
});
categories.change(function (e) {
showAppsList(categories);
});
$('#featured-add').click(_pd(function() { newAddonSlot(); }));
});

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

@ -0,0 +1,7 @@
CREATE TABLE `zadmin_featuredapp` (
`id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY,
`app_id` integer NOT NULL,
`category_id` int(11) unsigned,
`is_sponsor` bool NOT NULL,
FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`)
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci;

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

@ -13,6 +13,8 @@ from mkt.search.forms import DEVICE_CHOICES_IDS
from mkt.webapps.models import Webapp
from mkt.webapps.tests.test_views import PaidAppMixin
from search.tests.test_views import TestAjaxSearch
class SearchBase(amo.tests.ESTestCase):
@ -258,3 +260,39 @@ class TestWebappSearch(PaidAppMixin, SearchBase):
# `sort=price` should be removed if `price=free` is in querystring.
r = self.client.get(url, {'price': 'free', 'sort': 'price'})
self.assertRedirects(r, urlparams(url, price='free'))
class SuggestionsTests(TestAjaxSearch):
def check_suggestions(self, url, params, addons=()):
r = self.client.get(url + '?' + params)
eq_(r.status_code, 200)
data = json.loads(r.content)
data.sort(key=lambda x: x['id'])
addons.sort(key=lambda x: x.id)
eq_(len(data), len(addons))
for got, expected in zip(data, addons):
eq_(int(got['id']), expected.id)
eq_(got['name'], unicode(expected.name))
def test_webapp_search(self):
url = reverse('search.apps_ajax')
c1 = Category.objects.create(name='groovy',
type=amo.ADDON_WEBAPP)
c2 = Category.objects.create(name='awesome',
type=amo.ADDON_WEBAPP)
g1 = Webapp.objects.create(status=amo.STATUS_PUBLIC,
name='groovy app 1',
type=amo.ADDON_WEBAPP)
a2 = Webapp.objects.create(status=amo.STATUS_PUBLIC,
name='awesome app 2',
type=amo.ADDON_WEBAPP)
AddonCategory.objects.create(category=c1, addon=g1)
AddonCategory.objects.create(category=c2, addon=a2)
self.client.login(username='admin@mozilla.com', password='password')
for a in Webapp.objects.all():
a.save()
self.refresh()
self.check_suggestions(url, "q=app&category=", addons=[g1, a2])
self.check_suggestions(url, "q=app&category=%d" % c1.id, addons=[g1])
self.check_suggestions(url, "q=app&category=%d" % c2.id, addons=[a2])

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

@ -191,4 +191,7 @@ def app_search(request):
@json_view
def ajax_search(request):
return WebappSuggestionsAjax(request).items
category = request.GET.get('category', None) or None
if category:
category = int(category)
return WebappSuggestionsAjax(request, category=category).items

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

@ -182,7 +182,7 @@ def admin_site_links():
return {
'addons': [
('Search for apps by name or id', reverse('zadmin.addon-search')),
('Featured add-ons', reverse('admin.featured_apps')),
('Featured add-ons', reverse('zadmin.featured_apps')),
('Name blocklist', reverse('zadmin.addon-name-blocklist')),
('Fake mail', reverse('zadmin.mail')),
('Flagged reviews', reverse('zadmin.flagged')),

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

@ -12,8 +12,6 @@ from apps.users.urls import (detail_patterns as user_detail_patterns,
from mkt.account.urls import (purchases_patterns, settings_patterns,
users_patterns as mkt_users_patterns)
from mkt.developers.views import login
from mkt.zadmin.views import featured_apps_admin
admin.autodiscover()
@ -92,10 +90,6 @@ urlpatterns = patterns('',
# Paypal, needed for IPNs only.
('^services/', include('paypal.urls')),
# Featured apps selector.
url('^admin/apps/featured$', featured_apps_admin,
name='admin.featured_apps'),
# AMO admin (not django admin).
('^admin/', include('zadmin.urls')),

13
mkt/zadmin/models.py Normal file
Просмотреть файл

@ -0,0 +1,13 @@
from django.db import models
from addons.models import Category
from mkt.webapps.models import Webapp
class FeaturedApp(models.Model):
app = models.ForeignKey(Webapp, null=False)
category = models.ForeignKey(Category, null=True)
is_sponsor = models.BooleanField(default=False)
class Meta:
db_table = 'zadmin_featuredapp'

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

@ -0,0 +1,28 @@
{% block content %}
{% for row in apps -%}
<tr>
<td>
<table>
<tr>
<td><img src="{{ row.app.icon_url }}"></td>
<td>{{ row.app.name }}</td>
<td>{% for dt in row.app.device_types %}
{{ dt }}
{% endfor %}</td>
</tr>
<tr>
<td>{% if row.app.promo %}
<a href="{{ row.app.get_dev_url() }}">Manage featured graphics</a>
{% else %}
<a href="{{ row.app.get_dev_url() }}">No featured graphics</a>
{% endif %}
</td>
<td>{% if row.is_sponsor %}Sponsored{% else %}Not sponsored{% endif %}</td>
<td>Locale etc</td>
</tr>
</table>
</td>
<td><input type="hidden"><a class="remove" data-id="{{ row.app.id }}">×</a></td>
</tr>
{% endfor %}
{% endblock %}

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

@ -0,0 +1,6 @@
{% block content %}
<option value="">Home Page ({{ homecount }})</option>
{% for opt in categories -%}
<option value="{{ opt['id'] }}">{{ opt['name'] }} ({{ opt['count'] }})</option>
{%- endfor %}
{% endblock %}

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

@ -4,56 +4,52 @@
{% block js %}
{{ super() }}
<script src="{{ media('js/mkt/admin_featuredapp.js') }}"></script>
<script type="text/template" id="featured-app-template">
<tr><td>
<div class="current-webapp js-hidden" style="display: block;">
<div>
<a target="_blank" href="<%= app_url %>">
<img src="<%= featured_graphic %>"><span><%= _.escape(app_name) %></span></a><%= format_support %>
</div>
<div>
<span class="sponsor"><%= is_sponsor %></span> <span class="localepicker"><%= locale %></span>
</div>
<a class="remove">×</a>
</div>
</td></tr>
</script>
{% endblock %}
{% set title = 'Feature Manager' %}
{% block title %}{{ mkt_page_title(title) }}{% endblock %}
{% macro appsform(id, apps) %}
<table>
<thead>
<th>App</th>
<th class="js-hidden">Delete</th>
</thead>
<tbody id="{{ id }}-webapps">
{% 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 large" data-src="{{ url('search.apps_ajax') }}">
<input type="hidden"><a class="remove">×</a>
</td></tr>
</tfoot>
</table>
{% endmacro %}
{% block content %}
<h2>{{ title }}</h2>
<form method="post">
{{ csrf() }}
<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>Category Featured</h3>
{{ appsform('category', category_featured) }}
<p><a href="#" id="category-add">Add an app</a></p>
<p>
<button type="submit" name="category_submit">Save Changes</button> or <a href="">Cancel</a>
</p>
<h3>Featured Apps</h3>
Section: <select id="categories"
data-src="{{ url('zadmin.featured_categories_ajax') }}"
></select>
<table>
<thead>
<th>App</th>
<th>Delete</th>
</thead>
<tbody id="featured-webapps"
data-src="{{ url('zadmin.featured_apps_ajax') }}">
</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 large"
data-src="{{ url('search.apps_ajax') }}">
<input type="hidden"><a class="remove">×</a>
</td></tr>
</tfoot>
</table>
<p><a href="#" id="featured-add">Add an app</a></p>
</form>
{% endblock %}

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

@ -0,0 +1,102 @@
from nose.tools import eq_
from pyquery import PyQuery as pq
import amo
import amo.tests
from amo.utils import urlparams
from amo.urlresolvers import reverse
from addons.models import Category, AddonCategory
from mkt.webapps.models import Webapp
from mkt.zadmin.models import FeaturedApp
class TestFeaturedApps(amo.tests.TestCase):
fixtures = ['base/users']
def setUp(self):
self.c1 = Category.objects.create(name='awesome',
type=amo.ADDON_WEBAPP)
self.c2 = Category.objects.create(name='groovy',
type=amo.ADDON_WEBAPP)
self.a1 = Webapp.objects.create(status=amo.STATUS_PUBLIC,
name='awesome app 1',
type=amo.ADDON_WEBAPP)
self.a2 = Webapp.objects.create(status=amo.STATUS_PUBLIC,
name='awesome app 2',
type=amo.ADDON_WEBAPP)
self.g1 = Webapp.objects.create(status=amo.STATUS_PUBLIC,
name='groovy app 1',
type=amo.ADDON_WEBAPP)
self.s1 = Webapp.objects.create(status=amo.STATUS_PUBLIC,
name='splendid app 1',
type=amo.ADDON_WEBAPP)
AddonCategory.objects.create(category=self.c1, addon=self.a1)
AddonCategory.objects.create(category=self.c1, addon=self.a2)
AddonCategory.objects.create(category=self.c2, addon=self.g1)
AddonCategory.objects.create(category=self.c1, addon=self.s1)
AddonCategory.objects.create(category=self.c2, addon=self.s1)
self.client.login(username='admin@mozilla.com', password='password')
self.url = reverse('zadmin.featured_apps_ajax')
def test_get_featured_apps(self):
r = self.client.get(urlparams(self.url, category=self.c1.id))
assert not r.content
f1 = FeaturedApp.objects.create(app=self.a1, category=self.c1)
f2 = FeaturedApp.objects.create(app=self.s1, category=self.c2, is_sponsor=True)
r = self.client.get(urlparams(self.url, category=self.c1.id))
doc = pq(r.content)
eq_(len(doc), 1)
eq_(doc('table td').eq(1).text(), 'awesome app 1')
eq_(doc('table td').eq(4).text(), 'Not sponsored')
r = self.client.get(urlparams(self.url, category=self.c2.id))
doc = pq(r.content)
eq_(len(doc), 1)
eq_(doc('table td').eq(1).text(), 'splendid app 1')
eq_(doc('table td').eq(4).text(), 'Sponsored')
def test_get_categories(self):
url = reverse('zadmin.featured_categories_ajax')
FeaturedApp.objects.create(app=self.a1, category=self.c1)
FeaturedApp.objects.create(app=self.a2, category=self.c1)
FeaturedApp.objects.create(app=self.a2, category=None)
r = self.client.get(url)
doc = pq(r.content)
eq_(set(pq(x).text() for x in doc[0]),
set(['Home Page (1)', 'groovy (0)', 'awesome (2)']))
def test_add_featured_app(self):
self.client.post(self.url,
{'category': '',
'add': self.a1.id})
assert FeaturedApp.objects.filter(app=self.a1.id,
category=None).exists()
self.client.post(self.url,
{'category': self.c1.id,
'add': self.a1.id})
assert FeaturedApp.objects.filter(app=self.a1,
category=self.c1).exists()
def test_delete_featured_app(self):
FeaturedApp.objects.create(app=self.a1, category=None)
FeaturedApp.objects.create(app=self.a1, category=self.c1)
self.client.post(self.url,
{'category': '',
'delete': self.a1.id})
assert not FeaturedApp.objects.filter(app=self.a1,
category=None).exists()
assert FeaturedApp.objects.filter(app=self.a1,
category=self.c1).exists()
FeaturedApp.objects.create(app=self.a1, category=None)
self.client.post(self.url,
{'category': self.c1.id,
'delete': self.a1.id})
assert not FeaturedApp.objects.filter(app=self.a1,
category=self.c1).exists()

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

@ -2,7 +2,14 @@ from django.conf.urls.defaults import patterns, url
from . import views
urlpatterns = patterns('',
url('^ecosystem$', views.ecosystem, name='mkt.zadmin.ecosystem')
urlpatterns = patterns(
'',
url('^ecosystem$', views.ecosystem, name='mkt.zadmin.ecosystem'),
# Featured apps selector.
url('^apps/featured$', views.featured_apps_admin,
name='zadmin.featured_apps'),
url('^apps/featured_ajax$', views.featured_apps_ajax,
name='zadmin.featured_apps_ajax'),
url('^apps/featured_categories_ajax$', views.featured_categories_ajax,
name='zadmin.featured_categories_ajax'),
)

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

@ -4,45 +4,24 @@ from django.contrib import admin
from django.shortcuts import redirect
from django.db import transaction
import amo
from amo import messages
from amo.decorators import write
from amo.decorators import write, json_view
from amo.urlresolvers import reverse
from addons.models import Category, AddonCategory
from bandwagon.models import CollectionAddon
from mkt.ecosystem.tasks import refresh_mdn_cache, tutorials
from mkt.ecosystem.models import MdnCache
from mkt.webapps.models import Webapp
from mkt.zadmin.models import FeaturedApp
@transaction.commit_on_success
@write
@admin.site.admin_view
def featured_apps_admin(request):
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 '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())
if k.endswith(rowid + '-webapp'))
CollectionAddon.objects.filter(collection=coll,
addon__in=(existing - requested)).delete()
for id in requested - existing:
CollectionAddon.objects.create(collection=coll, addon_id=id)
messages.success(request, 'Changes successfully saved.')
return redirect(reverse('admin.featured_apps'))
return jingo.render(request, 'zadmin/featuredapp.html', {
'home_featured': enumerate(home_collection.addons.all()),
'category_featured': enumerate(category_collection.addons.all())
})
return jingo.render(request, 'zadmin/featuredapp.html')
@admin.site.admin_view
@ -58,3 +37,41 @@ def ecosystem(request):
}
return jingo.render(request, 'zadmin/ecosystem.html', ctx)
@admin.site.admin_view
def featured_apps_ajax(request):
if request.GET:
cat = request.GET.get('category', None) or None
if cat:
cat = int(cat)
elif request.POST:
cat = request.POST.get('category', None) or None
if cat:
cat = int(cat)
deleteid = request.POST.get('delete', None)
if deleteid:
FeaturedApp.objects.filter(category__id=cat,
app__id=int(deleteid)).delete()
appid = request.POST.get('add', None)
if appid:
FeaturedApp.objects.get_or_create(category_id=cat,
app_id=int(appid))
else:
cat = None
apps = FeaturedApp.objects.filter(category__id=cat)
return jingo.render(request, 'zadmin/featured_apps_ajax.html',
{'apps': apps})
@admin.site.admin_view
def featured_categories_ajax(request):
cats = Category.objects.filter(type=amo.ADDON_WEBAPP)
return jingo.render(request, 'zadmin/featured_categories_ajax.html', {
'homecount': FeaturedApp.objects.filter(
category=None).count(),
'categories': [{
'name': cat.name,
'id': cat.pk,
'count': FeaturedApp.objects.filter(category=cat).count()
} for cat in cats]})