diff --git a/.travis.yml b/.travis.yml
index 5152e3c..f07f4a3 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,8 +2,9 @@ language: python
python:
- "2.7"
env:
- - DJANGO=1.7.7
- - DJANGO=1.8
+ - DJANGO=1.7.11
+ - DJANGO=1.8.9
+ - DJANGO=1.9.2
install:
- pip install -q Django==$DJANGO --use-mirrors
- pip install coveralls
diff --git a/demo/demo/urls.py b/demo/demo/urls.py
index 54d06ad..3813e5a 100644
--- a/demo/demo/urls.py
+++ b/demo/demo/urls.py
@@ -11,6 +11,8 @@ urlpatterns = [
name='email-verification'),
url(r'^login/$', TemplateView.as_view(template_name="login.html"),
name='login'),
+ url(r'^logout/$', TemplateView.as_view(template_name="logout.html"),
+ name='logout'),
url(r'^password-reset/$',
TemplateView.as_view(template_name="password_reset.html"),
name='password-reset'),
diff --git a/demo/requirements.pip b/demo/requirements.pip
index f1c5057..4949275 100644
--- a/demo/requirements.pip
+++ b/demo/requirements.pip
@@ -1,4 +1,4 @@
django>=1.7.0
-django-rest-auth==0.6.0
+django-rest-auth==0.7.0
django-allauth==0.24.1
six==1.9.0
diff --git a/demo/templates/base.html b/demo/templates/base.html
index 8a0b0ed..03a2b73 100644
--- a/demo/templates/base.html
+++ b/demo/templates/base.html
@@ -40,6 +40,7 @@
User details
+ Logout
Password change
diff --git a/demo/templates/fragments/logout_form.html b/demo/templates/fragments/logout_form.html
new file mode 100644
index 0000000..7fd281d
--- /dev/null
+++ b/demo/templates/fragments/logout_form.html
@@ -0,0 +1,20 @@
+{% block content %}
+
+
+{% endblock %}
diff --git a/demo/templates/logout.html b/demo/templates/logout.html
new file mode 100644
index 0000000..2ae28e2
--- /dev/null
+++ b/demo/templates/logout.html
@@ -0,0 +1,8 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+
Logout
+ {% include "fragments/logout_form.html" %}
+
+{% endblock %}
diff --git a/docs/api_endpoints.rst b/docs/api_endpoints.rst
index 1f9660f..05c2196 100644
--- a/docs/api_endpoints.rst
+++ b/docs/api_endpoints.rst
@@ -11,7 +11,11 @@ Basic
- password (string)
-- /rest-auth/logout/ (POST)
+- /rest-auth/logout/ (POST, GET)
+
+ .. note:: ``ACCOUNT_LOGOUT_ON_GET = True`` to allow logout using GET (this is the exact same conf from allauth)
+
+ - token
- /rest-auth/password/reset/ (POST)
@@ -70,3 +74,8 @@ Basing on example from installation section :doc:`Installation `
- access_token
- code
+
+- /rest-auth/twitter/ (POST)
+
+ - access_token
+ - token_secret
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 41e982d..d1f64a3 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,9 +1,15 @@
Changelog
=========
-0.7.0
+0.8.0
-----
- added support for django-rest-framework-jwt
+0.7.0
+-----
+- Wrapped API returned strings in ugettext_lazy
+- Fixed not using ``get_username`` which caused issues when using custom user model without username field
+- Django 1.9 support
+- Added ``TwitterLoginSerializer``
0.6.0
-----
@@ -61,4 +67,4 @@ Changelog
- changed password reset confim url - uid and token should be sent in POST
- increase test coverage
- made compatibile with django 1.7
-- removed user profile support
+- removed user profile support
\ No newline at end of file
diff --git a/docs/configuration.rst b/docs/configuration.rst
index d726a99..0728e11 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -31,7 +31,7 @@ Configuration
...
}
-- **REST_AUTH_REGISTRATION_SERIALIZERS**
+- **REST_AUTH_REGISTER_SERIALIZERS**
You can define your custom serializers for registration endpoint.
Possible key values:
diff --git a/docs/index.rst b/docs/index.rst
index 2ad4a05..dc25e83 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -6,7 +6,7 @@
Welcome to django-rest-auth's documentation!
============================================
-.. warning:: Updating django-rest-auth to version **0.3.4** is highly recommended because of a security issue in PasswordResetConfirmation validation method.
+.. warning:: Updating django-rest-auth from version **0.3.3** is highly recommended because of a security issue in PasswordResetConfirmation validation method.
.. note:: django-rest-auth from v0.3.3 supports django-rest-framework v3.0
diff --git a/docs/installation.rst b/docs/installation.rst
index 1118826..9e47a47 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -65,11 +65,11 @@ Registration (optional)
Social Authentication (optional)
--------------------------------
-Using ``django-allauth``, ``django-rest-auth`` provides helpful class for creating social media authentication view. Below is an example with Facebook authentication.
+Using ``django-allauth``, ``django-rest-auth`` provides helpful class for creating social media authentication view.
-.. note:: Points 1, 2 and 3 are related with ``django-allauth`` configuration, so if you have already configured social authentication, then please go to step 4. See ``django-allauth`` documentation for more details.
+.. note:: Points 1 and 2 are related to ``django-allauth`` configuration, so if you have already configured social authentication, then please go to step 3. See ``django-allauth`` documentation for more details.
-1. Add ``allauth.socialaccount`` and ``allauth.socialaccount.providers.facebook`` apps to INSTALLED_APPS in your django settings.py:
+1. Add ``allauth.socialaccount`` and ``allauth.socialaccount.providers.facebook`` or ``allauth.socialaccount.providers.twitter`` apps to INSTALLED_APPS in your django settings.py:
.. code-block:: python
@@ -85,10 +85,15 @@ Using ``django-allauth``, ``django-rest-auth`` provides helpful class for creati
...,
'allauth.socialaccount',
'allauth.socialaccount.providers.facebook',
+ 'allauth.socialaccount.providers.twitter',
+
)
2. Add Social Application in django admin panel
+Facebook
+########
+
3. Create new view as a subclass of ``rest_auth.registration.views.SocialLoginView`` with ``FacebookOAuth2Adapter`` adapter as an attribute:
.. code-block:: python
@@ -108,6 +113,32 @@ Using ``django-allauth``, ``django-rest-auth`` provides helpful class for creati
url(r'^rest-auth/facebook/$', FacebookLogin.as_view(), name='fb_login')
)
+
+Twitter
+#######
+
+If you are using Twitter for your social authentication, it is a bit different since Twitter uses OAuth 1.0.
+
+3. Create new view as a subclass of ``rest_auth.views.LoginView`` with ``TwitterOAuthAdapter`` adapter and ``TwitterLoginSerializer`` as an attribute:
+
+.. code-block:: python
+
+ from allauth.socialaccount.providers.twitter.views import TwitterOAuthAdapter
+ from rest_auth.views import LoginView
+ from rest_auth.social_serializers import TwitterLoginSerializer
+
+ class TwitterLogin(LoginView):
+ serializer_class = TwitterLoginSerializer
+ adapter_class = TwitterOAuthAdapter
+
+4. Create url for TwitterLogin view:
+
+.. code-block:: python
+
+ urlpatterns += pattern('',
+ ...,
+ url(r'^rest-auth/twitter/$', TwitterLogin.as_view(), name='twitter_login')
+ )
.. note:: Starting from v0.21.0, django-allauth has dropped support for context processors. Check out http://django-allauth.readthedocs.org/en/latest/changelog.html#from-0-21-0 for more details.
diff --git a/rest_auth/locale/de/LC_MESSAGES/django.po b/rest_auth/locale/de/LC_MESSAGES/django.po
new file mode 100644
index 0000000..3ad22d0
--- /dev/null
+++ b/rest_auth/locale/de/LC_MESSAGES/django.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-02 14:11+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: registration/serializers.py:54
+msgid "View is not defined, pass it as a context variable"
+msgstr "\"View\" ist nicht definiert, übergib es als Contextvariable"
+
+#: registration/serializers.py:59
+msgid "Define adapter_class in view"
+msgstr "Definier \"adapter_class\" in view"
+
+#: registration/serializers.py:78
+msgid "Define callback_url in view"
+msgstr "Definier \"callback_url\" in view"
+
+#: registration/serializers.py:82
+msgid "Define client_class in view"
+msgstr "Definier \"client_class\" in view"
+
+#: registration/serializers.py:102
+msgid "Incorrect input. access_token or code is required."
+msgstr "Falsche Eingabe. \"access_token\" oder \"code\" erforderlich."
+
+#: registration/serializers.py:111
+msgid "Incorrect value"
+msgstr "Falscher Wert."
+
+#: registration/serializers.py:140
+msgid "A user is already registered with this e-mail address."
+msgstr "Ein User mit dieser E-Mail Adresse ist schon registriert."
+
+#: registration/serializers.py:148
+msgid "The two password fields didn't match."
+msgstr "Die beiden Passwörter sind nicht identisch."
+
+#: registration/views.py:64
+msgid "ok"
+msgstr "Ok"
+
+#: serializers.py:29
+msgid "Must include \"email\" and \"password\"."
+msgstr "Muss \"email\" und \"password\" enthalten."
+
+#: serializers.py:40
+msgid "Must include \"username\" and \"password\"."
+msgstr "Muss \"username\" und \"password\" enthalten."
+
+#: serializers.py:53
+msgid "Must include either \"username\" or \"email\" and \"password\"."
+msgstr "Muss entweder \"username\" oder \"email\" und password \"password\""
+
+#: serializers.py:94
+msgid "User account is disabled."
+msgstr "Der Useraccount ist deaktiviert."
+
+#: serializers.py:97
+msgid "Unable to log in with provided credentials."
+msgstr "Kann nicht mit den angegeben Zugangsdaten anmelden."
+
+#: serializers.py:106
+msgid "E-mail is not verified."
+msgstr "E-Mail Adresse ist nicht verifiziert."
+
+#: serializers.py:152
+msgid "Error"
+msgstr "Fehler"
+
+#: views.py:71
+msgid "Successfully logged out."
+msgstr "Erfolgreich ausgeloggt."
+
+#: views.py:111
+msgid "Password reset e-mail has been sent."
+msgstr "Die E-Mail zum Zurücksetzen des Passwortes wurde verschickt."
+
+#: views.py:132
+msgid "Password has been reset with the new password."
+msgstr "Das Passwort wurde mit dem neuen Passwort ersetzt."
+
+#: views.py:150
+msgid "New password has been saved."
+msgstr "Das neue Passwort wurde gespeichert."
diff --git a/rest_auth/registration/serializers.py b/rest_auth/registration/serializers.py
index e3c83fb..2390b05 100644
--- a/rest_auth/registration/serializers.py
+++ b/rest_auth/registration/serializers.py
@@ -1,5 +1,6 @@
from django.http import HttpRequest
from django.conf import settings
+from django.utils.translation import ugettext_lazy as _
try:
from allauth.account import app_settings as allauth_settings
@@ -22,7 +23,6 @@ if 'allauth.socialaccount' in settings.INSTALLED_APPS:
pass
-
class SocialLoginSerializer(serializers.Serializer):
access_token = serializers.CharField(required=False, allow_blank=True)
code = serializers.CharField(required=False, allow_blank=True)
@@ -53,12 +53,12 @@ class SocialLoginSerializer(serializers.Serializer):
if not view:
raise serializers.ValidationError(
- 'View is not defined, pass it as a context variable'
+ _('View is not defined, pass it as a context variable')
)
adapter_class = getattr(view, 'adapter_class', None)
if not adapter_class:
- raise serializers.ValidationError('Define adapter_class in view')
+ raise serializers.ValidationError(_('Define adapter_class in view'))
adapter = adapter_class()
app = adapter.get_provider().get_app(request)
@@ -77,11 +77,11 @@ class SocialLoginSerializer(serializers.Serializer):
if not self.callback_url:
raise serializers.ValidationError(
- 'Define callback_url in view'
+ _('Define callback_url in view')
)
if not self.client_class:
raise serializers.ValidationError(
- 'Define client_class in view'
+ _('Define client_class in view')
)
code = attrs.get('code')
@@ -101,7 +101,7 @@ class SocialLoginSerializer(serializers.Serializer):
access_token = token['access_token']
else:
- raise serializers.ValidationError('Incorrect input. access_token or code is required.')
+ raise serializers.ValidationError(_('Incorrect input. access_token or code is required.'))
token = adapter.parse_token({'access_token': access_token})
token.app = app
@@ -110,7 +110,7 @@ class SocialLoginSerializer(serializers.Serializer):
login = self.get_social_login(adapter, app, token, access_token)
complete_social_login(request, login)
except HTTPError:
- raise serializers.ValidationError('Incorrect value')
+ raise serializers.ValidationError(_('Incorrect value'))
if not login.is_existing:
login.lookup()
@@ -139,7 +139,7 @@ class RegisterSerializer(serializers.Serializer):
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.")
+ _("A user is already registered with this e-mail address."))
return email
def validate_password1(self, password):
@@ -147,7 +147,7 @@ class RegisterSerializer(serializers.Serializer):
def validate(self, data):
if data['password1'] != data['password2']:
- raise serializers.ValidationError("The two password fields didn't match.")
+ raise serializers.ValidationError(_("The two password fields didn't match."))
return data
def custom_signup(self, request, user):
diff --git a/rest_auth/registration/views.py b/rest_auth/registration/views.py
index 2689ec2..40316dd 100644
--- a/rest_auth/registration/views.py
+++ b/rest_auth/registration/views.py
@@ -1,4 +1,4 @@
-from django.conf import settings
+from django.utils.translation import ugettext_lazy as _
from rest_framework.views import APIView
from rest_framework.response import Response
@@ -75,7 +75,7 @@ class VerifyEmailView(APIView, ConfirmEmailView):
self.kwargs['key'] = serializer.validated_data['key']
confirmation = self.get_object()
confirmation.confirm(self.request)
- return Response({'message': 'ok'}, status=status.HTTP_200_OK)
+ return Response({'message': _('ok')}, status=status.HTTP_200_OK)
class SocialLoginView(LoginView):
@@ -102,4 +102,4 @@ class SocialLoginView(LoginView):
-------------
"""
- serializer_class = SocialLoginSerializer
+ serializer_class = SocialLoginSerializer
\ No newline at end of file
diff --git a/rest_auth/serializers.py b/rest_auth/serializers.py
index f1e8173..a414d23 100644
--- a/rest_auth/serializers.py
+++ b/rest_auth/serializers.py
@@ -81,7 +81,7 @@ class LoginSerializer(serializers.Serializer):
# Authentication without using allauth
if email:
try:
- username = UserModel.objects.get(email__iexact=email).username
+ username = UserModel.objects.get(email__iexact=email).get_username()
except UserModel.DoesNotExist:
pass
@@ -103,7 +103,7 @@ class LoginSerializer(serializers.Serializer):
if app_settings.EMAIL_VERIFICATION == app_settings.EmailVerificationMethod.MANDATORY:
email_address = user.emailaddress_set.get(email=user.email)
if not email_address.verified:
- raise serializers.ValidationError('E-mail is not verified.')
+ raise serializers.ValidationError(_('E-mail is not verified.'))
attrs['user'] = user
return attrs
diff --git a/rest_auth/social_serializers.py b/rest_auth/social_serializers.py
new file mode 100644
index 0000000..087e6e1
--- /dev/null
+++ b/rest_auth/social_serializers.py
@@ -0,0 +1,78 @@
+from django.http import HttpRequest
+from rest_framework import serializers
+from requests.exceptions import HTTPError
+# Import is needed only if we are using social login, in which
+# case the allauth.socialaccount will be declared
+try:
+ from allauth.socialaccount.helpers import complete_social_login
+except ImportError:
+ pass
+
+from allauth.socialaccount.models import SocialToken
+
+
+class TwitterLoginSerializer(serializers.Serializer):
+ access_token = serializers.CharField(required=True)
+ token_secret = serializers.CharField(required=True)
+
+ def _get_request(self):
+ request = self.context.get('request')
+ if not isinstance(request, HttpRequest):
+ request = request._request
+ return request
+
+ def get_social_login(self, adapter, app, token, response):
+ """
+
+ :param adapter: allauth.socialaccount Adapter subclass. Usually OAuthAdapter or Auth2Adapter
+ :param app: `allauth.socialaccount.SocialApp` instance
+ :param token: `allauth.socialaccount.SocialToken` instance
+ :param response: Provider's response for OAuth1. Not used in the
+ :return: :return: A populated instance of the `allauth.socialaccount.SocialLoginView` instance
+ """
+ request = self._get_request()
+ social_login = adapter.complete_login(request, app, token, response=response)
+ social_login.token = token
+ return social_login
+
+ def validate(self, attrs):
+ view = self.context.get('view')
+ request = self._get_request()
+
+ if not view:
+ raise serializers.ValidationError(
+ 'View is not defined, pass it as a context variable'
+ )
+
+ adapter_class = getattr(view, 'adapter_class', None)
+ if not adapter_class:
+ raise serializers.ValidationError('Define adapter_class in view')
+
+ adapter = adapter_class()
+ app = adapter.get_provider().get_app(request)
+
+ if('access_token' in attrs) and ('token_secret' in attrs):
+ access_token = attrs.get('access_token')
+ token_secret = attrs.get('token_secret')
+ else:
+ raise serializers.ValidationError('Incorrect input. access_token and token_secret are required.')
+
+ request.session['oauth_api.twitter.com_access_token'] = {
+ 'oauth_token': access_token,
+ 'oauth_token_secret': token_secret,
+ }
+ token = SocialToken(token=access_token, token_secret=token_secret)
+ token.app = app
+
+ try:
+ login = self.get_social_login(adapter, app, token, access_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)
+ attrs['user'] = login.account.user
+
+ return attrs
diff --git a/rest_auth/tests/test_api.py b/rest_auth/tests/test_api.py
index 9903c9d..f75322c 100644
--- a/rest_auth/tests/test_api.py
+++ b/rest_auth/tests/test_api.py
@@ -421,3 +421,29 @@ class APITestCase1(TestCase, BaseAPITestCase):
# try to login again
self._login()
self._logout()
+
+ @override_settings(ACCOUNT_LOGOUT_ON_GET=True)
+ def test_logout_on_get(self):
+ payload = {
+ "username": self.USERNAME,
+ "password": self.PASS
+ }
+
+ # create user
+ user = get_user_model().objects.create_user(self.USERNAME, '', self.PASS)
+
+ self.post(self.login_url, data=payload, status_code=200)
+ self.get(self.logout_url, status=status.HTTP_200_OK)
+
+ @override_settings(ACCOUNT_LOGOUT_ON_GET=False)
+ def test_logout_on_post_only(self):
+ payload = {
+ "username": self.USERNAME,
+ "password": self.PASS
+ }
+
+ # create user
+ user = get_user_model().objects.create_user(self.USERNAME, '', self.PASS)
+
+ self.post(self.login_url, data=payload, status_code=status.HTTP_200_OK)
+ self.get(self.logout_url, status_code=status.HTTP_405_METHOD_NOT_ALLOWED)
diff --git a/rest_auth/views.py b/rest_auth/views.py
index 5951a93..25f47f2 100644
--- a/rest_auth/views.py
+++ b/rest_auth/views.py
@@ -1,6 +1,7 @@
from django.contrib.auth import login, logout
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
+from django.utils.translation import ugettext_lazy as _
from rest_framework import status
from rest_framework.views import APIView
@@ -9,6 +10,8 @@ from rest_framework.generics import GenericAPIView
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.generics import RetrieveUpdateAPIView
+from allauth.account import app_settings as allauth_settings
+
from .app_settings import (
TokenSerializer, UserDetailsSerializer, LoginSerializer,
PasswordResetSerializer, PasswordResetConfirmSerializer,
@@ -84,7 +87,23 @@ class LogoutView(APIView):
"""
permission_classes = (AllowAny,)
+ def get(self, request, *args, **kwargs):
+ try:
+ if allauth_settings.LOGOUT_ON_GET:
+ response = self.logout(request)
+ else:
+ response = self.http_method_not_allowed(request, *args, **kwargs)
+ except Exception as exc:
+ response = self.handle_exception(exc)
+
+ return self.finalize_response(request, response, *args, **kwargs)
+ self.response = self.finalize_response(request, response, *args, **kwargs)
+ return self.response
+
def post(self, request):
+ return self.logout(request)
+
+ def logout(self, request):
try:
request.user.auth_token.delete()
except (AttributeError, ObjectDoesNotExist):
@@ -92,12 +111,11 @@ class LogoutView(APIView):
logout(request)
- return Response({"success": "Successfully logged out."},
+ return Response({"success": _("Successfully logged out.")},
status=status.HTTP_200_OK)
class UserDetailsView(RetrieveUpdateAPIView):
-
"""
Returns User's details in JSON format.
@@ -134,13 +152,12 @@ class PasswordResetView(GenericAPIView):
serializer.save()
# Return the success message with OK HTTP status
return Response(
- {"success": "Password reset e-mail has been sent."},
+ {"success": _("Password reset e-mail has been sent.")},
status=status.HTTP_200_OK
)
class PasswordResetConfirmView(GenericAPIView):
-
"""
Password reset e-mail link is confirmed, therefore this resets the user's password.
@@ -156,11 +173,10 @@ class PasswordResetConfirmView(GenericAPIView):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
- return Response({"success": "Password has been reset with the new password."})
+ return Response({"success": _("Password has been reset with the new password.")})
class PasswordChangeView(GenericAPIView):
-
"""
Calls Django Auth SetPasswordForm save method.
@@ -175,4 +191,4 @@ class PasswordChangeView(GenericAPIView):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
- return Response({"success": "New password has been saved."})
+ return Response({"success": _("New password has been saved.")})