Skip to content

Commit

Permalink
fixed issue #96: add phone number field to reg process
Browse files Browse the repository at this point in the history
  • Loading branch information
NdibeRaymond committed Feb 3, 2021
1 parent 8651198 commit a95e30e
Show file tree
Hide file tree
Showing 29 changed files with 1,217 additions and 100 deletions.
3 changes: 3 additions & 0 deletions zubhub_backend/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ services:
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_HOST=${POSTGRES_HOST}
- SENDGRID_API_KEY=${SENDGRID_API_KEY}
- DEFAULT_FROM_PHONE=${DEFAULT_FROM_PHONE}
- TWILIO_ACCOUNT_SID=${TWILIO_ACCOUNT_SID}
- TWILIO_AUTH_TOKEN=${TWILIO_AUTH_TOKEN}
volumes:
- .:/zubhub_backend
ports:
Expand Down
2 changes: 2 additions & 0 deletions zubhub_backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ psycopg2-binary==2.8.3
ptyprocess==0.6.0
py==1.9.0
Pygments==2.7.2
PyJWT==1.7.1
pyparsing==2.4.7
pytest==5.3.5
pytest-django==3.8.0
Expand All @@ -75,6 +76,7 @@ sqlparse==0.4.1
text-unidecode==1.3
tornado==6.1
traitlets==5.0.5
twilio==6.51.1
uritemplate==3.0.1
urllib3==1.25.11
vine==1.3.0
Expand Down
71 changes: 70 additions & 1 deletion zubhub_backend/zubhub/creators/adapter.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
from django.conf import settings
from django.template.loader import render_to_string
from allauth.account.adapter import DefaultAccountAdapter
from .models import Location
from django.template import TemplateDoesNotExist
from twilio.rest import Client

from .models import Location, Setting

from creators.tasks import send_text

from allauth.account import app_settings as allauth_settings


class CustomAccountAdapter(DefaultAccountAdapter):
Expand All @@ -9,7 +18,67 @@ def save_user(self, request, user, form, commit=False):
data = form.cleaned_data
location = Location.objects.get(name=data.get('location'))

creator.phone = data.get("phone")
creator.dateOfBirth = data.get('dateOfBirth')
creator.location = location
creator.save()

Setting(creator=creator, subscribe=data.get("subscribe")).save()

return creator

def confirm_phone(self, request, phone_number):
"""
Marks the phone number as confirmed on the db
"""
phone_number.verified = True
phone_number.set_as_primary(conditional=True)
phone_number.save()

def get_from_phone(self):
"""
This is a hook that can be overridden to programatically
set the 'from' phone number for sending phone texts messages
"""
return settings.DEFAULT_FROM_PHONE

def render_text(self, template_name, phone, context):
"""
Renders a text to `text`. `template_prefix` identifies the
text that is to be sent, e.g. "account/phone/phone_confirmation"
"""

from_phone = self.get_from_phone()

try:
body = render_to_string(
template_name,
context,
self.request,
).strip()
except TemplateDoesNotExist:
raise

return {"to": phone, "from_": from_phone, "body": body}

def send_text(self, template_name, phone, context):

client = Client(settings.TWILIO_ACCOUNT_SID,
settings.TWILIO_AUTH_TOKEN)
text = self.render_text(template_name, phone, context)
client.messages.create(**text)

def send_confirmation_text(self, request, phoneconfirmation, signup):

ctx = {
"user": phoneconfirmation.phone_number.user.username,
"key": phoneconfirmation.key,
}

template_name = "account/phone/phone_confirmation.txt"

send_text.delay(
phone=phoneconfirmation.phone_number.phone,
template_name=template_name,
ctx=ctx,
)
14 changes: 14 additions & 0 deletions zubhub_backend/zubhub/creators/admin.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.contrib.auth.admin import UserAdmin
from .models import PhoneNumber, Setting

Creator = get_user_model()


class PhoneNumberAdmin(admin.ModelAdmin):
list_display = ["phone", "verified", "primary"]
search_fields = ["phone", "user__username"]
list_filter = ['verified', "primary"]


class SettingAdmin(admin.ModelAdmin):
list_filter = ["subscribe"]


UserAdmin.fieldsets += ('Personal Info',
{'fields': ('avatar', 'phone', 'dateOfBirth', 'location', 'bio')}),
UserAdmin.readonly_fields += ("avatar"),

admin.site.register(Creator, UserAdmin)
admin.site.register(PhoneNumber, PhoneNumberAdmin)
admin.site.register(Setting, SettingAdmin)
61 changes: 61 additions & 0 deletions zubhub_backend/zubhub/creators/managers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from datetime import timedelta

from django.db import models
from django.db.models import Q
from django.utils import timezone


class PhoneNumberManager(models.Manager):
# def can_add_phone(self, user):
# ret = True
# if app_settings.MAX_EMAIL_ADDRESSES:
# count = self.filter(user=user).count()
# ret = count < app_settings.MAX_EMAIL_ADDRESSES
# return ret

def add_phone(self, request, user, phone, confirm=False, signup=False):
phone_number, created = self.get_or_create(
user=user, phone__iexact=phone, defaults={"phone": phone}
)

if created and confirm:
phone_number.send_confirmation(request, signup=signup)

return phone_number

def get_primary(self, user):
try:
return self.get(user=user, primary=True)
except self.model.DoesNotExist:
return None

def get_users_for(self, phone):
# this is a list rather than a generator because we probably want to
# do a len() on it right away
return [
phone_number.user for phone_number in self.filter(verified=True, phone__iexact=phone)
]

def fill_cache_for_user(self, user, phone_numbers):
"""
In a multi-db setup, inserting records and re-reading them later
on may result in not being able to find newly inserted
records. Therefore, we maintain a cache for the user so that
we can avoid database access when we need to re-read..
"""
user._phonenumber_cache = phone_numbers

def get_for_user(self, user, phone):
cache_key = "_phonenumber_cache"
phone_numbers = getattr(user, cache_key, None)
if phone_numbers is None or (type(phone_numbers) == list and phone_numbers[0] is None):
ret = self.get(user=user, phone__iexact=phone)
# To avoid additional lookups when e.g.
# EmailAddress.set_as_primary() starts touching self.user
ret.user = user
return ret
else:
for phone_number in phone_numbers:
if phone_number.phone == phone:
return phone_number
raise self.model.DoesNotExist()
125 changes: 124 additions & 1 deletion zubhub_backend/zubhub/creators/models.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import uuid
from math import floor
from django.utils import timezone
from django.core import signing
from django.utils.crypto import get_random_string
from django.utils.translation import gettext_lazy as _
from django.utils.text import slugify
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.db import models, transaction
from .managers import PhoneNumberManager
from .utils import user_phone
from . import signals

try:
from allauth.account import app_settings as allauth_settings
from allauth.account.adapter import get_adapter
except ImportError:
raise ImportError("allauth needs to be added to INSTALLED_APPS.")


class Location(models.Model):
Expand Down Expand Up @@ -44,3 +56,114 @@ def save(self, *args, **kwargs):
self.following_count = self.following.count()
self.projects_count = self.projects.count()
super().save(*args, **kwargs)


class Setting(models.Model):
creator = models.OneToOneField(
Creator, on_delete=models.CASCADE, primary_key=True)
subscribe = models.BooleanField(blank=True, default=False)

def __str__(self):
return self.creator.username


class PhoneNumber(models.Model):

user = models.ForeignKey(
Creator,
verbose_name="creator",
on_delete=models.CASCADE,
)
phone = models.CharField(
unique=True,
max_length=16,
verbose_name="phone number",
)
verified = models.BooleanField(verbose_name="verified", default=False)
primary = models.BooleanField(verbose_name="primary", default=False)

objects = PhoneNumberManager()

class Meta:
verbose_name = "phone number"
verbose_name_plural = "phone numbers"

def __str__(self):
return self.phone

def set_as_primary(self, conditional=False):
old_primary = PhoneNumber.objects.get_primary(self.user)
if old_primary:
if conditional:
return False
old_primary.primary = False
old_primary.save()
self.primary = True
self.save()
user_phone(self.user, self.phone)
self.user.save()
return True

def send_confirmation(self, request=None, signup=False):
confirmation = PhoneConfirmationHMAC(self)
confirmation.send(request, signup=signup)
return confirmation

# def change(self, request, new_email, confirm=True):
# """
# Given a new email address, change self and re-confirm.
# """
# with transaction.atomic():
# user_email(self.user, new_email)
# self.user.save()
# self.email = new_email
# self.verified = False
# self.save()
# if confirm:
# self.send_confirmation(request)


class PhoneConfirmationHMAC:
def __init__(self, phone_number):
self.phone_number = phone_number

@property
def key(self):
return signing.dumps(obj=self.phone_number.pk, salt=allauth_settings.SALT)

@classmethod
def from_key(cls, key):
PHONE_CONFIRMATION_EXPIRE_DAYS = 3
try:
max_age = 60 * 60 * 24 * PHONE_CONFIRMATION_EXPIRE_DAYS
pk = signing.loads(key, max_age=max_age,
salt=allauth_settings.SALT)
ret = PhoneConfirmationHMAC(PhoneNumber.objects.get(pk=pk))
except (
signing.SignatureExpired,
signing.BadSignature,
PhoneNumber.DoesNotExist,
):
ret = None
return ret

def confirm(self, request):

if not self.phone_number.verified:
phone_number = self.phone_number
get_adapter(request).confirm_phone(request, phone_number)
signals.phone_confirmed.send(
sender=self.__class__,
request=request,
phone_number=phone_number,
)
return phone_number

def send(self, request=None, signup=False):
get_adapter(request).send_confirmation_text(request, self, signup)
signals.phone_confirmation_sent.send(
sender=self.__class__,
request=request,
confirmation=self,
signup=signup,
)
Loading

0 comments on commit a95e30e

Please sign in to comment.