From f14b3b03f716f36d054abbe3fa8432ecddb5fd6d Mon Sep 17 00:00:00 2001 From: Mateusz Sikora Date: Wed, 1 Oct 2014 14:13:21 +0200 Subject: [PATCH 01/21] django-registration replacement - remove django-registration references - integrate with django-allauth - move all registration stuff to separated app - update unit tests --- rest_auth/registration/__init__.py | 0 rest_auth/registration/serializers.py | 17 ++++ rest_auth/registration/urls.py | 10 +++ rest_auth/registration/views.py | 44 +++++++++++ rest_auth/runtests.py | 48 +----------- rest_auth/serializers.py | 11 --- rest_auth/test_settings.py | 8 +- rest_auth/tests.py | 108 ++++---------------------- rest_auth/urls.py | 13 ++-- rest_auth/views.py | 99 +---------------------- setup.py | 2 +- 11 files changed, 103 insertions(+), 257 deletions(-) create mode 100644 rest_auth/registration/__init__.py create mode 100644 rest_auth/registration/serializers.py create mode 100644 rest_auth/registration/urls.py create mode 100644 rest_auth/registration/views.py diff --git a/rest_auth/registration/__init__.py b/rest_auth/registration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rest_auth/registration/serializers.py b/rest_auth/registration/serializers.py new file mode 100644 index 0000000..f041da8 --- /dev/null +++ b/rest_auth/registration/serializers.py @@ -0,0 +1,17 @@ +from rest_framework import serializers +from django.contrib.auth import get_user_model + + +class UserRegistrationSerializer(serializers.ModelSerializer): + + """ + Serializer for Django User model and most of its fields. + """ + + class Meta: + model = get_user_model() + fields = ('username', 'password', 'email', 'first_name', 'last_name') + + +class VerifyEmailSerializer(serializers.Serializer): + pass diff --git a/rest_auth/registration/urls.py b/rest_auth/registration/urls.py new file mode 100644 index 0000000..615b0a9 --- /dev/null +++ b/rest_auth/registration/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls import patterns, url + +from .views import Register, VerifyEmail + +urlpatterns = patterns('', + url(r'^$', Register.as_view(), name='rest_register'), + url(r'^verify-email/(?P\w+)/$', VerifyEmail.as_view(), + name='verify_email'), +) + diff --git a/rest_auth/registration/views.py b/rest_auth/registration/views.py new file mode 100644 index 0000000..f812e1d --- /dev/null +++ b/rest_auth/registration/views.py @@ -0,0 +1,44 @@ +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.permissions import AllowAny +from rest_framework import status + +from allauth.account.views import SignupView +from allauth.account.utils import complete_signup +from allauth.account import app_settings + +from rest_auth.serializers import UserDetailsSerializer + + +class Register(APIView, SignupView): + + permission_classes = (AllowAny,) + user_serializer_class = UserDetailsSerializer + + def form_valid(self, form): + self.user = form.save(self.request) + return complete_signup(self.request, self.user, + app_settings.EMAIL_VERIFICATION, + self.get_success_url()) + + def post(self, request, *args, **kwargs): + self.initial = {} + self.request.POST = self.request.DATA.copy() + form_class = self.get_form_class() + self.form = self.get_form(form_class) + if self.form.is_valid(): + self.form_valid(self.form) + return self.get_response() + else: + return self.get_response_with_errors() + + def get_response(self): + serializer = self.user_serializer_class(instance=self.user) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def get_response_with_errors(self): + return Response(self.form.errors, status=status.HTTP_400_BAD_REQUEST) + + +class VerifyEmail(APIView): + pass diff --git a/rest_auth/runtests.py b/rest_auth/runtests.py index 01a3797..3d7e413 100644 --- a/rest_auth/runtests.py +++ b/rest_auth/runtests.py @@ -1,55 +1,13 @@ #This file mainly exists to allow python setup.py test to work. -import os, sys +import os +import sys + os.environ['DJANGO_SETTINGS_MODULE'] = 'test_settings' test_dir = os.path.dirname(__file__) sys.path.insert(0, test_dir) from django.test.utils import get_runner from django.conf import settings -from django.contrib.auth.models import User -from django.contrib.sites.models import RequestSite -from django.contrib.sites.models import Site -from django.db import models - -from rest_framework.serializers import _resolve_model -from registration.models import RegistrationProfile -from registration.backends.default.views import RegistrationView as BaseRegistrationView -from registration import signals - -""" -create user profile model -""" -class UserProfile(models.Model): - user = models.ForeignKey(User, unique=True) - newsletter_subscribe = models.BooleanField(default=False) - - class Meta: - app_label = 'rest_auth' - - -""" -overwrite register to avoid sending email -""" -class RegistrationView(BaseRegistrationView): - def register(self, request, **cleaned_data): - username, email, password = cleaned_data['username'], cleaned_data['email'], cleaned_data['password1'] - if Site._meta.installed: - site = Site.objects.get_current() - else: - site = RequestSite(request) - new_user = RegistrationProfile.objects.create_inactive_user(username, email, - password, site, send_email=False) - signals.user_registered.send(sender=self.__class__, - user=new_user, - request=request) - - # create user profile - profile_model_path = getattr(settings, 'REST_PROFILE_MODULE', None) - if profile_model_path: - user_profile_model = _resolve_model(profile_model_path) - user_profile_model.objects.create(user=new_user) - - return new_user def runtests(): diff --git a/rest_auth/serializers.py b/rest_auth/serializers.py index af4725e..b2e708a 100644 --- a/rest_auth/serializers.py +++ b/rest_auth/serializers.py @@ -34,17 +34,6 @@ class UserDetailsSerializer(serializers.ModelSerializer): fields = ('username', 'email', 'first_name', 'last_name') -class UserRegistrationSerializer(serializers.ModelSerializer): - - """ - Serializer for Django User model and most of its fields. - """ - - class Meta: - model = get_user_model() - fields = ('username', 'password', 'email', 'first_name', 'last_name') - - class DynamicFieldsModelSerializer(serializers.ModelSerializer): """ diff --git a/rest_auth/test_settings.py b/rest_auth/test_settings.py index 1b6f82e..175f989 100644 --- a/rest_auth/test_settings.py +++ b/rest_auth/test_settings.py @@ -1,5 +1,6 @@ import django -import os, sys +import os +import sys PROJECT_ROOT = os.path.abspath(os.path.split(os.path.split(__file__)[0])[0]) ROOT_URLCONF = 'urls' @@ -37,11 +38,14 @@ INSTALLED_APPS = [ 'django.contrib.sitemaps', 'django.contrib.staticfiles', + 'allauth', + 'allauth.account', + 'rest_framework', 'rest_framework.authtoken', - 'registration', 'rest_auth', + 'rest_auth.registration' ] SECRET_KEY = "38dh*skf8sjfhs287dh&^hd8&3hdg*j2&sd" diff --git a/rest_auth/tests.py b/rest_auth/tests.py index 2ad4945..1a78b5e 100644 --- a/rest_auth/tests.py +++ b/rest_auth/tests.py @@ -9,7 +9,6 @@ from django.contrib.auth.models import User from django.contrib.auth import get_user_model from django.core import mail -from registration.models import RegistrationProfile from rest_framework.serializers import _resolve_model @@ -113,7 +112,6 @@ class BaseAPITestCase(object): # ----------------------- - class APITestCase1(TestCase, BaseAPITestCase): """ Case #1: @@ -129,16 +127,12 @@ class APITestCase1(TestCase, BaseAPITestCase): REGISTRATION_VIEW = 'rest_auth.runtests.RegistrationView' # data without user profile - BASIC_REGISTRATION_DATA = { + REGISTRATION_DATA = { "username": USERNAME, - "password": PASS, - "email": EMAIL + "password1": PASS, + "password2": PASS } - # data with user profile - REGISTRATION_DATA = BASIC_REGISTRATION_DATA.copy() - REGISTRATION_DATA['newsletter_subscribe'] = False - BASIC_USER_DATA = { 'first_name': "John", 'last_name': 'Smith', @@ -191,7 +185,7 @@ class APITestCase1(TestCase, BaseAPITestCase): # test wrong username/password payload = { - "username": self.USERNAME+'?', + "username": self.USERNAME + '?', "password": self.PASS } self.post(self.login_url, data=payload, status_code=401) @@ -199,13 +193,12 @@ class APITestCase1(TestCase, BaseAPITestCase): # test empty payload self.post(self.login_url, data={}, status_code=400) - def test_password_change(self): login_payload = { "username": self.USERNAME, "password": self.PASS } - user = User.objects.create_user(self.USERNAME, '', self.PASS) + User.objects.create_user(self.USERNAME, '', self.PASS) self.post(self.login_url, data=login_payload, status_code=200) self.token = self.response.json['key'] @@ -234,61 +227,6 @@ class APITestCase1(TestCase, BaseAPITestCase): # send empty payload self.post(self.password_change_url, data={}, status_code=400) - def test_registration(self): - user_count = User.objects.all().count() - - # test empty payload - self.post(self.register_url, data={}, status_code=400) - - self.post(self.register_url, data=self.REGISTRATION_DATA, status_code=201) - self.assertEqual(User.objects.all().count(), user_count+1) - new_user = get_user_model().objects.latest('id') - - if self.REGISTRATION_VIEW: - activation_key = RegistrationProfile.objects.latest('id').activation_key - verify_url = reverse('verify_email', - kwargs={'activation_key': activation_key}) - - # new user at this point shouldn't be active - self.assertEqual(new_user.is_active, False) - - # let's active new user and check is_active flag - self.get(verify_url) - new_user = get_user_model().objects.latest('id') - self.assertEqual(new_user.is_active, True) - if self.user_profile_model: - user_profile = self.user_profile_model.objects.get(user=new_user) - self.assertIsNotNone(user_profile) - else: - self.assertEqual(new_user.is_active, True) - - def test_registration_without_profile_data(self): - user_count = User.objects.all().count() - - self.post(self.register_url, data=self.BASIC_REGISTRATION_DATA, - status_code=201) - self.assertEqual(User.objects.all().count(), user_count+1) - new_user = get_user_model().objects.latest('id') - - if self.REGISTRATION_VIEW: - activation_key = RegistrationProfile.objects.latest('id').activation_key - verify_url = reverse('verify_email', - kwargs={'activation_key': activation_key}) - - # new user at this point shouldn't be active - self.assertEqual(new_user.is_active, False) - - # let's active new user and check is_active flag - self.get(verify_url) - new_user = get_user_model().objects.latest('id') - self.assertEqual(new_user.is_active, True) - if self.user_profile_model: - user_profile = self.user_profile_model.objects.get(user=new_user) - self.assertIsNotNone(user_profile) - else: - self.assertEqual(new_user.is_active, True) - - def test_password_reset(self): user = User.objects.create_user(self.USERNAME, self.EMAIL, self.PASS) @@ -296,7 +234,7 @@ class APITestCase1(TestCase, BaseAPITestCase): mail_count = len(mail.outbox) payload = {'email': self.EMAIL} self.post(self.password_reset_url, data=payload) - self.assertEqual(len(mail.outbox), mail_count+1) + self.assertEqual(len(mail.outbox), mail_count + 1) url_kwargs = self.generate_uid_and_token(user) @@ -340,7 +278,6 @@ class APITestCase1(TestCase, BaseAPITestCase): self.assertEqual(user.last_name, self.response.json['last_name']) self.assertEqual(user.email, self.response.json['email']) - def generate_uid_and_token(self, user): result = {} from django.utils.encoding import force_bytes @@ -355,30 +292,13 @@ class APITestCase1(TestCase, BaseAPITestCase): result['token'] = default_token_generator.make_token(user) return result + def test_registration(self): + user_count = User.objects.all().count() -class APITestCase2(APITestCase1): - """ - Case #2: - - user profile: not defined - - custom registration backend: not defined - """ - PROFILE_MODEL = None + # test empty payload + self.post(self.register_url, data={}, status_code=400) - -class APITestCase3(APITestCase1): - """ - Case #3: - - user profile: defined - - custom registration backend: not defined - """ - REGISTRATION_VIEW = None - - -class APITestCase4(APITestCase1): - """ - Case #4: - - user profile: not defined - - custom registration backend: not defined - """ - PROFILE_MODEL = None - REGISTRATION_VIEW = None + self.post(self.register_url, data=self.REGISTRATION_DATA, status_code=201) + self.assertEqual(User.objects.all().count(), user_count + 1) + new_user = get_user_model().objects.latest('id') + self.assertEqual(new_user.username, self.REGISTRATION_DATA['username']) diff --git a/rest_auth/urls.py b/rest_auth/urls.py index b51bc1e..b1abfe0 100644 --- a/rest_auth/urls.py +++ b/rest_auth/urls.py @@ -1,22 +1,18 @@ from django.conf import settings from django.conf.urls import patterns, url, include -from rest_auth.views import Login, Logout, Register, UserDetails, \ - PasswordChange, PasswordReset, VerifyEmail, PasswordResetConfirm +from rest_auth.views import Login, Logout, UserDetails, \ + PasswordChange, PasswordReset, PasswordResetConfirm urlpatterns = patterns('rest_auth.views', # URLs that do not require a session or valid token - url(r'^register/$', Register.as_view(), - name='rest_register'), url(r'^password/reset/$', PasswordReset.as_view(), name='rest_password_reset'), url(r'^password/reset/confirm/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', PasswordResetConfirm.as_view( ), name='rest_password_reset_confirm'), url(r'^login/$', Login.as_view(), name='rest_login'), - url(r'^verify-email/(?P\w+)/$', - VerifyEmail.as_view(), name='verify_email'), # URLs that require a user to be logged in with a valid # session / token. @@ -29,4 +25,7 @@ urlpatterns = patterns('rest_auth.views', if getattr(settings, 'IS_TEST', False): from django.contrib.auth.tests import urls - urlpatterns += patterns('', url(r'^test-admin/', include(urls))) + urlpatterns += patterns('', + url(r'^rest-registration/', include('registration.urls')), + url(r'^test-admin/', include(urls)) + ) diff --git a/rest_auth/views.py b/rest_auth/views.py index 50ee4e5..9bc5b20 100644 --- a/rest_auth/views.py +++ b/rest_auth/views.py @@ -7,7 +7,6 @@ except: # make compatible with django 1.5 from django.utils.http import base36_to_int as uid_decoder from django.conf import settings -from django.core.exceptions import ObjectDoesNotExist from rest_framework import status from rest_framework.views import APIView @@ -19,17 +18,11 @@ from rest_framework.authentication import SessionAuthentication, \ TokenAuthentication from rest_framework.authtoken.models import Token -from registration.models import RegistrationProfile -from registration import signals -from registration.views import ActivationView - from rest_auth.utils import construct_modules_and_import -from rest_auth.models import * from rest_auth.serializers import (TokenSerializer, UserDetailsSerializer, - LoginSerializer, UserRegistrationSerializer, + LoginSerializer, SetPasswordSerializer, PasswordResetSerializer, UserUpdateSerializer, - get_user_registration_profile_serializer, get_user_profile_serializer, - get_user_profile_update_serializer) + get_user_profile_serializer, get_user_profile_update_serializer) def get_user_profile_model(): @@ -40,16 +33,6 @@ def get_user_profile_model(): return _resolve_model(user_profile_path) -def get_registration_backend(): - # Get the REST Registration Backend for django-registration - registration_backend = getattr(settings, 'REST_REGISTRATION_BACKEND', - 'registration.backends.simple.views.RegistrationView') - - # Get the REST REGISTRATION BACKEND class from the setting value via above - # method - return construct_modules_and_import(registration_backend) - - class LoggedInRESTAPIView(APIView): authentication_classes = ((SessionAuthentication, TokenAuthentication)) permission_classes = ((IsAuthenticated,)) @@ -125,53 +108,6 @@ class Logout(LoggedInRESTAPIView): status=status.HTTP_200_OK) -class Register(LoggedOutRESTAPIView, GenericAPIView): - - """ - Registers a new Django User object by accepting required field values. - - Accepts the following POST parameters: - Required: username, password, email - Optional: first_name & last_name for User object and UserProfile fields - Returns the newly created User object including REST Framework Token key. - """ - - serializer_class = UserRegistrationSerializer - - def get_profile_serializer_class(self): - return get_user_registration_profile_serializer() - - def post(self, request): - # Create serializers with request.DATA - serializer = self.serializer_class(data=request.DATA) - profile_serializer_class = self.get_profile_serializer_class() - profile_serializer = profile_serializer_class(data=request.DATA) - - if serializer.is_valid() and profile_serializer.is_valid(): - # Change the password key to password1 so that RESTRegistrationView - # can accept the data - serializer.data['password1'] = serializer.data.pop('password') - - # TODO: Make this customizable backend via settings. - # Call RESTRegistrationView().register to create new Django User - # and UserProfile models - data = serializer.data.copy() - data.update(profile_serializer.data) - - RESTRegistrationView = get_registration_backend() - RESTRegistrationView().register(request, **data) - - # Return the User object with Created HTTP status - return Response(UserDetailsSerializer(serializer.data).data, - status=status.HTTP_201_CREATED) - - else: - return Response({ - 'user': serializer.errors, - 'profile': profile_serializer.errors}, - status=status.HTTP_400_BAD_REQUEST) - - class UserDetails(LoggedInRESTAPIView, GenericAPIView): """ @@ -328,37 +264,6 @@ class PasswordResetConfirm(LoggedOutRESTAPIView, GenericAPIView): return Response({"errors": "Couldn\'t find the user from uid."}, status=status.HTTP_400_BAD_REQUEST) -class VerifyEmail(LoggedOutRESTAPIView, GenericAPIView): - - """ - Verifies the email of the user through their activation_key. - - Accepts activation_key django argument: key from activation email. - Returns the success/fail message. - """ - - model = RegistrationProfile - - def get(self, request, activation_key=None): - # Get the user registration profile with the activation key - target_user = RegistrationProfile.objects.activate_user(activation_key) - - if target_user: - # Send the activation signal - signals.user_activated.send(sender=ActivationView.__class__, - user=target_user, - request=request) - - # Return the success message with OK HTTP status - ret_msg = "User {0}'s account was successfully activated!".format( - target_user.username) - return Response({"success": ret_msg}, status=status.HTTP_200_OK) - - else: - ret_msg = "The account was not able to be activated or already activated, please contact support." - return Response({"errors": ret_msg}, status=status.HTTP_400_BAD_REQUEST) - - class PasswordChange(LoggedInRESTAPIView, GenericAPIView): """ diff --git a/setup.py b/setup.py index a247e92..8bbfb04 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ setup( zip_safe=False, install_requires=[ 'Django>=1.5.0', - 'django-registration>=1.0', + 'django-allauth>=0.18.0', 'djangorestframework>=2.3.13', ], test_suite='rest_auth.runtests.runtests', From de1fb3d81f907f6a487041efc0910593d9543b05 Mon Sep 17 00:00:00 2001 From: Mateusz Sikora Date: Wed, 1 Oct 2014 15:31:10 +0200 Subject: [PATCH 02/21] registration with email verification: - rebuild login view - check email verification in LoginSerializer depends on allauth settings - add test for registration with email verification --- rest_auth/registration/urls.py | 7 ++++++ rest_auth/serializers.py | 18 +++++++++++--- rest_auth/tests.py | 45 ++++++++++++++++++++++++++++++---- rest_auth/urls.py | 7 +++++- rest_auth/views.py | 33 +++++++++---------------- 5 files changed, 79 insertions(+), 31 deletions(-) diff --git a/rest_auth/registration/urls.py b/rest_auth/registration/urls.py index 615b0a9..ad401ef 100644 --- a/rest_auth/registration/urls.py +++ b/rest_auth/registration/urls.py @@ -1,3 +1,4 @@ +from django.views.generic import TemplateView from django.conf.urls import patterns, url from .views import Register, VerifyEmail @@ -6,5 +7,11 @@ urlpatterns = patterns('', url(r'^$', Register.as_view(), name='rest_register'), url(r'^verify-email/(?P\w+)/$', VerifyEmail.as_view(), name='verify_email'), + + url(r'^account-email-verification-sent/$', TemplateView.as_view(), + name='account_email_verification_sent'), + url(r'^account-confirm-email/(?P\w+)/$', TemplateView.as_view(), + name='account_confirm_email'), + ) diff --git a/rest_auth/serializers.py b/rest_auth/serializers.py index b2e708a..c4e12a9 100644 --- a/rest_auth/serializers.py +++ b/rest_auth/serializers.py @@ -4,13 +4,25 @@ from django.conf import settings from rest_framework import serializers from rest_framework.serializers import _resolve_model from rest_framework.authtoken.models import Token +from rest_framework.authtoken.serializers import AuthTokenSerializer profile_model_path = lambda: getattr(settings, 'REST_PROFILE_MODULE', None) -class LoginSerializer(serializers.Serializer): - username = serializers.CharField(max_length=30) - password = serializers.CharField(max_length=128) + +class LoginSerializer(AuthTokenSerializer): + + def validate(self, attrs): + attrs = super(LoginSerializer, self).validate(attrs) + + if 'rest_auth.registration' in settings.INSTALLED_APPS: + from allauth.account import app_settings + if app_settings.EMAIL_VERIFICATION == app_settings.EmailVerificationMethod.MANDATORY: + user = attrs['user'] + email_address = user.emailaddress_set.get(email=user.email) + if not email_address.verified: + raise serializers.ValidationError('E-mail is not verified.') + return attrs class TokenSerializer(serializers.ModelSerializer): diff --git a/rest_auth/tests.py b/rest_auth/tests.py index 1a78b5e..1c4d59e 100644 --- a/rest_auth/tests.py +++ b/rest_auth/tests.py @@ -8,8 +8,10 @@ from django.test import TestCase from django.contrib.auth.models import User from django.contrib.auth import get_user_model from django.core import mail +from django.test.utils import override_settings from rest_framework.serializers import _resolve_model +from rest_framework import status class APIClient(Client): @@ -133,6 +135,9 @@ class APITestCase1(TestCase, BaseAPITestCase): "password2": PASS } + REGISTRATION_DATA_WITH_EMAIL = REGISTRATION_DATA.copy() + REGISTRATION_DATA_WITH_EMAIL['email'] = EMAIL + BASIC_USER_DATA = { 'first_name': "John", 'last_name': 'Smith', @@ -164,8 +169,8 @@ class APITestCase1(TestCase, BaseAPITestCase): "username": self.USERNAME, "password": self.PASS } - # there is no users in db so it should throw error (401) - self.post(self.login_url, data=payload, status_code=401) + # there is no users in db so it should throw error (400) + self.post(self.login_url, data=payload, status_code=400) self.post(self.password_change_url, status_code=403) @@ -181,14 +186,14 @@ class APITestCase1(TestCase, BaseAPITestCase): # test inactive user user.is_active = False user.save() - self.post(self.login_url, data=payload, status_code=401) + self.post(self.login_url, data=payload, status_code=400) # test wrong username/password payload = { "username": self.USERNAME + '?', "password": self.PASS } - self.post(self.login_url, data=payload, status_code=401) + self.post(self.login_url, data=payload, status_code=400) # test empty payload self.post(self.login_url, data={}, status_code=400) @@ -210,7 +215,7 @@ class APITestCase1(TestCase, BaseAPITestCase): status_code=200) # user should not be able to login using old password - self.post(self.login_url, data=login_payload, status_code=401) + self.post(self.login_url, data=login_payload, status_code=400) # new password should work login_payload['password'] = new_password_payload['new_password1'] @@ -302,3 +307,33 @@ class APITestCase1(TestCase, BaseAPITestCase): self.assertEqual(User.objects.all().count(), user_count + 1) new_user = get_user_model().objects.latest('id') self.assertEqual(new_user.username, self.REGISTRATION_DATA['username']) + + payload = { + "username": self.USERNAME, + "password": self.PASS + } + self.post(self.login_url, data=payload, status_code=200) + + @override_settings( + ACCOUNT_EMAIL_VERIFICATION='mandatory', + ACCOUNT_EMAIL_REQUIRED=True + ) + def test_registration_with_email_verification(self): + user_count = User.objects.all().count() + mail_count = len(mail.outbox) + + # test empty payload + self.post(self.register_url, data={}, status_code=400) + + self.post(self.register_url, data=self.REGISTRATION_DATA_WITH_EMAIL, status_code=201) + self.assertEqual(User.objects.all().count(), user_count + 1) + self.assertEqual(len(mail.outbox), mail_count + 1) + new_user = get_user_model().objects.latest('id') + self.assertEqual(new_user.username, self.REGISTRATION_DATA['username']) + + # email is not verified yet + payload = { + "username": self.USERNAME, + "password": self.PASS + } + self.post(self.login_url, data=payload, status=status.HTTP_400_BAD_REQUEST) diff --git a/rest_auth/urls.py b/rest_auth/urls.py index b1abfe0..701351f 100644 --- a/rest_auth/urls.py +++ b/rest_auth/urls.py @@ -1,5 +1,6 @@ from django.conf import settings from django.conf.urls import patterns, url, include +from django.views.generic import TemplateView from rest_auth.views import Login, Logout, UserDetails, \ PasswordChange, PasswordReset, PasswordResetConfirm @@ -27,5 +28,9 @@ if getattr(settings, 'IS_TEST', False): from django.contrib.auth.tests import urls urlpatterns += patterns('', url(r'^rest-registration/', include('registration.urls')), - url(r'^test-admin/', include(urls)) + url(r'^test-admin/', include(urls)), + url(r'^account-email-verification-sent/$', TemplateView.as_view(), + name='account_email_verification_sent'), + url(r'^account-confirm-email/(?P\w+)/$', TemplateView.as_view(), + name='account_confirm_email'), ) diff --git a/rest_auth/views.py b/rest_auth/views.py index 9bc5b20..25ff836 100644 --- a/rest_auth/views.py +++ b/rest_auth/views.py @@ -62,29 +62,18 @@ class Login(LoggedOutRESTAPIView, GenericAPIView): # Create a serializer with request.DATA serializer = self.serializer_class(data=request.DATA) - if serializer.is_valid(): - # Authenticate the credentials by grabbing Django User object - user = authenticate(username=serializer.data['username'], - password=serializer.data['password']) - - if user and user.is_authenticated(): - if user.is_active: - if getattr(settings, 'REST_SESSION_LOGIN', True): - login(request, user) - - # Return REST Token object with OK HTTP status - token, created = self.token_model.objects.get_or_create(user=user) - return Response(self.token_serializer(token).data, - status=status.HTTP_200_OK) - else: - return Response({'error': 'This account is disabled.'}, - status=status.HTTP_401_UNAUTHORIZED) - else: - return Response({'error': 'Invalid Username/Password.'}, - status=status.HTTP_401_UNAUTHORIZED) - else: + if not serializer.is_valid(): return Response(serializer.errors, - status=status.HTTP_400_BAD_REQUEST) + status=status.HTTP_400_BAD_REQUEST) + + user = serializer.object['user'] + token, created = self.token_model.objects.get_or_create(user=user) + + if getattr(settings, 'REST_SESSION_LOGIN', True): + login(request, user) + + return Response(self.token_serializer(token).data, + status=status.HTTP_200_OK) class Logout(LoggedInRESTAPIView): From 08fcca9b48b33626e9aa0c8d76c5e11d7cd2c64e Mon Sep 17 00:00:00 2001 From: Mateusz Sikora Date: Wed, 1 Oct 2014 16:34:51 +0200 Subject: [PATCH 03/21] veirfy email view and test --- rest_auth/registration/urls.py | 3 +-- rest_auth/registration/views.py | 13 ++++++++++--- rest_auth/tests.py | 19 ++++++++++++++++--- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/rest_auth/registration/urls.py b/rest_auth/registration/urls.py index ad401ef..aead53c 100644 --- a/rest_auth/registration/urls.py +++ b/rest_auth/registration/urls.py @@ -5,8 +5,7 @@ from .views import Register, VerifyEmail urlpatterns = patterns('', url(r'^$', Register.as_view(), name='rest_register'), - url(r'^verify-email/(?P\w+)/$', VerifyEmail.as_view(), - name='verify_email'), + url(r'^verify-email/$', VerifyEmail.as_view(), name='verify_email'), url(r'^account-email-verification-sent/$', TemplateView.as_view(), name='account_email_verification_sent'), diff --git a/rest_auth/registration/views.py b/rest_auth/registration/views.py index f812e1d..8225bcb 100644 --- a/rest_auth/registration/views.py +++ b/rest_auth/registration/views.py @@ -3,7 +3,7 @@ from rest_framework.response import Response from rest_framework.permissions import AllowAny from rest_framework import status -from allauth.account.views import SignupView +from allauth.account.views import SignupView, ConfirmEmailView from allauth.account.utils import complete_signup from allauth.account import app_settings @@ -40,5 +40,12 @@ class Register(APIView, SignupView): return Response(self.form.errors, status=status.HTTP_400_BAD_REQUEST) -class VerifyEmail(APIView): - pass +class VerifyEmail(APIView, ConfirmEmailView): + + permission_classes = (AllowAny,) + + def post(self, request, *args, **kwargs): + self.kwargs['key'] = self.request.DATA.get('key', '') + confirmation = self.get_object() + confirmation.confirm(self.request) + return Response({'message': 'ok'}, status=status.HTTP_200_OK) diff --git a/rest_auth/tests.py b/rest_auth/tests.py index 1c4d59e..0ce2ecb 100644 --- a/rest_auth/tests.py +++ b/rest_auth/tests.py @@ -153,6 +153,7 @@ class APITestCase1(TestCase, BaseAPITestCase): self.register_url = reverse('rest_register') self.password_reset_url = reverse('rest_password_reset') self.user_url = reverse('rest_user_details') + self.veirfy_email_url = reverse('verify_email') setattr(settings, 'REST_PROFILE_MODULE', self.PROFILE_MODEL) self.user_profile_model = None @@ -323,9 +324,11 @@ class APITestCase1(TestCase, BaseAPITestCase): mail_count = len(mail.outbox) # test empty payload - self.post(self.register_url, data={}, status_code=400) + self.post(self.register_url, data={}, + status_code=status.HTTP_400_BAD_REQUEST) - self.post(self.register_url, data=self.REGISTRATION_DATA_WITH_EMAIL, status_code=201) + self.post(self.register_url, data=self.REGISTRATION_DATA_WITH_EMAIL, + status_code=status.HTTP_201_BAD_REQUEST) self.assertEqual(User.objects.all().count(), user_count + 1) self.assertEqual(len(mail.outbox), mail_count + 1) new_user = get_user_model().objects.latest('id') @@ -336,4 +339,14 @@ class APITestCase1(TestCase, BaseAPITestCase): "username": self.USERNAME, "password": self.PASS } - self.post(self.login_url, data=payload, status=status.HTTP_400_BAD_REQUEST) + self.post(self.login_url, data=payload, + status=status.HTTP_400_BAD_REQUEST) + + # veirfy email + email_confirmation = new_user.emailaddress_set.get(email=self.EMAIL)\ + .emailconfirmation_set.last() + self.post(self.veirfy_email_url, data={"key": email_confirmation.key}, + status_code=status.HTTP_200_OK) + + # try to login again + self.post(self.login_url, data=payload, status_code=status.HTTP_200_OK) From 65b5caa3d0e9163618c91bec839d362fd9b268da Mon Sep 17 00:00:00 2001 From: Mateusz Sikora Date: Wed, 1 Oct 2014 16:36:34 +0200 Subject: [PATCH 04/21] typo --- rest_auth/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_auth/tests.py b/rest_auth/tests.py index 0ce2ecb..f5a26ec 100644 --- a/rest_auth/tests.py +++ b/rest_auth/tests.py @@ -328,7 +328,7 @@ class APITestCase1(TestCase, BaseAPITestCase): status_code=status.HTTP_400_BAD_REQUEST) self.post(self.register_url, data=self.REGISTRATION_DATA_WITH_EMAIL, - status_code=status.HTTP_201_BAD_REQUEST) + status_code=status.HTTP_201_CREATED) self.assertEqual(User.objects.all().count(), user_count + 1) self.assertEqual(len(mail.outbox), mail_count + 1) new_user = get_user_model().objects.latest('id') From 459d03e30d2399fd17a55882f20a0fc53851423f Mon Sep 17 00:00:00 2001 From: Mateusz Sikora Date: Thu, 2 Oct 2014 11:18:23 +0200 Subject: [PATCH 05/21] revised user details view --- rest_auth/serializers.py | 98 ++-------------------------------------- rest_auth/tests.py | 2 +- rest_auth/views.py | 91 ++++++++++++------------------------- 3 files changed, 33 insertions(+), 158 deletions(-) diff --git a/rest_auth/serializers.py b/rest_auth/serializers.py index c4e12a9..8cc1105 100644 --- a/rest_auth/serializers.py +++ b/rest_auth/serializers.py @@ -2,14 +2,10 @@ from django.contrib.auth import get_user_model from django.conf import settings from rest_framework import serializers -from rest_framework.serializers import _resolve_model from rest_framework.authtoken.models import Token from rest_framework.authtoken.serializers import AuthTokenSerializer -profile_model_path = lambda: getattr(settings, 'REST_PROFILE_MODULE', None) - - class LoginSerializer(AuthTokenSerializer): def validate(self, attrs): @@ -26,7 +22,6 @@ class LoginSerializer(AuthTokenSerializer): class TokenSerializer(serializers.ModelSerializer): - """ Serializer for Token model. """ @@ -43,96 +38,9 @@ class UserDetailsSerializer(serializers.ModelSerializer): """ class Meta: model = get_user_model() - fields = ('username', 'email', 'first_name', 'last_name') - - -class DynamicFieldsModelSerializer(serializers.ModelSerializer): - - """ - ModelSerializer that allows fields argument to control fields - """ - - def __init__(self, *args, **kwargs): - fields = kwargs.pop('fields', None) - - super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs) - - if fields: - allowed = set(fields) - existing = set(self.fields.keys()) - - for field_name in existing - allowed: - self.fields.pop(field_name) - - -class UserUpdateSerializer(DynamicFieldsModelSerializer): - - """ - User model w/o username and password - """ - class Meta: - model = get_user_model() - fields = ('id', 'email', 'first_name', 'last_name') - - - -def get_user_registration_profile_serializer(*args, **kwargs): - if profile_model_path(): - class UserRegistrationProfileSerializer(serializers.ModelSerializer): - - """ - Serializer that includes all profile fields except for user fk / id. - """ - class Meta: - - model = _resolve_model(profile_model_path()) - fields = filter(lambda x: x != 'id' and x != 'user', - map(lambda x: x.name, model._meta.fields)) - else: - class UserRegistrationProfileSerializer(serializers.Serializer): - pass - return UserRegistrationProfileSerializer - - -def get_user_profile_serializer(*args, **kwargs): - if profile_model_path(): - class UserProfileSerializer(serializers.ModelSerializer): - - """ - Serializer for UserProfile model. - """ - - user = UserDetailsSerializer() - - class Meta: - # http://stackoverflow.com/questions/4881607/django-get-model-from-string - model = _resolve_model(profile_model_path()) - - def __init__(self, *args, **kwargs): - super(UserProfileSerializer, self).__init__(*args, **kwargs) - else: - class UserProfileSerializer(serializers.Serializer): - pass - return UserProfileSerializer - - -def get_user_profile_update_serializer(*args, **kwargs): - if profile_model_path(): - class UserProfileUpdateSerializer(serializers.ModelSerializer): - - """ - Serializer for updating User and UserProfile model. - """ - - user = UserUpdateSerializer() - - class Meta: - # http://stackoverflow.com/questions/4881607/django-get-model-from-string - model = _resolve_model(profile_model_path()) - else: - class UserProfileUpdateSerializer(serializers.Serializer): - pass - return UserProfileUpdateSerializer + exclude = ('password', 'groups', 'user_permissions', 'is_staff', + 'is_superuser') + read_only_fields = ('id', 'last_login', 'is_active', 'date_joined') class SetPasswordSerializer(serializers.Serializer): diff --git a/rest_auth/tests.py b/rest_auth/tests.py index f5a26ec..908c595 100644 --- a/rest_auth/tests.py +++ b/rest_auth/tests.py @@ -269,7 +269,7 @@ class APITestCase1(TestCase, BaseAPITestCase): self.token = self.response.json['key'] self.get(self.user_url, status_code=200) - self.post(self.user_url, data=self.BASIC_USER_DATA, status_code=200) + self.patch(self.user_url, data=self.BASIC_USER_DATA, status_code=200) user = User.objects.get(pk=user.pk) if self.user_profile_model: diff --git a/rest_auth/views.py b/rest_auth/views.py index 25ff836..7ab19af 100644 --- a/rest_auth/views.py +++ b/rest_auth/views.py @@ -17,12 +17,10 @@ from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.authentication import SessionAuthentication, \ TokenAuthentication from rest_framework.authtoken.models import Token +from rest_framework.generics import RetrieveUpdateAPIView -from rest_auth.utils import construct_modules_and_import from rest_auth.serializers import (TokenSerializer, UserDetailsSerializer, - LoginSerializer, - SetPasswordSerializer, PasswordResetSerializer, UserUpdateSerializer, - get_user_profile_serializer, get_user_profile_update_serializer) + LoginSerializer, SetPasswordSerializer, PasswordResetSerializer) def get_user_profile_model(): @@ -56,24 +54,33 @@ class Login(LoggedOutRESTAPIView, GenericAPIView): serializer_class = LoginSerializer token_model = Token - token_serializer = TokenSerializer + response_serializer = TokenSerializer - def post(self, request): - # Create a serializer with request.DATA - serializer = self.serializer_class(data=request.DATA) + def get_serializer(self): + return self.serializer_class(data=self.request.DATA) - if not serializer.is_valid(): - return Response(serializer.errors, - status=status.HTTP_400_BAD_REQUEST) - - user = serializer.object['user'] - token, created = self.token_model.objects.get_or_create(user=user) + def login(self): + self.user = self.serializer.object['user'] + self.token, created = self.token_model.objects.get_or_create( + user=self.user) if getattr(settings, 'REST_SESSION_LOGIN', True): - login(request, user) + login(self.request, self.user) - return Response(self.token_serializer(token).data, - status=status.HTTP_200_OK) + def get_response(self): + return Response(self.response_serializer(self.token).data, + status=status.HTTP_200_OK) + + def get_error_response(self): + return Response(self.serializer.errors, + status=status.HTTP_400_BAD_REQUEST) + + def post(self, request, *args, **kwargs): + self.serializer = self.get_serializer() + if not self.serializer.is_valid(): + return self.get_error_response() + self.login() + return self.get_response() class Logout(LoggedInRESTAPIView): @@ -85,7 +92,7 @@ class Logout(LoggedInRESTAPIView): Accepts/Returns nothing. """ - def get(self, request): + def post(self, request): try: request.user.auth_token.delete() except: @@ -97,7 +104,7 @@ class Logout(LoggedInRESTAPIView): status=status.HTTP_200_OK) -class UserDetails(LoggedInRESTAPIView, GenericAPIView): +class UserDetails(LoggedInRESTAPIView, RetrieveUpdateAPIView): """ Returns User's details in JSON format. @@ -108,50 +115,10 @@ class UserDetails(LoggedInRESTAPIView, GenericAPIView): Optional: email, first_name, last_name and UserProfile fields Returns the updated UserProfile and/or User object. """ - if get_user_profile_model(): - serializer_class = get_user_profile_update_serializer() - else: - serializer_class = UserUpdateSerializer + serializer_class = UserDetailsSerializer - def get_profile_serializer_class(self): - return get_user_profile_serializer() - - def get_profile_update_serializer_class(self): - return get_user_profile_update_serializer() - - def get(self, request): - # Create serializers with request.user and profile - user_profile_model = get_user_profile_model() - if user_profile_model: - profile_serializer_class = self.get_profile_serializer_class() - serializer = profile_serializer_class(request.user.get_profile()) - else: - serializer = UserDetailsSerializer(request.user) - # Send the Return the User and its profile model with OK HTTP status - return Response(serializer.data, status=status.HTTP_200_OK) - - def post(self, request): - # Get the User object updater via this Serializer - user_profile_model = get_user_profile_model() - if user_profile_model: - profile_serializer_class = self.get_profile_update_serializer_class() - serializer = profile_serializer_class(request.user.get_profile(), - data=request.DATA, partial=True) - else: - serializer = UserUpdateSerializer(request.user, data=request.DATA, - partial=True) - - if serializer.is_valid(): - # Save UserProfileUpdateSerializer - serializer.save() - - # Return the User object with OK HTTP status - return Response(serializer.data, status=status.HTTP_200_OK) - - else: - # Return the UserProfileUpdateSerializer errors with Bad Request - # HTTP status - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + def get_object(self): + return self.request.user class PasswordReset(LoggedOutRESTAPIView, GenericAPIView): From 61cafa3cb617f89104b5018b97b1825d8c6585a1 Mon Sep 17 00:00:00 2001 From: Mateusz Sikora Date: Thu, 2 Oct 2014 11:23:52 +0200 Subject: [PATCH 06/21] fix tests for djnago 1.5 --- rest_auth/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_auth/tests.py b/rest_auth/tests.py index 908c595..ec87188 100644 --- a/rest_auth/tests.py +++ b/rest_auth/tests.py @@ -344,7 +344,7 @@ class APITestCase1(TestCase, BaseAPITestCase): # veirfy email email_confirmation = new_user.emailaddress_set.get(email=self.EMAIL)\ - .emailconfirmation_set.last() + .emailconfirmation_set.order_by('-created')[0] self.post(self.veirfy_email_url, data={"key": email_confirmation.key}, status_code=status.HTTP_200_OK) From 34d3627c7c4081c51dba9374f7c960c400c0e481 Mon Sep 17 00:00:00 2001 From: Mateusz Sikora Date: Thu, 2 Oct 2014 11:31:33 +0200 Subject: [PATCH 07/21] remove unused serializers.py form registration app --- rest_auth/registration/serializers.py | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 rest_auth/registration/serializers.py diff --git a/rest_auth/registration/serializers.py b/rest_auth/registration/serializers.py deleted file mode 100644 index f041da8..0000000 --- a/rest_auth/registration/serializers.py +++ /dev/null @@ -1,17 +0,0 @@ -from rest_framework import serializers -from django.contrib.auth import get_user_model - - -class UserRegistrationSerializer(serializers.ModelSerializer): - - """ - Serializer for Django User model and most of its fields. - """ - - class Meta: - model = get_user_model() - fields = ('username', 'password', 'email', 'first_name', 'last_name') - - -class VerifyEmailSerializer(serializers.Serializer): - pass From ff9fd1c3c1440c7f05c03eb42094079bb6717f16 Mon Sep 17 00:00:00 2001 From: Mateusz Sikora Date: Thu, 2 Oct 2014 11:40:07 +0200 Subject: [PATCH 08/21] cleanup tests --- rest_auth/tests.py | 75 +++++++++++++++++++--------------------------- rest_auth/views.py | 11 +------ 2 files changed, 32 insertions(+), 54 deletions(-) diff --git a/rest_auth/tests.py b/rest_auth/tests.py index ec87188..d2677d2 100644 --- a/rest_auth/tests.py +++ b/rest_auth/tests.py @@ -125,7 +125,6 @@ class APITestCase1(TestCase, BaseAPITestCase): PASS = 'person' EMAIL = "person1@world.com" NEW_PASS = 'new-test-pass' - PROFILE_MODEL = 'rest_auth.UserProfile' REGISTRATION_VIEW = 'rest_auth.runtests.RegistrationView' # data without user profile @@ -149,21 +148,36 @@ class APITestCase1(TestCase, BaseAPITestCase): def setUp(self): self.init() self.login_url = reverse('rest_login') + self.logout_url = reverse('rest_logout') self.password_change_url = reverse('rest_password_change') self.register_url = reverse('rest_register') self.password_reset_url = reverse('rest_password_reset') self.user_url = reverse('rest_user_details') self.veirfy_email_url = reverse('verify_email') - setattr(settings, 'REST_PROFILE_MODULE', self.PROFILE_MODEL) - self.user_profile_model = None - if self.PROFILE_MODEL: - self.user_profile_model = _resolve_model(self.PROFILE_MODEL) + def _login(self): + payload = { + "username": self.USERNAME, + "password": self.PASS + } + self.post(self.login_url, data=payload, status_code=status.HTTP_200_OK) - if self.REGISTRATION_VIEW: - setattr(settings, 'REST_REGISTRATION_BACKEND', self.REGISTRATION_VIEW) - elif hasattr(settings, 'REST_REGISTRATION_BACKEND'): - delattr(settings, 'REST_REGISTRATION_BACKEND') + def _logout(self): + self.post(self.logout_url, status=status.HTTP_200_OK) + + def _generate_uid_and_token(self, user): + result = {} + from django.utils.encoding import force_bytes + from django.contrib.auth.tokens import default_token_generator + from django import VERSION + if VERSION[1] == 6: + from django.utils.http import urlsafe_base64_encode + result['uid'] = urlsafe_base64_encode(force_bytes(user.pk)) + elif VERSION[1] == 5: + from django.utils.http import int_to_base36 + result['uid'] = int_to_base36(user.pk) + result['token'] = default_token_generator.make_token(user) + return result def test_login(self): payload = { @@ -242,7 +256,7 @@ class APITestCase1(TestCase, BaseAPITestCase): self.post(self.password_reset_url, data=payload) self.assertEqual(len(mail.outbox), mail_count + 1) - url_kwargs = self.generate_uid_and_token(user) + url_kwargs = self._generate_uid_and_token(user) data = { 'new_password1': self.NEW_PASS, @@ -259,8 +273,6 @@ class APITestCase1(TestCase, BaseAPITestCase): def test_user_details(self): user = User.objects.create_user(self.USERNAME, self.EMAIL, self.PASS) - if self.user_profile_model: - self.user_profile_model.objects.create(user=user) payload = { "username": self.USERNAME, "password": self.PASS @@ -271,32 +283,9 @@ class APITestCase1(TestCase, BaseAPITestCase): self.patch(self.user_url, data=self.BASIC_USER_DATA, status_code=200) user = User.objects.get(pk=user.pk) - - if self.user_profile_model: - self.post(self.user_url, data=self.USER_DATA, status_code=200) - user = User.objects.get(pk=user.pk) - self.assertEqual(user.first_name, self.response.json['user']['first_name']) - self.assertEqual(user.last_name, self.response.json['user']['last_name']) - self.assertEqual(user.email, self.response.json['user']['email']) - self.assertIn('newsletter_subscribe', self.response.json) - else: - self.assertEqual(user.first_name, self.response.json['first_name']) - self.assertEqual(user.last_name, self.response.json['last_name']) - self.assertEqual(user.email, self.response.json['email']) - - def generate_uid_and_token(self, user): - result = {} - from django.utils.encoding import force_bytes - from django.contrib.auth.tokens import default_token_generator - from django import VERSION - if VERSION[1] == 6: - from django.utils.http import urlsafe_base64_encode - result['uid'] = urlsafe_base64_encode(force_bytes(user.pk)) - elif VERSION[1] == 5: - from django.utils.http import int_to_base36 - result['uid'] = int_to_base36(user.pk) - result['token'] = default_token_generator.make_token(user) - return result + self.assertEqual(user.first_name, self.response.json['first_name']) + self.assertEqual(user.last_name, self.response.json['last_name']) + self.assertEqual(user.email, self.response.json['email']) def test_registration(self): user_count = User.objects.all().count() @@ -309,11 +298,8 @@ class APITestCase1(TestCase, BaseAPITestCase): new_user = get_user_model().objects.latest('id') self.assertEqual(new_user.username, self.REGISTRATION_DATA['username']) - payload = { - "username": self.USERNAME, - "password": self.PASS - } - self.post(self.login_url, data=payload, status_code=200) + self._login() + self._logout() @override_settings( ACCOUNT_EMAIL_VERIFICATION='mandatory', @@ -349,4 +335,5 @@ class APITestCase1(TestCase, BaseAPITestCase): status_code=status.HTTP_200_OK) # try to login again - self.post(self.login_url, data=payload, status_code=status.HTTP_200_OK) + self._login() + self._logout() diff --git a/rest_auth/views.py b/rest_auth/views.py index 7ab19af..2ea9707 100644 --- a/rest_auth/views.py +++ b/rest_auth/views.py @@ -1,5 +1,5 @@ from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm -from django.contrib.auth import authenticate, login, logout, get_user_model +from django.contrib.auth import login, logout, get_user_model from django.contrib.auth.tokens import default_token_generator try: from django.utils.http import urlsafe_base64_decode as uid_decoder @@ -12,7 +12,6 @@ from rest_framework import status from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.generics import GenericAPIView -from rest_framework.serializers import _resolve_model from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.authentication import SessionAuthentication, \ TokenAuthentication @@ -23,14 +22,6 @@ from rest_auth.serializers import (TokenSerializer, UserDetailsSerializer, LoginSerializer, SetPasswordSerializer, PasswordResetSerializer) -def get_user_profile_model(): - # Get the UserProfile model from the setting value - user_profile_path = getattr(settings, 'REST_PROFILE_MODULE', None) - if user_profile_path: - setattr(settings, 'AUTH_PROFILE_MODULE', user_profile_path) - return _resolve_model(user_profile_path) - - class LoggedInRESTAPIView(APIView): authentication_classes = ((SessionAuthentication, TokenAuthentication)) permission_classes = ((IsAuthenticated,)) From d4bc3a29c7736220dd764b8d528f104f6cfb0ca3 Mon Sep 17 00:00:00 2001 From: Mateusz Sikora Date: Thu, 2 Oct 2014 11:47:44 +0200 Subject: [PATCH 09/21] add django 1.7 and python 3.4 to travis.yml --- .travis.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 29d7152..b71e71d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,9 +2,11 @@ language: python python: - "2.6" - "2.7" + - "3.4" env: - - DJANGO=1.5.8 - - DJANGO=1.6.5 + - DJANGO=1.5.10 + - DJANGO=1.6.7 + - DJANGO=1.7 install: - pip install -q Django==$DJANGO --use-mirrors - pip install coveralls From f0cd45f7be2c21fabad0c2ce52b857347d5dda90 Mon Sep 17 00:00:00 2001 From: Mateusz Sikora Date: Thu, 2 Oct 2014 12:39:47 +0200 Subject: [PATCH 10/21] fix tests for django 1.7 --- rest_auth/runtests.py | 5 ++++- rest_auth/test_settings.py | 12 ++++++++++++ rest_auth/tests.py | 8 ++++---- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/rest_auth/runtests.py b/rest_auth/runtests.py index 3d7e413..380670e 100644 --- a/rest_auth/runtests.py +++ b/rest_auth/runtests.py @@ -1,4 +1,4 @@ -#This file mainly exists to allow python setup.py test to work. +# This file mainly exists to allow python setup.py test to work. import os import sys @@ -6,6 +6,7 @@ os.environ['DJANGO_SETTINGS_MODULE'] = 'test_settings' test_dir = os.path.dirname(__file__) sys.path.insert(0, test_dir) +import django from django.test.utils import get_runner from django.conf import settings @@ -13,6 +14,8 @@ from django.conf import settings def runtests(): TestRunner = get_runner(settings) test_runner = TestRunner(verbosity=1, interactive=True) + if hasattr(django, 'setup'): + django.setup() failures = test_runner.run_tests(['rest_auth']) sys.exit(bool(failures)) diff --git a/rest_auth/test_settings.py b/rest_auth/test_settings.py index 175f989..3d5e270 100644 --- a/rest_auth/test_settings.py +++ b/rest_auth/test_settings.py @@ -28,6 +28,14 @@ if django.VERSION[:2] >= (1, 3): else: DATABASE_ENGINE = 'sqlite3' +MIDDLEWARE_CLASSES = [ + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware' +] + INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', @@ -51,3 +59,7 @@ INSTALLED_APPS = [ SECRET_KEY = "38dh*skf8sjfhs287dh&^hd8&3hdg*j2&sd" ACCOUNT_ACTIVATION_DAYS = 1 SITE_ID = 1 + +MIGRATION_MODULES = { + 'authtoken': 'authtoken.migrations', +} diff --git a/rest_auth/tests.py b/rest_auth/tests.py index d2677d2..b0905cd 100644 --- a/rest_auth/tests.py +++ b/rest_auth/tests.py @@ -170,12 +170,12 @@ class APITestCase1(TestCase, BaseAPITestCase): from django.utils.encoding import force_bytes from django.contrib.auth.tokens import default_token_generator from django import VERSION - if VERSION[1] == 6: - from django.utils.http import urlsafe_base64_encode - result['uid'] = urlsafe_base64_encode(force_bytes(user.pk)) - elif VERSION[1] == 5: + if VERSION[1] == 5: from django.utils.http import int_to_base36 result['uid'] = int_to_base36(user.pk) + else: + from django.utils.http import urlsafe_base64_encode + result['uid'] = urlsafe_base64_encode(force_bytes(user.pk)) result['token'] = default_token_generator.make_token(user) return result From ad1d18936766dab57b513944a30bce1239786aec Mon Sep 17 00:00:00 2001 From: Mateusz Sikora Date: Thu, 2 Oct 2014 13:14:09 +0200 Subject: [PATCH 11/21] update travis config file --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b71e71d..ca708bd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,11 +2,14 @@ language: python python: - "2.6" - "2.7" - - "3.4" env: - DJANGO=1.5.10 - DJANGO=1.6.7 - DJANGO=1.7 +matrix: + exclude: + - python: "2.6" + env: DJANGO=1.7 install: - pip install -q Django==$DJANGO --use-mirrors - pip install coveralls From 85688940dfb2caf672af348bed987b7bd2f4e678 Mon Sep 17 00:00:00 2001 From: Mateusz Sikora Date: Thu, 2 Oct 2014 16:54:55 +0200 Subject: [PATCH 12/21] create base view and serializer for social authentication --- rest_auth/registration/serializers.py | 43 +++++++++++++++++++++++++++ rest_auth/registration/views.py | 15 ++++++++++ rest_auth/views.py | 6 ++-- 3 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 rest_auth/registration/serializers.py diff --git a/rest_auth/registration/serializers.py b/rest_auth/registration/serializers.py new file mode 100644 index 0000000..09d1c4e --- /dev/null +++ b/rest_auth/registration/serializers.py @@ -0,0 +1,43 @@ +from rest_framework import serializers +from requests.exceptions import HTTPError +from allauth.socialaccount.helpers import complete_social_login + + +class SocialLoginSerializer(serializers.Serializer): + + access_token = serializers.CharField(required=True) + + def validate_access_token(self, attrs, source): + access_token = attrs[source] + + view = self.context.get('view') + request = self.context.get('request') + + if not view: + raise serializers.ValidationError('View is not defined, pass it as\ + a context variable') + self.adapter_class = getattr(view, 'adapter_class', None) + + if not self.adapter_class: + raise serializers.ValidationError('Define adapter_class in view') + + self.adapter = self.adapter_class() + app = self.adapter.get_provider().get_app(request) + token = self.adapter.parse_token({'access_token': access_token}) + token.app = app + + try: + login = self.adapter.complete_login(request, app, token, + response=access_token) + token.account = login.account + login.token = token + complete_social_login(request, login) + except HTTPError: + raise serializers.ValidationError('Incorrect value') + + if not login.is_existing: + login.lookup() + login.save(request, connect=True) + self.object = {'user': login.account.user} + + return attrs diff --git a/rest_auth/registration/views.py b/rest_auth/registration/views.py index 8225bcb..6ffa776 100644 --- a/rest_auth/registration/views.py +++ b/rest_auth/registration/views.py @@ -8,6 +8,8 @@ from allauth.account.utils import complete_signup from allauth.account import app_settings from rest_auth.serializers import UserDetailsSerializer +from rest_auth.registration.serializers import SocialLoginSerializer +from rest_auth.views import Login class Register(APIView, SignupView): @@ -49,3 +51,16 @@ class VerifyEmail(APIView, ConfirmEmailView): confirmation = self.get_object() confirmation.confirm(self.request) return Response({'message': 'ok'}, status=status.HTTP_200_OK) + + +class SocialLogin(Login): + """ + class used for social authentications + example usage for facebook + + from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter + class FacebookLogin(SocialLogin): + adapter_class = FacebookOAuth2Adapter + """ + + serializer_class = SocialLoginSerializer diff --git a/rest_auth/views.py b/rest_auth/views.py index 2ea9707..24e2922 100644 --- a/rest_auth/views.py +++ b/rest_auth/views.py @@ -48,13 +48,13 @@ class Login(LoggedOutRESTAPIView, GenericAPIView): response_serializer = TokenSerializer def get_serializer(self): - return self.serializer_class(data=self.request.DATA) + return self.serializer_class(data=self.request.DATA, + context={'request': self.request, 'view': self}) def login(self): self.user = self.serializer.object['user'] self.token, created = self.token_model.objects.get_or_create( user=self.user) - if getattr(settings, 'REST_SESSION_LOGIN', True): login(self.request, self.user) @@ -244,3 +244,5 @@ class PasswordChange(LoggedInRESTAPIView, GenericAPIView): else: return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + From a7be2d178b8dc7252eca52af06c1dacb14b95e92 Mon Sep 17 00:00:00 2001 From: Mateusz Sikora Date: Tue, 7 Oct 2014 15:08:08 +0200 Subject: [PATCH 13/21] password reset and password change refactoring --- rest_auth/serializers.py | 104 +++++++++++++++++++++++++++----- rest_auth/tests.py | 9 +-- rest_auth/urls.py | 2 +- rest_auth/views.py | 125 ++++++++------------------------------- 4 files changed, 120 insertions(+), 120 deletions(-) diff --git a/rest_auth/serializers.py b/rest_auth/serializers.py index 8cc1105..aaa3d75 100644 --- a/rest_auth/serializers.py +++ b/rest_auth/serializers.py @@ -1,5 +1,12 @@ from django.contrib.auth import get_user_model from django.conf import settings +from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm +try: + from django.utils.http import urlsafe_base64_decode as uid_decoder +except: + # make compatible with django 1.5 + from django.utils.http import base36_to_int as uid_decoder +from django.contrib.auth.tokens import default_token_generator from rest_framework import serializers from rest_framework.authtoken.models import Token @@ -43,20 +50,6 @@ class UserDetailsSerializer(serializers.ModelSerializer): read_only_fields = ('id', 'last_login', 'is_active', 'date_joined') -class SetPasswordSerializer(serializers.Serializer): - - """ - Serializer for changing Django User password. - """ - - new_password1 = serializers.CharField(max_length=128) - new_password2 = serializers.CharField(max_length=128) - - def __init__(self, *args, **kwargs): - self.user = kwargs.pop('user', None) - return super(SetPasswordSerializer, self).__init__(*args, **kwargs) - - class PasswordResetSerializer(serializers.Serializer): """ @@ -64,3 +57,86 @@ class PasswordResetSerializer(serializers.Serializer): """ email = serializers.EmailField() + + password_reset_form_class = PasswordResetForm + + def validate_email(self, attrs, source): + # Create PasswordResetForm with the serializer + self.reset_form = self.password_reset_form_class(data=attrs) + if not self.reset_form.is_valid(): + raise serializers.ValidationError('Error') + return attrs + + def save(self): + request = self.context.get('request') + # Set some values to trigger the send_email method. + opts = { + 'use_https': request.is_secure(), + 'from_email': getattr(settings, 'DEFAULT_FROM_EMAIL'), + 'request': request, + } + self.reset_form.save(**opts) + + +class PasswordResetConfirmSerializer(serializers.Serializer): + + """ + Serializer for requesting a password reset e-mail. + """ + + new_password1 = serializers.CharField(max_length=128) + new_password2 = serializers.CharField(max_length=128) + + uid = serializers.CharField(required=True) + token = serializers.CharField(required=True) + + set_password_form_class = SetPasswordForm + + def custom_validation(self, attrs): + pass + + def validate(self, attrs): + self._errors = {} + # Get the UserModel + UserModel = get_user_model() + # Decode the uidb64 to uid to get User object + try: + uid = uid_decoder(attrs['uid']) + self.user = UserModel._default_manager.get(pk=uid) + except (TypeError, ValueError, OverflowError, UserModel.DoesNotExist): + self._errors['uid'] = ['Invalid value'] + + self.custom_validation(attrs) + + # Construct SetPasswordForm instance + self.set_password_form = self.set_password_form_class(user=self.user, + data=attrs) + if not self.set_password_form.is_valid(): + self._errors['token'] = ['Invalid value'] + + if not default_token_generator.check_token(self.user, attrs['token']): + self._errors['token'] = ['Invalid value'] + + def save(self): + self.set_password_form.save() + + +class PasswordChangeSerializer(serializers.Serializer): + + new_password1 = serializers.CharField(max_length=128) + new_password2 = serializers.CharField(max_length=128) + + set_password_form_class = SetPasswordForm + + def validate(self, attrs): + request = self.context.get('request') + self.set_password_form = self.set_password_form_class(user=request.user, + data=attrs) + + if not self.set_password_form.is_valid(): + self._errors = self.set_password_form.errors + return None + return attrs + + def save(self): + self.set_password_form.save() diff --git a/rest_auth/tests.py b/rest_auth/tests.py index b0905cd..587bca4 100644 --- a/rest_auth/tests.py +++ b/rest_auth/tests.py @@ -253,16 +253,17 @@ class APITestCase1(TestCase, BaseAPITestCase): # call password reset mail_count = len(mail.outbox) payload = {'email': self.EMAIL} - self.post(self.password_reset_url, data=payload) + self.post(self.password_reset_url, data=payload, status_code=200) self.assertEqual(len(mail.outbox), mail_count + 1) url_kwargs = self._generate_uid_and_token(user) - data = { 'new_password1': self.NEW_PASS, - 'new_password2': self.NEW_PASS + 'new_password2': self.NEW_PASS, + 'uid': url_kwargs['uid'], + 'token': url_kwargs['token'] } - url = reverse('rest_password_reset_confirm', kwargs=url_kwargs) + url = reverse('rest_password_reset_confirm') self.post(url, data=data, status_code=200) payload = { diff --git a/rest_auth/urls.py b/rest_auth/urls.py index 701351f..e1907d9 100644 --- a/rest_auth/urls.py +++ b/rest_auth/urls.py @@ -10,7 +10,7 @@ urlpatterns = patterns('rest_auth.views', # URLs that do not require a session or valid token url(r'^password/reset/$', PasswordReset.as_view(), name='rest_password_reset'), - url(r'^password/reset/confirm/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', + url(r'^password/reset/confirm/$', PasswordResetConfirm.as_view( ), name='rest_password_reset_confirm'), url(r'^login/$', Login.as_view(), name='rest_login'), diff --git a/rest_auth/views.py b/rest_auth/views.py index 24e2922..e814773 100644 --- a/rest_auth/views.py +++ b/rest_auth/views.py @@ -1,11 +1,4 @@ -from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm -from django.contrib.auth import login, logout, get_user_model -from django.contrib.auth.tokens import default_token_generator -try: - from django.utils.http import urlsafe_base64_decode as uid_decoder -except: - # make compatible with django 1.5 - from django.utils.http import base36_to_int as uid_decoder +from django.contrib.auth import login, logout from django.conf import settings from rest_framework import status @@ -19,7 +12,8 @@ from rest_framework.authtoken.models import Token from rest_framework.generics import RetrieveUpdateAPIView from rest_auth.serializers import (TokenSerializer, UserDetailsSerializer, - LoginSerializer, SetPasswordSerializer, PasswordResetSerializer) + LoginSerializer, PasswordResetSerializer, PasswordResetConfirmSerializer, + PasswordChangeSerializer) class LoggedInRESTAPIView(APIView): @@ -122,38 +116,18 @@ class PasswordReset(LoggedOutRESTAPIView, GenericAPIView): """ serializer_class = PasswordResetSerializer - password_reset_form_class = PasswordResetForm - def post(self, request): + def post(self, request, *args, **kwargs): # Create a serializer with request.DATA - serializer = self.serializer_class(data=request.DATA) + serializer = self.get_serializer(data=request.DATA) - if serializer.is_valid(): - # Create PasswordResetForm with the serializer - reset_form = self.password_reset_form_class(data=serializer.data) - - if reset_form.is_valid(): - # Sett some values to trigger the send_email method. - opts = { - 'use_https': request.is_secure(), - 'from_email': getattr(settings, 'DEFAULT_FROM_EMAIL'), - 'request': request, - } - - reset_form.save(**opts) - - # Return the success message with OK HTTP status - return Response( - {"success": "Password reset e-mail has been sent."}, - status=status.HTTP_200_OK) - - else: - return Response(reset_form._errors, - status=status.HTTP_400_BAD_REQUEST) - - else: + if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + serializer.save() + # Return the success message with OK HTTP status + return Response({"success": "Password reset e-mail has been sent."}, + status=status.HTTP_200_OK) class PasswordResetConfirm(LoggedOutRESTAPIView, GenericAPIView): @@ -166,49 +140,15 @@ class PasswordResetConfirm(LoggedOutRESTAPIView, GenericAPIView): Returns the success/fail message. """ - serializer_class = SetPasswordSerializer + serializer_class = PasswordResetConfirmSerializer - def post(self, request, uid=None, token=None): - # Get the UserModel - UserModel = get_user_model() - - # Decode the uidb64 to uid to get User object - try: - uid = uid_decoder(uid) - user = UserModel._default_manager.get(pk=uid) - except (TypeError, ValueError, OverflowError, UserModel.DoesNotExist): - user = None - - # If we get the User object - if user: - serializer = self.serializer_class(data=request.DATA, user=user) - - if serializer.is_valid(): - # Construct SetPasswordForm instance - form = SetPasswordForm(user=user, data=serializer.data) - - if form.is_valid(): - if default_token_generator.check_token(user, token): - form.save() - - # Return the success message with OK HTTP status - return Response( - {"success": - "Password has been reset with the new password."}, - status=status.HTTP_200_OK) - else: - return Response( - {"error": "Invalid password reset token."}, - status=status.HTTP_400_BAD_REQUEST) - else: - return Response(form._errors, status=status.HTTP_400_BAD_REQUEST) - - else: - return Response(serializer.errors, - status=status.HTTP_400_BAD_REQUEST) - - else: - return Response({"errors": "Couldn\'t find the user from uid."}, status=status.HTTP_400_BAD_REQUEST) + def post(self, request): + serializer = self.get_serializer(data=request.DATA) + if not serializer.is_valid(): + return Response(serializer.errors, + status=status.HTTP_400_BAD_REQUEST) + serializer.save() + return Response({"success": "Password has been reset with the new password."}) class PasswordChange(LoggedInRESTAPIView, GenericAPIView): @@ -220,29 +160,12 @@ class PasswordChange(LoggedInRESTAPIView, GenericAPIView): Returns the success/fail message. """ - serializer_class = SetPasswordSerializer + serializer_class = PasswordChangeSerializer def post(self, request): - # Create a serializer with request.DATA - serializer = self.serializer_class(data=request.DATA) - - if serializer.is_valid(): - # Construct the SetPasswordForm instance - form = SetPasswordForm(user=request.user, data=serializer.data) - - if form.is_valid(): - form.save() - - # Return the success message with OK HTTP status - return Response({"success": "New password has been saved."}, - status=status.HTTP_200_OK) - - else: - return Response(form._errors, - status=status.HTTP_400_BAD_REQUEST) - - else: + serializer = self.get_serializer(data=request.DATA) + if not serializer.is_valid(): return Response(serializer.errors, - status=status.HTTP_400_BAD_REQUEST) - - + status=status.HTTP_400_BAD_REQUEST) + serializer.save() + return Response({"success": "New password has been saved."}) From f134d8b1d610cb5326a1a92bdc3e72355614c6b2 Mon Sep 17 00:00:00 2001 From: Mateusz Sikora Date: Wed, 8 Oct 2014 10:29:22 +0200 Subject: [PATCH 14/21] remove unused utils.py --- rest_auth/utils.py | 43 ------------------------------------------- 1 file changed, 43 deletions(-) delete mode 100644 rest_auth/utils.py diff --git a/rest_auth/utils.py b/rest_auth/utils.py deleted file mode 100644 index 4b76312..0000000 --- a/rest_auth/utils.py +++ /dev/null @@ -1,43 +0,0 @@ -from django.utils.crypto import get_random_string - - -HASH_CHARACTERS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' - - -def generate_new_hash_with_length(length): - """ - Generates a random string with the alphanumerical character set and given length. - """ - return get_random_string(length, HASH_CHARACTERS) - - -# Based on http://stackoverflow.com/a/547867. Thanks! Credit goes to you! -def construct_modules_and_import(name): - """ - Grab the Python string to import - """ - - # Get all the components by dot notations - components = name.split('.') - module = '' - i = 1 - - # Construct the partial Python string except the last package name - for comp in components: - if i < len(components): - module += str(comp) - - if i < (len(components) - 1): - module += '.' - - i += 1 - - # Import the module from above python string - mod = __import__(module) - - # Import the component recursivcely - for comp in components[1:]: - mod = getattr(mod, comp) - - # Return the imported module's class - return mod From 40880817077efeffc6b2f7f95ad758ecca2d9308 Mon Sep 17 00:00:00 2001 From: Mateusz Sikora Date: Wed, 8 Oct 2014 12:19:15 +0200 Subject: [PATCH 15/21] add social medial authentication tests, separate test urls --- rest_auth/test_settings.py | 14 +++++++++++ rest_auth/test_urls.py | 22 +++++++++++++++++ rest_auth/tests.py | 48 +++++++++++++++++++++++++++++++++++- rest_auth/urls.py | 50 ++++++++++++-------------------------- setup.py | 1 + 5 files changed, 100 insertions(+), 35 deletions(-) create mode 100644 rest_auth/test_urls.py diff --git a/rest_auth/test_settings.py b/rest_auth/test_settings.py index 3d5e270..2fcc1e3 100644 --- a/rest_auth/test_settings.py +++ b/rest_auth/test_settings.py @@ -36,6 +36,18 @@ MIDDLEWARE_CLASSES = [ 'django.contrib.messages.middleware.MessageMiddleware' ] +TEMPLATE_CONTEXT_PROCESSORS = [ + 'django.contrib.auth.context_processors.auth', + 'django.core.context_processors.debug', + 'django.core.context_processors.media', + 'django.core.context_processors.request', + 'django.contrib.messages.context_processors.messages', + 'django.core.context_processors.static', + + "allauth.account.context_processors.account", + "allauth.socialaccount.context_processors.socialaccount", +] + INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', @@ -48,6 +60,8 @@ INSTALLED_APPS = [ 'allauth', 'allauth.account', + 'allauth.socialaccount', + 'allauth.socialaccount.providers.facebook', 'rest_framework', 'rest_framework.authtoken', diff --git a/rest_auth/test_urls.py b/rest_auth/test_urls.py new file mode 100644 index 0000000..fb730a3 --- /dev/null +++ b/rest_auth/test_urls.py @@ -0,0 +1,22 @@ +from django.conf.urls import patterns, url, include +from django.views.generic import TemplateView +from django.contrib.auth.tests import urls + +from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter + +from .urls import urlpatterns +from .registration.views import SocialLogin + + +class FacebookLogin(SocialLogin): + adapter_class = FacebookOAuth2Adapter + +urlpatterns += patterns('', + url(r'^rest-registration/', include('registration.urls')), + url(r'^test-admin/', include(urls)), + url(r'^account-email-verification-sent/$', TemplateView.as_view(), + name='account_email_verification_sent'), + url(r'^account-confirm-email/(?P\w+)/$', TemplateView.as_view(), + name='account_confirm_email'), + url(r'^social-login/facebook/$', FacebookLogin.as_view(), name='fb_login') +) diff --git a/rest_auth/tests.py b/rest_auth/tests.py index 587bca4..b18f3ee 100644 --- a/rest_auth/tests.py +++ b/rest_auth/tests.py @@ -9,8 +9,11 @@ from django.contrib.auth.models import User from django.contrib.auth import get_user_model from django.core import mail from django.test.utils import override_settings +from django.contrib.sites.models import Site + +from allauth.socialaccount.models import SocialApp +import responses -from rest_framework.serializers import _resolve_model from rest_framework import status @@ -121,6 +124,8 @@ class APITestCase1(TestCase, BaseAPITestCase): - custom registration: backend defined """ + urls = 'rest_auth.test_urls' + USERNAME = 'person' PASS = 'person' EMAIL = "person1@world.com" @@ -338,3 +343,44 @@ class APITestCase1(TestCase, BaseAPITestCase): # try to login again self._login() self._logout() + + +class TestSocialAuth(TestCase, BaseAPITestCase): + + urls = 'rest_auth.test_urls' + + def setUp(self): + social_app = SocialApp.objects.create( + provider='facebook', + name='Facebook', + client_id='123123123', + secret='321321321', + ) + site = Site.objects.get_current() + social_app.sites.add(site) + self.fb_login_url = reverse('fb_login') + + @responses.activate + def test_failed_social_auth(self): + # fake response + responses.add(responses.GET, 'https://graph.facebook.com/me', + body='', status=400, content_type='application/json') + + payload = { + 'access_token': 'abc123' + } + self.post(self.fb_login_url, data=payload, status_code=400) + + @responses.activate + def test_social_auth(self): + # fake response for facebook call + resp_body = '{"id":"123123123123","first_name":"John","gender":"male","last_name":"Smith","link":"https:\\/\\/www.facebook.com\\/john.smith","locale":"en_US","name":"John Smith","timezone":2,"updated_time":"2014-08-13T10:14:38+0000","username":"john.smith","verified":true}' + responses.add(responses.GET, 'https://graph.facebook.com/me', + body=resp_body, status=200, content_type='application/json') + + payload = { + 'access_token': 'abc123' + } + + self.post(self.fb_login_url, data=payload, status_code=200) + self.assertIn('key', self.response.json.keys()) diff --git a/rest_auth/urls.py b/rest_auth/urls.py index e1907d9..64f3bb2 100644 --- a/rest_auth/urls.py +++ b/rest_auth/urls.py @@ -1,36 +1,18 @@ -from django.conf import settings -from django.conf.urls import patterns, url, include -from django.views.generic import TemplateView +from django.conf.urls import patterns, url -from rest_auth.views import Login, Logout, UserDetails, \ - PasswordChange, PasswordReset, PasswordResetConfirm +from rest_auth.views import (Login, Logout, UserDetails, PasswordChange, + PasswordReset, PasswordResetConfirm) - -urlpatterns = patterns('rest_auth.views', - # URLs that do not require a session or valid token - url(r'^password/reset/$', PasswordReset.as_view(), - name='rest_password_reset'), - url(r'^password/reset/confirm/$', - PasswordResetConfirm.as_view( - ), name='rest_password_reset_confirm'), - url(r'^login/$', Login.as_view(), name='rest_login'), - - # URLs that require a user to be logged in with a valid - # session / token. - url(r'^logout/$', Logout.as_view(), name='rest_logout'), - url(r'^user/$', UserDetails.as_view(), - name='rest_user_details'), - url(r'^password/change/$', PasswordChange.as_view(), - name='rest_password_change'), - ) - -if getattr(settings, 'IS_TEST', False): - from django.contrib.auth.tests import urls - urlpatterns += patterns('', - url(r'^rest-registration/', include('registration.urls')), - url(r'^test-admin/', include(urls)), - url(r'^account-email-verification-sent/$', TemplateView.as_view(), - name='account_email_verification_sent'), - url(r'^account-confirm-email/(?P\w+)/$', TemplateView.as_view(), - name='account_confirm_email'), - ) +urlpatterns = patterns('', + # URLs that do not require a session or valid token + url(r'^password/reset/$', PasswordReset.as_view(), + name='rest_password_reset'), + url(r'^password/reset/confirm/$', PasswordResetConfirm.as_view(), + name='rest_password_reset_confirm'), + url(r'^login/$', Login.as_view(), name='rest_login'), + # URLs that require a user to be logged in with a valid session / token. + url(r'^logout/$', Logout.as_view(), name='rest_logout'), + url(r'^user/$', UserDetails.as_view(), name='rest_user_details'), + url(r'^password/change/$', PasswordChange.as_view(), + name='rest_password_change'), +) diff --git a/setup.py b/setup.py index 8bbfb04..1677ccf 100644 --- a/setup.py +++ b/setup.py @@ -31,6 +31,7 @@ setup( 'Django>=1.5.0', 'django-allauth>=0.18.0', 'djangorestframework>=2.3.13', + 'responses>=0.2.2' ], test_suite='rest_auth.runtests.runtests', include_package_data=True, From 489bac6e1f7eb24da5004bffd0fd7a54fc445f16 Mon Sep 17 00:00:00 2001 From: Mateusz Sikora Date: Wed, 8 Oct 2014 12:26:32 +0200 Subject: [PATCH 16/21] comment out unused part of code in tests --- rest_auth/tests.py | 60 ++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/rest_auth/tests.py b/rest_auth/tests.py index b18f3ee..65671a0 100644 --- a/rest_auth/tests.py +++ b/rest_auth/tests.py @@ -26,16 +26,16 @@ class APIClient(Client): return self.generic('OPTIONS', path, data, content_type, **extra) -class CustomJSONEncoder(json.JSONEncoder): - """ - Convert datetime/date objects into isoformat - """ +# class CustomJSONEncoder(json.JSONEncoder): +# """ +# Convert datetime/date objects into isoformat +# """ - def default(self, obj): - if isinstance(obj, (datetime, date, time)): - return obj.isoformat() - else: - return super(CustomJSONEncoder, self).default(obj) +# def default(self, obj): +# if isinstance(obj, (datetime, date, time)): +# return obj.isoformat() +# else: +# return super(CustomJSONEncoder, self).default(obj) class BaseAPITestCase(object): @@ -52,7 +52,7 @@ class BaseAPITestCase(object): kwargs['content_type'] = 'application/json' if 'data' in kwargs and request_method != 'get' and kwargs['content_type'] == 'application/json': data = kwargs.get('data', '') - kwargs['data'] = json.dumps(data, cls=CustomJSONEncoder) + kwargs['data'] = json.dumps(data) # , cls=CustomJSONEncoder if 'status_code' in kwargs: status_code = kwargs.pop('status_code') @@ -60,10 +60,6 @@ class BaseAPITestCase(object): if hasattr(self, 'token'): kwargs['HTTP_AUTHORIZATION'] = 'Token %s' % self.token - if hasattr(self, 'company_token'): - kwargs[ - 'HTTP_AUTHORIZATION'] = 'Company-Token %s' % self.company_token - self.response = request_func(*args, **kwargs) is_json = bool( filter(lambda x: 'json' in x, self.response._headers['content-type'])) @@ -84,28 +80,28 @@ class BaseAPITestCase(object): def patch(self, *args, **kwargs): return self.send_request('patch', *args, **kwargs) - def put(self, *args, **kwargs): - return self.send_request('put', *args, **kwargs) + # def put(self, *args, **kwargs): + # return self.send_request('put', *args, **kwargs) - def delete(self, *args, **kwargs): - return self.send_request('delete', *args, **kwargs) + # def delete(self, *args, **kwargs): + # return self.send_request('delete', *args, **kwargs) - def options(self, *args, **kwargs): - return self.send_request('options', *args, **kwargs) + # def options(self, *args, **kwargs): + # return self.send_request('options', *args, **kwargs) - def post_file(self, *args, **kwargs): - kwargs['content_type'] = MULTIPART_CONTENT - return self.send_request('post', *args, **kwargs) + # def post_file(self, *args, **kwargs): + # kwargs['content_type'] = MULTIPART_CONTENT + # return self.send_request('post', *args, **kwargs) - def get_file(self, *args, **kwargs): - content_type = None - if 'content_type' in kwargs: - content_type = kwargs.pop('content_type') - response = self.send_request('get', *args, **kwargs) - if content_type: - self.assertEqual( - bool(filter(lambda x: content_type in x, response._headers['content-type'])), True) - return response + # def get_file(self, *args, **kwargs): + # content_type = None + # if 'content_type' in kwargs: + # content_type = kwargs.pop('content_type') + # response = self.send_request('get', *args, **kwargs) + # if content_type: + # self.assertEqual( + # bool(filter(lambda x: content_type in x, response._headers['content-type'])), True) + # return response def init(self): settings.DEBUG = True From 2b5942c4acbe0aa2210b4e4a787235f678570280 Mon Sep 17 00:00:00 2001 From: Mateusz Sikora Date: Wed, 8 Oct 2014 13:19:34 +0200 Subject: [PATCH 17/21] custom serializers settings --- rest_auth/app_settings.py | 41 +++++++++++++++++++++++++++++++++++++++ rest_auth/views.py | 2 +- 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 rest_auth/app_settings.py diff --git a/rest_auth/app_settings.py b/rest_auth/app_settings.py new file mode 100644 index 0000000..dda56e0 --- /dev/null +++ b/rest_auth/app_settings.py @@ -0,0 +1,41 @@ +from django.conf import settings + +from rest_auth.serializers import ( + TokenSerializer as DefaultTokenSerializer, + UserDetailsSerializer as DefaultUserDetailsSerializer, + LoginSerializer as DefaultLoginSerializer, + PasswordResetSerializer as DefaultPasswordResetSerializer, + PasswordResetConfirmSerializer as DefaultPasswordResetConfirmSerializer, + PasswordChangeSerializer as DefaultPasswordChangeSerializer) +from allauth.utils import import_callable + + +serializers = getattr(settings, 'REST_AUTH_SERIALIZERS', {}) + +TokenSerializer = import_callable( + serializers.get('TOKEN_SERIALIZER', DefaultTokenSerializer)) + +UserDetailsSerializer = import_callable( + serializers.get('USER_DETAILS_SERIALIZER', DefaultUserDetailsSerializer) +) + +LoginSerializer = import_callable( + serializers.get('LOGIN_SERIALIZER', DefaultLoginSerializer) +) + +PasswordResetSerializer = import_callable( + serializers.get('PASSWORD_RESET_SERIALIZER', + DefaultPasswordResetSerializer) +) + +PasswordResetConfirmSerializer = import_callable( + serializers.get('PASSWORD_RESET_CONFIRM_SERIALIZER', + DefaultPasswordResetConfirmSerializer) +) + +PasswordChangeSerializer = import_callable( + serializers.get('PASSWORD_RESET_SERIALIZER', + DefaultPasswordChangeSerializer) +) + + diff --git a/rest_auth/views.py b/rest_auth/views.py index e814773..c9f2214 100644 --- a/rest_auth/views.py +++ b/rest_auth/views.py @@ -11,7 +11,7 @@ from rest_framework.authentication import SessionAuthentication, \ from rest_framework.authtoken.models import Token from rest_framework.generics import RetrieveUpdateAPIView -from rest_auth.serializers import (TokenSerializer, UserDetailsSerializer, +from app_settings import (TokenSerializer, UserDetailsSerializer, LoginSerializer, PasswordResetSerializer, PasswordResetConfirmSerializer, PasswordChangeSerializer) From 6ecb6a784723048dbdc36082ffecee284f49c10e Mon Sep 17 00:00:00 2001 From: Mateusz Sikora Date: Thu, 9 Oct 2014 10:49:20 +0200 Subject: [PATCH 18/21] fix setting name --- rest_auth/app_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_auth/app_settings.py b/rest_auth/app_settings.py index dda56e0..d77b163 100644 --- a/rest_auth/app_settings.py +++ b/rest_auth/app_settings.py @@ -34,7 +34,7 @@ PasswordResetConfirmSerializer = import_callable( ) PasswordChangeSerializer = import_callable( - serializers.get('PASSWORD_RESET_SERIALIZER', + serializers.get('PASSWORD_CHANGE_SERIALIZER', DefaultPasswordChangeSerializer) ) From 989f3fa7af85cb30784fc7b215def318526a4567 Mon Sep 17 00:00:00 2001 From: Mateusz Sikora Date: Thu, 9 Oct 2014 11:41:52 +0200 Subject: [PATCH 19/21] minor fixes --- .gitignore | 1 + rest_auth/app_settings.py | 2 +- rest_auth/registration/serializers.py | 4 ++-- rest_auth/serializers.py | 4 +--- rest_auth/utils.py | 11 +++++++++++ 5 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 rest_auth/utils.py diff --git a/.gitignore b/.gitignore index 51cbe85..6e5e982 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,4 @@ coverage.xml # Sphinx documentation docs/_build/ +.DS_Store diff --git a/rest_auth/app_settings.py b/rest_auth/app_settings.py index d77b163..349864d 100644 --- a/rest_auth/app_settings.py +++ b/rest_auth/app_settings.py @@ -7,7 +7,7 @@ from rest_auth.serializers import ( PasswordResetSerializer as DefaultPasswordResetSerializer, PasswordResetConfirmSerializer as DefaultPasswordResetConfirmSerializer, PasswordChangeSerializer as DefaultPasswordChangeSerializer) -from allauth.utils import import_callable +from .utils import import_callable serializers = getattr(settings, 'REST_AUTH_SERIALIZERS', {}) diff --git a/rest_auth/registration/serializers.py b/rest_auth/registration/serializers.py index 09d1c4e..7a403a0 100644 --- a/rest_auth/registration/serializers.py +++ b/rest_auth/registration/serializers.py @@ -14,8 +14,8 @@ class SocialLoginSerializer(serializers.Serializer): request = self.context.get('request') if not view: - raise serializers.ValidationError('View is not defined, pass it as\ - a context variable') + raise serializers.ValidationError('View is not defined, pass it ' + + 'as a context variable') self.adapter_class = getattr(view, 'adapter_class', None) if not self.adapter_class: diff --git a/rest_auth/serializers.py b/rest_auth/serializers.py index aaa3d75..0fdc5f4 100644 --- a/rest_auth/serializers.py +++ b/rest_auth/serializers.py @@ -45,9 +45,7 @@ class UserDetailsSerializer(serializers.ModelSerializer): """ class Meta: model = get_user_model() - exclude = ('password', 'groups', 'user_permissions', 'is_staff', - 'is_superuser') - read_only_fields = ('id', 'last_login', 'is_active', 'date_joined') + fields = ('username', 'email', 'first_name', 'last_name') class PasswordResetSerializer(serializers.Serializer): diff --git a/rest_auth/utils.py b/rest_auth/utils.py new file mode 100644 index 0000000..eb61bd7 --- /dev/null +++ b/rest_auth/utils.py @@ -0,0 +1,11 @@ +from django.utils.importlib import import_module + + +def import_callable(path_or_callable): + if hasattr(path_or_callable, '__call__'): + return path_or_callable + else: + assert isinstance(path_or_callable, (str, unicode)) + package, attr = path_or_callable.rsplit('.', 1) + return getattr(import_module(package), attr) + From 4c9abe80436b1a3b3e49845af475153604febe0e Mon Sep 17 00:00:00 2001 From: Mateusz Sikora Date: Thu, 9 Oct 2014 11:51:33 +0200 Subject: [PATCH 20/21] move django-allauth and responses from setup.py to test_requirements.pip --- .travis.yml | 1 + setup.py | 4 +--- test_requirements.pip | 2 ++ 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 test_requirements.pip diff --git a/.travis.yml b/.travis.yml index ca708bd..a85dec1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,3 +17,4 @@ script: - coverage run --source=rest_auth setup.py test after_success: - coveralls +install: "pip install -r test_requirements.pip" diff --git a/setup.py b/setup.py index 1677ccf..883d53c 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ except ImportError: import os here = os.path.dirname(os.path.abspath(__file__)) -f = open(os.path.join(here, 'README.md')) +f = open(os.path.join(here, 'README.md')) long_description = f.read().strip() f.close() @@ -29,9 +29,7 @@ setup( zip_safe=False, install_requires=[ 'Django>=1.5.0', - 'django-allauth>=0.18.0', 'djangorestframework>=2.3.13', - 'responses>=0.2.2' ], test_suite='rest_auth.runtests.runtests', include_package_data=True, diff --git a/test_requirements.pip b/test_requirements.pip new file mode 100644 index 0000000..667775b --- /dev/null +++ b/test_requirements.pip @@ -0,0 +1,2 @@ +django-allauth>=0.18.0 +responses>=0.2.2 From 43257ec308d823f2821f88219b4642e0adac3e20 Mon Sep 17 00:00:00 2001 From: Mateusz Sikora Date: Thu, 9 Oct 2014 12:24:26 +0200 Subject: [PATCH 21/21] fix travis config file --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a85dec1..88dc35e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,8 +13,8 @@ matrix: install: - pip install -q Django==$DJANGO --use-mirrors - pip install coveralls + - pip install -r test_requirements.pip script: - coverage run --source=rest_auth setup.py test after_success: - coveralls -install: "pip install -r test_requirements.pip"