diff --git a/README.rst b/README.rst index 33ec1c7..c169e73 100644 --- a/README.rst +++ b/README.rst @@ -216,11 +216,23 @@ Customize behaviour, and for instance attribute a default entrance year. Initial migration - 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``. - + 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 ``: 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 ********* diff --git a/allauth_ens/adapter.py b/allauth_ens/adapter.py index 662c98a..3c02f2a 100644 --- a/allauth_ens/adapter.py +++ b/allauth_ens/adapter.py @@ -147,14 +147,20 @@ def deprecate_clippers(): clippers.update(provider='clipper_inactive') -def install_longterm_adapter(fake=False): +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. """ - accounts = {u.username: u for u in User.objects.all() - if u.username.isalnum()} + 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() @@ -178,7 +184,10 @@ def install_longterm_adapter(fake=False): user = accounts.get(clipper_uid, None) if user is None: continue - user.username = ltc_adapter.get_username(clipper_uid, data) + + if not keep_usernames: + user.username = ltc_adapter.get_username(clipper_uid, data) + if fake: cases.append(clipper_uid) else: diff --git a/allauth_ens/management/commands/install_longterm.py b/allauth_ens/management/commands/install_longterm.py index 40ccde3..a839555 100644 --- a/allauth_ens/management/commands/install_longterm.py +++ b/allauth_ens/management/commands/install_longterm.py @@ -1,6 +1,9 @@ # 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 @@ -17,10 +20,52 @@ class Command(BaseCommand): 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): - logs = install_longterm_adapter(options.get("fake", False)) + 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"])) diff --git a/allauth_ens/tests.py b/allauth_ens/tests.py index a35ca3b..61bb3a4 100644 --- a/allauth_ens/tests.py +++ b/allauth_ens/tests.py @@ -7,6 +7,7 @@ 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 @@ -18,6 +19,7 @@ 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', @@ -331,6 +333,98 @@ class LongTermClipperTests(CASTestCase): 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.' diff --git a/allauth_ens/utils.py b/allauth_ens/utils.py index 02d4c10..7da3c6c 100644 --- a/allauth_ens/utils.py +++ b/allauth_ens/utils.py @@ -116,8 +116,7 @@ def remove_email(user, email): # Prefer a verified mail. new_primary = ( - others.filter(verified=True).last() or - others.last() + others.filter(verified=True).last() or others.last() ) if new_primary: