drop heading, heading_text, description from discovery api (#15856)
* drop heading, heading_text, description from discovery api * bump the migration (and don't need to drop recommendable field now) * turn off 'disco-heading-and-description-shim' in v4
This commit is contained in:
Родитель
0d345e17e1
Коммит
b222dfa200
|
@ -38,8 +38,6 @@ Firefox (about:addons).
|
|||
:query string telemetry-client-id: Optional sha256 hash of the telemetry client ID to be passed to the TAAR service to enable recommendations. Must be the hex value of a sha256 hash, otherwise it will be ignored.
|
||||
:>json int count: The number of results for this query.
|
||||
:>json array results: The array containing the results for this query.
|
||||
:>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 string|null results[].description_text: The description for this item, if any. Text-only, content might slightly differ from ``description`` because of that.
|
||||
:>json boolean results[].is_recommendation: If this item was from the recommendation service, rather than static curated content.
|
||||
:>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``.
|
||||
|
@ -61,5 +59,4 @@ of appropriate add-ons to recommended.
|
|||
:query boolean recommended: Filter to only add-ons recommended by Mozilla. Only ``recommended=true`` is supported.
|
||||
:>json array results: The array containing the results for this query. There is no pagination, all results are returned.
|
||||
:>json object results[].addon: A :ref:`add-on <addon-detail-object>` object for this item, but only containing one field: ``guid``.
|
||||
:>json string|null results[].custom_heading: The custom heading for this item, if any.
|
||||
:>json string|null results[].custom_description: The custom description for this item, if any.
|
||||
|
|
|
@ -383,6 +383,7 @@ v4 API changelog
|
|||
* 2020-10-15: added /shelves/sponsored/impression and /shelves/sponsored/click endpoints https://github.com/mozilla/addons-server/issues/15618 and https://github.com/mozilla/addons-server/issues/15619
|
||||
* 2020-10-22: added ``promoted`` to primary hero shelf addon object. https://github.com/mozilla/addons-server/issues/15741
|
||||
* 2020-10-22: added /shelves/sponsored/event endpoint for conversions, and to replace click endpoint https://github.com/mozilla/addons-server/issues/15718
|
||||
* 2020-11-05: dropped heading and description from discovery API https://github.com/mozilla/addons-server/issues/11272
|
||||
|
||||
.. _`#11380`: https://github.com/mozilla/addons-server/issues/11380/
|
||||
.. _`#11379`: https://github.com/mozilla/addons-server/issues/11379/
|
||||
|
|
|
@ -80,7 +80,7 @@ class DiscoveryItemAdmin(admin.ModelAdmin):
|
|||
'all': ('css/admin/discovery.css',)
|
||||
}
|
||||
list_display = ('__str__',
|
||||
'custom_addon_name', 'custom_heading', 'position',
|
||||
'position',
|
||||
'position_china',
|
||||
)
|
||||
list_filter = (PositionFilter, PositionChinaFilter)
|
||||
|
@ -115,16 +115,13 @@ class DiscoveryItemAdmin(admin.ModelAdmin):
|
|||
db_field, request, **kwargs)
|
||||
|
||||
def build_preview(self, obj, locale):
|
||||
# FIXME: when disco in Firefox itself lands, change this preview to
|
||||
# match the one Firefox uses.
|
||||
# https://github.com/mozilla/addons-server/issues/11272
|
||||
return format_html(
|
||||
u'<div class="discovery-preview" data-locale="{}">'
|
||||
u'<h2 class="heading">{}</h2>'
|
||||
u'<div class="editorial-description">{}</div></div>',
|
||||
'<div class="discovery-preview" data-locale="{}">'
|
||||
'<h2 class="heading">{}</h2>'
|
||||
'<div class="editorial-description">{}</div></div>',
|
||||
locale,
|
||||
mark_safe(obj.heading),
|
||||
mark_safe(obj.description))
|
||||
obj.addon.name,
|
||||
mark_safe(obj.description_text))
|
||||
|
||||
def previews(self, obj):
|
||||
translations = []
|
||||
|
|
|
@ -69,7 +69,7 @@ class BaseAPIParser():
|
|||
class DiscoItemAPIParser(BaseAPIParser):
|
||||
api = settings.DISCOVERY_EDITORIAL_CONTENT_API
|
||||
l10n_comment = 'editorial content for the discovery pane.'
|
||||
fields = ('custom_heading', 'custom_description')
|
||||
fields = ('custom_description',)
|
||||
|
||||
|
||||
class PrimaryHeroShelfAPIParser(BaseAPIParser):
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 2.2.16 on 2020-10-27 19:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('discovery', '0008_remove_discoveryitem_recommendable'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='discoveryitem',
|
||||
name='custom_addon_name',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='discoveryitem',
|
||||
name='custom_heading',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
]
|
|
@ -1,13 +1,9 @@
|
|||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.http import QueryDict
|
||||
from django.utils.html import conditional_escape, format_html
|
||||
from django.utils.translation import ugettext
|
||||
|
||||
from olympia import amo
|
||||
from olympia.addons.models import Addon
|
||||
from olympia.amo.models import ModelBase, OnChangeMixin
|
||||
from olympia.amo.templatetags.jinja_helpers import absolutify
|
||||
|
||||
|
||||
class DiscoveryItem(OnChangeMixin, ModelBase):
|
||||
|
@ -18,17 +14,6 @@ class DiscoveryItem(OnChangeMixin, ModelBase):
|
|||
'automatically for you. If you have access to the add-on '
|
||||
'admin page, you can use the magnifying glass to see '
|
||||
'all available add-ons.')
|
||||
custom_addon_name = models.CharField(
|
||||
max_length=255, blank=True,
|
||||
help_text='Custom add-on name, if needed for space constraints. '
|
||||
'Will be used in the heading if present, but will '
|
||||
'<strong>not</strong> be translated.')
|
||||
custom_heading = models.CharField(
|
||||
max_length=255, blank=True,
|
||||
help_text='Short text used in the header. Can contain the following '
|
||||
'special tags: {start_sub_heading}, {addon_name}, '
|
||||
'{end_sub_heading}. Will be translated. '
|
||||
'Currently *not* visible to the user - #11817')
|
||||
custom_description = models.TextField(
|
||||
blank=True, help_text='Longer text used to describe an add-on. Should '
|
||||
'not contain any HTML or special tags. Will be '
|
||||
|
@ -54,59 +39,12 @@ class DiscoveryItem(OnChangeMixin, ModelBase):
|
|||
def __str__(self):
|
||||
return str(self.addon)
|
||||
|
||||
def build_querystring(self):
|
||||
qs = QueryDict(mutable=True)
|
||||
qs.update({
|
||||
'utm_source': 'discovery.%s' % settings.DOMAIN,
|
||||
'utm_medium': 'firefox-browser',
|
||||
'utm_content': 'discopane-entry-link',
|
||||
'src': 'api',
|
||||
})
|
||||
return qs.urlencode()
|
||||
|
||||
def _build_heading(self, html=False):
|
||||
addon_name = str(self.custom_addon_name or self.addon.name)
|
||||
custom_heading = ugettext(
|
||||
self.custom_heading) if self.custom_heading else None
|
||||
|
||||
if html:
|
||||
authors = ', '.join(
|
||||
author.name for author in self.addon.listed_authors)
|
||||
url = absolutify(self.addon.get_url_path())
|
||||
# addons-frontend will add target and rel attributes to the <a>
|
||||
# link. Note: The translated "by" in the middle of both strings is
|
||||
# unfortunate, but the full strings are too opaque/dangerous to be
|
||||
# handled by translators, since they are just HTML and parameters.
|
||||
if self.custom_heading:
|
||||
addon_link = format_html(
|
||||
# The query string should not be encoded twice, so we add
|
||||
# it to the template first, via '%'.
|
||||
'<a href="{0}?%(query)s">{1} {2} {3}</a>' % {
|
||||
'query': self.build_querystring()},
|
||||
url, addon_name, ugettext('by'), authors)
|
||||
|
||||
value = conditional_escape(custom_heading).replace(
|
||||
'{start_sub_heading}', '<span>').replace(
|
||||
'{end_sub_heading}', '</span>').replace(
|
||||
'{addon_name}', addon_link)
|
||||
else:
|
||||
value = format_html(
|
||||
# The query string should not be encoded twice, so we add
|
||||
# it to the template first, via '%'.
|
||||
'{0} <span>{1} <a href="{2}?%(query)s">{3}</a></span>' % {
|
||||
'query': self.build_querystring()},
|
||||
addon_name, ugettext('by'), url, authors)
|
||||
else:
|
||||
if self.custom_heading:
|
||||
value = custom_heading.replace(
|
||||
'{start_sub_heading}', '').replace(
|
||||
'{end_sub_heading}', '').replace(
|
||||
'{addon_name}', addon_name)
|
||||
else:
|
||||
value = addon_name
|
||||
return value
|
||||
|
||||
def _build_description(self, html=False):
|
||||
@property
|
||||
def description_text(self):
|
||||
"""
|
||||
Return item description (translated, but not including HTML) ready to
|
||||
be returned by the disco pane API.
|
||||
"""
|
||||
if self.custom_description:
|
||||
value = ugettext(self.custom_description)
|
||||
else:
|
||||
|
@ -114,45 +52,5 @@ class DiscoveryItem(OnChangeMixin, ModelBase):
|
|||
if addon.type == amo.ADDON_EXTENSION and addon.summary:
|
||||
value = addon.summary
|
||||
else:
|
||||
value = u''
|
||||
if html:
|
||||
return format_html(
|
||||
u'<blockquote>{}</blockquote>', value) if value else value
|
||||
else:
|
||||
return value
|
||||
|
||||
@property
|
||||
def heading(self):
|
||||
"""
|
||||
Return item heading (translated, including HTML) ready to be returned
|
||||
by the disco pane API.
|
||||
"""
|
||||
return self._build_heading(html=True)
|
||||
|
||||
@property
|
||||
def heading_text(self):
|
||||
"""
|
||||
Return item heading (translated, but not including HTML) ready to be
|
||||
returned by the disco pane API.
|
||||
|
||||
It may differ from the HTML version slightly and contain less
|
||||
information, leaving clients the choice to use extra data returned by
|
||||
the API or not.
|
||||
"""
|
||||
return self._build_heading(html=False)
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
"""
|
||||
Return item description (translated, including HTML) ready to be
|
||||
returned by the disco pane API.
|
||||
"""
|
||||
return self._build_description(html=True)
|
||||
|
||||
@property
|
||||
def description_text(self):
|
||||
"""
|
||||
Return item description (translated, but not including HTML) ready to
|
||||
be returned by the disco pane API.
|
||||
"""
|
||||
return self._build_description(html=False)
|
||||
value = ''
|
||||
return value
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
from django.utils.html import format_html
|
||||
from django.utils.translation import ugettext
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from olympia.addons.models import Addon
|
||||
from olympia.addons.serializers import AddonSerializer, VersionSerializer
|
||||
from olympia.api.utils import is_gate_active
|
||||
from olympia.discovery.models import DiscoveryItem
|
||||
from olympia.versions.models import Version
|
||||
|
||||
|
@ -16,9 +20,9 @@ class DiscoveryEditorialContentSerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = DiscoveryItem
|
||||
# We only need fields that require a translation, that's custom_heading
|
||||
# and custom_description, plus a guid to identify the add-on.
|
||||
fields = ('addon', 'custom_heading', 'custom_description')
|
||||
# We only need fields that require a translation, that's
|
||||
# custom_description, plus a guid to identify the add-on.
|
||||
fields = ('addon', 'custom_description')
|
||||
|
||||
def get_addon(self, obj):
|
||||
return {
|
||||
|
@ -48,8 +52,8 @@ class DiscoveryAddonSerializer(AddonSerializer):
|
|||
|
||||
|
||||
class DiscoverySerializer(serializers.ModelSerializer):
|
||||
heading = serializers.CharField()
|
||||
description = serializers.CharField()
|
||||
heading = serializers.SerializerMethodField()
|
||||
description = serializers.SerializerMethodField()
|
||||
description_text = serializers.CharField()
|
||||
addon = DiscoveryAddonSerializer()
|
||||
is_recommendation = serializers.SerializerMethodField()
|
||||
|
@ -70,3 +74,24 @@ class DiscoverySerializer(serializers.ModelSerializer):
|
|||
position_field = 'position'
|
||||
position_value = getattr(obj, position_field)
|
||||
return position_value is None or position_value < 1
|
||||
|
||||
def get_heading(self, obj):
|
||||
return format_html(
|
||||
'{0} <span>{1} <a href="{2}">{3}</a></span>',
|
||||
obj.addon.name,
|
||||
ugettext('by'),
|
||||
obj.addon.get_absolute_url(),
|
||||
', '.join(author.name for author in obj.addon.listed_authors)
|
||||
)
|
||||
|
||||
def get_description(self, obj):
|
||||
return format_html('<blockquote>{}</blockquote>', obj.description_text)
|
||||
|
||||
def to_representation(self, instance):
|
||||
data = super().to_representation(instance)
|
||||
request = self.context.get('request', None)
|
||||
if request and not is_gate_active(
|
||||
request, 'disco-heading-and-description-shim'):
|
||||
data.pop('heading', None)
|
||||
data.pop('description', None)
|
||||
return data
|
||||
|
|
|
@ -99,7 +99,7 @@ class TestDiscoveryAdmin(TestCase):
|
|||
assert u'Âbsent' not in response.content.decode('utf-8')
|
||||
|
||||
def test_can_edit_with_discovery_edit_permission(self):
|
||||
addon = addon_factory(name=u'BarFöo')
|
||||
addon = addon_factory(name='BarFöo')
|
||||
item = DiscoveryItem.objects.create(addon=addon)
|
||||
self.detail_url = reverse(
|
||||
'admin:discovery_discoveryitem_change', args=(item.pk,)
|
||||
|
@ -110,30 +110,24 @@ class TestDiscoveryAdmin(TestCase):
|
|||
response = self.client.get(self.detail_url, follow=True)
|
||||
assert response.status_code == 200
|
||||
content = response.content.decode('utf-8')
|
||||
assert u'BarFöo' in content
|
||||
assert 'BarFöo' in content
|
||||
assert DiscoveryItem._meta.get_field('addon').help_text in content
|
||||
|
||||
response = self.client.post(
|
||||
self.detail_url,
|
||||
{
|
||||
'addon': str(addon.pk),
|
||||
'custom_addon_name': u'Xäxâxàxaxaxa !',
|
||||
'custom_heading': u'This heading is totally custom.',
|
||||
'custom_description': u'This description is as well!',
|
||||
'custom_description': 'This description is as well!',
|
||||
},
|
||||
follow=True)
|
||||
assert response.status_code == 200
|
||||
item.reload()
|
||||
assert DiscoveryItem.objects.count() == 1
|
||||
assert item.addon == addon
|
||||
assert item.custom_addon_name == u'Xäxâxàxaxaxa !'
|
||||
assert item.custom_heading == u'This heading is totally custom.'
|
||||
assert item.custom_description == u'This description is as well!'
|
||||
assert item.custom_description == 'This description is as well!'
|
||||
|
||||
def test_translations_interpolation(self):
|
||||
addon = addon_factory(
|
||||
name='{bar}', users=[user_factory(display_name='{foo}')]
|
||||
)
|
||||
addon = addon_factory(name='{bar}', summary='{foo}')
|
||||
item = DiscoveryItem.objects.create(addon=addon)
|
||||
self.detail_url = reverse(
|
||||
'admin:discovery_discoveryitem_change', args=(item.pk,)
|
||||
|
@ -148,26 +142,14 @@ class TestDiscoveryAdmin(TestCase):
|
|||
assert '{bar}' in previews_content
|
||||
assert '{foo}' in previews_content
|
||||
|
||||
item.update(custom_addon_name='{abc}')
|
||||
item.update(custom_description='{ghi}')
|
||||
self.client.login(email=user.email)
|
||||
response = self.client.get(self.detail_url, follow=True)
|
||||
assert response.status_code == 200
|
||||
doc = pq(response.content)
|
||||
previews_content = doc('.field-previews').text()
|
||||
assert '{bar}' in previews_content # in description
|
||||
assert '{foo}' in previews_content # in heading
|
||||
assert '{abc}' in previews_content # in heading
|
||||
|
||||
item.update(custom_heading='{def}', custom_description='{ghi}')
|
||||
self.client.login(email=user.email)
|
||||
response = self.client.get(self.detail_url, follow=True)
|
||||
assert response.status_code == 200
|
||||
doc = pq(response.content)
|
||||
previews_content = doc('.field-previews').text()
|
||||
assert '{bar}' not in previews_content # overridden
|
||||
assert '{bar}' in previews_content
|
||||
assert '{foo}' not in previews_content # overridden
|
||||
assert '{abc}' not in previews_content # overridden
|
||||
assert '{def}' in previews_content
|
||||
assert '{ghi}' in previews_content
|
||||
|
||||
def test_can_change_addon_with_discovery_edit_permission(self):
|
||||
|
@ -285,18 +267,14 @@ class TestDiscoveryAdmin(TestCase):
|
|||
self.add_url,
|
||||
{
|
||||
'addon': str(addon.pk),
|
||||
'custom_addon_name': u'Xäxâxàxaxaxa !',
|
||||
'custom_heading': u'This heading is totally custom.',
|
||||
'custom_description': u'This description is as well!',
|
||||
'custom_description': 'This description is as well!',
|
||||
},
|
||||
follow=True)
|
||||
assert response.status_code == 200
|
||||
assert DiscoveryItem.objects.count() == 1
|
||||
item = DiscoveryItem.objects.get()
|
||||
assert item.addon == addon
|
||||
assert item.custom_addon_name == u'Xäxâxàxaxaxa !'
|
||||
assert item.custom_heading == u'This heading is totally custom.'
|
||||
assert item.custom_description == u'This description is as well!'
|
||||
assert item.custom_description == 'This description is as well!'
|
||||
|
||||
def test_can_not_add_without_discovery_edit_permission(self):
|
||||
addon = addon_factory(name=u'BarFöo')
|
||||
|
@ -313,7 +291,7 @@ class TestDiscoveryAdmin(TestCase):
|
|||
assert DiscoveryItem.objects.count() == 0
|
||||
|
||||
def test_can_not_edit_without_discovery_edit_permission(self):
|
||||
addon = addon_factory(name=u'BarFöo')
|
||||
addon = addon_factory(name='BarFöo')
|
||||
item = DiscoveryItem.objects.create(addon=addon)
|
||||
self.detail_url = reverse(
|
||||
'admin:discovery_discoveryitem_change', args=(item.pk,)
|
||||
|
@ -326,17 +304,13 @@ class TestDiscoveryAdmin(TestCase):
|
|||
response = self.client.post(
|
||||
self.detail_url, {
|
||||
'addon': str(addon.pk),
|
||||
'custom_addon_name': u'Noooooô !',
|
||||
'custom_heading': u'I should not be able to do this.',
|
||||
'custom_description': u'This is wrong.',
|
||||
'custom_description': 'This is wrong.',
|
||||
}, follow=True)
|
||||
assert response.status_code == 403
|
||||
item.reload()
|
||||
assert DiscoveryItem.objects.count() == 1
|
||||
assert item.addon == addon
|
||||
assert item.custom_addon_name == u''
|
||||
assert item.custom_heading == u''
|
||||
assert item.custom_description == u''
|
||||
assert item.custom_description == ''
|
||||
|
||||
def test_can_not_delete_without_discovery_edit_permission(self):
|
||||
item = DiscoveryItem.objects.create(addon=addon_factory())
|
||||
|
|
|
@ -12,13 +12,10 @@ from olympia.amo.tests import TestCase
|
|||
|
||||
disco_fake_data = {
|
||||
'results': [{
|
||||
'custom_heading': 'sïïïck custom heading',
|
||||
'custom_description': 'greât custom description'
|
||||
}, {
|
||||
'custom_heading': None,
|
||||
'custom_description': 'custom description is custom '
|
||||
}, {
|
||||
'custom_heading': '{start_sub_heading}{addon_name}{end_sub_heading}',
|
||||
'custom_description': ''
|
||||
}]}
|
||||
|
||||
|
@ -63,15 +60,11 @@ secondary_hero_fake_data = {
|
|||
}]}
|
||||
|
||||
expected_content = """{# L10n: editorial content for the discovery pane. #}
|
||||
{% trans %}sïïïck custom heading{% endtrans %}
|
||||
{# L10n: editorial content for the discovery pane. #}
|
||||
{% trans %}greât custom description{% endtrans %}
|
||||
|
||||
{# L10n: editorial content for the discovery pane. #}
|
||||
{% trans %}custom description is custom {% endtrans %}
|
||||
|
||||
{# L10n: editorial content for the discovery pane. #}
|
||||
{% trans %}{start_sub_heading}{addon_name}{end_sub_heading}{% endtrans %}
|
||||
|
||||
{# L10n: editorial content for the primary hero shelves. #}
|
||||
{% trans %}greât primary custom description{% endtrans %}
|
||||
|
|
|
@ -1,223 +1,9 @@
|
|||
from unittest import mock
|
||||
|
||||
from django.http import QueryDict
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from olympia import amo
|
||||
from olympia.amo.tests import addon_factory, TestCase, user_factory
|
||||
from olympia.amo.tests import addon_factory, TestCase
|
||||
from olympia.discovery.models import DiscoveryItem
|
||||
|
||||
|
||||
class TestDiscoveryItem(TestCase):
|
||||
def test_heading_multiple_authors(self):
|
||||
addon = addon_factory(slug=u'somé-slug', name=u'Sôme Name')
|
||||
user1 = user_factory(display_name=u'Bàr')
|
||||
addon.addonuser_set.create(user=user1, position=1)
|
||||
user2 = user_factory(username=u'Fôo', id=345)
|
||||
addon.addonuser_set.create(user=user2, position=2)
|
||||
user3 = user_factory(username=u'Nôpe')
|
||||
addon.addonuser_set.create(user=user3, listed=False)
|
||||
|
||||
item = DiscoveryItem.objects.create(
|
||||
addon=addon,
|
||||
custom_heading=(u'Fancy Héading {start_sub_heading}with '
|
||||
u'{addon_name}{end_sub_heading}'))
|
||||
assert item.heading == (
|
||||
u'Fancy Héading <span>with '
|
||||
u'<a href="http://testserver/en-US/firefox/addon/som%C3%A9-slug/'
|
||||
u'?{}">'
|
||||
u'Sôme Name by Bàr, Firefox user 345</a></span>').format(
|
||||
item.build_querystring())
|
||||
|
||||
def test_heading_custom(self):
|
||||
addon = addon_factory(slug=u'somé-slug', name=u'Sôme Name')
|
||||
user = user_factory(display_name=u'Fløp')
|
||||
addon.addonuser_set.create(user=user)
|
||||
item = DiscoveryItem.objects.create(
|
||||
addon=addon,
|
||||
custom_heading=(u'Fancy Héading {start_sub_heading}with '
|
||||
u'{addon_name}{end_sub_heading}'))
|
||||
assert item.heading == (
|
||||
u'Fancy Héading <span>with '
|
||||
u'<a href="http://testserver/en-US/firefox/addon/som%C3%A9-slug/'
|
||||
u'?{}">'
|
||||
u'Sôme Name by Fløp</a></span>').format(item.build_querystring())
|
||||
|
||||
def test_heading_custom_xss(self):
|
||||
# Custom heading itself should not contain HTML; only the special {xxx}
|
||||
# tags we explicitely support.
|
||||
addon = addon_factory(
|
||||
slug=u'somé-slug', name=u'<script>alert(42)</script>')
|
||||
user = user_factory(display_name=u'<script>alert(666)</script>')
|
||||
addon.addonuser_set.create(user=user)
|
||||
item = DiscoveryItem.objects.create(
|
||||
addon=addon,
|
||||
custom_heading=u'<script>alert(0)</script>{addon_name}')
|
||||
assert item.heading == (
|
||||
u'<script>alert(0)</script>'
|
||||
u'<a href="http://testserver/en-US/firefox/addon/som%C3%A9-slug/'
|
||||
u'?{}">'
|
||||
u'<script>alert(42)</script> '
|
||||
u'by <script>alert(666)</script></a>').format(
|
||||
item.build_querystring())
|
||||
|
||||
def test_heading_non_custom(self):
|
||||
addon = addon_factory(slug=u'somé-slug', name=u'Sôme Name')
|
||||
addon.addonuser_set.create(user=user_factory(display_name=u'Fløp'))
|
||||
item = DiscoveryItem.objects.create(addon=addon)
|
||||
assert item.heading == (
|
||||
u'Sôme Name <span>by '
|
||||
u'<a href="http://testserver/en-US/firefox/addon/som%C3%A9-slug/'
|
||||
u'?{}">'
|
||||
u'Fløp</a></span>').format(item.build_querystring())
|
||||
|
||||
def test_heading_non_custom_xss(self):
|
||||
addon = addon_factory(
|
||||
slug=u'somé-slug', name=u'<script>alert(43)</script>')
|
||||
user = user_factory(display_name=u'<script>alert(667)</script>')
|
||||
addon.addonuser_set.create(user=user)
|
||||
item = DiscoveryItem.objects.create(addon=addon)
|
||||
assert item.heading == (
|
||||
u'<script>alert(43)</script> <span>by '
|
||||
u'<a href="http://testserver/en-US/firefox/addon/som%C3%A9-slug/'
|
||||
u'?{}">'
|
||||
u'<script>alert(667)</script></a></span>').format(
|
||||
item.build_querystring())
|
||||
|
||||
def test_heading_custom_with_custom_addon_name(self):
|
||||
addon = addon_factory(slug=u'somé-slug')
|
||||
addon.addonuser_set.create(user=user_factory(display_name=u'Fløp'))
|
||||
item = DiscoveryItem.objects.create(
|
||||
addon=addon, custom_addon_name=u'Custôm Name',
|
||||
custom_heading=(u'Fancy Héading {start_sub_heading}with '
|
||||
u'{addon_name}{end_sub_heading}'))
|
||||
assert item.heading == (
|
||||
u'Fancy Héading <span>with '
|
||||
u'<a href="http://testserver/en-US/firefox/addon/som%C3%A9-slug/'
|
||||
u'?{}">'
|
||||
u'Custôm Name by Fløp</a></span>').format(item.build_querystring())
|
||||
|
||||
def test_heading_custom_with_custom_addon_name_xss(self):
|
||||
addon = addon_factory(slug=u'somé-slug')
|
||||
user = user_factory(display_name=u'<script>alert(668)</script>')
|
||||
addon.addonuser_set.create(user=user)
|
||||
item = DiscoveryItem.objects.create(
|
||||
addon=addon, custom_addon_name=u'Custôm Name',
|
||||
custom_heading=(u'Fancy Héading {start_sub_heading}with '
|
||||
u'{addon_name}{end_sub_heading}'))
|
||||
item.custom_addon_name = '<script>alert(2)</script>'
|
||||
item.custom_heading = '<script>alert(2)</script>{addon_name}'
|
||||
assert item.heading == (
|
||||
u'<script>alert(2)</script>'
|
||||
u'<a href="http://testserver/en-US/firefox/addon/som%C3%A9-slug/'
|
||||
u'?{}">'
|
||||
u'<script>alert(2)</script> '
|
||||
u'by <script>alert(668)</script></a>').format(
|
||||
item.build_querystring())
|
||||
|
||||
def test_heading_non_custom_but_with_custom_addon_name(self):
|
||||
addon = addon_factory(slug=u'somé-slug')
|
||||
addon.addonuser_set.create(user=user_factory(display_name=u'Fløp'))
|
||||
item = DiscoveryItem.objects.create(
|
||||
addon=addon, custom_addon_name=u'Custôm Name')
|
||||
assert item.heading == (
|
||||
u'Custôm Name <span>by '
|
||||
u'<a href="http://testserver/en-US/firefox/addon/som%C3%A9-slug/'
|
||||
u'?{}">'
|
||||
u'Fløp</a></span>').format(item.build_querystring())
|
||||
|
||||
def test_heading_non_custom_but_with_custom_addon_name_xss(self):
|
||||
addon = addon_factory(slug=u'somé-slug')
|
||||
user = user_factory(display_name=u'<script>alert(669)</script>')
|
||||
addon.addonuser_set.create(user=user)
|
||||
item = DiscoveryItem.objects.create(
|
||||
addon=addon, custom_addon_name=u'<script>alert(3)</script>')
|
||||
assert item.heading == (
|
||||
u'<script>alert(3)</script> <span>by '
|
||||
u'<a href="http://testserver/en-US/firefox/addon/som%C3%A9-slug/'
|
||||
u'?{}">'
|
||||
u'<script>alert(669)</script></a></span>').format(
|
||||
item.build_querystring())
|
||||
|
||||
def test_heading_text(self):
|
||||
addon = addon_factory(slug='somé-slug', name='Sôme Name')
|
||||
user = user_factory(display_name='Fløp')
|
||||
addon.addonuser_set.create(user=user)
|
||||
item = DiscoveryItem.objects.create(addon=addon)
|
||||
assert item.heading_text == 'Sôme Name'
|
||||
|
||||
def test_heading_text_custom_addon_name(self):
|
||||
addon = addon_factory(slug='somé-slug', name='Sôme Name')
|
||||
user = user_factory(display_name='Fløp')
|
||||
addon.addonuser_set.create(user=user)
|
||||
item = DiscoveryItem.objects.create(
|
||||
addon=addon, custom_addon_name='Custôm Name')
|
||||
assert item.heading_text == 'Custôm Name'
|
||||
|
||||
def test_heading_text_custom(self):
|
||||
addon = addon_factory(slug='somé-slug', name=u'Sôme Name')
|
||||
user = user_factory(display_name='Fløp')
|
||||
addon.addonuser_set.create(user=user)
|
||||
item = DiscoveryItem.objects.create(
|
||||
addon=addon,
|
||||
custom_heading=('Fancy Héading {start_sub_heading}with '
|
||||
'{addon_name}{end_sub_heading}.'))
|
||||
assert item.heading_text == 'Fancy Héading with Sôme Name.'
|
||||
|
||||
def test_heading_text_custom_with_custom_addon_name(self):
|
||||
addon = addon_factory(slug='somé-slug', name='Sôme Name')
|
||||
user = user_factory(display_name='Fløp')
|
||||
addon.addonuser_set.create(user=user)
|
||||
item = DiscoveryItem.objects.create(
|
||||
addon=addon,
|
||||
custom_addon_name='Custôm Name',
|
||||
custom_heading=('Fancy Héading {start_sub_heading}with '
|
||||
'{addon_name}{end_sub_heading}.'))
|
||||
assert item.heading_text == 'Fancy Héading with Custôm Name.'
|
||||
|
||||
def test_heading_is_translated(self):
|
||||
addon = addon_factory(slug='somé-slug', name='Sôme Name')
|
||||
user = user_factory(display_name='Fløp')
|
||||
addon.addonuser_set.create(user=user)
|
||||
item = DiscoveryItem.objects.create(
|
||||
addon=addon,
|
||||
custom_addon_name='Custôm Name',
|
||||
custom_heading=('Fancy Héading {start_sub_heading}with '
|
||||
'{addon_name}{end_sub_heading}.'))
|
||||
with mock.patch('olympia.discovery.models.ugettext') as ugettext_mock:
|
||||
ugettext_mock.return_value = f'Trans {item.custom_heading}'
|
||||
assert item.heading_text == 'Trans Fancy Héading with Custôm Name.'
|
||||
assert item.heading.startswith('Trans Fancy Héading <span>with ')
|
||||
|
||||
def test_description_custom(self):
|
||||
addon = addon_factory(summary='Foo', description='Bar')
|
||||
item = DiscoveryItem.objects.create(
|
||||
addon=addon, custom_description=u'Custôm Desc')
|
||||
assert item.description == u'<blockquote>Custôm Desc</blockquote>'
|
||||
|
||||
item.custom_description = u'û<script>alert(4)</script>'
|
||||
assert item.description == (
|
||||
u'<blockquote>û<script>alert(4)</script></blockquote>')
|
||||
|
||||
def test_description_non_custom_extension(self):
|
||||
addon = addon_factory(summary='')
|
||||
item = DiscoveryItem.objects.create(addon=addon)
|
||||
assert item.description == u''
|
||||
|
||||
addon.summary = u'Mÿ Summary'
|
||||
assert item.description == u'<blockquote>Mÿ Summary</blockquote>'
|
||||
|
||||
def test_description_non_custom_extension_xss(self):
|
||||
addon = addon_factory(summary=u'Mÿ <script>alert(5)</script>')
|
||||
item = DiscoveryItem.objects.create(addon=addon)
|
||||
assert item.description == (
|
||||
u'<blockquote>'
|
||||
u'Mÿ <script>alert(5)</script></blockquote>')
|
||||
|
||||
def test_description_non_custom_fallback(self):
|
||||
item = DiscoveryItem.objects.create(addon=addon_factory(
|
||||
type=amo.ADDON_DICT))
|
||||
assert item.description == u''
|
||||
|
||||
def test_description_text_custom(self):
|
||||
addon = addon_factory(summary='Foo', description='Bar')
|
||||
|
@ -237,15 +23,3 @@ class TestDiscoveryItem(TestCase):
|
|||
item = DiscoveryItem.objects.create(addon=addon_factory(
|
||||
type=amo.ADDON_DICT))
|
||||
assert item.description_text == ''
|
||||
|
||||
@override_settings(DOMAIN='addons.mozilla.org')
|
||||
def test_build_querystring(self):
|
||||
item = DiscoveryItem.objects.create(addon=addon_factory(
|
||||
type=amo.ADDON_DICT))
|
||||
# We do not use `urlencode()` and a string comparison because QueryDict
|
||||
# does not preserve ordering.
|
||||
q = QueryDict(item.build_querystring())
|
||||
assert q.get('utm_source') == 'discovery.addons.mozilla.org'
|
||||
assert q.get('utm_medium') == 'firefox-browser'
|
||||
assert q.get('utm_content') == 'discopane-entry-link'
|
||||
assert q.get('src') == 'api'
|
||||
|
|
|
@ -237,26 +237,17 @@ def test_get_disco_recommendations_overrides(call_recommendation_server):
|
|||
@pytest.mark.django_db
|
||||
def test_replace_extensions():
|
||||
source = [
|
||||
DiscoveryItem(addon=addon_factory(), custom_addon_name=u'replacê me'),
|
||||
DiscoveryItem(
|
||||
addon=addon_factory(), custom_addon_name=u'replace me tøø'),
|
||||
DiscoveryItem(
|
||||
addon=addon_factory(type=amo.ADDON_STATICTHEME),
|
||||
custom_addon_name=u'ŋot me'),
|
||||
DiscoveryItem(
|
||||
addon=addon_factory(type=amo.ADDON_STATICTHEME),
|
||||
custom_addon_name=u'ŋor me'),
|
||||
DiscoveryItem(addon=addon_factory(), custom_addon_name=u'probably me'),
|
||||
DiscoveryItem(
|
||||
addon=addon_factory(type=amo.ADDON_STATICTHEME),
|
||||
custom_addon_name=u'safê')
|
||||
DiscoveryItem(addon=addon_factory()), # replaced
|
||||
DiscoveryItem(addon=addon_factory()), # also replaced
|
||||
DiscoveryItem(addon=addon_factory(type=amo.ADDON_STATICTHEME)), # not
|
||||
DiscoveryItem(addon=addon_factory(type=amo.ADDON_STATICTHEME)), # nope
|
||||
DiscoveryItem(addon=addon_factory()), # possibly replaced
|
||||
DiscoveryItem(addon=addon_factory(type=amo.ADDON_STATICTHEME)) # nope
|
||||
]
|
||||
# Just 2 replacements
|
||||
replacements = [
|
||||
DiscoveryItem(
|
||||
addon=addon_factory(), custom_addon_name=u'just for you'),
|
||||
DiscoveryItem(
|
||||
addon=addon_factory(), custom_addon_name=u'and this øne'),
|
||||
DiscoveryItem(addon=addon_factory()),
|
||||
DiscoveryItem(addon=addon_factory()),
|
||||
]
|
||||
result = replace_extensions(source, replacements)
|
||||
assert result == [
|
||||
|
@ -269,10 +260,8 @@ def test_replace_extensions():
|
|||
], result
|
||||
|
||||
# Add a few more so all extensions are replaced, with one spare.
|
||||
replacements.append(DiscoveryItem(
|
||||
addon=addon_factory(), custom_addon_name=u'extra ône'))
|
||||
replacements.append(DiscoveryItem(
|
||||
addon=addon_factory(), custom_addon_name=u'extra tôo'))
|
||||
replacements.append(DiscoveryItem(addon=addon_factory()))
|
||||
replacements.append(DiscoveryItem(addon=addon_factory()))
|
||||
result = replace_extensions(source, replacements)
|
||||
assert result == [
|
||||
replacements[0],
|
||||
|
|
|
@ -45,7 +45,7 @@ class DiscoveryTestMixin(object):
|
|||
assert result_file['url'] == file_.get_absolute_url()
|
||||
assert result_file['permissions'] == file_.permissions
|
||||
|
||||
def _check_disco_addon(self, result, item, flat_name=False):
|
||||
def _check_disco_addon(self, result, item, flat_name=False, heading=False):
|
||||
addon = item.addon
|
||||
assert result['addon']['id'] == item.addon_id == addon.pk
|
||||
if flat_name:
|
||||
|
@ -58,9 +58,16 @@ class DiscoveryTestMixin(object):
|
|||
assert (result['addon']['current_version']['files'][0]['id'] ==
|
||||
addon.current_version.all_files[0].pk)
|
||||
|
||||
assert result['heading'] == item.heading
|
||||
assert result['description'] == item.description
|
||||
assert result['description_text'] == item.description_text
|
||||
if heading:
|
||||
assert result['heading'] == (
|
||||
f'{addon.name} <span>by <a href="{addon.get_absolute_url()}">'
|
||||
f'{self.addon_user.name}</a></span>')
|
||||
assert result['description'] == (
|
||||
f'<blockquote>{result["description_text"]}</blockquote>')
|
||||
else:
|
||||
assert 'heading' not in result
|
||||
assert 'description' not in result
|
||||
|
||||
# https://github.com/mozilla/addons-server/issues/11817
|
||||
assert 'heading_text' not in result
|
||||
|
@ -141,6 +148,25 @@ class TestDiscoveryViewList(DiscoveryTestMixin, TestCase):
|
|||
self._check_disco_addon(
|
||||
result, discopane_items[i], flat_name=True)
|
||||
|
||||
@override_settings(DRF_API_GATES={
|
||||
'v5': ('disco-heading-and-description-shim',)})
|
||||
def test_list_html_heading_and_description(self):
|
||||
self.addon_user = user_factory()
|
||||
for addon in self.addons:
|
||||
addon.addonuser_set.create(user=self.addon_user)
|
||||
response = self.client.get(self.url, {'lang': 'en-US'})
|
||||
assert response.data
|
||||
|
||||
discopane_items = DiscoveryItem.objects.all().filter(
|
||||
position__gt=0).order_by('position')
|
||||
assert response.data['count'] == len(discopane_items)
|
||||
assert response.data['results']
|
||||
|
||||
for i, result in enumerate(response.data['results']):
|
||||
assert result['is_recommendation'] is False
|
||||
self._check_disco_addon(
|
||||
result, discopane_items[i], heading=True)
|
||||
|
||||
def test_list_unicode_locale(self):
|
||||
"""Test that disco pane API still works in a locale with non-ascii
|
||||
chars, like russian."""
|
||||
|
@ -347,16 +373,13 @@ class TestDiscoveryItemViewSet(TestCase):
|
|||
def setUp(self):
|
||||
self.items = [
|
||||
DiscoveryItem.objects.create(
|
||||
addon=addon_factory(),
|
||||
custom_addon_name=u'Fôoooo'),
|
||||
addon=addon_factory(summary='This is the addon summary')),
|
||||
DiscoveryItem.objects.create(
|
||||
addon=addon_factory(),
|
||||
custom_heading=u'My Custöm Headîng',
|
||||
custom_description=u''),
|
||||
custom_description=''),
|
||||
DiscoveryItem.objects.create(
|
||||
addon=addon_factory(),
|
||||
custom_heading=u'Änother custom heading',
|
||||
custom_description=u'This time with a custom description')
|
||||
custom_description='This time with a custom description')
|
||||
]
|
||||
self.url = reverse_ns('discovery-editorial-list')
|
||||
|
||||
|
@ -371,17 +394,14 @@ class TestDiscoveryItemViewSet(TestCase):
|
|||
assert 'results' in response.data
|
||||
|
||||
result = response.data['results'][0]
|
||||
assert result['custom_heading'] == u''
|
||||
assert result['custom_description'] == u''
|
||||
assert result['custom_description'] == ''
|
||||
assert result['addon'] == {'guid': self.items[0].addon.guid}
|
||||
|
||||
result = response.data['results'][1]
|
||||
assert result['custom_heading'] == u'My Custöm Headîng'
|
||||
assert result['custom_description'] == u''
|
||||
assert result['custom_description'] == ''
|
||||
assert result['addon'] == {'guid': self.items[1].addon.guid}
|
||||
|
||||
result = response.data['results'][2]
|
||||
assert result['custom_heading'] == u'Änother custom heading'
|
||||
assert result['custom_description'] == (
|
||||
u'This time with a custom description')
|
||||
assert result['addon'] == {'guid': self.items[2].addon.guid}
|
||||
|
|
|
@ -1710,6 +1710,7 @@ DRF_API_GATES = {
|
|||
'autocomplete-sort-param',
|
||||
'is-source-public-shim',
|
||||
'is-featured-addon-shim',
|
||||
'disco-heading-and-description-shim',
|
||||
),
|
||||
'v4': (
|
||||
'l10n_flat_input_output',
|
||||
|
|
Загрузка…
Ссылка в новой задаче