[bug 623429] Add is_draft field to gallery models.

This commit is contained in:
Paul Craciunoiu 2011-04-11 18:03:32 -07:00
Родитель 47b9796f26
Коммит eeb2903b12
11 изменённых файлов: 141 добавлений и 111 удалений

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

@ -1,4 +1,2 @@
# The number of items per page
ITEMS_PER_PAGE = 20
# Snowman draft
DRAFT_TITLE_PREFIX = u'\u2603\u2603\u2603 '

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

@ -4,7 +4,6 @@ from django.core.exceptions import ValidationError
from tower import ugettext_lazy as _lazy, ugettext as _
from gallery import DRAFT_TITLE_PREFIX
from gallery.models import Image, Video
from sumo.form_fields import StrippedCharField
from sumo_locales import LOCALES
@ -82,8 +81,8 @@ class MediaForm(forms.ModelForm):
self.fields['title'].required = True
self.fields['description'].required = True
def save(self, update_user=None, **kwargs):
return save_form(self, update_user, **kwargs)
def save(self, update_user=None, is_draft=True, **kwargs):
return save_form(self, update_user, is_draft=is_draft, **kwargs)
class ImageForm(MediaForm):
@ -103,7 +102,6 @@ class ImageForm(MediaForm):
def clean(self):
c = super(ImageForm, self).clean()
c = clean_draft(self, c)
clean_image_extension(c.get('file'))
return c
@ -149,7 +147,6 @@ class VideoForm(MediaForm):
'flv' in c and c['flv'] and c['flv'].name.endswith('.flv') or
'thumbnail' in c and c['thumbnail']):
raise ValidationError(MSG_VID_REQUIRED)
clean_draft(self, c)
clean_image_extension(c.get('thumbnail'))
return self.cleaned_data
@ -159,15 +156,7 @@ class VideoForm(MediaForm):
'title', 'description')
def clean_draft(form, cleaned_data):
"""Drafts reserve a special title."""
c = cleaned_data
if 'title' in c and c['title'].startswith(DRAFT_TITLE_PREFIX):
raise ValidationError(MSG_TITLE_DRAFT)
return c
def save_form(form, update_user=None, **kwargs):
def save_form(form, update_user=None, is_draft=True, **kwargs):
"""Save a media form, add user to updated_by.
Warning: this assumes you're calling it from a subclass of MediaForm.
@ -176,5 +165,6 @@ def save_form(form, update_user=None, **kwargs):
obj = super(MediaForm, form).save(commit=False, **kwargs)
if update_user:
obj.updated_by = update_user
obj.is_draft = is_draft
obj.save()
return obj

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

@ -18,11 +18,12 @@ class Media(ModelBase):
description = models.TextField(max_length=10000)
locale = LocaleField(default=settings.GALLERY_DEFAULT_LANGUAGE,
db_index=True)
is_draft = models.NullBooleanField(default=None, null=True, editable=False)
class Meta:
class Meta(object):
abstract = True
ordering = ['-created']
unique_together = ('locale', 'title')
unique_together = (('locale', 'title'), ('is_draft', 'creator'))
def __unicode__(self):
return '[%s] %s' % (self.locale, self.title)

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

@ -34,7 +34,7 @@
{% macro image_preview(form, type, name) %}
{% set classes = 'preview ' + name %}
{% if form.instance.pk and form.instance.thumbnail %}
{% if form.instance.pk and (type == 'image' or form.instance.thumbnail) %}
{% set html =
'<div class="preview-{type} {name}">
<img src="{thumbnail_url}">
@ -91,9 +91,7 @@ What JS needs to do:
* Hide/Show progress
* Show progress => Disable submit
* Hide progress => Enable submit
* Validate form for submission
TODO: use easing to fade in/out instead of JS
TODO: better error messages ("invalid file" means too big, or extension, etc)
#}

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

@ -9,7 +9,6 @@ from sumo.tests import TestCase, get, LocalizingClient, post
from sumo.urlresolvers import reverse
from gallery.models import Image, Video
from gallery.tests import image, video
from gallery.utils import get_draft_title
class GalleryPageCase(TestCase):
@ -51,12 +50,6 @@ class GalleryPageCase(TestCase):
imgs = doc('#media-list li img')
eq_(1, len(imgs))
def test_gallery_upload_modal(self):
# TODO(paul) this will probably be broken up into separate tests:
# * Upload modal has the URL's locale selected
# * POSTing invalid data shows error messages and pre-fills that data
raise SkipTest
class GalleryAsyncCase(TestCase):
fixtures = ['users.json']
@ -111,25 +104,25 @@ class GalleryUploadTestCase(TestCase):
def test_image_draft_shows(self):
"""The image draft is loaded for this user."""
image(title=get_draft_title(self.u), creator=self.u)
image(is_draft=True, creator=self.u)
response = get(self.client, 'gallery.gallery', args=['image'])
eq_(200, response.status_code)
doc = pq(response.content)
assert doc('.image-preview img').attr('src').endswith('098f6b.jpg')
eq_(1, doc('.image-preview img').length)
assert doc('.file.preview img').attr('src').endswith('098f6b.jpg')
eq_(1, doc('.file.preview img').length)
def test_video_draft_shows(self):
"""The video draft is loaded for this user."""
video(title=get_draft_title(self.u), creator=self.u)
video(is_draft=True, creator=self.u)
response = get(self.client, 'gallery.gallery', args=['image'])
eq_(200, response.status_code)
doc = pq(response.content)
# Preview for all 3 video formats: flv, ogv, webm
eq_(3, doc('ul li.video-preview').length)
eq_(3, doc('#gallery-upload-video .preview input').length)
def test_image_draft_post(self):
"""Posting to the page saves the field values for the image draft."""
image(title=get_draft_title(self.u), creator=self.u)
image(is_draft=True, creator=self.u)
response = post(self.client, 'gallery.gallery',
{'description': '??', 'title': 'test'}, args=['image'])
eq_(200, response.status_code)
@ -140,7 +133,7 @@ class GalleryUploadTestCase(TestCase):
def test_video_draft_post(self):
"""Posting to the page saves the field values for the video draft."""
video(title=get_draft_title(self.u), creator=self.u)
video(is_draft=True, creator=self.u)
response = post(self.client, 'gallery.gallery',
{'title': 'zTestz'}, args=['image'])
eq_(200, response.status_code)
@ -148,6 +141,20 @@ class GalleryUploadTestCase(TestCase):
# Preview for all 3 video formats: flv, ogv, webm
eq_('zTestz', doc('#gallery-upload-modal input[name="title"]').val())
def test_modal_locale_selected(self):
"""Locale value is selected for upload modal."""
response = get(self.client, 'gallery.gallery', args=['image'],
locale='fr')
doc = pq(response.content)
eq_('fr',
doc('#gallery-upload-image option[selected="selected"]').val())
eq_('fr',
doc('#gallery-upload-video option[selected="selected"]').val())
def test_invalid_messages(self):
# TODO(paul) POSTing invalid data shows error messages and pre-fills
raise SkipTest
class MediaPageCase(TestCase):
fixtures = ['users.json']

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

@ -9,7 +9,6 @@ from pyquery import PyQuery as pq
from gallery import forms
from gallery.models import Image, Video
from gallery.tests import image, video
from gallery.utils import get_draft_title
from gallery.views import _get_media_info
from sumo.tests import post, LocalizingClient, TestCase
from sumo.urlresolvers import reverse
@ -137,7 +136,7 @@ class UploadImageTests(TestCase):
eq_('pcraciunoiu', img.creator.username)
eq_(150, img.file.width)
eq_(200, img.file.height)
eq_(get_draft_title(img.creator), img.title)
assert 'pcraciunoiu' in img.title
eq_('Autosaved draft.', img.description)
eq_('en-US', img.locale)
@ -213,7 +212,7 @@ class UploadImageTests(TestCase):
def test_upload_draft_image(self):
"""Uploading draft image works, sets locale too."""
u = User.objects.get(username='pcraciunoiu')
img = image(creator=u, title=get_draft_title(u))
img = image(creator=u, is_draft=True)
# No thumbnail yet.
eq_(None, img.thumbnail)
@ -229,16 +228,17 @@ class UploadImageTests(TestCase):
eq_('Auf wiedersehen!', img.description)
# Thumbnail generated after form is saved.
eq_(90, img.thumbnail.width)
eq_(None, img.is_draft)
def test_image_title_locale_unique_validation(self):
"""Posting an existing locale/title combination shows a validation
error."""
u = User.objects.get(username='pcraciunoiu')
image(creator=u, title=get_draft_title(u))
image(creator=u, is_draft=True, title='Some title')
post(self.client, 'gallery.upload',
{'locale': 'de', 'title': 'Hasta la vista',
'description': 'Auf wiedersehen!'}, args=['image'])
image(creator=u, title=get_draft_title(u))
image(creator=u, is_draft=True, title='Some title')
r = post(self.client, 'gallery.upload',
{'locale': 'de', 'title': 'Hasta la vista',
'description': 'Auf wiedersehen!'},
@ -305,7 +305,7 @@ class UploadVideoTests(TestCase):
eq_(32, file['height'])
assert file['url'].endswith(vid.get_absolute_url())
eq_('pcraciunoiu', vid.creator.username)
eq_(get_draft_title(vid.creator), vid.title)
assert 'pcraciunoiu' in vid.title
eq_('Autosaved draft.', vid.description)
eq_('en-US', vid.locale)
with open(TEST_VID['ogv']) as f:
@ -424,6 +424,7 @@ class UploadVideoTests(TestCase):
# Thumbnail and poster generated after form is saved.
eq_(150, vid.poster.width)
eq_(90, vid.thumbnail.width)
eq_(None, vid.is_draft)
class SearchTests(TestCase):

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

@ -2,7 +2,6 @@ from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.core.files import File
from gallery import DRAFT_TITLE_PREFIX
from gallery.forms import ImageForm, VideoForm
from gallery.models import Image, Video
from sumo.urlresolvers import reverse
@ -15,14 +14,20 @@ def create_image(files, user):
up_file = files.values()[0]
check_file_size(up_file, settings.IMAGE_MAX_FILESIZE)
# Async uploads fallback to these defaults.
title = get_draft_title(user)
description = u'Autosaved draft.'
# Use default locale to make sure a user can only have one draft
locale = settings.WIKI_DEFAULT_LANGUAGE
try:
image = Image.objects.filter(creator=user, is_draft=True)
# Delete other drafts, if any:
image.exclude(pk=image[0].pk).delete()
image = image[0]
except IndexError: # No drafts, create one
image = Image(creator=user, is_draft=True)
image = Image(title=title, creator=user, locale=locale,
description=description)
# Async uploads fallback to these defaults.
image.title = get_draft_title(user)
image.description = u'Autosaved draft.'
image.locale = settings.WIKI_DEFAULT_LANGUAGE
# Finally save the image along with uploading the file.
image.file.save(up_file.name, File(up_file), save=True)
(width, height) = _scale_dimensions(image.file.width, image.file.height)
@ -40,16 +45,18 @@ def upload_image(request):
def create_video(files, user):
"""Given an uploaded file, a user, and other data, it creates a Video"""
# Async uploads fallback to these defaults.
title = get_draft_title(user)
description = u'Autosaved draft.'
# Use default locale to make sure a user can only have one draft
locale = settings.WIKI_DEFAULT_LANGUAGE
try:
vid = Video.objects.get(title=title, locale=locale)
except Video.DoesNotExist:
vid = Video(title=title, creator=user, description=description,
locale=locale)
vid = Video.objects.filter(creator=user, is_draft=True)
# Delete other drafts, if any:
vid.exclude(pk=vid[0].pk).delete()
vid = vid[0]
except IndexError: # No drafts, create one
vid = Video(creator=user, is_draft=True)
# Async uploads fallback to these defaults.
vid.title = get_draft_title(user)
vid.description = u'Autosaved draft.'
vid.locale = settings.WIKI_DEFAULT_LANGUAGE
for name in files:
up_file = files[name]
check_file_size(up_file, settings.VIDEO_MAX_FILESIZE)
@ -96,4 +103,4 @@ def check_media_permissions(media, user, perm_type):
def get_draft_title(user):
return DRAFT_TITLE_PREFIX + str(user.pk)
return u'Draft for user %s' % user.username

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

@ -14,7 +14,7 @@ import jingo
from tower import ugettext_lazy as _lazy
from access.decorators import login_required
from gallery import ITEMS_PER_PAGE, DRAFT_TITLE_PREFIX
from gallery import ITEMS_PER_PAGE
from gallery.forms import ImageForm, VideoForm, UploadTypeForm
from gallery.models import Image, Video
from gallery.utils import upload_image, upload_video, check_media_permissions
@ -45,8 +45,8 @@ def gallery(request, media_type='image'):
media = paginate(request, media_qs, per_page=ITEMS_PER_PAGE)
draft = _get_draft_info(request.user)
image_form, video_form, upload_type_form = _init_forms(request, draft)
drafts = _get_drafts(request.user)
image_form, video_form, upload_type_form = _init_forms(request, drafts)
return jingo.render(request, 'gallery/gallery.html',
{'media': media,
@ -60,12 +60,12 @@ def gallery(request, media_type='image'):
@require_POST
def upload(request, media_type='image'):
"""Finalizes an uploaded draft."""
draft = _get_draft_info(request.user)
if media_type == 'image' and draft['image']:
drafts = _get_drafts(request.user)
if media_type == 'image' and drafts['image']:
# We're publishing an image draft!
image_form = _init_media_form(ImageForm, request, draft['image'])
image_form = _init_media_form(ImageForm, request, drafts['image'][0])
if image_form.is_valid():
img = image_form.save()
img = image_form.save(is_draft=None)
generate_thumbnail.delay(img, 'file', 'thumbnail')
# TODO: We can drop this when we start using Redis.
invalidate = Image.objects.exclude(pk=img.pk)
@ -74,11 +74,11 @@ def upload(request, media_type='image'):
return HttpResponseRedirect(img.get_absolute_url())
else:
return gallery(request, media_type='image')
elif media_type == 'video' and draft['video']:
elif media_type == 'video' and drafts['video']:
# We're publishing a video draft!
video_form = _init_media_form(VideoForm, request, draft['video'])
video_form = _init_media_form(VideoForm, request, drafts['video'][0])
if video_form.is_valid():
vid = video_form.save()
vid = video_form.save(is_draft=None)
if vid.thumbnail:
generate_thumbnail.delay(vid, 'poster', 'poster',
max_size=settings.WIKI_VIDEO_WIDTH)
@ -98,22 +98,22 @@ def upload(request, media_type='image'):
@require_POST
def cancel_draft(request, media_type='image'):
"""Delete an existing draft for the user."""
draft = _get_draft_info(request.user)
if media_type == 'image' and draft['image']:
draft['image'].delete()
draft['image'] = None
elif media_type == 'video' and draft['video']:
drafts = _get_drafts(request.user)
if media_type == 'image' and drafts['image']:
drafts['image'].delete()
drafts['image'] = None
elif media_type == 'video' and drafts['video']:
delete_file = request.GET.get('field')
if delete_file not in ('flv', 'ogv', 'webm', 'thumbnail'):
delete_file = None
if delete_file and getattr(draft['video'], delete_file):
getattr(draft['video'], delete_file).delete()
if delete_file == 'thumbnail' and draft['video'].poster:
draft['video'].poster.delete()
if delete_file and getattr(drafts['video'][0], delete_file):
getattr(drafts['video'][0], delete_file).delete()
if delete_file == 'thumbnail' and drafts['video'][0].poster:
drafts['video'][0].poster.delete()
elif not delete_file:
draft['video'].delete()
draft['video'] = None
drafts['video'].delete()
drafts['video'] = None
else:
msg = u'Unrecognized request or nothing to cancel.'
mimetype = None
@ -280,18 +280,13 @@ def _get_media_info(media_id, media_type):
return (media, media_format)
def _get_draft_info(user):
def _get_drafts(user):
"""Get video and image drafts for a given user."""
draft = {'image': None, 'video': None}
drafts = {'image': None, 'video': None}
if user.is_authenticated():
title = DRAFT_TITLE_PREFIX + str(user.pk)
draft['image'] = Image.objects.filter(
creator=user, title=title, locale=settings.WIKI_DEFAULT_LANGUAGE)
draft['image'] = draft['image'][0] if draft['image'] else None
draft['video'] = Video.objects.filter(
creator=user, title=title, locale=settings.WIKI_DEFAULT_LANGUAGE)
draft['video'] = draft['video'][0] if draft['video'] else None
return draft
drafts['image'] = Image.objects.filter(creator=user, is_draft=True)
drafts['video'] = Video.objects.filter(creator=user, is_draft=True)
return drafts
def _init_media_form(form_cls, request=None, obj=None,
@ -316,21 +311,16 @@ def _init_media_form(form_cls, request=None, obj=None,
for f in ignore_fields:
post_data[f] = getattr(obj, f)
if ('title' in post_data and
post_data['title'].startswith(DRAFT_TITLE_PREFIX)):
post_data['title'] = ''
if obj and obj.title.startswith(DRAFT_TITLE_PREFIX):
obj.title = ''
return form_cls(post_data, file_data, instance=obj, initial=initial,
is_ajax=False)
def _init_forms(request, draft):
def _init_forms(request, drafts):
"""Initialize video and image upload forms given the request and drafts."""
image_form = _init_media_form(ImageForm, request, draft['image'])
video_form = _init_media_form(VideoForm, request, draft['video'])
image = drafts['image'][0] if drafts['image'] else None
image_form = _init_media_form(ImageForm, request, image)
video = drafts['video'][0] if drafts['video'] else None
video_form = _init_media_form(VideoForm, request, video)
upload_type_form = UploadTypeForm()
if request.method == 'POST':
image_form.is_valid()

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

@ -242,6 +242,14 @@ a.btn-upload {
margin-right: 2em;
}
#gallery-upload-modal {
display: none;
}
.kbox-wrap #gallery-upload-modal {
display: block;
}
#gallery-upload-modal .row-left {
float: left;
font-weight: bold;
@ -299,6 +307,8 @@ article.main #gallery-upload-modal input[name="cancel"] {
opacity: 0;
position: relative;
-moz-transition: opacity .5s ease-in-out;
-webkit-transition: opacity .5s ease-in-out;
transition: opacity .5s ease-in-out;
z-index: 0;
}
@ -306,6 +316,8 @@ article.main #gallery-upload-modal input[name="cancel"] {
opacity: 1;
position: relative;
-moz-transition: opacity .5s ease-in-out;
-webkit-transition: opacity .5s ease-in-out;
transition: opacity .5s ease-in-out;
z-index: 100;
}

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

@ -323,8 +323,9 @@
.removeClass('kbox-cancel')
.appendTo($preview_area.filter('.row-right'))
.makeCancelUpload();
// Show the preview area
// Show the preview area and make it a draft
$preview_area.showFade();
$form.addClass('draft');
},
/*
* Little helper function to set an error next to the file input.
@ -407,6 +408,7 @@
$form.find('.metadata').hide();
$('#gallery-upload-type').find('input[type="radio"]')
.attr('disabled', '');
$form.removeClass('draft');
}
// finally, show the input again
$form.find('.upload-media.' + type).showFade();
@ -452,16 +454,19 @@
modalClose: function() {
var self = this,
checked_index = self.$radios.index(self.$radios.filter(':checked')),
csrf = $('input[name="csrfmiddlewaretoken"]').first().val();
var $input = $('.upload-action input[name="cancel"]', self.$modal)
csrf = $('input[name="csrfmiddlewaretoken"]').first().val(),
$input = $('.upload-action input[name="cancel"]', self.$modal)
.eq(checked_index);
$.ajax({
url: $input.data('action'),
type: 'POST',
data: 'csrfmiddlewaretoken=' + csrf,
dataType: 'json'
// Ignore the response, nothing to do.
});
if (self.$modal.find('.draft').length) {
// If there's a draft to cancel.
$.ajax({
url: $input.data('action'),
type: 'POST',
data: 'csrfmiddlewaretoken=' + csrf,
dataType: 'json'
// Ignore the response, nothing to do.
});
}
self.modalReset();
},
/*
@ -506,7 +511,8 @@
var kbox = $('#gallery-upload-modal').kbox({preClose: preClose});
// Got draft? Show it.
if ($('#gallery-upload-type').hasClass('draft')) {
if (document.location.hash === '#upload' ||
$('#gallery-upload-type').hasClass('draft')) {
$('#btn-upload').click();
}
})(jQuery);

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

@ -0,0 +1,20 @@
-- Add is_draft column to gallery media
ALTER TABLE `gallery_video`
ADD `is_draft` bool DEFAULT NULL;
ALTER TABLE `gallery_image`
ADD `is_draft` bool DEFAULT NULL;
ALTER TABLE `gallery_video`
ADD UNIQUE `gallery_video_is_draft_creator_id` (`is_draft`, `creator_id`);
ALTER TABLE `gallery_image`
ADD UNIQUE `gallery_image_is_draft_creator_id` (`is_draft`, `creator_id`);
-- Drop an unnecessary index on locale
ALTER TABLE `gallery_video`
DROP INDEX `gallery_video_locale`;
ALTER TABLE `gallery_image`
DROP INDEX `gallery_image_locale`;