From 64887c6870546fd78082459dd44065f055a8fe54 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Wed, 11 Dec 2013 12:29:09 -0500 Subject: [PATCH] Flesh out findusers script, except for LinkedIn API call, with tests. --- .../linkedin/management/commands/findusers.py | 88 ++++++++++- .../management/commands/tests/__init__.py | 0 .../commands/tests/test_findusers.py | 143 ++++++++++++++++++ 3 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 lms/djangoapps/linkedin/management/commands/tests/__init__.py create mode 100644 lms/djangoapps/linkedin/management/commands/tests/test_findusers.py diff --git a/lms/djangoapps/linkedin/management/commands/findusers.py b/lms/djangoapps/linkedin/management/commands/findusers.py index fe5a572e0d..9ea7662366 100644 --- a/lms/djangoapps/linkedin/management/commands/findusers.py +++ b/lms/djangoapps/linkedin/management/commands/findusers.py @@ -1,8 +1,94 @@ +import datetime +import pytz +import time + +from django.contrib.auth.models import User from django.core.management.base import BaseCommand, CommandError +from django.utils import timezone + +from optparse import make_option + + +FRIDAY = 4 + +def get_call_limits(): + """ + Returns a tuple of: (max_checks, checks_per_call, time_between_calls) + + Here are the parameters provided by LinkedIn: + + Please note: in order to ensure a successful call, please run the calls + between Friday 6pm PST and Monday 5am PST. + + During the week, calls are limited to very low volume (500 profiles/day) + and must be run after 6pm and before 5am. This should only be used to do + subsequent trigger emails. Please contact the developer support alias for + more information. + + Use 80 emails per API call and 1 call per second. + """ + now = timezone.now().astimezone(pytz.timezone('US/Pacific')) + lastfriday = now + while lastfriday.weekday() != FRIDAY: + lastfriday -= datetime.timedelta(days=1) + safeharbor_begin = lastfriday.replace(hour=18, minute=0) + safeharbor_end = safeharbor_begin + datetime.timedelta(days=2, hours=11) + if safeharbor_begin < now < safeharbor_end: + return -1, 80, 1 + elif now.hour >= 18 or now.hour < 5: + return 500, 80, 1 + else: + return 0, 0, 0 + class Command(BaseCommand): args = '' help = 'Checks LinkedIn for students that are on LinkedIn' + option_list = BaseCommand.option_list + ( + make_option('--recheck', + action='store_true', + dest='recheck', + default=False, + help='Check users that have been checked in the past to see if ' + 'they have joined or left LinkedIn since the last check'), + ) def handle(self, *args, **options): - print "Hello World!" + api = LinkedinAPI() + recheck = options.pop('recheck', False) + max_checks, checks_per_call, time_between_calls = get_call_limits() + if not max_checks: + raise CommandError("No checks allowed during this time.") + + check_users = [] + for user in User.objects.all(): + checked = (hasattr(user, 'linkedin') and + user.linkedin.has_linkedin_account is not None) + if recheck or not checked: + check_users.append(user) + + if max_checks != -1 and len(check_users) > max_checks: + self.stderr.write( + "WARNING: limited to checking only %d users today." % + max_checks) + check_users = check_users[:max_checks] + batches = [check_users[i:i + checks_per_call] + for i in xrange(0, len(check_users), checks_per_call)] + + def do_batch(batch): + emails = [u.email for u in batch] + for user, has_account in zip(batch, api.batch(emails)): + user.linkedin.has_linkedin_account = has_account + + if batches: + do_batch(batches.pop(0)) + for batch in batches: + time.sleep(time_between_calls) + do_batch(batch) + + +class LinkedinAPI(object): + + def batch(self, emails): + pass + diff --git a/lms/djangoapps/linkedin/management/commands/tests/__init__.py b/lms/djangoapps/linkedin/management/commands/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/linkedin/management/commands/tests/test_findusers.py b/lms/djangoapps/linkedin/management/commands/tests/test_findusers.py new file mode 100644 index 0000000000..d016b67b6f --- /dev/null +++ b/lms/djangoapps/linkedin/management/commands/tests/test_findusers.py @@ -0,0 +1,143 @@ +import datetime +import mock +import pytz +import StringIO +import unittest + + +from linkedin.management.commands import findusers + + +class FindUsersTests(unittest.TestCase): + + @mock.patch('linkedin.management.commands.findusers.timezone') + def test_get_call_limits_in_safe_harbor(self, timezone): + fut = findusers.get_call_limits + tz = pytz.timezone('US/Eastern') + timezone.now.return_value = datetime.datetime( + 2013, 12, 14, 0, 0, tzinfo=tz) + self.assertEqual(fut(), (-1, 80, 1)) + timezone.now.return_value = datetime.datetime( + 2013, 12, 13, 21, 1, tzinfo=tz) + self.assertEqual(fut(), (-1, 80, 1)) + timezone.now.return_value = datetime.datetime( + 2013, 12, 15, 7, 59, tzinfo=tz) + self.assertEqual(fut(), (-1, 80, 1)) + + @mock.patch('linkedin.management.commands.findusers.timezone') + def test_get_call_limits_in_business_hours(self, timezone): + fut = findusers.get_call_limits + tz = pytz.timezone('US/Eastern') + timezone.now.return_value = datetime.datetime( + 2013, 12, 11, 11, 3, tzinfo=tz) + self.assertEqual(fut(), (0, 0, 0)) + timezone.now.return_value = datetime.datetime( + 2013, 12, 13, 20, 59, tzinfo=tz) + self.assertEqual(fut(), (0, 0, 0)) + timezone.now.return_value = datetime.datetime( + 2013, 12, 16, 8, 1, tzinfo=tz) + self.assertEqual(fut(), (0, 0, 0)) + + @mock.patch('linkedin.management.commands.findusers.timezone') + def test_get_call_limits_on_weeknights(self, timezone): + fut = findusers.get_call_limits + tz = pytz.timezone('US/Eastern') + timezone.now.return_value = datetime.datetime( + 2013, 12, 11, 21, 3, tzinfo=tz) + self.assertEqual(fut(), (500, 80, 1)) + timezone.now.return_value = datetime.datetime( + 2013, 12, 11, 7, 59, tzinfo=tz) + self.assertEqual(fut(), (500, 80, 1)) + + @mock.patch('linkedin.management.commands.findusers.time') + @mock.patch('linkedin.management.commands.findusers.User') + @mock.patch('linkedin.management.commands.findusers.LinkedinAPI') + @mock.patch('linkedin.management.commands.findusers.get_call_limits') + def test_command_success_recheck_no_limits( + self, get_call_limits, API, User, time): + + fut = findusers.Command().handle + get_call_limits.return_value = (-1, 6, 42) + api = API.return_value + users = [mock.Mock(email=i) for i in xrange(10)] + User.objects.all.return_value = users + def dummy_batch(emails): + return [email % 2 == 0 for email in emails] + api.batch = dummy_batch + fut(recheck=True) + time.sleep.assert_called_once_with(42) + self.assertEqual([u.linkedin.has_linkedin_account for u in users], + [i % 2 == 0 for i in xrange(10)]) + + @mock.patch('linkedin.management.commands.findusers.time') + @mock.patch('linkedin.management.commands.findusers.User') + @mock.patch('linkedin.management.commands.findusers.LinkedinAPI') + @mock.patch('linkedin.management.commands.findusers.get_call_limits') + def test_command_success_no_recheck_no_limits( + self, get_call_limits, API, User, time): + + fut = findusers.Command().handle + get_call_limits.return_value = (-1, 6, 42) + api = API.return_value + users = [mock.Mock(email=i) for i in xrange(10)] + for user in users[:6]: + user.linkedin.has_linkedin_account = user.email % 2 == 0 + for user in users[6:]: + user.linkedin.has_linkedin_account = None + User.objects.all.return_value = users + def dummy_batch(emails): + self.assertEqual(len(emails), 4) + return [email % 2 == 0 for email in emails] + api.batch = dummy_batch + fut() + time.sleep.assert_not_called() + self.assertEqual([u.linkedin.has_linkedin_account for u in users], + [i % 2 == 0 for i in xrange(10)]) + + @mock.patch('linkedin.management.commands.findusers.time') + @mock.patch('linkedin.management.commands.findusers.User') + @mock.patch('linkedin.management.commands.findusers.LinkedinAPI') + @mock.patch('linkedin.management.commands.findusers.get_call_limits') + def test_command_success_no_recheck_no_users( + self, get_call_limits, API, User, time): + + fut = findusers.Command().handle + get_call_limits.return_value = (-1, 6, 42) + api = API.return_value + users = [mock.Mock(email=i) for i in xrange(10)] + for user in users: + user.linkedin.has_linkedin_account = user.email % 2 == 0 + User.objects.all.return_value = users + def dummy_batch(emails): + self.assertTrue(False) # shouldn't be called + api.batch = dummy_batch + fut() + time.sleep.assert_not_called() + self.assertEqual([u.linkedin.has_linkedin_account for u in users], + [i % 2 == 0 for i in xrange(10)]) + + @mock.patch('linkedin.management.commands.findusers.time') + @mock.patch('linkedin.management.commands.findusers.User') + @mock.patch('linkedin.management.commands.findusers.LinkedinAPI') + @mock.patch('linkedin.management.commands.findusers.get_call_limits') + def test_command_success_recheck_with_limit( + self, get_call_limits, API, User, time): + + command = findusers.Command() + command.stderr = StringIO.StringIO() + fut = command.handle + get_call_limits.return_value = (9, 6, 42) + api = API.return_value + users = [mock.Mock(email=i) for i in xrange(10)] + for user in users: + user.linkedin.has_linkedin_account = None + User.objects.all.return_value = users + def dummy_batch(emails): + return [email % 2 == 0 for email in emails] + api.batch = dummy_batch + fut() + time.sleep.assert_called_once_with(42) + self.assertEqual([u.linkedin.has_linkedin_account for u in users[:9]], + [i % 2 == 0 for i in xrange(9)]) + self.assertEqual(users[9].linkedin.has_linkedin_account, None) + self.assertTrue(command.stderr.getvalue().startswith("WARNING"))