diff --git a/CHANGELOG.rst b/CHANGELOG.rst
new file mode 100644
index 0000000..de9840f
--- /dev/null
+++ b/CHANGELOG.rst
@@ -0,0 +1,5 @@
+******************
+1.0.0 (unreleased)
+******************
+
+- First official release.
diff --git a/README.rst b/README.rst
index e01082e..fab0404 100644
--- a/README.rst
+++ b/README.rst
@@ -1,22 +1,51 @@
-==================
+##################
django-allauth-cas
-==================
+##################
.. image:: https://travis-ci.org/aureplop/django-allauth-cas.svg?branch=master
- :target: https://travis-ci.org/aureplop/django-allauth-cas
+ :target: https://travis-ci.org/aureplop/django-allauth-cas
.. image:: https://coveralls.io/repos/github/aureplop/django-allauth-cas/badge.svg?branch=master
- :target: https://coveralls.io/github/aureplop/django-allauth-cas?branch=master
+ :target: https://coveralls.io/github/aureplop/django-allauth-cas?branch=master
-**Warning:** Still under development.
-
CAS support for django-allauth_.
-Supports:
+Requirements
+ * Django 1.8 → 2.0
-- Django 1.8-10 - Python 2.7, 3.4-5
-- Django 1.11 - Python 2.7, 3.4-6
+Dependencies
+ * django-allauth_
+ * python-cas_: CAS client library
+
+.. note::
+
+ Tests only target the latest allauth version compatible for each Django version
+ supported:
+
+ * Django 1.9 with django-allauth 0.32.0;
+ * Django 1.8, 1.10, 1.11, 2.0 with the latest django-allauth.
+
+If you have any problems at use or think docs can be clearer, take a little
+time to open an issue and/or a PR would be welcomed ;-)
+
+Acknowledgments
+ * This work is strongly inspired by the `OAuth2 support of django-allauth`_.
-.. _django-allauth: https://www.intenct.nl/projects/django-allauth/
+************
+Installation
+************
+
+Install the python package ``django-allauth-cas``. For example, using pip:
+
+.. code-block:: bash
+
+ $ pip install django-allauth-cas
+
+Add ``'allauth_cas'`` to ``INSTALLED_APPS``.
+
+
+.. _django-allauth: https://github.com/pennersr/django-allauth
+.. _OAuth2 support of django-allauth: https://github.com/pennersr/django-allauth/tree/master/allauth/socialaccount/providers/oauth2
+.. _python-cas: https://github.com/python-cas/python-cas
diff --git a/allauth_cas/__init__.py b/allauth_cas/__init__.py
index 02467c2..eddc85b 100644
--- a/allauth_cas/__init__.py
+++ b/allauth_cas/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-__version__ = '0.0.1.dev5'
+__version__ = '1.0.0b1'
default_app_config = 'allauth_cas.apps.CASAccountConfig'
diff --git a/allauth_cas/providers.py b/allauth_cas/providers.py
index cf5ec52..a46b4f1 100644
--- a/allauth_cas/providers.py
+++ b/allauth_cas/providers.py
@@ -3,7 +3,9 @@ from six.moves.urllib.parse import parse_qsl
import django
from django.contrib import messages
+from django.template.loader import render_to_string
from django.utils.http import urlencode
+from django.utils.safestring import mark_safe
from allauth.socialaccount.providers.base import Provider
@@ -15,41 +17,197 @@ else:
class CASProvider(Provider):
+ def get_auth_params(self, request, action):
+ settings = self.get_settings()
+ ret = dict(settings.get('AUTH_PARAMS', {}))
+ dynamic_auth_params = request.GET.get('auth_params')
+ if dynamic_auth_params:
+ ret.update(dict(parse_qsl(dynamic_auth_params)))
+ return ret
+
+ ##
+ # Data extraction from CAS responses.
+ ##
+
+ def extract_uid(self, data):
+ """Extract the user uid.
+
+ Notes:
+ Each pair ``(provider_id, uid)`` is unique and related to a single
+ user.
+
+ Args:
+ data (uid (str), extra (dict)): CAS response. Example:
+ ``('alice', {'name': 'Alice'})``
+
+ Returns:
+ str: Default to ``data[0]``, user identifier for the CAS server.
+
+ """
+ uid, _ = data
+ return uid
+
+ def extract_common_fields(self, data):
+ """Extract the data to pass to `SOCIALACCOUNT_ADAPTER.populate_user()`.
+
+ Args:
+ data (uid (str), extra (dict)): CAS response. Example:
+ ``('alice', {'name': 'Alice'})``
+
+ Returns:
+ dict: Default::
+
+ {
+ 'username': extra.get('username', uid),
+ 'email': extra.get('email'),
+ 'first_name': extra.get('first_name'),
+ 'last_name': extra.get('last_name'),
+ 'name': extra.get('name'),
+ }
+
+ """
+ uid, extra = data
+ return {
+ 'username': extra.get('username', uid),
+ 'email': extra.get('email'),
+ 'first_name': extra.get('first_name'),
+ 'last_name': extra.get('last_name'),
+ 'name': extra.get('name'),
+ }
+
+ def extract_email_addresses(self, data):
+ """Extract the email addresses.
+
+ Args:
+ data (uid (str), extra (dict)): CAS response. Example:
+ ``('alice', {'name': 'Alice'})``
+
+ Returns:
+ `list` of `EmailAddress`: By default, ``[]``.
+
+ Example::
+
+ [
+ EmailAddress(
+ email='user@domain.net',
+ verified=True, primary=True,
+ ),
+ EmailAddress(
+ email='alias@domain.net',
+ verified=True, primary=False,
+ ),
+ ]
+
+ """
+ return super(CASProvider, self).extract_email_addresses(data)
+
+ def extract_extra_data(self, data):
+ """Extract the data to save to `SocialAccount.extra_data`.
+
+ Args:
+ data (uid (str), extra (dict)): CAS response. Example:
+ ``('alice', {'name': 'Alice'})``
+
+ Returns:
+ dict: By default, ``data``.
+ """
+ uid, extra = data
+ return dict(extra, uid=uid)
+
+ ##
+ # Message to suggest users to logout of the CAS server.
+ ##
+
+ def add_message_suggest_caslogout(
+ self, request, next_page=None, level=None,
+ ):
+ """Add a message with a link for the user to logout of the CAS server.
+
+ It uses the template ``socialaccount/messages/suggest_caslogout.html``,
+ with the ``provider`` and the ``logout_url`` as context.
+
+ Args:
+ request: The request to which the message is added.
+ next_page (optional): Added to the logout link for the CAS server
+ to redirect the user to this url.
+ Default: ``request.get_full_path()``
+ level: The message level. Default: ``messages.INFO``
+
+ """
+ if next_page is None:
+ next_page = request.get_full_path()
+ if level is None:
+ level = messages.INFO
+
+ logout_url = self.get_logout_url(request, next=next_page)
+
+ # DefaultAccountAdapter.add_message is unusable because it always
+ # escape the message content.
+
+ template = 'socialaccount/messages/suggest_caslogout.html'
+ context = {
+ 'provider': self,
+ 'logout_url': logout_url,
+ }
+
+ messages.add_message(
+ request, level,
+ mark_safe(render_to_string(template, context).strip()),
+ fail_silently=True,
+ )
+
+ def message_suggest_caslogout_on_logout(self, request):
+ """Indicates whether the logout message should be sent on user logout.
+
+ By default, it returns
+ ``settings.SOCIALACCOUNT_PROVIDERS[self.id]['MESSAGE_SUGGEST_CASLOGOUT_ON_LOGOUT']``
+ or ``False``.
+
+ Notes:
+ The ``request`` argument is the one trigerring the emission of the
+ signal ``user_logged_out``.
+
+ """
+ return (
+ self.get_settings()
+ .get('MESSAGE_SUGGEST_CASLOGOUT_ON_LOGOUT', False)
+ )
+
+ def message_suggest_caslogout_on_logout_level(self, request):
+ """Level of the logout message issued on user logout.
+
+ By default, it returns
+ ``settings.SOCIALACCOUNT_PROVIDERS[self.id]['MESSAGE_SUGGEST_CASLOGOUT_ON_LOGOUT_LEVEL']``
+ or ``messages.INFO``.
+
+ Notes:
+ The ``request`` argument is the one trigerring the emission of the
+ signal ``user_logged_out``.
+
+ """
+ return (
+ self.get_settings()
+ .get('MESSAGE_SUGGEST_CASLOGOUT_ON_LOGOUT_LEVEL', messages.INFO)
+ )
+
+ ##
+ # Shortcuts functions.
+ ##
+
def get_login_url(self, request, **kwargs):
url = reverse(self.id + '_login')
if kwargs:
url += '?' + urlencode(kwargs)
return url
+ def get_callback_url(self, request, **kwargs):
+ url = reverse(self.id + '_callback')
+ if kwargs:
+ url += '?' + urlencode(kwargs)
+ return url
+
def get_logout_url(self, request, **kwargs):
url = reverse(self.id + '_logout')
if kwargs:
url += '?' + urlencode(kwargs)
return url
-
- def get_auth_params(self, request, action):
- settings = self.get_settings()
- ret = dict(settings.get('AUTH_PARAMS', {}))
- dynamic_auth_params = request.GET.get('auth_params')
- if dynamic_auth_params:
- ret.update(dict(parse_qsl(dynamic_auth_params)))
- return ret
-
- def message_on_logout(self, request):
- return self.get_settings().get('MESSAGE_ON_LOGOUT', True)
-
- def message_on_logout_level(self, request):
- return self.get_settings().get('MESSAGE_ON_LOGOUT_LEVEL',
- messages.INFO)
-
- def extract_uid(self, data):
- username, _, _ = data
- return username
-
- def extract_common_fields(self, data):
- username, _, _ = data
- return {'username': username}
-
- def extract_extra_data(self, data):
- _, extra_data, _ = data
- return extra_data
diff --git a/allauth_cas/signals.py b/allauth_cas/signals.py
index 208ee37..927cbfd 100644
--- a/allauth_cas/signals.py
+++ b/allauth_cas/signals.py
@@ -1,11 +1,8 @@
# -*- coding: utf-8 -*-
-from django.conf import settings
-from django.contrib import messages
from django.contrib.auth.signals import user_logged_out
from django.dispatch import receiver
-from django.template.loader import render_to_string
-from django.utils.safestring import mark_safe
+from allauth.account.adapter import get_adapter
from allauth.account.utils import get_next_redirect_url
from allauth.socialaccount import providers
@@ -16,35 +13,20 @@ from . import CAS_PROVIDER_SESSION_KEY
def cas_account_logout(sender, request, **kwargs):
provider_id = request.session.get(CAS_PROVIDER_SESSION_KEY)
- if (not provider_id or
- 'django.contrib.messages' not in settings.INSTALLED_APPS):
+ if not provider_id:
return
provider = providers.registry.by_id(provider_id, request)
- if not provider.message_on_logout(request):
+ if not provider.message_suggest_caslogout_on_logout(request):
return
- redirect_url = (
+ next_page = (
get_next_redirect_url(request) or
- request.get_full_path()
+ get_adapter(request).get_logout_redirect_url(request)
)
- logout_kwargs = {'next': redirect_url} if redirect_url else {}
- logout_url = provider.get_logout_url(request, **logout_kwargs)
- logout_link = mark_safe('link'.format(logout_url))
-
- level = provider.message_on_logout_level(request)
-
- # DefaultAccountAdapter.add_message from allauth.account.adapter is
- # unusable because HTML in message content is always escaped.
-
- template = 'cas_account/messages/logged_out.txt'
- context = {
- 'logout_url': logout_url,
- 'logout_link': logout_link,
- }
-
- message = mark_safe(render_to_string(template, context).strip())
-
- messages.add_message(request, level, message)
+ provider.add_message_suggest_caslogout(
+ request, next_page=next_page,
+ level=provider.message_suggest_caslogout_on_logout_level(request),
+ )
diff --git a/allauth_cas/templates/cas_account/messages/logged_out.txt b/allauth_cas/templates/cas_account/messages/logged_out.txt
deleted file mode 100644
index b0b88ab..0000000
--- a/allauth_cas/templates/cas_account/messages/logged_out.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-{% load i18n %}
-{% blocktrans %}
-To logout of CAS, please close your browser, or visit this {{ logout_link }}.
-{% endblocktrans %}
diff --git a/allauth_cas/templates/socialaccount/messages/suggest_caslogout.html b/allauth_cas/templates/socialaccount/messages/suggest_caslogout.html
new file mode 100644
index 0000000..1d0662d
--- /dev/null
+++ b/allauth_cas/templates/socialaccount/messages/suggest_caslogout.html
@@ -0,0 +1,5 @@
+{% load i18n %}
+
+{% blocktrans with provider_name=provider.name %}
+To logout of {{ provider_name }}, please close your browser, or visit this link.
+{% endblocktrans %}
diff --git a/allauth_cas/urls.py b/allauth_cas/urls.py
index be30316..687dca6 100644
--- a/allauth_cas/urls.py
+++ b/allauth_cas/urls.py
@@ -1,23 +1,51 @@
# -*- coding: utf-8 -*-
from django.conf.urls import include, url
-
-from allauth.utils import import_attribute
+from django.utils.module_loading import import_string
def default_urlpatterns(provider):
package = provider.get_package()
- login_view = import_attribute(package + '.views.login')
- callback_view = import_attribute(package + '.views.callback')
- logout_view = import_attribute(package + '.views.logout')
+ try:
+ login_view = import_string(package + '.views.login')
+ except ImportError:
+ raise ImportError(
+ "The login view for the '{id}' provider is lacking from the "
+ "'views' module of its app.\n"
+ "You may want to add:\n"
+ "from allauth_cas.views import CASLoginView\n\n"
+ "login = CASLoginView.adapter_view()"
+ .format(id=provider.id)
+ )
+
+ try:
+ callback_view = import_string(package + '.views.callback')
+ except ImportError:
+ raise ImportError(
+ "The callback view for the '{id}' provider is lacking from the "
+ "'views' module of its app.\n"
+ "You may want to add:\n"
+ "from allauth_cas.views import CASCallbackView\n\n"
+ "callback = CASCallbackView.adapter_view()"
+ .format(id=provider.id)
+ )
+
+ try:
+ logout_view = import_string(package + '.views.logout')
+ except ImportError:
+ logout_view = None
urlpatterns = [
- url('^login/$',
- login_view, name=provider.id + '_login'),
- url('^login/callback/$',
- callback_view, name=provider.id + '_callback'),
- url('^logout/$',
- logout_view, name=provider.id + '_logout'),
+ url('^login/$', login_view,
+ name=provider.id + '_login'),
+ url('^login/callback/$', callback_view,
+ name=provider.id + '_callback'),
]
+ if logout_view is not None:
+ urlpatterns += [
+ url('^logout/$', logout_view,
+ name=provider.id + '_logout'),
+ ]
+
return [url('^' + provider.get_slug() + '/', include(urlpatterns))]
diff --git a/allauth_cas/views.py b/allauth_cas/views.py
index e9a3685..d2ea9b7 100644
--- a/allauth_cas/views.py
+++ b/allauth_cas/views.py
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
-import django
from django.http import HttpResponseRedirect
-from django.utils.http import urlencode
+from django.utils.functional import cached_property
from allauth.account.adapter import get_adapter
from allauth.account.utils import get_next_redirect_url
@@ -16,11 +15,6 @@ import cas
from . import CAS_PROVIDER_SESSION_KEY
from .exceptions import CASAuthenticationError
-if django.VERSION >= (1, 10):
- from django.urls import reverse
-else:
- from django.core.urlresolvers import reverse
-
class AuthAction(object):
AUTHENTICATE = 'authenticate'
@@ -29,23 +23,38 @@ class AuthAction(object):
class CASAdapter(object):
+ #: CAS server url.
+ url = None
+ #: CAS server version.
+ #: Choices: ``1`` or ``'1'``, ``2`` or ``'2'``, ``3`` or ``'3'``,
+ #: ``'CAS_2_SAML_1_0'``
+ version = None
def __init__(self, request):
self.request = request
- @property
+ @cached_property
def renew(self):
- """
- If user is already authenticated on Django, he may already been
- connected to CAS, but still may want to use another CAS account.
- We set renew to True in this case, as the CAS server won't use the
- single sign-on.
- To specifically check, if the current user has used a CAS server,
- we check if the CAS session key is set.
+ """Controls presence of ``renew`` in requests to the CAS server.
+
+ If ``True``, opt out single sign-on (SSO) functionality of the CAS
+ server. So that, user is always prompted for his username and password.
+
+ If ``False``, the CAS server does not prompt users for their
+ credentials if a SSO exists.
+
+ The default allows user to connect via an already used CAS server
+ with other credentials.
+
+ Returns:
+ ``True`` if logged in user has already connected to Django using
+ **any** CAS provider in the current session, ``False`` otherwise.
+
"""
return CAS_PROVIDER_SESSION_KEY in self.request.session
- def get_provider(self):
+ @cached_property
+ def provider(self):
"""
Returns a provider instance for the current request.
"""
@@ -53,75 +62,66 @@ class CASAdapter(object):
def complete_login(self, request, response):
"""
- Executed by the callback view after successful authentication on CAS
- server.
+ Executed by the callback view after successful authentication on the
+ CAS server.
+
+ Args:
+ request
+ response (`dict`): Data returned by the CAS server.
+ ``response[username]`` contains the user identifier for the
+ server, and may contain extra user-attributes.
+
+ Returns:
+ `SocialLogin()` object: State of the login-session.
- Returns the SocialLogin object which represents the state of the
- current login-session.
"""
- login = (self.get_provider()
- .sociallogin_from_response(request, response))
+ login = self.provider.sociallogin_from_response(request, response)
return login
def get_service_url(self, request):
- """
- Returns the service url to for a CAS client.
+ """The service url, used by the CAS client.
- From CAS specification, the service url is used in order to redirect
- user after a successful login on CAS server. Also, service_url sent
- when ticket is verified must be the one for which ticket was issued.
+ According to the CAS spec, the service url is passed by the CAS client
+ at several times. It must be the same for all interactions with the CAS
+ server.
- To conform this, the service url is always the callback url.
+ It is used as redirection from the CAS server after a succssful
+ authentication. So, the callback url is used as service url.
- A redirect url is found from the current request and appended as
- parameter to the service url and is latter used by the callback view to
- redirect user.
+ If present, the GET param ``next`` is added to the service url.
"""
redirect_to = get_next_redirect_url(request)
callback_kwargs = {'next': redirect_to} if redirect_to else {}
- callback_url = self.get_callback_url(request, **callback_kwargs)
+ callback_url = (
+ self.provider.get_callback_url(request, **callback_kwargs))
service_url = request.build_absolute_uri(callback_url)
return service_url
- def get_callback_url(self, request, **kwargs):
- """
- Returns the callback url of the provider.
-
- Keyword arguments are set as query string.
- """
- url = reverse(self.provider_id + '_callback')
- if kwargs:
- url += '?' + urlencode(kwargs)
- return url
-
class CASView(object):
-
+ """
+ Base class for CAS views.
+ """
@classmethod
- def adapter_view(cls, adapter, **kwargs):
- """
- Similar to the Django as_view() method.
+ def adapter_view(cls, adapter):
+ """Transform the view class into a view function.
- It also setups a few things:
- - given adapter argument will be used in views internals.
- - if the view execution raises a CASAuthenticationError, the view
- renders an authentication error page.
+ Similar to the Django ``as_view()`` method.
- To use this:
+ Notes:
+ An (human) error page is rendered if any ``CASAuthenticationError``
+ is catched.
- - subclass CAS adapter as wanted:
+ Args:
+ adapter (:class:`CASAdapter`): Provide specifics of a CAS server.
- class MyAdapter(CASAdapter):
- url = 'https://my.cas.url'
+ Returns:
+ A view function. The given adapter and related provider are
+ accessible as attributes from the view class.
- - define views:
-
- login = views.CASLoginView.adapter_view(MyAdapter)
- callback = views.CASCallbackView.adapter_view(MyAdapter)
- logout = views.CASLogoutView.adapter_view(MyAdapter)
"""
def view(request, *args, **kwargs):
@@ -134,7 +134,7 @@ class CASView(object):
# Setup and store adapter as view attribute.
self.adapter = adapter(request)
- self.provider = self.adapter.get_provider()
+ self.provider = self.adapter.provider
try:
return self.dispatch(request, *args, **kwargs)
@@ -207,17 +207,20 @@ class CASCallbackView(CASView):
# - error: None, {}, None
response = client.verify_ticket(ticket)
- if not response[0]:
+ uid, extra, _ = response
+
+ if not uid:
raise CASAuthenticationError(
"CAS server doesn't validate the ticket."
)
- # The CAS provider in use is stored to propose to the user to
- # disconnect from the latter when he logouts.
+ # Keep tracks of the last used CAS provider.
request.session[CAS_PROVIDER_SESSION_KEY] = self.provider.id
- # Finish the login flow
- login = self.adapter.complete_login(request, response)
+ data = (uid, extra)
+
+ # Finish the login flow.
+ login = self.adapter.complete_login(request, data)
login.state = SocialLogin.unstash_state(request)
return complete_social_login(request, login)
@@ -242,7 +245,7 @@ class CASLogoutView(CASView):
def get_redirect_url(self):
"""
- Returns the url to redirect after logout from current request.
+ Returns the url to redirect after logout.
"""
request = self.request
return (
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 0000000..69fa449
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1 @@
+_build/
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..d363d4f
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+SPHINXPROJ = django-allauth-cas
+SOURCEDIR = .
+BUILDDIR = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
\ No newline at end of file
diff --git a/docs/README b/docs/README
new file mode 100644
index 0000000..5a76931
--- /dev/null
+++ b/docs/README
@@ -0,0 +1,14 @@
+#############
+Documentation
+#############
+
+
+The documentation is compiled from reStructuredText using `Sphinx`_.
+
+To compile your own html version in ``_build/html/``::
+
+ # First time only.
+ pip install sphinx
+
+ # Build html.
+ make html
diff --git a/docs/advanced/cas_client.txt b/docs/advanced/cas_client.txt
new file mode 100644
index 0000000..ad0f2ae
--- /dev/null
+++ b/docs/advanced/cas_client.txt
@@ -0,0 +1,40 @@
+########################
+Configure the CAS client
+########################
+
+.. seealso::
+
+ `CAS Protocol Specification`_
+
+The CAS client parameters can be set on the ``CASAdapter`` subclasses.
+
+
+******************
+Server information
+******************
+
+You must at least fill these attributes on an adapter class.
+
+.. autoattribute:: allauth_cas.views.CASAdapter.url
+
+.. autoattribute:: allauth_cas.views.CASAdapter.version
+
+
+*****************
+Client parameters
+*****************
+
+.. autoattribute:: allauth_cas.views.CASAdapter.renew
+
+.. note::
+
+ A SSO session is created when user successfully authenticates against the
+ server, which let an HTTP cookie in the browser current session. If SSO is
+ enabled (``renew = False``), server checks this cookie, if any, to bypass the
+ request of user credentials. Depending on the server configuration and user
+ input at login time, CAS server replies to login page requests with a warning
+ page, or transparently redirects to the callback url (path to come back to
+ your web service).
+
+
+.. _`CAS Protocol Specification`: https://apereo.github.io/cas/5.0.x/protocol/CAS-Protocol-Specification.html
diff --git a/docs/advanced/extract_data.txt b/docs/advanced/extract_data.txt
new file mode 100644
index 0000000..14b6cdf
--- /dev/null
+++ b/docs/advanced/extract_data.txt
@@ -0,0 +1,21 @@
+#################################
+Use data returned by a CAS server
+#################################
+
+.. seealso::
+
+ `Creating and Populating User instances`_
+
+The following methods of ``CASProvider`` are used to extract data from the CAS
+responses.
+
+.. automethod:: allauth_cas.providers.CASProvider.extract_uid
+
+.. automethod:: allauth_cas.providers.CASProvider.extract_common_fields
+
+.. automethod:: allauth_cas.providers.CASProvider.extract_email_addresses
+
+.. automethod:: allauth_cas.providers.CASProvider.extract_extra_data
+
+
+.. _`Creating and Populating User instances`: http://django-allauth.readthedocs.io/en/latest/advanced.html#creating-and-populating-user-instances
diff --git a/docs/advanced/index.txt b/docs/advanced/index.txt
new file mode 100644
index 0000000..f6abaa2
--- /dev/null
+++ b/docs/advanced/index.txt
@@ -0,0 +1,8 @@
+##############
+Advanced Usage
+##############
+
+.. toctree::
+ cas_client
+ extract_data
+ signout
diff --git a/docs/advanced/signout.txt b/docs/advanced/signout.txt
new file mode 100644
index 0000000..b6ab530
--- /dev/null
+++ b/docs/advanced/signout.txt
@@ -0,0 +1,87 @@
+################
+Sign out helpers
+################
+
+To use features described on this page, you must also add a logout view for
+your provider:
+
+.. code-block:: python
+
+ from allauth_cas.views import CASLogoutView
+
+ logout = CASLogoutView.adapter_view(MyCASAdapter)
+
+
+**************
+Suggest logout
+**************
+
+Sending message
+===============
+
+Using the method below, you can emit a message to suggest users to logout of
+the CAS server.
+
+.. automethod:: allauth_cas.providers.CASProvider.add_message_suggest_caslogout
+
+Sending message at user logout
+==============================
+
+When the user signs out your application, this message can be sent
+automatically using the following settings.
+
+The message contains a logout link for **the last used** CAS server during the
+session.
+
+In your settings:
+
+.. code-block:: python
+
+ SOCIALACCOUNT_PROVIDERS = {
+ # …
+ '': {
+ # …
+
+ 'MESSAGE_SUGGEST_CASLOGOUT_ON_LOGOUT': True,
+
+ # Optional. By default, messages.INFO
+ 'MESSAGE_SUGGEST_CASLOGOUT_ON_LOGOUT_LEVEL': messages.WARNING,
+ },
+ }
+
+If you need more control over the sending of the message, you can use the
+methods below of the provider class.
+
+.. automethod:: allauth_cas.providers.CASProvider.message_suggest_caslogout_on_logout
+
+.. automethod:: allauth_cas.providers.CASProvider.message_suggest_caslogout_on_logout_level
+
+
+****************************
+Redirection after CAS logout
+****************************
+
+An url is always given for the CAS server to redirect the user to your
+application.
+
+The target of this redirection is:
+
+ * If the link is created on user logout (using above configuration):
+
+ * if present, the url pointed by the GET parameter ``next``, which should
+ be the url the user has just landed after being logged out;
+ * otherwise, the value returned by
+ ``ACCOUNT_ADAPTER.get_logout_redirect_url()``.
+
+ * If the link is created using
+ :meth:`~allauth_cas.providers.CASProvider.add_message_suggest_caslogout`:
+
+ * if present, the value of the parameter ``next_page``;
+ * otherwise, the url of the current page.
+
+ * Otherwise, ``ACCOUNT_ADAPTER.get_logout_redirect_url()``.
+
+.. note::
+
+ If no redirection happens, you should check the version declared by the
+ ``CASAdapter`` class corresponds to the CAS server one.
diff --git a/docs/basic_setup.txt b/docs/basic_setup.txt
new file mode 100644
index 0000000..bea9060
--- /dev/null
+++ b/docs/basic_setup.txt
@@ -0,0 +1,153 @@
+###########
+Basic setup
+###########
+
+Following the instructions on this page, your will create a provider for
+allauth, which allows users to connect through a CAS server.
+
+
+****************
+1. Create an app
+****************
+
+``allauth`` determines available providers by scanning ``INSTALLED_APPS``.
+Let's begin by creating an app for the CAS provider:
+
+.. code-block:: bash
+
+ $ python manage.py startapp mycas
+
+And add it to the ``INSTALLED_APPS``:
+
+.. code-block:: python
+
+ INSTALLED_APPS = [
+ # …
+ 'allauth',
+ 'allauth.account',
+ 'allauth.socialaccount',
+
+ 'allauth_cas',
+
+ 'mycas',
+ ]
+
+
+**********************
+2. Create the provider
+**********************
+
+In ``mycas/provider.py``, create subclasses of ``ProviderAccount`` and
+``CASProvider``.
+
+The ``CASProvider`` subclass defines how to process data returned by the CAS
+server.
+
+.. code-block:: python
+
+ from allauth.socialaccount.providers.base import ProviderAccount
+ from allauth_cas.providers import CASProvider
+
+
+ class MyCASAccount(ProviderAccount):
+ pass
+
+
+ class MyCASProvider(CASProvider):
+ id = 'mycas' # Choose an identifier for your provider
+ name = 'My CAS' # Verbose name of your provider
+ account_class = MyCASAccount
+
+
+ provider_classes = [ClipperProvider]
+
+.. seealso::
+
+ :doc:`advanced/extract_data`
+
+
+*******************
+3. Create the views
+*******************
+
+Subclass ``CASAdapter`` to give your configuration as a CAS client.
+
+.. code-block:: python
+
+ from allauth_cas.views import CASAdapter
+
+ from .providers import MyCASProvider
+
+
+ class MyCASAdapter(CASAdapter):
+ provider_id = MyCASProvider.id
+ url = 'https://mycas.mydomain.net' # The CAS server url
+ version = 3 # Select the CAS protocol version used by the CAS server: 1, 2, 3…
+
+Then, you can simply create the login and callback views.
+
+.. code-block:: python
+
+ from allauth_cas.views import CASCallbackView, CASLoginView
+
+ login = CASLoginView.adapter_view(MyCASAdapter)
+ callback = CASLogoutView.adapter_view(MyCASAdapter)
+
+.. seealso::
+
+ :doc:`advanced/cas_client`
+
+
+******************
+4. Create the urls
+******************
+
+Finally, add the urls in ``mycas/urls.py``.
+
+.. code-block:: python
+
+ from allauth_cas.urls import default_urlpatterns
+
+ from .provider import MyCASProvider
+
+ urlpatterns = default_urlpatterns(MyCasProvider)
+
+There is no need to do more, as ``allauth`` is responsible for including these
+urls.
+
+
+*******************************************
+5. Allow your application at the CAS server
+*******************************************
+
+.. note::
+
+ This step is only required if the CAS server restricts access to known
+ applications.
+
+CAS servers may restrict their usage to a list of known clients. To do so,
+the service url must be known by the CAS server. For our case, the service
+url is the callback url of a CAS provider.
+
+The service url is formatted as:
+
+.. code-block:: none
+
+ ///login/callback/
+
+Assuming a site is served at ``https://mydomain.net``, that the allauth urls
+are included under ``accounts/``, and the provider id is ``mycas``, the service url
+is:
+
+.. code-block:: none
+
+ https://mydomain.net/accounts/mycas/login/callback
+
+While in local development, it can be:
+
+.. code-block:: none
+
+ http://127.0.0.1:8000/accounts/mycas/login/callback
+
+This url should be added to the authorized services within the CAS server
+configuration (by yourself or someone in charge of the server).
diff --git a/docs/changelog.txt b/docs/changelog.txt
new file mode 100644
index 0000000..0d526c7
--- /dev/null
+++ b/docs/changelog.txt
@@ -0,0 +1,5 @@
+#########
+Changelog
+#########
+
+.. include:: ../CHANGELOG.rst
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..5a4587e
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,181 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# django-allauth-cas documentation build configuration file.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import os
+import sys
+
+import django
+
+from allauth_cas import __version__
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+
+sys.path.insert(0, os.path.abspath('../'))
+
+# Setup django to avoid issues with autodoc.
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.settings')
+
+django.setup()
+
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.napoleon',
+ 'sphinx.ext.todo',
+ 'sphinx.ext.viewcode',
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+#
+# source_suffix = ['.rst', '.md']
+source_suffix = '.txt'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = 'django-allauth-cas'
+copyright = '2017, Aurélien Delobelle'
+author = 'Aurélien Delobelle'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = __version__
+# The full version, including alpha/beta/rc tags.
+release = __version__
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This patterns also effect to html_static_path and html_extra_path
+exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = True
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'sphinx_rtd_theme'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#
+# html_theme_options = {}
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Custom sidebar templates, must be a dictionary that maps document names
+# to template names.
+#
+# This is required for the alabaster theme
+# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
+html_sidebars = {
+ '**': [
+ 'relations.html', # needs 'show_related': True theme option to display
+ 'searchbox.html',
+ ]
+}
+
+
+# -- Options for HTMLHelp output ------------------------------------------
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'django-allauth-casdoc'
+
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+ # The paper size ('letterpaper' or 'a4paper').
+ #
+ # 'papersize': 'letterpaper',
+
+ # The font size ('10pt', '11pt' or '12pt').
+ #
+ # 'pointsize': '10pt',
+
+ # Additional stuff for the LaTeX preamble.
+ #
+ # 'preamble': '',
+
+ # Latex figure (float) alignment
+ #
+ # 'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ (master_doc, 'django-allauth-cas.tex', 'django-allauth-cas Documentation',
+ 'Aurélien Delobelle', 'manual'),
+]
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ (master_doc, 'django-allauth-cas', 'django-allauth-cas Documentation',
+ [author], 1)
+]
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ (master_doc, 'django-allauth-cas', 'django-allauth-cas Documentation',
+ author, 'django-allauth-cas', 'One line description of project.',
+ 'Miscellaneous'),
+]
diff --git a/docs/index.txt b/docs/index.txt
new file mode 100644
index 0000000..6fd5b85
--- /dev/null
+++ b/docs/index.txt
@@ -0,0 +1,16 @@
+.. django-allauth-cas documentation master file.
+
+.. include:: ../README.rst
+
+
+********
+Contents
+********
+
+.. toctree::
+ :maxdepth: 2
+
+ basic_setup
+ advanced/index
+
+ changelog
diff --git a/docs/make.bat b/docs/make.bat
new file mode 100644
index 0000000..260f274
--- /dev/null
+++ b/docs/make.bat
@@ -0,0 +1,36 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=.
+set BUILDDIR=_build
+set SPHINXPROJ=django-allauth-cas
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.http://sphinx-doc.org/
+ exit /b 1
+)
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
+
+:end
+popd
diff --git a/setup.py b/setup.py
index bd84887..b256fac 100644
--- a/setup.py
+++ b/setup.py
@@ -20,13 +20,14 @@ setup(
long_description=README,
url='https://github.com/aureplop/django-allauth-cas',
classifiers=[
- 'Development Status :: 3 - Alpha',
+ 'Development Status :: 4 - Beta',
'Environment :: Web Environment',
'Framework :: Django',
'Framework :: Django :: 1.8',
'Framework :: Django :: 1.9',
'Framework :: Django :: 1.10',
'Framework :: Django :: 1.11',
+ 'Framework :: Django :: 2.0',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
@@ -48,6 +49,7 @@ setup(
'six',
],
extras_require={
+ 'docs': ['sphinx'],
'tests': ['tox'],
},
)
diff --git a/tests/test_flows.py b/tests/test_flows.py
index 3c6dc4b..fb9fdbd 100644
--- a/tests/test_flows.py
+++ b/tests/test_flows.py
@@ -12,8 +12,8 @@ User = get_user_model()
class LogoutFlowTests(CASTestCase):
expected_msg_str = (
- "To logout of CAS, please close your browser, or visit this "
- ""
+ "To logout of The Provider, please close your browser, or visit this "
+ ""
"link."
)
@@ -28,43 +28,36 @@ class LogoutFlowTests(CASTestCase):
)
self.assertTemplateNotUsed(
response,
- 'cas_account/messages/logged_out.txt',
+ 'socialaccount/messages/suggest_caslogout.html',
)
@override_settings(SOCIALACCOUNT_PROVIDERS={
'theid': {
- 'MESSAGE_ON_LOGOUT': True,
- 'MESSAGE_ON_LOGOUT_LEVEL': messages.WARNING,
+ 'MESSAGE_SUGGEST_CASLOGOUT_ON_LOGOUT': True,
+ 'MESSAGE_SUGGEST_CASLOGOUT_ON_LOGOUT_LEVEL': messages.WARNING,
},
})
def test_message_on_logout(self):
"""
Message is sent to propose user to logout of CAS.
"""
- r = self.client.post('/accounts/logout/')
+ r = self.client.post('/accounts/logout/?next=/redir/')
r_messages = get_messages(r.wsgi_request)
expected_msg = Message(messages.WARNING, self.expected_msg_str)
self.assertIn(expected_msg, r_messages)
- self.assertTemplateUsed(r, 'cas_account/messages/logged_out.txt')
+ self.assertTemplateUsed(
+ r, 'socialaccount/messages/suggest_caslogout.html')
- @override_settings(SOCIALACCOUNT_PROVIDERS={
- 'theid': {
- 'MESSAGE_ON_LOGOUT': False,
- },
- })
def test_message_on_logout_disabled(self):
- """
- The logout message can be disabled in settings.
- """
r = self.client.post('/accounts/logout/')
self.assertCASLogoutNotInMessages(r)
@override_settings(SOCIALACCOUNT_PROVIDERS={
- 'theid': {'MESSAGE_ON_LOGOUT': True},
+ 'theid': {'MESSAGE_SUGGEST_CASLOGOUT_ON_LOGOUT': True},
})
- def test_default_logout(self):
+ def test_other_logout(self):
"""
The CAS logout message doesn't appear with other login methods.
"""
diff --git a/tests/test_providers.py b/tests/test_providers.py
index abe7bb2..ef68bb7 100644
--- a/tests/test_providers.py
+++ b/tests/test_providers.py
@@ -1,5 +1,11 @@
# -*- coding: utf-8 -*-
+from six.moves.urllib.parse import urlencode
+
from django.contrib import messages
+from django.contrib.messages.api import get_messages
+from django.contrib.messages.middleware import MessageMiddleware
+from django.contrib.messages.storage.base import Message
+from django.contrib.sessions.middleware import SessionMiddleware
from django.test import RequestFactory, TestCase, override_settings
from allauth.socialaccount.providers import registry
@@ -12,12 +18,14 @@ from .example.provider import ExampleCASProvider
class CASProviderTests(TestCase):
def setUp(self):
- factory = RequestFactory()
- request = factory.get('/test/')
- request.session = {}
- self.request = request
+ self.request = self._get_request()
+ self.provider = ExampleCASProvider(self.request)
- self.provider = ExampleCASProvider(request)
+ def _get_request(self):
+ request = RequestFactory().get('/test/')
+ SessionMiddleware().process_request(request)
+ MessageMiddleware().process_request(request)
+ return request
def test_register(self):
"""
@@ -26,10 +34,6 @@ class CASProviderTests(TestCase):
self.assertIsInstance(registry.by_id('theid'), ExampleCASProvider)
def test_get_login_url(self):
- """
- get_login_url returns the url to logout of the provider.
- Keyword arguments are set as query string.
- """
url = self.provider.get_login_url(self.request)
self.assertEqual('/accounts/theid/login/', url)
@@ -43,11 +47,21 @@ class CASProviderTests(TestCase):
'Dwhoam%25C3%25AF'
)
+ def test_get_callback_url(self):
+ url = self.provider.get_callback_url(self.request)
+ self.assertEqual('/accounts/theid/login/callback/', url)
+
+ url_with_qs = self.provider.get_callback_url(
+ self.request,
+ next='/path?quéry=string&two=whoam%C3%AF',
+ )
+ self.assertEqual(
+ url_with_qs,
+ '/accounts/theid/login/callback/?next=%2Fpath%3Fqu%C3%A9ry%3Dstrin'
+ 'g%26two%3Dwhoam%25C3%25AF'
+ )
+
def test_get_logout_url(self):
- """
- get_logout_url returns the url to logout of the provider.
- Keyword arguments are set as query string.
- """
url = self.provider.get_logout_url(self.request)
self.assertEqual('/accounts/theid/logout/', url)
@@ -97,35 +111,90 @@ class CASProviderTests(TestCase):
'next': 'two=whoam%C3%AF&qu%C3%A9ry=string',
})
+ def test_add_message_suggest_caslogout(self):
+ expected_msg_base_str = (
+ "To logout of The Provider, please close your browser, or visit "
+ "this link."
+ )
+
+ # Defaults.
+ req1 = self.request
+
+ self.provider.add_message_suggest_caslogout(req1)
+
+ expected_msg1 = Message(
+ messages.INFO,
+ expected_msg_base_str.format(urlencode({'next': '/test/'})),
+ )
+ self.assertIn(expected_msg1, get_messages(req1))
+
+ # Custom arguments.
+ req2 = self._get_request()
+
+ self.provider.add_message_suggest_caslogout(
+ req2, next_page='/redir/', level=messages.WARNING)
+
+ expected_msg2 = Message(
+ messages.WARNING,
+ expected_msg_base_str.format(urlencode({'next': '/redir/'})),
+ )
+ self.assertIn(expected_msg2, get_messages(req2))
+
+ def test_message_suggest_caslogout_on_logout(self):
+ self.assertFalse(
+ self.provider.message_suggest_caslogout_on_logout(self.request))
+
+ with override_settings(SOCIALACCOUNT_PROVIDERS={
+ 'theid': {'MESSAGE_SUGGEST_CASLOGOUT_ON_LOGOUT': True},
+ }):
+ self.assertTrue(
+ self.provider
+ .message_suggest_caslogout_on_logout(self.request)
+ )
+
@override_settings(SOCIALACCOUNT_PROVIDERS={
'theid': {
- 'MESSAGE_ON_LOGOUT_LEVEL': messages.WARNING,
+ 'MESSAGE_SUGGEST_CASLOGOUT_ON_LOGOUT_LEVEL': messages.WARNING,
},
})
- def test_message_on_logout(self):
- message_on_logout = self.provider.message_on_logout(self.request)
- self.assertTrue(message_on_logout)
-
- message_level = self.provider.message_on_logout_level(self.request)
- self.assertEqual(messages.WARNING, message_level)
+ def test_message_suggest_caslogout_on_logout_level(self):
+ self.assertEqual(messages.WARNING, (
+ self.provider
+ .message_suggest_caslogout_on_logout_level(self.request)
+ ))
def test_extract_uid(self):
- response = 'useRName', {}, None
+ response = 'useRName', {}
uid = self.provider.extract_uid(response)
self.assertEqual('useRName', uid)
def test_extract_common_fields(self):
- response = 'useRName', {}, None
+ response = 'useRName', {}
common_fields = self.provider.extract_common_fields(response)
self.assertDictEqual(common_fields, {
'username': 'useRName',
+ 'first_name': None,
+ 'last_name': None,
+ 'name': None,
+ 'email': None,
+ })
+
+ def test_extract_common_fields_with_extra(self):
+ response = 'useRName', {'username': 'user', 'email': 'user@mail.net'}
+ common_fields = self.provider.extract_common_fields(response)
+ self.assertDictEqual(common_fields, {
+ 'username': 'user',
+ 'first_name': None,
+ 'last_name': None,
+ 'name': None,
+ 'email': 'user@mail.net',
})
def test_extract_extra_data(self):
- attributes = {'user_attr': 'thevalue', 'another': 'value'}
- response = 'useRName', attributes, None
+ response = 'useRName', {'user_attr': 'thevalue', 'another': 'value'}
extra_data = self.provider.extract_extra_data(response)
self.assertDictEqual(extra_data, {
'user_attr': 'thevalue',
'another': 'value',
+ 'uid': 'useRName',
})
diff --git a/tests/test_views.py b/tests/test_views.py
index dcc99af..ebae670 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -48,20 +48,6 @@ class CASAdapterTests(CASTestCase):
service_url = adapter.get_service_url(request)
self.assertEqual(expected, service_url)
- def test_get_callback_url(self):
- expected = '/accounts/theid/login/callback/'
- callback_url = self.adapter.get_callback_url(self.request)
- self.assertEqual(expected, callback_url)
-
- def test_get_callback_url_with_kwargs(self):
- expected = (
- '/accounts/theid/login/callback/?next=%2Fpath%2F'
- )
- callback_url = self.adapter.get_callback_url(self.request, **{
- 'next': '/path/',
- })
- self.assertEqual(expected, callback_url)
-
def test_renew(self):
"""
From an anonymous request, renew is False to let using the single
diff --git a/tox.ini b/tox.ini
index 297f297..eeb6fdb 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,24 +1,29 @@
[tox]
envlist =
- django{18,19,110}-py{27,34,35},
+ django18-py{27,34,35},
+ django19-py{27,34,35}-allauth32,
+ django110-py{27,34,35},
django111-py{27,34,35,36},
django20-py{34,35,36},
+
cov_combine,
flake8,
isort
[testenv]
deps =
+ allauth32: django-allauth>=0.32.0,<0.33.0
+
django18: django>=1.8,<1.9
django19: django>=1.9,<1.10
django110: django>=1.10,<1.11
django111: django>=1.11,<2.0
django20: django>=2.0,<2.1
+
coverage
mock ; python_version < "3.0"
usedevelop = True
commands =
- python -V
coverage run \
--branch \
--source=allauth_cas --omit=*migrations* \