From c9aac8a49dec92e31a1220fc700f65c6b82fdbcc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?=
Date: Thu, 10 Aug 2017 15:02:08 +0200
Subject: [PATCH 001/101] [WIP] Tests for kfet views
---
kfet/tests/test_views.py | 360 ++++++++++++++++++++++++++++++++++++---
kfet/tests/testcases.py | 142 +++++++++++++++
kfet/tests/utils.py | 105 ++++++++++++
3 files changed, 585 insertions(+), 22 deletions(-)
create mode 100644 kfet/tests/testcases.py
create mode 100644 kfet/tests/utils.py
diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py
index ec9d6ffc..f7457786 100644
--- a/kfet/tests/test_views.py
+++ b/kfet/tests/test_views.py
@@ -1,26 +1,243 @@
+import json
from decimal import Decimal
-from unittest.mock import patch
-from django.test import TestCase, Client
-from django.contrib.auth.models import User
+from django.contrib.auth.models import Group, Permission
+from django.core.urlresolvers import reverse
+from django.test import Client, TestCase
from django.utils import timezone
-from ..models import Account, OperationGroup, Checkout, Operation
+from ..models import Account, Checkout, Operation, OperationGroup
+
+from .testcases import ViewTestCaseMixin
+from .utils import create_team, create_user
-class AccountTests(TestCase):
- """Account related views"""
+class LoginGenericTeamViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.login.genericteam'
+ url_expected = '/k-fet/login/genericteam'
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+ logged_in_username = r.wsgi_request.user.username
+ self.assertEqual(logged_in_username, 'kfet_genericteam')
+
+
+class AccountListViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.account'
+ url_expected = '/k-fet/accounts/'
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+
+class AccountValidFreeTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.account.is_validandfree.ajax'
+ url_expected = '/k-fet/accounts/is_validandfree'
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ def test_ok_isvalid_isfree(self):
+ """Upper case trigramme not taken is valid and free."""
+ r = self.client.get(self.url, {'trigramme': 'AAA'})
+ self.assertDictEqual(json.loads(r.content.decode('utf-8')), {
+ 'is_valid': True,
+ 'is_free': True,
+ })
+
+ def test_ok_isvalid_notfree(self):
+ """Already taken trigramme is not free, but valid."""
+ r = self.client.get(self.url, {'trigramme': '000'})
+ self.assertDictEqual(json.loads(r.content.decode('utf-8')), {
+ 'is_valid': True,
+ 'is_free': False,
+ })
+
+ def test_ok_notvalid_isfree(self):
+ """Lower case if forbidden but free."""
+ r = self.client.get(self.url, {'trigramme': 'aaa'})
+ self.assertDictEqual(json.loads(r.content.decode('utf-8')), {
+ 'is_valid': False,
+ 'is_free': True,
+ })
+
+
+class AccountCreateViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.account.create'
+ url_expected = '/k-fet/accounts/new'
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ @property
+ def users_extra(self):
+ return {
+ 'team__add_account': create_team(
+ 'team__add_account', '101',
+ perms=['kfet.add_account'],
+ ),
+ }
+
+ def test_get_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+ def test_post_ok(self):
+ post_data = {
+ 'trigramme': 'AAA',
+ 'username': 'plopplopplop',
+ 'first_name': 'first',
+ 'last_name': 'last',
+ 'email': 'email@domain.net',
+ }
+
+ client = Client()
+ client.login(
+ username='team__add_account',
+ password='team__add_account',
+ )
+ r = client.post(self.url, post_data)
+
+ self.assertRedirects(r, self.url)
+ a = Account.objects.get(trigramme='AAA')
+ self.assertEqual(a.username, 'plopplopplop')
+
+ def test_post_forbidden(self):
+ post_data = {
+ 'trigramme': 'AAA',
+ 'username': 'plopplopplop',
+ 'first_name': 'first',
+ 'last_name': 'last',
+ 'email': 'email@domain.net',
+ }
+
+ # A team member (without kfet.add_account) is authenticated with
+ # self.client.
+ r = self.client.post(self.url, post_data)
+
+ self.assertEqual(r.status_code, 200)
+ with self.assertRaises(Account.DoesNotExist):
+ Account.objects.get(trigramme='AAA')
+
+
+class AccountCreateAjaxViewTests(ViewTestCaseMixin, TestCase):
+ urls_conf = [
+ {
+ 'name': 'kfet.account.create.fromuser',
+ 'kwargs': {'username': 'user'},
+ 'expected': '/k-fet/accounts/new/user/user',
+ },
+ {
+ 'name': 'kfet.account.create.fromclipper',
+ 'kwargs': {
+ 'login_clipper': 'myclipper',
+ 'fullname': 'first last1 last2',
+ },
+ 'expected': (
+ '/k-fet/accounts/new/clipper/myclipper/first%20last1%20last2'
+ ),
+ },
+ {
+ 'name': 'kfet.account.create.empty',
+ 'expected': '/k-fet/accounts/new/empty',
+ },
+ ]
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ def test_fromuser(self):
+ r = self.client.get(self.t_urls[0])
+
+ user = self.users['user']
+
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.context['user_form'].instance, user)
+ self.assertEqual(r.context['cof_form'].instance, user.profile)
+ self.assertIn('account_form', r.context)
+
+ def test_fromclipper(self):
+ r = self.client.get(self.t_urls[1])
+
+ self.assertEqual(r.status_code, 200)
+ self.assertIn('user_form', r.context)
+ self.assertIn('cof_form', r.context)
+ self.assertIn('account_form', r.context)
+
+ def test_empty(self):
+ r = self.client.get(self.t_urls[0])
+
+ self.assertEqual(r.status_code, 200)
+ self.assertIn('user_form', r.context)
+ self.assertIn('cof_form', r.context)
+ self.assertIn('account_form', r.context)
+
+
+class AccountCreateAutocompleteViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.account.create.autocomplete'
+ url_expected = '/k-fet/autocomplete/account_new'
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ def test_ok(self):
+ r = self.client.get(self.url, {'q': 'first'})
+ self.assertEqual(r.status_code, 200)
+ self.assertListEqual(list(r.context['users_notcof']), [])
+ self.assertListEqual(list(r.context['users_cof']), [])
+ self.assertListEqual(
+ list(r.context['kfet']),
+ [(self.accounts['user'], self.users['user'])],
+ )
+
+
+class AccountSearchViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.account.search.autocomplete'
+ url_expected = '/k-fet/autocomplete/account_search'
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ def test_ok(self):
+ r = self.client.get(self.url, {'q': 'first'})
+ self.assertEqual(r.status_code, 200)
+ self.assertListEqual(
+ list(r.context['accounts']),
+ [('000', 'first last')],
+ )
+
+
+class AccountReadViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.account.read'
+ url_kwargs = {'trigramme': '001'}
+ url_expected = '/k-fet/accounts/001'
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ @property
+ def users_extra(self):
+ return {
+ 'user1': create_user('user1', '001'),
+ }
def setUp(self):
- # A user and its account
- self.user = User.objects.create_user(username="foobar", password="foo")
- acc = Account.objects.create(
- trigramme="FOO", cofprofile=self.user.profile
- )
+ super().setUp()
+
+ user1_acc = self.accounts['user1']
+ team_acc = self.accounts['team']
# Dummy operations and operation groups
checkout = Checkout.objects.create(
- created_by=acc, name="checkout",
+ created_by=team_acc, name="checkout",
valid_from=timezone.now(),
valid_to=timezone.now() + timezone.timedelta(days=365)
)
@@ -30,7 +247,7 @@ class AccountTests(TestCase):
]
OperationGroup.objects.bulk_create([
OperationGroup(
- on_acc=acc, checkout=checkout, at=at, is_cof=False,
+ on_acc=user1_acc, checkout=checkout, at=at, is_cof=False,
amount=amount
)
for (at, amount) in opeg_data
@@ -47,13 +264,112 @@ class AccountTests(TestCase):
amount=Decimal('3')
)
- @patch('gestioncof.signals.messages')
- def test_account_read(self, mock_messages):
- """We can query the "Account - Read" page."""
+ def test_ok(self):
+ """We can query the "Account - Read" page."""
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+ def test_ok_self(self):
client = Client()
- self.assertTrue(client.login(
- username="foobar",
- password="foo"
- ))
- resp = client.get("/k-fet/accounts/FOO")
- self.assertEqual(200, resp.status_code)
+ client.login(username='user1', password='user1')
+ r = client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+
+class AccountUpdateViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.account.update'
+ url_kwargs = {'trigramme': '001'}
+ url_expected = '/k-fet/accounts/001/edit'
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ @property
+ def users_extra(self):
+ return {
+ 'user1': create_user('user1', '001'),
+ }
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+ def test_ok_self(self):
+ client = Client()
+ client.login(username='user1', password='user1')
+ r = client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+
+class BaseAccountGroupViewTests(ViewTestCaseMixin):
+ auth_user = 'team__manage_perms'
+ auth_forbidden = [None, 'user', 'team']
+
+ @property
+ def users_extra(self):
+ return {
+ 'team__manage_perms': create_team(
+ 'team__manage_perms', '101',
+ perms=['kfet.manage_perms'],
+ ),
+ }
+
+
+class AccountGroupListViewTests(BaseAccountGroupViewTests, TestCase):
+ url_name = 'kfet.account.group'
+ url_expected = '/k-fet/accounts/groups'
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+ self.assertQuerysetEqual(
+ r.context['groups'],
+ Group.objects.filter(name__icontains='K-Fêt'),
+ ordered=False,
+ )
+
+
+class AccountGroupCreateViewTests(BaseAccountGroupViewTests, TestCase):
+ url_name = 'kfet.account.group.create'
+ url_expected = '/k-fet/accounts/groups/new'
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+
+class AccountGroupUpdateViewTests(BaseAccountGroupViewTests, TestCase):
+ url_name = 'kfet.account.group.update'
+ url_kwargs = {'pk': 42}
+ url_expected = '/k-fet/accounts/groups/42/edit'
+
+ def setUp(self):
+ super().setUp()
+ self.group1 = Group.objects.create(pk=42, name='K-Fêt - Group')
+ self.group1.permissions = [
+ Permission.objects.get(
+ content_type__app_label='kfet',
+ codename='is_team',
+ )
+ ]
+
+ def test_get_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+ def test_post_ok(self):
+ post_data = {
+ 'name': 'Group42',
+ 'permissions': (
+ self.group1.permissions
+ .values_list('pk', flat=True)
+ ),
+ }
+
+ r = self.client.post(self.url, post_data)
+
+ self.assertRedirects(r, reverse('kfet.account.group'))
+
+ self.group1.refresh_from_db()
+ self.assertEqual(self.group1.name, 'K-Fêt Group42')
diff --git a/kfet/tests/testcases.py b/kfet/tests/testcases.py
new file mode 100644
index 00000000..c2e1b848
--- /dev/null
+++ b/kfet/tests/testcases.py
@@ -0,0 +1,142 @@
+from unittest import mock
+
+from django.core.urlresolvers import reverse
+from django.http import QueryDict
+from django.test import Client
+
+from .utils import create_root, create_team, create_user
+
+
+class ViewTestCaseMixin:
+ url_name = None
+ url_expected = None
+
+ auth_user = None
+ auth_forbidden = []
+
+ def setUp(self):
+ # Signals handlers on login/logout send messages.
+ # Due to the way the Django' test Client performs login, this raise an
+ # error. As workaround, we mock the Django' messages module.
+ patcher_messages = mock.patch('gestioncof.signals.messages')
+ patcher_messages.start()
+ self.addCleanup(patcher_messages.stop)
+
+ self.users = {}
+ self.accounts = {}
+
+ for label, user in {**self.users_base, **self.users_extra}.items():
+ self.register_user(label, user)
+
+ if self.auth_user:
+ # The wrapper is a sanity check.
+ self.assertTrue(
+ self.client.login(
+ username=self.auth_user,
+ password=self.auth_user,
+ )
+ )
+
+ @property
+ def users_base(self):
+ # Format desc: username, password, trigramme
+ return {
+ # user, user, 000
+ 'user': create_user(),
+ # team, team, 100
+ 'team': create_team(),
+ # root, root, 200
+ 'root': create_root(),
+ }
+
+ @property
+ def users_extra(self):
+ return {}
+
+ def register_user(self, label, user):
+ self.users[label] = user
+ if hasattr(user.profile, 'account_kfet'):
+ self.accounts[label] = user.profile.account_kfet
+
+ @property
+ def urls_conf(self):
+ return [{
+ 'name': self.url_name,
+ 'args': getattr(self, 'url_args', []),
+ 'kwargs': getattr(self, 'url_kwargs', {}),
+ 'expected': self.url_expected,
+ }]
+
+ @property
+ def t_urls(self):
+ return [
+ reverse(
+ url_conf['name'],
+ args=url_conf.get('args', []),
+ kwargs=url_conf.get('kwargs', {}),
+ )
+ for url_conf in self.urls_conf]
+
+ @property
+ def url(self):
+ return self.t_urls[0]
+
+ def assertForbidden(self, response):
+ request = response.wsgi_request
+
+ try:
+ try:
+ # Is this an HTTP Forbidden response ?
+ self.assertEqual(response.status_code, 403)
+ except AssertionError:
+ # A redirection to the login view is fine too.
+
+ # Let's build the login url with the 'next' param on current
+ # page.
+ full_path = request.get_full_path()
+
+ querystring = QueryDict(mutable=True)
+ querystring['next'] = full_path
+
+ login_url = '/login?' + querystring.urlencode(safe='/')
+
+ # We don't focus on what the login view does.
+ # So don't fetch the redirect.
+ self.assertRedirects(
+ response, login_url,
+ fetch_redirect_response=False,
+ )
+ except AssertionError:
+ raise AssertionError(
+ "%(http_method)s request at %(path)s should be forbidden for "
+ "%(username)s user.\n"
+ "Response isn't 403, nor a redirect to login view. Instead, "
+ "response code is %(code)d." % {
+ 'http_method': request.method,
+ 'path': request.get_full_path(),
+ 'username': (
+ "'{}'".format(request.user.username)
+ if request.user.username
+ else 'anonymous'
+ ),
+ 'code': response.status_code,
+ }
+ )
+
+ def assertForbiddenKfet(self, response):
+ self.assertEqual(response.status_code, 200)
+ form = response.context['form']
+ self.assertIn("Permission refusée", form.non_field_errors)
+
+ def test_urls(self):
+ for url, conf in zip(self.t_urls, self.urls_conf):
+ self.assertEqual(url, conf['expected'])
+
+ def test_forbidden(self):
+ for creds in self.auth_forbidden:
+ for url in self.t_urls:
+ client = Client()
+ if creds is not None:
+ client.login(username=creds, password=creds)
+ r = client.get(url)
+ self.assertForbidden(r)
diff --git a/kfet/tests/utils.py b/kfet/tests/utils.py
new file mode 100644
index 00000000..4b739003
--- /dev/null
+++ b/kfet/tests/utils.py
@@ -0,0 +1,105 @@
+from django.contrib.auth import get_user_model
+from django.contrib.auth.models import Permission
+
+from ..models import Account
+
+
+User = get_user_model()
+
+
+def user_add_perms(user, perms_labels):
+ """
+ Add perms to a user.
+
+ Args:
+ user (User instance)
+ perms (list of str 'app.perm_name')
+
+ Returns:
+ The same user (refetched from DB to avoid missing perms)
+
+ """
+ u_labels = set(perms_labels)
+
+ perms = []
+ for label in u_labels:
+ app_label, codename = label.split('.', 1)
+ perms.append(
+ Permission.objects.get(
+ content_type__app_label=app_label,
+ codename=codename,
+ )
+ )
+
+ user.user_permissions.add(*perms)
+
+ # If permissions have already been fetched for this user, we need to reload
+ # it to avoid using of the previous permissions cache.
+ # https://docs.djangoproject.com/en/1.11/topics/auth/default/#permission-caching
+ return User.objects.get(pk=user.pk)
+
+
+def _create_user_and_account(user_attrs, account_attrs, perms=None):
+ user_attrs.setdefault('password', user_attrs['username'])
+ user = User.objects.create_user(**user_attrs)
+
+ account_attrs['cofprofile'] = user.profile
+ kfet_pwd = account_attrs.pop('password', None)
+
+ account = Account.objects.create(**account_attrs)
+
+ if perms is not None:
+ user = user_add_perms(user, perms)
+
+ if 'kfet.is_team' in perms:
+ if kfet_pwd is None:
+ kfet_pwd = 'kfetpwd_{}'.format(user_attrs['password'])
+ account.change_pwd(kfet_pwd)
+ account.save()
+
+ return user
+
+
+def create_user(username='user', trigramme='000', **kwargs):
+ user_attrs = kwargs.setdefault('user_attrs', {})
+
+ user_attrs.setdefault('username', username)
+ user_attrs.setdefault('first_name', 'first')
+ user_attrs.setdefault('last_name', 'last')
+ user_attrs.setdefault('email', 'mail@user.net')
+
+ account_attrs = kwargs.setdefault('account_attrs', {})
+ account_attrs.setdefault('trigramme', trigramme)
+
+ return _create_user_and_account(**kwargs)
+
+
+def create_team(username='team', trigramme='100', **kwargs):
+ user_attrs = kwargs.setdefault('user_attrs', {})
+
+ user_attrs.setdefault('username', username)
+ user_attrs.setdefault('first_name', 'team')
+ user_attrs.setdefault('last_name', 'member')
+ user_attrs.setdefault('email', 'mail@team.net')
+
+ account_attrs = kwargs.setdefault('account_attrs', {})
+ account_attrs.setdefault('trigramme', trigramme)
+
+ perms = kwargs.setdefault('perms', [])
+ perms.append('kfet.is_team')
+
+ return _create_user_and_account(**kwargs)
+
+
+def create_root(username='root', trigramme='200', **kwargs):
+ user_attrs = kwargs.setdefault('user_attrs', {})
+
+ user_attrs.setdefault('username', username)
+ user_attrs.setdefault('first_name', 'super')
+ user_attrs.setdefault('last_name', 'user')
+ user_attrs.setdefault('email', 'mail@root.net')
+
+ account_attrs = kwargs.setdefault('account_attrs', {})
+ account_attrs.setdefault('trigramme', trigramme)
+
+ return _create_user_and_account(**kwargs)
From 2cfce1c921d3a75185a6131061ce5edc797d7c0c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?=
Date: Wed, 16 Aug 2017 17:45:59 +0200
Subject: [PATCH 002/101] Add tests for kfet views.
kfet.tests.testcases embed mixins for TestCase:
- TestCaseMixin provides assertion helpers,
- ViewTestCaseMixin provides a few basic tests, which are common to
every view.
kfet.tests.utils provides helpers for users and permissions management.
Each kfet view get a testcase (at least very basic) in
kfet.tests.test_views.
---
kfet/tests/test_tests_utils.py | 95 ++
kfet/tests/test_views.py | 2121 ++++++++++++++++++++++++++++++--
kfet/tests/testcases.py | 180 ++-
kfet/tests/utils.py | 75 +-
4 files changed, 2265 insertions(+), 206 deletions(-)
create mode 100644 kfet/tests/test_tests_utils.py
diff --git a/kfet/tests/test_tests_utils.py b/kfet/tests/test_tests_utils.py
new file mode 100644
index 00000000..8308bd5b
--- /dev/null
+++ b/kfet/tests/test_tests_utils.py
@@ -0,0 +1,95 @@
+from django.contrib.auth import get_user_model
+from django.contrib.auth.models import Permission
+from django.contrib.contenttypes.models import ContentType
+from django.test import TestCase
+
+from gestioncof.models import CofProfile
+
+from ..models import Account
+from .testcases import TestCaseMixin
+from .utils import (
+ create_user, create_team, create_root, get_perms, user_add_perms,
+)
+
+
+User = get_user_model()
+
+
+class UserHelpersTests(TestCaseMixin, TestCase):
+
+ def test_create_user(self):
+ """create_user creates a basic user and its account."""
+ u = create_user()
+ a = u.profile.account_kfet
+
+ self.assertInstanceExpected(u, {
+ 'get_full_name': 'first last',
+ 'username': 'user',
+ })
+ self.assertFalse(u.user_permissions.exists())
+
+ self.assertEqual('000', a.trigramme)
+
+ def test_create_team(self):
+ u = create_team()
+ a = u.profile.account_kfet
+
+ self.assertInstanceExpected(u, {
+ 'get_full_name': 'team member',
+ 'username': 'team',
+ })
+ self.assertTrue(u.has_perm('kfet.is_team'))
+
+ self.assertEqual('100', a.trigramme)
+
+ def test_create_root(self):
+ u = create_root()
+ a = u.profile.account_kfet
+
+ self.assertInstanceExpected(u, {
+ 'get_full_name': 'super user',
+ 'username': 'root',
+ 'is_superuser': True,
+ 'is_staff': True,
+ })
+
+ self.assertEqual('200', a.trigramme)
+
+
+class PermHelpersTest(TestCaseMixin, TestCase):
+
+ def setUp(self):
+ cts = ContentType.objects.get_for_models(Account, CofProfile)
+ self.perm1 = Permission.objects.create(
+ content_type=cts[Account],
+ codename='test_perm',
+ name='Perm for test',
+ )
+ self.perm2 = Permission.objects.create(
+ content_type=cts[CofProfile],
+ codename='another_test_perm',
+ name='Another one',
+ )
+ self.perm_team = Permission.objects.get(
+ content_type__app_label='kfet',
+ codename='is_team',
+ )
+
+ def test_get_perms(self):
+ perms = get_perms('kfet.test_perm', 'gestioncof.another_test_perm')
+ self.assertDictEqual(perms, {
+ 'kfet.test_perm': self.perm1,
+ 'gestioncof.another_test_perm': self.perm2,
+ })
+
+ def test_user_add_perms(self):
+ user = User.objects.create_user(username='user', password='user')
+ user.user_permissions.add(self.perm1)
+
+ user_add_perms(user, ['kfet.is_team', 'gestioncof.another_test_perm'])
+
+ self.assertQuerysetEqual(
+ user.user_permissions.all(),
+ map(repr, [self.perm1, self.perm2, self.perm_team]),
+ ordered=False,
+ )
diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py
index f7457786..5ac82f33 100644
--- a/kfet/tests/test_views.py
+++ b/kfet/tests/test_views.py
@@ -1,15 +1,21 @@
import json
+from datetime import datetime, timedelta
from decimal import Decimal
+from unittest import mock
-from django.contrib.auth.models import Group, Permission
+from django.contrib.auth.models import Group
from django.core.urlresolvers import reverse
from django.test import Client, TestCase
from django.utils import timezone
-from ..models import Account, Checkout, Operation, OperationGroup
-
+from ..config import kfet_config
+from ..models import (
+ Account, Article, ArticleCategory, Checkout, CheckoutStatement, Inventory,
+ InventoryArticle, Operation, OperationGroup, Order, OrderArticle, Supplier,
+ SupplierArticle, Transfer, TransferGroup,
+)
from .testcases import ViewTestCaseMixin
-from .utils import create_team, create_user
+from .utils import create_team, create_user, get_perms
class LoginGenericTeamViewTests(ViewTestCaseMixin, TestCase):
@@ -22,8 +28,8 @@ class LoginGenericTeamViewTests(ViewTestCaseMixin, TestCase):
def test_ok(self):
r = self.client.get(self.url)
self.assertEqual(r.status_code, 200)
- logged_in_username = r.wsgi_request.user.username
- self.assertEqual(logged_in_username, 'kfet_genericteam')
+ logged_in = r.wsgi_request.user
+ self.assertEqual(logged_in.username, 'kfet_genericteam')
class AccountListViewTests(ViewTestCaseMixin, TestCase):
@@ -74,16 +80,23 @@ class AccountCreateViewTests(ViewTestCaseMixin, TestCase):
url_name = 'kfet.account.create'
url_expected = '/k-fet/accounts/new'
+ http_methods = ['GET', 'POST']
+
auth_user = 'team'
auth_forbidden = [None, 'user']
+ post_data = {
+ 'trigramme': 'AAA',
+ 'username': 'plopplopplop',
+ 'first_name': 'first',
+ 'last_name': 'last',
+ 'email': 'email@domain.net',
+ }
+
@property
def users_extra(self):
return {
- 'team__add_account': create_team(
- 'team__add_account', '101',
- perms=['kfet.add_account'],
- ),
+ 'team1': create_team('team1', '101', perms=['kfet.add_account']),
}
def test_get_ok(self):
@@ -91,91 +104,69 @@ class AccountCreateViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(r.status_code, 200)
def test_post_ok(self):
- post_data = {
- 'trigramme': 'AAA',
+ client = Client()
+ client.login(username='team1', password='team1')
+
+ r = client.post(self.url, self.post_data)
+ self.assertRedirects(r, reverse('kfet.account.create'))
+
+ account = Account.objects.get(trigramme='AAA')
+
+ self.assertInstanceExpected(account, {
'username': 'plopplopplop',
'first_name': 'first',
'last_name': 'last',
- 'email': 'email@domain.net',
- }
-
- client = Client()
- client.login(
- username='team__add_account',
- password='team__add_account',
- )
- r = client.post(self.url, post_data)
-
- self.assertRedirects(r, self.url)
- a = Account.objects.get(trigramme='AAA')
- self.assertEqual(a.username, 'plopplopplop')
+ })
def test_post_forbidden(self):
- post_data = {
- 'trigramme': 'AAA',
- 'username': 'plopplopplop',
- 'first_name': 'first',
- 'last_name': 'last',
- 'email': 'email@domain.net',
- }
-
- # A team member (without kfet.add_account) is authenticated with
- # self.client.
- r = self.client.post(self.url, post_data)
-
- self.assertEqual(r.status_code, 200)
- with self.assertRaises(Account.DoesNotExist):
- Account.objects.get(trigramme='AAA')
+ r = self.client.post(self.url, self.post_data)
+ self.assertForbiddenKfet(r)
class AccountCreateAjaxViewTests(ViewTestCaseMixin, TestCase):
- urls_conf = [
- {
- 'name': 'kfet.account.create.fromuser',
- 'kwargs': {'username': 'user'},
- 'expected': '/k-fet/accounts/new/user/user',
+ urls_conf = [{
+ 'name': 'kfet.account.create.fromuser',
+ 'kwargs': {'username': 'user'},
+ 'expected': '/k-fet/accounts/new/user/user',
+ }, {
+ 'name': 'kfet.account.create.fromclipper',
+ 'kwargs': {
+ 'login_clipper': 'myclipper',
+ 'fullname': 'first last1 last2',
},
- {
- 'name': 'kfet.account.create.fromclipper',
- 'kwargs': {
- 'login_clipper': 'myclipper',
- 'fullname': 'first last1 last2',
- },
- 'expected': (
- '/k-fet/accounts/new/clipper/myclipper/first%20last1%20last2'
- ),
- },
- {
- 'name': 'kfet.account.create.empty',
- 'expected': '/k-fet/accounts/new/empty',
- },
- ]
+ 'expected': (
+ '/k-fet/accounts/new/clipper/myclipper/first%20last1%20last2'
+ ),
+ }, {
+ 'name': 'kfet.account.create.empty',
+ 'expected': '/k-fet/accounts/new/empty',
+ }]
auth_user = 'team'
auth_forbidden = [None, 'user']
def test_fromuser(self):
r = self.client.get(self.t_urls[0])
+ self.assertEqual(r.status_code, 200)
user = self.users['user']
- self.assertEqual(r.status_code, 200)
self.assertEqual(r.context['user_form'].instance, user)
self.assertEqual(r.context['cof_form'].instance, user.profile)
self.assertIn('account_form', r.context)
def test_fromclipper(self):
r = self.client.get(self.t_urls[1])
-
self.assertEqual(r.status_code, 200)
+
self.assertIn('user_form', r.context)
self.assertIn('cof_form', r.context)
self.assertIn('account_form', r.context)
def test_empty(self):
- r = self.client.get(self.t_urls[0])
-
+ r = self.client.get(self.t_urls[2])
self.assertEqual(r.status_code, 200)
+
self.assertIn('user_form', r.context)
self.assertIn('cof_form', r.context)
self.assertIn('account_form', r.context)
@@ -191,12 +182,11 @@ class AccountCreateAutocompleteViewTests(ViewTestCaseMixin, TestCase):
def test_ok(self):
r = self.client.get(self.url, {'q': 'first'})
self.assertEqual(r.status_code, 200)
- self.assertListEqual(list(r.context['users_notcof']), [])
- self.assertListEqual(list(r.context['users_cof']), [])
- self.assertListEqual(
- list(r.context['kfet']),
- [(self.accounts['user'], self.users['user'])],
- )
+ self.assertEqual(len(r.context['users_notcof']), 0)
+ self.assertEqual(len(r.context['users_cof']), 0)
+ self.assertSetEqual(set(r.context['kfet']), set([
+ (self.accounts['user'], self.users['user']),
+ ]))
class AccountSearchViewTests(ViewTestCaseMixin, TestCase):
@@ -209,10 +199,9 @@ class AccountSearchViewTests(ViewTestCaseMixin, TestCase):
def test_ok(self):
r = self.client.get(self.url, {'q': 'first'})
self.assertEqual(r.status_code, 200)
- self.assertListEqual(
- list(r.context['accounts']),
- [('000', 'first last')],
- )
+ self.assertSetEqual(set(r.context['accounts']), set([
+ ('000', 'first last'),
+ ]))
class AccountReadViewTests(ViewTestCaseMixin, TestCase):
@@ -281,43 +270,105 @@ class AccountUpdateViewTests(ViewTestCaseMixin, TestCase):
url_kwargs = {'trigramme': '001'}
url_expected = '/k-fet/accounts/001/edit'
+ http_methods = ['GET', 'POST']
+
auth_user = 'team'
auth_forbidden = [None, 'user']
+ post_data = {
+ # User
+ 'first_name': 'The first',
+ 'last_name': 'The last',
+ 'email': '',
+ # Group
+ 'groups[]': [],
+ # Account
+ 'trigramme': '051',
+ 'nickname': '',
+ 'promo': '',
+ # 'is_frozen': not checked
+ # Account password
+ 'pwd1': '',
+ 'pwd2': '',
+ }
+
@property
def users_extra(self):
return {
'user1': create_user('user1', '001'),
+ 'team1': create_team('team1', '101', perms=[
+ 'kfet.change_account',
+ ]),
}
- def test_ok(self):
+ def test_get_ok(self):
r = self.client.get(self.url)
self.assertEqual(r.status_code, 200)
- def test_ok_self(self):
+ def test_get_ok_self(self):
client = Client()
client.login(username='user1', password='user1')
r = client.get(self.url)
self.assertEqual(r.status_code, 200)
+ def test_post_ok(self):
+ client = Client()
+ client.login(username='team1', password='team1')
-class BaseAccountGroupViewTests(ViewTestCaseMixin):
- auth_user = 'team__manage_perms'
+ r = client.post(self.url, self.post_data)
+ self.assertRedirects(r, reverse('kfet.account.read', args=['051']))
+
+ self.accounts['user1'].refresh_from_db()
+ self.users['user1'].refresh_from_db()
+
+ self.assertInstanceExpected(self.accounts['user1'], {
+ 'first_name': 'The first',
+ 'last_name': 'The last',
+ 'trigramme': '051',
+ })
+
+ def test_post_ok_self(self):
+ client = Client()
+ client.login(username='user1', password='user1')
+
+ post_data = {
+ 'first_name': 'The first',
+ 'last_name': 'The last',
+ }
+
+ r = client.post(self.url, post_data)
+ self.assertRedirects(r, reverse('kfet.account.read', args=['001']))
+
+ self.accounts['user1'].refresh_from_db()
+ self.users['user1'].refresh_from_db()
+
+ self.assertInstanceExpected(self.accounts['user1'], {
+ 'first_name': 'The first',
+ 'last_name': 'The last',
+ })
+
+ def test_post_forbidden(self):
+ r = self.client.post(self.url, self.post_data)
+ self.assertForbiddenKfet(r)
+
+
+class AccountGroupListViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.account.group'
+ url_expected = '/k-fet/accounts/groups'
+
+ auth_user = 'team1'
auth_forbidden = [None, 'user', 'team']
@property
def users_extra(self):
return {
- 'team__manage_perms': create_team(
- 'team__manage_perms', '101',
- perms=['kfet.manage_perms'],
- ),
+ 'team1': create_team('team1', '101', perms=['kfet.manage_perms']),
}
-
-class AccountGroupListViewTests(BaseAccountGroupViewTests, TestCase):
- url_name = 'kfet.account.group'
- url_expected = '/k-fet/accounts/groups'
+ def setUp(self):
+ super().setUp()
+ self.group1 = Group.objects.create(name='K-Fêt - Group1')
+ self.group2 = Group.objects.create(name='K-Fêt - Group2')
def test_ok(self):
r = self.client.get(self.url)
@@ -325,51 +376,1893 @@ class AccountGroupListViewTests(BaseAccountGroupViewTests, TestCase):
self.assertQuerysetEqual(
r.context['groups'],
- Group.objects.filter(name__icontains='K-Fêt'),
+ map(repr, [self.group1, self.group2]),
ordered=False,
)
-class AccountGroupCreateViewTests(BaseAccountGroupViewTests, TestCase):
+class AccountGroupCreateViewTests(ViewTestCaseMixin, TestCase):
url_name = 'kfet.account.group.create'
url_expected = '/k-fet/accounts/groups/new'
- def test_ok(self):
- r = self.client.get(self.url)
- self.assertEqual(r.status_code, 200)
+ http_methods = ['GET', 'POST']
+ auth_user = 'team1'
+ auth_forbidden = [None, 'user', 'team']
-class AccountGroupUpdateViewTests(BaseAccountGroupViewTests, TestCase):
- url_name = 'kfet.account.group.update'
- url_kwargs = {'pk': 42}
- url_expected = '/k-fet/accounts/groups/42/edit'
+ @property
+ def users_extra(self):
+ return {
+ 'team1': create_team('team1', '101', perms=['kfet.manage_perms']),
+ }
+
+ @property
+ def post_data(self):
+ return {
+ 'name': 'The Group',
+ 'permissions': [
+ str(self.perms['kfet.is_team'].pk),
+ str(self.perms['kfet.manage_perms'].pk),
+ ],
+ }
def setUp(self):
super().setUp()
- self.group1 = Group.objects.create(pk=42, name='K-Fêt - Group')
- self.group1.permissions = [
- Permission.objects.get(
- content_type__app_label='kfet',
- codename='is_team',
- )
- ]
+ self.perms = get_perms(
+ 'kfet.is_team',
+ 'kfet.manage_perms',
+ )
def test_get_ok(self):
r = self.client.get(self.url)
self.assertEqual(r.status_code, 200)
def test_post_ok(self):
- post_data = {
- 'name': 'Group42',
- 'permissions': (
- self.group1.permissions
- .values_list('pk', flat=True)
- ),
- }
-
- r = self.client.post(self.url, post_data)
-
+ r = self.client.post(self.url, self.post_data)
self.assertRedirects(r, reverse('kfet.account.group'))
- self.group1.refresh_from_db()
- self.assertEqual(self.group1.name, 'K-Fêt Group42')
+ group = Group.objects.get(name='K-Fêt The Group')
+
+ self.assertQuerysetEqual(
+ group.permissions.all(),
+ map(repr, [
+ self.perms['kfet.is_team'],
+ self.perms['kfet.manage_perms'],
+ ]),
+ ordered=False,
+ )
+
+
+class AccountGroupUpdateViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.account.group.update'
+
+ http_methods = ['GET', 'POST']
+
+ auth_user = 'team1'
+ auth_forbidden = [None, 'user', 'team']
+
+ @property
+ def url_kwargs(self):
+ return {'pk': self.group.pk}
+
+ @property
+ def url_expected(self):
+ return '/k-fet/accounts/groups/{}/edit'.format(self.group.pk)
+
+ @property
+ def users_extra(self):
+ return {
+ 'team1': create_team('team1', '101', perms=['kfet.manage_perms']),
+ }
+
+ @property
+ def post_data(self):
+ return {
+ 'name': 'The Group',
+ 'permissions': [
+ str(self.perms['kfet.is_team'].pk),
+ str(self.perms['kfet.manage_perms'].pk),
+ ],
+ }
+
+ def setUp(self):
+ super().setUp()
+ self.perms = get_perms(
+ 'kfet.is_team',
+ 'kfet.manage_perms',
+ )
+ self.group = Group.objects.create(name='K-Fêt - Group')
+ self.group.permissions = self.perms.values()
+
+ def test_get_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+ def test_post_ok(self):
+ r = self.client.post(self.url, self.post_data)
+ self.assertRedirects(r, reverse('kfet.account.group'))
+
+ self.group.refresh_from_db()
+
+ self.assertEqual(self.group.name, 'K-Fêt The Group')
+ self.assertQuerysetEqual(
+ self.group.permissions.all(),
+ map(repr, [
+ self.perms['kfet.is_team'],
+ self.perms['kfet.manage_perms'],
+ ]),
+ ordered=False,
+ )
+
+
+class AccountNegativeListViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.account.negative'
+ url_expected = '/k-fet/accounts/negatives'
+
+ auth_user = 'team1'
+ auth_forbidden = [None, 'user', 'team']
+
+ @property
+ def users_extra(self):
+ return {
+ 'team1': create_team('team1', '101', perms=['kfet.view_negs']),
+ }
+
+ def setUp(self):
+ super().setUp()
+ account = self.accounts['user']
+ account.balance = -5
+ account.save()
+ account.update_negative()
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+ self.assertQuerysetEqual(
+ r.context['negatives'],
+ map(repr, [self.accounts['user'].negative]),
+ ordered=False,
+ )
+
+
+class AccountStatOperationListViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.account.stat.operation.list'
+ url_kwargs = {'trigramme': '001'}
+ url_expected = '/k-fet/accounts/001/stat/operations/list'
+
+ auth_user = 'user1'
+ auth_forbidden = [None, 'user', 'team']
+
+ @property
+ def users_extra(self):
+ return {'user1': create_user('user1', '001')}
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+ content = json.loads(r.content.decode('utf-8'))
+
+ base_url = reverse('kfet.account.stat.operation', args=['001'])
+
+ expected_stats = [{
+ 'label': 'Derniers mois',
+ 'url': {
+ 'path': base_url,
+ 'query': {
+ 'scale_n_steps': ['7'],
+ 'scale_name': ['month'],
+ 'types': ["['purchase']"],
+ 'scale_last': ['True'],
+ },
+ },
+ }, {
+ 'label': 'Dernières semaines',
+ 'url': {
+ 'path': base_url,
+ 'query': {
+ 'scale_n_steps': ['7'],
+ 'scale_name': ['week'],
+ 'types': ["['purchase']"],
+ 'scale_last': ['True'],
+ },
+ },
+ }, {
+ 'label': 'Derniers jours',
+ 'url': {
+ 'path': base_url,
+ 'query': {
+ 'scale_n_steps': ['7'],
+ 'scale_name': ['day'],
+ 'types': ["['purchase']"],
+ 'scale_last': ['True'],
+ },
+ },
+ }]
+
+ for stat, expected in zip(content['stats'], expected_stats):
+ expected_url = expected.pop('url')
+ self.assertUrlsEqual(stat['url'], expected_url)
+ self.assertDictContainsSubset(expected, stat)
+
+
+class AccountStatOperationViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.account.stat.operation'
+ url_kwargs = {'trigramme': '001'}
+ url_expected = '/k-fet/accounts/001/stat/operations'
+
+ auth_user = 'user1'
+ auth_forbidden = [None, 'user', 'team']
+
+ @property
+ def users_extra(self):
+ return {'user1': create_user('user1', '001')}
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+
+class AccountStatBalanceListViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.account.stat.balance.list'
+ url_kwargs = {'trigramme': '001'}
+ url_expected = '/k-fet/accounts/001/stat/balance/list'
+
+ auth_user = 'user1'
+ auth_forbidden = [None, 'user', 'team']
+
+ @property
+ def users_extra(self):
+ return {'user1': create_user('user1', '001')}
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+ content = json.loads(r.content.decode('utf-8'))
+
+ base_url = reverse('kfet.account.stat.balance', args=['001'])
+
+ expected_stats = [{
+ 'label': 'Tout le temps',
+ 'url': base_url,
+ }, {
+ 'label': '1 an',
+ 'url': {
+ 'path': base_url,
+ 'query': {'last_days': ['365']},
+ },
+ }, {
+ 'label': '6 mois',
+ 'url': {
+ 'path': base_url,
+ 'query': {'last_days': ['183']},
+ },
+ }, {
+ 'label': '3 mois',
+ 'url': {
+ 'path': base_url,
+ 'query': {'last_days': ['90']},
+ },
+ }, {
+ 'label': '30 jours',
+ 'url': {
+ 'path': base_url,
+ 'query': {'last_days': ['30']},
+ },
+ }]
+
+ for stat, expected in zip(content['stats'], expected_stats):
+ expected_url = expected.pop('url')
+ self.assertUrlsEqual(stat['url'], expected_url)
+ self.assertDictContainsSubset(expected, stat)
+
+
+class AccountStatBalanceViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.account.stat.balance'
+ url_kwargs = {'trigramme': '001'}
+ url_expected = '/k-fet/accounts/001/stat/balance'
+
+ auth_user = 'user1'
+ auth_forbidden = [None, 'user', 'team']
+
+ @property
+ def users_extra(self):
+ return {'user1': create_user('user1', '001')}
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+
+class CheckoutListViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.checkout'
+ url_expected = '/k-fet/checkouts/'
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ def setUp(self):
+ super().setUp()
+ self.checkout1 = Checkout.objects.create(
+ name='Checkout 1',
+ created_by=self.accounts['team'],
+ valid_from=self.now,
+ valid_to=self.now + timedelta(days=5),
+ )
+ self.checkout2 = Checkout.objects.create(
+ name='Checkout 2',
+ created_by=self.accounts['team'],
+ valid_from=self.now + timedelta(days=10),
+ valid_to=self.now + timedelta(days=15),
+ )
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+ self.assertQuerysetEqual(
+ r.context['checkouts'],
+ map(repr, [self.checkout1, self.checkout2]),
+ ordered=False,
+ )
+
+
+class CheckoutCreateViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.checkout.create'
+ url_expected = '/k-fet/checkouts/new'
+
+ http_methods = ['GET', 'POST']
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ post_data = {
+ 'name': 'Checkout',
+ 'valid_from': '2017-10-08 17:45:00',
+ 'valid_to': '2017-11-08 16:00:00',
+ 'balance': '3.14',
+ # 'is_protected': not checked
+ }
+
+ @property
+ def users_extra(self):
+ return {
+ 'team1': create_team('team1', '101', perms=['kfet.add_checkout']),
+ }
+
+ def test_get_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+ def test_post_ok(self):
+ client = Client()
+ client.login(username='team1', password='team1')
+
+ r = client.post(self.url, self.post_data)
+
+ checkout = Checkout.objects.get(name='Checkout')
+ self.assertRedirects(r, checkout.get_absolute_url())
+
+ self.assertInstanceExpected(checkout, {
+ 'name': 'Checkout',
+ 'valid_from': timezone.make_aware(datetime(2017, 10, 8, 17, 45)),
+ 'valid_to': timezone.make_aware(datetime(2017, 11, 8, 16, 00)),
+ 'balance': Decimal('3.14'),
+ 'is_protected': False,
+ })
+
+ def test_post_forbidden(self):
+ r = self.client.post(self.url, self.post_data)
+ self.assertForbiddenKfet(r)
+
+
+class CheckoutReadViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.checkout.read'
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ @property
+ def url_kwargs(self):
+ return {'pk': self.checkout.pk}
+
+ @property
+ def url_expected(self):
+ return '/k-fet/checkouts/{}'.format(self.checkout.pk)
+
+ def setUp(self):
+ super().setUp()
+ self.checkout = Checkout.objects.create(
+ name='Checkout',
+ created_by=self.accounts['team'],
+ valid_from=self.now,
+ valid_to=self.now + timedelta(days=5),
+ )
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.context['checkout'], self.checkout)
+
+
+class CheckoutUpdateViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.checkout.update'
+
+ http_methods = ['GET', 'POST']
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ post_data = {
+ 'name': 'Checkout updated',
+ 'valid_from': '2018-01-01 08:00:00',
+ 'valid_to': '2018-07-01 16:00:00',
+ }
+
+ @property
+ def url_kwargs(self):
+ return {'pk': self.checkout.pk}
+
+ @property
+ def url_expected(self):
+ return '/k-fet/checkouts/{}/edit'.format(self.checkout.pk)
+
+ @property
+ def users_extra(self):
+ return {
+ 'team1': create_team('team1', '101', perms=[
+ 'kfet.change_checkout',
+ ]),
+ }
+
+ def setUp(self):
+ super().setUp()
+ self.checkout = Checkout.objects.create(
+ name='Checkout',
+ valid_from=self.now,
+ valid_to=self.now + timedelta(days=5),
+ balance='3.14',
+ is_protected=False,
+ created_by=self.accounts['team'],
+ )
+
+ def test_get_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+ def test_post_ok(self):
+ client = Client()
+ client.login(username='team1', password='team1')
+
+ r = client.post(self.url, self.post_data)
+ self.assertRedirects(r, self.checkout.get_absolute_url())
+
+ self.checkout.refresh_from_db()
+
+ self.assertInstanceExpected(self.checkout, {
+ 'name': 'Checkout updated',
+ 'valid_from': timezone.make_aware(datetime(2018, 1, 1, 8, 0, 0)),
+ 'valid_to': timezone.make_aware(datetime(2018, 7, 1, 16, 0, 0)),
+ })
+
+ def test_post_forbidden(self):
+ r = self.client.post(self.url, self.post_data)
+ self.assertForbiddenKfet(r)
+
+
+class CheckoutStatementListViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.checkoutstatement'
+ url_expected = '/k-fet/checkouts/statements/'
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ def setUp(self):
+ super().setUp()
+ self.checkout1 = Checkout.objects.create(
+ created_by=self.accounts['team'],
+ name='Checkout 1',
+ valid_from=self.now,
+ valid_to=self.now + timedelta(days=5),
+ )
+ self.checkout2 = Checkout.objects.create(
+ created_by=self.accounts['team'],
+ name='Checkout 2',
+ valid_from=self.now + timedelta(days=10),
+ valid_to=self.now + timedelta(days=15),
+ )
+ self.statement1 = CheckoutStatement.objects.create(
+ checkout=self.checkout1,
+ by=self.accounts['team'],
+ balance_old=5,
+ balance_new=0,
+ amount_taken=5,
+ )
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+ expected_statements = (
+ list(self.checkout1.statements.all()) +
+ list(self.checkout2.statements.all())
+ )
+
+ self.assertQuerysetEqual(
+ r.context['checkoutstatements'],
+ map(repr, expected_statements),
+ )
+
+
+class CheckoutStatementCreateViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.checkoutstatement.create'
+
+ http_methods = ['GET', 'POST']
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ post_data = {
+ # Let
+ 'balance_001': 0, 'balance_002': 0, 'balance_005': 0,
+ 'balance_01': 0, 'balance_02': 0, 'balance_05': 0,
+ 'balance_1': 1, 'balance_2': 0, 'balance_5': 0,
+ 'balance_10': 1, 'balance_20': 0, 'balance_50': 0,
+ 'balance_100': 1, 'balance_200': 0, 'balance_500': 0,
+ # Taken
+ 'taken_001': 0, 'taken_002': 0, 'taken_005': 0,
+ 'taken_01': 0, 'taken_02': 0, 'taken_05': 0,
+ 'taken_1': 2, 'taken_2': 0, 'taken_5': 0,
+ 'taken_10': 2, 'taken_20': 0, 'taken_50': 0,
+ 'taken_100': 2, 'taken_200': 0, 'taken_500': 0,
+ 'taken_cheque': 0,
+ # 'not_count': not checked
+ }
+
+ @property
+ def url_kwargs(self):
+ return {'pk_checkout': self.checkout.pk}
+
+ @property
+ def url_expected(self):
+ return '/k-fet/checkouts/{}/statements/add'.format(self.checkout.pk)
+
+ @property
+ def users_extra(self):
+ return {
+ 'team1': create_team('team1', '001', perms=[
+ 'kfet.add_checkoutstatement',
+ ]),
+ }
+
+ def setUp(self):
+ super().setUp()
+ self.checkout = Checkout.objects.create(
+ name='Checkout',
+ created_by=self.accounts['team'],
+ balance=5,
+ valid_from=self.now,
+ valid_to=self.now + timedelta(days=5),
+ )
+
+ def test_get_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+ @mock.patch('django.utils.timezone.now')
+ def test_post_ok(self, mock_now):
+ self.now += timedelta(days=2)
+ mock_now.return_value = self.now
+
+ client = Client()
+ client.login(username='team1', password='team1')
+
+ r = client.post(self.url, self.post_data)
+ self.assertRedirects(r, self.checkout.get_absolute_url())
+
+ statement = CheckoutStatement.objects.get(at=self.now)
+
+ self.assertInstanceExpected(statement, {
+ 'by': self.accounts['team1'],
+ 'checkout': self.checkout,
+ 'balance_old': Decimal('5'),
+ 'balance_new': Decimal('111'),
+ 'amount_taken': Decimal('222'),
+ 'amount_error': Decimal('328'),
+ 'at': self.now,
+ 'not_count': False,
+ })
+
+ def test_post_forbidden(self):
+ r = self.client.post(self.url, self.post_data)
+ self.assertForbiddenKfet(r)
+
+
+class CheckoutStatementUpdateViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.checkoutstatement.update'
+
+ http_methods = ['GET', 'POST']
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ post_data = {
+ 'amount_taken': 3,
+ 'amount_error': 2,
+ 'balance_old': 8,
+ 'balance_new': 5,
+ # Taken
+ 'taken_001': 0, 'taken_002': 0, 'taken_005': 0,
+ 'taken_01': 0, 'taken_02': 0, 'taken_05': 0,
+ 'taken_1': 1, 'taken_2': 1, 'taken_5': 0,
+ 'taken_10': 0, 'taken_20': 0, 'taken_50': 0,
+ 'taken_100': 0, 'taken_200': 0, 'taken_500': 0,
+ 'taken_cheque': 0,
+ }
+
+ @property
+ def url_kwargs(self):
+ return {
+ 'pk_checkout': self.checkout.pk,
+ 'pk': self.statement.pk,
+ }
+
+ @property
+ def url_expected(self):
+ return '/k-fet/checkouts/{pk_checkout}/statements/{pk}/edit'.format(
+ pk_checkout=self.checkout.pk,
+ pk=self.statement.pk,
+ )
+
+ @property
+ def users_extra(self):
+ return {
+ 'team1': create_team('team1', '101', perms=[
+ 'kfet.change_checkoutstatement',
+ ]),
+ }
+
+ def setUp(self):
+ super().setUp()
+ self.checkout = Checkout.objects.create(
+ name='Checkout',
+ created_by=self.accounts['team'],
+ balance=5,
+ valid_from=self.now,
+ valid_to=self.now + timedelta(days=5),
+ )
+ self.statement = CheckoutStatement.objects.create(
+ by=self.accounts['team'],
+ checkout=self.checkout,
+ balance_new=5,
+ balance_old=8,
+ amount_error=2,
+ amount_taken=5,
+ )
+
+ def test_get_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+ @mock.patch('django.utils.timezone.now')
+ def test_post_ok(self, mock_now):
+ self.now += timedelta(days=2)
+ mock_now.return_value = self.now
+
+ client = Client()
+ client.login(username='team1', password='team1')
+
+ r = client.post(self.url, self.post_data)
+ self.assertRedirects(r, self.checkout.get_absolute_url())
+
+ self.statement.refresh_from_db()
+
+ self.assertInstanceExpected(self.statement, {
+ 'taken_1': 1,
+ 'taken_2': 1,
+ 'balance_new': 5,
+ 'balance_old': 8,
+ 'amount_error': 0,
+ 'amount_taken': 3,
+ })
+
+ def test_post_forbidden(self):
+ r = self.client.post(self.url, self.post_data)
+ self.assertForbiddenKfet(r)
+
+
+class ArticleCategoryListViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.category'
+ url_expected = '/k-fet/categories/'
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ def setUp(self):
+ super().setUp()
+ self.category1 = ArticleCategory.objects.create(name='Category 1')
+ self.category2 = ArticleCategory.objects.create(name='Category 2')
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+ self.assertQuerysetEqual(
+ r.context['categories'],
+ map(repr, [self.category1, self.category2]),
+ )
+
+
+class ArticleCategoryUpdateViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.category.update'
+
+ http_methods = ['GET', 'POST']
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ @property
+ def url_kwargs(self):
+ return {'pk': self.category.pk}
+
+ @property
+ def url_expected(self):
+ return '/k-fet/categories/{}/edit'.format(self.category.pk)
+
+ @property
+ def users_extra(self):
+ return {
+ 'team1': create_team('team1', '101', perms=[
+ 'kfet.change_articlecategory',
+ ]),
+ }
+
+ @property
+ def post_data(self):
+ return {
+ 'name': 'The Category',
+ # 'has_addcost': not checked
+ }
+
+ def setUp(self):
+ super().setUp()
+ self.category = ArticleCategory.objects.create(name='Category')
+
+ def test_get_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+ def test_post_ok(self):
+ client = Client()
+ client.login(username='team1', password='team1')
+
+ r = client.post(self.url, self.post_data)
+ self.assertRedirects(r, reverse('kfet.category'))
+
+ self.category.refresh_from_db()
+
+ self.assertInstanceExpected(self.category, {
+ 'name': 'The Category',
+ 'has_addcost': False,
+ })
+
+ def test_post_forbidden(self):
+ r = self.client.post(self.url, self.post_data)
+ self.assertForbiddenKfet(r)
+
+
+class ArticleListViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.article'
+ url_expected = '/k-fet/articles/'
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ def setUp(self):
+ super().setUp()
+ category = ArticleCategory.objects.create(name='Category')
+ self.article1 = Article.objects.create(
+ name='Article 1',
+ category=category,
+ )
+ self.article2 = Article.objects.create(
+ name='Article 2',
+ category=category,
+ )
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+ self.assertQuerysetEqual(
+ r.context['articles'],
+ map(repr, [self.article1, self.article2]),
+ )
+
+
+class ArticleCreateViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.article.create'
+ url_expected = '/k-fet/articles/new'
+
+ http_methods = ['GET', 'POST']
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ @property
+ def users_extra(self):
+ return {
+ 'team1': create_team('team1', '101', perms=['kfet.add_article']),
+ }
+
+ @property
+ def post_data(self):
+ return {
+ 'name': 'Article',
+ 'category': self.category.pk,
+ 'stock': 5,
+ 'price': '2.5',
+ }
+
+ def setUp(self):
+ super().setUp()
+ self.category = ArticleCategory.objects.create(name='Category')
+
+ def test_get_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+ def test_post_ok(self):
+ client = Client()
+ client.login(username='team1', password='team1')
+
+ r = client.post(self.url, self.post_data)
+
+ article = Article.objects.get(name='Article')
+
+ self.assertRedirects(r, article.get_absolute_url())
+
+ self.assertInstanceExpected(article, {
+ 'name': 'Article',
+ 'category': self.category,
+ })
+
+ def test_post_forbidden(self):
+ r = self.client.post(self.url, self.post_data)
+ self.assertForbiddenKfet(r)
+
+
+class ArticleReadViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.article.read'
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ @property
+ def url_kwargs(self):
+ return {'pk': self.article.pk}
+
+ @property
+ def url_expected(self):
+ return '/k-fet/articles/{}'.format(self.article.pk)
+
+ def setUp(self):
+ super().setUp()
+ self.article = Article.objects.create(
+ name='Article',
+ category=ArticleCategory.objects.create(name='Category'),
+ stock=5,
+ price=Decimal('2.5'),
+ )
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.context['article'], self.article)
+
+
+class ArticleUpdateViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.article.update'
+
+ http_methods = ['GET', 'POST']
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ @property
+ def url_kwargs(self):
+ return {'pk': self.article.pk}
+
+ @property
+ def url_expected(self):
+ return '/k-fet/articles/{}/edit'.format(self.article.pk)
+
+ @property
+ def users_extra(self):
+ return {
+ 'team1': create_team('team1', '101', perms=[
+ 'kfet.change_article',
+ ]),
+ }
+
+ @property
+ def post_data(self):
+ return {
+ 'name': 'The Article',
+ 'category': self.article.category.pk,
+ 'is_sold': '1',
+ 'price': '3.5',
+ 'box_type': 'carton',
+ # 'hidden': not checked
+ }
+
+ def setUp(self):
+ super().setUp()
+ self.category = ArticleCategory.objects.create(name='Category')
+ self.article = Article.objects.create(
+ name='Article',
+ category=self.category,
+ stock=5,
+ price=Decimal('2.5'),
+ )
+
+ def test_get_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+ def test_post_ok(self):
+ client = Client()
+ client.login(username='team1', password='team1')
+
+ r = client.post(self.url, self.post_data)
+
+ self.assertRedirects(r, self.article.get_absolute_url())
+
+ self.article.refresh_from_db()
+
+ self.assertInstanceExpected(self.article, {
+ 'name': 'The Article',
+ 'price': Decimal('3.5'),
+ })
+
+ def test_post_forbidden(self):
+ r = self.client.post(self.url, self.post_data)
+ self.assertForbiddenKfet(r)
+
+
+class ArticleStatSalesListViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.article.stat.sales.list'
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ @property
+ def url_kwargs(self):
+ return {'pk': self.article.pk}
+
+ @property
+ def url_expected(self):
+ return '/k-fet/articles/{}/stat/sales/list'.format(self.article.pk)
+
+ def setUp(self):
+ super().setUp()
+ self.article = Article.objects.create(
+ name='Article',
+ category=ArticleCategory.objects.create(name='Category'),
+ )
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+ content = json.loads(r.content.decode('utf-8'))
+
+ base_url = reverse('kfet.article.stat.sales', args=[self.article.pk])
+
+ expected_stats = [
+ {
+ 'label': 'Derniers mois',
+ 'url': {
+ 'path': base_url,
+ 'query': {
+ 'scale_n_steps': ['7'],
+ 'scale_name': ['month'],
+ 'scale_last': ['True'],
+ },
+ },
+ },
+ {
+ 'label': 'Dernières semaines',
+ 'url': {
+ 'path': base_url,
+ 'query': {
+ 'scale_n_steps': ['7'],
+ 'scale_name': ['week'],
+ 'scale_last': ['True'],
+ },
+ },
+ },
+ {
+ 'label': 'Derniers jours',
+ 'url': {
+ 'path': base_url,
+ 'query': {
+ 'scale_n_steps': ['7'],
+ 'scale_name': ['day'],
+ 'scale_last': ['True'],
+ },
+ },
+ },
+ ]
+
+ for stat, expected in zip(content['stats'], expected_stats):
+ expected_url = expected.pop('url')
+ self.assertUrlsEqual(stat['url'], expected_url)
+ self.assertDictContainsSubset(expected, stat)
+
+
+class ArticleStatSalesViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.article.stat.sales'
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ @property
+ def url_kwargs(self):
+ return {'pk': self.article.pk}
+
+ @property
+ def url_expected(self):
+ return '/k-fet/articles/{}/stat/sales'.format(self.article.pk)
+
+ def setUp(self):
+ super().setUp()
+ self.article = Article.objects.create(
+ name='Article',
+ category=ArticleCategory.objects.create(name='Category'),
+ )
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+
+class KPsulViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.kpsul'
+ url_expected = '/k-fet/k-psul/'
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+
+class KPsulCheckoutDataViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.kpsul.checkout_data'
+ url_expected = '/k-fet/k-psul/checkout_data'
+
+ http_methods = ['POST']
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ def setUp(self):
+ super().setUp()
+ self.checkout = Checkout.objects.create(
+ name='Checkout',
+ balance=Decimal('10'),
+ created_by=self.accounts['team'],
+ valid_from=self.now,
+ valid_to=self.now + timedelta(days=5),
+ )
+
+ def test_ok(self):
+ r = self.client.post(self.url, {'pk': self.checkout.pk})
+ self.assertEqual(r.status_code, 200)
+
+ content = json.loads(r.content.decode('utf-8'))
+
+ expected = {
+ 'name': 'Checkout',
+ 'balance': '10.00',
+ }
+
+ self.assertDictContainsSubset(expected, content)
+
+ self.assertSetEqual(set(content.keys()), set([
+ 'balance', 'id', 'name', 'valid_from', 'valid_to',
+ 'last_statement_at', 'last_statement_balance',
+ 'last_statement_by_first_name', 'last_statement_by_last_name',
+ 'last_statement_by_trigramme',
+ ]))
+
+
+class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.kpsul.perform_operations'
+ url_expected = '/k-fet/k-psul/perform_operations'
+
+ http_methods = ['POST']
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ def test_ok(self):
+ pass
+
+
+class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.kpsul.cancel_operations'
+ url_expected = '/k-fet/k-psul/cancel_operations'
+
+ http_methods = ['POST']
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ def test_ok(self):
+ pass
+
+
+class KPsulArticlesData(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.kpsul.articles_data'
+ url_expected = '/k-fet/k-psul/articles_data'
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ def setUp(self):
+ super().setUp()
+ category = ArticleCategory.objects.create(name='Catégorie')
+ self.article1 = Article.objects.create(
+ category=category,
+ name='Article 1',
+ )
+ self.article2 = Article.objects.create(
+ category=category,
+ name='Article 2',
+ price=Decimal('2.5'),
+ )
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+ content = json.loads(r.content.decode('utf-8'))
+
+ articles = content['articles']
+
+ expected_list = [{
+ 'category__name': 'Catégorie',
+ 'name': 'Article 1',
+ 'price': '0.00',
+ }, {
+ 'category__name': 'Catégorie',
+ 'name': 'Article 2',
+ 'price': '2.50',
+ }]
+
+ for expected, article in zip(expected_list, articles):
+ self.assertDictContainsSubset(expected, article)
+ self.assertSetEqual(set(article.keys()), set([
+ 'id', 'name', 'price', 'stock',
+ 'category_id', 'category__name', 'category__has_addcost',
+ ]))
+
+
+class KPsulUpdateAddcost(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.kpsul.update_addcost'
+ url_expected = '/k-fet/k-psul/update_addcost'
+
+ http_methods = ['POST']
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ post_data = {
+ 'trigramme': '000',
+ 'amount': '0.5',
+ }
+
+ @property
+ def users_extra(self):
+ return {
+ 'team1': create_team('team1', '101', perms=[
+ 'kfet.manage_addcosts',
+ ]),
+ }
+
+ def test_ok(self):
+ client = Client()
+ client.login(username='team1', password='team1')
+
+ r = client.post(self.url, self.post_data)
+ self.assertEqual(r.status_code, 200)
+
+ self.assertEqual(
+ kfet_config.addcost_for,
+ Account.objects.get(trigramme='000'),
+ )
+ self.assertEqual(kfet_config.addcost_amount, Decimal('0.5'))
+
+ def test_post_forbidden(self):
+ r = self.client.post(self.url, self.post_data)
+ self.assertForbidden(r)
+
+
+class KPsulGetSettings(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.kpsul.get_settings'
+ url_expected = '/k-fet/k-psul/get_settings'
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+
+class HistoryJSONViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.history.json'
+ url_expected = '/k-fet/history.json'
+
+ auth_user = 'user'
+ auth_forbidden = [None]
+
+ def test_ok(self):
+ r = self.client.post(self.url)
+ self.assertEqual(r.status_code, 200)
+
+
+class AccountReadJSONViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.account.read.json'
+ url_expected = '/k-fet/accounts/read.json'
+
+ http_methods = ['POST']
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ def test_ok(self):
+ r = self.client.post(self.url, {'trigramme': '000'})
+ self.assertEqual(r.status_code, 200)
+
+ content = json.loads(r.content.decode('utf-8'))
+
+ expected = {
+ 'name': 'first last',
+ 'trigramme': '000',
+ 'balance': '0.00',
+ }
+ self.assertDictContainsSubset(expected, content)
+
+ self.assertSetEqual(set(content.keys()), set([
+ 'balance', 'departement', 'email', 'id', 'is_cof', 'is_frozen',
+ 'name', 'nickname', 'promo', 'trigramme',
+ ]))
+
+
+class SettingsListViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.settings'
+ url_expected = '/k-fet/settings/'
+
+ auth_user = 'team1'
+ auth_forbidden = [None, 'user', 'team']
+
+ @property
+ def users_extra(self):
+ return {
+ 'team1': create_team('team1', '101', perms=[
+ 'kfet.change_settings',
+ ]),
+ }
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+
+class SettingsUpdateViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.settings.update'
+ url_expected = '/k-fet/settings/edit'
+
+ http_methods = ['GET', 'POST']
+
+ auth_user = 'team1'
+ auth_forbidden = [None, 'user', 'team']
+
+ @property
+ def post_data(self):
+ return {
+ 'kfet_reduction_cof': '25',
+ 'kfet_addcost_amount': '0.5',
+ 'kfet_addcost_for': self.accounts['user'].pk,
+ 'kfet_overdraft_duration': '2 00:00:00',
+ 'kfet_overdraft_amount': '25',
+ 'kfet_cancel_duration': '00:20:00',
+ }
+
+ @property
+ def users_extra(self):
+ return {
+ 'team1': create_team('team1', '101', perms=[
+ 'kfet.change_settings',
+ ]),
+ }
+
+ def test_get_ok(self):
+ r = self.client.get(self.url)
+ self.assertequal(r.status_code, 200)
+
+ def test_post_ok(self):
+ r = self.client.post(self.url, self.post_data)
+ self.assertRedirects(r, reverse('kfet.settings'))
+
+ self.assertDictEqual(dict(kfet_config.list()), {
+ 'reduction_cof': Decimal('25'),
+ 'addcost_amount': Decimal('0.5'),
+ 'addcost_for': self.accounts['user'],
+ 'overdraft_duration': timedelta(day=2),
+ 'overdraft_amount': Decimal('25'),
+ 'kfet_cancel_duration': timedelta(minutes=20),
+ })
+
+
+class TransferListViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.transfers'
+ url_expected = '/k-fet/transfers/'
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+
+class TransferCreateViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.transfers.create'
+ url_expected = '/k-fet/transfers/new'
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+
+class TransferPerformViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.transfers.perform'
+ url_expected = '/k-fet/transfers/perform'
+
+ http_methods = ['POST']
+
+ auth_user = 'team1'
+ auth_forbidden = [None, 'user', 'team']
+
+ @property
+ def users_extra(self):
+ return {
+ 'team1': create_team('team1', '101', perms=[
+ # Required
+ 'kfet.add_transfer',
+ # Convenience
+ 'kfet.perform_negative_operations',
+ ]),
+ }
+
+ @property
+ def post_data(self):
+ return {
+ # General
+ 'comment': '',
+ # Formset management
+ 'form-TOTAL_FORMS': '10',
+ 'form-INITIAL_FORMS': '0',
+ 'form-MIN_NUM_FORMS': '1',
+ 'form-MAX_NUM_FORMS': '1000',
+ # Transfer 1
+ 'form-0-from_acc': str(self.accounts['user'].pk),
+ 'form-0-to_acc': str(self.accounts['team'].pk),
+ 'form-0-amount': '3.5',
+ # Transfer 2
+ 'form-1-from_acc': str(self.accounts['team'].pk),
+ 'form-1-to_acc': str(self.accounts['team1'].pk),
+ 'form-1-amount': '2.4',
+ }
+
+ def test_ok(self):
+ r = self.client.post(self.url, self.post_data)
+ self.assertEqual(r.status_code, 200)
+
+ user = self.accounts['user']
+ user.refresh_from_db()
+ self.assertEqual(user.balance, Decimal('-3.5'))
+
+ team = self.accounts['team']
+ team.refresh_from_db()
+ self.assertEqual(team.balance, Decimal('1.1'))
+
+ team1 = self.accounts['team1']
+ team1.refresh_from_db()
+ self.assertEqual(team1.balance, Decimal('2.4'))
+
+
+class TransferCancelViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.transfers.cancel'
+ url_expected = '/k-fet/transfers/cancel'
+
+ http_methods = ['POST']
+
+ auth_user = 'team1'
+ auth_forbidden = [None, 'user', 'team']
+
+ @property
+ def users_extra(self):
+ return {
+ 'team1': create_team('team1', '101', perms=[
+ # Convenience
+ 'kfet.perform_negative_operations',
+ ]),
+ }
+
+ @property
+ def post_data(self):
+ return {
+ 'transfers[]': [self.transfer1.pk, self.transfer2.pk],
+ }
+
+ def setUp(self):
+ super().setUp()
+ group = TransferGroup.objects.create()
+ self.transfer1 = Transfer.objects.create(
+ group=group,
+ from_acc=self.accounts['user'],
+ to_acc=self.accounts['team'],
+ amount='3.5',
+ )
+ self.transfer2 = Transfer.objects.create(
+ group=group,
+ from_acc=self.accounts['team'],
+ to_acc=self.accounts['root'],
+ amount='2.4',
+ )
+
+ def test_ok(self):
+ r = self.client.post(self.url, self.post_data)
+ self.assertEqual(r.status_code, 200)
+
+ user = self.accounts['user']
+ user.refresh_from_db()
+ self.assertEqual(user.balance, Decimal('3.5'))
+
+ team = self.accounts['team']
+ team.refresh_from_db()
+ self.assertEqual(team.balance, Decimal('-1.1'))
+
+ root = self.accounts['root']
+ root.refresh_from_db()
+ self.assertEqual(root.balance, Decimal('-2.4'))
+
+
+class InventoryListViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.inventory'
+ url_expected = '/k-fet/inventaires/'
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ def setUp(self):
+ super().setUp()
+ self.inventory = Inventory.objects.create(
+ by=self.accounts['team'],
+ )
+ category = ArticleCategory.objects.create(name='Category')
+ article = Article.objects.create(
+ name='Article',
+ category=category,
+ )
+ InventoryArticle.objects.create(
+ inventory=self.inventory,
+ article=article,
+ stock_old=5,
+ stock_new=0,
+ )
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+ inventories = r.context['inventories']
+ self.assertQuerysetEqual(
+ inventories,
+ map(repr, [self.inventory]),
+ )
+
+
+class InventoryCreateViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.inventory.create'
+ url_expected = '/k-fet/inventaires/new'
+
+ http_methods = ['GET', 'POST']
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ @property
+ def users_extra(self):
+ return {
+ 'team1': create_team('team1', '101', perms=[
+ 'kfet.add_inventory',
+ ]),
+ }
+
+ @property
+ def post_data(self):
+ return {
+ # Formset management
+ 'form-TOTAL_FORMS': '2',
+ 'form-INITIAL_FORMS': '2',
+ 'form-MIN_NUM_FORMS': '0',
+ 'form-MAX_NUM_FORMS': '1000',
+ # Article 1
+ 'form-0-article': str(self.article1.pk),
+ 'form-0-stock_new': '5',
+ # Article 2
+ 'form-1-article': str(self.article2.pk),
+ 'form-1-stock_new': '10',
+ }
+
+ def setUp(self):
+ super().setUp()
+ category = ArticleCategory.objects.create(name='Category')
+ self.article1 = Article.objects.create(
+ category=category,
+ name='Article 1',
+ )
+ self.article2 = Article.objects.create(
+ category=category,
+ name='Article 2',
+ )
+
+ def test_get_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+ def test_post_ok(self):
+ client = Client()
+ client.login(username='team1', password='team1')
+
+ r = client.post(self.url, self.post_data)
+ self.assertRedirects(r, reverse('kfet.inventory'))
+
+ def test_post_forbidden(self):
+ r = self.client.post(self.url, self.post_data)
+ self.assertForbiddenKfet(r)
+
+
+class InventoryReadViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.inventory.read'
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ @property
+ def url_kwargs(self):
+ return {'pk': self.inventory.pk}
+
+ @property
+ def url_expected(self):
+ return '/k-fet/inventaires/{}'.format(self.inventory.pk)
+
+ def setUp(self):
+ super().setUp()
+ self.inventory = Inventory.objects.create(
+ by=self.accounts['team'],
+ )
+ category = ArticleCategory.objects.create(name='Category')
+ article = Article.objects.create(
+ name='Article',
+ category=category,
+ )
+ InventoryArticle.objects.create(
+ inventory=self.inventory,
+ article=article,
+ stock_old=5,
+ stock_new=0,
+ )
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+
+class OrderListViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.order'
+ url_expected = '/k-fet/orders/'
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ def setUp(self):
+ super().setUp()
+ category = ArticleCategory.objects.create(name='Category')
+ article = Article.objects.create(name='Article', category=category)
+
+ supplier = Supplier.objects.create(name='Supplier')
+ SupplierArticle.objects.create(supplier=supplier, article=article)
+
+ self.order = Order.objects.create(supplier=supplier)
+ OrderArticle.objects.create(
+ order=self.order,
+ article=article,
+ quantity_ordered=24,
+ )
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+ orders = r.context['orders']
+ self.assertQuerysetEqual(
+ orders,
+ map(repr, [self.order]),
+ )
+
+
+class OrderReadViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.order.read'
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ @property
+ def url_kwargs(self):
+ return {'pk': self.order.pk}
+
+ @property
+ def url_expected(self):
+ return '/k-fet/orders/{}'.format(self.order.pk)
+
+ def setUp(self):
+ super().setUp()
+ category = ArticleCategory.objects.create(name='Category')
+ article = Article.objects.create(name='Article', category=category)
+
+ supplier = Supplier.objects.create(name='Supplier')
+ SupplierArticle.objects.create(supplier=supplier, article=article)
+
+ self.order = Order.objects.create(supplier=supplier)
+ OrderArticle.objects.create(
+ order=self.order,
+ article=article,
+ quantity_ordered=24,
+ )
+
+ def test_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+
+class SupplierUpdateViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.order.supplier.update'
+
+ http_methods = ['GET', 'POST']
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ @property
+ def url_kwargs(self):
+ return {'pk': self.supplier.pk}
+
+ @property
+ def url_expected(self):
+ return '/k-fet/orders/suppliers/{}/edit'.format(self.supplier.pk)
+
+ @property
+ def users_extra(self):
+ return {
+ 'team1': create_team('team1', '101', perms=[
+ 'kfet.change_supplier',
+ ]),
+ }
+
+ @property
+ def post_data(self):
+ return {
+ 'name': 'The Supplier',
+ 'phone': '',
+ 'comment': '',
+ 'address': '',
+ 'email': '',
+ }
+
+ def setUp(self):
+ super().setUp()
+ self.supplier = Supplier.objects.create(name='Supplier')
+
+ def test_get_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+ def test_post_ok(self):
+ client = Client()
+ client.login(username='team1', password='team1')
+
+ r = client.post(self.url, self.post_data)
+ self.assertRedirects(r, reverse('kfet.order'))
+
+ self.supplier.refresh_from_db()
+ self.assertEqual(self.supplier.name, 'The Supplier')
+
+ def test_post_forbidden(self):
+ r = self.client.post(self.url, self.post_data)
+ self.assertForbiddenKfet(r)
+
+
+class OrderCreateViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.order.new'
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ @property
+ def url_kwargs(self):
+ return {'pk': self.supplier.pk}
+
+ @property
+ def url_expected(self):
+ return '/k-fet/orders/suppliers/{}/new-order'.format(self.supplier.pk)
+
+ @property
+ def users_extra(self):
+ return {
+ 'team1': create_team('team1', '101', perms=['kfet.add_order']),
+ }
+
+ @property
+ def post_data(self):
+ return {
+ # Formset management
+ 'form-TOTAL_FORMS': '1',
+ 'form-INITIAL_FORMS': '1',
+ 'form-MIN_NUM_FORMS': '0',
+ 'form-MAX_NUM_FORMS': '1000',
+ # Article
+ 'form-0-article': self.article.pk,
+ 'form-0-quantity_ordered': '20',
+ }
+
+ def setUp(self):
+ super().setUp()
+ category = ArticleCategory.objects.create(name='Category')
+ self.article = Article.objects.create(
+ name='Article',
+ category=category,
+ )
+
+ self.supplier = Supplier.objects.create(name='Supplier')
+ SupplierArticle.objects.create(
+ supplier=self.supplier,
+ article=self.article,
+ )
+
+ def test_get_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+ @mock.patch('django.utils.timezone.now')
+ def test_post_ok(self, mock_now):
+ mock_now.return_value = self.now
+
+ client = Client()
+ client.login(username='team1', password='team1')
+
+ r = client.post(self.url, self.post_data)
+
+ order = Order.objects.get(at=self.now)
+
+ self.assertRedirects(r, reverse('kfet.order.read', args=[order.pk]))
+
+ def test_post_forbidden(self):
+ r = self.client.post(self.url, self.post_data)
+ self.assertForbiddenKfet(r)
+
+
+class OrderToInventoryViewTests(ViewTestCaseMixin, TestCase):
+ url_name = 'kfet.order.to_inventory'
+
+ http_methods = ['GET', 'POST']
+
+ auth_user = 'team'
+ auth_forbidden = [None, 'user']
+
+ @property
+ def url_kwargs(self):
+ return {'pk': self.order.pk}
+
+ @property
+ def url_expected(self):
+ return '/k-fet/orders/{}/to_inventory'.format(self.order.pk)
+
+ @property
+ def users_extra(self):
+ return {
+ 'team1': create_team('team1', '101', perms=[
+ 'kfet.order_to_inventory',
+ ]),
+ }
+
+ @property
+ def post_data(self):
+ return {
+ # Formset mangaement
+ 'form-TOTAL_FORMS': '1',
+ 'form-INITIAL_FORMS': '1',
+ 'form-MIN_NUM_FORMS': '0',
+ 'form-MAX_NUM_FORMS': '1000',
+ # Article 1
+ 'form-0-article': self.article.pk,
+ 'form-0-quantity_received': '20',
+ 'form-0-price_HT': '',
+ 'form-0-TVA': '',
+ 'form-0-rights': '',
+ }
+
+ def setUp(self):
+ super().setUp()
+ category = ArticleCategory.objects.create(name='Category')
+ self.article = Article.objects.create(
+ name='Article',
+ category=category,
+ )
+
+ supplier = Supplier.objects.create(name='Supplier')
+ SupplierArticle.objects.create(supplier=supplier, article=self.article)
+
+ self.order = Order.objects.create(supplier=supplier)
+ OrderArticle.objects.create(
+ order=self.order,
+ article=self.article,
+ quantity_ordered=24,
+ )
+
+ def test_get_ok(self):
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 200)
+
+ @mock.patch('django.utils.timezone.now')
+ def test_post_ok(self, mock_now):
+ mock_now.return_value = self.now
+
+ client = Client()
+ client.login(username='team1', password='team1')
+
+ r = client.post(self.url, self.post_data)
+ self.assertRedirects(r, reverse('kfet.order'))
+
+ inventory = Inventory.objects.first()
+
+ self.assertInstanceExpected(inventory, {
+ 'by': self.accounts['team1'],
+ 'at': self.now,
+ 'order': self.order,
+ })
+ self.assertQuerysetEqual(
+ inventory.articles.all(),
+ map(repr, [self.article]),
+ )
+
+ compte = InventoryArticle.objects.get(article=self.article)
+
+ self.assertInstanceExpected(compte, {
+ 'stock_old': 0,
+ 'stock_new': 20,
+ 'stock_error': 0,
+ })
+
+ def test_post_forbidden(self):
+ r = self.client.post(self.url, self.post_data)
+ self.assertForbiddenKfet(r)
diff --git a/kfet/tests/testcases.py b/kfet/tests/testcases.py
index c2e1b848..977345e7 100644
--- a/kfet/tests/testcases.py
+++ b/kfet/tests/testcases.py
@@ -1,16 +1,114 @@
from unittest import mock
+from urllib.parse import parse_qs, urlparse
from django.core.urlresolvers import reverse
from django.http import QueryDict
from django.test import Client
+from django.utils import timezone
from .utils import create_root, create_team, create_user
-class ViewTestCaseMixin:
+class TestCaseMixin:
+ def assertForbidden(self, response):
+ request = response.wsgi_request
+
+ try:
+ try:
+ # Is this an HTTP Forbidden response ?
+ self.assertEqual(response.status_code, 403)
+ except AssertionError:
+ # A redirection to the login view is fine too.
+
+ # Let's build the login url with the 'next' param on current
+ # page.
+ full_path = request.get_full_path()
+
+ querystring = QueryDict(mutable=True)
+ querystring['next'] = full_path
+
+ login_url = '/login?' + querystring.urlencode(safe='/')
+
+ # We don't focus on what the login view does.
+ # So don't fetch the redirect.
+ self.assertRedirects(
+ response, login_url,
+ fetch_redirect_response=False,
+ )
+ except AssertionError:
+ raise AssertionError(
+ "%(http_method)s request at %(path)s should be forbidden for "
+ "%(username)s user.\n"
+ "Response isn't 403, nor a redirect to login view. Instead, "
+ "response code is %(code)d." % {
+ 'http_method': request.method,
+ 'path': request.get_full_path(),
+ 'username': (
+ "'{}'".format(request.user)
+ if request.user.is_authenticated()
+ else 'anonymous'
+ ),
+ 'code': response.status_code,
+ }
+ )
+
+ def assertForbiddenKfet(self, response, form_ctx='form'):
+ try:
+ self.assertEqual(response.status_code, 200)
+ try:
+ form = response.context[form_ctx]
+ self.assertIn("Permission refusée", form.non_field_errors())
+ except (AssertionError, AttributeError, KeyError):
+ messages = [str(msg) for msg in response.context['messages']]
+ self.assertIn("Permission refusée", messages)
+ except AssertionError:
+ request = response.wsgi_request
+ raise AssertionError(
+ "%(http_method)s request at %(path)s should raise an error "
+ "for %(username)s user.\n"
+ "Cannot find any errors in non-field errors of form "
+ "'%(form_ctx)s', nor in messages." % {
+ 'http_method': request.method,
+ 'path': request.get_full_path(),
+ 'username': (
+ "'%s'" % request.user
+ if request.user.is_authenticated()
+ else 'anonymous'
+ ),
+ 'form_ctx': form_ctx,
+ }
+ )
+
+ def assertInstanceExpected(self, instance, expected):
+ for attr, expected_value in expected.items():
+ value = getattr(instance, attr)
+ if callable(value):
+ value = value()
+ self.assertEqual(value, expected_value)
+
+ def assertUrlsEqual(self, actual, expected):
+ if type(expected) == dict:
+ parsed = urlparse(actual)
+ checks = ['scheme', 'netloc', 'path', 'params']
+ for check in checks:
+ self.assertEqual(
+ getattr(parsed, check),
+ expected.get(check, ''),
+ )
+ self.assertDictEqual(
+ parse_qs(parsed.query),
+ expected.get('query', {}),
+ )
+ else:
+ self.assertEqual(actual, expected)
+
+
+class ViewTestCaseMixin(TestCaseMixin):
url_name = None
url_expected = None
+ http_methods = ['GET']
+
auth_user = None
auth_forbidden = []
@@ -22,6 +120,11 @@ class ViewTestCaseMixin:
patcher_messages.start()
self.addCleanup(patcher_messages.stop)
+ # A test can mock 'django.utils.timezone.now' and give this as return
+ # value. E.g. it is useful if the test checks values of 'auto_now' or
+ # 'auto_now_add' fields.
+ self.now = timezone.now()
+
self.users = {}
self.accounts = {}
@@ -58,6 +161,11 @@ class ViewTestCaseMixin:
if hasattr(user.profile, 'account_kfet'):
self.accounts[label] = user.profile.account_kfet
+ def get_user(self, label):
+ if self.auth_user is not None:
+ return self.auth_user
+ return self.auth_user_mapping.get(label)
+
@property
def urls_conf(self):
return [{
@@ -81,62 +189,24 @@ class ViewTestCaseMixin:
def url(self):
return self.t_urls[0]
- def assertForbidden(self, response):
- request = response.wsgi_request
-
- try:
- try:
- # Is this an HTTP Forbidden response ?
- self.assertEqual(response.status_code, 403)
- except AssertionError:
- # A redirection to the login view is fine too.
-
- # Let's build the login url with the 'next' param on current
- # page.
- full_path = request.get_full_path()
-
- querystring = QueryDict(mutable=True)
- querystring['next'] = full_path
-
- login_url = '/login?' + querystring.urlencode(safe='/')
-
- # We don't focus on what the login view does.
- # So don't fetch the redirect.
- self.assertRedirects(
- response, login_url,
- fetch_redirect_response=False,
- )
- except AssertionError:
- raise AssertionError(
- "%(http_method)s request at %(path)s should be forbidden for "
- "%(username)s user.\n"
- "Response isn't 403, nor a redirect to login view. Instead, "
- "response code is %(code)d." % {
- 'http_method': request.method,
- 'path': request.get_full_path(),
- 'username': (
- "'{}'".format(request.user.username)
- if request.user.username
- else 'anonymous'
- ),
- 'code': response.status_code,
- }
- )
-
- def assertForbiddenKfet(self, response):
- self.assertEqual(response.status_code, 200)
- form = response.context['form']
- self.assertIn("Permission refusée", form.non_field_errors)
-
def test_urls(self):
for url, conf in zip(self.t_urls, self.urls_conf):
self.assertEqual(url, conf['expected'])
def test_forbidden(self):
- for creds in self.auth_forbidden:
- for url in self.t_urls:
- client = Client()
- if creds is not None:
- client.login(username=creds, password=creds)
- r = client.get(url)
- self.assertForbidden(r)
+ for method in self.http_methods:
+ for user in self.auth_forbidden:
+ for url in self.t_urls:
+ self.check_forbidden(method, url, user)
+
+ def check_forbidden(self, method, url, user=None):
+ method = method.lower()
+ client = Client()
+ if user is not None:
+ client.login(username=user, password=user)
+
+ send_request = getattr(client, method)
+ data = getattr(self, '{}_data'.format(method), {})
+
+ r = send_request(url, data)
+ self.assertForbidden(r)
diff --git a/kfet/tests/utils.py b/kfet/tests/utils.py
index 4b739003..4681da67 100644
--- a/kfet/tests/utils.py
+++ b/kfet/tests/utils.py
@@ -7,44 +7,14 @@ from ..models import Account
User = get_user_model()
-def user_add_perms(user, perms_labels):
- """
- Add perms to a user.
-
- Args:
- user (User instance)
- perms (list of str 'app.perm_name')
-
- Returns:
- The same user (refetched from DB to avoid missing perms)
-
- """
- u_labels = set(perms_labels)
-
- perms = []
- for label in u_labels:
- app_label, codename = label.split('.', 1)
- perms.append(
- Permission.objects.get(
- content_type__app_label=app_label,
- codename=codename,
- )
- )
-
- user.user_permissions.add(*perms)
-
- # If permissions have already been fetched for this user, we need to reload
- # it to avoid using of the previous permissions cache.
- # https://docs.djangoproject.com/en/1.11/topics/auth/default/#permission-caching
- return User.objects.get(pk=user.pk)
-
-
def _create_user_and_account(user_attrs, account_attrs, perms=None):
- user_attrs.setdefault('password', user_attrs['username'])
- user = User.objects.create_user(**user_attrs)
+ user_pwd = user_attrs.pop('password', user_attrs['username'])
+ user = User.objects.create(**user_attrs)
+ user.set_password(user_pwd)
+ user.save()
account_attrs['cofprofile'] = user.profile
- kfet_pwd = account_attrs.pop('password', None)
+ kfet_pwd = account_attrs.pop('password', 'kfetpwd_{}'.format(user_pwd))
account = Account.objects.create(**account_attrs)
@@ -52,8 +22,6 @@ def _create_user_and_account(user_attrs, account_attrs, perms=None):
user = user_add_perms(user, perms)
if 'kfet.is_team' in perms:
- if kfet_pwd is None:
- kfet_pwd = 'kfetpwd_{}'.format(user_attrs['password'])
account.change_pwd(kfet_pwd)
account.save()
@@ -98,8 +66,41 @@ def create_root(username='root', trigramme='200', **kwargs):
user_attrs.setdefault('first_name', 'super')
user_attrs.setdefault('last_name', 'user')
user_attrs.setdefault('email', 'mail@root.net')
+ user_attrs['is_superuser'] = user_attrs['is_staff'] = True
account_attrs = kwargs.setdefault('account_attrs', {})
account_attrs.setdefault('trigramme', trigramme)
return _create_user_and_account(**kwargs)
+
+
+def get_perms(*labels):
+ perms = {}
+ for label in set(labels):
+ app_label, codename = label.split('.', 1)
+ perms[label] = Permission.objects.get(
+ content_type__app_label=app_label,
+ codename=codename,
+ )
+ return perms
+
+
+def user_add_perms(user, perms_labels):
+ """
+ Add perms to a user.
+
+ Args:
+ user (User instance)
+ perms (list of str 'app.perm_name')
+
+ Returns:
+ The same user (refetched from DB to avoid missing perms)
+
+ """
+ perms = get_perms(*perms_labels)
+ user.user_permissions.add(*perms.values())
+
+ # If permissions have already been fetched for this user, we need to reload
+ # it to avoid using of the previous permissions cache.
+ # https://docs.djangoproject.com/en/1.11/topics/auth/default/#permission-caching
+ return User.objects.get(pk=user.pk)
From 414b0eb433e86e6f758ef180537a04542fc28e00 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?=
Date: Wed, 16 Aug 2017 21:28:16 +0200
Subject: [PATCH 003/101] Add missing perms to view/edit kfet config
---
kfet/migrations/0057_add_perms_config.py | 18 ++++++++++++++++++
kfet/models.py | 2 ++
kfet/urls.py | 8 ++------
kfet/views.py | 9 ++++++++-
4 files changed, 30 insertions(+), 7 deletions(-)
create mode 100644 kfet/migrations/0057_add_perms_config.py
diff --git a/kfet/migrations/0057_add_perms_config.py b/kfet/migrations/0057_add_perms_config.py
new file mode 100644
index 00000000..1300665f
--- /dev/null
+++ b/kfet/migrations/0057_add_perms_config.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('kfet', '0056_change_account_meta'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='account',
+ options={'permissions': (('is_team', 'Is part of the team'), ('manage_perms', 'Gérer les permissions K-Fêt'), ('manage_addcosts', 'Gérer les majorations'), ('edit_balance_account', "Modifier la balance d'un compte"), ('change_account_password', "Modifier le mot de passe d'une personne de l'équipe"), ('special_add_account', 'Créer un compte avec une balance initiale'), ('can_force_close', 'Fermer manuellement la K-Fêt'), ('see_config', 'Voir la configuration K-Fêt'), ('change_config', 'Modifier la configuration K-Fêt'))},
+ ),
+ ]
diff --git a/kfet/models.py b/kfet/models.py
index ec146ad9..8b209468 100644
--- a/kfet/models.py
+++ b/kfet/models.py
@@ -75,6 +75,8 @@ class Account(models.Model):
('special_add_account',
"Créer un compte avec une balance initiale"),
('can_force_close', "Fermer manuellement la K-Fêt"),
+ ('see_config', "Voir la configuration K-Fêt"),
+ ('change_config', "Modifier la configuration K-Fêt"),
)
def __str__(self):
diff --git a/kfet/urls.py b/kfet/urls.py
index c3499b18..17ded7b8 100644
--- a/kfet/urls.py
+++ b/kfet/urls.py
@@ -188,13 +188,9 @@ urlpatterns = [
# Settings urls
# -----
- url(r'^settings/$',
- permission_required('kfet.change_settings')
- (views.SettingsList.as_view()),
+ url(r'^settings/$', views.config_list,
name='kfet.settings'),
- url(r'^settings/edit$',
- permission_required('kfet.change_settings')
- (views.SettingsUpdate.as_view()),
+ url(r'^settings/edit$', views.config_update,
name='kfet.settings.update'),
diff --git a/kfet/views.py b/kfet/views.py
index 5e451c9c..ec772a05 100644
--- a/kfet/views.py
+++ b/kfet/views.py
@@ -1452,6 +1452,9 @@ class SettingsList(TemplateView):
template_name = 'kfet/settings.html'
+config_list = permission_required('kfet.see_config')(SettingsList.as_view())
+
+
class SettingsUpdate(SuccessMessageMixin, FormView):
form_class = KFetConfigForm
template_name = 'kfet/settings_update.html'
@@ -1460,13 +1463,17 @@ class SettingsUpdate(SuccessMessageMixin, FormView):
def form_valid(self, form):
# Checking permission
- if not self.request.user.has_perm('kfet.change_settings'):
+ if not self.request.user.has_perm('kfet.change_config'):
form.add_error(None, 'Permission refusée')
return self.form_invalid(form)
form.save()
return super().form_valid(form)
+config_update = (
+ permission_required('kfet.change_config')(SettingsUpdate.as_view())
+)
+
# -----
# Transfer views
From b4b15ab371dde565761511232c3311de758bc230 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?=
Date: Wed, 16 Aug 2017 22:30:17 +0200
Subject: [PATCH 004/101] Tests of kfet config views pass
---
kfet/tests/test_views.py | 24 ++++++++++++++++--------
1 file changed, 16 insertions(+), 8 deletions(-)
diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py
index 5ac82f33..1cd83ffd 100644
--- a/kfet/tests/test_views.py
+++ b/kfet/tests/test_views.py
@@ -1653,7 +1653,7 @@ class SettingsListViewTests(ViewTestCaseMixin, TestCase):
def users_extra(self):
return {
'team1': create_team('team1', '101', perms=[
- 'kfet.change_settings',
+ 'kfet.see_config',
]),
}
@@ -1686,26 +1686,34 @@ class SettingsUpdateViewTests(ViewTestCaseMixin, TestCase):
def users_extra(self):
return {
'team1': create_team('team1', '101', perms=[
- 'kfet.change_settings',
+ 'kfet.change_config',
]),
}
def test_get_ok(self):
r = self.client.get(self.url)
- self.assertequal(r.status_code, 200)
+ self.assertEqual(r.status_code, 200)
def test_post_ok(self):
r = self.client.post(self.url, self.post_data)
- self.assertRedirects(r, reverse('kfet.settings'))
+ # Redirect is skipped because client may lack permissions.
+ self.assertRedirects(
+ r,
+ reverse('kfet.settings'),
+ fetch_redirect_response=False,
+ )
- self.assertDictEqual(dict(kfet_config.list()), {
+ expected_config = {
'reduction_cof': Decimal('25'),
'addcost_amount': Decimal('0.5'),
'addcost_for': self.accounts['user'],
- 'overdraft_duration': timedelta(day=2),
+ 'overdraft_duration': timedelta(days=2),
'overdraft_amount': Decimal('25'),
- 'kfet_cancel_duration': timedelta(minutes=20),
- })
+ 'cancel_duration': timedelta(minutes=20),
+ }
+
+ for key, expected in expected_config.items():
+ self.assertEqual(getattr(kfet_config, key), expected)
class TransferListViewTests(ViewTestCaseMixin, TestCase):
From 22d8317dee76a603ed102f486d0956bf5bfbb896 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?=
Date: Tue, 8 Aug 2017 19:03:42 +0200
Subject: [PATCH 005/101] Fix kfet.open.tests
Due to messages sent in signals handlers, the tests were failing.
---
kfet/open/tests.py | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/kfet/open/tests.py b/kfet/open/tests.py
index 1d6d5529..476eb6c0 100644
--- a/kfet/open/tests.py
+++ b/kfet/open/tests.py
@@ -1,5 +1,6 @@
import json
from datetime import timedelta
+from unittest import mock
from django.contrib.auth.models import AnonymousUser, Permission, User
from django.test import Client
@@ -118,6 +119,11 @@ class OpenKfetViewsTest(ChannelTestCase):
"""OpenKfet views unit-tests suite."""
def setUp(self):
+ # Need this (and here) because of '.login' in setUp
+ patcher_messages = mock.patch('gestioncof.signals.messages')
+ patcher_messages.start()
+ self.addCleanup(patcher_messages.stop)
+
# get some permissions
perms = {
'kfet.is_team': Permission.objects.get(codename='is_team'),
@@ -194,7 +200,8 @@ class OpenKfetConsumerTest(ChannelTestCase):
OpenKfetConsumer.group_send('kfet.open.team', {'test': 'plop'})
self.assertIsNone(c.receive())
- def test_team_user(self):
+ @mock.patch('gestioncof.signals.messages')
+ def test_team_user(self, mock_messages):
"""Team user is added to kfet.open.team group."""
# setup team user and its client
t = User.objects.create_user('team', '', 'team')
@@ -224,6 +231,11 @@ class OpenKfetScenarioTest(ChannelTestCase):
"""OpenKfet functionnal tests suite."""
def setUp(self):
+ # Need this (and here) because of '.login' in setUp
+ patcher_messages = mock.patch('gestioncof.signals.messages')
+ patcher_messages.start()
+ self.addCleanup(patcher_messages.stop)
+
# anonymous client (for views)
self.c = Client()
# anonymous client (for websockets)
From b4338ce8dbaeb5291dd7c98f16ee4f69a0a323a5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?=
Date: Wed, 16 Aug 2017 22:54:40 +0200
Subject: [PATCH 006/101] View 'search account' should be restricted.
---
kfet/autocomplete.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/kfet/autocomplete.py b/kfet/autocomplete.py
index 09057d4a..0a9bb42c 100644
--- a/kfet/autocomplete.py
+++ b/kfet/autocomplete.py
@@ -106,6 +106,7 @@ def account_create(request):
return render(request, "kfet/account_create_autocomplete.html", data)
+@teamkfet_required
def account_search(request):
if "q" not in request.GET:
raise Http404
From be1e67626c0fb8a9341a38501c3337b2872ccad4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?=
Date: Wed, 16 Aug 2017 23:04:22 +0200
Subject: [PATCH 007/101] Most data of suppliers should be optionnal.
---
kfet/migrations/0058_amend_supplier.py | 39 ++++++++++++++++++++++++++
kfet/models.py | 20 +++++++------
2 files changed, 51 insertions(+), 8 deletions(-)
create mode 100644 kfet/migrations/0058_amend_supplier.py
diff --git a/kfet/migrations/0058_amend_supplier.py b/kfet/migrations/0058_amend_supplier.py
new file mode 100644
index 00000000..0b45dade
--- /dev/null
+++ b/kfet/migrations/0058_amend_supplier.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('kfet', '0057_add_perms_config'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='supplier',
+ name='address',
+ field=models.TextField(verbose_name='adresse', blank=True),
+ ),
+ migrations.AlterField(
+ model_name='supplier',
+ name='articles',
+ field=models.ManyToManyField(verbose_name='articles vendus', through='kfet.SupplierArticle', related_name='suppliers', to='kfet.Article'),
+ ),
+ migrations.AlterField(
+ model_name='supplier',
+ name='comment',
+ field=models.TextField(verbose_name='commentaire', blank=True),
+ ),
+ migrations.AlterField(
+ model_name='supplier',
+ name='email',
+ field=models.EmailField(max_length=254, verbose_name='adresse mail', blank=True),
+ ),
+ migrations.AlterField(
+ model_name='supplier',
+ name='phone',
+ field=models.CharField(max_length=20, verbose_name='téléphone', blank=True),
+ ),
+ ]
diff --git a/kfet/models.py b/kfet/models.py
index 8b209468..fb0d8813 100644
--- a/kfet/models.py
+++ b/kfet/models.py
@@ -8,6 +8,7 @@ from gestioncof.models import CofProfile
from django.utils.six.moves import reduce
from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible
+from django.utils.translation import ugettext_lazy as _
from django.db import transaction
from django.db.models import F
from datetime import date
@@ -524,21 +525,24 @@ class InventoryArticle(models.Model):
self.stock_error = self.stock_new - self.stock_old
super(InventoryArticle, self).save(*args, **kwargs)
-@python_2_unicode_compatible
+
class Supplier(models.Model):
articles = models.ManyToManyField(
Article,
- through = 'SupplierArticle',
- related_name = "suppliers")
- name = models.CharField("nom", max_length = 45)
- address = models.TextField("adresse")
- email = models.EmailField("adresse mail")
- phone = models.CharField("téléphone", max_length = 10)
- comment = models.TextField("commentaire")
+ verbose_name=_("articles vendus"),
+ through='SupplierArticle',
+ related_name='suppliers',
+ )
+ name = models.CharField(_("nom"), max_length=45)
+ address = models.TextField(_("adresse"), blank=True)
+ email = models.EmailField(_("adresse mail"), blank=True)
+ phone = models.CharField(_("téléphone"), max_length=20, blank=True)
+ comment = models.TextField(_("commentaire"), blank=True)
def __str__(self):
return self.name
+
class SupplierArticle(models.Model):
supplier = models.ForeignKey(
Supplier, on_delete = models.PROTECT)
From d8391e54a5a5abb5b52f9d3c6867ddaefae92880 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?=
Date: Fri, 1 Sep 2017 12:39:17 +0200
Subject: [PATCH 008/101] Add docs to kfet TestCases
---
kfet/tests/testcases.py | 132 +++++++++++++++++++++++++++++++++++++---
1 file changed, 122 insertions(+), 10 deletions(-)
diff --git a/kfet/tests/testcases.py b/kfet/tests/testcases.py
index 977345e7..e2fc09ff 100644
--- a/kfet/tests/testcases.py
+++ b/kfet/tests/testcases.py
@@ -10,7 +10,18 @@ from .utils import create_root, create_team, create_user
class TestCaseMixin:
+ """Extends TestCase for kfet application tests."""
+
def assertForbidden(self, response):
+ """
+ Test that the response (retrieved with a Client) is a denial of access.
+
+ The response should verify one of the following:
+ - its HTTP response code is 403,
+ - it redirects to the login page with a GET parameter named 'next'
+ whose value is the url of the requested page.
+
+ """
request = response.wsgi_request
try:
@@ -53,6 +64,18 @@ class TestCaseMixin:
)
def assertForbiddenKfet(self, response, form_ctx='form'):
+ """
+ Test that a response (retrieved with a Client) contains error due to
+ lack of kfet permissions.
+
+ It checks that 'Permission refusée' is present in the non-field errors
+ of the form of response context at key 'form_ctx', or present in
+ messages.
+
+ This should be used for pages which can be accessed by the kfet team
+ members, but require additionnal permission(s) to make an operation.
+
+ """
try:
self.assertEqual(response.status_code, 200)
try:
@@ -80,6 +103,10 @@ class TestCaseMixin:
)
def assertInstanceExpected(self, instance, expected):
+ """
+ Test that the values of the attributes and without-argument methods of
+ 'instance' are equal to 'expected' pairs.
+ """
for attr, expected_value in expected.items():
value = getattr(instance, attr)
if callable(value):
@@ -87,23 +114,104 @@ class TestCaseMixin:
self.assertEqual(value, expected_value)
def assertUrlsEqual(self, actual, expected):
+ """
+ Test that the url 'actual' is as 'expected'.
+
+ Arguments:
+ actual (str): Url to verify.
+ expected: Two forms are accepted.
+ * (str): Expected url. Strings equality is checked.
+ * (dict): Its keys must be attributes of 'urlparse(actual)'.
+ Equality is checked for each present key, except for
+ 'query' which must be a dict of the expected query string
+ parameters.
+
+ """
if type(expected) == dict:
parsed = urlparse(actual)
- checks = ['scheme', 'netloc', 'path', 'params']
- for check in checks:
- self.assertEqual(
- getattr(parsed, check),
- expected.get(check, ''),
- )
- self.assertDictEqual(
- parse_qs(parsed.query),
- expected.get('query', {}),
- )
+ for part, expected_part in expected.items():
+ if part == 'query':
+ self.assertDictEqual(
+ parse_qs(parsed.query),
+ expected.get('query', {}),
+ )
+ else:
+ self.assertEqual(getattr(parsed, part), expected_part)
else:
self.assertEqual(actual, expected)
class ViewTestCaseMixin(TestCaseMixin):
+ """
+ TestCase extension to ease tests of kfet views.
+
+
+ Urls concerns
+ -------------
+
+ # Basic usage
+
+ Attributes:
+ url_name (str): Name of view under test, as given to 'reverse'
+ function.
+ url_args (list, optional): Will be given to 'reverse' call.
+ url_kwargs (dict, optional): Same.
+ url_expcted (str): What 'reverse' should return given previous
+ attributes.
+
+ View url can then be accessed at the 'url' attribute.
+
+ # Advanced usage
+
+ If multiple combinations of url name, args, kwargs can be used for a view,
+ it is possible to define 'urls_conf' attribute. It must be a list whose
+ each item is a dict defining arguments for 'reverse' call ('name', 'args',
+ 'kwargs' keys) and its expected result ('expected' key).
+
+ The reversed urls can be accessed at the 't_urls' attribute.
+
+
+ Users concerns
+ --------------
+
+ During setup, three users are created with their kfet account:
+ - 'user': a basic user without any permission, account trigramme: 000,
+ - 'team': a user with kfet.is_team permission, account trigramme: 100,
+ - 'root': a superuser, account trigramme: 200.
+ Their password is their username.
+
+ One can create additionnal users with 'users_extra' attribute, or prevent
+ these 3 users to be created with 'users_base' attribute. See these two
+ properties for further informations.
+
+ By using 'register_user' method, these users can then be accessed at
+ 'users' attribute by their label. Similarly, their kfet account is
+ registered on 'accounts' attribute.
+
+ A user label can be given to 'auth_user' attribute. The related user is
+ then authenticated on self.client during test setup. Its value defaults to
+ 'None', meaning no user is authenticated.
+
+
+ Automated tests
+ ---------------
+
+ # Url reverse
+
+ Based on url-related attributes/properties, the test 'test_urls' checks
+ that expected url is returned by 'reverse' (once with basic url usage and
+ each for advanced usage).
+
+ # Forbidden responses
+
+ The 'test_forbidden' test verifies that each user, from labels of
+ 'auth_forbidden' attribute, can't access the url(s), i.e. response should
+ be a 403, or a redirect to login view.
+
+ Tested HTTP requests are given by 'http_methods' attribute. Additional data
+ can be given by defining an attribute '_data'.
+
+ """
url_name = None
url_expected = None
@@ -113,6 +221,9 @@ class ViewTestCaseMixin(TestCaseMixin):
auth_forbidden = []
def setUp(self):
+ """
+ Warning: Do not forget to call super().setUp() in subclasses.
+ """
# Signals handlers on login/logout send messages.
# Due to the way the Django' test Client performs login, this raise an
# error. As workaround, we mock the Django' messages module.
@@ -125,6 +236,7 @@ class ViewTestCaseMixin(TestCaseMixin):
# 'auto_now_add' fields.
self.now = timezone.now()
+ # These attributes register users and accounts instances.
self.users = {}
self.accounts = {}
From 997b63d6b69828ee6c82b226b7891e877a1507ae Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?=
Date: Fri, 1 Sep 2017 13:35:32 +0200
Subject: [PATCH 009/101] More docs for kfet.tests.utils
---
kfet/tests/utils.py | 82 +++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 82 insertions(+)
diff --git a/kfet/tests/utils.py b/kfet/tests/utils.py
index 4681da67..30eb05ad 100644
--- a/kfet/tests/utils.py
+++ b/kfet/tests/utils.py
@@ -8,6 +8,21 @@ User = get_user_model()
def _create_user_and_account(user_attrs, account_attrs, perms=None):
+ """
+ Create a user and its account, and assign permissions to this user.
+
+ Arguments
+ user_attrs (dict): User data (first name, last name, password...).
+ account_attrs (dict): Account data (department, kfet password...).
+ perms (list of str: 'app.perm'): These permissions will be assigned to
+ the created user. No permission are assigned by default.
+
+ If 'password' is not given in 'user_attrs', username is used as password.
+
+ If 'kfet.is_team' is in 'perms' and 'password' is not in 'account_attrs',
+ the account password is 'kfetpwd_'.
+
+ """
user_pwd = user_attrs.pop('password', user_attrs['username'])
user = User.objects.create(**user_attrs)
user.set_password(user_pwd)
@@ -29,6 +44,27 @@ def _create_user_and_account(user_attrs, account_attrs, perms=None):
def create_user(username='user', trigramme='000', **kwargs):
+ """
+ Create a user without any permission and its kfet account.
+
+ username and trigramme are accepted as arguments (defaults to 'user' and
+ '000').
+
+ user_attrs, account_attrs and perms can be given as keyword arguments to
+ customize the user and its kfet account.
+
+ # Default values
+
+ User
+ * username: user
+ * password: user
+ * first_name: first
+ * last_name: last
+ * email: mail@user.net
+ Account
+ * trigramme: 000
+
+ """
user_attrs = kwargs.setdefault('user_attrs', {})
user_attrs.setdefault('username', username)
@@ -43,6 +79,28 @@ def create_user(username='user', trigramme='000', **kwargs):
def create_team(username='team', trigramme='100', **kwargs):
+ """
+ Create a user, member of the kfet team, and its kfet account.
+
+ username and trigramme are accepted as arguments (defaults to 'team' and
+ '100').
+
+ user_attrs, account_attrs and perms can be given as keyword arguments to
+ customize the user and its kfet account.
+
+ # Default values
+
+ User
+ * username: team
+ * password: team
+ * first_name: team
+ * last_name: member
+ * email: mail@team.net
+ Account
+ * trigramme: 100
+ * kfet password: kfetpwd_team
+
+ """
user_attrs = kwargs.setdefault('user_attrs', {})
user_attrs.setdefault('username', username)
@@ -60,6 +118,29 @@ def create_team(username='team', trigramme='100', **kwargs):
def create_root(username='root', trigramme='200', **kwargs):
+ """
+ Create a superuser and its kfet account.
+
+ username and trigramme are accepted as arguments (defaults to 'root' and
+ '200').
+
+ user_attrs, account_attrs and perms can be given as keyword arguments to
+ customize the user and its kfet account.
+
+ # Default values
+
+ User
+ * username: root
+ * password: root
+ * first_name: super
+ * last_name: user
+ * email: mail@root.net
+ * is_staff, is_superuser: True
+ Account
+ * trigramme: 200
+ * kfet password: kfetpwd_root
+
+ """
user_attrs = kwargs.setdefault('user_attrs', {})
user_attrs.setdefault('username', username)
@@ -75,6 +156,7 @@ def create_root(username='root', trigramme='200', **kwargs):
def get_perms(*labels):
+ """Return Permission instances from a list of '.'."""
perms = {}
for label in set(labels):
app_label, codename = label.split('.', 1)
From af97c0cda606ab112646ae850bc1b8c356ce6057 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?=
Date: Fri, 1 Sep 2017 16:37:14 +0200
Subject: [PATCH 010/101] Improve users management on kfet TestCase, and Py34
compat
---
kfet/tests/test_views.py | 81 ++++++++++++++--------------------------
kfet/tests/testcases.py | 45 ++++++++++++++++++----
2 files changed, 64 insertions(+), 62 deletions(-)
diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py
index 1cd83ffd..ff9803c9 100644
--- a/kfet/tests/test_views.py
+++ b/kfet/tests/test_views.py
@@ -93,8 +93,7 @@ class AccountCreateViewTests(ViewTestCaseMixin, TestCase):
'email': 'email@domain.net',
}
- @property
- def users_extra(self):
+ def get_users_extra(self):
return {
'team1': create_team('team1', '101', perms=['kfet.add_account']),
}
@@ -212,8 +211,7 @@ class AccountReadViewTests(ViewTestCaseMixin, TestCase):
auth_user = 'team'
auth_forbidden = [None, 'user']
- @property
- def users_extra(self):
+ def get_users_extra(self):
return {
'user1': create_user('user1', '001'),
}
@@ -292,8 +290,7 @@ class AccountUpdateViewTests(ViewTestCaseMixin, TestCase):
'pwd2': '',
}
- @property
- def users_extra(self):
+ def get_users_extra(self):
return {
'user1': create_user('user1', '001'),
'team1': create_team('team1', '101', perms=[
@@ -359,8 +356,7 @@ class AccountGroupListViewTests(ViewTestCaseMixin, TestCase):
auth_user = 'team1'
auth_forbidden = [None, 'user', 'team']
- @property
- def users_extra(self):
+ def get_users_extra(self):
return {
'team1': create_team('team1', '101', perms=['kfet.manage_perms']),
}
@@ -390,8 +386,7 @@ class AccountGroupCreateViewTests(ViewTestCaseMixin, TestCase):
auth_user = 'team1'
auth_forbidden = [None, 'user', 'team']
- @property
- def users_extra(self):
+ def get_users_extra(self):
return {
'team1': create_team('team1', '101', perms=['kfet.manage_perms']),
}
@@ -449,8 +444,7 @@ class AccountGroupUpdateViewTests(ViewTestCaseMixin, TestCase):
def url_expected(self):
return '/k-fet/accounts/groups/{}/edit'.format(self.group.pk)
- @property
- def users_extra(self):
+ def get_users_extra(self):
return {
'team1': create_team('team1', '101', perms=['kfet.manage_perms']),
}
@@ -502,8 +496,7 @@ class AccountNegativeListViewTests(ViewTestCaseMixin, TestCase):
auth_user = 'team1'
auth_forbidden = [None, 'user', 'team']
- @property
- def users_extra(self):
+ def get_users_extra(self):
return {
'team1': create_team('team1', '101', perms=['kfet.view_negs']),
}
@@ -533,8 +526,7 @@ class AccountStatOperationListViewTests(ViewTestCaseMixin, TestCase):
auth_user = 'user1'
auth_forbidden = [None, 'user', 'team']
- @property
- def users_extra(self):
+ def get_users_extra(self):
return {'user1': create_user('user1', '001')}
def test_ok(self):
@@ -594,8 +586,7 @@ class AccountStatOperationViewTests(ViewTestCaseMixin, TestCase):
auth_user = 'user1'
auth_forbidden = [None, 'user', 'team']
- @property
- def users_extra(self):
+ def get_users_extra(self):
return {'user1': create_user('user1', '001')}
def test_ok(self):
@@ -611,8 +602,7 @@ class AccountStatBalanceListViewTests(ViewTestCaseMixin, TestCase):
auth_user = 'user1'
auth_forbidden = [None, 'user', 'team']
- @property
- def users_extra(self):
+ def get_users_extra(self):
return {'user1': create_user('user1', '001')}
def test_ok(self):
@@ -666,8 +656,7 @@ class AccountStatBalanceViewTests(ViewTestCaseMixin, TestCase):
auth_user = 'user1'
auth_forbidden = [None, 'user', 'team']
- @property
- def users_extra(self):
+ def get_users_extra(self):
return {'user1': create_user('user1', '001')}
def test_ok(self):
@@ -724,8 +713,7 @@ class CheckoutCreateViewTests(ViewTestCaseMixin, TestCase):
# 'is_protected': not checked
}
- @property
- def users_extra(self):
+ def get_users_extra(self):
return {
'team1': create_team('team1', '101', perms=['kfet.add_checkout']),
}
@@ -807,8 +795,7 @@ class CheckoutUpdateViewTests(ViewTestCaseMixin, TestCase):
def url_expected(self):
return '/k-fet/checkouts/{}/edit'.format(self.checkout.pk)
- @property
- def users_extra(self):
+ def get_users_extra(self):
return {
'team1': create_team('team1', '101', perms=[
'kfet.change_checkout',
@@ -927,8 +914,7 @@ class CheckoutStatementCreateViewTests(ViewTestCaseMixin, TestCase):
def url_expected(self):
return '/k-fet/checkouts/{}/statements/add'.format(self.checkout.pk)
- @property
- def users_extra(self):
+ def get_users_extra(self):
return {
'team1': create_team('team1', '001', perms=[
'kfet.add_checkoutstatement',
@@ -1014,8 +1000,7 @@ class CheckoutStatementUpdateViewTests(ViewTestCaseMixin, TestCase):
pk=self.statement.pk,
)
- @property
- def users_extra(self):
+ def get_users_extra(self):
return {
'team1': create_team('team1', '101', perms=[
'kfet.change_checkoutstatement',
@@ -1109,8 +1094,7 @@ class ArticleCategoryUpdateViewTests(ViewTestCaseMixin, TestCase):
def url_expected(self):
return '/k-fet/categories/{}/edit'.format(self.category.pk)
- @property
- def users_extra(self):
+ def get_users_extra(self):
return {
'team1': create_team('team1', '101', perms=[
'kfet.change_articlecategory',
@@ -1188,8 +1172,7 @@ class ArticleCreateViewTests(ViewTestCaseMixin, TestCase):
auth_user = 'team'
auth_forbidden = [None, 'user']
- @property
- def users_extra(self):
+ def get_users_extra(self):
return {
'team1': create_team('team1', '101', perms=['kfet.add_article']),
}
@@ -1276,8 +1259,7 @@ class ArticleUpdateViewTests(ViewTestCaseMixin, TestCase):
def url_expected(self):
return '/k-fet/articles/{}/edit'.format(self.article.pk)
- @property
- def users_extra(self):
+ def get_users_extra(self):
return {
'team1': create_team('team1', '101', perms=[
'kfet.change_article',
@@ -1564,8 +1546,7 @@ class KPsulUpdateAddcost(ViewTestCaseMixin, TestCase):
'amount': '0.5',
}
- @property
- def users_extra(self):
+ def get_users_extra(self):
return {
'team1': create_team('team1', '101', perms=[
'kfet.manage_addcosts',
@@ -1649,8 +1630,7 @@ class SettingsListViewTests(ViewTestCaseMixin, TestCase):
auth_user = 'team1'
auth_forbidden = [None, 'user', 'team']
- @property
- def users_extra(self):
+ def get_users_extra(self):
return {
'team1': create_team('team1', '101', perms=[
'kfet.see_config',
@@ -1682,8 +1662,7 @@ class SettingsUpdateViewTests(ViewTestCaseMixin, TestCase):
'kfet_cancel_duration': '00:20:00',
}
- @property
- def users_extra(self):
+ def get_users_extra(self):
return {
'team1': create_team('team1', '101', perms=[
'kfet.change_config',
@@ -1749,8 +1728,7 @@ class TransferPerformViewTests(ViewTestCaseMixin, TestCase):
auth_user = 'team1'
auth_forbidden = [None, 'user', 'team']
- @property
- def users_extra(self):
+ def get_users_extra(self):
return {
'team1': create_team('team1', '101', perms=[
# Required
@@ -1806,8 +1784,7 @@ class TransferCancelViewTests(ViewTestCaseMixin, TestCase):
auth_user = 'team1'
auth_forbidden = [None, 'user', 'team']
- @property
- def users_extra(self):
+ def get_users_extra(self):
return {
'team1': create_team('team1', '101', perms=[
# Convenience
@@ -1898,8 +1875,7 @@ class InventoryCreateViewTests(ViewTestCaseMixin, TestCase):
auth_user = 'team'
auth_forbidden = [None, 'user']
- @property
- def users_extra(self):
+ def get_users_extra(self):
return {
'team1': create_team('team1', '101', perms=[
'kfet.add_inventory',
@@ -2069,8 +2045,7 @@ class SupplierUpdateViewTests(ViewTestCaseMixin, TestCase):
def url_expected(self):
return '/k-fet/orders/suppliers/{}/edit'.format(self.supplier.pk)
- @property
- def users_extra(self):
+ def get_users_extra(self):
return {
'team1': create_team('team1', '101', perms=[
'kfet.change_supplier',
@@ -2124,8 +2099,7 @@ class OrderCreateViewTests(ViewTestCaseMixin, TestCase):
def url_expected(self):
return '/k-fet/orders/suppliers/{}/new-order'.format(self.supplier.pk)
- @property
- def users_extra(self):
+ def get_users_extra(self):
return {
'team1': create_team('team1', '101', perms=['kfet.add_order']),
}
@@ -2195,8 +2169,7 @@ class OrderToInventoryViewTests(ViewTestCaseMixin, TestCase):
def url_expected(self):
return '/k-fet/orders/{}/to_inventory'.format(self.order.pk)
- @property
- def users_extra(self):
+ def get_users_extra(self):
return {
'team1': create_team('team1', '101', perms=[
'kfet.order_to_inventory',
diff --git a/kfet/tests/testcases.py b/kfet/tests/testcases.py
index e2fc09ff..d7d7eac5 100644
--- a/kfet/tests/testcases.py
+++ b/kfet/tests/testcases.py
@@ -5,6 +5,7 @@ from django.core.urlresolvers import reverse
from django.http import QueryDict
from django.test import Client
from django.utils import timezone
+from django.utils.functional import cached_property
from .utils import create_root, create_team, create_user
@@ -180,9 +181,9 @@ class ViewTestCaseMixin(TestCaseMixin):
- 'root': a superuser, account trigramme: 200.
Their password is their username.
- One can create additionnal users with 'users_extra' attribute, or prevent
- these 3 users to be created with 'users_base' attribute. See these two
- properties for further informations.
+ One can create additionnal users with 'get_users_extra' method, or prevent
+ these 3 users to be created with 'get_users_base' method. See these two
+ methods for further informations.
By using 'register_user' method, these users can then be accessed at
'users' attribute by their label. Similarly, their kfet account is
@@ -240,7 +241,7 @@ class ViewTestCaseMixin(TestCaseMixin):
self.users = {}
self.accounts = {}
- for label, user in {**self.users_base, **self.users_extra}.items():
+ for label, user in dict(self.users_base, **self.users_extra).items():
self.register_user(label, user)
if self.auth_user:
@@ -252,8 +253,20 @@ class ViewTestCaseMixin(TestCaseMixin):
)
)
- @property
- def users_base(self):
+ def tearDown(self):
+ del self.users_base
+ del self.users_extra
+
+ def get_users_base(self):
+ """
+ Dict of .
+
+ Note: Don't access yourself this property. Use 'users_base' attribute
+ which cache the returned value from here.
+ It allows to give functions calls, which creates users instances, as
+ values here.
+
+ """
# Format desc: username, password, trigramme
return {
# user, user, 000
@@ -264,10 +277,26 @@ class ViewTestCaseMixin(TestCaseMixin):
'root': create_root(),
}
- @property
- def users_extra(self):
+ @cached_property
+ def users_base(self):
+ return self.get_users_base()
+
+ def get_users_extra(self):
+ """
+ Dict of .
+
+ Note: Don't access yourself this property. Use 'users_base' attribute
+ which cache the returned value from here.
+ It allows to give functions calls, which create users instances, as
+ values here.
+
+ """
return {}
+ @cached_property
+ def users_extra(self):
+ return self.get_users_extra()
+
def register_user(self, label, user):
self.users[label] = user
if hasattr(user.profile, 'account_kfet'):
From bf61e41b50eac097093d424f58252ed618168da0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?=
Date: Wed, 13 Sep 2017 01:57:31 +0200
Subject: [PATCH 011/101] Move auth-related from 'kfet' app to 'kfet.auth'.
---
cof/settings/common.py | 7 +-
kfet/auth/__init__.py | 1 +
kfet/auth/apps.py | 8 +++
kfet/{ => auth}/backends.py | 0
kfet/auth/context_processors.py | 10 +++
kfet/auth/fields.py | 20 ++++++
kfet/auth/forms.py | 44 ++++++++++++
kfet/{ => auth}/middleware.py | 2 +-
kfet/auth/migrations/0001_initial.py | 21 ++++++
kfet/auth/migrations/__init__.py | 0
kfet/auth/models.py | 5 ++
.../templates/kfet/login_genericteam.html | 0
kfet/{tests/test_forms.py => auth/tests.py} | 0
kfet/auth/views.py | 69 +++++++++++++++++++
kfet/context_processors.py | 11 ---
kfet/forms.py | 61 ++--------------
.../0058_delete_genericteamtoken.py | 17 +++++
kfet/models.py | 6 +-
kfet/views.py | 65 ++---------------
19 files changed, 213 insertions(+), 134 deletions(-)
create mode 100644 kfet/auth/__init__.py
create mode 100644 kfet/auth/apps.py
rename kfet/{ => auth}/backends.py (100%)
create mode 100644 kfet/auth/context_processors.py
create mode 100644 kfet/auth/fields.py
create mode 100644 kfet/auth/forms.py
rename kfet/{ => auth}/middleware.py (95%)
create mode 100644 kfet/auth/migrations/0001_initial.py
create mode 100644 kfet/auth/migrations/__init__.py
create mode 100644 kfet/auth/models.py
rename kfet/{ => auth}/templates/kfet/login_genericteam.html (100%)
rename kfet/{tests/test_forms.py => auth/tests.py} (100%)
create mode 100644 kfet/auth/views.py
create mode 100644 kfet/migrations/0058_delete_genericteamtoken.py
diff --git a/cof/settings/common.py b/cof/settings/common.py
index ba0b6044..92759d21 100644
--- a/cof/settings/common.py
+++ b/cof/settings/common.py
@@ -90,6 +90,7 @@ INSTALLED_APPS = [
'wagtailmenus',
'modelcluster',
'taggit',
+ 'kfet.auth',
'kfet.cms',
]
@@ -99,7 +100,7 @@ MIDDLEWARE_CLASSES = [
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
- 'kfet.middleware.KFetAuthenticationMiddleware',
+ 'kfet.auth.middleware.KFetAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware',
@@ -127,7 +128,7 @@ TEMPLATES = [
'wagtailmenus.context_processors.wagtailmenus',
'djconfig.context_processors.config',
'gestioncof.shared.context_processor',
- 'kfet.context_processors.auth',
+ 'kfet.auth.context_processors.auth',
'kfet.context_processors.config',
],
},
@@ -190,7 +191,7 @@ CAS_EMAIL_FORMAT = "%s@clipper.ens.fr"
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'gestioncof.shared.COFCASBackend',
- 'kfet.backends.GenericTeamBackend',
+ 'kfet.auth.backends.GenericTeamBackend',
)
RECAPTCHA_USE_SSL = True
diff --git a/kfet/auth/__init__.py b/kfet/auth/__init__.py
new file mode 100644
index 00000000..63392684
--- /dev/null
+++ b/kfet/auth/__init__.py
@@ -0,0 +1 @@
+default_app_config = 'kfet.auth.apps.KFetAuthConfig'
diff --git a/kfet/auth/apps.py b/kfet/auth/apps.py
new file mode 100644
index 00000000..ab791d18
--- /dev/null
+++ b/kfet/auth/apps.py
@@ -0,0 +1,8 @@
+from django.apps import AppConfig
+from django.utils.translation import ugettext_lazy as _
+
+
+class KFetAuthConfig(AppConfig):
+ name = 'kfet.auth'
+ label = 'kfetauth'
+ verbose_name = _("K-Fêt - Authentification et Autorisation")
diff --git a/kfet/backends.py b/kfet/auth/backends.py
similarity index 100%
rename from kfet/backends.py
rename to kfet/auth/backends.py
diff --git a/kfet/auth/context_processors.py b/kfet/auth/context_processors.py
new file mode 100644
index 00000000..07c9537f
--- /dev/null
+++ b/kfet/auth/context_processors.py
@@ -0,0 +1,10 @@
+from django.contrib.auth.context_processors import PermWrapper
+
+
+def auth(request):
+ if hasattr(request, 'real_user'):
+ return {
+ 'user': request.real_user,
+ 'perms': PermWrapper(request.real_user),
+ }
+ return {}
diff --git a/kfet/auth/fields.py b/kfet/auth/fields.py
new file mode 100644
index 00000000..28ba1c9e
--- /dev/null
+++ b/kfet/auth/fields.py
@@ -0,0 +1,20 @@
+from django import forms
+from django.contrib.auth.models import Permission
+from django.contrib.contenttypes.models import ContentType
+from django.forms import widgets
+
+
+class KFetPermissionsField(forms.ModelMultipleChoiceField):
+
+ def __init__(self, *args, **kwargs):
+ queryset = Permission.objects.filter(
+ content_type__in=ContentType.objects.filter(app_label="kfet"),
+ )
+ super().__init__(
+ queryset=queryset,
+ widget=widgets.CheckboxSelectMultiple,
+ *args, **kwargs
+ )
+
+ def label_from_instance(self, obj):
+ return obj.name
diff --git a/kfet/auth/forms.py b/kfet/auth/forms.py
new file mode 100644
index 00000000..0c9fa53b
--- /dev/null
+++ b/kfet/auth/forms.py
@@ -0,0 +1,44 @@
+from django import forms
+from django.contrib.auth.models import Group, User
+
+from .fields import KFetPermissionsField
+
+
+class GroupForm(forms.ModelForm):
+ permissions = KFetPermissionsField()
+
+ def clean_name(self):
+ name = self.cleaned_data['name']
+ return 'K-Fêt %s' % name
+
+ def clean_permissions(self):
+ kfet_perms = self.cleaned_data['permissions']
+ # TODO: With Django >=1.11, the QuerySet method 'difference' can be
+ # used.
+ # other_groups = self.instance.permissions.difference(
+ # self.fields['permissions'].queryset
+ # )
+ other_perms = self.instance.permissions.exclude(
+ pk__in=[p.pk for p in self.fields['permissions'].queryset],
+ )
+ return list(kfet_perms) + list(other_perms)
+
+ class Meta:
+ model = Group
+ fields = ['name', 'permissions']
+
+
+class UserGroupForm(forms.ModelForm):
+ groups = forms.ModelMultipleChoiceField(
+ Group.objects.filter(name__icontains='K-Fêt'),
+ label='Statut équipe',
+ required=False)
+
+ def clean_groups(self):
+ kfet_groups = self.cleaned_data.get('groups')
+ other_groups = self.instance.groups.exclude(name__icontains='K-Fêt')
+ return list(kfet_groups) + list(other_groups)
+
+ class Meta:
+ model = User
+ fields = ['groups']
diff --git a/kfet/middleware.py b/kfet/auth/middleware.py
similarity index 95%
rename from kfet/middleware.py
rename to kfet/auth/middleware.py
index 9502d393..1a930c3b 100644
--- a/kfet/middleware.py
+++ b/kfet/auth/middleware.py
@@ -2,7 +2,7 @@
from django.contrib.auth.models import User
-from kfet.backends import KFetBackend
+from .backends import KFetBackend
class KFetAuthenticationMiddleware(object):
diff --git a/kfet/auth/migrations/0001_initial.py b/kfet/auth/migrations/0001_initial.py
new file mode 100644
index 00000000..30dfca70
--- /dev/null
+++ b/kfet/auth/migrations/0001_initial.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('auth', '0006_require_contenttypes_0002'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='GenericTeamToken',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', auto_created=True, serialize=False, primary_key=True)),
+ ('token', models.CharField(unique=True, max_length=50)),
+ ],
+ ),
+ ]
diff --git a/kfet/auth/migrations/__init__.py b/kfet/auth/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/kfet/auth/models.py b/kfet/auth/models.py
new file mode 100644
index 00000000..53aef6c9
--- /dev/null
+++ b/kfet/auth/models.py
@@ -0,0 +1,5 @@
+from django.db import models
+
+
+class GenericTeamToken(models.Model):
+ token = models.CharField(max_length=50, unique=True)
diff --git a/kfet/templates/kfet/login_genericteam.html b/kfet/auth/templates/kfet/login_genericteam.html
similarity index 100%
rename from kfet/templates/kfet/login_genericteam.html
rename to kfet/auth/templates/kfet/login_genericteam.html
diff --git a/kfet/tests/test_forms.py b/kfet/auth/tests.py
similarity index 100%
rename from kfet/tests/test_forms.py
rename to kfet/auth/tests.py
diff --git a/kfet/auth/views.py b/kfet/auth/views.py
new file mode 100644
index 00000000..ce44b007
--- /dev/null
+++ b/kfet/auth/views.py
@@ -0,0 +1,69 @@
+from django.contrib import messages
+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.db.models import Prefetch
+from django.shortcuts import render
+from django.utils.crypto import get_random_string
+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)
+
+ # 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)
+
+ messages.success(request, "Connecté en utilisateur partagé")
+
+ return cas_logout or render(request, "kfet/login_genericteam.html")
+
+
+@permission_required('kfet.manage_perms')
+def account_group(request):
+ user_pre = Prefetch(
+ 'user_set',
+ queryset=User.objects.select_related('profile__account_kfet'),
+ )
+ groups = (
+ Group.objects
+ .filter(name__icontains='K-Fêt')
+ .prefetch_related('permissions', user_pre)
+ )
+ return render(request, 'kfet/account_group.html', {
+ 'groups': groups,
+ })
+
+
+class AccountGroupCreate(SuccessMessageMixin, CreateView):
+ model = Group
+ template_name = 'kfet/account_group_form.html'
+ form_class = GroupForm
+ success_message = 'Nouveau groupe : %(name)s'
+ success_url = reverse_lazy('kfet.account.group')
+
+
+class AccountGroupUpdate(SuccessMessageMixin, UpdateView):
+ queryset = Group.objects.filter(name__icontains='K-Fêt')
+ template_name = 'kfet/account_group_form.html'
+ form_class = GroupForm
+ success_message = 'Groupe modifié : %(name)s'
+ success_url = reverse_lazy('kfet.account.group')
diff --git a/kfet/context_processors.py b/kfet/context_processors.py
index 4c7b4fe4..04feec81 100644
--- a/kfet/context_processors.py
+++ b/kfet/context_processors.py
@@ -1,18 +1,7 @@
# -*- coding: utf-8 -*-
-from django.contrib.auth.context_processors import PermWrapper
-
from kfet.config import kfet_config
-def auth(request):
- if hasattr(request, 'real_user'):
- return {
- 'user': request.real_user,
- 'perms': PermWrapper(request.real_user),
- }
- return {}
-
-
def config(request):
return {'kfet_config': kfet_config}
diff --git a/kfet/forms.py b/kfet/forms.py
index 6ef3aefb..66638e6c 100644
--- a/kfet/forms.py
+++ b/kfet/forms.py
@@ -5,9 +5,8 @@ from decimal import Decimal
from django import forms
from django.core.exceptions import ValidationError
-from django.contrib.auth.models import User, Group, Permission
-from django.contrib.contenttypes.models import ContentType
-from django.forms import modelformset_factory, widgets
+from django.contrib.auth.models import User
+from django.forms import modelformset_factory
from django.utils import timezone
from djconfig.forms import ConfigForm
@@ -18,6 +17,8 @@ from kfet.models import (
TransferGroup, Supplier)
from gestioncof.models import CofProfile
+from .auth.forms import UserGroupForm # noqa
+
# -----
# Widgets
@@ -128,60 +129,6 @@ class UserRestrictTeamForm(UserForm):
fields = ['first_name', 'last_name', 'email']
-class UserGroupForm(forms.ModelForm):
- groups = forms.ModelMultipleChoiceField(
- Group.objects.filter(name__icontains='K-Fêt'),
- label='Statut équipe',
- required=False)
-
- def clean_groups(self):
- kfet_groups = self.cleaned_data.get('groups')
- other_groups = self.instance.groups.exclude(name__icontains='K-Fêt')
- return list(kfet_groups) + list(other_groups)
-
- class Meta:
- model = User
- fields = ['groups']
-
-
-class KFetPermissionsField(forms.ModelMultipleChoiceField):
-
- def __init__(self, *args, **kwargs):
- queryset = Permission.objects.filter(
- content_type__in=ContentType.objects.filter(app_label="kfet"),
- )
- super().__init__(
- queryset=queryset,
- widget=widgets.CheckboxSelectMultiple,
- *args, **kwargs
- )
-
- def label_from_instance(self, obj):
- return obj.name
-
-
-class GroupForm(forms.ModelForm):
- permissions = KFetPermissionsField()
-
- def clean_name(self):
- name = self.cleaned_data['name']
- return 'K-Fêt %s' % name
-
- def clean_permissions(self):
- kfet_perms = self.cleaned_data['permissions']
- # TODO: With Django >=1.11, the QuerySet method 'difference' can be used.
- # other_groups = self.instance.permissions.difference(
- # self.fields['permissions'].queryset
- # )
- other_perms = self.instance.permissions.exclude(
- pk__in=[p.pk for p in self.fields['permissions'].queryset],
- )
- return list(kfet_perms) + list(other_perms)
-
- class Meta:
- model = Group
- fields = ['name', 'permissions']
-
class AccountNegativeForm(forms.ModelForm):
class Meta:
diff --git a/kfet/migrations/0058_delete_genericteamtoken.py b/kfet/migrations/0058_delete_genericteamtoken.py
new file mode 100644
index 00000000..ea8b55cd
--- /dev/null
+++ b/kfet/migrations/0058_delete_genericteamtoken.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('kfet', '0057_merge'),
+ ]
+
+ operations = [
+ migrations.DeleteModel(
+ name='GenericTeamToken',
+ ),
+ ]
diff --git a/kfet/models.py b/kfet/models.py
index ec146ad9..b06114d7 100644
--- a/kfet/models.py
+++ b/kfet/models.py
@@ -14,6 +14,8 @@ from datetime import date
import re
import hashlib
+from .auth.models import GenericTeamToken # noqa
+
from .config import kfet_config
from .utils import to_ukf
@@ -710,7 +712,3 @@ class Operation(models.Model):
return templates[self.type].format(nb=self.article_nb,
article=self.article,
amount=self.amount)
-
-
-class GenericTeamToken(models.Model):
- token = models.CharField(max_length = 50, unique = True)
diff --git a/kfet/views.py b/kfet/views.py
index c7eb677b..386eddb6 100644
--- a/kfet/views.py
+++ b/kfet/views.py
@@ -12,34 +12,30 @@ from django.views.generic.edit import CreateView, UpdateView
from django.core.urlresolvers import reverse, reverse_lazy
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
-from django.contrib.auth import authenticate, login
-from django.contrib.auth.decorators import login_required, permission_required
-from django.contrib.auth.models import User, Permission, Group
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth.models import User, Permission
from django.http import JsonResponse, Http404
from django.forms import formset_factory
from django.db import transaction
from django.db.models import F, Sum, Prefetch, Count
from django.db.models.functions import Coalesce
from django.utils import timezone
-from django.utils.crypto import get_random_string
from django.utils.decorators import method_decorator
-from django_cas_ng.views import logout as cas_logout_view
-
from gestioncof.models import CofProfile
from kfet.config import kfet_config
from kfet.decorators import teamkfet_required
from kfet.models import (
Account, Checkout, Article, AccountNegative,
- CheckoutStatement, GenericTeamToken, Supplier, SupplierArticle, Inventory,
+ CheckoutStatement, Supplier, SupplierArticle, Inventory,
InventoryArticle, Order, OrderArticle, Operation, OperationGroup,
TransferGroup, Transfer, ArticleCategory)
from kfet.forms import (
AccountTriForm, AccountBalanceForm, AccountNoTriForm, UserForm, CofForm,
UserRestrictTeamForm, UserGroupForm, AccountForm, CofRestrictForm,
AccountPwdForm, AccountNegativeForm, UserRestrictForm, AccountRestrictForm,
- GroupForm, CheckoutForm, CheckoutRestrictForm, CheckoutStatementCreateForm,
+ CheckoutForm, CheckoutRestrictForm, CheckoutStatementCreateForm,
CheckoutStatementUpdateForm, ArticleForm, ArticleRestrictForm,
KPsulOperationGroupForm, KPsulAccountForm, KPsulCheckoutForm,
KPsulOperationFormSet, AddcostForm, FilterHistoryForm,
@@ -54,25 +50,9 @@ import heapq
import statistics
from kfet.statistic import ScaleMixin, last_stats_manifest, tot_ventes, WeekScale
-
-@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)
-
- # 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)
-
- messages.success(request, "Connecté en utilisateur partagé")
-
- return cas_logout or render(request, "kfet/login_genericteam.html")
+from .auth.views import ( # noqa
+ account_group, login_genericteam, AccountGroupCreate, AccountGroupUpdate,
+)
def put_cleaned_data_in_dict(dict, form):
@@ -505,37 +485,6 @@ def account_update(request, trigramme):
})
-@permission_required('kfet.manage_perms')
-def account_group(request):
- user_pre = Prefetch(
- 'user_set',
- queryset=User.objects.select_related('profile__account_kfet'),
- )
- groups = (
- Group.objects
- .filter(name__icontains='K-Fêt')
- .prefetch_related('permissions', user_pre)
- )
- return render(request, 'kfet/account_group.html', {
- 'groups': groups,
- })
-
-
-class AccountGroupCreate(SuccessMessageMixin, CreateView):
- model = Group
- template_name = 'kfet/account_group_form.html'
- form_class = GroupForm
- success_message = 'Nouveau groupe : %(name)s'
- success_url = reverse_lazy('kfet.account.group')
-
-class AccountGroupUpdate(SuccessMessageMixin, UpdateView):
- queryset = Group.objects.filter(name__icontains='K-Fêt')
- template_name = 'kfet/account_group_form.html'
- form_class = GroupForm
- success_message = 'Groupe modifié : %(name)s'
- success_url = reverse_lazy('kfet.account.group')
-
-
class AccountNegativeList(ListView):
queryset = (
AccountNegative.objects
From 4091185a684a6d01d15f55fa452f3fa39d8ba8e3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Martin=20P=C3=A9pin?=
Date: Wed, 20 Sep 2017 18:19:15 +0200
Subject: [PATCH 012/101] import LDAP_SERVER_URL in settings
---
cof/settings/common.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/cof/settings/common.py b/cof/settings/common.py
index ba0b6044..799ecc52 100644
--- a/cof/settings/common.py
+++ b/cof/settings/common.py
@@ -45,6 +45,7 @@ RECAPTCHA_PUBLIC_KEY = import_secret("RECAPTCHA_PUBLIC_KEY")
RECAPTCHA_PRIVATE_KEY = import_secret("RECAPTCHA_PRIVATE_KEY")
KFETOPEN_TOKEN = import_secret("KFETOPEN_TOKEN")
+LDAP_SERVER_URL = import_secret("LDAP_SERVER_URL")
BASE_DIR = os.path.dirname(
From d89ba1efe5fd7878bf4883481e5bbf270f6ecb45 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Martin=20P=C3=A9pin?=
Date: Wed, 20 Sep 2017 18:21:59 +0200
Subject: [PATCH 013/101] Fix catalogue behaviour if id=0
---
bda/views.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/bda/views.py b/bda/views.py
index d6da2e9f..84b6c9d3 100644
--- a/bda/views.py
+++ b/bda/views.py
@@ -782,9 +782,9 @@ def catalogue(request, request_type):
.select_related('location')
.prefetch_related('quote_set')
)
- if categories_id:
+ if categories_id and 0 not in categories_id:
shows_qs = shows_qs.filter(category__id__in=categories_id)
- if locations_id:
+ if locations_id and 0 not in locations_id:
shows_qs = shows_qs.filter(location__id__in=locations_id)
# On convertit les descriptions à envoyer en une liste facilement
From 6f2652c4858b3bc2e20093422388659c38bde02f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Martin=20P=C3=A9pin?=
Date: Wed, 20 Sep 2017 18:23:36 +0200
Subject: [PATCH 014/101] Prod quick hack for Mega export
---
gestioncof/views.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/gestioncof/views.py b/gestioncof/views.py
index c5701510..ec9f6efd 100644
--- a/gestioncof/views.py
+++ b/gestioncof/views.py
@@ -577,7 +577,7 @@ def export_members(request):
writer = unicodecsv.writer(response)
for profile in CofProfile.objects.filter(is_cof=True).all():
user = profile.user
- bits = [profile.id, user.username, user.first_name, user.last_name,
+ bits = [user.id, user.username, user.first_name, user.last_name,
user.email, profile.phone, profile.occupation,
profile.departement, profile.type_cotiz]
writer.writerow([str(bit) for bit in bits])
@@ -596,7 +596,7 @@ def csv_export_mega(filename, qs):
comments = "---".join(
[comment.content for comment in reg.comments.all()])
bits = [user.username, user.first_name, user.last_name, user.email,
- profile.phone, profile.id,
+ profile.phone, user.id,
profile.comments if profile.comments else "", comments]
writer.writerow([str(bit) for bit in bits])
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 015/101] 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 016/101] 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 017/101] 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 018/101] 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 019/101] 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
+
+{% 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 020/101] 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 021/101] 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 022/101] 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 023/101] 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
From 503b305299fe66f9b84195312049b48962b6337e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Martin=20P=C3=A9pin?=
Date: Tue, 10 Oct 2017 18:34:18 +0200
Subject: [PATCH 024/101] djangorestframework 3.7 breaks with Django 1.8
JSONField doesn't exist in Django 1.8
---
requirements.txt | 3 +++
1 file changed, 3 insertions(+)
diff --git a/requirements.txt b/requirements.txt
index f3964212..1591656d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -26,5 +26,8 @@ python-dateutil
wagtail==1.10.*
wagtailmenus==2.2.*
+# Remove this when we switch to Django 1.11
+djangorestframework==3.6.4
+
# Production tools
wheel
From 29ef297b2a83177820d5cb087a5869f0c4996eb1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Martin=20P=C3=A9pin?=
Date: Tue, 10 Oct 2017 15:48:38 +0200
Subject: [PATCH 025/101] =?UTF-8?q?try=20to=20set=20the=20redis=20password?=
=?UTF-8?q?=E2=80=A6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitlab-ci.yml | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 85be668b..f297cf40 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -20,7 +20,6 @@ variables:
# psql password authentication
PGPASSWORD: $POSTGRES_PASSWORD
-
cache:
paths:
- vendor/python
@@ -31,10 +30,10 @@ before_script:
- mkdir -p vendor/{python,pip,apt}
- 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
+ - sed -i.bak -E 's;^REDIS_PASSWD = .*$;REDIS_PASSWD = "";' cof/settings/secret.py
# 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
From e0ab7f5f94b5812bb69551a84ff5569305beb8c4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Martin=20P=C3=A9pin?=
Date: Tue, 10 Oct 2017 21:21:28 +0200
Subject: [PATCH 026/101] Fix migration conflict
---
kfet/migrations/0057_merge.py | 3 ++-
kfet/migrations/0058_amend_supplier.py | 2 +-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/kfet/migrations/0057_merge.py b/kfet/migrations/0057_merge.py
index 48f63399..e3cadb23 100644
--- a/kfet/migrations/0057_merge.py
+++ b/kfet/migrations/0057_merge.py
@@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
-from django.db import migrations, models
+from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
+ ('kfet', '0057_add_perms_config'),
('kfet', '0056_change_account_meta'),
('kfet', '0054_update_promos'),
]
diff --git a/kfet/migrations/0058_amend_supplier.py b/kfet/migrations/0058_amend_supplier.py
index 0b45dade..764322b0 100644
--- a/kfet/migrations/0058_amend_supplier.py
+++ b/kfet/migrations/0058_amend_supplier.py
@@ -7,7 +7,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('kfet', '0057_add_perms_config'),
+ ('kfet', '0057_merge'),
]
operations = [
From 3f6c5be74836e325a5f6d8ee16e9499ba358c305 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Martin=20P=C3=A9pin?=
Date: Tue, 10 Oct 2017 21:27:15 +0200
Subject: [PATCH 027/101] Upgrade python packages before testing
---
.gitlab-ci.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index f297cf40..b00817de 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -33,7 +33,7 @@ before_script:
- sed -i.bak -E 's;^REDIS_PASSWD = .*$;REDIS_PASSWD = "";' cof/settings/secret.py
# 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
+ - pip install --upgrade --cache-dir vendor/pip -t vendor/python -r requirements.txt
test:
stage: test
From f4a7e9dbf1c7075f13c7673dba698af76ac42320 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?=
Date: Wed, 11 Oct 2017 23:34:43 +0200
Subject: [PATCH 028/101] Verbosity should stay calm.
---
.gitlab-ci.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b00817de..19bcc736 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -38,4 +38,4 @@ before_script:
test:
stage: test
script:
- - python manage.py test -v3
+ - python manage.py test
From 85657591f5f69f19fec15d94f6e1d3d7a72ebc85 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?=
Date: Thu, 12 Oct 2017 11:10:30 +0200
Subject: [PATCH 029/101] =?UTF-8?q?Fix=20fields=20cleaning=20with=20unreac?=
=?UTF-8?q?hable=20items=20when=E2=80=A6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
… object is being created.
---
kfet/forms.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/kfet/forms.py b/kfet/forms.py
index 6ef3aefb..7bafbdd7 100644
--- a/kfet/forms.py
+++ b/kfet/forms.py
@@ -136,6 +136,8 @@ class UserGroupForm(forms.ModelForm):
def clean_groups(self):
kfet_groups = self.cleaned_data.get('groups')
+ if self.instance.pk is None:
+ return kfet_groups
other_groups = self.instance.groups.exclude(name__icontains='K-Fêt')
return list(kfet_groups) + list(other_groups)
@@ -173,6 +175,8 @@ class GroupForm(forms.ModelForm):
# other_groups = self.instance.permissions.difference(
# self.fields['permissions'].queryset
# )
+ if self.instance.pk is None:
+ return kfet_perms
other_perms = self.instance.permissions.exclude(
pk__in=[p.pk for p in self.fields['permissions'].queryset],
)
From fccad5edee83df4726cf54aa19dee5b02661db1c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Martin=20P=C3=A9pin?=
Date: Mon, 16 Oct 2017 14:31:02 +0200
Subject: [PATCH 030/101] rename root -> kfet_genericteam in fixtures
---
kfet/cms/fixtures/kfet_wagtail_17_05.json | 86 +++++++++++------------
1 file changed, 43 insertions(+), 43 deletions(-)
diff --git a/kfet/cms/fixtures/kfet_wagtail_17_05.json b/kfet/cms/fixtures/kfet_wagtail_17_05.json
index f6a46c30..66ac7040 100644
--- a/kfet/cms/fixtures/kfet_wagtail_17_05.json
+++ b/kfet/cms/fixtures/kfet_wagtail_17_05.json
@@ -53,7 +53,7 @@
"kfetpage"
],
"owner": [
- "root"
+ "kfet_genericteam"
],
"expired": false,
"first_published_at": "2017-05-28T04:20:00.000Z",
@@ -83,7 +83,7 @@
"kfetpage"
],
"owner": [
- "root"
+ "kfet_genericteam"
],
"expired": false,
"first_published_at": "2017-05-28T04:20:00.000Z",
@@ -113,7 +113,7 @@
"kfetpage"
],
"owner": [
- "root"
+ "kfet_genericteam"
],
"expired": false,
"first_published_at": "2017-05-28T04:20:00.000Z",
@@ -143,7 +143,7 @@
"kfetpage"
],
"owner": [
- "root"
+ "kfet_genericteam"
],
"expired": false,
"first_published_at": "2017-05-28T04:20:00.000Z",
@@ -173,7 +173,7 @@
"kfetpage"
],
"owner": [
- "root"
+ "kfet_genericteam"
],
"expired": false,
"first_published_at": "2017-05-28T04:20:00.000Z",
@@ -203,7 +203,7 @@
"kfetpage"
],
"owner": [
- "root"
+ "kfet_genericteam"
],
"expired": false,
"first_published_at": "2017-05-28T04:20:00.000Z",
@@ -233,7 +233,7 @@
"page"
],
"owner": [
- "root"
+ "kfet_genericteam"
],
"expired": false,
"first_published_at": "2017-05-28T04:20:00.000Z",
@@ -263,7 +263,7 @@
"kfetpage"
],
"owner": [
- "root"
+ "kfet_genericteam"
],
"expired": false,
"first_published_at": "2017-05-28T04:20:00.000Z",
@@ -681,7 +681,7 @@
"fields": {
"created_at": "2017-05-30T04:20:00.000Z",
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"collection": 2,
"title": "K-F\u00eat - Plan d'acc\u00e8s",
@@ -694,7 +694,7 @@
"fields": {
"created_at": "2017-05-30T04:20:00.000Z",
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"collection": 2,
"title": "K-F\u00eat - Demande d'autorisation",
@@ -707,7 +707,7 @@
"fields": {
"created_at": "2017-05-30T04:20:00.000Z",
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"collection": 2,
"title": "K-F\u00eat - Trait\u00e9 de Flipper Th\u00e9orique",
@@ -730,7 +730,7 @@
"title": "K-F\u00eat - Amazon Hunt",
"width": 200,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
]
}
},
@@ -750,7 +750,7 @@
"title": "K-F\u00eat - Fun Machine",
"width": 200,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
]
}
},
@@ -767,7 +767,7 @@
"title": "Hugo Manet",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -787,7 +787,7 @@
"title": "Lisa Gourdon",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -807,7 +807,7 @@
"title": "Pierre Quesselaire",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -827,7 +827,7 @@
"title": "Thibault Scoquard",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -847,7 +847,7 @@
"title": "Arnaud Fanthomme",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -867,7 +867,7 @@
"title": "Vincent Balerdi",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -887,7 +887,7 @@
"title": "Nathana\u00ebl Willaime",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -907,7 +907,7 @@
"title": "\u00c9lisabeth Miller",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -927,7 +927,7 @@
"title": "Arthur Lesage",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -947,7 +947,7 @@
"title": "Sarah Asset",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -967,7 +967,7 @@
"title": "Alexandre Legrand",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -987,7 +987,7 @@
"title": "\u00c9tienne Baudel",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -1007,7 +1007,7 @@
"title": "Marine Snape",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -1027,7 +1027,7 @@
"title": "Anatole Gosset",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -1047,7 +1047,7 @@
"title": "Jacko Rastikian",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -1067,7 +1067,7 @@
"title": "Alexandre Jannaud",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -1087,7 +1087,7 @@
"title": "Aur\u00e9lien Delobelle",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -1107,7 +1107,7 @@
"title": "Sylvain Douteau",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -1127,7 +1127,7 @@
"title": "Rapha\u00ebl Lescanne",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -1147,7 +1147,7 @@
"title": "Romain Gourvil",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -1167,7 +1167,7 @@
"title": "Marie Labeye",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -1187,7 +1187,7 @@
"title": "Oscar Blumberg",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -1207,7 +1207,7 @@
"title": "Za\u00efd Allybokus",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -1227,7 +1227,7 @@
"title": "Damien Garreau",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -1247,7 +1247,7 @@
"title": "Andr\u00e9a Londono-Lopez",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -1267,7 +1267,7 @@
"title": "Tristan Roussel",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -1287,7 +1287,7 @@
"title": "Guillaume Vernade",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -1307,7 +1307,7 @@
"title": "Lucas Mercier",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -1327,7 +1327,7 @@
"title": "Fran\u00e7ois Maillot",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@@ -1347,7 +1347,7 @@
"title": "Fabrice Catoire",
"collection": 2,
"uploaded_by_user": [
- "root"
+ "kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
From 46187659ed7a91c11030308cdfb2879413c14526 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?=
Date: Tue, 17 Oct 2017 14:41:53 +0200
Subject: [PATCH 031/101] Fix tirage pk conflicts with postgres
---
bda/migrations/0002_add_tirage.py | 58 +++++++++++++++++++++++--------
1 file changed, 43 insertions(+), 15 deletions(-)
diff --git a/bda/migrations/0002_add_tirage.py b/bda/migrations/0002_add_tirage.py
index 1956a4a4..22c387a0 100644
--- a/bda/migrations/0002_add_tirage.py
+++ b/bda/migrations/0002_add_tirage.py
@@ -5,17 +5,34 @@ from django.db import migrations, models
from django.conf import settings
from django.utils import timezone
-def forwards_func(apps, schema_editor):
+
+def fill_tirage_fields(apps, schema_editor):
+ """
+ Create a `Tirage` to fill new field `tirage` of `Participant`
+ and `Spectacle` already existing.
+ """
+ Participant = apps.get_model("bda", "Participant")
+ Spectacle = apps.get_model("bda", "Spectacle")
Tirage = apps.get_model("bda", "Tirage")
- db_alias = schema_editor.connection.alias
- Tirage.objects.using(db_alias).bulk_create([
- Tirage(
- id=1,
- title="Tirage de test (migration)",
- active=False,
- ouverture=timezone.now(),
- fermeture=timezone.now()),
- ])
+
+ # These querysets only contains instances not linked to any `Tirage`.
+ participants = Participant.objects.filter(tirage=None)
+ spectacles = Spectacle.objects.filter(tirage=None)
+
+ if not participants.count() and not spectacles.count():
+ # No need to create a "trash" tirage.
+ return
+
+ tirage = Tirage.objects.create(
+ title="Tirage de test (migration)",
+ active=False,
+ ouverture=timezone.now(),
+ fermeture=timezone.now(),
+ )
+
+ participants.update(tirage=tirage)
+ spectacles.update(tirage=tirage)
+
class Migration(migrations.Migration):
@@ -35,22 +52,33 @@ class Migration(migrations.Migration):
('active', models.BooleanField(default=True, verbose_name=b'Tirage actif')),
],
),
- migrations.RunPython(forwards_func, migrations.RunPython.noop),
migrations.AlterField(
model_name='participant',
name='user',
field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
),
+ # Create fields `spectacle` for `Participant` and `Spectacle` models.
+ # These fields are not nullable, but we first create them as nullable
+ # to give a default value for existing instances of these models.
migrations.AddField(
model_name='participant',
name='tirage',
- field=models.ForeignKey(default=1, to='bda.Tirage'),
- preserve_default=False,
+ field=models.ForeignKey(to='bda.Tirage', null=True),
),
migrations.AddField(
model_name='spectacle',
name='tirage',
- field=models.ForeignKey(default=1, to='bda.Tirage'),
- preserve_default=False,
+ field=models.ForeignKey(to='bda.Tirage', null=True),
+ ),
+ migrations.RunPython(fill_tirage_fields, migrations.RunPython.noop),
+ migrations.AlterField(
+ model_name='participant',
+ name='tirage',
+ field=models.ForeignKey(to='bda.Tirage'),
+ ),
+ migrations.AlterField(
+ model_name='spectacle',
+ name='tirage',
+ field=models.ForeignKey(to='bda.Tirage'),
),
]
From 732e47707e96bb7047438edb1791511e89918fa1 Mon Sep 17 00:00:00 2001
From: Ludovic Stephan
Date: Mon, 23 Oct 2017 17:25:58 +0200
Subject: [PATCH 032/101] Add unsubscribe option + list of current draws
---
bda/forms.py | 41 +++++++++++++++++++--
bda/templates/bda/revente-tirages.html | 28 +++++++++++++++
bda/urls.py | 11 +++---
bda/views.py | 50 +++++++++++++++++++++++++-
gestioncof/templates/home.html | 7 ++--
5 files changed, 126 insertions(+), 11 deletions(-)
create mode 100644 bda/templates/bda/revente-tirages.html
diff --git a/bda/forms.py b/bda/forms.py
index c0417d1e..139ef45d 100644
--- a/bda/forms.py
+++ b/bda/forms.py
@@ -4,7 +4,7 @@ from django import forms
from django.forms.models import BaseInlineFormSet
from django.utils import timezone
-from bda.models import Attribution, Spectacle
+from bda.models import Attribution, Spectacle, SpectacleRevente
class InscriptionInlineFormSet(BaseInlineFormSet):
@@ -45,6 +45,9 @@ class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj):
return "%s" % str(obj.spectacle)
+class ReventeModelMultipleChoiceField(forms.ModelMultipleChoiceField):
+ def label_from_instance(self, obj):
+ return "%s" % str(obj.attribution.spectacle)
class ResellForm(forms.Form):
attributions = AttributionModelMultipleChoiceField(
@@ -63,7 +66,6 @@ class ResellForm(forms.Form):
'participant__user')
)
-
class AnnulForm(forms.Form):
attributions = AttributionModelMultipleChoiceField(
label='',
@@ -83,7 +85,6 @@ class AnnulForm(forms.Form):
'participant__user')
)
-
class InscriptionReventeForm(forms.Form):
spectacles = forms.ModelMultipleChoiceField(
queryset=Spectacle.objects.none(),
@@ -98,6 +99,40 @@ class InscriptionReventeForm(forms.Form):
.filter(date__gte=timezone.now())
)
+class ReventeTirageAnnulForm(forms.Form):
+ reventes = ReventeModelMultipleChoiceField(
+ label='',
+ queryset=SpectacleRevente.objects.none(),
+ widget=forms.CheckboxSelectMultiple,
+ required=False
+ )
+
+ def __init__(self, participant, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields['reventes'].queryset = (
+ participant.wanted.filter(soldTo__isnull=True)
+ .select_related('attribution__spectacle')
+ )
+
+
+class ReventeTirageForm(forms.Form):
+ reventes = ReventeModelMultipleChoiceField(
+ label='',
+ queryset=SpectacleRevente.objects.none(),
+ widget=forms.CheckboxSelectMultiple,
+ required=False
+ )
+
+ def __init__(self, participant, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields['reventes'].queryset = (
+ SpectacleRevente.objects.filter(
+ notif_sent=True,
+ shotgun=False,
+ tirage_done=False
+ ).exclude(answered_mail=participant)
+ .select_related('attribution__spectacle')
+ )
class SoldForm(forms.Form):
attributions = AttributionModelMultipleChoiceField(
diff --git a/bda/templates/bda/revente-tirages.html b/bda/templates/bda/revente-tirages.html
new file mode 100644
index 00000000..bd738673
--- /dev/null
+++ b/bda/templates/bda/revente-tirages.html
@@ -0,0 +1,28 @@
+{% extends "base_title.html" %}
+{% load bootstrap %}
+
+{% block realcontent %}
+
+Tirages au sort de reventes
+{% if annulform.reventes %}
+Mes inscriptions
+
+{% endif %}
+
+{% if subform.reventes %}
+Tirages en cours
+
+{% endif %}
+{% endblock %}
diff --git a/bda/urls.py b/bda/urls.py
index 876c84ea..51dd4235 100644
--- a/bda/urls.py
+++ b/bda/urls.py
@@ -32,16 +32,19 @@ urlpatterns = [
url(r'^spectacles/unpaid/(?P\d+)$',
views.unpaid,
name="bda-unpaid"),
- url(r'^liste-revente/(?P\d+)$',
+ url(r'^revente/(?P\d+)/list$',
views.list_revente,
name="bda-liste-revente"),
- url(r'^buy-revente/(?P\d+)$',
+ url(r'^revente/(?P\d+)/tirages$',
+ views.revente_tirages,
+ name="bda-revente-tirages"),
+ url(r'^revente/(?P\d+)/buy$',
views.buy_revente,
name="bda-buy-revente"),
- url(r'^revente-interested/(?P\d+)$',
+ url(r'^revente/(?P\d+)/interested$',
views.revente_interested,
name='bda-revente-interested'),
- url(r'^revente-immediat/(?P\d+)$',
+ url(r'^revente/(?P\d+)/immediat$',
views.revente_shotgun,
name="bda-shotgun"),
url(r'^mails-rappel/(?P\d+)$',
diff --git a/bda/views.py b/bda/views.py
index 84b6c9d3..4b75c116 100644
--- a/bda/views.py
+++ b/bda/views.py
@@ -30,7 +30,7 @@ from bda.models import (
from bda.algorithm import Algorithm
from bda.forms import (
TokenForm, ResellForm, AnnulForm, InscriptionReventeForm, SoldForm,
- InscriptionInlineFormSet,
+ InscriptionInlineFormSet, ReventeTirageForm, ReventeTirageAnnulForm
)
@@ -377,6 +377,7 @@ def revente(request, tirage_id):
if not created:
revente.seller = participant
revente.date = timezone.now()
+ revente.wanted = Participant.objects.none()
revente.soldTo = None
revente.notif_sent = False
revente.tirage_done = False
@@ -442,6 +443,53 @@ def revente(request, tirage_id):
"annulform": annulform, "resellform": resellform})
+@login_required
+def revente_tirages(request, tirage_id):
+ tirage = get_object_or_404(Tirage, id=tirage_id)
+ participant, _ = Participant.objects.get_or_create(
+ user=request.user, tirage=tirage)
+ unsub = 0
+ subform = ReventeTirageForm(participant, prefix="subscribe")
+ annulform = ReventeTirageAnnulForm(participant, prefix="annul")
+
+ if request.method == 'POST':
+ if "subscribe" in request.POST:
+ subform = ReventeTirageForm(participant, request.POST,
+ prefix="subscribe")
+ if subform.is_valid():
+ sub = 0
+ reventes = subform.cleaned_data['reventes']
+ for revente in reventes:
+ revente.answered_mail.add(participant)
+ sub += 1
+ if sub > 0:
+ plural = "s" if sub > 1 else ""
+ messages.success(
+ request,
+ "Tu as bien été inscrit à {} revente{}"
+ .format(sub, plural)
+ )
+ elif "annul" in request.POST:
+ annulform = ReventeTirageAnnulForm(participant, request.POST,
+ prefix="annul")
+ if annulform.is_valid():
+ unsub = 0
+ reventes = annulform.cleaned_data['reventes']
+ for revente in reventes:
+ revente.answered_mail.remove(participant)
+ unsub += 1
+ if unsub > 0:
+ plural = "s" if unsub > 1 else ""
+ messages.success(
+ request,
+ "Tu as bien été désinscrit de {} revente{}"
+ .format(unsub, plural)
+ )
+
+ return render(request, "bda/revente-tirages.html",
+ {"annulform": annulform, "subform": subform})
+
+
@login_required
def revente_interested(request, revente_id):
revente = get_object_or_404(SpectacleRevente, id=revente_id)
diff --git a/gestioncof/templates/home.html b/gestioncof/templates/home.html
index acc04f30..f7ca57b5 100644
--- a/gestioncof/templates/home.html
+++ b/gestioncof/templates/home.html
@@ -43,9 +43,10 @@
État des demandes
{% else %}
Mes places
- Revendre une place
- S'inscrire à BdA-Revente
- Places disponibles immédiatement
+ Gestion de mes reventes
+ Reventes en cours
+ S'inscrire à BdA-Revente
+ Places disponibles immédiatement
{% endif %}
{% endfor %}
From e74dbb11f1556a4ffb3cc42f83686a3a27cf5f45 Mon Sep 17 00:00:00 2001
From: Ludovic Stephan
Date: Mon, 23 Oct 2017 18:39:45 +0200
Subject: [PATCH 033/101] Organize revente files and function names
---
.../revente/confirm-shotgun.html} | 0
.../revente/confirmed.html} | 0
.../revente/mail-success.html} | 0
.../{reventes.html => revente/manage.html} | 0
.../revente/none.html} | 0
.../revente/notpaid.html} | 0
.../revente/shotgun.html} | 2 +-
.../subscribe.html} | 0
.../tirages.html} | 0
.../revente/wrongtime.html} | 2 +-
bda/urls.py | 30 +++++++++++--------
bda/views.py | 30 +++++++++----------
gestioncof/management/data/custommail.json | 2 +-
gestioncof/templates/home.html | 6 ++--
14 files changed, 38 insertions(+), 34 deletions(-)
rename bda/templates/{revente-confirm.html => bda/revente/confirm-shotgun.html} (100%)
rename bda/templates/{bda-interested.html => bda/revente/confirmed.html} (100%)
rename bda/templates/{bda-success.html => bda/revente/mail-success.html} (100%)
rename bda/templates/bda/{reventes.html => revente/manage.html} (100%)
rename bda/templates/{bda-no-revente.html => bda/revente/none.html} (100%)
rename bda/templates/{bda-notpaid.html => bda/revente/notpaid.html} (100%)
rename bda/templates/{bda-shotgun.html => bda/revente/shotgun.html} (83%)
rename bda/templates/bda/{liste-reventes.html => revente/subscribe.html} (100%)
rename bda/templates/bda/{revente-tirages.html => revente/tirages.html} (100%)
rename bda/templates/{bda-wrongtime.html => bda/revente/wrongtime.html} (86%)
diff --git a/bda/templates/revente-confirm.html b/bda/templates/bda/revente/confirm-shotgun.html
similarity index 100%
rename from bda/templates/revente-confirm.html
rename to bda/templates/bda/revente/confirm-shotgun.html
diff --git a/bda/templates/bda-interested.html b/bda/templates/bda/revente/confirmed.html
similarity index 100%
rename from bda/templates/bda-interested.html
rename to bda/templates/bda/revente/confirmed.html
diff --git a/bda/templates/bda-success.html b/bda/templates/bda/revente/mail-success.html
similarity index 100%
rename from bda/templates/bda-success.html
rename to bda/templates/bda/revente/mail-success.html
diff --git a/bda/templates/bda/reventes.html b/bda/templates/bda/revente/manage.html
similarity index 100%
rename from bda/templates/bda/reventes.html
rename to bda/templates/bda/revente/manage.html
diff --git a/bda/templates/bda-no-revente.html b/bda/templates/bda/revente/none.html
similarity index 100%
rename from bda/templates/bda-no-revente.html
rename to bda/templates/bda/revente/none.html
diff --git a/bda/templates/bda-notpaid.html b/bda/templates/bda/revente/notpaid.html
similarity index 100%
rename from bda/templates/bda-notpaid.html
rename to bda/templates/bda/revente/notpaid.html
diff --git a/bda/templates/bda-shotgun.html b/bda/templates/bda/revente/shotgun.html
similarity index 83%
rename from bda/templates/bda-shotgun.html
rename to bda/templates/bda/revente/shotgun.html
index e10fae00..fae36c04 100644
--- a/bda/templates/bda-shotgun.html
+++ b/bda/templates/bda/revente/shotgun.html
@@ -5,7 +5,7 @@
{% if shotgun %}
{% for spectacle in shotgun %}
- - {{spectacle}}
+ - {{spectacle}}
{% endfor %}
{% else %}
Pas de places disponibles immédiatement, désolé !
diff --git a/bda/templates/bda/liste-reventes.html b/bda/templates/bda/revente/subscribe.html
similarity index 100%
rename from bda/templates/bda/liste-reventes.html
rename to bda/templates/bda/revente/subscribe.html
diff --git a/bda/templates/bda/revente-tirages.html b/bda/templates/bda/revente/tirages.html
similarity index 100%
rename from bda/templates/bda/revente-tirages.html
rename to bda/templates/bda/revente/tirages.html
diff --git a/bda/templates/bda-wrongtime.html b/bda/templates/bda/revente/wrongtime.html
similarity index 86%
rename from bda/templates/bda-wrongtime.html
rename to bda/templates/bda/revente/wrongtime.html
index dfafb05f..18c417a2 100644
--- a/bda/templates/bda-wrongtime.html
+++ b/bda/templates/bda/revente/wrongtime.html
@@ -6,7 +6,7 @@
Le tirage au sort de cette revente a déjà été effectué !
Si personne n'était intéressé, elle est maintenant disponible
- ici.
+ ici.
{% else %}
Il n'est pas encore possible de s'inscrire à cette revente, réessaie dans quelque temps !
{% endif %}
diff --git a/bda/urls.py b/bda/urls.py
index 51dd4235..7588187c 100644
--- a/bda/urls.py
+++ b/bda/urls.py
@@ -16,9 +16,6 @@ urlpatterns = [
url(r'^places/(?P\d+)$',
views.places,
name="bda-places-attribuees"),
- url(r'^revente/(?P\d+)$',
- views.revente,
- name='bda-revente'),
url(r'^etat-places/(?P\d+)$',
views.etat_places,
name='bda-etat-places'),
@@ -32,21 +29,28 @@ urlpatterns = [
url(r'^spectacles/unpaid/(?P\d+)$',
views.unpaid,
name="bda-unpaid"),
- url(r'^revente/(?P\d+)/list$',
- views.list_revente,
- name="bda-liste-revente"),
+
+ # Urls BdA-Revente
+
+ url(r'^revente/(?P\d+)/manage$',
+ views.revente_manage,
+ name='bda-revente-manage'),
+ url(r'^revente/(?P\d+)/subscribe$',
+ views.revente_subscribe,
+ name="bda-revente-subscribe"),
url(r'^revente/(?P\d+)/tirages$',
views.revente_tirages,
name="bda-revente-tirages"),
url(r'^revente/(?P\d+)/buy$',
- views.buy_revente,
- name="bda-buy-revente"),
- url(r'^revente/(?P\d+)/interested$',
- views.revente_interested,
- name='bda-revente-interested'),
- url(r'^revente/(?P\d+)/immediat$',
+ views.revente_buy,
+ name="bda-revente-buy"),
+ url(r'^revente/(?P\d+)/confirm$',
+ views.revente_confirm,
+ name='bda-revente-confirm'),
+ url(r'^revente/(?P\d+)/shotgun$',
views.revente_shotgun,
- name="bda-shotgun"),
+ name="bda-revente-shotgun"),
+
url(r'^mails-rappel/(?P\d+)$',
views.send_rappel,
name="bda-rappels"
diff --git a/bda/views.py b/bda/views.py
index 4b75c116..c0e64230 100644
--- a/bda/views.py
+++ b/bda/views.py
@@ -349,13 +349,13 @@ def tirage(request, tirage_id):
@login_required
-def revente(request, tirage_id):
+def revente_manage(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id)
participant, created = Participant.objects.get_or_create(
user=request.user, tirage=tirage)
if not participant.paid:
- return render(request, "bda-notpaid.html", {})
+ return render(request, "bda/revente/notpaid.html", {})
resellform = ResellForm(participant, prefix='resell')
annulform = AnnulForm(participant, prefix='annul')
@@ -438,7 +438,7 @@ def revente(request, tirage_id):
.filter(
Q(revente__soldTo__isnull=True) | Q(revente__soldTo=participant))
- return render(request, "bda/reventes.html",
+ return render(request, "bda/revente/manage.html",
{'tirage': tirage, 'overdue': overdue, "soldform": soldform,
"annulform": annulform, "resellform": resellform})
@@ -486,27 +486,27 @@ def revente_tirages(request, tirage_id):
.format(unsub, plural)
)
- return render(request, "bda/revente-tirages.html",
+ return render(request, "bda/revente/tirages.html",
{"annulform": annulform, "subform": subform})
@login_required
-def revente_interested(request, revente_id):
+def revente_confirm(request, revente_id):
revente = get_object_or_404(SpectacleRevente, id=revente_id)
participant, _ = Participant.objects.get_or_create(
user=request.user, tirage=revente.attribution.spectacle.tirage)
if (timezone.now() < revente.date + timedelta(hours=1)) or revente.shotgun:
- return render(request, "bda-wrongtime.html",
+ return render(request, "bda/revente/wrongtime.html",
{"revente": revente})
revente.answered_mail.add(participant)
- return render(request, "bda-interested.html",
+ return render(request, "bda/revente/confirmed.html",
{"spectacle": revente.attribution.spectacle,
"date": revente.date_tirage})
@login_required
-def list_revente(request, tirage_id):
+def revente_subscribe(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id)
participant, _ = Participant.objects.get_or_create(
user=request.user, tirage=tirage)
@@ -560,11 +560,11 @@ def list_revente(request, tirage_id):
)
messages.info(request, msg, extra_tags="safe")
- return render(request, "bda/liste-reventes.html", {"form": form})
+ return render(request, "bda/revente/subscribe.html", {"form": form})
@login_required
-def buy_revente(request, spectacle_id):
+def revente_buy(request, spectacle_id):
spectacle = get_object_or_404(Spectacle, id=spectacle_id)
tirage = spectacle.tirage
participant, _ = Participant.objects.get_or_create(
@@ -578,13 +578,13 @@ def buy_revente(request, spectacle_id):
own_reventes = reventes.filter(seller=participant)
if len(own_reventes) > 0:
own_reventes[0].delete()
- return HttpResponseRedirect(reverse("bda-shotgun",
+ return HttpResponseRedirect(reverse("bda-revente-shotgun",
args=[tirage.id]))
reventes_shotgun = reventes.filter(shotgun=True)
if not reventes_shotgun:
- return render(request, "bda-no-revente.html", {})
+ return render(request, "bda/revente/none.html", {})
if request.POST:
revente = random.choice(reventes_shotgun)
@@ -601,11 +601,11 @@ def buy_revente(request, spectacle_id):
[revente.seller.user.email],
context=context,
)
- return render(request, "bda-success.html",
+ return render(request, "bda/revente/mail-success.html",
{"seller": revente.attribution.participant.user,
"spectacle": spectacle})
- return render(request, "revente-confirm.html",
+ return render(request, "bda/revente/confirm-shotgun.html",
{"spectacle": spectacle,
"user": request.user})
@@ -629,7 +629,7 @@ def revente_shotgun(request, tirage_id):
)
shotgun = [sp for sp in spectacles if len(sp.shotguns) > 0]
- return render(request, "bda-shotgun.html",
+ return render(request, "bda/revente/shotgun.html",
{"shotgun": shotgun})
diff --git a/gestioncof/management/data/custommail.json b/gestioncof/management/data/custommail.json
index 9ee9b1ea..bf59e5f6 100644
--- a/gestioncof/management/data/custommail.json
+++ b/gestioncof/management/data/custommail.json
@@ -151,7 +151,7 @@
"shortname": "bda-revente",
"subject": "{{ show }}",
"description": "Notification envoy\u00e9e \u00e0 toutes les personnes int\u00e9ress\u00e9es par un spectacle pour le signaler qu'une place vient d'\u00eatre mise en vente.",
- "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nSi ce spectacle t'int\u00e9resse toujours, merci de nous le signaler en cliquant\r\nsur ce lien : http://{{ site }}{% url \"bda-revente-interested\" revente.id %}.\r\nDans le cas o\u00f9 plusieurs personnes seraient int\u00e9ress\u00e9es, nous proc\u00e8derons \u00e0\r\nun tirage au sort le {{ revente.date_tirage|date:\"DATE_FORMAT\" }}.\r\n\r\nChaleureusement,\r\nLe BdA"
+ "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nSi ce spectacle t'int\u00e9resse toujours, merci de nous le signaler en cliquant\r\nsur ce lien : http://{{ site }}{% url \"bda-revente-confirm\" revente.id %}.\r\nDans le cas o\u00f9 plusieurs personnes seraient int\u00e9ress\u00e9es, nous proc\u00e8derons \u00e0\r\nun tirage au sort le {{ revente.date_tirage|date:\"DATE_FORMAT\" }}.\r\n\r\nChaleureusement,\r\nLe BdA"
}
},
{
diff --git a/gestioncof/templates/home.html b/gestioncof/templates/home.html
index f7ca57b5..943ef780 100644
--- a/gestioncof/templates/home.html
+++ b/gestioncof/templates/home.html
@@ -43,10 +43,10 @@
État des demandes
{% else %}
Mes places
- Gestion de mes reventes
+ Gestion de mes reventes
Reventes en cours
- S'inscrire à BdA-Revente
- Places disponibles immédiatement
+ S'inscrire à BdA-Revente
+ Places disponibles immédiatement
{% endif %}
{% endfor %}
From 919bcd197d077767bdc07355a70a84d39f8ebecf Mon Sep 17 00:00:00 2001
From: Ludovic Stephan
Date: Mon, 23 Oct 2017 18:59:30 +0200
Subject: [PATCH 034/101] Small code QoL improvements
---
bda/models.py | 10 ++++++++++
bda/views.py | 10 ++--------
2 files changed, 12 insertions(+), 8 deletions(-)
diff --git a/bda/models.py b/bda/models.py
index 41462d70..5533e3bb 100644
--- a/bda/models.py
+++ b/bda/models.py
@@ -252,6 +252,16 @@ class SpectacleRevente(models.Model):
class Meta:
verbose_name = "Revente"
+ def reset(self):
+ """Réinitialise la revente pour permettre une remise sur le marché"""
+ self.seller = self.attribution.participant
+ self.date = timezone.now()
+ self.answered_mail.clear()
+ self.soldTo = None
+ self.notif_sent = False
+ self.tirage_done = False
+ self.shotgun = False
+
def send_notif(self):
"""
Envoie une notification pour indiquer la mise en vente d'une place sur
diff --git a/bda/views.py b/bda/views.py
index c0e64230..311d530a 100644
--- a/bda/views.py
+++ b/bda/views.py
@@ -375,13 +375,7 @@ def revente_manage(request, tirage_id):
attribution=attribution,
defaults={'seller': participant})
if not created:
- revente.seller = participant
- revente.date = timezone.now()
- revente.wanted = Participant.objects.none()
- revente.soldTo = None
- revente.notif_sent = False
- revente.tirage_done = False
- revente.shotgun = False
+ revente.reset()
context = {
'vendeur': participant.user,
'show': attribution.spectacle,
@@ -495,7 +489,7 @@ def revente_confirm(request, revente_id):
revente = get_object_or_404(SpectacleRevente, id=revente_id)
participant, _ = Participant.objects.get_or_create(
user=request.user, tirage=revente.attribution.spectacle.tirage)
- if (timezone.now() < revente.date + timedelta(hours=1)) or revente.shotgun:
+ if not revente.notif_sent or revente.shotgun:
return render(request, "bda/revente/wrongtime.html",
{"revente": revente})
From 1b0e4285ecbc7ea224cb7fad9c725365d4a9ba01 Mon Sep 17 00:00:00 2001
From: Ludovic Stephan
Date: Mon, 23 Oct 2017 20:26:07 +0200
Subject: [PATCH 035/101] Reverse match fix
---
gestioncof/management/data/custommail.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gestioncof/management/data/custommail.json b/gestioncof/management/data/custommail.json
index bf59e5f6..029c03e0 100644
--- a/gestioncof/management/data/custommail.json
+++ b/gestioncof/management/data/custommail.json
@@ -161,7 +161,7 @@
"shortname": "bda-shotgun",
"subject": "{{ show }}",
"description": "Notification signalant qu'une place est au shotgun aux personnes int\u00e9ress\u00e9es.",
- "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nPuisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour\r\ncette place : elle est disponible imm\u00e9diatement \u00e0 l'adresse\r\nhttp://{{ site }}{% url \"bda-buy-revente\" show.id %}, \u00e0 la disposition de tous.\r\n\r\nChaleureusement,\r\nLe BdA"
+ "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nPuisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour\r\ncette place : elle est disponible imm\u00e9diatement \u00e0 l'adresse\r\nhttp://{{ site }}{% url \"bda-revente-buy\" show.id %}, \u00e0 la disposition de tous.\r\n\r\nChaleureusement,\r\nLe BdA"
}
},
{
From 684603709e90e471f5528ab35f1efc2011145fb5 Mon Sep 17 00:00:00 2001
From: Ludovic Stephan
Date: Mon, 23 Oct 2017 20:30:11 +0200
Subject: [PATCH 036/101] Class attributes and properties + more verbose log
SpectacleRevente gets brand new properties and attributes to simplify
code ; also, manage_reventes command output is more verbose
---
bda/management/commands/manage_reventes.py | 48 ++++++++++++++--------
bda/models.py | 45 +++++++++++++++++---
2 files changed, 69 insertions(+), 24 deletions(-)
diff --git a/bda/management/commands/manage_reventes.py b/bda/management/commands/manage_reventes.py
index 0302ec4b..5a767604 100644
--- a/bda/management/commands/manage_reventes.py
+++ b/bda/management/commands/manage_reventes.py
@@ -6,7 +6,6 @@ Gestion en ligne de commande des reventes.
from __future__ import unicode_literals
-from datetime import timedelta
from django.core.management import BaseCommand
from django.utils import timezone
from bda.models import SpectacleRevente
@@ -21,23 +20,36 @@ class Command(BaseCommand):
now = timezone.now()
reventes = SpectacleRevente.objects.all()
for revente in reventes:
- # Check si < 24h
- if (revente.attribution.spectacle.date <=
- revente.date + timedelta(days=1)) and \
- now >= revente.date + timedelta(minutes=15) and \
- not revente.notif_sent:
- self.stdout.write(str(now))
- revente.mail_shotgun()
- self.stdout.write("Mail de disponibilité immédiate envoyé")
- # Check si délai de retrait dépassé
- elif (now >= revente.date + timedelta(hours=1) and
- not revente.notif_sent):
+ # Le spectacle est bientôt et on a pas encore envoyé de mail :
+ # on met la place au shotgun et on prévient.
+ if revente.is_urgent and not revente.notif_sent:
+ if revente.can_notif:
+ self.stdout.write(str(now))
+ revente.mail_shotgun()
+ self.stdout.write(
+ "Mails de disponibilité immédiate envoyés "
+ "pour la revente [%s]" % revente
+ )
+
+ # Le spectacle est dans plus longtemps : on prévient
+ elif (revente.can_notif and not revente.notif_sent):
self.stdout.write(str(now))
revente.send_notif()
- self.stdout.write("Mail d'inscription à une revente envoyé")
- # Check si tirage à faire
- elif (now >= revente.date_tirage and
- not revente.tirage_done):
+ self.stdout.write(
+ "Mails d'inscription à la revente [%s] envoyés"
+ % revente
+ )
+
+ # On fait le tirage
+ elif (now >= revente.date_tirage and not revente.tirage_done):
self.stdout.write(str(now))
- revente.tirage()
- self.stdout.write("Tirage effectué, mails envoyés")
+ winner = revente.tirage()
+ self.stdout.write(
+ "Tirage effectué pour la revente [%s]"
+ % revente
+ )
+
+ if winner:
+ self.stdout.write("Gagnant : %s" % winner.user)
+ else:
+ self.stdout.write("Pas de gagnant ; place au shotgun")
diff --git a/bda/models.py b/bda/models.py
index 5533e3bb..b2882900 100644
--- a/bda/models.py
+++ b/bda/models.py
@@ -233,17 +233,46 @@ class SpectacleRevente(models.Model):
default=False)
shotgun = models.BooleanField("Disponible immédiatement",
default=False)
+ ####
+ # Some class attributes
+ ###
+ # TODO : settings ?
+
+ # Temps minimum entre le tirage et le spectacle
+ min_margin = timedelta(days=5)
+
+ # Temps entre la création d'une revente et l'envoi du mail
+ remorse_time = timedelta(hours=1)
+
+ # Temps min/max d'attente avant le tirage
+ max_wait_time = timedelta(days=3)
+ min_wait_time = timedelta(days=1)
@property
def date_tirage(self):
"""Renvoie la date du tirage au sort de la revente."""
- # L'acheteur doit être connu au plus 12h avant le spectacle
+ notif_time = self.date + self.remorse_time
+
remaining_time = (self.attribution.spectacle.date
- - self.date - timedelta(hours=13))
- # Au minimum, on attend 2 jours avant le tirage
- delay = min(remaining_time, timedelta(days=2))
- # Le vendeur a aussi 1h pour changer d'avis
- return self.date + delay + timedelta(hours=1)
+ - notif_time - self.min_margin)
+
+ delay = min(remaining_time, self.max_wait_time)
+
+ return notif_time + delay
+
+ @property
+ def is_urgent(self):
+ """
+ Renvoie True iff la revente doit être mise au shotgun directement.
+ Plus précisément, on doit avoir min_margin + min_wait_time de marge.
+ """
+ spectacle_date = self.attribution.spectacle.date
+ return (spectacle_date <= timezone.now() + self.min_margin
+ + self.min_wait_time)
+
+ @property
+ def can_notif(self):
+ return (timezone.now() >= self.date + self.remorse_time)
def __str__(self):
return "%s -- %s" % (self.seller,
@@ -353,8 +382,12 @@ class SpectacleRevente(models.Model):
[inscrit.user.email]
))
send_mass_custom_mail(datatuple)
+
+ return winner
+
# Si personne ne veut de la place, elle part au shotgun
else:
self.shotgun = True
+ return None
self.tirage_done = True
self.save()
From 6a6549e0d72937d9f5adaf7bf43ff090150b4891 Mon Sep 17 00:00:00 2001
From: Ludovic Stephan
Date: Mon, 23 Oct 2017 20:52:25 +0200
Subject: [PATCH 037/101] Add notif time
In case of a gestioCOF bug, we keep the notification time in memory to
still do the drawing 1-3 days after.
---
bda/admin.py | 6 +++---
bda/forms.py | 4 ++--
bda/migrations/0012_notif_time.py | 28 ++++++++++++++++++++++++++++
bda/models.py | 29 +++++++++++++++++++++--------
bda/views.py | 14 +++++++-------
5 files changed, 61 insertions(+), 20 deletions(-)
create mode 100644 bda/migrations/0012_notif_time.py
diff --git a/bda/admin.py b/bda/admin.py
index 60d3c1ba..4f5d821a 100644
--- a/bda/admin.py
+++ b/bda/admin.py
@@ -225,7 +225,7 @@ class SpectacleReventeAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.fields['answered_mail'].queryset = (
+ self.fields['confirmed_entry'].queryset = (
Participant.objects
.select_related('user', 'tirage')
)
@@ -292,8 +292,8 @@ class SpectacleReventeAdmin(admin.ModelAdmin):
revente.soldTo = None
revente.notif_sent = False
revente.tirage_done = False
- if revente.answered_mail:
- revente.answered_mail.clear()
+ if revente.confirmed_entry:
+ revente.confirmed_entry.clear()
revente.save()
self.message_user(
request,
diff --git a/bda/forms.py b/bda/forms.py
index 139ef45d..11d05b0e 100644
--- a/bda/forms.py
+++ b/bda/forms.py
@@ -110,7 +110,7 @@ class ReventeTirageAnnulForm(forms.Form):
def __init__(self, participant, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['reventes'].queryset = (
- participant.wanted.filter(soldTo__isnull=True)
+ participant.entered.filter(soldTo__isnull=True)
.select_related('attribution__spectacle')
)
@@ -130,7 +130,7 @@ class ReventeTirageForm(forms.Form):
notif_sent=True,
shotgun=False,
tirage_done=False
- ).exclude(answered_mail=participant)
+ ).exclude(confirmed_entry=participant)
.select_related('attribution__spectacle')
)
diff --git a/bda/migrations/0012_notif_time.py b/bda/migrations/0012_notif_time.py
new file mode 100644
index 00000000..be66efd1
--- /dev/null
+++ b/bda/migrations/0012_notif_time.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('bda', '0011_tirage_appear_catalogue'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='spectaclerevente',
+ name='answered_mail',
+ ),
+ migrations.AddField(
+ model_name='spectaclerevente',
+ name='confirmed_entry',
+ field=models.ManyToManyField(blank=True, related_name='entered', to='bda.Participant'),
+ ),
+ migrations.AddField(
+ model_name='spectaclerevente',
+ name='notif_time',
+ field=models.DateTimeField(blank=True, verbose_name="Moment d'envoi de la notification", null=True),
+ ),
+ ]
diff --git a/bda/models.py b/bda/models.py
index b2882900..2ad47dbf 100644
--- a/bda/models.py
+++ b/bda/models.py
@@ -218,9 +218,9 @@ class SpectacleRevente(models.Model):
related_name="revente")
date = models.DateTimeField("Date de mise en vente",
default=timezone.now)
- answered_mail = models.ManyToManyField(Participant,
- related_name="wanted",
- blank=True)
+ confirmed_entry = models.ManyToManyField(Participant,
+ related_name="entered",
+ blank=True)
seller = models.ForeignKey(Participant,
related_name="original_shows",
verbose_name="Vendeur")
@@ -229,8 +229,13 @@ class SpectacleRevente(models.Model):
notif_sent = models.BooleanField("Notification envoyée",
default=False)
+
+ notif_time = models.DateTimeField("Moment d'envoi de la notification",
+ blank=True, null=True)
+
tirage_done = models.BooleanField("Tirage effectué",
default=False)
+
shotgun = models.BooleanField("Disponible immédiatement",
default=False)
####
@@ -248,17 +253,23 @@ class SpectacleRevente(models.Model):
max_wait_time = timedelta(days=3)
min_wait_time = timedelta(days=1)
+ @property
+ def real_notif_time(self):
+ if self.notif_time:
+ return self.notif_time
+ else:
+ return self.date + self.remorse_time
+
@property
def date_tirage(self):
"""Renvoie la date du tirage au sort de la revente."""
- notif_time = self.date + self.remorse_time
remaining_time = (self.attribution.spectacle.date
- - notif_time - self.min_margin)
+ - self.real_notif_time - self.min_margin)
delay = min(remaining_time, self.max_wait_time)
- return notif_time + delay
+ return self.real_notif_time + delay
@property
def is_urgent(self):
@@ -285,7 +296,7 @@ class SpectacleRevente(models.Model):
"""Réinitialise la revente pour permettre une remise sur le marché"""
self.seller = self.attribution.participant
self.date = timezone.now()
- self.answered_mail.clear()
+ self.confirmed_entry.clear()
self.soldTo = None
self.notif_sent = False
self.tirage_done = False
@@ -311,6 +322,7 @@ class SpectacleRevente(models.Model):
]
send_mass_custom_mail(datatuple)
self.notif_sent = True
+ self.notif_time = timezone.now()
self.save()
def mail_shotgun(self):
@@ -332,6 +344,7 @@ class SpectacleRevente(models.Model):
]
send_mass_custom_mail(datatuple)
self.notif_sent = True
+ self.notif_time = timezone.now()
# Flag inutile, sauf si l'horloge interne merde
self.tirage_done = True
self.shotgun = True
@@ -343,7 +356,7 @@ class SpectacleRevente(models.Model):
parmis les personnes intéressées par le spectacle. Les personnes sont
ensuites prévenues par mail du résultat du tirage.
"""
- inscrits = list(self.answered_mail.all())
+ inscrits = list(self.confirmed_entry.all())
spectacle = self.attribution.spectacle
seller = self.seller
diff --git a/bda/views.py b/bda/views.py
index 311d530a..6ed22b21 100644
--- a/bda/views.py
+++ b/bda/views.py
@@ -420,8 +420,8 @@ def revente_manage(request, tirage_id):
revente.notif_sent = False
revente.tirage_done = False
revente.shotgun = False
- if revente.answered_mail:
- revente.answered_mail.clear()
+ if revente.confirmed_entry:
+ revente.confirmed_entry.clear()
revente.save()
overdue = participant.attribution_set.filter(
@@ -454,7 +454,7 @@ def revente_tirages(request, tirage_id):
sub = 0
reventes = subform.cleaned_data['reventes']
for revente in reventes:
- revente.answered_mail.add(participant)
+ revente.confirmed_entry.add(participant)
sub += 1
if sub > 0:
plural = "s" if sub > 1 else ""
@@ -470,7 +470,7 @@ def revente_tirages(request, tirage_id):
unsub = 0
reventes = annulform.cleaned_data['reventes']
for revente in reventes:
- revente.answered_mail.remove(participant)
+ revente.confirmed_entry.remove(participant)
unsub += 1
if unsub > 0:
plural = "s" if unsub > 1 else ""
@@ -493,7 +493,7 @@ def revente_confirm(request, revente_id):
return render(request, "bda/revente/wrongtime.html",
{"revente": revente})
- revente.answered_mail.add(participant)
+ revente.confirmed_entry.add(participant)
return render(request, "bda/revente/confirmed.html",
{"spectacle": revente.attribution.spectacle,
"date": revente.date_tirage})
@@ -526,12 +526,12 @@ def revente_subscribe(request, tirage_id):
# la revente ayant le moins d'inscrits
min_resell = (
qset.filter(shotgun=False)
- .annotate(nb_subscribers=Count('answered_mail'))
+ .annotate(nb_subscribers=Count('confirmed_entry'))
.order_by('nb_subscribers')
.first()
)
if min_resell is not None:
- min_resell.answered_mail.add(participant)
+ min_resell.confirmed_entry.add(participant)
inscrit_revente.append(spectacle)
success = True
else:
From 8b1f174b13f98c0bccb6a04d4e8bbb2f7d71b329 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?=
Date: Tue, 24 Oct 2017 16:46:15 +0200
Subject: [PATCH 038/101] manage.py is executable
---
manage.py | 0
1 file changed, 0 insertions(+), 0 deletions(-)
mode change 100644 => 100755 manage.py
diff --git a/manage.py b/manage.py
old mode 100644
new mode 100755
From 8673da18742fea41d8902f45170089cdc6fd5a87 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?=
Date: Tue, 24 Oct 2017 16:51:54 +0200
Subject: [PATCH 039/101] Fix migration conflict
---
.../{0058_amend_supplier.py => 0060_amend_supplier.py} | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
rename kfet/migrations/{0058_amend_supplier.py => 0060_amend_supplier.py} (96%)
diff --git a/kfet/migrations/0058_amend_supplier.py b/kfet/migrations/0060_amend_supplier.py
similarity index 96%
rename from kfet/migrations/0058_amend_supplier.py
rename to kfet/migrations/0060_amend_supplier.py
index 764322b0..4eb569f8 100644
--- a/kfet/migrations/0058_amend_supplier.py
+++ b/kfet/migrations/0060_amend_supplier.py
@@ -7,7 +7,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('kfet', '0057_merge'),
+ ('kfet', '0059_create_generic'),
]
operations = [
From 1cc51f17a390d88a75895bb09691e9244aa8989e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?=
Date: Tue, 24 Oct 2017 17:55:02 +0200
Subject: [PATCH 040/101] Prevent connection to LDAP when settings is None
---
gestioncof/autocomplete.py | 2 +-
kfet/autocomplete.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/gestioncof/autocomplete.py b/gestioncof/autocomplete.py
index 3532525d..968398fd 100644
--- a/gestioncof/autocomplete.py
+++ b/gestioncof/autocomplete.py
@@ -58,7 +58,7 @@ def autocomplete(request):
)
# Fetching data from the SPI
- if hasattr(settings, 'LDAP_SERVER_URL'):
+ if getattr(settings, 'LDAP_SERVER_URL', None):
# Fetching
ldap_query = '(&{:s})'.format(''.join(
'(|(cn=*{bit:s}*)(uid=*{bit:s}*))'.format(bit=bit)
diff --git a/kfet/autocomplete.py b/kfet/autocomplete.py
index 0a9bb42c..c4886180 100644
--- a/kfet/autocomplete.py
+++ b/kfet/autocomplete.py
@@ -76,7 +76,7 @@ def account_create(request):
queries['users_notcof'].values_list('username', flat=True))
# Fetching data from the SPI
- if hasattr(settings, 'LDAP_SERVER_URL'):
+ if getattr(settings, 'LDAP_SERVER_URL', None):
# Fetching
ldap_query = '(&{:s})'.format(''.join(
'(|(cn=*{bit:s}*)(uid=*{bit:s}*))'.format(bit=word)
From af3a7cf6971bfead6541f76048f61dc0407fb5e2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?=
Date: Tue, 24 Oct 2017 17:56:14 +0200
Subject: [PATCH 041/101] =?UTF-8?q?Reapply=20fix=20to=20kfetauth=20(?=
=?UTF-8?q?=E2=80=A6)=20and=20fix=20tests?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
kfet/auth/forms.py | 4 ++++
kfet/tests/test_views.py | 14 --------------
kfet/views.py | 2 +-
3 files changed, 5 insertions(+), 15 deletions(-)
diff --git a/kfet/auth/forms.py b/kfet/auth/forms.py
index 0c9fa53b..876e8814 100644
--- a/kfet/auth/forms.py
+++ b/kfet/auth/forms.py
@@ -18,6 +18,8 @@ class GroupForm(forms.ModelForm):
# other_groups = self.instance.permissions.difference(
# self.fields['permissions'].queryset
# )
+ if self.instance.pk is None:
+ return kfet_perms
other_perms = self.instance.permissions.exclude(
pk__in=[p.pk for p in self.fields['permissions'].queryset],
)
@@ -36,6 +38,8 @@ class UserGroupForm(forms.ModelForm):
def clean_groups(self):
kfet_groups = self.cleaned_data.get('groups')
+ if self.instance.pk is None:
+ return kfet_groups
other_groups = self.instance.groups.exclude(name__icontains='K-Fêt')
return list(kfet_groups) + list(other_groups)
diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py
index ff9803c9..c7ed5dda 100644
--- a/kfet/tests/test_views.py
+++ b/kfet/tests/test_views.py
@@ -18,20 +18,6 @@ from .testcases import ViewTestCaseMixin
from .utils import create_team, create_user, get_perms
-class LoginGenericTeamViewTests(ViewTestCaseMixin, TestCase):
- url_name = 'kfet.login.genericteam'
- url_expected = '/k-fet/login/genericteam'
-
- auth_user = 'team'
- auth_forbidden = [None, 'user']
-
- def test_ok(self):
- r = self.client.get(self.url)
- self.assertEqual(r.status_code, 200)
- logged_in = r.wsgi_request.user
- self.assertEqual(logged_in.username, 'kfet_genericteam')
-
-
class AccountListViewTests(ViewTestCaseMixin, TestCase):
url_name = 'kfet.account'
url_expected = '/k-fet/accounts/'
diff --git a/kfet/views.py b/kfet/views.py
index 7a28819e..f1dd6834 100644
--- a/kfet/views.py
+++ b/kfet/views.py
@@ -12,7 +12,7 @@ from django.views.generic.edit import CreateView, UpdateView
from django.core.urlresolvers import reverse, reverse_lazy
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
-from django.contrib.auth.decorators import login_required
+from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.models import User, Permission
from django.http import JsonResponse, Http404
from django.forms import formset_factory
From 8e8e9aa0764b911c8eb1e6726e4a120e6fb40d62 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?=
Date: Tue, 24 Oct 2017 19:19:57 +0200
Subject: [PATCH 042/101] Fix migration history
---
kfet/migrations/0057_merge.py | 3 +--
.../{0057_add_perms_config.py => 0061_add_perms_config.py} | 2 +-
2 files changed, 2 insertions(+), 3 deletions(-)
rename kfet/migrations/{0057_add_perms_config.py => 0061_add_perms_config.py} (94%)
diff --git a/kfet/migrations/0057_merge.py b/kfet/migrations/0057_merge.py
index e3cadb23..48f63399 100644
--- a/kfet/migrations/0057_merge.py
+++ b/kfet/migrations/0057_merge.py
@@ -1,13 +1,12 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
-from django.db import migrations
+from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('kfet', '0057_add_perms_config'),
('kfet', '0056_change_account_meta'),
('kfet', '0054_update_promos'),
]
diff --git a/kfet/migrations/0057_add_perms_config.py b/kfet/migrations/0061_add_perms_config.py
similarity index 94%
rename from kfet/migrations/0057_add_perms_config.py
rename to kfet/migrations/0061_add_perms_config.py
index 1300665f..01bdf51d 100644
--- a/kfet/migrations/0057_add_perms_config.py
+++ b/kfet/migrations/0061_add_perms_config.py
@@ -7,7 +7,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('kfet', '0056_change_account_meta'),
+ ('kfet', '0060_amend_supplier'),
]
operations = [
From a07b5308a322ac03b3f252b4a74b025ed1f0c1d2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Martin=20P=C3=A9pin?=
Date: Wed, 25 Oct 2017 22:01:58 +0200
Subject: [PATCH 043/101] PetitCoursAttributionCounter defaults to 0
---
gestioncof/petits_cours_models.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/gestioncof/petits_cours_models.py b/gestioncof/petits_cours_models.py
index 753e8674..d9ea9668 100644
--- a/gestioncof/petits_cours_models.py
+++ b/gestioncof/petits_cours_models.py
@@ -157,14 +157,16 @@ class PetitCoursAttributionCounter(models.Model):
compteurs de tout le monde.
"""
counter, created = cls.objects.get_or_create(
- user=user, matiere=matiere)
+ user=user,
+ matiere=matiere,
+ )
if created:
mincount = (
cls.objects.filter(matiere=matiere).exclude(user=user)
.aggregate(Min('count'))
['count__min']
)
- counter.count = mincount
+ counter.count = mincount or 0
counter.save()
return counter
From 40abe27e8185f843ed2d313c978701832fe0543b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Martin=20P=C3=A9pin?=
Date: Wed, 25 Oct 2017 22:05:14 +0200
Subject: [PATCH 044/101] EMAIL_HOST needs to be set but as a secret
---
cof/settings/common.py | 1 +
cof/settings/secret_example.py | 1 +
2 files changed, 2 insertions(+)
diff --git a/cof/settings/common.py b/cof/settings/common.py
index f92dc83b..a2ea3f5e 100644
--- a/cof/settings/common.py
+++ b/cof/settings/common.py
@@ -31,6 +31,7 @@ def import_secret(name):
SECRET_KEY = import_secret("SECRET_KEY")
ADMINS = import_secret("ADMINS")
SERVER_EMAIL = import_secret("SERVER_EMAIL")
+EMAIL_HOST = import_secret("EMAIL_HOST")
DBNAME = import_secret("DBNAME")
DBUSER = import_secret("DBUSER")
diff --git a/cof/settings/secret_example.py b/cof/settings/secret_example.py
index e9c0e63c..e966565a 100644
--- a/cof/settings/secret_example.py
+++ b/cof/settings/secret_example.py
@@ -1,6 +1,7 @@
SECRET_KEY = 'q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah'
ADMINS = None
SERVER_EMAIL = "root@vagrant"
+EMAIL_HOST = "localhost"
DBUSER = "cof_gestion"
DBNAME = "cof_gestion"
From 1a136088bfa8892eebde77cb5aa8619dd342b3ef Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Martin=20P=C3=A9pin?=
Date: Wed, 25 Oct 2017 22:08:29 +0200
Subject: [PATCH 045/101] Add missing type in custommail (dev only)
---
gestioncof/management/data/custommail.json | 655 +++++++++++----------
1 file changed, 334 insertions(+), 321 deletions(-)
diff --git a/gestioncof/management/data/custommail.json b/gestioncof/management/data/custommail.json
index 9ee9b1ea..590ebf18 100644
--- a/gestioncof/management/data/custommail.json
+++ b/gestioncof/management/data/custommail.json
@@ -1,587 +1,600 @@
[
{
- "model": "custommail.variabletype",
- "pk": 1,
+ "model": "custommail.type",
"fields": {
+ "kind": "model",
"content_type": [
"auth",
"user"
],
"inner1": null,
- "kind": "model",
"inner2": null
- }
+ },
+ "pk": 1
},
{
- "model": "custommail.variabletype",
- "pk": 2,
+ "model": "custommail.type",
"fields": {
+ "kind": "int",
"content_type": null,
"inner1": null,
- "kind": "int",
"inner2": null
- }
+ },
+ "pk": 2
},
{
- "model": "custommail.variabletype",
- "pk": 3,
+ "model": "custommail.type",
"fields": {
+ "kind": "model",
"content_type": [
"bda",
"spectacle"
],
"inner1": null,
- "kind": "model",
"inner2": null
- }
+ },
+ "pk": 3
},
{
- "model": "custommail.variabletype",
- "pk": 4,
+ "model": "custommail.type",
"fields": {
+ "kind": "model",
"content_type": [
"bda",
"spectaclerevente"
],
"inner1": null,
- "kind": "model",
"inner2": null
- }
+ },
+ "pk": 4
},
{
- "model": "custommail.variabletype",
- "pk": 5,
+ "model": "custommail.type",
"fields": {
+ "kind": "model",
"content_type": [
"sites",
"site"
],
"inner1": null,
- "kind": "model",
"inner2": null
- }
+ },
+ "pk": 5
},
{
- "model": "custommail.variabletype",
- "pk": 6,
+ "model": "custommail.type",
"fields": {
+ "kind": "model",
"content_type": [
"gestioncof",
"petitcoursdemande"
],
"inner1": null,
- "kind": "model",
"inner2": null
- }
+ },
+ "pk": 6
},
{
- "model": "custommail.variabletype",
- "pk": 7,
+ "model": "custommail.type",
"fields": {
- "content_type": null,
- "inner1": null,
"kind": "list",
+ "content_type": null,
+ "inner1": 12,
"inner2": null
- }
+ },
+ "pk": 7
},
{
- "model": "custommail.variabletype",
- "pk": 8,
+ "model": "custommail.type",
"fields": {
+ "kind": "list",
"content_type": null,
"inner1": 1,
- "kind": "list",
"inner2": null
- }
+ },
+ "pk": 8
},
{
- "model": "custommail.variabletype",
- "pk": 9,
+ "model": "custommail.type",
"fields": {
- "content_type": null,
- "inner1": null,
"kind": "pair",
+ "content_type": null,
+ "inner1": 12,
"inner2": 8
- }
+ },
+ "pk": 9
},
{
- "model": "custommail.variabletype",
- "pk": 10,
+ "model": "custommail.type",
"fields": {
+ "kind": "list",
"content_type": null,
"inner1": 9,
- "kind": "list",
"inner2": null
- }
+ },
+ "pk": 10
},
{
- "model": "custommail.variabletype",
- "pk": 11,
+ "model": "custommail.type",
"fields": {
+ "kind": "list",
"content_type": null,
"inner1": 3,
- "kind": "list",
"inner2": null
- }
+ },
+ "pk": 11
+},
+{
+ "model": "custommail.type",
+ "fields": {
+ "kind": "model",
+ "content_type": [
+ "gestioncof",
+ "petitcourssubject"
+ ],
+ "inner1": null,
+ "inner2": null
+ },
+ "pk": 12
},
{
"model": "custommail.custommail",
- "pk": 1,
"fields": {
"shortname": "welcome",
"subject": "Bienvenue au COF",
- "description": "Mail de bienvenue au COF envoy\u00e9 automatiquement \u00e0 l'inscription d'un nouveau membre",
- "body": "Bonjour {{ member.first_name }} et bienvenue au COF !\r\n\r\nTu trouveras plein de trucs cool sur le site du COF : https://www.cof.ens.fr/ et notre page Facebook : https://www.facebook.com/cof.ulm\r\nEt n'oublie pas d'aller d\u00e9couvrir GestioCOF, la plateforme de gestion du COF !\r\nSi tu as des questions, tu peux nous envoyer un mail \u00e0 cof@ens.fr (on aime le spam), ou passer nous voir au Bur\u00f4 pr\u00e8s de la Cour\u00f4 du lundi au vendredi de 12h \u00e0 14h et de 18h \u00e0 20h.\r\n\r\nRetrouvez les \u00e9v\u00e8nements de rentr\u00e9e pour les conscrit.e.s et les vieux/vieilles organis\u00e9s par le COF et ses clubs ici : http://www.cof.ens.fr/depot/Rentree.pdf \r\n\r\nAmicalement,\r\n\r\nTon COF qui t'aime."
- }
+ "body": "Bonjour {{ member.first_name }} et bienvenue au COF !\r\n\r\nTu trouveras plein de trucs cool sur le site du COF : https://www.cof.ens.fr/ et notre page Facebook : https://www.facebook.com/cof.ulm\r\nEt n'oublie pas d'aller d\u00e9couvrir GestioCOF, la plateforme de gestion du COF !\r\nSi tu as des questions, tu peux nous envoyer un mail \u00e0 cof@ens.fr (on aime le spam), ou passer nous voir au Bur\u00f4 pr\u00e8s de la Cour\u00f4 du lundi au vendredi de 12h \u00e0 14h et de 18h \u00e0 20h.\r\n\r\nRetrouvez les \u00e9v\u00e8nements de rentr\u00e9e pour les conscrit.e.s et les vieux/vieilles organis\u00e9s par le COF et ses clubs ici : http://www.cof.ens.fr/depot/Rentree.pdf \r\n\r\nAmicalement,\r\n\r\nTon COF qui t'aime.",
+ "description": "Mail de bienvenue au COF envoy\u00e9 automatiquement \u00e0 l'inscription d'un nouveau membre"
+ },
+ "pk": 1
},
{
"model": "custommail.custommail",
- "pk": 2,
"fields": {
"shortname": "bda-rappel",
"subject": "{{ show }}",
- "description": "Mail de rappel pour les spectacles BdA",
- "body": "Bonjour {{ member.first_name }},\r\n\r\nNous te rappellons que tu as eu la chance d'obtenir {{ nb_attr|pluralize:\"une place,deux places\" }}\r\npour {{ show.title }}, le {{ show.date }} au {{ show.location }}. N'oublie pas de t'y rendre !\r\n{% if nb_attr == 2 %}\r\nTu as obtenu deux places pour ce spectacle. Nous te rappelons que\r\nces places sont strictement r\u00e9serv\u00e9es aux personnes de moins de 28 ans.\r\n{% endif %}\r\n{% if show.listing %}Pour ce spectacle, tu as re\u00e7u des places sur\r\nlisting. Il te faudra donc te rendre 15 minutes en avance sur les lieux de la repr\u00e9sentation\r\npour retirer {{ nb_attr|pluralize:\"ta place,tes places\" }}.\r\n{% else %}Pour assister \u00e0 ce spectacle, tu dois pr\u00e9senter les billets qui ont\r\n\u00e9t\u00e9 distribu\u00e9s au bur\u00f4.\r\n{% endif %}\r\n\r\nSi tu ne peux plus assister \u00e0 cette repr\u00e9sentation, tu peux\r\nrevendre ta place via BdA-revente, accessible directement sur\r\nGestioCOF (lien \"revendre une place du premier tirage\" sur la page\r\nd'accueil https://www.cof.ens.fr/gestion/).\r\n\r\nEn te souhaitant un excellent spectacle,\r\n\r\nLe Bureau des Arts"
- }
+ "body": "Bonjour {{ member.first_name }},\r\n\r\nNous te rappellons que tu as eu la chance d'obtenir {{ nb_attr|pluralize:\"une place,deux places\" }}\r\npour {{ show.title }}, le {{ show.date }} au {{ show.location }}. N'oublie pas de t'y rendre !\r\n{% if nb_attr == 2 %}\r\nTu as obtenu deux places pour ce spectacle. Nous te rappelons que\r\nces places sont strictement r\u00e9serv\u00e9es aux personnes de moins de 28 ans.\r\n{% endif %}\r\n{% if show.listing %}Pour ce spectacle, tu as re\u00e7u des places sur\r\nlisting. Il te faudra donc te rendre 15 minutes en avance sur les lieux de la repr\u00e9sentation\r\npour retirer {{ nb_attr|pluralize:\"ta place,tes places\" }}.\r\n{% else %}Pour assister \u00e0 ce spectacle, tu dois pr\u00e9senter les billets qui ont\r\n\u00e9t\u00e9 distribu\u00e9s au bur\u00f4.\r\n{% endif %}\r\n\r\nSi tu ne peux plus assister \u00e0 cette repr\u00e9sentation, tu peux\r\nrevendre ta place via BdA-revente, accessible directement sur\r\nGestioCOF (lien \"revendre une place du premier tirage\" sur la page\r\nd'accueil https://www.cof.ens.fr/gestion/).\r\n\r\nEn te souhaitant un excellent spectacle,\r\n\r\nLe Bureau des Arts",
+ "description": "Mail de rappel pour les spectacles BdA"
+ },
+ "pk": 2
},
{
"model": "custommail.custommail",
- "pk": 3,
"fields": {
"shortname": "bda-revente",
"subject": "{{ show }}",
- "description": "Notification envoy\u00e9e \u00e0 toutes les personnes int\u00e9ress\u00e9es par un spectacle pour le signaler qu'une place vient d'\u00eatre mise en vente.",
- "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nSi ce spectacle t'int\u00e9resse toujours, merci de nous le signaler en cliquant\r\nsur ce lien : http://{{ site }}{% url \"bda-revente-interested\" revente.id %}.\r\nDans le cas o\u00f9 plusieurs personnes seraient int\u00e9ress\u00e9es, nous proc\u00e8derons \u00e0\r\nun tirage au sort le {{ revente.date_tirage|date:\"DATE_FORMAT\" }}.\r\n\r\nChaleureusement,\r\nLe BdA"
- }
+ "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nSi ce spectacle t'int\u00e9resse toujours, merci de nous le signaler en cliquant\r\nsur ce lien : http://{{ site }}{% url \"bda-revente-interested\" revente.id %}.\r\nDans le cas o\u00f9 plusieurs personnes seraient int\u00e9ress\u00e9es, nous proc\u00e8derons \u00e0\r\nun tirage au sort le {{ revente.date_tirage|date:\"DATE_FORMAT\" }}.\r\n\r\nChaleureusement,\r\nLe BdA",
+ "description": "Notification envoy\u00e9e \u00e0 toutes les personnes int\u00e9ress\u00e9es par un spectacle pour le signaler qu'une place vient d'\u00eatre mise en vente."
+ },
+ "pk": 3
},
{
"model": "custommail.custommail",
- "pk": 4,
"fields": {
"shortname": "bda-shotgun",
"subject": "{{ show }}",
- "description": "Notification signalant qu'une place est au shotgun aux personnes int\u00e9ress\u00e9es.",
- "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nPuisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour\r\ncette place : elle est disponible imm\u00e9diatement \u00e0 l'adresse\r\nhttp://{{ site }}{% url \"bda-buy-revente\" show.id %}, \u00e0 la disposition de tous.\r\n\r\nChaleureusement,\r\nLe BdA"
- }
+ "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nPuisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour\r\ncette place : elle est disponible imm\u00e9diatement \u00e0 l'adresse\r\nhttp://{{ site }}{% url \"bda-buy-revente\" show.id %}, \u00e0 la disposition de tous.\r\n\r\nChaleureusement,\r\nLe BdA",
+ "description": "Notification signalant qu'une place est au shotgun aux personnes int\u00e9ress\u00e9es."
+ },
+ "pk": 4
},
{
"model": "custommail.custommail",
- "pk": 5,
"fields": {
"shortname": "bda-revente-winner",
"subject": "BdA-Revente : {{ show.title }}",
- "description": "Mail envoy\u00e9 au gagnant d'un tirage BdA-Revente",
- "body": "Bonjour {{ acheteur.first_name }},\r\n\r\nTu as \u00e9t\u00e9 tir\u00e9-e au sort pour racheter une place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) \u00e0 {{ show.price|floatformat:2 }}\u20ac.\r\nTu peux contacter le/la vendeur-se \u00e0 l'adresse {{ vendeur.email }}.\r\n\r\nChaleureusement,\r\nLe BdA"
- }
+ "body": "Bonjour {{ acheteur.first_name }},\r\n\r\nTu as \u00e9t\u00e9 tir\u00e9-e au sort pour racheter une place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) \u00e0 {{ show.price|floatformat:2 }}\u20ac.\r\nTu peux contacter le/la vendeur-se \u00e0 l'adresse {{ vendeur.email }}.\r\n\r\nChaleureusement,\r\nLe BdA",
+ "description": "Mail envoy\u00e9 au gagnant d'un tirage BdA-Revente"
+ },
+ "pk": 5
},
{
"model": "custommail.custommail",
- "pk": 6,
"fields": {
"shortname": "bda-revente-loser",
"subject": "BdA-Revente : {{ show.title }}",
- "description": "Notification envoy\u00e9e aux perdants d'un tirage de revente.",
- "body": "Bonjour {{ acheteur.first_name }},\r\n\r\nTu t'\u00e9tais inscrit-e pour la revente de la place de {{ vendeur.get_full_name }}\r\npour {{ show.title }}.\r\nMalheureusement, une autre personne a \u00e9t\u00e9 tir\u00e9e au sort pour racheter la place.\r\nTu pourras certainement retenter ta chance pour une autre revente !\r\n\r\n\u00c0 tr\u00e8s bient\u00f4t,\r\nLe Bureau des Arts"
- }
+ "body": "Bonjour {{ acheteur.first_name }},\r\n\r\nTu t'\u00e9tais inscrit-e pour la revente de la place de {{ vendeur.get_full_name }}\r\npour {{ show.title }}.\r\nMalheureusement, une autre personne a \u00e9t\u00e9 tir\u00e9e au sort pour racheter la place.\r\nTu pourras certainement retenter ta chance pour une autre revente !\r\n\r\n\u00c0 tr\u00e8s bient\u00f4t,\r\nLe Bureau des Arts",
+ "description": "Notification envoy\u00e9e aux perdants d'un tirage de revente."
+ },
+ "pk": 6
},
{
"model": "custommail.custommail",
- "pk": 7,
"fields": {
"shortname": "bda-revente-seller",
"subject": "BdA-Revente : {{ show.title }}",
- "description": "Notification envoy\u00e9e au vendeur d'une place pour lui indiquer qu'elle vient d'\u00eatre attribu\u00e9e",
- "body": "Bonjour {{ vendeur.first_name }},\r\n\r\nLa personne tir\u00e9e au sort pour racheter ta place pour {{ show.title }} est {{ acheteur.get_full_name }}.\r\nTu peux le/la contacter \u00e0 l'adresse {{ acheteur.email }}, ou en r\u00e9pondant \u00e0 ce mail.\r\n\r\nChaleureusement,\r\nLe BdA"
- }
+ "body": "Bonjour {{ vendeur.first_name }},\r\n\r\nLa personne tir\u00e9e au sort pour racheter ta place pour {{ show.title }} est {{ acheteur.get_full_name }}.\r\nTu peux le/la contacter \u00e0 l'adresse {{ acheteur.email }}, ou en r\u00e9pondant \u00e0 ce mail.\r\n\r\nChaleureusement,\r\nLe BdA",
+ "description": "Notification envoy\u00e9e au vendeur d'une place pour lui indiquer qu'elle vient d'\u00eatre attribu\u00e9e"
+ },
+ "pk": 7
},
{
"model": "custommail.custommail",
- "pk": 8,
"fields": {
"shortname": "bda-revente-new",
"subject": "BdA-Revente : {{ show.title }}",
- "description": "Notification signalant au vendeur d'une place que sa mise en vente a bien eu lieu et lui donnant quelques informations compl\u00e9mentaires.",
- "body": "Bonjour {{ vendeur.first_name }},\r\n\r\nTu t\u2019es bien inscrit-e pour la revente de {{ show.title }}.\r\n\r\n{% with revente.date_tirage as time %}\r\nLe tirage au sort entre tout-e-s les racheteuse-eur-s potentiel-le-s aura lieu\r\nle {{ time|date:\"DATE_FORMAT\" }} \u00e0 {{ time|time:\"TIME_FORMAT\" }} (dans {{time|timeuntil }}).\r\nSi personne ne s\u2019est inscrit pour racheter la place, celle-ci apparaitra parmi\r\nles \u00ab Places disponibles imm\u00e9diatement \u00e0 la revente \u00bb sur GestioCOF.\r\n{% endwith %}\r\n\r\nBonne revente !\r\nLe Bureau des Arts"
- }
+ "body": "Bonjour {{ vendeur.first_name }},\r\n\r\nTu t\u2019es bien inscrit-e pour la revente de {{ show.title }}.\r\n\r\n{% with revente.date_tirage as time %}\r\nLe tirage au sort entre tout-e-s les racheteuse-eur-s potentiel-le-s aura lieu\r\nle {{ time|date:\"DATE_FORMAT\" }} \u00e0 {{ time|time:\"TIME_FORMAT\" }} (dans {{time|timeuntil }}).\r\nSi personne ne s\u2019est inscrit pour racheter la place, celle-ci apparaitra parmi\r\nles \u00ab Places disponibles imm\u00e9diatement \u00e0 la revente \u00bb sur GestioCOF.\r\n{% endwith %}\r\n\r\nBonne revente !\r\nLe Bureau des Arts",
+ "description": "Notification signalant au vendeur d'une place que sa mise en vente a bien eu lieu et lui donnant quelques informations compl\u00e9mentaires."
+ },
+ "pk": 8
},
{
"model": "custommail.custommail",
- "pk": 9,
"fields": {
"shortname": "bda-buy-shotgun",
"subject": "BdA-Revente : {{ show.title }}",
- "description": "Mail envoy\u00e9 au revendeur lors d'un achat au shotgun.",
- "body": "Bonjour {{ vendeur.first_name }} !\r\n\r\nJe souhaiterais racheter ta place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) \u00e0 {{ show.price|floatformat:2 }}\u20ac.\r\nContacte-moi si tu es toujours int\u00e9ress\u00e9\u00b7e !\r\n\r\n{{ acheteur.get_full_name }} ({{ acheteur.email }})"
- }
+ "body": "Bonjour {{ vendeur.first_name }} !\r\n\r\nJe souhaiterais racheter ta place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) \u00e0 {{ show.price|floatformat:2 }}\u20ac.\r\nContacte-moi si tu es toujours int\u00e9ress\u00e9\u00b7e !\r\n\r\n{{ acheteur.get_full_name }} ({{ acheteur.email }})",
+ "description": "Mail envoy\u00e9 au revendeur lors d'un achat au shotgun."
+ },
+ "pk": 9
},
{
"model": "custommail.custommail",
- "pk": 10,
"fields": {
"shortname": "petit-cours-mail-eleve",
"subject": "Petits cours ENS par le COF",
- "description": "Mail envoy\u00e9 aux personnes dont ont a donn\u00e9 les contacts \u00e0 des demandeurs de petits cours",
- "body": "Salut,\r\n\r\nLe COF a re\u00e7u une demande de petit cours qui te correspond. Tu es en haut de la liste d'attente donc on a transmis tes coordonn\u00e9es, ainsi que celles de 2 autres qui correspondaient aussi (c'est la vie, on donne les num\u00e9ros 3 par 3 pour que ce soit plus souple). Voici quelques infos sur l'annonce en question :\r\n\r\n\u00a4 Nom : {{ demande.name }}\r\n\r\n\u00a4 P\u00e9riode : {{ demande.quand }}\r\n\r\n\u00a4 Fr\u00e9quence : {{ demande.freq }}\r\n\r\n\u00a4 Lieu (si pr\u00e9f\u00e9r\u00e9) : {{ demande.lieu }}\r\n\r\n\u00a4 Niveau : {{ demande.get_niveau_display }}\r\n\r\n\u00a4 Remarques diverses (d\u00e9sol\u00e9 pour les balises HTML) : {{ demande.remarques }}\r\n\r\n{% if matieres|length > 1 %}\u00a4 Mati\u00e8res :\r\n{% for matiere in matieres %} \u00a4 {{ matiere }}\r\n{% endfor %}{% else %}\u00a4 Mati\u00e8re : {% for matiere in matieres %}{{ matiere }}\r\n{% endfor %}{% endif %}\r\nVoil\u00e0, cette personne te contactera peut-\u00eatre sous peu, tu pourras voir les d\u00e9tails directement avec elle (prix, modalit\u00e9s, ...). Pour indication, 30 Euro/h semble \u00eatre la moyenne.\r\n\r\nSi tu te rends compte qu'en fait tu ne peux pas/plus donner de cours en ce moment, \u00e7a serait cool que tu d\u00e9coches la case \"Recevoir des propositions de petits cours\" sur GestioCOF. Ensuite d\u00e8s que tu voudras r\u00e9appara\u00eetre tu pourras recocher la case et tu seras \u00e0 nouveau sur la liste.\r\n\r\n\u00c0 bient\u00f4t,\r\n\r\n--\r\nLe COF, pour les petits cours"
- }
+ "body": "Salut,\r\n\r\nLe COF a re\u00e7u une demande de petit cours qui te correspond. Tu es en haut de la liste d'attente donc on a transmis tes coordonn\u00e9es, ainsi que celles de 2 autres qui correspondaient aussi (c'est la vie, on donne les num\u00e9ros 3 par 3 pour que ce soit plus souple). Voici quelques infos sur l'annonce en question :\r\n\r\n\u00a4 Nom : {{ demande.name }}\r\n\r\n\u00a4 P\u00e9riode : {{ demande.quand }}\r\n\r\n\u00a4 Fr\u00e9quence : {{ demande.freq }}\r\n\r\n\u00a4 Lieu (si pr\u00e9f\u00e9r\u00e9) : {{ demande.lieu }}\r\n\r\n\u00a4 Niveau : {{ demande.get_niveau_display }}\r\n\r\n\u00a4 Remarques diverses (d\u00e9sol\u00e9 pour les balises HTML) : {{ demande.remarques }}\r\n\r\n{% if matieres|length > 1 %}\u00a4 Mati\u00e8res :\r\n{% for matiere in matieres %} \u00a4 {{ matiere }}\r\n{% endfor %}{% else %}\u00a4 Mati\u00e8re : {% for matiere in matieres %}{{ matiere }}\r\n{% endfor %}{% endif %}\r\nVoil\u00e0, cette personne te contactera peut-\u00eatre sous peu, tu pourras voir les d\u00e9tails directement avec elle (prix, modalit\u00e9s, ...). Pour indication, 30 Euro/h semble \u00eatre la moyenne.\r\n\r\nSi tu te rends compte qu'en fait tu ne peux pas/plus donner de cours en ce moment, \u00e7a serait cool que tu d\u00e9coches la case \"Recevoir des propositions de petits cours\" sur GestioCOF. Ensuite d\u00e8s que tu voudras r\u00e9appara\u00eetre tu pourras recocher la case et tu seras \u00e0 nouveau sur la liste.\r\n\r\n\u00c0 bient\u00f4t,\r\n\r\n--\r\nLe COF, pour les petits cours",
+ "description": "Mail envoy\u00e9 aux personnes dont ont a donn\u00e9 les contacts \u00e0 des demandeurs de petits cours"
+ },
+ "pk": 10
},
{
"model": "custommail.custommail",
- "pk": 11,
"fields": {
"shortname": "petits-cours-mail-demandeur",
"subject": "Cours particuliers ENS",
- "description": "Mail envoy\u00e9 aux personnent qui demandent des petits cours lorsque leur demande est trait\u00e9e.\r\n\r\n(Ne pas toucher \u00e0 {{ extra|safe }})",
- "body": "Bonjour,\r\n\r\nJe vous contacte au sujet de votre annonce pass\u00e9e sur le site du COF pour rentrer en contact avec un \u00e9l\u00e8ve normalien pour des cours particuliers. Voici les coordonn\u00e9es d'\u00e9l\u00e8ves qui sont motiv\u00e9s par de tels cours et correspondent aux crit\u00e8res que vous nous aviez transmis :\r\n\r\n{% for matiere, proposed in proposals %}\u00a4 {{ matiere }} :{% for user in proposed %}\r\n \u00a4 {{ user.get_full_name }}{% if user.profile.phone %}, {{ user.profile.phone }}{% endif %}{% if user.email %}, {{ user.email }}{% endif %}{% endfor %}\r\n\r\n{% endfor %}{% if unsatisfied %}Nous n'avons cependant pas pu trouver d'\u00e9l\u00e8ve disponible pour des cours de {% for matiere in unsatisfied %}{% if forloop.counter0 > 0 %}, {% endif %}{{ matiere }}{% endfor %}.\r\n\r\n{% endif %}Si pour une raison ou une autre ces num\u00e9ros ne suffisaient pas, n'h\u00e9sitez pas \u00e0 r\u00e9pondre \u00e0 cet e-mail et je vous en ferai parvenir d'autres sans probl\u00e8me.\r\n{% if extra|length > 0 %}\r\n{{ extra|safe }}\r\n{% endif %}\r\nCordialement,\r\n\r\n--\r\nLe COF, BdE de l'ENS"
- }
+ "body": "Bonjour,\r\n\r\nJe vous contacte au sujet de votre annonce pass\u00e9e sur le site du COF pour rentrer en contact avec un \u00e9l\u00e8ve normalien pour des cours particuliers. Voici les coordonn\u00e9es d'\u00e9l\u00e8ves qui sont motiv\u00e9s par de tels cours et correspondent aux crit\u00e8res que vous nous aviez transmis :\r\n\r\n{% for matiere, proposed in proposals %}\u00a4 {{ matiere }} :{% for user in proposed %}\r\n \u00a4 {{ user.get_full_name }}{% if user.profile.phone %}, {{ user.profile.phone }}{% endif %}{% if user.email %}, {{ user.email }}{% endif %}{% endfor %}\r\n\r\n{% endfor %}{% if unsatisfied %}Nous n'avons cependant pas pu trouver d'\u00e9l\u00e8ve disponible pour des cours de {% for matiere in unsatisfied %}{% if forloop.counter0 > 0 %}, {% endif %}{{ matiere }}{% endfor %}.\r\n\r\n{% endif %}Si pour une raison ou une autre ces num\u00e9ros ne suffisaient pas, n'h\u00e9sitez pas \u00e0 r\u00e9pondre \u00e0 cet e-mail et je vous en ferai parvenir d'autres sans probl\u00e8me.\r\n{% if extra|length > 0 %}\r\n{{ extra|safe }}\r\n{% endif %}\r\nCordialement,\r\n\r\n--\r\nLe COF, BdE de l'ENS",
+ "description": "Mail envoy\u00e9 aux personnes qui demandent des petits cours lorsque leur demande est trait\u00e9e.\r\n\r\n(Ne pas toucher \u00e0 {{ extra|safe }})"
+ },
+ "pk": 11
},
{
"model": "custommail.custommail",
- "pk": 12,
"fields": {
"shortname": "bda-attributions",
"subject": "R\u00e9sultats du tirage au sort",
- "description": "Mail annon\u00e7ant les r\u00e9sultats du tirage au sort du BdA aux gagnants d'une ou plusieurs places",
- "body": "Cher-e {{ member.first_name }},\r\n\r\nTu t'es inscrit-e pour le tirage au sort du BdA. Tu as \u00e9t\u00e9 s\u00e9lectionn\u00e9-e\r\npour les spectacles suivants :\r\n{% for place in places %}\r\n- 1 place pour {{ place }}{% endfor %}\r\n\r\n*Paiement*\r\nL'int\u00e9gralit\u00e9 de ces places de spectacles est \u00e0 r\u00e9gler d\u00e8s maintenant et AVANT\r\nvendredi prochain, au bureau du COF pendant les heures de permanences (du lundi au vendredi\r\nentre 12h et 14h, et entre 18h et 20h). Des facilit\u00e9s de paiement sont bien\r\n\u00e9videmment possibles : nous pouvons ne pas encaisser le ch\u00e8que imm\u00e9diatement,\r\nou bien d\u00e9couper votre paiement en deux fois. Pour ceux qui ne pourraient pas\r\nvenir payer au bureau, merci de nous contacter par mail.\r\n\r\n*Mode de retrait des places*\r\nAu moment du paiement, certaines places vous seront remises directement,\r\nd'autres seront \u00e0 r\u00e9cup\u00e9rer au cours de l'ann\u00e9e, d'autres encore seront\r\nnominatives et \u00e0 retirer le soir m\u00eame dans les the\u00e2tres correspondants.\r\nPour chaque spectacle, vous recevrez un mail quelques jours avant la\r\nrepr\u00e9sentation vous indiquant le mode de retrait.\r\n\r\nNous vous rappelons que l'obtention de places du BdA vous engage \u00e0\r\nrespecter les r\u00e8gles de fonctionnement :\r\nhttp://www.cof.ens.fr/bda/?page_id=1370\r\nUn syst\u00e8me de revente des places via les mails BdA-revente disponible\r\ndirectement sur votre compte GestioCOF.\r\n\r\nEn vous souhaitant de tr\u00e8s beaux spectacles tout au long de l'ann\u00e9e,\r\n--\r\nLe Bureau des Arts"
- }
+ "body": "Cher-e {{ member.first_name }},\r\n\r\nTu t'es inscrit-e pour le tirage au sort du BdA. Tu as \u00e9t\u00e9 s\u00e9lectionn\u00e9-e\r\npour les spectacles suivants :\r\n{% for place in places %}\r\n- 1 place pour {{ place }}{% endfor %}\r\n\r\n*Paiement*\r\nL'int\u00e9gralit\u00e9 de ces places de spectacles est \u00e0 r\u00e9gler d\u00e8s maintenant et AVANT\r\nvendredi prochain, au bureau du COF pendant les heures de permanences (du lundi au vendredi\r\nentre 12h et 14h, et entre 18h et 20h). Des facilit\u00e9s de paiement sont bien\r\n\u00e9videmment possibles : nous pouvons ne pas encaisser le ch\u00e8que imm\u00e9diatement,\r\nou bien d\u00e9couper votre paiement en deux fois. Pour ceux qui ne pourraient pas\r\nvenir payer au bureau, merci de nous contacter par mail.\r\n\r\n*Mode de retrait des places*\r\nAu moment du paiement, certaines places vous seront remises directement,\r\nd'autres seront \u00e0 r\u00e9cup\u00e9rer au cours de l'ann\u00e9e, d'autres encore seront\r\nnominatives et \u00e0 retirer le soir m\u00eame dans les the\u00e2tres correspondants.\r\nPour chaque spectacle, vous recevrez un mail quelques jours avant la\r\nrepr\u00e9sentation vous indiquant le mode de retrait.\r\n\r\nNous vous rappelons que l'obtention de places du BdA vous engage \u00e0\r\nrespecter les r\u00e8gles de fonctionnement :\r\nhttp://www.cof.ens.fr/bda/?page_id=1370\r\nUn syst\u00e8me de revente des places via les mails BdA-revente disponible\r\ndirectement sur votre compte GestioCOF.\r\n\r\nEn vous souhaitant de tr\u00e8s beaux spectacles tout au long de l'ann\u00e9e,\r\n--\r\nLe Bureau des Arts",
+ "description": "Mail annon\u00e7ant les r\u00e9sultats du tirage au sort du BdA aux gagnants d'une ou plusieurs places"
+ },
+ "pk": 12
},
{
"model": "custommail.custommail",
- "pk": 13,
"fields": {
"shortname": "bda-attributions-decus",
"subject": "R\u00e9sultats du tirage au sort",
- "description": "Mail annon\u00e7ant les r\u00e9sultats du tirage au sort du BdA aux personnes n'ayant pas obtenu de place",
- "body": "Cher-e {{ member.first_name }},\r\n\r\nTu t'es inscrit-e pour le tirage au sort du BdA. Malheureusement, tu n'as\r\nobtenu aucune place.\r\n\r\nNous proposons cependant de nombreuses offres hors-tirage tout au long de\r\nl'ann\u00e9e, et nous t'invitons \u00e0 nous contacter si l'une d'entre elles\r\nt'int\u00e9resse !\r\n--\r\nLe Bureau des Arts"
- }
+ "body": "Cher-e {{ member.first_name }},\r\n\r\nTu t'es inscrit-e pour le tirage au sort du BdA. Malheureusement, tu n'as\r\nobtenu aucune place.\r\n\r\nNous proposons cependant de nombreuses offres hors-tirage tout au long de\r\nl'ann\u00e9e, et nous t'invitons \u00e0 nous contacter si l'une d'entre elles\r\nt'int\u00e9resse !\r\n--\r\nLe Bureau des Arts",
+ "description": "Mail annon\u00e7ant les r\u00e9sultats du tirage au sort du BdA aux personnes n'ayant pas obtenu de place"
+ },
+ "pk": 13
},
{
- "model": "custommail.custommailvariable",
- "pk": 1,
+ "model": "custommail.variable",
"fields": {
- "name": "member",
- "description": "Utilisateur de GestioCOF",
"custommail": 1,
- "type": 1
- }
-},
-{
- "model": "custommail.custommailvariable",
- "pk": 2,
- "fields": {
+ "type": 1,
"name": "member",
- "description": "Utilisateur ayant eu une place pour ce spectacle",
- "custommail": 2,
- "type": 1
- }
+ "description": "Utilisateur de GestioCOF"
+ },
+ "pk": 1
},
{
- "model": "custommail.custommailvariable",
- "pk": 3,
+ "model": "custommail.variable",
"fields": {
+ "custommail": 2,
+ "type": 1,
+ "name": "member",
+ "description": "Utilisateur ayant eu une place pour ce spectacle"
+ },
+ "pk": 2
+},
+{
+ "model": "custommail.variable",
+ "fields": {
+ "custommail": 2,
+ "type": 3,
"name": "show",
- "description": "Spectacle",
- "custommail": 2,
- "type": 3
- }
+ "description": "Spectacle"
+ },
+ "pk": 3
},
{
- "model": "custommail.custommailvariable",
- "pk": 4,
+ "model": "custommail.variable",
"fields": {
+ "custommail": 2,
+ "type": 2,
"name": "nb_attr",
- "description": "Nombre de places obtenues",
- "custommail": 2,
- "type": 2
- }
+ "description": "Nombre de places obtenues"
+ },
+ "pk": 4
},
{
- "model": "custommail.custommailvariable",
- "pk": 5,
+ "model": "custommail.variable",
"fields": {
+ "custommail": 3,
+ "type": 4,
"name": "revente",
- "description": "Revente mentionn\u00e9e dans le mail",
- "custommail": 3,
- "type": 4
- }
+ "description": "Revente mentionn\u00e9e dans le mail"
+ },
+ "pk": 5
},
{
- "model": "custommail.custommailvariable",
- "pk": 6,
+ "model": "custommail.variable",
"fields": {
+ "custommail": 3,
+ "type": 1,
"name": "member",
- "description": "Personne int\u00e9ress\u00e9e par la place",
- "custommail": 3,
- "type": 1
- }
+ "description": "Personne int\u00e9ress\u00e9e par la place"
+ },
+ "pk": 6
},
{
- "model": "custommail.custommailvariable",
- "pk": 7,
+ "model": "custommail.variable",
"fields": {
+ "custommail": 3,
+ "type": 3,
"name": "show",
- "description": "Spectacle",
- "custommail": 3,
- "type": 3
- }
+ "description": "Spectacle"
+ },
+ "pk": 7
},
{
- "model": "custommail.custommailvariable",
- "pk": 8,
+ "model": "custommail.variable",
"fields": {
- "name": "site",
- "description": "Site web (gestioCOF)",
"custommail": 3,
- "type": 5
- }
+ "type": 5,
+ "name": "site",
+ "description": "Site web (gestioCOF)"
+ },
+ "pk": 8
},
{
- "model": "custommail.custommailvariable",
- "pk": 9,
+ "model": "custommail.variable",
"fields": {
- "name": "site",
- "description": "Site web (gestioCOF)",
"custommail": 4,
- "type": 5
- }
+ "type": 5,
+ "name": "site",
+ "description": "Site web (gestioCOF)"
+ },
+ "pk": 9
},
{
- "model": "custommail.custommailvariable",
- "pk": 10,
+ "model": "custommail.variable",
"fields": {
+ "custommail": 4,
+ "type": 3,
"name": "show",
- "description": "Spectacle",
- "custommail": 4,
- "type": 3
- }
+ "description": "Spectacle"
+ },
+ "pk": 10
},
{
- "model": "custommail.custommailvariable",
- "pk": 11,
+ "model": "custommail.variable",
"fields": {
+ "custommail": 4,
+ "type": 1,
"name": "member",
- "description": "Personne int\u00e9ress\u00e9e par la place",
- "custommail": 4,
- "type": 1
- }
+ "description": "Personne int\u00e9ress\u00e9e par la place"
+ },
+ "pk": 11
},
{
- "model": "custommail.custommailvariable",
- "pk": 12,
+ "model": "custommail.variable",
"fields": {
- "name": "acheteur",
- "description": "Gagnant-e du tirage",
"custommail": 5,
- "type": 1
- }
-},
-{
- "model": "custommail.custommailvariable",
- "pk": 13,
- "fields": {
- "name": "vendeur",
- "description": "Personne qui vend une place",
- "custommail": 5,
- "type": 1
- }
-},
-{
- "model": "custommail.custommailvariable",
- "pk": 14,
- "fields": {
- "name": "show",
- "description": "Spectacle",
- "custommail": 5,
- "type": 3
- }
-},
-{
- "model": "custommail.custommailvariable",
- "pk": 15,
- "fields": {
- "name": "show",
- "description": "Spectacle",
- "custommail": 6,
- "type": 3
- }
-},
-{
- "model": "custommail.custommailvariable",
- "pk": 16,
- "fields": {
- "name": "vendeur",
- "description": "Personne qui vend une place",
- "custommail": 6,
- "type": 1
- }
-},
-{
- "model": "custommail.custommailvariable",
- "pk": 17,
- "fields": {
+ "type": 1,
"name": "acheteur",
- "description": "Personne inscrite au tirage qui n'a pas eu la place",
- "custommail": 6,
- "type": 1
- }
+ "description": "Gagnant-e du tirage"
+ },
+ "pk": 12
},
{
- "model": "custommail.custommailvariable",
- "pk": 18,
- "fields": {
- "name": "acheteur",
- "description": "Gagnant-e du tirage",
- "custommail": 7,
- "type": 1
- }
-},
-{
- "model": "custommail.custommailvariable",
- "pk": 19,
+ "model": "custommail.variable",
"fields": {
+ "custommail": 5,
+ "type": 1,
"name": "vendeur",
- "description": "Personne qui vend une place",
- "custommail": 7,
- "type": 1
- }
+ "description": "Personne qui vend une place"
+ },
+ "pk": 13
},
{
- "model": "custommail.custommailvariable",
- "pk": 20,
+ "model": "custommail.variable",
"fields": {
+ "custommail": 5,
+ "type": 3,
"name": "show",
- "description": "Spectacle",
- "custommail": 7,
- "type": 3
- }
+ "description": "Spectacle"
+ },
+ "pk": 14
},
{
- "model": "custommail.custommailvariable",
- "pk": 21,
+ "model": "custommail.variable",
"fields": {
+ "custommail": 6,
+ "type": 3,
"name": "show",
- "description": "Spectacle",
+ "description": "Spectacle"
+ },
+ "pk": 15
+},
+{
+ "model": "custommail.variable",
+ "fields": {
+ "custommail": 6,
+ "type": 1,
+ "name": "vendeur",
+ "description": "Personne qui vend une place"
+ },
+ "pk": 16
+},
+{
+ "model": "custommail.variable",
+ "fields": {
+ "custommail": 6,
+ "type": 1,
+ "name": "acheteur",
+ "description": "Personne inscrite au tirage qui n'a pas eu la place"
+ },
+ "pk": 17
+},
+{
+ "model": "custommail.variable",
+ "fields": {
+ "custommail": 7,
+ "type": 1,
+ "name": "acheteur",
+ "description": "Gagnant-e du tirage"
+ },
+ "pk": 18
+},
+{
+ "model": "custommail.variable",
+ "fields": {
+ "custommail": 7,
+ "type": 1,
+ "name": "vendeur",
+ "description": "Personne qui vend une place"
+ },
+ "pk": 19
+},
+{
+ "model": "custommail.variable",
+ "fields": {
+ "custommail": 7,
+ "type": 3,
+ "name": "show",
+ "description": "Spectacle"
+ },
+ "pk": 20
+},
+{
+ "model": "custommail.variable",
+ "fields": {
"custommail": 8,
- "type": 3
- }
+ "type": 3,
+ "name": "show",
+ "description": "Spectacle"
+ },
+ "pk": 21
},
{
- "model": "custommail.custommailvariable",
- "pk": 22,
+ "model": "custommail.variable",
"fields": {
- "name": "vendeur",
- "description": "Personne qui vend la place",
"custommail": 8,
- "type": 1
- }
+ "type": 1,
+ "name": "vendeur",
+ "description": "Personne qui vend la place"
+ },
+ "pk": 22
},
{
- "model": "custommail.custommailvariable",
- "pk": 23,
+ "model": "custommail.variable",
"fields": {
+ "custommail": 8,
+ "type": 4,
"name": "revente",
- "description": "Revente mentionn\u00e9e dans le mail",
- "custommail": 8,
- "type": 4
- }
+ "description": "Revente mentionn\u00e9e dans le mail"
+ },
+ "pk": 23
},
{
- "model": "custommail.custommailvariable",
- "pk": 24,
+ "model": "custommail.variable",
"fields": {
+ "custommail": 9,
+ "type": 1,
"name": "vendeur",
- "description": "Personne qui vend la place",
- "custommail": 9,
- "type": 1
- }
+ "description": "Personne qui vend la place"
+ },
+ "pk": 24
},
{
- "model": "custommail.custommailvariable",
- "pk": 25,
+ "model": "custommail.variable",
"fields": {
+ "custommail": 9,
+ "type": 3,
"name": "show",
- "description": "Spectacle",
- "custommail": 9,
- "type": 3
- }
+ "description": "Spectacle"
+ },
+ "pk": 25
},
{
- "model": "custommail.custommailvariable",
- "pk": 26,
+ "model": "custommail.variable",
"fields": {
+ "custommail": 9,
+ "type": 1,
"name": "acheteur",
- "description": "Personne qui prend la place au shotgun",
- "custommail": 9,
- "type": 1
- }
+ "description": "Personne qui prend la place au shotgun"
+ },
+ "pk": 26
},
{
- "model": "custommail.custommailvariable",
- "pk": 27,
+ "model": "custommail.variable",
"fields": {
+ "custommail": 10,
+ "type": 6,
"name": "demande",
- "description": "Demande de petit cours",
- "custommail": 10,
- "type": 6
- }
+ "description": "Demande de petit cours"
+ },
+ "pk": 27
},
{
- "model": "custommail.custommailvariable",
- "pk": 28,
+ "model": "custommail.variable",
"fields": {
+ "custommail": 10,
+ "type": 7,
"name": "matieres",
- "description": "Liste des mati\u00e8res concern\u00e9es par la demande",
- "custommail": 10,
- "type": 7
- }
+ "description": "Liste des mati\u00e8res concern\u00e9es par la demande"
+ },
+ "pk": 28
},
{
- "model": "custommail.custommailvariable",
- "pk": 29,
+ "model": "custommail.variable",
"fields": {
+ "custommail": 11,
+ "type": 10,
"name": "proposals",
- "description": "Liste associant une liste d'enseignants \u00e0 chaque mati\u00e8re",
- "custommail": 11,
- "type": 10
- }
+ "description": "Liste associant une liste d'enseignants \u00e0 chaque mati\u00e8re"
+ },
+ "pk": 29
},
{
- "model": "custommail.custommailvariable",
- "pk": 30,
+ "model": "custommail.variable",
"fields": {
+ "custommail": 11,
+ "type": 7,
"name": "unsatisfied",
- "description": "Liste des mati\u00e8res pour lesquelles on n'a pas d'enseigant \u00e0 proposer",
- "custommail": 11,
- "type": 7
- }
+ "description": "Liste des mati\u00e8res pour lesquelles on n'a pas d'enseigant \u00e0 proposer"
+ },
+ "pk": 30
},
{
- "model": "custommail.custommailvariable",
- "pk": 31,
+ "model": "custommail.variable",
"fields": {
+ "custommail": 12,
+ "type": 11,
"name": "places",
- "description": "Places de spectacle du participant",
- "custommail": 12,
- "type": 11
- }
+ "description": "Places de spectacle du participant"
+ },
+ "pk": 31
},
{
- "model": "custommail.custommailvariable",
- "pk": 32,
+ "model": "custommail.variable",
"fields": {
- "name": "member",
- "description": "Participant du tirage au sort",
"custommail": 12,
- "type": 1
- }
+ "type": 1,
+ "name": "member",
+ "description": "Participant du tirage au sort"
+ },
+ "pk": 32
},
{
- "model": "custommail.custommailvariable",
- "pk": 33,
+ "model": "custommail.variable",
"fields": {
- "name": "member",
- "description": "Participant du tirage au sort",
"custommail": 13,
- "type": 1
- }
+ "type": 1,
+ "name": "member",
+ "description": "Participant du tirage au sort"
+ },
+ "pk": 33
}
]
From 785555c05cc874dfc2a9542608c0e94baffccc2e Mon Sep 17 00:00:00 2001
From: Ludovic Stephan
Date: Thu, 26 Oct 2017 12:40:11 +0200
Subject: [PATCH 046/101] Misc fixes
---
bda/forms.py | 10 ++++++++--
bda/migrations/0012_notif_time.py | 7 ++++---
bda/models.py | 11 ++++++-----
bda/views.py | 33 +++++++++++--------------------
4 files changed, 30 insertions(+), 31 deletions(-)
diff --git a/bda/forms.py b/bda/forms.py
index 11d05b0e..2929f771 100644
--- a/bda/forms.py
+++ b/bda/forms.py
@@ -43,11 +43,13 @@ class TokenForm(forms.Form):
class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj):
- return "%s" % str(obj.spectacle)
+ return str(obj.spectacle)
+
class ReventeModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj):
- return "%s" % str(obj.attribution.spectacle)
+ return str(obj.attribution.spectacle)
+
class ResellForm(forms.Form):
attributions = AttributionModelMultipleChoiceField(
@@ -66,6 +68,7 @@ class ResellForm(forms.Form):
'participant__user')
)
+
class AnnulForm(forms.Form):
attributions = AttributionModelMultipleChoiceField(
label='',
@@ -85,6 +88,7 @@ class AnnulForm(forms.Form):
'participant__user')
)
+
class InscriptionReventeForm(forms.Form):
spectacles = forms.ModelMultipleChoiceField(
queryset=Spectacle.objects.none(),
@@ -99,6 +103,7 @@ class InscriptionReventeForm(forms.Form):
.filter(date__gte=timezone.now())
)
+
class ReventeTirageAnnulForm(forms.Form):
reventes = ReventeModelMultipleChoiceField(
label='',
@@ -134,6 +139,7 @@ class ReventeTirageForm(forms.Form):
.select_related('attribution__spectacle')
)
+
class SoldForm(forms.Form):
attributions = AttributionModelMultipleChoiceField(
label='',
diff --git a/bda/migrations/0012_notif_time.py b/bda/migrations/0012_notif_time.py
index be66efd1..ee777e35 100644
--- a/bda/migrations/0012_notif_time.py
+++ b/bda/migrations/0012_notif_time.py
@@ -11,11 +11,12 @@ class Migration(migrations.Migration):
]
operations = [
- migrations.RemoveField(
+ migrations.RenameField(
model_name='spectaclerevente',
- name='answered_mail',
+ old_name='answered_mail',
+ new_name='confirmed_entry',
),
- migrations.AddField(
+ migrations.AlterField(
model_name='spectaclerevente',
name='confirmed_entry',
field=models.ManyToManyField(blank=True, related_name='entered', to='bda.Participant'),
diff --git a/bda/models.py b/bda/models.py
index 2ad47dbf..59827621 100644
--- a/bda/models.py
+++ b/bda/models.py
@@ -168,6 +168,7 @@ class Participant(models.Model):
def __str__(self):
return "%s - %s" % (self.user, self.tirage.title)
+
DOUBLE_CHOICES = (
("1", "1 place"),
("autoquit", "2 places si possible, 1 sinon"),
@@ -292,15 +293,16 @@ class SpectacleRevente(models.Model):
class Meta:
verbose_name = "Revente"
- def reset(self):
+ def reset(self, new_date=timezone.now()):
"""Réinitialise la revente pour permettre une remise sur le marché"""
self.seller = self.attribution.participant
- self.date = timezone.now()
+ self.date = new_date
self.confirmed_entry.clear()
self.soldTo = None
self.notif_sent = False
self.tirage_done = False
self.shotgun = False
+ self.save()
def send_notif(self):
"""
@@ -396,11 +398,10 @@ class SpectacleRevente(models.Model):
))
send_mass_custom_mail(datatuple)
- return winner
-
# Si personne ne veut de la place, elle part au shotgun
else:
+ winner = None
self.shotgun = True
- return None
self.tirage_done = True
self.save()
+ return winner
diff --git a/bda/views.py b/bda/views.py
index 6ed22b21..fb1a2e82 100644
--- a/bda/views.py
+++ b/bda/views.py
@@ -5,7 +5,6 @@ import random
import hashlib
import time
import json
-from datetime import timedelta
from custommail.shortcuts import send_mass_custom_mail, send_custom_mail
from custommail.models import CustomMail
from django.shortcuts import render, get_object_or_404
@@ -14,6 +13,7 @@ from django.contrib import messages
from django.db import transaction
from django.core import serializers
from django.db.models import Count, Q, Prefetch
+from django.template.defaultfilters import pluralize
from django.forms.models import inlineformset_factory
from django.http import (
HttpResponseBadRequest, HttpResponseRedirect, JsonResponse
@@ -376,6 +376,7 @@ def revente_manage(request, tirage_id):
defaults={'seller': participant})
if not created:
revente.reset()
+
context = {
'vendeur': participant.user,
'show': attribution.spectacle,
@@ -414,15 +415,10 @@ def revente_manage(request, tirage_id):
attributions = soldform.cleaned_data['attributions']
for attribution in attributions:
if attribution.spectacle.date > timezone.now():
- revente = attribution.revente
- revente.date = timezone.now() - timedelta(minutes=65)
- revente.soldTo = None
- revente.notif_sent = False
- revente.tirage_done = False
- revente.shotgun = False
- if revente.confirmed_entry:
- revente.confirmed_entry.clear()
- revente.save()
+ # On antidate pour envoyer le mail plus vite
+ new_date = (timezone.now()
+ - SpectacleRevente.remorse_time)
+ revente.reset(new_date=new_date)
overdue = participant.attribution_set.filter(
spectacle__date__gte=timezone.now(),
@@ -442,7 +438,6 @@ def revente_tirages(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id)
participant, _ = Participant.objects.get_or_create(
user=request.user, tirage=tirage)
- unsub = 0
subform = ReventeTirageForm(participant, prefix="subscribe")
annulform = ReventeTirageAnnulForm(participant, prefix="annul")
@@ -451,33 +446,29 @@ def revente_tirages(request, tirage_id):
subform = ReventeTirageForm(participant, request.POST,
prefix="subscribe")
if subform.is_valid():
- sub = 0
reventes = subform.cleaned_data['reventes']
+ count = reventes.count()
for revente in reventes:
revente.confirmed_entry.add(participant)
- sub += 1
- if sub > 0:
- plural = "s" if sub > 1 else ""
+ if count > 0:
messages.success(
request,
"Tu as bien été inscrit à {} revente{}"
- .format(sub, plural)
+ .format(count, pluralize(count))
)
elif "annul" in request.POST:
annulform = ReventeTirageAnnulForm(participant, request.POST,
prefix="annul")
if annulform.is_valid():
- unsub = 0
reventes = annulform.cleaned_data['reventes']
+ count = reventes.count()
for revente in reventes:
revente.confirmed_entry.remove(participant)
- unsub += 1
- if unsub > 0:
- plural = "s" if unsub > 1 else ""
+ if count > 0:
messages.success(
request,
"Tu as bien été désinscrit de {} revente{}"
- .format(unsub, plural)
+ .format(count, pluralize(count))
)
return render(request, "bda/revente/tirages.html",
From 1c90d067fa38542ca0877e8ba22dcd2a6108ad8f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Martin=20P=C3=A9pin?=
Date: Thu, 26 Oct 2017 18:13:09 +0200
Subject: [PATCH 047/101] Make cof.settings a module
---
cof/settings/__init__.py | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 cof/settings/__init__.py
diff --git a/cof/settings/__init__.py b/cof/settings/__init__.py
new file mode 100644
index 00000000..e69de29b
From 895f7e062cc9da5d590f18c6ed36fa1f1e6738cf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?=
Date: Fri, 27 Oct 2017 03:00:33 +0200
Subject: [PATCH 048/101] Delete GlobalPermissions model (migrations)
It is an old model which doesn't exist anymore in kfet.models module.
This adds its missing DeleteModel in migrations.
---
kfet/migrations/0062_delete_globalpermissions.py | 14 ++++++++++++++
1 file changed, 14 insertions(+)
create mode 100644 kfet/migrations/0062_delete_globalpermissions.py
diff --git a/kfet/migrations/0062_delete_globalpermissions.py b/kfet/migrations/0062_delete_globalpermissions.py
new file mode 100644
index 00000000..ee245412
--- /dev/null
+++ b/kfet/migrations/0062_delete_globalpermissions.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('kfet', '0061_add_perms_config'),
+ ]
+
+ operations = [
+ migrations.DeleteModel(
+ name='GlobalPermissions',
+ ),
+ ]
From 93fa79128cd4688e8fbe1adb07db49409cc2d9d8 Mon Sep 17 00:00:00 2001
From: Qwann
Date: Tue, 31 Oct 2017 15:10:21 +0100
Subject: [PATCH 049/101] order table striped
---
kfet/templates/kfet/order_create.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/kfet/templates/kfet/order_create.html b/kfet/templates/kfet/order_create.html
index 6ff9b824..d95cafe3 100644
--- a/kfet/templates/kfet/order_create.html
+++ b/kfet/templates/kfet/order_create.html
@@ -11,7 +11,7 @@