Merge pull request #136 from seanmonstar/custom-package-properties

Custom Package Properties
This commit is contained in:
Sean McArthur 2012-03-27 10:49:47 -07:00
Родитель dcea403dff 35e618f681
Коммит c8648edb1d
11 изменённых файлов: 279 добавлений и 41 удалений

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

@ -144,6 +144,9 @@ class PackageRevision(BaseModel):
#: SDK which should be used to create the XPI
sdk = models.ForeignKey('SDK', blank=True, null=True)
#: Extra package.json properties
extra_json = models.TextField(blank=True)
class Meta:
" PackageRevision ordering and uniqueness "
ordering = ('-revision_number',)
@ -528,8 +531,12 @@ class PackageRevision(BaseModel):
name = self.name
#if not self.package.is_addon():
# name = "%s-%s" % (name, self.package.id_number)
try:
manifest = simplejson.loads(self.extra_json)
except simplejson.JSONDecodeError:
manifest = {}
manifest = {
manifest.update({
'fullName': self.full_name,
'name': name,
'description': escape(self.package.description),
@ -543,7 +550,7 @@ class PackageRevision(BaseModel):
'url': str(self.package.url),
'contributors': self.get_contributors_list(),
'lib': self.get_lib_dir()
}
})
if (self.package.is_library()
and waffle.switch_is_active(
'LibDirInMainAttributeWorkaround')):
@ -778,6 +785,24 @@ class PackageRevision(BaseModel):
return super(PackageRevision, self).save()
def set_extra_json(self, extra_json, save=True):
"""
Sets self.extra_json, adds commit message, and saves revision
by default. Pass save=False if you will save later.
raises JSONDecodeError
"""
self.add_commit_message('Extra JSON properties changed')
if extra_json:
# if not an empty string or None, just check it is
# valid JSON
simplejson.loads(extra_json)
self.extra_json = extra_json
if save:
self.save()
def validate_module_filename(self, filename):
"""
returns False if the package revision contains a module with given

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

@ -1,9 +1,9 @@
<form class="UI_Forms" id="package-info_form" method="post" accept-charset="utf-8">
<h3>Edit {full_name} info</h3>
<h3>Edit #{full_name} info</h3>
<fieldset>
<label class="UI_Field">
<span>{{ revision.package.get_type_name_with_dash()|capfirst }} Name</span>
<input type="text" name="full_name" id="full_name" class="validate-alphanum_plus_space" value="{full_name}"/>
<input type="text" name="full_name" id="full_name" class="validate-alphanum_plus_space" value="#{full_name}"/>
</label>
<label class="UI_Field">
<span>{{ revision.package.get_type_name_with_dash()|capfirst }} Description</span>
@ -15,6 +15,10 @@
{% include "_package_privacy_toggle.html" %}
</ul>
</label>
<label class="UI_Field ace_editor_container">
<span>Extra package.json Properties</span>
<textarea id="package_extra_json" name="package_extra_json" rows="8" cols="40">{{revision.extra_json}}</textarea>
</label>
</fieldset>
<div class="UI_Modal_Actions">
<ul>

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

@ -354,6 +354,47 @@ class PackageRevisionTest(TestCase):
)
self.assertRaises(FilenameExistException, first.attachment_add, att)
def test_adding_extra_package_properties(self):
addon = Package(type='a', author=self.author)
addon.save()
pk = addon.pk
rev = addon.latest
rev.set_extra_json('''
{
"preferences": [{
"name": "example",
"type": "string",
"title": "foo",
"value": "bar"
}],
"id": "baz"
}
''')
addon = Package.objects.get(pk=pk) # breaking cache
manifest = addon.latest.get_manifest()
assert 'preferences' in manifest
eq_(manifest['preferences'][0]['name'], 'example')
# user-provide values don't override our generated ones
self.assertNotEqual(manifest['id'], 'baz')
assert 'Extra JSON' in addon.latest.commit_message
def test_adding_invalid_extra_json(self):
addon = Package(type='a', author=self.author)
addon.save()
pk = addon.pk
rev = addon.latest
from simplejson import JSONDecodeError
self.assertRaises(JSONDecodeError, rev.set_extra_json, '''
{
foo: baz
}
''')
def test_add_commit_message(self):
author = User.objects.all()[0]
addon = Package(type='a', author=author)

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

@ -236,6 +236,13 @@ class TestEditing(TestCase):
def setUp(self):
self.hashtag = hashtag()
def _login(self):
self.author = User.objects.get(username='jan')
self.author.set_password('test')
self.author.save()
self.client.login(username=self.author.username, password='test')
return self.author
def test_revision_list_contains_added_modules(self):
author = User.objects.get(username='john')
addon = Package(author=author, type='a')
@ -251,13 +258,10 @@ class TestEditing(TestCase):
assert 'test_filename' in r.content
def test_package_name_change(self):
author = User.objects.get(username='jan')
author.set_password('secure')
author.save()
author = self._login()
addon1 = Package(author=author, type='a')
addon1.save()
rev1 = addon1.latest
self.client.login(username=author.username, password='secure')
response = self.client.post(addon1.latest.get_save_url(), {
'full_name': 'FULL NAME'})
eq_(response.status_code, 200)
@ -266,6 +270,51 @@ class TestEditing(TestCase):
eq_(addon2.full_name, addon2.latest.full_name)
assert rev1.name != addon2.latest.name
def test_package_extra_json_change(self):
author = self._login()
addon = Package(author=author, type='a')
addon.save()
pk = addon.pk
homepage = 'https://builder.addons.mozilla.org'
extra_json = '{ "homepage": "%s" }' % homepage
response = self.client.post(addon.latest.get_save_url(), {
'package_extra_json': extra_json})
addon = Package.objects.get(pk=pk) # old one is cached
eq_(addon.latest.extra_json, extra_json)
def test_package_remove_extra_json(self):
author = self._login()
addon = Package(author=author, type='a')
addon.save()
pk = addon.pk
homepage = 'https://builder.addons.mozilla.org'
extra_json = '{ "homepage": "%s" }' % homepage
addon.latest.extra_json = extra_json
addon.latest.save()
response = self.client.post(addon.latest.get_save_url(), {
'package_extra_json': ''})
addon = Package.objects.get(pk=pk) # old on is cached
eq_(addon.latest.extra_json, '')
def test_package_invalid_extra_json(self):
author = self._login()
addon = Package(author=author, type='a')
addon.save()
extra_json = '{ foo: bar }'
response = self.client.post(addon.latest.get_save_url(), {
'package_extra_json': extra_json})
eq_(response.status_code, 400)
assert 'invalid JSON' in response.content
class TestRevision(TestCase):
fixtures = ('mozilla_user', 'core_sdk', 'users', 'packages')

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

@ -7,6 +7,7 @@ import shutil
import codecs
import tempfile
import urllib2
from simplejson import JSONDecodeError
from django.contrib import messages
from django.core.urlresolvers import reverse
@ -900,6 +901,19 @@ def save(request, id_number, type_id, revision_number=None,
revision.package.description = package_description
response_data['package_description'] = package_description
extra_json = request.POST.get('package_extra_json')
if extra_json is not None:
# None means it wasn't submitted. We want to accept blank strings.
save_revision = True
log.debug('extra_json: %s' % extra_json)
try:
revision.set_extra_json(extra_json, save=False)
except JSONDecodeError:
return HttpResponseBadRequest(
'Extra package properties were invalid JSON.')
response_data['package_extra_json'] = extra_json
changes = []
for mod in revision.modules.all():
if request.POST.get(mod.filename, False):

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

@ -287,3 +287,25 @@ authors:
width:69%;
padding:0 5px;
}
.UI_Modal .UI_Field.ace_editor_container {
position:relative;
}
.UI_Modal .UI_Field.ace_editor_container .ace_editor {
height:100px;
position:relative;
float:right;
width: 70%;
}
.UI_Modal .UI_Field.ace_editor_container .ace_editor .ace_gutter {
width:15px;
}
.UI_Modal .UI_Field.ace_editor_container textarea {
width:10px;
height:30px;
float:none;
padding:0;
}

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

@ -18,6 +18,7 @@ var Class = require('shipyard/class/Class'),
FloatingTips = require('../views/FloatingTips'),
Validator = require('../views/Validator'),
JSONValidator = require('../views/JSONValidator'),
//TODO: this is bad practice
settings = dom.window.get('settings');
@ -48,7 +49,11 @@ module.exports = new Class({
save_el: 'package-save',
menu_el: 'UI_Editor_Menu',
package_info_form_elements: ['full_name', 'package_description'],
package_info_form_elements: [
'full_name',
'package_description',
'package_extra_json'
],
check_dependencies: true,
check_if_latest: true // switch to false if displaying revisions
@ -1328,7 +1333,8 @@ module.exports = new Class({
this.savenow = false;
var modal = fd().editPackageInfoModal = fd().displayModal(
string.substitute(settings.edit_package_info_template,
object.merge({}, this.data, this.options)));
object.merge({}, this.data, this.options),
/#\{([^{}]+)\}/g));
modal.addListener('destroy', function() {
delete fd().editPackageInfoModal;
@ -1339,6 +1345,8 @@ module.exports = new Class({
dom.$('package_description').addListener('change', function() {
fd().emit('change');
});
var savenow = dom.$('savenow');
if (savenow) {
savenow.addListener('click', function() {
@ -1362,16 +1370,31 @@ module.exports = new Class({
pressedBtn.addClass('pressed').getElement('a').addClass('inactive');
notPressedBtn.removeClass('pressed').getElement('a').removeClass('inactive');
var validator = new Validator('full_name', {
var fullNameValidator = new Validator('full_name', {
pattern: /^[A-Za-z0-9\s\-_\.\(\)]*$/,
message: 'Please use only letters, numbers, spaces, or "_().-" in this field.'
});
var jsonValidator = new JSONValidator('package_extra_json', {
message: 'Must be blank or a valid JSON object.'
});
var package_extra_json_el = dom.$('package_extra_json');
package_extra_json_el.store('json-validator', jsonValidator);
if ('package_extra_json' in this.options) {
package_extra_json_el.set('value', this.options.package_extra_json);
}
this._setupExtraPropertiesEditor();
function isValid() {
return fullNameValidator.validate() && jsonValidator.validate();
}
dom.$('package-info_form').addListener('submit', function(e) {
e.stop();
if (validator.validate()) {
if (isValid()) {
controller.submitInfo();
} else {
log.debug('Form field full_name field has invalid characters.');
log.debug('Invalid name or JSON.');
}
});
@ -1384,6 +1407,34 @@ module.exports = new Class({
});
},
_setupExtraPropertiesEditor: function() {
// Make package.json textarea an ACE editor
var ace = require('ace/ace');
var extra_json_el = dom.$('package_extra_json');
extra_json_el.setStyle('display', 'none');
var extra_json_editor_el = new dom.Element('div', {
id: 'extra_json_ace',
text: extra_json_el.get('value')
});
extra_json_editor_el.inject(extra_json_el, 'before');
var editor = ace.edit(extra_json_editor_el.getNode());
var editorSession = editor.getSession();
editorSession.on('change', function() {
extra_json_el.set('value', editorSession.getValue());
fd().emit('change');
});
var validator = extra_json_el.retrieve('json-validator');
editor.on('blur', function() {
if (!validator.validate()) {
validator.show();
} else {
validator.hide();
}
});
},
/*
* Method: submitInfo
* submit info from EditInfoModalWindow

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

@ -31,6 +31,7 @@ var Package = module.exports = new Class({
revision_number: fields.NumberField(),
view_url: fields.TextField(),
active: fields.BooleanField(),
extra_json: fields.TextField(),
latest: fields.NumberField() // a FK to PackageRevision

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

@ -0,0 +1,23 @@
var Class = require('shipyard/class/Class'),
typeOf = require('shipyard/utils/type').typeOf,
Validator = require('./Validator');
module.exports = new Class({
Extends: Validator,
validate: function validate() {
var text = this.target.get('value').trim();
if (text) {
try {
var json = JSON.parse(text);
// number, string, array, etc are illegal.
return typeOf(json) === 'object';
} catch (jsonError) {
return false;
}
}
return true;
}
});

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

@ -10,7 +10,12 @@ module.exports = new Class({
options: {
pattern: /.*/,
message: 'Illegal characters found.'
messageTarget: null,
message: 'Illegal characters found.',
messageStyles: {
'visibility': 'visible',
'position': 'static'
}
},
initialize: function Validator(element, options) {
@ -42,37 +47,39 @@ module.exports = new Class({
return this.options.pattern.test(value);
},
createElement: function() {
var messageTarget = dom.$(this.getOption('messageTarget')) || this.target;
this.element = new dom.Element('div', {
'class': 'validation-advice',
'text': this.getOption('message'),
'styles': {
'height': 0,
'display': 'block',
'overflow': 'hidden',
'opacity': 0
}
});
//measure the height
this.element.setStyles({
'visibility': 'hidden',
'position': 'absolute'
});
this.element.inject(messageTarget, 'after');
this.height = this.element.getHeight();
this.element.dispose().setStyles(this.getOption('messageStyles'));
this.anim = new Anim(this.element, {
transition: Sine
});
},
show: function show() {
var validator = this;
if (!this.element) {
this.element = new dom.Element('div', {
'class': 'validation-advice',
'text': this.getOption('message'),
'styles': {
'height': 0,
'display': 'block',
'overflow': 'hidden',
'opacity': 0
}
});
//measure the height
this.element.setStyles({
'visibility': 'hidden',
'position': 'absolute'
});
this.element.inject(validator.target, 'after');
this.height = this.element.getHeight();
this.element.dispose().setStyles({
visibility: 'visible',
position: 'static'
});
this.anim = new Anim(this.element, {
transition: Sine
});
}
this.createElement();
}
this.anim.once('start', function() {
validator.element.inject(validator.target, 'after');

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

@ -0,0 +1 @@
ALTER TABLE `jetpack_packagerevision` ADD COLUMN `extra_json` TEXT;