* add secondary hero modules

* Rename 1114-add-secondary-hero-module.sql to 1115-add-secondary-hero-module.sql
This commit is contained in:
Andrew Williamson 2019-08-12 12:30:02 +01:00 коммит произвёл GitHub
Родитель 1e549f1497
Коммит 2963e41224
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
46 изменённых файлов: 480 добавлений и 48 удалений

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

@ -67,3 +67,9 @@ small number of shelves - and likely only one - this endpoint is not paginated.
:>json object|null cta: The optional call to action link and text to be displayed with the item. :>json object|null cta: The optional call to action link and text to be displayed with the item.
:>json string cta.url: The url the call to action would link to. :>json string cta.url: The url the call to action would link to.
:>json string cta.text: The call to action text. :>json string cta.text: The call to action text.
:>json array modules: The modules for this shelf. Should always be 3.
:>json string modules[].icon: The icon used to illustrate the item.
:>json string modules[].description: The description for this item.
:>json object|null modules[].cta: The optional call to action link and text to be displayed with the item.
:>json string modules[].cta.url: The url the call to action would link to.
:>json string modules[].cta.text: The call to action text.

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

@ -335,6 +335,7 @@ v4 API changelog
* 2019-08-08: removed ``heading_text`` property from discovery api. https://github.com/mozilla/addons-server/issues/11817 * 2019-08-08: removed ``heading_text`` property from discovery api. https://github.com/mozilla/addons-server/issues/11817
* 2019-08-08: add secondary shelf to /hero/ endpoint. https://github.com/mozilla/addons-server/issues/11779 * 2019-08-08: add secondary shelf to /hero/ endpoint. https://github.com/mozilla/addons-server/issues/11779
* 2019-08-15: dropped support for LWT specific statuses. * 2019-08-15: dropped support for LWT specific statuses.
* 2019-08-15: added promo modules to secondary hero shelves. https://github.com/mozilla/addons-server/issues/11780
---------------- ----------------
v5 API changelog v5 API changelog

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

@ -123,6 +123,9 @@ DJANGO_PERMISSIONS_MAPPING.update({
'hero.add_primaryhero': DISCOVERY_EDIT, 'hero.add_primaryhero': DISCOVERY_EDIT,
'hero.change_primaryhero': DISCOVERY_EDIT, 'hero.change_primaryhero': DISCOVERY_EDIT,
'hero.delete_primaryhero': DISCOVERY_EDIT, 'hero.delete_primaryhero': DISCOVERY_EDIT,
'hero.add_secondaryheromodule': DISCOVERY_EDIT,
'hero.change_secondaryheromodule': DISCOVERY_EDIT,
'hero.delete_secondaryheromodule': DISCOVERY_EDIT,
'reviewers.delete_reviewerscore': ADMIN_ADVANCED, 'reviewers.delete_reviewerscore': ADMIN_ADVANCED,

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

@ -2,7 +2,7 @@
from olympia.amo.tests import TestCase, addon_factory, user_factory from olympia.amo.tests import TestCase, addon_factory, user_factory
from olympia.amo.urlresolvers import django_reverse, reverse from olympia.amo.urlresolvers import django_reverse, reverse
from olympia.discovery.models import DiscoveryItem from olympia.discovery.models import DiscoveryItem
from olympia.hero.models import PrimaryHero, SecondaryHero from olympia.hero.models import PrimaryHero, SecondaryHero, SecondaryHeroModule
class TestDiscoveryAdmin(TestCase): class TestDiscoveryAdmin(TestCase):
@ -463,6 +463,29 @@ class TestSecondaryHeroShelfAdmin(TestCase):
'admin:discovery_secondaryheroshelf_changelist') 'admin:discovery_secondaryheroshelf_changelist')
self.detail_url_name = 'admin:discovery_secondaryheroshelf_change' self.detail_url_name = 'admin:discovery_secondaryheroshelf_change'
def _get_moduleform(self, item, module_data, initial=0):
count = str(len(module_data))
out = {
"modules-TOTAL_FORMS": count,
"modules-INITIAL_FORMS": initial,
"modules-MIN_NUM_FORMS": count,
"modules-MAX_NUM_FORMS": count,
"modules-__prefix__-icon": "",
"modules-__prefix__-description": "",
"modules-__prefix__-id": "",
"modules-__prefix__-shelf": str(item),
}
for index in range(0, len(module_data)):
out.update(**{
f"modules-{index}-icon": str(module_data[index]['icon']),
f"modules-{index}-description": str(
module_data[index]['description']),
f"modules-{index}-id": str(module_data[index].get('id', '')),
f"modules-{index}-shelf": str(item),
})
return out
def test_can_see_secondary_hero_module_in_admin_with_discovery_edit(self): def test_can_see_secondary_hero_module_in_admin_with_discovery_edit(self):
user = user_factory() user = user_factory()
self.grant_permission(user, 'Admin:Tools') self.grant_permission(user, 'Admin:Tools')
@ -490,6 +513,11 @@ class TestSecondaryHeroShelfAdmin(TestCase):
def test_can_edit_with_discovery_edit_permission(self): def test_can_edit_with_discovery_edit_permission(self):
item = SecondaryHero.objects.create(headline='BarFöo') item = SecondaryHero.objects.create(headline='BarFöo')
modules = [
SecondaryHeroModule.objects.create(shelf=item),
SecondaryHeroModule.objects.create(shelf=item),
SecondaryHeroModule.objects.create(shelf=item),
]
detail_url = reverse(self.detail_url_name, args=(item.pk,)) detail_url = reverse(self.detail_url_name, args=(item.pk,))
user = user_factory() user = user_factory()
self.grant_permission(user, 'Admin:Tools') self.grant_permission(user, 'Admin:Tools')
@ -500,16 +528,42 @@ class TestSecondaryHeroShelfAdmin(TestCase):
content = response.content.decode('utf-8') content = response.content.decode('utf-8')
assert 'BarFöo' in content assert 'BarFöo' in content
shelves = [
{
'id': modules[0].id,
'description': 'foo',
'icon': 'Audio.svg'
},
{
'id': modules[1].id,
'description': 'baa',
'icon': 'Developer.svg',
},
{
'id': modules[2].id,
'description': 'ugh',
'icon': 'Extensions.svg',
},
]
response = self.client.post( response = self.client.post(
detail_url, { detail_url,
dict(self._get_moduleform(item.id, shelves, initial=3), **{
'headline': 'This headline is ... something.', 'headline': 'This headline is ... something.',
'description': 'This description is as well!', 'description': 'This description is as well!',
}, follow=True) }), follow=True)
assert response.status_code == 200 assert response.status_code == 200
assert 'errors' not in response.context_data, (
response.context_data['errors'])
item.reload() item.reload()
assert SecondaryHero.objects.count() == 1 assert SecondaryHero.objects.count() == 1
assert item.headline == 'This headline is ... something.' assert item.headline == 'This headline is ... something.'
assert item.description == 'This description is as well!' assert item.description == 'This description is as well!'
assert SecondaryHeroModule.objects.count() == 3
(module.reload() for module in modules)
module_values = list(
SecondaryHeroModule.objects.all().values(
'id', 'description', 'icon'))
assert module_values == shelves
def test_can_delete_with_discovery_edit_permission(self): def test_can_delete_with_discovery_edit_permission(self):
item = SecondaryHero.objects.create() item = SecondaryHero.objects.create()
@ -528,10 +582,11 @@ class TestSecondaryHeroShelfAdmin(TestCase):
# Can actually delete. # Can actually delete.
response = self.client.post( response = self.client.post(
delete_url, delete_url,
{'post': 'yes'}, dict(self._get_moduleform(item.pk, {}), post='yes'),
follow=True) follow=True)
assert response.status_code == 200 assert response.status_code == 200
assert not SecondaryHero.objects.filter(pk=item.pk).exists() assert not SecondaryHero.objects.filter(pk=item.pk).exists()
assert not SecondaryHeroModule.objects.filter(shelf=item.pk).exists()
def test_can_add_with_discovery_edit_permission(self): def test_can_add_with_discovery_edit_permission(self):
add_url = reverse('admin:discovery_secondaryheroshelf_add') add_url = reverse('admin:discovery_secondaryheroshelf_add')
@ -542,17 +597,38 @@ class TestSecondaryHeroShelfAdmin(TestCase):
response = self.client.get(add_url, follow=True) response = self.client.get(add_url, follow=True)
assert response.status_code == 200 assert response.status_code == 200
assert SecondaryHero.objects.count() == 0 assert SecondaryHero.objects.count() == 0
assert SecondaryHeroModule.objects.count() == 0
shelves = [
{
'description': 'foo',
'icon': 'Audio.svg'
},
{
'description': 'baa',
'icon': 'Developer.svg',
},
{
'description': 'ugh',
'icon': 'Extensions.svg',
},
]
response = self.client.post( response = self.client.post(
add_url, { add_url,
dict(self._get_moduleform('', shelves), **{
'headline': 'This headline is ... something.', 'headline': 'This headline is ... something.',
'description': 'This description is as well!', 'description': 'This description is as well!',
}, }),
follow=True) follow=True)
assert response.status_code == 200 assert response.status_code == 200
assert 'errors' not in response.context_data
assert SecondaryHero.objects.count() == 1 assert SecondaryHero.objects.count() == 1
item = SecondaryHero.objects.get() item = SecondaryHero.objects.get()
assert item.headline == 'This headline is ... something.' assert item.headline == 'This headline is ... something.'
assert item.description == 'This description is as well!' assert item.description == 'This description is as well!'
assert SecondaryHeroModule.objects.count() == 3
module_values = list(
SecondaryHeroModule.objects.all().values('description', 'icon'))
assert module_values == shelves
def test_can_not_add_without_discovery_edit_permission(self): def test_can_not_add_without_discovery_edit_permission(self):
add_url = reverse('admin:discovery_secondaryheroshelf_add') add_url = reverse('admin:discovery_secondaryheroshelf_add')
@ -610,3 +686,24 @@ class TestSecondaryHeroShelfAdmin(TestCase):
delete_url, data={'post': 'yes'}, follow=True) delete_url, data={'post': 'yes'}, follow=True)
assert response.status_code == 403 assert response.status_code == 403
assert SecondaryHero.objects.filter(pk=item.pk).exists() assert SecondaryHero.objects.filter(pk=item.pk).exists()
def test_need_3_modules(self):
add_url = reverse('admin:discovery_secondaryheroshelf_add')
user = user_factory()
self.grant_permission(user, 'Admin:Tools')
self.grant_permission(user, 'Discovery:Edit')
self.client.login(email=user.email)
response = self.client.get(add_url, follow=True)
assert response.status_code == 200
assert SecondaryHero.objects.count() == 0
response = self.client.post(
add_url,
dict(self._get_moduleform('', {}), **{
'headline': 'This headline is ... something.',
'description': 'This description is as well!',
}),
follow=True)
assert response.status_code == 200
assert 'There must be exactly 3 modules in this shelf.' in (
response.context_data['errors'])
assert SecondaryHero.objects.count() == 0

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

@ -1,13 +1,38 @@
from django.contrib import admin from django.contrib import admin
from django.core.exceptions import ValidationError
from django.forms import BaseInlineFormSet
from .models import PrimaryHero from .models import PrimaryHero, SecondaryHeroModule
class PrimaryHeroInline(admin.StackedInline): class PrimaryHeroInline(admin.StackedInline):
model = PrimaryHero model = PrimaryHero
fields = ('image', 'gradient_color', 'is_external', 'enabled') fields = ('image', 'gradient_color', 'is_external', 'enabled')
view_on_site = False
class HeroModuleInlineFormSet(BaseInlineFormSet):
def clean(self):
super().clean()
if len(self.forms) != 3:
raise ValidationError(
'There must be exactly 3 modules in this shelf.')
class SecondaryHeroModuleInline(admin.StackedInline):
model = SecondaryHeroModule
view_on_site = False
max_num = 3
min_num = 3
can_delete = False
formset = HeroModuleInlineFormSet
class SecondaryHeroAdmin(admin.ModelAdmin): class SecondaryHeroAdmin(admin.ModelAdmin):
list_display = ('__str__', 'headline') class Media:
css = {
'all': ('css/admin/discovery.css',)
}
list_display = ('headline', 'description', 'enabled')
inlines = [SecondaryHeroModuleInline]
view_on_site = False view_on_site = False

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

@ -17,6 +17,12 @@ GRADIENT_CHOICES = (
('#712290', 'PURPLE70'), ('#712290', 'PURPLE70'),
('#582ACB', 'VIOLET70'), ('#582ACB', 'VIOLET70'),
) )
FEATURED_IMAGE_PATH = os.path.join(
settings.ROOT, 'static', 'img', 'hero', 'featured')
MODULE_ICON_PATH = os.path.join(
settings.ROOT, 'static', 'img', 'hero', 'icons')
FEATURED_IMAGE_URL = f'{settings.STATIC_URL}img/hero/featured/'
MODULE_ICON_URL = f'{settings.STATIC_URL}img/hero/icons/'
class GradientChoiceWidget(RadioSelect): class GradientChoiceWidget(RadioSelect):
@ -35,19 +41,26 @@ class GradientChoiceWidget(RadioSelect):
class ImageChoiceWidget(RadioSelect): class ImageChoiceWidget(RadioSelect):
option_template_name = 'hero/image_option.html' option_template_name = 'hero/image_option.html'
option_inherits_attrs = True option_inherits_attrs = True
image_url_base = FEATURED_IMAGE_URL
def create_option(self, name, value, label, selected, index, def create_option(self, name, value, label, selected, index,
subindex=None, attrs=None): subindex=None, attrs=None):
attrs['image_url'] = f'{settings.STATIC_URL}img/hero/featured/{value}' attrs['image_url'] = f'{self.image_url_base}{value}'
return super().create_option( return super().create_option(
name=name, value=value, label=label, selected=selected, name=name, value=value, label=label, selected=selected,
index=index, subindex=subindex, attrs=attrs) index=index, subindex=subindex, attrs=attrs)
class FeaturedImageChoices: class IconChoiceWidget(ImageChoiceWidget):
image_url_base = MODULE_ICON_URL
class DirImageChoices:
def __init__(self, path):
self.path = path
def __iter__(self): def __iter__(self):
path = os.path.join(settings.ROOT, 'static', 'img', 'hero', 'featured') self.os_iter = os.scandir(self.path)
self.os_iter = os.scandir(path)
return self return self
def __next__(self): def __next__(self):
@ -68,7 +81,7 @@ class WidgetCharField(models.CharField):
class PrimaryHero(ModelBase): class PrimaryHero(ModelBase):
image = WidgetCharField( image = WidgetCharField(
choices=FeaturedImageChoices(), choices=DirImageChoices(path=FEATURED_IMAGE_PATH),
max_length=255, widget=ImageChoiceWidget) max_length=255, widget=ImageChoiceWidget)
gradient_color = WidgetCharField( gradient_color = WidgetCharField(
choices=GRADIENT_CHOICES, max_length=7, widget=GradientChoiceWidget) choices=GRADIENT_CHOICES, max_length=7, widget=GradientChoiceWidget)
@ -81,8 +94,8 @@ class PrimaryHero(ModelBase):
return str(self.disco_addon) return str(self.disco_addon)
@property @property
def image_path(self): def image_url(self):
return f'{settings.STATIC_URL}img/hero/featured/{self.image}' return f'{FEATURED_IMAGE_URL}{self.image}'
@property @property
def gradient(self): def gradient(self):
@ -103,7 +116,18 @@ class PrimaryHero(ModelBase):
'primary shelves.') 'primary shelves.')
class SecondaryHero(ModelBase): class CTACheckMixin():
def clean(self):
super().clean()
both_or_neither = not (bool(self.cta_text) ^ bool(self.cta_url))
if getattr(self, 'enabled', True) and not both_or_neither:
raise ValidationError(
'Both the call to action URL and text must be defined, or '
'neither, for enabled shelves.')
class SecondaryHero(CTACheckMixin, ModelBase):
headline = models.CharField(max_length=50, blank=False) headline = models.CharField(max_length=50, blank=False)
description = models.CharField(max_length=100, blank=False) description = models.CharField(max_length=100, blank=False)
cta_url = models.CharField(max_length=255, blank=True) cta_url = models.CharField(max_length=255, blank=True)
@ -113,9 +137,22 @@ class SecondaryHero(ModelBase):
def __str__(self): def __str__(self):
return str(self.headline) return str(self.headline)
def clean(self):
both_or_neither = not (bool(self.cta_text) ^ bool(self.cta_url)) class SecondaryHeroModule(CTACheckMixin, ModelBase):
if self.enabled and not both_or_neither: icon = WidgetCharField(
raise ValidationError( choices=DirImageChoices(path=MODULE_ICON_PATH),
'Both the call to action URL and text must be defined, or ' max_length=255, widget=IconChoiceWidget)
'neither, for enabled shelves.') description = models.CharField(max_length=50, blank=False)
cta_url = models.CharField(max_length=255, blank=True)
cta_text = models.CharField(max_length=20, blank=True)
shelf = models.ForeignKey(
SecondaryHero, on_delete=models.CASCADE,
related_name='modules'
)
def __str__(self):
return str(self.description)
@property
def icon_url(self):
return f'{MODULE_ICON_URL}{self.icon}'

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

@ -5,7 +5,7 @@ from olympia.addons.serializers import AddonSerializer
from olympia.amo.templatetags.jinja_helpers import absolutify from olympia.amo.templatetags.jinja_helpers import absolutify
from olympia.discovery.serializers import DiscoveryAddonSerializer from olympia.discovery.serializers import DiscoveryAddonSerializer
from .models import PrimaryHero, SecondaryHero from .models import PrimaryHero, SecondaryHero, SecondaryHeroModule
class ExternalAddonSerializer(AddonSerializer): class ExternalAddonSerializer(AddonSerializer):
@ -16,7 +16,7 @@ class ExternalAddonSerializer(AddonSerializer):
class PrimaryHeroShelfSerializer(serializers.ModelSerializer): class PrimaryHeroShelfSerializer(serializers.ModelSerializer):
description = serializers.CharField(source='disco_addon.description') description = serializers.CharField(source='disco_addon.description')
featured_image = serializers.SerializerMethodField() featured_image = serializers.CharField(source='image_url')
addon = DiscoveryAddonSerializer(source='disco_addon.addon') addon = DiscoveryAddonSerializer(source='disco_addon.addon')
external = ExternalAddonSerializer(source='disco_addon.addon') external = ExternalAddonSerializer(source='disco_addon.addon')
@ -30,16 +30,8 @@ class PrimaryHeroShelfSerializer(serializers.ModelSerializer):
rep.pop('addon' if instance.is_external else 'external') rep.pop('addon' if instance.is_external else 'external')
return rep return rep
def get_featured_image(self, obj):
return absolutify(obj.image_path)
class CTAMixin():
class SecondaryHeroShelfSerializer(serializers.ModelSerializer):
cta = serializers.SerializerMethodField()
class Meta:
model = SecondaryHero
fields = ('headline', 'description', 'cta')
def get_cta(self, obj): def get_cta(self, obj):
if obj.cta_url and obj.cta_text: if obj.cta_url and obj.cta_text:
@ -49,3 +41,22 @@ class SecondaryHeroShelfSerializer(serializers.ModelSerializer):
} }
else: else:
return None return None
class SecondaryHeroShelfModuleSerializer(CTAMixin,
serializers.ModelSerializer):
cta = serializers.SerializerMethodField()
icon = serializers.CharField(source='icon_url')
class Meta:
model = SecondaryHeroModule
fields = ('icon', 'description', 'cta')
class SecondaryHeroShelfSerializer(CTAMixin, serializers.ModelSerializer):
cta = serializers.SerializerMethodField()
modules = SecondaryHeroShelfModuleSerializer(many=True)
class Meta:
model = SecondaryHero
fields = ('headline', 'description', 'cta', 'modules')

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

@ -1,2 +1,2 @@
<div style="background-image:url({{ widget.attrs.image_url }})"> <div style="background-image:url('{{ widget.attrs.image_url }}')">
{% include "django/forms/widgets/radio_option.html" %}</div> {% include "django/forms/widgets/radio_option.html" %}</div>

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

@ -1,16 +1,16 @@
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from olympia.amo.tests import addon_factory, TestCase from olympia.amo.tests import addon_factory, TestCase
from olympia.hero.models import PrimaryHero, SecondaryHero from olympia.hero.models import PrimaryHero, SecondaryHero, SecondaryHeroModule
from olympia.discovery.models import DiscoveryItem from olympia.discovery.models import DiscoveryItem
class TestPrimaryHero(TestCase): class TestPrimaryHero(TestCase):
def test_image_path(self): def test_image_url(self):
ph = PrimaryHero.objects.create( ph = PrimaryHero.objects.create(
disco_addon=DiscoveryItem.objects.create(addon=addon_factory()), disco_addon=DiscoveryItem.objects.create(addon=addon_factory()),
image='foo.png') image='foo.png')
assert ph.image_path == ( assert ph.image_url == (
'http://testserver/static/img/hero/featured/foo.png') 'http://testserver/static/img/hero/featured/foo.png')
def test_gradiant(self): def test_gradiant(self):
@ -51,6 +51,7 @@ class TestPrimaryHero(TestCase):
class TestSecondaryHero(TestCase): class TestSecondaryHero(TestCase):
def test_str(self): def test_str(self):
sh = SecondaryHero.objects.create( sh = SecondaryHero.objects.create(
headline='Its a héadline!', description='description') headline='Its a héadline!', description='description')
@ -85,3 +86,42 @@ class TestSecondaryHero(TestCase):
ph.enabled = True ph.enabled = True
ph.cta_url = 'http://goo.gl' ph.cta_url = 'http://goo.gl'
ph.clean() # it raises if there's an error ph.clean() # it raises if there's an error
class TestSecondaryHeroModule(TestCase):
def test_str(self):
shm = SecondaryHeroModule.objects.create(
description='descríption',
shelf=SecondaryHero.objects.create())
assert str(shm) == 'descríption'
def test_clean_cta(self):
ph = SecondaryHeroModule.objects.create(
shelf=SecondaryHero.objects.create())
# neither cta_url or cta_text are set, and that's okay.
ph.clean() # it raises if there's an error.
# just set the url without the text is invalid when enabled though.
ph.cta_url = 'http://goo.gl/'
with self.assertRaises(ValidationError):
ph.clean()
ph.cta_url = None
ph.cta_text = 'click it!'
with self.assertRaises(ValidationError):
ph.clean()
ph.cta_url = ''
with self.assertRaises(ValidationError):
ph.clean()
# And setting both is okay too.
ph.cta_url = 'http://goo.gl'
ph.clean() # it raises if there's an error
def test_icon_url(self):
ph = SecondaryHeroModule.objects.create(
shelf=SecondaryHero.objects.create(),
icon='foo.svg')
assert ph.icon_url == (
'http://testserver/static/img/hero/icons/foo.svg')

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

@ -3,7 +3,8 @@ from olympia.amo.tests import addon_factory, TestCase
from olympia.discovery.models import DiscoveryItem from olympia.discovery.models import DiscoveryItem
from olympia.discovery.serializers import DiscoveryAddonSerializer from olympia.discovery.serializers import DiscoveryAddonSerializer
from ..models import GRADIENT_START_COLOR, PrimaryHero, SecondaryHero from ..models import (
GRADIENT_START_COLOR, PrimaryHero, SecondaryHero, SecondaryHeroModule)
from ..serializers import ( from ..serializers import (
ExternalAddonSerializer, PrimaryHeroShelfSerializer, ExternalAddonSerializer, PrimaryHeroShelfSerializer,
SecondaryHeroShelfSerializer) SecondaryHeroShelfSerializer)
@ -19,7 +20,8 @@ class TestPrimaryHeroShelfSerializer(TestCase):
gradient_color='#123456') gradient_color='#123456')
data = PrimaryHeroShelfSerializer(instance=hero).data data = PrimaryHeroShelfSerializer(instance=hero).data
assert data == { assert data == {
'featured_image': hero.image_path, 'featured_image': (
'http://testserver/static/img/hero/featured/foo.png'),
'description': '<blockquote>Déscription</blockquote>', 'description': '<blockquote>Déscription</blockquote>',
'gradient': { 'gradient': {
'start': GRADIENT_START_COLOR, 'start': GRADIENT_START_COLOR,
@ -38,7 +40,8 @@ class TestPrimaryHeroShelfSerializer(TestCase):
gradient_color='#123456', gradient_color='#123456',
is_external=True) is_external=True)
assert PrimaryHeroShelfSerializer(instance=hero).data == { assert PrimaryHeroShelfSerializer(instance=hero).data == {
'featured_image': hero.image_path, 'featured_image': (
'http://testserver/static/img/hero/featured/foo.png'),
'description': '<blockquote>Summary</blockquote>', 'description': '<blockquote>Summary</blockquote>',
'gradient': { 'gradient': {
'start': GRADIENT_START_COLOR, 'start': GRADIENT_START_COLOR,
@ -64,6 +67,7 @@ class TestSecondaryHeroShelfSerializer(TestCase):
'headline': 'Its a héadline!', 'headline': 'Its a héadline!',
'description': 'description', 'description': 'description',
'cta': None, 'cta': None,
'modules': [],
} }
hero.update(cta_url='/extensions/', cta_text='Go here') hero.update(cta_url='/extensions/', cta_text='Go here')
data = SecondaryHeroShelfSerializer(instance=hero).data data = SecondaryHeroShelfSerializer(instance=hero).data
@ -74,4 +78,43 @@ class TestSecondaryHeroShelfSerializer(TestCase):
'url': 'http://testserver/extensions/', 'url': 'http://testserver/extensions/',
'text': 'Go here', 'text': 'Go here',
}, },
'modules': [],
}
def test_with_modules(self):
hero = SecondaryHero.objects.create()
promos = [
SecondaryHeroModule.objects.create(
description='It does things!', shelf=hero, icon='a.svg'),
SecondaryHeroModule.objects.create(
shelf=hero, cta_url='/extensions/', cta_text='Go here',
icon='b.svg'),
SecondaryHeroModule.objects.create(
shelf=hero, icon='c.svg'),
]
data = SecondaryHeroShelfSerializer(instance=hero).data
assert data == {
'headline': '',
'description': '',
'cta': None,
'modules': [
{
'description': promos[0].description,
'icon': 'http://testserver/static/img/hero/icons/a.svg',
'cta': None,
},
{
'description': '',
'icon': 'http://testserver/static/img/hero/icons/b.svg',
'cta': {
'url': 'http://testserver/extensions/',
'text': 'Go here',
},
},
{
'description': '',
'icon': 'http://testserver/static/img/hero/icons/c.svg',
'cta': None,
},
],
} }

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

@ -2,7 +2,7 @@ from olympia.amo.templatetags.jinja_helpers import absolutify
from olympia.amo.tests import addon_factory, TestCase, reverse_ns from olympia.amo.tests import addon_factory, TestCase, reverse_ns
from olympia.discovery.models import DiscoveryItem from olympia.discovery.models import DiscoveryItem
from ..models import PrimaryHero, SecondaryHero from ..models import PrimaryHero, SecondaryHero, SecondaryHeroModule
from ..serializers import ( from ..serializers import (
PrimaryHeroShelfSerializer, SecondaryHeroShelfSerializer) PrimaryHeroShelfSerializer, SecondaryHeroShelfSerializer)
@ -96,15 +96,22 @@ class TestSecondaryHeroShelfViewSet(TestCase):
SecondaryHero.objects.create( SecondaryHero.objects.create(
headline='dfdfd!', headline='dfdfd!',
description='dfdfd') description='dfdfd')
SecondaryHeroModule.objects.create(shelf=hero_a)
SecondaryHeroModule.objects.create(shelf=hero_a)
SecondaryHeroModule.objects.create(shelf=hero_a)
# The shelf isn't enabled so still won't show up # The shelves aren't enabled so won't show up.
response = self.client.get(self.url) response = self.client.get(self.url)
assert response.json() == {'results': []} assert response.json() == {'results': []}
hero_a.update(enabled=True) hero_a.update(enabled=True)
hero_b.update(enabled=True) hero_b.update(enabled=True)
# don't enable the 3rd PrimaryHero object # don't enable the 3rd PrimaryHero object
response = self.client.get(self.url) with self.assertNumQueries(2):
# 2 queries:
# - 1 to fetch all SecondaryHero results
# - 1 to fetch all the SecondaryHeroModules
response = self.client.get(self.url)
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == { assert response.json() == {
'results': [ 'results': [
@ -123,11 +130,14 @@ class TestHeroShelvesView(TestCase):
shero = SecondaryHero.objects.create( shero = SecondaryHero.objects.create(
headline='headline', description='description', headline='headline', description='description',
enabled=True) enabled=True)
SecondaryHeroModule.objects.create(shelf=shero)
SecondaryHeroModule.objects.create(shelf=shero)
SecondaryHeroModule.objects.create(shelf=shero)
with self.assertNumQueries(12): with self.assertNumQueries(13):
# 12 queries: # 13 queries:
# first 11 as TestPrimaryHeroShelfViewSet.test_basic above # - 11 as TestPrimaryHeroShelfViewSet.test_basic above
# + 1 to fetch SecondaryHero result # - 2 as TestSecondaryHeroShelfViewSet.test_basic above
response = self.client.get(self.url, {'lang': 'en-US'}) response = self.client.get(self.url, {'lang': 'en-US'})
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == { assert response.json() == {

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

@ -51,6 +51,11 @@ class SecondaryHeroShelfViewSet(ShelfViewSet):
queryset = SecondaryHero.objects queryset = SecondaryHero.objects
serializer_class = SecondaryHeroShelfSerializer serializer_class = SecondaryHeroShelfSerializer
def get_queryset(self):
qs = super().get_queryset()
qs = qs.prefetch_related('modules')
return qs
class HeroShelvesView(APIView): class HeroShelvesView(APIView):
def get(self, request, format=None): def get(self, request, format=None):

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

@ -0,0 +1,13 @@
CREATE TABLE `hero_secondaryheromodule` (
`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
`created` datetime(6) NOT NULL,
`modified` datetime(6) NOT NULL,
`icon` varchar(255) NOT NULL,
`description` varchar(50) NOT NULL,
`cta_url` varchar(255) NOT NULL,
`cta_text` varchar(20) NOT NULL,
`shelf_id` integer NOT NULL
);
ALTER TABLE `hero_secondaryheromodule`
ADD CONSTRAINT `hero_secondaryheromo_shelf_id_dabb040a_fk_hero_seco`
FOREIGN KEY (`shelf_id`) REFERENCES `hero_secondaryhero` (`id`);

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

@ -57,10 +57,16 @@
} }
.dynamic-primaryhero .field-gradient_color li, .dynamic-primaryhero .field-gradient_color li,
.dynamic-primaryhero .field-image li { .dynamic-primaryhero .field-image li,
.dynamic-modules .field-icon li {
display: inline-block; display: inline-block;
} }
.dynamic-modules .field-icon li {
margin: 0.5em;
vertical-align: top;
}
.dynamic-primaryhero .field-gradient_color li div, .dynamic-primaryhero .field-gradient_color li div,
.dynamic-primaryhero .field-image li div { .dynamic-primaryhero .field-image li div {
height: 9em; height: 9em;
@ -68,3 +74,14 @@
background-size: contain; background-size: contain;
background-repeat: no-repeat; background-repeat: no-repeat;
} }
.dynamic-modules .field-icon li div {
height: 3em;
width: 3em;
background-size: contain;
background-repeat: no-repeat;
}
.dynamic-modules .field-icon li div label {
color: transparent;
}

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><circle fill="context-fill" cx="8" cy="2" r="2"></circle><path fill="context-fill" d="M14.1 4.5H2c-.6 0-1 .4-1 1s.4 1 1 1h3.5v8.4c0 .6.4 1.1 1 1.1s1-.5 1-1.1v-4.1h1v4.1c0 .6.4 1.1 1 1.1s1-.5 1-1.1V6.4H14c.6 0 1-.4 1-1s-.4-.9-.9-.9z"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 541 B

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><g fill="context-fill"><path d="M8.587 2.354L5.5 5H4.191A2.191 2.191 0 0 0 2 7.191v1.618A2.191 2.191 0 0 0 4.191 11H5.5l3.17 2.717a.2.2 0 0 0 .33-.152V2.544a.25.25 0 0 0-.413-.19zm2.988.921a.5.5 0 0 0-.316.949 3.97 3.97 0 0 1 0 7.551.5.5 0 0 0 .316.949 4.971 4.971 0 0 0 0-9.449z"></path><path d="M13 8a3 3 0 0 0-2.056-2.787.5.5 0 1 0-.343.939A2.008 2.008 0 0 1 12 8a2.008 2.008 0 0 1-1.4 1.848.5.5 0 0 0 .343.939A3 3 0 0 0 13 8z"></path></g></svg>

После

Ширина:  |  Высота:  |  Размер: 743 B

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="context-fill" d="M3.8 15.922a1.1 1.1 0 0 1-1.09-1.253l.609-4.36L.392 7.163a1.1 1.1 0 0 1 .616-1.833l4.081-.73L7.015.734a1.1 1.1 0 0 1 1.969 0L10.911 4.6l4.084.729a1.1 1.1 0 0 1 .611 1.833L12.68 10.31l.609 4.359a1.1 1.1 0 0 1-1.6 1.127L8 13.873 4.307 15.8a1.093 1.093 0 0 1-.507.122zm-.415-1.9zm9.228 0zM2.981 7.01l2.451 2.635-.5 3.572L8 11.618l3.067 1.6-.5-3.572 2.451-2.636-3.45-.616L8 3.244l-1.569 3.15zm11.659.29zm-13.278 0zm12.78-1.5zm-12.286 0z"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 771 B

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="context-fill" d="M4 10a1.994 1.994 0 0 0-1.911 1.44c0 .01-.014.015-.017.025-.362 1.135-.705 2.11-1.759 2.573l-.023.012-.024.012A.5.5 0 0 0 0 14.5a.5.5 0 0 0 .5.5 6.974 6.974 0 0 0 4.825-1.5c.006-.006.007-.013.013-.019A1.993 1.993 0 0 0 4 10zM15.693.307a.984.984 0 0 0-1.338-.046l-8.031 7a.982.982 0 0 0-.049 1.433l1.032 1.031a.983.983 0 0 0 .693.287h.033a.982.982 0 0 0 .706-.335l7-8.031a.982.982 0 0 0-.046-1.339z"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 736 B

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="context-fill" d="M14.555 3.2l-2.434 2.436a1.243 1.243 0 1 1-1.757-1.757L12.8 1.445A3.956 3.956 0 0 0 11 1a3.976 3.976 0 0 0-3.434 6.02l-6.273 6.273a1 1 0 1 0 1.414 1.414L8.98 8.434A3.96 3.96 0 0 0 11 9a4 4 0 0 0 4-4 3.956 3.956 0 0 0-.445-1.8z"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 565 B

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="context-fill" d="M12 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zM9 15H7v-1h2zm3-2.5a.5.5 0 0 1-.5.5h-7a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5z"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 505 B

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M12 8a4 4 0 1 0 4 4 4 4 0 0 0-4-4zm2.2 4.46l-1.92 1.92a.38.38 0 0 1-.54 0L9.8 12.46a.38.38 0 0 1 .54-.54l1.27 1.27V9.88a.38.38 0 0 1 .77 0v3.3l1.27-1.27a.38.38 0 0 1 .54.54z" fill="context-fill"></path><path d="M6.93.64a1 1 0 0 0-1.86 0l-5 14a1 1 0 0 0 .61 1.28A1 1 0 0 0 1 16a1 1 0 0 0 .94-.66l1.58-4.43h3.6a5 5 0 0 1 2.34-3.19zM4.24 8.91L6 4l1.76 4.91z" fill="context-fill"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 693 B

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="context-fill" d="M7.293 12.707a1 1 0 0 0 1.414 0l5-5a1 1 0 0 0-1.414-1.414L9 9.586V1a1 1 0 1 0-2 0v8.586L3.707 6.293a1 1 0 0 0-1.414 1.414zM13 14H3a1 1 0 0 0 0 2h10a1 1 0 0 0 0-2z"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 501 B

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="context-fill" d="M14.5 8c-.971 0-1 1-1.75 1a.765.765 0 0 1-.75-.75V5a1 1 0 0 0-1-1H7.75A.765.765 0 0 1 7 3.25c0-.75 1-.779 1-1.75C8 .635 7.1 0 6 0S4 .635 4 1.5c0 .971 1 1 1 1.75a.765.765 0 0 1-.75.75H1a1 1 0 0 0-1 1v2.25A.765.765 0 0 0 .75 8c.75 0 .779-1 1.75-1C3.365 7 4 7.9 4 9s-.635 2-1.5 2c-.971 0-1-1-1.75-1a.765.765 0 0 0-.75.75V15a1 1 0 0 0 1 1h3.25a.765.765 0 0 0 .75-.75c0-.75-1-.779-1-1.75 0-.865.9-1.5 2-1.5s2 .635 2 1.5c0 .971-1 1-1 1.75a.765.765 0 0 0 .75.75H11a1 1 0 0 0 1-1v-3.25a.765.765 0 0 1 .75-.75c.75 0 .779 1 1.75 1 .865 0 1.5-.9 1.5-2s-.635-2-1.5-2z"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 894 B

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="context-fill" d="M14 3H8.151L6.584 1.538A2 2 0 0 0 5.219 1H2a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zM5.219 3l1.072 1H2V3zM14 13H2V5h6v-.014c.05 0 .1.014.151.014H14z"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 511 B

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="context-fill" d="M8 0a8 8 0 1 0 8 8 8.009 8.009 0 0 0-8-8zm5.163 4.958h-1.552a7.7 7.7 0 0 0-1.051-2.376 6.03 6.03 0 0 1 2.603 2.376zM14 8a5.963 5.963 0 0 1-.335 1.958h-1.821A12.327 12.327 0 0 0 12 8a12.327 12.327 0 0 0-.156-1.958h1.821A5.963 5.963 0 0 1 14 8zm-6 6c-1.075 0-2.037-1.2-2.567-2.958h5.135C10.037 12.8 9.075 14 8 14zM5.174 9.958a11.084 11.084 0 0 1 0-3.916h5.651A11.114 11.114 0 0 1 11 8a11.114 11.114 0 0 1-.174 1.958zM2 8a5.963 5.963 0 0 1 .335-1.958h1.821a12.361 12.361 0 0 0 0 3.916H2.335A5.963 5.963 0 0 1 2 8zm6-6c1.075 0 2.037 1.2 2.567 2.958H5.433C5.963 3.2 6.925 2 8 2zm-2.56.582a7.7 7.7 0 0 0-1.051 2.376H2.837A6.03 6.03 0 0 1 5.44 2.582zm-2.6 8.46h1.549a7.7 7.7 0 0 0 1.051 2.376 6.03 6.03 0 0 1-2.603-2.376zm7.723 2.376a7.7 7.7 0 0 0 1.051-2.376h1.552a6.03 6.03 0 0 1-2.606 2.376z"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 1.1 KiB

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M9.5 3s.428 2.43 1.249 3.251S14 7.5 14 7.5s-2.43.394-3.251 1.215S9.5 12 9.5 12s-.394-2.464-1.215-3.285S5 7.5 5 7.5s2.464-.428 3.285-1.249S9.5 3 9.5 3m0-2h-.014a2 2 0 0 0-1.96 1.68 7.536 7.536 0 0 1-.659 2.154 7.9 7.9 0 0 1-2.212.7 2 2 0 0 0 .029 3.945 7.733 7.733 0 0 1 2.183.658 7.74 7.74 0 0 1 .658 2.185A2 2 0 0 0 9.489 14H9.5a2 2 0 0 0 1.971-1.657 7.891 7.891 0 0 1 .7-2.209 7.566 7.566 0 0 1 2.154-.659 2 2 0 0 0 .027-3.944 7.694 7.694 0 0 1-2.181-.7 7.731 7.731 0 0 1-.7-2.181A2 2 0 0 0 9.5 1zM3 15.5a.5.5 0 0 1-.49-.421 3.047 3.047 0 0 0-.4-1.186 3.047 3.047 0 0 0-1.186-.4.5.5 0 0 1-.007-.986 3.147 3.147 0 0 0 1.192-.417 3.051 3.051 0 0 0 .4-1.171A.5.5 0 0 1 3 10.5a.5.5 0 0 1 .492.413 3.094 3.094 0 0 0 .417 1.179 3.142 3.142 0 0 0 1.178.416.5.5 0 0 1-.007.985 3.007 3.007 0 0 0-1.172.4 3.166 3.166 0 0 0-.416 1.192A.5.5 0 0 1 3 15.5zm-.5-11a.5.5 0 0 1-.49-.42 2.344 2.344 0 0 0-.265-.82 2.344 2.344 0 0 0-.82-.265.5.5 0 0 1-.007-.986 2.41 2.41 0 0 0 .827-.277A2.306 2.306 0 0 0 2.007.92.5.5 0 0 1 2.5.5a.5.5 0 0 1 .492.412 2.353 2.353 0 0 0 .278.818 2.372 2.372 0 0 0 .816.276.5.5 0 0 1-.007.985 2.306 2.306 0 0 0-.811.266 2.41 2.41 0 0 0-.277.827.5.5 0 0 1-.491.416z" fill="context-fill"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 1.5 KiB

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="context-fill" d="M8 0a8 8 0 1 0 8 8 8.009 8.009 0 0 0-8-8zm0 14a6 6 0 1 1 6-6 6.007 6.007 0 0 1-6 6zm3.5-6H8V4.5a.5.5 0 0 0-1 0v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 0-1z"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 485 B

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="context-fill" d="M10.992 1a4.009 4.009 0 0 0-4.009 4.008c0 .1.022.187.028.282-.059.05-.119.087-.178.143L5.667 6.6a.366.366 0 0 0 0 .467A1.878 1.878 0 0 0 6 7.5.353.353 0 0 1 6 8l-5 5v1.767a.229.229 0 0 0 .233.233H3.77a.229.229 0 0 0 .23-.233v-.778h.75a.227.227 0 0 0 .233-.228v-.768H5.2s.28 0 .28-.235V12.5h.779s.233-.1.233-.244v-1.271h.855l1.12-1.118H8.7l.467.467c.233.233.233.233.365.233a.437.437 0 0 0 .275-.127l.993-1.273c.034-.053.054-.107.084-.161.036 0 .07.011.107.011a4.008 4.008 0 1 0 0-8.017zM12.5 4.489a1 1 0 1 1 1-1 1 1 0 0 1-1 1z"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 864 B

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><g fill="context-fill"><path d="M13.577 1H2.423A2.426 2.426 0 0 0 0 3.423v6.154A2.426 2.426 0 0 0 2.423 12H8.26l2.966 3.633A1 1 0 0 0 13 15v-3h.577A2.426 2.426 0 0 0 16 9.577V3.423A2.426 2.426 0 0 0 13.577 1zM14 9.577a.423.423 0 0 1-.423.423H12a1 1 0 0 0-1 1v1.194l-1.491-1.827A1 1 0 0 0 8.734 10H2.423A.423.423 0 0 1 2 9.577V3.423A.423.423 0 0 1 2.423 3h11.154a.423.423 0 0 1 .423.423z"></path><path d="M11.5 5h-7a.5.5 0 0 0 0 1h7a.5.5 0 0 0 0-1zm0 2h-7a.5.5 0 0 0 0 1h7a.5.5 0 0 0 0-1z"></path></g></svg>

После

Ширина:  |  Высота:  |  Размер: 801 B

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="context-fill" d="M8 1a8.009 8.009 0 0 0-8 8 7.917 7.917 0 0 0 .78 3.43 1 1 0 1 0 1.8-.86A5.943 5.943 0 0 1 2 9a6 6 0 1 1 11.414 2.571 1 1 0 1 0 1.807.858A7.988 7.988 0 0 0 8 1z"></path><path fill="context-fill" d="M11.769 7.078a.5.5 0 0 0-.69.153L8.616 11.1a2 2 0 1 0 .5 3.558 2.011 2.011 0 0 0 .54-.54 1.954 1.954 0 0 0-.2-2.479l2.463-3.871a.5.5 0 0 0-.15-.69z"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 683 B

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12"><path fill="context-fill" d="M10.53 9.47L8.25 7.19 9.8 5.643a.694.694 0 0 0 0-.98 3.04 3.04 0 0 0-2.161-.894h-.122A1.673 1.673 0 0 1 5.846 2.1v-.408A.693.693 0 0 0 4.664 1.2L1.2 4.664a.693.693 0 0 0 .49 1.182h.41a1.672 1.672 0 0 1 1.669 1.671v.117a2.8 2.8 0 0 0 .925 2.192.693.693 0 0 0 .949-.026L7.19 8.251l2.28 2.28a.75.75 0 0 0 1.06-1.061z"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 652 B

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><g fill="context-fill"><path d="M8 2a6 6 0 1 0 6 6 6.007 6.007 0 0 0-6-6zm0 11a5 5 0 1 1 5-5 5.006 5.006 0 0 1-5 5z"></path><path d="M6.75 4.969A.5.5 0 0 0 6 5.4v5.2a.5.5 0 0 0 .75.433l4.5-2.6a.5.5 0 0 0 0-.866z"></path></g></svg>

После

Ширина:  |  Высота:  |  Размер: 525 B

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="context-fill" d="M15.207 2.793a1 1 0 0 0-.932-.268l-6.5 1.5a1 1 0 1 0 .45 1.949l3.1-.716L6.5 10.086 5.207 8.793a1 1 0 0 0-1.414 0l-3 3a1 1 0 1 0 1.414 1.414L4.5 10.914l1.293 1.293a1 1 0 0 0 1.414 0l5.535-5.535-.716 3.1a1 1 0 0 0 .75 1.2A1.025 1.025 0 0 0 13 11a1 1 0 0 0 .974-.775l1.5-6.5a1 1 0 0 0-.267-.932z"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 631 B

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="context-fill" d="M12.408 11.992c-1.663 0-2.813-2-4.408-2s-2.844 2-4.408 2C1.54 11.992.025 10.048 0 6.719c-.015-2.068.6-2.727 3.265-2.727S6.709 5.082 8 5.082s2.071-1.091 4.735-1.091 3.28.66 3.265 2.727c-.025 3.33-1.54 5.274-3.592 5.274zM4.572 6.537c-1.619.07-2.286 1.035-2.286 1.273s1.073.909 2.122.909 2.286-.384 2.286-.727a1.9 1.9 0 0 0-2.122-1.455zm6.857 0a1.9 1.9 0 0 0-2.123 1.455c0 .343 1.236.727 2.286.727s2.122-.671 2.122-.909-.667-1.203-2.286-1.273z"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 779 B

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="context-fill" d="M12 0H4a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h8a3 3 0 0 0 3-3V3a3 3 0 0 0-3-3zm1 13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1z"></path><path fill="context-fill" d="M10.5 5h-5a.5.5 0 0 1 0-1h5a.5.5 0 0 1 0 1zm0 2h-5a.5.5 0 0 1 0-1h5a.5.5 0 0 1 0 1zm0 2h-5a.5.5 0 0 1 0-1h5a.5.5 0 0 1 0 1zm-3 2h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 0 1z"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 680 B

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="context-fill" d="M14 3h-2v2h2v8H2V5h7V3h-.849L6.584 1.538A2 2 0 0 0 5.219 1H2a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zM2 3h3.219l1.072 1H2z"></path><path fill="context-fill" d="M8.146 6.146a.5.5 0 0 0 0 .707l2 2a.5.5 0 0 0 .707 0l2-2a.5.5 0 1 0-.707-.707L11 7.293V.5a.5.5 0 0 0-1 0v6.793L8.854 6.146a.5.5 0 0 0-.708 0z"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 664 B

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="context-fill" d="M15.707 14.293l-4.822-4.822a6.019 6.019 0 1 0-1.414 1.414l4.822 4.822a1 1 0 0 0 1.414-1.414zM6 10a4 4 0 1 1 4-4 4 4 0 0 1-4 4z"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 465 B

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M18.75 9.977H18V7A6 6 0 0 0 6 7v2.977h-.75A2.25 2.25 0 0 0 3 12.227v7.523A2.25 2.25 0 0 0 5.25 22h13.5A2.25 2.25 0 0 0 21 19.75v-7.523a2.25 2.25 0 0 0-2.25-2.25zM9 7a3 3 0 0 1 6 0v2.977H9z"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 507 B

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="context-fill" d="M3.5 10A2.5 2.5 0 1 0 6 12.5 2.5 2.5 0 0 0 3.5 10zM2 1a1 1 0 0 0 0 2 10.883 10.883 0 0 1 11 11 1 1 0 0 0 2 0A12.862 12.862 0 0 0 2 1zm0 4a1 1 0 0 0 0 2 6.926 6.926 0 0 1 7 7 1 1 0 0 0 2 0 8.9 8.9 0 0 0-9-9z"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 545 B

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="context-fill" d="M11 11V9a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1V5a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v6H1a1 1 0 0 0 0 2h7v-1a1 1 0 0 1 1-1zm4.5 1H13V9.5a.5.5 0 0 0-1 0V12H9.5a.5.5 0 0 0 0 1H12v2.5a.5.5 0 0 0 1 0V13h2.5a.5.5 0 0 0 0-1z"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 542 B

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="context-fill" d="M4 10a1.994 1.994 0 0 0-1.911 1.44c0 .01-.014.015-.017.025-.362 1.135-.705 2.11-1.759 2.573l-.023.012-.024.012A.5.5 0 0 0 0 14.5a.5.5 0 0 0 .5.5 6.974 6.974 0 0 0 4.825-1.5c.006-.006.007-.013.013-.019A1.993 1.993 0 0 0 4 10zM15.693.307a.984.984 0 0 0-1.338-.046l-8.031 7a.982.982 0 0 0-.049 1.433l1.032 1.031a.983.983 0 0 0 .693.287h.033a.982.982 0 0 0 .706-.335l7-8.031a.982.982 0 0 0-.046-1.339z"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 736 B

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="context-fill" fill-opacity="context-fill-opacity"><path d="M9.306 9.694c.071.416.132.8.15 1.1a4.938 4.938 0 0 1-.058 1 18.45 18.45 0 0 0 3.478.193l.052-.537c.2-2.032.885-2.574 1.028-3.707a5.874 5.874 0 0 0-.223-2.475zm-.07 3.073c-.069.365-.136.737-.177 1.13a1.675 1.675 0 0 0 1.579 2.079c1.235.16 1.779-.976 1.944-1.635a8.594 8.594 0 0 0 .2-1.35c-.2.005-.4.009-.606.009a18.258 18.258 0 0 1-2.94-.233zm5.471-11.474a1 1 0 0 0-1.414 0L6.547 8.039c0-.083-.008-.167 0-.249a25.267 25.267 0 0 0 .432-3.949C6.724 1.833 5.853-.177 4.414 0 2.8.2 1.766 2.521 2.045 4.742c.143 1.133.828 1.675 1.028 3.707l.052.536a20.41 20.41 0 0 0 2.545-.069L4.614 9.972c-.256.012-.514.028-.76.028-.221 0-.432 0-.636-.01a9.6 9.6 0 0 0 .169 1.21l-2.094 2.093a1 1 0 1 0 1.414 1.414l12-12a1 1 0 0 0 0-1.414z"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 1.1 KiB

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="context-fill" d="M27 7.154a2.688 2.688 0 0 0-2.226-2.662L16 2.985 7.227 4.491A2.69 2.69 0 0 0 5 7.153c-.006 2.031.007 5.681.155 7.319.425 4.65 1.282 7.191 3.4 10.07a11.4 11.4 0 0 0 7.33 4.452l.112.012.112-.012a11.4 11.4 0 0 0 7.33-4.452c2.12-2.879 2.977-5.42 3.4-10.07.153-1.638.166-5.288.161-7.318zm-2.147 7.137c-.391 4.287-1.125 6.49-3.021 9.065A9.562 9.562 0 0 1 16 26.989a9.568 9.568 0 0 1-5.831-3.633c-1.9-2.575-2.63-4.778-3.021-9.065C7 12.676 7 8.765 7 7.159a.7.7 0 0 1 .563-.7L16 5.015l8.436 1.448a.694.694 0 0 1 .563.7c.001 1.602.001 5.512-.147 7.128z"></path><path fill="context-fill" d="M10.148 13.584c.465 5.1 1.336 6.611 2.716 8.486A6.459 6.459 0 0 0 16 24.337V7.75l-6 1.03c.021 2.264.073 3.979.148 4.804z"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 1.0 KiB

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="context-fill" d="M14.037 3.828L11 6.479v-2.09A1.345 1.345 0 0 0 9.7 3H2.3A1.345 1.345 0 0 0 1 4.389v7.222A1.345 1.345 0 0 0 2.3 13h7.4a1.345 1.345 0 0 0 1.3-1.389V9.552l3.037 2.648a1.007 1.007 0 0 0 .963.285V3.542a1.007 1.007 0 0 0-.963.286z"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 563 B

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

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="context-fill" d="M15.5 12H13V9.5a.5.5 0 0 0-1 0V12H9.5a.5.5 0 0 0 0 1H12v2.5a.5.5 0 0 0 1 0V13h2.5a.5.5 0 0 0 0-1z"></path><path fill="context-fill" d="M16 4a3 3 0 0 0-3-3H3a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3h4.03v-.006a.994.994 0 0 0 0-1.987V13H3a1 1 0 0 1-1-1V6h12v1.952h.01c0 .017-.01.031-.01.048a1 1 0 0 0 2 0c0-.017-.009-.031-.01-.048H16zM2 5V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1z"></path></svg>

После

Ширина:  |  Высота:  |  Размер: 700 B

Двоичные данные
static/img/hero/icons/artist.png

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 760 B