From 125b0d79964d0e89d843cecde9c41ca2872df7d8 Mon Sep 17 00:00:00 2001 From: Mathieu Pillard Date: Tue, 2 Dec 2014 15:15:18 +0100 Subject: [PATCH] Remove rocketfuel code, keep a small compatibility layer for the API (bug 1106586, bug 1058292) --- migrations/645-collection-backfill-slugs.py | 7 +- mkt/api/tests/nose.cfg | 4 - mkt/api/tests/test_urls.py | 12 +- mkt/api/v1/urls.py | 12 +- mkt/collections/__init__.py | 0 mkt/collections/authorization.py | 102 -- mkt/collections/constants.py | 12 - mkt/collections/fields.py | 24 - mkt/collections/filters.py | 194 --- mkt/collections/managers.py | 7 - mkt/collections/models.py | 188 -- mkt/collections/serializers.py | 240 --- mkt/collections/tasks.py | 82 - mkt/collections/tests/__init__.py | 20 - mkt/collections/tests/test_authorization.py | 230 --- mkt/collections/tests/test_fields.py | 32 - mkt/collections/tests/test_managers.py | 28 - mkt/collections/tests/test_models.py | 179 -- mkt/collections/tests/test_serializers.py | 398 ----- mkt/collections/tests/test_tasks.py | 51 - mkt/collections/tests/test_views.py | 1707 ------------------- mkt/collections/views.py | 298 ---- mkt/commonplace/tests/test_views.py | 11 +- mkt/commonplace/urls.py | 4 - mkt/feed/fields.py | 50 +- mkt/feed/models.py | 2 +- mkt/feed/views.py | 63 +- mkt/fireplace/serializers.py | 13 - mkt/fireplace/tests/test_views.py | 47 - mkt/fireplace/urls.py | 19 +- mkt/fireplace/views.py | 13 - mkt/settings.py | 3 +- mkt/webapps/tasks.py | 5 +- 33 files changed, 134 insertions(+), 3923 deletions(-) delete mode 100644 mkt/collections/__init__.py delete mode 100644 mkt/collections/authorization.py delete mode 100644 mkt/collections/constants.py delete mode 100644 mkt/collections/fields.py delete mode 100644 mkt/collections/filters.py delete mode 100644 mkt/collections/managers.py delete mode 100644 mkt/collections/models.py delete mode 100644 mkt/collections/serializers.py delete mode 100644 mkt/collections/tasks.py delete mode 100644 mkt/collections/tests/__init__.py delete mode 100644 mkt/collections/tests/test_authorization.py delete mode 100644 mkt/collections/tests/test_fields.py delete mode 100644 mkt/collections/tests/test_managers.py delete mode 100644 mkt/collections/tests/test_models.py delete mode 100644 mkt/collections/tests/test_serializers.py delete mode 100644 mkt/collections/tests/test_tasks.py delete mode 100644 mkt/collections/tests/test_views.py delete mode 100644 mkt/collections/views.py diff --git a/migrations/645-collection-backfill-slugs.py b/migrations/645-collection-backfill-slugs.py index 027373bd282..e67864fc916 100644 --- a/migrations/645-collection-backfill-slugs.py +++ b/migrations/645-collection-backfill-slugs.py @@ -1,7 +1,2 @@ -from mkt.collections.models import Collection - - def run(): - """Backfill slugs.""" - for c in Collection.objects.all(): - c.save() + return diff --git a/mkt/api/tests/nose.cfg b/mkt/api/tests/nose.cfg index 501756c3e71..2de545d50be 100644 --- a/mkt/api/tests/nose.cfg +++ b/mkt/api/tests/nose.cfg @@ -17,10 +17,6 @@ tests=mkt.abuse.tests.test_views, mkt.api.tests.test_urls, mkt.api.tests.test_views, mkt.comm.tests.test_views, - mkt.collections.tests.test_authorization, - mkt.collections.tests.test_fields, - mkt.collections.tests.test_serializers, - mkt.collections.tests.test_views, mkt.developers.tests.test_api_payments, mkt.developers.tests.test_views_api, mkt.features.tests.test_serializers, diff --git a/mkt/api/tests/test_urls.py b/mkt/api/tests/test_urls.py index 5366e682dae..efd7379b95a 100644 --- a/mkt/api/tests/test_urls.py +++ b/mkt/api/tests/test_urls.py @@ -78,11 +78,9 @@ class TestAPIv1URLs(BaseTestAPIVersionURLs, amo.tests.TestCase): def test_collections(self): """ - Tests the v1 half of a move of the collection endpoints from: - - v1: /rocketfuel/collections/ - - v2: /feed/collections/ + Tests the v1 endpoints removed in v2 still work with v1. """ - self.assertViewName('/rocketfuel/collections/', 'CollectionViewSet') + self.assertViewName('/apps/search/featured/', 'FeaturedSearchView') class TestAPIv2URLs(BaseTestAPIVersionURLs, amo.tests.TestCase): @@ -94,8 +92,6 @@ class TestAPIv2URLs(BaseTestAPIVersionURLs, amo.tests.TestCase): def test_collections(self): """ - Tests the v2 half of a move of the collection endpoints from: - - v1: /rocketfuel/collections/ - - v2: /feed/collections/ + Tests the v2 endpoints removal. """ - self.assertView404('/rocketfuel/collections/') + self.assertView404('/apps/search/featured/') diff --git a/mkt/api/v1/urls.py b/mkt/api/v1/urls.py index 13c7b5c4d55..2dd6b3caff3 100644 --- a/mkt/api/v1/urls.py +++ b/mkt/api/v1/urls.py @@ -5,12 +5,11 @@ from mkt.abuse.urls import api_patterns as abuse_api_patterns from mkt.account.urls import api_patterns as account_api_patterns -from mkt.api.base import SubRouter, SubRouterWithFormat +from mkt.api.base import SubRouter from mkt.api.views import (CarrierViewSet, CategoryViewSet, error_reporter, ErrorViewSet, PriceTierViewSet, PriceCurrencyViewSet, RefreshManifestViewSet, RegionViewSet, site_config) -from mkt.collections.views import CollectionImageViewSet, CollectionViewSet from mkt.comm.urls import api_patterns as comm_api_patterns from mkt.developers.urls import dev_api_patterns, payments_api_patterns from mkt.features.views import AppFeaturesList @@ -22,13 +21,6 @@ from mkt.submit.views import PreviewViewSet, StatusViewSet, ValidationViewSet from mkt.webapps.views import AppTagViewSet, AppViewSet, PrivacyPolicyViewSet -rocketfuel = SimpleRouter() -rocketfuel.register(r'collections', CollectionViewSet, - base_name='collections') - -subcollections = SubRouterWithFormat() -subcollections.register('image', CollectionImageViewSet, - base_name='collection-image') apps = SimpleRouter() apps.register(r'preview', PreviewViewSet, base_name='app-preview') @@ -70,8 +62,6 @@ url(r'^services/', include(services.urls)), url(r'^services/config/site/', site_config, name='site-config'), url(r'^fireplace/report_error/\d*', error_reporter, name='error-reporter'), - url(r'^rocketfuel/', include(rocketfuel.urls)), - url(r'^rocketfuel/collections/', include(subcollections.urls)), url(r'^apps/', include('mkt.versions.urls')), url(r'^apps/', include('mkt.ratings.urls')), url(r'^apps/features/', AppFeaturesList.as_view(), diff --git a/mkt/collections/__init__.py b/mkt/collections/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/mkt/collections/authorization.py b/mkt/collections/authorization.py deleted file mode 100644 index 9c7d0075d5f..00000000000 --- a/mkt/collections/authorization.py +++ /dev/null @@ -1,102 +0,0 @@ -from django.contrib.auth.models import AnonymousUser -from django.core.exceptions import ImproperlyConfigured - -import commonware.log -from rest_framework.permissions import BasePermission, SAFE_METHODS - -from mkt.access import acl - - -log = commonware.log.getLogger('mkt.collections') - - -class CuratorAuthorization(BasePermission): - """ - Permission class governing ability to interact with Collection-related - APIs. - - Rules: - - All users may make GET, HEAD, OPTIONS requests. - - Users with Collections:Curate may make any request. - - Users in Collection().curators may make any request using a verb in the - curator_verbs property. - - Note: rest-framework does not allow for situations where a user fails - has_permission but passes has_object_permission, so the logic - determining whether a user is a curator or has the Collections:Curate - permission is abstracted from those methods and situationally called in - each. - """ - allow_public_safe_requests = True - curator_verbs = ['POST', 'PUT', 'PATCH'] - - def is_public_safe_request(self, request): - return (self.allow_public_safe_requests and - request.method in SAFE_METHODS) - - def is_curator_for(self, request, obj): - if isinstance(request.user, AnonymousUser): - return False - return (obj.has_curator(request.user) and request.method - in self.curator_verbs) - - def has_curate_permission(self, request): - return (acl.action_allowed(request, 'Collections', 'Curate') or - acl.action_allowed(request, 'Feed', 'Curate')) - - def has_permission(self, request, view): - if self.is_public_safe_request(request): - return True - - try: - obj = view.get_object() - except ImproperlyConfigured: - # i.e. We're calling get_object from a non-object view. - return self.has_curate_permission(request) - else: - return (self.has_curate_permission(request) or - self.is_curator_for(request, obj)) - - def has_object_permission(self, request, view, obj): - if (self.is_public_safe_request(request) or - self.has_curate_permission(request)): - return True - return self.is_curator_for(request, obj) - - -class StrictCuratorAuthorization(CuratorAuthorization): - """ - The same as CuratorAuthorization, with GET / HEAD / OPTIONS requests - disallowed for unauthorized users. - """ - allow_public_safe_requests = False - curator_verbs = CuratorAuthorization.curator_verbs + SAFE_METHODS - - -class CanBeHeroAuthorization(BasePermission): - """ - Only users with Collections:Curate can modify the can_be_hero field. - """ - def has_curate_permission(self, request): - return CuratorAuthorization().has_curate_permission(request) - - def is_modifying_request(self, request): - return request.method in ('PUT', 'PATCH', 'POST',) - - def hero_field_modified(self, request): - if request.method == 'POST' and 'can_be_hero' in request.POST: - return True - elif request.method in ('PATCH', 'POST', 'PUT'): - return (isinstance(request.DATA, dict) and 'can_be_hero' in - request.DATA.keys()) - return False - - def has_object_permission(self, request, view, obj): - """ - Returns false if the request is attempting to modify the can_be_hero - field and the authenticating use does not have the Collections:Curate - permission. - """ - return not (not self.has_curate_permission(request) and - self.is_modifying_request(request) and - self.hero_field_modified(request)) diff --git a/mkt/collections/constants.py b/mkt/collections/constants.py deleted file mode 100644 index ec206e20c78..00000000000 --- a/mkt/collections/constants.py +++ /dev/null @@ -1,12 +0,0 @@ -from tower import ugettext_lazy as _lazy - - -COLLECTIONS_TYPE_BASIC = 0 # Header graphic. -COLLECTIONS_TYPE_FEATURED = 1 # No header graphic. -COLLECTIONS_TYPE_OPERATOR = 2 # Different graphic. - -COLLECTION_TYPES = ( - (COLLECTIONS_TYPE_BASIC, _lazy(u'Basic Collection')), - (COLLECTIONS_TYPE_FEATURED, _lazy(u'Featured App List')), - (COLLECTIONS_TYPE_OPERATOR, _lazy(u'Operator Shelf')), -) diff --git a/mkt/collections/fields.py b/mkt/collections/fields.py deleted file mode 100644 index 78d68b6457e..00000000000 --- a/mkt/collections/fields.py +++ /dev/null @@ -1,24 +0,0 @@ -import re - -from django.core import exceptions -from django.db.models.fields import CharField -from django.utils.translation import ugettext_lazy as _ - - -class ColorField(CharField): - """ - Model field that only accepts 7-character hexadecimal color representations, - e.g. #FF0035. - """ - description = _('Hexadecimal color') - - def __init__(self, *args, **kwargs): - kwargs['max_length'] = kwargs.get('max_length', 7) - self.default_error_messages.update({ - 'bad_hex': _('Must be a valid hex color code, e.g. #FF0035.'), - }) - super(ColorField, self).__init__(*args, **kwargs) - - def validate(self, value, model_instance): - if value and not re.match('^\#([0-9a-fA-F]{6})$', value): - raise exceptions.ValidationError(self.error_messages['bad_hex']) diff --git a/mkt/collections/filters.py b/mkt/collections/filters.py deleted file mode 100644 index 4ee1ea53ba2..00000000000 --- a/mkt/collections/filters.py +++ /dev/null @@ -1,194 +0,0 @@ -from django import forms -from django.core.validators import EMPTY_VALUES - -from django_filters.filters import ChoiceFilter, ModelChoiceFilter -from django_filters.filterset import FilterSet - -import mkt -from mkt.api.forms import SluggableModelChoiceField -from mkt.collections.models import Collection -from mkt.constants.categories import CATEGORY_CHOICES - - -class ChoiceFilter(ChoiceFilter): - """ - Like ChoiceFilter, but considering '' as None. - """ - def filter(self, qs, value): - if value == '' or value is None: - return qs.filter(**{self.name: None}) - return super(ChoiceFilter, self).filter(qs, value) - - -class SlugChoiceFilter(ChoiceFilter): - def __init__(self, *args, **kwargs): - self.choices_dict = kwargs.pop('choices_dict') - # Create a choice dynamically to allow None, slugs and ids. - slugs_choices = self.choices_dict.items() - ids_choices = [(v.id, v) for v in self.choices_dict.values()] - kwargs['choices'] = [(None, None)] + slugs_choices + ids_choices - - return super(SlugChoiceFilter, self).__init__(*args, **kwargs) - - def filter(self, qs, value): - if value == '' or value is None: - value = None - elif not value.isdigit(): - # We are passed a slug, get the id by looking at the choices - # dict, defaulting to None if no corresponding value is found. - value = self.choices_dict.get(value, None) - if value is not None: - value = value.id - return qs.filter(**{self.name: value}) - - -class SlugModelChoiceFilter(ModelChoiceFilter): - field_class = SluggableModelChoiceField - - def filter(self, qs, value): - return qs.filter(**{'%s__%s' % (self.name, self.lookup_type): value}) - - -class CollectionFilterSet(FilterSet): - # Note: the filter names must match what ApiSearchForm and CategoryViewSet - # are using. - carrier = SlugChoiceFilter(name='carrier', - choices_dict=mkt.carriers.CARRIER_MAP) - region = SlugChoiceFilter(name='region', - choices_dict=mkt.regions.REGION_LOOKUP) - cat = ChoiceFilter(name='category', choices=CATEGORY_CHOICES) - - class Meta: - model = Collection - # All fields are provided above, but django-filter needs Meta.field to - # exist. - fields = [] - - def get_queryset(self): - """ - Return the queryset to use for the filterset. - - Copied from django-filter qs property, modified to support filtering on - 'empty' values, at the expense of multi-lookups like 'x < 4 and x > 2'. - """ - valid = self.is_bound and self.form.is_valid() - - if self.strict and self.is_bound and not valid: - qs = self.queryset.none() - qs.filter_errors = self.form.errors - return qs - - # Start with all the results and filter from there. - qs = self.queryset.all() - for name, filter_ in self.filters.items(): - if valid: - if name in self.form.data: - value = self.form.cleaned_data[name] - else: - continue - else: - raw_value = self.form[name].value() - try: - value = self.form.fields[name].clean(raw_value) - except forms.ValidationError: - if self.strict: - return self.queryset.none() - else: - continue - - # At this point we should have valid & clean data. - qs = filter_.filter(qs, value) - - # Optional ordering. - if self._meta.order_by: - order_field = self.form.fields[self.order_by_field] - data = self.form[self.order_by_field].data - ordered = None - try: - ordered = order_field.clean(data) - except forms.ValidationError: - pass - - if ordered in EMPTY_VALUES and self.strict: - ordered = self.form.fields[self.order_by_field].choices[0][0] - - if ordered: - qs = qs.order_by(*self.get_order_by(ordered)) - - return qs - - @property - def qs(self): - if hasattr(self, '_qs'): - return self._qs - self._qs = self.get_queryset() - return self._qs - - -class CollectionFilterSetWithFallback(CollectionFilterSet): - """ - FilterSet with a fallback mechanism, dropping filters in a certain order - if no results are found. - """ - - # Combinations of fields to try to set to NULL, in order, when no results - # are found. See `next_fallback()`. - fields_fallback_order = ( - ('region',), - ('carrier',), - ('region', 'carrier',) - ) - - def next_fallback(self): - """ - Yield the next set of filters to set to NULL when refiltering the - queryset to find results. See `refilter_queryset()`. - """ - for f in self.fields_fallback_order: - yield f - - def refilter_queryset(self): - """ - Reset self.data, then override fields yield by the `fallback` generator - to NULL. Then recall the `qs` property and return it. - - When we are using this FilterSet, we really want to return something, - even if it's less relevant to the original query. When the `qs` - property is evaluated, if no results are found, it will call this - method to refilter the queryset in order to find /something/. - - Can raise StopIteration if the fallback generator is exhausted. - """ - self.data = self.original_data.copy() - self.fields_to_null = next(self.fallback) - for field in self.fields_to_null: - if field in self.data: - self.data[field] = None - del self._form - return self.qs - - def __init__(self, *args, **kwargs): - super(CollectionFilterSetWithFallback, self).__init__(*args, **kwargs) - self.original_data = self.data.copy() - self.fallback = self.next_fallback() - self.fields_to_null = None - - @property - def qs(self): - if hasattr(self, '_qs'): - return self._qs - - qs = self.get_queryset() - - if hasattr(qs, 'filter_errors'): - # Immediately return if there was an error. - self._qs = qs - return self._qs - elif not qs.exists(): - try: - qs = self.refilter_queryset() - except StopIteration: - pass - self._qs = qs - self._qs.filter_fallback = self.fields_to_null - return self._qs diff --git a/mkt/collections/managers.py b/mkt/collections/managers.py deleted file mode 100644 index c22be169916..00000000000 --- a/mkt/collections/managers.py +++ /dev/null @@ -1,7 +0,0 @@ -from mkt.site.models import ManagerBase - - -class PublicCollectionsManager(ManagerBase): - def get_query_set(self): - qs = super(PublicCollectionsManager, self).get_query_set() - return qs.filter(is_public=True) diff --git a/mkt/collections/models.py b/mkt/collections/models.py deleted file mode 100644 index 23e57665594..00000000000 --- a/mkt/collections/models.py +++ /dev/null @@ -1,188 +0,0 @@ -import os - -from django.conf import settings -from django.db import models - -import amo -import mkt.carriers -import mkt.regions -from mkt.constants.categories import CATEGORY_CHOICES -from mkt.site.decorators import use_master -from mkt.site.models import ManagerBase, ModelBase -from mkt.translations.fields import PurifiedField, save_signal -from mkt.translations.utils import to_language -from mkt.webapps.models import clean_slug, Webapp -from mkt.webapps.tasks import index_webapps - -from .constants import COLLECTION_TYPES -from .fields import ColorField -from .managers import PublicCollectionsManager - - -class Collection(ModelBase): - # `collection_type` for rocketfuel, not transonic. - collection_type = models.IntegerField(choices=COLLECTION_TYPES) - description = PurifiedField() - name = PurifiedField() - is_public = models.BooleanField(default=False) - category = models.CharField(default=None, null=True, blank=True, - max_length=30, choices=CATEGORY_CHOICES) - region = models.PositiveIntegerField(default=None, null=True, blank=True, - choices=mkt.regions.REGIONS_CHOICES_ID, db_index=True) - carrier = models.IntegerField(default=None, null=True, blank=True, - choices=mkt.carriers.CARRIER_CHOICES, db_index=True) - author = models.CharField(max_length=255, default='', blank=True) - slug = models.CharField(blank=True, max_length=30, - help_text='Used in collection URLs.') - default_language = models.CharField(max_length=10, - choices=((to_language(lang), desc) - for lang, desc in settings.LANGUAGES.items()), - default=to_language(settings.LANGUAGE_CODE)) - curators = models.ManyToManyField('users.UserProfile') - background_color = ColorField(null=True) - text_color = ColorField(null=True) - image_hash = models.CharField(default=None, max_length=8, null=True) - can_be_hero = models.BooleanField(default=False, help_text=( - 'Indicates whether an operator shelf collection can be displayed with' - 'a hero graphic')) - _apps = models.ManyToManyField(Webapp, through='CollectionMembership', - related_name='app_collections') - - objects = ManagerBase() - public = PublicCollectionsManager() - - class Meta: - db_table = 'app_collections' - # This ordering will change soon since we'll need to be able to order - # collections themselves, but this helps tests for now. - ordering = ('-id',) - - def __unicode__(self): - return self.name.localized_string_clean - - def save(self, **kw): - self.clean_slug() - return super(Collection, self).save(**kw) - - @use_master - def clean_slug(self): - clean_slug(self, 'slug') - - @classmethod - def get_fallback(cls): - return cls._meta.get_field('default_language') - - def image_path(self, suffix=''): - # The argument `suffix` isn't used here but is in the feed. - return os.path.join(settings.COLLECTIONS_ICON_PATH, - str(self.pk / 1000), - 'app_collection_%s.png' % (self.pk,)) - - def apps(self): - """ - Public apps on the collection, ordered by their position in the - CollectionMembership model. - - Use this method everytime you want to display apps for a collection to - an user. - """ - return self._apps.filter(disabled_by_user=False, - status=amo.STATUS_PUBLIC).order_by('collectionmembership') - - def add_app(self, app, order=None): - """ - Add an app to this collection. If specified, the app will be created - with the specified `order`. If not, it will be added to the end of the - collection. - """ - qs = CollectionMembership.objects.filter(collection=self) - if order is None: - aggregate = qs.aggregate(models.Max('order'))['order__max'] - order = aggregate + 1 if aggregate is not None else 0 - rval = CollectionMembership.objects.create(collection=self, app=app, - order=order) - # Help django-cache-machine: it doesn't like many 2 many relations, - # the cache is never invalidated properly when adding a new object. - CollectionMembership.objects.invalidate(*qs) - index_webapps.delay([app.pk]) - return rval - - def remove_app(self, app): - """ - Remove the passed app from this collection, returning a boolean - indicating whether a successful deletion took place. - """ - try: - membership = self.collectionmembership_set.get(app=app) - except CollectionMembership.DoesNotExist: - return False - else: - membership.delete() - index_webapps.delay([app.pk]) - return True - - def reorder(self, new_order): - """ - Passed a list of app IDs, e.g. - - [18, 24, 9] - - will change the order of each item in the collection to match the - passed order. A ValueError will be raised if each app in the - collection is not included in the ditionary. - """ - existing_pks = self.apps().no_cache().values_list('pk', flat=True) - if set(existing_pks) != set(new_order): - raise ValueError('Not all apps included') - for order, pk in enumerate(new_order): - CollectionMembership.objects.get(collection=self, - app_id=pk).update(order=order) - index_webapps.delay(new_order) - - def has_curator(self, userprofile): - """ - Returns boolean indicating whether the passed user profile is a curator - on this collection. - - ID comparison used instead of directly checking objects to ensure that - UserProfile objects could be passed. - """ - return userprofile.id in self.curators.values_list('id', flat=True) - - def add_curator(self, userprofile): - ret = self.curators.add(userprofile) - Collection.objects.invalidate(*self.curators.all()) - return ret - - def remove_curator(self, userprofile): - ret = self.curators.remove(userprofile) - Collection.objects.invalidate(*self.curators.all()) - return ret - - -class CollectionMembership(ModelBase): - collection = models.ForeignKey(Collection) - app = models.ForeignKey(Webapp) - order = models.SmallIntegerField(null=True) - - def __unicode__(self): - return u'"%s" in "%s"' % (self.app.name, self.collection.name) - - class Meta: - db_table = 'app_collection_membership' - unique_together = ('collection', 'app',) - ordering = ('order',) - - -def remove_deleted_apps(*args, **kwargs): - instance = kwargs.get('instance') - CollectionMembership.objects.filter(app_id=instance.pk).delete() - - -# Save translations when saving a Collection. -models.signals.pre_save.connect(save_signal, sender=Collection, - dispatch_uid='collection_translations') - -# Delete collection membership when deleting an app. -models.signals.post_delete.connect(remove_deleted_apps, sender=Webapp, - dispatch_uid='apps_collections_cleanup') diff --git a/mkt/collections/serializers.py b/mkt/collections/serializers.py deleted file mode 100644 index c2901ca2d71..00000000000 --- a/mkt/collections/serializers.py +++ /dev/null @@ -1,240 +0,0 @@ -# -*- coding: utf-8 -*- -import hashlib -import StringIO -import uuid - -from rest_framework import serializers -from rest_framework.fields import get_component -from rest_framework.reverse import reverse -from tower import ugettext_lazy as _ - -from django.conf import settings -from django.core.exceptions import ImproperlyConfigured -from django.core.files.base import File - -import amo -import mkt -from mkt.api.fields import (SlugChoiceField, TranslationSerializerField, - UnicodeChoiceField) -from mkt.constants.applications import get_device -from mkt.constants.categories import CATEGORY_CHOICES -from mkt.features.utils import get_feature_profile -from mkt.users.models import UserProfile -from mkt.webapps.indexers import WebappIndexer -from mkt.webapps.serializers import SimpleAppSerializer, SimpleESAppSerializer - -from .constants import COLLECTIONS_TYPE_FEATURED, COLLECTIONS_TYPE_OPERATOR -from .models import Collection - - -class CollectionMembershipField(serializers.RelatedField): - """ - RelatedField subclass that serializes apps in a Collection, taking into - account feature profile and optionally relying on ElasticSearch to find - the apps instead of making a DB query. - - Specifically created for use with CollectionSerializer; you probably don't - want to use this elsewhere. - """ - app_serializer_classes = { - 'es': SimpleESAppSerializer, - 'normal': SimpleAppSerializer, - } - - def to_native(self, qs, use_es=False): - if use_es: - serializer_class = self.app_serializer_classes['es'] - # To work around elasticsearch default limit of 10, hardcode a - # higher limit. - qs = qs[:100].execute() - else: - serializer_class = self.app_serializer_classes['normal'] - return serializer_class(qs, context=self.context, many=True).data - - def field_to_native(self, obj, field_name): - if not hasattr(self, 'context') or 'request' not in self.context: - raise ImproperlyConfigured('Pass request in self.context when' - ' using CollectionMembershipField.') - - request = self.context['request'] - - # Having 'use-es-for-apps' in the context means the parent view wants - # us to use ES to fetch the apps. If that key is present, check that we - # have a view in the context and that the waffle flag is active. If - # everything checks out, bypass the db and use ES to fetch apps for a - # nice performance boost. - if self.context.get('use-es-for-apps') and self.context.get('view'): - return self.field_to_native_es(obj, request) - - qs = get_component(obj, self.source) - - # Filter apps based on device and feature profiles. - device = get_device(request) - profile = get_feature_profile(request) - if device and device != amo.DEVICE_DESKTOP: - qs = qs.filter(addondevicetype__device_type=device.id) - if profile: - qs = qs.filter(**profile.to_kwargs( - prefix='_current_version__features__has_')) - - return self.to_native(qs) - - def field_to_native_es(self, obj, request): - """ - A version of field_to_native that uses ElasticSearch to fetch the apps - belonging to the collection instead of SQL. - - Relies on a FeaturedSearchView instance in self.context['view'] - to properly rehydrate results returned by ES. - """ - device = get_device(request) - - app_filters = {'profile': get_feature_profile(request)} - if device and device != amo.DEVICE_DESKTOP: - app_filters['device'] = device.id - - qs = WebappIndexer.get_app_filter(request, app_filters) - qs = qs.filter('term', **{'collection.id': obj.pk}) - - qs = qs.sort({ - 'collection.order': { - 'order': 'asc', - 'nested_filter': { - 'term': {'collection.id': obj.pk} - } - } - }) - - return self.to_native(qs, use_es=True) - - -class CollectionImageField(serializers.HyperlinkedRelatedField): - read_only = True - - def get_url(self, obj, view_name, request, format): - if getattr(obj, 'image_hash', None): - # Always prefix with STATIC_URL to return images from our CDN. - prefix = settings.STATIC_URL.strip('/') - # Always append image_hash so that we can send far-future expires. - suffix = '?%s' % obj.image_hash - url = reverse(view_name, kwargs={'pk': obj.pk}, request=request, - format=format) - return '%s%s%s' % (prefix, url, suffix) - else: - return None - - -class CollectionSerializer(serializers.ModelSerializer): - name = TranslationSerializerField(min_length=1) - description = TranslationSerializerField() - slug = serializers.CharField(required=False) - collection_type = serializers.IntegerField() - apps = CollectionMembershipField(many=True, source='apps') - image = CollectionImageField( - source='*', - view_name='collection-image-detail', - format='png') - carrier = SlugChoiceField(required=False, empty=None, - choices_dict=mkt.carriers.CARRIER_MAP) - region = SlugChoiceField(required=False, empty=None, - choices_dict=mkt.regions.REGION_LOOKUP) - category = UnicodeChoiceField(required=False, empty=None, - choices=CATEGORY_CHOICES) - - class Meta: - fields = ('apps', 'author', 'background_color', 'can_be_hero', - 'carrier', 'category', 'collection_type', 'default_language', - 'description', 'id', 'image', 'is_public', 'name', 'region', - 'slug', 'text_color',) - model = Collection - - def to_native(self, obj): - """ - Remove `can_be_hero` from the serialization if this is not an operator - shelf. - """ - native = super(CollectionSerializer, self).to_native(obj) - if native['collection_type'] != COLLECTIONS_TYPE_OPERATOR: - del native['can_be_hero'] - return native - - def validate(self, attrs): - """ - Prevent operator shelves from being associated with a category. - """ - existing = getattr(self, 'object') - exc = 'Operator shelves may not be associated with a category.' - - if (not existing and attrs['collection_type'] == - COLLECTIONS_TYPE_OPERATOR and attrs.get('category')): - raise serializers.ValidationError(exc) - - elif existing: - collection_type = attrs.get('collection_type', - existing.collection_type) - category = attrs.get('category', existing.category) - if collection_type == COLLECTIONS_TYPE_OPERATOR and category: - raise serializers.ValidationError(exc) - - return attrs - - def full_clean(self, instance): - instance = super(CollectionSerializer, self).full_clean(instance) - if not instance: - return None - # For featured apps and operator shelf collections, we need to check if - # one already exists for the same region/category/carrier combination. - # - # Sadly, this can't be expressed as a db-level unique constraint, - # because this doesn't apply to basic collections. - # - # We have to do it ourselves, and we need the rest of the validation - # to have already taken place, and have the incoming data and original - # data from existing instance if it's an edit, so full_clean() is the - # best place to do it. - unique_collections_types = (COLLECTIONS_TYPE_FEATURED, - COLLECTIONS_TYPE_OPERATOR) - qs = Collection.objects.filter( - collection_type=instance.collection_type, - category=instance.category, - region=instance.region, - carrier=instance.carrier) - if instance.pk: - qs = qs.exclude(pk=instance.pk) - if (instance.collection_type in unique_collections_types and - qs.exists()): - self._errors['collection_uniqueness'] = _( - u'You can not have more than one Featured Apps/Operator Shelf ' - u'collection for the same category/carrier/region combination.' - ) - return instance - - -class CuratorSerializer(serializers.ModelSerializer): - class Meta: - fields = ('display_name', 'email', 'id') - model = UserProfile - - -class DataURLImageField(serializers.CharField): - def from_native(self, data): - if data.startswith('"') and data.endswith('"'): - # Strip quotes if necessary. - data = data[1:-1] - if not data.startswith('data:'): - raise serializers.ValidationError('Not a data URI.') - - metadata, encoded = data.rsplit(',', 1) - parts = metadata.rsplit(';', 1) - if parts[-1] == 'base64': - content = encoded.decode('base64') - f = StringIO.StringIO(content) - f.size = len(content) - tmp = File(f, name=uuid.uuid4().hex) - hash_ = hashlib.md5(content).hexdigest()[:8] - return serializers.ImageField().from_native(tmp), hash_ - else: - raise serializers.ValidationError('Not a base64 data URI.') - - def to_native(self, value): - return value.name diff --git a/mkt/collections/tasks.py b/mkt/collections/tasks.py deleted file mode 100644 index 72cfa0f4cb5..00000000000 --- a/mkt/collections/tasks.py +++ /dev/null @@ -1,82 +0,0 @@ -import json -import logging -import os - -from django.conf import settings -from django.contrib.auth.models import AnonymousUser -from django.test.client import RequestFactory - -from celeryutils import task -from rest_framework import serializers - -from amo.utils import chunked, JSONEncoder -from mkt.collections.models import Collection -from mkt.collections.serializers import CollectionSerializer -from mkt.constants.regions import RESTOFWORLD -from mkt.webapps.models import Webapp - - -task_log = logging.getLogger('collections.tasks') - - -class ShortAppSerializer(serializers.ModelSerializer): - pk = serializers.IntegerField() - filepath = serializers.SerializerMethodField('get_filepath') - - class Meta: - model = Webapp - fields = ('pk', 'filepath') - - def get_filepath(self, obj): - return os.path.join('apps', object_path(obj)) - - -class ShortAppsCollectionSerializer(CollectionSerializer): - apps = ShortAppSerializer(many=True, read_only=True, source='apps') - - -def object_path(obj): - return os.path.join(str(obj.pk / 1000), '{pk}.json'.format(pk=obj.pk)) - - -def collection_filepath(collection): - return os.path.join(settings.DUMPED_APPS_PATH, - 'collections', - object_path(collection)) - - -def collection_data(collection): - request = RequestFactory().get('/') - request.user = AnonymousUser() - request.REGION = RESTOFWORLD - return ShortAppsCollectionSerializer(collection, - context={'request': request}).data - - -def write_file(filepath, output): - target_path = os.path.dirname(filepath) - if not os.path.exists(target_path): - os.makedirs(target_path) - with open(filepath, 'w') as f: - f.write(output) - return filepath - - -def dump_collection(collection): - target_file = collection_filepath(collection) - task_log.info('Dumping collection {0} to {1}'.format(collection.pk, - target_file)) - json_collection = json.dumps(collection_data(collection), - cls=JSONEncoder) - return write_file(target_file, json_collection) - - -@task(ignore_result=False) -def dump_collections(pks): - return [dump_collection(collection) - for collection in Collection.public.filter(pk__in=pks).iterator()] - - -def dump_all_collections_tasks(): - all_pks = Collection.public.values_list('pk', flat=True).order_by('pk') - return [dump_collections.si(pks) for pks in chunked(all_pks, 100)] diff --git a/mkt/collections/tests/__init__.py b/mkt/collections/tests/__init__.py deleted file mode 100644 index f8f79c1eceb..00000000000 --- a/mkt/collections/tests/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -from mkt.collections.constants import COLLECTIONS_TYPE_BASIC -from mkt.collections.models import Collection - - -class CollectionTestMixin(object): - collection_data = { - 'author': u'BastaCorp', - 'background_color': '#FFF000', - 'collection_type': COLLECTIONS_TYPE_BASIC, - 'description': {'en-US': u"BastaCorp's favorite HVAC apps"}, - 'is_public': True, - 'name': {'en-US': u'HVAC Apps'}, - 'slug': u'hvac-apps', - 'text_color': '#000FFF', - } - - def make_collection(self, **kwargs): - if kwargs: - self.collection_data.update(kwargs) - return Collection.objects.create(**self.collection_data) diff --git a/mkt/collections/tests/test_authorization.py b/mkt/collections/tests/test_authorization.py deleted file mode 100644 index 36e46981c19..00000000000 --- a/mkt/collections/tests/test_authorization.py +++ /dev/null @@ -1,230 +0,0 @@ -import json -from urllib import urlencode - -from django.test.client import RequestFactory - -from nose.tools import ok_ -from rest_framework.generics import GenericAPIView -from rest_framework.request import Request -from rest_framework.settings import api_settings - -from amo.tests import TestCase -from mkt.access.middleware import ACLMiddleware -from mkt.collections.authorization import (CanBeHeroAuthorization, - CuratorAuthorization, - StrictCuratorAuthorization) -from mkt.collections.tests import CollectionTestMixin -from mkt.site.fixtures import fixture -from mkt.users.models import UserProfile - - -class TestCuratorAuthorization(CollectionTestMixin, TestCase): - auth_class = CuratorAuthorization - fixtures = fixture('user_2519') - - def setUp(self): - super(TestCuratorAuthorization, self).setUp() - self.collection = self.make_collection() - self.auth = self.auth_class() - self.user = UserProfile.objects.get(pk=2519) - self.profile = self.user - self.view = GenericAPIView() - - def give_permission(self): - self.grant_permission(self.profile, 'Collections:Curate') - - def make_curator(self): - self.collection.add_curator(self.profile) - - def request(self, verb): - request = getattr(RequestFactory(), verb.lower())('/') - request.user = self.user - ACLMiddleware().process_request(request) - return request - - def is_authorized(self, request): - return self.auth.has_permission(request, self.view) - - def is_authorized_object(self, request): - return self.auth.has_object_permission(request, self.view, - self.collection) - - def test_get_list(self): - ok_(self.is_authorized(self.request('GET'))) - - def test_get_list_permission(self): - self.give_permission() - ok_(self.is_authorized(self.request('GET'))) - - def test_post_list(self): - ok_(not self.is_authorized(self.request('POST'))) - - def test_post_list_permission(self): - self.give_permission() - ok_(self.is_authorized(self.request('POST'))) - - def test_delete_list(self): - ok_(not self.is_authorized(self.request('DELETE'))) - - def test_delete_list_permission(self): - self.give_permission() - ok_(self.is_authorized(self.request('DELETE'))) - - def test_get_detail(self): - ok_(self.is_authorized_object(self.request('GET'))) - - def test_get_detail_permission(self): - self.give_permission() - ok_(self.is_authorized_object(self.request('GET'))) - - def test_get_detail_curator(self): - self.make_curator() - ok_(self.is_authorized_object(self.request('GET'))) - - def test_get_detail_permission_curator(self): - self.give_permission() - self.make_curator() - ok_(self.is_authorized_object(self.request('GET'))) - - def test_post_detail(self): - ok_(not self.is_authorized_object(self.request('POST'))) - - def test_post_detail_permission(self): - self.give_permission() - ok_(self.is_authorized_object(self.request('POST'))) - - def test_post_detail_curator(self): - self.make_curator() - ok_(self.is_authorized_object(self.request('POST'))) - - def test_post_detail_permission_curator(self): - self.give_permission() - self.make_curator() - ok_(self.is_authorized_object(self.request('POST'))) - - def test_delete_detail(self): - ok_(not self.is_authorized_object(self.request('DELETE'))) - - def test_delete_detail_permission(self): - self.give_permission() - ok_(self.is_authorized_object(self.request('DELETE'))) - - def test_delete_detail_curator(self): - self.make_curator() - ok_(not self.is_authorized_object(self.request('DELETE'))) - - def test_delete_detail_permission_curator(self): - self.give_permission() - self.make_curator() - ok_(self.is_authorized_object(self.request('DELETE'))) - - -class TestStrictCuratorAuthorization(TestCuratorAuthorization): - auth_class = StrictCuratorAuthorization - - def test_get_list(self): - ok_(not self.is_authorized(self.request('GET'))) - - def test_get_detail(self): - ok_(not self.is_authorized_object(self.request('GET'))) - - -class TestCanBeHeroAuthorization(CollectionTestMixin, TestCase): - enforced_verbs = ['POST', 'PUT'] - fixtures = fixture('user_2519') - - def setUp(self): - super(TestCanBeHeroAuthorization, self).setUp() - self.collection = self.make_collection() - self.auth = CanBeHeroAuthorization() - self.user = UserProfile.objects.get(pk=2519) - self.profile = self.user - self.view = GenericAPIView() - - def give_permission(self): - self.grant_permission(self.profile, 'Collections:Curate') - - def is_authorized_object(self, request): - return self.auth.has_object_permission(request, self.view, - self.collection) - - def request(self, verb, qs=None, content_type='application/json', - encoder=json.dumps, **data): - if not qs: - qs = '' - request = getattr(RequestFactory(), verb.lower()) - request = request('/?' + qs, content_type=content_type, - data=encoder(data) if data else '') - request.user = self.user - ACLMiddleware().process_request(request) - return Request(request, parsers=[parser_cls() for parser_cls in - api_settings.DEFAULT_PARSER_CLASSES]) - - def test_unenforced(self): - """ - Should always pass for GET requests. - """ - ok_(self.is_authorized_object(self.request('GET'))) - - def test_no_qs_modification(self): - """ - Non-GET requests should not be rejected if there is a can_be_true - querystring param (which hypothetically shouldn't do anything). - - We're effectively testing that request.GET doesn't bleed into - request.POST. - """ - self.give_permission() - for verb in self.enforced_verbs: - request = self.request(verb, qs='can_be_hero=1') - ok_(not self.auth.hero_field_modified(request), verb) - - def test_change_permission(self): - """ - Should pass if the user is attempting to modify the can_be_hero field - and has the permission. - """ - self.give_permission() - for verb in self.enforced_verbs: - request = self.request(verb, can_be_hero=True) - ok_(self.auth.hero_field_modified(request), verb) - - def test_change_permission_urlencode(self): - """ - Should pass if the user is attempting to modify the can_be_hero field - and has the permission. - """ - self.give_permission() - for verb in self.enforced_verbs: - request = self.request(verb, encoder=urlencode, - content_type='application/x-www-form-urlencoded', - can_be_hero=True) - ok_(self.auth.hero_field_modified(request), verb) - - def test_no_change_no_permission(self): - """ - Should pass if the user does not have the permission and is not - attempting to modify the can_be_hero field. - """ - for verb in self.enforced_verbs: - request = self.request(verb) - ok_(self.is_authorized_object(request), verb) - - def test_no_change(self): - """ - Should pass if the user does have the permission and is not attempting - to modify the can_be_hero field. - """ - self.give_permission() - for verb in self.enforced_verbs: - request = self.request(verb) - ok_(self.is_authorized_object(request), verb) - - def test_post_change_no_permission(self): - """ - Should not pass if the user is attempting to modify the can_be_hero - field without the permission. - """ - for verb in self.enforced_verbs: - request = self.request(verb, can_be_hero=True) - ok_(not self.is_authorized_object(request), verb) diff --git a/mkt/collections/tests/test_fields.py b/mkt/collections/tests/test_fields.py deleted file mode 100644 index 311cc00c7e1..00000000000 --- a/mkt/collections/tests/test_fields.py +++ /dev/null @@ -1,32 +0,0 @@ -from django.core import exceptions - -import amo.tests - -from mkt.collections.fields import ColorField - - -class TestColorField(amo.tests.TestCase): - def setUp(self): - self.field = ColorField() - - def test_validation_letters_after_f(self): - with self.assertRaises(exceptions.ValidationError): - self.field.validate('#GGGGGG', None) - - def test_validation_too_short(self): - with self.assertRaises(exceptions.ValidationError): - self.field.validate('#00000', None) - - def test_validation_no_pound(self): - with self.assertRaises(exceptions.ValidationError): - self.field.validate('FF00FF', None) - - def should_pass(self, val): - try: - self.field.validate(val, None) - except exceptions.ValidationError: - self.fail('Value "%s" should pass validation.') - - def test_validation_passes(self): - for value in ['#010101', '#FF00FF', '#FFFFFF']: - self.should_pass(value) diff --git a/mkt/collections/tests/test_managers.py b/mkt/collections/tests/test_managers.py deleted file mode 100644 index 20803d663d8..00000000000 --- a/mkt/collections/tests/test_managers.py +++ /dev/null @@ -1,28 +0,0 @@ -from nose.tools import ok_ - -import amo.tests - -from mkt.collections.constants import COLLECTIONS_TYPE_BASIC -from mkt.collections.models import Collection - - -class TestPublicCollectionsManager(amo.tests.TestCase): - - def setUp(self): - self.public_collection = Collection.objects.create(**{ - 'name': 'Public', - 'description': 'The public one', - 'is_public': True, - 'collection_type': COLLECTIONS_TYPE_BASIC - }) - self.private_collection = Collection.objects.create(**{ - 'name': 'Private', - 'description': 'The private one', - 'is_public': False, - 'collection_type': COLLECTIONS_TYPE_BASIC - }) - - def test_public(self): - qs = Collection.public.all() - ok_(self.public_collection in qs) - ok_(self.private_collection not in qs) diff --git a/mkt/collections/tests/test_models.py b/mkt/collections/tests/test_models.py deleted file mode 100644 index dc5866ebac9..00000000000 --- a/mkt/collections/tests/test_models.py +++ /dev/null @@ -1,179 +0,0 @@ -from mock import patch -from nose.tools import eq_ - -import amo.tests -from mkt.collections.constants import COLLECTIONS_TYPE_FEATURED -from mkt.collections.models import Collection, CollectionMembership - - -class TestCollection(amo.tests.TestCase): - - def setUp(self): - self.collection_data = { - 'background_color': '#FF00FF', - 'collection_type': COLLECTIONS_TYPE_FEATURED, - 'description': 'A collection of my favourite games', - 'name': 'My Favourite Games', - 'slug': 'my-favourite-games', - 'text_color': '#00FF00', - } - self.collection = Collection.objects.create(**self.collection_data) - - def test_save(self): - self.collection = Collection.objects.all()[0] - self.collection.save() - - @patch('mkt.collections.models.index_webapps.delay') - def _add_apps(self, mocked_index_webapps): - for app in self.apps: - self.collection.add_app(app) - mocked_index_webapps.assert_called_with([app.pk]) - - def _generate_apps(self): - self.apps = [amo.tests.app_factory() for n in xrange(1, 5)] - - def test_collection(self): - for name, value in self.collection_data.iteritems(): - eq_(self.collection_data[name], getattr(self.collection, name)) - - def test_collection_no_colors(self): - self.collection_data.pop('background_color') - self.collection_data.pop('text_color') - self.collection_data['slug'] = 'favorite-games-2' - self.collection = Collection.objects.create(**self.collection_data) - self.test_collection() - - @patch('mkt.collections.models.index_webapps.delay') - def test_add_app_order_override(self, mocked_index_webapps): - self._generate_apps() - - added = self.collection.add_app(self.apps[1], order=3) - mocked_index_webapps.assert_called_with([self.apps[1].pk]) - eq_(added.order, 3) - eq_(added.app, self.apps[1]) - eq_(added.collection, self.collection) - - added = self.collection.add_app(self.apps[2], order=1) - mocked_index_webapps.assert_called_with([self.apps[2].pk]) - eq_(added.order, 1) - eq_(added.app, self.apps[2]) - eq_(added.collection, self.collection) - - eq_(list(self.collection.apps()), [self.apps[2], self.apps[1]]) - - def test_apps(self): - self._generate_apps() - - # First fetch the apps. Depending CACHE_EMPTY_QUERYSETS an empty list - # will be cached, or not. - self.assertSetEqual(self.collection.apps(), []) - eq_(list(CollectionMembership.objects.values_list('order', flat=True)), - []) - - # Add an app and re-check the apps list. Regardless of whether caching - # took place in the previous step, we should get a new, up to date apps - # list. - self.collection.add_app(self.apps[0]) - self.assertSetEqual(self.collection.apps(), [self.apps[0]]) - eq_(list(CollectionMembership.objects.values_list('order', flat=True)), - [0]) - - # Add an app again. This time we know for sure caching took place in - # the previous step, and we still want to get the new, up to date apps - # list. - self.collection.add_app(self.apps[1]) - self.assertSetEqual(self.collection.apps(), - [self.apps[0], self.apps[1]]) - eq_(list(CollectionMembership.objects.values_list('order', flat=True)), - [0, 1]) - - # Add and test the rest of the apps in one go. - self.collection.add_app(self.apps[2]) - self.collection.add_app(self.apps[3]) - self.assertSetEqual(self.collection.apps(), self.apps) - eq_(list(CollectionMembership.objects.values_list('order', flat=True)), - [0, 1, 2, 3]) - - def test_remove_apps(self): - self._generate_apps() - self._add_apps() - self.assertSetEqual(self.collection.apps(), self.apps) - self.collection.remove_app(self.apps[0]) - self.assertSetEqual(self.collection.apps(), - [self.apps[1], self.apps[2], self.apps[3]]) - eq_(list(CollectionMembership.objects.values_list('order', flat=True)), - [1, 2, 3]) - self.collection.remove_app(self.apps[2]) - self.assertSetEqual(self.collection.apps(), - [self.apps[1], self.apps[3]]) - eq_(list(CollectionMembership.objects.values_list('order', flat=True)), - [1, 3]) - - @patch('mkt.collections.models.index_webapps.delay') - def test_apps_reorder(self, mocked_index_webapps): - self._generate_apps() - self._add_apps() - reordered_pks = [self.apps[3].pk, self.apps[2].pk, - self.apps[0].pk, self.apps[1].pk] - self.collection.reorder(reordered_pks) - self.assertSetEqual(self.collection.apps().values_list('pk', flat=True), - reordered_pks) - mocked_index_webapps.assert_called_with(reordered_pks) - - def test_app_deleted(self): - collection = self.collection - app = amo.tests.app_factory() - collection.add_app(app) - self.assertSetEqual(collection.apps(), [app]) - self.assertSetEqual(collection.collectionmembership_set.all(), - [CollectionMembership.objects.get(collection=collection, app=app)]) - - app.delete() - - self.assertSetEqual(collection.apps(), []) - self.assertSetEqual(collection.collectionmembership_set.all(), []) - - def test_app_disabled_by_user(self): - collection = self.collection - app = amo.tests.app_factory() - collection.add_app(app) - self.assertSetEqual(collection.apps(), [app]) - self.assertSetEqual(collection.collectionmembership_set.all(), - [CollectionMembership.objects.get(collection=collection, app=app)]) - - app.update(disabled_by_user=True) - - self.assertSetEqual(collection.apps(), []) - - # The collection membership still exists here, the app is not deleted, - # only disabled. - self.assertSetEqual(collection.collectionmembership_set.all(), - [CollectionMembership.objects.get(collection=collection, app=app)]) - - def test_app_pending(self): - collection = self.collection - app = amo.tests.app_factory() - collection.add_app(app) - self.assertSetEqual(collection.apps(), [app]) - self.assertSetEqual(collection.collectionmembership_set.all(), - [CollectionMembership.objects.get(collection=collection, app=app)]) - - app.update(status=amo.STATUS_PENDING) - - self.assertSetEqual(collection.apps(), []) - - # The collection membership still exists here, the app is not deleted, - # just not public. - self.assertSetEqual(collection.collectionmembership_set.all(), - [CollectionMembership.objects.get(collection=collection, app=app)]) - - def test_mixed_ordering(self): - self._generate_apps() - - extra_app = amo.tests.app_factory() - added = self.collection.add_app(extra_app, order=3) - eq_(added.order, 3) - self.assertSetEqual(self.collection.apps(), [extra_app]) - self._add_apps() - eq_(list(CollectionMembership.objects.values_list('order', flat=True)), - [3, 4, 5, 6, 7]) diff --git a/mkt/collections/tests/test_serializers.py b/mkt/collections/tests/test_serializers.py deleted file mode 100644 index b595e0ec7ef..00000000000 --- a/mkt/collections/tests/test_serializers.py +++ /dev/null @@ -1,398 +0,0 @@ -# -*- coding: utf-8 -*- -import hashlib -import json - -from django.contrib.auth.models import AnonymousUser -from django.test.client import RequestFactory -from django.test.utils import override_settings - -from nose.tools import eq_, ok_ -from rest_framework import serializers - -import amo -import amo.tests -import mkt -from mkt.collections.constants import (COLLECTIONS_TYPE_BASIC, - COLLECTIONS_TYPE_OPERATOR) -from mkt.collections.models import Collection, CollectionMembership -from mkt.collections.serializers import (CollectionMembershipField, - CollectionSerializer, - DataURLImageField) -from mkt.constants.features import FeatureProfile -from mkt.search.views import FeaturedSearchView -from mkt.site.fixtures import fixture -from mkt.users.models import UserProfile -from mkt.webapps.models import AddonUser -from mkt.webapps.serializers import SimpleAppSerializer - - -class CollectionDataMixin(object): - collection_data = { - 'collection_type': COLLECTIONS_TYPE_BASIC, - 'name': {'en-US': u'A collection of my favourite gàmes'}, - 'slug': 'my-favourite-games', - 'description': {'en-US': u'A collection of my favourite gamés'}, - } - - -class BaseTestCollectionMembershipField(object): - - def setUp(self): - self.collection = Collection.objects.create(**self.collection_data) - self.app = amo.tests.app_factory() - self.app.addondevicetype_set.get_or_create( - device_type=amo.DEVICE_GAIA.id) - self.collection.add_app(self.app, order=1) - self.field = CollectionMembershipField() - self.field.context = {} - self.membership = CollectionMembership.objects.all()[0] - self.profile = FeatureProfile(apps=True).to_signature() - - def get_request(self, data=None): - if data is None: - data = {} - request = RequestFactory().get('/', data) - request.REGION = mkt.regions.RESTOFWORLD - request.API = True - request.user = AnonymousUser() - return request - - def test_to_native(self): - self.app2 = amo.tests.app_factory() - self.collection.add_app(self.app2) - apps = [self.app, self.app2] - request = self.get_request({}) - resource = SimpleAppSerializer(apps) - resource.context = {'request': request} - self.field.context['request'] = request - data = self.field.to_native(self.collection.apps()) - eq_(len(data), 2) - eq_(data[0]['id'], int(self.app.pk)) - eq_(data[0]['resource_uri'], self.app.get_api_url(pk=self.app.pk)) - eq_(data[1]['id'], int(self.app2.id)) - eq_(data[1]['resource_uri'], self.app2.get_api_url(pk=self.app2.pk)) - - def _field_to_native_profile(self, **kwargs): - query = {'pro': '0.0', 'dev': 'firefoxos'} - query.update(kwargs) - request = self.get_request(query) - self.field.parent = self.collection - self.field.source = 'apps' - self.field.context['request'] = request - - return self.field.field_to_native(self.collection, 'apps') - - def test_ordering(self): - self.app2 = amo.tests.app_factory() - self.app2.addondevicetype_set.get_or_create( - device_type=amo.DEVICE_GAIA.id) - self.collection.add_app(self.app2, order=0) - self.app3 = amo.tests.app_factory() - self.app3.addondevicetype_set.get_or_create( - device_type=amo.DEVICE_GAIA.id) - self.collection.add_app(self.app3) - result = self._field_to_native_profile() - eq_(len(result), 3) - eq_(int(result[0]['id']), self.app2.id) - eq_(int(result[1]['id']), self.app.id) - eq_(int(result[2]['id']), self.app3.id) - - def test_app_delete(self): - self.app.delete() - result = self._field_to_native_profile() - eq_(len(result), 0) - - def test_app_disable(self): - self.app.update(disabled_by_user=True) - result = self._field_to_native_profile() - eq_(len(result), 0) - - def test_app_pending(self): - self.app.update(status=amo.STATUS_PENDING) - result = self._field_to_native_profile() - eq_(len(result), 0) - - def test_field_to_native_profile(self): - result = self._field_to_native_profile(pro=self.profile) - eq_(len(result), 1) - eq_(int(result[0]['id']), self.app.id) - - def test_field_to_native_profile_mismatch(self): - self.app.current_version.features.update(has_geolocation=True) - result = self._field_to_native_profile(pro=self.profile) - eq_(len(result), 0) - - def test_field_to_native_invalid_profile(self): - result = self._field_to_native_profile(pro='muahahah') - # Ensure that no filtering took place. - eq_(len(result), 1) - eq_(int(result[0]['id']), self.app.id) - - def test_field_to_native_device_filter(self): - result = self._field_to_native_profile(pro='muahahah', dev='android', - device='mobile') - eq_(len(result), 0) - - -class TestCollectionMembershipField(BaseTestCollectionMembershipField, - CollectionDataMixin, amo.tests.TestCase): - pass - - -class TestCollectionMembershipFieldES(BaseTestCollectionMembershipField, - CollectionDataMixin, - amo.tests.ESTestCase): - """ - Same tests as TestCollectionMembershipField above, but we need a - different setUp and more importantly, we need to force a sync refresh - in ES when we modify our app. - """ - - fixtures = fixture('user_2519') - - def setUp(self): - super(TestCollectionMembershipFieldES, self).setUp() - self.field.context['view'] = FeaturedSearchView() - self.user = UserProfile.objects.get(pk=2519) - AddonUser.objects.create(addon=self.app, user=self.user) - self.refresh('webapp') - - def _field_to_native_profile(self, **kwargs): - """ - Like _field_to_native_profile in BaseTestCollectionMembershipField, - but calling field_to_native_es directly. - """ - query = {'pro': '0.0', 'dev': 'firefoxos'} - query.update(kwargs) - request = self.get_request(query) - self.field.context['request'] = request - return self.field.field_to_native_es(self.collection, request) - - def test_field_to_native_profile_mismatch(self): - self.app.current_version.features.update(has_geolocation=True) - # FIXME: a simple refresh() wasn't enough, don't we reindex apps when - # feature profiles change ? Investigate. - self.reindex(self.app.__class__, 'webapp') - result = self._field_to_native_profile(pro=self.profile) - eq_(len(result), 0) - - def test_ordering(self): - self.app2 = amo.tests.app_factory() - self.collection.add_app(self.app2, order=0) - self.app3 = amo.tests.app_factory() - self.collection.add_app(self.app3) - - # Extra app not belonging to a collection. - amo.tests.app_factory() - - # Extra collection that has the apps in different positions. - extra_collection = Collection.objects.create(**self.collection_data) - extra_collection.add_app(self.app3) - extra_collection.add_app(self.app2) - extra_collection.add_app(self.app) - - # Force refresh in ES. - self.refresh('webapp') - - request = self.get_request() - self.field.context['request'] = request - result = self.field.field_to_native_es(self.collection, request) - - eq_(len(result), 3) - eq_(int(result[0]['id']), self.app2.id) - eq_(int(result[1]['id']), self.app.id) - eq_(int(result[2]['id']), self.app3.id) - - def test_app_delete(self): - self.app.delete() - self.refresh('webapp') - result = self._field_to_native_profile() - eq_(len(result), 0) - - def test_app_disable(self): - self.app.update(disabled_by_user=True) - self.refresh('webapp') - result = self._field_to_native_profile() - eq_(len(result), 0) - - def test_app_pending(self): - self.app.update(status=amo.STATUS_PENDING) - self.refresh('webapp') - result = self._field_to_native_profile() - eq_(len(result), 0) - - -class TestCollectionSerializer(CollectionDataMixin, amo.tests.TestCase): - - def setUp(self): - minimal_context = { - 'request': RequestFactory().get('/whatever') - } - minimal_context['request'].user = AnonymousUser() - self.collection = Collection.objects.create(**self.collection_data) - self.serializer = CollectionSerializer(self.collection, - context=minimal_context) - - def test_metadata_is_serialized_to_json(self): - ok_(json.dumps(self.serializer.metadata())) - - def test_to_native(self, apps=None): - if apps: - for app in apps: - self.collection.add_app(app) - else: - apps = [] - - data = self.serializer.to_native(self.collection) - for name, value in self.collection_data.iteritems(): - eq_(self.collection_data[name], data[name]) - self.assertSetEqual(data.keys(), [ - 'apps', 'author', 'background_color', 'carrier', 'category', - 'collection_type', 'default_language', 'description', 'id', - 'image', 'is_public', 'name', 'region', 'slug', 'text_color' - ]) - for order, app in enumerate(apps): - eq_(data['apps'][order]['slug'], app.app_slug) - return data - - def test_to_native_operator(self): - self.collection.update(collection_type=COLLECTIONS_TYPE_OPERATOR) - data = self.serializer.to_native(self.collection) - ok_('can_be_hero' in data.keys()) - - @override_settings(STATIC_URL='https://testserver-cdn/') - def test_image(self): - data = self.serializer.to_native(self.collection) - eq_(data['image'], None) - self.collection.update(image_hash='bbbbbb') - data = self.serializer.to_native(self.collection) - self.assertApiUrlEqual(data['image'], - '/rocketfuel/collections/%s/image.png?bbbbbb' % self.collection.pk, - scheme='https', netloc='testserver-cdn') - - def test_wrong_default_language_serialization(self): - # The following is wrong because we only accept the 'en-us' form. - data = {'default_language': u'en_US'} - serializer = CollectionSerializer(instance=self.collection, data=data, - partial=True) - eq_(serializer.is_valid(), False) - ok_('default_language' in serializer.errors) - - def test_name_required(self): - data = { - 'description': u'some description', - 'collection_type': u'1' - } - serializer = CollectionSerializer(instance=self.collection, data=data) - eq_(serializer.is_valid(), False) - ok_('name' in serializer.errors) - - def test_name_cannot_be_empty(self): - data = { - 'name': u'' - } - serializer = CollectionSerializer(instance=self.collection, data=data, - partial=True) - eq_(serializer.is_valid(), False) - ok_('name' in serializer.errors) - eq_(serializer.errors['name'], - [u'The field must have a length of at least 1 characters.']) - - def test_name_all_locales_cannot_be_empty(self): - data = { - 'name': { - 'fr': u'', - 'en-US': u'' - } - } - serializer = CollectionSerializer(instance=self.collection, data=data, - partial=True) - eq_(serializer.is_valid(), False) - ok_('name' in serializer.errors) - - def test_name_one_locale_must_be_non_empty(self): - data = { - 'name': { - 'fr': u'', - 'en-US': u'Non-Empty Name' - } - } - serializer = CollectionSerializer(instance=self.collection, data=data, - partial=True) - eq_(serializer.is_valid(), True) - - def test_translation_deserialization(self): - data = { - 'name': u'¿Dónde está la biblioteca?' - } - serializer = CollectionSerializer(instance=self.collection, data=data, - partial=True) - eq_(serializer.errors, {}) - ok_(serializer.is_valid()) - - def test_translation_deserialization_multiples_locales(self): - data = { - 'name': { - 'fr': u'Chat grincheux…', - 'en-US': u'Grumpy Cat...' - } - } - serializer = CollectionSerializer(instance=self.collection, data=data, - partial=True) - eq_(serializer.errors, {}) - ok_(serializer.is_valid()) - - def test_empty_choice_deserialization(self): - # Build data from existing object. - data = self.serializer.to_native(self.collection) - data.pop('id') - # Emulate empty values passed via POST. - data.update({'carrier': '', 'region': ''}) - - instance = self.serializer.from_native(data, None) - eq_(self.serializer.errors, {}) - ok_(self.serializer.is_valid()) - eq_(instance.region, None) - eq_(instance.carrier, None) - - def test_to_native_with_apps(self): - apps = [amo.tests.app_factory() for n in xrange(1, 5)] - data = self.test_to_native(apps=apps) - keys = data['apps'][0].keys() - ok_('name' in keys) - ok_('id' in keys) - - def validate(self, **kwargs): - return self.serializer.validate(kwargs) - - def test_validation_operatorshelf_category(self): - category = 'games' - ok_(self.validate(collection_type=COLLECTIONS_TYPE_BASIC, - category=category)) - ok_(self.validate(collection_type=COLLECTIONS_TYPE_OPERATOR)) - with self.assertRaises(serializers.ValidationError): - self.validate(collection_type=COLLECTIONS_TYPE_OPERATOR, - category=category) - - -IMAGE_DATA = """ -R0lGODlhKAAoAPMAAP////vzBf9kA90JB/IIhEcApQAA0wKr6h+3FABkElYsBZBxOr+/v4CAgEBA -QAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/h1HaWZCdWlsZGVyIDAuMiBieSBZdmVzIFBpZ3VldAAh -+QQECgD/ACwAAAAAKAAoAEMEx5DJSSt9z+rNcfgf5oEBxlVjWIreQ77wqqWrW8e4fKJ2ru9ACS2U -CW6GIBaSOOu9lMknK2dqrog2pYhp7Dir3fAIHN4tk8XyBKmFkU9j0tQnT6+d2K2qrnen2W10MW93 -WIZogGJ4dIRqZ41qTZCRXpOUPHWXXjiWioKdZniBaI6LNX2ZQS1aLnOcdhYpPaOfsAxDrXOiqKlL -rL+0mb5Qg7ypQru5Z1S2yIiHaK9Aq1lfxFxGLYe/P2XLUprOzOGY4ORW3edNkREAIfkEBAoA/wAs -AAAAACgAKABDBMqQyUkrfc/qzXH4YBhiXOWNAaZ6q+iS1vmps1y3Y1aaj/vqu6DEVhN2einfipgC -XpA/HNRHbW5YSFpzmXUaY1PYd3wSj3fM3JlXrZpLsrIc9wNHW71pGyRmcpM0dHUaczc5WnxeaHp7 -b2sMaVaPQSuTZCqWQjaOmUOMRZ2ee5KTkVSci22CoJRQiDeviXBhh1yfrBNEWH+jspC3S3y9dWnB -sb1muru1x6RshlvMeqhP0U3Sal8s0LZ5ikamItTat7ihft+hv+bqYI8RADs= -""" - - -class TestDataURLImageField(CollectionDataMixin, amo.tests.TestCase): - - def test_from_native(self): - data, hash_ = DataURLImageField().from_native( - 'data:image/gif;base64,' + IMAGE_DATA) - eq_(hash_, hashlib.md5(IMAGE_DATA.decode('base64')).hexdigest()[:8]) - eq_(data.read(), IMAGE_DATA.decode('base64')) diff --git a/mkt/collections/tests/test_tasks.py b/mkt/collections/tests/test_tasks.py deleted file mode 100644 index 43687e19133..00000000000 --- a/mkt/collections/tests/test_tasks.py +++ /dev/null @@ -1,51 +0,0 @@ -import json -import mock -from tempfile import mkdtemp - -from django.test.utils import override_settings -from nose.tools import eq_ - -import amo -import amo.tests -from mkt.collections.models import Collection -from mkt.collections.tasks import dump_collection, dump_collections -from mkt.webapps.tasks import rm_directory -from mkt.site.fixtures import fixture - -temp_directory = mkdtemp() - - -@override_settings(DUMPED_APPS_PATH=temp_directory) -class TestDumpCollections(amo.tests.TestCase): - fixtures = fixture('webapp_337141', 'collection_81721') - - def get_collection(self): - return Collection.objects.get(pk=81721) - - def tearDown(self): - rm_directory(temp_directory) - - def test_dump_collections(self): - filename = dump_collections([81721])[0] - collection_json = json.load(open(filename, 'r')) - eq_(collection_json['id'], 81721) - eq_(collection_json['slug'], 'public-apps') - - def test_dump_collection(self): - collection = self.get_collection() - filename = dump_collection(collection) - collection_json = json.load(open(filename, 'r')) - eq_(collection_json['id'], 81721) - eq_(collection_json['slug'], 'public-apps') - - @mock.patch('mkt.collections.tasks.dump_collection') - def test_dumps_public_collection(self, dump_collection): - dump_collections([81721]) - assert dump_collection.called - - @mock.patch('mkt.collections.tasks.dump_collection') - def test_doesnt_dump_public_collection(self, dump_collection): - collection = self.get_collection() - collection.update(is_public=False) - dump_collections([81721]) - assert not dump_collection.called diff --git a/mkt/collections/tests/test_views.py b/mkt/collections/tests/test_views.py deleted file mode 100644 index 9ae595fc419..00000000000 --- a/mkt/collections/tests/test_views.py +++ /dev/null @@ -1,1707 +0,0 @@ -# -*- coding: utf-8 -*- -import hashlib -import json -import os -from random import shuffle -from urlparse import urlparse - -from django.conf import settings -from django.core.files.storage import default_storage as storage -from django.core.urlresolvers import reverse -from django.http import QueryDict -from django.test.utils import override_settings -from django.utils import translation - -from nose import SkipTest -from nose.tools import eq_, ok_ -from PIL import Image -from rest_framework.exceptions import PermissionDenied - -import amo -import amo.tests -import mkt -from amo.utils import slugify -from mkt.api.tests.test_oauth import RestOAuth -from mkt.collections.constants import (COLLECTIONS_TYPE_BASIC, - COLLECTIONS_TYPE_FEATURED, - COLLECTIONS_TYPE_OPERATOR) -from mkt.collections.models import Collection -from mkt.collections.tests.test_serializers import (CollectionDataMixin, - IMAGE_DATA) -from mkt.collections.views import CollectionViewSet -from mkt.site.fixtures import fixture -from mkt.webapps.models import Webapp -from mkt.users.models import UserProfile - - -class BaseCollectionViewSetTest(RestOAuth): - """ - Base class for all CollectionViewSet tests. - """ - fixtures = fixture('user_2519', 'user_999') - - def setUp(self): - super(BaseCollectionViewSetTest, self).setUp() - self.collection_data = { - 'author': u'My Àuthør', - 'background_color': '#FFF000', - 'collection_type': COLLECTIONS_TYPE_BASIC, - 'description': {'en-US': u'A cöllection of my favorite games'}, - 'is_public': True, - 'name': {'en-US': u'My Favorite Gamés'}, - 'slug': u'my-favourite-gamés', - 'text_color': '#000FFF', - } - self.collection = Collection.objects.create(**self.collection_data) - self.apps = [] - self.list_url = reverse('collections-list') - self.user = UserProfile.objects.get(pk=2519) - self.user2 = UserProfile.objects.get(pk=999) - - def setup_unique(self): - """ - Additional setup required to test collection category/region/carrier - uniqueness constraints. - """ - self.category = 'utilities' - self.collection_data = { - 'collection_type': COLLECTIONS_TYPE_FEATURED, - 'name': 'Featured Apps are cool', - 'slug': 'featured-apps-are-cool', - 'description': 'Featured Apps really are the bomb', - 'region': mkt.regions.SPAIN.id, - 'carrier': mkt.carriers.TELEFONICA.id, - 'category': self.category, - 'is_public': True, - } - self.collection = Collection.objects.create(**self.collection_data) - self.grant_permission(self.profile, 'Collections:Curate') - - def create_apps(self, number=1): - """ - Create `number` apps, adding them to `self.apps`. - """ - for n in xrange(0, number): - self.apps.append(amo.tests.app_factory()) - - def add_apps_to_collection(self, *args): - """ - Add each app passed to `*args` to `self.collection`. - """ - for app in args: - self.collection.add_app(app) - - def make_curator(self): - """ - Make the authenticating user a curator on self.collection. - """ - self.collection.add_curator(self.profile) - - def make_publisher(self): - """ - Grant the Collections:Curate permission to the authenticating user. - """ - self.grant_permission(self.profile, 'Collections:Curate') - - def collection_url(self, action, pk): - """ - Return the URL to a collection API endpoint with primary key `pk` to do - action `action`. - """ - return reverse('collections-%s' % action, kwargs={'pk': pk}) - - def create_additional_data(self): - """ - Creates two additional categories and three additional collections. - """ - self.category = 'games' - self.empty_category = 'social' - - collection_data = { - 'collection_type': COLLECTIONS_TYPE_BASIC, - 'description': 'A collection of my favorite spanish games', - 'name': 'My Favorite spanish games', - 'region': mkt.regions.SPAIN.id, - 'carrier': mkt.carriers.UNKNOWN_CARRIER.id, - } - self.collection2 = Collection.objects.create(**collection_data) - - collection_data = { - 'collection_type': COLLECTIONS_TYPE_BASIC, - 'description': 'A collection of my favorite phone games', - 'name': 'My Favorite phone games', - 'carrier': mkt.carriers.TELEFONICA.id, - } - self.collection3 = Collection.objects.create(**collection_data) - - collection_data = { - 'collection_type': COLLECTIONS_TYPE_BASIC, - 'description': 'A collection of my favorite categorized games', - 'name': 'My Favorite categorized games', - 'region': mkt.regions.SPAIN.id, - 'carrier': mkt.carriers.TELEFONICA.id, - 'category': self.category - } - self.collection4 = Collection.objects.create(**collection_data) - - -class TestCollectionViewSetListing(BaseCollectionViewSetTest): - """ - Tests the handling of GET/OPTIONS requests to the list endpoint of - CollectionViewSet. - """ - def listing(self, client): - self.create_apps() - self.add_apps_to_collection(*self.apps) - res = client.get(self.list_url) - data = json.loads(res.content) - eq_(res.status_code, 200) - collection = data['objects'][0] - apps = collection['apps'] - - # Verify that the apps are present in the correct order. - for order, app in enumerate(self.apps): - eq_(apps[order]['slug'], app.app_slug) - - # Verify that the collection metadata is in tact. - for field, value in self.collection_data.iteritems(): - eq_(collection[field], self.collection_data[field]) - - def test_listing(self): - self.listing(self.anon) - - def test_listing_no_perms(self): - self.listing(self.client) - - def test_listing_has_perms(self): - self.make_publisher() - self.listing(self.client) - - def test_options_has_perms(self): - self.make_publisher() - res = self.client.options(self.list_url) - eq_(res.status_code, 200) - - def test_listing_curator(self): - self.make_curator() - self.listing(self.client) - - def test_listing_single_lang(self): - self.collection.name = { - 'fr': u'Basta la pomme de terre frite', - } - self.collection.save() - res = self.client.get(self.list_url, {'lang': 'fr'}) - data = json.loads(res.content) - eq_(res.status_code, 200) - collection = data['objects'][0] - eq_(collection['name'], u'Basta la pomme de terre frite') - eq_(collection['description'], u'A cöllection of my favorite games') - - def test_listing_filter_unowned_hidden(self): - """ - Hidden collections that you do not own should not be returned. - """ - self.create_apps() - self.add_apps_to_collection(*self.apps) - self.collection.update(is_public=False) - res = self.client.get(self.list_url) - data = json.loads(res.content) - eq_(res.status_code, 200) - eq_(len(data['objects']), 0) - - def test_listing_filter_owned_hidden(self): - """ - Hidden collections that you do own should be returned. - """ - self.create_apps() - self.add_apps_to_collection(*self.apps) - self.collection.update(is_public=False) - self.collection.curators.add(self.user) - res = self.client.get(self.list_url) - data = json.loads(res.content) - eq_(res.status_code, 200) - eq_(len(data['objects']), 1) - - def test_listing_pagination(self): - self.create_additional_data() - self.make_publisher() # To be able to see non-public collections. - res = self.client.get(self.list_url, {'limit': 3}) - eq_(res.status_code, 200) - data = json.loads(res.content) - - eq_(len(data['objects']), 3) - eq_(data['objects'][0]['id'], self.collection4.pk) - eq_(data['objects'][1]['id'], self.collection3.pk) - eq_(data['objects'][2]['id'], self.collection2.pk) - eq_(data['meta']['total_count'], 4) - eq_(data['meta']['limit'], 3) - eq_(data['meta']['previous'], None) - eq_(data['meta']['offset'], 0) - next = urlparse(data['meta']['next']) - ok_(next.path.startswith('/api/v1')) - eq_(next.path, self.list_url) - eq_(QueryDict(next.query).dict(), {u'limit': u'3', u'offset': u'3'}) - - res = self.client.get(self.list_url, {'limit': 3, 'offset': 3}) - eq_(res.status_code, 200) - data = json.loads(res.content) - - eq_(len(data['objects']), 1) - eq_(data['objects'][0]['id'], self.collection.pk) - eq_(data['meta']['total_count'], 4) - eq_(data['meta']['limit'], 3) - prev = urlparse(data['meta']['previous']) - ok_(prev.path.startswith('/api/v1')) - eq_(next.path, self.list_url) - eq_(QueryDict(prev.query).dict(), {u'limit': u'3', u'offset': u'0'}) - eq_(data['meta']['offset'], 3) - eq_(data['meta']['next'], None) - - def test_listing_no_filtering(self): - self.create_additional_data() - self.make_publisher() - - res = self.client.get(self.list_url) - eq_(res.status_code, 200) - data = json.loads(res.content) - collections = data['objects'] - eq_(len(collections), 4) - ok_('API-Fallback' not in res) - - def test_listing_filtering_error(self): - res = self.client.get(self.list_url, {'region': 'whateverdude'}) - eq_(res.status_code, 400) - data = json.loads(res.content) - eq_(data['detail'], 'Filtering error.') - errors = data['filter_errors'] - ok_(errors['region'][0].startswith('Select a valid choice.')) - - def test_listing_filtering_worldwide(self): - self.create_additional_data() - self.make_publisher() - - row_res = self.client.get(self.list_url, {'region': 'restofworld'}) - row_data = json.loads(row_res.content) - ww_res = self.client.get(self.list_url, {'region': 'worldwide'}) - ww_data = json.loads(ww_res.content) - - eq_(row_res.status_code, 200) - eq_(ww_res.status_code, 200) - eq_(len(row_data['objects']), 2) - eq_(row_data, ww_data) - - def test_listing_filtering_region(self): - self.create_additional_data() - self.make_publisher() - - self.collection.update(region=mkt.regions.PL.id) - - res = self.client.get(self.list_url, - {'region': mkt.regions.SPAIN.slug}) - eq_(res.status_code, 200) - data = json.loads(res.content) - collections = data['objects'] - eq_(len(collections), 2) - eq_(collections[0]['id'], self.collection4.pk) - eq_(collections[1]['id'], self.collection2.pk) - ok_('API-Fallback' not in res) - - def test_listing_filtering_region_id(self): - self.create_additional_data() - self.make_publisher() - - self.collection.update(region=mkt.regions.PL.id) - - res = self.client.get(self.list_url, - {'region': mkt.regions.SPAIN.id}) - eq_(res.status_code, 200) - data = json.loads(res.content) - collections = data['objects'] - eq_(len(collections), 2) - eq_(collections[0]['id'], self.collection4.pk) - eq_(collections[1]['id'], self.collection2.pk) - ok_('API-Fallback' not in res) - - def test_listing_filtering_carrier(self): - self.create_additional_data() - self.make_publisher() - - res = self.client.get(self.list_url, - {'carrier': mkt.carriers.TELEFONICA.slug}) - eq_(res.status_code, 200) - data = json.loads(res.content) - collections = data['objects'] - eq_(len(collections), 2) - eq_(collections[0]['id'], self.collection4.pk) - eq_(collections[1]['id'], self.collection3.pk) - ok_('API-Fallback' not in res) - - def test_listing_filtering_carrier_id(self): - self.create_additional_data() - self.make_publisher() - - res = self.client.get(self.list_url, - {'carrier': mkt.carriers.TELEFONICA.id}) - eq_(res.status_code, 200) - data = json.loads(res.content) - collections = data['objects'] - eq_(len(collections), 2) - eq_(collections[0]['id'], self.collection4.pk) - eq_(collections[1]['id'], self.collection3.pk) - ok_('API-Fallback' not in res) - - def test_listing_filtering_carrier_null(self): - self.create_additional_data() - self.make_publisher() - - res = self.client.get(self.list_url, {'carrier': ''}) - eq_(res.status_code, 200) - data = json.loads(res.content) - collections = data['objects'] - eq_(len(collections), 1) - eq_(collections[0]['id'], self.collection.pk) - ok_('API-Fallback' not in res) - - def test_listing_filtering_carrier_0(self): - self.create_additional_data() - self.make_publisher() - - res = self.client.get(self.list_url, - {'carrier': mkt.carriers.UNKNOWN_CARRIER.id}) - eq_(res.status_code, 200) - data = json.loads(res.content) - collections = data['objects'] - eq_(len(collections), 1) - eq_(collections[0]['id'], self.collection2.pk) - ok_('API-Fallback' not in res) - - def test_listing_filtering_region_null(self): - self.create_additional_data() - self.make_publisher() - - res = self.client.get(self.list_url, {'region': ''}) - eq_(res.status_code, 200) - data = json.loads(res.content) - collections = data['objects'] - eq_(len(collections), 2) - eq_(collections[0]['id'], self.collection3.pk) - eq_(collections[1]['id'], self.collection.pk) - ok_('API-Fallback' not in res) - - def test_listing_filtering_category(self): - self.create_additional_data() - self.make_publisher() - - res = self.client.get(self.list_url, {'cat': self.category}) - eq_(res.status_code, 200) - data = json.loads(res.content) - collections = data['objects'] - eq_(len(collections), 1) - eq_(collections[0]['id'], self.collection4.pk) - ok_('API-Fallback' not in res) - - def test_listing_filtering_category_null(self): - self.create_additional_data() - self.make_publisher() - - res = self.client.get(self.list_url, {'cat': ''}) - eq_(res.status_code, 200) - data = json.loads(res.content) - collections = data['objects'] - eq_(len(collections), 3) - eq_(collections[0]['id'], self.collection3.pk) - eq_(collections[1]['id'], self.collection2.pk) - eq_(collections[2]['id'], self.collection.pk) - ok_('API-Fallback' not in res) - - def test_listing_filtering_category_region_carrier(self): - self.create_additional_data() - self.make_publisher() - - res = self.client.get(self.list_url, { - 'cat': self.category, - 'region': mkt.regions.SPAIN.slug, - 'carrier': mkt.carriers.TELEFONICA.slug - }) - eq_(res.status_code, 200) - data = json.loads(res.content) - collections = data['objects'] - eq_(len(collections), 1) - eq_(collections[0]['id'], self.collection4.pk) - ok_('API-Fallback' not in res) - - def test_listing_filtering_category_region_carrier_fallback(self): - self.create_additional_data() - self.make_publisher() - - # Test filtering with a non-existant category + region + carrier. - # It should fall back on category + carrier + region=NULL, not find - # anything either, then fall back to category + region + carrier=NULL, - # again not find anything, then category + region=NULL + carrier=NULL, - # and stop there, still finding no results. - res = self.client.get(self.list_url, { - 'cat': self.empty_category, - 'region': mkt.regions.SPAIN.slug, - 'carrier': mkt.carriers.SPRINT.slug - }) - eq_(res.status_code, 200) - data = json.loads(res.content) - collections = data['objects'] - eq_(len(collections), 0) - eq_(res['API-Fallback'], 'region,carrier') - - def test_listing_filtering_nonexistant_carrier(self): - self.create_additional_data() - self.make_publisher() - - Collection.objects.all().update(carrier=mkt.carriers.TELEFONICA.id) - self.collection.update(region=mkt.regions.SPAIN.id, carrier=None) - - # Test filtering with a region+carrier that doesn't match any - # Collection. It should fall back on region=NULL+carrier, not find - # anything, then fallback on carrier=NULL+region and find the - # Collection left in spain with no carrier. - res = self.client.get(self.list_url, { - 'region': mkt.regions.SPAIN.slug, - 'carrier': mkt.carriers.SPRINT.slug - }) - eq_(res.status_code, 200) - data = json.loads(res.content) - collections = data['objects'] - eq_(len(collections), 1) - eq_(collections[0]['id'], self.collection.pk) - eq_(res['API-Fallback'], 'carrier') - - def test_listing_filtering_nonexistant_carrier_and_region(self): - self.create_additional_data() - self.make_publisher() - - Collection.objects.all().update(carrier=None, region=None, - category='books') - self.collection.update(category=self.category) - - # Test filtering with a non-existant carrier and region. It should - # go through all fallback till ending up filtering on category + - # carrier=NULL + region=NULL. - res = self.client.get(self.list_url, { - 'region': mkt.regions.UK.slug, - 'carrier': mkt.carriers.SPRINT.slug, - 'cat': self.category - }) - eq_(res.status_code, 200) - data = json.loads(res.content) - collections = data['objects'] - eq_(len(collections), 1) - eq_(collections[0]['id'], self.collection.pk) - eq_(res['API-Fallback'], 'region,carrier') - - -class TestCollectionViewSetDetail(BaseCollectionViewSetTest): - """ - Tests the handling of GET requests to a single collection on - CollectionViewSet. - """ - def detail(self, client, collection_id=None): - self.create_apps(number=2) - self.add_apps_to_collection(*self.apps) - url = self.collection_url('detail', - collection_id or self.collection.pk) - res = client.get(url) - data = json.loads(res.content) - eq_(res.status_code, 200) - - # Verify that the collection metadata is in tact. - for field, value in self.collection_data.iteritems(): - eq_(data[field], self.collection_data[field]) - - # Verify that the apps are present in the correct order. - for order, app in enumerate(self.apps): - eq_(data['apps'][order]['slug'], app.app_slug) - - return res, data - - def test_detail_filtering(self): - self.collection.update(region=mkt.regions.SPAIN.id) - url = self.collection_url('detail', self.collection.pk) - res = self.client.get(url, { - 'region': mkt.regions.RESTOFWORLD.slug - }) - # Filtering should not be applied. - eq_(res.status_code, 200) - data = json.loads(res.content) - eq_(data['id'], self.collection.pk) - ok_('API-Fallback' not in res) - - def test_detail(self): - res, data = self.detail(self.anon) - ok_(not data['image']) - - @override_settings(STATIC_URL='https://testserver-cdn/') - def test_detail_image(self): - storage.open(self.collection.image_path(), 'w').write(IMAGE_DATA) - self.collection.update(image_hash='bbbbbb') - res, data = self.detail(self.anon) - self.assertApiUrlEqual(data['image'], - '/rocketfuel/collections/%s/image.png?bbbbbb' % self.collection.pk, - scheme='https', netloc='testserver-cdn') - - def test_detail_slug(self): - self.detail(self.client, collection_id=self.collection.slug) - - def test_detail_slug_anon(self): - self.detail(self.anon, collection_id=self.collection.slug) - - def test_detail_no_perms(self): - self.detail(self.client) - - def test_detail_has_perms(self): - self.make_publisher() - self.detail(self.client) - - def test_detail_curator(self): - self.make_curator() - self.detail(self.client) - - -class TestCollectionViewSetCreate(BaseCollectionViewSetTest): - """ - Tests the handling of POST requests to the list endpoint of - CollectionViewSet. - """ - def create(self, client): - res = client.post(self.list_url, json.dumps(self.collection_data)) - data = json.loads(res.content) - return res, data - - def test_create_anon(self): - res, data = self.create(self.anon) - eq_(res.status_code, 403) - - def test_create_no_perms(self): - res, data = self.create(self.client) - eq_(res.status_code, 403) - - def test_create_curator(self): - self.make_curator - res, data = self.create(self.client) - eq_(res.status_code, 403) - - def test_create_has_perms(self): - self.make_publisher() - res, data = self.create(self.client) - eq_(res.status_code, 201) - new_collection = Collection.objects.get(pk=data['id']) - ok_(new_collection.pk != self.collection.pk) - - self.collection_data['slug'] = u'my-favourite-gamés-1' - - # Verify that the collection metadata is correct. - keys = self.collection_data.keys() - keys.remove('name') - keys.remove('description') - for field in keys: - eq_(data[field], self.collection_data[field]) - eq_(getattr(new_collection, field), self.collection_data[field]) - - # Test name and description separately as we return the whole dict - # with all translations. - eq_(data['name'], data['name']) - eq_(new_collection.name, data['name']['en-US']) - - eq_(data['description'], data['description']) - eq_(new_collection.description, data['description']['en-US']) - - def test_create_validation_operatorshelf_category(self): - self.category = 'books' - self.make_publisher() - self.collection_data.update({ - 'category': self.category, - 'collection_type': COLLECTIONS_TYPE_OPERATOR - }) - res, data = self.create(self.client) - ok_(res.status_code, 400) - ok_('non_field_errors' in data.keys()) - - def test_create_empty_description_dict_in_default_language(self): - """ - Test that we can't have an empty Translation for the default_language. - """ - # See bug https://bugzilla.mozilla.org/show_bug.cgi?id=915652 - raise SkipTest - self.make_publisher() - self.collection_data = { - 'collection_type': COLLECTIONS_TYPE_BASIC, - 'name': 'whatever', - 'description': {'en-US': ' ', 'fr': 'lol'}, - } - res, data = self.create(self.client) - # The description dict is not empty, but it contains an empty - # translation for en-US, which is incorrect since it's the default - # language (it'll save ok, but then fail when reloading). - # It could work if translation system wasn't insisting on - # loading only the default+current language... - eq_(res.status_code, 400) - ok_('description' in data) - - def test_create_no_colors(self): - self.collection_data['background_color'] = '' - self.collection_data['text_color'] = '' - self.test_create_has_perms() - - def test_create_has_perms_no_type(self): - self.make_publisher() - self.collection_data.pop('collection_type') - res, data = self.create(self.client) - eq_(res.status_code, 400) - - def test_create_has_perms_no_slug(self): - self.make_publisher() - self.collection_data.pop('slug') - res, data = self.create(self.client) - eq_(res.status_code, 201) - eq_(data['slug'], slugify(self.collection_data['name']['en-US'])) - - def test_create_collection_no_author(self): - self.make_publisher() - self.collection_data.pop('author') - res, data = self.create(self.client) - eq_(res.status_code, 201) - new_collection = Collection.objects.get(pk=data['id']) - ok_(new_collection.pk != self.collection.pk) - eq_(new_collection.author, '') - - def test_create_featured_duplicate(self): - """ - Featured Apps & Operator Shelf should not have duplicates for a - region / carrier / category combination. Make sure this is respected - when creating a new collection. - """ - self.setup_unique() - res, data = self.create(self.client) - eq_(res.status_code, 400) - ok_('collection_uniqueness' in data) - - def test_create_featured_duplicate_different_category(self): - """ - Try to create a new collection with the duplicate data from our - featured collection, this time changing the category. - """ - self.setup_unique() - different_category = 'books' - self.collection_data['category'] = different_category - res, data = self.create(self.client) - eq_(res.status_code, 201) - - -class TestCollectionViewSetDelete(BaseCollectionViewSetTest): - """ - Tests the handling of DELETE requests to a single collection on - CollectionViewSet. - """ - def delete(self, client, collection_id=None): - url = self.collection_url('detail', - collection_id or self.collection.pk) - res = client.delete(url) - data = json.loads(res.content) if res.content else None - return res, data - - def test_delete_anon(self): - res, data = self.delete(self.anon) - eq_(res.status_code, 403) - eq_(PermissionDenied.default_detail, data['detail']) - - def test_delete_no_perms(self): - res, data = self.delete(self.client) - eq_(res.status_code, 403) - eq_(PermissionDenied.default_detail, data['detail']) - - def test_delete_curator(self): - self.make_curator() - res, data = self.delete(self.client) - eq_(res.status_code, 403) - eq_(PermissionDenied.default_detail, data['detail']) - - def test_delete_has_perms(self): - self.make_publisher() - res, data = self.delete(self.client) - eq_(res.status_code, 204) - ok_(not data) - - def test_delete_slug(self): - self.make_publisher() - res, data = self.delete(self.client, - collection_id=self.collection.slug) - eq_(res.status_code, 204) - ok_(not data) - - def test_delete_nonexistent(self): - self.make_publisher() - res, data = self.delete(self.client, collection_id=100000) - eq_(res.status_code, 404) - - -class TestCollectionViewSetDuplicate(BaseCollectionViewSetTest): - """ - Tests the `duplicate` action on CollectionViewSet. - """ - def duplicate(self, client, data=None, collection_id=None): - if not data: - data = {} - url = self.collection_url('duplicate', - collection_id or self.collection.pk) - res = client.post(url, json.dumps(data)) - data = json.loads(res.content) - return res, data - - def test_duplicate_anon(self): - res, data = self.duplicate(self.anon) - eq_(res.status_code, 403) - - def test_duplicate_no_perms(self): - res, data = self.duplicate(self.client) - eq_(res.status_code, 403) - - def test_duplicate_curator(self): - self.make_curator() - res, data = self.duplicate(self.client) - eq_(res.status_code, 201) - - def test_duplicate_has_perms(self): - self.make_publisher() - original = self.collection - - res, data = self.duplicate(self.client) - eq_(res.status_code, 201) - new_collection = Collection.objects.get(pk=data['id']) - ok_(new_collection.pk != original.pk) - ok_(new_collection.slug) - ok_(new_collection.slug != original.slug) - - # Verify that the collection metadata is correct. We duplicated - # self.collection, which was created with self.collection_data, so - # use that. - original = self.collection - keys = self.collection_data.keys() - keys.remove('name') - keys.remove('description') - keys.remove('slug') - for field in keys: - eq_(data[field], self.collection_data[field]) - eq_(getattr(new_collection, field), self.collection_data[field]) - eq_(getattr(new_collection, field), getattr(original, field)) - - # Test name and description separately as we return the whole dict - # with all translations. - eq_(data['name'], self.collection_data['name']) - eq_(new_collection.name, data['name']['en-US']) - eq_(new_collection.name, original.name) - - eq_(data['description'], self.collection_data['description']) - eq_(new_collection.description, data['description']['en-US']) - eq_(new_collection.description, original.description) - - def test_duplicate_slug(self): - self.make_publisher() - res, data = self.duplicate(self.client, - collection_id=self.collection.slug) - eq_(res.status_code, 201) - - def test_duplicate_apps(self): - self.make_publisher() - self.create_apps(number=2) - self.add_apps_to_collection(*self.apps) - res, data = self.duplicate(self.client) - eq_(res.status_code, 201) - new_collection = Collection.objects.get(pk=data['id']) - ok_(new_collection.pk != self.collection.pk) - eq_(list(new_collection.apps()), list(self.collection.apps())) - eq_(len(data['apps']), len(self.apps)) - for order, app in enumerate(self.apps): - eq_(int(data['apps'][order]['id']), self.apps[order].pk) - - def test_duplicate_override(self): - self.make_publisher() - override_data = { - 'collection_type': COLLECTIONS_TYPE_OPERATOR, - 'region': mkt.regions.SPAIN.id - } - res, data = self.duplicate(self.client, override_data) - eq_(res.status_code, 201) - new_collection = Collection.objects.get(pk=data['id']) - ok_(new_collection.pk != self.collection.pk) - for key in override_data: - eq_(getattr(new_collection, key), override_data[key]) - ok_(getattr(new_collection, key) != getattr(self.collection, key)) - - # We return slugs always in data, so test that separately. - expected_data = { - 'collection_type': COLLECTIONS_TYPE_OPERATOR, - 'region': mkt.regions.SPAIN.slug - } - for key in expected_data: - eq_(data[key], expected_data[key]) - - def test_duplicate_invalid_data(self): - self.make_publisher() - override_data = { - 'collection_type': COLLECTIONS_TYPE_OPERATOR, - 'region': max(mkt.regions.REGION_IDS) + 1 - } - res, data = self.duplicate(self.client, override_data) - eq_(res.status_code, 400) - - def test_duplicate_featured(self): - self.setup_unique() - res, data = self.duplicate(self.client) - eq_(res.status_code, 400) - ok_('collection_uniqueness' in data) - - def test_duplicate_operator(self): - self.setup_unique() - self.collection.update(collection_type=COLLECTIONS_TYPE_OPERATOR, - carrier=None, category=None) - res, data = self.duplicate(self.client) - eq_(res.status_code, 400) - ok_('collection_uniqueness' in data) - - -class CollectionViewSetChangeAppsMixin(BaseCollectionViewSetTest): - """ - Mixin containing common methods to actions that modify the apps belonging - to a collection. - """ - def add_app(self, client, app_id=None, collection_id=None): - if app_id is None: - self.create_apps() - app_id = self.apps[0].pk - form_data = {'app': app_id} if app_id else {} - url = self.collection_url('add-app', - collection_id or self.collection.pk) - res = client.post(url, json.dumps(form_data)) - data = json.loads(res.content) - return res, data - - def remove_app(self, client, app_id=None, collection_id=None): - if app_id is None: - self.create_apps(number=2) - app_id = self.apps[0].pk - form_data = {'app': app_id} if app_id else {} - url = self.collection_url('remove-app', - collection_id or self.collection.pk) - remove_res = client.post(url, json.dumps(form_data)) - remove_data = (json.loads(remove_res.content) - if remove_res.content else None) - return remove_res, remove_data - - def reorder(self, client, order=None, collection_id=None): - if order is None: - order = {} - url = self.collection_url('reorder', - collection_id or self.collection.pk) - res = client.post(url, json.dumps(order)) - data = json.loads(res.content) - return res, data - - -class TestCollectionViewSetAddApp(CollectionViewSetChangeAppsMixin): - """ - Tests the `add-app` action on CollectionViewSet. - """ - def test_add_app_anon(self): - res, data = self.add_app(self.anon) - eq_(res.status_code, 403) - eq_(PermissionDenied.default_detail, data['detail']) - - def test_add_app_no_perms(self): - res, data = self.add_app(self.client) - eq_(res.status_code, 403) - eq_(PermissionDenied.default_detail, data['detail']) - - def test_add_app_has_perms(self): - eq_(list(self.collection.apps()), []) - self.make_publisher() - res, data = self.add_app(self.client) - eq_(res.status_code, 200) - eq_(list(self.collection.apps()), [self.apps[0]]) - - def test_add_app_slug(self): - eq_(list(self.collection.apps()), []) - self.make_publisher() - res, data = self.add_app(self.client, - collection_id=self.collection.slug) - eq_(res.status_code, 200) - eq_(list(self.collection.apps()), [self.apps[0]]) - - def test_add_app_curator(self): - eq_(list(self.collection.apps()), []) - self.make_curator() - res, data = self.add_app(self.client) - eq_(res.status_code, 200) - eq_(list(self.collection.apps()), [self.apps[0]]) - - def test_add_app_nonexistent(self): - self.make_publisher() - res, data = self.add_app(self.client, app_id=100000) - eq_(res.status_code, 400) - eq_(CollectionViewSet.exceptions['doesnt_exist'], data['detail']) - - def test_add_app_empty(self): - self.make_publisher() - res, data = self.add_app(self.client, app_id=False) - eq_(res.status_code, 400) - eq_(CollectionViewSet.exceptions['not_provided'], data['detail']) - - def test_add_app_duplicate(self): - self.make_publisher() - self.add_app(self.client) - res, data = self.add_app(self.client) - eq_(res.status_code, 400) - eq_(CollectionViewSet.exceptions['already_in'], data['detail']) - - -class TestCollectionViewSetRemoveApp(CollectionViewSetChangeAppsMixin): - """ - Tests the `remove-app` action on CollectionViewSet. - """ - def test_remove_app_anon(self): - res, data = self.remove_app(self.anon) - eq_(res.status_code, 403) - eq_(PermissionDenied.default_detail, data['detail']) - - def test_remove_app_no_perms(self): - res, data = self.remove_app(self.client) - eq_(res.status_code, 403) - eq_(PermissionDenied.default_detail, data['detail']) - - def test_remove_app_has_perms(self): - self.make_publisher() - self.add_app(self.client) - res, data = self.remove_app(self.client) - eq_(res.status_code, 200) - eq_(len(data['apps']), 0) - - def test_remove_app_slug(self): - self.make_publisher() - self.add_app(self.client) - res, data = self.remove_app(self.client, - collection_id=self.collection.slug) - eq_(res.status_code, 200) - eq_(len(data['apps']), 0) - - def test_remove_app_curator(self): - self.make_curator() - self.add_app(self.client) - res, data = self.remove_app(self.client) - eq_(res.status_code, 200) - eq_(len(data['apps']), 0) - - def test_remove_app_nonexistent(self): - self.make_publisher() - res, data = self.remove_app(self.client, app_id=100000) - eq_(res.status_code, 400) - eq_(CollectionViewSet.exceptions['doesnt_exist'], data['detail']) - - def test_remove_app_empty(self): - self.make_publisher() - res, data = self.remove_app(self.client, app_id=False) - eq_(res.status_code, 400) - eq_(CollectionViewSet.exceptions['not_provided'], data['detail']) - - def test_remove_app_invalid(self): - self.make_publisher() - self.create_apps(number=2) - self.add_app(self.client, app_id=self.apps[0].pk) - res, data = self.remove_app(self.client, app_id=self.apps[1].pk) - eq_(res.status_code, 205) - ok_(not data) - - -class TestCollectionViewSetReorderApps(CollectionViewSetChangeAppsMixin): - """ - Tests the `reorder` action on CollectionViewSet. - """ - def random_app_order(self): - apps = list(a.pk for a in self.apps) - shuffle(apps) - return apps - - def test_reorder_anon(self): - res, data = self.reorder(self.anon) - eq_(res.status_code, 403) - eq_(PermissionDenied.default_detail, data['detail']) - - def test_reorder_no_perms(self): - res, data = self.reorder(self.client) - eq_(res.status_code, 403) - eq_(PermissionDenied.default_detail, data['detail']) - - def test_reorder_has_perms(self): - self.make_publisher() - self.create_apps() - self.add_apps_to_collection(*self.apps) - new_order = self.random_app_order() - res, data = self.reorder(self.client, order=new_order) - eq_(res.status_code, 200) - for order, app in enumerate(data['apps']): - app_pk = new_order[order] - eq_(Webapp.objects.get(pk=app_pk).app_slug, app['slug']) - - def test_reorder_slug(self): - self.make_publisher() - self.create_apps() - self.add_apps_to_collection(*self.apps) - new_order = self.random_app_order() - res, data = self.reorder(self.client, order=new_order, - collection_id=self.collection.slug) - eq_(res.status_code, 200) - for order, app in enumerate(data['apps']): - app_pk = new_order[order] - eq_(Webapp.objects.get(pk=app_pk).app_slug, app['slug']) - - def test_reorder_curator(self): - self.make_curator() - self.create_apps() - self.add_apps_to_collection(*self.apps) - new_order = self.random_app_order() - res, data = self.reorder(self.client, order=new_order) - eq_(res.status_code, 200) - for order, app in enumerate(data['apps']): - app_pk = new_order[order] - eq_(Webapp.objects.get(pk=app_pk).app_slug, app['slug']) - - def test_reorder_missing_apps(self): - self.make_publisher() - self.create_apps() - self.add_apps_to_collection(*self.apps) - new_order = self.random_app_order() - new_order.pop() - res, data = self.reorder(self.client, order=new_order) - eq_(res.status_code, 400) - eq_(data['detail'], CollectionViewSet.exceptions['app_mismatch']) - self.assertSetEqual(data['apps'], - [a.pk for a in self.collection.apps()]) - - -class TestCollectionViewSetEditCollection(BaseCollectionViewSetTest): - """ - Tests the handling of PATCH requests to a single collection on - CollectionViewSet. - """ - def edit_collection(self, client, collection_id=None, **kwargs): - url = self.collection_url('detail', - collection_id or self.collection.pk) - res = client.patch(url, json.dumps(kwargs)) - data = json.loads(res.content) - return res, data - - def test_edit_collection_anon(self): - res, data = self.edit_collection(self.anon) - eq_(res.status_code, 403) - eq_(PermissionDenied.default_detail, data['detail']) - - def test_edit_collection_name_and_description_simple(self): - self.make_publisher() - updates = { - 'description': u'¿Dónde está la biblioteca?', - 'name': u'Allö', - } - res, data = self.edit_collection(self.client, **updates) - eq_(res.status_code, 200) - self.collection.reload() - for key, value in updates.iteritems(): - eq_(data[key], {'en-US': value}) - eq_(getattr(self.collection, key), value) - - def test_edit_collection_name_and_description_slug(self): - self.make_publisher() - updates = { - 'description': u'¿Dónde está la biblioteca?', - 'name': u'Allö', - } - res, data = self.edit_collection(self.client, - collection_id=self.collection.slug, - **updates) - eq_(res.status_code, 200) - self.collection.reload() - for key, value in updates.iteritems(): - eq_(data[key], {'en-US': value}) - eq_(getattr(self.collection, key), value) - - def test_edit_collection_name_and_description_curator(self): - self.make_curator() - updates = { - 'description': u'¿Dónde está la biblioteca?', - 'name': u'Allö', - } - res, data = self.edit_collection(self.client, **updates) - eq_(res.status_code, 200) - self.collection.reload() - for key, value in updates.iteritems(): - eq_(data[key], {'en-US': value}) - eq_(getattr(self.collection, key), value) - - def test_edit_collection_name_and_description_multiple_translations(self): - self.make_publisher() - updates = { - 'name': { - 'en-US': u'Basta the potato', - 'fr': u'Basta la pomme de terre', - 'es': u'Basta la pâtätà', - 'it': u'Basta la patata' - }, - 'description': { - 'en-US': 'Basta likes potatoes and Le Boulanger', - 'fr': 'Basta aime les patates et Le Boulanger', - 'es': 'Basta gusta las patatas y Le Boulanger', - 'it': 'Basta ama patate e Le Boulanger' - } - } - res, data = self.edit_collection(self.client, **updates) - eq_(res.status_code, 200) - self.collection = Collection.objects.get(pk=self.collection.pk) - for key, value in updates.iteritems(): - eq_(getattr(self.collection, key), updates[key]['en-US']) - - with translation.override('es'): - collection_in_es = Collection.objects.get(pk=self.collection.pk) - eq_(getattr(collection_in_es, key), updates[key]['es']) - - with translation.override('fr'): - collection_in_fr = Collection.objects.get(pk=self.collection.pk) - eq_(getattr(collection_in_fr, key), updates[key]['fr']) - - def test_edit_collection_name_strip(self): - self.make_publisher() - updates = { - 'name': { - 'en-US': u' New Nâme! ' - }, - } - res, data = self.edit_collection(self.client, **updates) - eq_(res.status_code, 200) - self.collection = Collection.objects.get(pk=self.collection.pk) - eq_(data['name'], {u'en-US': u'New Nâme!'}) - eq_(self.collection.name, u'New Nâme!') - - def test_edit_collection_has_perms(self): - self.make_publisher() - cat = 'books' - updates = { - 'author': u'Nöt Me!', - 'region': mkt.regions.SPAIN.id, - 'is_public': False, - 'name': {'en-US': u'clôuserw soundboard'}, - 'description': {'en-US': u'Gèt off my lawn!'}, - 'category': cat, - 'carrier': mkt.carriers.TELEFONICA.id, - } - res, data = self.edit_collection(self.client, **updates) - eq_(res.status_code, 200) - collection = self.collection.reload() - - # Test that the result and object contain the right values. We can't - # easily loop on updates dict because data is stored and serialized - # in different ways depending on the field. - eq_(data['author'], updates['author']) - eq_(collection.author, updates['author']) - - eq_(data['is_public'], updates['is_public']) - eq_(collection.is_public, updates['is_public']) - - eq_(data['name'], updates['name']) - eq_(collection.name, updates['name']['en-US']) - - eq_(data['description'], updates['description']) - eq_(collection.description, updates['description']['en-US']) - - eq_(data['category'], cat) - eq_(collection.category, cat) - - eq_(data['region'], mkt.regions.SPAIN.slug) - eq_(collection.region, updates['region']) - - eq_(data['carrier'], mkt.carriers.TELEFONICA.slug) - eq_(collection.carrier, updates['carrier']) - - def test_edit_collection_with_slugs(self): - self.make_publisher() - cat = 'books' - updates = { - 'region': mkt.regions.SPAIN.slug, - 'category': cat, - 'carrier': mkt.carriers.TELEFONICA.slug, - } - res, data = self.edit_collection(self.client, **updates) - eq_(res.status_code, 200) - collection = self.collection.reload() - - # Test that the result and object contain the right values. We can't - # easily loop on updates dict because data is stored and serialized - # in different ways depending on the field. - eq_(data['region'], mkt.regions.SPAIN.slug) - eq_(collection.region, mkt.regions.SPAIN.id) - - eq_(data['carrier'], mkt.carriers.TELEFONICA.slug) - eq_(collection.carrier, mkt.carriers.TELEFONICA.id) - - eq_(data['category'], cat) - eq_(collection.category, cat) - - def test_edit_collection_invalid_carrier_slug(self): - self.make_publisher() - # Invalid carrier slug. - updates = {'carrier': 'whateverlol'} - res, data = self.edit_collection(self.client, **updates) - eq_(res.status_code, 400) - - def test_edit_collection_invalid_carrier(self): - self.make_publisher() - # Invalid carrier id. - updates = {'carrier': 1576} - res, data = self.edit_collection(self.client, **updates) - eq_(res.status_code, 400) - - def test_edit_collection_null_values(self): - self.make_publisher() - cat = 'books' - self.collection.update(**{ - 'carrier': mkt.carriers.UNKNOWN_CARRIER.id, - 'region': mkt.regions.SPAIN.id, - 'category': cat, - }) - - updates = { - 'carrier': None, - 'region': None, - 'category': None, - } - res, data = self.edit_collection(self.client, **updates) - eq_(res.status_code, 200) - self.collection.reload() - for key, value in updates.iteritems(): - eq_(data[key], value) - eq_(getattr(self.collection, key), value) - - def test_edit_collection_invalid_region_0(self): - self.make_publisher() - updates = {'region': 0} - res, data = self.edit_collection(self.client, **updates) - eq_(res.status_code, 400) - - def test_edit_collection_invalid_region(self): - self.make_publisher() - # Invalid region id. - updates = {'region': max(mkt.regions.REGION_IDS) + 1} - res, data = self.edit_collection(self.client, **updates) - eq_(res.status_code, 400) - - def test_edit_collection_invalid_region_slug(self): - self.make_publisher() - # Invalid region slug. - updates = {'region': 'idontexist'} - res, data = self.edit_collection(self.client, **updates) - eq_(res.status_code, 400) - - def test_edit_collection_invalid_category(self): - self.make_publisher() - # Invalid (non-existant) category. - updates = {'category': 1} - res, data = self.edit_collection(self.client, **updates) - eq_(res.status_code, 400) - - def test_edit_collection_invalid_category_slug(self): - self.make_publisher() - # Invalid (non-existant) category slug. - updates = {'category': 'nosuchcat'} - res, data = self.edit_collection(self.client, **updates) - eq_(res.status_code, 400) - - def test_edit_collection_featured_duplicate(self): - """ - Featured Apps & Operator Shelf should not have duplicates for a - region / carrier / category combination. Make sure this is respected - when editing a collection. - """ - self.setup_unique() - self.collection_data.update({ - 'region': mkt.regions.US.id, - 'carrier': mkt.carriers.SPRINT.id - }) - extra_collection = Collection.objects.create(**self.collection_data) - - # Try to edit self.collection with the data from our extra_collection. - update_data = { - 'region': extra_collection.region, - 'carrier': extra_collection.carrier, - } - res, data = self.edit_collection(self.client, **update_data) - eq_(res.status_code, 400) - ok_('collection_uniqueness' in data) - - # Changing the collection type should be enough to make it work. - update_data['collection_type'] = COLLECTIONS_TYPE_BASIC - res, data = self.edit_collection(self.client, **update_data) - eq_(res.status_code, 200) - - # A dumb change to see if you can still edit afterwards. The uniqueness - # check should exclude the current instance and allow it, obviously. - update_data = {'is_public': False} - res, data = self.edit_collection(self.client, **update_data) - eq_(res.status_code, 200) - - def test_edit_collection_operator_shelf_duplicate(self): - """ - Featured Apps & Operator Shelf should not have duplicates for a - region / carrier / category combination. Make sure this is respected - when editing a collection. - """ - self.setup_unique() - self.collection.update(category=None, - collection_type=COLLECTIONS_TYPE_OPERATOR) - self.collection_data.update({ - 'category': None, - 'collection_type': COLLECTIONS_TYPE_OPERATOR, - 'carrier': mkt.carriers.VIMPELCOM.id, - }) - extra_collection = Collection.objects.create(**self.collection_data) - - # Try to edit self.collection with the data from our extra_collection. - update_data = {'carrier': extra_collection.carrier} - res, data = self.edit_collection(self.client, **update_data) - eq_(res.status_code, 400) - ok_('collection_uniqueness' in data) - - # Changing the carrier should be enough to make it work. - update_data['carrier'] = mkt.carriers.SPRINT.id - res, data = self.edit_collection(self.client, **update_data) - eq_(res.status_code, 200) - - # A dumb change to see if you can still edit afterwards. The uniqueness - # check should exclude the current instance and allow it, obviously. - update_data = {'is_public': False} - res, data = self.edit_collection(self.client, **update_data) - eq_(res.status_code, 200) - - def test_edit_collection_validation_operatorshelf_category(self): - self.make_publisher() - category = 'books' - updates = { - 'category': category, - 'collection_type': COLLECTIONS_TYPE_OPERATOR - } - res, data = self.edit_collection(self.client, **updates) - eq_(res.status_code, 400) - ok_('non_field_errors' in data) - - -class TestCollectionViewSetListCurators(BaseCollectionViewSetTest): - """ - Tests the `curators` action on CollectionViewSet. - """ - def list_curators(self, client, collection_id=None): - self.collection.add_curator(self.user2) - url = self.collection_url('curators', - collection_id or self.collection.pk) - res = client.get(url) - data = json.loads(res.content) - return res, data - - def test_list_curators_no_perms(self): - res, data = self.list_curators(self.client) - eq_(res.status_code, 403) - eq_(PermissionDenied.default_detail, data['detail']) - - def test_list_curators_has_perms(self): - self.make_publisher() - res, data = self.list_curators(self.client) - eq_(res.status_code, 200) - eq_(len(data), 1) - eq_(data[0]['id'], self.user2.pk) - - def test_list_curators_slug(self): - self.make_publisher() - res, data = self.list_curators(self.client, - collection_id=self.collection.slug) - eq_(res.status_code, 200) - eq_(len(data), 1) - eq_(data[0]['id'], self.user2.pk) - - def test_list_curators_as_curator(self): - self.make_curator() - res, data = self.list_curators(self.client) - eq_(res.status_code, 200) - eq_(len(data), 2) - for item in data: - ok_(item['id'] in [self.user.pk, self.user2.pk]) - - -class TestCollectionViewSetAddCurator(BaseCollectionViewSetTest): - """ - Tests the `add-curator` action on CollectionViewSet. - """ - def add_curator(self, client, collection_id=None, user_id=None): - if user_id is None: - user_id = self.user.pk - form_data = {'user': user_id} if user_id else {} - url = self.collection_url('add-curator', - collection_id or self.collection.pk) - res = client.post(url, json.dumps(form_data)) - data = json.loads(res.content) - return res, data - - def test_add_curator_anon(self): - res, data = self.add_curator(self.anon) - eq_(res.status_code, 403) - eq_(PermissionDenied.default_detail, data['detail']) - - def test_add_curator_no_perms(self): - res, data = self.add_curator(self.client) - eq_(res.status_code, 403) - eq_(PermissionDenied.default_detail, data['detail']) - - def test_add_curator_has_perms(self): - self.make_publisher() - res, data = self.add_curator(self.client) - eq_(res.status_code, 200) - eq_(data[0]['id'], self.user.pk) - - def test_add_curator_slug(self): - self.make_publisher() - res, data = self.add_curator(self.client, - collection_id=self.collection.slug) - eq_(res.status_code, 200) - eq_(data[0]['id'], self.user.pk) - - def test_add_curator_multiple_cache(self): - self.make_publisher() - self.add_curator(self.client) - res, data = self.add_curator(self.client, user_id=self.user2.pk) - self.assertSetEqual([user['id'] for user in data], - [self.user.pk, self.user2.pk]) - - def test_add_curator_as_curator(self): - self.make_curator() - res, data = self.add_curator(self.client) - eq_(res.status_code, 200) - eq_(data[0]['id'], self.user.pk) - - def test_add_curator_nonexistent(self): - self.make_publisher() - res, data = self.add_curator(self.client, user_id=100000) - eq_(res.status_code, 400) - eq_(CollectionViewSet.exceptions['user_doesnt_exist'], data['detail']) - - res, data = self.add_curator(self.client, user_id='doesnt@exi.st') - eq_(res.status_code, 400) - eq_(CollectionViewSet.exceptions['user_doesnt_exist'], data['detail']) - - def test_add_curator_empty(self): - self.make_publisher() - res, data = self.add_curator(self.client, user_id=False) - eq_(res.status_code, 400) - eq_(CollectionViewSet.exceptions['user_not_provided'], data['detail']) - - def test_add_curator_email(self): - self.make_curator() - res, data = self.add_curator(self.client, user_id=self.user.email) - eq_(res.status_code, 200) - eq_(data[0]['id'], self.user.pk) - - def test_add_curator_garbage(self): - self.make_publisher() - res, data = self.add_curator(self.client, user_id='garbage') - eq_(res.status_code, 400) - eq_(CollectionViewSet.exceptions['wrong_user_format'], data['detail']) - - res, data = self.add_curator(self.client, user_id='garbage@') - eq_(res.status_code, 400) - eq_(CollectionViewSet.exceptions['wrong_user_format'], data['detail']) - - -class TestCollectionViewSetRemoveCurator(BaseCollectionViewSetTest): - """ - Tests the `remove-curator` action on CollectionViewSet. - """ - def remove_curator(self, client, collection_id=None, user_id=None): - if user_id is None: - user_id = self.user.pk - form_data = {'user': user_id} if user_id else {} - url = self.collection_url('remove-curator', self.collection.pk) - res = client.post(url, json.dumps(form_data)) - data = json.loads(res.content) if res.content else None - return res, data - - def test_remove_curator_anon(self): - res, data = self.remove_curator(self.anon) - eq_(res.status_code, 403) - eq_(PermissionDenied.default_detail, data['detail']) - - def test_remove_curator_no_perms(self): - res, data = self.remove_curator(self.client) - eq_(res.status_code, 403) - eq_(PermissionDenied.default_detail, data['detail']) - - def test_remove_curator_has_perms(self): - self.make_publisher() - res, data = self.remove_curator(self.client) - eq_(res.status_code, 205) - - def test_remove_curator_slug(self): - self.make_publisher() - res, data = self.remove_curator(self.client, - collection_id=self.collection.slug) - eq_(res.status_code, 205) - - def test_remove_curator_as_curator(self): - self.make_curator() - res, data = self.remove_curator(self.client) - eq_(res.status_code, 205) - - def test_remove_curator_email(self): - self.make_curator() - res, data = self.remove_curator(self.client, user_id=self.user.email) - eq_(res.status_code, 205) - - def test_remove_curator_nonexistent(self): - self.make_publisher() - res, data = self.remove_curator(self.client, user_id=100000) - eq_(res.status_code, 400) - eq_(CollectionViewSet.exceptions['user_doesnt_exist'], data['detail']) - - res, data = self.remove_curator(self.client, user_id='doesnt@exi.st') - eq_(res.status_code, 400) - eq_(CollectionViewSet.exceptions['user_doesnt_exist'], data['detail']) - - def test_remove_curator_empty(self): - self.make_publisher() - res, data = self.remove_curator(self.client, user_id=False) - eq_(res.status_code, 400) - eq_(CollectionViewSet.exceptions['user_not_provided'], data['detail']) - - def test_remove_curator_garbage(self): - self.make_publisher() - res, data = self.remove_curator(self.client, user_id='garbage') - eq_(res.status_code, 400) - eq_(CollectionViewSet.exceptions['wrong_user_format'], data['detail']) - - res, data = self.remove_curator(self.client, user_id='garbage@') - eq_(res.status_code, 400) - eq_(CollectionViewSet.exceptions['wrong_user_format'], data['detail']) - - -class TestCollectionImageViewSet(RestOAuth): - - def setUp(self): - super(TestCollectionImageViewSet, self).setUp() - self.collection = Collection.objects.create( - **CollectionDataMixin.collection_data) - self.url = reverse('collection-image-detail', - kwargs={'pk': self.collection.pk}) - self.img = ( - 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAA' - 'Cnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAA' - 'SUVORK5CYII=').decode('base64') - - def add_img(self): - path = self.collection.image_path() - storage.open(path, 'w').write(self.img) - self.collection.update(image_hash='fakehash') - return path - - def test_put(self, pk_or_slug=None): - if pk_or_slug is None: - pk_or_slug = self.collection.pk - self.url = reverse('collection-image-detail', kwargs={'pk': pk_or_slug}) - self.assertApiUrlEqual(self.url, - '/rocketfuel/collections/%s/image/' % pk_or_slug) - self.grant_permission(self.profile, 'Collections:Curate') - res = self.client.put(self.url, 'data:image/gif;base64,' + IMAGE_DATA) - eq_(res.status_code, 204) - assert os.path.exists(self.collection.image_path()) - self.collection.reload() - expected_hash = hashlib.md5(IMAGE_DATA.decode('base64')).hexdigest() - eq_(self.collection.image_hash, expected_hash[:8]) - im = Image.open(self.collection.image_path()) - im.verify() - assert im.format == 'PNG' - - def test_put_slug(self): - self.test_put(pk_or_slug=self.collection.slug) - - def test_replace_image(self): - self.add_img() - self.test_put() - - def test_put_non_data_uri(self): - self.grant_permission(self.profile, 'Collections:Curate') - res = self.client.put(self.url, 'some junk') - eq_(res.status_code, 400) - ok_(not Collection.objects.get(pk=self.collection.pk).image_hash) - - def test_put_non_image(self): - self.grant_permission(self.profile, 'Collections:Curate') - res = self.client.put(self.url, 'data:text/plain;base64,AAA=') - eq_(res.status_code, 400) - ok_(not Collection.objects.get(pk=self.collection.pk).image_hash) - - def test_put_unauthorized(self): - res = self.client.put(self.url, 'some junk') - eq_(res.status_code, 403) - - @override_settings(XSENDFILE=True) - def _test_get(self): - img_path = self.add_img() - res = self.client.get(self.url) - eq_(res.status_code, 200) - eq_(res[settings.XSENDFILE_HEADER], img_path) - ok_('max-age=31536000' in res['Cache-Control']) - - def test_get(self): - self.assertApiUrlEqual(self.url, - '/rocketfuel/collections/%s/image/' % self.collection.pk) - self._test_get() - - def test_get_slug(self): - slug = self.collection.slug - self.url = reverse('collection-image-detail', kwargs={'pk': slug}) - self.assertApiUrlEqual(self.url, - '/rocketfuel/collections/%s/image/' % slug) - self._test_get() - - def test_get_png(self): - pk = self.collection.pk - self.url = reverse('collection-image-detail', - kwargs={'pk': pk, 'format': 'png'}) - self.assertApiUrlEqual(self.url, - '/rocketfuel/collections/%s/image.png' % pk) - self._test_get() - - def test_get_png_slug(self): - slug = self.collection.slug - self.url = reverse('collection-image-detail', - kwargs={'pk': slug, 'format': 'png'}) - self.assertApiUrlEqual(self.url, - '/rocketfuel/collections/%s/image.png' % slug) - self._test_get() - - def test_get_no_image(self): - res = self.client.get(self.url) - eq_(res.status_code, 404) - - def test_delete(self, url=None): - self.grant_permission(self.profile, 'Collections:Curate') - img_path = self.add_img() - res = self.client.delete(self.url) - eq_(res.status_code, 204) - ok_(not self.collection.reload().image_hash) - ok_(not storage.exists(img_path)) - - def test_delete_slug(self): - url = reverse('collection-image-detail', - kwargs={'pk': self.collection.slug}) - self.test_delete(url) - - def test_delete_unauthorized(self): - res = self.client.delete(self.url) - eq_(res.status_code, 403) diff --git a/mkt/collections/views.py b/mkt/collections/views.py deleted file mode 100644 index b011cf76a6d..00000000000 --- a/mkt/collections/views.py +++ /dev/null @@ -1,298 +0,0 @@ -from django.core.exceptions import ValidationError -from django.core.files.storage import default_storage as storage -from django.core.validators import validate_email -from django.db import IntegrityError -from django.db.models import Q -from django.http import Http404 -from django.utils.datastructures import MultiValueDictKeyError -from django.views.decorators.cache import cache_control - -from PIL import Image - -from rest_framework import generics, serializers, status, viewsets -from rest_framework.decorators import action, link -from rest_framework.exceptions import ParseError -from rest_framework.response import Response - -from amo.utils import HttpResponseSendFile - -from mkt.api.authentication import (RestAnonymousAuthentication, - RestOAuthAuthentication, - RestSharedSecretAuthentication) -from mkt.api.base import CORSMixin, MarketplaceView, SlugOrIdMixin -from mkt.collections.serializers import DataURLImageField -from mkt.developers.tasks import pngcrush_image -from mkt.webapps.models import Webapp -from mkt.users.models import UserProfile - -from .authorization import (CanBeHeroAuthorization, CuratorAuthorization, - StrictCuratorAuthorization) -from .filters import CollectionFilterSetWithFallback -from .models import Collection -from .serializers import CollectionSerializer, CuratorSerializer - - -class CollectionViewSet(CORSMixin, SlugOrIdMixin, MarketplaceView, - viewsets.ModelViewSet): - serializer_class = CollectionSerializer - queryset = Collection.objects.all() - cors_allowed_methods = ('get', 'post', 'delete', 'patch') - permission_classes = [CanBeHeroAuthorization, CuratorAuthorization] - authentication_classes = [RestOAuthAuthentication, - RestSharedSecretAuthentication, - RestAnonymousAuthentication] - filter_class = CollectionFilterSetWithFallback - - exceptions = { - 'not_provided': '`app` was not provided.', - 'user_not_provided': '`user` was not provided.', - 'wrong_user_format': '`user` must be an ID or email.', - 'doesnt_exist': '`app` does not exist.', - 'user_doesnt_exist': '`user` does not exist.', - 'not_in': '`app` not in collection.', - 'already_in': '`app` already exists in collection.', - 'app_mismatch': 'All apps in this collection must be included.', - } - - def filter_queryset(self, queryset): - queryset = super(CollectionViewSet, self).filter_queryset(queryset) - self.filter_fallback = getattr(queryset, 'filter_fallback', None) - self.filter_errors = getattr(queryset, 'filter_errors', None) - return queryset - - def list(self, request, *args, **kwargs): - response = super(CollectionViewSet, self).list( - request, *args, **kwargs) - if response: - filter_fallback = getattr(self, 'filter_fallback', None) - if filter_fallback: - response['API-Fallback'] = ','.join(filter_fallback) - - filter_errors = getattr(self, 'filter_errors', None) - if filter_errors: - # If we had errors filtering, the default behaviour of DRF - # and django-filter is to produce an empty queryset and ignore - # the problem. We want to fail loud and clear and expose the - # errors instead. - response.data = { - 'detail': 'Filtering error.', - 'filter_errors': filter_errors - } - response.status_code = status.HTTP_400_BAD_REQUEST - return response - - def get_object(self, queryset=None): - """ - Custom get_object implementation to prevent DRF from filtering when we - do a specific pk/slug/etc lookup (we only want filtering on list API). - - Calls DRF's get_object() with the queryset (filtered or not), since DRF - get_object() implementation will then just use the queryset without - attempting to filter it. - """ - if queryset is None: - queryset = self.get_queryset() - if (self.pk_url_kwarg not in self.kwargs and - self.slug_url_kwarg not in self.kwargs and - self.lookup_field not in self.kwargs): - # Only filter queryset if we don't have an explicit lookup. - queryset = self.filter_queryset(queryset) - return super(CollectionViewSet, self).get_object(queryset=queryset) - - def get_queryset(self): - auth = CuratorAuthorization() - qs = super(CollectionViewSet, self).get_queryset() - if self.request.user.is_authenticated(): - if auth.has_curate_permission(self.request): - return qs - profile = self.request.user - return qs.filter(Q(curators__id=profile.id) | - Q(is_public=True)).distinct() - return qs.filter(is_public=True) - - def return_updated(self, status, collection=None): - """ - Passed an HTTP status from rest_framework.status, returns a response - of that status with the body containing the updated values of - self.object. - """ - if collection is None: - collection = self.get_object() - serializer = self.get_serializer(instance=collection) - return Response(serializer.data, status=status) - - @action() - def duplicate(self, request, *args, **kwargs): - """ - Duplicate the specified collection, copying over all fields and apps. - Anything passed in request.DATA will override the corresponding value - on the resulting object. - """ - # Serialize data from specified object, removing the id and then - # updating with custom data in request.DATA. - collection = self.get_object() - collection_data = self.get_serializer(instance=collection).data - collection_data.pop('id') - collection_data.update(request.DATA) - - # Pretend we didn't have anything in kwargs (removing 'pk'). - self.kwargs = {} - - # Override request.DATA with the result from above. - request._data = collection_data - - # Now create the collection. - result = self.create(request) - if result.status_code != status.HTTP_201_CREATED: - return result - - # And now, add apps from the original collection. - for app in collection.apps(): - self.object.add_app(app) - - # Re-Serialize to include apps. - return self.return_updated(status.HTTP_201_CREATED, - collection=self.object) - - @action() - def add_app(self, request, *args, **kwargs): - """ - Add an app to the specified collection. - """ - collection = self.get_object() - try: - new_app = Webapp.objects.get(pk=request.DATA['app']) - except (KeyError, MultiValueDictKeyError): - raise ParseError(detail=self.exceptions['not_provided']) - except Webapp.DoesNotExist: - raise ParseError(detail=self.exceptions['doesnt_exist']) - try: - collection.add_app(new_app) - except IntegrityError: - raise ParseError(detail=self.exceptions['already_in']) - return self.return_updated(status.HTTP_200_OK) - - @action() - def remove_app(self, request, *args, **kwargs): - """ - Remove an app from the specified collection. - """ - collection = self.get_object() - try: - to_remove = Webapp.objects.get(pk=request.DATA['app']) - except (KeyError, MultiValueDictKeyError): - raise ParseError(detail=self.exceptions['not_provided']) - except Webapp.DoesNotExist: - raise ParseError(detail=self.exceptions['doesnt_exist']) - removed = collection.remove_app(to_remove) - if not removed: - return Response(status=status.HTTP_205_RESET_CONTENT) - return self.return_updated(status.HTTP_200_OK) - - @action() - def reorder(self, request, *args, **kwargs): - """ - Reorder the specified collection. - """ - collection = self.get_object() - try: - collection.reorder(request.DATA) - except ValueError: - return Response({ - 'detail': self.exceptions['app_mismatch'], - 'apps': [a.pk for a in collection.apps()] - }, status=status.HTTP_400_BAD_REQUEST, exception=True) - return self.return_updated(status.HTTP_200_OK) - - def serialized_curators(self, no_cache=False): - queryset = self.get_object().curators.all() - if no_cache: - queryset = queryset.no_cache() - return Response([CuratorSerializer(instance=c).data for c in queryset]) - - def get_curator(self, request): - try: - userdata = request.DATA['user'] - if (isinstance(userdata, int) or isinstance(userdata, basestring) - and userdata.isdigit()): - return UserProfile.objects.get(pk=userdata) - else: - validate_email(userdata) - return UserProfile.objects.get(email=userdata) - except (KeyError, MultiValueDictKeyError): - raise ParseError(detail=self.exceptions['user_not_provided']) - except UserProfile.DoesNotExist: - raise ParseError(detail=self.exceptions['user_doesnt_exist']) - except ValidationError: - raise ParseError(detail=self.exceptions['wrong_user_format']) - - @link(permission_classes=[StrictCuratorAuthorization]) - def curators(self, request, *args, **kwargs): - return self.serialized_curators() - - @action(methods=['POST']) - def add_curator(self, request, *args, **kwargs): - self.get_object().add_curator(self.get_curator(request)) - return self.serialized_curators(no_cache=True) - - @action(methods=['POST']) - def remove_curator(self, request, *args, **kwargs): - removed = self.get_object().remove_curator(self.get_curator(request)) - if not removed: - return Response(status=status.HTTP_205_RESET_CONTENT) - return self.serialized_curators(no_cache=True) - - -class CollectionImageViewSet(CORSMixin, SlugOrIdMixin, MarketplaceView, - generics.GenericAPIView, viewsets.ViewSet): - queryset = Collection.objects.all() - permission_classes = [CuratorAuthorization] - authentication_classes = [RestOAuthAuthentication, - RestSharedSecretAuthentication, - RestAnonymousAuthentication] - cors_allowed_methods = ('get', 'put', 'delete') - - hash_field = 'image_hash' - image_suffix = '' - - # Dummy serializer to keep DRF happy when it's answering to OPTIONS. - serializer_class = serializers.Serializer - - def perform_content_negotiation(self, request, force=False): - """ - Force DRF's content negociation to not raise an error - It wants to use - the format passed to the URL, but we don't care since we only deal with - "raw" content: we don't even use the renderers. - """ - return super(CollectionImageViewSet, self).perform_content_negotiation( - request, force=True) - - @cache_control(max_age=60 * 60 * 24 * 365) - def retrieve(self, request, *args, **kwargs): - obj = self.get_object() - if not getattr(obj, 'image_hash', None): - raise Http404 - return HttpResponseSendFile(request, obj.image_path(self.image_suffix), - content_type='image/png') - - def update(self, request, *args, **kwargs): - obj = self.get_object() - try: - img, hash_ = DataURLImageField().from_native(request.read()) - except ValidationError: - return Response(status=status.HTTP_400_BAD_REQUEST) - i = Image.open(img) - with storage.open(obj.image_path(self.image_suffix), 'wb') as f: - i.save(f, 'png') - # Store the hash of the original image data sent. - obj.update(**{self.hash_field: hash_}) - - pngcrush_image.delay(obj.image_path(self.image_suffix)) - return Response(status=status.HTTP_204_NO_CONTENT) - - def destroy(self, request, *args, **kwargs): - obj = self.get_object() - if getattr(obj, 'image_hash', None): - storage.delete(obj.image_path(self.image_suffix)) - obj.update(**{self.hash_field: None}) - return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/mkt/commonplace/tests/test_views.py b/mkt/commonplace/tests/test_views.py index d6222768bf6..1f4f518b44d 100644 --- a/mkt/commonplace/tests/test_views.py +++ b/mkt/commonplace/tests/test_views.py @@ -61,15 +61,6 @@ def test_commbadge(self, mock_fxa): self.assertNotContains(res, 'splash.css') eq_(res['Cache-Control'], 'max-age=180') - @mock.patch('mkt.commonplace.views.fxa_auth_info') - def test_rocketfuel(self, mock_fxa): - mock_fxa.return_value = ('fakestate', 'http://example.com/fakeauthurl') - res = self._test_url('/curation/') - self.assertTemplateUsed(res, 'commonplace/index.html') - self.assertEquals(res.context['repo'], 'rocketfuel') - self.assertNotContains(res, 'splash.css') - eq_(res['Cache-Control'], 'max-age=180') - @mock.patch('mkt.commonplace.views.fxa_auth_info') def test_transonic(self, mock_fxa): mock_fxa.return_value = ('fakestate', 'http://example.com/fakeauthurl') @@ -133,7 +124,7 @@ def test_bad_repo(self): raise SkipTest res = self.client.get(reverse('commonplace.appcache'), - {'repo': 'rocketfuel'}) + {'repo': 'transonic'}) eq_(res.status_code, 404) @mock.patch('mkt.commonplace.views.get_build_id', new=lambda x: 'p00p') diff --git a/mkt/commonplace/urls.py b/mkt/commonplace/urls.py index f96a493278f..0fecf9d7325 100644 --- a/mkt/commonplace/urls.py +++ b/mkt/commonplace/urls.py @@ -54,10 +54,6 @@ def fireplace_route(path, name=None): url('^comm/.*$', views.commonplace, {'repo': 'commbadge'}, name='commonplace.commbadge'), - # Rocketfuel: - url('^curation/.*$', views.commonplace, {'repo': 'rocketfuel'}, - name='commonplace.rocketfuel'), - # Transonic: url('^curate/.*$', views.commonplace, {'repo': 'transonic'}, name='commonplace.transonic'), diff --git a/mkt/feed/fields.py b/mkt/feed/fields.py index 8c8e701fdd6..b46e92e2b9b 100644 --- a/mkt/feed/fields.py +++ b/mkt/feed/fields.py @@ -1,13 +1,17 @@ +import hashlib +import re import StringIO +import uuid from django.conf import settings +from django.core.files.base import File +from django.db.models.fields import CharField import requests from PIL import Image from rest_framework import exceptions, serializers from tower import ugettext as _ -from mkt.collections.serializers import DataURLImageField from mkt.fireplace.serializers import FeedFireplaceESAppSerializer from mkt.webapps.serializers import (AppSerializer, ESAppFeedSerializer, ESAppFeedCollectionSerializer, @@ -144,6 +148,31 @@ def serializer_class(self): return ESAppFeedCollectionSerializer +class DataURLImageField(serializers.CharField): + def from_native(self, data): + if data.startswith('"') and data.endswith('"'): + # Strip quotes if necessary. + data = data[1:-1] + if not data.startswith('data:'): + raise serializers.ValidationError('Not a data URI.') + + metadata, encoded = data.rsplit(',', 1) + parts = metadata.rsplit(';', 1) + if parts[-1] == 'base64': + content = encoded.decode('base64') + f = StringIO.StringIO(content) + f.size = len(content) + tmp = File(f, name=uuid.uuid4().hex) + hash_ = hashlib.md5(content).hexdigest()[:8] + return serializers.ImageField().from_native(tmp), hash_ + else: + raise serializers.ValidationError('Not a base64 data URI.') + + def to_native(self, value): + return value.name + + + class ImageURLField(serializers.Field): """ Takes a URL pointing to an image (intended to be from Aviary's Feather). @@ -181,3 +210,22 @@ def from_native(self, image_url): # Return image file object and hash. return DataURLImageField().from_native(img_data_uri) + + +class ColorField(CharField): + """ + Model field that only accepts 7-character hexadecimal color representations, + e.g. #FF0035. + """ + description = _('Hexadecimal color') + + def __init__(self, *args, **kwargs): + kwargs['max_length'] = kwargs.get('max_length', 7) + self.default_error_messages.update({ + 'bad_hex': _('Must be a valid hex color code, e.g. #FF0035.'), + }) + super(ColorField, self).__init__(*args, **kwargs) + + def validate(self, value, model_instance): + if value and not re.match('^\#([0-9a-fA-F]{6})$', value): + raise exceptions.ValidationError(self.error_messages['bad_hex']) diff --git a/mkt/feed/models.py b/mkt/feed/models.py index 06333a21968..8ca757ef27b 100644 --- a/mkt/feed/models.py +++ b/mkt/feed/models.py @@ -21,9 +21,9 @@ import amo import mkt.carriers import mkt.regions -from mkt.collections.fields import ColorField from mkt.constants.categories import CATEGORY_CHOICES from mkt.feed import indexers +from mkt.feed.fields import ColorField from mkt.ratings.validators import validate_rating from mkt.site.decorators import use_master from mkt.site.models import ManagerBase, ModelBase diff --git a/mkt/feed/views.py b/mkt/feed/views.py index a99175e34c2..0fbaee20029 100644 --- a/mkt/feed/views.py +++ b/mkt/feed/views.py @@ -1,6 +1,9 @@ from django.conf import settings from django.core.files.storage import default_storage as storage from django.db.models import Q +from django.http import Http404 +from django.views.decorators.cache import cache_control + import commonware from django_statsd.clients import statsd @@ -12,17 +15,18 @@ from rest_framework.exceptions import ParseError from rest_framework.filters import BaseFilterBackend, OrderingFilter from rest_framework.response import Response +from rest_framework.serializers import Serializer, ValidationError from rest_framework.views import APIView import mkt import mkt.feed.constants as feed +from amo.utils import HttpResponseSendFile from mkt.api.authentication import (RestAnonymousAuthentication, RestOAuthAuthentication, RestSharedSecretAuthentication) from mkt.api.authorization import AllowReadOnly, AnyOf, GroupPermission from mkt.api.base import CORSMixin, MarketplaceView, SlugOrIdMixin from mkt.api.paginator import ESPaginator -from mkt.collections.views import CollectionImageViewSet from mkt.constants.applications import get_device_id from mkt.developers.tasks import pngcrush_image from mkt.feed.indexers import FeedItemIndexer @@ -32,7 +36,7 @@ from mkt.webapps.models import Webapp from .authorization import FeedAuthorization -from .fields import ImageURLField +from .fields import DataURLImageField, ImageURLField from .models import FeedApp, FeedBrand, FeedCollection, FeedItem, FeedShelf from .serializers import (FeedAppESSerializer, FeedAppSerializer, FeedBrandESSerializer, FeedBrandSerializer, @@ -400,6 +404,61 @@ def delete(self, request, *args, **kwargs): return response.Response(status=status.HTTP_204_NO_CONTENT) +class CollectionImageViewSet(CORSMixin, SlugOrIdMixin, MarketplaceView, + generics.GenericAPIView, viewsets.ViewSet): + permission_classes = [AnyOf(AllowReadOnly, + GroupPermission('Feed', 'Curate'))] + authentication_classes = [RestOAuthAuthentication, + RestSharedSecretAuthentication, + RestAnonymousAuthentication] + cors_allowed_methods = ('get', 'put', 'delete') + + hash_field = 'image_hash' + image_suffix = '' + + # Dummy serializer to keep DRF happy when it's answering to OPTIONS. + serializer_class = Serializer + + def perform_content_negotiation(self, request, force=False): + """ + Force DRF's content negociation to not raise an error - It wants to use + the format passed to the URL, but we don't care since we only deal with + "raw" content: we don't even use the renderers. + """ + return super(CollectionImageViewSet, self).perform_content_negotiation( + request, force=True) + + @cache_control(max_age=60 * 60 * 24 * 365) + def retrieve(self, request, *args, **kwargs): + obj = self.get_object() + if not getattr(obj, 'image_hash', None): + raise Http404 + return HttpResponseSendFile(request, obj.image_path(self.image_suffix), + content_type='image/png') + + def update(self, request, *args, **kwargs): + obj = self.get_object() + try: + img, hash_ = DataURLImageField().from_native(request.read()) + except ValidationError: + return Response(status=status.HTTP_400_BAD_REQUEST) + i = Image.open(img) + with storage.open(obj.image_path(self.image_suffix), 'wb') as f: + i.save(f, 'png') + # Store the hash of the original image data sent. + obj.update(**{self.hash_field: hash_}) + + pngcrush_image.delay(obj.image_path(self.image_suffix)) + return Response(status=status.HTTP_204_NO_CONTENT) + + def destroy(self, request, *args, **kwargs): + obj = self.get_object() + if getattr(obj, 'image_hash', None): + storage.delete(obj.image_path(self.image_suffix)) + obj.update(**{self.hash_field: None}) + return Response(status=status.HTTP_204_NO_CONTENT) + + class FeedAppImageViewSet(CollectionImageViewSet): queryset = FeedApp.objects.all() diff --git a/mkt/fireplace/serializers.py b/mkt/fireplace/serializers.py index b146d89a03d..cd6ac9e9e60 100644 --- a/mkt/fireplace/serializers.py +++ b/mkt/fireplace/serializers.py @@ -1,5 +1,3 @@ -from mkt.collections.serializers import (CollectionSerializer, - CollectionMembershipField) from mkt.webapps.serializers import SimpleAppSerializer, SimpleESAppSerializer @@ -44,14 +42,3 @@ class FeedFireplaceESAppSerializer(BaseFireplaceAppSerializer, class Meta(SimpleESAppSerializer.Meta): fields = sorted(FireplaceAppSerializer.Meta.fields + ['group']) exclude = FireplaceAppSerializer.Meta.exclude - - -class FireplaceCollectionMembershipField(CollectionMembershipField): - app_serializer_classes = { - 'es': FireplaceESAppSerializer, - 'normal': FireplaceAppSerializer, - } - - -class FireplaceCollectionSerializer(CollectionSerializer): - apps = FireplaceCollectionMembershipField(many=True, source='apps') diff --git a/mkt/fireplace/tests/test_views.py b/mkt/fireplace/tests/test_views.py index 21def629ba5..0081e2847e2 100644 --- a/mkt/fireplace/tests/test_views.py +++ b/mkt/fireplace/tests/test_views.py @@ -13,8 +13,6 @@ from amo.tests import app_factory, ESTestCase, TestCase from mkt.api.tests import BaseAPI from mkt.api.tests.test_oauth import RestOAuth -from mkt.collections.constants import COLLECTIONS_TYPE_BASIC -from mkt.collections.models import Collection from mkt.fireplace.serializers import FireplaceAppSerializer from mkt.site.fixtures import fixture from mkt.users.models import UserProfile @@ -87,51 +85,6 @@ def test_get(self): ok_('operator' not in res.json) -class TestCollectionViewSet(RestOAuth, ESTestCase): - fixtures = fixture('user_2519', 'webapp_337141') - - def setUp(self): - super(TestCollectionViewSet, self).setUp() - self.webapp = Webapp.objects.get(pk=337141) - collection = Collection.objects.create(name='Hi', description='Mom', - collection_type=COLLECTIONS_TYPE_BASIC, is_public=True) - collection.add_app(self.webapp) - self.reindex(Webapp, 'webapp') - self.url = reverse('fireplace-collection-detail', - kwargs={'pk': collection.pk}) - - def test_get(self): - res = self.client.get(self.url) - eq_(res.status_code, 200) - eq_(res.json['name'], {u'en-US': u'Hi'}) - data = res.json['apps'][0] - assert_fireplace_app(data) - - @patch('mkt.collections.serializers.CollectionMembershipField.to_native') - def test_get_preview(self, mock_field_to_native): - mock_field_to_native.return_value = [] - res = self.client.get(self.url, {'preview': 1}) - eq_(res.status_code, 200) - eq_(res.json['name'], {u'en-US': u'Hi'}) - eq_(res.json['apps'], []) - - eq_(mock_field_to_native.call_count, 1) - ok_(isinstance(mock_field_to_native.call_args[0][0], QuerySet)) - eq_(mock_field_to_native.call_args[1].get('use_es', False), False) - - @patch('mkt.collections.serializers.CollectionMembershipField.to_native') - def test_no_get_preview(self, mock_field_to_native): - mock_field_to_native.return_value = [] - res = self.client.get(self.url) - eq_(res.status_code, 200) - eq_(res.json['name'], {u'en-US': u'Hi'}) - eq_(res.json['apps'], []) - - eq_(mock_field_to_native.call_count, 1) - ok_(isinstance(mock_field_to_native.call_args[0][0], Search)) - eq_(mock_field_to_native.call_args[1].get('use_es', False), True) - - class TestSearchView(RestOAuth, ESTestCase): fixtures = fixture('user_2519', 'webapp_337141') diff --git a/mkt/fireplace/urls.py b/mkt/fireplace/urls.py index adf5ff9a876..a701347fec1 100644 --- a/mkt/fireplace/urls.py +++ b/mkt/fireplace/urls.py @@ -1,23 +1,30 @@ +from django.core.urlresolvers import reverse from django.conf.urls import include, patterns, url +from django.shortcuts import redirect from rest_framework.routers import SimpleRouter -from mkt.fireplace.views import (AppViewSet, CollectionViewSet, - ConsumerInfoView, SearchView) +from mkt.fireplace.views import AppViewSet, ConsumerInfoView, SearchView apps = SimpleRouter() apps.register(r'app', AppViewSet, base_name='fireplace-app') -collections = SimpleRouter() -collections.register(r'collection', CollectionViewSet, - base_name='fireplace-collection') +def redirect_to_feed_element(request, slug): + url = reverse('api-v2:feed.fire_feed_element_get', + kwargs={'item_type': 'collections', 'slug': slug}) + return redirect(url, permanent=True) urlpatterns = patterns('', url(r'^fireplace/', include(apps.urls)), - url(r'^fireplace/', include(collections.urls)), + + # Compatibility for old apps that still hit the rocketfuel collection API, + # we redirect them to the feed. + url(r'^fireplace/collection/(?P[^/.]+)/$', redirect_to_feed_element, + name='feed.fire_rocketfuel_compat'), + url(r'^fireplace/consumer-info/', ConsumerInfoView.as_view(), name='fireplace-consumer-info'), diff --git a/mkt/fireplace/views.py b/mkt/fireplace/views.py index dab834de1ba..66310e3bb39 100644 --- a/mkt/fireplace/views.py +++ b/mkt/fireplace/views.py @@ -8,25 +8,12 @@ RestOAuthAuthentication, RestSharedSecretAuthentication) from mkt.api.base import CORSMixin -from mkt.collections.views import CollectionViewSet as BaseCollectionViewSet from mkt.fireplace.serializers import (FireplaceAppSerializer, - FireplaceCollectionSerializer, FireplaceESAppSerializer) from mkt.search.views import SearchView as BaseSearchView from mkt.webapps.views import AppViewSet as BaseAppViewset -class CollectionViewSet(BaseCollectionViewSet): - serializer_class = FireplaceCollectionSerializer - - def get_serializer_context(self): - """Context passed to the serializer. Since we are in Fireplace, we - always want to use ES to fetch apps.""" - context = super(CollectionViewSet, self).get_serializer_context() - context['use-es-for-apps'] = not self.request.GET.get('preview') - return context - - class AppViewSet(BaseAppViewset): serializer_class = FireplaceAppSerializer diff --git a/mkt/settings.py b/mkt/settings.py index b63d6f809a2..4ab179a12d0 100644 --- a/mkt/settings.py +++ b/mkt/settings.py @@ -118,7 +118,6 @@ 'mkt.access', 'mkt.account', 'mkt.api', - 'mkt.collections', 'mkt.comm', 'mkt.commonplace', 'mkt.detail', @@ -571,7 +570,7 @@ def langs(languages): # Name of our Commonplace repositories on GitHub. COMMONPLACE_REPOS = ['commbadge', 'fireplace', 'marketplace-stats', - 'rocketfuel', 'transonic', 'discoplace', + 'transonic', 'discoplace', 'marketplace-operator-dashboard'] COMMONPLACE_REPOS_APPCACHED = [] diff --git a/mkt/webapps/tasks.py b/mkt/webapps/tasks.py index 7a012e2b568..4ace78ce34d 100644 --- a/mkt/webapps/tasks.py +++ b/mkt/webapps/tasks.py @@ -435,16 +435,15 @@ def dump_all_apps_tasks(): @task def export_data(name=None): - from mkt.collections.tasks import dump_all_collections_tasks today = datetime.datetime.today().strftime('%Y-%m-%d') if name is None: name = today root = settings.DUMPED_APPS_PATH - directories = ['apps', 'collections'] + directories = ['apps'] for directory in directories: rm_directory(os.path.join(root, directory)) files = directories + compile_extra_files(date=today) - chord(dump_all_apps_tasks() + dump_all_collections_tasks(), + chord(dump_all_apps_tasks(), compress_export.si(filename=name, files=files)).apply_async()