diff --git a/docs/api/topics/comm.rst b/docs/api/topics/comm.rst index 3740db4f3ab..d7d67fdad45 100644 --- a/docs/api/topics/comm.rst +++ b/docs/api/topics/comm.rst @@ -113,7 +113,7 @@ Thread "id": 5, "name": "Test App (kinkajou3969)", "review_url": "/reviewers/apps/review/test-app-kinkajou3969/", - "thumbnail_url": "/media/img/icons/no-preview.png", + "thumbnail_url": "/tmp/uploads/previews/thumbs/0/37.png?modified=1362762723", "url": "/app/test-app-kinkajou3969/" }, "created": "2013-06-14T11:54:24", diff --git a/lib/utils.py b/lib/utils.py index e9ac197677e..d30f078f872 100644 --- a/lib/utils.py +++ b/lib/utils.py @@ -57,12 +57,13 @@ def static_url(url): If the URL starts with https:// or http://, then no changes are made. """ prefix = { - 'ADDON_ICONS_DEFAULT_URL': settings.MEDIA_URL, + 'ICONS_DEFAULT_URL': settings.MEDIA_URL, 'ADDON_ICON_URL': settings.STATIC_URL, 'PREVIEW_THUMBNAIL_URL': settings.STATIC_URL, 'PREVIEW_FULL_URL': settings.STATIC_URL, 'PRODUCT_ICON_URL': settings.MEDIA_URL, - 'WEBAPPS_RECEIPT_URL': settings.SITE_URL + 'WEBAPPS_RECEIPT_URL': settings.SITE_URL, + 'WEBSITE_ICON_URL': settings.STATIC_URL, } value = getattr(settings, url) diff --git a/media/css/devreg/landing.styl b/media/css/devreg/landing.styl deleted file mode 100644 index a125ef637ee..00000000000 --- a/media/css/devreg/landing.styl +++ /dev/null @@ -1,118 +0,0 @@ -@import 'lib'; - -#phases { - background-color: #FAF2BD; - border-radius: 7px; - box-shadow: 1px 1px 1px #E1D58C inset, -1px -1px 0 #FFFFFF inset; - margin-bottom: 2em; - overflow: hidden; - padding: 1.2em; - - h2 { - color: #6E6D6D; - font-size: 1.8em; - font-weight: bold; - margin: 0; - text-shadow: 1px 1px 0 #FFFFFF; - border-bottom: 1px solid #D9D9B2; - padding: 0 0 0.3em; - - strong { - color: darken($red, 10%); - } - } - - p { - margin-top: 1em; - color: #707070; - line-height: 1.3em; - } - - .phase-desc { - float: left; - width: 35%; - } - - #timeline { - width: 65%; - margin-top: 1.5em; - overflow: hidden; - float: left; - - > div { - width: 25%; - position: relative; - float: left; - color: #787144; - text-align: center; - font-size: .9em; - text-shadow: 1px 1px 0 #fff; - - span { - font-size: .9em; - opacity: .7; - display: block; - padding-top: 5px; - } - - &:before, &:after { - content: " "; - display: block; - float: left; - height: 14px; - position: absolute; - top: 8px; - width: 50%; - background-image: url(../../img/hub/phase.png); - } - &:before { - left: 0; - } - &:after { - right: 0; - } - } - .bubble { - background-image: url(../../img/hub/phase.png); - background-repeat: repeat-x; - margin: auto; - position: relative; - z-index: 10; - padding-bottom: 1em; - } - #bubble1 { - .bubble { - height: 30px; - width: 28px; - } - &:before { background: none; } - &:after { background-position: 0 -51px; } - } - #bubble2 { - .bubble { - height: 30px; - width: 27px; - background-position: -28px 0; - } - &:before { background-position: 0 -51px; } - &:after { background-position: 0 -85px; } - } - #bubble3 { - .bubble { - height: 30px; - width: 27px; - background-position: -54px 0; - } - &:before, &:after { background-position: 0 -85px; } - } - #bubble4 { - .bubble { - height: 30px; - width: 27px; - background-position: -80px 0; - } - &:before { background-position: 0 -85px; } - &:after { background: none; } - } - } -} diff --git a/media/img/hub/assetback.png b/media/img/hub/assetback.png deleted file mode 100644 index ce3f6f95102..00000000000 Binary files a/media/img/hub/assetback.png and /dev/null differ diff --git a/media/img/hub/default-128.png b/media/img/hub/default-128.png new file mode 100644 index 00000000000..c3ca6eb32d4 Binary files /dev/null and b/media/img/hub/default-128.png differ diff --git a/media/img/hub/default-48.png b/media/img/hub/default-48.png new file mode 100644 index 00000000000..c6f4e3c3fc0 Binary files /dev/null and b/media/img/hub/default-48.png differ diff --git a/media/img/hub/headzilla.png b/media/img/hub/headzilla.png deleted file mode 100644 index 6af1ffd20da..00000000000 Binary files a/media/img/hub/headzilla.png and /dev/null differ diff --git a/media/img/hub/phase.png b/media/img/hub/phase.png deleted file mode 100644 index 394e6f94ee5..00000000000 Binary files a/media/img/hub/phase.png and /dev/null differ diff --git a/media/js/devreg/devhub.js b/media/js/devreg/devhub.js index 26b71738d69..da91a9aadef 100644 --- a/media/js/devreg/devhub.js +++ b/media/js/devreg/devhub.js @@ -776,8 +776,6 @@ function initUploadIcon() { $('#id_unsaved_icon_data').val(file.dataURL); $('#icons_default input:checked').attr('checked', false); - $('input[name="icon_type"][value="'+file.type+'"]') - .attr('checked', true); }, upload_start = function(e, file) { diff --git a/mkt/asset_bundles.py b/mkt/asset_bundles.py index b0aa990e3d2..1bd851fc06f 100644 --- a/mkt/asset_bundles.py +++ b/mkt/asset_bundles.py @@ -44,9 +44,6 @@ # Tables. 'css/devreg/data-grid.styl', - # Landing page - 'css/devreg/landing.styl', - # "Manage ..." pages. 'css/devreg/manage.styl', 'css/devreg/prose.styl', diff --git a/mkt/constants/base.py b/mkt/constants/base.py index 62bc0b5d07f..6bb2de87f05 100644 --- a/mkt/constants/base.py +++ b/mkt/constants/base.py @@ -163,7 +163,7 @@ MAX_CATEGORIES = 2 # Icon sizes we want to generate and expose in the API. -APP_ICON_SIZES = [32, 48, 64, 128] +CONTENT_ICON_SIZES = [32, 48, 64, 128] # Preview upload sizes [thumb, full] ADDON_PREVIEW_SIZES = [(200, 150), (700, 525)] diff --git a/mkt/developers/forms.py b/mkt/developers/forms.py index 8350c39be40..724e97e2b3a 100644 --- a/mkt/developers/forms.py +++ b/mkt/developers/forms.py @@ -48,12 +48,10 @@ from mkt.translations.models import Translation from mkt.translations.widgets import TranslationTextarea, TransTextarea from mkt.versions.models import Version -from mkt.webapps.forms import clean_slug, icons from mkt.webapps.models import (AddonUser, BlockedSlug, IARCInfo, Preview, Webapp) from mkt.webapps.tasks import (index_webapps, set_storefront_data, update_manifests) -from mkt.webapps.widgets import IconWidgetRenderer from . import tasks @@ -509,9 +507,6 @@ class Meta: models = Webapp fields = ('name', 'slug') - def clean_slug(self): - return clean_slug(self.cleaned_data['slug'], self.instance) - class AppFormBasic(AddonFormBase): """Form to edit basic app info.""" @@ -676,9 +671,6 @@ def clean(self): class AppFormMedia(AddonFormBase): - icon_type = forms.CharField( - required=False, - widget=forms.RadioSelect(renderer=IconWidgetRenderer, choices=[])) icon_upload_hash = forms.CharField(required=False) unsaved_icon_data = forms.CharField(required=False, widget=forms.HiddenInput) @@ -687,13 +679,6 @@ class Meta: model = Webapp fields = ('icon_upload_hash', 'icon_type') - def __init__(self, *args, **kwargs): - super(AppFormMedia, self).__init__(*args, **kwargs) - - # Add icons here so we only read the directory when - # AppFormMedia is actually being used. - self.fields['icon_type'].widget.choices = icons() - def save(self, addon, commit=True): if self.cleaned_data['icon_upload_hash']: upload_hash = self.cleaned_data['icon_upload_hash'] @@ -704,7 +689,7 @@ def save(self, addon, commit=True): remove_icons(destination) tasks.resize_icon.delay(upload_path, destination, - mkt.APP_ICON_SIZES, + mkt.CONTENT_ICON_SIZES, set_modified_on=[addon]) return super(AppFormMedia, self).save(commit) diff --git a/mkt/developers/tasks.py b/mkt/developers/tasks.py index 01a7e8fbc9a..d54d206524d 100644 --- a/mkt/developers/tasks.py +++ b/mkt/developers/tasks.py @@ -139,7 +139,7 @@ def _hash_file(fd): @post_request_task @set_modified_on def resize_icon(src, dst, sizes, locally=False, **kw): - """Resizes addon icons.""" + """Resizes addon/websites icons.""" log.info('[1@None] Resizing icon: %s' % dst) try: for s in sizes: @@ -160,7 +160,7 @@ def resize_icon(src, dst, sizes, locally=False, **kw): log.info('Icon resizing completed for: %s' % dst) return {'icon_hash': icon_hash} except Exception, e: - log.error("Error saving addon icon: %s; %s" % (e, dst)) + log.error("Error resizing icon: %s; %s" % (e, dst)) @task @@ -283,23 +283,27 @@ def get_content_and_check_size(response, max_size): return content -def save_icon(webapp, content): +def save_icon(obj, icon_content): + """ + Saves the icon for `obj` to its final destination. `obj` can be an app or a + website. + """ tmp_dst = os.path.join(settings.TMP_PATH, 'icon', uuid.uuid4().hex) with storage.open(tmp_dst, 'wb') as fd: - fd.write(content) + fd.write(icon_content) - dirname = webapp.get_icon_dir() - destination = os.path.join(dirname, '%s' % webapp.id) + dirname = obj.get_icon_dir() + destination = os.path.join(dirname, '%s' % obj.pk) remove_icons(destination) - resize_icon(tmp_dst, destination, mkt.APP_ICON_SIZES, - set_modified_on=[webapp]) + resize_icon(tmp_dst, destination, mkt.CONTENT_ICON_SIZES, + set_modified_on=[obj]) # Need to set the icon type so .get_icon_url() works # normally submit step 4 does it through AppFormMedia, # but we want to beat them to the punch. # resize_icon outputs pngs, so we know it's 'image/png' - webapp.icon_type = 'image/png' - webapp.save() + obj.icon_type = 'image/png' + obj.save() @post_request_task diff --git a/mkt/developers/templates/developers/apps/forms_shared/media.html b/mkt/developers/templates/developers/apps/forms_shared/media.html index e719570e5a8..e81f9c23f00 100644 --- a/mkt/developers/templates/developers/apps/forms_shared/media.html +++ b/mkt/developers/templates/developers/apps/forms_shared/media.html @@ -84,7 +84,7 @@
- +
{% endcall %} {% endif %} diff --git a/mkt/developers/tests/test_tasks.py b/mkt/developers/tests/test_tasks.py index 483b328e85b..39fa2b8cc42 100644 --- a/mkt/developers/tests/test_tasks.py +++ b/mkt/developers/tests/test_tasks.py @@ -507,7 +507,7 @@ def check_icons(self, webapp, file_obj=None): biggest = max([int(size) for size in manifest['icons']]) icon_dir = webapp.get_icon_dir() - for size in mkt.APP_ICON_SIZES: + for size in mkt.CONTENT_ICON_SIZES: if not size <= biggest: continue icon_path = os.path.join(icon_dir, '%s-%s.png' diff --git a/mkt/developers/tests/test_views_edit.py b/mkt/developers/tests/test_views_edit.py index eb436bcd022..7663378032f 100644 --- a/mkt/developers/tests/test_views_edit.py +++ b/mkt/developers/tests/test_views_edit.py @@ -638,21 +638,6 @@ def test_edit_defaulticon(self): for k in data: eq_(unicode(getattr(webapp, k)), data[k]) - def test_edit_preuploadedicon(self): - data = dict(icon_type='icon/appearance') - data_formset = self.formset_media(prev_blank=self.new_preview_hash(), - **data) - - r = self.client.post(self.edit_url, data_formset) - self.assertNoFormErrors(r) - webapp = self.get_webapp() - - assert webapp.get_icon_url(64).endswith('appearance-64.png') - assert webapp.get_icon_url(128).endswith('appearance-128.png') - - for k in data: - eq_(unicode(getattr(webapp, k)), data[k]) - def test_edit_uploadedicon(self): img = get_image_path('mozilla-sq.png') src_image = open(img, 'rb') diff --git a/mkt/operators/templates/operators/preloads.html b/mkt/operators/templates/operators/preloads.html index 1de4145e74d..20c6e10ca17 100644 --- a/mkt/operators/templates/operators/preloads.html +++ b/mkt/operators/templates/operators/preloads.html @@ -23,7 +23,7 @@

{{ _('Preload Candidates') }}

{% for preload in preloads %} - +

{{ preload.addon.name }}

{{ preload.addon.description|truncate(length=150) }}

diff --git a/mkt/reviewers/templates/reviewers/review.html b/mkt/reviewers/templates/reviewers/review.html index 3fdde6591bf..3ab93b631f1 100644 --- a/mkt/reviewers/templates/reviewers/review.html +++ b/mkt/reviewers/templates/reviewers/review.html @@ -14,7 +14,7 @@

- + {{ product.name }}

{{ _('by') }} {{ product.listed_authors|join(', ', attribute='email') }}

diff --git a/mkt/search/tests/test_views.py b/mkt/search/tests/test_views.py index 3ae3b0b5e34..f262c9c1ae9 100644 --- a/mkt/search/tests/test_views.py +++ b/mkt/search/tests/test_views.py @@ -264,6 +264,8 @@ def test_dehydrate(self): {'en-US': self.webapp.description.localized_string}) eq_(obj['icons']['128'], self.webapp.get_icon_url(128)) ok_(obj['icons']['128'].endswith('?modified=fakehash')) + eq_(sorted(int(k) for k in obj['icons'].keys()), + mkt.CONTENT_ICON_SIZES) eq_(obj['id'], long(self.webapp.id)) eq_(obj['is_offline'], False) eq_(obj['manifest_url'], self.webapp.get_manifest_url()) diff --git a/mkt/settings.py b/mkt/settings.py index 00320116107..9cbbada9993 100644 --- a/mkt/settings.py +++ b/mkt/settings.py @@ -320,14 +320,13 @@ def path(*args): PREVIEWS_PATH = UPLOADS_PATH + '/previews' -ADDON_ICONS_DEFAULT_PATH = os.path.join(MEDIA_ROOT, 'img/hub') ADDON_ICONS_PATH = UPLOADS_PATH + '/addon_icons' +WEBSITE_ICONS_PATH = UPLOADS_PATH + '/website_icons' # File path for storing XPI/JAR files (or any files associated with an # add-on). Example: /mnt/netapp_amo/addons.mozilla.org-remora/files ADDONS_PATH = NETAPP_STORAGE + '/addons' CA_CERT_BUNDLE_PATH = os.path.join(ROOT, 'mkt/site/certificates/roots.pem') -COLLECTIONS_ICON_PATH = UPLOADS_PATH + '/collection_icons' # Where dumped apps will be written too. DUMPED_APPS_PATH = NETAPP_STORAGE + '/dumped-apps' @@ -371,8 +370,11 @@ def path(*args): SITE_URL = 'http://%s' % DOMAIN STATIC_URL = SITE_URL + '/' -ADDON_ICONS_DEFAULT_URL = 'img/hub' +ICONS_DEFAULT_URL = 'img/hub' + +# Directory must match ADDON_ICONS_PATH and WEBSITE_ICONS_PATH, respectively. ADDON_ICON_URL = 'img/uploads/addon_icons/%s/%s-%s.png?modified=%s' +WEBSITE_ICON_URL = 'img/uploads/website_icons/%s/%s-%s.png?modified=%s' LOCAL_MIRROR_URL = 'https://marketplace.cdn.mozilla.net/_files' PREVIEW_THUMBNAIL_URL = 'img/uploads/previews/thumbs/%s/%d.png?modified=%d' diff --git a/mkt/site/monitors.py b/mkt/site/monitors.py index b539631411d..92e688381fb 100644 --- a/mkt/site/monitors.py +++ b/mkt/site/monitors.py @@ -132,7 +132,7 @@ def path(): settings.ADDONS_PATH, settings.GUARDED_ADDONS_PATH, settings.ADDON_ICONS_PATH, - settings.COLLECTIONS_ICON_PATH, + settings.WEBSITE_ICONS_PATH, settings.PREVIEWS_PATH, settings.REVIEWER_ATTACHMENTS_PATH,) r = [os.path.join(settings.ROOT, 'locale')] diff --git a/mkt/site/utils.py b/mkt/site/utils.py index 6d5dabe39e3..19cb96c1720 100644 --- a/mkt/site/utils.py +++ b/mkt/site/utils.py @@ -41,6 +41,7 @@ from tower import ugettext as _, ungettext as ngettext import mkt +from lib.utils import static_url from mkt.access import acl from mkt.api.paginator import ESPaginator from mkt.constants.applications import DEVICE_TYPES @@ -228,7 +229,7 @@ def resize_image(src, dst, size=None, remove_src=True, locally=False): def remove_icons(destination): - for size in mkt.APP_ICON_SIZES: + for size in mkt.CONTENT_ICON_SIZES: filename = '%s-%s.png' % (destination, size) if storage.exists(filename): storage.delete(filename) @@ -736,3 +737,30 @@ def clean_tags(request, tags): raise forms.ValidationError(msg) return target + + +def get_icon_url(base_url_format, obj, size): + """ + Returns either the icon URL for a given (`obj`, `size`). base_url_format` + is a string that will be used for url formatting, see ADDON_ICON_URL for an + example. + + If no icon type if set on the `obj`, then the url for the + appropriate default icon for the given `size` will be returned. + + `obj` needs to implement `icon_type` and `icon_hash` properties for this + function to work. + + Note: does not check size, so it can return 404 URLs if you specify an + invalid size. + """ + # Return default image if no icon_type was stored. + if not obj.icon_type: + return '%s/default-%s.png' % (static_url('ICONS_DEFAULT_URL'), size) + else: + # [1] is the whole ID, [2] is the directory. + split_id = re.match(r'((\d*?)\d{1,3})$', str(obj.pk)) + # If we don't have the icon_hash set to a dummy string ("never"), + # when the icon is eventually changed, icon_hash will be updated. + suffix = obj.icon_hash or 'never' + return base_url_format % (split_id.group(2) or 0, obj.pk, size, suffix) diff --git a/mkt/submit/tests/test_views.py b/mkt/submit/tests/test_views.py index 2151eccfddf..738d9d1fb82 100644 --- a/mkt/submit/tests/test_views.py +++ b/mkt/submit/tests/test_views.py @@ -805,7 +805,7 @@ def test_icon(self): eq_(rp.status_code, 302) ad = self.get_webapp() eq_(ad.icon_type, 'image/png') - for size in mkt.APP_ICON_SIZES: + for size in mkt.CONTENT_ICON_SIZES: fn = '%s-%s.png' % (ad.id, size) assert os.path.exists(os.path.join(ad.get_icon_dir(), fn)), ( 'Expected %s in %s' % (fn, os.listdir(ad.get_icon_dir()))) diff --git a/mkt/webapps/forms.py b/mkt/webapps/forms.py deleted file mode 100644 index 65d49cab711..00000000000 --- a/mkt/webapps/forms.py +++ /dev/null @@ -1,42 +0,0 @@ -from django import forms -from django.conf import settings -from django.core.files.storage import default_storage as storage - -import commonware.log -from tower import ugettext as _ - -from mkt.site.utils import slug_validator - -from .models import BlockedSlug, Webapp - - -log = commonware.log.getLogger('z.addons') - - -def clean_slug(slug, instance): - slug_validator(slug, lower=False) - slug_field = 'app_slug' - - if slug != getattr(instance, slug_field): - if Webapp.objects.filter(**{slug_field: slug}).exists(): - raise forms.ValidationError( - _('This slug is already in use. Please choose another.')) - if BlockedSlug.blocked(slug): - raise forms.ValidationError( - _('The slug cannot be "%s". Please choose another.' % slug)) - - return slug - - -def icons(): - """ - Generates a list of tuples for the default icons for add-ons, - in the format (psuedo-mime-type, description). - """ - icons = [('image/jpeg', 'jpeg'), ('image/png', 'png'), ('', 'default')] - dirs, files = storage.listdir(settings.ADDON_ICONS_DEFAULT_PATH) - for fname in files: - if '32' in fname and 'default' not in fname: - icon_name = fname.split('-')[0] - icons.append(('icon/%s' % icon_name, icon_name)) - return icons diff --git a/mkt/webapps/management/commands/convert_icons.py b/mkt/webapps/management/commands/convert_icons.py index 369120c958e..46fcb51dc4b 100644 --- a/mkt/webapps/management/commands/convert_icons.py +++ b/mkt/webapps/management/commands/convert_icons.py @@ -13,7 +13,7 @@ extensions = ['.png', '.jpg', '.gif'] -sizes = mkt.APP_ICON_SIZES +sizes = mkt.CONTENT_ICON_SIZES size_suffixes = ['-%s' % s for s in sizes] diff --git a/mkt/webapps/models.py b/mkt/webapps/models.py index 568d6b17ce5..c26ceee7aea 100644 --- a/mkt/webapps/models.py +++ b/mkt/webapps/models.py @@ -5,7 +5,6 @@ import json import operator import os -import re import time import urlparse import uuid @@ -49,7 +48,7 @@ from mkt.site.models import (DynamicBoolFieldsMixin, ManagerBase, ModelBase, OnChangeMixin) from mkt.site.storage_utils import copy_stored_file -from mkt.site.utils import (cached_property, slugify, smart_path, +from mkt.site.utils import (cached_property, get_icon_url, slugify, smart_path, sorted_groupby, urlparams) from mkt.tags.models import Tag from mkt.translations.fields import (PurifiedField, save_signal, @@ -804,17 +803,6 @@ def has_author(self, user, roles=None): return AddonUser.objects.filter(addon=self, user=user, role__in=roles).exists() - @property - def thumbnail_url(self): - """ - Returns the addon's thumbnail url or a default. - """ - try: - preview = self.all_previews[0] - return preview.thumbnail_url - except IndexError: - return settings.MEDIA_URL + '/img/icons/no-preview.png' - def get_purchase_type(self, user): if user and isinstance(user, UserProfile): try: @@ -961,10 +949,6 @@ def upsold(self): except IndexError: pass - @property - def icon_url(self): - return self.get_icon_url(32) - @classmethod def get_fallback(cls): return cls._meta.get_field('default_locale') @@ -983,34 +967,7 @@ def get_icon_dir(self): return os.path.join(settings.ADDON_ICONS_PATH, str(self.id / 1000)) def get_icon_url(self, size): - """ - Returns either the icon URL or a default icon. - """ - icon_type_split = [] - if self.icon_type: - icon_type_split = self.icon_type.split('/') - - # Get the closest allowed size without going over. - if (size not in mkt.APP_ICON_SIZES and size >= mkt.APP_ICON_SIZES[0]): - size = [s for s in mkt.APP_ICON_SIZES if s < size][-1] - elif size < mkt.APP_ICON_SIZES[0]: - size = mkt.APP_ICON_SIZES[0] - - # Figure out what to return for an image URL. - if not self.icon_type: - return '%s/%s-%s.png' % (static_url('ADDON_ICONS_DEFAULT_URL'), - 'default', size) - elif icon_type_split[0] == 'icon': - return '%s/%s-%s.png' % (static_url('ADDON_ICONS_DEFAULT_URL'), - icon_type_split[1], size) - else: - # [1] is the whole ID, [2] is the directory. - split_id = re.match(r'((\d*?)\d{1,3})$', str(self.id)) - # If we don't have the icon_hash set to a dummy string ("never"), - # when the icon is eventually changed, icon_hash will be updated. - suffix = getattr(self, 'icon_hash', None) or 'never' - return static_url('ADDON_ICON_URL') % ( - split_id.group(2) or 0, self.id, size, suffix) + return get_icon_url(static_url('ADDON_ICON_URL'), self, size) @staticmethod def transformer(apps): diff --git a/mkt/webapps/serializers.py b/mkt/webapps/serializers.py index 0ffc8bd8ff1..b57787a6d43 100644 --- a/mkt/webapps/serializers.py +++ b/mkt/webapps/serializers.py @@ -173,7 +173,7 @@ def get_content_ratings(self, app): def get_icons(self, app): return dict([(icon_size, app.get_icon_url(icon_size)) - for icon_size in mkt.APP_ICON_SIZES]) + for icon_size in mkt.CONTENT_ICON_SIZES]) def get_payment_account(self, app): # Avoid a query for payment_account if the app is not premium. diff --git a/mkt/webapps/tests/test_models.py b/mkt/webapps/tests/test_models.py index 00ab5eec86b..d8c88eb2e76 100644 --- a/mkt/webapps/tests/test_models.py +++ b/mkt/webapps/tests/test_models.py @@ -79,30 +79,18 @@ def add_payment_account(self, app, provider_id, user=None): return AddonPaymentAccount.objects.create( addon=app, payment_account=payment, product_uri=uuid.uuid4()) - def test_icon_url(self): + def test_get_icon_url(self): app = self.get_app() expected = (static_url('ADDON_ICON_URL') % (str(app.id)[0:3], app.id, 32, 'never')) - assert app.icon_url.endswith(expected), ( + assert app.get_icon_url(32).endswith(expected), ( 'Expected %s, got %s' % (expected, app.icon_url)) app.icon_hash = 'abcdef' - assert app.icon_url.endswith('?modified=abcdef') + assert app.get_icon_url(32).endswith('?modified=abcdef') app.icon_type = None - assert app.icon_url.endswith('hub/default-32.png') - - def test_thumbnail_url_no_preview(self): - app = self.get_app() - assert app.thumbnail_url.endswith('/icons/no-preview.png'), ( - 'No match for %s' % app.thumbnail_url) - - def test_thumbnail_url(self): - app = self.get_app() - preview = Preview.objects.create(addon=app, filetype='image/png', - position=0) - assert app.thumbnail_url.index('/previews/thumbs/%s/%s.png?modified=' - % (preview.id / 1000, preview.id)) + assert app.get_icon_url(32).endswith('hub/default-32.png') def test_has_payment_account(self): app = self.get_app() diff --git a/mkt/webapps/widgets.py b/mkt/webapps/widgets.py deleted file mode 100644 index 546f5fdddd2..00000000000 --- a/mkt/webapps/widgets.py +++ /dev/null @@ -1,24 +0,0 @@ -from django import forms -from django.utils.safestring import mark_safe - -from lib.utils import static_url - - -class IconWidgetRenderer(forms.RadioSelect.renderer): - """ Return radiobox as a list of images. """ - - def render(self): - """ This will output radios as li>img+input. """ - output = [] - for w in self: - value = w.choice_value - if value.split('/')[0] == 'icon' or value == '': - o = (("
  • " - "%s
  • ") % - ('active' if self.value == w.choice_value else '', - static_url('ADDON_ICONS_DEFAULT_URL'), - w.choice_label, w)) - else: - o = "
  • %s
  • " % w - output.append(o) - return mark_safe(u'\n'.join(output)) diff --git a/mkt/webpay/webpay_jwt.py b/mkt/webpay/webpay_jwt.py index d865c11a13c..8b05fdb0996 100644 --- a/mkt/webpay/webpay_jwt.py +++ b/mkt/webpay/webpay_jwt.py @@ -112,7 +112,7 @@ def price(self): def icons(self): icons = {} - for size in mkt.APP_ICON_SIZES: + for size in mkt.CONTENT_ICON_SIZES: icons[str(size)] = absolutify(self.webapp.get_icon_url(size)) return icons diff --git a/mkt/websites/models.py b/mkt/websites/models.py index 98c8818ba12..7f0c125a82d 100644 --- a/mkt/websites/models.py +++ b/mkt/websites/models.py @@ -1,13 +1,17 @@ # -*- coding: utf-8 -*- +import os.path + from django.conf import settings from django.db import models from django.dispatch import receiver from django_extensions.db.fields.json import JSONField +from lib.utils import static_url from mkt.constants.applications import DEVICE_TYPES from mkt.constants.base import LISTED_STATUSES, STATUS_CHOICES, STATUS_NULL from mkt.site.models import ManagerBase, ModelBase +from mkt.site.utils import get_icon_url from mkt.tags.models import Tag from mkt.translations.fields import save_signal, TranslatedField from mkt.translations.utils import no_translation @@ -69,6 +73,12 @@ def is_dummy_content_for_qa(self): # Webapp implementation. return False + def get_icon_dir(self): + return os.path.join(settings.WEBSITE_ICONS_PATH, str(self.pk / 1000)) + + def get_icon_url(self, size): + return get_icon_url(static_url('WEBSITE_ICON_URL'), self, size) + class WebsitePopularity(ModelBase): website = models.ForeignKey(Website, related_name='popularity') diff --git a/mkt/websites/serializers.py b/mkt/websites/serializers.py index b8940a019fb..46085c9d840 100644 --- a/mkt/websites/serializers.py +++ b/mkt/websites/serializers.py @@ -1,6 +1,7 @@ from drf_compound_fields.fields import ListField from rest_framework import serializers +from mkt.constants.base import CONTENT_ICON_SIZES from mkt.api.fields import TranslationSerializerField from mkt.search.serializers import BaseESSerializer from mkt.websites.models import Website @@ -14,15 +15,20 @@ class WebsiteSerializer(serializers.ModelSerializer): short_name = TranslationSerializerField() name = TranslationSerializerField() title = TranslationSerializerField() + icons = serializers.SerializerMethodField('get_icons') - # FIXME: keywords, regions, icons... try to stay compatible with Webapp API + # FIXME: keywords, regions. try to stay compatible with Webapp API # as much as possible. class Meta: model = Website - fields = ['categories', 'description', 'device_types', 'id', + fields = ['categories', 'description', 'device_types', 'icons', 'id', 'mobile_url', 'name', 'short_name', 'title', 'url'] + def get_icons(self, obj): + return dict([(icon_size, obj.get_icon_url(icon_size)) + for icon_size in CONTENT_ICON_SIZES]) + class ESWebsiteSerializer(BaseESSerializer, WebsiteSerializer): def fake_object(self, data): @@ -30,13 +36,18 @@ def fake_object(self, data): obj = Website(id=data['id']) # Set basic attributes on the fake instance using the data from ES. - self._attach_fields(obj, data, ('default_locale', 'url')) + self._attach_fields(obj, data, ('default_locale', 'icon_hash', 'url')) # Set attributes with names that don't exactly match the one on the # model. obj.categories = data['category'] obj.devices = data['device'] + if obj.icon_hash: + # If we have an icon_hash, then we have an icon. All the icons we + # store are PNGs. + obj.icon_type = 'image/png' + # Attach translations for all translated attributes. obj.default_locale # should be set first for this to work. self._attach_translations( diff --git a/mkt/websites/tests/test_models.py b/mkt/websites/tests/test_models.py index d71152aa69d..ca89a84234a 100644 --- a/mkt/websites/tests/test_models.py +++ b/mkt/websites/tests/test_models.py @@ -1,11 +1,43 @@ import mock from nose.tools import eq_ +from lib.utils import static_url from mkt.site.tests import TestCase from mkt.websites.models import Website from mkt.websites.utils import website_factory +class TestWebsiteModel(TestCase): + def test_get_icon_url(self): + website = Website(pk=1, icon_type='image/png') + expected = (static_url('WEBSITE_ICON_URL') + % ('0', website.pk, 32, 'never')) + assert website.get_icon_url(32).endswith(expected), ( + 'Expected %s, got %s' % (expected, website.get_icon_url(32))) + + def test_get_icon_url_big_pk(self): + website = Website(pk=9876, icon_type='image/png') + expected = (static_url('WEBSITE_ICON_URL') + % (str(website.pk)[:-3], website.pk, 32, 'never')) + assert website.get_icon_url(32).endswith(expected), ( + 'Expected %s, got %s' % (expected, website.get_icon_url(32))) + + def test_get_icon_url_bigger_pk(self): + website = Website(pk=98765432, icon_type='image/png') + expected = (static_url('WEBSITE_ICON_URL') + % (str(website.pk)[:-3], website.pk, 32, 'never')) + assert website.get_icon_url(32).endswith(expected), ( + 'Expected %s, got %s' % (expected, website.get_icon_url(32))) + + def test_get_icon_url_hash(self): + website = Website(pk=1, icon_type='image/png', icon_hash='abcdef') + assert website.get_icon_url(32).endswith('?modified=abcdef') + + def test_get_icon_no_icon(self): + website = Website(pk=1) + assert website.get_icon_url(32).endswith('/default-32.png') + + class TestWebsiteESIndexation(TestCase): @mock.patch('mkt.search.indexers.BaseIndexer.index_ids') def test_update_search_index(self, update_mock): diff --git a/mkt/websites/tests/test_views.py b/mkt/websites/tests/test_views.py index cd45d33f639..1351e4066aa 100644 --- a/mkt/websites/tests/test_views.py +++ b/mkt/websites/tests/test_views.py @@ -2,10 +2,10 @@ from django.core.urlresolvers import reverse -from nose.tools import eq_ +from nose.tools import eq_, ok_ from mkt.api.tests.test_oauth import RestOAuth -from mkt.constants.base import STATUS_PENDING +from mkt.constants.base import CONTENT_ICON_SIZES, STATUS_PENDING from mkt.constants.applications import DEVICE_GAIA, DEVICE_DESKTOP from mkt.constants.regions import BRA, GTM, URY from mkt.site.fixtures import fixture @@ -26,6 +26,8 @@ def setUp(self): # array of ids, not slugs. 'devices': json.dumps([DEVICE_GAIA.id, DEVICE_DESKTOP.id]), 'region_exclusions': json.dumps([BRA.id, GTM.id, URY.id]), + 'icon_type': 'image/png', + 'icon_hash': 'fakehash', }) self.category = 'books' self.url = reverse('api-v2:website-search-api') @@ -55,7 +57,9 @@ def test_basic(self): eq_(data['url'], self.website.url) eq_(data['device_types'], ['firefoxos', 'desktop']) eq_(data['categories'], ['books', 'sports']) - # FIXME: regions, keywords, icon + eq_(data['icons']['128'], self.website.get_icon_url(128)) + ok_(data['icons']['128'].endswith('?modified=fakehash')) + eq_(sorted(int(k) for k in data['icons'].keys()), CONTENT_ICON_SIZES) def test_list(self): self.website2 = website_factory(url='http://www.lol.com/') @@ -136,6 +140,8 @@ def setUp(self): # array of ids, not slugs. 'devices': json.dumps([DEVICE_GAIA.id, DEVICE_DESKTOP.id]), 'region_exclusions': json.dumps([BRA.id, GTM.id, URY.id]), + 'icon_type': 'image/png', + 'icon_hash': 'fakehash', }) self.url = reverse('api-v2:website-detail', kwargs={'pk': self.website.pk}) @@ -157,7 +163,9 @@ def test_basic(self): eq_(data['url'], self.website.url) eq_(data['device_types'], ['firefoxos', 'desktop']) eq_(data['categories'], ['books', 'sports']) - # FIXME: regions, keywords, icon + eq_(data['icons']['128'], self.website.get_icon_url(128)) + ok_(data['icons']['128'].endswith('?modified=fakehash')) + eq_(sorted(int(k) for k in data['icons'].keys()), CONTENT_ICON_SIZES) def test_disabled(self): self.website.update(is_disabled=True) diff --git a/settings_test.py b/settings_test.py index a88a0cb2ffb..bb3d183ef60 100644 --- a/settings_test.py +++ b/settings_test.py @@ -32,12 +32,13 @@ def _polite_tmpdir(): # Various paths. See mkt/settings.py for documentation: NETAPP_STORAGE = _polite_tmpdir() ADDONS_PATH = _polite_tmpdir() +ADDON_ICONS_PATH = _polite_tmpdir() +WEBSITE_ICONS_PATH = _polite_tmpdir() GUARDED_ADDONS_PATH = _polite_tmpdir() SIGNED_APPS_PATH = _polite_tmpdir() SIGNED_APPS_REVIEWER_PATH = _polite_tmpdir() UPLOADS_PATH = _polite_tmpdir() TMP_PATH = _polite_tmpdir() -COLLECTIONS_ICON_PATH = _polite_tmpdir() REVIEWER_ATTACHMENTS_PATH = _polite_tmpdir() DUMPED_APPS_PATH = _polite_tmpdir() diff --git a/sites/altdev/settings_base.py b/sites/altdev/settings_base.py index ca9ffc4abdf..ff0b9c71225 100644 --- a/sites/altdev/settings_base.py +++ b/sites/altdev/settings_base.py @@ -66,7 +66,6 @@ GUARDED_ADDONS_PATH = private.NETAPP_STORAGE_ROOT + '/guarded-addons' UPLOADS_PATH = NETAPP_STORAGE + '/uploads' ADDON_ICONS_PATH = UPLOADS_PATH + '/addon_icons' -COLLECTIONS_ICON_PATH = UPLOADS_PATH + '/collection_icons' FEATURED_APP_BG_PATH = UPLOADS_PATH + '/featured_app_background' FEED_COLLECTION_BG_PATH = UPLOADS_PATH + '/feed_collection_background' FEED_SHELF_BG_PATH = UPLOADS_PATH + '/feed_shelf_background' diff --git a/sites/altdev/settings_mkt.py b/sites/altdev/settings_mkt.py index 79ec92da5fd..169bed47b61 100644 --- a/sites/altdev/settings_mkt.py +++ b/sites/altdev/settings_mkt.py @@ -17,10 +17,6 @@ CSP_SCRIPT_SRC = CSP_SCRIPT_SRC + (STATIC_URL[:-1],) -ADDON_ICON_URL = 'img/uploads/addon_icons/%s/%s-%s.png?modified=%s' -PREVIEW_THUMBNAIL_URL = 'img/uploads/previews/thumbs/%s/%d.png?modified=%d' -PREVIEW_FULL_URL = 'img/uploads/previews/full/%s/%d.%s?modified=%d' - SESSION_COOKIE_SECURE = True SESSION_COOKIE_DOMAIN = ".%s" % DOMAIN diff --git a/sites/dev/settings_base.py b/sites/dev/settings_base.py index 42c1a4e380e..bb30565abab 100644 --- a/sites/dev/settings_base.py +++ b/sites/dev/settings_base.py @@ -68,7 +68,6 @@ GUARDED_ADDONS_PATH = private.NETAPP_STORAGE_ROOT + '/guarded-addons' UPLOADS_PATH = NETAPP_STORAGE + '/uploads' ADDON_ICONS_PATH = UPLOADS_PATH + '/addon_icons' -COLLECTIONS_ICON_PATH = UPLOADS_PATH + '/collection_icons' FEATURED_APP_BG_PATH = UPLOADS_PATH + '/featured_app_background' FEED_COLLECTION_BG_PATH = UPLOADS_PATH + '/feed_collection_background' FEED_SHELF_BG_PATH = UPLOADS_PATH + '/feed_shelf_background' diff --git a/sites/dev/settings_mkt.py b/sites/dev/settings_mkt.py index 4eab0b3defc..97af8eb9c01 100644 --- a/sites/dev/settings_mkt.py +++ b/sites/dev/settings_mkt.py @@ -18,10 +18,6 @@ CSP_SCRIPT_SRC = CSP_SCRIPT_SRC + (STATIC_URL[:-1],) -ADDON_ICON_URL = 'img/uploads/addon_icons/%s/%s-%s.png?modified=%s' -PREVIEW_THUMBNAIL_URL = 'img/uploads/previews/thumbs/%s/%d.png?modified=%d' -PREVIEW_FULL_URL = 'img/uploads/previews/full/%s/%d.%s?modified=%d' - SESSION_COOKIE_SECURE = True SESSION_COOKIE_DOMAIN = ".%s" % DOMAIN diff --git a/sites/identitystage/settings_base.py b/sites/identitystage/settings_base.py index 71977c7f213..c4298daba32 100644 --- a/sites/identitystage/settings_base.py +++ b/sites/identitystage/settings_base.py @@ -63,7 +63,6 @@ GUARDED_ADDONS_PATH = private.NETAPP_STORAGE_ROOT + '/guarded-addons' UPLOADS_PATH = NETAPP_STORAGE + '/uploads' ADDON_ICONS_PATH = UPLOADS_PATH + '/addon_icons' -COLLECTIONS_ICON_PATH = UPLOADS_PATH + '/collection_icons' IMAGEASSETS_PATH = UPLOADS_PATH + '/imageassets' REVIEWER_ATTACHMENTS_PATH = UPLOADS_PATH + '/reviewer_attachment' PREVIEWS_PATH = UPLOADS_PATH + '/previews' diff --git a/sites/identitystage/settings_mkt.py b/sites/identitystage/settings_mkt.py index c5dc71c9543..56a4fee482f 100644 --- a/sites/identitystage/settings_mkt.py +++ b/sites/identitystage/settings_mkt.py @@ -27,10 +27,6 @@ NATIVE_BROWSERID_JS_URL = ('https://%s/include.js' % NATIVE_BROWSERID_DOMAIN) -ADDON_ICON_URL = 'img/uploads/addon_icons/%s/%s-%s.png?modified=%s' -PREVIEW_THUMBNAIL_URL = 'img/uploads/previews/thumbs/%s/%d.png?modified=%d' -PREVIEW_FULL_URL = 'img/uploads/previews/full/%s/%d.%s?modified=%d' - SESSION_COOKIE_SECURE = True SESSION_COOKIE_DOMAIN = ".%s" % DOMAIN diff --git a/sites/paymentsalt/settings_base.py b/sites/paymentsalt/settings_base.py index 1db6d4ef285..05fe00355b1 100644 --- a/sites/paymentsalt/settings_base.py +++ b/sites/paymentsalt/settings_base.py @@ -66,7 +66,6 @@ GUARDED_ADDONS_PATH = private.NETAPP_STORAGE_ROOT + '/guarded-addons' UPLOADS_PATH = NETAPP_STORAGE + '/uploads' ADDON_ICONS_PATH = UPLOADS_PATH + '/addon_icons' -COLLECTIONS_ICON_PATH = UPLOADS_PATH + '/collection_icons' IMAGEASSETS_PATH = UPLOADS_PATH + '/imageassets' REVIEWER_ATTACHMENTS_PATH = UPLOADS_PATH + '/reviewer_attachment' PREVIEWS_PATH = UPLOADS_PATH + '/previews' diff --git a/sites/paymentsalt/settings_mkt.py b/sites/paymentsalt/settings_mkt.py index bfac0d75204..a6c7e40d763 100644 --- a/sites/paymentsalt/settings_mkt.py +++ b/sites/paymentsalt/settings_mkt.py @@ -15,10 +15,6 @@ CSP_SCRIPT_SRC = CSP_SCRIPT_SRC + (STATIC_URL[:-1],) -ADDON_ICON_URL = 'img/uploads/addon_icons/%s/%s-%s.png?modified=%s' -PREVIEW_THUMBNAIL_URL = 'img/uploads/previews/thumbs/%s/%d.png?modified=%d' -PREVIEW_FULL_URL = 'img/uploads/previews/full/%s/%d.%s?modified=%d' - SESSION_COOKIE_SECURE = True SESSION_COOKIE_DOMAIN = ".%s" % DOMAIN diff --git a/sites/prod/settings_base.py b/sites/prod/settings_base.py index 4e25a882330..cd661994cf4 100644 --- a/sites/prod/settings_base.py +++ b/sites/prod/settings_base.py @@ -68,7 +68,6 @@ GUARDED_ADDONS_PATH = NETAPP_STORAGE_ROOT + '/guarded-addons' UPLOADS_PATH = NETAPP_STORAGE + '/uploads' ADDON_ICONS_PATH = UPLOADS_PATH + '/addon_icons' -COLLECTIONS_ICON_PATH = UPLOADS_PATH + '/collection_icons' FEATURED_APP_BG_PATH = UPLOADS_PATH + '/featured_app_background' FEED_COLLECTION_BG_PATH = UPLOADS_PATH + '/feed_collection_background' FEED_SHELF_BG_PATH = UPLOADS_PATH + '/feed_shelf_background' diff --git a/sites/prod/settings_mkt.py b/sites/prod/settings_mkt.py index 248e2140700..009f24668ff 100644 --- a/sites/prod/settings_mkt.py +++ b/sites/prod/settings_mkt.py @@ -15,10 +15,6 @@ CSP_SCRIPT_SRC = CSP_SCRIPT_SRC + (STATIC_URL[:-1],) -ADDON_ICON_URL = 'img/uploads/addon_icons/%s/%s-%s.png?modified=%s' -PREVIEW_THUMBNAIL_URL = 'img/uploads/previews/thumbs/%s/%d.png?modified=%d' -PREVIEW_FULL_URL = 'img/uploads/previews/full/%s/%d.%s?modified=%d' - PREVIEW_FULL_PATH = PREVIEWS_PATH + '/full/%s/%d.%s' SESSION_COOKIE_SECURE = True diff --git a/sites/stage/settings_base.py b/sites/stage/settings_base.py index 0fa149aec41..89be7de4324 100644 --- a/sites/stage/settings_base.py +++ b/sites/stage/settings_base.py @@ -66,7 +66,6 @@ GUARDED_ADDONS_PATH = private.NETAPP_STORAGE_ROOT + '/guarded-addons' UPLOADS_PATH = NETAPP_STORAGE + '/uploads' ADDON_ICONS_PATH = UPLOADS_PATH + '/addon_icons' -COLLECTIONS_ICON_PATH = UPLOADS_PATH + '/collection_icons' FEATURED_APP_BG_PATH = UPLOADS_PATH + '/featured_app_background' FEED_COLLECTION_BG_PATH = UPLOADS_PATH + '/feed_collection_background' FEED_SHELF_BG_PATH = UPLOADS_PATH + '/feed_shelf_background' diff --git a/sites/stage/settings_mkt.py b/sites/stage/settings_mkt.py index 967ff19d1bf..7e87bfe9ac2 100644 --- a/sites/stage/settings_mkt.py +++ b/sites/stage/settings_mkt.py @@ -16,10 +16,6 @@ CSP_SCRIPT_SRC = CSP_SCRIPT_SRC + (STATIC_URL[:-1],) -ADDON_ICON_URL = 'img/uploads/addon_icons/%s/%s-%s.png?modified=%s' -PREVIEW_THUMBNAIL_URL = 'img/uploads/previews/thumbs/%s/%d.png?modified=%d' -PREVIEW_FULL_URL = 'img/uploads/previews/full/%s/%d.%s?modified=%d' - SESSION_COOKIE_SECURE = True SESSION_COOKIE_DOMAIN = ".%s" % DOMAIN