Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
26 changes: 26 additions & 0 deletions fitapp/migrations/0006_add_expires_timezone_fields.py
Original file line number Diff line number Diff line change
@@ -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,
),
]
3 changes: 3 additions & 0 deletions fitapp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brad I see that the migration file for the timezone field has default='UTC'. Did you remove default='UTC' here after the migration file was created? If not, how default='UTC' was added to the migration file?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@percyperez When I ran makemigrations it asked for a default and I specifie UTC

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brad Ah Cool 👍


def __str__(self):
return self.user.__str__()
Expand Down
76 changes: 53 additions & 23 deletions fitapp/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

""" 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')
26 changes: 17 additions & 9 deletions fitapp/tests/base.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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


Expand All @@ -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):
Expand All @@ -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']
}
Expand All @@ -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


Expand Down Expand Up @@ -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)
Expand Down
48 changes: 32 additions & 16 deletions fitapp/tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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.
Expand All @@ -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):
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Loading