Skip to content

Commit

Permalink
Twilio Integration and More update notification (#1719)
Browse files Browse the repository at this point in the history
* Twilio Gateway

* Enable Twilio test account for Travis

* Pass twilio settings into tox env

* Pass all the tests

* 100% test coverage

* Remove unnecessary lines of code

* Currently removed TWILIO_PHONE_NUMBER from settings, add that later once we have phone numbers, remove unnecessary settings from default settings

* Remove comments

* Make tests pass

* Make tests pass, remove TwilioGateway logger, move FakeGateway logger to settings/default.py

* Send email/sms regardless of phone/email('s) verification status

* Add changes addressed to PR

* Rebasing and changes
  • Loading branch information
valaparthvi committed Aug 31, 2017
1 parent 0b94377 commit 3a50a2a
Show file tree
Hide file tree
Showing 18 changed files with 342 additions and 39 deletions.
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ before_script:
- unzip $DATADIR/$WBDATA -d $DATADIR

env:
global:
- secure: bHitijj3QQGevw88b1OdkqWJzuADpQkqM2Jrr3iD/HC5E3Jh9JtoJkfBE8iemfTfbuiGjCPDKJA8ZFNs1/8yyuLbukohpBnGZ3EVNfkhZEoECMIbd6E7EGcQoosNtJi6Z7MBWj2hvzpsJqGdAXmj6aC91hKP6kLSa2ponruEsQWNljmxkCVLymOQZhsi5FHzQflAzEtCEzWzSr1g7EznxeZMqARMYJJe0bM4zue1nNkVUqlrwoZEQ+PKu9DwYJCJ32HdcKknlOLjiORtpaudiuSTE/H1HugqEteAyzgt6ugpKaD90e1INKffOCZou24RyGkkn6ndhi7eehGhxCE0ZW8TKpWSkjHLBR2wjGZlRyW0Zu+rsHH1Y5NsnRUHiGXX9WlFJJa6eiV7X9jjQiDo2zx8KFshPlOyGIHBOcjwlcOrCLdCLJbJvMQQP8kF0z8SjgLgerxg2DYqGm04RQSeNxGPY0t5zvsKKz3FUsnbMC/xB3y7kCrS92iuCcQagvI/KWzSG09vhtO0BiEuhgpX/R7LVDRgGtFpIcNliWPYhCocbtWu/8WG7e4up3dOMpHsM2bimGrgtx3QVwCmQ8dKxiGuRV6W2WZluhVmygvUyMctlK5m9tDrbDDbxaRvsIMz/iGRMnV+fJ9BHGo4QInu6HYzMhLMFsT26DuSvqlZROI=
- secure: EGMobaU7AzPFxOySQSTU8IdKKykRoEZBJJ+MXE8pDcgRJ31XlaekspexbDSCzNrQ64lpeYpfUSwunHSNDykVPBiPJAvMEGumsGguEm4dmZUIPWAGDHCdd2bEZAVgdX5rThcHlYhifza4H9mB8cQNi1VmY+svUT41lJdvGqVDP4waoI1jcdJ1mtKYaIdlL/2tcXfpOzBGggBzeocDbdgKyO5KwhEcQxv8qcr0hF0kURZG40C3U1Nm6p58U0J3RoEJ+kePwVyYBJD2MUNpCvMbVuh+yuWncGjEQQE5l2k4QBvChHguc0OCEX6zILikZ69vrdrAvaMfxX2argcb8YaLCHw6NKTaNFqunUaH7FO41j//YPToYq+u8GFA+cJsi/O/7KEP7tFGb+uikZSqAaTGuPfU/4m39xtGNziucFezRNt/p/2GSOin9VWUhb1LPmMwwg6wK7ZtN2Vf8MnUgXqKBCuHaXVohlPN5FqUAziHjGgT0jxoyR2PZZ3bW/sjLieQzQtweln5WkccMaZLbJCD5ZNKgw2RdmfEaV5Hn1eAdjoe1ykUvKmKSJpj7deYcWfrILrZtqqWfuzQfhPIMsrWx6AW9KLO+9AvTxAnnc3Zw4iJSOeozcEacAos2MrJ2tAVB+LfnAEl9ooXvwuvGL/F4fmc6QBDQD4v2Pb9kBgpZ/M=
matrix:
- TOX_ENV=py35-flake8
- TOX_ENV=py35-django1.9-migration
Expand Down
27 changes: 20 additions & 7 deletions cadasta/accounts/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
from allauth.account.models import EmailAddress

from core.form_mixins import SanitizeFieldsForm
from .utils import send_email_update_notification
from . import utils
from .models import User, VerificationDevice
from .validators import check_username_case_insensitive, phone_validator
from .messages import phone_format
from . import messages

from parsley.decorators import parsleyfy
from phonenumbers import parse as parse_phone
Expand All @@ -22,7 +22,7 @@ class RegisterForm(SanitizeFieldsForm, forms.ModelForm):
email = forms.EmailField(required=False)

phone = forms.RegexField(regex=r'^\+(?:[0-9]?){6,14}[0-9]$',
error_messages={'invalid': phone_format},
error_messages={'invalid': messages.phone_format},
required=False)
password = forms.CharField(widget=forms.PasswordInput())
MIN_LENGTH = 10
Expand Down Expand Up @@ -114,7 +114,7 @@ class ProfileForm(SanitizeFieldsForm, forms.ModelForm):
email = forms.EmailField(required=False)

phone = forms.RegexField(regex=r'^\+(?:[0-9]?){6,14}[0-9]$',
error_messages={'invalid': phone_format},
error_messages={'invalid': messages.phone_format},
required=False)
password = forms.CharField(widget=forms.PasswordInput())

Expand Down Expand Up @@ -184,6 +184,7 @@ def clean_email(self):

def save(self, *args, **kwargs):
user = super().save(commit=False, *args, **kwargs)
email_update_message = None

if self.current_email != user.email:
current_email_set = self.instance.emailaddress_set.all()
Expand All @@ -192,11 +193,15 @@ def save(self, *args, **kwargs):

if user.email:
send_email_confirmation(self.request, user)
email_update_message = messages.email_change

if self.current_email:
send_email_update_notification(self.current_email)
utils.send_email_update_notification(self.current_email)
user.email = self.current_email
else:
user.email_verified = False
email_update_message = messages.email_delete
utils.send_email_deleted_notification(self.current_email)

if self.current_phone != user.phone:
current_phone_set = VerificationDevice.objects.filter(
Expand All @@ -209,10 +214,18 @@ def save(self, *args, **kwargs):
user=self.instance, unverified_phone=user.phone)
device.generate_challenge()
if self.current_phone:
utils.send_sms(self.current_phone, messages.phone_change)
user.phone = self.current_phone
if user.email:
utils.send_phone_update_notification(user.email)
else:
user.phone_verified = False
utils.send_sms(self.current_phone, messages.phone_delete)
if user.email:
utils.send_phone_deleted_notification(user.email)

if user.phone and email_update_message:
utils.send_sms(user.phone, email_update_message)
user.save()
return user

Expand Down Expand Up @@ -273,7 +286,7 @@ class ResetPasswordForm(allauth_forms.ResetPasswordForm):
email = forms.EmailField(required=False)

phone = forms.RegexField(regex=r'^\+(?:[0-9]?){6,14}[0-9]$',
error_messages={'invalid': phone_format},
error_messages={'invalid': messages.phone_format},
required=False)

def clean(self):
Expand Down Expand Up @@ -346,7 +359,7 @@ class ResendTokenForm(forms.Form):
email = forms.EmailField(required=False)

phone = forms.RegexField(regex=r'^\+(?:[0-9]?){6,14}[0-9]$',
error_messages={'invalid': phone_format},
error_messages={'invalid': messages.phone_format},
required=False)

def clean(self):
Expand Down
38 changes: 38 additions & 0 deletions cadasta/accounts/gateways.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import random
import logging

from django.conf import settings
from twilio.rest import Client, TwilioException

fake_logger = logging.getLogger(name='accounts.FakeGateway')


class TwilioGateway(object):

def __init__(self,
account_sid=settings.TWILIO_ACCOUNT_SID,
auth_token=settings.TWILIO_AUTH_TOKEN,
from_phone_number_list=[]):

self.client = Client(account_sid, auth_token)
self.from_phone_number_list = from_phone_number_list

def send_sms(self, to, body):
try:
# create and send a message with client
message = self.client.messages.create(
to=to,
from_=random.choice(self.from_phone_number_list),
body=body)

except TwilioException as e:
message = e

return message


class FakeGateway(object):

def send_sms(self, to, body):
fake_logger.debug("Message sent to: {phone}. Message: {body}".format(
phone=to, body=body))
34 changes: 34 additions & 0 deletions cadasta/accounts/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,37 @@
unverified_identifier = _(
"You have not verified your phone or email. We request you to verify"
" your registered phone or email in order to access your account.")

# send an sms to the user's phone(if any) notifying the removal of email
email_delete = _(
"You are receiving this message because a user at Cadasta Platform removed"
" the email address from the account linked to this phone number."
" If it wasn't you, please contact us immediately at [email protected]")

# send an sms to the user's phone(if any) notifying the change in email
email_change = _(
"You are receiving this message because a user at Cadasta Platform updated"
" the email address for the account linked to this phone"
" number."
" If it wasn't you, please contact us immediately at [email protected]")

# send an sms to the user's old phone(if any) notifying the removal of phone
phone_delete = _(
"You are receiving this message because a user at Cadasta Platform removed"
" the phone number from the account previously linked to this"
" phone number."
" If it wasn't you, please contact us immediately at [email protected]")

# send an sms to the user's old phone(if any) notifying the change in phone
phone_change = _(
"You are receiving this message because a user at Cadasta Platform changed"
" the phone number for the account previously linked to this"
" phone number."
" If it wasn't you, please contact us immediately at [email protected]")

# send an sms to the user's phone notifying the change in password
password_change_or_reset = _(
"You are receiving this message because a user at Cadasta Platform has"
" changed or reset the password for your account linked to this phone"
" number."
" If it wasn't you, please contact us immediately at [email protected]")
7 changes: 2 additions & 5 deletions cadasta/accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,11 @@
from django_otp.util import random_hex, hex_validator
from binascii import unhexlify

import logging
import time

from simple_history.models import HistoricalRecords
from .manager import UserManager

logger = logging.getLogger("accounts.token")
from .utils import send_sms

PERMISSIONS_DIR = settings.BASE_DIR + '/permissions/'

Expand Down Expand Up @@ -187,8 +185,7 @@ def generate_challenge(self):
message = message.format(
token_value=token, time_validity=self.step // 60)

logger.debug("Token has been sent to %s " % self.unverified_phone)
logger.debug("%s" % message)
send_sms(to=self.unverified_phone, body=message)

return token

Expand Down
25 changes: 24 additions & 1 deletion cadasta/accounts/tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,9 +509,10 @@ def test_update_user(self):
assert user.email_verified is True
assert user.phone == '+919327768250'
assert user.phone_verified is True
assert len(mail.outbox) == 2
assert len(mail.outbox) == 3
assert '[email protected]' in mail.outbox[0].to
assert '[email protected]' in mail.outbox[1].to
assert '[email protected]' in mail.outbox[2].to

def test_display_name(self):
user = UserFactory.create(username='imagine71',
Expand Down Expand Up @@ -725,6 +726,8 @@ def test_update_phone_only(self):
assert user.phone_verified is True
assert VerificationDevice.objects.filter(
unverified_phone='+919327768250').exists() is False
assert len(mail.outbox) == 1
assert '[email protected]' in mail.outbox[0].to

def test_update_email_only(self):
user = UserFactory.create(username='sherlock',
Expand Down Expand Up @@ -764,6 +767,7 @@ def test_update_email_only(self):
assert '[email protected]' in mail.outbox[1].to
assert EmailAddress.objects.filter(
email="[email protected]").exists() is False
# sms must be sent about email change to phone '+919327768250'

def test_update_with_duplicate_phone(self):
UserFactory.create(phone='+12345678990')
Expand Down Expand Up @@ -814,6 +818,8 @@ def test_update_add_phone(self):
assert user.phone == '+919327768250'
assert user.phone_verified is False
assert VerificationDevice.objects.count() == 1
assert len(mail.outbox) == 1
assert '[email protected]' in mail.outbox[0].to

def test_update_add_email(self):
user = UserFactory.create(username='sherlock',
Expand Down Expand Up @@ -929,6 +935,8 @@ def test_update_remove_phone(self):
assert not user.phone
assert user.phone_verified is False
assert VerificationDevice.objects.count() == 0
assert len(mail.outbox) == 1
assert '[email protected]' in mail.outbox[0].to

def test_update_remove_email(self):
user = UserFactory.create(username='sherlock',
Expand Down Expand Up @@ -957,6 +965,8 @@ def test_update_remove_email(self):
assert not user.email
assert user.email_verified is False
assert EmailAddress.objects.count() == 0
assert len(mail.outbox) == 1
assert '[email protected]' in mail.outbox[0].to

def test_update_add_phone_and_remove_email(self):
user = UserFactory.create(username='sherlock',
Expand Down Expand Up @@ -988,6 +998,8 @@ def test_update_add_phone_and_remove_email(self):
assert user.email_verified is False
assert EmailAddress.objects.count() == 0
assert VerificationDevice.objects.count() == 1
assert len(mail.outbox) == 1
assert '[email protected]' in mail.outbox[0].to

def test_update_add_email_and_remove_phone(self):
user = UserFactory.create(username='sherlock',
Expand Down Expand Up @@ -1027,6 +1039,9 @@ def test_update_add_email_and_remove_phone(self):
assert user.email_verified is False
assert EmailAddress.objects.count() == 1
assert VerificationDevice.objects.count() == 0
assert len(mail.outbox) == 2
assert '[email protected]' in mail.outbox[0].to
assert '[email protected]' in mail.outbox[0].to

def test_update_phone_and_remove_email(self):
user = UserFactory.create(username='sherlock',
Expand Down Expand Up @@ -1062,6 +1077,10 @@ def test_update_phone_and_remove_email(self):
assert EmailAddress.objects.count() == 0
assert VerificationDevice.objects.filter(
unverified_phone='+12345678990').exists() is False
assert len(mail.outbox) == 1
assert '[email protected]' in mail.outbox[0].to
# send sms to user's phone '+12345678990' about email removal
# send sms to user's phone '+12345678990' about phone change

def test_update_email_and_remove_phone(self):
user = UserFactory.create(username='sherlock',
Expand Down Expand Up @@ -1103,6 +1122,10 @@ def test_update_email_and_remove_phone(self):
assert EmailAddress.objects.filter(
email='[email protected]').exists() is False
assert VerificationDevice.objects.count() == 0
assert len(mail.outbox) == 3
assert '[email protected]' in mail.outbox[0].to
assert '[email protected]' in mail.outbox[1].to
assert '[email protected]' in mail.outbox[2].to

def test_update_with_existing_email_in_EmailAddress(self):
user = UserFactory.create()
Expand Down
63 changes: 63 additions & 0 deletions cadasta/accounts/tests/test_gateways.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from django.test import TestCase
from django.conf import settings
from accounts.gateways import TwilioGateway, FakeGateway
from django.test.utils import override_settings
from unittest import mock


class TwilioGatewayTest(TestCase):

@override_settings(
TWILIO_ACCOUNT_SID='SID',
TWILIO_AUTH_TOKEN='TOKEN',
TWILIO_PHONE_NUMBER_LIST=['+123'])
@mock.patch('accounts.gateways.Client')
def test_gateway(self, mock_client):
twilio = TwilioGateway(
account_sid=settings.TWILIO_ACCOUNT_SID,
auth_token=settings.TWILIO_AUTH_TOKEN,
from_phone_number_list=settings.TWILIO_PHONE_NUMBER_LIST
)
mock_client.assert_called_with('SID', 'TOKEN')
body = 'Testing Twilio SMS gateway!'
to = '+456'
twilio.send_sms(to, body)
mock_client.return_value.messages.create.assert_called_with(
to=to,
body=body,
from_='+123')

def test_message_sent_successfully(self):
twiliobj = TwilioGateway(
account_sid=settings.TWILIO_ACCOUNT_SID,
auth_token=settings.TWILIO_AUTH_TOKEN,
from_phone_number_list=['+15005550006', ]
)
to = '+919327768250'
body = 'Test message send successfully'
message = twiliobj.send_sms(to, body)
assert message.status == 'queued'
assert message.sid is not None

def test_message_send_to_invalid_number(self):
twiliobj = TwilioGateway(
account_sid=settings.TWILIO_ACCOUNT_SID,
auth_token=settings.TWILIO_AUTH_TOKEN,
from_phone_number_list=['+15005550006', ]
)
to = '+15005550001'
body = 'This is an invalid phone number!'
message = twiliobj.send_sms(to, body)
assert message.status == 400


class FakeGatewayTest(TestCase):
@mock.patch('accounts.gateways.fake_logger')
def test_gateway(self, mock_logger):
fake_gateway = FakeGateway()
to = '+456'
body = 'Testing Fake SMS gateway!'
fake_gateway.send_sms(to, body)
mock_logger.debug.assert_called_with(
"Message sent to: {phone}. Message: {body}".format(
phone=to, body=body))
Loading

0 comments on commit 3a50a2a

Please sign in to comment.