Merge pull request #3 from Tivix/master

get new updates
This commit is contained in:
Egor 2016-02-16 15:41:33 +03:00
commit 2acf4dd115
30 changed files with 317 additions and 149 deletions

1
.gitignore vendored
View File

@ -33,6 +33,7 @@ htmlcov/
.cache .cache
nosetests.xml nosetests.xml
coverage.xml coverage.xml
coverage_html
# Translations # Translations
*.mo *.mo

View File

@ -106,9 +106,9 @@ TEMPLATE_DIRS = [os.path.join(BASE_DIR, 'templates')]
REST_SESSION_LOGIN = False REST_SESSION_LOGIN = False
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
SITE_ID = 1 SITE_ID = 1
ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_EMAIL_REQUIRED = False
ACCOUNT_AUTHENTICATION_METHOD = 'username' ACCOUNT_AUTHENTICATION_METHOD = 'username'
ACCOUNT_EMAIL_VERIFICATION = 'mandatory' ACCOUNT_EMAIL_VERIFICATION = 'optional'
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': ( 'DEFAULT_AUTHENTICATION_CLASSES': (

View File

@ -1,8 +1,8 @@
from django.conf.urls import patterns, include, url from django.conf.urls import include, url
from django.contrib import admin from django.contrib import admin
from django.views.generic import TemplateView, RedirectView from django.views.generic import TemplateView, RedirectView
urlpatterns = patterns('', urlpatterns = [
url(r'^$', TemplateView.as_view(template_name="home.html"), name='home'), url(r'^$', TemplateView.as_view(template_name="home.html"), name='home'),
url(r'^signup/$', TemplateView.as_view(template_name="signup.html"), url(r'^signup/$', TemplateView.as_view(template_name="signup.html"),
name='signup'), name='signup'),
@ -36,4 +36,4 @@ urlpatterns = patterns('',
url(r'^account/', include('allauth.urls')), url(r'^account/', include('allauth.urls')),
url(r'^admin/', include(admin.site.urls)), url(r'^admin/', include(admin.site.urls)),
url(r'^accounts/profile/$', RedirectView.as_view(url='/', permanent=True), name='profile-redirect'), url(r'^accounts/profile/$', RedirectView.as_view(url='/', permanent=True), name='profile-redirect'),
) ]

View File

@ -1,5 +1,5 @@
<!-- Signup form --> <!-- Signup form -->
<form class="form-horizontal ajax-post" role="form" action="{% url 'rest_verify_email' %}"> <form class="form-horizontal ajax-post" role="form" action="{% url 'rest_verify_email' %}">{% csrf_token %}
<div class="form-group"> <div class="form-group">
<label for="key" class="col-sm-2 control-label">Key</label> <label for="key" class="col-sm-2 control-label">Key</label>
<div class="col-sm-10"> <div class="col-sm-10">

View File

@ -1,5 +1,5 @@
<!-- Signup form --> <!-- Signup form -->
<form class="form-horizontal ajax-post" role="form" action="{% url 'rest_login' %}"> <form class="form-horizontal ajax-post" role="form" action="{% url 'rest_login' %}">{% csrf_token %}
<div class="form-group"> <div class="form-group">
<label for="username" class="col-sm-2 control-label">Username</label> <label for="username" class="col-sm-2 control-label">Username</label>
<div class="col-sm-10"> <div class="col-sm-10">

View File

@ -1,6 +1,5 @@
<!-- Signup form --> <!-- Signup form -->
<form class="form-horizontal ajax-post" role="form" action="{% url 'rest_password_change' %}"> <form class="form-horizontal ajax-post" role="form" action="{% url 'rest_password_change' %}">{% csrf_token %}
<div class="form-group"> <div class="form-group">
<label for="new_password1" class="col-sm-2 control-label">Password</label> <label for="new_password1" class="col-sm-2 control-label">Password</label>
<div class="col-sm-10"> <div class="col-sm-10">

View File

@ -1,5 +1,5 @@
<!-- Signup form --> <!-- Signup form -->
<form class="form-horizontal ajax-post" role="form" action="{% url 'rest_password_reset_confirm' %}"> <form class="form-horizontal ajax-post" role="form" action="{% url 'rest_password_reset_confirm' %}">{% csrf_token %}
<div class="form-group"> <div class="form-group">
<label for="uid" class="col-sm-2 control-label">Uid</label> <label for="uid" class="col-sm-2 control-label">Uid</label>
<div class="col-sm-10"> <div class="col-sm-10">

View File

@ -1,5 +1,5 @@
<!-- Signup form --> <!-- Signup form -->
<form class="form-horizontal ajax-post" role="form" action="{% url 'rest_password_reset' %}"> <form class="form-horizontal ajax-post" role="form" action="{% url 'rest_password_reset' %}">{% csrf_token %}
<div class="form-group"> <div class="form-group">
<label for="email" class="col-sm-2 control-label">E-mail</label> <label for="email" class="col-sm-2 control-label">E-mail</label>
<div class="col-sm-10"> <div class="col-sm-10">

View File

@ -1,5 +1,5 @@
<!-- Signup form --> <!-- Signup form -->
<form class="form-horizontal ajax-post" id="signup" role="form" action="{% url 'rest_register' %}"> <form class="form-horizontal ajax-post" id="signup" role="form" action="{% url 'rest_register' %}">{% csrf_token %}
<div class="form-group"> <div class="form-group">
<label for="email" class="col-sm-2 control-label">Email</label> <label for="email" class="col-sm-2 control-label">Email</label>
<div class="col-sm-10"> <div class="col-sm-10">

View File

@ -1,5 +1,5 @@
<!-- Signup form --> <!-- Signup form -->
<form class="form-horizontal" id="signup" role="form" action="{% url 'rest_user_details' %}"> <form class="form-horizontal" id="signup" role="form" action="{% url 'rest_user_details' %}">{% csrf_token %}
<div class="form-group"> <div class="form-group">
<label for="email" class="col-sm-2 control-label">Email</label> <label for="email" class="col-sm-2 control-label">Email</label>

View File

@ -7,6 +7,7 @@ Basic
- /rest-auth/login/ (POST) - /rest-auth/login/ (POST)
- username (string) - username (string)
- email (string)
- password (string) - password (string)
@ -55,16 +56,6 @@ Registration
- password2 - password2
- email - email
.. note:: This endpoint is based on ``allauth.account.views.SignupView`` and uses the same form as in this view. To override fields you have to create custom Signup Form and define it in django settings:
.. code-block:: python
ACCOUNT_FORMS = {
'signup': 'path.to.custom.SignupForm'
}
See allauth documentation for more details.
- /rest-auth/registration/verify-email/ (POST) - /rest-auth/registration/verify-email/ (POST)
- key - key

View File

@ -29,10 +29,19 @@ Configuration
... ...
} }
- **REST_AUTH_REGISTRATION_SERIALIZERS**
You can define your custom serializers for registration endpoint.
Possible key values:
- REGISTER_SERIALIZER - serializer class in ``rest_auth.register.views.RegisterView``, default value ``rest_auth.register.serializers.RegisterSerializer``
- **REST_AUTH_TOKEN_MODEL** - model class for tokens, default value ``rest_framework.authtoken.models``
- **REST_AUTH_TOKEN_CREATOR** - callable to create tokens, default value ``rest_auth.utils.default_create_token``.
- **REST_SESSION_LOGIN** - Enable session login in Login API view (default: True) - **REST_SESSION_LOGIN** - Enable session login in Login API view (default: True)
- **OLD_PASSWORD_FIELD_ENABLED** - set it to True if you want to have old password verification on password change enpoint (default: False) - **OLD_PASSWORD_FIELD_ENABLED** - set it to True if you want to have old password verification on password change enpoint (default: False)
- **LOGOUT_ON_PASSWORD_CHANGE** - set to False if you want to keep the current user logged in after a password change - **LOGOUT_ON_PASSWORD_CHANGE** - set to False if you want to keep the current user logged in after a password change

View File

@ -11,7 +11,7 @@ Do these steps to make it running (ideally in virtualenv).
git clone https://github.com/Tivix/django-rest-auth.git git clone https://github.com/Tivix/django-rest-auth.git
cd django-rest-auth/demo/ cd django-rest-auth/demo/
pip install -r requirements.pip pip install -r requirements.pip
python manage.py syncdb --settings=demo.settings --noinput python manage.py migrate --settings=demo.settings --noinput
python manage.py runserver --settings=demo.settings python manage.py runserver --settings=demo.settings
Now, go to ``http://127.0.0.1:8000/`` in your browser. Now, go to ``http://127.0.0.1:8000/`` in your browser.

View File

@ -17,7 +17,12 @@ FAQ
djang-allauth https://github.com/pennersr/django-allauth/blob/master/allauth/account/views.py#L190 djang-allauth https://github.com/pennersr/django-allauth/blob/master/allauth/account/views.py#L190
2. How can I update UserProfile assigned to User model? 2. I get an error: Reverse for 'password_reset_confirm' not found.
You need to add `password_reset_confirm` url into your ``urls.py`` (at the top of any other included urls). Please check the ``urls.py`` module inside demo app example for more details.
3. How can I update UserProfile assigned to User model?
Assuming you already have UserProfile model defined like this Assuming you already have UserProfile model defined like this

View File

@ -38,7 +38,7 @@ You're good to go now!
Registration (optional) Registration (optional)
----------------------- -----------------------
1. If you want to enable standard registration process you will need to install ``django-allauth`` - see this doc for installation http://django-allauth.readthedocs.org/en/latest/installation.html. 1. If you want to enable standard registration process you will need to install ``django-allauth`` by using ``pip install django-rest-auth[extras]`` or ``pip install django-rest-auth[with_social]``.
2. Add ``allauth``, ``allauth.account`` and ``rest_auth.registration`` apps to INSTALLED_APPS in your django settings.py: 2. Add ``allauth``, ``allauth.account`` and ``rest_auth.registration`` apps to INSTALLED_APPS in your django settings.py:

View File

@ -7,8 +7,10 @@ from rest_auth.serializers import (
PasswordResetSerializer as DefaultPasswordResetSerializer, PasswordResetSerializer as DefaultPasswordResetSerializer,
PasswordResetConfirmSerializer as DefaultPasswordResetConfirmSerializer, PasswordResetConfirmSerializer as DefaultPasswordResetConfirmSerializer,
PasswordChangeSerializer as DefaultPasswordChangeSerializer) PasswordChangeSerializer as DefaultPasswordChangeSerializer)
from .utils import import_callable from .utils import import_callable, default_create_token
create_token = import_callable(
getattr(settings, 'REST_AUTH_TOKEN_CREATOR', default_create_token))
serializers = getattr(settings, 'REST_AUTH_SERIALIZERS', {}) serializers = getattr(settings, 'REST_AUTH_SERIALIZERS', {})

View File

@ -1,3 +1,10 @@
# from django.db import models from django.conf import settings
from rest_framework.authtoken.models import Token as DefaultTokenModel
from .utils import import_callable
# Register your models here. # Register your models here.
TokenModel = import_callable(
getattr(settings, 'REST_AUTH_TOKEN_MODEL', DefaultTokenModel))

View File

@ -0,0 +1,11 @@
from django.conf import settings
from rest_auth.registration.serializers import (
RegisterSerializer as DefaultRegisterSerializer)
from ..utils import import_callable
serializers = getattr(settings, 'REST_AUTH_REGISTER_SERIALIZERS', {})
RegisterSerializer = import_callable(
serializers.get('REGISTER_SERIALIZER', DefaultRegisterSerializer))

View File

@ -1,17 +1,26 @@
from django.http import HttpRequest from django.http import HttpRequest
from django.conf import settings from django.conf import settings
try:
from allauth.account import app_settings as allauth_settings
from allauth.utils import (email_address_exists,
get_username_max_length)
from allauth.account.adapter import get_adapter
from allauth.account.utils import setup_user_email
except ImportError:
raise ImportError('allauth needs to be added to INSTALLED_APPS.')
from rest_framework import serializers from rest_framework import serializers
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
# Import is needed only if we are using social login, in which # Import is needed only if we are using social login, in which
# case the allauth.socialaccount will be declared # case the allauth.socialaccount will be declared
try:
from allauth.socialaccount.helpers import complete_social_login
except ImportError:
raise ImportError('allauth.socialaccount needs to be installed.')
if 'allauth.socialaccount' not in settings.INSTALLED_APPS: if 'allauth.socialaccount' in settings.INSTALLED_APPS:
raise ImportError('allauth.socialaccount needs to be added to INSTALLED_APPS.') try:
from allauth.socialaccount.helpers import complete_social_login
except ImportError:
pass
class SocialLoginSerializer(serializers.Serializer): class SocialLoginSerializer(serializers.Serializer):
@ -109,3 +118,57 @@ class SocialLoginSerializer(serializers.Serializer):
attrs['user'] = login.account.user attrs['user'] = login.account.user
return attrs return attrs
class RegisterSerializer(serializers.Serializer):
username = serializers.CharField(
max_length=get_username_max_length(),
min_length=allauth_settings.USERNAME_MIN_LENGTH,
required=allauth_settings.USERNAME_REQUIRED
)
email = serializers.EmailField(required=allauth_settings.EMAIL_REQUIRED)
password1 = serializers.CharField(required=True, write_only=True)
password2 = serializers.CharField(required=True, write_only=True)
def validate_username(self, username):
username = get_adapter().clean_username(username)
return username
def validate_email(self, email):
email = get_adapter().clean_email(email)
if allauth_settings.UNIQUE_EMAIL:
if email and email_address_exists(email):
raise serializers.ValidationError(
"A user is already registered with this e-mail address.")
return email
def validate_password1(self, password):
return get_adapter().clean_password(password)
def validate(self, data):
if data['password1'] != data['password2']:
raise serializers.ValidationError("The two password fields didn't match.")
return data
def custom_signup(self, request, user):
pass
def get_cleaned_data(self):
return {
'username': self.validated_data.get('username', ''),
'password1': self.validated_data.get('password1', ''),
'email': self.validated_data.get('email', '')
}
def save(self, request):
adapter = get_adapter()
user = adapter.new_user(request)
self.cleaned_data = self.get_cleaned_data()
adapter.save_user(request, user, self)
self.custom_signup(request, user)
setup_user_email(request, user, [])
return user
class VerifyEmailSerializer(serializers.Serializer):
key = serializers.CharField()

View File

@ -1,10 +1,9 @@
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django.conf.urls import patterns, url from django.conf.urls import url
from .views import RegisterView, VerifyEmailView from .views import RegisterView, VerifyEmailView
urlpatterns = patterns( urlpatterns = [
'',
url(r'^$', RegisterView.as_view(), name='rest_register'), url(r'^$', RegisterView.as_view(), name='rest_register'),
url(r'^verify-email/$', VerifyEmailView.as_view(), name='rest_verify_email'), url(r'^verify-email/$', VerifyEmailView.as_view(), name='rest_verify_email'),
@ -21,4 +20,4 @@ urlpatterns = patterns(
# djang-allauth https://github.com/pennersr/django-allauth/blob/master/allauth/account/views.py#L190 # djang-allauth https://github.com/pennersr/django-allauth/blob/master/allauth/account/views.py#L190
url(r'^account-confirm-email/(?P<key>\w+)/$', TemplateView.as_view(), url(r'^account-confirm-email/(?P<key>\w+)/$', TemplateView.as_view(),
name='account_confirm_email'), name='account_confirm_email'),
) ]

View File

@ -1,77 +1,50 @@
from django.http import HttpRequest
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.generics import CreateAPIView
from rest_framework import status from rest_framework import status
from rest_framework.authtoken.models import Token from rest_framework.exceptions import MethodNotAllowed
from allauth.account.views import SignupView, ConfirmEmailView from allauth.account.views import ConfirmEmailView
from allauth.account.utils import complete_signup from allauth.account.utils import complete_signup
from allauth.account import app_settings from allauth.account import app_settings as allauth_settings
from rest_auth.app_settings import TokenSerializer from rest_auth.app_settings import (TokenSerializer,
from rest_auth.registration.serializers import SocialLoginSerializer create_token)
from rest_auth.registration.serializers import (SocialLoginSerializer,
VerifyEmailSerializer)
from rest_auth.views import LoginView from rest_auth.views import LoginView
from rest_auth.models import TokenModel
from .app_settings import RegisterSerializer
class RegisterView(APIView, SignupView): class RegisterView(CreateAPIView):
""" serializer_class = RegisterSerializer
Accepts the credentials and creates a new user permission_classes = (AllowAny, )
if user does not exist already token_model = TokenModel
Return the REST Token if the credentials are valid and authenticated.
Calls allauth complete_signup method
Accept the following POST parameters: username, email, password def get_response_data(self, user):
Return the REST Framework Token Object's key. if allauth_settings.EMAIL_VERIFICATION == \
""" allauth_settings.EmailVerificationMethod.MANDATORY:
return {}
permission_classes = (AllowAny,) return TokenSerializer(user.auth_token).data
allowed_methods = ('POST', 'OPTIONS', 'HEAD')
token_model = Token
serializer_class = TokenSerializer
def get(self, *args, **kwargs): def create(self, request, *args, **kwargs):
return Response({}, status=status.HTTP_405_METHOD_NOT_ALLOWED) serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
def put(self, *args, **kwargs): return Response(self.get_response_data(user), status=status.HTTP_201_CREATED, headers=headers)
return Response({}, status=status.HTTP_405_METHOD_NOT_ALLOWED)
def form_valid(self, form): def perform_create(self, serializer):
self.user = form.save(self.request) user = serializer.save(self.request)
self.token, created = self.token_model.objects.get_or_create( create_token(self.token_model, user, serializer)
user=self.user complete_signup(self.request._request, user,
) allauth_settings.EMAIL_VERIFICATION,
if isinstance(self.request, HttpRequest): None)
request = self.request return user
else:
request = self.request._request
return complete_signup(request, self.user,
app_settings.EMAIL_VERIFICATION,
self.get_success_url())
def get_form_kwargs(self, *args, **kwargs):
kwargs = super(RegisterView, self).get_form_kwargs(*args, **kwargs)
kwargs['data'] = self.request.data
return kwargs
def post(self, request, *args, **kwargs):
self.initial = {}
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)
serializer = self.serializer_class(instance=self.token,
context={'request': self.request})
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 VerifyEmailView(APIView, ConfirmEmailView): class VerifyEmailView(APIView, ConfirmEmailView):
@ -80,10 +53,12 @@ class VerifyEmailView(APIView, ConfirmEmailView):
allowed_methods = ('POST', 'OPTIONS', 'HEAD') allowed_methods = ('POST', 'OPTIONS', 'HEAD')
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
return Response({}, status=status.HTTP_405_METHOD_NOT_ALLOWED) raise MethodNotAllowed('GET')
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
self.kwargs['key'] = self.request.data.get('key', '') serializer = VerifyEmailSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.kwargs['key'] = serializer.validated_data['key']
confirmation = self.get_object() confirmation = self.get_object()
confirmation.confirm(self.request) confirmation.confirm(self.request)
return Response({'message': 'ok'}, status=status.HTTP_200_OK) return Response({'message': 'ok'}, status=status.HTTP_200_OK)

View File

@ -6,8 +6,9 @@ from django.utils.http import urlsafe_base64_decode as uid_decoder
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import force_text from django.utils.encoding import force_text
from .models import TokenModel
from rest_framework import serializers, exceptions from rest_framework import serializers, exceptions
from rest_framework.authtoken.models import Token
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
# Get the UserModel # Get the UserModel
@ -19,43 +20,73 @@ class LoginSerializer(serializers.Serializer):
email = serializers.EmailField(required=False, allow_blank=True) email = serializers.EmailField(required=False, allow_blank=True)
password = serializers.CharField(style={'input_type': 'password'}) password = serializers.CharField(style={'input_type': 'password'})
def _validate_email(self, email, password):
user = None
if email and password:
user = authenticate(email=email, password=password)
else:
msg = _('Must include "email" and "password".')
raise exceptions.ValidationError(msg)
return user
def _validate_username(self, username, password):
user = None
if username and password:
user = authenticate(username=username, password=password)
else:
msg = _('Must include "username" and "password".')
raise exceptions.ValidationError(msg)
return user
def _validate_username_email(self, username, email, password):
user = None
if email and password:
user = authenticate(email=email, password=password)
elif username and password:
user = authenticate(username=username, password=password)
else:
msg = _('Must include either "username" or "email" and "password".')
raise exceptions.ValidationError(msg)
return user
def validate(self, attrs): def validate(self, attrs):
username = attrs.get('username') username = attrs.get('username')
email = attrs.get('email') email = attrs.get('email')
password = attrs.get('password') password = attrs.get('password')
user = None
if 'allauth' in settings.INSTALLED_APPS: if 'allauth' in settings.INSTALLED_APPS:
from allauth.account import app_settings from allauth.account import app_settings
# Authentication through email # Authentication through email
if app_settings.AUTHENTICATION_METHOD == app_settings.AuthenticationMethod.EMAIL: if app_settings.AUTHENTICATION_METHOD == app_settings.AuthenticationMethod.EMAIL:
if email and password: user = self._validate_email(email, password)
user = authenticate(email=email, password=password)
else:
msg = _('Must include "email" and "password".')
raise exceptions.ValidationError(msg)
# Authentication through username # Authentication through username
elif app_settings.AUTHENTICATION_METHOD == app_settings.AuthenticationMethod.USERNAME: if app_settings.AUTHENTICATION_METHOD == app_settings.AuthenticationMethod.USERNAME:
if username and password: user = self._validate_username(username, password)
user = authenticate(username=username, password=password)
else:
msg = _('Must include "username" and "password".')
raise exceptions.ValidationError(msg)
# Authentication through either username or email # Authentication through either username or email
else: else:
if email and password: user = self._validate_username_email(username, email, password)
user = authenticate(email=email, password=password)
elif username and password:
user = authenticate(username=username, password=password)
else:
msg = _('Must include either "username" or "email" and "password".')
raise exceptions.ValidationError(msg)
elif username and password:
user = authenticate(username=username, password=password)
else: else:
msg = _('Must include "username" and "password".') # Authentication without using allauth
raise exceptions.ValidationError(msg) if email:
try:
username = UserModel.objects.get(email__iexact=email).username
except UserModel.DoesNotExist:
pass
if username:
user = self._validate_username_email(username, '', password)
# Did we get back an active user? # Did we get back an active user?
if user: if user:
@ -84,7 +115,7 @@ class TokenSerializer(serializers.ModelSerializer):
""" """
class Meta: class Meta:
model = Token model = TokenModel
fields = ('key',) fields = ('key',)
@ -109,15 +140,17 @@ class PasswordResetSerializer(serializers.Serializer):
password_reset_form_class = PasswordResetForm password_reset_form_class = PasswordResetForm
def get_email_options(self):
""" Override this method to change default e-mail options
"""
return {}
def validate_email(self, value): def validate_email(self, value):
# Create PasswordResetForm with the serializer # Create PasswordResetForm with the serializer
self.reset_form = self.password_reset_form_class(data=self.initial_data) self.reset_form = self.password_reset_form_class(data=self.initial_data)
if not self.reset_form.is_valid(): if not self.reset_form.is_valid():
raise serializers.ValidationError(_('Error')) raise serializers.ValidationError(_('Error'))
if not UserModel.objects.filter(email__iexact=value).exists():
raise serializers.ValidationError(_('Invalid e-mail address'))
return value return value
def save(self): def save(self):
@ -128,6 +161,8 @@ class PasswordResetSerializer(serializers.Serializer):
'from_email': getattr(settings, 'DEFAULT_FROM_EMAIL'), 'from_email': getattr(settings, 'DEFAULT_FROM_EMAIL'),
'request': request, 'request': request,
} }
opts.update(self.get_email_options())
self.reset_form.save(**opts) self.reset_form.save(**opts)

View File

@ -70,7 +70,3 @@ INSTALLED_APPS = [
SECRET_KEY = "38dh*skf8sjfhs287dh&^hd8&3hdg*j2&sd" SECRET_KEY = "38dh*skf8sjfhs287dh&^hd8&3hdg*j2&sd"
ACCOUNT_ACTIVATION_DAYS = 1 ACCOUNT_ACTIVATION_DAYS = 1
SITE_ID = 1 SITE_ID = 1
MIGRATION_MODULES = {
'authtoken': 'authtoken.migrations',
}

View File

@ -2,6 +2,7 @@ from django.core.urlresolvers import reverse
from django.test import TestCase from django.test import TestCase
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core import mail from django.core import mail
from django.conf import settings
from django.test.utils import override_settings from django.test.utils import override_settings
from django.utils.encoding import force_text from django.utils.encoding import force_text
@ -90,6 +91,51 @@ class APITestCase1(TestCase, BaseAPITestCase):
# test empty payload # test empty payload
self.post(self.login_url, data={}, status_code=400) self.post(self.login_url, data={}, status_code=400)
def test_login_by_email(self):
# starting test without allauth app
settings.INSTALLED_APPS.remove('allauth')
payload = {
"email": self.EMAIL.lower(),
"password": self.PASS
}
# 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)
# create user
user = get_user_model().objects.create_user(self.USERNAME, self.EMAIL, self.PASS)
# test auth by email
self.post(self.login_url, data=payload, status_code=200)
self.assertEqual('key' in self.response.json.keys(), True)
self.token = self.response.json['key']
# test auth by email in different case
payload = {
"email": self.EMAIL.upper(),
"password": self.PASS
}
self.post(self.login_url, data=payload, status_code=200)
self.assertEqual('key' in self.response.json.keys(), True)
self.token = self.response.json['key']
# test inactive user
user.is_active = False
user.save()
self.post(self.login_url, data=payload, status_code=400)
# test wrong email/password
payload = {
"email": 't' + self.EMAIL,
"password": self.PASS
}
self.post(self.login_url, data=payload, status_code=400)
# test empty payload
self.post(self.login_url, data={}, status_code=400)
def test_password_change(self): def test_password_change(self):
login_payload = { login_payload = {
"username": self.USERNAME, "username": self.USERNAME,
@ -224,13 +270,25 @@ class APITestCase1(TestCase, BaseAPITestCase):
} }
self.post(self.login_url, data=payload, status_code=200) self.post(self.login_url, data=payload, status_code=200)
def test_password_reset_with_email_in_different_case(self):
get_user_model().objects.create_user(self.USERNAME, self.EMAIL.lower(), self.PASS)
# call password reset in upper case
mail_count = len(mail.outbox)
payload = {'email': self.EMAIL.upper()}
self.post(self.password_reset_url, data=payload, status_code=200)
self.assertEqual(len(mail.outbox), mail_count + 1)
def test_password_reset_with_invalid_email(self): def test_password_reset_with_invalid_email(self):
"""
Invalid email should not raise error, as this would leak users
"""
get_user_model().objects.create_user(self.USERNAME, self.EMAIL, self.PASS) get_user_model().objects.create_user(self.USERNAME, self.EMAIL, self.PASS)
# call password reset # call password reset
mail_count = len(mail.outbox) mail_count = len(mail.outbox)
payload = {'email': 'nonexisting@email.com'} payload = {'email': 'nonexisting@email.com'}
self.post(self.password_reset_url, data=payload, status_code=400) self.post(self.password_reset_url, data=payload, status_code=200)
self.assertEqual(len(mail.outbox), mail_count) self.assertEqual(len(mail.outbox), mail_count)
def test_user_details(self): def test_user_details(self):
@ -255,14 +313,22 @@ class APITestCase1(TestCase, BaseAPITestCase):
# test empty payload # test empty payload
self.post(self.register_url, data={}, status_code=400) self.post(self.register_url, data={}, status_code=400)
self.post(self.register_url, data=self.REGISTRATION_DATA, status_code=201) result = self.post(self.register_url, data=self.REGISTRATION_DATA, status_code=201)
self.assertIn('key', result.data)
self.assertEqual(get_user_model().objects.all().count(), user_count + 1) self.assertEqual(get_user_model().objects.all().count(), user_count + 1)
new_user = get_user_model().objects.latest('id') new_user = get_user_model().objects.latest('id')
self.assertEqual(new_user.username, self.REGISTRATION_DATA['username']) self.assertEqual(new_user.username, self.REGISTRATION_DATA['username'])
self._login() self._login()
self._logout() self._logout()
def test_registration_with_invalid_password(self):
data = self.REGISTRATION_DATA.copy()
data['password2'] = 'foobar'
self.post(self.register_url, data=data, status_code=400)
@override_settings( @override_settings(
ACCOUNT_EMAIL_VERIFICATION='mandatory', ACCOUNT_EMAIL_VERIFICATION='mandatory',
ACCOUNT_EMAIL_REQUIRED=True ACCOUNT_EMAIL_REQUIRED=True
@ -278,11 +344,12 @@ class APITestCase1(TestCase, BaseAPITestCase):
status_code=status.HTTP_400_BAD_REQUEST status_code=status.HTTP_400_BAD_REQUEST
) )
self.post( result = self.post(
self.register_url, self.register_url,
data=self.REGISTRATION_DATA_WITH_EMAIL, data=self.REGISTRATION_DATA_WITH_EMAIL,
status_code=status.HTTP_201_CREATED status_code=status.HTTP_201_CREATED
) )
self.assertNotIn('key', result.data)
self.assertEqual(get_user_model().objects.all().count(), user_count + 1) self.assertEqual(get_user_model().objects.all().count(), user_count + 1)
self.assertEqual(len(mail.outbox), mail_count + 1) self.assertEqual(len(mail.outbox), mail_count + 1)
new_user = get_user_model().objects.latest('id') new_user = get_user_model().objects.latest('id')

View File

@ -99,7 +99,6 @@ class TestSocialAuth(TestCase, BaseAPITestCase):
# test empty payload # test empty payload
self.post(self.register_url, data={}, status_code=400) self.post(self.register_url, data={}, status_code=400)
self.post( self.post(
self.register_url, self.register_url,
data=self.REGISTRATION_DATA, data=self.REGISTRATION_DATA,

View File

@ -1,4 +1,4 @@
from django.conf.urls import patterns, url, include from django.conf.urls import url, include
from django.views.generic import TemplateView from django.views.generic import TemplateView
from . import django_urls from . import django_urls
@ -11,8 +11,7 @@ from rest_auth.registration.views import SocialLoginView
class FacebookLogin(SocialLoginView): class FacebookLogin(SocialLoginView):
adapter_class = FacebookOAuth2Adapter adapter_class = FacebookOAuth2Adapter
urlpatterns += patterns( urlpatterns += [
'',
url(r'^rest-registration/', include('rest_auth.registration.urls')), url(r'^rest-registration/', include('rest_auth.registration.urls')),
url(r'^test-admin/', include(django_urls)), url(r'^test-admin/', include(django_urls)),
url(r'^account-email-verification-sent/$', TemplateView.as_view(), url(r'^account-email-verification-sent/$', TemplateView.as_view(),
@ -21,4 +20,4 @@ urlpatterns += patterns(
name='account_confirm_email'), name='account_confirm_email'),
url(r'^social-login/facebook/$', FacebookLogin.as_view(), name='fb_login'), url(r'^social-login/facebook/$', FacebookLogin.as_view(), name='fb_login'),
url(r'^accounts/', include('allauth.socialaccount.urls')) url(r'^accounts/', include('allauth.socialaccount.urls'))
) ]

View File

@ -1,12 +1,11 @@
from django.conf.urls import patterns, url from django.conf.urls import url
from rest_auth.views import ( from rest_auth.views import (
LoginView, LogoutView, UserDetailsView, PasswordChangeView, LoginView, LogoutView, UserDetailsView, PasswordChangeView,
PasswordResetView, PasswordResetConfirmView PasswordResetView, PasswordResetConfirmView
) )
urlpatterns = patterns( urlpatterns = [
'',
# URLs that do not require a session or valid token # URLs that do not require a session or valid token
url(r'^password/reset/$', PasswordResetView.as_view(), url(r'^password/reset/$', PasswordResetView.as_view(),
name='rest_password_reset'), name='rest_password_reset'),
@ -18,4 +17,4 @@ urlpatterns = patterns(
url(r'^user/$', UserDetailsView.as_view(), name='rest_user_details'), url(r'^user/$', UserDetailsView.as_view(), name='rest_user_details'),
url(r'^password/change/$', PasswordChangeView.as_view(), url(r'^password/change/$', PasswordChangeView.as_view(),
name='rest_password_change'), name='rest_password_change'),
) ]

View File

@ -9,3 +9,8 @@ def import_callable(path_or_callable):
assert isinstance(path_or_callable, string_types) assert isinstance(path_or_callable, string_types)
package, attr = path_or_callable.rsplit('.', 1) package, attr = path_or_callable.rsplit('.', 1)
return getattr(import_module(package), attr) return getattr(import_module(package), attr)
def default_create_token(token_model, user, serializer):
token, _ = token_model.objects.get_or_create(user=user)
return token

View File

@ -7,14 +7,14 @@ from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.generics import GenericAPIView from rest_framework.generics import GenericAPIView
from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.authtoken.models import Token
from rest_framework.generics import RetrieveUpdateAPIView from rest_framework.generics import RetrieveUpdateAPIView
from .app_settings import ( from .app_settings import (
TokenSerializer, UserDetailsSerializer, LoginSerializer, TokenSerializer, UserDetailsSerializer, LoginSerializer,
PasswordResetSerializer, PasswordResetConfirmSerializer, PasswordResetSerializer, PasswordResetConfirmSerializer,
PasswordChangeSerializer PasswordChangeSerializer, create_token
) )
from .models import TokenModel
class LoginView(GenericAPIView): class LoginView(GenericAPIView):
@ -30,13 +30,12 @@ class LoginView(GenericAPIView):
""" """
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
serializer_class = LoginSerializer serializer_class = LoginSerializer
token_model = Token token_model = TokenModel
response_serializer = TokenSerializer response_serializer = TokenSerializer
def login(self): def login(self):
self.user = self.serializer.validated_data['user'] self.user = self.serializer.validated_data['user']
self.token, created = self.token_model.objects.get_or_create( self.token = create_token(self.token_model, self.user, self.serializer)
user=self.user)
if getattr(settings, 'REST_SESSION_LOGIN', True): if getattr(settings, 'REST_SESSION_LOGIN', True):
login(self.request, self.user) login(self.request, self.user)

View File

@ -32,6 +32,13 @@ setup(
'djangorestframework>=3.1.0', 'djangorestframework>=3.1.0',
'six>=1.9.0', 'six>=1.9.0',
], ],
extras_require={
'with_social': ['django-allauth>=0.24.1'],
},
tests_require=[
'responses>=0.5.0',
'django-allauth>=0.24.1',
],
test_suite='runtests.runtests', test_suite='runtests.runtests',
include_package_data=True, include_package_data=True,
# cmdclass={}, # cmdclass={},