Skip to content
Snippets Groups Projects
Commit 362987e8 authored by florimondmanca's avatar florimondmanca
Browse files

change authentication backend: use email and password isntead of username and password

parent 3eb0925f
No related branches found
No related tags found
No related merge requests found
"""API tests.""" """API tests."""
import random
from string import ascii_lowercase
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from rest_framework import status from rest_framework import status
from django.contrib.auth import get_user_model
from users.models import User from tests.utils import random_email
# Create your tests here.
def random_username(): User = get_user_model()
"""Return a random username with 12 lowercase letters."""
return random.choices(ascii_lowercase, k=12)
class UserAPITest(APITestCase): class UserAPITest(APITestCase):
...@@ -21,13 +15,13 @@ class UserAPITest(APITestCase): ...@@ -21,13 +15,13 @@ class UserAPITest(APITestCase):
def test_list(self): def test_list(self):
for _ in range(5): for _ in range(5):
User.objects.create(username=random_username()) User.objects.create(email=random_email())
response = self.client.get('/api/users/') response = self.client.get('/api/users/')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 5) self.assertEqual(len(response.data), 5)
def test_retrieve(self): def test_retrieve(self):
user = User.objects.create(username=random_username()) user = User.objects.create(email=random_email())
response = self.client.get(f'/api/users/{user.id}/') response = self.client.get(f'/api/users/{user.id}/')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
......
"""Generic test function generators and other test utilities.""" """Generic test function generators and other test utilities."""
import random
from string import ascii_lowercase
from django.test import TestCase from django.test import TestCase
def random_username():
"""Return a random username with 12 lowercase letters."""
return random.choices(ascii_lowercase, k=12)
def random_email():
"""Return a random email."""
return '{}@random.net'.format(random.choices(ascii_lowercase, k=12))
class MyTestCase(TestCase): class MyTestCase(TestCase):
"""Extends Django's test case with extra assert methods.""" """Extends Django's test case with extra assert methods."""
......
"""Users admin panel configuration.""" """Users admin panel configuration."""
from django import forms
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from .models import User from .models import User
...@@ -10,23 +12,91 @@ from .models import User ...@@ -10,23 +12,91 @@ from .models import User
# Register your models here. # Register your models here.
@admin.register(User) class UserCreationForm(forms.ModelForm):
class UserAdminWithExtraFields(UserAdmin): """A form for creating new users.
"""User admin panel, adding extra fields to the bottom of the page."""
def __init__(self, *args, **kwargs): Includes all the required fields, plus a repeated password.
super().__init__(*args, **kwargs) """
abstract_fields = [field.name for field in AbstractUser._meta.fields] password1 = forms.CharField(label='Password',
user_fields = [field.name for field in self.model._meta.fields] widget=forms.PasswordInput)
password2 = forms.CharField(label='Confirm password',
widget=forms.PasswordInput)
self.fieldsets += ( class Meta: # noqa
(_('Autres champs'), { model = User
'fields': [ fields = ('email', 'first_name', 'last_name', 'date_of_birth',)
field for field in user_fields if (
field not in abstract_fields and def clean_password2(self):
field != self.model._meta.pk.name # Check that the two password entries match
) password1 = self.cleaned_data.get('password1')
], password2 = self.cleaned_data.get('password2')
if password1 and password2 and password1 != password2:
raise forms.ValidationError(_("Passwords do not match"))
return password2
def save(self, commit=True):
# Save the provided password in hashed format
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.save()
return user
class UserChangeForm(forms.ModelForm):
"""A form for updating users.
Includes all the fields on the user, but replaces the password field
with admin's password hash display field.
"""
password = ReadOnlyPasswordHashField()
class Meta: # noqa
model = User
fields = ('email', 'first_name', 'last_name',
'date_of_birth', 'is_active', 'password',)
def clean_password(self):
# Regardless of what the user provides, return the initial value.
# This is done here, rather than on the field, because the
# field does not have access to the initial value
return self.initial['password']
@admin.register(User)
class CustomUserAdmin(UserAdmin):
"""Custom user admin panel."""
# The forms to add and change user instances
form = UserChangeForm
add_form = UserCreationForm
# The fields to be used in displaying the User model.
# These override the definitions on the base UserAdmin
# that reference specific fields on auth.User.
list_display = ('email', 'first_name', 'last_name', 'is_staff',)
fieldsets = (
(None, {
'fields': ('email', 'first_name', 'last_name', 'password'),
}),
('Informations personnelles', {
'fields': ('date_of_birth', 'phone_number',)
}), }),
('Permissions', {
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups',)
}),
)
# add_fieldsets is not a standard ModelAdmin attribute. UserAdmin
# overrides get_fieldsets to use this attribute when creating a user.
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('email', 'first_name', 'last_name',
'date_of_birth', 'password1', 'password2')}
),
) )
search_fields = ('email',)
ordering = ('email',)
filter_horizontal = ()
...@@ -3,19 +3,65 @@ ...@@ -3,19 +3,65 @@
from django.db import models from django.db import models
from django.shortcuts import reverse from django.shortcuts import reverse
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import UserManager as _UserManager
from utils import modify_fields
# Create your models here. # Create your models here.
class UserManager(_UserManager):
"""Custom user manager.
Makes email mendatory instead of username.
"""
def create_user(self, email, password, **extra_fields):
"""Create and save a User with email and password."""
if not email:
raise ValueError('Users must have an email address')
user = self.model(
email=self.normalize_email(email),
**extra_fields
)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password, **extra_fields):
"""Create and save a superuser with email and password."""
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
if extra_fields.get('is_staff') is not True:
raise ValueError('Superuser must have is_staff=True.')
if extra_fields.get('is_superuser') is not True:
raise ValueError('Superuser must have is_superuser=True.')
return self.create_user(email, password, **extra_fields)
@modify_fields(
username={'blank': True, '_unique': False},
email={'_unique': True, 'blank': False},
first_name={'blank': False},
last_name={'blank': False},
)
class User(AbstractUser): class User(AbstractUser):
"""Django Contrib user. For possible future refinements only. """Django Contrib user. For possible future refinements only.
Fields Fields
---------- ----------
phone_number : char field phone_number : char field
""" """
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username', 'first_name', 'last_name']
objects = UserManager()
date_of_birth = models.DateField('date de naissance', null=True)
MALE = 'M' MALE = 'M'
FEMALE = 'F' FEMALE = 'F'
GENDER_CHOICES = ( GENDER_CHOICES = (
...@@ -23,11 +69,12 @@ class User(AbstractUser): ...@@ -23,11 +69,12 @@ class User(AbstractUser):
(FEMALE, 'féminin'), (FEMALE, 'féminin'),
) )
gender = models.CharField('sexe', gender = models.CharField('sexe',
max_length=1, choices=GENDER_CHOICES) max_length=1, choices=GENDER_CHOICES,
default=MALE)
# TODO add a proper phone number validator # TODO add a proper phone number validator
phone_number = models.CharField('téléphone', phone_number = models.CharField('téléphone',
max_length=12, blank=True) max_length=12, blank=True)
def get_absolute_url(self): def get_absolute_url(self):
return reverse('users:user-detail', args=[str(self.id)]) return reverse('api:user-detail', args=[str(self.id)])
"""Users tests.""" """Users tests."""
from django.test import TestCase from django.test import TestCase
from django.db import IntegrityError
from django.contrib.auth import get_user_model
from tests.utils import random_email
from rest_framework.test import APIClient
from rest_framework import status
from users.models import User User = get_user_model()
from tests.utils import MyTestCase
class UserTest(MyTestCase): class UserTest(TestCase):
"""Unit tests for the customized User model.""" """Unit tests for the customized User model."""
@classmethod @classmethod
def setUpTestData(self): def setUpTestData(self):
self.obj = User.objects.create(username='johndoe') self.obj = User.objects.create(email=random_email())
def test_gender_field_label(self): def test_gender_field_label(self):
self.assertFieldVerboseName(User, 'gender', 'sexe') self.assertEqual(User._meta.get_field('gender').verbose_name, 'sexe')
def test_gender_choices(self): def test_gender_choices(self):
self.assertTupleEqual(User._meta.get_field('gender').choices, self.assertTupleEqual(User._meta.get_field('gender').choices,
(('M', 'masculin'), ('F', 'féminin'))) (('M', 'masculin'), ('F', 'féminin')))
def test_gender_max_length(self): def test_gender_max_length(self):
self.assertMaxLength(User, 'gender', 1) self.assertEqual(User._meta.get_field('gender').max_length, 1)
def test_phone_number_field_label(self): def test_phone_number_field_label(self):
self.assertFieldVerboseName(User, 'phone_number', 'téléphone') self.assertEqual(User._meta.get_field('phone_number').verbose_name,
'téléphone')
def test_get_absolute_url(self):
response = self.client.get(f'/api/users/{self.obj.id}', follow=True)
self.assertEqual(200, response.status_code)
# TODO add phone number validation tests # TODO add phone number validation tests
class EmailAuthenticationTest(TestCase):
"""Test the custom email authentication backend."""
def test_authenticate_with_email(self):
user = User.objects.create(email='john.doe@email.net')
user.set_password('secret')
user.save()
logged_in = self.client.login(username='john.doe@email.net',
password='secret')
self.assertTrue(logged_in)
def test_username_not_unique(self):
self.assertFalse(User._meta.get_field('username').unique)
User.objects.create(email=random_email())
def test_username_blank(self):
self.assertTrue(User._meta.get_field('username').blank)
def test_email_is_unique(self):
self.assertTrue(User._meta.get_field('email').unique)
def test_two_users_with_same_email_not_allowed(self):
with self.assertRaises(IntegrityError):
User.objects.create(email='same.email@example.net')
User.objects.create(email='same.email@example.net')
def test_email_not_blank(self):
self.assertFalse(User._meta.get_field('email').blank)
"""Various utilities."""
def modify_fields(**kwargs):
"""Modify the fields of a superclass.
Caution: will affect the superclass' fields too. It is preferable to
restrict the usage to cases when the superclass is abstract.
Example
-------
class Foo(models.Model):
health = models.IntegerField()
@modify_fields(health={'blank': True})
class Bar(models.Model):
pass
"""
def wrap(cls):
for field, prop_dict in kwargs.items():
for prop, val in prop_dict.items():
setattr(cls._meta.get_field(field), prop, val)
return cls
return wrap
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment