diff --git a/apps/search/views.py b/apps/search/views.py index df2603e1f6..d621612bd7 100644 --- a/apps/search/views.py +++ b/apps/search/views.py @@ -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 diff --git a/media/js/mkt/admin_featuredapp.js b/media/js/mkt/admin_featuredapp.js index 3fddec001f..996e8c78e3 100644 --- a/media/js/mkt/admin_featuredapp.js +++ b/media/js/mkt/admin_featuredapp.js @@ -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( - ' {name}'); - $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(); })); }); diff --git a/migrations/421-featured-app-admin.sql b/migrations/421-featured-app-admin.sql new file mode 100644 index 0000000000..1d133f5d9d --- /dev/null +++ b/migrations/421-featured-app-admin.sql @@ -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; diff --git a/mkt/search/tests/test_views.py b/mkt/search/tests/test_views.py index bb58470dba..bb777233bd 100644 --- a/mkt/search/tests/test_views.py +++ b/mkt/search/tests/test_views.py @@ -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]) diff --git a/mkt/search/views.py b/mkt/search/views.py index 69250874cd..1185a4d047 100644 --- a/mkt/search/views.py +++ b/mkt/search/views.py @@ -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 diff --git a/mkt/site/helpers.py b/mkt/site/helpers.py index 9b2002a86e..dbe7ee8dea 100644 --- a/mkt/site/helpers.py +++ b/mkt/site/helpers.py @@ -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')), diff --git a/mkt/urls.py b/mkt/urls.py index 9ef3d79e96..cc936ee1db 100644 --- a/mkt/urls.py +++ b/mkt/urls.py @@ -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')), diff --git a/mkt/zadmin/models.py b/mkt/zadmin/models.py new file mode 100644 index 0000000000..913681b07b --- /dev/null +++ b/mkt/zadmin/models.py @@ -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' diff --git a/mkt/zadmin/templates/zadmin/featured_apps_ajax.html b/mkt/zadmin/templates/zadmin/featured_apps_ajax.html new file mode 100644 index 0000000000..839060a3da --- /dev/null +++ b/mkt/zadmin/templates/zadmin/featured_apps_ajax.html @@ -0,0 +1,28 @@ +{% block content %} +{% for row in apps -%} + + + + + + + + + + + + + +
{{ row.app.name }}{% for dt in row.app.device_types %} + {{ dt }} + {% endfor %}
{% if row.app.promo %} + Manage featured graphics + {% else %} + No featured graphics + {% endif %} + {% if row.is_sponsor %}Sponsored{% else %}Not sponsored{% endif %}Locale etc
+ + × + +{% endfor %} +{% endblock %} diff --git a/mkt/zadmin/templates/zadmin/featured_categories_ajax.html b/mkt/zadmin/templates/zadmin/featured_categories_ajax.html new file mode 100644 index 0000000000..1f01a146a7 --- /dev/null +++ b/mkt/zadmin/templates/zadmin/featured_categories_ajax.html @@ -0,0 +1,6 @@ +{% block content %} + +{% for opt in categories -%} + +{%- endfor %} +{% endblock %} diff --git a/mkt/zadmin/templates/zadmin/featuredapp.html b/mkt/zadmin/templates/zadmin/featuredapp.html index 271435f6b4..3bf7baff84 100644 --- a/mkt/zadmin/templates/zadmin/featuredapp.html +++ b/mkt/zadmin/templates/zadmin/featuredapp.html @@ -4,56 +4,52 @@ {% block js %} {{ super() }} + {% endblock %} {% set title = 'Feature Manager' %} {% block title %}{{ mkt_page_title(title) }}{% endblock %} -{% macro appsform(id, apps) %} - - - - - - - {% for idx, app in apps %} - - {% endfor %} - - - - -
AppDelete
- - - × -
-{% endmacro %} - {% block content %}

{{ title }}

{{ csrf() }} -

Home Featured

- {{ appsform('home', home_featured) }} -

Add an app

-

- or Cancel -

-

Category Featured

- {{ appsform('category', category_featured) }} -

Add an app

-

- or Cancel -

+

Featured Apps

+ Section: + + + + + + + + + + +
AppDelete
+

Add an app

{% endblock %} diff --git a/mkt/zadmin/tests/test_views.py b/mkt/zadmin/tests/test_views.py new file mode 100644 index 0000000000..2921da59f4 --- /dev/null +++ b/mkt/zadmin/tests/test_views.py @@ -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() diff --git a/mkt/zadmin/urls.py b/mkt/zadmin/urls.py index 3c1dc77d5b..9ee5025ac5 100644 --- a/mkt/zadmin/urls.py +++ b/mkt/zadmin/urls.py @@ -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'), ) diff --git a/mkt/zadmin/views.py b/mkt/zadmin/views.py index 6380abcd4a..e66659f86a 100644 --- a/mkt/zadmin/views.py +++ b/mkt/zadmin/views.py @@ -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]})