diff --git a/media/js/devreg/lookup-tool.js b/media/js/devreg/lookup-tool.js index 057d9e31c87..679f478bd24 100644 --- a/media/js/devreg/lookup-tool.js +++ b/media/js/devreg/lookup-tool.js @@ -45,10 +45,9 @@ require(['prefetchManifest']); })); // Search suggestions. - $('#account-search').searchSuggestions($('#account-search-suggestions'), - processResults); - $('#app-search').searchSuggestions($('#app-search-suggestions'), - processResults); + $('#account-search').searchSuggestions($('#account-search-suggestions'), processResults); + $('#app-search').searchSuggestions($('#app-search-suggestions'), processResults); + $('#website-search').searchSuggestions($('#website-search-suggestions'), processResults); // Show All Results. var searchTerm = ''; @@ -114,7 +113,7 @@ require(['prefetchManifest']); id: item.id, email: item.email || '', name: item.name || item.display_name, - status: item.status + status: item.status || 'none' }; if (d.url && d.id) { d.name = escape_(d.name); diff --git a/migrations/913-websites-lookup-perms.sql b/migrations/913-websites-lookup-perms.sql new file mode 100644 index 00000000000..ba3cc3a63cb --- /dev/null +++ b/migrations/913-websites-lookup-perms.sql @@ -0,0 +1,3 @@ +-- Changing Websites:Review to Websites:* and removing duplicate Stats:View. +UPDATE groups SET rules='Apps:*,Websites:*,Users:Edit,Stats:View,AdminTools:View,AccountLookup:View,AppLookup:View,Lookup:View' WHERE name='Staff'; +UPDATE groups SET rules=CONCAT(rules, ',WebsiteLookup:View') WHERE name IN ('Staff', 'Support Staff', 'Carriers and Operators'); diff --git a/mkt/lookup/helpers.py b/mkt/lookup/helpers.py index d1964955573..edf79e42455 100644 --- a/mkt/lookup/helpers.py +++ b/mkt/lookup/helpers.py @@ -15,7 +15,6 @@ def format_currencies(context, currencies): return jinja2.Markup(cs) -# page_type is used for setting the link 'sel' class (activity/purchases) @register.function def user_header(account, title, is_admin=False, page_type=''): t = env.get_template('lookup/helpers/user_header.html') @@ -23,7 +22,6 @@ def user_header(account, title, is_admin=False, page_type=''): 'title': title, 'page_type': page_type})) -# page_type is used for setting the link 'sel' class @register.function @jinja2.contextfunction def app_header(context, app, page_type=''): @@ -41,6 +39,15 @@ def app_header(context, app, page_type=''): 'is_operator': is_operator})) +@register.function +@jinja2.contextfunction +def website_header(context, website, page_type=''): + t = env.get_template('lookup/helpers/website_header.html') + + return jinja2.Markup(t.render({'website': website, + 'page_type': page_type})) + + @register.function @jinja2.contextfunction def is_operator(context): diff --git a/mkt/lookup/serializers.py b/mkt/lookup/serializers.py index c4b60fdef96..fc85de8bf51 100644 --- a/mkt/lookup/serializers.py +++ b/mkt/lookup/serializers.py @@ -4,6 +4,7 @@ import mkt from mkt.webapps.serializers import ESAppSerializer +from mkt.websites.serializers import ESWebsiteSerializer class AppLookupSerializer(ESAppSerializer): @@ -30,3 +31,13 @@ def get_app_status(self, obj): else: status = mkt.STATUS_CHOICES_API_v2[obj.status] return status + + +class WebsiteLookupSerializer(ESWebsiteSerializer): + url = serializers.SerializerMethodField('get_website_summary_url') + + class Meta(ESWebsiteSerializer.Meta): + fields = ['id', 'name', 'url'] + + def get_website_summary_url(self, obj): + return reverse('lookup.website_summary', args=[obj.id]) diff --git a/mkt/lookup/templates/lookup/helpers/website_header.html b/mkt/lookup/templates/lookup/helpers/website_header.html new file mode 100644 index 00000000000..ad61ce85d70 --- /dev/null +++ b/mkt/lookup/templates/lookup/helpers/website_header.html @@ -0,0 +1,15 @@ +

+ {{ _('Website Lookup results for') }} + {{ website.name }} + ({{ website.pk }}) +

+ +
+ + +
diff --git a/mkt/lookup/templates/lookup/home.html b/mkt/lookup/templates/lookup/home.html index 7a0f2cf3b71..b92aeb973f5 100644 --- a/mkt/lookup/templates/lookup/home.html +++ b/mkt/lookup/templates/lookup/home.html @@ -13,4 +13,7 @@ {% if action_allowed('AppLookup', 'View') %} {% include 'lookup/includes/app_search.html' %} {% endif %} + {% if action_allowed('WebsiteLookup', 'View') %} + {% include 'lookup/includes/website_search.html' %} + {% endif %} {% endblock %} diff --git a/mkt/lookup/templates/lookup/includes/website_search.html b/mkt/lookup/templates/lookup/includes/website_search.html new file mode 100644 index 00000000000..382721cdc16 --- /dev/null +++ b/mkt/lookup/templates/lookup/includes/website_search.html @@ -0,0 +1,13 @@ +
+
+ {{ csrf() }} + + + + + +
+
diff --git a/mkt/lookup/templates/lookup/website_summary.html b/mkt/lookup/templates/lookup/website_summary.html new file mode 100644 index 00000000000..088b3de8c8f --- /dev/null +++ b/mkt/lookup/templates/lookup/website_summary.html @@ -0,0 +1,56 @@ +{% extends 'lookup/base.html' %} + +{% block breadcrumbs %} +{% endblock %} + +{% block content %} + {% include 'lookup/includes/website_search.html' %} + +
+ {{ website_header(website, 'summary') }} + +
+
+
{{ _('Title') }}
+
{{ website.title }}
+
+
+
{{ _('Name - Short Name') }}
+
{{ website.name }} - {{ website.short_name }}
+
+
+
{{ _('Description') }}
+
{{ website.description }}
+
+
+
{{ _('URL') }}
+
{{ website.url }}
+
+
+
{{ _('Mobile URL') }}
+
{{ website.mobile_url }}
+
+
+
{{ _('Keywords') }}
+
{{ website.keywords_list|join(', ') }}
+
+
+
{{ _('Preferred Regions') }}
+
{{ website.get_preferred_regions(sort_by='name')|map(attribute='name')|join(', ') }}
+
+
+
{{ _('Categories') }}
+
{{ website.categories|categories_names|join(', ') }}
+
+
+
{{ _('Status') }}
+
+ {{ mkt.STATUS_CHOICES[website.status] }} + {% if website.is_disabled %} + ({{ _('disabled') }}) + {% endif %} +
+
+
+ +{% endblock %} diff --git a/mkt/lookup/tests/test_serializers.py b/mkt/lookup/tests/test_serializers.py new file mode 100644 index 00000000000..a61485c48ff --- /dev/null +++ b/mkt/lookup/tests/test_serializers.py @@ -0,0 +1,27 @@ +from django.core.urlresolvers import reverse + +from nose.tools import eq_ + +from mkt.lookup.serializers import WebsiteLookupSerializer +from mkt.site.tests import ESTestCase +from mkt.websites.indexers import WebsiteIndexer +from mkt.websites.utils import website_factory + + +class TestWebsiteLookupSerializer(ESTestCase): + + def setUp(self): + self.website = website_factory() + self.refresh('website') + + def serialize(self): + obj = WebsiteIndexer.search().filter( + 'term', id=self.website.pk).execute().hits[0] + return WebsiteLookupSerializer(obj).data + + def test_basic(self): + data = self.serialize() + eq_(data['id'], self.website.id) + eq_(data['name'], {'en-US': self.website.name}) + eq_(data['url'], + reverse('lookup.website_summary', args=[self.website.id])) diff --git a/mkt/lookup/tests/test_views.py b/mkt/lookup/tests/test_views.py index f9e2b6600e5..c36e9be0e5a 100644 --- a/mkt/lookup/tests/test_views.py +++ b/mkt/lookup/tests/test_views.py @@ -18,9 +18,9 @@ import mkt.site.tests from mkt.abuse.models import AbuseReport from mkt.access.models import Group, GroupUser -from mkt.constants.payments import ( - FAILED, PENDING, PROVIDER_BANGO, PROVIDER_REFERENCE, - SOLITUDE_REFUND_STATUSES) +from mkt.constants.payments import (FAILED, PENDING, PROVIDER_BANGO, + PROVIDER_REFERENCE, + SOLITUDE_REFUND_STATUSES) from mkt.developers.models import (ActivityLog, AddonPaymentAccount, PaymentAccount, SolitudeSeller) from mkt.developers.providers import get_provider @@ -38,6 +38,7 @@ from mkt.tags.models import Tag from mkt.users.models import UserProfile from mkt.webapps.models import AddonUser, Webapp +from mkt.websites.utils import website_factory class SummaryTest(TestCase): @@ -293,6 +294,7 @@ class SearchTestMixin(object): def search(self, expect_objects=True, **data): res = self.client.get(self.url, data) + eq_(res.status_code, 200) data = json.loads(res.content) if expect_objects: assert len(data['objects']), 'should be more than 0 objects' @@ -1174,3 +1176,38 @@ def test_logs(self): doc = pq(res.content) assert 'manifest updated' in doc('li.item').eq(0).text() assert 'Comment on' in doc('li.item').eq(1).text() + + +class TestWebsiteSearch(ESTestCase, SearchTestMixin): + fixtures = fixture('user_support_staff', 'user_999') + + def setUp(self): + super(TestWebsiteSearch, self).setUp() + self.url = reverse('lookup.website_search') + self.website = website_factory() + self.refresh('website') + self.login('support-staff@mozilla.com') + + def search(self, *args, **kwargs): + if 'lang' not in kwargs: + kwargs.update({'lang': 'en-US'}) + return super(TestWebsiteSearch, self).search(*args, **kwargs) + + def verify_result(self, data): + eq_(data['objects'][0]['id'], self.website.pk) + eq_(data['objects'][0]['name'], self.website.name.localized_string) + eq_(data['objects'][0]['url'], reverse('lookup.website_summary', + args=[self.website.pk])) + + def test_auth_required(self): + self.client.logout() + res = self.client.get(self.url) + eq_(res.status_code, 403) + + def test_by_name(self): + data = self.search(q=self.website.name.localized_string) + self.verify_result(data) + + def test_by_id(self): + data = self.search(q=self.website.pk) + self.verify_result(data) diff --git a/mkt/lookup/urls.py b/mkt/lookup/urls.py index 8c6ff0a583a..4407adc4014 100644 --- a/mkt/lookup/urls.py +++ b/mkt/lookup/urls.py @@ -27,6 +27,14 @@ ) +# These views all start with website/. +website_patterns = patterns( + '', + url(r'^summary$', views.website_summary, + name='lookup.website_summary'), +) + + # These views all start with transaction ID. transaction_patterns = patterns( '', @@ -49,7 +57,10 @@ name='lookup.transaction_search'), url(r'^user_search$', views.user_search, name='lookup.user_search'), + url(r'^website_search$', views.WebsiteLookupSearchView.as_view(), + name='lookup.website_search'), (r'^app/(?P[^/]+)/', include(app_patterns)), + (r'^website/(?P[^/]+)/', include(website_patterns)), (r'^transaction/(?P[^/]+)/', include(transaction_patterns)), (r'^user/(?P[^/]+)/', include(user_patterns)), diff --git a/mkt/lookup/views.py b/mkt/lookup/views.py index bbfd2931f2f..b7b076e3c59 100644 --- a/mkt/lookup/views.py +++ b/mkt/lookup/views.py @@ -33,7 +33,7 @@ from mkt.developers.views_payments import _redirect_to_bango_portal from mkt.lookup.forms import (APIFileStatusForm, APIStatusForm, DeleteUserForm, TransactionRefundForm, TransactionSearchForm) -from mkt.lookup.serializers import AppLookupSerializer +from mkt.lookup.serializers import AppLookupSerializer, WebsiteLookupSerializer from mkt.prices.models import AddonPaymentData, Refund from mkt.purchase.models import Contribution from mkt.reviewers.models import QUEUE_TARAKO @@ -41,8 +41,11 @@ from mkt.search.views import SearchView from mkt.site.decorators import json_view, login_required, permission_required from mkt.site.utils import paginate +from mkt.tags.models import attach_tags from mkt.users.models import UserProfile from mkt.webapps.models import Webapp +from mkt.websites.models import Website +from mkt.websites.views import WebsiteSearchView log = commonware.log.getLogger('z.lookup') @@ -314,6 +317,18 @@ def app_summary(request, addon_id): }) +@login_required +@permission_required([('WebsiteLookup', 'View')]) +def website_summary(request, addon_id): + website = get_object_or_404(Website, pk=addon_id) + if not hasattr(website, 'keywords_list'): + attach_tags([website], m2m_name='keywords') + + return render(request, 'lookup/website_summary.html', { + 'website': website, + }) + + @login_required @permission_required([('AccountLookup', 'View')]) def app_activity(request, addon_id): @@ -443,6 +458,21 @@ def get_paginate_by(self, *args, **kwargs): **kwargs) +class WebsiteLookupSearchView(WebsiteSearchView): + permission_classes = [GroupPermission('WebsiteLookup', 'View')] + filter_backends = [SearchQueryFilter] + serializer_class = WebsiteLookupSerializer + paginate_by = lkp.SEARCH_LIMIT + max_paginate_by = lkp.MAX_RESULTS + + def get_paginate_by(self, *args, **kwargs): + if self.request.GET.get(self.paginate_by_param) == 'max': + return self.max_paginate_by + else: + return super(WebsiteLookupSearchView, + self).get_paginate_by(*args, **kwargs) + + def _app_summary(user_id): sql = """ select currency, diff --git a/mkt/site/fixtures/data/user_operator.json b/mkt/site/fixtures/data/user_operator.json index cca553cc596..9d3d7411605 100644 --- a/mkt/site/fixtures/data/user_operator.json +++ b/mkt/site/fixtures/data/user_operator.json @@ -19,7 +19,7 @@ "pk": 322, "model": "access.group", "fields": { - "rules": "AppLookup:View,Lookup:View,Operators:*", + "rules": "AppLookup:View,WebsiteLookup:View,Lookup:View,Operators:*", "notes": "", "modified": "2012-05-22 17:53:57", "name": "Operators", diff --git a/mkt/site/fixtures/data/user_support_staff.json b/mkt/site/fixtures/data/user_support_staff.json index 11306241a55..e7d8be9d563 100644 --- a/mkt/site/fixtures/data/user_support_staff.json +++ b/mkt/site/fixtures/data/user_support_staff.json @@ -19,7 +19,7 @@ "pk": 50059, "model": "access.group", "fields": { - "rules": "AccountLookup:View,Apps:Configure,Transaction:Refund,Transaction:View,Lookup:View,AppLookup:View,BangoPortal:Redirect", + "rules": "AccountLookup:View,Apps:Configure,Transaction:Refund,Transaction:View,Lookup:View,AppLookup:View,WebsiteLookup:View,BangoPortal:Redirect", "notes": "", "modified": "2012-05-22 17:53:57", "name": "Support Staff", diff --git a/mkt/site/fixtures/data/users.json b/mkt/site/fixtures/data/users.json index 675d3e9cca9..3fb0d5f7676 100644 --- a/mkt/site/fixtures/data/users.json +++ b/mkt/site/fixtures/data/users.json @@ -167,7 +167,7 @@ "pk": 50057, "model": "access.group", "fields": { - "rules": "AccountLookup:View,Apps:Configure,Transaction:View,Transaction:Refund,Lookup:View,AppLookup:View", + "rules": "AccountLookup:View,Apps:Configure,Transaction:View,Transaction:Refund,Lookup:View,AppLookup:View,WebsiteLookup:View", "notes": "", "modified": "2012-05-22 17:53:57", "name": "Support Staff", diff --git a/mkt/site/fixtures/init.json b/mkt/site/fixtures/init.json index d1a1f08a5df..f92567389cd 100644 --- a/mkt/site/fixtures/init.json +++ b/mkt/site/fixtures/init.json @@ -1344,7 +1344,7 @@ "modified": "2012-04-19T18:43:22", "name": "Staff", "notes": "", - "rules": "Apps:*,Users:Edit,Stats:View,AdminTools:View,AccountLookup:View,AppLookup:View,Lookup:View,Stats:View,Websites:*" + "rules": "Apps:*,Users:Edit,Stats:View,AdminTools:View,AccountLookup:View,AppLookup:View,WebsiteLookup:View,Lookup:View,Stats:View,Websites:*" }, "model": "access.group", "pk": 50000 @@ -1476,7 +1476,7 @@ "modified": "2012-05-22T17:12:04", "name": "Support Staff", "notes": "", - "rules": "AccountLookup:View,Apps:Configure,Transaction:View,Transaction:View,Transaction:Refund,Lookup:View,AppLookup:View" + "rules": "AccountLookup:View,Apps:Configure,Transaction:View,Transaction:View,Transaction:Refund,Lookup:View,AppLookup:View,WebsiteLookup:View" }, "model": "access.group", "pk": 50057 @@ -1564,7 +1564,7 @@ "modified": "2013-09-13T12:54:58", "name": "Operators", "notes": "For operators to perform app lookups", - "rules": "Lookup:View,AppLookup:View,Operators:*" + "rules": "Lookup:View,AppLookup:View,WebsiteLookup:View,Operators:*" }, "model": "access.group", "pk": 50070 diff --git a/mkt/websites/models.py b/mkt/websites/models.py index 7ddceaa00a6..1131bb9053d 100644 --- a/mkt/websites/models.py +++ b/mkt/websites/models.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import operator import os.path from django.conf import settings @@ -8,6 +9,7 @@ from django_extensions.db.fields.json import JSONField +import mkt from lib.utils import static_url from mkt.constants.applications import DEVICE_TYPE_LIST from mkt.constants.base import LISTED_STATUSES, STATUS_CHOICES, STATUS_NULL @@ -116,6 +118,17 @@ def get_icon_url(self, size): def get_url_path(self): return reverse('website.detail', kwargs={'pk': self.pk}) + def get_preferred_regions(self, sort_by='slug'): + """ + Return a list of region objects the website is preferred in, e.g.:: + + [, ...] + + """ + _regions = map(mkt.regions.REGIONS_CHOICES_ID_DICT.get, + self.preferred_regions) + return sorted(_regions, key=operator.attrgetter(sort_by)) + class WebsitePopularity(ModelBase): website = models.ForeignKey(Website, related_name='popularity') diff --git a/mkt/websites/tests/test_models.py b/mkt/websites/tests/test_models.py index a556d6d6c6d..e1671366c05 100644 --- a/mkt/websites/tests/test_models.py +++ b/mkt/websites/tests/test_models.py @@ -1,8 +1,11 @@ +import json + import mock from nose.tools import eq_ from lib.utils import static_url from mkt.constants.applications import DEVICE_TYPE_LIST +from mkt.constants.regions import URY, USA from mkt.site.tests import TestCase from mkt.websites.models import Website from mkt.websites.utils import website_factory @@ -43,6 +46,12 @@ def test_get_icon_no_icon(self): website = Website(pk=1) assert website.get_icon_url(32).endswith('/default-32.png') + def test_get_preferred_regions(self): + website = Website() + website.preferred_regions = json.dumps([URY.id, USA.id]) + eq_([r.slug for r in website.get_preferred_regions()], + [USA.slug, URY.slug]) + class TestWebsiteESIndexation(TestCase): @mock.patch('mkt.search.indexers.BaseIndexer.index_ids')