add-on dependencies front-end (bug 678650)

This commit is contained in:
Chris Van 2011-08-30 17:28:36 -07:00
Родитель ef871b02a6
Коммит eefbe4fad6
18 изменённых файлов: 615 добавлений и 82 удалений

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

@ -112,6 +112,17 @@ ADDON_TYPES = {
ADDON_WEBAPP: _(u'Apps'),
}
# Searchable Add-on Types
ADDON_SEARCH_TYPES = [
ADDON_ANY,
ADDON_EXTENSION,
ADDON_THEME,
ADDON_DICT,
ADDON_SEARCH,
ADDON_LPAPP,
ADDON_PERSONA,
]
# Icons
ADDON_ICONS = {
ADDON_ANY: 'default-addon.png',

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

@ -17,8 +17,8 @@ from quieter_formset.formset import BaseModelFormSet
import amo
import addons.forms
import paypal
from addons.models import (Addon, AddonUpsell, AddonUser, BlacklistedSlug,
Charity, Preview)
from addons.models import (Addon, AddonDependency, AddonUpsell, AddonUser,
BlacklistedSlug, Charity, Preview)
from amo.forms import AMOModelForm
from amo.urlresolvers import reverse
from amo.utils import slug_validator
@ -934,3 +934,25 @@ class PremiumForm(happyforms.Form):
upsell.save()
elif not self.cleaned_data['do_upsell'] and upsell:
upsell.delete()
def DependencyFormSet(*args, **kw):
addon_parent = kw.pop('addon')
class _Form(happyforms.ModelForm):
addon = forms.CharField(required=False, widget=forms.HiddenInput)
dependent_addon = forms.ModelChoiceField(
Addon.objects.public().exclude(Q(id=addon_parent.id) |
Q(type=amo.ADDON_PERSONA)),
widget=forms.HiddenInput)
class Meta:
model = AddonDependency
fields = ('addon', 'dependent_addon')
def clean_addon(self):
return addon_parent
FormSet = modelformset_factory(AddonDependency, form=_Form,
extra=0, can_delete=True)
return FormSet(*args, **kw)

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

@ -38,14 +38,26 @@
{% endif %}
</td>
</tr>
{# bug 620431
<tr>
<th>{{ _('Required Add-ons') }}</th>
<td>
<strong>{{ _('Coming Soon') }}</strong>
</td>
#}
</tr>
{% if waffle.flag('edit-dependencies') %}
<tr>
<th>{{ _('Required Add-ons') }}</th>
<td id="required-addons" data-src="{{ url('search.ajax') }}">
{% if editable %}
{% include "devhub/addons/edit/technical_dependencies.html" %}
{% else %}
{% set deps = addon.all_dependencies %}
{% call empty_unless(deps) %}
<ul>
{% for d in deps %}
<li><a href="{{ d.get_url_path() }}" target="_blank">
{{ d.name }}</a></li>
{% endfor %}
</ul>
{% endcall %}
{% endif %}
</td>
</tr>
{% endif %}
<tr>
<th>
{{ tip(_("Add-on flags"),

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

@ -0,0 +1,41 @@
{% macro dp(form) %}
<li {% if form.initial %}
data-addonid="{{ form.instance.dependent_addon.id }}"
{% endif %}>
<label class="js-hidden">
{{ form.DELETE.label }} {{ form.DELETE }}
</label>
<a href="#" class="remove"
title="{{ _('Remove this dependent add-on') }}">x</a>
{% if form.initial %}
{% with dep = form.instance.dependent_addon %}
<div style="background-image:url({{ dep.icon_url }})">
<a href="{{ dep.get_url_path() }}" target="_blank">{{ dep.name }}</a>
</div>
{% endwith %}
{{ form.id }}
{% else %}
<div style="background-image:url({{ '{icon}' }})">
<a href="{{ '{url}' }}" target="_blank">{{ '{name}' }}</a>
</div>
{% endif %}
{{ form.non_field_errors() }}
{{ form.dependent_addon }}
{{ form.dependent_addon.errors }}
{{ form.DELETE.errors }}
</li>
{% endmacro %}
{{ dependency_form.management_form }}
{{ dependency_form.non_form_errors() }}
<ul class="dependencies">
{% for form in dependency_form.initial_forms %}
{{ dp(form) }}
{% endfor %}
</ul>
<ul class="extra-form">
{{ dp(dependency_form.empty_form) }}
</ul>
<input type="text" class="autocomplete"
placeholder="{{ _('Enter the name of an add-on') }}">

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

@ -10,6 +10,7 @@ import mock
from nose.tools import eq_
from PIL import Image
from pyquery import PyQuery as pq
from waffle.models import Flag
import amo
import amo.tests
@ -17,7 +18,8 @@ from amo.tests import formset, initial
from amo.tests.test_helpers import get_image_path
from amo.urlresolvers import reverse
from addons.forms import AddonFormBasic
from addons.models import Addon, AddonCategory, AddonUser, Category
from addons.models import (Addon, AddonCategory, AddonDependency, AddonUser,
Category)
from bandwagon.models import Collection, CollectionAddon, FeaturedCollection
from devhub.models import ActivityLog
from tags.models import Tag, AddonTag
@ -25,8 +27,8 @@ from users.models import UserProfile
class TestEdit(amo.tests.TestCase):
fixtures = ('base/apps', 'base/users', 'base/addon_3615',
'base/addon_5579', 'base/addon_3615_categories')
fixtures = ['base/apps', 'base/users', 'base/addon_3615',
'base/addon_5579', 'base/addon_3615_categories']
def setUp(self):
super(TestEdit, self).setUp()
@ -1009,17 +1011,35 @@ class TestEditSupport(TestEdit):
class TestEditTechnical(TestEdit):
fixtures = TestEdit.fixtures + ['base/addon_5299_gcal', 'base/addon_40',
'addons/persona']
def setUp(self):
super(TestEditTechnical, self).setUp()
self.technical_url = self.get_url('technical', edit=True)
self.technical_edit_url = self.get_url('technical')
self.dependent_addon = Addon.objects.get(id=5579)
Flag.objects.create(name='edit-dependencies', everyone=True)
AddonDependency.objects.create(addon=self.addon,
dependent_addon=self.dependent_addon)
self.technical_url = self.get_url('technical')
self.technical_edit_url = self.get_url('technical', edit=True)
ctx = self.client.get(self.technical_edit_url).context
self.dep = initial(ctx['dependency_form'].initial_forms[0])
self.dep_initial = formset(self.dep, prefix='dependencies',
initial_count=1)
def dep_formset(self, *args, **kw):
kw.setdefault('initial_count', 1)
kw.setdefault('prefix', 'dependencies')
return formset(self.dep, *args, **kw)
def formset(self, data):
return self.dep_formset(**data)
def test_log(self):
data = {'developer_comments': 'This is a test'}
data = self.formset({'developer_comments': 'This is a test'})
o = ActivityLog.objects
eq_(o.count(), 0)
r = self.client.post(self.technical_url, data)
r = self.client.post(self.technical_edit_url, data)
eq_(r.context['form'].errors, {})
eq_(o.filter(action=amo.LOG.EDIT_PROPERTIES.id).count(), 1)
@ -1031,7 +1051,7 @@ class TestEditTechnical(TestEdit):
site_specific='on',
view_source='on')
r = self.client.post(self.technical_url, data)
r = self.client.post(self.technical_edit_url, self.formset(data))
eq_(r.context['form'].errors, {})
addon = self.get_addon()
@ -1043,7 +1063,7 @@ class TestEditTechnical(TestEdit):
# Andddd offf
data = dict(developer_comments='Test comment!')
r = self.client.post(self.technical_url, data)
r = self.client.post(self.technical_edit_url, self.formset(data))
addon = self.get_addon()
eq_(addon.binary, False)
@ -1057,8 +1077,7 @@ class TestEditTechnical(TestEdit):
external_software='on',
site_specific='on',
view_source='on')
r = self.client.post(self.technical_url, data)
r = self.client.post(self.technical_edit_url, self.formset(data))
eq_(r.context['form'].errors, {})
addon = self.get_addon()
@ -1082,6 +1101,131 @@ class TestEditTechnical(TestEdit):
r = self.client.get(self.technical_edit_url)
self.assertContains(r, 'Upgrade SDK?')
def test_edit_dependencies_overview(self):
eq_([d.id for d in self.addon.all_dependencies], [5579])
r = self.client.get(self.technical_url)
req = pq(r.content)('td#required-addons')
eq_(req.length, 1)
eq_(req.attr('data-src'), reverse('search.ajax'))
eq_(req.find('li').length, 1)
a = req.find('a')
eq_(a.attr('href'), self.dependent_addon.get_url_path())
eq_(a.text(), unicode(self.dependent_addon.name))
def test_edit_dependencies_initial(self):
r = self.client.get(self.technical_edit_url)
form = pq(r.content)('#required-addons .dependencies li[data-addonid]')
eq_(form.length, 1)
eq_(form.find('input[id$=-dependent_addon]').val(),
str(self.dependent_addon.id))
div = form.find('div')
eq_(div.attr('style'),
'background-image:url(%s)' % self.dependent_addon.icon_url)
a = div.find('a')
eq_(a.attr('href'), self.dependent_addon.get_url_path())
eq_(a.text(), unicode(self.dependent_addon.name))
def test_edit_dependencies_add(self):
addon = Addon.objects.get(id=5299)
eq_(addon.type, amo.ADDON_EXTENSION)
eq_(addon in list(Addon.objects.public()), True)
d = self.dep_formset({'dependent_addon': addon.id})
r = self.client.post(self.technical_edit_url, d)
eq_(any(r.context['dependency_form'].errors), False)
self.check_dep_ids([self.dependent_addon.id, addon.id])
r = self.client.get(self.technical_edit_url)
reqs = pq(r.content)('#required-addons .dependencies')
eq_(reqs.find('li[data-addonid]').length, 2)
req = reqs.find('li[data-addonid=5299]')
eq_(req.length, 1)
a = req.find('div a')
eq_(a.attr('href'), addon.get_url_path())
eq_(a.text(), unicode(addon.name))
def check_dep_ids(self, expected=[]):
a = AddonDependency.objects.values_list('dependent_addon__id',
flat=True)
eq_(sorted(list(a)), sorted(expected))
def check_bad_dep(self, r):
"""This helper checks that bad dependency data doesn't go through."""
eq_(r.context['dependency_form'].errors[1]['dependent_addon'],
['Select a valid choice. That choice is not one of the available '
'choices.'])
self.check_dep_ids([self.dependent_addon.id])
def test_edit_dependencies_add_public(self):
"""Ensure that non-public add-ons cannot be added."""
addon = Addon.objects.get(id=40)
d = self.dep_formset({'dependent_addon': addon.id})
r = self.client.post(self.technical_edit_url, d)
self.check_bad_dep(r)
def test_edit_dependencies_add_nonpublic(self):
"""Ensure that non-public add-ons cannot be made as dependencies."""
addon = Addon.objects.get(id=40)
eq_(addon in list(Addon.objects.public()), False)
d = self.dep_formset({'dependent_addon': addon.id})
r = self.client.post(self.technical_edit_url, d)
self.check_bad_dep(r)
def test_edit_dependencies_add_public_persona(self):
"""Ensure that public Personas cannot be made as dependencies."""
addon = Addon.objects.get(id=15663)
eq_(addon.type, amo.ADDON_PERSONA)
eq_(addon in list(Addon.objects.public()), True)
d = self.dep_formset({'dependent_addon': addon.id})
r = self.client.post(self.technical_edit_url, d)
self.check_bad_dep(r)
def test_edit_dependencies_add_nonpublic_persona(self):
"""Ensure that non-public Personas cannot be made as dependencies."""
addon = Addon.objects.get(id=15663)
addon.update(status=amo.STATUS_UNREVIEWED)
eq_(addon.status, amo.STATUS_UNREVIEWED)
eq_(addon in list(Addon.objects.public()), False)
d = self.dep_formset({'dependent_addon': addon.id})
r = self.client.post(self.technical_edit_url, d)
self.check_bad_dep(r)
def test_edit_dependencies_add_self(self):
"""Ensure that an add-on cannot be made dependent on itself."""
d = self.dep_formset({'dependent_addon': self.addon.id})
r = self.client.post(self.technical_edit_url, d)
self.check_bad_dep(r)
def test_edit_dependencies_add_invalid(self):
"""Ensure that a non-existent add-on cannot be a dependency."""
d = self.dep_formset({'dependent_addon': 9999})
r = self.client.post(self.technical_edit_url, d)
self.check_bad_dep(r)
def test_edit_dependencies_add_duplicate(self):
"""Ensure that an add-on cannot be made dependent more than once."""
d = self.dep_formset({'dependent_addon': self.dependent_addon.id})
r = self.client.post(self.technical_edit_url, d)
eq_(r.context['dependency_form'].forms[1].non_field_errors(),
['Addon dependency with this Addon and Dependent addon already '
'exists.'])
self.check_dep_ids([self.dependent_addon.id])
def test_edit_dependencies_delete(self):
self.dep['DELETE'] = True
d = self.dep_formset(total_count=1, initial_count=1)
r = self.client.post(self.technical_edit_url, d)
eq_(any(r.context['dependency_form'].errors), False)
self.check_dep_ids()
def test_edit_dependencies_add_delete(self):
"""Ensure that we can both delete a dependency and add another."""
self.dep['DELETE'] = True
d = self.dep_formset({'dependent_addon': 5299})
r = self.client.post(self.technical_edit_url, d)
eq_(any(r.context['dependency_form'].errors), False)
self.check_dep_ids([5299])
class TestAdmin(amo.tests.TestCase):
fixtures = ['base/apps', 'base/users', 'base/addon_3615']

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

@ -13,6 +13,7 @@ from django import http
from django.conf import settings
from django import forms as django_forms
from django.db.models import Count
from django.forms.models import modelformset_factory
from django.shortcuts import get_object_or_404, redirect
from django.utils.http import urlquote
from django.utils.encoding import smart_unicode
@ -39,7 +40,7 @@ from amo.urlresolvers import reverse
from access import acl
from addons import forms as addon_forms
from addons.decorators import addon_view
from addons.models import Addon, AddonUser
from addons.models import Addon, AddonDependency, AddonUser
from addons.views import BaseFilter
from devhub.decorators import dev_required
from devhub.forms import CheckCompatibilityForm
@ -935,14 +936,13 @@ def addons_section(request, addon_id, addon, section, editable=False):
'details': addon_forms.AddonFormDetails,
'support': addon_forms.AddonFormSupport,
'technical': addon_forms.AddonFormTechnical,
'admin': forms.AdminForm,
}
'admin': forms.AdminForm}
if section not in models:
return http.HttpResponseNotFound()
raise http.Http404()
tags = previews = restricted_tags = []
cat_form = None
cat_form = dependency_form = None
if section == 'basic':
tags = addon.tags.not_blacklisted().values_list('tag_text', flat=True)
@ -952,14 +952,20 @@ def addons_section(request, addon_id, addon, section, editable=False):
elif section == 'media':
previews = forms.PreviewFormSet(request.POST or None,
prefix='files', queryset=addon.previews.all())
prefix='files', queryset=addon.previews.all())
elif section == 'technical':
if waffle.flag_is_active(request, 'edit-dependencies'):
dependency_form = forms.DependencyFormSet(request.POST or None,
queryset=addon.addons_dependencies.all(), addon=addon,
prefix='dependencies')
# Get the slug before the form alters it to the form data.
valid_slug = addon.slug
if editable:
if request.method == 'POST':
form = models[section](request.POST, request.FILES,
instance=addon, request=request)
instance=addon, request=request)
if form.is_valid() and (not previews or previews.is_valid()):
addon = form.save(addon)
@ -979,6 +985,11 @@ def addons_section(request, addon_id, addon, section, editable=False):
cat_form.save()
else:
editable = True
if dependency_form:
if dependency_form.is_valid():
dependency_form.save()
else:
editable = True
else:
form = models[section](instance=addon, request=request)
else:
@ -991,8 +1002,8 @@ def addons_section(request, addon_id, addon, section, editable=False):
'restricted_tags': restricted_tags,
'cat_form': cat_form,
'preview_form': previews,
'valid_slug': valid_slug,
}
'dependency_form': dependency_form,
'valid_slug': valid_slug}
return jingo.render(request,
'devhub/addons/edit/%s.html' % section, data)

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

@ -9,9 +9,6 @@ import amo
from amo import helpers
from applications.models import AppVersion
types = (amo.ADDON_ANY, amo.ADDON_EXTENSION, amo.ADDON_THEME, amo.ADDON_DICT,
amo.ADDON_SEARCH, amo.ADDON_LPAPP, amo.ADDON_PERSONA,)
sort_by = (
('', _lazy(u'Keyword Match')),
('updated', _lazy(u'Updated', 'advanced_search_form_updated')),
@ -118,8 +115,8 @@ def SearchForm(request):
appver = forms.CharField(required=False)
atype = forms.TypedChoiceField(label=_('Type'),
choices=[(t, amo.ADDON_TYPE[t]) for t in types], required=False,
coerce=int, empty_value=amo.ADDON_ANY)
choices=[(t, amo.ADDON_TYPE[t]) for t in amo.ADDON_SEARCH_TYPES],
required=False, coerce=int, empty_value=amo.ADDON_ANY)
pid = forms.TypedChoiceField(label=_('Platform'),
choices=[(p[0], p[1].name) for p in amo.PLATFORMS.iteritems()
@ -267,7 +264,7 @@ class ESSearchForm(forms.Form):
choices=[(p.shortname, p.id) for p in amo.PLATFORMS.values()])
appver = forms.CharField(required=False)
atype = forms.TypedChoiceField(required=False, coerce=int,
choices=[(t, amo.ADDON_TYPE[t]) for t in types])
choices=[(t, amo.ADDON_TYPE[t]) for t in amo.ADDON_SEARCH_TYPES])
cat = forms.CharField(required=False)
sort = forms.ChoiceField(required=False, choices=SORT_CHOICES)

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

@ -1,5 +1,6 @@
from collections import defaultdict
from django.conf import settings
from django.db.models import Q
from django.shortcuts import redirect
from django.utils.encoding import smart_str
@ -257,18 +258,36 @@ def ajax_search(request):
...
]
"""
# TODO(cvan): Replace with better ES-powered JSON views. Coming soon.
results = []
if 'q' in request.GET:
if settings.USE_ELASTIC:
q = request.GET['q']
exclude_personas = request.GET.get('exclude_personas', False)
if q.isdigit():
qs = Addon.objects.filter(id=int(q))
else:
qs = Addon.search().query(or_=name_query(q))
types = amo.ADDON_SEARCH_TYPES[:]
if exclude_personas:
types.remove(amo.ADDON_PERSONA)
qs = qs.filter(type__in=types)
results = qs.filter(status__in=amo.REVIEWED_STATUSES,
is_disabled=False)[:10]
else:
# TODO: Let this die when we kill Sphinx.
q = request.GET.get('q', '')
client = SearchClient()
try:
results = client.query('@name ' + q, limit=10,
match=sphinx.SPH_MATCH_EXTENDED2)
except SearchError:
pass
return [dict(id=result.id, label=unicode(result.name),
url=result.get_url_path(), icon=result.icon_url,
value=unicode(result.name).lower())
for result in results]
q = request.GET.get('q', '')
client = SearchClient()
try:
results = client.query('@name ' + q, limit=10,
match=sphinx.SPH_MATCH_EXTENDED2)
return [dict(id=result.id, label=unicode(result.name),
icon=result.icon_url, value=unicode(result.name).lower())
for result in results]
except SearchError:
return []
def name_query(q):

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

@ -238,13 +238,12 @@
}
}
#upload-file {
#validate-field {
display: table;
width: 100%;
margin: 0;
border: 1px solid #5875A0;
border: 1px solid @teal;
overflow: hidden;
.border-radius(5px);
#upload-webapp-url {
@ -282,7 +281,7 @@
&.vf-button button {
.border-radius(0);
border: none;
border-left: 1px solid #5875A0;
border-left: 1px solid @teal;
font-family: @sans-stack;
font-size: 1.3em;
line-height: 1em;
@ -293,3 +292,35 @@
}
}
#required-addons {
ul {
margin: 0;
}
input.autocomplete {
width: 300px;
}
.dependencies {
margin: 0 0 .5em;
li {
padding-bottom: .5em;
position: relative;
&:last-child {
margin-bottom: .5em;
}
a.remove {
float: none;
position: absolute;
right: 0;
top: 2px;
}
}
div {
background: #eee no-repeat 6px 50%;
.background-size(12px auto);
border: 1px solid @teal;
color: @medium-gray;
padding: 2px 5px 2px 25px;
width: 280px;
}
}
}

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

@ -0,0 +1,38 @@
@import 'lib';
input.autocomplete {
width: 16em;
}
.ui-autocomplete {
background-color: #fff;
border: 1px solid #000;
width: 300px;
z-index: 100 !important;
li, a {
min-height: 32px;
}
li {
clear: both;
}
a {
cursor: pointer;
display: block;
padding: 2px;
b {
color: #999;
display: block;
font-size: 10px;
}
}
img {
float: left;
height: 32px;
margin-right: 4px;
width: 32px;
}
}
.extra-form {
display: none;
}

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

@ -7,6 +7,7 @@
@red: #C63717;
@error-red: #C00000;
@orange: #D16B00;
@teal: #5875A0;
@shadow-blue: #98B2C9;
@border-blue: #C9DDF2;
@border-black: fadeOut(#000, 80%);

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

@ -163,7 +163,8 @@ span.remove {
.devhub-form tr:hover > td > a.remove,
.addon-submission-process #icon_preview:hover .tip,
.compat-versions tr:hover a.remove,
#existing_locales li:hover a.remove {
#existing_locales li:hover a.remove,
#required-addons ul.dependencies li:hover a.remove {
background-color: #CCC;
}
@ -172,13 +173,9 @@ span.remove {
cursor: help;
}
.devhub-form tr:hover .tip:hover,
.addon-submission-process #icon_preview .tip:hover,
.devhub-form tr:hover a.remove:hover,
.compat-versions tr:hover a.remove:hover,
#existing_locales li:hover a.remove:hover,
span.tip:hover,
a.remove:hover {
background-color: #2A4364;
background-color: #2A4364 !important;
}
.devhub-form .empty {

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

@ -3154,23 +3154,6 @@ td.input {
#id_name {
display: block;
}
.ui-autocomplete {
background-color: #fff;
border: 1px solid black;
width: 300px;
z-index: 11;
}
.ui-autocomplete span {
line-height: 32px;
}
.ui-autocomplete a {
display: block;
padding: 2px;
cursor: pointer;
}
.ui-autocomplete img {
float:left;
}
#addons-list a.remove,
#addons-list a.comment,
#contributors-list a.remove {

187
media/js/impala/formset.js Normal file
Просмотреть файл

@ -0,0 +1,187 @@
function updateTotalForms(prefix, inc) {
var $totalForms = $('#id_' + prefix + '-TOTAL_FORMS'),
inc = inc || 1,
num = parseInt($totalForms.val(), 10) + inc;
$totalForms.val(num);
return num;
}
/**
* zAutoFormset: handles Django formsets with autocompletion like a champ!
* by cvan
*/
(function($) {
$.fn.zAutoFormset = function(o) {
o = $.extend({
delegate: document.body, // Delegate (probably some nearby parent).
forms: null, // Where all the forms live (maybe a <ul>).
extraForm: '.extra-form', // Selector for element that contains the
// HTML for extra-form template.
prefix: 'form', // Formset prefix (Django default: 'form').
hiddenField: null, // This is the name of a (hidden) field
// that will contain the value of the
// formPK for each newly added form.
removeClass: 'remove', // Class for button triggering form removal.
formSelector: 'li', // Selector for each form container.
formPK: 'id', // Primary key for initial forms.
src: null, // Source URL of JSON search results.
input: null, // Input field for autocompletion search.
searchField: 'q', // Name of form field for search query.
excludePersonas: true, // Exclude Personas in search results?
minSearchLength: 3, // Minimum character length for queries.
width: 300, // Width (pixels) of autocomplete dropdown.
addedCB: null, // Callback for each new form added.
removedCB: null, // Callback for each form removed.
autocomplete: null // Custom handler you can provide to handle
// autocompletion yourself.
}, o);
var $delegate = $(o.delegate),
$forms = o.forms ? $delegate.find(o.forms) : $delegate,
$extraForm = $delegate.find(o.extraForm),
formsetPrefix = o.prefix,
hiddenField = o.hiddenField,
removeClass = o.removeClass,
formSelector = o.formSelector,
formPK = o.formPK,
src = o.src || $delegate.attr('data-src'),
$input = o.input ? $(o.input) : $delegate.find('input.autocomplete'),
searchField = o.searchField,
excludePersonas = o.excludePersonas,
minLength = o.minSearchLength,
width = o.width,
addedCB = o.addedCB,
removedCB = o.removedCB,
autocomplete = o.autocomplete;
function findItem(item) {
if (item) {
var $item = $forms.find('[name$=-' + hiddenField + '][value=' + item[formPK] + ']');
if ($item.length) {
var $f = $item.closest(formSelector);
return {'exists': true, 'visible': $f.is(':visible'), 'item': $f};
}
}
return {'exists': false, 'visible': false};
}
function clearInput() {
$input.val('');
$input.removeAttr('data-item');
}
function added() {
var item = JSON.parse($input.attr('data-item'));
// Check if this item has already been added.
var dupe = findItem(item);
if (dupe.exists) {
if (!dupe.visible) {
// Undelete the item.
var $item = dupe.item;
$item.find('input[name$=-DELETE]').removeAttr('checked');
$item.slideDown();
}
clearInput();
return;
}
clearInput();
var formId = updateTotalForms(formsetPrefix, 1) - 1,
emptyForm = $extraForm.html().replace(/__prefix__/g, formId);
var $f;
if (addedCB) {
$f = addedCB(emptyForm, item);
} else {
$f = $(f);
}
$f.hide().appendTo($forms).slideDown();
// Update hidden field.
$forms.find(formSelector + ':last [name$=-' + hiddenField + ']')
.val(item[formPK]);
}
function removed(el) {
el.slideUp();
// Mark as deleted.
el.find('input[name$=-DELETE]').attr('checked', true);
if (removedCB) {
removedCB(el);
}
// If this was not an initial form (i.e., an extra form), delete the
// form and decrement the TOTAL_FORMS count.
if (!el.find('input[name$=-' + formPK + ']').length) {
el.remove();
updateTotalForms(formsetPrefix, -1);
}
}
if (autocomplete) {
autocomplete();
} else {
$input.autocomplete({
minLength: minLength,
width: width,
source: function(request, response) {
var d = {};
d[searchField] = request.term;
if (excludePersonas) {
d['exclude_personas'] = true;
}
$.getJSON(src, d, response);
},
focus: function(event, ui) {
event.preventDefault();
$input.val(ui.item.label);
},
select: function(event, ui) {
event.preventDefault();
if (ui.item) {
$input.val(ui.item.label);
$input.attr('data-item', JSON.stringify(ui.item));
added();
}
}
}).data('autocomplete')._renderItem = function(ul, item) {
if (!findItem(item).visible) {
var $a = $(format('<a><img src="{0}">{1}</a>',
[item.icon, item.label]));
return $('<li>').data('item.autocomplete', item)
.append($a).appendTo(ul);
}
};
}
$delegate.delegate('.' + removeClass, 'click', _pd(function() {
removed($(this).closest(formSelector));
}));
};
})(jQuery);

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

@ -394,12 +394,12 @@ if (addon_ac.length) {
.attr('data-icon', ui.item.icon);
return false;
}
}).data( "autocomplete" )._renderItem = function( ul, item ) {
if (!$("#addons-list input[value='" + item.id + "']").length) {
return $( "<li></li>" )
.data( "item.autocomplete", item )
.append( '<a><img src="' + item.icon + '"/>&nbsp;<span>' + item.label + "</span></a>" )
.appendTo( ul );
}).data('autocomplete')._renderItem = function(ul, item) {
if (!$("#addons-list input[value=" + item.id + "]").length) {
return $('<li>')
.data('item.autocomplete', item)
.append('<a><img src="' + item.icon + '">' + item.label + '</a>')
.appendTo(ul);
}
};
}

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

@ -417,6 +417,12 @@ function addonFormSubmit() {
imageStatus.start();
hideSameSizedIcons();
}
if ($('#addon-categories-edit').length) {
initCatFields();
}
if ($('#required-addons').length) {
initRequiredAddons();
}
if (!parent_div.find(".errorlist").length) {
var e = $(format('<b class="save-badge">{0}</b>',
@ -445,15 +451,18 @@ function initEditAddon() {
$('#edit-addon').delegate('h3 a', 'click', function(e){
e.preventDefault();
a = e.target;
var a = e.target;
parent_div = $(a).closest('.edit-addon-section');
(function(parent_div, a){
parent_div.find(".item").addClass("loading");
parent_div.load($(a).attr('data-editurl'), function(){
if($('#addon-categories-edit').length) {
if ($('#addon-categories-edit').length) {
initCatFields();
}
if ($('#required-addons').length) {
initRequiredAddons();
}
$(this).each(addonFormSubmit);
});
})(parent_div, a);
@ -467,6 +476,28 @@ function initEditAddon() {
initUploadPreview();
}
function initRequiredAddons() {
$.fn.zAutoFormset({
delegate: '#required-addons',
forms: 'ul.dependencies',
prefix: 'dependencies',
hiddenField: 'dependent_addon',
addedCB: function(emptyForm, item) {
var f = template(emptyForm)({
icon: item.icon,
name: item.label || ''
});
// Firefox automatically escapes the contents of `href`, borking
// the curly braces in the {url} placeholder, so let's do this.
var $f = $(f);
$f.find('div a').attr('href', item.url);
return $f;
}
});
}
function create_new_preview_field() {
var forms_count = $('#id_files-TOTAL_FORMS').val(),
last = $('#file-list .preview').last(),

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

@ -0,0 +1,4 @@
INSERT INTO `waffle_flag`
(name, everyone, percent, superusers, staff, authenticated, rollout, note) VALUES
('edit-dependencies', 0, NULL, 1, 0, 0, 0,
'Toggles ability to edit add-on dependencies on devhub add-on edit pages');

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

@ -433,6 +433,7 @@ MINIFY_BUNDLES = {
'css/zamboni/amo_headerfooter.css',
'css/zamboni/tags.css',
'css/zamboni/tabs.css',
'css/impala/formset.less',
),
'zamboni/impala': (
'css/impala/base.css',
@ -471,6 +472,7 @@ MINIFY_BUNDLES = {
'css/impala/login.less',
'css/impala/dictionaries.less',
'css/impala/apps.less',
'css/impala/formset.less',
),
'zamboni/discovery-pane': (
'css/zamboni/discovery-pane.css',
@ -483,6 +485,7 @@ MINIFY_BUNDLES = {
'css/zamboni/docs.less',
'css/impala/developers.less',
'css/impala/devhub/packager.less',
'css/impala/formset.less',
),
'zamboni/devhub_impala': (
'css/impala/developers.less',
@ -666,6 +669,7 @@ MINIFY_BUNDLES = {
'zamboni/devhub': (
'js/zamboni/truncation.js',
'js/zamboni/upload.js',
'js/impala/formset.js',
'js/zamboni/devhub.js',
'js/zamboni/validator.js',
'js/zamboni/packager.js',