bug 574271, Add collection
This commit is contained in:
Родитель
d0560d67ca
Коммит
3fec546902
|
@ -48,7 +48,7 @@ def gc(test_result=True):
|
|||
TestResultCache.objects.filter(date__lt=one_hour_ago).delete()
|
||||
|
||||
log.debug('Cleaning up test results extraction cache.')
|
||||
if settings.NETAPP_STORAGE:
|
||||
if settings.NETAPP_STORAGE and settings.NETAPP_STORAGE != '/':
|
||||
cmd = ('find', settings.NETAPP_STORAGE, '-maxdepth', '1', '-name',
|
||||
'validate-*', '-mtime', '+7', '-type', 'd',
|
||||
'-exec', 'rm', '-rf', "{}", ';')
|
||||
|
@ -61,6 +61,17 @@ def gc(test_result=True):
|
|||
else:
|
||||
log.warning('NETAPP_STORAGE not defined.')
|
||||
|
||||
if settings.COLLECTIONS_ICON_PATH:
|
||||
log.debug('Cleaning up uncompressed icons.')
|
||||
|
||||
cmd = ('find', settings.COLLECTIONS_ICON_PATH,
|
||||
'-name', '*__unconverted', '-mtime', '+1', '-type', 'f',
|
||||
'-exec', 'rm', '{}', ';')
|
||||
output = Popen(cmd, stdout=PIPE).communicate()[0]
|
||||
|
||||
for line in output.split("\n"):
|
||||
log.debug(line)
|
||||
|
||||
# Paypal only keeps retrying to verify transactions for up to 3 days. If we
|
||||
# still have an unverified transaction after 6 days, we might as well get
|
||||
# rid of it.
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
import os
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
|
||||
import commonware
|
||||
from tower import ugettext as _
|
||||
|
||||
from addons.models import Addon
|
||||
from .models import Collection, CollectionAddon
|
||||
from . import tasks
|
||||
|
||||
privacy_choices = (
|
||||
(False, _('Only I can view this collection.')),
|
||||
(True, _('Anybody can view this collection.')))
|
||||
|
||||
log = commonware.log.getLogger('z.image')
|
||||
|
||||
|
||||
class CollectionForm(forms.ModelForm):
|
||||
|
||||
name = forms.CharField(max_length=100,
|
||||
label=_('Give your collection a name.'))
|
||||
slug = forms.CharField(label=_('URL:'))
|
||||
description = forms.CharField(label=_('Describe your collections.'),
|
||||
widget=forms.Textarea, required=False)
|
||||
listed = forms.ChoiceField(
|
||||
label=_('Who can view your collection?'),
|
||||
widget=forms.RadioSelect,
|
||||
choices=privacy_choices,
|
||||
initial=True,
|
||||
)
|
||||
|
||||
icon = forms.FileField(label=_('Give your collection an icon.'),
|
||||
required=False)
|
||||
|
||||
addon = forms.CharField(widget=forms.MultipleHiddenInput, required=False)
|
||||
addon_comment = forms.CharField(widget=forms.MultipleHiddenInput,
|
||||
required=False)
|
||||
|
||||
def clean_addon(self):
|
||||
addon_ids = self.data.getlist('addon')
|
||||
return Addon.objects.filter(pk__in=addon_ids)
|
||||
|
||||
def clean_addon_comment(self):
|
||||
addon_ids = self.data.getlist('addon')
|
||||
return dict(zip(map(int, addon_ids),
|
||||
self.data.getlist('addon_comment')))
|
||||
|
||||
def clean_description(self):
|
||||
description = self.cleaned_data['description']
|
||||
if description.strip() == '':
|
||||
description = None
|
||||
|
||||
return description
|
||||
|
||||
def clean_slug(self):
|
||||
author = self.initial['author']
|
||||
slug = self.cleaned_data['slug']
|
||||
|
||||
if author.collections.filter(slug=slug).count():
|
||||
raise forms.ValidationError(
|
||||
_('This url is already in use by another collection'))
|
||||
|
||||
return slug
|
||||
|
||||
def clean_icon(self):
|
||||
icon = self.cleaned_data['icon']
|
||||
if not icon:
|
||||
return
|
||||
if icon.content_type not in ('image/png', 'image/jpeg'):
|
||||
raise forms.ValidationError(
|
||||
_('Icons must be either PNG or JPG.'))
|
||||
|
||||
if icon.size > settings.MAX_ICON_UPLOAD_SIZE:
|
||||
raise forms.ValidationError(
|
||||
_('Please use images smaller than %dMB.' %
|
||||
(settings.MAX_ICON_UPLOAD_SIZE / 1024 / 1024 - 1)))
|
||||
return icon
|
||||
|
||||
def save(self):
|
||||
c = super(CollectionForm, self).save(commit=False)
|
||||
c.author = self.initial['author']
|
||||
c.application_id = self.initial['application_id']
|
||||
icon = self.cleaned_data.get('icon')
|
||||
|
||||
if icon:
|
||||
c.icontype = 'image/png'
|
||||
|
||||
c.save()
|
||||
if icon:
|
||||
dirname = os.path.join(settings.COLLECTIONS_ICON_PATH,
|
||||
str(c.id / 1000), )
|
||||
|
||||
destination = os.path.join(dirname, '%d.png' % c.id)
|
||||
tmp_destination = os.path.join(dirname,
|
||||
'%d.png__unconverted' % c.id)
|
||||
|
||||
if not os.path.exists(dirname):
|
||||
os.mkdir(dirname)
|
||||
|
||||
fh = open(tmp_destination, 'w')
|
||||
for chunk in icon.chunks():
|
||||
fh.write(chunk)
|
||||
|
||||
fh.close()
|
||||
|
||||
tasks.resize_icon.delay(tmp_destination, destination)
|
||||
|
||||
for addon in self.cleaned_data['addon']:
|
||||
ca = CollectionAddon(collection=c, addon=addon)
|
||||
comment = self.cleaned_data['addon_comment'].get(addon.id)
|
||||
if comment:
|
||||
ca.comments = comment
|
||||
|
||||
ca.save()
|
||||
c.save() # Update counts, etc.
|
||||
|
||||
return c
|
||||
|
||||
class Meta:
|
||||
model = Collection
|
||||
fields = ('name', 'slug', 'description', 'listed')
|
|
@ -1,8 +1,11 @@
|
|||
import logging
|
||||
import os
|
||||
|
||||
from django.db.models import Count
|
||||
|
||||
from celery.decorators import task
|
||||
from easy_thumbnails import processors
|
||||
from PIL import Image
|
||||
|
||||
from . import cron # Pull in tasks run through cron.
|
||||
from .models import Collection, CollectionVote
|
||||
|
@ -19,3 +22,15 @@ def collection_votes(*ids):
|
|||
votes = dict(v.values_list('vote').annotate(Count('vote')))
|
||||
qs = Collection.objects.filter(id=collection)
|
||||
qs.update(upvotes=votes.get(1, 0), downvotes=votes.get(-1, 0))
|
||||
|
||||
|
||||
@task
|
||||
def resize_icon(src, dest):
|
||||
"""Resizes collection icons to 32x32"""
|
||||
try:
|
||||
im = Image.open(src)
|
||||
im = processors.scale_and_crop(im, (32, 32))
|
||||
im.save(dest)
|
||||
os.remove(src)
|
||||
except Exception, e:
|
||||
log.error("Error saving collection icon: %s" % e)
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ page_title(_('Create a New Collection')) }}{% endblock %}
|
||||
{% block bodyclass %}collection-add{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="primary" role="main">
|
||||
<header>
|
||||
{{ breadcrumbs([ (remora_url('collections'), _('Collections')),]) }}
|
||||
|
||||
<h2>{{ _('Create a New Collection') }}</h2>
|
||||
</header>
|
||||
|
||||
{% if form.errors %}
|
||||
<p class="error">
|
||||
{{ _('There are errors in this form. Please correct them below.') }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
<form method="post" action="{{ url('collections.add') }}"
|
||||
enctype="multipart/form-data">
|
||||
{{ csrf() }}
|
||||
|
||||
<h3>{{ _('Collection Description') }}</h3>
|
||||
<fieldset>
|
||||
<p>
|
||||
{{ form.errors['name']|safe }}
|
||||
{{ form.name.label|safe }}
|
||||
{{ form.name|safe }}
|
||||
</p>
|
||||
<p>
|
||||
{{ form.errors['slug']|safe }}
|
||||
{{ form.slug.label|safe }}
|
||||
{{ url('collections.user', user.get_profile().nickname)|absolutify -}}
|
||||
{{ form.slug|safe }}
|
||||
</p>
|
||||
<p>
|
||||
{{ form.description.label|safe }} {{ _('(optional)') }}
|
||||
{{ form.description|safe }}
|
||||
</p>
|
||||
<p>
|
||||
{{ form.listed.label|safe }}
|
||||
</p>
|
||||
{{ form.listed|safe }}
|
||||
<p>
|
||||
{{ form.errors['icon']|safe }}
|
||||
{{ form.icon.label|safe }} {{ _('(optional)') }}
|
||||
{{ form.icon|safe }}
|
||||
</p>
|
||||
<p>
|
||||
{{ _('PNG and JPG supported. Image will be resized to 32x32.') }}
|
||||
</p>
|
||||
</fieldset>
|
||||
|
||||
<h3>{{ _('Add-ons in Your Collection') }}</h3>
|
||||
<p>
|
||||
{% trans %}
|
||||
To include an add-on in this collection, enter its name
|
||||
in the box below and hit enter. You can also use the
|
||||
<strong>Add to Collection</strong> links throughout the
|
||||
site to include add-ons later.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
|
||||
<fieldset>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Add-on</th>
|
||||
<th>Comment</th>
|
||||
<th>Remove</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<input id="addon-ac" data-src="{{ url('search.ajax') }}" />
|
||||
<button id="addon-select">
|
||||
{{ _('Add to Collection') }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% for addon in addons %}
|
||||
<tr>
|
||||
<td>
|
||||
<input type="hidden" value="{{ addon.id }}" name="addon">
|
||||
<img src="{{ addon.icon_url }}">
|
||||
<p>{{ addon.name }}</p>
|
||||
<p>
|
||||
<textarea name="addon_comment">{{ comments.get(addon.id) }}
|
||||
</textarea>
|
||||
</p>
|
||||
</td>
|
||||
<td class="comment">x</td>
|
||||
<td class="remove">x</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</fieldset>
|
||||
<p>
|
||||
<input type="submit" value="{{ _('Create Collection') }}">
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,3 +1,8 @@
|
|||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import QueryDict
|
||||
|
||||
from nose.tools import eq_
|
||||
import test_utils
|
||||
|
||||
|
@ -14,7 +19,7 @@ class TestViews(test_utils.TestCase):
|
|||
eq_(response.status_code, 404)
|
||||
elif code in (301, 302):
|
||||
self.assertRedirects(response, to, status_code=code)
|
||||
else:
|
||||
else: # pragma: no cover
|
||||
assert code in (301, 302, 404), code
|
||||
|
||||
def test_legacy_redirects(self):
|
||||
|
@ -92,3 +97,43 @@ class TestVotes(test_utils.TestCase):
|
|||
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
assert not r.redirect_chain
|
||||
eq_(r.status_code, 200)
|
||||
|
||||
|
||||
class TestAdd(test_utils.TestCase):
|
||||
"""Test the collection form."""
|
||||
fixtures = ['base/fixtures']
|
||||
|
||||
def setUp(self):
|
||||
self.client.login(username='admin@mozilla.com', password='password')
|
||||
self.add_url = reverse('collections.add')
|
||||
self.data = {
|
||||
'addon': 3615,
|
||||
'addon_comment': "fff",
|
||||
'name': "flagtir's ye ole favorites",
|
||||
'slug': "pornstar",
|
||||
'description': '',
|
||||
'listed': 'True'
|
||||
}
|
||||
|
||||
def test_showform(self):
|
||||
"""Shows form if logged in."""
|
||||
r = self.client.get(self.add_url)
|
||||
eq_(r.status_code, 200)
|
||||
|
||||
def test_submit(self):
|
||||
"""Test submission of addons."""
|
||||
# TODO(davedash): Test file uploads, test multiple addons.
|
||||
r = self.client.post(self.add_url, self.data, follow=True)
|
||||
eq_(r.request['PATH_INFO'],
|
||||
'/en-US/firefox/collections/admin/pornstar/')
|
||||
c = Collection.objects.get(slug='pornstar')
|
||||
eq_(unicode(c.name), self.data['name'])
|
||||
eq_(c.description, None)
|
||||
eq_(c.addons.all()[0].id, 3615)
|
||||
|
||||
def test_duplicate_slug(self):
|
||||
"""Try the same thing twice. AND FAIL"""
|
||||
self.client.post(self.add_url, self.data, follow=True)
|
||||
r = self.client.post(self.add_url, self.data, follow=True)
|
||||
eq_(r.context['form'].errors['slug'][0],
|
||||
'This url is already in use by another collection')
|
||||
|
|
|
@ -18,4 +18,5 @@ urlpatterns = patterns('',
|
|||
name='collections.user'),
|
||||
url('^collections/(?P<username>[^/]+)/(?P<slug>[^/]+)/',
|
||||
include(detail_urls)),
|
||||
url('^collections/add$', views.add, name='collections.add'),
|
||||
)
|
||||
|
|
|
@ -10,6 +10,7 @@ from addons.models import Addon
|
|||
from addons.views import BaseFilter
|
||||
from translations.query import order_by_translation
|
||||
from .models import Collection, CollectionAddon, CollectionVote
|
||||
from . import forms
|
||||
|
||||
|
||||
def legacy_redirect(self, uuid):
|
||||
|
@ -88,3 +89,25 @@ def collection_vote(request, username, slug, direction):
|
|||
return http.HttpResponse()
|
||||
else:
|
||||
return redirect(cn.get_url_path())
|
||||
|
||||
|
||||
@login_required
|
||||
def add(request):
|
||||
"Displays/processes a form to create a collection."
|
||||
data = {}
|
||||
if request.method == 'POST':
|
||||
form = forms.CollectionForm(
|
||||
request.POST, request.FILES,
|
||||
initial={'author': request.amo_user,
|
||||
'application_id': request.APP.id})
|
||||
if form.is_valid():
|
||||
collection = form.save()
|
||||
return http.HttpResponseRedirect(collection.get_url_path())
|
||||
else:
|
||||
data['addons'] = form.clean_addon()
|
||||
data['comments'] = form.clean_addon_comment()
|
||||
else:
|
||||
form = forms.CollectionForm()
|
||||
|
||||
data['form'] = form
|
||||
return jingo.render(request, 'bandwagon/add.html', data)
|
||||
|
|
|
@ -303,8 +303,89 @@ $(document).ready(function(){
|
|||
collections.hijack_favorite_button();
|
||||
});
|
||||
|
||||
/* Slugifier for Collection slug based on Collection name */
|
||||
if ($('body.collections-add')) {
|
||||
var url_customized = !!$('#id_slug').val();
|
||||
var slugify = function() {
|
||||
var slug = $('#id_slug');
|
||||
if (!url_customized || !slug.val()) {
|
||||
var s = $('#id_name').val().replace(/[^\w\s-]/g, '');
|
||||
s = s.replace(/[-\s]+/g, '-').toLowerCase();
|
||||
slug.val(s);
|
||||
}
|
||||
}
|
||||
|
||||
$('#id_name').keyup(slugify);
|
||||
$('#id_name').blur(slugify);
|
||||
|
||||
$('#id_slug').change(function() {
|
||||
url_customized = true;
|
||||
if (!$('#id_slug').val()) {
|
||||
url_customized = false;
|
||||
slugify();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* Autocomplete for collection add form. */
|
||||
$('#addon-ac').autocomplete({
|
||||
minLength: 3,
|
||||
source: function(request, response) {
|
||||
$.getJSON($('#addon-ac').attr('data-src'), {
|
||||
q: request.term
|
||||
}, response);
|
||||
},
|
||||
focus: function(event, ui) {
|
||||
$('#addon-ac').val(ui.item.label);
|
||||
return false;
|
||||
},
|
||||
select: function(event, ui) {
|
||||
$('#addon-ac').val(ui.item.label).attr('data-id', ui.item.id)
|
||||
.attr('data-icon', ui.item.icon);
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
$('#addon-select').click(function() {
|
||||
var id = $('#addon-ac').attr('data-id');
|
||||
var name = $('#addon-ac').val();
|
||||
var icon = $('#addon-ac').attr('data-icon');
|
||||
|
||||
// Verify that we aren't listed already
|
||||
if ($('input[name=addon][value='+id+']').length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (id && name && icon) {
|
||||
|
||||
var tr = _.template('<tr>' +
|
||||
'<td>' +
|
||||
'<input name="addon" value="{{ id }}" type="hidden">' +
|
||||
'<p><img src="{{ icon }}"> {{ name }}' +
|
||||
'</p><p style="display:none">' +
|
||||
'<textarea name="addon_comment"></textarea>' +
|
||||
'</p></td>' +
|
||||
'<td class="comment">x</td>' +
|
||||
'<td class="remove">x</td>' +
|
||||
'</tr>'
|
||||
);
|
||||
var str = tr({id: id, name: name, icon: icon});
|
||||
$('#addon-select').closest('tbody').append(str);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
var table = $('#addon-ac').closest('table')
|
||||
table.delegate(".remove", "click", function() {
|
||||
$(this).closest('tr').remove();
|
||||
})
|
||||
.delegate(".comment", "click", function() {
|
||||
var row = $(this).closest('tr');
|
||||
row.find('textarea').parent().show();
|
||||
});
|
||||
|
||||
|
||||
})();
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
Jinja2==2.3.1
|
||||
MySQL-python==1.2.3c1
|
||||
lxml==2.2.6
|
||||
PIL
|
||||
|
|
|
@ -26,3 +26,7 @@ django-pylibmc==0.2.1
|
|||
-e git://github.com/clouserw/tower.git#egg=tower
|
||||
-e git://github.com/jbalogh/django-queryset-transform.git#egg=django-queryset-transform
|
||||
-e git://github.com/jsocol/commonware.git#egg=commonware
|
||||
|
||||
|
||||
# Image cropping
|
||||
-e git://github.com/SmileyChris/easy-thumbnails.git#egg=easy_thumbnails
|
||||
|
|
|
@ -354,6 +354,7 @@ MINIFY_BUNDLES = {
|
|||
'js/zamboni/personas.js',
|
||||
|
||||
# Collections
|
||||
'js/zamboni/jquery-ui/custom-1.8.2.min.js',
|
||||
'js/zamboni/collections.js',
|
||||
),
|
||||
}
|
||||
|
@ -382,6 +383,7 @@ JAVA_BIN = '/usr/bin/java'
|
|||
|
||||
# File paths
|
||||
USERPICS_PATH = UPLOADS_PATH + '/userpics'
|
||||
COLLECTIONS_ICON_PATH = UPLOADS_PATH + '/addon_icons'
|
||||
|
||||
# URL paths
|
||||
# paths for images, e.g. mozcdn.com/amo or '/static'
|
||||
|
@ -520,6 +522,10 @@ def read_only_mode(env):
|
|||
env['MIDDLEWARE_CLASSES'] = tuple(m)
|
||||
|
||||
|
||||
# Uploaded file limits
|
||||
MAX_ICON_UPLOAD_SIZE = 4 * 1024 * 1024
|
||||
|
||||
|
||||
## Feature switches
|
||||
# Use this to keep collections compatible with remora before we're ready to
|
||||
# switch to zamboni/bandwagon3.
|
||||
|
|
1
urls.py
1
urls.py
|
@ -95,6 +95,7 @@ urlpatterns = patterns('',
|
|||
|
||||
('^addons/contribute/(\d+)/?$',
|
||||
lambda r, id: redirect('addons.contribute', id, permanent=True)),
|
||||
|
||||
)
|
||||
|
||||
if settings.DEBUG:
|
||||
|
|
Загрузка…
Ссылка в новой задаче