Merge pull request #12 from Tivix/develop

Develop
This commit is contained in:
Mateusz Sikora 2014-10-09 12:26:42 +02:00
commit 0ac575e603
17 changed files with 586 additions and 687 deletions

1
.gitignore vendored
View File

@ -52,3 +52,4 @@ coverage.xml
# Sphinx documentation
docs/_build/
.DS_Store

View File

@ -3,11 +3,17 @@ python:
- "2.6"
- "2.7"
env:
- DJANGO=1.5.8
- DJANGO=1.6.5
- 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
- pip install -r test_requirements.pip
script:
- coverage run --source=rest_auth setup.py test
after_success:

41
rest_auth/app_settings.py Normal file
View File

@ -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 .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_CHANGE_SERIALIZER',
DefaultPasswordChangeSerializer)
)

View File

View File

@ -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

View File

@ -0,0 +1,16 @@
from django.views.generic import TemplateView
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/$', 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<key>\w+)/$', TemplateView.as_view(),
name='account_confirm_email'),
)

View File

@ -0,0 +1,66 @@
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, ConfirmEmailView
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):
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, 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)
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

View File

@ -1,60 +1,21 @@
#This file mainly exists to allow python setup.py test to work.
import os, sys
# This file mainly exists to allow python setup.py test to work.
import os
import sys
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
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():
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))

View File

@ -1,20 +1,34 @@
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.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):
class LoginSerializer(serializers.Serializer):
username = serializers.CharField(max_length=30)
password = serializers.CharField(max_length=128)
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):
"""
Serializer for Token model.
"""
@ -34,120 +48,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):
"""
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
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):
"""
@ -155,3 +55,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()

View File

@ -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'
@ -27,6 +28,26 @@ 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'
]
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',
@ -37,13 +58,22 @@ INSTALLED_APPS = [
'django.contrib.sitemaps',
'django.contrib.staticfiles',
'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth.socialaccount.providers.facebook',
'rest_framework',
'rest_framework.authtoken',
'registration',
'rest_auth',
'rest_auth.registration'
]
SECRET_KEY = "38dh*skf8sjfhs287dh&^hd8&3hdg*j2&sd"
ACCOUNT_ACTIVATION_DAYS = 1
SITE_ID = 1
MIGRATION_MODULES = {
'authtoken': 'authtoken.migrations',
}

22
rest_auth/test_urls.py Normal file
View File

@ -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<key>\w+)/$', TemplateView.as_view(),
name='account_confirm_email'),
url(r'^social-login/facebook/$', FacebookLogin.as_view(), name='fb_login')
)

View File

@ -8,9 +8,13 @@ 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 django.contrib.sites.models import Site
from registration.models import RegistrationProfile
from rest_framework.serializers import _resolve_model
from allauth.socialaccount.models import SocialApp
import responses
from rest_framework import status
class APIClient(Client):
@ -22,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):
@ -48,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')
@ -56,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']))
@ -80,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
@ -113,7 +113,6 @@ class BaseAPITestCase(object):
# -----------------------
class APITestCase1(TestCase, BaseAPITestCase):
"""
Case #1:
@ -121,23 +120,23 @@ class APITestCase1(TestCase, BaseAPITestCase):
- custom registration: backend defined
"""
urls = 'rest_auth.test_urls'
USERNAME = 'person'
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
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
REGISTRATION_DATA_WITH_EMAIL = REGISTRATION_DATA.copy()
REGISTRATION_DATA_WITH_EMAIL['email'] = EMAIL
BASIC_USER_DATA = {
'first_name': "John",
@ -150,28 +149,44 @@ 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] == 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
def test_login(self):
payload = {
"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)
@ -187,25 +202,24 @@ 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+'?',
"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)
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']
@ -217,7 +231,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']
@ -234,77 +248,23 @@ 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)
# call password reset
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)
url_kwargs = self.generate_uid_and_token(user)
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 = {
@ -315,8 +275,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
@ -325,60 +283,100 @@ 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)
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'])
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 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')
self.assertEqual(new_user.username, self.REGISTRATION_DATA['username'])
self._login()
self._logout()
@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=status.HTTP_400_BAD_REQUEST)
self.post(self.register_url, data=self.REGISTRATION_DATA_WITH_EMAIL,
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')
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)
# veirfy email
email_confirmation = new_user.emailaddress_set.get(email=self.EMAIL)\
.emailconfirmation_set.order_by('-created')[0]
self.post(self.veirfy_email_url, data={"key": email_confirmation.key},
status_code=status.HTTP_200_OK)
# try to login again
self._login()
self._logout()
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
class TestSocialAuth(TestCase, BaseAPITestCase):
urls = 'rest_auth.test_urls'
class APITestCase2(APITestCase1):
"""
Case #2:
- user profile: not defined
- custom registration backend: not defined
"""
PROFILE_MODEL = None
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')
class APITestCase3(APITestCase1):
"""
Case #3:
- user profile: defined
- custom registration backend: not defined
"""
REGISTRATION_VIEW = None
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')
class APITestCase4(APITestCase1):
"""
Case #4:
- user profile: not defined
- custom registration backend: not defined
"""
PROFILE_MODEL = None
REGISTRATION_VIEW = None
payload = {
'access_token': 'abc123'
}
self.post(self.fb_login_url, data=payload, status_code=200)
self.assertIn('key', self.response.json.keys())

View File

@ -1,32 +1,18 @@
from django.conf import settings
from django.conf.urls import patterns, url, include
from django.conf.urls import patterns, url
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<uid>[0-9A-Za-z_\-]+)/(?P<token>[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<activation_key>\w+)/$',
VerifyEmail.as_view(), name='verify_email'),
# 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'^test-admin/', include(urls)))
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'),
)

View File

@ -1,43 +1,11 @@
from django.utils.crypto import get_random_string
from django.utils.importlib import import_module
HASH_CHARACTERS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
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)
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

View File

@ -1,53 +1,19 @@
from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm
from django.contrib.auth import authenticate, 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 django.core.exceptions import ObjectDoesNotExist
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
from rest_framework.authtoken.models import Token
from rest_framework.generics import RetrieveUpdateAPIView
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,
SetPasswordSerializer, PasswordResetSerializer, UserUpdateSerializer,
get_user_registration_profile_serializer, get_user_profile_serializer,
get_user_profile_update_serializer)
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)
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)
from app_settings import (TokenSerializer, UserDetailsSerializer,
LoginSerializer, PasswordResetSerializer, PasswordResetConfirmSerializer,
PasswordChangeSerializer)
class LoggedInRESTAPIView(APIView):
@ -73,35 +39,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,
context={'request': self.request, 'view': self})
if serializer.is_valid():
# Authenticate the credentials by grabbing Django User object
user = authenticate(username=serializer.data['username'],
password=serializer.data['password'])
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)
if user and user.is_authenticated():
if user.is_active:
if getattr(settings, 'REST_SESSION_LOGIN', True):
login(request, user)
def get_response(self):
return Response(self.response_serializer(self.token).data,
status=status.HTTP_200_OK)
# 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:
return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
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):
@ -113,7 +77,7 @@ class Logout(LoggedInRESTAPIView):
Accepts/Returns nothing.
"""
def get(self, request):
def post(self, request):
try:
request.user.auth_token.delete()
except:
@ -125,54 +89,7 @@ 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):
class UserDetails(LoggedInRESTAPIView, RetrieveUpdateAPIView):
"""
Returns User's details in JSON format.
@ -183,50 +100,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):
@ -239,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):
@ -283,80 +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)
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)
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):
@ -368,27 +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."})

View File

@ -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,7 +29,6 @@ setup(
zip_safe=False,
install_requires=[
'Django>=1.5.0',
'django-registration>=1.0',
'djangorestframework>=2.3.13',
],
test_suite='rest_auth.runtests.runtests',

2
test_requirements.pip Normal file
View File

@ -0,0 +1,2 @@
django-allauth>=0.18.0
responses>=0.2.2