From 148d7d76b08ac08015e60d086cfa7f5b08d7bf85 Mon Sep 17 00:00:00 2001 From: hwkns Date: Tue, 22 Jul 2014 00:13:48 -0400 Subject: [PATCH 1/4] Python 3 compatibility, and updated documentation --- .gitignore | 4 + .travis.yml | 30 +- Makefile | 8 +- django_twilio/__init__.py | 9 +- django_twilio/admin.py | 10 +- django_twilio/client.py | 15 +- django_twilio/decorators.py | 58 ++-- django_twilio/fixtures/django_twilio.json | 18 -- django_twilio/models.py | 29 +- django_twilio/settings.py | 9 +- .../south_migrations/0001_initial.py | 4 +- django_twilio/tests/client.py | 34 --- django_twilio/tests/decorators.py | 172 ----------- django_twilio/tests/models.py | 53 ---- django_twilio/tests/urls.py | 10 - django_twilio/tests/views.py | 273 ------------------ django_twilio/utils.py | 46 ++- django_twilio/views.py | 43 ++- docs/source/conf.py | 76 +++-- docs/source/contribute.rst | 88 +++--- docs/source/contributors.rst | 8 +- docs/source/decorators.rst | 57 ++-- docs/source/foreword.rst | 41 +-- docs/source/gotchas.rst | 44 +-- docs/source/howto.rst | 29 +- docs/source/install.rst | 70 +++-- docs/source/rest.rst | 39 ++- docs/source/settings.rst | 75 ++--- docs/source/views.rst | 227 ++++++++------- manage.py | 12 + requirements.txt | 4 +- run_tests.py | 50 ---- setup.py | 52 +++- test_project/__init__.py | 0 test_project/settings.py | 173 +++++++++++ .../test_app}/__init__.py | 3 + test_project/test_app/client.py | 40 +++ test_project/test_app/decorators.py | 158 ++++++++++ test_project/test_app/models.py | 69 +++++ test_project/test_app/urls.py | 14 + test_project/test_app/utils.py | 39 +++ test_project/test_app/views.py | 188 ++++++++++++ test_project/urls.py | 13 + test_requirements.txt | 5 + tox.ini | 8 + 45 files changed, 1356 insertions(+), 1051 deletions(-) delete mode 100644 django_twilio/fixtures/django_twilio.json delete mode 100644 django_twilio/tests/client.py delete mode 100644 django_twilio/tests/decorators.py delete mode 100644 django_twilio/tests/models.py delete mode 100644 django_twilio/tests/urls.py delete mode 100644 django_twilio/tests/views.py create mode 100644 manage.py delete mode 100644 run_tests.py create mode 100644 test_project/__init__.py create mode 100644 test_project/settings.py rename {django_twilio/tests => test_project/test_app}/__init__.py (52%) create mode 100644 test_project/test_app/client.py create mode 100644 test_project/test_app/decorators.py create mode 100644 test_project/test_app/models.py create mode 100644 test_project/test_app/urls.py create mode 100644 test_project/test_app/utils.py create mode 100644 test_project/test_app/views.py create mode 100644 test_project/urls.py create mode 100644 test_requirements.txt create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index e50f851..296c9c0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ dist # Editor temp files: +.idea/ *.swp *~ @@ -18,3 +19,6 @@ build # Coverage reports: .coverage *htmlcov* + +# Tox testing +.tox/ diff --git a/.travis.yml b/.travis.yml index e54ad59..66640d6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,29 @@ language: python + python: -- '2.7' -- '2.6' + - '2.6' + - '2.7' + - '3.3' + - '3.4' + - 'pypy' env: - global: - - secure: TVFg6VDgzyE8VjPQVepE2JTXNbjpCBiiB3tF+rtjwzinnyPaesJ/a1P4XazzdP98Yjx1BPUxvB5kR0uDhseoUOGnZ0F5oeAr44sXZ1itGyANmFsF+cTK/zU2Ov7j+8deDypBxvgOad/LzAE0wihgNFpFopCPs0OdaBn7pfBiv40= - - secure: dAgkutD1GS4VhlRF6tmC0MoKUTakIaFcaQ8vKBQzW9Z5Q+Knjmuwx4iArc73kByg5T1KloERNcKoAGrya9rwuUYFMldLtqeYK/nTMx8ejRZ0PmGqlpmtGvJR8iN+FSdLuiVkfKvZewRHybgQt0mL2886tihf1dbVWDWTcIk/gSc= + - DJANGO="django>=1.4,<1.5" + - DJANGO="django>=1.5,<1.6" + - DJANGO="django>=1.6,<1.7" + - DJANGO="https://www.djangoproject.com/download/1.7c1/tarball/" + +install: + - pip install --use-mirrors ${DJANGO} + - pip install --use-mirrors -r requirements.txt + - pip install --use-mirrors -r test_requirements.txt +script: + - python manage.py test -install: pip install -r requirements.txt -script: python run_tests.py +matrix: + exclude: + - python: "3.3" + env: DJANGO="django>=1.4,<1.5" + - python: "3.4" + env: DJANGO="django>=1.4,<1.5" diff --git a/Makefile b/Makefile index 8966670..26cbcb2 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,15 @@ lint: - flake8 django_twilio + flake8 django_twilio --exclude=migrations test: - python run_tests.py + python manage.py test coverage: - coverage run --source django_twilio run_tests.py + coverage run --source django_twilio manage.py test coverage report -m htmlcov: - coverage run --source django_twilio run_tests.py + coverage run --source django_twilio manage.py test coverage html open htmlcov/index.html diff --git a/django_twilio/__init__.py b/django_twilio/__init__.py index 2ffe93c..09b95d0 100644 --- a/django_twilio/__init__.py +++ b/django_twilio/__init__.py @@ -1,3 +1,8 @@ -"""A simple library for building twilio-powered Django webapps.""" +# -*- coding: utf-8 -*- +from __future__ import unicode_literals, absolute_import -__version__ = '0.6' +""" +A simple library for building twilio-powered Django webapps. +""" + +__version__ = '0.7' diff --git a/django_twilio/admin.py b/django_twilio/admin.py index 36ed27a..779f631 100644 --- a/django_twilio/admin.py +++ b/django_twilio/admin.py @@ -1,17 +1,21 @@ -"""This module provides useful django admin hooks that allow you to manage +# -*- coding: utf-8 -*- +from __future__ import unicode_literals, absolute_import + +""" +This module provides useful django admin hooks that allow you to manage various components through the django admin panel (if enabled). """ from django.contrib import admin -from django_twilio.models import Caller, Credential +from .models import Caller, Credential class CallerAdmin(admin.ModelAdmin): """This class provides admin panel integration for our :class:`django_twilio.models.Caller` model. """ - list_display = ('__unicode__', 'blacklisted') + list_display = ('__str__', 'blacklisted') admin.site.register(Caller, CallerAdmin) diff --git a/django_twilio/client.py b/django_twilio/client.py index 7fbeae9..02b23a4 100644 --- a/django_twilio/client.py +++ b/django_twilio/client.py @@ -1,10 +1,17 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals, absolute_import -"""Twilio REST client helpers.""" -from django_twilio import settings +""" +Twilio REST client helpers. +""" from twilio.rest import TwilioRestClient +from .settings import TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN + + twilio_client = TwilioRestClient( - settings.TWILIO_ACCOUNT_SID, - settings.TWILIO_AUTH_TOKEN, version='2010-04-01') + TWILIO_ACCOUNT_SID, + TWILIO_AUTH_TOKEN, + version='2010-04-01' +) diff --git a/django_twilio/decorators.py b/django_twilio/decorators.py index b0bc0df..dc5a4d8 100644 --- a/django_twilio/decorators.py +++ b/django_twilio/decorators.py @@ -1,8 +1,11 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals, absolute_import -"""Useful decorators.""" - +""" +Useful decorators. +""" +import sys from functools import wraps from django.conf import settings @@ -13,13 +16,20 @@ from twilio.twiml import Verb from twilio.util import RequestValidator -from django_twilio import settings as django_twilio_settings -from django_twilio.utils import get_blacklisted_response +from .settings import TWILIO_AUTH_TOKEN +from .utils import get_blacklisted_response + + +# Snippet from the `six` library to help with Python3 compatibility +if sys.version_info[0] == 3: + text_type = str +else: + text_type = unicode def twilio_view(f): - """This decorator provides several helpful shortcuts for writing Twilio - views. + """ + This decorator provides several helpful shortcuts for writing Twilio views. - It ensures that only requests from Twilio are passed through. This helps protect you from forged requests. @@ -32,7 +42,7 @@ def twilio_view(f): blacklisted, any requests from them will be rejected. - It allows your view to (optionally) return TwiML to pass back to - Twilio's servers instead of building a ``HttpResponse`` object + Twilio's servers instead of building an ``HttpResponse`` object manually. - It allows your view to (optionally) return any ``twilio.Verb`` object @@ -44,30 +54,34 @@ def twilio_view(f): Usage:: - from twilio.twiml import Response + from twilio import twiml @twilio_view def my_view(request): - r = Response() + r = twiml.Response() r.message('Thanks for the SMS message!') return r """ @csrf_exempt @wraps(f) - def decorator(request_or_self, methods=['POST'], + def decorator(request_or_self, methods=('POST',), blacklist=True, *args, **kwargs): - class_based_view = not(isinstance(request_or_self, HttpRequest)) + class_based_view = not isinstance(request_or_self, HttpRequest) if not class_based_view: request = request_or_self else: assert len(args) >= 1 request = args[0] - # Turn off Twilio authentication when explicitly requested, or in debug mode. - # Otherwise things do not work properly. For more information see the docs. - use_forgery_protection = ( - getattr(settings, 'DJANGO_TWILIO_FORGERY_PROTECTION', not settings.DEBUG)) + # Turn off Twilio authentication when explicitly requested, or + # in debug mode. Otherwise things do not work properly. For + # more information, see the docs. + use_forgery_protection = getattr( + settings, + 'DJANGO_TWILIO_FORGERY_PROTECTION', + not settings.DEBUG, + ) if use_forgery_protection: if request.method not in methods: @@ -75,8 +89,7 @@ def decorator(request_or_self, methods=['POST'], # Forgery check try: - validator = RequestValidator( - django_twilio_settings.TWILIO_AUTH_TOKEN) + validator = RequestValidator(TWILIO_AUTH_TOKEN) url = request.build_absolute_uri() signature = request.META['HTTP_X_TWILIO_SIGNATURE'] except (AttributeError, KeyError): @@ -90,16 +103,19 @@ def decorator(request_or_self, methods=['POST'], return HttpResponseForbidden() # Blacklist check - checkBlackList = ( - getattr(settings, 'DJANGO_TWILIO_BLACKLIST_CHECK', blacklist)) - if checkBlackList: + check_blacklist = getattr( + settings, + 'DJANGO_TWILIO_BLACKLIST_CHECK', + blacklist + ) + if check_blacklist: blacklisted_resp = get_blacklisted_response(request) if blacklisted_resp: return blacklisted_resp response = f(request_or_self, *args, **kwargs) - if isinstance(response, str): + if isinstance(response, (text_type, bytes)): return HttpResponse(response, content_type='application/xml') elif isinstance(response, Verb): return HttpResponse(str(response), content_type='application/xml') diff --git a/django_twilio/fixtures/django_twilio.json b/django_twilio/fixtures/django_twilio.json deleted file mode 100644 index 8ac2176..0000000 --- a/django_twilio/fixtures/django_twilio.json +++ /dev/null @@ -1,18 +0,0 @@ -[ - { - "pk": 1, - "model": "django_twilio.caller", - "fields": { - "phone_number": "+12222222222", - "blacklisted": false - } - }, - { - "pk": 2, - "model": "django_twilio.caller", - "fields": { - "phone_number": "+13333333333", - "blacklisted": true - } - } -] diff --git a/django_twilio/models.py b/django_twilio/models.py index da6751a..660f65a 100644 --- a/django_twilio/models.py +++ b/django_twilio/models.py @@ -1,15 +1,20 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals, absolute_import +from django.utils.encoding import python_2_unicode_compatible from django.db import models from django.conf import settings -AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User') - from phonenumber_field.modelfields import PhoneNumberField +AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User') + + +@python_2_unicode_compatible class Caller(models.Model): - """ A caller is defined uniquely by their phone number. + """ + A caller is defined uniquely by their phone number. :param bool blacklisted: Designates whether the caller can use our services. @@ -20,15 +25,17 @@ class Caller(models.Model): blacklisted = models.BooleanField(default=False) phone_number = PhoneNumberField(unique=True) - def __unicode__(self): - name = str(self.phone_number) - if self.blacklisted: - name += ' (blacklisted)' - return name + def __str__(self): + return '{phone_number}{blacklist_status}'.format( + phone_number=str(self.phone_number), + blacklist_status=' (blacklisted)' if self.blacklisted else '', + ) +@python_2_unicode_compatible class Credential(models.Model): - """ A Credential model is a set of SID / AUTH tokens for the Twilio.com API + """ + A Credential model is a set of SID / AUTH tokens for the Twilio.com API The Credential model can be used if a project uses more than one Twilio account, or provides Users with access to Twilio powered @@ -41,8 +48,8 @@ class Credential(models.Model): """ - def __unicode__(self): - return ' '.join([self.name, '-', self.account_sid]) + def __str__(self): + return '{name} - {sid}'.format(name=self.name, sid=self.account_sid) name = models.CharField(max_length=30) diff --git a/django_twilio/settings.py b/django_twilio/settings.py index 6b799a2..008b877 100644 --- a/django_twilio/settings.py +++ b/django_twilio/settings.py @@ -1,7 +1,10 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals, absolute_import -"""django_twilio specific settings.""" +""" +django_twilio specific settings. +""" -from .utils import discover_twilio_creds +from .utils import discover_twilio_credentials -TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN = discover_twilio_creds() +TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN = discover_twilio_credentials() diff --git a/django_twilio/south_migrations/0001_initial.py b/django_twilio/south_migrations/0001_initial.py index 6580ded..39e180a 100644 --- a/django_twilio/south_migrations/0001_initial.py +++ b/django_twilio/south_migrations/0001_initial.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- -import datetime from south.db import db from south.v2 import SchemaMigration -from django.db import models -from django_twilio.compat import AUTH_USER_MODEL +from django_twilio.models import AUTH_USER_MODEL class Migration(SchemaMigration): diff --git a/django_twilio/tests/client.py b/django_twilio/tests/client.py deleted file mode 100644 index 306a072..0000000 --- a/django_twilio/tests/client.py +++ /dev/null @@ -1,34 +0,0 @@ -from django.test import TestCase -from django.contrib.auth.models import User - -from twilio.rest import TwilioRestClient - -from django_twilio.client import twilio_client -from django_twilio.models import Credential -from django_twilio import settings -from django_twilio.utils import discover_twilio_creds - - -class TwilioClientTestCase(TestCase): - - def test_twilio_client_exists(self): - self.assertIsInstance(twilio_client, TwilioRestClient) - - def test_twilio_client_sets_creds(self): - self.assertEqual( - twilio_client.auth, - (settings.TWILIO_ACCOUNT_SID, settings.TWILIO_AUTH_TOKEN)) - - def test_twilio_client_with_creds_model(self): - self.user = User.objects.create(username='test', password='pass') - self.creds = Credential.objects.create( - name='Test Creds', - account_sid='AAA', - auth_token='BBB', - user=self.user, - ) - - deets = discover_twilio_creds(user=self.user) - - self.assertEquals(deets[0], self.creds.account_sid) - self.assertEquals(deets[1], self.creds.auth_token) diff --git a/django_twilio/tests/decorators.py b/django_twilio/tests/decorators.py deleted file mode 100644 index 884149f..0000000 --- a/django_twilio/tests/decorators.py +++ /dev/null @@ -1,172 +0,0 @@ -from hmac import new -from hashlib import sha1 -from base64 import encodestring - -from django.conf import settings -from django.http import HttpResponse -from django.test import Client, RequestFactory, TestCase -from django.test.utils import override_settings - -from twilio.twiml import Response - -from django_twilio import settings as django_twilio_settings -from django_twilio.tests.views import response_view, str_view, verb_view - - -class TwilioViewTestCase(TestCase): - fixtures = ['django_twilio.json'] - urls = 'django_twilio.tests.urls' - - def setUp(self): - self.client = Client(enforce_csrf_checks=True) - self.factory = RequestFactory(enforce_csrf_checks=True) - - # Test URIs. - self.uri = 'http://testserver/tests/decorators' - self.str_uri = '/tests/decorators/str_view/' - self.verb_uri = '/tests/decorators/verb_view/' - self.response_uri = '/tests/decorators/response_view/' - - # Guarantee a value for the required configuration settings after each - # test case. - django_twilio_settings.TWILIO_ACCOUNT_SID = 'xxx' - django_twilio_settings.TWILIO_AUTH_TOKEN = 'xxx' - - # Pre-calculate Twilio signatures for our test views. - self.response_signature = encodestring( - new(django_twilio_settings.TWILIO_AUTH_TOKEN, - '%s/response_view/' % self.uri, sha1).digest()).strip() - self.str_signature = encodestring( - new(django_twilio_settings.TWILIO_AUTH_TOKEN, - '%s/str_view/' % self.uri, sha1).digest()).strip() - self.sig_with_from_field_normal_caller = encodestring( - new(django_twilio_settings.TWILIO_AUTH_TOKEN, - '%s/str_view/From+12222222222' % self.uri, - sha1).digest()).strip() - self.sig_with_from_field_blacklisted_caller = encodestring( - new(django_twilio_settings.TWILIO_AUTH_TOKEN, - '%s/str_view/From+13333333333' % self.uri, - sha1).digest()).strip() - self.verb_signature = encodestring( - new(django_twilio_settings.TWILIO_AUTH_TOKEN, - '%s/verb_view/' % self.uri, sha1).digest()).strip() - - def test_requires_post(self): - debug_orig = settings.DEBUG - settings.DEBUG = False - self.assertEquals(self.client.get(self.str_uri).status_code, 405) - self.assertEquals(self.client.head(self.str_uri).status_code, 405) - self.assertEquals(self.client.options(self.str_uri).status_code, 405) - self.assertEquals(self.client.put(self.str_uri).status_code, 405) - self.assertEquals(self.client.delete(self.str_uri).status_code, 405) - settings.DEBUG = True - self.assertEquals(self.client.get(self.str_uri).status_code, 200) - self.assertEquals(self.client.head(self.str_uri).status_code, 200) - self.assertEquals(self.client.options(self.str_uri).status_code, 200) - self.assertEquals(self.client.put(self.str_uri).status_code, 200) - self.assertEquals(self.client.delete(self.str_uri).status_code, 200) - settings.DEBUG = debug_orig - - def test_allows_post(self): - request = self.factory.post( - self.str_uri, HTTP_X_TWILIO_SIGNATURE=self.str_signature) - self.assertEquals(str_view(request).status_code, 200) - - def test_decorator_preserves_metadata(self): - self.assertEqual(str_view.__name__, 'str_view') - - def test_missing_settings_return_forbidden(self): - del django_twilio_settings.TWILIO_ACCOUNT_SID - del django_twilio_settings.TWILIO_AUTH_TOKEN - debug_orig = settings.DEBUG - settings.DEBUG = False - self.assertEquals(self.client.post(self.str_uri).status_code, 403) - settings.DEBUG = True - self.assertEquals(self.client.post(self.str_uri).status_code, 200) - settings.DEBUG = debug_orig - - def test_missing_signature_returns_forbidden(self): - debug_orig = settings.DEBUG - settings.DEBUG = False - self.assertEquals(self.client.post(self.str_uri).status_code, 403) - settings.DEBUG = True - self.assertEquals(self.client.post(self.str_uri).status_code, 200) - settings.DEBUG = debug_orig - - def test_incorrect_signature_returns_forbidden(self): - debug_orig = settings.DEBUG - settings.DEBUG = False - request = self.factory.post( - self.str_uri, HTTP_X_TWILIO_SIGNATURE='fakesignature') - self.assertEquals(str_view(request).status_code, 403) - settings.DEBUG = True - self.assertEquals(str_view(request).status_code, 200) - settings.DEBUG = debug_orig - - def test_no_from_field(self): - request = self.factory.post( - self.str_uri, - HTTP_X_TWILIO_SIGNATURE=self.str_signature) - self.assertEquals(str_view(request).status_code, 200) - - def test_from_field_no_caller(self): - request = self.factory.post( - self.str_uri, {'From': '+12222222222'}, - HTTP_X_TWILIO_SIGNATURE=self.sig_with_from_field_normal_caller) - self.assertEquals(str_view(request).status_code, 200) - - def test_blacklist_works(self): - debug_orig = settings.DEBUG - settings.DEBUG = False - request = self.factory.post( - self.str_uri, {'From': '+13333333333'}, - HTTP_X_TWILIO_SIGNATURE=self.sig_with_from_field_blacklisted_caller) - response = str_view(request) - r = Response() - r.reject() - self.assertEquals(response.content, str(r)) - settings.DEBUG = True - request = self.factory.post( - self.str_uri, {'From': '+13333333333'}, - HTTP_X_TWILIO_SIGNATURE=self.sig_with_from_field_blacklisted_caller) - response = str_view(request) - r = Response() - r.reject() - self.assertEquals(response.content, str(r)) - settings.DEBUG = debug_orig - - def test_decorator_modifies_str(self): - request = self.factory.post( - self.str_uri, - HTTP_X_TWILIO_SIGNATURE=self.str_signature) - self.assertTrue(isinstance(str_view(request), HttpResponse)) - - def test_decorator_modifies_verb(self): - request = self.factory.post( - self.verb_uri, HTTP_X_TWILIO_SIGNATURE=self.verb_signature) - self.assertTrue(isinstance(verb_view(request), HttpResponse)) - - def test_decorator_preserves_httpresponse(self): - request = self.factory.post( - self.response_uri, HTTP_X_TWILIO_SIGNATURE=self.response_signature) - self.assertTrue(isinstance(response_view(request), HttpResponse)) - - def test_override_forgery_protection_off_debug_off(self): - with override_settings(DJANGO_TWILIO_FORGERY_PROTECTION=False, DEBUG=False): - request = self.factory.post(self.str_uri) - self.assertEquals(str_view(request).status_code, 200) - - def test_override_forgery_protection_off_debug_on(self): - with override_settings(DJANGO_TWILIO_FORGERY_PROTECTION=False, DEBUG=True): - request = self.factory.post(self.str_uri) - self.assertEquals(str_view(request).status_code, 200) - - def test_override_forgery_protection_on_debug_off(self): - with override_settings(DJANGO_TWILIO_FORGERY_PROTECTION=True, DEBUG=False): - request = self.factory.post(self.str_uri) - self.assertEquals(str_view(request).status_code, 403) - - def test_override_forgery_protection_on_debug_on(self): - with override_settings(DJANGO_TWILIO_FORGERY_PROTECTION=True, DEBUG=True): - request = self.factory.post(self.str_uri) - self.assertEquals(str_view(request).status_code, 403) diff --git a/django_twilio/tests/models.py b/django_twilio/tests/models.py deleted file mode 100644 index 56c4f09..0000000 --- a/django_twilio/tests/models.py +++ /dev/null @@ -1,53 +0,0 @@ -from types import MethodType - -from django.test import TestCase -from django.contrib.auth.models import User -from django_twilio.models import Caller, Credential - - -class CallerTestCase(TestCase): - """Run tests against the :class:`django_twilio.models.Caller` model .""" - - def setUp(self): - self.caller = Caller.objects.create( - phone_number='12223334444', blacklisted=False) - - def test_has_unicode(self): - self.assertTrue(isinstance(self.caller.__unicode__, MethodType)) - - def test_unicode_returns_str(self): - self.assertTrue(isinstance(self.caller.__unicode__(), str)) - - def test_unicode_doesnt_contain_blacklisted(self): - self.assertFalse('blacklisted' in self.caller.__unicode__()) - - def test_unicode_contains_blacklisted(self): - self.caller.blacklisted = True - self.caller.save() - self.assertTrue('blacklisted' in self.caller.__unicode__()) - - def tearDown(self): - self.caller.delete() - - -class CredentialTests(TestCase): - - def setUp(self): - self.user = User.objects.create(username='test', password='pass') - self.creds = Credential.objects.create( - name='Test Creds', - account_sid='XXX', - auth_token='YYY', - user=self.user, - ) - - def test_unicode(self): - ''' Assert that unicode renders how we'd like it too ''' - self.assertEquals(self.creds.__unicode__(), 'Test Creds - XXX') - - def test_credentials_fields(self): - ''' Assert the fields are working correctly ''' - self.assertEquals(self.creds.name, 'Test Creds') - self.assertEquals(self.creds.account_sid, 'XXX') - self.assertEquals(self.creds.auth_token, 'YYY') - self.assertEquals(self.creds.user, self.user) diff --git a/django_twilio/tests/urls.py b/django_twilio/tests/urls.py deleted file mode 100644 index ad8e3e2..0000000 --- a/django_twilio/tests/urls.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.conf.urls import patterns, url - - -# Test URLs for our ``django_twilio.decorators`` module. -urlpatterns = patterns( - 'django_twilio.tests.views', - url(r'^tests/decorators/response_view/$', 'response_view'), - url(r'^tests/decorators/str_view/$', 'str_view'), - url(r'^tests/decorators/verb_view/$', 'verb_view'), -) diff --git a/django_twilio/tests/views.py b/django_twilio/tests/views.py deleted file mode 100644 index b993df2..0000000 --- a/django_twilio/tests/views.py +++ /dev/null @@ -1,273 +0,0 @@ -from hmac import new -from hashlib import sha1 -from base64 import encodestring - -import os - -from django.http import HttpResponse -from django.test import Client, RequestFactory, TestCase -from twilio.twiml import Response - -from django_twilio import settings -from django_twilio.decorators import twilio_view -from django_twilio.views import ( - conference, dial, gather, play, record, say, sms) - - -@twilio_view -def response_view(request): - """A simple test view that returns a HttpResponse object.""" - return HttpResponse( - 'Hello from Django', - content_type='text/xml') - - -@twilio_view -def str_view(request): - """A simple test view that returns a string.""" - return 'Hi!' - - -@twilio_view -def verb_view(request): - """A simple test view that returns a ``twilio.Verb`` object.""" - r = Response() - r.reject() - return r - - -class SayTestCase(TestCase): - - def setUp(self): - self.client = Client() - self.factory = RequestFactory() - - # Test URIs. - self.uri = 'http://testserver/tests/views' - self.say_uri = '/tests/views/say/' - - # Guarantee a value for the required configuration settings after each - # test case. - settings.TWILIO_ACCOUNT_SID = 'xxx' - settings.TWILIO_AUTH_TOKEN = 'xxx' - - # Pre-calculate Twilio signatures for our test views. - self.signature = encodestring( - new(settings.TWILIO_AUTH_TOKEN, - '%s/say/' % self.uri, sha1).digest()).strip() - - def test_say_no_text(self): - request = self.factory.post( - self.say_uri, HTTP_X_TWILIO_SIGNATURE=self.signature) - self.assertRaises(TypeError, say, request) - - def test_say_with_text(self): - request = self.factory.post( - self.say_uri, HTTP_X_TWILIO_SIGNATURE=self.signature) - self.assertEquals(say(request, text='hi').status_code, 200) - - def tearDown(self): - settings.TWILIO_ACCOUNT_SID = os.environ['TWILIO_ACCOUNT_SID'] - settings.TWILIO_AUTH_TOKEN = os.environ['TWILIO_AUTH_TOKEN'] - - -class PlayTestCase(TestCase): - - def setUp(self): - self.client = Client() - self.factory = RequestFactory() - - # Test URIs. - self.uri = 'http://testserver/tests/views' - self.play_uri = '/tests/views/play/' - - # Guarantee a value for the required configuration settings after each - # test case. - settings.TWILIO_ACCOUNT_SID = 'xxx' - settings.TWILIO_AUTH_TOKEN = 'xxx' - - # Pre-calculate twilio signatures for our test views. - self.signature = encodestring( - new(settings.TWILIO_AUTH_TOKEN, - '%s/play/' % self.uri, sha1).digest()).strip() - - def test_play_no_url(self): - request = self.factory.post( - self.play_uri, HTTP_X_TWILIO_SIGNATURE=self.signature) - self.assertRaises(TypeError, play, request) - - def test_play_with_url(self): - request = self.factory.post( - self.play_uri, HTTP_X_TWILIO_SIGNATURE=self.signature) - self.assertEquals( - play(request, url='http://b.com/b.wav').status_code, 200) - - def tearDown(self): - settings.TWILIO_ACCOUNT_SID = os.environ['TWILIO_ACCOUNT_SID'] - settings.TWILIO_AUTH_TOKEN = os.environ['TWILIO_AUTH_TOKEN'] - - -class GatherTestCase(TestCase): - - def setUp(self): - self.client = Client() - self.factory = RequestFactory() - - # Test URIs. - self.uri = 'http://testserver/tests/views' - self.gather_uri = '/tests/views/gather/' - - # Guarantee a value for the required configuration settings after each - # test case. - settings.TWILIO_ACCOUNT_SID = 'xxx' - settings.TWILIO_AUTH_TOKEN = 'xxx' - - # Pre-calculate twilio signatures for our test views. - self.signature = encodestring( - new(settings.TWILIO_AUTH_TOKEN, - '%s/gather/' % self.uri, sha1).digest()).strip() - - def test_gather(self): - request = self.factory.post( - self.gather_uri, HTTP_X_TWILIO_SIGNATURE=self.signature) - self.assertEquals(gather(request).status_code, 200) - - def tearDown(self): - settings.TWILIO_ACCOUNT_SID = os.environ['TWILIO_ACCOUNT_SID'] - settings.TWILIO_AUTH_TOKEN = os.environ['TWILIO_AUTH_TOKEN'] - - -class RecordTestCase(TestCase): - - def setUp(self): - self.client = Client() - self.factory = RequestFactory() - - # Test URIs. - self.uri = 'http://testserver/tests/views' - self.record_uri = '/tests/views/record/' - - # Guarantee a value for the required configuration settings after each - # test case. - settings.TWILIO_ACCOUNT_SID = 'xxx' - settings.TWILIO_AUTH_TOKEN = 'xxx' - - # Pre-calculate twilio signatures for our test views. - self.signature = encodestring( - new(settings.TWILIO_AUTH_TOKEN, - '%s/record/' % self.uri, sha1).digest()).strip() - - def test_record(self): - request = self.factory.post( - self.record_uri, HTTP_X_TWILIO_SIGNATURE=self.signature) - self.assertEquals(record(request).status_code, 200) - - def tearDown(self): - settings.TWILIO_ACCOUNT_SID = os.environ['TWILIO_ACCOUNT_SID'] - settings.TWILIO_AUTH_TOKEN = os.environ['TWILIO_AUTH_TOKEN'] - - -class SmsTestCase(TestCase): - - def setUp(self): - self.client = Client() - self.factory = RequestFactory() - - # Test URIs. - self.uri = 'http://testserver/tests/views' - self.sms_uri = '/tests/views/sms/' - - # Guarantee a value for the required configuration settings after each - # test case. - settings.TWILIO_ACCOUNT_SID = 'xxx' - settings.TWILIO_AUTH_TOKEN = 'xxx' - - # Pre-calculate twilio signatures for our test views. - self.signature = encodestring( - new(settings.TWILIO_AUTH_TOKEN, - '%s/sms/' % self.uri, sha1).digest()).strip() - - def test_sms_no_message(self): - request = self.factory.post( - self.sms_uri, HTTP_X_TWILIO_SIGNATURE=self.signature) - self.assertRaises(TypeError, sms, request) - - def test_sms_with_message(self): - request = self.factory.post( - self.sms_uri, HTTP_X_TWILIO_SIGNATURE=self.signature) - self.assertEquals(sms(request, message='test').status_code, 200) - - def tearDown(self): - settings.TWILIO_ACCOUNT_SID = os.environ['TWILIO_ACCOUNT_SID'] - settings.TWILIO_AUTH_TOKEN = os.environ['TWILIO_AUTH_TOKEN'] - - -class DialTestCase(TestCase): - - def setUp(self): - self.client = Client() - self.factory = RequestFactory() - - # Test URIs. - self.uri = 'http://testserver/tests/views' - self.dial_uri = '/tests/views/dial/' - - # Guarantee a value for the required configuration settings after each - # test case. - settings.TWILIO_ACCOUNT_SID = 'xxx' - settings.TWILIO_AUTH_TOKEN = 'xxx' - - # Pre-calculate twilio signatures for our test views. - self.signature = encodestring( - new(settings.TWILIO_AUTH_TOKEN, - '%s/dial/' % self.uri, sha1).digest()).strip() - - def test_dial_no_number(self): - request = self.factory.post( - self.dial_uri, HTTP_X_TWILIO_SIGNATURE=self.signature) - self.assertRaises(TypeError, dial, request) - - def test_dial_with_number(self): - request = self.factory.post( - self.dial_uri, HTTP_X_TWILIO_SIGNATURE=self.signature) - self.assertEquals(dial( - request, number='+18182223333').status_code, 200) - - def tearDown(self): - settings.TWILIO_ACCOUNT_SID = os.environ['TWILIO_ACCOUNT_SID'] - settings.TWILIO_AUTH_TOKEN = os.environ['TWILIO_AUTH_TOKEN'] - - -class ConferenceTestCase(TestCase): - - def setUp(self): - self.client = Client() - self.factory = RequestFactory() - - # Test URIs. - self.uri = 'http://testserver/tests/views' - self.conf_uri = '/tests/views/conference/' - - # Guarantee a value for the required configuration settings after each - # test case. - settings.TWILIO_ACCOUNT_SID = 'xxx' - settings.TWILIO_AUTH_TOKEN = 'xxx' - - # Pre-calculate twilio signatures for our test views. - self.signature = encodestring( - new(settings.TWILIO_AUTH_TOKEN, - '%s/conference/' % self.uri, sha1).digest()).strip() - - def test_conference_no_name(self): - request = self.factory.post( - self.conf_uri, HTTP_X_TWILIO_SIGNATURE=self.signature) - self.assertRaises(TypeError, conference, request) - - def test_conference_with_name(self): - request = self.factory.post( - self.conf_uri, HTTP_X_TWILIO_SIGNATURE=self.signature) - self.assertEquals(conference(request, name='a').status_code, 200) - - def tearDown(self): - settings.TWILIO_ACCOUNT_SID = os.environ['TWILIO_ACCOUNT_SID'] - settings.TWILIO_AUTH_TOKEN = os.environ['TWILIO_AUTH_TOKEN'] diff --git a/django_twilio/utils.py b/django_twilio/utils.py index ae312fb..617ea0a 100644 --- a/django_twilio/utils.py +++ b/django_twilio/utils.py @@ -1,17 +1,21 @@ # -*- coding: utf-8 -*- -"""Useful utility functions.""" +from __future__ import unicode_literals, absolute_import + +""" +Useful utility functions. +""" import os from django.http import HttpResponse from django.conf import settings -from twilio.twiml import Response +from twilio import twiml -from django_twilio.models import Caller, Credential +from .models import Caller, Credential -def discover_twilio_creds(user=None): +def discover_twilio_credentials(user=None): """ Due to the multiple ways of providing SID / AUTH tokens through this package, this function will search in the various places that credentials might be stored. @@ -23,24 +27,32 @@ def discover_twilio_creds(user=None): 2. Environment variables 3. django.conf settings - We recommend using enviornment variables were possible, it is the + We recommend using environment variables were possible; it is the most secure option - """ SID = 'TWILIO_ACCOUNT_SID' AUTH = 'TWILIO_AUTH_TOKEN' if user: - creds = Credential.objects.filter(user=user.id) - if creds.exists(): - creds = creds[0] - return (creds.account_sid, creds.auth_token) + credentials = Credential.objects.filter(user=user.id) + if credentials.exists(): + credentials = credentials[0] + return credentials.account_sid, credentials.auth_token + + if SID in os.environ and AUTH in os.environ: + return os.environ[SID], os.environ[AUTH] - if SID and AUTH in os.environ: - return (os.environ[SID], os.environ[AUTH]) + if hasattr(settings, SID) and hasattr(settings, AUTH): + return settings.TWILIO_ACCOUNT_SID, settings.TWILIO_AUTH_TOKEN - return (settings.TWILIO_ACCOUNT_SID, settings.TWILIO_AUTH_TOKEN) + raise AttributeError( + "Could not find {sid} or {auth} in environment variables, " + "User credentials, or django project settings.".format( + sid=SID, + auth=AUTH, + ) + ) def get_blacklisted_response(request): @@ -56,10 +68,14 @@ def get_blacklisted_response(request): try: caller = Caller.objects.get(phone_number=request.REQUEST['From']) if caller.blacklisted: - r = Response() + r = twiml.Response() r.reject() return HttpResponse(str(r), content_type='application/xml') - except Exception, e: + except Exception: pass return None + + +# Backwards compatibility for a poorly named function +discover_twilio_creds = discover_twilio_credentials diff --git a/django_twilio/views.py b/django_twilio/views.py index 43579ea..e0fe71e 100644 --- a/django_twilio/views.py +++ b/django_twilio/views.py @@ -1,10 +1,15 @@ -from twilio.twiml import Response -from django_twilio.decorators import twilio_view +# -*- coding: utf-8 -*- +from __future__ import unicode_literals, absolute_import + +from twilio import twiml + +from .decorators import twilio_view @twilio_view def say(request, text, voice=None, language=None, loop=None): - """See: http://www.twilio.com/docs/api/twiml/say. + """ + See: http://www.twilio.com/docs/api/twiml/say. Usage:: @@ -15,14 +20,15 @@ def say(request, text, voice=None, language=None, loop=None): # ... ) """ - r = Response() + r = twiml.Response() r.say(text, voice=voice, language=language, loop=loop) return r @twilio_view def play(request, url, loop=None): - """See: twilio's website: http://www.twilio.com/docs/api/twiml/play. + """ + See: http://www.twilio.com/docs/api/twiml/play. Usage:: @@ -35,7 +41,7 @@ def play(request, url, loop=None): # ... ) """ - r = Response() + r = twiml.Response() r.play(url, loop=loop) return r @@ -43,7 +49,8 @@ def play(request, url, loop=None): @twilio_view def gather(request, action=None, method='POST', num_digits=None, timeout=None, finish_on_key=None): - """See: http://www.twilio.com/docs/api/twiml/gather. + """ + See: http://www.twilio.com/docs/api/twiml/gather. Usage:: @@ -54,7 +61,7 @@ def gather(request, action=None, method='POST', num_digits=None, timeout=None, # ... ) """ - r = Response() + r = twiml.Response() r.gather(action=action, method=method, numDigits=num_digits, timeout=timeout, finishOnKey=finish_on_key) return r @@ -64,7 +71,8 @@ def gather(request, action=None, method='POST', num_digits=None, timeout=None, def record(request, action=None, method='POST', timeout=None, finish_on_key=None, max_length=None, transcribe=None, transcribe_callback=None, play_beep=None): - """See: http://www.twilio.com/docs/api/twiml/record. + """ + See: http://www.twilio.com/docs/api/twiml/record. Usage:: @@ -75,7 +83,7 @@ def record(request, action=None, method='POST', timeout=None, # ... ) """ - r = Response() + r = twiml.Response() r.record(action=action, method=method, timeout=timeout, finishOnKey=finish_on_key, maxLength=max_length, transcribe=transcribe, transcribeCallback=transcribe_callback, @@ -86,7 +94,8 @@ def record(request, action=None, method='POST', timeout=None, @twilio_view def sms(request, message, to=None, sender=None, action=None, method='POST', status_callback=None): - """See: http://www.twilio.com/docs/api/twiml/sms. + """ + See: http://www.twilio.com/docs/api/twiml/sms. Usage:: @@ -99,7 +108,7 @@ def sms(request, message, to=None, sender=None, action=None, method='POST', # ... ) """ - r = Response() + r = twiml.Response() r.message(msg=message, to=to, sender=sender, method='POST', action=action, statusCallback=status_callback) return r @@ -108,7 +117,8 @@ def sms(request, message, to=None, sender=None, action=None, method='POST', @twilio_view def dial(request, number, action=None, method='POST', timeout=None, hangup_on_star=None, time_limit=None, caller_id=None): - """See: http://www.twilio.com/docs/api/twiml/dial. + """ + See: http://www.twilio.com/docs/api/twiml/dial. Usage:: @@ -119,7 +129,7 @@ def dial(request, number, action=None, method='POST', timeout=None, # ... ) """ - r = Response() + r = twiml.Response() r.dial(number=number, action=action, method=method, timeout=timeout, hangupOnStar=hangup_on_star, timeLimit=time_limit, callerId=caller_id) @@ -130,7 +140,8 @@ def dial(request, number, action=None, method='POST', timeout=None, def conference(request, name, muted=None, beep=None, start_conference_on_enter=None, end_conference_on_exit=None, wait_url=None, wait_method='POST', max_participants=None): - """See: http://www.twilio.com/docs/api/twiml/conference. + """ + See: http://www.twilio.com/docs/api/twiml/conference. Usage:: @@ -142,7 +153,7 @@ def conference(request, name, muted=None, beep=None, # ... ) """ - r = Response() + r = twiml.Response() r.dial().conference(name=name, muted=muted, beep=beep, startConferenceOnEnter=start_conference_on_enter, endConferenceOnExit=end_conference_on_exit, diff --git a/docs/source/conf.py b/docs/source/conf.py index 4a7b265..fd395fb 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,5 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +from __future__ import unicode_literals, absolute_import # # complexity documentation build configuration file, created by # sphinx-quickstart on Tue Jul 9 22:26:36 2013. @@ -12,7 +13,8 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import os +import sys RTD_NEW_THEME = True @@ -54,8 +56,8 @@ master_doc = 'index' # General information about the project. -project = u'django-twilio' -copyright = u'2012-2014, Randall Degges' +project = 'django-twilio' +copyright = '2012-2014, Randall Degges' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -66,7 +68,7 @@ # The full version, including alpha/beta/rc tags. release = django_twilio.__version__ -# The language for content autogenerated by Sphinx. Refer to documentation +# The language for content auto-generated by Sphinx. Refer to documentation # for a list of supported languages. #language = None @@ -187,21 +189,25 @@ # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'django-twilio.tex', u'django-twilio Documentation', - u'Randall Degges', 'manual'), + ( + 'index', + 'django-twilio.tex', + 'django-twilio Documentation', + 'Randall Degges', 'manual' + ), ] # The name of an image file (relative to this directory) to place at the top of @@ -230,8 +236,25 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'django-twilio', u'django-twilio Documentation', - [u'Randall, Degges', u'Paul Hallett'], 1) + ( + # Source start file + 'index', + + # Name + 'django-twilio', + + # Description + 'django-twilio Documentation', + + # Authors + [ + 'Randall, Degges', + 'Paul Hallett', + ], + + # Manual section + 1, + ) ] # If true, show URL addresses after external links. @@ -244,9 +267,28 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'django-twilio', u'django-twilio Documentation', - u'Randall Degges', 'django-twilio', 'Twilio integration for Django', - 'Miscellaneous'), + ( + # Source start file + 'index', + + # Target name + 'django-twilio', + + # Title + 'django-twilio Documentation', + + # Author + 'Randall Degges', + + # Dir menu entry + 'django-twilio', + + # Description + 'Twilio integration for Django', + + # Category + 'Miscellaneous', + ), ] # Documents to append as an appendix to all manuals. diff --git a/docs/source/contribute.rst b/docs/source/contribute.rst index 6693d8c..a232a8b 100644 --- a/docs/source/contribute.rst +++ b/docs/source/contribute.rst @@ -1,7 +1,7 @@ Contributing ============ -django-twilio is always under development, and welcomes any contributions! +``django-twilio`` is always under development, and welcomes any contributions! If you'd like to get your hands dirty with the source code, please fork the project on `our GitHub page `_. @@ -12,12 +12,15 @@ Setup ----- 1. Fork the project on Github -2. Create a separate, **well named** branch to work on, off of the **develop** branch. +2. Create a separate, **well named** branch to work on, off of the **develop** + branch. 3. Install the requirements using pip:: $ pip install -r requirements.txt + $ pip install -r test_requirements.txt -You should now have the django-twilio source code and development ready to go. +You should now have the ``django-twilio`` source code and development +environment ready to go. Style ----- @@ -28,7 +31,7 @@ codebase. Right now, that means: * 100% `PEP-8 compliance `_. * Proper spelling / punctuation in the source code. -After setting up your developer environment you can run:: +After setting up your development environment, you can run:: $ make lint @@ -50,77 +53,84 @@ documentation online at `ReadTheDocs `_. Tests ----- -In order to ensure high-quality releases, django-twilio aims to have an +In order to ensure high-quality releases, ``django-twilio`` aims to have an extensive test suite. All test suite patches and additions are welcome, and encouraged for new developers! The tests are well documented, and can be a great way to introduce yourself to the codebase! -To run the tests, you'll need to do the following: +To run the tests, you can either use:: + $ make test -1. Before running these tests, you need to set up some environment variables. - If you're using virtualenv, open the */bin/activate* file in vi or nano and - add the following to the end:: - - export TWILIO_ACCOUNT_SID=XXXXXXXXXXXXXX - export TWILIO_AUTH_TOKEN=YYYYYYYYYYYYYYY - -Obviously you'll need to replace this with your own account details. - -.. note:: - - The test suite will not cost you any credit from your Twilio account. - -2. Run the test suite using the run_tests.py command like so:: +or:: - $ make test + $ python manage.py test You'll see output that looks something like:: + nosetests --with-coverage --cover-package=django_twilio --verbosity=1 Creating test database for alias 'default'... - .............................. - ------------------------------ - Ran 30 tests in 0.071s + ...................................... + Name Stmts Miss Cover Missing + --------------------------------------------------------------------------- + django_twilio 2 0 100% + django_twilio.client 4 0 100% + django_twilio.decorators 50 4 92% 74-75, 103-104 + django_twilio.migrations 0 0 100% + django_twilio.models 20 0 100% + django_twilio.settings 3 0 100% + django_twilio.south_migrations 0 0 100% + django_twilio.south_migrations.0001_initial 14 2 86% 30-33 + django_twilio.utils 30 2 93% 44, 49 + django_twilio.views 38 0 100% + --------------------------------------------------------------------------- + TOTAL 161 8 95% + ---------------------------------------------------------------------- + Ran 38 tests in 0.184s OK Destroying test database for alias 'default'... -That's it! As you can see, when you run the test suite, django-twilio should +That's it! As you can see, when you run the test suite, ``django-twilio`` should output not only failing test results, but also the coverage reports. -When you submit patches or add functionality to django-twilio, be sure to run -the test suite to ensure that no functionality is broken. +When you submit patches or add functionality to ``django-twilio``, be sure to +run the test suite to ensure that no functionality is broken! Workflow -------- -When contributing to django-twilio, here's a typical developer workflow:: +When contributing to ``django-twilio``, here's a typical developer workflow:: # Preparing the environment: + $ pip install --upgrade virtualenv virtualenvwrapper + $ source "/usr/local/bin/virtualenvwrapper.sh" $ mkvirtualenv --no-site-packages djtw - $ cd ~/django_twilio - $ git checkout develop - $ pip install -r requirements.txt + (djtw)$ git clone https://github.com//django-twilio.git + (djtw)$ cd django_twilio/ + (djtw)$ git remote add upstream https://github.com/rdegges/django-twilio.git + (djtw)$ git checkout develop + (djtw)$ git pull upstream develop + (djtw)$ pip install -r requirements.txt + (djtw)$ pip install -r test_requirements.txt # Hacking: - $ cd ~/django_twilio/django_twilio - $ git checkout develop - $ vim ... + (djtw)$ git checkout develop + (djtw)$ vim ... <<< hack time >>> # Writing tests: - $ cd ~/django_twilio/django_twilio/tests + (djtw)$ cd test_project/test_app/ $ vim ... <<< hack time >>> # Running tests: - $ cd ~/django_twilio/test_project - $ workon djtw - $ make test + (djtw)$ cd django_twilio/ + (djtw)$ make test <<< check test output >>> .. note:: @@ -132,7 +142,7 @@ When contributing to django-twilio, here's a typical developer workflow:: Bugs / Feature Requests / Comments ---------------------------------- -If you've got any concerns about django-twilio, make your voice heard by +If you've got any concerns about ``django-twilio``, make your voice heard by posting an issue on our `GitHub issue tracker `_. All bugs / feature requests / comments are welcome. diff --git a/docs/source/contributors.rst b/docs/source/contributors.rst index 6f923a9..09314b8 100644 --- a/docs/source/contributors.rst +++ b/docs/source/contributors.rst @@ -1,8 +1,8 @@ Contributors ------------ -The Django-twilio library is the result of effort given by the following awesome -Twilio / Django / Python community members: +The ``django-twilio`` library is the result of effort given by the following +awesome Twilio / Django / Python community members: * `Randall Degges `_ - original author * `Paul Hallett `_ - current maintainer @@ -19,5 +19,7 @@ Twilio / Django / Python community members: * `Florian Le Goff `_ * `Richard Kolkovich `_ * `Sunah Suh `_ +* `Daniel Hawkins `_ -Without their support we wouldn't have such a well written library, thanks folks! +Without their support, we wouldn't have such a well-written library. +Thanks folks! diff --git a/docs/source/decorators.rst b/docs/source/decorators.rst index abd46a3..8be8446 100644 --- a/docs/source/decorators.rst +++ b/docs/source/decorators.rst @@ -1,34 +1,34 @@ Decorators ========== -One of the key features of django-twilio is making it easy to build Django views that return TwiML instructions back to Twilio, without having to deal with all the complex security issues. +One of the key features of ``django-twilio`` is making it easy to build Django +views that return TwiML instructions back to Twilio, without having to deal with +all the complex security issues. All-In-One Decorator -------------------- -The most useful decorator that ships with django-twilio is -:func:`twilio_view`, which will make your life much -easier. +The most useful decorator that ships with ``django-twilio`` is ``twilio_view``, +which will make your life much easier. -The :func:`django_twilio.decorators.twilio_view` decorator: +The ``django_twilio.decorators.twilio_view`` decorator: 1. Protects your views against forgery, and ensures that the request which hits - your view originated from twilio's servers. This way, you don't have to + your view originated from Twilio's servers. This way, you don't have to worry about fake requests hitting your views that may cause you to launch - calls, or waste any money on fradulent activity. + calls, or waste any money on fraudulent activity. -2. Ensures your view is CSRF exempt. Since twilio will always POST data to your - views, you'd normally have to explicitly declare your view CSRF exempt. - :func:`django_twilio.decorators.twilio_view` does this automatically. +2. Ensures your view is CSRF exempt. Since Twilio will always POST data to your + views, you'd normally have to explicitly declare your view CSRF exempt. The + decorator does this automatically. -3. Enforces a blacklist. If you've got any - :class:`django_twilio.models.Caller` objects who are blacklisted, any - service requests from them will be rejected. +3. Enforces a blacklist. If you've got any :class:`django_twilio.models.Caller` + objects who are blacklisted, any service requests from them will be rejected. .. note:: You can manage your blacklist via the Django admin panel (if you have it - enabled). django-twilio provides a ``Caller`` admin hook that allows you - to create new callers, and blacklist them if you wish. + enabled). ``django-twilio`` provides a ``Caller`` admin hook that allows + you to create new callers, and blacklist them if you wish. 4. Allows you to (optionally) return raw TwiML responses without building an ``HttpResponse`` object. This can save a lot of redundant typing. @@ -38,34 +38,35 @@ Example usage Let's take a look at an example:: - from twilio.twiml import Response + from twilio import twiml from django_twilio.decorators import twilio_view @twilio_view def reply_to_sms_messages(request): - r = Response() + r = twiml.Response() r.message('Thanks for the SMS message!') return r -In the example above, we built a view that twilio can POST data to, and that -will instruct twilio to send a SMS message back to the person who messaged us -saying "Thanks for the SMS message!". +In the example above, we built a view that Twilio can POST data to, and that +will instruct Twilio to send a SMS message back to the person who messaged us +saying, "Thanks for the SMS message!". How Forgery Protection Works -**************************** +---------------------------- Forgery protection is extremely important when writing Twilio code. Since your -code will be doing stuff that costs money (sending calls, SMS messages, -etc.), ensuring all incoming HTTP requests actually originate from Twilio is -really important. +code will be doing stuff that costs money (sending calls, SMS messages, etc.), +ensuring all incoming HTTP requests actually originate from Twilio is really +important. -The way django-twilio implements forgery protection is by checking for a specific -flag in the django configuration settings:: +The way ``django-twilio`` implements forgery protection is by checking for a +specific flag in the django configuration settings:: - DJANGO_TWILIO_FORGERY_PROTECTION = False + DJANGO_TWILIO_FORGERY_PROTECTION = False -If this is not set, this will default to the **opposite** of ``settings.DEBUG``, so in debug mode the forgery protection will be off. +If this setting is not present, it will default to the **opposite** of +``settings.DEBUG``; in debug mode, forgery protection will be off. This behavior has been specifically implemented this way so that, while in development mode, you can: diff --git a/docs/source/foreword.rst b/docs/source/foreword.rst index b8c3403..0f9251e 100644 --- a/docs/source/foreword.rst +++ b/docs/source/foreword.rst @@ -2,43 +2,44 @@ Foreword ======== -Read this before you get started with django-twilio. This will hopefully answer -some questions about the purpose of the project, and why you should (or +Read this before you get started with ``django-twilio``. This will hopefully +answer some questions about the purpose of the project, and why you should (or shouldn't) be using it. Purpose ======= Building telephony applications has always been something of a complex and time -consuming task for developers. With `Twilio `_'s entry -into the telephony world, developers were able to build large scale telephony +consuming task for developers. With the advent of `Twilio +`_, developers were able to build large scale telephony applications for the first time, without the massive learning curve associated with traditional telephony development. -While Twilio's APIs allow you to build powerful voice & sms apps in your +While Twilio's APIs allow you to build powerful voice and SMS apps in your favorite programming language, it can still be quite difficult and time -consuming to roll out your own telephony apps for your Django-powered website. +consuming to roll out your own telephony apps for your +`Django `_-powered website. -django-twilio's core purpose is to abstract away as much telephony knowledge as -possible, so that you can focus on the functionality and logic of your -telephony app, and have it seamlessly integrate into your website without +The core purpose of ``django-twilio`` is to abstract away as much telephony +knowledge as possible, so that you can focus on the functionality and logic of +your telephony app, and have it seamlessly integrate into your website without confusion. Use Case ======== -django-twilio is a complete solution for anyone who wants to integrate voice or -SMS functionality into their django website without requiring any additional -infrastructure. +``django-twilio`` is a complete solution for anyone who wants to integrate +voice or SMS functionality into their Django website without requiring any +additional infrastructure. Here are some common use cases: * You want to build one or more telephone conference rooms for talking with co-workers or clients. * You want be able to accept SMS messages from your clients, and respond to - them pragmatically. -* You want to record import phone calls (incoming or outgoing) and store the - call recordings for analysis. + them programmatically. +* You want to record important phone calls (incoming or outgoing) and store + the recordings for analysis. * You want to track telephone marketing campaigns, and review detailed data. * You want to build rich, interactive telephone systems. (EG: "Press 1 to talk with a sales agent, press 2 to schedule a reservation, press 3 to purchase a @@ -48,14 +49,14 @@ Here are some common use cases: Prerequisite Knowledge ====================== -Before getting started with django-twilio, it will serve you best to read the -`How it Works `_ page on Twilio's website. That +Before getting started with ``django-twilio``, it will serve you best to read +the `How it Works `_ page on Twilio's website. That describes the architecture and API flow that all of your applications will be -using, and that django-twilio will help to abstract. +using, and that ``django-twilio`` will help to abstract. -django-twilio also depends on the official `Twilio python library +``django-twilio`` also depends on the official `Twilio python library `_, so you may want to familiarize yourself with their docs before continuing so you have a good idea of how things work. -Other then that, you're good to go! +Other than that, you're good to go! diff --git a/docs/source/gotchas.rst b/docs/source/gotchas.rst index 1669396..a2cd407 100644 --- a/docs/source/gotchas.rst +++ b/docs/source/gotchas.rst @@ -2,46 +2,48 @@ Gotchas ======= Below is a list (which is being continuously expanded) on things which may "get -you" at one point or another. We've done our best to try and make django-twilio -as easy to use as possible, but sometimes problems are unavoidable! +you" at one point or another. We've done our best to try and make +``django-twilio`` as easy to use as possible, but sometimes problems are +unavoidable! Help! I Get HTTP 403 Forbidden ------------------------------ -There are two common problems that cause django-twilio to return HTTP 403 errors -in your views: +There are two common problems that cause ``django-twilio`` to return HTTP 403 +errors in your views: Forgery Protection ------------------- +****************** -django-twilio has built in forgery protection in some decorators -to help verify that requests made to any of your twilio views actually -originate from twilio. +``django-twilio`` has built in forgery protection in some decorators +to help verify that requests made to any of your Twilio views actually +originate from Twilio. We do this by analyzing HTTP requests sent to your views and comparing a special cryptographic hash. This way, attackers are not able to simply POST data to your -views and waste your twilio resources. Attacks of this nature can be expensive +views and waste your Twilio resources. Attacks of this nature can be expensive and troublesome. In the event that HTTP requests to your views are determined to be forged, -django-twilio will return an HTTP 403 (forbidden) response. +``django-twilio`` will return an HTTP 403 (forbidden) response. Because of the way this forgery protection works, you'll get HTTP 403 errors -when hitting django-twilio views if you test them yourself and you have +when hitting ``django-twilio`` views if you test them yourself and you have ``settings.DEBUG = False``. If you'd like to test your views, be sure to do so with Django's DEBUG setting ON. Missing Settings ----------------- +**************** -django-twilio *requires* that you specify the variables ``TWILIO_ACCOUNT_SID`` -and ``TWILIO_AUTH_TOKEN`` either in your site's settings module, or as environment -variables. These are used to verify the legitimacy of HTTP requests to your -twilio views and to instantiate the TwilioRestClient. +``django-twilio`` *requires* that you specify the variables +``TWILIO_ACCOUNT_SID`` and ``TWILIO_AUTH_TOKEN``, either as environment +variables, or in your site's ``settings`` module. These are used to verify the +legitimacy of HTTP requests to your Twilio views, and to instantiate the +TwilioRestClient. -If these variables are missing, django-twilio will raise HTTP 403 (forbidden) -errors since it is unable to determine whether or not the HTTP request -originated from twilio. +If these variables are missing, ``django-twilio`` will raise HTTP 403 +(forbidden) errors, because it is unable to determine whether or not the HTTP +request originated from Twilio. -To fix this, simply add these variables into your site's settings module, or -into your environment variables. +To fix this, simply set these environment variables or add them to your +settings variables. diff --git a/docs/source/howto.rst b/docs/source/howto.rst index cdb87b7..dcbee42 100644 --- a/docs/source/howto.rst +++ b/docs/source/howto.rst @@ -1,25 +1,25 @@ How To... ========= -This section documents lots of commong "how do I..." questions. Since -django-twilio has a lot of native functionality, some features that don't +This section documents lots of common "How do I..." questions. Since +``django-twilio`` has a lot of native functionality, some features that don't necessarily fit into other parts of the documentation are only documented here. Blacklist Callers ----------------- One common problem is dealing with users who abuse services. Regardless of what -your telephony app does--it can be dangerous and expensive to run your +your telephony app does, it can be dangerous and expensive to run your application without the ability to blacklist users. -Luckily, django-twilio provides built-in blacklist functionality, and will fit -your needs (whatever they may be). +Luckily, ``django-twilio`` provides built-in blacklist functionality, and will +fit your needs (whatever they may be). Hypothetical Abuse Scenario --------------------------- -Let's say you run a twilio app that forwards calls from your twilio toll-free -number to your private business number. Since you have to pay for twilio calls, +Let's say you run a Twilio app that forwards calls from your Twilio toll-free +number to your private business number. Since you have to pay for Twilio calls, it could potentially become very expensive for you if a caller repeatedly calls your toll-free number, causing you to quickly drain your account balance. @@ -27,7 +27,7 @@ Blacklisting Callers via Django Admin ------------------------------------- The simplest way to blacklist callers is via the Django admin panel. If you're -using the django-admin panel, you'll see a django-twilo Caller section, that +using the Django admin panel, you'll see a ``django-twilo`` Caller section that allows you to manage callers. To blacklist a caller, do the following: @@ -35,16 +35,17 @@ To blacklist a caller, do the following: 1. Click the ``Add`` button next to the ``Caller`` object in the admin panel. If you're running the server locally, the URL would be: http://localhost:8000/admin/django_twilio/caller/add/. -2. Enter in the caller's details who you wish to block. The phone number should +2. Enter in the caller's details that you wish to block. The phone number should be of the form: ``+1NXXNXXXXXX`` (`E.164 format `_). -3. Check the ``Blacklisted`` box. +3. Check the ``blacklisted`` box. 4. Click the ``Save`` button. -Now--any of of django-twilio's built in views and decorators will automatically +Now any ``django-twilio`` built-in views or decorators will automatically reject calls from the specified caller! .. note:: - This does NOT effect code that does NOT use django-twilio. For example, if - you write code that places outbound calls or SMS messages, since your code - won't be interacting with django-twilio--the blacklist will NOT be honored. + This does NOT effect code that does NOT use ``django-twilio``. For example, + if you write code that places outbound calls or SMS messages, since your code + won't be interacting with ``django-twilio``, the blacklist will NOT be + honored. diff --git a/docs/source/install.rst b/docs/source/install.rst index 421ac8d..278bf01 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -1,9 +1,9 @@ Installation ============ -Installing django-twilio is simple. +Installing ``django-twilio`` is simple. -Firstly, download and install django-twilio. +Firstly, download and install the ``django-twilio`` package. The easiest way is with `pip `_ :: @@ -14,11 +14,17 @@ The easiest way is with `pip Requirements ------------ +``django-twilio`` will automatically install the official `twilio-python library +`_. The ``twilio-python`` library helps you +rapidly build Twilio applications, and it is heavily suggested that you check +out that project before using ``django-twilio``. -Django-twilio will automatically install the official `twilio-python library -`_. The twilio-python library helps you rapidly build twilio applications, and it is heavily suggested that you check out that project before using django-twilio. - -If you are using django-twilio with Django version 1.6.* or less, you **will** need to install `South 1.0 `_. Older versions of South will not work. This is due to to recent database migration changes that have happened with Django 1.7, you can read more about `the changes here `_. +If you are using ``django-twilio`` with Django version 1.6.* or less, you **will** +need to install `South 1.0 `_. +Older versions of South will not work. This is due to to recent database migration +changes that have happened with Django 1.7, you can read more about `the changes here +`_. +You will also need to install South 1.0 or higher if you are using Python 3. If you already have South installed, upgrading is easy:: @@ -28,8 +34,8 @@ If you already have South installed, upgrading is easy:: Django Integration ------------------ -After django-twilio is installed, add it to your ``INSTALLED_APPS`` tuple in -your settings.py file:: +After ``django-twilio`` is installed, add it to your ``INSTALLED_APPS`` tuple in +your ``settings.py`` file:: INSTALLED_APPS = ( 'django.contrib.auth', @@ -49,7 +55,9 @@ your settings.py file:: Databases for Django 1.6 or lower --------------------------------- -To use django-twilio with Django 1.6.* or less, you will need to install `South 1.0 `_ with the following terminal command:: +To use ``django-twilio`` with Django 1.6.* or less, you will need to install +`South 1.0 `_ with the following terminal +command:: $ pip install South==1.0 @@ -76,7 +84,7 @@ and run the default ```syncdb``` command:: $ python manage.py syncdb -To sync the django-twilio models to the database run:: +To sync the ``django-twilio`` models to the database run:: $ python manage.py migrate django_twilio @@ -84,14 +92,16 @@ To sync the django-twilio models to the database run:: Databases for Django 1.7 ------------------------ -Django 1.7 has built in migrations, so there is no need to install any third-party schema management tool. To sync the django-twilio models with your Django 1.7 project just run:: +Django 1.7 has built in migrations, so there is no need to install any +third-party schema management tool. To sync the ``django-twilio`` models +with your Django 1.7 project, just run:: $ python manage.py migrate Upgrading --------- -Upgrading django-twilio gracefully is easy using pip:: +Upgrading ``django-twilio`` gracefully is easy using pip:: $ pip install --upgrade django-twilio @@ -103,44 +113,46 @@ Then you just need to update the models:: Authentication Token Management ------------------------------- -Django-twilio offers multiple ways to add your Twilio credentials to your +``django-twilio`` offers multiple ways to add your Twilio credentials to your Django application. The ``TWILIO_ACCOUNT_SID`` and ``TWILIO_AUTH_TOKEN`` variables can be found by -logging into your `twilio account dashboard +logging into your `Twilio account dashboard `_. These tokens are used to communicate -with the twilio API, be sure to keep these credentials safe! +with the Twilio API, so be sure to keep these credentials safe! The order in which Django will check for credentials is: 1. Environment variables in your environment - 3. Settings variables in the Django settings + 2. Settings variables in the Django settings -We recommend you use environment variables as these keep secret tokens out -of your code base (and therefore they're far more secure). +We recommend using environment variables so you can keep secret tokens out +of your code base. This practice is far more secure. -Using virtualenv open up your /bin/activate.sh file and add the following to the -end:: +Using ``virtualenv``, open up your ``/bin/activate.sh`` file and add the +following to the end:: export TWILIO_ACCOUNT_SID=XXXXXXXXXXXXX export TWILO_AUTH_TOKEN=YYYYYYYYYYYY You'll need to deactivate and restart your virtualenv for it to take effect. -To use settings variables, you'll need to add them to your settings.py file:: +To use settings variables, you'll need to add them to your ``settings.py`` +file:: TWILIO_ACCOUNT_SID = 'ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' TWILIO_AUTH_TOKEN = 'YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY' .. note:: - Storing tokens in your settings.py is bad security! Only do this if you are certain you will not be sharing the file publicly. + Storing tokens in ``settings.py`` is very bad for security! Only do this + if you are certain you will not be sharing the file publicly. And optionally add the default caller:: TWILIO_DEFAULT_CALLERID = 'NNNNNNNNNN' -If you specify a value for ``TWILIO_DEFAULT_CALLERID``, than all SMS and voice -messages sent through django-twilio's functions will use the default caller id +If you specify a value for ``TWILIO_DEFAULT_CALLERID``, then all SMS and voice +messages sent through ``django-twilio`` functions will use the default caller id as a convenience. You can create a Credential object to store your variables if you want to use @@ -150,13 +162,13 @@ When you want to use the credentials in a Credential object you need to manually build a TwilioRestClient like so:: from twilio.rest import TwilioRestClient - from django_twilio.utils import discover_twilio_creds + from django_twilio.utils import discover_twilio_credentials from django.contrib.auth.models import User - myUser = User.objects.get(pk=USER_ID) + my_user = User.objects.get(pk=USER_ID) - creds = discover_twilio_creds(myUser) + account_sid, auth_token = discover_twilio_credentials(my_user) - # Here we'll build a new twilio_client with different credentials - twilio_client = TwilioRestClient(creds[0], creds[1]) + # Here we'll build a new Twilio_client with different credentials + twilio_client = TwilioRestClient(account_sid, auth_token) diff --git a/docs/source/rest.rst b/docs/source/rest.rst index 95c13d6..223cc98 100644 --- a/docs/source/rest.rst +++ b/docs/source/rest.rst @@ -2,8 +2,8 @@ Accessing Twilio Resources ========================== Let's say you're building a Twilio application that needs access to all of your -account data--stuff like call logs, recordings, SMS messages, etc. -django-twilio makes accessing this information extremely easy. +account data -- stuff like call logs, recordings, SMS messages, etc. +``django-twilio`` makes accessing this information extremely easy. The Twilio REST Client @@ -23,39 +23,38 @@ How it Works If you are using the `Twilio python library `_ by itself (without -django-twilio), you could see a list of all the phone numbers you have +``django-twilio``), you could see a list of all the phone numbers you have provisioned to your Twilio account by running the following code:: from twilio.rest import TwilioRestClient - # Your private Twilio API creds. + # Your private Twilio API credentials. ACCOUNT_SID = 'xxx' AUTH_TOKEN = 'xxx' - client = TwilioRestClient(ACCOUNT_SID, AUTH_TOKEN) for number in client.phone_numbers.iter(): - print number.friendly_name + print(number.friendly_name) While this is really convenient, it breaks the `Don't Repeat Yourself -`_ law of software +`_ rule of software engineering by making you manually specify your account credentials. -Since django-twilio already requires you to enter your Twilio credentials in -your ``settings.py`` file, django-twilio provides a simple wrapper around -TwilioRestClient, :attr:`django_twilio.client.twilio_client`. +Since ``django-twilio`` already requires you to enter your Twilio credentials in +your ``settings.py`` file, ``django-twilio`` provides a simple wrapper around +``TwilioRestClient``: ``django_twilio.client.twilio_client``. -The twilio_client Wrapper -------------------------- +The ``twilio_client`` Wrapper +----------------------------- -As mentioned in the previous section, django-twilio ships with an instantiated -TwilioRestClient, so that you can use the `Twilio REST API +As mentioned in the previous section, ``django-twilio`` ships with an +instantiated ``TwilioRestClient``, so that you can use the `Twilio REST API `_ with -as little effort as possible :) +as little effort as possible. :) -Using :attr:`django_twilio.client.twilio_client`, you can print a list of all +Using ``django_twilio.client.twilio_client``, you can print a list of all the phone numbers you have provisioned to your Twilio account by running the following code:: @@ -63,9 +62,9 @@ following code:: for number in twilio_client.phone_numbers.iter(): - print number.friendly_name + print(number.friendly_name) -See how you didn't have to worry about credentials or anything? Niceeee. +See how you didn't have to worry about credentials or anything? Niiiiice. Further Reading @@ -90,6 +89,6 @@ can: * etc... To learn more about what you can do, I suggest reading the `Twilio REST -documentation `_ and the `twilio-python +documentation `_ and the `twilio-python REST documentation -`_. +`_. diff --git a/docs/source/settings.rst b/docs/source/settings.rst index f24ed2c..a5f1a45 100644 --- a/docs/source/settings.rst +++ b/docs/source/settings.rst @@ -3,79 +3,80 @@ Settings Django-twilio has various settings that can be used to turn features on and off. -Here we explain each setting, it's requirement, and why you might want to use it. +Here we explain each setting, its requirement, and why you might want to use it. -Each setting should be placed in your settings.py file, except the ``TWILIO_ACCOUNT_SID`` and ``TWILIO_AUTH_TOKEN`` variables, which should be kept in your environment variables to ensure proper security. +Each setting should be placed in your ``settings.py`` file, except the +``TWILIO_ACCOUNT_SID`` and ``TWILIO_AUTH_TOKEN`` variables, which should be kept +in your environment variables to ensure their security. -**TWILIO_ACCOUNT_SID** +TWILIO_ACCOUNT_SID (REQUIRED) +----------------------------- -**REQUIRED** +The ``TWILIO_ACCOUNT_SID`` setting is required, and will throw an exception if +it is not configured correctly:: -The :func:`TWILIO_ACCOUNT_SID` setting is required and will throw an exception -if it is not configured correctly:: - - TWILIO_ACCOUNT_SID='SIDXXXXXXXXXXXXXXX' + TWILIO_ACCOUNT_SID = 'SIDXXXXXXXXXXXXXXX' This setting is used to authenticate your Twilio account. .. note:: This setting can be placed in your Python environment instead, if you wish. - This is a far more secure option and is recommended. + This is a far more secure option, and is recommended. -To add this setting to your environment, using virtualenv open up your /bin/activate.sh file and add the following to the end:: +To add this setting to your environment, using ``virtualenv`` open up your +``/bin/activate.sh`` file and add the following to the end:: export TWILIO_ACCOUNT_SID=XXXXXXXXXXXXX -**TWILIO_AUTH_TOKEN** - -**REQUIRED** +TWILIO_AUTH_TOKEN (REQUIRED) +---------------------------- -The :func:`TWILIO_AUTH_TOKEN` setting is required and will throw an exception -if it is not configured correctly:: +The ``TWILIO_AUTH_TOKEN`` setting is required, and will throw an exception if +it is not configured correctly:: - TWILIO_AUTH_TOKEN='ATXXXXXXXXXXXXXXX' + TWILIO_AUTH_TOKEN = 'ATXXXXXXXXXXXXXXX' This setting is used to authenticate your Twilio account. .. note:: This setting can be placed in your Python environment instead, if you wish. - This is a far more secure option and is recommended. + This is a far more secure option, and is recommended. -Using virtualenv open up your /bin/activate.sh file and add the following to the end:: +Using ``virtualenv`` open up your ``/bin/activate.sh`` file and add the following +to the end:: export TWILIO_AUTH_TOKEN=XXXXXXXXXXXXX -**DJANGO_TWILIO_FORGERY_PROTECTION** +DJANGO_TWILIO_FORGERY_PROTECTION (optional) +------------------------------------------- -The :func:`DJANGO_TWILIO_FORGERY_PROTECTION` setting is not required. -This setting is a boolean and should be placed in the settings.py file: +The ``DJANGO_TWILIO_FORGERY_PROTECTION`` setting is optional. This setting is a +boolean, and should be placed in the ``settings.py`` file: DJANGO_TWILIO_FORGERY_PROTECTION=False -This setting is used to determine the forgery protection used by Django-twilio. -If not set, this will always be the opposite of :func:`settings.DEBUG`, so in -production mode the forgery protection will be on, and in debug mode the protection -will be turned off. +This setting is used to determine the forgery protection used by +``django-twilio``. If not set, this will always be the opposite of +``settings.DEBUG``, so in production mode the forgery protection will be on, and +in debug mode the protection will be turned off. -It is recommended that you leave the protection off in debug mode, but this setting -will allow you to test the forgery protection with a tool like `ngrok +It is recommended that you leave the protection off in debug mode, but this +setting will allow you to test the forgery protection with a tool like `ngrok `_ -**DJANGO_TWILIO_BLACKLIST_CHECK** - +DJANGO_TWILIO_BLACKLIST_CHECK (optional) +---------------------------------------- -The :func:`DJANGO_TWILIO_BLACKLIST_CHECK` setting is not required. -This setting is a boolean and should be placed in the settings.py file: +The ``DJANGO_TWILIO_BLACKLIST_CHECK`` setting is optional. This setting is a +boolean, and should be placed in the ``settings.py`` file:: - DJANGO_TWILIO_BLACKLIST_CHECK=True + DJANGO_TWILIO_BLACKLIST_CHECK = True -This setting will determine if Django-twilio will run a database query comparing -the incoming request :func:`From` attribute with any potential :class:`Caller` objects -in your database. +This setting will determine if ``django-twilio`` will run a database query +comparing the incoming request ``From`` attribute with any potential +:class:`Caller` objects in your database. In short: turning this off will remove an unnecessary database query if you are not using any blacklists. - - diff --git a/docs/source/views.rst b/docs/source/views.rst index daeb8d5..ffdc59e 100644 --- a/docs/source/views.rst +++ b/docs/source/views.rst @@ -2,17 +2,17 @@ Views ===== In order to help speed up development of your telephony application, -django-twilio ships with a few useful views that you can plug straight into -your project's urlconf, and immediately use! +``django-twilio`` ships with a few useful views that you can plug straight into +your project's URL configuration and immediately use! Saying Stuff ------------ -In a majority of telephony apps--you'll want to say something. It can be tedious +In a majority of telephony apps, you'll want to say something. It can be tedious to record your own voice prompts for every bit of call flow, which is why you'll -want to use django-twilio's :func:`django_twilio.views.say` view. +want to use the ``django_twilio.views.say`` view. -The :func:`django_twilio.views.say` view allows you to simply "say stuff" in a +The ``django_twilio.views.say`` view allows you to simply "say stuff" in a variety of languages (in either a male or female voice). Hello, World! @@ -21,7 +21,8 @@ Hello, World! Let's take a look at a *classic* example:: # urls.py - urlpatterns = patterns('', + urlpatterns = patterns( + '', # ... url(r'^hello_world/$', 'django_twilio.views.say', { 'text': 'Hello, world!' @@ -29,18 +30,19 @@ Let's take a look at a *classic* example:: # ... ) -Hook a twilio number up to that URL, and you'll hear a man say "Hello, world!" +Hook a Twilio number up to that URL, and you'll hear a man say "Hello, world!" when called. Nice! Changing the Voice (and Language) ********************************* -By default, twilio reads off all text in the English language in a man's voice. -By it's easy to change that. In the example below, we'll say "goodbye" in +By default, Twilio reads off all text in the English language in a man's voice. +But it's easy to change that. In the example below, we'll say "goodbye" in Spanish, with a female voice:: # urls.py - urlpatterns = patterns('', + urlpatterns = patterns( + '', # ... url(r'^goodbye/$', 'django_twilio.views.say', { 'text': 'Adios!', @@ -55,11 +57,12 @@ Simple, right? Repeating Text ************** -On occasion, you'll also want to repeat some text, without copy+paste. In this +On occasion, you'll also want to repeat some text, without copy/paste. In this situation, you can simply specify an optional ``loop`` parameter:: # urls.py - urlpatterns = patterns('', + urlpatterns = patterns( + '', # ... url(r'^lol/$', 'django_twilio.views.say', { 'text': 'lol', @@ -72,14 +75,14 @@ In this example, we'll just keep repeating "lol" to the caller until they hang up. For more information, be sure to read the API docs on -:func:`django_twilio.views.say`. +``django_twilio.views.say``. Playing Audio ------------- -django-twilio makes it easy to play audio files to callers. Below, we'll look -at two examples which demonstrate how to do so using the excellent -:func:`django_twilio.views.play` view. +``django-twilio`` makes it easy to play audio files to callers. Below, we'll +look at two examples which demonstrate how to do so using the excellent +``django_twilio.views.play`` view. Playing a WAV File ****************** @@ -88,7 +91,8 @@ In this example, we'll play a simple WAV file to a caller. For simplicity's sake, just assume that this WAV file actually exists:: # urls.py - urlpatterns = patterns('', + urlpatterns = patterns( + '', # ... url(r'^play/$', 'django_twilio.views.play', { 'url': 'http://mysite.com/greeting.wav', @@ -97,22 +101,23 @@ sake, just assume that this WAV file actually exists:: ) Assuming the url http://mysite.com/greeting.wav exists, and is a legitimate -WAV file, when you call your twilio application, you should hear the audio +WAV file, when you call your Twilio application, you should hear the audio file play. .. note:: - You can play lots of different types of audio files. For a full list of the - formats twilio accepts, look at the API reference material for the - :func:`django_twilio.views.play` view. + You can play lots of different types of audio files. For a full list of + the formats Twilio accepts, look at the API reference material for the + ``django_twilio.views.play`` view. Looping Audio ************* In this example, we'll play the same greeting audio clip as we did above, but -this time--we'll loop it 3 times:: +this time we'll loop it 3 times:: # urls.py - urlpatterns = patterns('', + urlpatterns = patterns( + '', # ... url(r'^play/$', 'django_twilio.views.play', { 'url': 'http://mysite.com/greeting.wav', @@ -127,39 +132,41 @@ Grabbing Caller Input --------------------- As you begin to build more and more complicated telephony applications, you'll -need a way to accept caller input via their telephone touch pad. For this -purpose, django-twilio ships with the :func:`django_twilio.views.gather` view. +need a way to accept callers' input via their telephone touch pad. For this +purpose, ``django-twilio`` ships with the ``django_twilio.views.gather`` view. Below we'll look at a few examples displaying proper usage. Collecting Touchtone Input ************************** -The simplest thing we can do using the :func:`django_twilio.views.gather` view +The simplest thing we can do using the ``django_twilio.views.gather`` view is to collect caller touchtone input until the caller stops hitting keys. To do -this, we can write our URLconf as follows:: +this, we can write our URL configuration as follows:: # urls.py - urlpatterns = patterns('', + urlpatterns = patterns( + '', # ... url(r'^gather/$', 'django_twilio.views.gather'), # ... ) -By default--once the caller finishes entering their input, twilio will send a +By default, once the caller finishes entering their input, Twilio will send an HTTP POST request to the same URL. So in our example above, if a caller enters -'666#', then twilio would send a POST request to our ``/gather/`` URL with a -``Digits`` parameter that contains the value '666#'. +'666#', then Twilio would send a POST request to our ``/gather/`` URL with a +``Digits`` parameter that contains the value ``'666#'``. Redirect After Collecting Input ******************************* -Let's say that instead of POST'ing the caller's input to the same URL, you want +Let's say that instead of POSTing the caller's input to the same URL, you want to instead POST the data to another URL (or view). No problem! In fact, we'll -even tell twilio to send the data in GET format instead of POST:: +even tell Twilio to send the data in GET format instead of POST:: # urls.py - urlpatterns = patterns('', + urlpatterns = patterns( + '', # ... url(r'^gather/$', 'django_twilio.views.gather', { 'action': '/process_input/', @@ -173,7 +180,7 @@ even tell twilio to send the data in GET format instead of POST:: from django.http import HttpResponse def process(request): - print request.GET # Output GET data to terminal (for debug). + print(request.GET) # Output GET data to terminal (for debug). return HttpResponse() If you test out this application, you'll see that the caller's input is sent @@ -182,27 +189,28 @@ If you test out this application, you'll see that the caller's input is sent Controlling Input Patterns ************************** -Lastly, the :func:`django_twilio.views.gather` view allows you to control -various aspects of the input collection process. +Lastly, the ``django_twilio.views.gather`` view allows you to control various +aspects of the input collection process. Our example below: -* Limits the amount of seconds that twilio will wait for the caller to press - another digit to 5. If no input is entered after 5 seconds, then twilio will +* Limits the number of seconds that Twilio will wait for the caller to press + another digit to 5. If no input is entered after 5 seconds, then Twilio will automatically pass the data along to the URL specified in the ``action`` parameter. * Automatically end the input collection process if the caller hits the '#' key. This way, if the caller enters '12345#', regardless of what the ``timeout`` - parameter is set to, twilio will pass the data along to the URL specified in + parameter is set to, Twilio will pass the data along to the URL specified in the ``action`` parameter. -* Limit the total amount of digits collected to 10. Once 10 digits has been - reached, twilio will pass the data along to the URL specified in the +* Limit the total amount of digits collected to 10. Once 10 digits have been + collected, Twilio will pass the data along to the URL specified in the ``action`` parameter. :: # urls.py - urlpatterns = patterns('', + urlpatterns = patterns( + '', # ... url(r'^gather/$', 'django_twilio.views.gather', { 'action': '/process_input/', @@ -217,20 +225,21 @@ Our example below: Recording Calls --------------- -django-twilio also comes with a built-in call recording view: -:func:`django_twilio.views.record`. In the examples below, we'll walk through -plugging the :func:`django_twilio.views.record` view into our fictional Django +``django-twilio`` also comes with a built-in call recording view: +``django_twilio.views.record``. In the examples below, we'll walk through +plugging the ``django_twilio.views.record`` view into our fictional Django website in a variety of situations. Record a Call ************* -Let's start simple. In this example, we'll setup our URLconf to record our call, -then hit another URL in our application to provide TwiML instructions for -twilio:: +Let's start simple. In this example, we'll set up our URL configuration to +record our call, then hit another URL in our application to provide TwiML +instructions for Twilio:: # urls.py - urlpatterns = patterns('', + urlpatterns = patterns( + '', # ... url(r'^record/$', 'django_twilio.views.record', { 'action': '/call_john/', @@ -239,21 +248,21 @@ twilio:: # ... ) -If we call our application, twilio will start recording our call (after playing +If we call our application, Twilio will start recording our call (after playing a beep), then send a POST request to our ``/call_john/`` URL and continue executing call logic. This allows us to start recording, then continue on -passing instructions to twilio (maybe we'll call our lawyer :)). +passing instructions to Twilio (maybe we'll call our lawyer :)). Stop Recording on Silence ************************* In most cases, you'll only want to record calls that actually have talking in -them. It's pointless to record silence. That's why twilio provides a ``timeout`` -parameter that we can use with django-twilio's -:func:`django_twilio.views.record` view:: +them. It's pointless to record silence. That's why Twilio provides a ``timeout`` +parameter that we can use with the ``django_twilio.views.record`` view:: # urls.py - urlpatterns = patterns('', + urlpatterns = patterns( + '', # ... url(r'^record/$', 'django_twilio.views.record', { 'action': '/call_john/', @@ -264,38 +273,39 @@ parameter that we can use with django-twilio's # ... ) -By default, twilio will stop the recording after 5 seconds of silence have been -detected--but you can easily adjust this number as you see fit. If you're +By default, Twilio will stop the recording after 5 seconds of silence has been +detected, but you can easily adjust this number as you see fit. If you're planning on recording calls that may include hold times or other things, then -you should probably bump this number up to avoid ending the recording if you get -put on hold. +you should probably bump this number up to avoid ending the recording if you +get put on hold. Transcribe Your Call Recording ****************************** On occasion, you may want to transcribe your call recordings. Maybe you're -making a call to your secretary to describe your TODO list, and want to ensure -you get it in text format--or maybe you're just talking with colleagues about -how to best destroy the earth. Whatever the situation may be, twilio's got you +making a call to your secretary to describe your TODO list and want to ensure +you get it in text format, or maybe you're just talking with colleagues about +how to best take over the world. Whatever the situation may be, Twilio has you covered! -In this example, we'll record our call, and force twilio to transcribe it after -we hang up. We'll also give twilio a URL to POST to once it's finished +In this example, we'll record our call, and force Twilio to transcribe it after +we hang up. We'll also give Twilio a URL to POST to once it's finished transcribing, so that we can do some stuff with our transcribed text (maybe we'll email it to ourselves, or something). .. note:: - Transcribing is a **paid** feature. See twilio's `pricing page - `_ for the current rates. Also--twilio + Transcribing is a **paid** feature. See Twilio's `pricing page + `_ for the current rates. Also, Twilio limits transcription time to 2 minutes or less. If you set the - ``max_length`` attribute to > 120 (seconds), then twilio will **not** - transcribe your call, and will instead write an error to your debug log (in - the twilio web panel). + ``max_length`` attribute to > 120 (seconds), then Twilio will **not** + transcribe your call, and will instead write a warning to your debug log + (in the Twilio web panel). :: # urls.py - urlpatterns = patterns('', + urlpatterns = patterns( + '', # ... url(r'^record/$', 'django_twilio.views.record', { 'action': '/call_john/', @@ -310,17 +320,18 @@ Sending SMS Messages -------------------- In addition to building plug-n-play voice applications, we can also build -plug-n-play SMS applications using the :func:`django_twilio.views.sms` view. -This view allows us to send off arbitrary SMS messages based on incoming twilio +plug-n-play SMS applications using the ``django_twilio.views.sms`` view. +This view allows us to send off arbitrary SMS messages based on incoming Twilio requests. -Reply With a SMS -**************** +Reply With an SMS +***************** -This example demonstrates a simple SMS reply. Whenever twilio sends us an -incoming request, we'll simply send back a SMS message to the sender:: +This example demonstrates a simple SMS reply. Whenever Twilio sends us an +incoming request, we'll simply send back an SMS message to the sender:: - urlpatterns = patterns('', + urlpatterns = patterns( + '', # ... url(r'^sms/$', 'django_twilio.views.sms', { 'message': 'Thanks for the SMS. Talk to you soon!', @@ -331,7 +342,7 @@ incoming request, we'll simply send back a SMS message to the sender:: Sending SMS Messages (with Additional Options) ********************************************** -Like most of our other views, the :func:`django_twilio.views.sms` view also +Like most of our other views, the ``django_twilio.views.sms`` view also allows us to specify some other parameters to change our view's behavior:: urlpatterns = patterns('', @@ -345,48 +356,48 @@ allows us to specify some other parameters to change our view's behavior:: # ... ) -Here, we instruct django-twilio to send a SMS message to the caller +Here, we instruct ``django-twilio`` to send an SMS message to the caller '+1-222-333-4444' from the sender '+1-888-222-3333'. As you can see, -django-twilio allows you to fully customize the SMS sending. +``django-twilio`` allows you to fully customize the SMS sending. -Furthermore, the ``status_callback`` parameter that we specified will be POST'ed -to by twilio once it attempts to send this SMS message. twilio will send us some +Furthermore, the ``status_callback`` parameter that we specified will be POSTed +to by Twilio once it attempts to send this SMS message. Twilio will send us some metadata about the SMS message that we can use in our application as desired. Teleconferencing ---------------- A common development problem for telephony developers has traditionally been -conference rooms--until now. django-twilio provides the simplest possible +conference rooms -- until now. ``django-twilio`` provides the simplest possible teleconferencing solution, and it only requires a single line of code to implement! Let's take a look at a few conference patterns, and see how we can easily -implement them into our webapp. +implement them in our webapp. Simple Conference Room ********************** Let's say you want to build the world's simplest conference room. It would consist of nothing more than a phone number that, when called, dumps the -callers into a conference room and let's them chat with each other. - -Assuming you've already installed django-twilio, here's how you can build this -simple conference room: +callers into a conference room and lets them chat with each other. +Assuming you've already installed ``django-twilio``, here's how you can build +this simple conference room: 1. Edit your project's ``urls.py`` and add the following:: - urlpatterns = patterns('', + urlpatterns = patterns( + '', # ... url(r'^conference/(?P\w+)/$', 'django_twilio.views.conference'), # ... ) -2. Now, log into your `twilio dashboard +2. Now, log into your `Twilio dashboard `_ and create a new app. Point the voice URL of your app at http://yourserver.com/conference/business/. -3. Call your new application's phone number. twilio will send a HTTP POST +3. Call your new application's phone number. Twilio will send an HTTP POST request to your web server at ``/conference/business/``, and you should be dumped into your new *business* conference room! @@ -404,7 +415,8 @@ Luckily, that's a quick fix! Open up your ``urls.py`` once more, and add the following:: - urlpatterns = patterns('', + urlpatterns = patterns( + '', # ... url(r'^conference/(?P\w+)/$', 'django_twilio.views.conference', { 'wait_url': 'http://twimlets.com/holdmusic?Bucket=com.twilio.music.rock', @@ -413,12 +425,12 @@ Open up your ``urls.py`` once more, and add the following:: # ... ) -:func:`django_twilio.views.conference` allows you to specify optional -parameters easily in your urlconf. Here, we're using the ``wait_url`` parameter -to instruct twilio to play the rock music while the participant is waiting for +``django_twilio.views.conference`` allows you to specify optional parameters +easily in your URL configuration. Here, we're using the ``wait_url`` parameter +to instruct Twilio to play the rock music while the participant is waiting for other callers to enter the conference. The ``wait_method`` parameter is simply -for efficiency's sake--telling twilio to use the HTTP GET method (instead of -POST, which is the default), allows twilio to properly cache the sound files. +for efficiency's sake -- telling Twilio to use the HTTP GET method (instead of +POST, which is the default) allows Twilio to properly cache the sound files. Conference Room with Custom Greeting ************************************ @@ -429,10 +441,11 @@ build a conference room that greets each user before putting them into the conference. This example shows off how flexible our views can be, and how much we can do -with just the build in :func:`django_twilio.views.conference` view:: +with just the built-in ``django_twilio.views.conference`` view:: # urls.py - urlpatterns = patterns('', + urlpatterns = patterns( + '', # ... url(r'^say_hi/$', 'mysite.views.say_hi'), url(r'^conference/(?P\w+)/$', 'django_twilio.views.conference', { @@ -442,25 +455,25 @@ with just the build in :func:`django_twilio.views.conference` view:: ) # views.py - from twilio.twiml import Response + from twilio import twiml from django_twilio.decorators import twilio_view @twilio_view def say_hi(request): - r = Response() - r.say('Thanks for joining the conference! Django and twilio rock!') + r = twiml.Response() + r.say('Thanks for joining the conference! Django and Twilio rock!') return r If you run this example code, you'll notice that when you call your -application, twilio first says "Thanks for joining the conference..." before -joining you--pretty neat, eh? +application, Twilio first says "Thanks for joining the conference..." before +joining you -- pretty neat, eh? As you can see, this is a great way to build custom logic into your conference room call flow. One pattern that is commonly requested is to play an estimated -wait time--a simple project using :func:`django_twilio.views.conference`. +wait time -- a simple project using ``django_twilio.views.conference``. Other Conferencing Goodies ************************** Now may be a good time to check out the API docs for -:func:`django_twilio.views.conference` to see all the other goodies available. +``django_twilio.views.conference`` to see all the other goodies available. diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..53b30a6 --- /dev/null +++ b/manage.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import unicode_literals, absolute_import + +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/requirements.txt b/requirements.txt index 661033f..4f9b7ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,6 @@ -coverage==3.5.1 -django-nose==1.2 +Django>=1.4 South==1.0 Sphinx>=1.2.0 -flake8==2.1.0 django-phonenumber-field==0.6 twilio>=3.3.6 wheel>=0.22.0 diff --git a/run_tests.py b/run_tests.py deleted file mode 100644 index 443c703..0000000 --- a/run_tests.py +++ /dev/null @@ -1,50 +0,0 @@ -import sys - -import django - -from django.conf import settings - -settings.configure( - DEBUG=True, - USE_TZ=True, - DATABASES={ - "default": { - "ENGINE": "django.db.backends.sqlite3", - } - }, - ROOT_URLCONF="django_twilio.tests.urls", - INSTALLED_APPS=[ - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sites", - "django_twilio", - ], - SITE_ID=1, - NOSE_ARGS=['-s'], - AUTH_USER_MODEL='auth.User', - TWILIO_AUTH_TOKEN='ACXXXXXXXXXXXX', - TWILIO_ACCOUNT_SID='SIXXXXXXXXXXXX', -) - -from django_nose import NoseTestSuiteRunner - -def run_tests(*test_args): - - if '1.7' in django.get_version(): - print 'Runnning Django 1.7 test suite' - django.setup() - - if not test_args: - test_args = ['django_twilio/tests'] - - # Run tests - test_runner = NoseTestSuiteRunner(verbosity=1) - - failures = test_runner.run_tests(test_args) - - if failures: - sys.exit(failures) - - -if __name__ == '__main__': - run_tests(*sys.argv[1:]) diff --git a/setup.py b/setup.py index 9e6383e..f001d50 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,6 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals, absolute_import + from os.path import abspath, dirname, join, normpath from setuptools import find_packages, setup @@ -6,25 +9,46 @@ setup( # Basic package information: - name = 'django-twilio', - version = '0.6', - packages = find_packages(), + name='django-twilio', + version='0.7', + packages=find_packages(), # Packaging options: - zip_safe = False, - include_package_data = True, + zip_safe=False, + include_package_data=True, # Package dependencies: - install_requires = ['twilio>=3.3.6', 'Django>=1.3.1', 'django-phonenumber-field>=0.2a3'], + install_requires=[ + 'twilio>=3.3.6', + 'Django>=1.4', + 'django-phonenumber-field>=0.6' + ], # Metadata for PyPI: - author = 'Randall Degges', - author_email = 'rdegges@gmail.com', - license = 'UNLICENSE', - url = 'http://twilio.com/', - keywords = 'twilio telephony call phone voip sms', - description = 'Build Twilio functionality into your Django apps.', - long_description = open(normpath(join(dirname(abspath(__file__)), - 'README.rst'))).read() + author='Randall Degges', + author_email='rdegges@gmail.com', + license='UNLICENSE', + url='http://twilio.com/', + keywords='twilio telephony call phone voip sms', + description='Build Twilio functionality into your Django apps.', + long_description=open( + normpath(join(dirname(abspath(__file__)), 'README.rst')) + ).read(), + classifiers=[ + 'Framework :: Django', + 'Intended Audience :: Developers', + 'License :: Public Domain', + 'Operating System :: OS Independent', + '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', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + 'Topic :: Internet :: WWW/HTTP', + ] ) diff --git a/test_project/__init__.py b/test_project/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test_project/settings.py b/test_project/settings.py new file mode 100644 index 0000000..cdeb873 --- /dev/null +++ b/test_project/settings.py @@ -0,0 +1,173 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals, absolute_import + +import sys + + +# Django settings for test_project project. + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + # ('Your Name', 'your_email@example.com'), +) + +MANAGERS = ADMINS + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + } +} + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# On Unix systems, a value of None will cause Django to use the same +# timezone as the operating system. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'America/Los_Angeles' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# If you set this to False, Django will not format dates, numbers and +# calendars according to the current locale +USE_L10N = True + +# Absolute filesystem path to the directory that will hold user-uploaded files. +# Example: "/home/media/media.lawrence.com/media/" +MEDIA_ROOT = '' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash. +# Examples: "http://media.lawrence.com/media/", "http://example.com/media/" +MEDIA_URL = '' + +# Absolute path to the directory static files should be collected to. +# Don't put anything in this directory yourself; store your static files +# in apps' "static/" subdirectories and in STATICFILES_DIRS. +# Example: "/home/media/media.lawrence.com/static/" +STATIC_ROOT = '' + +# URL prefix for static files. +# Example: "http://media.lawrence.com/static/" +STATIC_URL = '/static/' + +# URL prefix for admin static files -- CSS, JavaScript and images. +# Make sure to use a trailing slash. +# Examples: "http://foo.com/static/admin/", "/static/admin/". +ADMIN_MEDIA_PREFIX = '/static/admin/' + +# Additional locations of static files +STATICFILES_DIRS = ( + # Put strings here, like "/home/html/static" or "C:/www/django/static". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. +) + +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +) + +# Make this unique, and don't share it with anybody. +SECRET_KEY = 'j1wd@qqodn-r9h&o@0jj!uw^#pm5wcdu2^cdsax=hm+-mk705p' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +) + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', +) + +ROOT_URLCONF = 'test_project.test_app.urls' + +TEMPLATE_DIRS = () + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.admin', + 'django.contrib.admindocs', + + # Use south for database migrations: + 'south', + + # Use django-nose for running our tests: + 'django_nose', + + # django-twilio, of course! + 'django_twilio', +) + +# Nose test settings. +if sys.version_info[0] == 3: + TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' +else: + # Apparently the fix for unicode TEST_RUNNER strings was not back-ported + # See: https://code.djangoproject.com/ticket/19833 + TEST_RUNNER = b'django_nose.NoseTestSuiteRunner' + +NOSE_ARGS = ['--with-coverage', '--cover-package=django_twilio'] + +# Until South is once again Python 3 compatible, +# skip testing migrations in Python 3 +if sys.version_info[0] == 3: + SOUTH_TESTS_MIGRATE = False + +# A sample logging configuration. The only tangible logging +# performed by this configuration is to send an email to +# the site admins on every HTTP 500 error. +# See http://docs.djangoproject.com/en/dev/topics/logging for +# more details on how to customize your logging configuration. +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'mail_admins': { + 'level': 'ERROR', + 'class': 'django.utils.log.AdminEmailHandler' + } + }, + 'loggers': { + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': True, + }, + } +} + +# django-twilio account credentials. These fields are required to use the REST +# API (initiate outbound calls and SMS messages). +TWILIO_ACCOUNT_SID = 'ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' +TWILIO_AUTH_TOKEN = 'YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY' + +# The default callerid will be used for all outgoing phone calls and SMS +# messages if not explicitly specified. This number must be previously +# validated with twilio in order to work. See +# https://www.twilio.com/user/account/phone-numbers# +TWILIO_DEFAULT_CALLERID = 'NNNNNNNNNN' diff --git a/django_twilio/tests/__init__.py b/test_project/test_app/__init__.py similarity index 52% rename from django_twilio/tests/__init__.py rename to test_project/test_app/__init__.py index 031277f..158d230 100644 --- a/django_twilio/tests/__init__.py +++ b/test_project/test_app/__init__.py @@ -1,3 +1,6 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals, absolute_import + from .client import * from .decorators import * from .models import * diff --git a/test_project/test_app/client.py b/test_project/test_app/client.py new file mode 100644 index 0000000..83a3c2f --- /dev/null +++ b/test_project/test_app/client.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals, absolute_import + +from django.test import TestCase +from django.contrib.auth.models import User +from django.conf import settings + +from twilio.rest import TwilioRestClient +from django_dynamic_fixture import G + +from django_twilio.client import twilio_client +from django_twilio.models import Credential +from django_twilio.utils import discover_twilio_credentials + + +class TwilioClientTestCase(TestCase): + + def test_twilio_client_exists(self): + self.assertIsInstance(twilio_client, TwilioRestClient) + + def test_twilio_client_sets_credentials(self): + self.assertEqual( + twilio_client.auth, + (settings.TWILIO_ACCOUNT_SID, settings.TWILIO_AUTH_TOKEN) + ) + + def test_twilio_client_with_credentials_model(self): + self.user = G(User, username='test', password='pass') + self.credentials = G( + Credential, + name='Test Credentials', + account_sid='AAA', + auth_token='BBB', + user=self.user, + ) + + credentials = discover_twilio_credentials(user=self.user) + + self.assertEquals(credentials[0], self.credentials.account_sid) + self.assertEquals(credentials[1], self.credentials.auth_token) diff --git a/test_project/test_app/decorators.py b/test_project/test_app/decorators.py new file mode 100644 index 0000000..46e3615 --- /dev/null +++ b/test_project/test_app/decorators.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals, absolute_import + +from django.conf import settings +from django.http import HttpResponse +from django.test import Client, TestCase +from django.test.utils import override_settings + +from twilio import twiml +from django_dynamic_fixture import G + +from django_twilio.models import Caller + +from .views import response_view, str_view, bytes_view, verb_view +from .utils import TwilioRequestFactory + + +class TwilioViewTestCase(TestCase): + + urls = 'test_project.test_app.urls' + + def setUp(self): + + self.regular_caller = G(Caller, phone_number='+12222222222', blacklisted=False) + self.blocked_caller = G(Caller, phone_number='+13333333333', blacklisted=True) + + self.factory = TwilioRequestFactory( + token=settings.TWILIO_AUTH_TOKEN, + enforce_csrf_checks=True, + ) + + # Test URIs. + self.str_uri = '/test_app/decorators/str_view/' + self.bytes_uri = '/test_app/decorators/bytes_view/' + self.verb_uri = '/test_app/decorators/verb_view/' + self.response_uri = '/test_app/decorators/response_view/' + + def test_requires_post(self): + client = Client(enforce_csrf_checks=True) + with override_settings(DEBUG=False): + self.assertEquals(client.get(self.str_uri).status_code, 405) + self.assertEquals(client.head(self.str_uri).status_code, 405) + self.assertEquals(client.options(self.str_uri).status_code, 405) + self.assertEquals(client.put(self.str_uri).status_code, 405) + self.assertEquals(client.delete(self.str_uri).status_code, 405) + with override_settings(DEBUG=True): + self.assertEquals(client.get(self.str_uri).status_code, 200) + self.assertEquals(client.head(self.str_uri).status_code, 200) + self.assertEquals(client.options(self.str_uri).status_code, 200) + self.assertEquals(client.put(self.str_uri).status_code, 200) + self.assertEquals(client.delete(self.str_uri).status_code, 200) + + def test_allows_post(self): + request = self.factory.post(self.str_uri) + self.assertEquals(str_view(request).status_code, 200) + + def test_decorator_preserves_metadata(self): + self.assertEqual(str_view.__name__, 'str_view') + + @override_settings(TWILIO_ACCOUNT_SID=None) + @override_settings(TWILIO_AUTH_TOKEN=None) + def test_missing_settings_return_forbidden(self): + with override_settings(DEBUG=False): + self.assertEquals(self.client.post(self.str_uri).status_code, 403) + with override_settings(DEBUG=True): + self.assertEquals(self.client.post(self.str_uri).status_code, 200) + + def test_missing_signature_returns_forbidden(self): + with override_settings(DEBUG=False): + self.assertEquals(self.client.post(self.str_uri).status_code, 403) + with override_settings(DEBUG=True): + self.assertEquals(self.client.post(self.str_uri).status_code, 200) + + def test_incorrect_signature_returns_forbidden(self): + with override_settings(DEBUG=False): + request = self.factory.post( + self.str_uri, + HTTP_X_TWILIO_SIGNATURE='fake_signature', + ) + self.assertEquals(str_view(request).status_code, 403) + with override_settings(DEBUG=True): + self.assertEquals(str_view(request).status_code, 200) + + def test_no_from_field(self): + request = self.factory.post(self.str_uri) + self.assertEquals(str_view(request).status_code, 200) + + def test_from_field_no_caller(self): + request = self.factory.post(self.str_uri, {'From': '+12222222222'}) + self.assertEquals(str_view(request).status_code, 200) + + def test_blacklist_works(self): + with override_settings(DEBUG=False): + request = self.factory.post(self.str_uri, {'From': '+13333333333'}) + response = str_view(request) + r = twiml.Response() + r.reject() + self.assertEquals( + response.content, + str(r).encode('utf-8'), + ) + with override_settings(DEBUG=True): + request = self.factory.post(self.str_uri, {'From': '+13333333333'}) + response = str_view(request) + r = twiml.Response() + r.reject() + self.assertEquals( + response.content, + str(r).encode('utf-8'), + ) + + def test_decorator_modifies_str(self): + request = self.factory.post(self.str_uri) + self.assertIsInstance(str_view(request), HttpResponse) + + def test_decorator_modifies_bytes(self): + request = self.factory.post(self.bytes_uri) + self.assertIsInstance(bytes_view(request), HttpResponse) + + def test_decorator_modifies_verb(self): + request = self.factory.post(self.verb_uri) + self.assertIsInstance(verb_view(request), HttpResponse) + + def test_decorator_preserves_http_response(self): + request = self.factory.post(self.response_uri) + self.assertIsInstance(response_view(request), HttpResponse) + + def test_override_forgery_protection_off_debug_off(self): + with override_settings(DJANGO_TWILIO_FORGERY_PROTECTION=False, DEBUG=False): + request = self.factory.post( + self.str_uri, + HTTP_X_TWILIO_SIGNATURE='fake_signature', + ) + self.assertEquals(str_view(request).status_code, 200) + + def test_override_forgery_protection_off_debug_on(self): + with override_settings(DJANGO_TWILIO_FORGERY_PROTECTION=False, DEBUG=True): + request = self.factory.post( + self.str_uri, + HTTP_X_TWILIO_SIGNATURE='fake_signature', + ) + self.assertEquals(str_view(request).status_code, 200) + + def test_override_forgery_protection_on_debug_off(self): + with override_settings(DJANGO_TWILIO_FORGERY_PROTECTION=True, DEBUG=False): + request = self.factory.post( + self.str_uri, + HTTP_X_TWILIO_SIGNATURE='fake_signature', + ) + self.assertEquals(str_view(request).status_code, 403) + + def test_override_forgery_protection_on_debug_on(self): + with override_settings(DJANGO_TWILIO_FORGERY_PROTECTION=True, DEBUG=True): + request = self.factory.post( + self.str_uri, + HTTP_X_TWILIO_SIGNATURE='fake_signature', + ) + self.assertEquals(str_view(request).status_code, 403) diff --git a/test_project/test_app/models.py b/test_project/test_app/models.py new file mode 100644 index 0000000..3b9fa53 --- /dev/null +++ b/test_project/test_app/models.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals, absolute_import + +from types import MethodType + +from django.test import TestCase +from django.contrib.auth.models import User + +from django_dynamic_fixture import G + +from django_twilio.models import Caller, Credential + + +class CallerTestCase(TestCase): + """ + Run tests against the :class:`django_twilio.models.Caller` model. + """ + + def setUp(self): + self.caller = G( + Caller, + phone_number='12223334444', + blacklisted=False, + ) + + def test_has_str_method(self): + self.assertIsInstance(self.caller.__str__, MethodType) + + def test_str_returns_a_string(self): + self.assertIsInstance(self.caller.__str__(), str) + + def test_str_doesnt_contain_blacklisted(self): + self.assertNotIn('blacklisted', self.caller.__str__()) + + def test_unicode_contains_blacklisted(self): + self.caller.blacklisted = True + self.caller.save() + self.assertIn('blacklisted', self.caller.__str__()) + + +class CredentialTests(TestCase): + + def setUp(self): + self.user = G(User, username='test', password='pass') + self.credentials = G( + Credential, + name='Test Credentials', + account_sid='XXX', + auth_token='YYY', + user=self.user, + ) + + def test_str(self): + """ + Assert that str renders how we'd like it too + """ + self.assertEquals( + self.credentials.__str__(), + 'Test Credentials - XXX', + ) + + def test_credentials_fields(self): + """ + Assert the fields are working correctly + """ + self.assertEquals(self.credentials.name, 'Test Credentials') + self.assertEquals(self.credentials.account_sid, 'XXX') + self.assertEquals(self.credentials.auth_token, 'YYY') + self.assertEquals(self.credentials.user, self.user) diff --git a/test_project/test_app/urls.py b/test_project/test_app/urls.py new file mode 100644 index 0000000..49ad9c0 --- /dev/null +++ b/test_project/test_app/urls.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals, absolute_import + +from django.conf.urls import patterns, url + + +# Test URLs for our ``django_twilio.decorators`` module. +urlpatterns = patterns( + 'test_project.test_app.views', + url(r'^test_app/decorators/response_view/$', 'response_view'), + url(r'^test_app/decorators/str_view/$', 'str_view'), + url(r'^test_app/decorators/bytes_view/$', 'bytes_view'), + url(r'^test_app/decorators/verb_view/$', 'verb_view'), +) diff --git a/test_project/test_app/utils.py b/test_project/test_app/utils.py new file mode 100644 index 0000000..7f28470 --- /dev/null +++ b/test_project/test_app/utils.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals, absolute_import + +import sys + +if sys.version_info[0] == 3: + from urllib.parse import urljoin +else: + from urlparse import urljoin + +from twilio.util import RequestValidator + +from django.test import RequestFactory + + +class TwilioRequestFactory(RequestFactory): + + def __init__(self, token, **defaults): + super(TwilioRequestFactory, self).__init__(**defaults) + self.base_url = 'http://testserver/' + self.twilio_auth_token = token + + def _compute_signature(self, path, params): + return RequestValidator( + self.twilio_auth_token + ).compute_signature(urljoin(self.base_url, path), params=params) + + def get(self, path, data={}, **extra): + if 'HTTP_X_TWILIO_SIGNATURE' not in extra: + extra.update({'HTTP_X_TWILIO_SIGNATURE': self._compute_signature(path, params=data)}) + return super(TwilioRequestFactory, self).get(path, data, **extra) + + def post(self, path, data={}, content_type=None, **extra): + if 'HTTP_X_TWILIO_SIGNATURE' not in extra: + extra.update({'HTTP_X_TWILIO_SIGNATURE': self._compute_signature(path, params=data)}) + if content_type is None: + return super(TwilioRequestFactory, self).post(path, data, **extra) + else: + return super(TwilioRequestFactory, self).post(path, data, content_type, **extra) diff --git a/test_project/test_app/views.py b/test_project/test_app/views.py new file mode 100644 index 0000000..124e615 --- /dev/null +++ b/test_project/test_app/views.py @@ -0,0 +1,188 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals, absolute_import + +from django.conf import settings +from django.http import HttpResponse +from django.test import TestCase +from twilio import twiml + +from django_twilio.decorators import twilio_view +from django_twilio.views import ( + conference, dial, gather, play, record, say, sms) + +from .utils import TwilioRequestFactory + + +@twilio_view +def response_view(request): + """ + A simple test view that returns a HttpResponse object. + """ + return HttpResponse( + 'Hello from Django', + content_type='text/xml', + ) + + +@twilio_view +def str_view(request): + """ + A simple test view that returns a string. + """ + return 'Hi!' + + +@twilio_view +def bytes_view(request): + """ + A simple test view that returns ASCII bytes. + """ + return b'Hi!' + + +@twilio_view +def verb_view(request): + """ + A simple test view that returns a ``twilio.Verb`` object. + """ + r = twiml.Response() + r.reject() + return r + + +class SayTestCase(TestCase): + + def setUp(self): + + # Test URI + self.say_uri = '/test_app/views/say/' + + self.factory = TwilioRequestFactory(token=settings.TWILIO_AUTH_TOKEN) + self.request = self.factory.post(self.say_uri) + + def test_say_no_text(self): + self.assertRaises(TypeError, say, self.request) + + def test_say_with_text(self): + self.assertEquals( + say(self.request, text='hi').status_code, + 200 + ) + + +class PlayTestCase(TestCase): + + def setUp(self): + + # Test URI + self.play_uri = '/test_app/views/play/' + + self.factory = TwilioRequestFactory(token=settings.TWILIO_AUTH_TOKEN) + + def test_play_no_url(self): + request = self.factory.post(self.play_uri) + self.assertRaises(TypeError, play, request) + + def test_play_with_url(self): + request = self.factory.post(self.play_uri) + self.assertEquals( + play(request, url='http://b.com/b.wav').status_code, + 200, + ) + + +class GatherTestCase(TestCase): + + def setUp(self): + + # Test URI + self.gather_uri = '/test_app/views/gather/' + + self.factory = TwilioRequestFactory(token=settings.TWILIO_AUTH_TOKEN) + + def test_gather(self): + request = self.factory.post(self.gather_uri) + self.assertEquals( + gather(request).status_code, + 200, + ) + + +class RecordTestCase(TestCase): + + def setUp(self): + + # Test URI + self.record_uri = '/test_app/views/record/' + + self.factory = TwilioRequestFactory(token=settings.TWILIO_AUTH_TOKEN) + + def test_record(self): + request = self.factory.post(self.record_uri) + self.assertEquals( + record(request).status_code, + 200, + ) + + +class SmsTestCase(TestCase): + + def setUp(self): + + # Test URI + self.sms_uri = '/test_app/views/sms/' + + self.factory = TwilioRequestFactory(token=settings.TWILIO_AUTH_TOKEN) + + def test_sms_no_message(self): + request = self.factory.post(self.sms_uri) + self.assertRaises(TypeError, sms, request) + + def test_sms_with_message(self): + request = self.factory.post(self.sms_uri) + self.assertEquals( + sms(request, message='test').status_code, + 200, + ) + + +class DialTestCase(TestCase): + + def setUp(self): + + # Test URI + self.dial_uri = '/test_app/views/dial/' + + self.factory = TwilioRequestFactory(token=settings.TWILIO_AUTH_TOKEN) + + def test_dial_no_number(self): + request = self.factory.post(self.dial_uri) + self.assertRaises(TypeError, dial, request) + + def test_dial_with_number(self): + request = self.factory.post(self.dial_uri) + self.assertEquals( + dial(request, number='+18182223333').status_code, + 200, + ) + + +class ConferenceTestCase(TestCase): + + def setUp(self): + + # Test URI + self.conf_uri = '/test_app/views/conference/' + + self.factory = TwilioRequestFactory(token=settings.TWILIO_AUTH_TOKEN) + + def test_conference_no_name(self): + request = self.factory.post(self.conf_uri) + self.assertRaises(TypeError, conference, request) + + def test_conference_with_name(self): + request = self.factory.post(self.conf_uri) + self.assertEquals( + conference(request, name='a').status_code, + 200, + ) diff --git a/test_project/urls.py b/test_project/urls.py new file mode 100644 index 0000000..db8ab33 --- /dev/null +++ b/test_project/urls.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals, absolute_import + +from django.conf.urls import patterns, include, url + +# Uncomment the next two lines to enable the admin: +from django.contrib import admin +admin.autodiscover() + +urlpatterns = patterns( + '', + url(r'^admin/', include(admin.site.urls)), +) diff --git a/test_requirements.txt b/test_requirements.txt new file mode 100644 index 0000000..8f5d496 --- /dev/null +++ b/test_requirements.txt @@ -0,0 +1,5 @@ +coverage==3.7.1 +django-dynamic-fixture==1.7.0 +django-nose==1.2 +flake8==2.2.2 +ipdb==0.8 diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..ec9cc17 --- /dev/null +++ b/tox.ini @@ -0,0 +1,8 @@ +[tox] +envlist = py27,py33 +[testenv] +deps = + -rrequirements.txt + -rtest_requirements.txt +commands= + python manage.py test From 2df2742430b66e8c4ad32418fe7ca3983e224156 Mon Sep 17 00:00:00 2001 From: hwkns Date: Tue, 22 Jul 2014 00:16:45 -0400 Subject: [PATCH 2/4] exclude Django 1.7 from Python 2.6 testing --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 66640d6..2ce5118 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,3 +27,5 @@ matrix: env: DJANGO="django>=1.4,<1.5" - python: "3.4" env: DJANGO="django>=1.4,<1.5" + - python: "2.6" + env: DJANGO="https://www.djangoproject.com/download/1.7c1/tarball/" From 9b90653761305b5f0cc7412bf3f4f7256a011d43 Mon Sep 17 00:00:00 2001 From: hwkns Date: Tue, 22 Jul 2014 00:31:13 -0400 Subject: [PATCH 3/4] excluded Djanog 1.7c1 from Travis tests for now --- .travis.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2ce5118..ec5f55b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,6 @@ env: - DJANGO="django>=1.4,<1.5" - DJANGO="django>=1.5,<1.6" - DJANGO="django>=1.6,<1.7" - - DJANGO="https://www.djangoproject.com/download/1.7c1/tarball/" install: - pip install --use-mirrors ${DJANGO} @@ -27,5 +26,3 @@ matrix: env: DJANGO="django>=1.4,<1.5" - python: "3.4" env: DJANGO="django>=1.4,<1.5" - - python: "2.6" - env: DJANGO="https://www.djangoproject.com/download/1.7c1/tarball/" From 7552433326c0fb9c6417d75a99215890405cd36f Mon Sep 17 00:00:00 2001 From: hwkns Date: Tue, 22 Jul 2014 01:01:30 -0400 Subject: [PATCH 4/4] fixed one last pep8 thing in docs --- docs/source/settings.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/settings.rst b/docs/source/settings.rst index a5f1a45..acfa8cf 100644 --- a/docs/source/settings.rst +++ b/docs/source/settings.rst @@ -55,7 +55,7 @@ DJANGO_TWILIO_FORGERY_PROTECTION (optional) The ``DJANGO_TWILIO_FORGERY_PROTECTION`` setting is optional. This setting is a boolean, and should be placed in the ``settings.py`` file: - DJANGO_TWILIO_FORGERY_PROTECTION=False + DJANGO_TWILIO_FORGERY_PROTECTION = False This setting is used to determine the forgery protection used by ``django-twilio``. If not set, this will always be the opposite of