Merge branch 'master' into aureplop/cof-tests_event

This commit is contained in:
Martin Pépin 2018-04-06 00:16:08 +02:00
commit bf464f9378
22 changed files with 2049 additions and 49 deletions

View file

@ -14,7 +14,7 @@
</tr></thead> </tr></thead>
<tbody class="bda_formset_content"> <tbody class="bda_formset_content">
{% endif %} {% endif %}
<tr class="{% cycle row1,row2 %} dynamic-form {% if form.instance.pk %}has_original{% endif %}"> <tr class="{% cycle 'row1' 'row2' %} dynamic-form {% if form.instance.pk %}has_original{% endif %}">
{% for field in form.visible_fields %} {% for field in form.visible_fields %}
{% if field.name != "DELETE" and field.name != "priority" %} {% if field.name != "DELETE" and field.name != "priority" %}
<td class="bda-field-{{ field.name }}"> <td class="bda-field-{{ field.name }}">

View file

@ -27,6 +27,14 @@ var django = {
var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-'); var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-');
$(this).attr('for', newFor); $(this).attr('for', newFor);
}); });
// Cloning <select> element doesn't properly propagate the default
// selected <option>, so we set it manually.
newElement.find('select').each(function (index, select) {
var defaultValue = $(select).find('option[selected]').val();
if (typeof defaultValue !== 'undefined') {
$(select).val(defaultValue);
}
});
total++; total++;
$('#id_' + type + '-TOTAL_FORMS').val(total); $('#id_' + type + '-TOTAL_FORMS').val(total);
$(selector).after(newElement); $(selector).after(newElement);

View file

@ -7,6 +7,7 @@ the local development server should be here.
""" """
import os import os
import sys
try: try:
from . import secret from . import secret
@ -53,9 +54,13 @@ BASE_DIR = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))) os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
) )
TESTING = sys.argv[1] == 'test'
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'shared',
'gestioncof', 'gestioncof',
# Must be before 'django.contrib.admin'. # Must be before 'django.contrib.admin'.

View file

@ -4,13 +4,18 @@ The settings that are not listed here are imported from .common
""" """
from .common import * # NOQA from .common import * # NOQA
from .common import INSTALLED_APPS, MIDDLEWARE from .common import INSTALLED_APPS, MIDDLEWARE, TESTING
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
DEBUG = True DEBUG = True
if TESTING:
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.MD5PasswordHasher',
]
# --- # ---
# Apache static/media config # Apache static/media config
@ -36,12 +41,13 @@ def show_toolbar(request):
""" """
return DEBUG return DEBUG
INSTALLED_APPS += ["debug_toolbar", "debug_panel"] if not TESTING:
INSTALLED_APPS += ["debug_toolbar", "debug_panel"]
MIDDLEWARE = [ MIDDLEWARE = [
"debug_panel.middleware.DebugPanelMiddleware" "debug_panel.middleware.DebugPanelMiddleware"
] + MIDDLEWARE ] + MIDDLEWARE
DEBUG_TOOLBAR_CONFIG = { DEBUG_TOOLBAR_CONFIG = {
'SHOW_TOOLBAR_CALLBACK': show_toolbar, 'SHOW_TOOLBAR_CALLBACK': show_toolbar,
} }

View file

@ -8,7 +8,7 @@
{% if survey.details %} {% if survey.details %}
<p>{{ survey.details }}</p> <p>{{ survey.details }}</p>
{% endif %} {% endif %}
<form class="form-horizontal" method="post" action="{% url 'gestioncof.views.survey' survey.id %}"> <form class="form-horizontal" method="post" action="{% url 'survey.details' survey.id %}">
{% csrf_token %} {% csrf_token %}
{{ form | bootstrap}} {{ form | bootstrap}}

View file

@ -16,7 +16,7 @@
</tr></thead> </tr></thead>
<tbody class="bda_formset_content"> <tbody class="bda_formset_content">
{% endif %} {% endif %}
<tr class="{% cycle row1,row2 %} dynamic-form {% if form.instance.pk %}has_original{% endif %}"> <tr class="{% cycle 'row1' 'row2' %} dynamic-form {% if form.instance.pk %}has_original{% endif %}">
{% for field in form.visible_fields %} {% for field in form.visible_fields %}
{% if field.name != "DELETE" and field.name != "priority" %} {% if field.name != "DELETE" and field.name != "priority" %}
<td class="bda-field-{{ field.name }}"> <td class="bda-field-{{ field.name }}">

View file

@ -4,7 +4,7 @@
{% block page_size %}col-sm-8{% endblock %} {% block page_size %}col-sm-8{% endblock %}
{% block extra_head %} {% block extra_head %}
<script src="{% static "autocomplete_light/autocomplete.js" %}" type="text/javascript"></script> <script src="{% static "vendor/jquery.autocomplete-light/3.5.0/dist/jquery.autocomplete-light.min.js" %}" type="text/javascript"></script>
{% endblock %} {% endblock %}
{% block realcontent %} {% block realcontent %}

View file

@ -11,7 +11,7 @@
{% endif %} {% endif %}
<h3>Filtres</h3> <h3>Filtres</h3>
{% include "tristate_js.html" %} {% include "tristate_js.html" %}
<form method="post" action="{% url 'gestioncof.views.survey_status' survey.id %}"> <form method="post" action="{% url 'survey.details.status' survey.id %}">
{% csrf_token %} {% csrf_token %}
{{ form.as_p }} {{ form.as_p }}
<input style="margin-top:10px;" type="submit" class="btn btn-primary" value="Filtrer" /> <input style="margin-top:10px;" type="submit" class="btn btn-primary" value="Filtrer" />

View file

@ -3,13 +3,12 @@ from django.contrib.messages import get_messages
from django.contrib.messages.storage.base import Message from django.contrib.messages.storage.base import Message
from django.test import TestCase from django.test import TestCase
from gestioncof.models import Event from gestioncof.models import Event, Survey, SurveyAnswer
from gestioncof.tests.testcases import ViewTestCaseMixin from gestioncof.tests.testcases import ViewTestCaseMixin
class EventViewTests(ViewTestCaseMixin, TestCase): class EventViewTests(ViewTestCaseMixin, TestCase):
url_name = 'event.details' url_name = 'event.details'
http_methods = ['GET', 'POST'] http_methods = ['GET', 'POST']
auth_user = 'user' auth_user = 'user'
@ -161,3 +160,173 @@ class EventStatusViewTests(ViewTestCaseMixin, TestCase):
def test_filter_no(self): def test_filter_no(self):
self._test_filters([(self.oc1, 'no')], [self.er2]) self._test_filters([(self.oc1, 'no')], [self.er2])
class SurveyViewTests(ViewTestCaseMixin, TestCase):
url_name = 'survey.details'
http_methods = ['GET', 'POST']
auth_user = 'user'
auth_forbidden = [None]
post_expected_message = Message(messages.SUCCESS, (
"Votre réponse a bien été enregistrée ! Vous pouvez cependant la "
"modifier jusqu'à la fin du sondage."
))
@property
def url_kwargs(self):
return {'survey_id': self.s.pk}
@property
def url_expected(self):
return '/survey/{}'.format(self.s.pk)
def setUp(self):
super().setUp()
self.s = Survey.objects.create(title='Title')
self.q1 = self.s.questions.create(question='Question 1 ?')
self.q2 = self.s.questions.create(
question='Question 2 ?',
multi_answers=True,
)
self.qa1 = self.q1.answers.create(answer='Q1 - Answer 1')
self.qa2 = self.q1.answers.create(answer='Q1 - Answer 2')
self.qa3 = self.q2.answers.create(answer='Q2 - Answer 1')
self.qa4 = self.q2.answers.create(answer='Q2 - Answer 2')
def test_get(self):
r = self.client.get(self.url)
self.assertEqual(r.status_code, 200)
def test_post_new(self):
r = self.client.post(self.url, {
'question_{}'.format(self.q1.pk): [str(self.qa1.pk)],
'question_{}'.format(self.q2.pk): [
str(self.qa3.pk), str(self.qa4.pk),
],
})
self.assertEqual(r.status_code, 200)
self.assertIn(self.post_expected_message, get_messages(r.wsgi_request))
a = self.s.surveyanswer_set.get(user=self.users['user'])
self.assertQuerysetEqual(
a.answers.all(), map(repr, [self.qa1, self.qa3, self.qa4]),
ordered=False,
)
def test_post_edit(self):
a = self.s.surveyanswer_set.create(user=self.users['user'])
a.answers.add(self.qa1, self.qa1, self.qa4)
r = self.client.post(self.url, {
'question_{}'.format(self.q1.pk): [],
'question_{}'.format(self.q2.pk): [str(self.qa3.pk)],
})
self.assertEqual(r.status_code, 200)
self.assertIn(self.post_expected_message, get_messages(r.wsgi_request))
a.refresh_from_db()
self.assertQuerysetEqual(
a.answers.all(), map(repr, [self.qa3]),
ordered=False,
)
def test_post_delete(self):
a = self.s.surveyanswer_set.create(user=self.users['user'])
a.answers.add(self.qa1, self.qa4)
r = self.client.post(self.url, {'delete': '1'})
self.assertEqual(r.status_code, 200)
expected_message = Message(
messages.SUCCESS, "Votre réponse a bien été supprimée")
self.assertIn(expected_message, get_messages(r.wsgi_request))
with self.assertRaises(SurveyAnswer.DoesNotExist):
a.refresh_from_db()
def test_forbidden_closed(self):
self.s.survey_open = False
self.s.save()
r = self.client.get(self.url)
self.assertNotEqual(r.status_code, 200)
def test_forbidden_old(self):
self.s.old = True
self.s.save()
r = self.client.get(self.url)
self.assertNotEqual(r.status_code, 200)
class SurveyStatusViewTests(ViewTestCaseMixin, TestCase):
url_name = 'survey.details.status'
http_methods = ['GET', 'POST']
auth_user = 'staff'
auth_forbidden = [None, 'user', 'member']
@property
def url_kwargs(self):
return {'survey_id': self.s.pk}
@property
def url_expected(self):
return '/survey/{}/status'.format(self.s.pk)
def setUp(self):
super().setUp()
self.s = Survey.objects.create(title='Title')
self.q1 = self.s.questions.create(question='Question 1 ?')
self.q2 = self.s.questions.create(
question='Question 2 ?',
multi_answers=True,
)
self.qa1 = self.q1.answers.create(answer='Q1 - Answer 1')
self.qa2 = self.q1.answers.create(answer='Q1 - Answer 2')
self.qa3 = self.q2.answers.create(answer='Q2 - Answer 1')
self.qa4 = self.q2.answers.create(answer='Q2 - Answer 2')
self.a1 = self.s.surveyanswer_set.create(user=self.users['user'])
self.a1.answers.add(self.qa1)
self.a2 = self.s.surveyanswer_set.create(user=self.users['member'])
def test_get(self):
r = self.client.get(self.url)
self.assertEqual(r.status_code, 200)
def _get_qa_filter_name(self, qa):
return 'question_{}_answer_{}'.format(qa.survey_question.pk, qa.pk)
def _test_filters(self, filters, expected):
r = self.client.post(self.url, {
self._get_qa_filter_name(qa): v for qa, v in filters
})
self.assertEqual(r.status_code, 200)
self.assertQuerysetEqual(
r.context['user_answers'], map(repr, expected),
ordered=False,
)
def test_filter_none(self):
self._test_filters([(self.qa1, 'none')], [self.a1, self.a2])
def test_filter_yes(self):
self._test_filters([(self.qa1, 'yes')], [self.a1])
def test_filter_no(self):
self._test_filters([(self.qa1, 'no')], [self.a2])

View file

@ -296,17 +296,17 @@ class KPsulAccountForm(forms.ModelForm):
class KPsulCheckoutForm(forms.Form): class KPsulCheckoutForm(forms.Form):
checkout = forms.ModelChoiceField( checkout = forms.ModelChoiceField(
queryset=( queryset=None,
Checkout.objects
.filter(
is_protected=False,
valid_from__lte=timezone.now(),
valid_to__gte=timezone.now(),
)
),
widget=forms.Select(attrs={'id': 'id_checkout_select'}), widget=forms.Select(attrs={'id': 'id_checkout_select'}),
) )
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Create the queryset on form instanciation to use the current time.
self.fields['checkout'].queryset = (
Checkout.objects.is_valid().filter(is_protected=False))
class KPsulOperationForm(forms.ModelForm): class KPsulOperationForm(forms.ModelForm):
article = forms.ModelChoiceField( article = forms.ModelChoiceField(

View file

@ -341,6 +341,13 @@ class AccountNegative(models.Model):
return self.start + kfet_config.overdraft_duration return self.start + kfet_config.overdraft_duration
class CheckoutQuerySet(models.QuerySet):
def is_valid(self):
now = timezone.now()
return self.filter(valid_from__lte=now, valid_to__gte=now)
class Checkout(models.Model): class Checkout(models.Model):
created_by = models.ForeignKey( created_by = models.ForeignKey(
Account, on_delete = models.PROTECT, Account, on_delete = models.PROTECT,
@ -353,6 +360,8 @@ class Checkout(models.Model):
default = 0) default = 0)
is_protected = models.BooleanField(default = False) is_protected = models.BooleanField(default = False)
objects = CheckoutQuerySet.as_manager()
def get_absolute_url(self): def get_absolute_url(self):
return reverse('kfet.checkout.read', kwargs={'pk': self.pk}) return reverse('kfet.checkout.read', kwargs={'pk': self.pk})
@ -362,6 +371,22 @@ class Checkout(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
def save(self, *args, **kwargs):
created = self.pk is None
ret = super().save(*args, **kwargs)
if created:
self.statements.create(
amount_taken=0,
balance_old=self.balance,
balance_new=self.balance,
by=self.created_by,
)
return ret
class CheckoutTransfer(models.Model): class CheckoutTransfer(models.Model):
from_checkout = models.ForeignKey( from_checkout = models.ForeignKey(
Checkout, on_delete = models.PROTECT, Checkout, on_delete = models.PROTECT,

View file

@ -5,7 +5,7 @@
{% block header-title %}Création d'un compte{% endblock %} {% block header-title %}Création d'un compte{% endblock %}
{% block extra_head %} {% block extra_head %}
<script src="{% static "autocomplete_light/autocomplete.js" %}" type="text/javascript"></script> <script src="{% static "vendor/jquery.autocomplete-light/3.5.0/dist/jquery.autocomplete-light.min.js" %}" type="text/javascript"></script>
{% endblock %} {% endblock %}
{% block main %} {% block main %}

48
kfet/tests/test_forms.py Normal file
View file

@ -0,0 +1,48 @@
import datetime
from unittest import mock
from django.test import TestCase
from django.utils import timezone
from kfet.forms import KPsulCheckoutForm
from kfet.models import Checkout
from .utils import create_user
class KPsulCheckoutFormTests(TestCase):
def setUp(self):
self.now = timezone.now()
user = create_user()
self.c1 = Checkout.objects.create(
name='C1', balance=10,
created_by=user.profile.account_kfet,
valid_from=self.now,
valid_to=self.now + datetime.timedelta(days=1),
)
self.form = KPsulCheckoutForm()
def test_checkout(self):
checkout_f = self.form.fields['checkout']
self.assertListEqual(list(checkout_f.choices), [
('', '---------'),
(self.c1.pk, 'C1'),
])
@mock.patch('django.utils.timezone.now')
def test_checkout_valid(self, mock_now):
"""
Checkout are filtered using the current datetime.
Regression test for #184.
"""
self.now += datetime.timedelta(days=2)
mock_now.return_value = self.now
form = KPsulCheckoutForm()
checkout_f = form.fields['checkout']
self.assertListEqual(list(checkout_f.choices), [('', '---------')])

View file

@ -1,7 +1,12 @@
import datetime
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.test import TestCase from django.test import TestCase
from django.utils import timezone
from kfet.models import Account from kfet.models import Account, Checkout
from .utils import create_user
User = get_user_model() User = get_user_model()
@ -23,3 +28,33 @@ class AccountTests(TestCase):
with self.assertRaises(Account.DoesNotExist): with self.assertRaises(Account.DoesNotExist):
Account.objects.get_by_password('bernard') Account.objects.get_by_password('bernard')
class CheckoutTests(TestCase):
def setUp(self):
self.now = timezone.now()
self.u = create_user()
self.u_acc = self.u.profile.account_kfet
self.c = Checkout(
created_by=self.u_acc,
valid_from=self.now,
valid_to=self.now + datetime.timedelta(days=1),
)
def test_initial_statement(self):
"""A statement is added with initial balance on creation."""
self.c.balance = 10
self.c.save()
st = self.c.statements.get()
self.assertEqual(st.balance_new, 10)
self.assertEqual(st.amount_taken, 0)
self.assertEqual(st.amount_error, 0)
# Saving again doesn't create a new statement.
self.c.save()
self.assertEqual(self.c.statements.count(), 1)

View file

@ -746,11 +746,15 @@ class CheckoutReadViewTests(ViewTestCaseMixin, TestCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
with mock.patch('django.utils.timezone.now') as mock_now:
mock_now.return_value = self.now
self.checkout = Checkout.objects.create( self.checkout = Checkout.objects.create(
name='Checkout', name='Checkout', balance=Decimal('10'),
created_by=self.accounts['team'], created_by=self.accounts['team'],
valid_from=self.now, valid_from=self.now,
valid_to=self.now + timedelta(days=5), valid_to=self.now + timedelta(days=1),
) )
def test_ok(self): def test_ok(self):
@ -794,7 +798,7 @@ class CheckoutUpdateViewTests(ViewTestCaseMixin, TestCase):
name='Checkout', name='Checkout',
valid_from=self.now, valid_from=self.now,
valid_to=self.now + timedelta(days=5), valid_to=self.now + timedelta(days=5),
balance='3.14', balance=Decimal('3.14'),
is_protected=False, is_protected=False,
created_by=self.accounts['team'], created_by=self.accounts['team'],
) )
@ -864,6 +868,7 @@ class CheckoutStatementListViewTests(ViewTestCaseMixin, TestCase):
self.assertQuerysetEqual( self.assertQuerysetEqual(
r.context['checkoutstatements'], r.context['checkoutstatements'],
map(repr, expected_statements), map(repr, expected_statements),
ordered=False,
) )

View file

@ -245,13 +245,7 @@ class ViewTestCaseMixin(TestCaseMixin):
self.register_user(label, user) self.register_user(label, user)
if self.auth_user: if self.auth_user:
# The wrapper is a sanity check. self.client.force_login(self.users[self.auth_user])
self.assertTrue(
self.client.login(
username=self.auth_user,
password=self.auth_user,
)
)
def tearDown(self): def tearDown(self):
del self.users_base del self.users_base

View file

@ -528,15 +528,7 @@ class CheckoutCreate(SuccessMessageMixin, CreateView):
# Creating # Creating
form.instance.created_by = self.request.user.profile.account_kfet form.instance.created_by = self.request.user.profile.account_kfet
checkout = form.save() form.save()
# Création d'un relevé avec balance initiale
CheckoutStatement.objects.create(
checkout = checkout,
by = self.request.user.profile.account_kfet,
balance_old = checkout.balance,
balance_new = checkout.balance,
amount_taken = 0)
return super(CheckoutCreate, self).form_valid(form) return super(CheckoutCreate, self).form_valid(form)

View file

@ -1,5 +1,8 @@
#!/bin/sh #!/bin/sh
# Stop if an error is encountered
set -e
# Configuration de la base de données. Le mot de passe est constant car c'est # Configuration de la base de données. Le mot de passe est constant car c'est
# pour une installation de dév locale qui ne sera accessible que depuis la # pour une installation de dév locale qui ne sera accessible que depuis la
# machine virtuelle. # machine virtuelle.

View file

@ -1,5 +1,8 @@
#!/bin/bash #!/bin/bash
# Stop if an error is encountered.
set -e
python manage.py migrate python manage.py migrate
python manage.py loaddata gestion sites articles python manage.py loaddata gestion sites articles
python manage.py loaddevdata python manage.py loaddevdata

View file

@ -11,7 +11,7 @@ psycopg2
Pillow Pillow
six six
unicodecsv unicodecsv
django-bootstrap-form==3.2.1 django-bootstrap-form==3.3
asgiref==1.1.1 asgiref==1.1.1
daphne==1.3.0 daphne==1.3.0
asgi-redis==1.3.0 asgi-redis==1.3.0

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long