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:
Andrew Williamson 2020-10-29 13:47:36 +00:00 коммит произвёл GitHub
Родитель 0d345e17e1
Коммит b222dfa200
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
13 изменённых файлов: 127 добавлений и 435 удалений

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

@ -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'&lt;script&gt;alert(0)&lt;/script&gt;'
u'<a href="http://testserver/en-US/firefox/addon/som%C3%A9-slug/'
u'?{}">'
u'&lt;script&gt;alert(42)&lt;/script&gt; '
u'by &lt;script&gt;alert(666)&lt;/script&gt;</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'&lt;script&gt;alert(43)&lt;/script&gt; <span>by '
u'<a href="http://testserver/en-US/firefox/addon/som%C3%A9-slug/'
u'?{}">'
u'&lt;script&gt;alert(667)&lt;/script&gt;</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'&lt;script&gt;alert(2)&lt;/script&gt;'
u'<a href="http://testserver/en-US/firefox/addon/som%C3%A9-slug/'
u'?{}">'
u'&lt;script&gt;alert(2)&lt;/script&gt; '
u'by &lt;script&gt;alert(668)&lt;/script&gt;</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'&lt;script&gt;alert(3)&lt;/script&gt; <span>by '
u'<a href="http://testserver/en-US/firefox/addon/som%C3%A9-slug/'
u'?{}">'
u'&lt;script&gt;alert(669)&lt;/script&gt;</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>û&lt;script&gt;alert(4)&lt;/script&gt;</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ÿ &lt;script&gt;alert(5)&lt;/script&gt;</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',