Compare commits
59 commits
Qwann/arch
...
master
Author | SHA1 | Date | |
---|---|---|---|
122d99e515 | |||
|
ad4ebfe7ef | ||
|
ab40368c65 | ||
|
4c8c66b005 | ||
|
239247efc9 | ||
|
572d58722d | ||
|
a216a0cad1 | ||
|
884020cc20 | ||
|
a30d3866c5 | ||
|
1a91ca8090 | ||
|
126f367e76 | ||
|
2c57fb7a4d | ||
|
0272edd77d | ||
|
14d54f528b | ||
|
85f09ca9f7 | ||
|
bc772851d1 | ||
|
a27921016b | ||
|
fc16b3fedf | ||
|
5d978e89ea | ||
|
d22d1ea567 | ||
|
9d3e7a805a | ||
|
b0451097fc | ||
|
e35169d544 | ||
|
e936fb70de | ||
|
509a1bbd96 | ||
|
4a119c7d13 | ||
|
52a5e21ade | ||
|
845b09cc2b | ||
|
c0059bea80 | ||
|
28a8127d35 | ||
|
e193239231 | ||
|
b09bada052 | ||
|
bc2b606288 | ||
|
a812a43a2c | ||
|
6a79b02fa8 | ||
|
b189c11454 | ||
|
232236b50b | ||
|
6e77b31e0d | ||
|
534e0b188f | ||
|
7a0ec189e2 | ||
|
cca8da5772 | ||
|
35a3bc3e2d | ||
|
08a47150db | ||
|
4cf633ed81 | ||
|
5ee1c774ac | ||
|
17fef409a8 | ||
|
787efe96d0 | ||
|
bfc0bb42ad | ||
|
a1671a3dd7 | ||
|
f0a73f6ef6 | ||
|
b6f5acaa46 | ||
|
9c3fc72ec0 | ||
|
6fc012cb39 | ||
|
1142db73f7 | ||
|
54963e9f91 | ||
|
dc8873cafc | ||
|
5a78561d17 | ||
|
cdb9c3c722 | ||
|
021d50fade |
28 changed files with 1522 additions and 68 deletions
67
.gitlab-ci.yml
Normal file
67
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,67 @@
|
|||
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
|
|
@ -1,5 +1,33 @@
|
|||
******************
|
||||
1.0.0 (unreleased)
|
||||
******************
|
||||
*****
|
||||
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
|
||||
*******
|
||||
|
||||
- First official release.
|
||||
|
|
|
@ -2,3 +2,4 @@ include LICENSE README.rst
|
|||
|
||||
recursive-include allauth_ens/static *
|
||||
recursive-include allauth_ens/templates *
|
||||
recursive-include allauth_ens/locale *
|
||||
|
|
82
README.rst
82
README.rst
|
@ -77,6 +77,13 @@ 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
|
||||
*****
|
||||
|
@ -148,22 +155,83 @@ 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
|
||||
Poulated data
|
||||
Populated 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
|
||||
|
@ -201,7 +269,11 @@ Tests
|
|||
Local environment
|
||||
-----------------
|
||||
|
||||
``$ ./runtests.py``
|
||||
Requirements
|
||||
* fakeldap and mock, install with ``$ pip install mock fakeldap``
|
||||
|
||||
Run
|
||||
* ``$ ./runtests.py``
|
||||
|
||||
All
|
||||
---
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
__version__ = '1.0.0b2'
|
||||
__version__ = "1.1.3"
|
||||
|
||||
default_app_config = 'allauth_ens.apps.ENSAllauthConfig'
|
||||
default_app_config = "allauth_ens.apps.ENSAllauthConfig"
|
||||
|
|
215
allauth_ens/adapter.py
Normal file
215
allauth_ens/adapter.py
Normal file
|
@ -0,0 +1,215 @@
|
|||
# -*- 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
|
|
@ -1,5 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class ENSAllauthConfig(AppConfig):
|
||||
|
|
BIN
allauth_ens/locale/fr/LC_MESSAGES/django.mo
Normal file
BIN
allauth_ens/locale/fr/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
415
allauth_ens/locale/fr/LC_MESSAGES/django.po
Normal file
415
allauth_ens/locale/fr/LC_MESSAGES/django.po
Normal file
|
@ -0,0 +1,415 @@
|
|||
# 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"
|
||||
" "
|
0
allauth_ens/management/__init__.py
Normal file
0
allauth_ens/management/__init__.py
Normal file
0
allauth_ens/management/commands/__init__.py
Normal file
0
allauth_ens/management/commands/__init__.py
Normal file
16
allauth_ens/management/commands/deprecate_clippers.py
Normal file
16
allauth_ens/management/commands/deprecate_clippers.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
# 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'))
|
80
allauth_ens/management/commands/install_longterm.py
Normal file
80
allauth_ens/management/commands/install_longterm.py
Normal file
|
@ -0,0 +1,80 @@
|
|||
# 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"))
|
|
@ -1,11 +1,8 @@
|
|||
# -*- 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
|
||||
from allauth_cas.providers import CASProvider
|
||||
|
||||
|
||||
class ClipperAccount(ProviderAccount):
|
||||
|
@ -21,41 +18,14 @@ class ClipperProvider(CASProvider):
|
|||
uid, extra = data
|
||||
return '{}@clipper.ens.fr'.format(uid.strip().lower())
|
||||
|
||||
def extract_uid(self, data):
|
||||
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):
|
||||
|
@ -67,8 +37,23 @@ 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):
|
||||
|
|
|
@ -17,6 +17,18 @@ 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):
|
||||
|
||||
|
|
35
allauth_ens/scss/_highlight_clipper.scss
Normal file
35
allauth_ens/scss/_highlight_clipper.scss
Normal file
|
@ -0,0 +1,35 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,3 +5,5 @@
|
|||
@import "mixins";
|
||||
|
||||
@import "base";
|
||||
|
||||
@import "highlight_clipper";
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* line 5, ../../../vendor/bundle/ruby/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 5, ../../../../../../../../var/lib/gems/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, ../../../vendor/bundle/ruby/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 22, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
html {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* line 24, ../../../vendor/bundle/ruby/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 24, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* line 26, ../../../vendor/bundle/ruby/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 26, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
/* line 28, ../../../vendor/bundle/ruby/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 28, ../../../../../../../../var/lib/gems/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, ../../../vendor/bundle/ruby/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 30, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
q, blockquote {
|
||||
quotes: none;
|
||||
}
|
||||
/* line 103, ../../../vendor/bundle/ruby/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 103, ../../../../../../../../var/lib/gems/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, ../../../vendor/bundle/ruby/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 32, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
a img {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* line 116, ../../../vendor/bundle/ruby/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 116, ../../../../../../../../var/lib/gems/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;
|
||||
}
|
||||
|
@ -775,3 +775,35 @@ 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;
|
||||
}
|
||||
|
|
|
@ -30,10 +30,23 @@
|
|||
{% 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">
|
||||
|
@ -68,7 +81,6 @@
|
|||
{% endif %}
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
|
|
|
@ -79,7 +79,7 @@
|
|||
|
||||
{% block messages-extra %}{% endblock %}
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div class="content-wrapper {% block content-extra-classes %}{% endblock %}">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
{% for provider in socialaccount_providers %}
|
||||
{% if provider.id == "openid" %}
|
||||
{% for brand in provider.get_brands %}
|
||||
<li class="method-wrapper">
|
||||
<li class="method-wrapper provider-openid-{{ brand.id }}">
|
||||
<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">
|
||||
<li class="method-wrapper provider-{{ provider.id }}">
|
||||
<a title="{{ provider.name }}"
|
||||
href="{% provider_login_url provider.id process=process scope=scope auth_params=auth_parms %}"
|
||||
href="{% provider_login_url provider.id process=process scope=scope auth_params=auth_params %}"
|
||||
>
|
||||
{{ provider.name }}
|
||||
</a>
|
||||
|
|
|
@ -33,3 +33,10 @@ 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)
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
import django
|
||||
|
@ -5,6 +7,23 @@ 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
|
||||
|
@ -135,3 +154,320 @@ 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))
|
||||
|
|
129
allauth_ens/utils.py
Normal file
129
allauth_ens/utils.py
Normal file
|
@ -0,0 +1,129 @@
|
|||
# -*- 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()
|
|
@ -1,11 +1,17 @@
|
|||
# -*- 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(DefaultSocialAccountAdapter):
|
||||
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'))
|
||||
|
|
|
@ -7,7 +7,6 @@ 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
|
||||
|
|
5
setup.py
5
setup.py
|
@ -45,7 +45,10 @@ setup(
|
|||
include_package_data=True,
|
||||
install_requires=[
|
||||
'django-allauth',
|
||||
'django-allauth-cas>=1.0.0b2,<1.1',
|
||||
'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-widget-tweaks',
|
||||
'python-ldap',
|
||||
],
|
||||
|
|
4
tox.ini
4
tox.ini
|
@ -1,4 +1,5 @@
|
|||
[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},
|
||||
|
@ -15,7 +16,8 @@ deps =
|
|||
django111: django>=1.11,<2.0
|
||||
django20: django>=2.0,<2.1
|
||||
coverage
|
||||
mock ; python_version < "3.0"
|
||||
fakeldap
|
||||
mock
|
||||
usedevelop= True
|
||||
commands =
|
||||
python -V
|
||||
|
|
Loading…
Reference in a new issue