diff --git a/rest_auth/registration/serializers.py b/rest_auth/registration/serializers.py index 4f99c18..9513a1d 100644 --- a/rest_auth/registration/serializers.py +++ b/rest_auth/registration/serializers.py @@ -215,3 +215,7 @@ class RegisterSerializer(serializers.Serializer): class VerifyEmailSerializer(serializers.Serializer): key = serializers.CharField() + + +class ResendVerificationEmailSerializer(serializers.Serializer): + email = serializers.EmailField() diff --git a/rest_auth/registration/urls.py b/rest_auth/registration/urls.py index 1004695..3ac727b 100644 --- a/rest_auth/registration/urls.py +++ b/rest_auth/registration/urls.py @@ -1,12 +1,16 @@ from django.views.generic import TemplateView from django.conf.urls import url -from .views import RegisterView, VerifyEmailView +from .views import ( + RegisterView, + VerifyEmailView, + ResendVerificationEmailView +) urlpatterns = [ url(r'^$', RegisterView.as_view(), name='rest_register'), + url(r'^resend-verification-email/$', ResendVerificationEmailView.as_view(), name='rest_resend_verification_email'), url(r'^verify-email/$', VerifyEmailView.as_view(), name='rest_verify_email'), - # This url is used by django-allauth and empty TemplateView is # defined just to allow reverse() call inside app, for example when email # with verification link is being sent, then it's required to render email diff --git a/rest_auth/registration/views.py b/rest_auth/registration/views.py index 0e0ab0d..c30afb2 100644 --- a/rest_auth/registration/views.py +++ b/rest_auth/registration/views.py @@ -14,6 +14,7 @@ from rest_framework import status from allauth.account.adapter import get_adapter from allauth.account.views import ConfirmEmailView from allauth.account.utils import complete_signup +from allauth.account.models import EmailAddress from allauth.account import app_settings as allauth_settings from allauth.socialaccount import signals from allauth.socialaccount.adapter import get_adapter as get_social_adapter @@ -24,6 +25,7 @@ from rest_auth.app_settings import (TokenSerializer, create_token) from rest_auth.models import TokenModel from rest_auth.registration.serializers import (VerifyEmailSerializer, + ResendVerificationEmailSerializer, SocialLoginSerializer, SocialAccountSerializer, SocialConnectSerializer) @@ -98,6 +100,25 @@ class VerifyEmailView(APIView, ConfirmEmailView): return Response({'detail': _('ok')}, status=status.HTTP_200_OK) +class ResendVerificationEmailView(GenericAPIView): + serializer_class = ResendVerificationEmailSerializer + permission_classes = (AllowAny,) + allowed_methods = ('POST', 'OPTIONS', 'HEAD') + + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + email = serializer.data['email'] + + try: + email_address = EmailAddress.objects.get(email__exact=email, verified=False) + email_address.send_confirmation(self.request, True) + except EmailAddress.DoesNotExist: + pass + + return Response({'detail': _('Verification e-mail sent.')}) + + class SocialLoginView(LoginView): """ class used for social authentications diff --git a/rest_auth/tests/test_api.py b/rest_auth/tests/test_api.py index 9c5fd9e..235fe0f 100644 --- a/rest_auth/tests/test_api.py +++ b/rest_auth/tests/test_api.py @@ -11,6 +11,8 @@ from rest_framework.test import APIRequestFactory from rest_auth.registration.views import RegisterView from rest_auth.registration.app_settings import register_permission_classes +from allauth.account.models import EmailAddress + from .mixins import TestsMixin, CustomPermissionClass try: @@ -516,3 +518,66 @@ class APIBasicTests(TestsMixin, TestCase): self.post(self.login_url, data=payload, status_code=status.HTTP_200_OK) self.get(self.logout_url, status_code=status.HTTP_405_METHOD_NOT_ALLOWED) + + @override_settings(ACCOUNT_EMAIL_VERIFICATION='mandatory') + def test_resend_account_verification_email(self): + self.post( + self.register_url, + data=self.REGISTRATION_DATA_WITH_EMAIL, + status_code=status.HTTP_201_CREATED + ) + + self.assertEqual(EmailAddress.objects.count(), 1) + self.assertEqual(EmailAddress.objects.first().email, self.EMAIL) + self.assertEqual(EmailAddress.objects.first().verified, False) + + self.post( + reverse('rest_resend_verification_email'), + data={ + 'email': self.EMAIL + }, + status_code=status.HTTP_200_OK + ) + + self.assertEqual(len(mail.outbox), 2) + + @override_settings(ACCOUNT_EMAIL_VERIFICATION='mandatory') + def test_resend_not_registered_account_verification_email(self): + self.assertEqual(EmailAddress.objects.count(), 0) + + self.post( + reverse('rest_resend_verification_email'), + data={ + 'email': self.EMAIL + }, + status_code=status.HTTP_200_OK + ) + + self.assertEqual(len(mail.outbox), 0) + + @override_settings(ACCOUNT_EMAIL_VERIFICATION='mandatory') + def test_resend_already_verified_account_verification_email(self): + self.post( + self.register_url, + data=self.REGISTRATION_DATA_WITH_EMAIL, + status_code=status.HTTP_201_CREATED + ) + + self.assertEqual(EmailAddress.objects.count(), 1) + self.assertEqual(EmailAddress.objects.first().email, self.EMAIL) + self.assertEqual(EmailAddress.objects.first().verified, False) + self.assertEqual(len(mail.outbox), 1) + + email_address = EmailAddress.objects.first() + email_address.verified = True + email_address.save() + + self.post( + reverse('rest_resend_verification_email'), + data={ + 'email': self.EMAIL + }, + status_code=status.HTTP_200_OK + ) + + self.assertEqual(len(mail.outbox), 1)