diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 85be668b..19bcc736 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,12 +30,12 @@ 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 + - pip install --upgrade --cache-dir vendor/pip -t vendor/python -r requirements.txt test: stage: test script: - - python manage.py test -v3 + - python manage.py test 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'), ), ] 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/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/autocomplete.py b/kfet/autocomplete.py index 09057d4a..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) @@ -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 diff --git a/kfet/forms.py b/kfet/forms.py index 66638e6c..963e4254 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -129,7 +129,6 @@ class UserRestrictTeamForm(UserForm): fields = ['first_name', 'last_name', 'email'] - class AccountNegativeForm(forms.ModelForm): class Meta: model = AccountNegative 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/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/0060_amend_supplier.py b/kfet/migrations/0060_amend_supplier.py new file mode 100644 index 00000000..4eb569f8 --- /dev/null +++ b/kfet/migrations/0060_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', '0059_create_generic'), + ] + + 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 e547d248..b1e351d5 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 @@ -94,6 +95,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): @@ -540,21 +543,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) 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) 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 ec9d6ffc..c7ed5dda 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -1,26 +1,216 @@ +import json +from datetime import datetime, timedelta from decimal import Decimal -from unittest.mock import patch +from unittest import mock -from django.test import TestCase, Client -from django.contrib.auth.models import User +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, OperationGroup, Checkout, Operation +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, get_perms -class AccountTests(TestCase): - """Account related views""" +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' + + 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', + } + + def get_users_extra(self): + return { + 'team1': create_team('team1', '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): + 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', + }) + + def test_post_forbidden(self): + 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', + }, { + '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]) + self.assertEqual(r.status_code, 200) + + user = self.users['user'] + + 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[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) + + +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.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): + 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.assertSetEqual(set(r.context['accounts']), set([ + ('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'] + + def get_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 +220,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 +237,1999 @@ 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' + + 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': '', + } + + def get_users_extra(self): + return { + 'user1': create_user('user1', '001'), + 'team1': create_team('team1', '101', perms=[ + 'kfet.change_account', + ]), + } + + def test_get_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + 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') + + 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'] + + def get_users_extra(self): + return { + 'team1': create_team('team1', '101', perms=['kfet.manage_perms']), + } + + 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) + self.assertEqual(r.status_code, 200) + + self.assertQuerysetEqual( + r.context['groups'], + map(repr, [self.group1, self.group2]), + ordered=False, + ) + + +class AccountGroupCreateViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.account.group.create' + url_expected = '/k-fet/accounts/groups/new' + + http_methods = ['GET', 'POST'] + + auth_user = 'team1' + auth_forbidden = [None, 'user', 'team'] + + def get_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', + ) + + 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')) + + 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) + + def get_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'] + + def get_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'] + + def get_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'] + + def get_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'] + + def get_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'] + + def get_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 + } + + def get_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) + + def get_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) + + def get_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, + ) + + def get_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) + + def get_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'] + + def get_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) + + def get_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', + } + + def get_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'] + + def get_users_extra(self): + return { + 'team1': create_team('team1', '101', perms=[ + 'kfet.see_config', + ]), + } + + 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', + } + + def get_users_extra(self): + return { + 'team1': create_team('team1', '101', perms=[ + 'kfet.change_config', + ]), + } + + 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) + # Redirect is skipped because client may lack permissions. + self.assertRedirects( + r, + reverse('kfet.settings'), + fetch_redirect_response=False, + ) + + expected_config = { + 'reduction_cof': Decimal('25'), + 'addcost_amount': Decimal('0.5'), + 'addcost_for': self.accounts['user'], + 'overdraft_duration': timedelta(days=2), + 'overdraft_amount': Decimal('25'), + 'cancel_duration': timedelta(minutes=20), + } + + for key, expected in expected_config.items(): + self.assertEqual(getattr(kfet_config, key), expected) + + +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'] + + def get_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'] + + def get_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'] + + def get_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) + + def get_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) + + def get_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) + + def get_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 new file mode 100644 index 00000000..d7d7eac5 --- /dev/null +++ b/kfet/tests/testcases.py @@ -0,0 +1,353 @@ +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 django.utils.functional import cached_property + +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: + 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'): + """ + 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: + 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): + """ + 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): + value = value() + 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) + 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 '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 + 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 + + http_methods = ['GET'] + + auth_user = None + 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. + patcher_messages = mock.patch('gestioncof.signals.messages') + 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() + + # These attributes register users and accounts instances. + self.users = {} + self.accounts = {} + + for label, user in dict(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, + ) + ) + + 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 + 'user': create_user(), + # team, team, 100 + 'team': create_team(), + # root, root, 200 + 'root': create_root(), + } + + @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'): + 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 [{ + '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 test_urls(self): + for url, conf in zip(self.t_urls, self.urls_conf): + self.assertEqual(url, conf['expected']) + + def test_forbidden(self): + 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 new file mode 100644 index 00000000..30eb05ad --- /dev/null +++ b/kfet/tests/utils.py @@ -0,0 +1,188 @@ +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Permission + +from ..models import Account + + +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) + user.save() + + account_attrs['cofprofile'] = user.profile + kfet_pwd = account_attrs.pop('password', 'kfetpwd_{}'.format(user_pwd)) + + account = Account.objects.create(**account_attrs) + + if perms is not None: + user = user_add_perms(user, perms) + + if 'kfet.is_team' in perms: + account.change_pwd(kfet_pwd) + account.save() + + return user + + +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) + 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): + """ + 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) + 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): + """ + 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) + 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): + """Return Permission instances from a list of '.'.""" + 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) diff --git a/kfet/urls.py b/kfet/urls.py index eb4f8311..f39299a5 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 79fe184d..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 @@ -1401,6 +1401,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' @@ -1409,13 +1412,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 diff --git a/manage.py b/manage.py old mode 100644 new mode 100755