From 1d19d1797c0e3949890c317964eae367e3bb4dc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Thu, 21 Sep 2017 23:39:27 +0200 Subject: [PATCH 1/9] Clean setup/retrieve of kfet generic account --- kfet/auth/__init__.py | 3 ++ kfet/auth/apps.py | 5 +++ kfet/auth/backends.py | 20 ++++-------- kfet/auth/migrations/0001_initial.py | 3 ++ kfet/auth/tests.py | 16 +++++++++ kfet/auth/utils.py | 28 ++++++++++++++++ kfet/migrations/0059_create_generic.py | 45 ++++++++++++++++++++++++++ kfet/models.py | 7 ++++ 8 files changed, 113 insertions(+), 14 deletions(-) create mode 100644 kfet/auth/utils.py create mode 100644 kfet/migrations/0059_create_generic.py diff --git a/kfet/auth/__init__.py b/kfet/auth/__init__.py index 63392684..00926030 100644 --- a/kfet/auth/__init__.py +++ b/kfet/auth/__init__.py @@ -1 +1,4 @@ default_app_config = 'kfet.auth.apps.KFetAuthConfig' + +KFET_GENERIC_USERNAME = 'kfet_genericteam' +KFET_GENERIC_TRIGRAMME = 'GNR' diff --git a/kfet/auth/apps.py b/kfet/auth/apps.py index ab791d18..03742843 100644 --- a/kfet/auth/apps.py +++ b/kfet/auth/apps.py @@ -1,4 +1,5 @@ from django.apps import AppConfig +from django.db.models.signals import post_migrate from django.utils.translation import ugettext_lazy as _ @@ -6,3 +7,7 @@ class KFetAuthConfig(AppConfig): name = 'kfet.auth' label = 'kfetauth' verbose_name = _("K-Fêt - Authentification et Autorisation") + + def ready(self): + from .utils import setup_kfet_generic_user + post_migrate.connect(setup_kfet_generic_user, sender=self) diff --git a/kfet/auth/backends.py b/kfet/auth/backends.py index fb9538d0..c972fb55 100644 --- a/kfet/auth/backends.py +++ b/kfet/auth/backends.py @@ -2,10 +2,13 @@ import hashlib -from django.contrib.auth.models import User, Permission -from gestioncof.models import CofProfile +from django.contrib.auth import get_user_model from kfet.models import Account, GenericTeamToken +from .utils import get_kfet_generic_user + +User = get_user_model() + class KFetBackend(object): def authenticate(self, request): @@ -29,18 +32,7 @@ class GenericTeamBackend(object): def authenticate(self, username=None, token=None): valid_token = GenericTeamToken.objects.get(token=token) if username == 'kfet_genericteam' and valid_token: - # Création du user s'il n'existe pas déjà - user, _ = User.objects.get_or_create(username='kfet_genericteam') - profile, _ = CofProfile.objects.get_or_create(user=user) - account, _ = Account.objects.get_or_create( - cofprofile=profile, - trigramme='GNR') - - # Ajoute la permission kfet.is_team à ce user - perm_is_team = Permission.objects.get(codename='is_team') - user.user_permissions.add(perm_is_team) - - return user + return get_kfet_generic_user() return None def get_user(self, user_id): diff --git a/kfet/auth/migrations/0001_initial.py b/kfet/auth/migrations/0001_initial.py index 30dfca70..061570a8 100644 --- a/kfet/auth/migrations/0001_initial.py +++ b/kfet/auth/migrations/0001_initial.py @@ -8,6 +8,9 @@ class Migration(migrations.Migration): dependencies = [ ('auth', '0006_require_contenttypes_0002'), + # Following dependency allows using Account model to set up the kfet + # generic user in post_migrate receiver. + ('kfet', '0058_delete_genericteamtoken'), ] operations = [ diff --git a/kfet/auth/tests.py b/kfet/auth/tests.py index 7f129a3f..47cc2376 100644 --- a/kfet/auth/tests.py +++ b/kfet/auth/tests.py @@ -4,6 +4,10 @@ from django.test import TestCase from django.contrib.auth.models import User, Group from kfet.forms import UserGroupForm +from kfet.models import Account + +from . import KFET_GENERIC_TRIGRAMME, KFET_GENERIC_USERNAME +from .utils import get_kfet_generic_user class UserGroupFormTests(TestCase): @@ -54,3 +58,15 @@ class UserGroupFormTests(TestCase): [repr(g) for g in [self.other_group] + self.kfet_groups], ordered=False, ) + + +class KFetGenericUserTests(TestCase): + + def test_exists(self): + """ + The account is set up when app is ready, so it should exist. + """ + generic = Account.objects.get_generic() + self.assertEqual(generic.trigramme, KFET_GENERIC_TRIGRAMME) + self.assertEqual(generic.user.username, KFET_GENERIC_USERNAME) + self.assertEqual(get_kfet_generic_user(), generic.user) diff --git a/kfet/auth/utils.py b/kfet/auth/utils.py new file mode 100644 index 00000000..78f31028 --- /dev/null +++ b/kfet/auth/utils.py @@ -0,0 +1,28 @@ +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Permission + +from kfet.models import Account + +User = get_user_model() + + +def get_kfet_generic_user(): + """ + Return the user related to the kfet generic account. + """ + return Account.objects.get_generic().user + + +def setup_kfet_generic_user(**kwargs): + """ + First steps of setup of the kfet generic user are done in a migration, as + it is more robust against database schema changes. + Following steps cannot be done from migration. + """ + generic = get_kfet_generic_user() + generic.user_permissions.add( + Permission.objects.get( + content_type__app_label='kfet', + codename='is_team', + ) + ) diff --git a/kfet/migrations/0059_create_generic.py b/kfet/migrations/0059_create_generic.py new file mode 100644 index 00000000..4f04770c --- /dev/null +++ b/kfet/migrations/0059_create_generic.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + +from kfet.auth import KFET_GENERIC_TRIGRAMME, KFET_GENERIC_USERNAME + + +def setup_kfet_generic_user(apps, schema_editor): + """ + Setup models instances for the kfet generic account. + + Username and trigramme are retrieved from kfet.auth.__init__ module. + Other data are registered here. + + See also setup_kfet_generic_user from kfet.auth.utils module. + """ + User = apps.get_model('auth', 'User') + CofProfile = apps.get_model('gestioncof', 'CofProfile') + Account = apps.get_model('kfet', 'Account') + + user, _ = User.objects.update_or_create( + username=KFET_GENERIC_USERNAME, + defaults={ + 'first_name': 'Compte générique K-Fêt', + }, + ) + profile, _ = CofProfile.objects.update_or_create(user=user) + account, _ = Account.objects.update_or_create( + cofprofile=profile, + defaults={ + 'trigramme': KFET_GENERIC_TRIGRAMME, + }, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('kfet', '0058_delete_genericteamtoken'), + ] + + operations = [ + migrations.RunPython(setup_kfet_generic_user), + ] diff --git a/kfet/models.py b/kfet/models.py index b06114d7..9aefb782 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -14,6 +14,7 @@ from datetime import date import re import hashlib +from .auth import KFET_GENERIC_TRIGRAMME from .auth.models import GenericTeamToken # noqa from .config import kfet_config @@ -35,6 +36,12 @@ class AccountManager(models.Manager): return super().get_queryset().select_related('cofprofile__user', 'negative') + def get_generic(self): + """ + Get the kfet generic account instance. + """ + return self.get(trigramme=KFET_GENERIC_TRIGRAMME) + class Account(models.Model): objects = AccountManager() From e5d19811e859ba366fe595bbbf345b97df83bb60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Fri, 22 Sep 2017 23:31:46 +0200 Subject: [PATCH 2/9] Clean code related to kfet password --- kfet/auth/backends.py | 10 +--------- kfet/auth/utils.py | 6 ++++++ kfet/models.py | 19 ++++++++++++++----- kfet/tests/test_models.py | 25 +++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 14 deletions(-) create mode 100644 kfet/tests/test_models.py diff --git a/kfet/auth/backends.py b/kfet/auth/backends.py index c972fb55..1c9290d6 100644 --- a/kfet/auth/backends.py +++ b/kfet/auth/backends.py @@ -1,7 +1,4 @@ # -*- coding: utf-8 -*- - -import hashlib - from django.contrib.auth import get_user_model from kfet.models import Account, GenericTeamToken @@ -18,12 +15,7 @@ class KFetBackend(object): return None try: - password_sha256 = ( - hashlib.sha256(password.encode('utf-8')) - .hexdigest() - ) - account = Account.objects.get(password=password_sha256) - return account.cofprofile.user + return Account.objects.get_by_password(password).user except Account.DoesNotExist: return None diff --git a/kfet/auth/utils.py b/kfet/auth/utils.py index 78f31028..0edc555d 100644 --- a/kfet/auth/utils.py +++ b/kfet/auth/utils.py @@ -1,3 +1,5 @@ +import hashlib + from django.contrib.auth import get_user_model from django.contrib.auth.models import Permission @@ -26,3 +28,7 @@ def setup_kfet_generic_user(**kwargs): codename='is_team', ) ) + + +def hash_password(password): + return hashlib.sha256(password.encode('utf-8')).hexdigest() diff --git a/kfet/models.py b/kfet/models.py index 9aefb782..e547d248 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -12,7 +12,6 @@ from django.db import transaction from django.db.models import F from datetime import date import re -import hashlib from .auth import KFET_GENERIC_TRIGRAMME from .auth.models import GenericTeamToken # noqa @@ -42,6 +41,17 @@ class AccountManager(models.Manager): """ return self.get(trigramme=KFET_GENERIC_TRIGRAMME) + def get_by_password(self, password): + """ + Get a kfet generic account by clear password. + + Raises Account.DoesNotExist if no Account has this password. + """ + from .auth.utils import hash_password + if password is None: + raise self.model.DoesNotExist + return self.get(password=hash_password(password)) + class Account(models.Model): objects = AccountManager() @@ -245,10 +255,9 @@ class Account(models.Model): self.cofprofile = cof super(Account, self).save(*args, **kwargs) - def change_pwd(self, pwd): - pwd_sha256 = hashlib.sha256(pwd.encode('utf-8'))\ - .hexdigest() - self.password = pwd_sha256 + def change_pwd(self, clear_password): + from .auth.utils import hash_password + self.password = hash_password(clear_password) # Surcharge de delete # Pas de suppression possible diff --git a/kfet/tests/test_models.py b/kfet/tests/test_models.py new file mode 100644 index 00000000..ea132acd --- /dev/null +++ b/kfet/tests/test_models.py @@ -0,0 +1,25 @@ +from django.contrib.auth import get_user_model +from django.test import TestCase + +from kfet.models import Account + +User = get_user_model() + + +class AccountTests(TestCase): + + def setUp(self): + self.account = Account(trigramme='000') + self.account.save({'username': 'user'}) + + def test_password(self): + self.account.change_pwd('anna') + self.account.save() + + self.assertEqual(Account.objects.get_by_password('anna'), self.account) + + with self.assertRaises(Account.DoesNotExist): + Account.objects.get_by_password(None) + + with self.assertRaises(Account.DoesNotExist): + Account.objects.get_by_password('bernard') From 3fa7754ff4d31e300e9241d8a2122340a3a8de89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sat, 23 Sep 2017 20:48:28 +0200 Subject: [PATCH 3/9] KFet Backends inherit from BaseKFetBackend Users who authenticate via a KFetBackend got extra select related. It should save 2 db queries on each request for these users. --- kfet/auth/backends.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/kfet/auth/backends.py b/kfet/auth/backends.py index 1c9290d6..b2f1cb03 100644 --- a/kfet/auth/backends.py +++ b/kfet/auth/backends.py @@ -7,7 +7,22 @@ from .utils import get_kfet_generic_user User = get_user_model() -class KFetBackend(object): +class BaseKFetBackend: + def get_user(self, user_id): + """ + Add extra select related up to Account. + """ + try: + return ( + User.objects + .select_related('profile__account_kfet') + .get(pk=user_id) + ) + except User.DoesNotExist: + return None + + +class KFetBackend(BaseKFetBackend): def authenticate(self, request): password = request.POST.get('KFETPASSWORD', '') password = request.META.get('HTTP_KFETPASSWORD', password) @@ -20,7 +35,7 @@ class KFetBackend(object): return None -class GenericTeamBackend(object): +class GenericTeamBackend(BaseKFetBackend): def authenticate(self, username=None, token=None): valid_token = GenericTeamToken.objects.get(token=token) if username == 'kfet_genericteam' and valid_token: From db512a97f6a8086c7d25d45e0d5fbca8eebe1d62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Mon, 25 Sep 2017 14:22:46 +0200 Subject: [PATCH 4/9] In /admin: displays "given" when it's relevant --- bda/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bda/admin.py b/bda/admin.py index 0cc66d43..83c89ea5 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -61,12 +61,12 @@ class AttributionInline(admin.TabularInline): class WithListingAttributionInline(AttributionInline): + exclude = ('given', ) form = WithListingAttributionTabularAdminForm listing = True class WithoutListingAttributionInline(AttributionInline): - exclude = ('given', ) form = WithoutListingAttributionTabularAdminForm listing = False From b42452080f36d2a669b19c5c93ea9194a2703afe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 25 Sep 2017 17:16:19 +0200 Subject: [PATCH 5/9] Mass cleaning of kfet' authentication machinery AccountBackend - Should now work if used in AUTHENTICATION_BACKENDS settings. - It does not retieve itself the password, as it should not be used this way. GenericBackend - Delete useless 'username' arg of its 'authenticate()' method. - Now delete the token in DB. TemporaryAuthMiddleware - New name of the middleware is more meaningful. - Is now responsible to retrieve the password from the request, instead of the AccountBackend. GenericTeamToken model - Add a manager' method to create token, avoiding possible error due to unicity constraint. GenericLoginView (authentication with the kfet generic user) - Replace obscure system with a 100% HTTP handling. - See comments for more information. Misc - More docstrings! - More tests! - Add some i18n. - Add kfet/confirm_form.html template: Ask user to confirm sth via a form (which will send a POST request). Context variables: * title: the page title * confirm_url: action attribute for
* text: displayed confirmation text - kfet.js : Add functions allowing to emit POST request from tag. - Non-link nav items from kfet navbar also get a 'title'. - A utility has been found for the 'sunglasses' glyphicon! --- cof/settings/common.py | 6 +- kfet/apps.py | 1 - kfet/auth/apps.py | 1 + kfet/auth/backends.py | 36 +-- kfet/auth/context_processors.py | 2 +- kfet/auth/middleware.py | 25 +- kfet/auth/models.py | 12 + kfet/auth/signals.py | 40 +++ .../templates/kfet/login_genericteam.html | 7 - kfet/auth/tests.py | 299 +++++++++++++++++- kfet/auth/views.py | 111 +++++-- kfet/signals.py | 21 -- kfet/static/kfet/js/kfet.js | 66 +++- kfet/templates/kfet/base_nav.html | 21 +- kfet/templates/kfet/confirm_form.html | 20 ++ kfet/templates/kfet/nav_item.html | 4 + kfet/urls.py | 4 +- kfet/views.py | 2 +- 18 files changed, 559 insertions(+), 119 deletions(-) create mode 100644 kfet/auth/signals.py delete mode 100644 kfet/auth/templates/kfet/login_genericteam.html delete mode 100644 kfet/signals.py create mode 100644 kfet/templates/kfet/confirm_form.html diff --git a/cof/settings/common.py b/cof/settings/common.py index 92759d21..0437f5db 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -100,7 +100,7 @@ MIDDLEWARE_CLASSES = [ 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', - 'kfet.auth.middleware.KFetAuthenticationMiddleware', + 'kfet.auth.middleware.TemporaryAuthMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.security.SecurityMiddleware', @@ -128,7 +128,7 @@ TEMPLATES = [ 'wagtailmenus.context_processors.wagtailmenus', 'djconfig.context_processors.config', 'gestioncof.shared.context_processor', - 'kfet.auth.context_processors.auth', + 'kfet.auth.context_processors.temporary_auth', 'kfet.context_processors.config', ], }, @@ -191,7 +191,7 @@ CAS_EMAIL_FORMAT = "%s@clipper.ens.fr" AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', 'gestioncof.shared.COFCASBackend', - 'kfet.auth.backends.GenericTeamBackend', + 'kfet.auth.backends.GenericBackend', ) RECAPTCHA_USE_SSL = True diff --git a/kfet/apps.py b/kfet/apps.py index 3dd2c0e8..4f114c37 100644 --- a/kfet/apps.py +++ b/kfet/apps.py @@ -11,7 +11,6 @@ class KFetConfig(AppConfig): verbose_name = "Application K-Fêt" def ready(self): - import kfet.signals self.register_config() def register_config(self): diff --git a/kfet/auth/apps.py b/kfet/auth/apps.py index 03742843..d91931f5 100644 --- a/kfet/auth/apps.py +++ b/kfet/auth/apps.py @@ -9,5 +9,6 @@ class KFetAuthConfig(AppConfig): verbose_name = _("K-Fêt - Authentification et Autorisation") def ready(self): + from . import signals # noqa from .utils import setup_kfet_generic_user post_migrate.connect(setup_kfet_generic_user, sender=self) diff --git a/kfet/auth/backends.py b/kfet/auth/backends.py index b2f1cb03..c6ad21b2 100644 --- a/kfet/auth/backends.py +++ b/kfet/auth/backends.py @@ -22,32 +22,22 @@ class BaseKFetBackend: return None -class KFetBackend(BaseKFetBackend): - def authenticate(self, request): - password = request.POST.get('KFETPASSWORD', '') - password = request.META.get('HTTP_KFETPASSWORD', password) - if not password: - return None - +class AccountBackend(BaseKFetBackend): + def authenticate(self, request, kfet_password=None): try: - return Account.objects.get_by_password(password).user + return Account.objects.get_by_password(kfet_password).user except Account.DoesNotExist: return None -class GenericTeamBackend(BaseKFetBackend): - def authenticate(self, username=None, token=None): - valid_token = GenericTeamToken.objects.get(token=token) - if username == 'kfet_genericteam' and valid_token: - return get_kfet_generic_user() - return None - - def get_user(self, user_id): +class GenericBackend(BaseKFetBackend): + def authenticate(self, request, kfet_token=None): try: - return ( - User.objects - .select_related('profile__account_kfet') - .get(pk=user_id) - ) - except User.DoesNotExist: - return None + team_token = GenericTeamToken.objects.get(token=kfet_token) + except GenericTeamToken.DoesNotExist: + return + + # No need to keep the token. + team_token.delete() + + return get_kfet_generic_user() diff --git a/kfet/auth/context_processors.py b/kfet/auth/context_processors.py index 07c9537f..7b59b88b 100644 --- a/kfet/auth/context_processors.py +++ b/kfet/auth/context_processors.py @@ -1,7 +1,7 @@ from django.contrib.auth.context_processors import PermWrapper -def auth(request): +def temporary_auth(request): if hasattr(request, 'real_user'): return { 'user': request.real_user, diff --git a/kfet/auth/middleware.py b/kfet/auth/middleware.py index 1a930c3b..748ce4dd 100644 --- a/kfet/auth/middleware.py +++ b/kfet/auth/middleware.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- +from django.contrib.auth import get_user_model -from django.contrib.auth.models import User +from .backends import AccountBackend -from .backends import KFetBackend +User = get_user_model() -class KFetAuthenticationMiddleware(object): - """Authenticate another user for this request if KFetBackend succeeds. +class TemporaryAuthMiddleware: + """Authenticate another user for this request if AccountBackend succeeds. By the way, if a user is authenticated, we refresh its from db to add values from CofProfile and Account of this user. @@ -15,15 +16,23 @@ class KFetAuthenticationMiddleware(object): def process_request(self, request): if request.user.is_authenticated(): # avoid multiple db accesses in views and templates - user_pk = request.user.pk request.user = ( User.objects .select_related('profile__account_kfet') - .get(pk=user_pk) + .get(pk=request.user.pk) ) - kfet_backend = KFetBackend() - temp_request_user = kfet_backend.authenticate(request) + temp_request_user = AccountBackend().authenticate( + request, + kfet_password=self.get_kfet_password(request), + ) + if temp_request_user: request.real_user = request.user request.user = temp_request_user + + def get_kfet_password(self, request): + return ( + request.META.get('HTTP_KFETPASSWORD') or + request.POST.get('KFETPASSWORD') + ) diff --git a/kfet/auth/models.py b/kfet/auth/models.py index 53aef6c9..ecd40091 100644 --- a/kfet/auth/models.py +++ b/kfet/auth/models.py @@ -1,5 +1,17 @@ from django.db import models +from django.utils.crypto import get_random_string + + +class GenericTeamTokenManager(models.Manager): + + def create_token(self): + token = get_random_string(50) + while self.filter(token=token).exists(): + token = get_random_string(50) + return self.create(token=token) class GenericTeamToken(models.Model): token = models.CharField(max_length=50, unique=True) + + objects = GenericTeamTokenManager() diff --git a/kfet/auth/signals.py b/kfet/auth/signals.py new file mode 100644 index 00000000..3d7af18b --- /dev/null +++ b/kfet/auth/signals.py @@ -0,0 +1,40 @@ +from django.contrib import messages +from django.contrib.auth.signals import user_logged_in +from django.core.urlresolvers import reverse +from django.dispatch import receiver +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext as _ + +from .utils import get_kfet_generic_user + + +@receiver(user_logged_in) +def suggest_auth_generic(sender, request, user, **kwargs): + """ + Suggest logged in user to continue as the kfet generic user. + + Message is only added if the following conditions are met: + - the next page (where user is going to be redirected due to successful + authentication) is related to kfet, i.e. 'k-fet' is in its url. + - logged in user is a kfet staff member (except the generic user). + """ + # Filter against the next page. + if not(hasattr(request, 'GET') and 'next' in request.GET): + return + + next_page = request.GET['next'] + generic_url = reverse('kfet.login.generic') + + if not('k-fet' in next_page and not next_page.startswith(generic_url)): + return + + # Filter against the logged in user. + if not(user.has_perm('kfet.is_team') and user != get_kfet_generic_user()): + return + + # Seems legit to add message. + text = _("K-Fêt — Ouvrir une session partagée ?") + messages.info(request, mark_safe( + '{}' + .format(generic_url, text) + )) diff --git a/kfet/auth/templates/kfet/login_genericteam.html b/kfet/auth/templates/kfet/login_genericteam.html deleted file mode 100644 index d2f8eca0..00000000 --- a/kfet/auth/templates/kfet/login_genericteam.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends 'kfet/base.html' %} - -{% block extra_head %} - -{% endblock %} diff --git a/kfet/auth/tests.py b/kfet/auth/tests.py index 47cc2376..c2f183cd 100644 --- a/kfet/auth/tests.py +++ b/kfet/auth/tests.py @@ -1,15 +1,26 @@ # -*- coding: utf-8 -*- +from unittest import mock -from django.test import TestCase -from django.contrib.auth.models import User, Group +from django.core import signing +from django.core.urlresolvers import reverse +from django.contrib.auth.models import AnonymousUser, Group, Permission, User +from django.test import RequestFactory, TestCase from kfet.forms import UserGroupForm from kfet.models import Account from . import KFET_GENERIC_TRIGRAMME, KFET_GENERIC_USERNAME +from .backends import AccountBackend, GenericBackend +from .middleware import TemporaryAuthMiddleware +from .models import GenericTeamToken from .utils import get_kfet_generic_user +from .views import GenericLoginView +## +# Forms +## + class UserGroupFormTests(TestCase): """Test suite for UserGroupForm.""" @@ -70,3 +81,287 @@ class KFetGenericUserTests(TestCase): self.assertEqual(generic.trigramme, KFET_GENERIC_TRIGRAMME) self.assertEqual(generic.user.username, KFET_GENERIC_USERNAME) self.assertEqual(get_kfet_generic_user(), generic.user) + + +## +# Backends +## + +class AccountBackendTests(TestCase): + + def setUp(self): + self.request = RequestFactory().get('/') + + def test_valid(self): + acc = Account(trigramme='000') + acc.change_pwd('valid') + acc.save({'username': 'user'}) + + auth = AccountBackend().authenticate( + self.request, kfet_password='valid') + + self.assertEqual(auth, acc.user) + + def test_invalid(self): + auth = AccountBackend().authenticate( + self.request, kfet_password='invalid') + self.assertIsNone(auth) + + +class GenericBackendTests(TestCase): + + def setUp(self): + self.request = RequestFactory().get('/') + + def test_valid(self): + token = GenericTeamToken.objects.create_token() + + auth = GenericBackend().authenticate( + self.request, kfet_token=token.token) + + self.assertEqual(auth, get_kfet_generic_user()) + self.assertEqual(GenericTeamToken.objects.all().count(), 0) + + def test_invalid(self): + auth = GenericBackend().authenticate( + self.request, kfet_token='invalid') + self.assertIsNone(auth) + + +## +# Views +## + +class GenericLoginViewTests(TestCase): + + def setUp(self): + patcher_messages = mock.patch('gestioncof.signals.messages') + patcher_messages.start() + self.addCleanup(patcher_messages.stop) + + user_acc = Account(trigramme='000') + user_acc.save({'username': 'user'}) + self.user = user_acc.user + self.user.set_password('user') + self.user.save() + + team_acc = Account(trigramme='100') + team_acc.save({'username': 'team'}) + self.team = team_acc.user + self.team.set_password('team') + self.team.save() + self.team.user_permissions.add( + Permission.objects.get( + content_type__app_label='kfet', codename='is_team'), + ) + + self.url = reverse('kfet.login.generic') + self.generic_user = get_kfet_generic_user() + + def test_url(self): + self.assertEqual(self.url, '/k-fet/login/generic') + + def test_notoken_get(self): + """ + Send confirmation for user to emit POST request, instead of GET. + """ + self.client.login(username='team', password='team') + + r = self.client.get(self.url) + + self.assertEqual(r.status_code, 200) + self.assertTemplateUsed(r, 'kfet/confirm_form.html') + + def test_notoken_post(self): + """ + POST request without token in COOKIES sets a token and redirects to + logout url. + """ + self.client.login(username='team', password='team') + + r = self.client.post(self.url) + + self.assertRedirects( + r, '/logout?next={}'.format(self.url), + fetch_redirect_response=False, + ) + + def test_notoken_not_team(self): + """ + Logged in user must be a team user to initiate login as generic user. + """ + self.client.login(username='user', password='user') + + # With GET. + r = self.client.get(self.url) + self.assertRedirects( + r, '/login?next={}'.format(self.url), + fetch_redirect_response=False, + ) + + # Also with POST. + r = self.client.post(self.url) + self.assertRedirects( + r, '/login?next={}'.format(self.url), + fetch_redirect_response=False, + ) + + def _set_signed_cookie(self, client, key, value): + signed_value = signing.get_cookie_signer(salt=key).sign(value) + client.cookies.load({key: signed_value}) + + def _is_cookie_deleted(self, client, key): + try: + self.assertNotIn(key, client.cookies) + except AssertionError: + try: + cookie = client.cookies[key] + # It also can be emptied. + self.assertEqual(cookie.value, '') + self.assertEqual( + cookie['expires'], 'Thu, 01-Jan-1970 00:00:00 GMT') + self.assertEqual(cookie['max-age'], 0) + except AssertionError: + raise AssertionError("The cookie '%s' still exists." % key) + + def test_withtoken_valid(self): + """ + The kfet generic user is logged in. + """ + token = GenericTeamToken.objects.create(token='valid') + self._set_signed_cookie( + self.client, GenericLoginView.TOKEN_COOKIE_NAME, 'valid') + + r = self.client.get(self.url) + + self.assertRedirects(r, reverse('kfet.kpsul')) + self.assertEqual(r.wsgi_request.user, self.generic_user) + self._is_cookie_deleted( + self.client, GenericLoginView.TOKEN_COOKIE_NAME) + with self.assertRaises(GenericTeamToken.DoesNotExist): + token.refresh_from_db() + + def test_withtoken_invalid(self): + """ + If token is invalid, delete it and try again. + """ + self._set_signed_cookie( + self.client, GenericLoginView.TOKEN_COOKIE_NAME, 'invalid') + + r = self.client.get(self.url) + + self.assertRedirects(r, self.url, fetch_redirect_response=False) + self.assertEqual(r.wsgi_request.user, AnonymousUser()) + self._is_cookie_deleted( + self.client, GenericLoginView.TOKEN_COOKIE_NAME) + + def test_flow_ok(self): + """ + A team user is logged in as the kfet generic user. + """ + self.client.login(username='team', password='team') + next_url = '/k-fet/' + + r = self.client.post( + '{}?next={}'.format(self.url, next_url), follow=True) + + self.assertEqual(r.wsgi_request.user, self.generic_user) + self.assertEqual(r.wsgi_request.path, '/k-fet/') + + +## +# Temporary authentication +# +# Includes: +# - TemporaryAuthMiddleware +# - temporary_auth context processor +## + +class TemporaryAuthTests(TestCase): + + def setUp(self): + patcher_messages = mock.patch('gestioncof.signals.messages') + patcher_messages.start() + self.addCleanup(patcher_messages.stop) + + self.factory = RequestFactory() + + user1_acc = Account(trigramme='000') + user1_acc.change_pwd('kfet_user1') + user1_acc.save({'username': 'user1'}) + self.user1 = user1_acc.user + self.user1.set_password('user1') + self.user1.save() + + user2_acc = Account(trigramme='100') + user2_acc.change_pwd('kfet_user2') + user2_acc.save({'username': 'user2'}) + self.user2 = user2_acc.user + self.user2.set_password('user2') + self.user2.save() + + self.perm = Permission.objects.get( + content_type__app_label='kfet', codename='is_team') + self.user2.user_permissions.add(self.perm) + + def test_middleware_header(self): + """ + A user can be authenticated if ``HTTP_KFETPASSWORD`` header of a + request contains a valid kfet password. + """ + request = self.factory.get('/', HTTP_KFETPASSWORD='kfet_user2') + request.user = self.user1 + + TemporaryAuthMiddleware().process_request(request) + + self.assertEqual(request.user, self.user2) + self.assertEqual(request.real_user, self.user1) + + def test_middleware_post(self): + """ + A user can be authenticated if ``KFETPASSWORD`` of POST data contains + a valid kfet password. + """ + request = self.factory.post('/', {'KFETPASSWORD': 'kfet_user2'}) + request.user = self.user1 + + TemporaryAuthMiddleware().process_request(request) + + self.assertEqual(request.user, self.user2) + self.assertEqual(request.real_user, self.user1) + + def test_middleware_invalid(self): + """ + The given password must be a password of an Account. + """ + request = self.factory.post('/', {'KFETPASSWORD': 'invalid'}) + request.user = self.user1 + + TemporaryAuthMiddleware().process_request(request) + + self.assertEqual(request.user, self.user1) + self.assertFalse(hasattr(request, 'real_user')) + + def test_context_processor(self): + """ + Context variables give the real authenticated user and his permissions. + """ + self.client.login(username='user1', password='user1') + + r = self.client.get('/k-fet/accounts/', HTTP_KFETPASSWORD='kfet_user2') + + self.assertEqual(r.context['user'], self.user1) + self.assertNotIn('kfet.is_team', r.context['perms']) + + def test_auth_not_persistent(self): + """ + The authentication is temporary, i.e. for one request. + """ + self.client.login(username='user1', password='user1') + + r1 = self.client.get( + '/k-fet/accounts/', HTTP_KFETPASSWORD='kfet_user2') + self.assertEqual(r1.wsgi_request.user, self.user2) + + r2 = self.client.get('/k-fet/accounts/') + self.assertEqual(r2.wsgi_request.user, self.user1) diff --git a/kfet/auth/views.py b/kfet/auth/views.py index ce44b007..7b9f4099 100644 --- a/kfet/auth/views.py +++ b/kfet/auth/views.py @@ -3,38 +3,105 @@ from django.contrib.messages.views import SuccessMessageMixin from django.contrib.auth import authenticate, login from django.contrib.auth.decorators import permission_required from django.contrib.auth.models import Group, User -from django.core.urlresolvers import reverse_lazy +from django.contrib.auth.views import redirect_to_login +from django.core.urlresolvers import reverse, reverse_lazy from django.db.models import Prefetch -from django.shortcuts import render -from django.utils.crypto import get_random_string +from django.http import QueryDict +from django.shortcuts import redirect, render +from django.utils.decorators import method_decorator +from django.utils.translation import ugettext_lazy as _ +from django.views.generic import View +from django.views.decorators.http import require_http_methods from django.views.generic.edit import CreateView, UpdateView -from django_cas_ng.views import logout as cas_logout_view - -from kfet.decorators import teamkfet_required - from .forms import GroupForm from .models import GenericTeamToken -@teamkfet_required -def login_genericteam(request): - # Check si besoin de déconnecter l'utilisateur de CAS - cas_logout = None - if request.user.profile.login_clipper: - # Récupèration de la vue de déconnexion de CAS - # Ici, car request sera modifié après - next_page = request.META.get('HTTP_REFERER', None) - cas_logout = cas_logout_view(request, next_page=next_page) +class GenericLoginView(View): + """ + View to authenticate as kfet generic user. - # Authentification du compte générique - token = GenericTeamToken.objects.create(token=get_random_string(50)) - user = authenticate(username="kfet_genericteam", token=token.token) - login(request, user) + It is a 2-step view. First, issue a token if user is a team member and send + him to the logout view (for proper disconnect) with callback url to here. + Then authenticate the token to log in as the kfet generic user. - messages.success(request, "Connecté en utilisateur partagé") + Token is stored in COOKIES to avoid share it with the authentication + provider, which can be external. Session is unusable as it will be cleared + on logout. + """ + TOKEN_COOKIE_NAME = 'kfettoken' - return cas_logout or render(request, "kfet/login_genericteam.html") + @method_decorator(require_http_methods(['GET', 'POST'])) + def dispatch(self, request, *args, **kwargs): + token = request.get_signed_cookie(self.TOKEN_COOKIE_NAME, None) + if not token: + if not request.user.has_perm('kfet.is_team'): + return redirect_to_login(request.get_full_path()) + + if request.method == 'POST': + # Step 1: set token and logout user. + return self.prepare_auth() + else: + # GET request should not change server/client states. Send a + # confirmation template to emit a POST request. + return render(request, 'kfet/confirm_form.html', { + 'title': _("Ouvrir une session partagée"), + 'text': _( + "Êtes-vous sûr·e de vouloir ouvrir une session " + "partagée ?" + ), + }) + else: + # Step 2: validate token. + return self.validate_auth(token) + + def prepare_auth(self): + # Issue token. + token = GenericTeamToken.objects.create_token() + + # Prepare callback of logout. + here_url = reverse(login_generic) + if 'next' in self.request.GET: + # Keep given next page. + here_qd = QueryDict(mutable=True) + here_qd['next'] = self.request.GET['next'] + here_url += '?{}'.format(here_qd.urlencode()) + + logout_url = reverse('cof-logout') + logout_qd = QueryDict(mutable=True) + logout_qd['next'] = here_url + logout_url += '?{}'.format(logout_qd.urlencode(safe='/')) + + resp = redirect(logout_url) + resp.set_signed_cookie( + self.TOKEN_COOKIE_NAME, token.token, httponly=True) + return resp + + def validate_auth(self, token): + # Authenticate with GenericBackend. + user = authenticate(request=self.request, kfet_token=token) + + if user: + # Log in generic user. + login(self.request, user) + messages.success(self.request, _( + "K-Fêt — Ouverture d'une session partagée." + )) + resp = redirect(self.get_next_url()) + else: + # Try again. + resp = redirect(self.request.get_full_path()) + + # Prevents blocking due to an invalid COOKIE. + resp.delete_cookie(self.TOKEN_COOKIE_NAME) + return resp + + def get_next_url(self): + return self.request.GET.get('next', reverse('kfet.kpsul')) + + +login_generic = GenericLoginView.as_view() @permission_required('kfet.manage_perms') diff --git a/kfet/signals.py b/kfet/signals.py deleted file mode 100644 index c677ac9c..00000000 --- a/kfet/signals.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- - -from django.contrib import messages -from django.contrib.auth.signals import user_logged_in -from django.core.urlresolvers import reverse -from django.dispatch import receiver -from django.utils.safestring import mark_safe - - -@receiver(user_logged_in) -def messages_on_login(sender, request, user, **kwargs): - if (not user.username == 'kfet_genericteam' and - user.has_perm('kfet.is_team') and - hasattr(request, 'GET') and - 'k-fet' in request.GET.get('next', '')): - messages.info(request, mark_safe( - '' - ' Connexion en utilisateur partagé ?' - '' - .format(reverse('kfet.login.genericteam')) - )) diff --git a/kfet/static/kfet/js/kfet.js b/kfet/static/kfet/js/kfet.js index b34f2005..75b80b04 100644 --- a/kfet/static/kfet/js/kfet.js +++ b/kfet/static/kfet/js/kfet.js @@ -1,22 +1,33 @@ -$(document).ready(function() { - if (typeof Cookies !== 'undefined') { - // Retrieving csrf token - csrftoken = Cookies.get('csrftoken'); - // Appending csrf token to ajax post requests - function csrfSafeMethod(method) { - // these HTTP methods do not require CSRF protection - return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); +/** + * CSRF Token + */ + +var csrftoken = ''; +if (typeof Cookies !== 'undefined') + csrftoken = Cookies.get('csrftoken'); + +// Add CSRF token in header of AJAX requests. + +function csrfSafeMethod(method) { + // these HTTP methods do not require CSRF protection + return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); +} + +$.ajaxSetup({ + beforeSend: function(xhr, settings) { + if (!csrfSafeMethod(settings.type) && !this.crossDomain) { + xhr.setRequestHeader("X-CSRFToken", csrftoken); } - $.ajaxSetup({ - beforeSend: function(xhr, settings) { - if (!csrfSafeMethod(settings.type) && !this.crossDomain) { - xhr.setRequestHeader("X-CSRFToken", csrftoken); - } - } - }); } }); +function add_csrf_form($form) { + $form.append( + $('', {'name': 'csrfmiddlewaretoken', 'value': csrftoken}) + ); +} + + /* * Generic Websocket class and k-psul ws instanciation */ @@ -199,3 +210,28 @@ jconfirm.defaults = { confirmButton: '', cancelButton: '' }; + + +/** + * Create form node, given an url used as 'action', with csrftoken set. + */ +function create_form(url) { + let $form = $('', { + 'action': url, + 'method': 'post', + }); + add_csrf_form($form); + return $form; +} + + +/** + * Emit a POST request from tag. + * + * Usage: + * {…} + */ +function submit_url(el) { + let url = $(el).data('url'); + create_form(url).appendTo($('body')).submit(); +} diff --git a/kfet/templates/kfet/base_nav.html b/kfet/templates/kfet/base_nav.html index abcb8e18..dda6c1ef 100644 --- a/kfet/templates/kfet/base_nav.html +++ b/kfet/templates/kfet/base_nav.html @@ -1,4 +1,4 @@ -{% load static %} +{% load i18n static %} {% load wagtailcore_tags %} - - diff --git a/kfet/templates/kfet/confirm_form.html b/kfet/templates/kfet/confirm_form.html new file mode 100644 index 00000000..1cffd171 --- /dev/null +++ b/kfet/templates/kfet/confirm_form.html @@ -0,0 +1,20 @@ +{% extends "kfet/base_form.html" %} +{% load i18n %} + +{% block title %}{{ title }}{% endblock %} +{% block header %}{% endblock %} + +{% block main-class %}main-bg main-padding text-center{% endblock %} +{% block main-size %}col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3{% endblock %} + +{% block main %} + + +

+ {{ text }} +

+ + {% csrf_token %} +
+ +{% endblock %} diff --git a/kfet/templates/kfet/nav_item.html b/kfet/templates/kfet/nav_item.html index 8bc311b8..b5981266 100644 --- a/kfet/templates/kfet/nav_item.html +++ b/kfet/templates/kfet/nav_item.html @@ -4,6 +4,8 @@
  • {% if href %} + {% else %} + {% endif %} {% if href %} + {% else %} + {% endif %}
  • diff --git a/kfet/urls.py b/kfet/urls.py index c3499b18..eb4f8311 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -8,8 +8,8 @@ from kfet.decorators import teamkfet_required urlpatterns = [ - url(r'^login/genericteam$', views.login_genericteam, - name='kfet.login.genericteam'), + url(r'^login/generic$', views.login_generic, + name='kfet.login.generic'), url(r'^history$', views.history, name='kfet.history'), diff --git a/kfet/views.py b/kfet/views.py index 386eddb6..79fe184d 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -51,7 +51,7 @@ import statistics from kfet.statistic import ScaleMixin, last_stats_manifest, tot_ventes, WeekScale from .auth.views import ( # noqa - account_group, login_genericteam, AccountGroupCreate, AccountGroupUpdate, + account_group, login_generic, AccountGroupCreate, AccountGroupUpdate, ) From d18fb86a98b5044a169c154961da10b6001e1507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 25 Sep 2017 18:26:54 +0200 Subject: [PATCH 6/9] Fix attribution inlines of participant in admin --- bda/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bda/admin.py b/bda/admin.py index 83c89ea5..60d3c1ba 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -56,7 +56,7 @@ class AttributionInline(admin.TabularInline): def get_queryset(self, request): qs = super().get_queryset(request) if self.listing is not None: - qs.filter(spectacle__listing=self.listing) + qs = qs.filter(spectacle__listing=self.listing) return qs From 596868f5b6762629ac4d6e4f0347aa7b9b77d785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sat, 30 Sep 2017 02:39:45 +0200 Subject: [PATCH 7/9] plop --- .gitlab-ci.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5080ef32..e2c36d8d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,6 +16,9 @@ variables: POSTGRES_USER: "cof_gestion" POSTGRES_DB: "cof_gestion" + # psql password authentication + PGPASSWORD: $POSTGRES_PASSWORD + cache: paths: @@ -28,11 +31,10 @@ before_script: - apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client - sed -E 's/^REDIS_HOST.*/REDIS_HOST = "redis"/' cof/settings/secret_example.py > cof/settings/secret.py # Remove the old test database if it has not been done yet - - psql --username=cof_gestion --password="4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4" --host="$DBHOST" - -e "DROP DATABASE test_$DBNAME" || true + - psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB" - pip install --cache-dir vendor/pip -t vendor/python -r requirements.txt test: stage: test script: - - python manage.py test + - python manage.py test -v3 From 435e211b3d064204eb7d77a624cdbb1f00a687a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Mon, 2 Oct 2017 13:58:52 +0200 Subject: [PATCH 8/9] Add a "PEI" status + "Gratis" subscription fees --- gestioncof/migrations/0013_pei.py | 47 ++++++++++++++++++++++++++++ gestioncof/models.py | 51 ++++++++++++++++++++----------- 2 files changed, 81 insertions(+), 17 deletions(-) create mode 100644 gestioncof/migrations/0013_pei.py diff --git a/gestioncof/migrations/0013_pei.py b/gestioncof/migrations/0013_pei.py new file mode 100644 index 00000000..2fbddf1f --- /dev/null +++ b/gestioncof/migrations/0013_pei.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('gestioncof', '0012_merge'), + ] + + operations = [ + migrations.AlterField( + model_name='cofprofile', + name='occupation', + field=models.CharField( + verbose_name='Occupation', + max_length=9, + default='1A', + choices=[ + ('exterieur', 'Extérieur'), + ('1A', '1A'), + ('2A', '2A'), + ('3A', '3A'), + ('4A', '4A'), + ('archicube', 'Archicube'), + ('doctorant', 'Doctorant'), + ('CST', 'CST'), + ('PEI', 'PEI') + ]), + ), + migrations.AlterField( + model_name='cofprofile', + name='type_cotiz', + field=models.CharField( + verbose_name='Type de cotisation', + max_length=9, + default='normalien', + choices=[ + ('etudiant', 'Normalien étudiant'), + ('normalien', 'Normalien élève'), + ('exterieur', 'Extérieur'), + ('gratis', 'Gratuit') + ]), + ), + ] diff --git a/gestioncof/models.py b/gestioncof/models.py index 6aa6f9cd..ea2cacc4 100644 --- a/gestioncof/models.py +++ b/gestioncof/models.py @@ -8,23 +8,6 @@ from gestioncof.petits_cours_models import choices_length from bda.models import Spectacle -OCCUPATION_CHOICES = ( - ('exterieur', _("Extérieur")), - ('1A', _("1A")), - ('2A', _("2A")), - ('3A', _("3A")), - ('4A', _("4A")), - ('archicube', _("Archicube")), - ('doctorant', _("Doctorant")), - ('CST', _("CST")), -) - -TYPE_COTIZ_CHOICES = ( - ('etudiant', _("Normalien étudiant")), - ('normalien', _("Normalien élève")), - ('exterieur', _("Extérieur")), -) - TYPE_COMMENT_FIELD = ( ('text', _("Texte long")), ('char', _("Texte court")), @@ -32,6 +15,40 @@ TYPE_COMMENT_FIELD = ( class CofProfile(models.Model): + STATUS_EXTE = "exterieur" + STATUS_1A = "1A" + STATUS_2A = "2A" + STATUS_3A = "3A" + STATUS_4A = "4A" + STATUS_ARCHI = "archicube" + STATUS_DOCTORANT = "doctorant" + STATUS_CST = "CST" + STATUS_PEI = "PEI" + + OCCUPATION_CHOICES = ( + (STATUS_EXTE, _("Extérieur")), + (STATUS_1A, _("1A")), + (STATUS_2A, _("2A")), + (STATUS_3A, _("3A")), + (STATUS_4A, _("4A")), + (STATUS_ARCHI, _("Archicube")), + (STATUS_DOCTORANT, _("Doctorant")), + (STATUS_CST, _("CST")), + (STATUS_PEI, _("PEI")), + ) + + COTIZ_ETUDIANT = "etudiant" + COTIZ_NORMALIEN = "normalien" + COTIZ_EXTE = "exterieur" + COTIZ_GRATIS = "gratis" + + TYPE_COTIZ_CHOICES = ( + (COTIZ_ETUDIANT, _("Normalien étudiant")), + (COTIZ_NORMALIEN, _("Normalien élève")), + (COTIZ_EXTE, _("Extérieur")), + (COTIZ_GRATIS, _("Gratuit")), + ) + user = models.OneToOneField(User, related_name="profile") login_clipper = models.CharField( "Login clipper", max_length=32, blank=True From 4d1cb3c2d7032fe2a481610bf5e68393ac44365e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Tue, 10 Oct 2017 15:26:14 +0200 Subject: [PATCH 9/9] Set password for redis in CI --- .gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e2c36d8d..85be668b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -7,6 +7,7 @@ variables: DJANGO_SETTINGS_MODULE: "cof.settings.prod" DBHOST: "postgres" REDIS_HOST: "redis" + REDIS_PASSWD: "dummy" # Cached packages PYTHONPATH: "$CI_PROJECT_DIR/vendor/python" @@ -33,6 +34,7 @@ before_script: # Remove the old test database if it has not been done yet - psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB" - pip install --cache-dir vendor/pip -t vendor/python -r requirements.txt + - redis-cli config set requirepass $REDIS_PASSWD || true test: stage: test