diff --git a/.travis.yml b/.travis.yml index 4863a0d..57dd03a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,6 @@ env: #- TOX_ENV=pypy-1.9.X #- TOX_ENV=pypy-1.8.X #- TOX_ENV=pypy-1.7.X - #- TOX_ENV=pypy-1.4.X - TOX_ENV=py35-trunk - TOX_ENV=py35-1.9.X - TOX_ENV=py34-trunk @@ -18,8 +17,6 @@ env: - TOX_ENV=py27-1.9.X - TOX_ENV=py27-1.8.X - TOX_ENV=py27-1.7.X - - TOX_ENV=py27-1.4.X - - TOX_ENV=py26-1.4.X install: - pip install coveralls - pip install tox diff --git a/fitapp/migrations/0006_add_expires_timezone_fields.py b/fitapp/migrations/0006_add_expires_timezone_fields.py new file mode 100644 index 0000000..4083496 --- /dev/null +++ b/fitapp/migrations/0006_add_expires_timezone_fields.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('fitapp', '0005_upgrade_oauth1_tokens_to_oauth2'), + ] + + operations = [ + migrations.AddField( + model_name='userfitbit', + name='expires_at', + field=models.FloatField(default=0), + preserve_default=False, + ), + migrations.AddField( + model_name='userfitbit', + name='timezone', + field=models.CharField(default='UTC', max_length=128), + preserve_default=False, + ), + ] diff --git a/fitapp/models.py b/fitapp/models.py index cff0110..3af8010 100644 --- a/fitapp/models.py +++ b/fitapp/models.py @@ -13,6 +13,9 @@ class UserFitbit(models.Model): access_token = models.TextField() auth_secret = models.TextField() refresh_token = models.TextField() + # We will store the timestamp float number as it comes from Fitbit here + expires_at = models.FloatField() + timezone = models.CharField(max_length=128) def __str__(self): return self.user.__str__() diff --git a/fitapp/tasks.py b/fitapp/tasks.py index 46ad9d3..dd7d7fe 100644 --- a/fitapp/tasks.py +++ b/fitapp/tasks.py @@ -12,22 +12,42 @@ logger = logging.getLogger(__name__) -LOCK_EXPIRE = 60 * 5 # Lock expires in 5 minutes +LOCK_EXPIRE = 60 * 5 # Lock expires in 5 minutes + + +def _hit_rate_limit(exc, task): + # We have hit the rate limit for the user, retry when it's reset, + # according to the reply from the failing API call + logger.debug('Rate limit reached, will try again in %s seconds' % + exc.retry_after_secs) + raise task.retry(exc=exc, countdown=exc.retry_after_secs) + + +def _generic_task_exception(exc, task_name): + logger.exception("Exception running task %s: %s" % (task_name, exc)) + raise Reject(exc, requeue=False) @shared_task def subscribe(fitbit_user, subscriber_id): - """ Subscribe to the user's fitbit data """ - - fbusers = UserFitbit.objects.filter(fitbit_user=fitbit_user) - for fbuser in fbusers: + """ Subscribe the user and retrieve historical data for it """ + update_user_timezone.apply_async((fitbit_user,), countdown=1) + for fbuser in UserFitbit.objects.filter(fitbit_user=fitbit_user): fb = utils.create_fitbit(**fbuser.get_user_data()) try: fb.subscription(fbuser.user.id, subscriber_id) - except: - exc = sys.exc_info()[1] - logger.exception("Error subscribing user: %s" % exc) - raise Reject(exc, requeue=False) + except HTTPTooManyRequests: + _hit_rate_limit(sys.exc_info()[1], subscribe) + except Exception: + _generic_task_exception(sys.exc_info()[1], 'subscribe') + + # Create tasks for all data in all data types + for i, _type in enumerate(TimeSeriesDataType.objects.all()): + # Delay execution for a few seconds to speed up response Offset each + # call by 5 seconds so they don't bog down the server + get_time_series_data.apply_async( + (fitbit_user, _type.category, _type.resource,), + countdown=10 + (i * 5)) @shared_task @@ -40,11 +60,10 @@ def unsubscribe(*args, **kwargs): if sub['ownerId'] == kwargs['user_id']: fb.subscription(sub['subscriptionId'], sub['subscriberId'], method="DELETE") - except: - exc = sys.exc_info()[1] - logger.exception("Error unsubscribing user: %s" % exc) - raise Reject(exc, requeue=False) - + except HTTPTooManyRequests: + _hit_rate_limit(sys.exc_info()[1], unsubscribe) + except Exception: + _generic_task_exception(sys.exc_info()[1], 'unsubscribe') @shared_task @@ -83,12 +102,7 @@ def get_time_series_data(fitbit_user, cat, resource, date=None): # Release the lock cache.delete(lock_id) except HTTPTooManyRequests: - # We have hit the rate limit for the user, retry when it's reset, - # according to the reply from the failing API call - e = sys.exc_info()[1] - logger.debug('Rate limit reached, will try again in %s seconds' % - e.retry_after_secs) - raise get_time_series_data.retry(exc=e, countdown=e.retry_after_secs) + _hit_rate_limit(sys.exc_info()[1], get_time_series_data) except HTTPBadRequest: # If the resource is elevation or floors, we are just getting this # error because the data doesn't exist for this user, so we can ignore @@ -98,6 +112,22 @@ def get_time_series_data(fitbit_user, cat, resource, date=None): logger.exception("Exception updating data: %s" % exc) raise Reject(exc, requeue=False) except Exception: - exc = sys.exc_info()[1] - logger.exception("Exception updating data: %s" % exc) - raise Reject(exc, requeue=False) + _generic_task_exception(sys.exc_info()[1], 'get_time_series_data') + + +@shared_task +def update_user_timezone(fitbit_user): + """ Get the user's profile and update the timezone we have on file """ + + fbusers = UserFitbit.objects.filter(fitbit_user=fitbit_user) + try: + for fbuser in fbusers: + fb = utils.create_fitbit(**fbuser.get_user_data()) + profile = fb.user_profile_get() + fbuser.timezone = profile['user']['timezone'] + fbuser.save() + utils.check_for_new_token(fbuser, fb.client.token) + except HTTPTooManyRequests: + _hit_rate_limit(sys.exc_info()[1], update_user_timezone) + except Exception: + _generic_task_exception(sys.exc_info()[1], 'update_user_timezone') diff --git a/fitapp/tests/base.py b/fitapp/tests/base.py index 981cc03..c95be9f 100644 --- a/fitapp/tests/base.py +++ b/fitapp/tests/base.py @@ -1,6 +1,15 @@ -from mock import patch, Mock import django +import json import random + + +from datetime import datetime +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse +from django.test import TestCase +from fitbit.api import Fitbit +from mock import patch, Mock + try: from urllib.parse import urlencode from string import ascii_letters @@ -9,12 +18,6 @@ from urllib import urlencode from string import letters as ascii_letters -from django.contrib.auth.models import User -from django.core.urlresolvers import reverse -from django.test import TestCase - -from fitbit.api import Fitbit - from fitapp.models import UserFitbit @@ -25,6 +28,7 @@ def __init__(self, **kwargs): self.client_secret = kwargs.get('client_secret', 'S12345Secret') self.access_token = kwargs.get('access_token', None) self.refresh_token = kwargs.get('refresh_token', None) + self.make_request_resp = kwargs.get('make_request_resp', {}) self.error = kwargs.get('error', None) def authorize_token_url(self, *args, **kwargs): @@ -37,6 +41,7 @@ def fetch_access_token(self, *args, **kwargs): token = { 'user_id': self.user_id, 'refresh_token': self.refresh_token, + 'expires_at': 1461103848.405841, 'token_type': 'Bearer', 'scope': ['weight', 'sleep', 'heartrate', 'activity'] } @@ -47,7 +52,7 @@ def fetch_access_token(self, *args, **kwargs): def make_request(self, *args, **kwargs): response = Mock() response.status_code = 204 - response.content = "{}".encode('utf8') + response.content = json.dumps(self.make_request_resp).encode('utf8') return response @@ -83,7 +88,10 @@ def create_userfitbit(self, **kwargs): 'fitbit_user': kwargs.pop('fitbit_user', self.random_string(25)), 'access_token': self.random_string(25), 'auth_secret': self.random_string(25), - 'refresh_token': self.random_string(25) + 'refresh_token': self.random_string(25), + # Set the token to expire on 2016-4-18 11:24:08.405841 + 'expires_at': 1461003848.405841, + 'timezone': 'America/Los_Angeles' } defaults.update(kwargs) return UserFitbit.objects.create(**defaults) diff --git a/fitapp/tests/test_integration.py b/fitapp/tests/test_integration.py index e88e7c7..1f21949 100644 --- a/fitapp/tests/test_integration.py +++ b/fitapp/tests/test_integration.py @@ -136,25 +136,34 @@ class TestCompleteView(FitappTestBase): token = { 'access_token': 'AccessToken123', 'refresh_token': 'RefreshToken123', + 'expires_at': 1461103848.405841, 'user_id': user_id } + fetch_token = { + 'user_id': 'userid', + 'access_token': 'AccessToken123', + 'refresh_token': 'RefreshToken123', + 'expires_at': 1461103848.405841, + 'token_type': 'Bearer', + 'scope': ['weight', 'sleep', 'heartrate', 'activity'] + } code = 'Code123' def setUp(self): super(TestCompleteView, self).setUp() self.fbuser.delete() - @patch('fitapp.tasks.subscribe.apply_async') + @patch('fitapp.tasks.update_user_timezone.apply_async') @patch('fitapp.tasks.get_time_series_data.apply_async') - def test_complete(self, tsd_apply_async, sub_apply_async): + def test_complete(self, tsd_apply_async, uut_apply_async): """Complete view should fetch & store user's access credentials.""" response = self._mock_client( client_kwargs=self.token, get_kwargs={'code': self.code}) self.assertRedirectsNoFollow( response, utils.get_setting('FITAPP_LOGIN_REDIRECT')) fbuser = UserFitbit.objects.get() - sub_apply_async.assert_called_once_with( - (fbuser.fitbit_user, settings.FITAPP_SUBSCRIBER_ID), countdown=5) + uut_apply_async.assert_called_once_with( + (fbuser.fitbit_user,), countdown=1) tsdts = TimeSeriesDataType.objects.all() self.assertEqual(tsd_apply_async.call_count, tsdts.count()) for i, _type in enumerate(tsdts): @@ -166,9 +175,10 @@ def test_complete(self, tsd_apply_async, sub_apply_async): self.assertEqual(fbuser.refresh_token, self.token['refresh_token']) self.assertEqual(fbuser.fitbit_user, self.user_id) - @patch('fitapp.tasks.subscribe.apply_async') + @patch('fitapp.tasks.update_user_timezone.apply_async') @patch('fitapp.tasks.get_time_series_data.apply_async') - def test_complete_already_integrated(self, tsd_apply_async, sub_apply_async): + def test_complete_already_integrated(self, tsd_apply_async, + uut_apply_async): """ Complete view redirect to the error view if a user attempts to connect an already integrated fitbit user to a second user. @@ -182,7 +192,7 @@ def test_complete_already_integrated(self, tsd_apply_async, sub_apply_async): client_kwargs=self.token, get_kwargs={'code': self.code}) self.assertRedirectsNoFollow(response, reverse('fitbit-error')) self.assertEqual(UserFitbit.objects.all().count(), 1) - self.assertEqual(sub_apply_async.call_count, 0) + self.assertEqual(uut_apply_async.call_count, 0) self.assertEqual(tsd_apply_async.call_count, 0) def test_unauthenticated(self): @@ -192,19 +202,24 @@ def test_unauthenticated(self): self.assertEqual(response.status_code, 302) self.assertEqual(UserFitbit.objects.count(), 0) - @patch('fitapp.tasks.subscribe.apply_async') + @patch('fitapp.tasks.update_user_timezone.apply_async') @patch('fitapp.tasks.get_time_series_data.apply_async') - def test_next(self, tsd_apply_async, sub_apply_async): + def test_next(self, tsd_apply_async, uut_apply_async): """ Complete view should redirect to session['fitbit_next'] if available. """ self._set_session_vars(fitbit_next='/test') + + profile = {'user': {'timezone': 'America/Los_Angeles'}} + client_kwargs = dict(list(self.token.items()) + [ + ('make_request_resp', profile,) + ]) response = self._mock_client( - client_kwargs=self.token, get_kwargs={'code': self.code}) + client_kwargs=client_kwargs, get_kwargs={'code': self.code}) self.assertRedirectsNoFollow(response, '/test') fbuser = UserFitbit.objects.get() - sub_apply_async.assert_called_once_with( - (fbuser.fitbit_user, settings.FITAPP_SUBSCRIBER_ID), countdown=5) + uut_apply_async.assert_called_once_with( + (fbuser.fitbit_user,), countdown=1) self.assertEqual( tsd_apply_async.call_count, TimeSeriesDataType.objects.count()) self.assertEqual(fbuser.user, self.user) @@ -241,17 +256,18 @@ def test_no_access_token(self): self.assertRedirectsNoFollow(response, reverse('fitbit-error')) self.assertEqual(UserFitbit.objects.count(), 0) - @patch('fitapp.tasks.subscribe.apply_async') + @patch('fitapp.tasks.update_user_timezone.apply_async') @patch('fitapp.tasks.get_time_series_data.apply_async') - def test_integrated(self, tsd_apply_async, sub_apply_async): + def test_integrated(self, tsd_apply_async, uut_apply_async): """Complete view should overwrite existing credentials for this user. """ self.fbuser = self.create_userfitbit(user=self.user) + response = self._mock_client( client_kwargs=self.token, get_kwargs={'code': self.code}) fbuser = UserFitbit.objects.get() - sub_apply_async.assert_called_with( - (fbuser.fitbit_user, settings.FITAPP_SUBSCRIBER_ID), countdown=5) + uut_apply_async.assert_called_with( + (fbuser.fitbit_user,), countdown=1) self.assertEqual(tsd_apply_async.call_count, TimeSeriesDataType.objects.count()) self.assertEqual(fbuser.user, self.user) diff --git a/fitapp/tests/test_retrieval.py b/fitapp/tests/test_retrieval.py index 2cc0a20..2c7e1ce 100644 --- a/fitapp/tests/test_retrieval.py +++ b/fitapp/tests/test_retrieval.py @@ -4,16 +4,17 @@ import json import sys +from datetime import datetime from dateutil import parser from django.core.cache import cache from django.core.files.uploadedfile import InMemoryUploadedFile from django.core.urlresolvers import reverse +from django.test import TransactionTestCase from django.test.utils import override_settings from freezegun import freeze_time from mock import MagicMock, patch from fitbit import exceptions as fitbit_exceptions -from fitbit.api import Fitbit from fitapp import utils from fitapp.models import UserFitbit, TimeSeriesData, TimeSeriesDataType @@ -27,7 +28,7 @@ from .base import FitappTestBase -class TestRetrievalUtility(FitappTestBase): +class TestRetrievalUtility(FitappTestBase, TransactionTestCase): """Tests for the get_fitbit_data utility function.""" def setUp(self): @@ -36,16 +37,32 @@ def setUp(self): self.base_date = '2012-06-01' self.end_date = None - @patch.object(Fitbit, 'time_series') - def _mock_time_series(self, time_series=None, error=None, response=None, - error_attrs={}): + @patch('fitapp.utils.create_fitbit') + def _mock_time_series(self, create_fitbit, error=None, response=None, + error_attrs={}, access_token=None, profile_mock={}, + refresh_token=None, expires_at=None): + fitbit = MagicMock() + fitbit.time_series = MagicMock() + fitbit.user_profile_get = MagicMock() if error: exc = error(self._error_response()) for k, v in error_attrs.items(): setattr(exc, k, v) - time_series.side_effect = exc + fitbit.time_series.side_effect = exc elif response: - time_series.return_value = response + fitbit.time_series.return_value = response + fitbit.user_profile_get.return_value = profile_mock + client = MagicMock() + client.token = self.fbuser.get_user_data() + if access_token and refresh_token: + client.token.update({ + 'access_token': access_token, + 'refresh_token': refresh_token + }) + if expires_at: + client.token['expires_at'] = expires_at + fitbit.client = client + create_fitbit.return_value = fitbit resource_type = TimeSeriesDataType.objects.get( category=TimeSeriesDataType.activities, resource='steps') return utils.get_fitbit_data( @@ -104,6 +121,62 @@ def test_retrieval(self): steps = self._mock_time_series(response=response) self.assertEqual(steps, response['activities-steps']) + def test_expired_token(self): + """ + get_fitbit_data should save updated token if it has changed. + To prevent an old refresh_token from being re-saved on the UserFitbit + model due to a race condition in celery events, we check that the + expiration date in the returned token is greater than the model's + expiration date and greater than now before saving. This tests that + as well. + """ + userfitbit = UserFitbit.objects.all()[0] + self.assertEqual(userfitbit.access_token, self.fbuser.access_token) + self.assertEqual(userfitbit.refresh_token, self.fbuser.refresh_token) + response = {'activities-steps': [1, 2, 3]} + kwargs = { + 'response': response, + 'access_token': 'new_at', + 'refresh_token': 'new_rt', + 'profile_mock': {'user': {'timezone': 'America/Denver'}} + } + + # Check that when the new expiration date is less than the old one, we + # don't update the model. Current expires_at is + # 2016-4-18 11:24:08.405841 + self.assertEqual(userfitbit.expires_at, 1461003848.405841) + kwargs['expires_at'] = 1460899999.405841 # 2016-4-17 6:33:19.405841 + steps = self._mock_time_series(**kwargs) + self.assertEqual(steps, response['activities-steps']) + userfitbit = UserFitbit.objects.all()[0] + self.assertEqual(userfitbit.access_token, self.fbuser.access_token) + self.assertEqual(userfitbit.refresh_token, self.fbuser.refresh_token) + self.assertEqual(userfitbit.timezone, 'America/Los_Angeles') + + # Check that when expires_at is less than now, we don't update the + # model + with freeze_time('2016-04-22'): + # 2016-4-20 18:57:38.405841 + kwargs['expires_at'] = 1461203858.405841 + steps = self._mock_time_series(**kwargs) + self.assertEqual(steps, response['activities-steps']) + userfitbit = UserFitbit.objects.all()[0] + self.assertEqual(userfitbit.access_token, self.fbuser.access_token) + self.assertEqual(userfitbit.refresh_token, self.fbuser.refresh_token) + self.assertEqual(userfitbit.timezone, 'America/Los_Angeles') + + # Now that `now` is sufficiently in the past, we update the model with + # the new tokens + with freeze_time('2016-04-19'): + # 2016-4-20 18:57:38.405841 + kwargs['expires_at'] = 1461203858.405841 + steps = self._mock_time_series(**kwargs) + self.assertEqual(steps, response['activities-steps']) + userfitbit = UserFitbit.objects.all()[0] + self.assertEqual(userfitbit.access_token, 'new_at') + self.assertEqual(userfitbit.refresh_token, 'new_rt') + self.assertEqual(userfitbit.timezone, 'America/Denver') + class TestRetrievalTask(FitappTestBase): def setUp(self): @@ -187,6 +260,7 @@ def test_subscription_update_too_many(self, get_fitbit_data): self.fbuser.fitbit_user, _type, self.date) exc = fitbit_exceptions.HTTPTooManyRequests(self._error_response()) exc.retry_after_secs = 21 + def side_effect(*args, **kwargs): # Delete the cache lock after the first try and adjust the # get_fitbit_data mock to be successful @@ -211,10 +285,10 @@ def side_effect(*args, **kwargs): self.assertEqual(TimeSeriesData.objects.count(), 1) self.assertEqual(TimeSeriesData.objects.get().value, '34') - def test_problem_queueing_task(self): - get_time_series_data = MagicMock() + @patch('fitapp.tasks.get_time_series_data.apply_async') + def test_problem_queueing_task(self, gtsd_apply_async): # If queueing the task raises an exception, it doesn't propagate - get_time_series_data.apply_async.side_effect = Exception + gtsd_apply_async.side_effect = Exception try: self._receive_fitbit_updates() except: diff --git a/fitapp/utils.py b/fitapp/utils.py index 6f56393..0ce3f14 100644 --- a/fitapp/utils.py +++ b/fitapp/utils.py @@ -1,10 +1,14 @@ +import pytz + +from datetime import datetime from django.conf import settings from django.core.exceptions import ImproperlyConfigured +from django.db import transaction +from django.utils.timezone import localtime, make_aware from fitbit import Fitbit -from . import defaults -from .models import UserFitbit +from . import defaults, models def create_fitbit(consumer_key=None, consumer_secret=None, **kwargs): @@ -36,7 +40,7 @@ def is_integrated(user): :param user: A Django User. """ if user.is_authenticated() and user.is_active: - return UserFitbit.objects.filter(user=user).exists() + return models.UserFitbit.objects.filter(user=user).exists() return False @@ -45,6 +49,7 @@ def get_valid_periods(): return ['1d', '7d', '30d', '1w', '1m', '3m', '6m', '1y', 'max'] +@transaction.atomic() def get_fitbit_data(fbuser, resource_type, base_date=None, period=None, end_date=None): """Creates a Fitbit API instance and retrieves step data for the period. @@ -68,15 +73,34 @@ def get_fitbit_data(fbuser, resource_type, base_date=None, period=None, period=period, base_date=base_date, end_date=end_date) - # Update the token if necessary. We are making sure we have a valid - # access_token and refresh_token next time we request Fitbit data - if fb.client.token['access_token'] != fbuser.access_token: - fbuser.access_token = fb.client.token['access_token'] - fbuser.refresh_token = fb.client.token['refresh_token'] - fbuser.save() + check_for_new_token(fbuser, fb.client.token) return data[resource_path.replace('/', '-')] +def check_for_new_token(fbuser, token): + """ + Update the token if necessary. We are making sure we have a valid + access_token and refresh_token next time we request Fitbit data + """ + expires_at = token.get('expires_at', None) + if expires_at and expires_at > fbuser.expires_at: + # We've compared the expires_at float values sent by fitbit, now let's + # check that the timezone aware expires_at datetime is greater than now + # in the fitbit user's timezone + timezone = pytz.timezone(fbuser.timezone) + expires_at_local = make_aware(datetime.fromtimestamp(expires_at), + timezone) + utc_now = make_aware(datetime.utcnow(), pytz.timezone('UTC')) + if expires_at_local > localtime(utc_now, timezone): + fbuser.access_token = token['access_token'] + fbuser.refresh_token = token['refresh_token'] + fbuser.expires_at = expires_at + fbuser.save() + from .tasks import update_user_timezone + update_user_timezone.apply_async( + (fbuser.fitbit_user,), countdown=1) + + def get_setting(name, use_defaults=True): """Retrieves the specified setting from the settings file. diff --git a/fitapp/views.py b/fitapp/views.py index f3873aa..f373530 100644 --- a/fitapp/views.py +++ b/fitapp/views.py @@ -1,5 +1,7 @@ +import pytz import simplejson as json +from datetime import datetime from dateutil import parser from dateutil.relativedelta import relativedelta from django.contrib.auth.decorators import login_required @@ -20,7 +22,7 @@ from . import forms from . import utils from .models import UserFitbit, TimeSeriesData, TimeSeriesDataType -from .tasks import get_time_series_data, subscribe, unsubscribe +from .tasks import subscribe, get_time_series_data, unsubscribe @login_required @@ -83,54 +85,33 @@ def complete(request): token = fb.client.fetch_access_token(code, callback_uri) access_token = token['access_token'] fitbit_user = token['user_id'] + expires_at = token['expires_at'] except KeyError: return redirect(reverse('fitbit-error')) if UserFitbit.objects.filter(fitbit_user=fitbit_user).exists(): return redirect(reverse('fitbit-error')) - fbuser, _ = UserFitbit.objects.get_or_create(user=request.user) - fbuser.access_token = access_token - fbuser.fitbit_user = fitbit_user - fbuser.refresh_token = token['refresh_token'] - fbuser.save() + UserFitbit.objects.update_or_create(user_id=request.user.id, defaults={ + 'fitbit_user': fitbit_user, + 'access_token': access_token, + 'refresh_token': token['refresh_token'], + 'expires_at': expires_at, + 'timezone': 'UTC' + }) - # Add the Fitbit user info to the session - request.session['fitbit_profile'] = fb.user_profile_get() if utils.get_setting('FITAPP_SUBSCRIBE'): try: SUBSCRIBER_ID = utils.get_setting('FITAPP_SUBSCRIBER_ID') + subscribe.apply_async((fitbit_user, SUBSCRIBER_ID,), countdown=1) except ImproperlyConfigured: return redirect(reverse('fitbit-error')) - subscribe.apply_async((fbuser.fitbit_user, SUBSCRIBER_ID), countdown=5) - # Create tasks for all data in all data types - for i, _type in enumerate(TimeSeriesDataType.objects.all()): - # Delay execution for a few seconds to speed up response - # Offset each call by 2 seconds so they don't bog down the server - get_time_series_data.apply_async( - (fbuser.fitbit_user, _type.category, _type.resource,), - countdown=10 + (i * 5)) next_url = request.session.pop('fitbit_next', None) or utils.get_setting( 'FITAPP_LOGIN_REDIRECT') return redirect(next_url) -@receiver(user_logged_in) -def create_fitbit_session(sender, request, user, **kwargs): - """ If the user is a fitbit user, update the profile in the session. """ - - if user.is_authenticated() and utils.is_integrated(user) and \ - user.is_active: - fbuser = UserFitbit.objects.filter(user=user) - if fbuser.exists(): - fb = utils.create_fitbit(**fbuser[0].get_user_data()) - try: - request.session['fitbit_profile'] = fb.user_profile_get() - except: - pass - - @login_required def error(request): """ @@ -233,6 +214,10 @@ def update(request): countdown=(2 * i)) except (KeyError, ValueError, OverflowError): raise Http404 + except Exception: + # Unexpected error, ignore and return 204 so fitbit doesn't disable + # our subscriber + pass return HttpResponse(status=204) elif request.method == 'GET': @@ -263,9 +248,11 @@ def normalize_date_range(request, fitbit_data): base_date = fitbit_data['base_date'] if base_date == 'today': now = timezone.now() - if 'fitbit_profile' in request.session.keys(): - tz = request.session['fitbit_profile']['user']['timezone'] - now = timezone.pytz.timezone(tz).normalize(timezone.now()) + fbuser = UserFitbit.objects.filter(user_id=request.user.id) + if fbuser.exists(): + tz = pytz.timezone(fbuser[0].timezone) + utc_now = timezone.make_aware(datetime.utcnow(), pytz.utc) + now = timezone.localtime(utc_now, tz) base_date = now.date().strftime('%Y-%m-%d') result['date__gte'] = base_date diff --git a/requirements/base.txt b/requirements/base.txt index 549baa7..368a575 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,4 +1,5 @@ fitbit==0.2.2 celery>=3.1.13 +pytz>=2016.3 simplejson six diff --git a/setup.py b/setup.py index 33afdb1..348e18f 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,6 @@ "Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", diff --git a/tox.ini b/tox.ini index 55b55bd..e1d92f9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,9 @@ [tox] -envlist = pypy-1.9.X,pypy-1.8.X,pypy-1.7.X,pypy-1.4.X, +envlist = pypy-1.9.X,pypy-1.8.X,pypy-1.7.X py35-trunk,py35-1.9.X, py34-trunk,py34-1.9.X,py34-1.8.X,py34-1.7.X, py33-1.8.X,py33-1.7.X, - py27-1.9.X,py27-1.8.X,py27-1.7.X,py27-1.4.X, - py26-1.4.X + py27-1.9.X,py27-1.8.X,py27-1.7.X [testenv] commands = {envpython} run_tests.py