Skip to content

Commit

Permalink
Fix bug 1132961: Add cache to twitter feeds.
Browse files Browse the repository at this point in the history
  • Loading branch information
pmac committed Feb 13, 2015
1 parent fa436fa commit ec262fe
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 15 deletions.
23 changes: 23 additions & 0 deletions bedrock/mozorg/models.py
@@ -1,10 +1,33 @@
from django.core.cache import cache
from django.db import models
from django.db.utils import DatabaseError

from picklefield import PickledObjectField
from django_extensions.db.fields import ModificationDateTimeField


class TwitterCacheManager(models.Manager):
def get_tweets_for(self, account):
cache_key = 'tweets-for-' + str(account)
tweets = cache.get(cache_key)
if tweets is None:
try:
tweets = TwitterCache.objects.get(account=account).tweets
except (TwitterCache.DoesNotExist, DatabaseError):
# TODO: see if we should catch other errors
tweets = []

cache.set(cache_key, tweets, 60 * 60 * 6) # 6 hours, same as cron

return tweets


class TwitterCache(models.Model):
account = models.CharField(max_length=100, db_index=True, unique=True)
tweets = PickledObjectField(default=list)
updated = ModificationDateTimeField()

objects = TwitterCacheManager()

def __unicode__(self):
return u'Tweets from @' + self.account
57 changes: 57 additions & 0 deletions bedrock/mozorg/tests/test_models.py
@@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

from django.core.cache import cache
from django.db.utils import DatabaseError

from mock import patch

from bedrock.mozorg.models import TwitterCache
from bedrock.mozorg.tests import TestCase


@patch.object(TwitterCache.objects, 'get')
class TestTwitterCacheManager(TestCase):
def setUp(self):
cache.clear()

def test_results_cached(self, get_mock):
"""Results from get_tweets_for() should be cached."""
get_mock.return_value.tweets = ['dude']

tweets = TwitterCache.objects.get_tweets_for('dude')
self.assertEqual(['dude'], tweets)

tweets = TwitterCache.objects.get_tweets_for('dude')
self.assertEqual(['dude'], tweets)

get_mock.assert_called_once_with(account='dude')

get_mock.return_value.tweets = ['donny']

tweets = TwitterCache.objects.get_tweets_for('donny')
self.assertEqual(['donny'], tweets)

tweets = TwitterCache.objects.get_tweets_for('donny')
self.assertEqual(['donny'], tweets)

self.assertEqual(get_mock.call_count, 2)

def test_errors_fail_silently(self, get_mock):
"""Errors should return an empty list"""
get_mock.side_effect = TwitterCache.DoesNotExist
self.assertEqual(TwitterCache.objects.get_tweets_for('dude'), [])
self.assertEqual(TwitterCache.objects.get_tweets_for('dude'), [])

# and even errors should be cached
get_mock.assert_called_once_with(account='dude')

get_mock.reset_mock()
get_mock.side_effect = DatabaseError
self.assertEqual(TwitterCache.objects.get_tweets_for('walter'), [])
self.assertEqual(TwitterCache.objects.get_tweets_for('walter'), [])

# and even errors should be cached
get_mock.assert_called_once_with(account='walter')
15 changes: 9 additions & 6 deletions bedrock/mozorg/tests/test_views.py
Expand Up @@ -5,6 +5,8 @@

from django.conf import settings
from django.core import mail
from django.core.cache import cache
from django.db.utils import DatabaseError
from django.test.client import RequestFactory
from django.test.utils import override_settings
from django.utils import simplejson
Expand Down Expand Up @@ -249,29 +251,30 @@ def setUp(self):
self.rf = RequestFactory()
self.get_req = self.rf.get('/')
self.no_exist = views.TwitterCache.DoesNotExist()
cache.clear()

@patch.object(views.l10n_utils, 'render')
@patch.object(views.TwitterCache, 'objects')
@patch.object(views.TwitterCache.objects, 'get')
def test_db_exception_works(self, mock_manager, mock_render):
"""View should function properly without the DB."""
mock_manager.get.side_effect = views.DatabaseError
mock_manager.side_effect = DatabaseError
views.contribute_studentambassadors_landing(self.get_req)
mock_render.assert_called_with(ANY, ANY, {'tweets': []})

@patch.object(views.l10n_utils, 'render')
@patch.object(views.TwitterCache, 'objects')
@patch.object(views.TwitterCache.objects, 'get')
def test_no_db_row_works(self, mock_manager, mock_render):
"""View should function properly without data in the DB."""
mock_manager.get.side_effect = views.TwitterCache.DoesNotExist
mock_manager.side_effect = views.TwitterCache.DoesNotExist
views.contribute_studentambassadors_landing(self.get_req)
mock_render.assert_called_with(ANY, ANY, {'tweets': []})

@patch.object(views.l10n_utils, 'render')
@patch.object(views.TwitterCache, 'objects')
@patch.object(views.TwitterCache.objects, 'get')
def test_db_cache_works(self, mock_manager, mock_render):
"""View should use info returned by DB."""
good_val = 'The Dude tweets, man.'
mock_manager.get.return_value.tweets = good_val
mock_manager.return_value.tweets = good_val
views.contribute_studentambassadors_landing(self.get_req)
mock_render.assert_called_with(ANY, ANY, {'tweets': good_val})

Expand Down
11 changes: 2 additions & 9 deletions bedrock/mozorg/views.py
Expand Up @@ -8,7 +8,6 @@
from django.conf import settings
from django.contrib.staticfiles.finders import find as find_static
from django.core.context_processors import csrf
from django.db.utils import DatabaseError
from django.http import HttpResponseRedirect
from django.views.decorators.csrf import csrf_exempt, csrf_protect
from django.views.decorators.http import last_modified, require_safe
Expand Down Expand Up @@ -299,10 +298,7 @@ def plugincheck(request, template='mozorg/plugincheck.html'):

@xframe_allow
def contribute_studentambassadors_landing(request):
try:
tweets = TwitterCache.objects.get(account='mozstudents').tweets
except (TwitterCache.DoesNotExist, DatabaseError):
tweets = []
tweets = TwitterCache.objects.get_tweets_for('mozstudents')
return l10n_utils.render(request,
'mozorg/contribute/studentambassadors/landing.html',
{'tweets': tweets})
Expand Down Expand Up @@ -395,10 +391,7 @@ def get_context_data(self, **kwargs):
def home_tweets(locale):
account = settings.HOMEPAGE_TWITTER_ACCOUNTS.get(locale)
if account:
try:
return TwitterCache.objects.get(account=account).tweets
except (TwitterCache.DoesNotExist, DatabaseError):
pass # TODO: see if we should catch other errors
return TwitterCache.objects.get_tweets_for(account)
return []


Expand Down
2 changes: 2 additions & 0 deletions bedrock/settings/__init__.py
Expand Up @@ -38,6 +38,8 @@
if len(sys.argv) > 1 and sys.argv[1] == 'test':
# Using the CachedStaticFilesStorage for tests breaks all the things.
STATICFILES_STORAGE = 'pipeline.storage.PipelineStorage'
# Turn off less compilation in tests
PIPELINE_ENABLED = True
# TEMPLATE_DEBUG has to be True for jingo to call the template_rendered
# signal which Django's test client uses to save away the contexts for your
# test to look at later.
Expand Down

0 comments on commit ec262fe

Please sign in to comment.