Eww, hard to sum up…

- Update django-allauth-cas to the last version.
- Add docs (README, example/README).
- Add tests for Clipper provider.
- Add tests to check templates do not contain syntax error.
- Add the last missing templates to override all allauth's displayable
  templates.
- Improve stylesheets.
This commit is contained in:
Aurélien Delobelle 2018-01-02 17:06:12 +01:00
parent 4cdbb049df
commit fe21f9c6af
58 changed files with 1304 additions and 4566 deletions

5
CHANGELOG.rst Normal file
View file

@ -0,0 +1,5 @@
******************
1.0.0 (unreleased)
******************
- First official release.

View file

@ -2,4 +2,3 @@
source "https://rubygems.org" source "https://rubygems.org"
gem 'compass' gem 'compass'
gem 'font-awesome-sass', :git => "https://github.com/TruePath/font-awesome-sass.git", :branch => "patch-1"

View file

@ -1,11 +1,3 @@
GIT
remote: https://github.com/TruePath/font-awesome-sass.git
revision: b3974fe0632d09a7744c4a3b42f4ccf8dc9f919e
branch: patch-1
specs:
font-awesome-sass (4.7.0)
sass (>= 3.2)
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
@ -23,7 +15,7 @@ GEM
compass-import-once (1.0.5) compass-import-once (1.0.5)
sass (>= 3.2, < 3.5) sass (>= 3.2, < 3.5)
ffi (1.9.18) ffi (1.9.18)
multi_json (1.12.1) multi_json (1.12.2)
rb-fsevent (0.10.2) rb-fsevent (0.10.2)
rb-inotify (0.9.10) rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2) ffi (>= 0.5.0, < 2)
@ -34,7 +26,6 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
compass compass
font-awesome-sass!
BUNDLED WITH BUNDLED WITH
1.15.3 1.16.1

View file

@ -1,8 +1,259 @@
================== ##################
django-allauth-ens django-allauth-ens
================== ##################
Providers for django-allauth_ allowing using the ENS' auth-systems. This package is meant to ease the management of authentication of django-apps
at the ENS.
On top of django-allauth_, which provides easy ways to configure the
authentication of django-apps, this package provides:
* social authentication using Clipper (*cas.eleves*);
* ready-to-use templates in replacement of allauth' ones;
* helpers to use *allauth*'s login and logout views instead of those
provided by third-parties (Django admin, wagtail, *etc*).
.. _django-allauth: https://www.intenct.nl/projects/django-allauth/ **Contents**
.. contents:: :local:
************
Installation
************
First, `install django-allauth`_.
Then, install *django-allauth-ens*:
.. code-block:: bash
$ pip install django-allauth-ens
And edit your settings file:
.. code-block:: python
INSTALLED_APPS = [
# …
# Above allauth to replace its templates.
'allauth_ens',
# Added when you installed allauth.
'allauth',
'allauth.account',
'allauth.socialaccount',
# Required to use CAS-based providers (e.g. Clipper).
'allauth_cas',
# …
]
*************
Configuration
*************
See also the `allauth configuration`_ and `advanced usage`_ docs pages.
``ACCOUNT_HOME_URL``
*Optional* — A view name or an url path.
Used as a link from the templates of ``allauth_ens`` to return to your
application.
**Examples:** ``'home'``, ``'/home/'``
``ACCOUNT_DETAILS_URL``
*Optional* — A view name or an url path.
Used as a link from the templates of ``allauth_ens`` for a logged in user to
access their profile in your app.
**Examples:** ``'my-account'``, ``'/my-account/'``
*****
Views
*****
Capture other login and logout views
====================================
You can use the ``capture_login`` and ``capture_logout`` views to replace the
login and logout views of other applications. They redirect to their similar
*allauth*'s view and forward the query string, so that if a GET parameter
``next`` is given along the initial request, user is redirected to this url on
successful login and logout.
This requires to add urls before the include of the app' urls.
For example, to replace the Django admin login and logout views with allauth's
ones:
.. code-block:: python
from allauth_ens.views import capture_login, capture_logout
urlpatterns = [
# …
# Add it before include of admin urls.
url(r'^admin/login/$', capture_login),
url(r'^admin/logout/$', capture_logout),
url(r'^admin/$', include(admin.site.urls)),
# …
]
*********
Templates
*********
The templates provided by *allauth* only contains the bare minimum. Hopefully,
this package includes ready-to-use templates. They are automatically used if
you put ``'allauth_ens'`` before ``'allauth'`` in your ``INSTALLED_APPS``,
*********
Providers
*********
*Google, Facebook¸ but also Clipper…*
To interact with an external authentication service, you must add the
corresponding provider application to your ``INSTALLED_APPS``.
*allauth* already includes `several providers`_ (see also `their python path`_).
In addition to that, this package adds the following providers:
Clipper
=======
It uses the CAS server `<https://cas.eleves.ens.fr/>`_.
Installation
Add ``'allauth_ens.providers.clipper'`` to the ``INSTALLED_APPS``.
Configuration
Available settings and their default value:
.. code-block:: python
SOCIALACCOUNT_PROVIDERS = {
# …
'clipper': {
# These settings control whether a message containing a link to
# disconnect from the CAS server is added when users log out.
'MESSAGE_SUGGEST_LOGOUT_ON_LOGOUT': True,
'MESSAGE_SUGGEST_LOGOUT_ON_LOGOUT_LEVEL': messages.INFO,
},
}
Auto-signup
Poulated data
- username: ``<clipper>``
- email (primary and verified): ``<clipper>@clipper.ens.fr``
*********
Demo Site
*********
See ``example/README``.
***********
Development
***********
First, you need to clone the repository.
Stylesheets
===========
This project uses `compass`_ to compile SCSS files to CSS.
Using bundler
-------------
Requirements
* Ensure Ruby is installed (``$ ruby -v``) or `install Ruby`_
* Ensure bundler is installed (``$ bundle -v``) or install bundler
(``$ gem install bundler``)
* Install dependencies: ``$ bundle install``
Compile
* Watch changes and recompile: ``$ bundle exec compass watch``
Tests
=====
Local environment
-----------------
``$ ./runtests.py``
All
---
Requirements
* tox, install with ``$ pip install tox``
* ``python{2.7,3.4,3.5,3.6}`` must be available on your system path
Run
* all (django/python with combined coverage + flake8 + isort): ``$ tox``
******
Howtos
******
Assuming you use the following settings (when needed):
.. code-block:: python
ACCOUNT_ADAPTER = 'shared.allauth_adapter.AccountAdapter'
SOCIALACCOUNT_ADAPTER = 'shared.allauth_adapter.SocialAccountAdapter'
Signup disabled, except for clipper provider (auto-signup)
==========================================================
In ``shared/allauth_adapter.py``:
.. code-block:: python
class AccountAdapter(DefaultAccountAdapter):
def is_open_for_signup(self, request):
return False
class SocialAccountAdapter(DefaultSocialAccountAdapter):
def is_open_for_signup(self, request, sociallogin):
# sociallogin.account is a SocialAccount instance.
# See https://github.com/pennersr/django-allauth/blob/master/allauth/socialaccount/models.py
if sociallogin.account.provider == 'clipper':
return True
# It returns AccountAdapter.is_open_for_signup().
# See https://github.com/pennersr/django-allauth/blob/master/allauth/socialaccount/adapter.py
return super().is_open_for_signup(request, sociallogin)
.. _django-allauth: https://django-allauth.readthedocs.io/en/latest/overview.html
.. _install django-allauth: https://django-allauth.readthedocs.io/en/latest/installation.html
.. _several providers: https://django-allauth.readthedocs.io/en/latest/providers.html
.. _allauth configuration: https://django-allauth.readthedocs.io/en/latest/configuration.html
.. _advanced usage: https://django-allauth.readthedocs.io/en/latest/advanced.html
.. _their python path: https://django-allauth.readthedocs.io/en/latest/installation.html
.. _compass: https://compass-style.org/
.. _install Ruby: https://www.ruby-lang.org/en/documentation/installation/

View file

@ -1,3 +1,3 @@
__version__ = '0.0.1.dev1' __version__ = '0.0.1.dev1'
default_app_config = 'allauth_ens.apps.ENSAllauthAppConfig' default_app_config = 'allauth_ens.apps.ENSAllauthConfig'

View file

@ -2,6 +2,6 @@ from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
class ENSAllauthAppConfig(AppConfig): class ENSAllauthConfig(AppConfig):
name = 'allauth_ens' name = 'allauth_ens'
verbose_name = _("ENS Authentication") verbose_name = _("ENS Authentication")

View file

@ -15,8 +15,8 @@ class ClipperProvider(CASProvider):
account_class = ClipperAccount account_class = ClipperAccount
def extract_email(self, data): def extract_email(self, data):
username, _, _ = data uid, extra = data
return '{}@clipper.ens.fr'.format(username) return '{}@clipper.ens.fr'.format(uid.strip().lower())
def extract_common_fields(self, data): def extract_common_fields(self, data):
common = super(ClipperProvider, self).extract_common_fields(data) common = super(ClipperProvider, self).extract_common_fields(data)
@ -24,20 +24,23 @@ class ClipperProvider(CASProvider):
return common return common
def extract_email_addresses(self, data): def extract_email_addresses(self, data):
email = self.extract_email(data)
return [ return [
EmailAddress( EmailAddress(
email=email, email=self.extract_email(data),
verified=True, verified=True, primary=True,
primary=True, ),
)
] ]
def extract_extra_data(self, data): def extract_extra_data(self, data):
extra = super(ClipperProvider, self).extract_extra_data(data) extra_data = super(ClipperProvider, self).extract_extra_data(data)
extra['username'] = data[0] extra_data['email'] = self.extract_email(data)
extra['email'] = self.extract_email(data) return extra_data
return extra
def message_suggest_caslogout_on_logout(self, request):
return (
self.get_settings()
.get('MESSAGE_SUGGEST_CASLOGOUT_ON_LOGOUT', True)
)
provider_classes = [ClipperProvider] provider_classes = [ClipperProvider]

View file

@ -1,4 +1,21 @@
from allauth_cas.test.testcases import CASViewTestCase from django.contrib.auth import get_user_model
from allauth_cas.test.testcases import CASTestCase, CASViewTestCase
User = get_user_model()
class ClipperProviderTests(CASTestCase):
def setUp(self):
self.u = User.objects.create_user('user', 'user@mail.net', 'user')
def test_auto_signup(self):
self.client_cas_login(
self.client, provider_id='clipper', username='clipper_uid')
u = User.objects.get(username='clipper_uid')
self.assertEqual(u.email, 'clipper_uid@clipper.ens.fr')
class ClipperViewsTests(CASViewTestCase): class ClipperViewsTests(CASViewTestCase):
@ -15,6 +32,10 @@ class ClipperViewsTests(CASViewTestCase):
) )
def test_callback_view(self): def test_callback_view(self):
# Required to initialize a SocialLogin.
r = self.client.get('/accounts/clipper/login/')
# Tests.
self.patch_cas_response(valid_ticket='__all__') self.patch_cas_response(valid_ticket='__all__')
r = self.client.get('/accounts/clipper/login/callback/', { r = self.client.get('/accounts/clipper/login/callback/', {
'ticket': '123456', 'ticket': '123456',

View file

@ -27,16 +27,14 @@ b {
* Layout structure * * Layout structure *
********************/ ********************/
$main-max-width: 700px; $main-max-width: 500px;
$divider-size: 2px;
.wrapper { .wrapper {
max-width: $main-max-width; max-width: $main-max-width;
margin: 0 auto; margin: 0 auto;
background: $white; background: $white;
box-shadow: 0 0 10px $gray-lighter; box-shadow: 0 0 5px rgba(0,0,0,.1);
} }
@ -65,49 +63,24 @@ $divider-size: 2px;
padding: 15px; padding: 15px;
} }
@media (min-width: 576px) { @media (min-width: 500px) {
& > section { & > section {
flex: 1 1 auto; flex: 1 1 auto;
width: 350px - $divider-size / 2; width: 250px;
} }
} }
/* Divider */ @media (min-width: 120px) {
& > section {
& > .divider { flex: 1 1 auto;
display: none; width: 350px;
&::before {
display: block;
content: " ";
background: $gray-lighter;
height: $divider-size;
width: $divider-size;
}
@media (max-width: 575px) {
& {
flex: 100%;
padding: 0 15px;
}
&::before { width: 100%; }
}
@media (min-width: 576px) {
& {
align-self: stretch;
padding: 15px 0;
}
&::before { height: 100%; }
} }
} }
& > section + .divider {
display: block;
} }
#providers {
width:150px;
} }
@ -132,23 +105,6 @@ header {
font-size: 20px; font-size: 20px;
font-weight: bold; font-weight: bold;
.history-back {
@include transition;
width: 55px;
cursor: pointer;
background: transparent;
color: $white;
font-size: $header-history-icon-size;
@include hover-focus {
background: lighten($header-bg, 5%);
}
}
a { a {
color: $white !important; color: $white !important;
@ -159,42 +115,41 @@ header {
} }
.right { .right {
border-left: 1px solid lighten($header-bg, 15%);
display: flex; display: flex;
flex-flow: column; flex-flow: column;
align-items: stretch; align-items: stretch;
justify-content: space-around; justify-content: space-around;
flex: 0 0 auto;
border-left: 1px solid lighten($header-bg, 15%);
font-size: 14px; font-size: 14px;
text-align: center; text-align: center;
& > * { & > * {
flex: 0 0 auto; flex: 0 0 auto;
line-height: 28px;
& > * {
display: block;
padding: 5px 10px;
}
} }
& > * > * { #connect-status {
display: inline-block;
height: 100%;
width: 100%;
padding: 0 15px;
}
& #connect-status {
font-weight: normal; font-weight: normal;
font-size: 12px;
.fa { .fa {
margin-right: 10px; margin-right: 5px;
} }
} }
} }
h1 { h1 {
flex: 1 0 auto; flex: 1 1 auto;
padding: 15px 35px; padding: 15px 25px;
line-height: 25px;
} }
} }
@ -252,128 +207,92 @@ section {
/* Methods list */ /* Methods list */
$space-between: 15px;
.method-list { .method-list {
display: flex; display: flex;
flex-flow: row wrap; flex-flow: row wrap;
justify-content: space-between; justify-content: space-between;
margin: - $space-between / 2;
}
.method-wrapper { & > .method-wrapper {
flex: 1 50%; flex: 1 100%;
padding: $space-between / 2; padding: 2px 0;
a { a {
@include btn; @include btn;
@include btn-primary-hov;
display: block; border-radius: 0;
border-left: 5px solid $brand-primary;
background: $gray-lighter;
color: $black;
font-size: 16px;
text-align: left;
@include hover-focus {
background: lighten($brand-primary, 50%);
}
}
} }
} }
/* Connected accounts list */ /* Connected accounts list */
.provider-list { .connections-providers-list {
& > li { & > * {
&:not(:first-child) { & + * {
margin-top: 5px; margin-top: 2px;
} }
& > .heading { & > .heading {
@include clearfix; @include clearfix;
height: 45px;
width: 100%; width: 100%;
padding: 10px;
background-color: $gray-lighter; border-left: 5px solid $brand-primary;
background: $gray-lighter;
.connect {
@include btn;
@include btn-primary;
@include btn-sm;
float: right;
width: auto;
}
}
}
}
.connections-list {
border-left: 5px solid $gray-lightest;
& > * { & > * {
float: left;
height: 100%;
}
& > .connect, & > .brand-icon {
text-align: center;
}
& > .connect a {
@include transition;
display: inline-block;
background-color: $brand-success;
color: $white;
height: 100%;
line-height: 100%;
width: 45px;
padding: 12px;
font-size: 20px;
text-align: center;
@include hover-focus {
background-color: darken($brand-success, 5%);
text-decoration: none;
}
}
& > .brand-icon {
padding: 10px; padding: 10px;
width: 45px; font-size: 14px;
color: $brand-primary;
& + * {
border-top: 1px dotted $gray-lighter;
} }
& > .name { & > .fa {
padding: 10px;
font-weight: bold;
}
}
& > .connected-list {
padding: 0 15px;
& > li {
@include clearfix;
height: 30px;
& > * {
float: left;
display: block;
height: 100%;
}
& > .connected-delete [type=submit] {
background-color: $red;
color: $white;
min-height: 30px;
width: 30px;
@include hover-focus {
background-color: darken($red, 5%);
}
}
& > .connected-label {
padding: 7px 15px;
width: calc(100% - 30px);
border-bottom: 1px solid $red;
font-size: 12px;
.fa {
margin-right: 5px; margin-right: 5px;
} }
.delete {
float: right;
margin-top: -2px;
& [type=submit] {
@include btn;
@include btn-danger;
@include btn-sm;
opacity: .8;
} }
} }
} }
}
form { form {
display: inline-block; display: inline-block;
} }
@ -412,7 +331,7 @@ $space-between: 15px;
} }
& > .primary { & > .primary {
color: darken($brand-primary, 15%); color: $brand-primary;
} }
& > .verified { & > .verified {
@ -424,19 +343,6 @@ $space-between: 15px;
} }
} }
.actions {
@include clearfix;
margin-bottom: 10px;
& > * {
float: right;
margin-right: 10px;
font-size: 12px;
}
}
} }
} }
@ -557,9 +463,9 @@ $label-top: $label-height + $input-wrapper-padding + $input-padding;
} }
} }
@include input-special('has-value', $green); @include input-special('has-value', $brand-success);
@include input-special('error', $red); @include input-special('error', $brand-danger);
@include input-special('focused', $blue); @include input-special('focused', $brand-primary);
.infos-spacer { .infos-spacer {
float: right; float: right;
@ -577,16 +483,47 @@ $label-top: $label-height + $input-wrapper-padding + $input-padding;
} }
.widget-checkbox {
display: inline-flex;
& > input[type="checkbox"] {
display: none;
}
& > button {
@include transition;
flex: 0 1 auto;
display: inline-block;
padding: 5px 10px;
background: white;
color: $gray-light;
}
& > button.choice-yes {
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
& > button.choice-no {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
& > button.focus {
background: $brand-primary;
color: white;
}
}
[type=submit]:not(.link) { [type=submit]:not(.link) {
@include btn; @include btn;
@include btn-success-hov; @include btn-success;
float: right; border-radius: 3px;
} }
[type=submit].link { [type=submit].link {
@include link; @include link;
background: transparent; background: transparent;
padding: 0;
@include hover-focus { @include hover-focus {
cursor: pointer; cursor: pointer;
@ -595,13 +532,15 @@ $label-top: $label-height + $input-wrapper-padding + $input-padding;
.form-inline { .form-inline {
display: flex; display: flex;
flex-flow: row nowrap;
align-items: center; align-items: center;
.input-list { & > .input-wrapper {
flex: 1 0 auto;
} }
[type=submit] { [type=submit] {
margin-top: -5px;
margin-left: 8px;
font-size: 14px; font-size: 14px;
} }
} }
@ -611,6 +550,5 @@ $label-top: $label-height + $input-wrapper-padding + $input-padding;
display: block; display: block;
} }
.btn-primary-hov { .btn-primary { @include btn-primary; }
@include btn-primary-hov; .btn-success { @include btn-success; }
}

View file

@ -30,8 +30,8 @@
} }
} }
@mixin transition($time: .3s) { @mixin transition($time: .15s) {
transition: background $time, color $time; transition: background $time, color $time, border-color $time;
} }
@mixin link { @mixin link {
@ -50,13 +50,15 @@
@mixin btn { @mixin btn {
@include transition; @include transition;
//width: 100%; display: block;
width: 100%;
min-height: 30px; min-height: 30px;
line-height: initial;
border: 0; border: 0;
padding: 10px 15px; border-radius: 3px;
padding: 7px 15px;
font-family: "Roboto Slab";
font-size: 18px; font-size: 18px;
text-align: center; text-align: center;
@ -67,23 +69,38 @@
} }
} }
@mixin btn-primary-hov { @mixin btn-primary {
color: $black; background: $brand-primary;
color: $white;
@include hover-focus { @include hover-focus {
background: darken($brand-primary, 15%); background: darken($brand-primary, 5%);
color: $white; color: $white;
} }
} }
@mixin btn-success-hov { @mixin btn-success {
background-color: $brand-success; background: $brand-success;
color: $white; color: $white;
//background: $gray-lighter;
//color: $black;
@include hover-focus { @include hover-focus {
background: darken($brand-success, 15%); background: darken($brand-success, 15%);
color: $white; color: $white;
} }
} }
@mixin btn-danger {
background: $brand-danger;
color: $white;
@include hover-focus {
background: darken($brand-danger, 15%);
color: $white;
}
}
@mixin btn-sm {
min-height: auto;
padding: 4px 7px;
font-size: 12px;
}

View file

@ -13,6 +13,7 @@ a, input, button {
} }
input, button { input, button {
padding: 0;
border: 0; border: 0;
border-radius: 0; border-radius: 0;
box-shadow: none; box-shadow: none;

View file

@ -17,10 +17,10 @@ $gray-light: #636c72 !default;
$gray-lighter: #eceeef !default; $gray-lighter: #eceeef !default;
$gray-lightest: #f7f7f9 !default; $gray-lightest: #f7f7f9 !default;
$brand-primary: $blue !default; $brand-primary: darken($blue, 10%) !default;
$brand-success: $green !default; $brand-success: darken($green, 10%) !default;
$brand-info: $teal !default; $brand-info: $teal !default;
$brand-warning: $orange !default; $brand-warning: darken($orange, 10%) !default;
$brand-danger: $red !default; $brand-danger: $red !default;
$brand-inverse: $gray-dark !default; $brand-inverse: $gray-dark !default;

View file

@ -1,16 +1,6 @@
/* Welcome to Compass.
* In this file you should write your main styles. (or centralize your imports)
* Import this file using the following HTML or equivalent:
* <link href="/stylesheets/screen.css" media="screen, projection" rel="stylesheet" type="text/css" /> */
// @import "vendor/normalize";
// @import "reset";
@import "compass/reset"; @import "compass/reset";
@import "reset"; @import "reset";
@import "font-awesome-compass";
@import "font-awesome";
@import "variables"; @import "variables";
@import "mixins"; @import "mixins";

View file

@ -40,9 +40,15 @@ Input.prototype = {
} }
}; };
Object.assign(Input.prototype, { $.extend(Input.prototype, {
update_focus: toggleWrapperClass('input-focused', Input.prototype.has_focus), update_focus: toggleWrapperClass('input-focused', Input.prototype.has_focus),
update_error: toggleWrapperClass('input-error', Input.prototype.has_error), update_error: function () {
let has_error = this.has_error();
toggleWrapperClass('input-error').bind(this)(has_error);
if (!has_error) {
this.wrapper.find('.messages .error-desc').hide();
}
},
update_has_value: toggleWrapperClass('input-has-value', Input.prototype.has_value), update_has_value: toggleWrapperClass('input-has-value', Input.prototype.has_value),
}); });
@ -52,6 +58,23 @@ $( function() {
fields.map( function() { return new Input(this); }); fields.map( function() { return new Input(this); });
}); });
$(function () {
let choice_yes = $('.choice-yes');
let choice_no = $('.choice-no');
choice_yes.click(function () {
$(this).siblings('input').prop('checked', true);
$(this).addClass('focus');
$(this).siblings('.choice-no').removeClass('focus');
});
choice_no.click(function () {
$(this).siblings('input').prop('checked', true);
$(this).addClass('focus');
$(this).siblings('.choice-yes').removeClass('focus');
});
});
/** /**
* Keyboard shortcuts * Keyboard shortcuts

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,18 @@
{% extends "allauth_ens/base.html" %}
{% load i18n %}
{% block header-title %}{% trans "Account Inactive" %}{% endblock %}
{% block title %}{% trans "Account Inactive" %}{% endblock %}
{% block content %}
<section>
<p>
{% blocktrans %}
This account is inactive.
{% endblocktrans %}
</p>
</section>
{% endblock %}

View file

@ -1,29 +0,0 @@
{% load widget_tweaks %}
{% csrf_token %}
<ul class="input-list">
{% for field in form %}
{% with field|field_type as type %}
<li class="input-wrapper {% if type == "booleanfield" %}input-skip{% endif %}">
{% if type == "booleanfield" %}
<label for="{{ field.id_for_label }}">
{{ field.label }}
{% render_field field class+="field" %}
</label>
{% else %}
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{% render_field field class+="field" autocomplete="off" autocapitalize="none" placeholder="" %}
{% endif %}
<div class="infos-spacer"></div>
<ul class="messages">
{% if field.help_text %}
<li>{{ field.help_text|safe }}</li>
{% endif %}
{% for error in field.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</li>
{% endwith %}
{% endfor %}
</ul>

View file

@ -1,4 +1,4 @@
{% extends "account/base.html" %} {% extends "allauth_ens/base.html" %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "E-mail Addresses" %}{% endblock %} {% block title %}{% trans "E-mail Addresses" %}{% endblock %}
@ -97,7 +97,7 @@
{% endif %} {% endif %}
<form action="{% url "account_email" %}" method="post" class="form-inline"> <form action="{% url "account_email" %}" method="post" class="form-inline">
{% include "account/block-form.html" with form=form %} {% include "allauth_ens/block-form.html" with form=form %}
<button type="submit" class="link" name="action_add">{% trans "Add E-mail" %}</button> <button type="submit" class="link" name="action_add">{% trans "Add E-mail" %}</button>
</form> </form>

View file

@ -1,4 +1,4 @@
{% extends "account/base.html" %} {% extends "allauth_ens/base.html" %}
{% load i18n %} {% load i18n %}
{% load account %} {% load account %}

View file

@ -1,4 +1,4 @@
{% extends "account/base.html" %} {% extends "allauth_ens/base.html" %}
{% load i18n %} {% load i18n %}
{% load account socialaccount allauth_ens %} {% load account socialaccount allauth_ens %}
@ -18,9 +18,10 @@
{% endif %} {% endif %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
<li class="message warning"> <li class="message warning">
{% user_display user as user_str %}
{% blocktrans %} {% blocktrans %}
You are unauthorized to view this page. Please sign in with an account Your are authenticated as {{ user_str }}, but are not authorized to access
with the required permissions. this page. Would you like to login to a different account ?
{% endblocktrans %} {% endblocktrans %}
</li> </li>
{% endif %} {% endif %}
@ -34,31 +35,32 @@
{% get_providers as socialaccount_providers %} {% get_providers as socialaccount_providers %}
{% if socialaccount_providers %} {% if socialaccount_providers %}
<section> <section id="providers">
<p>
{% blocktrans %}
Please sign in with one of your existing third party accounts, or with the
form opposite.
{% endblocktrans %}
</p>
<ul class="method-list"> <ul class="method-list">
{% include "socialaccount/snippets/provider_list.html" with process="login" %} {% include "socialaccount/snippets/provider_list.html" with process="login" %}
</ul> </ul>
</section>
{% include "socialaccount/snippets/login_extra.html" %} {% include "socialaccount/snippets/login_extra.html" %}
</section>
<div class="divider"></div> <div class="divider"></div>
{% endif %} {% endif %}
<section> <section id="main">
<p>
{% blocktrans %}
Please sign in with one of your existing third party accounts, or with the form below.
{% endblocktrans %}
</p>
<ul class="actions"> <ul class="actions">
<li><a href="{{ signup_url }}">{% trans "Sign Up" %}</a></li>
<li><a href="{% url "account_reset_password" %}">{% trans "Forgot Password?" %}</a> <li><a href="{% url "account_reset_password" %}">{% trans "Forgot Password?" %}</a>
{% is_open_for_signup as open_signup %}
{% if open_signup %}
<li><a href="{{ signup_url }}">{% trans "Sign Up" %}</a></li>
{% endif %}
</ul> </ul>
<form action="{% url "account_login" %}" method="post"> <form action="{% url "account_login" %}" method="post">
{% include "account/block-form.html" with form=form %} {% include "allauth_ens/block-form.html" with form=form %}
<input type="submit" value="{% trans "Sign In" %}"> <input type="submit" value="{% trans "Sign In" %}">
{% if redirect_field_value %} {% if redirect_field_value %}
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}"> <input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}">

View file

@ -1,4 +1,4 @@
{% extends "account/base.html" %} {% extends "allauth_ens/base.html" %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Sign Out" %}{% endblock %} {% block title %}{% trans "Sign Out" %}{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends "account/base.html" %} {% extends "allauth_ens/base.html" %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Change Password" %}{% endblock %} {% block title %}{% trans "Change Password" %}{% endblock %}
@ -6,7 +6,7 @@
{% block messages-extra %} {% block messages-extra %}
{% include "account/block-messages-form-errors.html" with form_errors=form.non_field_errors %} {% include "allauth_ens/block-messages-form-errors.html" with form_errors=form.non_field_errors %}
{% endblock %} {% endblock %}
@ -14,7 +14,7 @@
<section> <section>
<form action="{% url "account_change_password" %}" method="post"> <form action="{% url "account_change_password" %}" method="post">
{% include "account/block-form.html" with form=form submit_value=submit_value %} {% include "allauth_ens/block-form.html" with form=form submit_value=submit_value %}
<input type="submit" value="{% trans "Change Password" %}"> <input type="submit" value="{% trans "Change Password" %}">
</form> </form>
</section> </section>

View file

@ -1,4 +1,4 @@
{% extends "account/base.html" %} {% extends "allauth_ens/base.html" %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Password Reset" %}{% endblock %} {% block title %}{% trans "Password Reset" %}{% endblock %}
@ -14,7 +14,7 @@
{% endblocktrans %} {% endblocktrans %}
</p> </p>
<form action="{% url "account_reset_password" %}" method="post"> <form action="{% url "account_reset_password" %}" method="post">
{% include "account/block-form.html" with form=form %} {% include "allauth_ens/block-form.html" with form=form %}
<input type="submit" value="{% trans "Reset Password" %}"> <input type="submit" value="{% trans "Reset Password" %}">
</form> </form>
</section> </section>

View file

@ -1,4 +1,4 @@
{% extends "account/base.html" %} {% extends "allauth_ens/base.html" %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Password Reset" %}{% endblock %} {% block title %}{% trans "Password Reset" %}{% endblock %}

View file

@ -0,0 +1,28 @@
{% extends "allauth_ens/base.html" %}
{% load i18n %}
{% block title %}{% trans "Change Password" %}{% endblock %}
{% block header-title %}{% trans "Change Password" %}{% endblock %}
{% block content %}
<section>
{% if token_fail %}
<p>
{% url 'account_reset_password' as passwd_reset_url %}
{% blocktrans %}
The password reset link was invalid, possibly because it has already been used.
Please request a <a href="{{ passwd_reset_url }}">new password reset</a>.
{% endblocktrans %}
</p>
{% elif form %}
<form action="" method="post">
{% include "allauth_ens/block-form.html" with form=form %}
<input type="submit" value="{% trans "Reset Password" %}">
</form>
{% else %}
<p>{% trans "Your password is now changed." %}</p>
{% endif %}
</section>
{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends "account/base.html" %} {% extends "allauth_ens/base.html" %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Change Password" %}{% endblock %} {% block title %}{% trans "Change Password" %}{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends "account/base.html" %} {% extends "allauth_ens/base.html" %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Set Password" %}{% endblock %} {% block title %}{% trans "Set Password" %}{% endblock %}
@ -6,7 +6,7 @@
{% block messages-extra %} {% block messages-extra %}
{% include "account/block-messages-form-errors.html" with form_errors=form.non_field_errors %} {% include "allauth_ens/block-messages-form-errors.html" with form_errors=form.non_field_errors %}
{% endblock %} {% endblock %}
@ -19,9 +19,8 @@
third parties. third parties.
{% endblocktrans %} {% endblocktrans %}
</p> </p>
<hr>
<form action="{% url "account_set_password" %}" method="post"> <form action="{% url "account_set_password" %}" method="post">
{% include "account/block-form.html" with form=form %} {% include "allauth_ens/block-form.html" with form=form %}
<input type="submit" value="{% trans "Set Password" %}"> <input type="submit" value="{% trans "Set Password" %}">
</form> </form>
</section> </section>

View file

@ -1,4 +1,4 @@
{% extends "account/base.html" %} {% extends "allauth_ens/base.html" %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Signup" %}{% endblock %} {% block title %}{% trans "Signup" %}{% endblock %}
@ -13,7 +13,7 @@
</li> </li>
</ul> </ul>
<form action="{% url "account_signup" %}" method="post"> <form action="{% url "account_signup" %}" method="post">
{% include "account/block-form.html" with form=form %} {% include "allauth_ens/block-form.html" with form=form %}
<input type="submit" value="{% trans "Sign Up" %}"> <input type="submit" value="{% trans "Sign Up" %}">
{% if redirect_field_value %} {% if redirect_field_value %}
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}"> <input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}">

View file

@ -1,4 +1,4 @@
{% extends "account/base.html" %} {% extends "allauth_ens/base.html" %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Sign Up Closed" %}{% endblock %} {% block title %}{% trans "Sign Up Closed" %}{% endblock %}

View file

@ -8,7 +8,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<title>{% block title %}{% endblock %}{% if request.site.name %} - {{ request.site.name }}{% endif %}</title> <title>{% block title %}{% endblock %}{% if request.site.name %} · {{ request.site.name }}{% endif %}</title>
{# Responsive UI #} {# Responsive UI #}
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
@ -17,6 +17,10 @@
{# CSS #} {# CSS #}
<link rel="stylesheet" <link rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Roboto:400,700|Roboto+Slab:400"> href="https://fonts.googleapis.com/css?family=Roboto:400,700|Roboto+Slab:400">
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"
integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN"
crossorigin="anonymous">
<link rel="stylesheet" <link rel="stylesheet"
href="{% static "allauth_ens/screen.css" %}"> href="{% static "allauth_ens/screen.css" %}">
@ -44,10 +48,10 @@
{% get_home_url as home_url %} {% get_home_url as home_url %}
{% if home_url %} {% if home_url %}
<a href="{{ home_url }}"> <a href="{{ home_url }}">
{{ request.site.name|default:"Voir le site" }} {{ request.site.name|default:"View site" }}
</a> </a>
{% else %} {% else %}
<span>{{ request.site.name }}</span> <span>{{ request.site.name|default:"View site" }}</span>
{% endif %} {% endif %}
</div> </div>
@ -66,16 +70,16 @@
</span> </span>
{% endif %} {% endif %}
{% else %} {% else %}
<span> <a href="{% url "account_login" %}">
<i class="fa fa-user"></i> <i class="fa fa-sign-in"></i>
{% trans "Not Connected" %} {% trans "Not Connected" %}
</span> </a>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</header> </header>
{% include "account/block-messages.html" %} {% include "allauth_ens/block-messages.html" %}
{% block messages-extra %}{% endblock %} {% block messages-extra %}{% endblock %}

View file

@ -0,0 +1,33 @@
{% load widget_tweaks %}
{% csrf_token %}
{% for field in form %}
{% with widget=field|widget_type %}
{% if widget == "checkboxinput" %}
<div li class="input-wrapper input-skip {% if field.errors %}input-error{% endif %}">
<label for="{{ field.id_for_label }}">
{{ field.label }}
<div class="widget-checkbox">
{% render_field field class+="field" %}
<button type="button" class="choice-yes">Oui</button>
<button type="button" class="choice-no focus">Non</button>
</div>
</label>
{% else %}
<div class="input-wrapper{% if field.errors %} input-error{% endif %}">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{% render_field field class+="field" size="" autocomplete="off" autocapitalize="none" placeholder="" %}
{% endif %}
<div class="infos-spacer"></div>
<ul class="messages">
{% if field.help_text %}
<li>{{ field.help_text|safe }}</li>
{% endif %}
{% for error in field.errors %}
<li class="error-desc">{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endwith %}
{% endfor %}

View file

@ -1,4 +1,4 @@
{% extends "account/block-messages-base.html" %} {% extends "allauth_ens/block-messages-base.html" %}
{% block messages-container %} {% block messages-container %}
{% if form_errors %}{{ block.super }}{% endif %} {% if form_errors %}{{ block.super }}{% endif %}

View file

@ -1,4 +1,4 @@
{% extends "account/block-messages-base.html" %} {% extends "allauth_ens/block-messages-base.html" %}
{% block messages-container %} {% block messages-container %}
{% if messages %}{{ block.super }}{% endif %} {% if messages %}{{ block.super }}{% endif %}

View file

@ -0,0 +1,17 @@
{% extends "allauth_ens/base.html" %}
{% load i18n %}
{% block title %}{% trans "Login Failure" %}{% endblock %}
{% block header-title %}{% trans "Login Failure" %}{% endblock %}
{% block content %}
<section>
<p>
{% blocktrans %}
An error occured while attempting to login via your social network account.
{% endblocktrans %}
</p>
</section>
{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends "account/base.html" %} {% extends "allauth_ens/base.html" %}
{% load i18n %} {% load i18n %}
{% load socialaccount allauth_ens_social %} {% load socialaccount allauth_ens_social %}
@ -19,48 +19,45 @@
{% endblocktrans %} {% endblocktrans %}
{% endif %} {% endif %}
</p> </p>
<ul class="provider-list"> <ul class="connections-providers-list">
{% get_accounts_by_providers user as accounts_by_providers %} {% get_accounts_by_providers user as accounts_by_providers %}
{% for provider, accounts in accounts_by_providers.items %} {% for provider, accounts in accounts_by_providers.items %}
<li class="provider"> <li>
<ul class="heading"> <div class="heading">
<li class="connect"> <a class="connect" href="{% provider_login_url provider.id process="connect" scope=scope auth_params=auth_params %}">
<a href="{% provider_login_url provider.id process="connect" scope=scope auth_params=auth_params %}">
<i class="fa fa-plus"></i> <i class="fa fa-plus"></i>
</a> </a>
</li>
<li class="name">
{{ provider.name }} {{ provider.name }}
</li> </div>
</ul>
{% if accounts %} {% if accounts %}
<ul class="connected-list"> <ul class="connections-list">
{% for account in accounts %} {% for account in accounts %}
<li> <li>
<span class="connected-delete"> <div class="delete">
<form action="{% url "socialaccount_connections" %}" method="post"> <form action="{% url "socialaccount_connections" %}" method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="account" value="{{ account.id }}"> <input type="hidden" name="account" value="{{ account.id }}">
<button type="submit" class="link"><i class="fa fa-remove"></i></button> <button type="submit" class="link">
<i class="fa fa-remove"></i>
</button>
</form> </form>
</span> </div>
<span class="connected-label">
<i class="fa fa-user"></i> <i class="fa fa-user"></i>
{% with account.get_profile_url as profile_url %} {% with profile_urlaccount.get_profile_url as profile_url %}
{% trans "Connected Account - No ID" as fallback_label %} {% firstof account.extra_data.name account.extra_data.username account.uid as account_label %}
{% firstof account.extra_data.name account.extra_data.username fallback_label as account_label %}
{% if profile_url %} {% if profile_url %}
<a href="{{ profile_url }}" target="blank">{{ account_label }}</a> <a href="{{ get_profile_url }}" target="blank">
{{ account_label }}
</a>
{% else %} {% else %}
{{ account_label }} {{ account_label }}
{% endif %} {% endif %}
{% endwith %} {% endwith %}
</span>
</li> </li>
{% endfor %} {% endfor %}

View file

@ -0,0 +1,19 @@
{% extends "allauth_ens/base.html" %}
{% load i18n %}
{% block title %}{% trans "Login Cancelled" %}{% endblock %}
{% block header-title %}{% trans "Login Cancelled" %}{% endblock %}
{% block content %}
<section>
<p>
{% url "account_login" as login_url %}
{% blocktrans %}
You decided to cancel logging into our site using one of your existing accounts. If this was a mistake, please proceed to <a href="{{login_url}}">sign in</a>.{% endblocktrans %}
{% endblocktrans %}
</p>
</section>
{% endblock %}

View file

@ -0,0 +1,26 @@
{% extends "allauth_ens/base.html" %}
{% load i18n %}
{% block title %}{% trans "Signup" %}{% endblock %}
{% block header-title %}{% trans "Signup" %}{% endblock %}
{% block content %}
<section>
<p>
{% blocktrans with provider_name=account.get_provider.name %}
You are about to use your {{ provider_name }} account to login.
As a final step, please complete the following form:
{% endblocktrans %}
</p>
<form action="{% url "socialaccount_signup" %}" method="post">
{% include "allauth_ens/block-form.html" with form=form %}
<input type="submit" value="{% trans "Sign Up" %}">
{% if redirect_field_value %}
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}">
{% endif %}
</form>
</section>
{% endblock %}

View file

@ -3,6 +3,7 @@ from django import template
from django.conf import settings from django.conf import settings
from django.shortcuts import resolve_url from django.shortcuts import resolve_url
from allauth.account.adapter import get_adapter
register = template.Library() register = template.Library()
@ -26,3 +27,9 @@ def get_profile_url():
if profile_url is None: if profile_url is None:
return None return None
return resolve_url(profile_url) return resolve_url(profile_url)
@simple_tag(takes_context=True)
def is_open_for_signup(context):
request = context['request']
return get_adapter(request).is_open_for_signup(request)

View file

@ -6,7 +6,6 @@ from django import template
from allauth import app_settings as allauth_settings from allauth import app_settings as allauth_settings
from allauth.socialaccount.templatetags import socialaccount as tt_social from allauth.socialaccount.templatetags import socialaccount as tt_social
register = template.Library() register = template.Library()
if django.VERSION >= (1, 9): if django.VERSION >= (1, 9):

137
allauth_ens/tests.py Normal file
View file

@ -0,0 +1,137 @@
import re
import django
from django.contrib.auth import HASH_SESSION_KEY, get_user_model
from django.contrib.sites.models import Site
from django.core import mail
from django.test import TestCase, override_settings
if django.VERSION >= (1, 10):
from django.urls import reverse
else:
from django.core.urlresolvers import reverse
User = get_user_model()
def prevent_logout_pwd_change(client, user):
"""
Updating a user's password logs out all sessions for the user.
By calling this function this behavior will be prevented.
See this link, and the source code of `update_session_auth_hash`:
https://docs.djangoproject.com/en/dev/topics/auth/default/#session-invalidation-on-password-change
"""
if hasattr(user, 'get_session_auth_hash'):
session = client.session
session[HASH_SESSION_KEY] = user.get_session_auth_hash()
session.save()
class ViewsTests(TestCase):
"""
Checks (barely) that templates do not contain errors.
"""
def setUp(self):
self.u = User.objects.create_user('user', 'user@mail.net', 'user')
Site.objects.filter(pk=1).update(domain='testserver')
def _login(self, client=None):
if client is None:
client = self.client
client.login(username='user', password='user')
def _get_confirm_email_link(self, email_msg):
m = re.search(
r'http://testserver(/accounts/confirm-email/.*/)',
email_msg.body,
)
return m.group(1)
def _get_reset_password_link(self, email_msg):
m = re.search(
r'http://testserver(/accounts/password/reset/key/.*/)',
email_msg.body,
)
return m.group(1)
def test_account_signup(self):
r = self.client.get(reverse('account_signup'))
self.assertEqual(r.status_code, 200)
@override_settings(
ACCOUNT_ADAPTER='tests.adapter.ClosedSignupAccountAdapter',
)
def test_account_closed_signup(self):
r = self.client.get(reverse('account_signup'))
self.assertEqual(r.status_code, 200)
def test_account_login(self):
r = self.client.get(reverse('account_login'))
self.assertEqual(r.status_code, 200)
def test_account_logout(self):
self._login()
r = self.client.get(reverse('account_logout'))
self.assertEqual(r.status_code, 200)
def test_account_change_password(self):
self._login()
r = self.client.get(reverse('account_change_password'))
self.assertEqual(r.status_code, 200)
def test_account_set_password(self):
self._login()
self.u.set_unusable_password()
self.u.save()
prevent_logout_pwd_change(self.client, self.u)
r = self.client.get(reverse('account_set_password'))
self.assertEqual(r.status_code, 200)
def test_account_inactive(self):
r = self.client.get(reverse('account_inactive'))
self.assertEqual(r.status_code, 200)
def test_account_email(self):
self._login()
r = self.client.get(reverse('account_email'))
self.assertEqual(r.status_code, 200)
def test_account_email_verification_sent(self):
self._login()
r = self.client.get(reverse('account_email_verification_sent'))
self.assertEqual(r.status_code, 200)
def test_account_confirm_email(self):
self._login()
self.client.post(reverse('account_email'), {
'action_add': '',
'email': 'test@mail.net',
})
confirm_url = self._get_confirm_email_link(mail.outbox[0])
r = self.client.get(confirm_url)
self.assertEqual(r.status_code, 200)
def test_account_reset_password(self):
r = self.client.get(reverse('account_reset_password'))
self.assertEqual(r.status_code, 200)
def test_account_reset_password_done(self):
r = self.client.get(reverse('account_reset_password_done'))
self.assertEqual(r.status_code, 200)
def test_account_reset_password_from_key(self):
self.client.post(reverse('account_reset_password'), {
'email': 'user@mail.net',
})
reset_url = self._get_reset_password_link(mail.outbox[0])
r = self.client.get(reset_url, follow=True)
self.assertEqual(r.status_code, 200)
def test_account_reset_password_from_key_done(self):
r = self.client.get(reverse('account_reset_password_from_key_done'))
self.assertEqual(r.status_code, 200)

View file

@ -1,10 +1,16 @@
from django.core.urlresolvers import reverse_lazy import django
from django.views.generic import RedirectView from django.views.generic import RedirectView
if django.VERSION >= (1, 10):
from django.urls import reverse_lazy
else:
from django.core.urlresolvers import reverse_lazy
class CaptureLogin(RedirectView): class CaptureLogin(RedirectView):
url = reverse_lazy('account_login') url = reverse_lazy('account_login')
query_string = True query_string = True
permanent = False
capture_login = CaptureLogin.as_view() capture_login = CaptureLogin.as_view()
@ -13,6 +19,7 @@ capture_login = CaptureLogin.as_view()
class CaptureLogout(RedirectView): class CaptureLogout(RedirectView):
url = reverse_lazy('account_logout') url = reverse_lazy('account_logout')
query_string = True query_string = True
permanent = False
capture_logout = CaptureLogout.as_view() capture_logout = CaptureLogout.as_view()

View file

@ -1,6 +1,4 @@
require 'compass/import-once/activate' require 'compass/import-once/activate'
# Require any additional compass plugins here.
require 'font-awesome-sass'
# Set this to the root of your project when deployed: # Set this to the root of your project when deployed:
http_path = '/' http_path = '/'

54
example/README.rst Normal file
View file

@ -0,0 +1,54 @@
*****
Setup
*****
Clone the repository and go to the directory containing this file.
If it is the first time you start the example website, run these commands::
# Create a virtual env.
$ virtualenv -p python3 venv
$ . venv/bin/activate
# Install dependencies (django-allauth-ens is installed from local copy).
$ pip install -r requirements.txt
# Initialize the database (sqlite).
$ ./manage.py migrate
Start the server with::
$ ./manage.py runserver
Then, open your browser at `<http://localhost:8000/>`_.
*****
Usage
*****
Pre-existing users
==================
You can try to login using one of the existing users:
* a "normal" user (username: 'user', password: 'user')
* a superuser (username: 'root', password: 'root')
Auth Providers
==============
Facebook and Google are activated but they won't work unless you provide valid
API credentials in the Django Admin at
`<http://localhost:8060/admin/socialaccount/socialapp/>`_.
Clipper is available (which requires valid credentials). For auto-signup, the
new user is populated with the clipper identifier as username (plus a suffix if
not available) and ``<clipper id>@clipper.ens.fr`` as email.
Adapters
========
``ACCOUNT_ADAPTER`` and ``SOCIALACCOUNT_ADAPTER`` can be customized in
``adapter.py``.

11
example/adapter.py Normal file
View file

@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
from allauth.account.adapter import DefaultAccountAdapter
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
class AccountAdapter(DefaultAccountAdapter):
pass
class SocialAccountAdapter(DefaultSocialAccountAdapter):
pass

View file

@ -1,27 +0,0 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import AbstractUser
from django.utils.translation import ugettext as _
from .models import User
class ExtendedUserAdmin(UserAdmin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
base_fields = [field.name for field in AbstractUser._meta.fields]
all_fields = [field.name for field in self.model._meta.fields]
extra_fields = [
f for f in all_fields
if f not in base_fields and f != self.model._meta.pk.name
]
self.fieldsets += (
(_('Champs additionnels'), {'fields': extra_fields}),
)
admin.site.register(User, ExtendedUserAdmin)

View file

@ -1,34 +1,42 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.db.models.signals import post_migrate from django.db.models.signals import post_migrate
from django.utils.module_loading import import_string
from allauth.socialaccount.providers import registry
def prepare_site(sender, **kwargs): def setup_site(sender, **kwargs):
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
Site.objects.filter(id=1).update(name="Demo Site", domain="localhost") Site.objects.filter(id=1).update(name="Demo Site", domain="localhost")
def prepare_superuser(sender, **kwargs): def setup_users(sender, **kwargs):
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
User = get_user_model() User = get_user_model()
root, created = User.objects.get_or_create(
username='root', root, r_created = User.objects.get_or_create(username='root', defaults={
defaults={ 'email': 'root@mail.net',
'is_staff': True, 'is_staff': True,
'is_superuser': True, 'is_superuser': True,
}, })
) if r_created:
if created:
root.set_password('root') root.set_password('root')
root.save() root.save()
print('Superuser created - Credentials: root:root') print('Superuser created - Credentials: root:root')
user, u_created = User.objects.get_or_create(username='user', defaults={
'email': 'user@mail.net',
})
if u_created:
user.set_password('user')
user.save()
print('User created - Credentials: user:user')
def setup_dummy_social(sender, **kwargs): def setup_dummy_social(sender, **kwargs):
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.utils.module_loading import import_string
from allauth.socialaccount.models import SocialApp from allauth.socialaccount.models import SocialApp
from allauth.socialaccount.providers import registry
need_credentials = [ need_credentials = [
'allauth.socialaccount.providers.oauth.provider.OAuthProvider', 'allauth.socialaccount.providers.oauth.provider.OAuthProvider',
@ -68,6 +76,6 @@ class BasicAppConfig(AppConfig):
name = 'app' name = 'app'
def ready(self): def ready(self):
post_migrate.connect(prepare_site, sender=self) post_migrate.connect(setup_site, sender=self)
post_migrate.connect(prepare_superuser, sender=self)
post_migrate.connect(setup_dummy_social, sender=self) post_migrate.connect(setup_dummy_social, sender=self)
post_migrate.connect(setup_users, sender=self)

View file

@ -1,9 +1,9 @@
[ [
{ {
"model": "testapp.user", "model": "auth.user",
"pk": 1, "pk": 1,
"fields": { "fields": {
"password": "pbkdf2_sha256$36000$RNuQMJ1hqN0P$GFFyxtTONjkh4IUMifNYrsXs4/SnX5uMnGtRNR2WrFo=", "password": "pbkdf2_sha256$100000$WDs2nLZ0eIl1$oNqrYphOf0AVRQ8aPIA3g7xM2gI8/c8NJkp2geVT7mc=",
"last_login": null, "last_login": null,
"is_superuser": false, "is_superuser": false,
"username": "user", "username": "user",
@ -13,19 +13,15 @@
"is_staff": false, "is_staff": false,
"is_active": true, "is_active": true,
"date_joined": "2017-07-03T10:10:18.675Z", "date_joined": "2017-07-03T10:10:18.675Z",
"departement": "",
"occupation": "1A",
"phone": "",
"promo": 2016,
"groups": [], "groups": [],
"user_permissions": [] "user_permissions": []
} }
}, },
{ {
"model": "testapp.user", "model": "auth.user",
"pk": 2, "pk": 2,
"fields": { "fields": {
"password": "pbkdf2_sha256$36000$NFfbdDHHuq0A$auDXrWn6xr+FnsAOW8uq0aa8m2kyUPtgY/QgThMDKF0=", "password": "pbkdf2_sha256$100000$EjBzWe7Ce5Bc$abqTFywweKuMaRux2MMUwcLchcwxmXGduN320oYaV28=",
"last_login": null, "last_login": null,
"is_superuser": true, "is_superuser": true,
"username": "root", "username": "root",
@ -35,12 +31,16 @@
"is_staff": true, "is_staff": true,
"is_active": true, "is_active": true,
"date_joined": "2017-07-03T10:10:36.413Z", "date_joined": "2017-07-03T10:10:36.413Z",
"departement": "",
"occupation": "1A",
"phone": "",
"promo": 2016,
"groups": [], "groups": [],
"user_permissions": [] "user_permissions": []
} }
},
{
"model": "sites.Site",
"pk": 1,
"fields": {
"name": "Example Site",
"domain": "localhost"
}
} }
] ]

View file

@ -1,46 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-07-27 15:06
from __future__ import unicode_literals
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0008_alter_user_username_max_length'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=30, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
],
options={
'verbose_name_plural': 'users',
'verbose_name': 'user',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

View file

@ -1,11 +0,0 @@
from django.contrib.auth.models import AbstractUser, UserManager
# from authens.models import ENSUserMixin
"""
class User(ENSUserMixin, AbstractUser):
objects = UserManager()
"""
class User(AbstractUser):
objects = UserManager()

View file

@ -1,38 +0,0 @@
[
{
"model": "auth.user",
"pk": 1,
"fields": {
"password": "pbkdf2_sha256$36000$xjRTvXipQpaq$z0kj/h2b4yZp8DNOjhu2TUxSOWHWYX+S+0rvsaWx7TU=",
"last_login": null,
"is_superuser": false,
"username": "user",
"first_name": "",
"last_name": "",
"email": "",
"is_staff": false,
"is_active": true,
"date_joined": "2017-07-01T07:11:08.549Z",
"groups": [],
"user_permissions": []
}
},
{
"model": "auth.user",
"pk": 3,
"fields": {
"password": "pbkdf2_sha256$36000$7IcQUVbp8OM1$7mKhCjAPFLkcEUfu9djXxn/scOw2uvlWLFrMtGfhd0U=",
"last_login": null,
"is_superuser": true,
"username": "root",
"first_name": "",
"last_name": "",
"email": "",
"is_staff": true,
"is_active": true,
"date_joined": "2017-07-01T07:11:20.745Z",
"groups": [],
"user_permissions": []
}
}
]

View file

@ -9,34 +9,26 @@ SECRET_KEY = 'iamaplop'
DEBUG = True DEBUG = True
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
SITE_ID = 1
INSTALLED_APPS = [ INSTALLED_APPS = [
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.sessions',
'django.contrib.sites', 'django.contrib.sites',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'debug_toolbar', 'debug_toolbar',
'widget_tweaks', 'widget_tweaks',
# This one must be before 'allauth' to replace its templates.
'allauth_ens', 'allauth_ens',
'allauth', 'allauth',
'allauth.account', 'allauth.account',
'allauth.socialaccount', 'allauth.socialaccount',
'allauth_cas', 'allauth_cas',
'allauth.socialaccount.providers.facebook', 'allauth.socialaccount.providers.facebook',
'allauth.socialaccount.providers.google', 'allauth.socialaccount.providers.google',
'allauth_ens.providers.clipper', 'allauth_ens.providers.clipper',
'app', 'app',
@ -59,6 +51,12 @@ else:
ROOT_URLCONF = 'urls' ROOT_URLCONF = 'urls'
SITE_ID = 1
WSGI_APPLICATION = 'wsgi.application'
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
@ -75,8 +73,6 @@ TEMPLATES = [
}, },
] ]
WSGI_APPLICATION = 'wsgi.application'
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
@ -86,16 +82,28 @@ DATABASES = {
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ {
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 'NAME': (
'django.contrib.auth.password_validation'
'.UserAttributeSimilarityValidator',
),
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'NAME': (
'django.contrib.auth.password_validation'
'.MinimumLengthValidator'
),
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 'NAME': (
'django.contrib.auth.password_validation'
'.CommonPasswordValidator'
),
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 'NAME': (
'django.contrib.auth.password_validation'
'.NumericPasswordValidator'
),
}, },
] ]
@ -115,7 +123,10 @@ DEBUG_TOOLBAR_CONFIG = {
'SHOW_TOOLBAR_CALLBACK': lambda r: True, 'SHOW_TOOLBAR_CALLBACK': lambda r: True,
} }
AUTH_USER_MODEL = 'app.User' AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
]
LOGIN_URL = '/account/login/' LOGIN_URL = '/account/login/'
LOGIN_REDIRECT_URL = 'user-view' LOGIN_REDIRECT_URL = 'user-view'
@ -125,4 +136,13 @@ SOCIALACCOUNT_QUERY_EMAIL = True
# allauth settings # allauth settings
ACCOUNT_ADAPTER = 'adapter.AccountAdapter'
ACCOUNT_AUTHENTICATED_LOGIN_REDIRECTS = False ACCOUNT_AUTHENTICATED_LOGIN_REDIRECTS = False
ACCOUNT_AUTHENTICATION_METHOD = 'username_email'
SOCIALACCOUNT_ADAPTER = 'adapter.SocialAccountAdapter'
# allauth_ens settings
ACCOUNT_HOME_URL = 'home'
ACCOUNT_DETAILS_URL = '/my-account/'

View file

@ -4,13 +4,14 @@ from django.contrib.auth.decorators import login_required, permission_required
from django.views.generic import RedirectView from django.views.generic import RedirectView
import debug_toolbar import debug_toolbar
from allauth_ens.views import capture_login, capture_logout
from app import views from app import views
urlpatterns = [ urlpatterns = [
# Catch admin login/logout views. # Catch admin login/logout views.
# url(r'^admin/login/', authens_views.CaptureLogin.as_view()), url(r'^admin/login/', capture_login),
# url(r'^admin/logout/', authens_views.CaptureLogout.as_view()), url(r'^admin/logout/', capture_logout),
# Admin urls include comes after. # Admin urls include comes after.
url(r'^admin/', admin.site.urls), url(r'^admin/', admin.site.urls),
@ -28,7 +29,8 @@ urlpatterns = [
# (Redirect from /) # (Redirect from /)
url(r'^$', RedirectView.as_view(url='/view/')), url(r'^$', RedirectView.as_view(url='/view/'),
name='home'),
] ]
urlpatterns += [url(r'^__debug__/', include(debug_toolbar.urls))] urlpatterns += [url(r'^__debug__/', include(debug_toolbar.urls))]

View file

@ -45,7 +45,7 @@ setup(
include_package_data=True, include_package_data=True,
install_requires=[ install_requires=[
'django-allauth', 'django-allauth',
'django-allauth-cas', 'django-allauth-cas>=1.0.0b2,<1.1',
'django-widget-tweaks', 'django-widget-tweaks',
], ],
) )

6
tests/adapter.py Normal file
View file

@ -0,0 +1,6 @@
from allauth.account.adapter import DefaultAccountAdapter
class ClosedSignupAccountAdapter(DefaultAccountAdapter):
def is_open_for_signup(self, request):
return False

View file

@ -12,10 +12,12 @@ INSTALLED_APPS = [
'django.contrib.sites', 'django.contrib.sites',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'widget_tweaks',
'allauth_ens',
'allauth', 'allauth',
'allauth.account', 'allauth.account',
'allauth.socialaccount', 'allauth.socialaccount',
'allauth_cas', 'allauth_cas',
'allauth_ens.providers.clipper', 'allauth_ens.providers.clipper',
@ -36,6 +38,7 @@ _MIDDLEWARES = [
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.sites.middleware.CurrentSiteMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
] ]
@ -61,3 +64,7 @@ TEMPLATES = [
] ]
ROOT_URLCONF = 'tests.urls' ROOT_URLCONF = 'tests.urls'
SITE_ID = 1
STATIC_URL = '/static/'

View file

@ -1,7 +1,8 @@
[tox] [tox]
envlist = envlist =
py{27,34,35}-django{18,19,110} django{18,19,110}-py{27,34,35},
py{27,34,35,36}-django111, django111-py{27,34,35,36},
django20-py{34,35,36},
cov_combine, cov_combine,
flake8, flake8,
isort isort
@ -12,6 +13,7 @@ deps =
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
coverage coverage
mock ; python_version < "3.0" mock ; python_version < "3.0"
usedevelop= True usedevelop= True