submission step 3, categories for multiple apps (bug 620422, bug 618238)

This commit is contained in:
Chris Van 2011-01-06 15:36:36 -05:00
Родитель de6eb48613
Коммит 6113c2c3d9
12 изменённых файлов: 425 добавлений и 279 удалений

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

@ -3,15 +3,18 @@ import re
from django import forms
from django.conf import settings
from django.forms.formsets import BaseFormSet, formset_factory
import happyforms
from tower import ugettext as _, ungettext as ngettext
import amo
import captcha.fields
from amo.utils import ImageCheck, slug_validator, slugify, remove_icons
from amo.utils import (ImageCheck, slug_validator, slugify, sorted_groupby,
remove_icons)
from addons.models import Addon, ReverseNameLookup, Category, AddonCategory
from addons.widgets import IconWidgetRenderer, CategoriesSelectMultiple
from applications.models import Application
from devhub import tasks
from tags.models import Tag
from translations.fields import TransField, TransTextarea
@ -45,8 +48,6 @@ class AddonFormBasic(AddonFormBase):
summary = TransField(widget=TransTextarea(attrs={'rows': 4}),
max_length=250)
tags = forms.CharField(required=False)
categories = forms.ModelMultipleChoiceField(queryset=False,
widget=CategoriesSelectMultiple)
def __init__(self, *args, **kw):
super(AddonFormBasic, self).__init__(*args, **kw)
@ -59,12 +60,6 @@ class AddonFormBasic(AddonFormBase):
name_validators.append(validate_name)
self.fields['name'].validators = name_validators
# TODO(gkoberger/help from chowse):
# Make it so the categories aren't hardcoded as Firefox only
self.fields['categories'].queryset = (order_by_translation(
Category.objects.filter(application=1, type=self.instance.type),
'name'))
def save(self, addon, commit=False):
tags_new = self.cleaned_data['tags']
tags_old = [slugify(t.tag_text, spaces=True) for t in addon.tags.all()]
@ -77,17 +72,6 @@ class AddonFormBasic(AddonFormBase):
for t in set(tags_old) - set(tags_new):
Tag(tag_text=t).remove_tag(addon, amo.get_user())
categories_new = self.cleaned_data['categories']
categories_old = list(addon.categories.all())
# Add new categories.
for c in set(categories_new) - set(categories_old):
AddonCategory(addon=addon, category=c).save()
# Remove old categories.
for c in set(categories_old) - set(categories_new):
AddonCategory.objects.filter(addon=addon, category=c).delete()
# We ignore `commit`, since we need it to be `False` so we can save
# the ManyToMany fields on our own.
addonform = super(AddonFormBasic, self).save(commit=False)
@ -140,26 +124,82 @@ class AddonFormBasic(AddonFormBase):
raise forms.ValidationError(_('This slug is already in use.'))
return target
class ApplicationChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj):
return obj.id
class CategoryForm(forms.Form):
application = ApplicationChoiceField(Application.objects.all(),
widget=forms.HiddenInput)
categories = forms.ModelMultipleChoiceField(
queryset=Category.objects.all(), widget=CategoriesSelectMultiple)
def save(self, addon):
categories_new = self.cleaned_data['categories']
categories_old = [cats for app, cats in addon.app_categories
if app.id == self.cleaned_data['application'].id]
if categories_old:
categories_old = categories_old[0]
# Add new categories.
for c in set(categories_new) - set(categories_old):
AddonCategory(addon=addon, category=c).save()
# Remove old categories.
for c in set(categories_old) - set(categories_new):
AddonCategory.objects.filter(addon=addon, category=c).delete()
def clean_categories(self):
# TODO(gkoberger): When we support multiple categories, this needs
# to be changed so they can have 2 categories per application.
categories = self.cleaned_data['categories']
total = categories.count()
max_cat = amo.MAX_CATEGORIES
total = len(self.cleaned_data['categories'].all())
if total > max_cat:
raise forms.ValidationError(ngettext(
'You can only have {0} category.',
'You can only have {0} categories.',
max_cat).format(max_cat))
'You can have only {0} category.',
'You can have only {0} categories.',
max_cat).format(max_cat))
has_other = len([i.name for i in categories if i.name=="Other"]) > 0
has_misc = filter(lambda x: x.misc, categories)
if has_misc and total > 1:
raise forms.ValidationError(
_("The miscellaneous category cannot be combined with "
"additional categories."))
if has_other and total > 1:
raise forms.ValidationError(_("The category 'Other' can not be "
"combined with additional "
"categories."))
return categories
return self.cleaned_data['categories']
class BaseCategoryFormSet(BaseFormSet):
def __init__(self, *args, **kw):
self.addon = kw.pop('addon')
super(BaseCategoryFormSet, self).__init__(*args, **kw)
self.initial = []
apps = sorted(self.addon.compatible_apps.keys(), key=lambda x: x.id)
for app in apps:
cats = [c for a, c in self.addon.app_categories if a == app]
if cats:
cats = cats[0]
self.initial.append({'categories': [c.id for c in cats]})
self._construct_forms()
for app, form in zip(apps, self.forms):
form.initial['application'] = app.id
form.app = app
cats = order_by_translation(Category.objects.filter(
type=self.addon.type, application=app.id), 'name')
form.fields['categories'].choices = [(c.id, c.name) for c in cats]
def save(self):
for f in self.forms:
f.save(self.addon)
CategoryFormSet = formset_factory(form=CategoryForm,
formset=BaseCategoryFormSet, extra=0)
def icons():

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

@ -27,6 +27,7 @@ from files.models import File
from reviews.models import Review
from stats.models import AddonShareCountTotal
from translations.fields import TranslatedField, PurifiedField, LinkifiedField
from translations.query import order_by_translation
from users.models import UserProfile, PersonaAuthor, UserForeignKey
from versions.compare import version_int
from versions.models import Version
@ -782,6 +783,16 @@ class Addon(amo.models.ModelBase):
def all_categories(self):
return list(self.categories.all())
@property
def app_categories(self):
categories = sorted_groupby(order_by_translation(self.categories.all(),
'name'),
key=lambda x: x.application_id)
app_cats = []
for app, cats in categories:
app_cats.append((amo.APP_IDS[app], list(cats)))
return app_cats
def update_name_table(sender, **kw):
from . import cron
@ -1031,6 +1042,7 @@ class Category(amo.models.ModelBase):
count = models.IntegerField('Addon count')
weight = models.IntegerField(
help_text='Category weight used in sort ordering')
misc = models.BooleanField(default=False)
addons = models.ManyToManyField(Addon, through='AddonCategory')

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

@ -17,9 +17,9 @@ import amo
import files.tests
from amo import set_user
from amo.signals import _connect, _disconnect
from addons.models import (Addon, AddonDependency, AddonRecommendation,
AddonType, BlacklistedGuid, Category, Charity,
Feature, Persona, Preview)
from addons.models import (Addon, AddonCategory, AddonDependency,
AddonRecommendation, AddonType, BlacklistedGuid,
Category, Charity, Feature, Persona, Preview)
from applications.models import Application, AppVersion
from devhub.models import ActivityLog
from files.models import File, Platform
@ -88,6 +88,7 @@ class TestAddonModels(test_utils.TestCase):
'base/addon_6704_grapple.json',
'base/addon_4594_a9',
'base/addon_4664_twitterbar',
'base/thunderbird',
'addons/featured',
'addons/invalid_latest_version']
@ -283,6 +284,36 @@ class TestAddonModels(test_utils.TestCase):
a.save()
assert addon().has_eula
def test_app_categories(self):
addon = lambda: Addon.objects.get(pk=3615)
c22 = Category.objects.get(id=22)
c22.name = 'CCC'
c22.save()
c23 = Category.objects.get(id=23)
c23.name = 'BBB'
c23.save()
c24 = Category.objects.get(id=24)
c24.name = 'AAA'
c24.save()
cats = addon().all_categories
eq_(cats, [c22, c23, c24])
for cat in cats:
eq_(cat.application.id, amo.FIREFOX.id)
cats = [c24, c23, c22]
app_cats = [(amo.FIREFOX, cats)]
eq_(addon().app_categories, app_cats)
tb = Application.objects.get(id=amo.THUNDERBIRD.id)
c = Category(application=tb, name='XXX', type=addon().type, count=1,
weight=1)
c.save()
ac = AddonCategory(addon=addon(), category=c).save()
app_cats += [(amo.THUNDERBIRD, [c])]
eq_(addon().app_categories, app_cats)
def test_review_replies(self):
"""
Make sure that developer replies are not returned as if they were

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

@ -5,6 +5,8 @@ from django.utils.html import conditional_escape
from django.utils.safestring import mark_safe
from tower import ugettext as _
from addons.models import Category
class IconWidgetRenderer(forms.RadioSelect.renderer):
""" Return radiobox as a list of images. """
@ -26,52 +28,42 @@ class IconWidgetRenderer(forms.RadioSelect.renderer):
class CategoriesSelectMultiple(forms.CheckboxSelectMultiple):
"""
Widget that formats the Categories checkboxes.
"""
"""Widget that formats the Categories checkboxes."""
def __init__(self, **kwargs):
super(self.__class__, self).__init__(**kwargs)
def render(self, name, value, attrs=None):
if value is None: value = []
value = value or []
has_id = attrs and 'id' in attrs
final_attrs = self.build_attrs(attrs, name=name)
choices = []
other = None
miscs = Category.objects.filter(misc=True).values_list('id', flat=True)
for c in self.choices:
if c[1] == 'Other':
if c[0] in miscs:
other = (c[0],
_("My add-on doesn't fit into any of the categories"))
else:
choices.append(c)
choices= list(enumerate(choices))
choices = list(enumerate(choices))
choices_size = len(choices)
columns = []
# Left column
columns.append(choices[:len(choices) / 2])
# Right column
columns.append(choices[len(choices) / 2:])
# "Other" column
groups = [choices]
if other:
columns.append([(choices_size,other)])
groups.append([(choices_size, other)])
str_values = set([force_unicode(v) for v in value])
output = []
for (k, column) in enumerate(columns):
if k == 2:
# We know it's the "other" column
output.append(u'<ul class="other">')
else:
output.append(u'<ul>')
for (k, group) in enumerate(groups):
cls = 'addon-misc-category' if k == 1 else 'addon-categories'
output.append(u'<ul class="%s">' % cls)
str_values = set([force_unicode(v) for v in value])
for i, (option_value, option_label) in column:
for i, (option_value, option_label) in group:
if has_id:
final_attrs = dict(final_attrs, id='%s_%s' % (
attrs['id'], i))
@ -86,7 +78,7 @@ class CategoriesSelectMultiple(forms.CheckboxSelectMultiple):
option_label = conditional_escape(force_unicode(option_label))
output.append(u'<li><label%s>%s %s</label></li>' % (
label_for, rendered_cb, option_label))
output.append(u'</ul>')
return mark_safe(u'\n'.join(output))

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

@ -1,4 +1,4 @@
{% from "devhub/includes/macros.html" import some_html_tip %}
{% from "devhub/includes/macros.html" import some_html_tip, select_cats %}
{% extends "devhub/addons/submit/base.html" %}
{% block title %}{{ dev_page_title(_('Step 3'), addon) }}{% endblock %}
@ -11,56 +11,60 @@
<label for="id_name">{{ _("Your add-on's name and version:") }}</label>
{{ form.name|safe }}
{% set version = addon.current_version %}
<input type="text" disabled id="current_version"
value="{{ version.version }}" size="6" />
{% set version = addon.current_version %}
<input type="text" disabled id="current_version"
value="{{ version.version }}" size="6">
{{ form.name.errors|safe }}
</div>
<div class="addon-submission-field">
<label>{{ _("Your add-on's detail page will be:") }}</label>
<span id="slug_edit" class="edit_with_prefix edit_initially_hidden">
<span>{{ settings.SITE_URL }}</span>{{ form.slug|safe }}
</span>
<span id="slug_readonly">
{{ settings.SITE_URL }}/&hellip;/<span id="slug_value"></span>
<a id="edit_slug" href="#">{{ _('Edit') }}</a>
</span>
{{ form.slug.errors|safe }}
</div>
<div class="addon-submission-field">
<label>{{ _('Provide a brief summary of your add-on:') }}</label>
{{ form.summary|safe }}
{{ form.summary.errors|safe }}
<div class="edit-addon-details">
{% trans %}
This summary will be shown with your add-on in listings and searches.
{% endtrans %}
<div class="char-count"
data-for-startswith="{{ form.summary.auto_id }}_"
data-maxlength="{{ form.summary.field.max_length }}"></div>
{{ form.name.errors|safe }}
</div>
</div>
<div class="addon-submission-field">
<label>
{{ _('Select 1 or 2 categories that best describe your add-on:') }}
</label>
{{ form.categories|safe }}
{{ form.categories.errors|safe }}
</div>
<div class="addon-submission-field">
<label>{{ _('Provide a more detailed description of your add-on:') }}</label>
{{ form.description|safe }}
{{ form.description.errors|safe }}
<div class="edit-addon-details">
{{ _("The description will appear on your add-on's detail page.") }}
{{ some_html_tip() }}
<div class="addon-submission-field">
<label>{{ _("Your add-on's detail page will be:") }}</label>
<div id="slug_edit" class="edit_with_prefix edit_initially_hidden">
<span>{{ settings.SITE_URL }}</span>{{ form.slug|safe }}
<div class="edit-addon-details">
{{ _('Please use only letters, numbers, and dashes in your URL.') }}
</div>
</div>
<span id="slug_readonly">
{{ settings.SITE_URL }}/&hellip;/<span id="slug_value"></span>
<a id="edit_slug" href="#">{{ _('Edit') }}</a>
</span>
{{ form.slug.errors|safe }}
</div>
<div class="addon-submission-field">
<label>{{ _('Provide a brief summary of your add-on:') }}</label>
{{ form.summary|safe }}
{{ form.summary.errors|safe }}
<div class="edit-addon-details">
{% trans %}
This summary will be shown with your add-on in listings and searches.
{% endtrans %}
<div class="char-count"
data-for-startswith="{{ form.summary.auto_id }}_"
data-maxlength="{{ form.summary.field.max_length }}"></div>
</div>
</div>
<div class="addon-submission-field"
data-max-categories="{{ amo.MAX_CATEGORIES }}">
{{ cat_form.non_form_errors()|safe }}
{{ cat_form.management_form|safe }}
{% for form in cat_form.initial_forms %}
{{ select_cats(amo.MAX_CATEGORIES, form) }}
{% endfor %}
</div>
<div class="addon-submission-field">
<label>{{ _('Provide a more detailed description of your add-on:') }}</label>
{{ form.description|safe }}
{{ form.description.errors|safe }}
<div class="edit-addon-details">
{{ _("The description will appear on your add-on's detail page.") }}
{{ some_html_tip() }}
</div>
</div>
<div class="submission-buttons addon-submission-field">
<button type="submit">
{{ _('Continue') }}
</button>
</div>
</div>
</form>
{% endblock primary %}

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

@ -1,4 +1,4 @@
{% from "devhub/includes/macros.html" import tip, empty_unless, trans_readonly %}
{% from "devhub/includes/macros.html" import tip, empty_unless, select_cats, trans_readonly %}
<form method="post" action="{{ url('devhub.addons.section', addon.slug, 'basic', 'edit') }}"
id="addon-edit-basic" data-baseurl="{{ url('devhub.addons.edit', addon.slug) }}">
@ -73,19 +73,25 @@
Choose any that fit your add-on's functionality for the most
exposure.")) }}
</th>
<td id="addon_categories_edit" data-max-categories="{{ amo.MAX_CATEGORIES }}">
<td id="addon_categories_edit"
data-max-categories="{{ amo.MAX_CATEGORIES }}">
{% if editable %}
<p>{{ ngettext('Select <b>up to {0}</b> category for this add-on.',
'Select <b>up to {0}</b> categories for this add-on.',
amo.MAX_CATEGORIES)|f(amo.MAX_CATEGORIES)|safe }}</p>
{{ form.categories|safe }}
{{ form.categories.errors|safe }}
{% else %}
{% set pipe = joiner("&middot;") %}
{% for category in categories %}
{{ pipe()|safe }}
{{ category }}
{{ cat_form.non_form_errors()|safe }}
{{ cat_form.management_form|safe }}
{% for form in cat_form.initial_forms %}
{{ select_cats(amo.MAX_CATEGORIES, form) }}
{% endfor %}
{% else %}
{% set categories = addon.app_categories %}
{% if categories %}
<ul class="addon-app-categories">
{% for app, cats in categories %}
<li>
<b>{{ app.pretty }}:</b> {{ cats|join(' &middot; ')|safe }}
</li>
{% endfor %}
</ul>
{% endif %}
{% endif %}
</td>
</tr>

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

@ -64,3 +64,16 @@
</td>
</tr>
{% endmacro %}
{% macro select_cats(max, form) %}
<div class="select-addon-cats">
{# L10n: {0} is the maximum number of add-on categories allowed.
{1} is the application name. #}
<label>{{ ngettext('Select <b>up to {0}</b> {1} category for this add-on:',
'Select <b>up to {0}</b> {1} categories for this add-on:',
max)|f(max, form.app.pretty)|safe }}</label>
{{ form.application|safe }}
{{ form.categories|safe }}
{{ form.categories.errors|safe }}
</div>
{% endmacro %}

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

@ -983,6 +983,10 @@ class TestEdit(test_utils.TestCase):
settings.PREVIEW_THUMBNAIL_PATH = tempfile.mkstemp()[1] + '%s/%d.png'
settings.ADDON_ICONS_PATH = tempfile.mkdtemp()
self.basic_url = self.get_url('basic', True)
ctx = self.client.get(self.basic_url).context['cat_form']
self.cat_initial = initial(ctx.initial_forms[0])
def tearDown(self):
reset_redis(self._redis)
settings.PREVIEW_THUMBNAIL_PATH = self.old_settings['preview']
@ -1009,7 +1013,6 @@ class TestEdit(test_utils.TestCase):
args = [self.addon.slug, section]
if edit:
args.append('edit')
return reverse('devhub.addons.section', args=args)
def test_redirect(self):
@ -1019,10 +1022,12 @@ class TestEdit(test_utils.TestCase):
self.assertRedirects(r, url, 301)
def get_dict(self, **kw):
fs = formset(self.cat_initial, initial_count=1)
result = {'name': 'new name', 'slug': 'test_slug',
'summary': 'new summary', 'categories': ['22'],
'summary': 'new summary',
'tags': ', '.join(self.tags)}
result.update(**kw)
result.update(fs)
return result
def test_edit_basic(self):
@ -1197,80 +1202,78 @@ class TestEdit(test_utils.TestCase):
eq_(tag.tag_text, 'scriptalertfooscript')
def test_edit_basic_categories_add(self):
eq_([c.id for c in self.get_addon().categories.all()], [22])
data = self.get_dict(name='new name!', categories=[22, 23])
self.client.post(self.get_url('basic', True), data)
categories = self.get_addon().categories.all()
eq_(categories[0].id, 22)
eq_(categories[1].id, 23)
eq_([c.id for c in self.get_addon().all_categories], [22])
self.cat_initial['categories'] = [22, 23]
r = self.client.post(self.basic_url, formset(self.cat_initial,
initial_count=1))
eq_([c.id for c in self.get_addon().all_categories], [22, 23])
def test_edit_basic_categories_addandremove(self):
AddonCategory(addon=self.addon, category_id=23).save()
eq_([c.id for c in self.get_addon().all_categories], [22, 23])
eq_([c.id for c in self.get_addon().categories.all()], [22, 23])
data = self.get_dict(name='new name!', categories=[22, 24])
self.client.post(self.get_url('basic', True), data)
category_ids_new = [c.id for c in self.get_addon().categories.all()]
self.cat_initial['categories'] = [22, 24]
r = self.client.post(self.basic_url, formset(self.cat_initial,
initial_count=1))
category_ids_new = [c.id for c in self.get_addon().all_categories]
eq_(category_ids_new, [22, 24])
def test_edit_basic_categories_xss(self):
category_other = Category.objects.get(id=22)
category_other.name = '<script>alert("test");</script>'
category_other.save()
data = self.get_dict(name='new name!', categories=[22, 24])
r = self.client.post(self.get_url('basic', True), data)
c = Category.objects.get(id=22)
c.name = '<script>alert("test");</script>'
c.save()
self.cat_initial['categories'] = [22, 24]
r = self.client.post(self.basic_url, formset(self.cat_initial,
initial_count=1))
assert '<script>alert' not in r.content
assert '&lt;script&gt;alert' in r.content
def test_edit_basic_categories_remove(self):
category = Category.objects.get(id=23)
AddonCategory(addon=self.addon, category=category).save()
c = Category.objects.get(id=23)
AddonCategory(addon=self.addon, category=c).save()
eq_([c.id for c in self.get_addon().all_categories], [22, 23])
eq_([c.id for c in self.get_addon().categories.all()], [22, 23])
data = self.get_dict(name='new name!')
self.client.post(self.get_url('basic', True), data)
category_ids_new = [c.id for c in self.get_addon().categories.all()]
self.cat_initial['categories'] = [22]
r = self.client.post(self.basic_url, formset(self.cat_initial,
initial_count=1))
category_ids_new = [c.id for c in self.get_addon().all_categories]
eq_(category_ids_new, [22])
def test_edit_basic_categories_required(self):
data = self.get_dict(categories=[])
r = self.client.post(self.get_url('basic', True), data)
self.assertFormError(r, 'form', 'categories',
'This field is required.')
del self.cat_initial['categories']
r = self.client.post(self.basic_url, formset(self.cat_initial,
initial_count=1))
eq_(r.context['cat_form'].errors[0]['categories'],
['This field is required.'])
def test_edit_basic_categories_max(self):
data = self.get_dict(categories=[22, 23, 24])
r = self.client.post(self.get_url('basic', True), data)
error = 'You can only have 2 categories.'
self.assertFormError(r, 'form', 'categories', error)
eq_(amo.MAX_CATEGORIES, 2)
self.cat_initial['categories'] = [22, 23, 24]
r = self.client.post(self.basic_url, formset(self.cat_initial,
initial_count=1))
eq_(r.context['cat_form'].errors[0]['categories'],
['You can have only 2 categories.'])
def test_edit_basic_categories_other_failure(self):
category_other = Category.objects.get(id=22)
category_other.name = 'Other'
category_other.save()
data = self.get_dict(categories=[22, 23])
r = self.client.post(self.get_url('basic', True), data)
error = ("The category 'Other' can not be combined with "
"additional categories.")
self.assertFormError(r, 'form', 'categories', error)
Category.objects.get(id=22).update(misc=True)
self.cat_initial['categories'] = [22, 23]
r = self.client.post(self.basic_url, formset(self.cat_initial,
initial_count=1))
eq_(r.context['cat_form'].errors[0]['categories'],
['The miscellaneous category cannot be combined with additional '
'categories.'])
def test_edit_basic_categories_nonexistent(self):
data = self.get_dict(categories=[100])
r = self.client.post(self.get_url('basic', True), data)
# Users will only get this if they're messing with the form, so a
# human readable error isn't necessary.
err = 'Select a valid choice. 100 is not one of the available choices.'
self.assertFormError(r, 'form', 'categories', err)
self.cat_initial['categories'] = [100]
r = self.client.post(self.basic_url, formset(self.cat_initial,
initial_count=1))
eq_(r.context['cat_form'].errors[0]['categories'],
['Select a valid choice. 100 is not one of the available '
'choices.'])
def test_edit_basic_name_not_empty(self):
data = self.get_dict(name='', slug=self.addon.slug,
@ -2598,6 +2601,7 @@ class TestSubmitStep3(test_utils.TestCase):
def setUp(self):
super(TestSubmitStep3, self).setUp()
self.addon = self.get_addon()
self.url = reverse('devhub.submit.3', args=['a3615'])
assert self.client.login(username='del@icio.us', password='password')
SubmitStep.objects.create(addon_id=3615, step=3)
@ -2609,22 +2613,30 @@ class TestSubmitStep3(test_utils.TestCase):
AddonCategory.objects.filter(addon=self.get_addon(),
category=Category.objects.get(id=24)).delete()
ctx = self.client.get(self.url).context['cat_form']
self.cat_initial = initial(ctx.initial_forms[0])
def get_addon(self):
return Addon.objects.no_cache().get(id=3615)
def tearDown(self):
reset_redis(self._redis)
def get_dict(self, **kw):
cat_initial = kw.pop('cat_initial', self.cat_initial)
fs = formset(cat_initial, initial_count=1)
result = {'name': 'Test name', 'slug': 'testname',
'description': 'desc', 'summary': 'Hello!'}
result.update(**kw)
result.update(fs)
return result
def test_submit_success(self):
r = self.client.get(self.url)
eq_(r.status_code, 200)
# Post and be redirected.
d = {'name': 'Test name',
'slug': 'testname',
'description': 'desc',
'categories': ['22'],
'summary': 'Hello!'}
d = self.get_dict()
r = self.client.post(self.url, d)
eq_(r.status_code, 302)
eq_(SubmitStep.objects.get(addon=3615).step, 4)
@ -2641,38 +2653,39 @@ class TestSubmitStep3(test_utils.TestCase):
def test_submit_name_unique(self):
# Make sure name is unique.
r = self.client.post(self.url, {'name': 'Cooliris'})
r = self.client.post(self.url, self.get_dict(name='Cooliris'))
error = 'This add-on name is already in use. Please choose another.'
self.assertFormError(r, 'form', 'name', error)
def test_submit_name_unique_strip(self):
# Make sure we can't sneak in a name by adding a space or two.
r = self.client.post(self.url, {'name': ' Cooliris '})
r = self.client.post(self.url, self.get_dict(name=' Cooliris '))
error = 'This add-on name is already in use. Please choose another.'
self.assertFormError(r, 'form', 'name', error)
def test_submit_name_unique_case(self):
# Make sure unique names aren't case sensitive.
r = self.client.post(self.url, {'name': 'cooliris'})
r = self.client.post(self.url, self.get_dict(name='cooliris'))
error = 'This add-on name is already in use. Please choose another.'
self.assertFormError(r, 'form', 'name', error)
def test_submit_name_required(self):
# Make sure name is required.
r = self.client.post(self.url, {'dummy': 'text'})
r = self.client.post(self.url, self.get_dict(name=''))
eq_(r.status_code, 200)
self.assertFormError(r, 'form', 'name', 'This field is required.')
def test_submit_name_length(self):
# Make sure the name isn't too long.
r = self.client.post(self.url, {'name': 'a' * 51})
d = self.get_dict(name='a' * 51)
r = self.client.post(self.url, d)
eq_(r.status_code, 200)
error = 'Ensure this value has at most 50 characters (it has 51).'
self.assertFormError(r, 'form', 'name', error)
def test_submit_slug_invalid(self):
# Submit an invalid slug.
d = dict(slug='slug!!! aksl23%%')
d = self.get_dict(slug='slug!!! aksl23%%')
r = self.client.post(self.url, d)
eq_(r.status_code, 200)
self.assertFormError(r, 'form', 'slug', "Enter a valid 'slug' " +
@ -2680,82 +2693,61 @@ class TestSubmitStep3(test_utils.TestCase):
def test_submit_slug_required(self):
# Make sure the slug is required.
r = self.client.post(self.url, {'dummy': 'text'})
r = self.client.post(self.url, self.get_dict(slug=''))
eq_(r.status_code, 200)
self.assertFormError(r, 'form', 'slug', 'This field is required.')
def test_submit_summary_required(self):
# Make sure summary is required.
r = self.client.post(self.url, {'dummy': 'text'})
r = self.client.post(self.url, self.get_dict(summary=''))
eq_(r.status_code, 200)
self.assertFormError(r, 'form', 'summary', 'This field is required.')
def test_submit_summary_length(self):
# Summary is too long.
r = self.client.post(self.url, {'summary': 'a' * 251})
r = self.client.post(self.url, self.get_dict(summary='a' * 251))
eq_(r.status_code, 200)
error = 'Ensure this value has at most 250 characters (it has 251).'
self.assertFormError(r, 'form', 'summary', error)
def test_submit_categories_required(self):
r = self.client.post(self.url, {'summary': 'Hello.', 'categories': []})
self.assertFormError(r, 'form', 'categories',
'This field is required.')
del self.cat_initial['categories']
r = self.client.post(self.url,
self.get_dict(cat_initial=self.cat_initial))
eq_(r.context['cat_form'].errors[0]['categories'],
['This field is required.'])
def test_submit_categories_max(self):
r = self.client.post(self.url, {'categories': ['22', '23', '24']})
error = 'You can only have 2 categories.'
self.assertFormError(r, 'form', 'categories', error)
eq_(amo.MAX_CATEGORIES, 2)
self.cat_initial['categories'] = [22, 23, 24]
r = self.client.post(self.url,
self.get_dict(cat_initial=self.cat_initial))
eq_(r.context['cat_form'].errors[0]['categories'],
['You can have only 2 categories.'])
def test_submit_categories_add(self):
eq_([c.id for c in self.get_addon().categories.all()], [22])
data = dict(name='new name!',
slug='test_slug',
summary='new summary',
categories=[22, 23],
tags='ab, cd, ef')
self.client.post(self.url, data)
categories = self.get_addon().categories.all()
eq_(categories[0].id, 22)
eq_(categories[1].id, 23)
eq_([c.id for c in self.get_addon().all_categories], [22])
self.cat_initial['categories'] = [22, 23]
self.client.post(self.url, self.get_dict(cat_initial=self.cat_initial))
eq_([c.id for c in self.get_addon().all_categories], [22, 23])
def test_submit_categories_addandremove(self):
AddonCategory(addon=self.get_addon(), category_id=23).save()
eq_([c.id for c in self.get_addon().categories.all()], [22, 23])
data = dict(name='new name!',
slug='test_slug',
summary='new summary',
categories=[22, 24],
tags='ab, cd, ef')
self.client.post(self.url, data)
category_ids_new = [c.id for c in self.get_addon().categories.all()]
AddonCategory(addon=self.addon, category_id=23).save()
eq_([c.id for c in self.get_addon().all_categories], [22, 23])
self.cat_initial['categories'] = [22, 24]
self.client.post(self.url, self.get_dict(cat_initial=self.cat_initial))
category_ids_new = [c.id for c in self.get_addon().all_categories]
eq_(category_ids_new, [22, 24])
def test_submit_categories_remove(self):
category = Category.objects.get(id=23)
AddonCategory(addon=self.get_addon(), category=category).save()
eq_([c.id for c in self.get_addon().categories.all()], [22, 23])
data = dict(name='new name!',
slug='test_slug',
summary='new summary',
categories=[22],
tags='ab, cd, ef')
self.client.post(self.url, data)
category_ids_new = [c.id for c in self.get_addon().categories.all()]
c = Category.objects.get(id=23)
AddonCategory(addon=self.addon, category=c).save()
eq_([c.id for c in self.get_addon().all_categories], [22, 23])
self.cat_initial['categories'] = [22]
self.client.post(self.url, self.get_dict(cat_initial=self.cat_initial))
category_ids_new = [c.id for c in self.get_addon().all_categories]
eq_(category_ids_new, [22])
def test_check_version(self):
@ -2949,7 +2941,7 @@ class TestSubmitStep5(TestSubmitBase):
def test_set_eula_nomsg(self):
"""
You should not get punished with a 500 for not writing your EULA...
but perhaps you shoudl feel shame for lying to us. This test does not
but perhaps you should feel shame for lying to us. This test does not
test for shame.
"""
self.get_addon().update(eula=None, privacy_policy=None)

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

@ -247,8 +247,7 @@ def edit(request, addon_id, addon):
'page': 'edit',
'addon': addon,
'tags': addon.tags.not_blacklisted().values_list('tag_text', flat=True),
'previews': addon.previews.all(),
'categories': addon.categories.all()}
'previews': addon.previews.all()}
return jingo.render(request, 'devhub/addons/edit.html', data)
@ -563,8 +562,14 @@ def addons_section(request, addon_id, addon, section, editable=False):
if section not in models:
return http.HttpResponseNotFound()
previews = []
if section == 'media':
tags = previews = []
cat_form = None
if section == 'basic':
tags = addon.tags.not_blacklisted().values_list('tag_text', flat=True)
cat_form = addon_forms.CategoryFormSet(request.POST or None,
addon=addon)
elif section == 'media':
previews = forms.PreviewFormSet(request.POST or None,
prefix='files', queryset=addon.previews.all())
@ -572,7 +577,9 @@ def addons_section(request, addon_id, addon, section, editable=False):
if request.method == 'POST':
form = models[section](request.POST, request.FILES,
instance=addon, request=request)
if form.is_valid() and (not previews or previews.is_valid()):
if (form.is_valid() and (not previews or previews.is_valid()) and
(cat_form and cat_form.is_valid())):
addon = form.save(addon)
if previews:
@ -584,23 +591,19 @@ def addons_section(request, addon_id, addon, section, editable=False):
amo.log(amo.LOG.CHANGE_ICON, addon)
else:
amo.log(amo.LOG.EDIT_PROPERTIES, addon)
if cat_form:
cat_form.save()
else:
form = models[section](instance=addon, request=request)
else:
form = False
tags = []
categories = []
if section == 'basic':
tags = addon.tags.not_blacklisted().values_list('tag_text', flat=True)
categories = addon.categories.all()
data = {'addon': addon,
'form': form,
'editable': editable,
'categories': categories,
'tags': tags,
'cat_form': cat_form,
'preview_form': previews}
return jingo.render(request,
@ -827,13 +830,15 @@ def submit_addon(request, step):
def submit_describe(request, addon_id, addon, step):
form = forms.Step3Form(request.POST or None, instance=addon,
request=request)
if request.method == 'POST' and form.is_valid():
cat_form = addon_forms.CategoryFormSet(request.POST or None, addon=addon)
if request.method == 'POST' and form.is_valid() and cat_form.is_valid():
addon = form.save(addon)
cat_form.save()
SubmitStep.objects.filter(addon=addon).update(step=4)
return redirect('devhub.submit.4', addon.slug)
return jingo.render(request, 'devhub/addons/submit/describe.html',
{'form': form, 'addon': addon, 'step': step})
{'form': form, 'cat_form': cat_form, 'addon': addon,
'step': step})
@dev_required

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

@ -258,11 +258,6 @@ label.above-the-field .locale {
margin-left: 0.5em;
}
#addon_categories_edit ul {
float: left;
margin-right: 20px;
}
/*
Bug 622030- TODO (potch) fix this later
@ -294,6 +289,55 @@ form .char-count b {
width: 16px;
}
/* @group Add-on category selection */
.select-addon-cats {
margin-bottom: 1.5em;
}
#edit-addon-basic .select-addon-cats {
border-top: 1px dotted #add0dc;
margin: 8px 0 0;
padding-top: 8px;
}
#edit-addon-basic input + .select-addon-cats {
border-top-width: 0;
margin-top: 0;
padding-top: 0;
}
.addon-categories {
-moz-column-count: 3;
-webkit-column-count: 3;
column-count: 3;
-moz-column-gap: 1.5em;
-webkit-column-gap: 1.5em;
column-gap: 1.5em;
margin-bottom: 0.5em;
}
#addon_categories_edit .addon-categories {
-moz-column-count: 2;
-webkit-column-count: 2;
column-count: 2;
}
.addon-misc-category,
.addon-app-categories {
margin-bottom: 0;
}
.addon-categories label,
.addon-misc-category label {
font-weight: normal;
}
.addon-app-categories li b {
margin-right: 0.25em;
}
/* @end */
#file-list .preview {
overflow: auto;
margin-bottom: 15px;
@ -770,7 +814,8 @@ h3 a.subscribe-feed:hover {
margin-bottom: 1em;
}
.addon-submission-process form label {
.addon-submission-process form label,
#edit-addon-basic .select-addon-cats label {
display: block;
padding-bottom: 3px;
}

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

@ -44,6 +44,11 @@ $(document).ready(function() {
initUploadControls();
}
// Submission > Describe
if ($("#submit-describe").length) {
initCatFields();
}
// Submission > Media
if($('#submit-media').length) {
initUploadIcon();
@ -241,29 +246,6 @@ $("#user-form-template .email-autocomplete")
function initEditAddon() {
if (z.noEdit) return;
// Manage checked/unchecked categories.
var max_categories = parseInt($('#addon_categories_edit').attr('data-max-categories'));
var manage_checked_all = function() {
var p = $('#addon_categories_edit'),
checked_length = $('ul:not(.other) input:checked', p).length,
disabled = checked_length >= max_categories;
$('ul.other input', p).attr('checked', checked_length <= 0);
$('ul:not(.other) input:not(:checked)', p).attr('disabled', disabled)
};
var manage_checked_other = function() {
$('#addon_categories_edit ul:not(.other) input').attr('checked', false);
$('#addon_categories_edit ul:not(.other) input').attr('disabled', false);
};
$('#edit-addon').delegate('#addon_categories_edit ul:not(.other) input',
'change', manage_checked_all);
$('#edit-addon').delegate('#addon_categories_edit ul.other input',
'change', manage_checked_other);
// Load the edit form.
$('#edit-addon').delegate('h3 a', 'click', function(e){
e.preventDefault();
@ -274,7 +256,7 @@ function initEditAddon() {
(function(parent_div, a){
parent_div.load($(a).attr('data-editurl'), function(){
if($('#addon_categories_edit').length) {
manage_checked_all();
initCatFields();
}
$(this).each(addonFormSubmit);
});
@ -416,7 +398,7 @@ function initUploadIcon() {
$('#icons_default a.active').removeClass('active');
$(this).addClass('active');
$("#id_icon_upload").val("")
$("#id_icon_upload").val("");
$('#icon_preview_32 img').attr('src', $('img', $parent).attr('src'));
$('#icon_preview_64 img').attr('src', $('img',
@ -431,7 +413,7 @@ function initUploadIcon() {
$('#icons_default input:checked').attr('checked', false);
$('input[name=icon_type][value='+file.type+']', $('#icons_default'))
.attr('checked', true)
.attr('checked', true);
$('#icons_default a.active').removeClass('active');
$('#icon_preview img').attr('src', file.getAsDataURL());
@ -899,6 +881,26 @@ function initPayments() {
}).change();
}
function initCatFields() {
$(".select-addon-cats").each(function() {
var $parent = $(this).closest("[data-max-categories]"),
$main = $(this).find(".addon-categories"),
$misc = $(this).find(".addon-misc-category"),
maxCats = parseInt($parent.attr("data-max-categories"));
var checkMain = function() {
var checkedLength = $("input:checked", $main).length,
disabled = checkedLength >= maxCats;
$("input", $misc).attr("checked", checkedLength <= 0);
$("input:not(:checked)", $main).attr("disabled", disabled);
};
var checkOther = function() {
$("input", $main).attr("checked", false).attr("disabled", false);
};
$("input", $main).live("change", checkMain).trigger("change");
$("input", $misc).live("change", checkOther);
});
}
function initLicenseFields() {
$("#id_has_eula").change(function (e) {
if ($(this).attr("checked")) {
@ -1182,7 +1184,7 @@ function hideSameSizedIcons() {
if($.inArray(size, icon_sizes) >= 0) {
$(this).hide();
}
icon_sizes.push(size)
icon_sizes.push(size);
});
}

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

@ -0,0 +1,4 @@
ALTER TABLE categories
ADD COLUMN misc tinyint(1) UNSIGNED NOT NULL DEFAULT '0';
UPDATE categories SET misc=1 WHERE slug IN ('miscellaneous', 'other');