Beta release (with docs)
This commit is contained in:
parent
e3f75a0c9f
commit
5340ef0d1a
26 changed files with 1048 additions and 205 deletions
5
CHANGELOG.rst
Normal file
5
CHANGELOG.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
******************
|
||||||
|
1.0.0 (unreleased)
|
||||||
|
******************
|
||||||
|
|
||||||
|
- First official release.
|
49
README.rst
49
README.rst
|
@ -1,22 +1,51 @@
|
||||||
==================
|
##################
|
||||||
django-allauth-cas
|
django-allauth-cas
|
||||||
==================
|
##################
|
||||||
|
|
||||||
.. image:: https://travis-ci.org/aureplop/django-allauth-cas.svg?branch=master
|
.. 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
|
.. 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_.
|
CAS support for django-allauth_.
|
||||||
|
|
||||||
Supports:
|
Requirements
|
||||||
|
* Django 1.8 → 2.0
|
||||||
|
|
||||||
- Django 1.8-10 - Python 2.7, 3.4-5
|
Dependencies
|
||||||
- Django 1.11 - Python 2.7, 3.4-6
|
* 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
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
__version__ = '0.0.1.dev5'
|
__version__ = '1.0.0b1'
|
||||||
|
|
||||||
default_app_config = 'allauth_cas.apps.CASAccountConfig'
|
default_app_config = 'allauth_cas.apps.CASAccountConfig'
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,9 @@ from six.moves.urllib.parse import parse_qsl
|
||||||
|
|
||||||
import django
|
import django
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
from django.template.loader import render_to_string
|
||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from allauth.socialaccount.providers.base import Provider
|
from allauth.socialaccount.providers.base import Provider
|
||||||
|
|
||||||
|
@ -15,41 +17,197 @@ else:
|
||||||
|
|
||||||
class CASProvider(Provider):
|
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):
|
def get_login_url(self, request, **kwargs):
|
||||||
url = reverse(self.id + '_login')
|
url = reverse(self.id + '_login')
|
||||||
if kwargs:
|
if kwargs:
|
||||||
url += '?' + urlencode(kwargs)
|
url += '?' + urlencode(kwargs)
|
||||||
return url
|
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):
|
def get_logout_url(self, request, **kwargs):
|
||||||
url = reverse(self.id + '_logout')
|
url = reverse(self.id + '_logout')
|
||||||
if kwargs:
|
if kwargs:
|
||||||
url += '?' + urlencode(kwargs)
|
url += '?' + urlencode(kwargs)
|
||||||
return url
|
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
|
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
# -*- coding: utf-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.contrib.auth.signals import user_logged_out
|
||||||
from django.dispatch import receiver
|
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.account.utils import get_next_redirect_url
|
||||||
from allauth.socialaccount import providers
|
from allauth.socialaccount import providers
|
||||||
|
|
||||||
|
@ -16,35 +13,20 @@ from . import CAS_PROVIDER_SESSION_KEY
|
||||||
def cas_account_logout(sender, request, **kwargs):
|
def cas_account_logout(sender, request, **kwargs):
|
||||||
provider_id = request.session.get(CAS_PROVIDER_SESSION_KEY)
|
provider_id = request.session.get(CAS_PROVIDER_SESSION_KEY)
|
||||||
|
|
||||||
if (not provider_id or
|
if not provider_id:
|
||||||
'django.contrib.messages' not in settings.INSTALLED_APPS):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
provider = providers.registry.by_id(provider_id, request)
|
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
|
return
|
||||||
|
|
||||||
redirect_url = (
|
next_page = (
|
||||||
get_next_redirect_url(request) or
|
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 {}
|
provider.add_message_suggest_caslogout(
|
||||||
logout_url = provider.get_logout_url(request, **logout_kwargs)
|
request, next_page=next_page,
|
||||||
logout_link = mark_safe('<a href="{}">link</a>'.format(logout_url))
|
level=provider.message_suggest_caslogout_on_logout_level(request),
|
||||||
|
)
|
||||||
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)
|
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
{% load i18n %}
|
|
||||||
{% blocktrans %}
|
|
||||||
To logout of CAS, please close your browser, or visit this {{ logout_link }}.
|
|
||||||
{% endblocktrans %}
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% blocktrans with provider_name=provider.name %}
|
||||||
|
To logout of {{ provider_name }}, please close your browser, or visit this <a href="{{ logout_url }}">link</a>.
|
||||||
|
{% endblocktrans %}
|
|
@ -1,23 +1,51 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
|
from django.utils.module_loading import import_string
|
||||||
from allauth.utils import import_attribute
|
|
||||||
|
|
||||||
|
|
||||||
def default_urlpatterns(provider):
|
def default_urlpatterns(provider):
|
||||||
package = provider.get_package()
|
package = provider.get_package()
|
||||||
|
|
||||||
login_view = import_attribute(package + '.views.login')
|
try:
|
||||||
callback_view = import_attribute(package + '.views.callback')
|
login_view = import_string(package + '.views.login')
|
||||||
logout_view = import_attribute(package + '.views.logout')
|
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(<LocalCASAdapter>)"
|
||||||
|
.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(<LocalCASAdapter>)"
|
||||||
|
.format(id=provider.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
logout_view = import_string(package + '.views.logout')
|
||||||
|
except ImportError:
|
||||||
|
logout_view = None
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url('^login/$',
|
url('^login/$', login_view,
|
||||||
login_view, name=provider.id + '_login'),
|
name=provider.id + '_login'),
|
||||||
url('^login/callback/$',
|
url('^login/callback/$', callback_view,
|
||||||
callback_view, name=provider.id + '_callback'),
|
name=provider.id + '_callback'),
|
||||||
url('^logout/$',
|
|
||||||
logout_view, name=provider.id + '_logout'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if logout_view is not None:
|
||||||
|
urlpatterns += [
|
||||||
|
url('^logout/$', logout_view,
|
||||||
|
name=provider.id + '_logout'),
|
||||||
|
]
|
||||||
|
|
||||||
return [url('^' + provider.get_slug() + '/', include(urlpatterns))]
|
return [url('^' + provider.get_slug() + '/', include(urlpatterns))]
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import django
|
|
||||||
from django.http import HttpResponseRedirect
|
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.adapter import get_adapter
|
||||||
from allauth.account.utils import get_next_redirect_url
|
from allauth.account.utils import get_next_redirect_url
|
||||||
|
@ -16,11 +15,6 @@ import cas
|
||||||
from . import CAS_PROVIDER_SESSION_KEY
|
from . import CAS_PROVIDER_SESSION_KEY
|
||||||
from .exceptions import CASAuthenticationError
|
from .exceptions import CASAuthenticationError
|
||||||
|
|
||||||
if django.VERSION >= (1, 10):
|
|
||||||
from django.urls import reverse
|
|
||||||
else:
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
|
|
||||||
|
|
||||||
class AuthAction(object):
|
class AuthAction(object):
|
||||||
AUTHENTICATE = 'authenticate'
|
AUTHENTICATE = 'authenticate'
|
||||||
|
@ -29,23 +23,38 @@ class AuthAction(object):
|
||||||
|
|
||||||
|
|
||||||
class CASAdapter(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):
|
def __init__(self, request):
|
||||||
self.request = request
|
self.request = request
|
||||||
|
|
||||||
@property
|
@cached_property
|
||||||
def renew(self):
|
def renew(self):
|
||||||
"""
|
"""Controls presence of ``renew`` in requests to the CAS server.
|
||||||
If user is already authenticated on Django, he may already been
|
|
||||||
connected to CAS, but still may want to use another CAS account.
|
If ``True``, opt out single sign-on (SSO) functionality of the CAS
|
||||||
We set renew to True in this case, as the CAS server won't use the
|
server. So that, user is always prompted for his username and password.
|
||||||
single sign-on.
|
|
||||||
To specifically check, if the current user has used a CAS server,
|
If ``False``, the CAS server does not prompt users for their
|
||||||
we check if the CAS session key is set.
|
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
|
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.
|
Returns a provider instance for the current request.
|
||||||
"""
|
"""
|
||||||
|
@ -53,75 +62,66 @@ class CASAdapter(object):
|
||||||
|
|
||||||
def complete_login(self, request, response):
|
def complete_login(self, request, response):
|
||||||
"""
|
"""
|
||||||
Executed by the callback view after successful authentication on CAS
|
Executed by the callback view after successful authentication on the
|
||||||
server.
|
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()
|
login = self.provider.sociallogin_from_response(request, response)
|
||||||
.sociallogin_from_response(request, response))
|
|
||||||
return login
|
return login
|
||||||
|
|
||||||
def get_service_url(self, request):
|
def get_service_url(self, request):
|
||||||
"""
|
"""The service url, used by the CAS client.
|
||||||
Returns the service url to for a CAS client.
|
|
||||||
|
|
||||||
From CAS specification, the service url is used in order to redirect
|
According to the CAS spec, the service url is passed by the CAS client
|
||||||
user after a successful login on CAS server. Also, service_url sent
|
at several times. It must be the same for all interactions with the CAS
|
||||||
when ticket is verified must be the one for which ticket was issued.
|
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
|
If present, the GET param ``next`` is added to the service url.
|
||||||
parameter to the service url and is latter used by the callback view to
|
|
||||||
redirect user.
|
|
||||||
"""
|
"""
|
||||||
redirect_to = get_next_redirect_url(request)
|
redirect_to = get_next_redirect_url(request)
|
||||||
|
|
||||||
callback_kwargs = {'next': redirect_to} if redirect_to else {}
|
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)
|
service_url = request.build_absolute_uri(callback_url)
|
||||||
|
|
||||||
return service_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):
|
class CASView(object):
|
||||||
|
"""
|
||||||
|
Base class for CAS views.
|
||||||
|
"""
|
||||||
@classmethod
|
@classmethod
|
||||||
def adapter_view(cls, adapter, **kwargs):
|
def adapter_view(cls, adapter):
|
||||||
"""
|
"""Transform the view class into a view function.
|
||||||
Similar to the Django as_view() method.
|
|
||||||
|
|
||||||
It also setups a few things:
|
Similar to the Django ``as_view()`` method.
|
||||||
- given adapter argument will be used in views internals.
|
|
||||||
- if the view execution raises a CASAuthenticationError, the view
|
|
||||||
renders an authentication error page.
|
|
||||||
|
|
||||||
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):
|
Returns:
|
||||||
url = 'https://my.cas.url'
|
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):
|
def view(request, *args, **kwargs):
|
||||||
|
@ -134,7 +134,7 @@ class CASView(object):
|
||||||
|
|
||||||
# Setup and store adapter as view attribute.
|
# Setup and store adapter as view attribute.
|
||||||
self.adapter = adapter(request)
|
self.adapter = adapter(request)
|
||||||
self.provider = self.adapter.get_provider()
|
self.provider = self.adapter.provider
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self.dispatch(request, *args, **kwargs)
|
return self.dispatch(request, *args, **kwargs)
|
||||||
|
@ -207,17 +207,20 @@ class CASCallbackView(CASView):
|
||||||
# - error: None, {}, None
|
# - error: None, {}, None
|
||||||
response = client.verify_ticket(ticket)
|
response = client.verify_ticket(ticket)
|
||||||
|
|
||||||
if not response[0]:
|
uid, extra, _ = response
|
||||||
|
|
||||||
|
if not uid:
|
||||||
raise CASAuthenticationError(
|
raise CASAuthenticationError(
|
||||||
"CAS server doesn't validate the ticket."
|
"CAS server doesn't validate the ticket."
|
||||||
)
|
)
|
||||||
|
|
||||||
# The CAS provider in use is stored to propose to the user to
|
# Keep tracks of the last used CAS provider.
|
||||||
# disconnect from the latter when he logouts.
|
|
||||||
request.session[CAS_PROVIDER_SESSION_KEY] = self.provider.id
|
request.session[CAS_PROVIDER_SESSION_KEY] = self.provider.id
|
||||||
|
|
||||||
# Finish the login flow
|
data = (uid, extra)
|
||||||
login = self.adapter.complete_login(request, response)
|
|
||||||
|
# Finish the login flow.
|
||||||
|
login = self.adapter.complete_login(request, data)
|
||||||
login.state = SocialLogin.unstash_state(request)
|
login.state = SocialLogin.unstash_state(request)
|
||||||
return complete_social_login(request, login)
|
return complete_social_login(request, login)
|
||||||
|
|
||||||
|
@ -242,7 +245,7 @@ class CASLogoutView(CASView):
|
||||||
|
|
||||||
def get_redirect_url(self):
|
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
|
request = self.request
|
||||||
return (
|
return (
|
||||||
|
|
1
docs/.gitignore
vendored
Normal file
1
docs/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
_build/
|
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
|
@ -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)
|
14
docs/README
Normal file
14
docs/README
Normal file
|
@ -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
|
40
docs/advanced/cas_client.txt
Normal file
40
docs/advanced/cas_client.txt
Normal file
|
@ -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
|
21
docs/advanced/extract_data.txt
Normal file
21
docs/advanced/extract_data.txt
Normal file
|
@ -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
|
8
docs/advanced/index.txt
Normal file
8
docs/advanced/index.txt
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
##############
|
||||||
|
Advanced Usage
|
||||||
|
##############
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
cas_client
|
||||||
|
extract_data
|
||||||
|
signout
|
87
docs/advanced/signout.txt
Normal file
87
docs/advanced/signout.txt
Normal file
|
@ -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 = {
|
||||||
|
# …
|
||||||
|
'<provider id>': {
|
||||||
|
# …
|
||||||
|
|
||||||
|
'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.
|
153
docs/basic_setup.txt
Normal file
153
docs/basic_setup.txt
Normal file
|
@ -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
|
||||||
|
|
||||||
|
<url of your application>/<path to allauth urls>/<provider id>/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).
|
5
docs/changelog.txt
Normal file
5
docs/changelog.txt
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
#########
|
||||||
|
Changelog
|
||||||
|
#########
|
||||||
|
|
||||||
|
.. include:: ../CHANGELOG.rst
|
181
docs/conf.py
Normal file
181
docs/conf.py
Normal file
|
@ -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'),
|
||||||
|
]
|
16
docs/index.txt
Normal file
16
docs/index.txt
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
.. django-allauth-cas documentation master file.
|
||||||
|
|
||||||
|
.. include:: ../README.rst
|
||||||
|
|
||||||
|
|
||||||
|
********
|
||||||
|
Contents
|
||||||
|
********
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
basic_setup
|
||||||
|
advanced/index
|
||||||
|
|
||||||
|
changelog
|
36
docs/make.bat
Normal file
36
docs/make.bat
Normal file
|
@ -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
|
4
setup.py
4
setup.py
|
@ -20,13 +20,14 @@ setup(
|
||||||
long_description=README,
|
long_description=README,
|
||||||
url='https://github.com/aureplop/django-allauth-cas',
|
url='https://github.com/aureplop/django-allauth-cas',
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 3 - Alpha',
|
'Development Status :: 4 - Beta',
|
||||||
'Environment :: Web Environment',
|
'Environment :: Web Environment',
|
||||||
'Framework :: Django',
|
'Framework :: Django',
|
||||||
'Framework :: Django :: 1.8',
|
'Framework :: Django :: 1.8',
|
||||||
'Framework :: Django :: 1.9',
|
'Framework :: Django :: 1.9',
|
||||||
'Framework :: Django :: 1.10',
|
'Framework :: Django :: 1.10',
|
||||||
'Framework :: Django :: 1.11',
|
'Framework :: Django :: 1.11',
|
||||||
|
'Framework :: Django :: 2.0',
|
||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
'License :: OSI Approved :: MIT License',
|
'License :: OSI Approved :: MIT License',
|
||||||
'Operating System :: OS Independent',
|
'Operating System :: OS Independent',
|
||||||
|
@ -48,6 +49,7 @@ setup(
|
||||||
'six',
|
'six',
|
||||||
],
|
],
|
||||||
extras_require={
|
extras_require={
|
||||||
|
'docs': ['sphinx'],
|
||||||
'tests': ['tox'],
|
'tests': ['tox'],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,8 +12,8 @@ User = get_user_model()
|
||||||
|
|
||||||
class LogoutFlowTests(CASTestCase):
|
class LogoutFlowTests(CASTestCase):
|
||||||
expected_msg_str = (
|
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 "
|
||||||
"<a href=\"/accounts/theid/logout/?next=%2Faccounts%2Flogout%2F\">"
|
"<a href=\"/accounts/theid/logout/?next=%2Fredir%2F\">"
|
||||||
"link</a>."
|
"link</a>."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -28,43 +28,36 @@ class LogoutFlowTests(CASTestCase):
|
||||||
)
|
)
|
||||||
self.assertTemplateNotUsed(
|
self.assertTemplateNotUsed(
|
||||||
response,
|
response,
|
||||||
'cas_account/messages/logged_out.txt',
|
'socialaccount/messages/suggest_caslogout.html',
|
||||||
)
|
)
|
||||||
|
|
||||||
@override_settings(SOCIALACCOUNT_PROVIDERS={
|
@override_settings(SOCIALACCOUNT_PROVIDERS={
|
||||||
'theid': {
|
'theid': {
|
||||||
'MESSAGE_ON_LOGOUT': True,
|
'MESSAGE_SUGGEST_CASLOGOUT_ON_LOGOUT': True,
|
||||||
'MESSAGE_ON_LOGOUT_LEVEL': messages.WARNING,
|
'MESSAGE_SUGGEST_CASLOGOUT_ON_LOGOUT_LEVEL': messages.WARNING,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
def test_message_on_logout(self):
|
def test_message_on_logout(self):
|
||||||
"""
|
"""
|
||||||
Message is sent to propose user to logout of CAS.
|
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)
|
r_messages = get_messages(r.wsgi_request)
|
||||||
|
|
||||||
expected_msg = Message(messages.WARNING, self.expected_msg_str)
|
expected_msg = Message(messages.WARNING, self.expected_msg_str)
|
||||||
|
|
||||||
self.assertIn(expected_msg, r_messages)
|
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):
|
def test_message_on_logout_disabled(self):
|
||||||
"""
|
|
||||||
The logout message can be disabled in settings.
|
|
||||||
"""
|
|
||||||
r = self.client.post('/accounts/logout/')
|
r = self.client.post('/accounts/logout/')
|
||||||
self.assertCASLogoutNotInMessages(r)
|
self.assertCASLogoutNotInMessages(r)
|
||||||
|
|
||||||
@override_settings(SOCIALACCOUNT_PROVIDERS={
|
@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.
|
The CAS logout message doesn't appear with other login methods.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from six.moves.urllib.parse import urlencode
|
||||||
|
|
||||||
from django.contrib import messages
|
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 django.test import RequestFactory, TestCase, override_settings
|
||||||
|
|
||||||
from allauth.socialaccount.providers import registry
|
from allauth.socialaccount.providers import registry
|
||||||
|
@ -12,12 +18,14 @@ from .example.provider import ExampleCASProvider
|
||||||
class CASProviderTests(TestCase):
|
class CASProviderTests(TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
factory = RequestFactory()
|
self.request = self._get_request()
|
||||||
request = factory.get('/test/')
|
self.provider = ExampleCASProvider(self.request)
|
||||||
request.session = {}
|
|
||||||
self.request = 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):
|
def test_register(self):
|
||||||
"""
|
"""
|
||||||
|
@ -26,10 +34,6 @@ class CASProviderTests(TestCase):
|
||||||
self.assertIsInstance(registry.by_id('theid'), ExampleCASProvider)
|
self.assertIsInstance(registry.by_id('theid'), ExampleCASProvider)
|
||||||
|
|
||||||
def test_get_login_url(self):
|
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)
|
url = self.provider.get_login_url(self.request)
|
||||||
self.assertEqual('/accounts/theid/login/', url)
|
self.assertEqual('/accounts/theid/login/', url)
|
||||||
|
|
||||||
|
@ -43,11 +47,21 @@ class CASProviderTests(TestCase):
|
||||||
'Dwhoam%25C3%25AF'
|
'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):
|
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)
|
url = self.provider.get_logout_url(self.request)
|
||||||
self.assertEqual('/accounts/theid/logout/', url)
|
self.assertEqual('/accounts/theid/logout/', url)
|
||||||
|
|
||||||
|
@ -97,35 +111,90 @@ class CASProviderTests(TestCase):
|
||||||
'next': 'two=whoam%C3%AF&qu%C3%A9ry=string',
|
'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 <a href=\"/accounts/theid/logout/?{}\">link</a>."
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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={
|
@override_settings(SOCIALACCOUNT_PROVIDERS={
|
||||||
'theid': {
|
'theid': {
|
||||||
'MESSAGE_ON_LOGOUT_LEVEL': messages.WARNING,
|
'MESSAGE_SUGGEST_CASLOGOUT_ON_LOGOUT_LEVEL': messages.WARNING,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
def test_message_on_logout(self):
|
def test_message_suggest_caslogout_on_logout_level(self):
|
||||||
message_on_logout = self.provider.message_on_logout(self.request)
|
self.assertEqual(messages.WARNING, (
|
||||||
self.assertTrue(message_on_logout)
|
self.provider
|
||||||
|
.message_suggest_caslogout_on_logout_level(self.request)
|
||||||
message_level = self.provider.message_on_logout_level(self.request)
|
))
|
||||||
self.assertEqual(messages.WARNING, message_level)
|
|
||||||
|
|
||||||
def test_extract_uid(self):
|
def test_extract_uid(self):
|
||||||
response = 'useRName', {}, None
|
response = 'useRName', {}
|
||||||
uid = self.provider.extract_uid(response)
|
uid = self.provider.extract_uid(response)
|
||||||
self.assertEqual('useRName', uid)
|
self.assertEqual('useRName', uid)
|
||||||
|
|
||||||
def test_extract_common_fields(self):
|
def test_extract_common_fields(self):
|
||||||
response = 'useRName', {}, None
|
response = 'useRName', {}
|
||||||
common_fields = self.provider.extract_common_fields(response)
|
common_fields = self.provider.extract_common_fields(response)
|
||||||
self.assertDictEqual(common_fields, {
|
self.assertDictEqual(common_fields, {
|
||||||
'username': 'useRName',
|
'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):
|
def test_extract_extra_data(self):
|
||||||
attributes = {'user_attr': 'thevalue', 'another': 'value'}
|
response = 'useRName', {'user_attr': 'thevalue', 'another': 'value'}
|
||||||
response = 'useRName', attributes, None
|
|
||||||
extra_data = self.provider.extract_extra_data(response)
|
extra_data = self.provider.extract_extra_data(response)
|
||||||
self.assertDictEqual(extra_data, {
|
self.assertDictEqual(extra_data, {
|
||||||
'user_attr': 'thevalue',
|
'user_attr': 'thevalue',
|
||||||
'another': 'value',
|
'another': 'value',
|
||||||
|
'uid': 'useRName',
|
||||||
})
|
})
|
||||||
|
|
|
@ -48,20 +48,6 @@ class CASAdapterTests(CASTestCase):
|
||||||
service_url = adapter.get_service_url(request)
|
service_url = adapter.get_service_url(request)
|
||||||
self.assertEqual(expected, service_url)
|
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):
|
def test_renew(self):
|
||||||
"""
|
"""
|
||||||
From an anonymous request, renew is False to let using the single
|
From an anonymous request, renew is False to let using the single
|
||||||
|
|
9
tox.ini
9
tox.ini
|
@ -1,24 +1,29 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist =
|
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},
|
django111-py{27,34,35,36},
|
||||||
django20-py{34,35,36},
|
django20-py{34,35,36},
|
||||||
|
|
||||||
cov_combine,
|
cov_combine,
|
||||||
flake8,
|
flake8,
|
||||||
isort
|
isort
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
deps =
|
deps =
|
||||||
|
allauth32: django-allauth>=0.32.0,<0.33.0
|
||||||
|
|
||||||
django18: django>=1.8,<1.9
|
django18: django>=1.8,<1.9
|
||||||
django19: django>=1.9,<1.10
|
django19: django>=1.9,<1.10
|
||||||
django110: django>=1.10,<1.11
|
django110: django>=1.10,<1.11
|
||||||
django111: django>=1.11,<2.0
|
django111: django>=1.11,<2.0
|
||||||
django20: django>=2.0,<2.1
|
django20: django>=2.0,<2.1
|
||||||
|
|
||||||
coverage
|
coverage
|
||||||
mock ; python_version < "3.0"
|
mock ; python_version < "3.0"
|
||||||
usedevelop = True
|
usedevelop = True
|
||||||
commands =
|
commands =
|
||||||
python -V
|
|
||||||
coverage run \
|
coverage run \
|
||||||
--branch \
|
--branch \
|
||||||
--source=allauth_cas --omit=*migrations* \
|
--source=allauth_cas --omit=*migrations* \
|
||||||
|
|
Loading…
Reference in a new issue