allow externally hosted addons to be added as PrimaryHero shelves (#11970)
This commit is contained in:
Родитель
5aad9783c3
Коммит
ea8ac4633a
|
@ -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;
|
Загрузка…
Ссылка в новой задаче