Generate static theme Preview on version submit (#7475)

This commit is contained in:
Andrew Williamson 2018-02-14 22:27:16 +08:00 коммит произвёл GitHub
Родитель 8be6a7b606
Коммит c05e4cfdf1
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
21 изменённых файлов: 357 добавлений и 42 удалений

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

@ -41,6 +41,7 @@ addons:
- oracle-java8-set-default - oracle-java8-set-default
- elasticsearch - elasticsearch
- gettext - gettext
- librsvg2-bin
before_install: before_install:
- mysql -e 'create database olympia;' - mysql -e 'create database olympia;'

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

@ -40,6 +40,8 @@ RUN apt-get update && apt-get install -y \
default-libmysqlclient-dev \ default-libmysqlclient-dev \
swig \ swig \
gettext \ gettext \
# Use rsvg-convert to render our static theme previews
librsvg2-bin \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Compile required locale # Compile required locale

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

@ -43,6 +43,8 @@ RUN apt-get update && apt-get install -y \
default-libmysqlclient-dev \ default-libmysqlclient-dev \
swig \ swig \
gettext \ gettext \
# Use rsvg-convert to render our static theme previews
librsvg2-bin \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Compile required locale # Compile required locale

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

@ -522,7 +522,7 @@ class Addon(OnChangeMixin, ModelBase):
return True return True
@classmethod @classmethod
def initialize_addon_from_upload(cls, data, upload, channel): def initialize_addon_from_upload(cls, data, upload, channel, user):
fields = [field.name for field in cls._meta.get_fields()] fields = [field.name for field in cls._meta.get_fields()]
guid = data.get('guid') guid = data.get('guid')
old_guid_addon = None old_guid_addon = None
@ -561,23 +561,20 @@ class Addon(OnChangeMixin, ModelBase):
if old_guid_addon: if old_guid_addon:
old_guid_addon.update(guid='guid-reused-by-pk-{}'.format(addon.pk)) old_guid_addon.update(guid='guid-reused-by-pk-{}'.format(addon.pk))
old_guid_addon.save() old_guid_addon.save()
return addon
@classmethod if user:
def create_addon_from_upload_data(cls, data, upload, channel, user=None,
**kwargs):
addon = cls.initialize_addon_from_upload(data, upload, channel,
**kwargs)
AddonUser(addon=addon, user=user).save() AddonUser(addon=addon, user=user).save()
return addon return addon
@classmethod @classmethod
def from_upload(cls, upload, platforms, source=None, def from_upload(cls, upload, platforms, source=None,
channel=amo.RELEASE_CHANNEL_LISTED, parsed_data=None): channel=amo.RELEASE_CHANNEL_LISTED, parsed_data=None,
user=None):
if not parsed_data: if not parsed_data:
parsed_data = parse_addon(upload) parsed_data = parse_addon(upload)
addon = cls.initialize_addon_from_upload(parsed_data, upload, channel) addon = cls.initialize_addon_from_upload(
parsed_data, upload, channel, user)
if upload.validation_timeout: if upload.validation_timeout:
AddonReviewerFlags.objects.update_or_create( AddonReviewerFlags.objects.update_or_create(

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

@ -1,4 +1,5 @@
import re import re
from collections import namedtuple
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -230,6 +231,8 @@ ADDON_ICON_SIZES = [32, 48, 64, 128, 256, 512]
# Preview upload sizes [thumb, full] # Preview upload sizes [thumb, full]
ADDON_PREVIEW_SIZES = [(200, 150), (700, 525)] ADDON_PREVIEW_SIZES = [(200, 150), (700, 525)]
THEME_PREVIEW_SIZE = namedtuple('SizeTuple', 'width height')(680, 100)
# Persona image sizes [preview, full] # Persona image sizes [preview, full]
PERSONA_IMAGE_SIZES = { PERSONA_IMAGE_SIZES = {
'header': [(680, 100), (3000, 200)], 'header': [(680, 100), (3000, 200)],

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

@ -1,18 +1,18 @@
{% autoescape true %} {% autoescape true %}
<svg id="preview-svg-root" width="680" height="100" xmlns="http://www.w3.org/2000/svg" <svg id="preview-svg-root" width="{{ amo.THEME_PREVIEW_SIZE.width }}" height="{{amo.THEME_PREVIEW_SIZE.height }}" xmlns="http://www.w3.org/2000/svg"
version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:svgjs="http://svgjs.com/svgjs" font-size="16px" font-family="Helvetica, Arial, sans-serif"> xmlns:svgjs="http://svgjs.com/svgjs" font-size="16px" font-family="Helvetica, Arial, sans-serif">
<defs id="SvgDefs1007"></defs> <defs id="SvgDefs1007"></defs>
<rect id="SvgRect1008" width="680" height="100" <rect id="SvgRect1008" width="{{ amo.THEME_PREVIEW_SIZE.width }}" height="{{ amo.THEME_PREVIEW_SIZE.height }}"
class="accentcolor" fill="{{ accentcolor|d('rgba(229,230,232,1)') }}" data-fill="rgba(229,230,232,1)"></rect> class="accentcolor" fill="{{ accentcolor|d('rgba(229,230,232,1)') }}" data-fill="rgba(229,230,232,1)"></rect>
<image id="svg-header-img" width="680" height="200" preserveAspectRatio="xMaxYMin slice" <image id="svg-header-img" width="{{ amo.THEME_PREVIEW_SIZE.width }}" height="{{ header_src_height|d(amo.THEME_PREVIEW_SIZE.height) }}" preserveAspectRatio="{{ preserve_aspect_ratio|d('xMaxYMin slice') }}"
xlink:href="{{ header_src|d }}"></image> xlink:href="{{ header_src|d }}"></image>
<text id="SvgText1012" x="200" y="25" class="textcolor" fill="{{ textcolor|d }}"> <text id="SvgText1012" x="200" y="25" class="textcolor" fill="{{ textcolor|d }}">
<tspan id="SvgTspan1013" dy="2">🌐 .... ....</tspan> <tspan id="SvgTspan1013" dy="2">. .... ....</tspan>
<tspan id="SvgTspan1013x" dy="5" dx="45" font-size="150%">×</tspan> <tspan id="SvgTspan1013x" dy="5" dx="45" font-size="150%">×</tspan>
</text> </text>
<text id="SvgText1012a" x="340" y="25" class="textcolor" fill="{{ textcolor|d }}"> <text id="SvgText1012a" x="340" y="25" class="textcolor" fill="{{ textcolor|d }}">
<tspan id="SvgTspan1024a" dy="2">🌐 .... ....</tspan> <tspan id="SvgTspan1024a" dy="2">. .... ....</tspan>
<tspan id="SvgTspan1024ax" dy="5" dx="45" font-size="150%">×</tspan> <tspan id="SvgTspan1024ax" dy="5" dx="45" font-size="150%">×</tspan>
</text> </text>
<text id="SvgText1012b" x="480" y="25" class="textcolor" fill="{{ textcolor|d }}"> <text id="SvgText1012b" x="480" y="25" class="textcolor" fill="{{ textcolor|d }}">
@ -20,45 +20,46 @@
</text> </text>
<path d="M330,0 v45 h1 v-45 h-1 z" class="textcolor" fill="{{ text_color|d }}" fill-opacity="0.6"></path> <path d="M330,0 v45 h1 v-45 h-1 z" class="textcolor" fill="{{ text_color|d }}" fill-opacity="0.6"></path>
<path d="M470,0 v45 h1 v-45 h-1 z" class="textcolor" fill="{{ text_color|d }}" fill-opacity="0.6"></path> <path d="M470,0 v45 h1 v-45 h-1 z" class="textcolor" fill="{{ text_color|d }}" fill-opacity="0.6"></path>
<rect id="SvgRect1014" width="680" height="55" y="45" <rect id="SvgRect1014" width="{{ amo.THEME_PREVIEW_SIZE.width }}" height="55" y="45"
class="toolbar" fill="{{ toolbar|d('rgba(255,255,255,0.6)') }}" data-fill="rgba(255,255,255,0.6)"></rect> class="toolbar" fill="{{ toolbar|d('rgba(255,255,255,0.6)') }}" data-fill="rgba(255,255,255,0.6)"></rect>
<rect id="SvgRect1015" width="140" height="42" x="50" y="3" <rect id="SvgRect1015" width="140" height="42" x="50" y="3"
class="toolbar" fill="{{ toolbar|d('rgba(255,255,255,0.6)') }}" data-fill="rgba(255,255,255,0.6)"></rect> class="toolbar" fill="{{ toolbar|d('rgba(255,255,255,0.6)') }}" data-fill="rgba(255,255,255,0.6)"></rect>
<text id="SvgText1016" x="60" y="25" <text id="SvgText1016" x="60" y="25"
class="toolbar_text" fill="{{ toolbar_text|d }}"> class="toolbar_text" fill="{{ toolbar_text|d }}">
<tspan id="SvgTspan1017" dy="2">🌐 .... .... </tspan> <tspan id="SvgTspan1017" dy="2">. .... .... </tspan>
<tspan id="SvgTspan1013x" dy="5" dx="45" font-size="150%">×</tspan> <tspan id="SvgTspan1013x" dy="5" dx="45" font-size="150%">×</tspan>
</text> </text>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" x="10" y="65" class="toolbar_text" fill="{{ toolbar_text|d }}"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" x="10" y="65" id="button-back-arrow-svg">
<path d="M15 7H3.414l4.293-4.293a1 1 0 0 0-1.414-1.414l-6 6a1 1 0 0 0 0 1.414l6 6a1 1 0 0 0 1.414-1.414L3.414 9H15a1 1 0 0 0 0-2z"></path> <path d="M15 7H3.414l4.293-4.293a1 1 0 0 0-1.414-1.414l-6 6a1 1 0 0 0 0 1.414l6 6a1 1 0 0 0 1.414-1.414L3.414 9H15a1 1 0 0 0 0-2z" class="toolbar_text" fill="{{ toolbar_text|d }}" ></path>
</svg> </svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" x="50" y="65" class="toolbar_text" fill="{{ toolbar_text|d }}"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" x="50" y="65" id="button-forward-arrow-svg">
<path d="M15.707 7.293l-6-6a1 1 0 0 0-1.414 1.414L12.586 7H1a1 1 0 0 0 0 2h11.586l-4.293 4.293a1 1 0 1 0 1.414 1.414l6-6a1 1 0 0 0 0-1.414z"></path> <path d="M15.707 7.293l-6-6a1 1 0 0 0-1.414 1.414L12.586 7H1a1 1 0 0 0 0 2h11.586l-4.293 4.293a1 1 0 1 0 1.414 1.414l6-6a1 1 0 0 0 0-1.414z" class="toolbar_text" fill="{{ toolbar_text|d }}" ></path>
</svg> </svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" x="90" y="65" class="toolbar_text" fill="{{ toolbar_text|d }}"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" x="90" y="65" id="button-refresh-svg">
<path d="M15 1a1 1 0 0 0-1 1v2.418A6.995 6.995 0 1 0 8 15a6.954 6.954 0 0 0 4.95-2.05 1 1 0 0 0-1.414-1.414A5.019 5.019 0 1 1 12.549 6H10a1 1 0 0 0 0 2h5a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1z"></path> <path d="M15 1a1 1 0 0 0-1 1v2.418A6.995 6.995 0 1 0 8 15a6.954 6.954 0 0 0 4.95-2.05 1 1 0 0 0-1.414-1.414A5.019 5.019 0 1 1 12.549 6H10a1 1 0 0 0 0 2h5a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1z" class="toolbar_text" fill="{{ toolbar_text|d }}" ></path>
</svg> </svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" x="130" y="65" class="toolbar_text" fill="{{ toolbar_text|d }}"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" x="130" y="65" id="button-home-svg">
<path d="M15.7 7.3l-7-7c-.4-.4-1-.4-1.4 0l-7 7c-.4.4-.4 1 0 1.4.4.4 1 .4 1.4 0l.3-.3V13c0 1.7 1.3 3 3 3h6c1.7 0 3-1.3 3-3V8.4l.3.3c.2.2.4.3.7.3.3 0 .5-.1.7-.3.4-.4.4-1 0-1.4zM8 11.5c0-.3.2-.5.5-.5s.5.2.5.5-.2.5-.5.5-.5-.2-.5-.5zm4 1.5c0 .6-.4 1-1 1h-1V9c0-.6-.4-1-1-1H7c-.6 0-1 .4-1 1v5H5c-.6 0-1-.4-1-1V6.4l4-4 4 4V13z"></path> <path d="M15.7 7.3l-7-7c-.4-.4-1-.4-1.4 0l-7 7c-.4.4-.4 1 0 1.4.4.4 1 .4 1.4 0l.3-.3V13c0 1.7 1.3 3 3 3h6c1.7 0 3-1.3 3-3V8.4l.3.3c.2.2.4.3.7.3.3 0 .5-.1.7-.3.4-.4.4-1 0-1.4zM8 11.5c0-.3.2-.5.5-.5s.5.2.5.5-.2.5-.5.5-.5-.2-.5-.5zm4 1.5c0 .6-.4 1-1 1h-1V9c0-.6-.4-1-1-1H7c-.6 0-1 .4-1 1v5H5c-.6 0-1-.4-1-1V6.4l4-4 4 4V13z" class="toolbar_text" fill="{{ toolbar_text|d }}"></path>
</svg> </svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" x="610" y="65" class="toolbar_text" fill="{{ toolbar_text|d }}"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" x="610" y="65" id="button-library-svg">
<path d="M13 1H3a3.007 3.007 0 0 0-3 3v8a3.009 3.009 0 0 0 3 3h10a3.005 3.005 0 0 0 3-3V4a3.012 3.012 0 0 0-3-3zM2 12V4a1 1 0 0 1 1-1h5v10H3a1 1 0 0 1-1-1zm12 0a1 1 0 0 1-1 1H9V3h4a1 1 0 0 1 1 1z"></path><path d="M12.5 5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 0 1zm0 2h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 0 1zm-1 2h-1a.5.5 0 0 1 0-1h1a.5.5 0 0 1 0 1z"></path> <path d="M13 1H3a3.007 3.007 0 0 0-3 3v8a3.009 3.009 0 0 0 3 3h10a3.005 3.005 0 0 0 3-3V4a3.012 3.012 0 0 0-3-3zM2 12V4a1 1 0 0 1 1-1h5v10H3a1 1 0 0 1-1-1zm12 0a1 1 0 0 1-1 1H9V3h4a1 1 0 0 1 1 1z" class="toolbar_text" fill="{{ toolbar_text|d }}"></path>
<path d="M12.5 5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 0 1zm0 2h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 0 1zm-1 2h-1a.5.5 0 0 1 0-1h1a.5.5 0 0 1 0 1z" class="toolbar_text" fill="{{ toolbar_text|d }}"></path>
</svg> </svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" x="650" y="65" class="toolbar_text" fill="{{ toolbar_text|d }}"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" x="650" y="65" id="button-hamburger-svg">
<path d="M3 4h10a1 1 0 0 0 0-2H3a1 1 0 0 0 0 2zm10 3H3a1 1 0 0 0 0 2h10a1 1 0 0 0 0-2zm0 5H3a1 1 0 0 0 0 2h10a1 1 0 0 0 0-2z"></path> <path d="M3 4h10a1 1 0 0 0 0-2H3a1 1 0 0 0 0 2zm10 3H3a1 1 0 0 0 0 2h10a1 1 0 0 0 0-2zm0 5H3a1 1 0 0 0 0 2h10a1 1 0 0 0 0-2z" class="toolbar_text" fill="{{ toolbar_text|d }}"></path>
</svg> </svg>
<rect id="SvgRect1020" width="350" height="38" x="200" y="53" <rect id="SvgRect1020" width="350" height="38" x="200" y="53"
class="toolbar_field" fill="{{ toolbar_field|d('rgba(255,255,255,1)') }}" data-fill="rgba(255,255,255,1)" class="toolbar_field" fill="{{ toolbar_field|d('rgba(255,255,255,1)') }}" data-fill="rgba(255,255,255,1)"
stroke="black" stroke-width="1" stroke-linejoin="round" stroke-opacity="0.5"></rect> stroke="black" stroke-width="1" stroke-linejoin="round" stroke-opacity="0.5"></rect>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" x="210" y="63" class="toolbar_field_text" fill="{{ toolbar_field_text|d }}"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" x="210" y="63" id="icon-url-info-svg">
<path d="M8 1a7 7 0 1 0 7 7 7.008 7.008 0 0 0-7-7zm0 13a6 6 0 1 1 6-6 6.007 6.007 0 0 1-6 6zm0-7a1 1 0 0 0-1 1v3a1 1 0 1 0 2 0V8a1 1 0 0 0-1-1zm0-3.188A1.188 1.188 0 1 0 9.188 5 1.188 1.188 0 0 0 8 3.812z"></path> <path d="M8 1a7 7 0 1 0 7 7 7.008 7.008 0 0 0-7-7zm0 13a6 6 0 1 1 6-6 6.007 6.007 0 0 1-6 6zm0-7a1 1 0 0 0-1 1v3a1 1 0 1 0 2 0V8a1 1 0 0 0-1-1zm0-3.188A1.188 1.188 0 1 0 9.188 5 1.188 1.188 0 0 0 8 3.812z" class="toolbar_field_text" fill="{{ toolbar_field_text|d }}" ></path>
</svg> </svg>
<text id="SvgText1021" x="230" y="77" class="toolbar_field_text" fill="{{ toolbar_field_text|d }}"> <text id="SvgText1021" x="230" y="77" class="toolbar_field_text" fill="{{ toolbar_field_text|d }}">
<tspan id="SvgTspan1022">https://addons.mozilla.org/</tspan> <tspan id="SvgTspan1022">https://addons.mozilla.org/</tspan>
</text> </text>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="25" x="525" y="63" class="toolbar_field_text"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="25" x="525" y="63" id="menu-url-more-svg">
<path d="M2 6a2 2 0 1 0 2 2 2 2 0 0 0-2-2zm6 0a2 2 0 1 0 2 2 2 2 0 0 0-2-2zm6 0a2 2 0 1 0 2 2 2 2 0 0 0-2-2z"></path> <path d="M2 6a2 2 0 1 0 2 2 2 2 0 0 0-2-2zm6 0a2 2 0 1 0 2 2 2 2 0 0 0-2-2zm6 0a2 2 0 1 0 2 2 2 2 0 0 0-2-2z" class="toolbar_field_text" fill="{{ toolbar_field_text|d }}" ></path>
</svg> </svg>
<line id="SvgText1023" x1="0" y1="99" x2="680" y2="99" stroke-width="1" stroke="black"/> <line id="SvgText1023" x1="0" y1="99" x2="{{ amo.THEME_PREVIEW_SIZE.width }}" y2="99" stroke-width="1" stroke="black"/>
</svg> </svg>
{% endautoescape %} {% endautoescape %}

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

@ -5,6 +5,7 @@ from datetime import datetime, timedelta
from django.conf import settings from django.conf import settings
from django.core.files import temp from django.core.files import temp
from django.core.files.storage import default_storage as storage
import mock import mock
@ -390,6 +391,10 @@ class TestAddonSubmitUpload(UploadTest, TestCase):
all_ = sorted([f.filename for f in addon.current_version.all_files]) all_ = sorted([f.filename for f in addon.current_version.all_files])
assert all_ == [u'weta_fade-1.0.xpi'] # One XPI for all platforms. assert all_ == [u'weta_fade-1.0.xpi'] # One XPI for all platforms.
assert addon.type == amo.ADDON_STATICTHEME assert addon.type == amo.ADDON_STATICTHEME
assert addon.previews.all().count() == 1
preview = addon.previews.last()
assert storage.exists(preview.image_path)
assert preview.caption == unicode(addon.current_version.version)
@override_switch('allow-static-theme-uploads', active=True) @override_switch('allow-static-theme-uploads', active=True)
def test_static_theme_submit_unlisted(self): def test_static_theme_submit_unlisted(self):
@ -409,6 +414,8 @@ class TestAddonSubmitUpload(UploadTest, TestCase):
all_ = sorted([f.filename for f in latest_version.all_files]) all_ = sorted([f.filename for f in latest_version.all_files])
assert all_ == [u'weta_fade-1.0.xpi'] # One XPI for all platforms. assert all_ == [u'weta_fade-1.0.xpi'] # One XPI for all platforms.
assert addon.type == amo.ADDON_STATICTHEME assert addon.type == amo.ADDON_STATICTHEME
# Only listed submissions need a preview generated.
assert addon.previews.all().count() == 0
@override_switch('allow-static-theme-uploads', active=True) @override_switch('allow-static-theme-uploads', active=True)
def test_static_theme_wizard_listed(self): def test_static_theme_wizard_listed(self):
@ -437,6 +444,10 @@ class TestAddonSubmitUpload(UploadTest, TestCase):
all_ = sorted([f.filename for f in addon.current_version.all_files]) all_ = sorted([f.filename for f in addon.current_version.all_files])
assert all_ == [u'weta_fade-1.0.xpi'] # One XPI for all platforms. assert all_ == [u'weta_fade-1.0.xpi'] # One XPI for all platforms.
assert addon.type == amo.ADDON_STATICTHEME assert addon.type == amo.ADDON_STATICTHEME
assert addon.previews.all().count() == 1
preview = addon.previews.last()
assert storage.exists(preview.image_path)
assert preview.caption == unicode(addon.current_version.version)
@override_switch('allow-static-theme-uploads', active=True) @override_switch('allow-static-theme-uploads', active=True)
def test_static_theme_wizard_unlisted(self): def test_static_theme_wizard_unlisted(self):
@ -468,6 +479,8 @@ class TestAddonSubmitUpload(UploadTest, TestCase):
all_ = sorted([f.filename for f in latest_version.all_files]) all_ = sorted([f.filename for f in latest_version.all_files])
assert all_ == [u'weta_fade-1.0.xpi'] # One XPI for all platforms. assert all_ == [u'weta_fade-1.0.xpi'] # One XPI for all platforms.
assert addon.type == amo.ADDON_STATICTHEME assert addon.type == amo.ADDON_STATICTHEME
# Only listed submissions need a preview generated.
assert addon.previews.all().count() == 0
class DetailsPageMixin(object): class DetailsPageMixin(object):
@ -1361,6 +1374,13 @@ class VersionSubmitUploadMixin(object):
self.assert3xx(response, self.get_next_url(version)) self.assert3xx(response, self.get_next_url(version))
log_items = ActivityLog.objects.for_addons(self.addon) log_items = ActivityLog.objects.for_addons(self.addon)
assert log_items.filter(action=amo.LOG.ADD_VERSION.id) assert log_items.filter(action=amo.LOG.ADD_VERSION.id)
if self.channel == amo.RELEASE_CHANNEL_LISTED:
assert self.addon.previews.all().count() == 1
preview = self.addon.previews.last()
assert storage.exists(preview.image_path)
assert preview.caption == unicode(version)
else:
assert self.addon.previews.all().count() == 0
class TestVersionSubmitUploadListed(VersionSubmitUploadMixin, UploadTest): class TestVersionSubmitUploadListed(VersionSubmitUploadMixin, UploadTest):

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

@ -1380,9 +1380,9 @@ def _submit_upload(request, addon, channel, next_details, next_finish,
platforms=data.get('supported_platforms', []), platforms=data.get('supported_platforms', []),
source=data['source'], source=data['source'],
channel=channel, channel=channel,
parsed_data=data['parsed_data']) parsed_data=data['parsed_data'],
user=request.user)
version = addon.find_latest_version(channel=channel) version = addon.find_latest_version(channel=channel)
AddonUser(addon=addon, user=request.user).save()
url_args = [addon.slug] url_args = [addon.slug]
check_validation_override(request, form, addon, version) check_validation_override(request, form, addon, version)

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

@ -9,6 +9,7 @@ from datetime import timedelta
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.core.files.storage import default_storage
import flufl.lock import flufl.lock
import lxml import lxml
@ -448,6 +449,7 @@ class TestManifestJSONExtractor(TestCase):
class TestManifestJSONExtractorStaticTheme(TestManifestJSONExtractor): class TestManifestJSONExtractorStaticTheme(TestManifestJSONExtractor):
def parse(self, base_data): def parse(self, base_data):
if 'theme' not in base_data.keys():
base_data.update(theme={}) base_data.update(theme={})
return super( return super(
TestManifestJSONExtractorStaticTheme, self).parse(base_data) TestManifestJSONExtractorStaticTheme, self).parse(base_data)
@ -535,6 +537,11 @@ class TestManifestJSONExtractorStaticTheme(TestManifestJSONExtractor):
assert exc.value.message.startswith('Cannot find min/max version.') assert exc.value.message.startswith('Cannot find min/max version.')
def test_theme_json_extracted(self):
# Check theme data is extracted from the manifest and returned.
data = {'theme': {'colors': {'textcolor': "#3deb60"}}}
assert self.parse(data)['theme'] == data['theme']
def test_zip_folder_content(): def test_zip_folder_content():
extension_file = 'src/olympia/files/fixtures/files/extension.xpi' extension_file = 'src/olympia/files/fixtures/files/extension.xpi'
@ -898,3 +905,16 @@ class TestXMLVulnerabilities(TestCase):
# Setting it explicitly to `False` is fine too. # Setting it explicitly to `False` is fine too.
lxml.etree.XMLParser(resolve_entities=False) lxml.etree.XMLParser(resolve_entities=False)
def test_extract_header_img():
file_obj = os.path.join(
settings.ROOT, 'src/olympia/devhub/tests/addons/static_theme.zip')
data = {'images': {'headerURL': 'weta.png'}}
dest_path = tempfile.mkdtemp()
header_file = dest_path + '/weta.png'
assert not default_storage.exists(header_file)
utils.extract_header_img(file_obj, data, dest_path)
assert default_storage.exists(header_file)
assert default_storage.size(header_file) == 126447

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

@ -498,6 +498,8 @@ class ManifestJSONExtractor(object):
'permissions': self.get('permissions', []), 'permissions': self.get('permissions', []),
'content_scripts': self.get('content_scripts', []), 'content_scripts': self.get('content_scripts', []),
}) })
elif self.type == amo.ADDON_STATICTHEME:
data.update(theme=self.get('theme', {}))
return data return data
@ -1146,6 +1148,21 @@ def resolve_i18n_message(message, messages, locale, default_locale=None):
return message['message'] return message['message']
def extract_header_img(file_obj, theme_data, dest_path):
"""Extract static theme header image from `file_obj`."""
xpi = get_filepath(file_obj)
images_dict = theme_data.get('images', {})
# Get the reference in the manifest. theme_frame is the Chrome variant.
header_url = images_dict.get(
'headerURL', images_dict.get('theme_frame'))
try:
with zipfile.ZipFile(xpi, 'r') as source:
source.extract(header_url, dest_path)
except IOError as ioerror:
log.debug(ioerror)
@contextlib.contextmanager @contextlib.contextmanager
def atomic_lock(lock_dir, lock_name, lifetime=60): def atomic_lock(lock_dir, lock_name, lifetime=60):
"""A atomic, NFS safe implementation of a file lock. """A atomic, NFS safe implementation of a file lock.

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

@ -78,6 +78,9 @@ CLEANCSS_BIN = 'cleancss'
# Path to uglifyjs (our JS minifier). # Path to uglifyjs (our JS minifier).
UGLIFY_BIN = 'uglifyjs' # Set as None to use YUI instead (at your risk). UGLIFY_BIN = 'uglifyjs' # Set as None to use YUI instead (at your risk).
# rsvg-convert is used to save our svg static theme previews to png
RSVG_CONVERT_BIN = 'rsvg-convert'
FLIGTAR = 'amo-admins+fligtar-rip@mozilla.org' FLIGTAR = 'amo-admins+fligtar-rip@mozilla.org'
REVIEWERS_EMAIL = 'amo-editors@mozilla.org' REVIEWERS_EMAIL = 'amo-editors@mozilla.org'
THEMES_EMAIL = 'theme-reviews@mozilla.org' THEMES_EMAIL = 'theme-reviews@mozilla.org'
@ -1141,6 +1144,8 @@ CELERY_TASK_ROUTES = {
'olympia.addons.tasks.save_theme_reupload': {'queue': 'priority'}, 'olympia.addons.tasks.save_theme_reupload': {'queue': 'priority'},
'olympia.bandwagon.tasks.index_collections': {'queue': 'priority'}, 'olympia.bandwagon.tasks.index_collections': {'queue': 'priority'},
'olympia.bandwagon.tasks.unindex_collections': {'queue': 'priority'}, 'olympia.bandwagon.tasks.unindex_collections': {'queue': 'priority'},
'olympia.versions.tasks.generate_static_theme_preview': {
'queue': 'priority'},
# Other queues we prioritize below. # Other queues we prioritize below.

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

@ -199,8 +199,8 @@ class VersionView(APIView):
# channel will be ignored for new addons. # channel will be ignored for new addons.
if addon is None: if addon is None:
channel = amo.RELEASE_CHANNEL_UNLISTED # New is always unlisted. channel = amo.RELEASE_CHANNEL_UNLISTED # New is always unlisted.
addon = Addon.create_addon_from_upload_data( addon = Addon.initialize_addon_from_upload(
data=pkg, user=request.user, upload=filedata, channel=channel) data=pkg, upload=filedata, channel=channel, user=request.user)
created = True created = True
else: else:
created = False created = False

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

@ -34,6 +34,7 @@ from olympia.translations.fields import (
LinkifiedField, PurifiedField, TranslatedField, save_signal) LinkifiedField, PurifiedField, TranslatedField, save_signal)
from .compare import version_dict, version_int from .compare import version_dict, version_int
from .tasks import generate_static_theme_preview
log = olympia.core.logger.getLogger('z.versions') log = olympia.core.logger.getLogger('z.versions')
@ -206,6 +207,21 @@ class Version(OnChangeMixin, ModelBase):
if send_signal: if send_signal:
version_uploaded.send(sender=version) version_uploaded.send(sender=version)
# Generate a preview and icon for listed static themes
if (addon.type == amo.ADDON_STATICTHEME and
channel == amo.RELEASE_CHANNEL_LISTED):
from olympia.addons.models import Preview # Avoid circular ref.
dst_root = os.path.join(user_media_path('addons'), str(addon.id))
theme_data = parsed_data.get('theme', {})
version_root = os.path.join(dst_root, unicode(version.id))
utils.extract_header_img(
version.all_files[0].file_path, theme_data, version_root)
preview = Preview.objects.create(
addon=addon, caption=unicode(version.version))
generate_static_theme_preview.delay(
theme_data, version_root, preview)
# Track the time it took from first upload through validation # Track the time it took from first upload through validation
# (and whatever else) until a version was created. # (and whatever else) until a version was created.
upload_start = utc_millesecs_from_epoch(upload.created) upload_start = utc_millesecs_from_epoch(upload.created)

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

@ -0,0 +1,75 @@
import os
import StringIO
import subprocess
import tempfile
from base64 import b64encode
from django.conf import settings
from django.core.files.storage import default_storage as storage
from django.template import loader
from PIL import Image
import olympia.core.logger
from olympia import amo
from olympia.amo.celery import task
from olympia.amo.decorators import write
log = olympia.core.logger.getLogger('z.files.utils')
def write_svg_to_png(svg_content, out):
tmp_args = {'dir': settings.TMP_PATH, 'mode': 'wb', 'suffix': '.svg'}
with tempfile.NamedTemporaryFile(**tmp_args) as temporary_svg:
temporary_svg.write(svg_content)
temporary_svg.flush()
size = None
try:
if not os.path.exists(os.path.dirname(out)):
os.makedirs(out)
command = [
settings.RSVG_CONVERT_BIN,
'-o', out,
temporary_svg.name
]
subprocess.check_call(command)
size = amo.THEME_PREVIEW_SIZE
except IOError as io_error:
log.debug(io_error)
except subprocess.CalledProcessError as process_error:
log.debug(process_error)
return size
@task
@write
def generate_static_theme_preview(theme_manifest, header_root, preview):
tmpl = loader.get_template(
'devhub/addons/includes/static_theme_preview_svg.xml')
context = {'amo': amo}
context.update(theme_manifest.get('colors', {}))
header_url = theme_manifest.get('images', {}).get('headerURL')
header_path = os.path.join(header_root, header_url)
try:
with storage.open(header_path, 'rb') as header_file:
header_blob = header_file.read()
with Image.open(StringIO.StringIO(header_blob)) as header_image:
(width, height) = header_image.size
context.update(header_src_height=height)
meetOrSlice = ('meet' if width < amo.THEME_PREVIEW_SIZE.width
else 'slice')
context.update(
preserve_aspect_ratio='xMaxYMin %s' % meetOrSlice)
data_url = 'data:image/%s;base64,%s' % (
header_image.format.lower(), b64encode(header_blob))
context.update(header_src=data_url)
except IOError as io_error:
log.debug(io_error)
svg = tmpl.render(context).encode('utf-8')
size = write_svg_to_png(svg, preview.image_path)
if size:
preview.update(sizes={'image': size})

Двоичные данные
src/olympia/versions/tests/static_themes/transparent.gif Normal file

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

После

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

Двоичные данные
src/olympia/versions/tests/static_themes/weta.png Normal file

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

После

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

Двоичные данные
src/olympia/versions/tests/static_themes/weta_theme.png Normal file

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

После

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

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

После

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

Двоичные данные
src/olympia/versions/tests/static_themes/wetalong.png Normal file

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

После

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

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

@ -0,0 +1,94 @@
import math
import os
import tempfile
from base64 import b64encode
from django.conf import settings
from django.core.files.storage import default_storage as storage
import mock
import pytest
from PIL import Image, ImageChops
from olympia.addons.models import Preview
from olympia.amo.tests import addon_factory
from olympia.versions.tasks import (
generate_static_theme_preview, write_svg_to_png)
def test_write_svg_to_png():
out = tempfile.mktemp()
svg_xml = os.path.join(
settings.ROOT,
'src/olympia/versions/tests/static_themes/weta_theme.svg')
svg_png = os.path.join(
settings.ROOT,
'src/olympia/versions/tests/static_themes/weta_theme.png')
with storage.open(svg_xml, 'rb') as svgfile:
svg = svgfile.read()
write_svg_to_png(svg, out)
assert storage.exists(out)
# compare the image content. rms should be 0 but travis renders it
# different... 19 is the magic difference.
svg_png_img = Image.open(svg_png)
svg_out_img = Image.open(out)
image_diff = ImageChops.difference(svg_png_img, svg_out_img)
sum_of_squares = sum(
value * ((idx % 256) ** 2)
for idx, value in enumerate(image_diff.histogram()))
rms = math.sqrt(
sum_of_squares / float(svg_png_img.size[0] * svg_png_img.size[1]))
assert rms < 19
@pytest.mark.django_db
@mock.patch('olympia.versions.tasks.write_svg_to_png')
@pytest.mark.parametrize(
'header_url, header_height, preserve_aspect_ratio, mimetype', (
('transparent.gif', 1, 'xMaxYMin meet', 'image/gif'),
('weta.png', 200, 'xMaxYMin meet', 'image/png'),
('wetalong.png', 200, 'xMaxYMin slice', 'image/png'),
)
)
def test_generate_static_theme_preview(
write_svg_to_png, header_url, header_height, preserve_aspect_ratio,
mimetype):
write_svg_to_png.return_value = (123, 456)
theme_manifest = {
"images": {
"headerURL": header_url
},
"colors": {
"accentcolor": "#918e43",
"textcolor": "#3deb60",
"toolbar_text": "#b5ba5b",
"toolbar_field": "#cc29cc",
"toolbar_field_text": "#17747d"
}
}
header_root = os.path.join(
settings.ROOT, 'src/olympia/versions/tests/static_themes/')
addon = addon_factory()
preview = Preview.objects.create(addon=addon)
generate_static_theme_preview(theme_manifest, header_root, preview)
write_svg_to_png.assert_called()
((svg_content, png_path), _) = write_svg_to_png.call_args
assert png_path == preview.image_path
# check header is there.
assert 'width="680" height="100" xmlns="http://www.w3.org/2000/' in (
svg_content)
# check image xml is correct
image_tag = (
'<image id="svg-header-img" width="680" height="%s" '
'preserveAspectRatio="%s"' % (header_height, preserve_aspect_ratio))
assert image_tag in svg_content, svg_content
# and image content is included and was encoded
with storage.open(header_root + header_url, 'rb') as header_file:
header_blob = header_file.read()
base_64_uri = 'data:%s;base64,%s' % (mimetype, b64encode(header_blob))
assert 'xlink:href="%s"></image>' % base_64_uri in svg_content
# check each of our colors above was included
for (key, color) in theme_manifest['colors'].items():
snippet = 'class="%s" fill="%s"' % (key, color)
assert snippet in svg_content

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

@ -14,7 +14,7 @@ import requests
import olympia.core.logger import olympia.core.logger
from olympia import amo from olympia import amo
from olympia.addons.models import Addon, AddonCategory, AddonUser, Category from olympia.addons.models import Addon, AddonCategory, Category
from olympia.amo.celery import task from olympia.amo.celery import task
from olympia.amo.decorators import write from olympia.amo.decorators import write
from olympia.amo.templatetags.jinja_helpers import absolutify from olympia.amo.templatetags.jinja_helpers import absolutify
@ -220,8 +220,7 @@ def fetch_langpack(url, xpi, **kw):
u'{name!r}.'.format(**data)) u'{name!r}.'.format(**data))
addon = Addon.from_upload( addon = Addon.from_upload(
upload, [amo.PLATFORM_ALL.id], parsed_data=data) upload, [amo.PLATFORM_ALL.id], parsed_data=data, user=owner)
AddonUser(addon=addon, user=owner).save()
version = addon.versions.get() version = addon.versions.get()
if addon.default_locale.lower() == lang.lower(): if addon.default_locale.lower() == lang.lower():