Skip to content
Open
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
107 changes: 68 additions & 39 deletions accounts/models.py
Original file line number Diff line number Diff line change
@@ -1,72 +1,101 @@
import secrets
import string
import time
import uuid

from django.conf import settings
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.db import models
from phonenumber_field.modelfields import PhoneNumberField

import random
import time
import uuid
"""
Substituting a custom User by extending the AbstractUser
Making Users email unique
Adding extra attributes that are not present in the default User model(phone_number and verification code)
USERNAME_FIELD - changing login to use email rather than username.
REQUIRED_FIELDS - the required fields to create a superuser
"""

# Generate six digit random code
def generate_verification_code(size=6):
return ''.join(str(random.randint(0,9)) for i in range(size))

_VERIFICATION_CODE_RANGE: range = range(4, 7)


def _generate_verification_code(size: int = 6) -> str:
"""
Securely generates a random code in valid range.

Note:
This util is currently coupled to the `secrets` library.
Changes may result in failing tests.
"""
if size not in _VERIFICATION_CODE_RANGE:
errmsg = "Attempt to generate code outside of required range."
raise ValueError(errmsg)
sequence = string.digits
return "".join(secrets.choice(sequence) for _ in range(size))


class User(AbstractUser):
"""
Substituting a custom `User` by extending the `AbstractUser`.

Note:
Make `User` email unique.
Add extra attributes not present in the default `User` model:
- phone_number
- verification code
- code_generated_at
- is_verified
USERNAME_FIELD - change login to use email rather than username.
REQUIRED_FIELDS - fields required to create a superuser.
"""

id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
email = models.EmailField(unique=True, max_length=50)
first_name = models.CharField(max_length=150)
last_name = models.CharField(max_length=150)
phone_number = PhoneNumberField(blank=True, help_text='Contact phone number', null=True, unique= True)
verification_code = models.CharField(max_length=6, unique=True,default=generate_verification_code())
phone_number = PhoneNumberField(
blank=True, help_text="Contact phone number", null=True, unique=True
)
verification_code = models.CharField(
max_length=6, unique=True, default=_generate_verification_code()
)
code_generated_at = models.DateTimeField(auto_now_add=True)
is_verified = models.BooleanField(default=False) # to be set up later in views to change if user verified

USERNAME_FIELD = 'email'

# add phone number as a requirement while signing up
REQUIRED_FIELDS = ['first_name', 'last_name', 'username', 'phone_number']
is_verified = models.BooleanField(
default=False
) # to be set up later in views to change if user verified

def __str__(self) -> str:
return f'{self.first_name} {self.last_name}'
USERNAME_FIELD = "email"

# add phone number as a requirement while signing up
REQUIRED_FIELDS = ["first_name", "last_name", "username", "phone_number"]

def __init__(self, *args, region=None, **kwargs):
"""
The function takes in a region and then sets the region to the region that was passed in when the user is created
"""
"""
The function takes in a region and then sets the region to the region
that was passed in when the user is created
"""
super().__init__(*args, **kwargs)
self.region = region



# resets the verification code after every 1hr
def get_verification_code(self):
now = time.time()
elapsed = now - self.code_generated_at.timestamp()
if elapsed > 3600:
self.verification_code = generate_verification_code()
if elapsed > 3600:
self.verification_code = _generate_verification_code()
self.code_generated_at = now
self.save
return self.verification_code


def __str__(self) -> str:
return f"{self.first_name} {self.last_name}"


class Account(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
# Referencing the customized user
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='account')
account_name = models.CharField(max_length=50, unique= True)
user = models.OneToOneField(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="account"
)
account_name = models.CharField(max_length=50, unique=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
display_picture = models.ImageField(default='blank-profile-picture.png', upload_to='profile_images')
display_picture = models.ImageField(
default="blank-profile-picture.png", upload_to="profile_images"
)
bio = models.TextField(blank=True, null=True)

def __str__(self) -> str:
return self.account_name


62 changes: 62 additions & 0 deletions accounts/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""
Tests for helpers and utilities for the `mastori::accounts` app.

Note:
Prefer to inherit from `unittest` for util functions or services.
This reduces the DB overhead of using `django.test.TestCase`.
Consider a different design approach if you find your utilities
dependent on DB transactions.
"""

import secrets
import unittest
from unittest.mock import call, patch

from accounts.models import _generate_verification_code


BASE_MODULE: str = "accounts.models"


def _mock_choice_response(sequence: str, calls: int = 1) -> list[int]:
"""
Test helper to mock and simulate `secrets.choice` func calls.
"""
assert isinstance(sequence, str)
return [secrets.choice(sequence) for _ in range(calls)]


class VerificationCodeTestCase(unittest.TestCase):
"""
Tests to cover generating verification codes for the `user` model.
"""

def setUp(self) -> None:
return super().setUp()

def test_should_raise_exc_for_out_of_range_code_size_arg(self) -> None:
# Given
invalid_lengths = (3, 12, -1)

for length in invalid_lengths:
# Then
with self.assertRaises(ValueError):
_generate_verification_code(size=length) # When

def test_should_securely_generate_user_verification_code(self) -> None:
# Given
length = 6
sequence = "0123456789"
choice_side_effect = _mock_choice_response(sequence, length)
expected = "".join(choice_side_effect)

# When
with patch(f"{BASE_MODULE}.secrets", autospec=True) as mock_secrets_lib:
mock_secrets_lib.choice.side_effect = choice_side_effect
actual = _generate_verification_code(size=length)

# Then
mock_secrets_lib.choice.assert_has_calls([call(sequence)] * length)
self.assertIsInstance(actual, str)
self.assertEqual(mock_secrets_lib.choice.call_count, length)
self.assertEqual(actual, expected)