Generate static theme Preview on version submit (#7475)
This commit is contained in:
Родитель
8be6a7b606
Коммит
c05e4cfdf1
|
@ -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})
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 42 B |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 124 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 60 KiB |
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
После Ширина: | Высота: | Размер: 170 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 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():
|
||||||
|
|
Загрузка…
Ссылка в новой задаче