Compare commits

..

1 commit

Author SHA1 Message Date
Robin Champenois
914e405762 Ajout de la normalisation des clippers 2018-04-18 22:31:24 +02:00
42 changed files with 159 additions and 6650 deletions

2
.gitignore vendored
View file

@ -3,7 +3,7 @@
.tox/
build/
dist/
/vendor/
vendor/
venv/
*~

View file

@ -1,67 +0,0 @@
image: python
stages:
- test
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/vendor/pip"
cache:
paths:
- vendor
before_script:
- mkdir -p vendor/{apt,pip}
# http://www.python-ldap.org/en/latest/installing.html
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq build-essential python2.7-dev python3-dev libldap2-dev libsasl2-dev
- pip install tox
python27:
image: python:2.7
stage: test
script:
- tox -e django18-py27
- tox -e django19-py27
- tox -e django110-py27
- tox -e django111-py27
python34:
image: python:3.4
stage: test
script:
- tox -e django18-py34
- tox -e django19-py34
- tox -e django110-py34
- tox -e django111-py34
- tox -e django20-py34
python35:
image: python:3.5
stage: test
script:
- tox -e django18-py35
- tox -e django19-py35
- tox -e django110-py35
- tox -e django111-py35
- tox -e django20-py35
- tox -e cov_combine
# Catch coverage here. Python3.5 supports more Django versions.
# For GitLab, keep this commented.
# coverage: '/TOTAL.*\s(\d+\.\d+)\%$/'
python36:
image: python:3.6
stage: test
script:
- tox -e django111-py36
- tox -e django20-py36
flake8:
image: python:3.6
stage: test
script: tox -e flake8
isort:
image: python:3.6
stage: test
script: tox -e isort

View file

@ -1,33 +1,5 @@
*****
1.1.3
*****
- Fix: translation was not included in the package
*****
1.1.2
*****
- Options added to ``install_longterm`` command to preserve usernames and source
clipper identifiers from various sources. !18
*****
1.1.1
*****
- Pin python-cas version to make Clipper provider work. !17
*****
1.1.0
*****
- Add long-term support for Clipper (see README for instructions on setup/update)
- Highlight Clipper provider in login screens (see related settings in README)
- Vendorize some static files (JS)
*******
1.0.0b2
*******
******************
1.0.0 (unreleased)
******************
- First official release.

View file

@ -2,4 +2,3 @@ include LICENSE README.rst
recursive-include allauth_ens/static *
recursive-include allauth_ens/templates *
recursive-include allauth_ens/locale *

View file

@ -77,13 +77,6 @@ See also the `allauth configuration`_ and `advanced usage`_ docs pages.
**Examples:** ``'my-account'``, ``'/my-account/'``
``ALLAUTH_ENS_HIGHLIGHT_CLIPPER``
*Optional* — Boolean (default: `True`).
When set to `True`, displays prominently the Clipper option in the login view
(if you use the `allauth_ens` templates).
*****
Views
*****
@ -155,83 +148,22 @@ Configuration
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
Populated data
Poulated data
- username: ``<clipper>``
- email (primary and verified): ``<clipper>@clipper.ens.fr``
********
Adapters
********
Long Term Clipper Adapter
=========================
We provide an easy-to-use SocialAccountAdapter to handle the fact that Clipper
accounts are not eternal, and that there is no guarantee that the clipper
usernames won't be reused later.
This adapter also handles getting basic information about the user from SPI's
LDAP.
**Important:** If you are building on top of *allauth*, take care to preserve
the ``extra_data['ldap']`` of ``SocialAccount`` instances related to *Clipper*
(where ``provider_id`` is ``clipper`` or ``clipper_inactive``).
Configuration
Set ``SOCIALACCOUNT_ADAPTER='allauth_ens.adapter.LongTermClipperAccountAdapter'``
in `settings.py`
Auto-signup
Populated data
- *username*: ``<clipper>@<entrance year>``
- *email*: from LDAP's *mailRoutingAddress* field, or ``<clipper>@clipper.ens.fr``
- *first_name*, *last_name* from LDAP's *cn* field
- *entrance_year* (as 2-digit string), *department_code*, *department* and *promotion* (department+year) parsed from LDAP's *homeDirectory* field
- *extra_data* in SocialAccount instance, containing all these field except *promotion* (and available only on first connection)
Account deprecation
At the beginning of each year (i.e. early November), to prevent clipper
username conflicts, you should run ``$ python manage.py deprecate_clippers``.
Every association clipper username <-> user will then be put on hold, and at
the first subsequent connection, a verification of the account will be made
(using LDAP), so that a known user keeps his account, but a newcomer won't
inherit an archicube's.
Customize
You can customize the SocialAccountAdapter by inheriting
``allauth_ens.adapter.LongTermClipperAccountAdapter``. You might want to
modify ``get_username(clipper, data)`` to change the default username format.
By default, ``get_username`` raises a ``ValueError`` when the connexion to the
LDAP failed or did not allow to retrieve the user's entrance year. Overriding
``get_username`` (as done in the example website) allows to get rid of that
behaviour, and for instance attribute a default entrance year.
Initial migration
Description
If you used allauth without LongTermClipperAccountAdapter, or another CAS
interface to log in, you need to update the Users to the new username policy,
and (in the second case) to create the SocialAccount instances to link CAS and
Users. This can be done easily with ``$ python manage.py install_longterm``.
Install_longterm options
- ``--use-socialaccounts``: Use the existing SocialAccounts rather than all the Users. Useful if you are already using Allauth and don't want ``install_longterm`` to mess with the non-clipper authentications.
- ``--keep-usernames``: Do not apply the username template (e.g. ``clipper@promo``) to the existing accounts, only populate the SocialAccounts with LDAP informations. Useful if you don't want to change the usernames of previous users, but do want such a template for future accounts.
- ``--clipper-field <field_name>``: Use a special field rather than the username to get the clipper username (for LDAP lookup and SocialAccount creation/update). This parameter is compatible with ForeignKeys (e.g. ``profile.clipper``). Note: ``--use-socialaccounts`` will ignore the ``--clipper-field`` parameter.
- ``--fake``: Do not modify the database. Use it to test there is no conflict, and be sure the changes are the ones expected. This command does not check for uniqueness errors, so there it may succeed and the actual command fail eventually.
Typical use cases
- *Django-cas-ng -> Longterm*: Use ``install_longterm`` without parameters, or maybe ``--keep-usernames``. If you had a custom username handling, ``--clipper_field`` may be useful.
- *Allauth -> Longterm*: Use ``install_longterm`` with ``--use-socialaccounts``, and maybe ``--keep-usernames``.
*********
Demo Site
@ -269,11 +201,7 @@ Tests
Local environment
-----------------
Requirements
* fakeldap and mock, install with ``$ pip install mock fakeldap``
Run
* ``$ ./runtests.py``
``$ ./runtests.py``
All
---

View file

@ -1,3 +1,3 @@
__version__ = "1.1.3"
__version__ = '1.0.0b2'
default_app_config = "allauth_ens.apps.ENSAllauthConfig"
default_app_config = 'allauth_ens.apps.ENSAllauthConfig'

View file

@ -1,215 +0,0 @@
# -*- coding: utf-8 -*-
from django.contrib.auth import get_user_model
from allauth.account.utils import user_email, user_field, user_username
from allauth.socialaccount.adapter import (
DefaultSocialAccountAdapter, get_account_adapter, get_adapter,
)
from allauth.socialaccount.models import SocialAccount
import ldap
from .utils import (
extract_infos_from_ldap, get_clipper_email, get_ldap_infos, init_ldap,
remove_email,
)
User = get_user_model()
class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter):
"""
A class to manage the fact that people loose their account at the end of
their scolarity and that their clipper login might be reused later
"""
def pre_social_login(self, request, sociallogin):
"""
If a clipper connection has already existed with the uid, it checks
that this connection still belongs to the user it was associated with.
This check is performed by comparing the entrance years provided by the
LDAP.
If the check succeeds, it simply reactivates the clipper connection as
belonging to the associated user.
If the check fails, it frees the elements (as the clipper email
address) which will be assigned to the new connection later.
"""
if sociallogin.account.provider != "clipper":
return super(LongTermClipperAccountAdapter,
self).pre_social_login(request, sociallogin)
clipper_uid = sociallogin.account.uid
try:
old_conn = SocialAccount.objects.get(provider='clipper_inactive',
uid=clipper_uid)
except SocialAccount.DoesNotExist:
return
# An account with that uid was registered, but potentially
# deprecated at the beginning of the year
# We need to check that the user is still the same as before
ldap_data = get_ldap_infos(clipper_uid)
sociallogin._ldap_data = ldap_data
if ldap_data is None or 'entrance_year' not in ldap_data:
raise ValueError("No entrance year in LDAP data")
old_conn_entrance_year = (
old_conn.extra_data.get('ldap', {}).get('entrance_year'))
if old_conn_entrance_year != ldap_data['entrance_year']:
# We cannot reuse this SocialAccount, so we need to invalidate
# the email address of the previous user to prevent conflicts
# if a new SocialAccount is created
email = ldap_data.get('email', get_clipper_email(clipper_uid))
remove_email(old_conn.user, email)
return
# The admission year is the same, we can update the model and keep
# the previous SocialAccount instance
old_conn.provider = 'clipper'
old_conn.save()
# Redo the thing that had failed just before
sociallogin.lookup()
def get_username(self, clipper_uid, data):
"""
Util function to generate a unique username, by default 'clipper@promo'
"""
if data is None or 'entrance_year' not in data:
raise ValueError("No entrance year in LDAP data")
return "{}@{}".format(clipper_uid, data['entrance_year'])
def save_user(self, request, sociallogin, form=None):
if sociallogin.account.provider != "clipper":
return super(LongTermClipperAccountAdapter,
self).save_user(request, sociallogin, form)
user = sociallogin.user
user.set_unusable_password()
clipper_uid = sociallogin.account.uid
ldap_data = sociallogin._ldap_data if \
hasattr(sociallogin, '_ldap_data') \
else get_ldap_infos(clipper_uid)
username = self.get_username(clipper_uid, ldap_data)
email = ldap_data.get('email', get_clipper_email(clipper_uid))
name = ldap_data.get('name')
user_username(user, username or '')
user_email(user, email or '')
name_parts = (name or '').split(' ')
user_field(user, 'first_name', name_parts[0])
user_field(user, 'last_name', ' '.join(name_parts[1:]))
# Entrance year and department, if the user has these fields
entrance_year = ldap_data.get('entrance_year', '')
dep_code = ldap_data.get('department_code', '')
dep_fancy = ldap_data.get('department', '')
promotion = u'%s %s' % (dep_fancy, entrance_year)
user_field(user, 'entrance_year', entrance_year)
user_field(user, 'department_code', dep_code)
user_field(user, 'department', dep_fancy)
user_field(user, 'promotion', promotion)
# Ignore form
get_account_adapter().populate_username(request, user)
# Save extra data (only once)
sociallogin.account.extra_data['ldap'] = ldap_data
sociallogin.save(request)
sociallogin.account.save()
return user
def deprecate_clippers():
"""
Marks all the SocialAccount with clipper as deprecated, by setting their
provider to 'clipper_inactive'
"""
clippers = SocialAccount.objects.filter(provider='clipper')
c_uids = clippers.values_list('uid', flat=True)
# Clear old clipper accounts that were replaced by new ones
# (to avoid conflicts)
SocialAccount.objects.filter(provider='clipper_inactive',
uid__in=c_uids).delete()
# Deprecate accounts
clippers.update(provider='clipper_inactive')
def install_longterm_adapter(fake=False, accounts=None, keep_usernames=False):
"""
Manages the transition from an older django_cas or an allauth_ens
installation without LongTermClipperAccountAdapter
accounts is an optional dictionary containing the association between
clipper usernames and django's User accounts. If not provided, the
function will assumer Users' usernames are their clipper uid.
"""
if accounts is None:
accounts = {u.username: u for u in User.objects.all()
if u.username.isalnum()}
ldap_connection = init_ldap()
ltc_adapter = get_adapter()
info = ldap_connection.search_s(
'dc=spi,dc=ens,dc=fr',
ldap.SCOPE_SUBTREE,
("(|{})".format(''.join(("(uid=%s)" % (un,))
for un in accounts.keys()))),
['uid',
'cn',
'mailRoutingAddress',
'homeDirectory'])
logs = {"created": [], "updated": []}
cases = []
for userinfo in info:
infos = userinfo[1]
data = extract_infos_from_ldap(infos)
clipper_uid = data['clipper_uid']
user = accounts.get(clipper_uid, None)
if user is None:
continue
if not keep_usernames:
user.username = ltc_adapter.get_username(clipper_uid, data)
if fake:
cases.append(clipper_uid)
else:
user.save()
cases.append(user.username)
try:
sa = SocialAccount.objects.get(provider='clipper', uid=clipper_uid)
if not sa.extra_data.get('ldap'):
sa.extra_data['ldap'] = data
if not fake:
sa.save(update_fields=['extra_data'])
logs["updated"].append((clipper_uid, user.username))
except SocialAccount.DoesNotExist:
sa = SocialAccount(
provider='clipper', uid=clipper_uid,
user=user, extra_data={'ldap': data},
)
if not fake:
sa.save()
logs["created"].append((clipper_uid, user.username))
logs["unmodified"] = User.objects.exclude(username__in=cases)\
.values_list("username", flat=True)
return logs

View file

@ -1,5 +1,5 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ugettext_lazy as _
class ENSAllauthConfig(AppConfig):

View file

@ -1,415 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-24 20:10+0200\n"
"PO-Revision-Date: 2019-06-25 10:35+0200\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Generator: Poedit 2.2.1\n"
#: apps.py:7
msgid "ENS Authentication"
msgstr "Connexion pour l'ENS"
#: templates/account/account_inactive.html:5
#: templates/account/account_inactive.html:6
msgid "Account Inactive"
msgstr "Compte inactif"
#: templates/account/account_inactive.html:12
msgid ""
"\n"
" This account is inactive.\n"
" "
msgstr ""
"\n"
" Ce compte est inactif.\n"
" "
#: templates/account/email.html:4 templates/account/email.html:5
msgid "E-mail Addresses"
msgstr "Adresses e-mail"
#: templates/account/email.html:16
msgid ""
"\n"
" The following e-mail addresses are associated with your account:\n"
" "
msgstr ""
"\n"
" Les adresses mails suivantes sont associées à votre compte :\n"
" "
#: templates/account/email.html:30
msgid "This email address is verified."
msgstr "Cette adresse e-mail est vérifiée."
#: templates/account/email.html:34
msgid "This email address is not verified."
msgstr "Cette adresse e-mail n'est pas vérifiée."
#: templates/account/email.html:40
msgid "This is your primary email address."
msgstr "Ceci est votre adresse e-mail primaire."
#: templates/account/email.html:54
msgid "Remove"
msgstr "Supprimer"
#: templates/account/email.html:65
msgid "Re-send verification"
msgstr "Ré-envoyer le message de vérification"
#: templates/account/email.html:77
msgid "Make primary"
msgstr "Rendre principale"
#: templates/account/email.html:90
msgid ""
"\n"
" You currently do not have any e-mail address set up. You should really\n"
" add an e-mail address so you can receive notifications, reset your\n"
" password, etc.\n"
" "
msgstr ""
"\n"
" Vous n'avez actuellement aucune adresse e-mail associée à votre compte.\n"
" Vous devriez vraiment ajouter une adresse e-mail afin que vous puissiez "
"recevoir\n"
" des notifications, réinitialiser votre mot de passe, etc.\n"
" "
#: templates/account/email.html:101
msgid "Add E-mail"
msgstr "Ajouter un e-mail"
#: templates/account/email_confirm.html:5
#: templates/account/email_confirm.html:6
msgid "Confirm E-mail Address"
msgstr "Confirmer l'adresse e-mail"
#: templates/account/email_confirm.html:18
#, python-format
msgid ""
"\n"
" Please confirm that <b>%(email)s</b> is an e-mail address for user\n"
" %(user_display)s.\n"
" "
msgstr ""
"\n"
" Merci de confirmer que <b>%(email)s</b> est une adresse e-mail pour "
"l'utilisateur\n"
" %(user_display)s.\n"
" "
#: templates/account/email_confirm.html:26
msgid "Confirm"
msgstr "Confirmer"
#: templates/account/email_confirm.html:34
#, python-format
msgid ""
"\n"
" This e-mail confirmation link expired or is invalid.<br>\n"
" Please <a href=\"%(email_url)s\">issue a new e-mail confirmation "
"request</a>.\n"
" "
msgstr ""
"\n"
" Ce lien de confirmation d'e-mail a expiré ou est invalide.<br>\n"
" Merci de <a href=\"%(email_url)s\">soumettre une nouvelle requête de "
"confirmation d'e-mail</a>.\n"
" "
#: templates/account/login.html:5 templates/account/login.html:6
#: templates/account/login.html:78
msgid "Sign In"
msgstr "Connexion"
#: templates/account/login.html:14
msgid ""
"\n"
" Authentication failed. Please check your credentials and try "
"again.\n"
" "
msgstr ""
"\n"
" L'authentification a échoué. Merci de vérifier vos identifiants et "
"essayer à nouveau.\n"
" "
#: templates/account/login.html:22
#, python-format
msgid ""
"\n"
" Your are authenticated as %(user_str)s, but are not authorized to "
"access\n"
" this page. Would you like to login to a different account ?\n"
" "
msgstr ""
"\n"
" Vous êtes identifié comme %(user_str)s, mais vous n'être pas "
"autorisé à accéder\n"
" à cette page. Voulez-vous essayer avec un compte différent ?\n"
" "
#: templates/account/login.html:44
msgid "Connect with Clipper"
msgstr "Connexion via Clipper"
#: templates/account/login.html:45
msgid "Other choices"
msgstr "Autres choix"
#: templates/account/login.html:45
msgid "Other ways to sign in/sign up"
msgstr "Autres méthodes de connexion"
#: templates/account/login.html:65
msgid ""
"\n"
" Please sign in with one of your existing third party accounts, or with "
"the form below.\n"
" "
msgstr ""
"\n"
" Connectez-vous avec l'un de vos comptes tiers existants, ou avec le "
"formulaire ci-dessous.\n"
" "
#: templates/account/login.html:70
msgid "Forgot Password?"
msgstr "Mot de passe oublié ?"
#: templates/account/login.html:73 templates/account/signup.html:17
#: templates/socialaccount/signup.html:19
msgid "Sign Up"
msgstr "Nouveau compte"
#: templates/account/logout.html:4 templates/account/logout.html:5
#: templates/account/logout.html:20
msgid "Sign Out"
msgstr "Déconnexion"
#: templates/account/logout.html:11
msgid ""
"\n"
" Are you sure you want to sign out?\n"
" "
msgstr ""
"\n"
" Êtes-vous sûr de vouloir vous déconnecter ?\n"
" "
#: templates/account/password_change.html:4
#: templates/account/password_change.html:5
#: templates/account/password_change.html:18
#: templates/account/password_reset_from_key.html:4
#: templates/account/password_reset_from_key.html:5
#: templates/account/password_reset_from_key_done.html:4
#: templates/account/password_reset_from_key_done.html:5
msgid "Change Password"
msgstr "Changer le mot de passe"
#: templates/account/password_reset.html:4
#: templates/account/password_reset.html:5
#: templates/account/password_reset_done.html:4
#: templates/account/password_reset_done.html:5
msgid "Password Reset"
msgstr "Réinitialisation du mot de passe"
#: templates/account/password_reset.html:11
msgid ""
"\n"
" Forgotten your password? Enter your e-mail address below, and we'll "
"send\n"
" you an e-mail allowing you to reset it.\n"
" "
msgstr ""
"\n"
" Vous avez oublié votre mot de passe ? Entrez votre adresse e-mail ci-"
"dessous, et\n"
" nous vous enverrons un e-mail qui vous permettra de le réinitialiser.\n"
" "
#: templates/account/password_reset.html:18
#: templates/account/password_reset_from_key.html:21
msgid "Reset Password"
msgstr "Réinitialiser le mot de passe"
#: templates/account/password_reset_done.html:11
msgid ""
"\n"
" We have sent you an e-mail. Please contact us if you do not receive it "
"within a few minutes.\n"
" "
msgstr ""
"\n"
" Nous vous avons envoyé un e-mail. Merci de nous contacter si vous ne "
"l'avez pas reçu d'ici quelques minutes.\n"
" "
#: templates/account/password_reset_from_key.html:13
#, python-format
msgid ""
"\n"
" The password reset link was invalid, possibly because it has already "
"been used.\n"
" Please request a <a href=\"%(passwd_reset_url)s\">new password reset</"
"a>.\n"
" "
msgstr ""
"\n"
" Le lien de réinitialisation de mot de passe est invalide, possiblement "
"parce qu'il a déjà été utilisé.\n"
" Merci de faire une <a href=\"%(passwd_reset_url)s\">nouvelle demande</"
"a>.\n"
" "
#: templates/account/password_reset_from_key.html:24
msgid "Your password is now changed."
msgstr "Votre mot de passe a été modifié."
#: templates/account/password_reset_from_key_done.html:11
msgid ""
"\n"
" Your password is now changed.\n"
" "
msgstr ""
"\n"
" Votre mot de passe a été modifié.\n"
" "
#: templates/account/password_set.html:4 templates/account/password_set.html:5
#: templates/account/password_set.html:24
msgid "Set Password"
msgstr "Définir le mot de passe"
#: templates/account/password_set.html:17
msgid ""
"\n"
" Your account does not have a password yet. Add one to authenticate "
"without\n"
" third parties.\n"
" "
msgstr ""
"\n"
" Votre compte n'a pas encore de mot de passe. Ajoutez-en un pour vous "
"connecter\n"
" sans compte tiers.\n"
" "
#: templates/account/signup.html:4 templates/account/signup.html:5
#: templates/socialaccount/signup.html:4 templates/socialaccount/signup.html:5
msgid "Signup"
msgstr "Nouveau compte"
#: templates/account/signup.html:12
msgid "Already have an account?"
msgstr "Vous avez déjà un compte ?"
#: templates/account/signup_closed.html:4
#: templates/account/signup_closed.html:5
msgid "Sign Up Closed"
msgstr "Création de compte fermée"
#: templates/account/signup_closed.html:11
msgid ""
"\n"
" We are sorry, but the sign up is currently closed.\n"
" "
msgstr ""
"\n"
" Nous sommes désolés, mais la création de compte est actuellement "
"désactivée.\n"
" "
#: templates/allauth_ens/base.html:71
msgid "Not Connected"
msgstr "Non connecté"
#: templates/socialaccount/authentication_error.html:4
#: templates/socialaccount/authentication_error.html:5
msgid "Login Failure"
msgstr "Échec de la connexion"
#: templates/socialaccount/authentication_error.html:11
msgid ""
"\n"
" An error occured while attempting to login via your social network "
"account.\n"
" "
msgstr ""
"\n"
" Une erreur s'est produite lors de la connexion via le site externe.\n"
" "
#: templates/socialaccount/connections.html:5
#: templates/socialaccount/connections.html:6
msgid "Account Connections"
msgstr "Méthodes de connexion"
#: templates/socialaccount/connections.html:13
msgid ""
"\n"
" You can sign in to your account using any of the following third party "
"accounts:\n"
" "
msgstr ""
"\n"
" Vous pouvez vous connecter à votre compte en utilisant n'importe lequel "
"de ces comptes tiers :\n"
" "
#: templates/socialaccount/connections.html:17
msgid ""
"\n"
" You currently have no third party accounts connected to this account.\n"
" "
msgstr ""
"\n"
" Vous n'avez actuellement aucun compte tiers associé à ce compte.\n"
" "
#: templates/socialaccount/login_cancelled.html:4
#: templates/socialaccount/login_cancelled.html:5
msgid "Login Cancelled"
msgstr "Connexion annulée"
#: templates/socialaccount/login_cancelled.html:13
#, python-format
msgid ""
"\n"
" 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)s"
"\">sign in</a>."
msgstr ""
"\n"
" Vous avez décidé d'annuler la connexion à notre site via un de vos "
"comptes existants. Si cela était une erreur, vous pouvez revenir sur <a href="
"\"%(login_url)s\">la page de connexion</a>."
#: templates/socialaccount/signup.html:11
#, python-format
msgid ""
"\n"
" You are about to use your %(provider_name)s account to login.\n"
" As a final step, please complete the following form:\n"
" "
msgstr ""
"\n"
" Vous êtes sur le point d'utiliser votre compte %(provider_name)s pour "
"vous connecter\n"
" Pour finaliser la procédure, merci de remplir le formulaire suivant :\n"
" "

View file

@ -1,16 +0,0 @@
# coding: utf-8
from django.core.management.base import BaseCommand
from allauth_ens.adapter import deprecate_clippers
class Command(BaseCommand):
help = 'Deprecates clipper SocialAccounts so as to avoid conflicts'
def add_arguments(self, parser):
pass
def handle(self, *args, **options):
deprecate_clippers()
self.stdout.write(self.style.SUCCESS(
'Clippers deprecation successful'))

View file

@ -1,80 +0,0 @@
# coding: utf-8
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand
from allauth.socialaccount.models import SocialAccount
from allauth_ens.adapter import install_longterm_adapter
class Command(BaseCommand):
help = 'Manages the transition from an older django_cas' \
'or an allauth_ens installation without ' \
'LongTermClipperAccountAdapter'
def add_arguments(self, parser):
parser.add_argument(
'--fake',
action='store_true',
default=False,
help=('Does not save the models created/updated,'
'only shows the list'),
)
parser.add_argument(
'--use-socialaccounts',
action='store_true',
default=False,
help=('Use the existing SocialAccounts rather than all the Users'),
)
parser.add_argument(
'--keep-usernames',
action='store_true',
default=False,
help=('Do not apply the username template (e.g. clipper@promo) to'
'the existing account, only populate the SocialAccounts with'
'ldap informations'),
)
parser.add_argument(
'--clipper-field',
default=None,
type=str
)
pass
def handle(self, *args, **options):
fake = options.get("fake", False)
keep_usernames = options.get("keep_usernames", False)
if options.get('use_socialaccounts', False):
accounts = {account.uid: account.user for account in
(SocialAccount.objects.filter(provider="clipper")
.prefetch_related("user"))}
elif options.get('clipper_field', None):
fields = options['clipper_field'].split('.')
User = get_user_model()
def get_subattr(obj, fields):
# Allows to follow OneToOne relationships
if len(fields) == 1:
return getattr(obj, fields[0])
return get_subattr(getattr(obj, fields[0]), fields[1:])
accounts = {get_subattr(account, fields): account for account in
User.objects.all()}
else:
accounts = None
logs = install_longterm_adapter(fake, accounts, keep_usernames)
self.stdout.write("Social accounts created : %d"
% len(logs["created"]))
self.stdout.write(" ".join(("%s -> %s" % s) for s in logs["created"]))
self.stdout.write("Social accounts displaced : %d"
% len(logs["updated"]))
self.stdout.write(" ".join(("%s -> %s" % s) for s in logs["updated"]))
self.stdout.write("User accounts unmodified : %d"
% len(logs["unmodified"]))
self.stdout.write(" ".join(logs["unmodified"]))
self.stdout.write(self.style.SUCCESS(
"LongTermClipper migration successful"))

View file

@ -1,9 +1,12 @@
# -*- coding: utf-8 -*-
import ldap
from allauth.account.models import EmailAddress
from allauth.socialaccount.providers.base import ProviderAccount
from allauth_cas.providers import CASProvider
from django.conf import settings
class ClipperAccount(ProviderAccount):
pass
@ -19,13 +22,46 @@ class ClipperProvider(CASProvider):
return '{}@clipper.ens.fr'.format(uid.strip().lower())
def extract_uid(self, data):
# Normalize UID
uid, _ = data
uid = uid.lower().strip()
return uid
def extract_common_fields(self, data):
def get_names(clipper):
assert clipper.isalnum()
try:
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT,
ldap.OPT_X_TLS_NEVER)
l = ldap.initialize("ldaps://ldap.spi.ens.fr:636")
l.set_option(ldap.OPT_REFERRALS, 0)
l.set_option(ldap.OPT_PROTOCOL_VERSION, 3)
l.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND)
l.set_option(ldap.OPT_X_TLS_DEMAND, True)
l.set_option(ldap.OPT_DEBUG_LEVEL, 255)
l.set_option(ldap.OPT_NETWORK_TIMEOUT, 10)
l.set_option(ldap.OPT_TIMEOUT, 10)
info = l.search_s('dc=spi,dc=ens,dc=fr',
ldap.SCOPE_SUBTREE,
('(uid=%s)' % (clipper,)),
[str("cn"), ])
if len(info) > 0:
fullname = info[0][1].get('cn', [''])[0].decode("utf-8")
first_name, last_name = fullname.split(' ', 1)
return first_name, last_name
except ldap.LDAPError:
pass
return '', ''
common = super(ClipperProvider, self).extract_common_fields(data)
fn, ln = get_names(common['username'])
common['email'] = self.extract_email(data)
common['name'] = fn
common['last_name'] = ln
return common
def extract_email_addresses(self, data):
@ -37,23 +73,8 @@ class ClipperProvider(CASProvider):
]
def extract_extra_data(self, data):
"""
If LongTermClipperAccountAdapter is in use, keep the data retrieved
from the LDAP server.
"""
from allauth.socialaccount.models import SocialAccount # noqa
extra_data = super(ClipperProvider, self).extract_extra_data(data)
extra_data['email'] = self.extract_email(data)
# Preserve LDAP data at all cost.
try:
clipper_account = SocialAccount.objects.get(
provider=self.id, uid=self.extract_uid(data))
if 'ldap' in clipper_account.extra_data:
extra_data['ldap'] = clipper_account.extra_data['ldap']
except SocialAccount.DoesNotExist:
pass
return extra_data
def message_suggest_caslogout_on_logout(self, request):

View file

@ -17,18 +17,6 @@ class ClipperProviderTests(CASTestCase):
u = User.objects.get(username='clipper_uid')
self.assertEqual(u.email, 'clipper_uid@clipper.ens.fr')
def test_extra_data_keeps_ldap_data(self):
clipper_conn = self.u.socialaccount_set.create(
uid='user', provider='clipper',
extra_data={'ldap': {'aa': 'bb'}},
)
self.client_cas_login(
self.client, provider_id='clipper', username='user')
clipper_conn.refresh_from_db()
self.assertEqual(clipper_conn.extra_data['ldap'], {'aa': 'bb'})
class ClipperViewsTests(CASViewTestCase):

View file

@ -57,7 +57,7 @@ $main-max-width: 500px;
.content-wrapper {
display: flex;
flex-flow: row wrap;
flex-flow: row nowrap;
align-items: center;
/* Blocks */
@ -68,17 +68,13 @@ $main-max-width: 500px;
}
@media (min-width: 500px) {
& {
flex-flow: row nowrap;
}
& > section {
flex: 1 1 auto;
min-width: 250px;
}
}
@media (min-width: 1200px) {
@media (min-width: 120px) {
& > section {
flex: 1 1 auto;
min-width: 350px;
@ -220,6 +216,7 @@ section {
.method-list {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
& > .method-wrapper {
flex: 1 100%;

View file

@ -1,35 +0,0 @@
.content-wrapper.highlight-clipper {
.main-login-choices {
li:not(:first-child) {
margin-top: 5px;
}
a {
display: block;
text-align: center;
background: $gray-lighter;
padding: 35px 20px;
color: $black;
font-size: 1.1em;
@include hover-focus {
background: lighten($brand-primary, 50%);
text-decoration: none;
}
}
}
&:not(.not-clipper) {
width: 100vw;
max-width: 500px;
& > :not(.main-login-choices) {
display: none;
}
}
&.not-clipper {
.main-login-choices {
display: none;
}
}
}

View file

@ -5,5 +5,3 @@
@import "mixins";
@import "base";
@import "highlight_clipper";

View file

@ -1,24 +0,0 @@
/**
* The font "Roboto" and its derivatives are under Apache license, as published
* here: https://www.apache.org/licenses/LICENSE-2.0
* The font is by Christian Robertson. Thanks!
*/
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(roboto-regular.ttf) format('truetype');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'), url(roboto-bold.ttf) format('truetype');
}
@font-face {
font-family: 'Roboto Slab';
font-style: normal;
font-weight: 400;
src: local('Roboto Slab Regular'), local('RobotoSlab-Regular'), url(roboto-slab-regular.ttf) format('truetype');
}

View file

@ -1,4 +1,4 @@
/* line 5, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
/* line 5, ../../../vendor/bundle/ruby/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
@ -20,45 +20,45 @@ time, mark, audio, video {
vertical-align: baseline;
}
/* line 22, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
/* line 22, ../../../vendor/bundle/ruby/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
html {
line-height: 1;
}
/* line 24, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
/* line 24, ../../../vendor/bundle/ruby/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
ol, ul {
list-style: none;
}
/* line 26, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
/* line 26, ../../../vendor/bundle/ruby/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
table {
border-collapse: collapse;
border-spacing: 0;
}
/* line 28, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
/* line 28, ../../../vendor/bundle/ruby/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
caption, th, td {
text-align: left;
font-weight: normal;
vertical-align: middle;
}
/* line 30, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
/* line 30, ../../../vendor/bundle/ruby/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
q, blockquote {
quotes: none;
}
/* line 103, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
/* line 103, ../../../vendor/bundle/ruby/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
q:before, q:after, blockquote:before, blockquote:after {
content: "";
content: none;
}
/* line 32, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
/* line 32, ../../../vendor/bundle/ruby/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
a img {
border: none;
}
/* line 116, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
/* line 116, ../../../vendor/bundle/ruby/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section, summary {
display: block;
}
@ -160,7 +160,7 @@ b {
/* line 58, ../../scss/_base.scss */
.content-wrapper {
display: flex;
flex-flow: row wrap;
flex-flow: row nowrap;
align-items: center;
/* Blocks */
}
@ -171,24 +171,20 @@ b {
}
@media (min-width: 500px) {
/* line 71, ../../scss/_base.scss */
.content-wrapper {
flex-flow: row nowrap;
}
/* line 75, ../../scss/_base.scss */
.content-wrapper > section {
flex: 1 1 auto;
min-width: 250px;
}
}
@media (min-width: 1200px) {
/* line 82, ../../scss/_base.scss */
@media (min-width: 120px) {
/* line 78, ../../scss/_base.scss */
.content-wrapper > section {
flex: 1 1 auto;
min-width: 350px;
}
}
/* line 90, ../../scss/_base.scss */
/* line 86, ../../scss/_base.scss */
#providers {
width: 150px;
min-width: unset;
@ -198,7 +194,7 @@ b {
/**********
* Header *
**********/
/* line 104, ../../scss/_base.scss */
/* line 100, ../../scss/_base.scss */
header {
display: flex;
align-items: stretch;
@ -214,7 +210,7 @@ header::after {
content: "";
clear: both;
}
/* line 118, ../../scss/_base.scss */
/* line 114, ../../scss/_base.scss */
header a {
color: #fff !important;
}
@ -223,7 +219,7 @@ header a:focus, header a:hover {
background: #d43f3a;
text-decoration: none;
}
/* line 127, ../../scss/_base.scss */
/* line 123, ../../scss/_base.scss */
header .right {
display: flex;
flex-flow: column;
@ -234,25 +230,25 @@ header .right {
font-size: 14px;
text-align: center;
}
/* line 140, ../../scss/_base.scss */
/* line 136, ../../scss/_base.scss */
header .right > * {
flex: 0 0 auto;
}
/* line 143, ../../scss/_base.scss */
/* line 139, ../../scss/_base.scss */
header .right > * > * {
display: block;
padding: 5px 10px;
}
/* line 149, ../../scss/_base.scss */
/* line 145, ../../scss/_base.scss */
header .right #connect-status {
font-weight: normal;
font-size: 12px;
}
/* line 153, ../../scss/_base.scss */
/* line 149, ../../scss/_base.scss */
header .right #connect-status .fa {
margin-right: 5px;
}
/* line 159, ../../scss/_base.scss */
/* line 155, ../../scss/_base.scss */
header h1 {
flex: 1 1 auto;
padding: 15px 25px;
@ -261,11 +257,11 @@ header h1 {
/************
* Messages *
************/
/* line 172, ../../scss/_base.scss */
/* line 168, ../../scss/_base.scss */
.messages-container {
padding: 0 15px;
}
/* line 175, ../../scss/_base.scss */
/* line 171, ../../scss/_base.scss */
.messages-container::after {
display: block;
content: "";
@ -273,30 +269,30 @@ header h1 {
height: 2px;
}
/* line 183, ../../scss/_base.scss */
/* line 179, ../../scss/_base.scss */
.messages-list {
padding: 10px 0;
}
/* line 193, ../../scss/_base.scss */
/* line 189, ../../scss/_base.scss */
.message {
padding: 5px;
font-size: 14px;
color: #292b2c;
}
/* line 188, ../../scss/_base.scss */
/* line 184, ../../scss/_base.scss */
.message.info {
color: #28a1c5;
}
/* line 188, ../../scss/_base.scss */
/* line 184, ../../scss/_base.scss */
.message.success {
color: #2d672d;
}
/* line 188, ../../scss/_base.scss */
/* line 184, ../../scss/_base.scss */
.message.warning {
color: #b06d0f;
}
/* line 188, ../../scss/_base.scss */
/* line 184, ../../scss/_base.scss */
.message.error {
color: #b52b27;
}
@ -304,23 +300,24 @@ header h1 {
/***********
* Content *
***********/
/* line 212, ../../scss/_base.scss */
/* line 208, ../../scss/_base.scss */
section > * + * {
margin-top: 15px;
}
/* Methods list */
/* line 220, ../../scss/_base.scss */
/* line 216, ../../scss/_base.scss */
.method-list {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
}
/* line 224, ../../scss/_base.scss */
/* line 221, ../../scss/_base.scss */
.method-list > .method-wrapper {
flex: 1 100%;
padding: 2px 0;
}
/* line 228, ../../scss/_base.scss */
/* line 225, ../../scss/_base.scss */
.method-list > .method-wrapper a {
transition: background 0.15s, color 0.15s, border-color 0.15s;
display: block;
@ -350,11 +347,11 @@ section > * + * {
}
/* Connected accounts list */
/* line 252, ../../scss/_base.scss */
/* line 249, ../../scss/_base.scss */
.connections-providers-list > * + * {
margin-top: 2px;
}
/* line 256, ../../scss/_base.scss */
/* line 253, ../../scss/_base.scss */
.connections-providers-list > * > .heading {
width: 100%;
padding: 10px;
@ -367,7 +364,7 @@ section > * + * {
content: "";
clear: both;
}
/* line 265, ../../scss/_base.scss */
/* line 262, ../../scss/_base.scss */
.connections-providers-list > * > .heading .connect {
transition: background 0.15s, color 0.15s, border-color 0.15s;
display: block;
@ -398,29 +395,29 @@ section > * + * {
color: #fff;
}
/* line 277, ../../scss/_base.scss */
/* line 274, ../../scss/_base.scss */
.connections-list {
border-left: 5px solid #f7f7f9;
}
/* line 280, ../../scss/_base.scss */
/* line 277, ../../scss/_base.scss */
.connections-list > * {
padding: 10px;
font-size: 14px;
}
/* line 284, ../../scss/_base.scss */
/* line 281, ../../scss/_base.scss */
.connections-list > * + * {
border-top: 1px dotted #eceeef;
}
/* line 288, ../../scss/_base.scss */
/* line 285, ../../scss/_base.scss */
.connections-list > * > .fa {
margin-right: 5px;
}
/* line 292, ../../scss/_base.scss */
/* line 289, ../../scss/_base.scss */
.connections-list > * .delete {
float: right;
margin-top: -2px;
}
/* line 296, ../../scss/_base.scss */
/* line 293, ../../scss/_base.scss */
.connections-list > * .delete [type=submit] {
transition: background 0.15s, color 0.15s, border-color 0.15s;
display: block;
@ -449,17 +446,17 @@ section > * + * {
background: #b52b27;
color: #fff;
}
/* line 305, ../../scss/_base.scss */
/* line 302, ../../scss/_base.scss */
.connections-list form {
display: inline-block;
}
/* E-mail adresses list */
/* line 314, ../../scss/_base.scss */
/* line 311, ../../scss/_base.scss */
.emailaddress-list .emailaddress {
border-bottom: 1px dotted #464a4c;
}
/* line 318, ../../scss/_base.scss */
/* line 315, ../../scss/_base.scss */
.emailaddress-list .emailaddress .summary {
height: 45px;
}
@ -469,13 +466,13 @@ section > * + * {
content: "";
clear: both;
}
/* line 323, ../../scss/_base.scss */
/* line 320, ../../scss/_base.scss */
.emailaddress-list .emailaddress .summary > * {
float: left;
height: 100%;
padding: 12px;
}
/* line 329, ../../scss/_base.scss */
/* line 326, ../../scss/_base.scss */
.emailaddress-list .emailaddress .summary > .primary, .emailaddress-list .emailaddress .summary > .verified-status {
float: right;
width: 45px;
@ -483,25 +480,25 @@ section > * + * {
text-align: center;
font-size: 20px;
}
/* line 337, ../../scss/_base.scss */
/* line 334, ../../scss/_base.scss */
.emailaddress-list .emailaddress .summary > .email {
padding: 12px;
font-weight: bold;
}
/* line 342, ../../scss/_base.scss */
/* line 339, ../../scss/_base.scss */
.emailaddress-list .emailaddress .summary > .primary {
color: #025aa5;
}
/* line 346, ../../scss/_base.scss */
/* line 343, ../../scss/_base.scss */
.emailaddress-list .emailaddress .summary > .verified {
color: #449d44;
}
/* line 350, ../../scss/_base.scss */
/* line 347, ../../scss/_base.scss */
.emailaddress-list .emailaddress .summary > .unverified {
color: #ec971f;
}
/* line 359, ../../scss/_base.scss */
/* line 356, ../../scss/_base.scss */
.actions {
margin-bottom: 10px;
}
@ -511,7 +508,7 @@ section > * + * {
content: "";
clear: both;
}
/* line 364, ../../scss/_base.scss */
/* line 361, ../../scss/_base.scss */
.actions > * {
float: right;
margin-right: 10px;
@ -521,7 +518,7 @@ section > * + * {
/*********
* Forms *
*********/
/* line 396, ../../scss/_base.scss */
/* line 393, ../../scss/_base.scss */
.input-wrapper {
position: relative;
display: block;
@ -534,13 +531,13 @@ section > * + * {
content: "";
clear: both;
}
/* line 404, ../../scss/_base.scss */
/* line 401, ../../scss/_base.scss */
.input-wrapper label {
margin-right: 10px;
color: rgba(0, 0, 0, 0.38);
font-size: 16px;
}
/* line 411, ../../scss/_base.scss */
/* line 408, ../../scss/_base.scss */
.input-wrapper .transform-label label {
padding-left: 0;
width: 100%;
@ -561,7 +558,7 @@ section > * + * {
text-overflow: ellipsis;
white-space: nowrap;
}
/* line 433, ../../scss/_base.scss */
/* line 430, ../../scss/_base.scss */
.input-wrapper input.field:not([type=checkbox]) {
height: 30px;
width: 100%;
@ -571,80 +568,80 @@ section > * + * {
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
font-size: 16px;
}
/* line 447, ../../scss/_base.scss */
/* line 444, ../../scss/_base.scss */
.input-wrapper input[type="checkbox"] {
vertical-align: sub;
width: 20px;
height: 20px;
}
/* line 453, ../../scss/_base.scss */
/* line 450, ../../scss/_base.scss */
.input-wrapper select {
height: 30px;
background: #fff;
}
/* line 471, ../../scss/_base.scss */
/* line 468, ../../scss/_base.scss */
.input-wrapper.input-focused label, .input-wrapper.input-focused .messages {
color: inherit;
}
/* line 477, ../../scss/_base.scss */
/* line 474, ../../scss/_base.scss */
.input-wrapper.input-focused .transform-label label, .input-wrapper.input-has-value .transform-label label {
transform: translate3d(0, 6.5px, 0) scale(0.75);
}
/* line 459, ../../scss/_base.scss */
/* line 456, ../../scss/_base.scss */
.input-wrapper.input-has-value {
color: #449d44;
}
/* line 463, ../../scss/_base.scss */
/* line 460, ../../scss/_base.scss */
.input-wrapper.input-has-value input.field {
padding-bottom: 0px;
border-width: 2px;
border-color: #449d44;
}
/* line 459, ../../scss/_base.scss */
/* line 456, ../../scss/_base.scss */
.input-wrapper.input-error {
color: #d9534f;
}
/* line 463, ../../scss/_base.scss */
/* line 460, ../../scss/_base.scss */
.input-wrapper.input-error input.field {
padding-bottom: 0px;
border-width: 2px;
border-color: #d9534f;
}
/* line 459, ../../scss/_base.scss */
/* line 456, ../../scss/_base.scss */
.input-wrapper.input-focused {
color: #025aa5;
}
/* line 463, ../../scss/_base.scss */
/* line 460, ../../scss/_base.scss */
.input-wrapper.input-focused input.field {
padding-bottom: 0px;
border-width: 2px;
border-color: #025aa5;
}
/* line 486, ../../scss/_base.scss */
/* line 483, ../../scss/_base.scss */
.input-wrapper .messages-spacer {
float: right;
min-height: 10px;
min-width: 1px;
}
/* line 492, ../../scss/_base.scss */
/* line 489, ../../scss/_base.scss */
.input-wrapper .messages {
color: rgba(0, 0, 0, 0.38);
}
/* line 495, ../../scss/_base.scss */
/* line 492, ../../scss/_base.scss */
.input-wrapper .messages > * {
padding-top: 5px;
font-size: 12px;
}
/* line 502, ../../scss/_base.scss */
/* line 499, ../../scss/_base.scss */
.input-wrapper.input-checkbox > :first-child, .input-wrapper.input-radio > :first-child {
display: block;
}
/* line 508, ../../scss/_base.scss */
/* line 505, ../../scss/_base.scss */
.buttons-choices {
display: inline-flex;
}
/* line 511, ../../scss/_base.scss */
/* line 508, ../../scss/_base.scss */
.buttons-choices > button {
transition: background 0.15s, color 0.15s, border-color 0.15s;
flex: 0 1 auto;
@ -653,28 +650,28 @@ section > * + * {
background: #daeeff;
color: #636c72;
}
/* line 520, ../../scss/_base.scss */
/* line 517, ../../scss/_base.scss */
.buttons-choices > button:first-child {
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
/* line 525, ../../scss/_base.scss */
/* line 522, ../../scss/_base.scss */
.buttons-choices > button:last-child {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
/* line 530, ../../scss/_base.scss */
/* line 527, ../../scss/_base.scss */
.buttons-choices > button:focus {
background: #43a7fd;
color: white;
}
/* line 535, ../../scss/_base.scss */
/* line 532, ../../scss/_base.scss */
.buttons-choices > button.selected {
background: #025aa5;
color: white;
}
/* line 541, ../../scss/_base.scss */
/* line 538, ../../scss/_base.scss */
[type=submit]:not(.link) {
transition: background 0.15s, color 0.15s, border-color 0.15s;
display: block;
@ -701,7 +698,7 @@ section > * + * {
color: #fff;
}
/* line 547, ../../scss/_base.scss */
/* line 544, ../../scss/_base.scss */
[type=submit].link {
transition: background 0.15s, color 0.15s, border-color 0.15s;
color: #025aa5;
@ -718,23 +715,23 @@ section > * + * {
cursor: pointer;
}
/* line 556, ../../scss/_base.scss */
/* line 553, ../../scss/_base.scss */
.form-inline {
display: flex;
align-items: center;
}
/* line 560, ../../scss/_base.scss */
/* line 557, ../../scss/_base.scss */
.form-inline > .input-wrapper {
flex: 1 0 auto;
}
/* line 564, ../../scss/_base.scss */
/* line 561, ../../scss/_base.scss */
.form-inline [type=submit] {
margin-top: -5px;
margin-left: 8px;
font-size: 14px;
}
/* line 571, ../../scss/_base.scss */
/* line 568, ../../scss/_base.scss */
.btn {
transition: background 0.15s, color 0.15s, border-color 0.15s;
display: block;
@ -754,7 +751,7 @@ section > * + * {
cursor: pointer;
}
/* line 576, ../../scss/_base.scss */
/* line 573, ../../scss/_base.scss */
.btn-primary {
background: #025aa5;
color: #fff;
@ -765,7 +762,7 @@ section > * + * {
color: #fff;
}
/* line 577, ../../scss/_base.scss */
/* line 574, ../../scss/_base.scss */
.btn-success {
background: #449d44;
color: #fff;
@ -775,35 +772,3 @@ section > * + * {
background: #2d672d;
color: #fff;
}
/* line 3, ../../scss/_highlight_clipper.scss */
.content-wrapper.highlight-clipper .main-login-choices li:not(:first-child) {
margin-top: 5px;
}
/* line 7, ../../scss/_highlight_clipper.scss */
.content-wrapper.highlight-clipper .main-login-choices a {
display: block;
text-align: center;
background: #eceeef;
padding: 35px 20px;
color: #000;
font-size: 1.1em;
}
/* line 10, ../../scss/_mixins.scss */
.content-wrapper.highlight-clipper .main-login-choices a:focus, .content-wrapper.highlight-clipper .main-login-choices a:hover {
background: #a8d6fe;
text-decoration: none;
}
/* line 21, ../../scss/_highlight_clipper.scss */
.content-wrapper.highlight-clipper:not(.not-clipper) {
width: 100vw;
max-width: 500px;
}
/* line 25, ../../scss/_highlight_clipper.scss */
.content-wrapper.highlight-clipper:not(.not-clipper) > :not(.main-login-choices) {
display: none;
}
/* line 31, ../../scss/_highlight_clipper.scss */
.content-wrapper.highlight-clipper.not-clipper .main-login-choices {
display: none;
}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 434 KiB

File diff suppressed because one or more lines are too long

View file

@ -30,32 +30,18 @@
{% endif %}
{% endblock %}
{% block content-extra-classes %}{% is_clipper_highlighted as highlight_clipper %}{% if highlight_clipper %}highlight-clipper{% endif %}{% endblock %}
{% block content %}
{% get_providers as socialaccount_providers %}
{% is_clipper_highlighted as highlight_clipper %}
{% if highlight_clipper %}
<section class="main-login-choices">
<ul>
<li><a title="Clipper" href="{% provider_login_url "clipper" process="login" scope=scope auth_params=auth_params %}">{% trans "Connect with Clipper" %}</a></li>
<li><a title="{% trans "Other choices" %}" href="javascript:void(0);" onclick="$('.content-wrapper').toggleClass('not-clipper')">{% trans "Other ways to sign in/sign up" %}</a></li>
</ul>
</section>
{% endif %}
{% if socialaccount_providers %}
<section id="providers">
<ul class="method-list">
{% include "socialaccount/snippets/provider_list.html" with process="login" %}
</ul>
{% include "socialaccount/snippets/login_extra.html" %}
</section>
{% include "socialaccount/snippets/login_extra.html" %}
<div class="divider"></div>
{% endif %}
@ -81,6 +67,7 @@
{% endif %}
</form>
</section>
{% endblock %}
{% block extra_js %}

View file

@ -16,15 +16,19 @@
{# CSS #}
<link rel="stylesheet"
href="{% static "vendor/font-awesome/4.7.0/css/font-awesome.min.css" %}">
href="https://fonts.googleapis.com/css?family=Roboto:400,700|Roboto+Slab:400">
<link rel="stylesheet"
href="{% static "allauth_ens/fonts/index.css" %}">
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"
integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN"
crossorigin="anonymous">
<link rel="stylesheet"
href="{% static "allauth_ens/screen.css" %}">
{# JS #}
<script type="text/javascript"
src="{% static "vendor/jquery/3.2.1/jquery-3.2.1.slim.min.js" %}"></script>
src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
integrity="sha256-k2WSCIexGzOj3Euiig+TlR8gA0EmPjuc79OEeY5L45g="
crossorigin="anonymous"></script>
<script type="text/javascript"
src="{% static "allauth_ens/authens.js" %}"></script>
@ -79,7 +83,7 @@
{% block messages-extra %}{% endblock %}
<div class="content-wrapper {% block content-extra-classes %}{% endblock %}">
<div class="content-wrapper">
{% block content %}{% endblock %}
</div>

View file

@ -7,7 +7,7 @@
{% for provider in socialaccount_providers %}
{% if provider.id == "openid" %}
{% for brand in provider.get_brands %}
<li class="method-wrapper provider-openid-{{ brand.id }}">
<li class="method-wrapper">
<a title="{{ brand.name }}"
href="{% provider_login_url provider.id openid=brand.openid_url process=process %}"
>
@ -16,9 +16,9 @@
</li>
{% endfor %}
{% endif %}
<li class="method-wrapper provider-{{ provider.id }}">
<li class="method-wrapper">
<a title="{{ provider.name }}"
href="{% provider_login_url provider.id process=process scope=scope auth_params=auth_params %}"
href="{% provider_login_url provider.id process=process scope=scope auth_params=auth_parms %}"
>
{{ provider.name }}
</a>

View file

@ -33,10 +33,3 @@ def get_profile_url():
def is_open_for_signup(context):
request = context['request']
return get_adapter(request).is_open_for_signup(request)
@simple_tag
def is_clipper_highlighted():
installed_apps = getattr(settings, 'INSTALLED_APPS', [])
return ('allauth_ens.providers.clipper' in installed_apps) \
and getattr(settings, 'ALLAUTH_ENS_HIGHLIGHT_CLIPPER', True)

View file

@ -1,5 +1,3 @@
from __future__ import unicode_literals
import re
import django
@ -7,23 +5,6 @@ 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
from django.test.utils import captured_stdout
from allauth.socialaccount.models import SocialAccount
import six
from allauth_cas.test.testcases import CASTestCase
from fakeldap import MockLDAP
from mock import patch
from allauth_ens.utils import get_ldap_infos
from .adapter import deprecate_clippers, install_longterm_adapter
from .management.commands.install_longterm import Command as InstallLongterm
_mock_ldap = MockLDAP()
ldap_patcher = patch('allauth_ens.utils.ldap.initialize',
lambda x: _mock_ldap)
if django.VERSION >= (1, 10):
from django.urls import reverse
@ -154,320 +135,3 @@ class ViewsTests(TestCase):
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)
@override_settings(
SOCIALACCOUNT_ADAPTER='allauth_ens.adapter.LongTermClipperAccountAdapter'
)
class LongTermClipperTests(CASTestCase):
def setUp(self):
ldap_patcher.start()
def tearDown(self):
ldap_patcher.stop()
_mock_ldap.reset()
def _setup_ldap(self, promo=12, username="test"):
try:
buid = six.binary_type(username, 'utf-8')
home = six.binary_type('/users/%d/phy/test/' % promo, 'utf-8')
except TypeError:
buid = six.binary_type(username)
home = six.binary_type('/users/%d/phy/test/' % promo)
_mock_ldap.directory['dc=spi,dc=ens,dc=fr'] = {
'uid': [buid],
'cn': [b'John Smith'],
'mailRoutingAddress': [b'test@clipper.ens.fr'],
'homeDirectory': [home],
}
def _count_ldap_queries(self):
queries = _mock_ldap.ldap_methods_called()
count = len([op for op in queries if op != 'set_option'])
return count
def test_new_connexion(self):
self._setup_ldap()
r = self.client_cas_login(self.client, provider_id="clipper",
username="test")
u = r.context['user']
self.assertEqual(u.username, "test@12")
self.assertEqual(u.first_name, "John")
self.assertEqual(u.last_name, "Smith")
self.assertEqual(u.email, "test@clipper.ens.fr")
self.assertEqual(self._count_ldap_queries(), 1)
sa = list(SocialAccount.objects.all())[-1]
self.assertEqual(sa.user.id, u.id)
self.assertEqual(sa.extra_data['ldap']['entrance_year'], '12')
def test_connect_disconnect(self):
self._setup_ldap()
r0 = self.client_cas_login(self.client, provider_id="clipper",
username="test")
self.assertIn("_auth_user_id", self.client.session)
self.assertIn('user', r0.context)
self.client.logout()
self.assertNotIn("_auth_user_id", self.client.session)
def test_second_connexion(self):
self._setup_ldap()
self.client_cas_login(self.client, provider_id="clipper",
username="test")
self.client.logout()
nu = User.objects.count()
self.client_cas_login(self.client, provider_id="clipper",
username="test")
self.assertEqual(User.objects.count(), nu)
self.assertEqual(self._count_ldap_queries(), 1)
def test_deprecation(self):
self._setup_ldap()
self.client_cas_login(self.client, provider_id="clipper",
username="test")
deprecate_clippers()
sa = SocialAccount.objects.all()[0]
self.assertEqual(sa.provider, "clipper_inactive")
def test_reconnect_after_deprecation(self):
self._setup_ldap()
r = self.client_cas_login(self.client, provider_id="clipper",
username="test")
user0 = r.context['user']
n_sa0 = SocialAccount.objects.count()
n_u0 = User.objects.count()
self.client.logout()
deprecate_clippers()
r = self.client_cas_login(self.client, provider_id="clipper",
username="test")
user1 = r.context['user']
sa1 = list(SocialAccount.objects.all())
n_u1 = User.objects.count()
self.assertEqual(len(sa1), n_sa0)
self.assertEqual(n_u1, n_u0)
self.assertEqual(user1.id, user0.id)
self.assertEqual(self._count_ldap_queries(), 2)
def test_override_inactive_account(self):
self._setup_ldap(12)
r = self.client_cas_login(self.client, provider_id="clipper",
username="test")
user0 = r.context['user']
n_sa0 = SocialAccount.objects.count()
n_u0 = User.objects.count()
self.client.logout()
deprecate_clippers()
self._setup_ldap(13)
r = self.client_cas_login(self.client, provider_id="clipper",
username="test")
user1 = r.context['user']
sa1 = list(SocialAccount.objects.all())
n_u1 = User.objects.count()
self.assertEqual(len(sa1), n_sa0 + 1)
self.assertEqual(n_u1, n_u0 + 1)
self.assertNotEqual(user1.id, user0.id)
def test_multiple_deprecation(self):
self._setup_ldap(12)
self.client_cas_login(self.client, provider_id="clipper",
username="test")
self.client.logout()
self._setup_ldap(15, "truc")
self.client_cas_login(self.client, provider_id="clipper",
username="truc")
self.client.logout()
sa0 = SocialAccount.objects.count()
deprecate_clippers()
self._setup_ldap(13)
self.client_cas_login(self.client, provider_id="clipper",
username="test")
self.client.logout()
sa1 = SocialAccount.objects.count()
deprecate_clippers()
sa2 = SocialAccount.objects.count()
# Older "test" inactive SocialAccount gets erased by new one
# while "truc" remains
self.assertEqual(sa0, sa2)
self.assertEqual(sa1, sa0 + 1)
def test_longterm_installer_from_allauth(self):
self._setup_ldap(12)
with self.settings(
SOCIALACCOUNT_ADAPTER='allauth.socialaccount.'
'adapter.DefaultSocialAccountAdapter'):
r = self.client_cas_login(self.client, provider_id="clipper",
username='test')
user0 = r.context["user"]
nsa0 = SocialAccount.objects.count()
self.assertEqual(user0.username, "test")
self.client.logout()
outputs = install_longterm_adapter()
self.assertEqual(outputs["updated"], [("test", "test@12")])
r = self.client_cas_login(self.client, provider_id="clipper",
username='test')
user1 = r.context["user"]
nsa1 = SocialAccount.objects.count()
conn = user1.socialaccount_set.get(provider='clipper')
self.assertEqual(user1.id, user0.id)
self.assertEqual(nsa1, nsa0)
self.assertEqual(user1.username, "test@12")
self.assertEqual(conn.extra_data['ldap']['entrance_year'], '12')
def test_longterm_installer_from_allauth_command_using_username(self):
self._setup_ldap(12)
with self.settings(
SOCIALACCOUNT_ADAPTER='allauth.socialaccount.'
'adapter.DefaultSocialAccountAdapter'):
r = self.client_cas_login(self.client, provider_id="clipper",
username='test')
user0 = r.context["user"]
nsa0 = SocialAccount.objects.count()
self.assertEqual(user0.username, "test")
self.client.logout()
with captured_stdout() as stdout:
command = InstallLongterm()
command.handle(clipper_field='username')
output = stdout.getvalue()
self.assertIn('test -> test@12', output)
r = self.client_cas_login(self.client, provider_id="clipper",
username='test')
user1 = r.context["user"]
nsa1 = SocialAccount.objects.count()
conn = user1.socialaccount_set.get(provider='clipper')
self.assertEqual(user1.id, user0.id)
self.assertEqual(nsa1, nsa0)
self.assertEqual(user1.username, "test@12")
self.assertEqual(conn.extra_data['ldap']['entrance_year'], '12')
def test_longterm_installer_from_allauth_command_keeping_username(self):
self._setup_ldap(12)
with self.settings(
SOCIALACCOUNT_ADAPTER='allauth.socialaccount.'
'adapter.DefaultSocialAccountAdapter'):
r = self.client_cas_login(self.client, provider_id="clipper",
username='test')
user0 = r.context["user"]
nsa0 = SocialAccount.objects.count()
self.assertEqual(user0.username, "test")
self.client.logout()
with captured_stdout() as stdout:
command = InstallLongterm()
command.handle(keep_usernames=True)
output = stdout.getvalue()
self.assertIn('test -> test', output)
r = self.client_cas_login(self.client, provider_id="clipper",
username='test')
user1 = r.context["user"]
nsa1 = SocialAccount.objects.count()
conn = user1.socialaccount_set.get(provider='clipper')
self.assertEqual(user1.id, user0.id)
self.assertEqual(nsa1, nsa0)
self.assertEqual(user1.username, "test")
self.assertEqual(conn.extra_data['ldap']['entrance_year'], '12')
def test_longterm_installer_from_allauth_command_socialaccounts(self):
self._setup_ldap(12)
with self.settings(
SOCIALACCOUNT_ADAPTER='allauth.socialaccount.'
'adapter.DefaultSocialAccountAdapter'):
r = self.client_cas_login(self.client, provider_id="clipper",
username='test')
user0 = r.context["user"]
self.assertEqual(user0.username, "test")
self.client.logout()
user1 = User.objects.create_user('bidule', 'bidule@clipper.ens.fr',
'bidule')
nsa0 = SocialAccount.objects.count()
with captured_stdout() as stdout:
command = InstallLongterm()
command.handle(use_socialaccounts=True)
output = stdout.getvalue()
self.assertIn('test -> test@12', output)
self.assertNotIn('bidule ->', output)
r = self.client_cas_login(self.client, provider_id="clipper",
username='test')
user1 = r.context["user"]
nsa1 = SocialAccount.objects.count()
conn = user1.socialaccount_set.get(provider='clipper')
self.assertEqual(user1.id, user0.id)
self.assertEqual(nsa1, nsa0)
self.assertEqual(nsa1, nsa0)
self.assertEqual(user1.username, "test@12")
self.assertEqual(conn.extra_data['ldap']['entrance_year'], '12')
def test_longterm_installer_from_djangocas(self):
with self.settings(
SOCIALACCOUNT_ADAPTER='allauth.socialaccount.'
'adapter.DefaultSocialAccountAdapter'):
user0 = User.objects.create_user('test', 'test@clipper.ens.fr',
'test')
nsa0 = SocialAccount.objects.count()
self._setup_ldap(12)
outputs = install_longterm_adapter()
self.assertEqual(outputs["created"], [("test", "test@12")])
r = self.client_cas_login(self.client, provider_id="clipper",
username='test')
user1 = r.context["user"]
nsa1 = SocialAccount.objects.count()
conn = user1.socialaccount_set.get(provider='clipper')
self.assertEqual(user1.id, user0.id)
self.assertEqual(nsa1, nsa0 + 1)
self.assertEqual(user1.username, "test@12")
self.assertEqual(conn.extra_data['ldap']['entrance_year'], '12')
def test_disconnect_ldap(self):
nu0 = User.objects.count()
nsa0 = SocialAccount.objects.count()
ldap_patcher.stop()
with self.settings(CLIPPER_LDAP_SERVER=''):
self.assertRaises(ValueError, self.client_cas_login,
self.client, provider_id="clipper",
username="test")
nu1 = User.objects.count()
nsa1 = SocialAccount.objects.count()
self.assertEqual(nu0, nu1)
self.assertEqual(nsa0, nsa1)
ldap_patcher.start()
def test_invalid_uid(self):
self._setup_ldap(12, "test")
uids = [" test", "test ", "\\test", "test)"]
for uid in uids:
with self.assertRaises(ValueError) as cm:
get_ldap_infos(uid)
self.assertIn(uid, str(cm.exception))

View file

@ -1,129 +0,0 @@
# -*- coding: utf-8 -*-
from django.conf import settings
from allauth.account.models import EmailAddress
from allauth.account.utils import user_email
import ldap
DEPARTMENTS_LIST = {
'phy': u'Physique',
'maths': u'Maths',
'bio': u'Biologie',
'chimie': u'Chimie',
'geol': u'Géosciences',
'dec': u'DEC',
'info': u'Informatique',
'litt': u'Littéraire',
'guests': u'Pensionnaires étrangers',
'pei': u'PEI',
}
def init_ldap():
server = getattr(settings, "CLIPPER_LDAP_SERVER",
"ldaps://ldap.spi.ens.fr:636")
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT,
ldap.OPT_X_TLS_NEVER)
ldap_connection = ldap.initialize(server)
ldap_connection.set_option(ldap.OPT_REFERRALS, 0)
ldap_connection.set_option(ldap.OPT_PROTOCOL_VERSION, 3)
ldap_connection.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND)
ldap_connection.set_option(ldap.OPT_X_TLS_DEMAND, True)
ldap_connection.set_option(ldap.OPT_DEBUG_LEVEL, 255)
ldap_connection.set_option(ldap.OPT_NETWORK_TIMEOUT, 10)
ldap_connection.set_option(ldap.OPT_TIMEOUT, 10)
return ldap_connection
def extract_infos_from_ldap(infos):
data = {}
# Name
if 'cn' in infos:
data['name'] = infos['cn'][0].decode("utf-8")
# Parsing homeDirectory to get entrance year and departments
if 'homeDirectory' in infos:
dirs = infos['homeDirectory'][0].decode("utf-8").split('/')
if len(dirs) >= 4 and dirs[1] == 'users':
# Assume template "/users/<year>/<department>/clipper/"
annee = dirs[2]
dep = dirs[3]
dep_fancy = DEPARTMENTS_LIST.get(dep.lower(), '')
data['entrance_year'] = annee
data['department_code'] = dep
data['department'] = dep_fancy
# Mail
pmail = infos.get('mailRoutingAddress', [])
if pmail:
data['email'] = pmail[0].decode("utf-8")
# User id
if 'uid' in infos:
data['clipper_uid'] = infos['uid'][0].decode("utf-8").strip().lower()
return data
def get_ldap_infos(clipper_uid):
if not clipper_uid.isalnum():
raise ValueError(
'Invalid uid "{}": contains non-alphanumeric characters'
.format(clipper_uid)
)
data = {}
try:
ldap_connection = init_ldap()
info = ldap_connection.search_s('dc=spi,dc=ens,dc=fr',
ldap.SCOPE_SUBTREE,
('(uid=%s)' % (clipper_uid,)),
['cn',
'mailRoutingAddress',
'homeDirectory'])
if len(info) > 0:
data = extract_infos_from_ldap(info[0][1])
except ldap.LDAPError:
pass
return data
def get_clipper_email(clipper):
return '{}@clipper.ens.fr'.format(clipper.strip().lower())
def remove_email(user, email):
"""
Removes an email address of a user.
If it is his primary email address, it sets another email address as
primary, preferably verified.
"""
u_mailaddrs = user.emailaddress_set.filter(user=user)
try:
mailaddr = user.emailaddress_set.get(email=email)
except EmailAddress.DoesNotExist:
return
if mailaddr.primary:
others = u_mailaddrs.filter(primary=False)
# Prefer a verified mail.
new_primary = (
others.filter(verified=True).last() or others.last()
)
if new_primary:
# It also updates 'user.EMAIL_FIELD'.
new_primary.set_as_primary()
else:
user_email(user, '')
user.save()
mailaddr.delete()

View file

@ -1,17 +1,11 @@
# -*- coding: utf-8 -*-
from allauth.account.adapter import DefaultAccountAdapter
from allauth_ens.adapter import LongTermClipperAccountAdapter
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
class AccountAdapter(DefaultAccountAdapter):
pass
class SocialAccountAdapter(LongTermClipperAccountAdapter):
def get_username(self, clipper, data):
"""
Exception-free version of get_username, so that it works even outside of
the ENS (if no access to LDAP server)
"""
return "{}@{}".format(clipper, data.get('entrance_year', '00'))
class SocialAccountAdapter(DefaultSocialAccountAdapter):
pass

View file

@ -7,6 +7,7 @@ combine_as_imports = True
default_section = THIRDPARTY
include_trailing_comma = True
known_allauth = allauth
known_future_library = future,six
known_django = django
known_first_party = allauth_ens
multi_line_output = 5

View file

@ -45,10 +45,7 @@ setup(
include_package_data=True,
install_requires=[
'django-allauth',
'django-allauth-cas>=1.0,<1.1',
# The version of CAS used by cas.eleves.ens.fr is unclear…
# Stick to python-cas 1.2.0 until we solve this mystery.
'python-cas==1.2.0',
'django-allauth-cas>=1.0.0b2,<1.1',
'django-widget-tweaks',
'python-ldap',
],

View file

@ -1,5 +1,4 @@
[tox]
# Update .gitlab-ci.yml if you change the Django/Python matrix
envlist =
django{18,19,110}-py{27,34,35},
django111-py{27,34,35,36},
@ -16,8 +15,7 @@ deps =
django111: django>=1.11,<2.0
django20: django>=2.0,<2.1
coverage
fakeldap
mock
mock ; python_version < "3.0"
usedevelop= True
commands =
python -V