This commit is contained in:
Dave Dash 2010-07-27 11:35:26 -07:00
Родитель d0560d67ca
Коммит 3fec546902
12 изменённых файлов: 425 добавлений и 4 удалений

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

@ -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.

123
apps/bandwagon/forms.py Normal file
Просмотреть файл

@ -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.

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

@ -95,6 +95,7 @@ urlpatterns = patterns('',
('^addons/contribute/(\d+)/?$',
lambda r, id: redirect('addons.contribute', id, permanent=True)),
)
if settings.DEBUG: