allow externally hosted addons to be added as PrimaryHero shelves (#11970)

This commit is contained in:
Andrew Williamson 2019-08-02 11:19:10 +01:00 коммит произвёл GitHub
Родитель 5aad9783c3
Коммит ea8ac4633a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
11 изменённых файлов: 96 добавлений и 27 удалений

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

@ -42,4 +42,5 @@ small number of shelves this endpoint is not paginated.
:>json string results[].featured_image: The image used to illustrate the item.
:>json string results[].heading: The heading for this item. May contain some HTML tags.
:>json string|null results[].description: The description for this item, if any. May contain some HTML tags.
:>json object results[].addon: The :ref:`add-on <addon-detail-object>` for this item. Only a subset of fields are present: ``id``, ``authors``, ``average_daily_users``, ``current_version`` (with only the ``id``, ``compatibility``, ``is_strict_compatibility_enabled`` and ``files`` fields present), ``guid``, ``icon_url``, ``name``, ``ratings``, ``previews``, ``slug``, ``theme_data``, ``type`` and ``url``.
:>json object results[].addon: The :ref:`add-on <addon-detail-object>` for this item if the addon is hosted on AMO. Either this field or ``external`` will be present. Only a subset of fields are present: ``id``, ``authors``, ``average_daily_users``, ``current_version`` (with only the ``id``, ``compatibility``, ``is_strict_compatibility_enabled`` and ``files`` fields present), ``guid``, ``icon_url``, ``name``, ``ratings``, ``previews``, ``slug``, ``theme_data``, ``type`` and ``url``.
:>json object results[].external: The :ref:`add-on <addon-detail-object>` for this item if the addon is externally hosted. Either this field or ``addon`` will be present. Only a subset of fields are present: ``id``, ``guid``, ``homepage``, ``name`` and ``type``.

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

@ -331,6 +331,7 @@ v4 API changelog
* 2019-06-27: removed ``sort`` parameter from addon autocomplete API. https://github.com/mozilla/addons-server/issues/11664
* 2019-07-25: added /hero/ endpoint to expose recommended addons and other content to frontend to allow customizable promos https://github.com/mozilla/addons-server/issues/11842.
* 2019-08-01: added alias ``edition=MozillaOnline`` for ``edition=china`` in /discovery/ endpoint.
* 2019-08-08: add support for externally hosted addons to /hero/ endpoints. https://github.com/mozilla/addons-server/issues/11882
----------------
v5 API changelog

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

@ -88,7 +88,7 @@ class DiscoveryItemAdmin(admin.ModelAdmin):
kwargs['widget'] = ForeignKeyRawIdWidget(
db_field.remote_field, self.admin_site,
using=kwargs.get('using'))
kwargs['queryset'] = Addon.objects.valid()
kwargs['queryset'] = Addon.objects.all()
kwargs['help_text'] = db_field.help_text
return SlugOrPkChoiceField(**kwargs)
return super(DiscoveryItemAdmin, self).formfield_for_foreignkey(

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

@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
from olympia import amo
from olympia.amo.tests import TestCase, addon_factory, user_factory
from olympia.amo.urlresolvers import django_reverse, reverse
from olympia.discovery.models import DiscoveryItem
@ -297,19 +296,6 @@ class TestDiscoveryAdmin(TestCase):
item.reload()
assert item.addon == addon
# Try changing using a non-public add-on id.
addon3 = addon_factory(status=amo.STATUS_DISABLED)
response = self.client.post(
self.detail_url,
dict(self._get_heroform(str(item.id)), **{
'addon': str(addon3.pk)}),
follow=True)
assert response.status_code == 200
assert not response.context_data['adminform'].form.is_valid()
assert 'addon' in response.context_data['adminform'].form.errors
item.reload()
assert item.addon == addon
# Try changing to an add-on that is already used by another item.
item2 = DiscoveryItem.objects.create(addon=addon2)
response = self.client.post(

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

@ -5,4 +5,4 @@ from .models import PrimaryHero
class PrimaryHeroInline(admin.StackedInline):
model = PrimaryHero
fields = ('image', 'gradient_color', 'enabled')
fields = ('image', 'gradient_color', 'is_external', 'enabled')

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

@ -75,6 +75,7 @@ class PrimaryHero(ModelBase):
enabled = models.BooleanField(db_index=True, null=False, default=False,)
disco_addon = models.OneToOneField(
DiscoveryItem, on_delete=models.CASCADE, null=False)
is_external = models.BooleanField(null=False, default=False)
def __str__(self):
return str(self.disco_addon)
@ -88,8 +89,15 @@ class PrimaryHero(ModelBase):
return {'start': GRADIENT_START_COLOR, 'end': self.gradient_color}
def clean(self):
recommended = (self.disco_addon.recommended_status ==
self.disco_addon.RECOMMENDED)
if self.enabled and not recommended:
raise ValidationError(
'Only recommended add-ons can be enabled for primary shelves.')
if self.is_external:
if self.enabled and not self.disco_addon.addon.homepage:
raise ValidationError(
'External primary shelves need a homepage defined in '
'addon details.')
else:
recommended = (self.disco_addon.recommended_status ==
self.disco_addon.RECOMMENDED)
if self.enabled and not recommended:
raise ValidationError(
'Only recommended add-ons can be enabled for non-external '
'primary shelves.')

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

@ -1,21 +1,35 @@
from rest_framework import serializers
from olympia.addons.models import Addon
from olympia.addons.serializers import AddonSerializer
from olympia.amo.templatetags.jinja_helpers import absolutify
from olympia.discovery.serializers import DiscoveryAddonSerializer
from .models import PrimaryHero
class ExternalAddonSerializer(AddonSerializer):
class Meta:
fields = ('id', 'guid', 'homepage', 'name', 'type',)
model = Addon
class PrimaryHeroShelfSerializer(serializers.ModelSerializer):
description = serializers.CharField(source='disco_addon.description')
featured_image = serializers.SerializerMethodField()
heading = serializers.CharField(source='disco_addon.heading')
addon = DiscoveryAddonSerializer(source='disco_addon.addon')
external = ExternalAddonSerializer(source='disco_addon.addon')
class Meta:
model = PrimaryHero
fields = ('addon', 'description', 'featured_image', 'gradient',
'heading')
fields = ('addon', 'description', 'external', 'featured_image',
'gradient', 'heading')
def to_representation(self, instance):
rep = super().to_representation(instance)
rep.pop('addon' if instance.is_external else 'external')
return rep
def get_featured_image(self, obj):
return absolutify(obj.image_path)

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

@ -34,3 +34,17 @@ class TestPrimaryHero(TestCase):
recommendation_approved=True)
assert ph.disco_addon.recommended_status == ph.disco_addon.RECOMMENDED
ph.clean() # it raises if there's an error
def test_clean_external(self):
ph = PrimaryHero.objects.create(
disco_addon=DiscoveryItem.objects.create(addon=addon_factory()),
is_external=True)
assert not ph.enabled
ph.clean() # it raises if there's an error
ph.enabled = True
with self.assertRaises(ValidationError):
ph.clean()
ph.disco_addon.addon.homepage = 'https://foobar.com/'
ph.disco_addon.addon.save()
ph.clean() # it raises if there's an error

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

@ -1,9 +1,10 @@
from olympia import amo
from olympia.amo.tests import addon_factory, TestCase
from olympia.discovery.models import DiscoveryItem
from olympia.discovery.serializers import DiscoveryAddonSerializer
from ..models import GRADIENT_START_COLOR, PrimaryHero
from ..serializers import PrimaryHeroShelfSerializer
from ..serializers import ExternalAddonSerializer, PrimaryHeroShelfSerializer
class TestPrimaryHeroShelfSerializer(TestCase):
@ -26,3 +27,32 @@ class TestPrimaryHeroShelfSerializer(TestCase):
},
'addon': DiscoveryAddonSerializer(instance=addon).data,
}
def test_external_addon(self):
addon = addon_factory(
summary='Summary', homepage='https://foo.baa', version_kw={
'channel': amo.RELEASE_CHANNEL_UNLISTED})
hero = PrimaryHero.objects.create(
disco_addon=DiscoveryItem.objects.create(
addon=addon,
custom_heading='Its a héading!'),
image='foo.png',
gradient_color='#123456',
is_external=True)
assert PrimaryHeroShelfSerializer(instance=hero).data == {
'featured_image': hero.image_path,
'heading': 'Its a héading!',
'description': '<blockquote>Summary</blockquote>',
'gradient': {
'start': GRADIENT_START_COLOR,
'end': '#123456'
},
'external': ExternalAddonSerializer(instance=addon).data,
}
assert ExternalAddonSerializer(instance=addon).data == {
'id': addon.id,
'guid': addon.guid,
'homepage': {'en-US': str(addon.homepage)},
'name': {'en-US': str(addon.name)},
'type': 'extension',
}

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

@ -1,3 +1,4 @@
from olympia.amo.templatetags.jinja_helpers import absolutify
from olympia.amo.tests import addon_factory, TestCase, reverse_ns
from olympia.discovery.models import DiscoveryItem
@ -25,6 +26,12 @@ class TestPrimaryHeroShelfViewSet(TestCase):
addon=addon_factory()),
image='baa.png',
gradient_color='#987654')
hero_external = PrimaryHero.objects.create(
disco_addon=DiscoveryItem.objects.create(
addon=addon_factory(homepage='https://mozilla.org/')),
image='external.png',
gradient_color='#FABFAB',
is_external=True)
# The shelf isn't enabled so still won't show up
response = self.client.get(self.url)
@ -36,7 +43,14 @@ class TestPrimaryHeroShelfViewSet(TestCase):
assert response.json() == {
'results': [
PrimaryHeroShelfSerializer(instance=hero_a).data,
PrimaryHeroShelfSerializer(instance=hero_b).data]}
PrimaryHeroShelfSerializer(instance=hero_b).data,
PrimaryHeroShelfSerializer(instance=hero_external).data]}
# double check the different serializer representations
assert response.json()['results'][0]['addon']['url'] == (
absolutify(hero_a.disco_addon.addon.get_detail_url()))
assert response.json()['results'][2]['external']['homepage'] == {
'en-US': 'https://mozilla.org/'
}
class TestHeroShelvesView(TestCase):
@ -75,7 +89,7 @@ class TestHeroShelvesView(TestCase):
found_ids = set()
# check its not just returning the same add-on each time
for count in range(0, 11):
for count in range(0, 19):
response = self.client.get(self.url)
found_ids.add(response.json()['primary']['addon']['id'])
if len(found_ids) == 3:

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

@ -0,0 +1 @@
ALTER TABLE `hero_primaryhero` ADD COLUMN `is_external` bool NOT NULL default false;