preload theme wizard with existing properties (#9644)

This commit is contained in:
Andrew Williamson 2018-10-15 16:14:49 +01:00 коммит произвёл GitHub
Родитель 1cbaf538b0
Коммит 6c8ab97032
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
11 изменённых файлов: 297 добавлений и 48 удалений

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

@ -9,6 +9,18 @@
{% block primary %}
<h3>{{ _('Theme generator') }}</h3>
<div id="theme-wizard" data-version="{{ version_number }}">
{% if unsupported_properties %}
<div class="notification-box error">
{{ _('Warning: the following manifest properties that your most recent version '
"upload used in it's manifest are unsupported in this wizard and will be ignored:") }}
<ul class="note">
{% for prop in unsupported_properties %}
<li>{{ prop }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<div>
<h3>{{ _('Theme name') }}<span class="req" title="{{ _('required') }}">*</span></h3>
{% if addon %}
@ -19,7 +31,7 @@
<input type="text" id="theme-name"/>
{% endif %}
</div>
<div id="theme-header" class="row">
<div id="theme-header" class="row" data-existing-header="{{ existing_properties.get('images', {}).get('headerURL','') }}">
<label class="row" for="header-img">
<h3>{{ _('Select a header image for your theme') }}<span class="req" title="{{ _('required') }}">*</span></h3>
</label>
@ -38,18 +50,11 @@
</div>
<div class="colors">
<h3>{{ _('Select colors for your theme') }}</h3>
{% set colors = [
('accentcolor', _('Header area background'), _('The color of the header area background, displayed in the part of the header not covered or visible through the header image. Manifest field: accentcolor.'), 'rgba(229,230,232,1)'),
('textcolor', _('Header area text and icons'), _('The color of the text and icons in the header area, except the active tab. Manifest field: textcolor.'), 'rgba(0,0,0,1'),
('toolbar', _('Toolbar area background'), _('The background color for the navigation bar, the bookmarks bar, and the selected tab. Manifest field: toolbar.'), false),
('toolbar_text', _('Toolbar area text and icons'), _('The color of the text and icons in the toolbar and the active tab. Manifest field: toolbar_text.'), false),
('toolbar_field', _('Toolbar field area background'), _('The background color for fields in the toolbar, such as the URL bar. Manifest field: toolbar_field.'), false),
('toolbar_field_text', _('Toolbar field area text'), _('The color of text in fields in the toolbar, such as the URL bar. Manifest field: toolbar_field_text.'), false)] %}
<ul class="colors">
{% set property_list = ['textcolor','toolbar_text', 'toolbar_field_text'] %}
{% set property_list_left = ['textcolor','toolbar_text', 'toolbar_field_text'] %}
{% set existing_colors = existing_properties['colors'] or {} %}
{% for (property, label, tip, val_default) in colors %}
{% if property in property_list %}
{% if property in property_list_left %}
<li class="row left">
{% else %}
<li class="row">
@ -65,8 +70,10 @@
<span class="tip tooltip" title="{{ tip }}" data-oldtitle="">?</span>
</span>
</label>
<input class="color-picker" id="{{ property }}" name="{{ property }}"
type="text"{{ ('value=' + val_default + '') if val_default else '' }}>
{% with value = existing_colors.get(property, val_default) %}
<input class="color-picker" id="{{ property }}" name="{{ property }}"
type="text"{{ (' value=' + value) if value else '' }}>
{% endwith %}
</li>
{% endfor %}
</ul>

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

@ -5,12 +5,14 @@ from django.conf import settings
from django.test.utils import override_settings
import mock
import pytest
from waffle.testutils import override_switch
from celery.result import AsyncResult
from six import text_type
from olympia import amo
from olympia.amo.storage_utils import copy_stored_file
from olympia.amo.tests import (
addon_factory, TestCase, user_factory, version_factory)
from olympia.devhub import tasks, utils
@ -414,3 +416,49 @@ class TestGetAddonAkismetReports(TestCase):
property_name='description', property_value=u'lé foo',
user_agent=user_agent, referrer=referrer)]
self.create_for_addon_mock.assert_has_calls(calls, any_order=True)
@pytest.mark.django_db
def test_extract_theme_properties():
addon = addon_factory(type=amo.ADDON_STATICTHEME)
result = utils.extract_theme_properties(
addon, addon.current_version.channel)
assert result == {} # There's no file, but it be should safely handled.
# Add the zip in the right place
zip_file = os.path.join(
settings.ROOT, 'src/olympia/devhub/tests/addons/static_theme.zip')
copy_stored_file(zip_file, addon.current_version.all_files[0].file_path)
result = utils.extract_theme_properties(
addon, addon.current_version.channel)
assert result['colors'] == {
"accentcolor": "#adb09f",
"textcolor": "#000"
}
assert result['images'] == {
"headerURL": '%s%s//%s/%s/%s' % (
settings.MEDIA_URL, 'addons', text_type(addon.id),
text_type(addon.current_version.id), 'weta.png')
}
@pytest.mark.django_db
def test_wizard_unsupported_properties():
data = {
'colors': {
'foo': '#111111',
'baa': '#222222',
'extracolor': 'rgb(1,2,3,0)',
},
'images': {
'headerURL': 'png.png',
'additionalBackground': 'somethingelse.png',
},
'extrathing': {
'doesnt': 'matter',
},
}
fields = ['foo', 'baa']
properties = utils.wizard_unsupported_properties(
data, fields)
assert properties == ['extrathing', 'extracolor', 'additionalBackground']

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

@ -1590,10 +1590,21 @@ class VersionSubmitUploadMixin(object):
channel = ('listed' if self.channel == amo.RELEASE_CHANNEL_LISTED else
'unlisted')
self.addon.update(type=amo.ADDON_STATICTHEME)
# Check we get the correct template.
# Get the correct template.
self.url = reverse('devhub.submit.version.wizard',
args=[self.addon.slug, channel])
response = self.client.get(self.url)
mock_point = 'olympia.devhub.views.extract_theme_properties'
with mock.patch(mock_point) as extract_theme_properties_mock:
extract_theme_properties_mock.return_value = {
'colors': {
'accentcolor': '#123456',
'textcolor': 'rgba(1,2,3,0.4)',
},
'images': {
'headerURL': 'header.png',
}
}
response = self.client.get(self.url)
assert response.status_code == 200
doc = pq(response.content)
assert doc('#theme-wizard')
@ -1601,6 +1612,77 @@ class VersionSubmitUploadMixin(object):
assert doc('input#theme-name').attr('type') == 'hidden'
assert doc('input#theme-name').attr('value') == (
unicode(self.addon.name))
# Existing colors should be the default values for the fields
assert doc('#accentcolor').attr('value') == '#123456'
assert doc('#textcolor').attr('value') == 'rgba(1,2,3,0.4)'
# And the theme header url is there for the JS to load
assert doc('#theme-header').attr('data-existing-header') == (
'header.png')
# No warning about extra properties
assert 'are unsupported in this wizard' not in response.content
# And then check the upload works.
path = os.path.join(
settings.ROOT, 'src/olympia/devhub/tests/addons/static_theme.zip')
self.upload = self.get_upload(abspath=path)
response = self.post()
version = self.addon.find_latest_version(channel=self.channel)
assert version.channel == self.channel
assert version.all_files[0].status == (
amo.STATUS_AWAITING_REVIEW
if self.channel == amo.RELEASE_CHANNEL_LISTED else
amo.STATUS_PUBLIC)
self.assert3xx(response, self.get_next_url(version))
log_items = ActivityLog.objects.for_addons(self.addon)
assert log_items.filter(action=amo.LOG.ADD_VERSION.id)
if self.channel == amo.RELEASE_CHANNEL_LISTED:
previews = list(version.previews.all())
assert len(previews) == 3
assert storage.exists(previews[0].image_path)
assert storage.exists(previews[1].image_path)
assert storage.exists(previews[1].image_path)
else:
assert version.previews.all().count() == 0
def test_static_theme_wizard_unsupported_properties(self):
channel = ('listed' if self.channel == amo.RELEASE_CHANNEL_LISTED else
'unlisted')
self.addon.update(type=amo.ADDON_STATICTHEME)
# Get the correct template.
self.url = reverse('devhub.submit.version.wizard',
args=[self.addon.slug, channel])
mock_point = 'olympia.devhub.views.extract_theme_properties'
with mock.patch(mock_point) as extract_theme_properties_mock:
extract_theme_properties_mock.return_value = {
'colors': {
'accentcolor': '#123456',
'textcolor': 'rgba(1,2,3,0.4)',
'tab_line': '#123',
},
'images': {
'additional_backgrounds': [],
},
'something_extra': {},
}
response = self.client.get(self.url)
assert response.status_code == 200
doc = pq(response.content)
assert doc('#theme-wizard')
assert doc('#theme-wizard').attr('data-version') == '3.0'
assert doc('input#theme-name').attr('type') == 'hidden'
assert doc('input#theme-name').attr('value') == (
unicode(self.addon.name))
# Existing colors should be the default values for the fields
assert doc('#accentcolor').attr('value') == '#123456'
assert doc('#textcolor').attr('value') == 'rgba(1,2,3,0.4)'
# Warning about extra properties this time:
assert 'are unsupported in this wizard' in response.content
unsupported_list = doc('.notification-box.error ul.note li')
assert unsupported_list.length == 3
assert 'tab_line' in unsupported_list.text()
assert 'additional_backgrounds' in unsupported_list.text()
assert 'something_extra' in unsupported_list.text()
# And then check the upload works.
path = os.path.join(

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

@ -11,15 +11,17 @@ from six import text_type
import olympia.core.logger
from olympia import amo
from olympia import amo, core
from olympia.addons.models import Addon
from olympia.amo.templatetags.jinja_helpers import user_media_url
from olympia.amo.urlresolvers import linkify_escape
from olympia.files.models import File, FileUpload
from olympia.files.utils import parse_addon
from olympia.files.utils import parse_addon, parse_xpi
from olympia.lib.akismet.models import AkismetReport
from olympia.tags.models import Tag
from olympia.translations.models import Translation
from olympia.versions.compare import version_int
from olympia.versions.utils import process_color_value
from . import tasks
@ -357,3 +359,45 @@ def get_addon_akismet_reports(user, user_agent, referrer, upload=None,
referrer=referrer)
reports.append((prop, report))
return reports
def extract_theme_properties(addon, channel):
version = addon.find_latest_version(channel)
if not version or not version.all_files:
return {}
try:
parsed_data = parse_xpi(
version.all_files[0].file_path, addon=addon, user=core.get_user())
except ValidationError:
# If we can't parse the existing manifest safely return.
return {}
theme_props = parsed_data.get('theme', {})
# pre-process colors to convert chrome style colors and strip spaces
theme_props['colors'] = dict(
process_color_value(prop, color)
for prop, color in theme_props.get('colors', {}).items())
# replace headerURL with path to existing background
if 'images' in theme_props:
if 'theme_frame' in theme_props['images']:
header_url = theme_props['images'].pop('theme_frame')
if 'headerURL' in theme_props['images']:
header_url = theme_props['images'].pop('headerURL')
if header_url:
theme_props['images']['headerURL'] = '/'.join((
user_media_url('addons'), text_type(addon.id),
text_type(version.id), header_url))
return theme_props
def wizard_unsupported_properties(data, wizard_fields):
# collect any 'theme' level unsupported properties
unsupported = [
key for key in data.keys() if key not in ['colors', 'images']]
# and any unsupported 'colors' properties
unsupported += [
key for key in data.get('colors', {}) if key not in wizard_fields]
# and finally any 'images' properties (wizard only supports the background)
unsupported += [
key for key in data.get('images', {}) if key != 'headerURL']
return unsupported

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

@ -45,8 +45,10 @@ from olympia.devhub.forms import (
AgreementForm, CheckCompatibilityForm, SourceForm)
from olympia.devhub.models import BlogPost, RssKey
from olympia.devhub.utils import (
add_dynamic_theme_tag, fetch_existing_translations_from_addon,
get_addon_akismet_reports, process_validation)
add_dynamic_theme_tag, extract_theme_properties,
fetch_existing_translations_from_addon,
get_addon_akismet_reports, process_validation,
wizard_unsupported_properties)
from olympia.files.models import File, FileUpload, FileValidation
from olympia.files.utils import parse_addon
from olympia.lib.crypto.packaged import sign_file
@ -1327,6 +1329,41 @@ def submit_version_distribution(request, addon_id, addon):
return _submit_distribution(request, addon, 'devhub.submit.version.upload')
WIZARD_COLOR_FIELDS = [
('accentcolor',
_(u'Header area background'),
_(u'The color of the header area background, displayed in the part of '
u'the header not covered or visible through the header image. Manifest '
u'field: accentcolor.'),
'rgba(229,230,232,1)'),
('textcolor',
_(u'Header area text and icons'),
_(u'The color of the text and icons in the header area, except the '
u'active tab. Manifest field: textcolor.'),
'rgba(0,0,0,1'),
('toolbar',
_(u'Toolbar area background'),
_(u'The background color for the navigation bar, the bookmarks bar, and '
u'the selected tab. Manifest field: toolbar.'),
False),
('toolbar_text',
_(u'Toolbar area text and icons'),
_(u'The color of the text and icons in the toolbar and the active tab. '
u'Manifest field: toolbar_text.'),
False),
('toolbar_field',
_(u'Toolbar field area background'),
_(u'The background color for fields in the toolbar, such as the URL bar. '
u'Manifest field: toolbar_field.'),
False),
('toolbar_field_text',
_(u'Toolbar field area text'),
_(u'The color of text in fields in the toolbar, such as the URL bar. '
u'Manifest field: toolbar_field_text.'),
False)
]
@transaction.atomic
def _submit_upload(request, addon, channel, next_view, wizard=False):
""" If this is a new addon upload `addon` will be None.
@ -1381,6 +1418,14 @@ def _submit_upload(request, addon, channel, next_view, wizard=False):
submit_page = 'version' if addon else 'addon'
template = ('devhub/addons/submit/upload.html' if not wizard else
'devhub/addons/submit/wizard.html')
existing_properties = (
extract_theme_properties(addon, channel)
if wizard and addon else {})
unsupported_properties = (
wizard_unsupported_properties(
existing_properties,
[field for field, _, _, _ in WIZARD_COLOR_FIELDS])
if existing_properties else [])
return render(request, template,
{'new_addon_form': form,
'is_admin': is_admin,
@ -1390,6 +1435,9 @@ def _submit_upload(request, addon, channel, next_view, wizard=False):
'submit_page': submit_page,
'channel': channel,
'channel_choice_text': channel_choice_text,
'existing_properties': existing_properties,
'colors': WIZARD_COLOR_FIELDS,
'unsupported_properties': unsupported_properties,
'version_number':
get_next_version_number(addon) if wizard else None})

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

@ -19,9 +19,9 @@ from .utils import (
def _build_static_theme_preview_context(theme_manifest, header_root):
# First build the context shared by both the main preview and the thumb
context = {'amo': amo}
context.update(
{process_color_value(prop, color)
for prop, color in theme_manifest.get('colors', {}).items()})
context.update(dict(
process_color_value(prop, color)
for prop, color in theme_manifest.get('colors', {}).items()))
images_dict = theme_manifest.get('images', {})
header_url = images_dict.get(
'headerURL', images_dict.get('theme_frame', ''))

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

@ -210,7 +210,7 @@ def test_generate_static_theme_preview_with_chrome_properties(
}
for (chrome_prop, firefox_prop) in chrome_colors.items():
color_list = theme_manifest['colors'][chrome_prop]
color = 'rgb(%s, %s, %s)' % tuple(color_list)
color = 'rgb(%s,%s,%s)' % tuple(color_list)
colors.append('class="%s" fill="%s"' % (firefox_prop, color))
header_svg = write_svg_to_png_mock.call_args_list[0][0][0]

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

@ -127,10 +127,11 @@ def test_additional_background(
@pytest.mark.parametrize(
'chrome_prop, chrome_color, firefox_prop, css_color', (
('bookmark_text', [2, 3, 4], 'toolbar_text', u'rgb(2, 3, 4)'),
('frame', [12, 13, 14], 'accentcolor', u'rgb(12, 13, 14)'),
('frame_inactive', [22, 23, 24], 'accentcolor', u'rgb(22, 23, 24)'),
('tab_background_text', [32, 33, 34], 'textcolor', u'rgb(32, 33, 34)'),
('bookmark_text', [2, 3, 4], 'toolbar_text', u'rgb(2,3,4)'),
('frame', [12, 13, 14], 'accentcolor', u'rgb(12,13,14)'),
('frame_inactive', [22, 23, 24], 'accentcolor', u'rgb(22,23,24)'),
('tab_background_text', [32, 33, 34], 'textcolor', u'rgb(32,33,34)'),
('accentcolor', u'rgb(32, 33, 34)', 'accentcolor', u'rgb(32,33,34)'),
)
)
def test_process_color_value(chrome_prop, chrome_color, firefox_prop,

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

@ -119,5 +119,6 @@ CHROME_COLOR_TO_CSS = {
def process_color_value(prop, value):
prop = CHROME_COLOR_TO_CSS.get(prop, prop)
if isinstance(value, list) and len(value) == 3:
return prop, u'rgb(%s, %s, %s)' % tuple(value)
return prop, unicode(value)
return prop, u'rgb(%s,%s,%s)' % tuple(value)
# strip out spaces because jquery.minicolors chokes on them
return prop, unicode(value).replace(' ', '')

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

@ -6,6 +6,7 @@
.note {
display: block;
padding-top: 2px;
margin-bottom: 0px;
li {
display: inline-block;
font-size: 0.9em;
@ -107,4 +108,4 @@
.html-rtl .button.upload.uploading {
background-position: 5% center;
}
}
}

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

@ -6,6 +6,13 @@ $(document).ready(function() {
function initThemeWizard() {
var $wizard = $(this);
var preLoadBlob = null;
function getFile() {
file_selector = $wizard.find('#header-img')[0];
file = file_selector.files[0];
return file ? file : preLoadBlob;
}
$wizard.on('click', '.reset', _pd(function() {
var $this = $(this),
@ -16,25 +23,24 @@ $(document).ready(function() {
$wizard.on('change', 'input[type="file"]', function() {
var $row = $(this).closest('.row');
var reader = new FileReader(),
file = getFile($row.find('input[type=file]'));
file = getFile();
if (!file) return; // don't do anything if no file selected.
$row.find('input[type=file], .note').hide();
var $preview_img = $row.find('.preview');
reader.onload = function(e) {
$preview_img.attr('src', e.target.result);
$preview_img.show().addClass('loaded');
$row.find('.reset').show().css('display', 'block');
updateManifest();
$row.find('input[type=file], .note').hide();
var filename = file.name.replace(/\.[^/.]+$/, "");
$wizard.find('a.download').attr('download', filename + ".zip");
var name_input = $wizard.find('#theme-name');
if (!name_input.val()) {
name_input.val(filename);
}
};
reader.readAsDataURL(file);
var filename = file.name.replace(/\.[^/.]+$/, "");
$wizard.find('a.download').attr('download', filename + ".zip");
var name_input = $wizard.find('#theme-name');
if (!name_input.val()) {
name_input.val(filename);
}
});
$wizard.find('input[type="file"]').trigger('change');
@ -47,6 +53,22 @@ $(document).ready(function() {
$svg_img.attr('preserveAspectRatio', 'xMaxYMin '+ meetOrSlice);
});
$wizard.find('#theme-header').each(function(index, element) {
var img_src = $(element).data('existing-header');
// If we already have a preview from a selected file don't overwrite it.
if (getFile() || !img_src) return;
var xhr = new XMLHttpRequest();
xhr.open("GET", img_src);
xhr.responseType = "blob";
// load the image as a blob so we can treat it as a File
xhr.onload = function() {
preLoadBlob = xhr.response;
preLoadBlob.name = img_src.split('/').slice(-1)[0];
$wizard.find('input[type="file"]').trigger('change');
};
xhr.send();
});
function updateManifest() {
textarea = $wizard.find('#manifest').val(generateManifest());
toggleSubmitIfNeeded();
@ -56,13 +78,8 @@ $(document).ready(function() {
$wizard.find('button.upload').attr('disabled', ! required_fields_present());
}
function getFile($input) {
file_selector = $input[0];
return file_selector.files[0];
}
function generateManifest() {
var headerFile = getFile($wizard.find('#header-img')),
var headerFile = getFile(),
headerURL = headerFile ? headerFile.name : "";
function colVal(id) {
@ -96,7 +113,7 @@ $(document).ready(function() {
function buildZip() {
var zip = new JSZip();
zip.file('manifest.json', generateManifest());
var header_img = getFile($wizard.find('#header-img'));
var header_img = getFile();
if (header_img) {
zip.file(header_img.name, header_img);
}
@ -203,7 +220,7 @@ $(document).ready(function() {
function required_fields_present() {
return $wizard.find('#theme-name').val() !== "" &&
$wizard.find('#header-img')[0].files.length > 0 &&
getFile() &&
$wizard.find('#accentcolor').val() !== "" &&
$wizard.find('#textcolor').val() !== "";
}