diff --git a/apps/addons/forms.py b/apps/addons/forms.py index 13b09f797d..2a232e93a4 100644 --- a/apps/addons/forms.py +++ b/apps/addons/forms.py @@ -9,14 +9,61 @@ import amo import captcha.fields from addons.models import Addon from amo.utils import slug_validator -from tower import ugettext as _ -from translations.widgets import TranslationTextInput, TranslationTextarea +from tags.models import Tag +from tower import ugettext as _, ungettext as ngettext +from translations.widgets import (TranslationTextInput, TranslationTextarea, + TransTextarea) +from translations.fields import TranslatedField, PurifiedField, LinkifiedField class AddonFormBasic(happyforms.ModelForm): name = forms.CharField(widget=TranslationTextInput, max_length=50) slug = forms.CharField(max_length=30) summary = forms.CharField(widget=TranslationTextarea, max_length=250) + tags = forms.CharField() + + def __init__(self, *args, **kw): + self.request = kw.pop('request') + super(AddonFormBasic, self).__init__(*args, **kw) + + self.fields['tags'].initial = ', '.join(tag.tag_text for tag in + self.instance.tags.all()) + + def save(self, addon, commit=False): + tags_new = self.cleaned_data['tags'] + tags_old = [t.tag_text for t in addon.tags.all()] + + # Add new tags. + for t in set(tags_new) - set(tags_old): + Tag(tag_text=t).save_tag(addon, self.request.amo_user) + + # Remove old tags. + for t in set(tags_old) - set(tags_new): + Tag(tag_text=t).remove_tag(addon, self.request.amo_user) + + return super(AddonFormBasic, self).save() + + def clean_tags(self): + target = [t.strip() for t in self.cleaned_data['tags'].split(',')] + + max_tags = amo.MAX_TAGS + min_len = amo.MIN_TAG_LENGTH + total = len(target) + tags_short = [t for t in target if len(t.strip()) < min_len] + + if total > max_tags: + raise forms.ValidationError(ngettext( + 'You have {0} too many tags.', + 'You have {0} too many tags.', + total - max_tags) + .format(total - max_tags)) + + if tags_short: + raise forms.ValidationError(ngettext( + 'All tags must be at least {0} character.', + 'All tags must be at least {0} characters.', + min_len).format(min_len)) + return target def clean_slug(self): target = self.cleaned_data['slug'] @@ -32,6 +79,10 @@ class AddonFormDetails(happyforms.ModelForm): default_locale = forms.TypedChoiceField(choices=Addon.LOCALES) homepage = forms.URLField(widget=TranslationTextInput) + def __init__(self, *args, **kw): + self.request = kw.pop('request') + super(AddonFormDetails, self).__init__(*args, **kw) + class Meta: model = Addon fields = ('description', 'default_locale', 'homepage') @@ -41,7 +92,11 @@ class AddonFormSupport(happyforms.ModelForm): support_url = forms.URLField(widget=TranslationTextInput) support_email = forms.EmailField(widget=TranslationTextInput) - def save(self, addon, commit=False): + def __init__(self, *args, **kw): + self.request = kw.pop('request') + super(AddonFormSupport, self).__init__(*args, **kw) + + def save(self, addon, commit=True): instance = self.instance # If there's a GetSatisfaction URL entered, we'll extract the product @@ -57,7 +112,7 @@ class AddonFormSupport(happyforms.ModelForm): instance.get_satisfaction_company = company instance.get_satisfaction_product = product - return super(AddonFormSupport, self).save() + return super(AddonFormSupport, self).save(commit) class Meta: model = Addon @@ -68,6 +123,10 @@ class AddonFormTechnical(forms.ModelForm): developer_comments = forms.CharField(widget=TranslationTextarea, required=False) + def __init__(self, *args, **kw): + self.request = kw.pop('request') + super(AddonFormTechnical, self).__init__(*args, **kw) + class Meta: model = Addon fields = ('developer_comments', 'view_source', 'site_specific', diff --git a/apps/amo/__init__.py b/apps/amo/__init__.py index 4f54c86eb4..3a31cd6e5c 100644 --- a/apps/amo/__init__.py +++ b/apps/amo/__init__.py @@ -169,6 +169,10 @@ ADDON_SLUGS = { ADDON_SEARCH: 'search-tools', } +# Edit addon information +MAX_TAGS = 20 +MIN_TAG_LENGTH = 2 + # These types don't maintain app compatibility in the db. Instead, we look at # APP.types and APP_TYPE_SUPPORT to figure out where they are compatible. NO_COMPAT = (ADDON_SEARCH, ADDON_PERSONA) diff --git a/apps/amo/log.py b/apps/amo/log.py index cb4478c931..46639bf90b 100644 --- a/apps/amo/log.py +++ b/apps/amo/log.py @@ -168,18 +168,14 @@ class REQUEST_VERSION: keep = True -# TODO(gkoberger): When he does 606248 class ADD_TAG: id = 25 - # L10n: {0} is the tag name. - format = _(u'{user.name} added tag {0} to {addon}') + format = _(u'{tag} added to {addon}') -# TODO(gkoberger): When he does 606248 class REMOVE_TAG: id = 26 - # L10n: {0} is the tag name. - format = _(u'{user.name} removed tag {0} from {addon}') + format = _(u'{tag} removed from {addon}') class ADD_TO_COLLECTION: diff --git a/apps/devhub/models.py b/apps/devhub/models.py index 5ea4e134eb..513eed1dc9 100644 --- a/apps/devhub/models.py +++ b/apps/devhub/models.py @@ -13,6 +13,7 @@ import amo.models from addons.models import Addon from bandwagon.models import Collection from reviews.models import Review +from tags.models import Tag from translations.fields import TranslatedField from users.models import UserProfile from versions.models import Version @@ -180,6 +181,8 @@ class ActivityLog(amo.models.ModelBase): review = None version = None collection = None + tag = None + for arg in self.arguments: if isinstance(arg, Addon) and not addon: addon = u'%s' % (arg.get_url_path(), arg.name) @@ -196,7 +199,9 @@ class ActivityLog(amo.models.ModelBase): collection = u'%s' % (arg.get_url_path(), arg.name) arguments.remove(arg) - + if isinstance(arg, Tag) and not tag: + tag = u'%s' % (arg.get_url_path(), + arg.tag_text) try: data = dict(user=self.user, addon=addon, review=review, version=version, collection=collection) diff --git a/apps/devhub/templates/devhub/includes/addon_edit_basic.html b/apps/devhub/templates/devhub/includes/addon_edit_basic.html index b810844c1c..50372d5474 100644 --- a/apps/devhub/templates/devhub/includes/addon_edit_basic.html +++ b/apps/devhub/templates/devhub/includes/addon_edit_basic.html @@ -81,10 +81,16 @@ descriptors such as tabs, toolbar, or twitter. You may have a maximum of {0} tags.").format(amo.MAX_TAGS)) }} - + {% if editable %} - {# TODO(gkoberger): Add tags #} - Coming Soon + {{ form.tags|safe }} + {{ form.tags.errors|safe }} +
+ {{ ngettext('Comma-separated, minimum of {0} character.', + 'Comma-separated, minimum of {0} characters.', + amo.MIN_TAG_LENGTH)|f(amo.MIN_TAG_LENGTH) }} + {{ _('Example: ocean, sail boat, water.') }} +
{% else %} {% call empty_unless(tags) %} {{ tags|join(', ') }} diff --git a/apps/devhub/tests/test_views.py b/apps/devhub/tests/test_views.py index 2fce1d6185..d32f441301 100644 --- a/apps/devhub/tests/test_views.py +++ b/apps/devhub/tests/test_views.py @@ -22,6 +22,7 @@ from devhub.forms import ContribForm from devhub.models import ActivityLog, RssKey from files.models import File, Platform from reviews.models import Review +from tags.models import Tag from users.models import UserProfile from versions.models import ApplicationsVersions, License, Version @@ -910,6 +911,11 @@ class TestEdit(test_utils.TestCase): self.addon = self.get_addon() assert self.client.login(username='del@icio.us', password='password') self.url = reverse('devhub.addons.edit', args=[self.addon.id]) + self.user = UserProfile.objects.get(pk=55021) + + self.tags = ['tag3', 'tag2', 'tag1'] + for t in self.tags: + Tag(tag_text=t).save_tag(self.addon, self.user) def get_addon(self): return Addon.objects.no_cache().get(id=3615) @@ -932,7 +938,8 @@ class TestEdit(test_utils.TestCase): data = dict(name='new name', slug='test_addon', - summary='new summary') + summary='new summary', + tags=', '.join(self.tags)) r = self.client.post(self.get_url('basic', True), data) eq_(r.status_code, 200) @@ -944,20 +951,100 @@ class TestEdit(test_utils.TestCase): eq_(unicode(addon.slug), data['slug']) eq_(unicode(addon.summary), data['summary']) + self.tags.sort() + eq_([unicode(t) for t in addon.tags.all()], self.tags) + def test_edit_basic_slugs_unique(self): Addon.objects.get(id=5579).update(slug='test_slug') data = dict(name='new name', slug='test_slug', - summary='new summary') + summary='new summary', + tags=','.join(self.tags)) r = self.client.post(self.get_url('basic', True), data) eq_(r.status_code, 200) self.assertFormError(r, 'form', 'slug', 'This slug is already in use.') - def test_edit_basic_name_not_empty(self): + def test_edit_basic_add_tag(self): + count = ActivityLog.objects.all().count() + self.tags.insert(0, 'tag4') + data = dict(name='new name', + slug='test_slug', + summary='new summary', + tags=', '.join(self.tags)) + r = self.client.post(self.get_url('basic', True), data) + eq_(r.status_code, 200) + + doc = pq(r.content) + + result = doc('#addon_tags_edit').eq(0).text() + + self.tags.sort() + eq_(result, ', '.join(self.tags)) + + eq_(ActivityLog.objects.filter(action=amo.LOG.ADD_TAG.id).count(), + count + 1) + + def test_edit_basic_remove_tag(self): + self.tags.remove('tag2') + + count = ActivityLog.objects.all().count() + + data = dict(name='new name', + slug='test_slug', + summary='new summary', + tags=', '.join(self.tags)) + + r = self.client.post(self.get_url('basic', True), data) + eq_(r.status_code, 200) + + doc = pq(r.content) + + result = doc('#addon_tags_edit').eq(0).text() + + self.tags.sort() + eq_(result, ', '.join(self.tags)) + + eq_(ActivityLog.objects.filter(action=amo.LOG.REMOVE_TAG.id).count(), + count + 1) + + def test_edit_basic_minlength_tags(self): + tags = self.tags + tags.append('a' * (amo.MIN_TAG_LENGTH - 1)) + + data = dict(name='new name', + slug='test_slug', + summary='new summary', + tags=', '.join(tags)) + + r = self.client.post(self.get_url('basic', True), data) + eq_(r.status_code, 200) + + self.assertFormError(r, 'form', 'tags', + 'All tags must be at least %d characters.' % + amo.MIN_TAG_LENGTH) + + def test_edit_basic_max_tags(self): + tags = self.tags + + for i in range(amo.MAX_TAGS + 1): + tags.append('test%d' % i) + + data = dict(name='new name', + slug='test_slug', + summary='new summary', + tags=', '.join(tags)) + + r = self.client.post(self.get_url('basic', True), data) + eq_(r.status_code, 200) + + self.assertFormError(r, 'form', 'tags', 'You have %d too many tags.' % + (len(tags) - amo.MAX_TAGS)) + + def test_edit_basic_name_not_empty(self): data = dict(name='', slug=self.addon.slug, summary=self.addon.summary) diff --git a/apps/devhub/views.py b/apps/devhub/views.py index 701f2b39aa..6036559bde 100644 --- a/apps/devhub/views.py +++ b/apps/devhub/views.py @@ -414,13 +414,13 @@ def addons_section(request, addon_id, addon, section, editable=False): if editable: if request.method == 'POST': form = models[section](request.POST, request.FILES, - instance=addon) + instance=addon, request=request) if form.is_valid(): addon = form.save(addon) editable = False amo.log(amo.LOG.EDIT_PROPERTIES, addon) else: - form = models[section](instance=addon) + form = models[section](instance=addon, request=request) else: form = False diff --git a/apps/tags/models.py b/apps/tags/models.py index 2d06cee216..b54a311544 100644 --- a/apps/tags/models.py +++ b/apps/tags/models.py @@ -38,6 +38,17 @@ class Tag(amo.models.ModelBase): return urls + def save_tag(self, addon, user): + tag, _ = Tag.objects.get_or_create(tag_text=self.tag_text) + AddonTag.objects.get_or_create(addon=addon, tag=tag, user=user) + amo.log(amo.LOG.ADD_TAG, tag, addon) + return tag + + def remove_tag(self, addon, user): + tag, created = Tag.objects.get_or_create(tag_text=self.tag_text) + AddonTag.objects.filter(addon=addon, tag=tag).delete() + amo.log(amo.LOG.REMOVE_TAG, tag, addon) + class TagStat(amo.models.ModelBase): tag = models.OneToOneField(Tag, primary_key=True) diff --git a/media/css/zamboni/developers.css b/media/css/zamboni/developers.css index 6f68494009..c26ca1c0e4 100644 --- a/media/css/zamboni/developers.css +++ b/media/css/zamboni/developers.css @@ -177,6 +177,12 @@ a.remove:hover, overflow: hidden; } +.edit-addon-details { + padding-top: 3px; + font-size: 0.8em; + color: #555; +} + /* @end */ /* @group Version Compatibility */