Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Bug 840716: Download thumbnail image instead of storing URLs.

- Adds an admin command to download thumbnails for existing videos.
- Alters the approval processing to happen synchronously on save instead
  of asynchronously via celery to help lower the risk of making a video
  public before it is fully processed.
- Move approval email outside of process_approval to avoid emailing the
  user before the video changes have completed and been saved 
  successfully.
- Bug 840717: Only show pagination if there is more than one page of
  results.
- send_approval_email now uses the default locale if the user doesn't
  have a locale or profile.
- Improve test patching to avoid contacting outside services during
  tests.
  • Loading branch information...
commit a3028ebacf40607abb76962fa5c0c286fe037733 1 parent dc67518
@Osmose Osmose authored
View
2  .gitignore
@@ -20,5 +20,5 @@ tmp/*
flicks/locale/templates
flicks/locale/.svn
*.egg-info
-media/previews/*
+media/*
venv/*
View
5 flicks/base/admin.py
@@ -1,4 +1,4 @@
-from django.contrib import admin
+from django.contrib import admin, messages
from django.contrib.admin.models import LogEntry, DELETION
from django.core.urlresolvers import NoReverseMatch, reverse
from django.utils.html import escape
@@ -20,6 +20,9 @@ def queryset(self, request):
qs = qs.no_cache()
return qs
+ def message_user_error(self, request, msg):
+ messages.error(request, msg)
+
class LogEntryAdmin(admin.ModelAdmin):
"""
View
12 flicks/base/tests/__init__.py
@@ -6,6 +6,7 @@
from django.contrib.sessions.middleware import SessionMiddleware
from django.test.client import RequestFactory
+import requests
import test_utils
from django_browserid.tests import mock_browserid
from funfactory.urlresolvers import reverse
@@ -31,15 +32,12 @@ def request(self, *args, **kwargs):
class TestCase(test_utils.TestCase):
"""Base class for Flicks test cases."""
def setUp(self):
- self.approval_patch = patch('flicks.videos.models.process_approval')
- self.approval_patch.start()
-
- self.deletion_patch = patch('flicks.videos.models.process_deletion')
- self.deletion_patch.start()
+ self.requests_patch = patch.object(requests, 'request',
+ spec=requests.Response)
+ self.requests_patch.start()
def tearDown(self):
- self.approval_patch.stop()
- self.deletion_patch.stop()
+ self.requests_patch.stop()
@contextmanager
def activate(self, locale):
View
3  flicks/urls.py
@@ -52,4 +52,7 @@ def robots_txt(request):
urlpatterns += patterns('',
(r'^404$', handler404),
(r'^500$', handler500),
+ url(r'^media/(?P<path>.*)$', 'django.views.static.serve', {
+ 'document_root': settings.MEDIA_ROOT,
+ }),
) + staticfiles_urlpatterns()
View
31 flicks/videos/admin.py
@@ -17,14 +17,14 @@ class Video2013Admin(BaseModelAdmin):
fieldsets = (
(None, {
'fields': ('title', 'user', 'created', 'vimeo_id', 'filename',
- 'description')
+ 'description', 'thumbnail')
}),
('Moderation', {
'fields': ('processed', 'approved')
})
)
- actions = ['process_videos']
+ actions = ['process_videos', 'download_thumbnails']
change_form_template = 'admin/video2013_change_form.html'
def user_full_name(self, instance):
@@ -38,12 +38,31 @@ def process_videos(self, request, queryset):
for video in queryset:
process_video(video.id)
- count = len(queryset)
- count_string = '1 video' if count == 1 else '{0} videos'.format(count)
- self.message_user(request,
- '{0} processed successfully.'.format(count_string))
+ msg = '{0} videos processed successfully.'
+ self.message_user(request, msg.format(len(queryset)))
process_videos.short_description = 'Manually run video processing'
+ def download_thumbnails(self, request, queryset):
+ """Attempt to download thumbnails for the selected videos."""
+ errors = []
+ for video in queryset:
+ try:
+ video.download_thumbnail()
+ except Exception, e:
+ msg = 'Error downloading thumbnail for "{0}": {1}'
+ errors.append(msg.format(video, e))
+
+ # Notify user of results.
+ count = len(queryset) - len(errors)
+ if count > 0:
+ msg = '{0} videos updated successfully.'
+ self.message_user(request, msg.format(count))
+
+ for error in errors:
+ self.message_user_error(request, error)
+ download_thumbnails.short_description = 'Download thumbnails from Vimeo'
+
+
admin.site.register(Video2013, Video2013Admin)
View
127 ...migrations/0023_auto__del_field_video2013_medium_thumbnail_url__del_field_video2013_la.py
@@ -0,0 +1,127 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Deleting field 'Video2013.medium_thumbnail_url'
+ db.delete_column('videos_video2013', 'medium_thumbnail_url')
+
+ # Deleting field 'Video2013.large_thumbnail_url'
+ db.delete_column('videos_video2013', 'large_thumbnail_url')
+
+ # Deleting field 'Video2013.small_thumbnail_url'
+ db.delete_column('videos_video2013', 'small_thumbnail_url')
+
+ # Adding field 'Video2013.thumbnail'
+ db.add_column('videos_video2013', 'thumbnail',
+ self.gf('django.db.models.fields.files.ImageField')(default='', max_length=100, blank=True),
+ keep_default=False)
+
+
+ def backwards(self, orm):
+ # Adding field 'Video2013.medium_thumbnail_url'
+ db.add_column('videos_video2013', 'medium_thumbnail_url',
+ self.gf('django.db.models.fields.URLField')(default='', max_length=200, blank=True),
+ keep_default=False)
+
+ # Adding field 'Video2013.large_thumbnail_url'
+ db.add_column('videos_video2013', 'large_thumbnail_url',
+ self.gf('django.db.models.fields.URLField')(default='', max_length=200, blank=True),
+ keep_default=False)
+
+ # Adding field 'Video2013.small_thumbnail_url'
+ db.add_column('videos_video2013', 'small_thumbnail_url',
+ self.gf('django.db.models.fields.URLField')(default='', max_length=200, blank=True),
+ keep_default=False)
+
+ # Deleting field 'Video2013.thumbnail'
+ db.delete_column('videos_video2013', 'thumbnail')
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'videos.award': {
+ 'Meta': {'object_name': 'Award'},
+ 'award_type': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'category': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'preview': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'blank': 'True'}),
+ 'region': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
+ 'video': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['videos.Video2012']", 'null': 'True', 'blank': 'True'})
+ },
+ 'videos.video2012': {
+ 'Meta': {'object_name': 'Video2012'},
+ 'bitly_link_db': ('django.db.models.fields.URLField', [], {'default': "''", 'max_length': '200', 'blank': 'True'}),
+ 'category': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'judge_mark': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'region': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'shortlink': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
+ 'state': ('django.db.models.fields.CharField', [], {'default': "'unsent'", 'max_length': '10'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'upload_url': ('django.db.models.fields.URLField', [], {'default': "''", 'max_length': '200'}),
+ 'user_country': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+ 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'user_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100'}),
+ 'views': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}),
+ 'votes': ('django.db.models.fields.BigIntegerField', [], {'default': '0'})
+ },
+ 'videos.video2013': {
+ 'Meta': {'object_name': 'Video2013'},
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'processed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'thumbnail': ('django.db.models.fields.files.ImageField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'user_notified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'vimeo_id': ('django.db.models.fields.IntegerField', [], {})
+ }
+ }
+
+ complete_apps = ['videos']
View
77 flicks/videos/models.py
@@ -1,17 +1,24 @@
+import os
from datetime import datetime
from django.conf import settings
from django.contrib.auth.models import User
+from django.core.files.base import ContentFile
from django.db import models
from django.dispatch import receiver
import jinja2
+import requests
from caching.base import CachingManager, CachingMixin
+from funfactory.helpers import static
+from requests.exceptions import RequestException
from tower import ugettext as _, ugettext_lazy as _lazy
from flicks.base.util import get_object_or_none
-from flicks.videos.tasks import process_approval, process_deletion
-from flicks.videos.util import vidly_embed_code, vimeo_embed_code
+from flicks.videos import vimeo
+from flicks.videos.tasks import process_deletion
+from flicks.videos.util import (send_approval_email, vidly_embed_code,
+ vimeo_embed_code)
class Video2013(models.Model, CachingMixin):
@@ -27,9 +34,11 @@ class Video2013(models.Model, CachingMixin):
processed = models.BooleanField(default=False)
user_notified = models.BooleanField(default=False)
- small_thumbnail_url = models.URLField(blank=True)
- medium_thumbnail_url = models.URLField(blank=True)
- large_thumbnail_url = models.URLField(blank=True)
+ def _thumbnail_path(self, filename):
+ root, ext = os.path.splitext(filename)
+ return 'vimeo_thumbs/{0}{1}'.format(self.vimeo_id, ext)
+ thumbnail = models.ImageField(blank=True, upload_to=_thumbnail_path,
+ max_length=settings.MAX_FILEPATH_LENGTH)
objects = CachingManager()
@@ -37,17 +46,32 @@ class Video2013(models.Model, CachingMixin):
def region(self):
return self.user.userprofile.region
+ @property
+ def thumbnail_url(self):
+ return (self.thumbnail.url if self.thumbnail else
+ static('img/video-blank.jpg'))
+
def save(self, *args, **kwargs):
"""
- Prior to saving, set the video's privacy on Vimeo depending on if it is
- approved.
+ Prior to saving, trigger approval processing if approval status has
+ changed.
"""
original = get_object_or_none(Video2013, id=self.id)
- return_value = super(Video2013, self).save(*args, **kwargs)
# Only process approval if the value changed.
- if not original or original.approved != self.approved:
- process_approval.delay(self.id)
+ if original and original.approved != self.approved:
+ self.process_approval()
+
+ # Save after processing to prevent making the video public until
+ # processing is complete.
+ return_value = super(Video2013, self).save(*args, **kwargs)
+
+ # Send an email out if the user hasn't been notified and re-save.
+ # Don't send an email out if this is a new, already-approved video.
+ if original and self.approved and not self.user_notified:
+ send_approval_email(self)
+ self.user_notified = True
+ return_value = super(Video2013, self).save(*args, **kwargs)
return return_value
@@ -55,8 +79,37 @@ def embed_html(self, **kwargs):
"""Return the HTML code to embed this video."""
return jinja2.Markup(vimeo_embed_code(self.vimeo_id, **kwargs))
- def thumbnail(self, size):
- return getattr(self, '{0}_thumbnail_url'.format(size), '')
+ def process_approval(self):
+ """Update privacy and gather more metadata based on approval status."""
+ if self.approved:
+ # Fetch the medium-sized thumbail from Vimeo. If it isn't available,
+ # ignore and continue; a default thumbnail will be used.
+ try:
+ self.download_thumbnail(commit=False)
+ except (RequestException, vimeo.VimeoServiceError):
+ pass
+
+ vimeo.set_privacy(self.vimeo_id, 'anybody')
+ else:
+ vimeo.set_privacy(self.vimeo_id, 'password',
+ password=settings.VIMEO_VIDEO_PASSWORD)
+
+ def download_thumbnail(self, commit=True):
+ """Download the thumbnail for the given video and save it."""
+ thumbnails = vimeo.get_thumbnail_urls(self.vimeo_id)
+ try:
+ thumbnail = next(t for t in thumbnails if t['height'] == '150')
+ except StopIteration:
+ raise vimeo.VimeoServiceError('No medium thumbnail found.')
+
+ response = requests.get(thumbnail['_content'])
+
+ filename = response.url.rsplit('/')[-1]
+ content_file = ContentFile(response.content)
+ self.thumbnail.save(filename, content_file, save=False)
+
+ if commit:
+ self.save()
def __unicode__(self):
profile = self.user.profile
View
35 flicks/videos/tasks.py
@@ -6,7 +6,7 @@
from flicks.base.util import get_object_or_none
from flicks.videos.decorators import vimeo_task
-from flicks.videos.util import send_approval_email, send_rejection_email
+from flicks.videos.util import send_rejection_email
# We explicitly import flicks.videos.vimeo here in order to register the Vimeo
@@ -56,39 +56,6 @@ def process_video(video_id):
@vimeo_task
-def process_approval(video_id):
- """Update privacy and gather more metadata once a video is approved."""
- from flicks.videos.models import Video
-
- video = get_object_or_none(Video, id=video_id)
- if video:
- if video.approved:
- vimeo.set_privacy(video.vimeo_id, 'anybody')
-
- # Send an email out if the user hasn't been notified.
- if not video.user_notified:
- send_approval_email(video)
- video.user_notified = True
-
- # Pull the thumbnail url for the video. We pull it here instead of
- # during processing to give Vimeo a chance to finish encoding and
- # processing the video on their side.
- thumbnails = vimeo.get_thumbnail_urls(video.vimeo_id)
- for thumbnail in thumbnails:
- if thumbnail['height'] == '75':
- video.small_thumbnail_url = thumbnail['_content']
- elif thumbnail['height'] == '150':
- video.medium_thumbnail_url = thumbnail['_content']
- elif thumbnail['height'] == '480':
- video.large_thumbnail_url = thumbnail['_content']
-
- video.save()
- else:
- vimeo.set_privacy(video.vimeo_id, 'password',
- password=settings.VIMEO_VIDEO_PASSWORD)
-
-
-@vimeo_task
def process_deletion(vimeo_id, user_id):
vimeo.delete_video(vimeo_id)
send_rejection_email(user_id)
View
12 flicks/videos/templates/videos/2013/list.html
@@ -28,14 +28,12 @@ <h1 class="page-title">{{ _('Welcome to the<br> Firefox Flicks 2013 gallery') }}
</div>
{% block results %}
- {# Only show results if at least 1 video is found. #}
- {% if videos.paginator.count > 1 %}
<div id="gallery" class="main">
<ul class="entry-list">
{% for video in videos %}
<li class="entry">
<a href="{{ url('flicks.videos.detail', video.id) }}">
- <img src="{{ video.thumbnail('medium') }}" alt="" class="thumbnail">
+ <img src="{{ video.thumbnail_url }}" alt="" class="thumbnail">
<hgroup>
<h2 class="title">{{ video.title }}</h2>
<h3 class="vcard">
@@ -54,10 +52,12 @@ <h3 class="vcard">
{% endfor %}
</ul>
- {{ pagination(videos, url('flicks.videos.list'),
- region=request.GET.get('region', None)) }}
+ {# Only show pagination if there is more than one page of results. #}
+ {% if videos.paginator.num_pages > 1 %}
+ {{ pagination(videos, url('flicks.videos.list'),
+ region=request.GET.get('region', None)) }}
+ {% endif %}
</div>
- {% endif %}
{% endblock %}
<aside id="trailers" class="samples aside">
View
3  flicks/videos/tests/__init__.py
@@ -4,7 +4,7 @@
from flicks.videos import models
-class VideoFactory(Factory):
+class Video2013Factory(Factory):
FACTORY_FOR = models.Video
user = SubFactory(UserFactory)
@@ -12,6 +12,7 @@ class VideoFactory(Factory):
description = 'Test desc'
vimeo_id = Sequence(lambda n: int(n))
filename = Sequence(lambda n: '{0}.mp4'.format(n))
+VideoFactory = Video2013Factory
class Video2012Factory(Factory):
View
191 flicks/videos/tests/test_models.py
@@ -1,55 +1,91 @@
-from mock import patch
-from nose.tools import ok_
+from mock import ANY, Mock, patch
+from nose.tools import eq_, ok_
+from requests.exceptions import RequestException
from flicks.base.tests import TestCase
from flicks.users.tests import UserProfileFactory
-from flicks.videos.models import Video
-from flicks.videos.tests import VideoFactory
+from flicks.videos.models import Video2013
+from flicks.videos.tests import Video2013Factory
+from flicks.videos.vimeo import VimeoServiceError
class Video2013Tests(TestCase):
- @patch('flicks.videos.models.process_approval')
- def test_save_process_new(self, process_approval):
- """Trigger the process_approval task if the video is new."""
+ @patch('flicks.videos.models.send_approval_email')
+ def test_save_process_new(self, send_approval_email):
+ """
+ Do not call process_approval or send_approval_email if the video is new.
+ """
user = UserProfileFactory.create().user
- video = VideoFactory.build(title='blahtest', user=user)
-
- process_approval.delay.reset_mock()
- ok_(not process_approval.delay.called)
+ video = Video2013Factory.build(title='blahtest', user=user)
+ video.process_approval = Mock()
video.save()
- video = Video.objects.get(title='blahtest')
- process_approval.delay.assert_called_with(video.id)
- @patch('flicks.videos.models.process_approval')
- def test_save_process_changed(self, process_approval):
- """Trigger the process_approval task if the approval status changed."""
- video = VideoFactory.create(approved=False)
+ ok_(not video.process_approval.called)
+ ok_(not send_approval_email.called)
+
+ def test_save_process_changed(self):
+ """Call process_approval if the approval status changed."""
+ video = Video2013Factory.create(approved=False)
+ video.process_approval = Mock()
- process_approval.delay.reset_mock()
- ok_(not process_approval.delay.called)
video.approved = True
video.save()
- process_approval.delay.assert_called_with(video.id)
+ video.process_approval.assert_called_with()
- process_approval.delay.reset_mock()
- ok_(not process_approval.delay.called)
+ video.process_approval.reset_mock()
+ ok_(not video.process_approval.called)
video.approved = False
video.save()
- process_approval.delay.assert_called_with(video.id)
+ video.process_approval.assert_called_with()
- @patch('flicks.videos.models.process_approval')
- def test_save_noprocess_old(self, process_approval):
+ def test_save_noprocess_old(self):
"""
- Do not rigger the process_approval task if the approval status did not
- change.
+ Do not call process_approval if the approval status did not change.
"""
- video = VideoFactory.create(approved=False)
+ video = Video2013Factory.create(approved=False)
+ video.process_approval = Mock()
- process_approval.delay.reset_mock()
- ok_(not process_approval.delay.called)
video.title = 'new_title'
video.save()
- ok_(not process_approval.delay.called)
+ ok_(not video.process_approval.called)
+
+ @patch('flicks.videos.models.send_approval_email')
+ def test_save_unapproved_no_email(self, send_approval_email):
+ """If the video is not approved, do not call send_approval_email."""
+ video = Video2013Factory.create(approved=True, user_notified=False)
+ eq_(video.user_notified, False)
+ video.approved = False
+ video.save()
+
+ ok_(not send_approval_email.called)
+
+ @patch('flicks.videos.models.send_approval_email')
+ def test_save_approved_notified_no_email(self, send_approval_email):
+ """
+ If the video is approved and the user has already been notified, do not
+ call send_approval_email.
+ """
+ video = Video2013Factory.create(approved=False, user_notified=True)
+ eq_(video.user_notified, True)
+ video.approved = True
+ video.save()
+
+ ok_(not send_approval_email.called)
+
+ @patch('flicks.videos.models.send_approval_email')
+ def test_save_approved_not_notified(self, send_approval_email):
+ """
+ If the video is approved and the user hasn't been notified, call
+ send_approval_email and update user_notified.
+ """
+ video = Video2013Factory.create(approved=False, user_notified=False)
+ eq_(video.user_notified, False)
+ video.approved = True
+ video.save()
+
+ video = Video2013.objects.get(id=video.id)
+ send_approval_email.assert_called_with(video)
+ ok_(video.user_notified)
@patch('flicks.videos.models.process_deletion')
def test_delete_process(self, process_deletion):
@@ -57,8 +93,99 @@ def test_delete_process(self, process_deletion):
When a video is deleted, the process_deletion task should be triggered.
"""
user = UserProfileFactory.create().user
- video = VideoFactory.create(user=user, vimeo_id=123456)
+ video = Video2013Factory.create(user=user, vimeo_id=123456)
ok_(not process_deletion.delay.called)
video.delete()
process_deletion.delay.assert_called_with(123456, user.id)
+
+ @patch('flicks.videos.models.vimeo')
+ def test_process_approval(self, vimeo):
+ """
+ If the video is approved, download the thumbnails and reset the privacy
+ on vimeo to 'anybody'.
+ """
+ video = Video2013Factory.build(approved=True, user_notified=False)
+ video.download_thumbnail = Mock()
+ video.process_approval()
+
+ video.download_thumbnail.assert_called_with(commit=False)
+ vimeo.set_privacy.assert_called_with(video.vimeo_id, 'anybody')
+
+ @patch('flicks.videos.models.vimeo')
+ def test_process_approval_unapproved(self, vimeo):
+ """
+ If the video is not approved, reset the privacy on vimeo to 'password'.
+ """
+ video = Video2013Factory.build(approved=False)
+ video.download_thumbnail = Mock()
+
+ with self.settings(VIMEO_VIDEO_PASSWORD='testpass'):
+ video.process_approval()
+ vimeo.set_privacy.assert_called_with(video.vimeo_id, 'password',
+ password='testpass')
+
+ @patch('flicks.videos.models.vimeo')
+ def test_process_approval_thumbnail_fail(self, vimeo):
+ """
+ If a video is approved but downloading thumbnails has failed, continue
+ processing the video approval.
+ """
+ video = Video2013Factory.build(approved=True, user_notified=False)
+ video.download_thumbnail = Mock()
+ video.download_thumbnail.side_effect = RequestException
+ video.process_approval()
+
+ video.download_thumbnail.assert_called_with(commit=False)
+ vimeo.set_privacy.assert_called_with(video.vimeo_id, 'anybody')
+
+ @patch('flicks.videos.models.vimeo')
+ @patch('flicks.videos.models.requests')
+ def test_download_thumbnail(self, requests, vimeo):
+ video = Video2013Factory.build()
+ video.thumbnail = Mock()
+ video.save = Mock()
+
+ vimeo.get_thumbnail_urls.return_value = [
+ {'height': '250', '_content': 'http://example.com/qwer.png'},
+ {'height': '150', '_content': 'http://example.com/asdf.png'},
+ {'height': '80', '_content': 'http://example.com/zxcv.png'},
+ ]
+ requests.get.return_value = Mock(content='asdf',
+ url='http://example.com/asdf.png')
+
+ video.download_thumbnail()
+ requests.get.assert_called_with('http://example.com/asdf.png')
+ video.thumbnail.save.assert_called_with('asdf.png', ANY, save=False)
+ video.save.assert_called_with()
+
+ @patch('flicks.videos.models.vimeo')
+ def test_download_thumbnail_error(self, vimeo):
+ """If no medium-sized thumbnail is found, raise a VimeoServiceError."""
+ video = Video2013Factory.build()
+ vimeo.get_thumbnail_urls.return_value = [
+ {'height': '250', '_content': 'http://example.com/qwer.png'},
+ {'height': '80', '_content': 'http://example.com/zxcv.png'},
+ ]
+ vimeo.VimeoServiceError = VimeoServiceError
+
+ with self.assertRaises(VimeoServiceError):
+ video.download_thumbnail()
+
+ @patch('flicks.videos.models.vimeo')
+ @patch('flicks.videos.models.requests')
+ def test_commit(self, requests, vimeo):
+ """If commit is False, do not save the video."""
+ video = Video2013Factory.build()
+ video.save = Mock()
+
+ vimeo.get_thumbnail_urls.return_value = [
+ {'height': '250', '_content': 'http://example.com/qwer.png'},
+ {'height': '150', '_content': 'http://example.com/asdf.png'},
+ {'height': '80', '_content': 'http://example.com/zxcv.png'},
+ ]
+ requests.get.return_value = Mock(content='asdf',
+ url='http://example.com/asdf.png')
+
+ video.download_thumbnail(commit=False)
+ ok_(not video.save.called)
View
83 flicks/videos/tests/test_tasks.py
@@ -1,15 +1,14 @@
from django.contrib.auth.models import Permission
from mock import ANY, patch
-from nose.tools import eq_, ok_
+from nose.tools import ok_
from flicks.base import regions
from flicks.base.tests import TestCase
from flicks.base.tests.tools import CONTAINS
from flicks.users.tests import GroupFactory, UserFactory, UserProfileFactory
-from flicks.videos.models import Video
-from flicks.videos.tasks import process_approval, process_video
+from flicks.videos.tasks import process_video
from flicks.videos.tests import VideoFactory
@@ -56,81 +55,3 @@ def test_valid_video(self, send_mail, mock_vimeo):
send_mail.assert_called_with(ANY, ANY, 'blah@test.com',
CONTAINS('test1@test.com',
'test2@test.com'))
-
-
-class ProcessApprovalTests(TestCase):
- @patch('flicks.videos.tasks.vimeo')
- @patch('flicks.videos.tasks.send_approval_email')
- def test_approved_not_notified(self, send_approval_email, vimeo):
- """
- If the video is approved, reset the privacy on vimeo to 'anybody',
- and if the user hasn't been notified, send an approval email.
- """
- video = VideoFactory.create(approved=True, user_notified=False)
- vimeo.set_privacy.reset_mock()
- send_approval_email.reset_mock()
- ok_(not vimeo.set_privacy.called)
- ok_(not send_approval_email.called)
-
- process_approval(video.id)
- vimeo.set_privacy.assert_called_with(video.vimeo_id, 'anybody')
- send_approval_email.assert_called_with(video)
-
- @patch('flicks.videos.tasks.vimeo')
- @patch('flicks.videos.tasks.send_approval_email')
- def test_approved_notified(self, send_approval_email, vimeo):
- """
- If the video is approved, reset the privacy on vimeo to 'anybody', and
- if the user has already been notified, don't send a new email.
- """
- video = VideoFactory.create(user__email='blah@test.com', approved=True,
- user_notified=True)
- vimeo.set_privacy.reset_mock()
- send_approval_email.reset_mock()
- ok_(not vimeo.set_privacy.called)
- ok_(not send_approval_email.called)
-
- process_approval(video.id)
- vimeo.set_privacy.assert_called_with(video.vimeo_id, 'anybody')
- ok_(not send_approval_email.called)
-
- @patch('flicks.videos.tasks.vimeo')
- @patch('flicks.videos.tasks.send_approval_email')
- def test_unapproved(self, send_approval_email, vimeo):
- """
- If the video is not approved, reset the privacy on vimeo to 'password'.
- """
- video = VideoFactory.create(user__email='blah@test.com', approved=False)
- vimeo.set_privacy.reset_mock()
- send_approval_email.reset_mock()
- ok_(not vimeo.set_privacy.called)
- ok_(not send_approval_email.called)
-
- with self.settings(VIMEO_VIDEO_PASSWORD='testpass'):
- process_approval(video.id)
- vimeo.set_privacy.assert_called_with(video.vimeo_id, 'password',
- password='testpass')
- ok_(not send_approval_email.called)
-
- @patch('flicks.videos.tasks.vimeo')
- def test_thumbnails(self, vimeo):
- """If a video is approved, pull the latest thumbnails from Vimeo."""
- vimeo.get_thumbnail_urls.return_value = [
- {'height': '75', '_content': 'http://test1.com'},
- {'height': '150', '_content': 'http://test2.com'},
- {'height': '480', '_content': 'http://test3.com'},
- {'height': '7532', '_content': 'http://test4.com'},
- ]
-
- user = UserProfileFactory.create().user
- video = VideoFactory.create(approved=True, user=user)
- vimeo.get_thumbnail_urls.reset_mock()
- ok_(not vimeo.get_thumbnail_urls.called)
-
- process_approval(video.id)
-
- vimeo.get_thumbnail_urls.assert_called_with(video.vimeo_id)
- video = Video.objects.get(id=video.id)
- eq_(video.small_thumbnail_url, 'http://test1.com')
- eq_(video.medium_thumbnail_url, 'http://test2.com')
- eq_(video.large_thumbnail_url, 'http://test3.com')
View
18 flicks/videos/tests/test_util.py
@@ -1,10 +1,12 @@
from django.core import mail
+from django.test.utils import override_settings
+from mock import patch
from nose.tools import eq_, ok_
from pyquery import PyQuery as pq
from flicks.base.tests import TestCase
-from flicks.users.tests import UserProfileFactory
+from flicks.users.tests import UserFactory, UserProfileFactory
from flicks.videos.tests import VideoFactory
from flicks.videos.util import (send_approval_email, send_rejection_email,
vimeo_embed_code)
@@ -30,6 +32,20 @@ def test_basic(self):
eq_(len(mail.outbox), 1)
eq_(mail.outbox[0].to, ['boo@example.com'])
+ @patch('flicks.videos.util.use_lang')
+ @override_settings(LANGUAGE_CODE='fr')
+ def test_no_profile(self, use_lang):
+ """
+ If the user has no profile, use the installation's default language
+ code for the email locale.
+ """
+ user = UserFactory.create(email='bar@example.com')
+ video = VideoFactory.create(user=user)
+ send_approval_email(video)
+ eq_(len(mail.outbox), 1)
+ eq_(mail.outbox[0].to, ['bar@example.com'])
+ use_lang.assert_called_with('fr')
+
class SendRejectionEmailTests(TestCase):
def test_invalid_user_id(self):
View
5 flicks/videos/tests/test_views.py
@@ -65,8 +65,11 @@ def test_get_valid_ticket(self, vimeo):
eq_(vimeo.get_new_ticket.called, False)
self.assertTemplateUsed(response, 'videos/upload.html')
- def test_post_invalid_form(self):
+ @patch('flicks.videos.views.vimeo')
+ def test_post_invalid_form(self, vimeo):
"""If the POSTed form is invalid, redisplay the page."""
+ vimeo.is_ticket_valid.return_value = False
+ vimeo.get_new_ticket.return_value = {'id': 'qwer'}
response = self._upload('post', title=None)
self.assertTemplateUsed(response, 'videos/upload.html')
View
3  flicks/videos/util.py
@@ -50,7 +50,8 @@ def send_approval_email(video):
Send email to the video's creator telling them that their video has been
approved.
"""
- with use_lang(video.user.profile.locale):
+ profile = video.user.profile
+ with use_lang(profile.locale if profile else settings.LANGUAGE_CODE):
body = render_to_string('videos/2013/approval_email.html', {
'user': video.user,
'video': video
View
0  media/.gitinclude
No changes.
Please sign in to comment.
Something went wrong with that request. Please try again.