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

Added option to add phone number during registration or update of creator profile #101

Merged
merged 3 commits into from
Feb 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
72 changes: 71 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,68 @@ 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.bio = data.get('bio')
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