Compare commits
64 commits
tobast-cap
...
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 | ||
|
fdafd407d4 | ||
|
9b45fc54c7 | ||
|
2568bf9b6f | ||
|
d204405959 | ||
|
93740066b3 | ||
|
787efe96d0 | ||
|
bfc0bb42ad | ||
|
a1671a3dd7 | ||
|
f0a73f6ef6 | ||
|
b6f5acaa46 | ||
|
9c3fc72ec0 | ||
|
6fc012cb39 | ||
|
1142db73f7 | ||
|
54963e9f91 | ||
|
dc8873cafc | ||
|
5a78561d17 | ||
|
cdb9c3c722 | ||
|
021d50fade |
44 changed files with 6656 additions and 214 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -3,7 +3,7 @@
|
|||
.tox/
|
||||
build/
|
||||
dist/
|
||||
vendor/
|
||||
/vendor/
|
||||
venv/
|
||||
|
||||
*~
|
||||
|
|
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 *
|
||||
|
|
91
README.rst
91
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
|
||||
*****
|
||||
|
@ -90,9 +97,6 @@ login and logout views of other applications. They redirect to their similar
|
|||
``next`` is given along the initial request, user is redirected to this url on
|
||||
successful login and logout.
|
||||
|
||||
If you need to do this for the admin site, you shoud use
|
||||
``capture_login_admin`` instead, performing checks to avoid redirection loops.
|
||||
|
||||
This requires to add urls before the include of the app' urls.
|
||||
|
||||
For example, to replace the Django admin login and logout views with allauth's
|
||||
|
@ -100,13 +104,13 @@ ones:
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
from allauth_ens.views import capture_login_admin, capture_logout
|
||||
from allauth_ens.views import capture_login, capture_logout
|
||||
|
||||
urlpatterns = [
|
||||
# …
|
||||
|
||||
# Add it before include of admin urls.
|
||||
url(r'^admin/login/$', capture_login_admin),
|
||||
url(r'^admin/login/$', capture_login),
|
||||
url(r'^admin/logout/$', capture_logout),
|
||||
|
||||
url(r'^admin/$', include(admin.site.urls)),
|
||||
|
@ -151,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
|
||||
|
@ -204,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):
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ $main-max-width: 500px;
|
|||
|
||||
.content-wrapper {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
flex-flow: row wrap;
|
||||
align-items: center;
|
||||
|
||||
/* Blocks */
|
||||
|
@ -68,13 +68,17 @@ $main-max-width: 500px;
|
|||
}
|
||||
|
||||
@media (min-width: 500px) {
|
||||
& {
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
|
||||
& > section {
|
||||
flex: 1 1 auto;
|
||||
min-width: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 120px) {
|
||||
@media (min-width: 1200px) {
|
||||
& > section {
|
||||
flex: 1 1 auto;
|
||||
min-width: 350px;
|
||||
|
@ -216,7 +220,6 @@ section {
|
|||
.method-list {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
|
||||
& > .method-wrapper {
|
||||
flex: 1 100%;
|
||||
|
|
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";
|
||||
|
|
24
allauth_ens/static/allauth_ens/fonts/index.css
Normal file
24
allauth_ens/static/allauth_ens/fonts/index.css
Normal file
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* 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');
|
||||
}
|
BIN
allauth_ens/static/allauth_ens/fonts/roboto-bold.ttf
Normal file
BIN
allauth_ens/static/allauth_ens/fonts/roboto-bold.ttf
Normal file
Binary file not shown.
BIN
allauth_ens/static/allauth_ens/fonts/roboto-regular.ttf
Normal file
BIN
allauth_ens/static/allauth_ens/fonts/roboto-regular.ttf
Normal file
Binary file not shown.
BIN
allauth_ens/static/allauth_ens/fonts/roboto-slab-regular.ttf
Normal file
BIN
allauth_ens/static/allauth_ens/fonts/roboto-slab-regular.ttf
Normal file
Binary file not shown.
|
@ -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;
|
||||
}
|
||||
|
@ -160,7 +160,7 @@ b {
|
|||
/* line 58, ../../scss/_base.scss */
|
||||
.content-wrapper {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
flex-flow: row wrap;
|
||||
align-items: center;
|
||||
/* Blocks */
|
||||
}
|
||||
|
@ -171,20 +171,24 @@ 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: 120px) {
|
||||
/* line 78, ../../scss/_base.scss */
|
||||
@media (min-width: 1200px) {
|
||||
/* line 82, ../../scss/_base.scss */
|
||||
.content-wrapper > section {
|
||||
flex: 1 1 auto;
|
||||
min-width: 350px;
|
||||
}
|
||||
}
|
||||
|
||||
/* line 86, ../../scss/_base.scss */
|
||||
/* line 90, ../../scss/_base.scss */
|
||||
#providers {
|
||||
width: 150px;
|
||||
min-width: unset;
|
||||
|
@ -194,7 +198,7 @@ b {
|
|||
/**********
|
||||
* Header *
|
||||
**********/
|
||||
/* line 100, ../../scss/_base.scss */
|
||||
/* line 104, ../../scss/_base.scss */
|
||||
header {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
@ -210,7 +214,7 @@ header::after {
|
|||
content: "";
|
||||
clear: both;
|
||||
}
|
||||
/* line 114, ../../scss/_base.scss */
|
||||
/* line 118, ../../scss/_base.scss */
|
||||
header a {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
@ -219,7 +223,7 @@ header a:focus, header a:hover {
|
|||
background: #d43f3a;
|
||||
text-decoration: none;
|
||||
}
|
||||
/* line 123, ../../scss/_base.scss */
|
||||
/* line 127, ../../scss/_base.scss */
|
||||
header .right {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
|
@ -230,25 +234,25 @@ header .right {
|
|||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
/* line 136, ../../scss/_base.scss */
|
||||
/* line 140, ../../scss/_base.scss */
|
||||
header .right > * {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
/* line 139, ../../scss/_base.scss */
|
||||
/* line 143, ../../scss/_base.scss */
|
||||
header .right > * > * {
|
||||
display: block;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
/* line 145, ../../scss/_base.scss */
|
||||
/* line 149, ../../scss/_base.scss */
|
||||
header .right #connect-status {
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
}
|
||||
/* line 149, ../../scss/_base.scss */
|
||||
/* line 153, ../../scss/_base.scss */
|
||||
header .right #connect-status .fa {
|
||||
margin-right: 5px;
|
||||
}
|
||||
/* line 155, ../../scss/_base.scss */
|
||||
/* line 159, ../../scss/_base.scss */
|
||||
header h1 {
|
||||
flex: 1 1 auto;
|
||||
padding: 15px 25px;
|
||||
|
@ -257,11 +261,11 @@ header h1 {
|
|||
/************
|
||||
* Messages *
|
||||
************/
|
||||
/* line 168, ../../scss/_base.scss */
|
||||
/* line 172, ../../scss/_base.scss */
|
||||
.messages-container {
|
||||
padding: 0 15px;
|
||||
}
|
||||
/* line 171, ../../scss/_base.scss */
|
||||
/* line 175, ../../scss/_base.scss */
|
||||
.messages-container::after {
|
||||
display: block;
|
||||
content: "";
|
||||
|
@ -269,30 +273,30 @@ header h1 {
|
|||
height: 2px;
|
||||
}
|
||||
|
||||
/* line 179, ../../scss/_base.scss */
|
||||
/* line 183, ../../scss/_base.scss */
|
||||
.messages-list {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
/* line 189, ../../scss/_base.scss */
|
||||
/* line 193, ../../scss/_base.scss */
|
||||
.message {
|
||||
padding: 5px;
|
||||
font-size: 14px;
|
||||
color: #292b2c;
|
||||
}
|
||||
/* line 184, ../../scss/_base.scss */
|
||||
/* line 188, ../../scss/_base.scss */
|
||||
.message.info {
|
||||
color: #28a1c5;
|
||||
}
|
||||
/* line 184, ../../scss/_base.scss */
|
||||
/* line 188, ../../scss/_base.scss */
|
||||
.message.success {
|
||||
color: #2d672d;
|
||||
}
|
||||
/* line 184, ../../scss/_base.scss */
|
||||
/* line 188, ../../scss/_base.scss */
|
||||
.message.warning {
|
||||
color: #b06d0f;
|
||||
}
|
||||
/* line 184, ../../scss/_base.scss */
|
||||
/* line 188, ../../scss/_base.scss */
|
||||
.message.error {
|
||||
color: #b52b27;
|
||||
}
|
||||
|
@ -300,24 +304,23 @@ header h1 {
|
|||
/***********
|
||||
* Content *
|
||||
***********/
|
||||
/* line 208, ../../scss/_base.scss */
|
||||
/* line 212, ../../scss/_base.scss */
|
||||
section > * + * {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
/* Methods list */
|
||||
/* line 216, ../../scss/_base.scss */
|
||||
/* line 220, ../../scss/_base.scss */
|
||||
.method-list {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
/* line 221, ../../scss/_base.scss */
|
||||
/* line 224, ../../scss/_base.scss */
|
||||
.method-list > .method-wrapper {
|
||||
flex: 1 100%;
|
||||
padding: 2px 0;
|
||||
}
|
||||
/* line 225, ../../scss/_base.scss */
|
||||
/* line 228, ../../scss/_base.scss */
|
||||
.method-list > .method-wrapper a {
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
display: block;
|
||||
|
@ -347,11 +350,11 @@ section > * + * {
|
|||
}
|
||||
|
||||
/* Connected accounts list */
|
||||
/* line 249, ../../scss/_base.scss */
|
||||
/* line 252, ../../scss/_base.scss */
|
||||
.connections-providers-list > * + * {
|
||||
margin-top: 2px;
|
||||
}
|
||||
/* line 253, ../../scss/_base.scss */
|
||||
/* line 256, ../../scss/_base.scss */
|
||||
.connections-providers-list > * > .heading {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
|
@ -364,7 +367,7 @@ section > * + * {
|
|||
content: "";
|
||||
clear: both;
|
||||
}
|
||||
/* line 262, ../../scss/_base.scss */
|
||||
/* line 265, ../../scss/_base.scss */
|
||||
.connections-providers-list > * > .heading .connect {
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
display: block;
|
||||
|
@ -395,29 +398,29 @@ section > * + * {
|
|||
color: #fff;
|
||||
}
|
||||
|
||||
/* line 274, ../../scss/_base.scss */
|
||||
/* line 277, ../../scss/_base.scss */
|
||||
.connections-list {
|
||||
border-left: 5px solid #f7f7f9;
|
||||
}
|
||||
/* line 277, ../../scss/_base.scss */
|
||||
/* line 280, ../../scss/_base.scss */
|
||||
.connections-list > * {
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
/* line 281, ../../scss/_base.scss */
|
||||
/* line 284, ../../scss/_base.scss */
|
||||
.connections-list > * + * {
|
||||
border-top: 1px dotted #eceeef;
|
||||
}
|
||||
/* line 285, ../../scss/_base.scss */
|
||||
/* line 288, ../../scss/_base.scss */
|
||||
.connections-list > * > .fa {
|
||||
margin-right: 5px;
|
||||
}
|
||||
/* line 289, ../../scss/_base.scss */
|
||||
/* line 292, ../../scss/_base.scss */
|
||||
.connections-list > * .delete {
|
||||
float: right;
|
||||
margin-top: -2px;
|
||||
}
|
||||
/* line 293, ../../scss/_base.scss */
|
||||
/* line 296, ../../scss/_base.scss */
|
||||
.connections-list > * .delete [type=submit] {
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
display: block;
|
||||
|
@ -446,17 +449,17 @@ section > * + * {
|
|||
background: #b52b27;
|
||||
color: #fff;
|
||||
}
|
||||
/* line 302, ../../scss/_base.scss */
|
||||
/* line 305, ../../scss/_base.scss */
|
||||
.connections-list form {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* E-mail adresses list */
|
||||
/* line 311, ../../scss/_base.scss */
|
||||
/* line 314, ../../scss/_base.scss */
|
||||
.emailaddress-list .emailaddress {
|
||||
border-bottom: 1px dotted #464a4c;
|
||||
}
|
||||
/* line 315, ../../scss/_base.scss */
|
||||
/* line 318, ../../scss/_base.scss */
|
||||
.emailaddress-list .emailaddress .summary {
|
||||
height: 45px;
|
||||
}
|
||||
|
@ -466,13 +469,13 @@ section > * + * {
|
|||
content: "";
|
||||
clear: both;
|
||||
}
|
||||
/* line 320, ../../scss/_base.scss */
|
||||
/* line 323, ../../scss/_base.scss */
|
||||
.emailaddress-list .emailaddress .summary > * {
|
||||
float: left;
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
}
|
||||
/* line 326, ../../scss/_base.scss */
|
||||
/* line 329, ../../scss/_base.scss */
|
||||
.emailaddress-list .emailaddress .summary > .primary, .emailaddress-list .emailaddress .summary > .verified-status {
|
||||
float: right;
|
||||
width: 45px;
|
||||
|
@ -480,25 +483,25 @@ section > * + * {
|
|||
text-align: center;
|
||||
font-size: 20px;
|
||||
}
|
||||
/* line 334, ../../scss/_base.scss */
|
||||
/* line 337, ../../scss/_base.scss */
|
||||
.emailaddress-list .emailaddress .summary > .email {
|
||||
padding: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
/* line 339, ../../scss/_base.scss */
|
||||
/* line 342, ../../scss/_base.scss */
|
||||
.emailaddress-list .emailaddress .summary > .primary {
|
||||
color: #025aa5;
|
||||
}
|
||||
/* line 343, ../../scss/_base.scss */
|
||||
/* line 346, ../../scss/_base.scss */
|
||||
.emailaddress-list .emailaddress .summary > .verified {
|
||||
color: #449d44;
|
||||
}
|
||||
/* line 347, ../../scss/_base.scss */
|
||||
/* line 350, ../../scss/_base.scss */
|
||||
.emailaddress-list .emailaddress .summary > .unverified {
|
||||
color: #ec971f;
|
||||
}
|
||||
|
||||
/* line 356, ../../scss/_base.scss */
|
||||
/* line 359, ../../scss/_base.scss */
|
||||
.actions {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
@ -508,7 +511,7 @@ section > * + * {
|
|||
content: "";
|
||||
clear: both;
|
||||
}
|
||||
/* line 361, ../../scss/_base.scss */
|
||||
/* line 364, ../../scss/_base.scss */
|
||||
.actions > * {
|
||||
float: right;
|
||||
margin-right: 10px;
|
||||
|
@ -518,7 +521,7 @@ section > * + * {
|
|||
/*********
|
||||
* Forms *
|
||||
*********/
|
||||
/* line 393, ../../scss/_base.scss */
|
||||
/* line 396, ../../scss/_base.scss */
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
display: block;
|
||||
|
@ -531,13 +534,13 @@ section > * + * {
|
|||
content: "";
|
||||
clear: both;
|
||||
}
|
||||
/* line 401, ../../scss/_base.scss */
|
||||
/* line 404, ../../scss/_base.scss */
|
||||
.input-wrapper label {
|
||||
margin-right: 10px;
|
||||
color: rgba(0, 0, 0, 0.38);
|
||||
font-size: 16px;
|
||||
}
|
||||
/* line 408, ../../scss/_base.scss */
|
||||
/* line 411, ../../scss/_base.scss */
|
||||
.input-wrapper .transform-label label {
|
||||
padding-left: 0;
|
||||
width: 100%;
|
||||
|
@ -558,7 +561,7 @@ section > * + * {
|
|||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* line 430, ../../scss/_base.scss */
|
||||
/* line 433, ../../scss/_base.scss */
|
||||
.input-wrapper input.field:not([type=checkbox]) {
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
|
@ -568,80 +571,80 @@ section > * + * {
|
|||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
font-size: 16px;
|
||||
}
|
||||
/* line 444, ../../scss/_base.scss */
|
||||
/* line 447, ../../scss/_base.scss */
|
||||
.input-wrapper input[type="checkbox"] {
|
||||
vertical-align: sub;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
/* line 450, ../../scss/_base.scss */
|
||||
/* line 453, ../../scss/_base.scss */
|
||||
.input-wrapper select {
|
||||
height: 30px;
|
||||
background: #fff;
|
||||
}
|
||||
/* line 468, ../../scss/_base.scss */
|
||||
/* line 471, ../../scss/_base.scss */
|
||||
.input-wrapper.input-focused label, .input-wrapper.input-focused .messages {
|
||||
color: inherit;
|
||||
}
|
||||
/* line 474, ../../scss/_base.scss */
|
||||
/* line 477, ../../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 456, ../../scss/_base.scss */
|
||||
/* line 459, ../../scss/_base.scss */
|
||||
.input-wrapper.input-has-value {
|
||||
color: #449d44;
|
||||
}
|
||||
/* line 460, ../../scss/_base.scss */
|
||||
/* line 463, ../../scss/_base.scss */
|
||||
.input-wrapper.input-has-value input.field {
|
||||
padding-bottom: 0px;
|
||||
border-width: 2px;
|
||||
border-color: #449d44;
|
||||
}
|
||||
/* line 456, ../../scss/_base.scss */
|
||||
/* line 459, ../../scss/_base.scss */
|
||||
.input-wrapper.input-error {
|
||||
color: #d9534f;
|
||||
}
|
||||
/* line 460, ../../scss/_base.scss */
|
||||
/* line 463, ../../scss/_base.scss */
|
||||
.input-wrapper.input-error input.field {
|
||||
padding-bottom: 0px;
|
||||
border-width: 2px;
|
||||
border-color: #d9534f;
|
||||
}
|
||||
/* line 456, ../../scss/_base.scss */
|
||||
/* line 459, ../../scss/_base.scss */
|
||||
.input-wrapper.input-focused {
|
||||
color: #025aa5;
|
||||
}
|
||||
/* line 460, ../../scss/_base.scss */
|
||||
/* line 463, ../../scss/_base.scss */
|
||||
.input-wrapper.input-focused input.field {
|
||||
padding-bottom: 0px;
|
||||
border-width: 2px;
|
||||
border-color: #025aa5;
|
||||
}
|
||||
/* line 483, ../../scss/_base.scss */
|
||||
/* line 486, ../../scss/_base.scss */
|
||||
.input-wrapper .messages-spacer {
|
||||
float: right;
|
||||
min-height: 10px;
|
||||
min-width: 1px;
|
||||
}
|
||||
/* line 489, ../../scss/_base.scss */
|
||||
/* line 492, ../../scss/_base.scss */
|
||||
.input-wrapper .messages {
|
||||
color: rgba(0, 0, 0, 0.38);
|
||||
}
|
||||
/* line 492, ../../scss/_base.scss */
|
||||
/* line 495, ../../scss/_base.scss */
|
||||
.input-wrapper .messages > * {
|
||||
padding-top: 5px;
|
||||
font-size: 12px;
|
||||
}
|
||||
/* line 499, ../../scss/_base.scss */
|
||||
/* line 502, ../../scss/_base.scss */
|
||||
.input-wrapper.input-checkbox > :first-child, .input-wrapper.input-radio > :first-child {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* line 505, ../../scss/_base.scss */
|
||||
/* line 508, ../../scss/_base.scss */
|
||||
.buttons-choices {
|
||||
display: inline-flex;
|
||||
}
|
||||
/* line 508, ../../scss/_base.scss */
|
||||
/* line 511, ../../scss/_base.scss */
|
||||
.buttons-choices > button {
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
flex: 0 1 auto;
|
||||
|
@ -650,28 +653,28 @@ section > * + * {
|
|||
background: #daeeff;
|
||||
color: #636c72;
|
||||
}
|
||||
/* line 517, ../../scss/_base.scss */
|
||||
/* line 520, ../../scss/_base.scss */
|
||||
.buttons-choices > button:first-child {
|
||||
border-top-left-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
/* line 522, ../../scss/_base.scss */
|
||||
/* line 525, ../../scss/_base.scss */
|
||||
.buttons-choices > button:last-child {
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
/* line 527, ../../scss/_base.scss */
|
||||
/* line 530, ../../scss/_base.scss */
|
||||
.buttons-choices > button:focus {
|
||||
background: #43a7fd;
|
||||
color: white;
|
||||
}
|
||||
/* line 532, ../../scss/_base.scss */
|
||||
/* line 535, ../../scss/_base.scss */
|
||||
.buttons-choices > button.selected {
|
||||
background: #025aa5;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* line 538, ../../scss/_base.scss */
|
||||
/* line 541, ../../scss/_base.scss */
|
||||
[type=submit]:not(.link) {
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
display: block;
|
||||
|
@ -698,7 +701,7 @@ section > * + * {
|
|||
color: #fff;
|
||||
}
|
||||
|
||||
/* line 544, ../../scss/_base.scss */
|
||||
/* line 547, ../../scss/_base.scss */
|
||||
[type=submit].link {
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
color: #025aa5;
|
||||
|
@ -715,23 +718,23 @@ section > * + * {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* line 553, ../../scss/_base.scss */
|
||||
/* line 556, ../../scss/_base.scss */
|
||||
.form-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
/* line 557, ../../scss/_base.scss */
|
||||
/* line 560, ../../scss/_base.scss */
|
||||
.form-inline > .input-wrapper {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
/* line 561, ../../scss/_base.scss */
|
||||
/* line 564, ../../scss/_base.scss */
|
||||
.form-inline [type=submit] {
|
||||
margin-top: -5px;
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* line 568, ../../scss/_base.scss */
|
||||
/* line 571, ../../scss/_base.scss */
|
||||
.btn {
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
display: block;
|
||||
|
@ -751,7 +754,7 @@ section > * + * {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* line 573, ../../scss/_base.scss */
|
||||
/* line 576, ../../scss/_base.scss */
|
||||
.btn-primary {
|
||||
background: #025aa5;
|
||||
color: #fff;
|
||||
|
@ -762,7 +765,7 @@ section > * + * {
|
|||
color: #fff;
|
||||
}
|
||||
|
||||
/* line 574, ../../scss/_base.scss */
|
||||
/* line 577, ../../scss/_base.scss */
|
||||
.btn-success {
|
||||
background: #449d44;
|
||||
color: #fff;
|
||||
|
@ -772,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;
|
||||
}
|
||||
|
|
2337
allauth_ens/static/vendor/font-awesome/4.7.0/css/font-awesome.css
vendored
Normal file
2337
allauth_ens/static/vendor/font-awesome/4.7.0/css/font-awesome.css
vendored
Normal file
File diff suppressed because it is too large
Load diff
4
allauth_ens/static/vendor/font-awesome/4.7.0/css/font-awesome.min.css
vendored
Normal file
4
allauth_ens/static/vendor/font-awesome/4.7.0/css/font-awesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
allauth_ens/static/vendor/font-awesome/4.7.0/fonts/fontawesome-webfont.eot
vendored
Normal file
BIN
allauth_ens/static/vendor/font-awesome/4.7.0/fonts/fontawesome-webfont.eot
vendored
Normal file
Binary file not shown.
2671
allauth_ens/static/vendor/font-awesome/4.7.0/fonts/fontawesome-webfont.svg
vendored
Normal file
2671
allauth_ens/static/vendor/font-awesome/4.7.0/fonts/fontawesome-webfont.svg
vendored
Normal file
File diff suppressed because it is too large
Load diff
After Width: | Height: | Size: 434 KiB |
BIN
allauth_ens/static/vendor/font-awesome/4.7.0/fonts/fontawesome-webfont.ttf
vendored
Normal file
BIN
allauth_ens/static/vendor/font-awesome/4.7.0/fonts/fontawesome-webfont.ttf
vendored
Normal file
Binary file not shown.
BIN
allauth_ens/static/vendor/font-awesome/4.7.0/fonts/fontawesome-webfont.woff
vendored
Normal file
BIN
allauth_ens/static/vendor/font-awesome/4.7.0/fonts/fontawesome-webfont.woff
vendored
Normal file
Binary file not shown.
BIN
allauth_ens/static/vendor/font-awesome/4.7.0/fonts/fontawesome-webfont.woff2
vendored
Normal file
BIN
allauth_ens/static/vendor/font-awesome/4.7.0/fonts/fontawesome-webfont.woff2
vendored
Normal file
Binary file not shown.
4
allauth_ens/static/vendor/jquery/3.2.1/jquery-3.2.1.slim.min.js
vendored
Normal file
4
allauth_ens/static/vendor/jquery/3.2.1/jquery-3.2.1.slim.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -30,18 +30,32 @@
|
|||
{% 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 %}
|
||||
|
@ -67,7 +81,6 @@
|
|||
{% endif %}
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
|
|
|
@ -16,19 +16,15 @@
|
|||
|
||||
{# CSS #}
|
||||
<link rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css?family=Roboto:400,700|Roboto+Slab:400">
|
||||
href="{% static "vendor/font-awesome/4.7.0/css/font-awesome.min.css" %}">
|
||||
<link rel="stylesheet"
|
||||
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"
|
||||
integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN"
|
||||
crossorigin="anonymous">
|
||||
href="{% static "allauth_ens/fonts/index.css" %}">
|
||||
<link rel="stylesheet"
|
||||
href="{% static "allauth_ens/screen.css" %}">
|
||||
|
||||
{# JS #}
|
||||
<script type="text/javascript"
|
||||
src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
|
||||
integrity="sha256-k2WSCIexGzOj3Euiig+TlR8gA0EmPjuc79OEeY5L45g="
|
||||
crossorigin="anonymous"></script>
|
||||
src="{% static "vendor/jquery/3.2.1/jquery-3.2.1.slim.min.js" %}"></script>
|
||||
<script type="text/javascript"
|
||||
src="{% static "allauth_ens/authens.js" %}"></script>
|
||||
|
||||
|
@ -83,7 +79,7 @@
|
|||
|
||||
{% block messages-extra %}{% endblock %}
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div class="content-wrapper {% block content-extra-classes %}{% endblock %}">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
{% load i18n static %}
|
||||
{% load account allauth_ens %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Error{% if request.site.name %} · {{ request.site.name }}{% endif %}</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
background-color: #ffffd8;
|
||||
}
|
||||
#messagebox {
|
||||
max-width: 500px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 50px;
|
||||
background-color: white;
|
||||
border: 2px solid black;
|
||||
border-radius: 15px;
|
||||
padding: 25px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="messagebox">
|
||||
{{ message }}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -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,8 +1,5 @@
|
|||
import django
|
||||
from django.views.generic import RedirectView
|
||||
from django.contrib import admin
|
||||
from django.shortcuts import render
|
||||
|
||||
|
||||
if django.VERSION >= (1, 10):
|
||||
from django.urls import reverse_lazy
|
||||
|
@ -26,20 +23,3 @@ class CaptureLogout(RedirectView):
|
|||
|
||||
|
||||
capture_logout = CaptureLogout.as_view()
|
||||
|
||||
|
||||
def capture_login_admin(request):
|
||||
""" Redirect the user to allauth login page if they are not logged in, or
|
||||
fails and display a message if they are logged in *but* are not
|
||||
administrators """
|
||||
|
||||
if admin.site.has_permission(request):
|
||||
return capture_login(request)
|
||||
|
||||
context = {
|
||||
'message': ("The account you're authenticated with is not an "
|
||||
"administrator account."),
|
||||
}
|
||||
return render(request,
|
||||
"allauth_ens/simple_message.html",
|
||||
context=context)
|
||||
|
|
|
@ -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