add-on dependencies front-end (bug 678650)
This commit is contained in:
Родитель
ef871b02a6
Коммит
eefbe4fad6
|
@ -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 {
|
||||
|
|
|
@ -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 + '"/> <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',
|
||||
|
|
Загрузка…
Ссылка в новой задаче