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 01/27] [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 02/27] 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 03/27] 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 04/27] 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 05/27] 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 06/27] 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 07/27] 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 08/27] 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 09/27] 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 10/27] 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 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 11/27] =?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 12/27] 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 13/27] 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 14/27] 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 15/27] =?UTF-8?q?Fix=20fields=20cleaning=20with=20unreacha?= =?UTF-8?q?ble=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 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 16/27] 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 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 17/27] 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 18/27] 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 19/27] 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 20/27] =?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 21/27] 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 22/27] 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 23/27] 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 24/27] 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 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 25/27] 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 26/27] 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 273e6374ef072c6b589ddb46268a8085a2085359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Wed, 1 Nov 2017 11:09:16 +0100 Subject: [PATCH 27/27] Pluralization in bda -> participant list --- bda/templates/bda/participants.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bda/templates/bda/participants.html b/bda/templates/bda/participants.html index 85af4a2e..c3ff31d6 100644 --- a/bda/templates/bda/participants.html +++ b/bda/templates/bda/participants.html @@ -47,11 +47,11 @@
- +