Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Twilio Integration and More update notification #1719

Merged
merged 13 commits into from
Aug 28, 2017
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