add secondary hero modules (#12068)
* add secondary hero modules * Rename 1114-add-secondary-hero-module.sql to 1115-add-secondary-hero-module.sql
|
@ -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 string cta.url: The url the call to action would link to.
|
||||
:>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: 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: added promo modules to secondary hero shelves. https://github.com/mozilla/addons-server/issues/11780
|
||||
|
||||
----------------
|
||||
v5 API changelog
|
||||
|
|
|
@ -123,6 +123,9 @@ DJANGO_PERMISSIONS_MAPPING.update({
|
|||
'hero.add_primaryhero': DISCOVERY_EDIT,
|
||||
'hero.change_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,
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
from olympia.amo.tests import TestCase, addon_factory, user_factory
|
||||
from olympia.amo.urlresolvers import django_reverse, reverse
|
||||
from olympia.discovery.models import DiscoveryItem
|
||||
from olympia.hero.models import PrimaryHero, SecondaryHero
|
||||
from olympia.hero.models import PrimaryHero, SecondaryHero, SecondaryHeroModule
|
||||
|
||||
|
||||
class TestDiscoveryAdmin(TestCase):
|
||||
|
@ -463,6 +463,29 @@ class TestSecondaryHeroShelfAdmin(TestCase):
|
|||
'admin:discovery_secondaryheroshelf_changelist')
|
||||
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):
|
||||
user = user_factory()
|
||||
self.grant_permission(user, 'Admin:Tools')
|
||||
|
@ -490,6 +513,11 @@ class TestSecondaryHeroShelfAdmin(TestCase):
|
|||
|
||||
def test_can_edit_with_discovery_edit_permission(self):
|
||||
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,))
|
||||
user = user_factory()
|
||||
self.grant_permission(user, 'Admin:Tools')
|
||||
|
@ -500,16 +528,42 @@ class TestSecondaryHeroShelfAdmin(TestCase):
|
|||
content = response.content.decode('utf-8')
|
||||
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(
|
||||
detail_url, {
|
||||
detail_url,
|
||||
dict(self._get_moduleform(item.id, shelves, initial=3), **{
|
||||
'headline': 'This headline is ... something.',
|
||||
'description': 'This description is as well!',
|
||||
}, follow=True)
|
||||
}), follow=True)
|
||||
assert response.status_code == 200
|
||||
assert 'errors' not in response.context_data, (
|
||||
response.context_data['errors'])
|
||||
item.reload()
|
||||
assert SecondaryHero.objects.count() == 1
|
||||
assert item.headline == 'This headline is ... something.'
|
||||
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):
|
||||
item = SecondaryHero.objects.create()
|
||||
|
@ -528,10 +582,11 @@ class TestSecondaryHeroShelfAdmin(TestCase):
|
|||
# Can actually delete.
|
||||
response = self.client.post(
|
||||
delete_url,
|
||||
{'post': 'yes'},
|
||||
dict(self._get_moduleform(item.pk, {}), post='yes'),
|
||||
follow=True)
|
||||
assert response.status_code == 200
|
||||
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):
|
||||
add_url = reverse('admin:discovery_secondaryheroshelf_add')
|
||||
|
@ -542,17 +597,38 @@ class TestSecondaryHeroShelfAdmin(TestCase):
|
|||
response = self.client.get(add_url, follow=True)
|
||||
assert response.status_code == 200
|
||||
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(
|
||||
add_url, {
|
||||
add_url,
|
||||
dict(self._get_moduleform('', shelves), **{
|
||||
'headline': 'This headline is ... something.',
|
||||
'description': 'This description is as well!',
|
||||
},
|
||||
}),
|
||||
follow=True)
|
||||
assert response.status_code == 200
|
||||
assert 'errors' not in response.context_data
|
||||
assert SecondaryHero.objects.count() == 1
|
||||
item = SecondaryHero.objects.get()
|
||||
assert item.headline == 'This headline is ... something.'
|
||||
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):
|
||||
add_url = reverse('admin:discovery_secondaryheroshelf_add')
|
||||
|
@ -610,3 +686,24 @@ class TestSecondaryHeroShelfAdmin(TestCase):
|
|||
delete_url, data={'post': 'yes'}, follow=True)
|
||||
assert response.status_code == 403
|
||||
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.core.exceptions import ValidationError
|
||||
from django.forms import BaseInlineFormSet
|
||||
|
||||
from .models import PrimaryHero
|
||||
from .models import PrimaryHero, SecondaryHeroModule
|
||||
|
||||
|
||||
class PrimaryHeroInline(admin.StackedInline):
|
||||
model = PrimaryHero
|
||||
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):
|
||||
list_display = ('__str__', 'headline')
|
||||
class Media:
|
||||
css = {
|
||||
'all': ('css/admin/discovery.css',)
|
||||
}
|
||||
list_display = ('headline', 'description', 'enabled')
|
||||
inlines = [SecondaryHeroModuleInline]
|
||||
view_on_site = False
|
||||
|
|
|
@ -17,6 +17,12 @@ GRADIENT_CHOICES = (
|
|||
('#712290', 'PURPLE70'),
|
||||
('#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):
|
||||
|
@ -35,19 +41,26 @@ class GradientChoiceWidget(RadioSelect):
|
|||
class ImageChoiceWidget(RadioSelect):
|
||||
option_template_name = 'hero/image_option.html'
|
||||
option_inherits_attrs = True
|
||||
image_url_base = FEATURED_IMAGE_URL
|
||||
|
||||
def create_option(self, name, value, label, selected, index,
|
||||
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(
|
||||
name=name, value=value, label=label, selected=selected,
|
||||
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):
|
||||
path = os.path.join(settings.ROOT, 'static', 'img', 'hero', 'featured')
|
||||
self.os_iter = os.scandir(path)
|
||||
self.os_iter = os.scandir(self.path)
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
|
@ -68,7 +81,7 @@ class WidgetCharField(models.CharField):
|
|||
|
||||
class PrimaryHero(ModelBase):
|
||||
image = WidgetCharField(
|
||||
choices=FeaturedImageChoices(),
|
||||
choices=DirImageChoices(path=FEATURED_IMAGE_PATH),
|
||||
max_length=255, widget=ImageChoiceWidget)
|
||||
gradient_color = WidgetCharField(
|
||||
choices=GRADIENT_CHOICES, max_length=7, widget=GradientChoiceWidget)
|
||||
|
@ -81,8 +94,8 @@ class PrimaryHero(ModelBase):
|
|||
return str(self.disco_addon)
|
||||
|
||||
@property
|
||||
def image_path(self):
|
||||
return f'{settings.STATIC_URL}img/hero/featured/{self.image}'
|
||||
def image_url(self):
|
||||
return f'{FEATURED_IMAGE_URL}{self.image}'
|
||||
|
||||
@property
|
||||
def gradient(self):
|
||||
|
@ -103,7 +116,18 @@ class PrimaryHero(ModelBase):
|
|||
'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)
|
||||
description = models.CharField(max_length=100, blank=False)
|
||||
cta_url = models.CharField(max_length=255, blank=True)
|
||||
|
@ -113,9 +137,22 @@ class SecondaryHero(ModelBase):
|
|||
def __str__(self):
|
||||
return str(self.headline)
|
||||
|
||||
def clean(self):
|
||||
both_or_neither = not (bool(self.cta_text) ^ bool(self.cta_url))
|
||||
if self.enabled and not both_or_neither:
|
||||
raise ValidationError(
|
||||
'Both the call to action URL and text must be defined, or '
|
||||
'neither, for enabled shelves.')
|
||||
|
||||
class SecondaryHeroModule(CTACheckMixin, ModelBase):
|
||||
icon = WidgetCharField(
|
||||
choices=DirImageChoices(path=MODULE_ICON_PATH),
|
||||
max_length=255, widget=IconChoiceWidget)
|
||||
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.discovery.serializers import DiscoveryAddonSerializer
|
||||
|
||||
from .models import PrimaryHero, SecondaryHero
|
||||
from .models import PrimaryHero, SecondaryHero, SecondaryHeroModule
|
||||
|
||||
|
||||
class ExternalAddonSerializer(AddonSerializer):
|
||||
|
@ -16,7 +16,7 @@ class ExternalAddonSerializer(AddonSerializer):
|
|||
|
||||
class PrimaryHeroShelfSerializer(serializers.ModelSerializer):
|
||||
description = serializers.CharField(source='disco_addon.description')
|
||||
featured_image = serializers.SerializerMethodField()
|
||||
featured_image = serializers.CharField(source='image_url')
|
||||
addon = DiscoveryAddonSerializer(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')
|
||||
return rep
|
||||
|
||||
def get_featured_image(self, obj):
|
||||
return absolutify(obj.image_path)
|
||||
|
||||
|
||||
class SecondaryHeroShelfSerializer(serializers.ModelSerializer):
|
||||
cta = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = SecondaryHero
|
||||
fields = ('headline', 'description', 'cta')
|
||||
class CTAMixin():
|
||||
|
||||
def get_cta(self, obj):
|
||||
if obj.cta_url and obj.cta_text:
|
||||
|
@ -49,3 +41,22 @@ class SecondaryHeroShelfSerializer(serializers.ModelSerializer):
|
|||
}
|
||||
else:
|
||||
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>
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
from django.core.exceptions import ValidationError
|
||||
|
||||
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
|
||||
|
||||
|
||||
class TestPrimaryHero(TestCase):
|
||||
def test_image_path(self):
|
||||
def test_image_url(self):
|
||||
ph = PrimaryHero.objects.create(
|
||||
disco_addon=DiscoveryItem.objects.create(addon=addon_factory()),
|
||||
image='foo.png')
|
||||
assert ph.image_path == (
|
||||
assert ph.image_url == (
|
||||
'http://testserver/static/img/hero/featured/foo.png')
|
||||
|
||||
def test_gradiant(self):
|
||||
|
@ -51,6 +51,7 @@ class TestPrimaryHero(TestCase):
|
|||
|
||||
|
||||
class TestSecondaryHero(TestCase):
|
||||
|
||||
def test_str(self):
|
||||
sh = SecondaryHero.objects.create(
|
||||
headline='Its a héadline!', description='description')
|
||||
|
@ -85,3 +86,42 @@ class TestSecondaryHero(TestCase):
|
|||
ph.enabled = True
|
||||
ph.cta_url = 'http://goo.gl'
|
||||
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.serializers import DiscoveryAddonSerializer
|
||||
|
||||
from ..models import GRADIENT_START_COLOR, PrimaryHero, SecondaryHero
|
||||
from ..models import (
|
||||
GRADIENT_START_COLOR, PrimaryHero, SecondaryHero, SecondaryHeroModule)
|
||||
from ..serializers import (
|
||||
ExternalAddonSerializer, PrimaryHeroShelfSerializer,
|
||||
SecondaryHeroShelfSerializer)
|
||||
|
@ -19,7 +20,8 @@ class TestPrimaryHeroShelfSerializer(TestCase):
|
|||
gradient_color='#123456')
|
||||
data = PrimaryHeroShelfSerializer(instance=hero).data
|
||||
assert data == {
|
||||
'featured_image': hero.image_path,
|
||||
'featured_image': (
|
||||
'http://testserver/static/img/hero/featured/foo.png'),
|
||||
'description': '<blockquote>Déscription</blockquote>',
|
||||
'gradient': {
|
||||
'start': GRADIENT_START_COLOR,
|
||||
|
@ -38,7 +40,8 @@ class TestPrimaryHeroShelfSerializer(TestCase):
|
|||
gradient_color='#123456',
|
||||
is_external=True)
|
||||
assert PrimaryHeroShelfSerializer(instance=hero).data == {
|
||||
'featured_image': hero.image_path,
|
||||
'featured_image': (
|
||||
'http://testserver/static/img/hero/featured/foo.png'),
|
||||
'description': '<blockquote>Summary</blockquote>',
|
||||
'gradient': {
|
||||
'start': GRADIENT_START_COLOR,
|
||||
|
@ -64,6 +67,7 @@ class TestSecondaryHeroShelfSerializer(TestCase):
|
|||
'headline': 'Its a héadline!',
|
||||
'description': 'description',
|
||||
'cta': None,
|
||||
'modules': [],
|
||||
}
|
||||
hero.update(cta_url='/extensions/', cta_text='Go here')
|
||||
data = SecondaryHeroShelfSerializer(instance=hero).data
|
||||
|
@ -74,4 +78,43 @@ class TestSecondaryHeroShelfSerializer(TestCase):
|
|||
'url': 'http://testserver/extensions/',
|
||||
'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.discovery.models import DiscoveryItem
|
||||
|
||||
from ..models import PrimaryHero, SecondaryHero
|
||||
from ..models import PrimaryHero, SecondaryHero, SecondaryHeroModule
|
||||
from ..serializers import (
|
||||
PrimaryHeroShelfSerializer, SecondaryHeroShelfSerializer)
|
||||
|
||||
|
@ -96,15 +96,22 @@ class TestSecondaryHeroShelfViewSet(TestCase):
|
|||
SecondaryHero.objects.create(
|
||||
headline='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)
|
||||
assert response.json() == {'results': []}
|
||||
|
||||
hero_a.update(enabled=True)
|
||||
hero_b.update(enabled=True)
|
||||
# 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.json() == {
|
||||
'results': [
|
||||
|
@ -123,11 +130,14 @@ class TestHeroShelvesView(TestCase):
|
|||
shero = SecondaryHero.objects.create(
|
||||
headline='headline', description='description',
|
||||
enabled=True)
|
||||
SecondaryHeroModule.objects.create(shelf=shero)
|
||||
SecondaryHeroModule.objects.create(shelf=shero)
|
||||
SecondaryHeroModule.objects.create(shelf=shero)
|
||||
|
||||
with self.assertNumQueries(12):
|
||||
# 12 queries:
|
||||
# first 11 as TestPrimaryHeroShelfViewSet.test_basic above
|
||||
# + 1 to fetch SecondaryHero result
|
||||
with self.assertNumQueries(13):
|
||||
# 13 queries:
|
||||
# - 11 as TestPrimaryHeroShelfViewSet.test_basic above
|
||||
# - 2 as TestSecondaryHeroShelfViewSet.test_basic above
|
||||
response = self.client.get(self.url, {'lang': 'en-US'})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
|
|
|
@ -51,6 +51,11 @@ class SecondaryHeroShelfViewSet(ShelfViewSet):
|
|||
queryset = SecondaryHero.objects
|
||||
serializer_class = SecondaryHeroShelfSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
qs = qs.prefetch_related('modules')
|
||||
return qs
|
||||
|
||||
|
||||
class HeroShelvesView(APIView):
|
||||
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-image li {
|
||||
.dynamic-primaryhero .field-image li,
|
||||
.dynamic-modules .field-icon li {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dynamic-modules .field-icon li {
|
||||
margin: 0.5em;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.dynamic-primaryhero .field-gradient_color li div,
|
||||
.dynamic-primaryhero .field-image li div {
|
||||
height: 9em;
|
||||
|
@ -68,3 +74,14 @@
|
|||
background-size: contain;
|
||||
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 |