Skip to content

Commit

Permalink
Merge pull request #384 from Chadys/reset-resend-custom-email-field
Browse files Browse the repository at this point in the history
Custom email field for reset and resend views and custom user tests
  • Loading branch information
dekoza committed May 25, 2019
2 parents 147bf88 + cf202dd commit d1dae34
Show file tree
Hide file tree
Showing 10 changed files with 333 additions and 19 deletions.
23 changes: 15 additions & 8 deletions djoser/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,16 +128,23 @@ def _validate_user_exists(self, user):


class PasswordResetSerializer(serializers.Serializer):
email = serializers.EmailField()

default_error_messages = {'email_not_found': settings.CONSTANTS.messages.EMAIL_NOT_FOUND}

def validate_email(self, value):
users = self.context['view'].get_users(value)
if settings.PASSWORD_RESET_SHOW_EMAIL_NOT_FOUND and not users:
self.fail('email_not_found')
else:
return value
def __init__(self, *args, **kwargs):
super(PasswordResetSerializer, self).__init__(*args, **kwargs)

email_field = get_user_email_field_name(User)
self.fields[email_field] = serializers.EmailField()
validate_email_fn_name = 'validate_' + email_field

def validate_email_fn(self, value):
users = self.context['view'].get_users(value)
if settings.PASSWORD_RESET_SHOW_EMAIL_NOT_FOUND and not users:
self.fail('email_not_found')
else:
return value

setattr(PasswordResetSerializer, validate_email_fn_name, validate_email_fn)


class UidAndTokenSerializer(serializers.Serializer):
Expand Down
4 changes: 2 additions & 2 deletions djoser/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ class ResendActivationView(ActionViewMixin, generics.GenericAPIView):
def _action(self, serializer):
if not settings.SEND_ACTIVATION_EMAIL:
return response.Response(status=status.HTTP_400_BAD_REQUEST)
for user in self.get_users(serializer.data['email']):
for user in self.get_users(serializer.data[get_user_email_field_name(User)]):
self.send_activation_email(user)
return response.Response(status=status.HTTP_204_NO_CONTENT)

Expand Down Expand Up @@ -181,7 +181,7 @@ class PasswordResetView(utils.ActionViewMixin, generics.GenericAPIView):
_users = None

def _action(self, serializer):
for user in self.get_users(serializer.data['email']):
for user in self.get_users(serializer.data[get_user_email_field_name(User)]):
self.send_password_reset_email(user)
return Response(status=status.HTTP_204_NO_CONTENT)

Expand Down
8 changes: 4 additions & 4 deletions docs/source/base_endpoints.rst
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ will result in ``HTTP_400_BAD_REQUEST``
+----------+--------------------------------------+----------------------------------+
| Method | Request | Response |
+==========+======================================+==================================+
| ``POST`` | * ``email`` | ``HTTP_204_NO_CONTENT`` |
| ``POST`` | * ``{{ User.EMAIL_FIELD }}`` | ``HTTP_204_NO_CONTENT`` |
| | | ``HTTP_400_BAD_REQUEST`` |
+----------+--------------------------------------+----------------------------------+

Expand Down Expand Up @@ -181,16 +181,16 @@ setup ``PASSWORD_RESET_CONFIRM_URL``.

``HTTP_204_NO_CONTENT`` if ``PASSWORD_RESET_SHOW_EMAIL_NOT_FOUND`` is ``False``

Otherwise and if ``email`` does not exist in database ``HTTP_400_BAD_REQUEST``
Otherwise and if ``{{ User.EMAIL_FIELD }}`` does not exist in database ``HTTP_400_BAD_REQUEST``

+----------+---------------------------------+------------------------------+
| Method | Request | Response |
+==========+=================================+==============================+
| ``POST`` | ``email`` | ``HTTP_204_NO_CONTENT`` |
| ``POST`` | ``{{ User.EMAIL_FIELD }}`` | ``HTTP_204_NO_CONTENT`` |
| | | |
| | | ``HTTP_400_BAD_REQUEST`` |
| | | |
| | | * ``email`` |
| | | * ``{{ User.EMAIL_FIELD }}`` |
+----------+---------------------------------+------------------------------+

Reset Password Confirmation
Expand Down
30 changes: 30 additions & 0 deletions testproject/testapp/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from django.contrib.auth.base_user import BaseUserManager
from django.contrib.auth.models import AbstractBaseUser
from django.db import models


class CustomUserManager(BaseUserManager):
use_in_migrations = True

def create_user(self, custom_username, custom_email=None, password=None, **extra_fields):
if not custom_username:
raise ValueError('The given custom_username must be set')
email = self.normalize_email(custom_email)
username = self.model.normalize_username(custom_username)
user = self.model(custom_username=username, custom_email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user


class CustomUser(AbstractBaseUser):
custom_username = models.CharField(max_length=150)
custom_email = models.EmailField(blank=True)
custom_required_field = models.CharField(max_length=2)
is_active = models.BooleanField(default=True)
objects = CustomUserManager()

EMAIL_FIELD = 'custom_email'
USERNAME_FIELD = 'custom_username'
REQUIRED_FIELDS = ['custom_email', 'custom_required_field']

7 changes: 6 additions & 1 deletion testproject/testapp/tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@
__all__ = ['get_user_model', 'IntegrityError', 'mock']


def create_user(**kwargs):
def create_user(use_custom_data=False, **kwargs):
data = {
'username': 'john',
'password': 'secret',
'email': 'john@beatles.com',
} if not use_custom_data else {
'custom_username': 'john',
'password': 'secret',
'custom_email': 'john@beatles.com',
'custom_required_field': '42',
}
data.update(kwargs)
user = get_user_model().objects.create_user(**data)
Expand Down
1 change: 0 additions & 1 deletion testproject/testapp/tests/test_activation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from rest_framework.reverse import reverse
from rest_framework.test import APITestCase

import djoser.constants
import djoser.signals
import djoser.utils
import djoser.views
Expand Down
47 changes: 46 additions & 1 deletion testproject/testapp/tests/test_password_reset.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
from rest_framework import status

import djoser.views
from djoser.compat import get_user_email
from djoser.conf import settings as default_settings
from .common import create_user
from .common import create_user, mock
from testapp.models import CustomUser


class PasswordResetViewTest(restframework.APIViewTestCase,
Expand Down Expand Up @@ -78,3 +80,46 @@ def test_post_should_return_bad_request_if_user_does_not_exist(self):
self.assertEqual(
response.data['email'][0], default_settings.CONSTANTS.messages.EMAIL_NOT_FOUND
)

@mock.patch(
'djoser.serializers.User', CustomUser)
@mock.patch(
'djoser.views.User', CustomUser)
@override_settings(
AUTH_USER_MODEL='testapp.CustomUser')
def test_post_should_send_email_to_custom_user_with_password_reset_link(self):
user = create_user(use_custom_data=True)
data = {
'custom_email': get_user_email(user),
}
request = self.factory.post(data=data)

response = self.view(request)

self.assert_status_equal(response, status.HTTP_204_NO_CONTENT)
self.assert_emails_in_mailbox(1)
self.assert_email_exists(to=[get_user_email(user)])
site = get_current_site(request)
self.assertIn(site.domain, mail.outbox[0].body)
self.assertIn(site.name, mail.outbox[0].body)

@mock.patch(
'djoser.serializers.User', CustomUser)
@mock.patch(
'djoser.views.User', CustomUser)
@override_settings(
AUTH_USER_MODEL='testapp.CustomUser',
DJOSER=dict(settings.DJOSER,
**{'PASSWORD_RESET_SHOW_EMAIL_NOT_FOUND': True}))
def test_post_should_return_bad_request_with_custom_email_field_if_user_does_not_exist(self):
data = {
'custom_email': 'john@beatles.com',
}
request = self.factory.post(data=data)

response = self.view(request)

self.assert_status_equal(response, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
response.data['custom_email'][0], default_settings.CONSTANTS.messages.EMAIL_NOT_FOUND
)
1 change: 0 additions & 1 deletion testproject/testapp/tests/test_password_reset_confirm.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from djet import assertions, restframework
from rest_framework import status

import djoser.constants
import djoser.utils
import djoser.views
from djoser.conf import settings as default_settings
Expand Down
107 changes: 106 additions & 1 deletion testproject/testapp/tests/test_set_username.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

import djoser.views

from .common import create_user
from .common import create_user, mock
from testapp.models import CustomUser


class SetUsernameViewTest(restframework.APIViewTestCase,
Expand Down Expand Up @@ -111,6 +112,57 @@ def test_post_not_set_new_username_if_same(self):
self.assert_status_equal(response, status.HTTP_400_BAD_REQUEST)
self.assertTrue(user.is_active)

@mock.patch(
'djoser.serializers.User', CustomUser)
@mock.patch(
'djoser.serializers.SetUsernameSerializer.Meta.model', CustomUser)
@mock.patch(
'djoser.serializers.SetUsernameSerializer.Meta.fields', (CustomUser.USERNAME_FIELD, 'current_password'))
@mock.patch(
'djoser.views.User', CustomUser)
@override_settings(
AUTH_USER_MODEL='testapp.CustomUser')
def test_post_set_new_custom_username(self):
user = create_user(use_custom_data=True)
data = {
'new_custom_username': 'ringo',
'current_password': 'secret',
}
request = self.factory.post(user=user, data=data)

response = self.view(request)

self.assert_status_equal(response, status.HTTP_204_NO_CONTENT)
user.refresh_from_db()
self.assertEqual(data['new_custom_username'], user.get_username())

@mock.patch(
'djoser.serializers.User', CustomUser)
@mock.patch(
'djoser.serializers.SetUsernameSerializer.Meta.model', CustomUser)
@mock.patch(
'djoser.serializers.SetUsernameSerializer.Meta.fields', (CustomUser.USERNAME_FIELD, 'current_password'))
@mock.patch(
'djoser.views.User', CustomUser)
@override_settings(
AUTH_USER_MODEL='testapp.CustomUser',
DJOSER=dict(settings.DJOSER, **{'SET_USERNAME_RETYPE': True})
)
def test_post_not_set_new_custom_username_if_mismatch(self):
user = create_user(use_custom_data=True)
data = {
'new_custom_username': 'ringo',
're_new_custom_username': 'wrong',
'current_password': 'secret',
}
request = self.factory.post(user=user, data=data)

response = self.view(request)

self.assert_status_equal(response, status.HTTP_400_BAD_REQUEST)
user.refresh_from_db()
self.assertNotEqual(data['new_custom_username'], user.get_username())


class UserViewSetChangeUsernameTest(APITestCase,
assertions.EmailAssertionsMixin,
Expand Down Expand Up @@ -201,3 +253,56 @@ def test_post_not_set_new_username_if_same(self):

self.assert_status_equal(response, status.HTTP_400_BAD_REQUEST)
self.assertTrue(self.user.is_active)

@mock.patch(
'djoser.serializers.User', CustomUser)
@mock.patch(
'djoser.serializers.SetUsernameSerializer.Meta.model', CustomUser)
@mock.patch(
'djoser.serializers.SetUsernameSerializer.Meta.fields', (CustomUser.USERNAME_FIELD, 'current_password'))
@mock.patch(
'djoser.urls.base.User', CustomUser)
@mock.patch(
'djoser.views.User', CustomUser)
@override_settings(
AUTH_USER_MODEL='testapp.CustomUser')
def test_post_set_new_custom_username(self):
user = create_user(use_custom_data=True)
data = {
'new_custom_username': 'ringo',
'current_password': 'secret',
}
self.client.force_authenticate(user)
response = self.client.post(reverse('user-change-username'), data=data)

self.assert_status_equal(response, status.HTTP_204_NO_CONTENT)
user.refresh_from_db()
self.assertEqual(data['new_custom_username'], user.get_username())

@mock.patch(
'djoser.serializers.User', CustomUser)
@mock.patch(
'djoser.serializers.SetUsernameSerializer.Meta.model', CustomUser)
@mock.patch(
'djoser.serializers.SetUsernameSerializer.Meta.fields', (CustomUser.USERNAME_FIELD, 'current_password'))
@mock.patch(
'djoser.urls.base.User', CustomUser)
@mock.patch(
'djoser.views.User', CustomUser)
@override_settings(
AUTH_USER_MODEL='testapp.CustomUser',
DJOSER=dict(settings.DJOSER, **{'SET_USERNAME_RETYPE': True})
)
def test_post_not_set_new_custom_username_if_mismatch(self):
user = create_user(use_custom_data=True)
data = {
'new_custom_username': 'ringo',
're_new_custom_username': 'wrong',
'current_password': 'secret',
}
self.client.force_authenticate(user)
response = self.client.post(reverse('user-change-username'), data=data)

self.assert_status_equal(response, status.HTTP_400_BAD_REQUEST)
user.refresh_from_db()
self.assertNotEqual(data['new_custom_username'], user.get_username())
Loading

0 comments on commit d1dae34

Please sign in to comment.