Merge branch 'master' into aureplop/cof-tests_club

This commit is contained in:
Martin Pépin 2018-04-07 10:49:52 +02:00
commit 660f395b67
30 changed files with 2420 additions and 55 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

@ -16,7 +16,7 @@
<h4 class="bda-prix">Total à payer : {{ total|floatformat }}€</h4> <h4 class="bda-prix">Total à payer : {{ total|floatformat }}€</h4>
<br/> <br/>
<p>Ne manque pas un spectacle avec le <p>Ne manque pas un spectacle avec le
<a href="{% url "gestioncof.views.calendar" %}">calendrier <a href="{% url "calendar" %}">calendrier
automatique&#8239;!</a></p> automatique&#8239;!</a></p>
{% else %} {% else %}
<h3>Vous n'avez aucune place :(</h3> <h3>Vous n'avez aucune place :(</h3>

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

@ -351,10 +351,12 @@ EventFormset = formset_factory(AdminEventForm, BaseEventRegistrationFormset)
class CalendarForm(forms.ModelForm): class CalendarForm(forms.ModelForm):
subscribe_to_events = forms.BooleanField( subscribe_to_events = forms.BooleanField(
initial=True, initial=True,
label="Événements du COF") label="Événements du COF",
required=False)
subscribe_to_my_shows = forms.BooleanField( subscribe_to_my_shows = forms.BooleanField(
initial=True, initial=True,
label="Les spectacles pour lesquels j'ai obtenu une place") label="Les spectacles pour lesquels j'ai obtenu une place",
required=False)
other_shows = forms.ModelMultipleChoiceField( other_shows = forms.ModelMultipleChoiceField(
label="Spectacles supplémentaires", label="Spectacles supplémentaires",
queryset=Spectacle.objects.filter(tirage__active=True), queryset=Spectacle.objects.filter(tirage__active=True),

View file

@ -11,7 +11,7 @@
{% endif %} {% endif %}
{% include "tristate_js.html" %} {% include "tristate_js.html" %}
<h3>Filtres</h3> <h3>Filtres</h3>
<form method="post" action="{% url 'gestioncof.views.event_status' event.id %}"> <form method="post" action="{% url 'event.details.status' event.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

@ -12,7 +12,7 @@ souscrire aux événements du COF et/ou aux spectacles BdA.
{% if token %} {% if token %}
<p>Votre calendrier (compatible avec toutes les applications d'agenda) se trouve à <p>Votre calendrier (compatible avec toutes les applications d'agenda) se trouve à
<a href="{% url 'gestioncof.views.calendar_ics' token %}">cette adresse</a>.</p> <a href="{% url 'calendar.ics' token %}">cette adresse</a>.</p>
<ul> <ul>
<li>Pour l'ajouter à Thunderbird (lightning), il faut copier ce lien et aller <li>Pour l'ajouter à Thunderbird (lightning), il faut copier ce lien et aller

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

@ -1,7 +1,16 @@
import uuid
from datetime import timedelta
from django.contrib import messages
from django.contrib.messages.api import get_messages
from django.contrib.messages.storage.base import Message
from django.test import Client, TestCase from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from gestioncof.models import Club from bda.models import Salle, Tirage
from gestioncof.models import (
CalendarSubscription, Club, Event, Survey, SurveyAnswer
)
from gestioncof.tests.testcases import ViewTestCaseMixin from gestioncof.tests.testcases import ViewTestCaseMixin
from .utils import create_user from .utils import create_user
@ -120,3 +129,468 @@ class ClubChangeRespoViewTests(ViewTestCaseMixin, TestCase):
self.client.get(self.url) self.client.get(self.url)
self.assertNotIn(u, self.c.respos.all()) self.assertNotIn(u, self.c.respos.all())
class CalendarViewTests(ViewTestCaseMixin, TestCase):
url_name = 'calendar'
url_expected = '/calendar/subscription'
auth_user = 'member'
auth_forbidden = [None, 'user']
post_expected_message = Message(
messages.SUCCESS, "Calendrier mis à jour avec succès.")
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, {
'subscribe_to_events': True,
'subscribe_to_my_shows': True,
'other_shows': [],
})
self.assertEqual(r.status_code, 200)
self.assertIn(self.post_expected_message, get_messages(r.wsgi_request))
cs = self.users['member'].calendarsubscription
self.assertTrue(cs.subscribe_to_events)
self.assertTrue(cs.subscribe_to_my_shows)
def test_post_edit(self):
u = self.users['member']
token = uuid.uuid4()
cs = CalendarSubscription.objects.create(token=token, user=u)
r = self.client.post(self.url, {
'other_shows': [],
})
self.assertEqual(r.status_code, 200)
self.assertIn(self.post_expected_message, get_messages(r.wsgi_request))
cs.refresh_from_db()
self.assertEqual(cs.token, token)
self.assertFalse(cs.subscribe_to_events)
self.assertFalse(cs.subscribe_to_my_shows)
def test_post_other_shows(self):
t = Tirage.objects.create(
ouverture=self.now,
fermeture=self.now,
active=True,
)
l = Salle.objects.create()
s = t.spectacle_set.create(
date=self.now, price=3.5, slots=20, location=l, listing=True)
r = self.client.post(self.url, {'other_shows': [str(s.pk)]})
self.assertEqual(r.status_code, 200)
class CalendarICSViewTests(ViewTestCaseMixin, TestCase):
url_name = 'calendar.ics'
auth_user = None
auth_forbidden = []
@property
def url_kwargs(self):
return {'token': self.token}
@property
def url_expected(self):
return '/calendar/{}/calendar.ics'.format(self.token)
def setUp(self):
super().setUp()
self.token = uuid.uuid4()
self.t = Tirage.objects.create(
ouverture=self.now,
fermeture=self.now,
active=True,
)
l = Salle.objects.create(name='Location')
self.s1 = self.t.spectacle_set.create(
price=1, slots=10, location=l, listing=True,
title='Spectacle 1', date=self.now + timedelta(days=1),
)
self.s2 = self.t.spectacle_set.create(
price=2, slots=20, location=l, listing=True,
title='Spectacle 2', date=self.now + timedelta(days=2),
)
self.s3 = self.t.spectacle_set.create(
price=3, slots=30, location=l, listing=True,
title='Spectacle 3', date=self.now + timedelta(days=3),
)
def test(self):
u = self.users['user']
p = u.participant_set.create(tirage=self.t)
p.attribution_set.create(spectacle=self.s1)
self.cs = CalendarSubscription.objects.create(
user=u, token=self.token,
subscribe_to_my_shows=True, subscribe_to_events=True,
)
self.cs.other_shows.add(self.s2)
r = self.client.get(self.url)
def get_dt_from_ical(v):
return v.dt
self.assertCalEqual(r.content.decode('utf-8'), [
{
'summary': 'Spectacle 1',
'dtstart': (get_dt_from_ical, (
(self.now + timedelta(days=1)).replace(microsecond=0)
)),
'dtend': (get_dt_from_ical, (
(self.now + timedelta(days=1, hours=2)).replace(
microsecond=0)
)),
'location': 'Location',
'uid': 'show-{}-{}@example.com'.format(self.s1.pk, self.t.pk),
},
{
'summary': 'Spectacle 2',
'dtstart': (get_dt_from_ical, (
(self.now + timedelta(days=2)).replace(microsecond=0)
)),
'dtend': (get_dt_from_ical, (
(self.now + timedelta(days=2, hours=2)).replace(
microsecond=0)
)),
'location': 'Location',
'uid': 'show-{}-{}@example.com'.format(self.s2.pk, self.t.pk),
},
])
class EventViewTests(ViewTestCaseMixin, TestCase):
url_name = 'event.details'
http_methods = ['GET', 'POST']
auth_user = 'user'
auth_forbidden = [None]
post_expected_message = Message(messages.SUCCESS, (
"Votre inscription a bien été enregistrée ! Vous pouvez cependant la "
"modifier jusqu'à la fin des inscriptions."
))
@property
def url_kwargs(self):
return {'event_id': self.e.pk}
@property
def url_expected(self):
return '/event/{}'.format(self.e.pk)
def setUp(self):
super().setUp()
self.e = Event.objects.create()
self.ecf1 = self.e.commentfields.create(name='Comment Field 1')
self.ecf2 = self.e.commentfields.create(
name='Comment Field 2', fieldtype='char',
)
self.o1 = self.e.options.create(name='Option 1')
self.o2 = self.e.options.create(name='Option 2', multi_choices=True)
self.oc1 = self.o1.choices.create(value='O1 - Choice 1')
self.oc2 = self.o1.choices.create(value='O1 - Choice 2')
self.oc3 = self.o2.choices.create(value='O2 - Choice 1')
self.oc4 = self.o2.choices.create(value='O2 - Choice 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, {
'option_{}'.format(self.o1.pk): [str(self.oc1.pk)],
'option_{}'.format(self.o2.pk): [
str(self.oc3.pk), str(self.oc4.pk),
],
})
self.assertEqual(r.status_code, 200)
self.assertIn(self.post_expected_message, get_messages(r.wsgi_request))
er = self.e.eventregistration_set.get(user=self.users['user'])
self.assertQuerysetEqual(
er.options.all(), map(repr, [self.oc1, self.oc3, self.oc4]),
ordered=False,
)
# TODO: Make the view care about comments.
# self.assertQuerysetEqual(
# er.comments.all(), map(repr, []),
# ordered=False,
# )
def test_post_edit(self):
er = self.e.eventregistration_set.create(user=self.users['user'])
er.options.add(self.oc1, self.oc3, self.oc4)
er.comments.create(
commentfield=self.ecf1, content='Comment 1',
)
r = self.client.post(self.url, {
'option_{}'.format(self.o1.pk): [],
'option_{}'.format(self.o2.pk): [str(self.oc3.pk)],
})
self.assertEqual(r.status_code, 200)
self.assertIn(self.post_expected_message, get_messages(r.wsgi_request))
er.refresh_from_db()
self.assertQuerysetEqual(
er.options.all(), map(repr, [self.oc3]),
ordered=False,
)
# TODO: Make the view care about comments.
# self.assertQuerysetEqual(
# er.comments.all(), map(repr, []),
# ordered=False,
# )
class EventStatusViewTests(ViewTestCaseMixin, TestCase):
url_name = 'event.details.status'
http_methods = ['GET', 'POST']
auth_user = 'staff'
auth_forbidden = [None, 'user', 'member']
@property
def url_kwargs(self):
return {'event_id': self.e.pk}
@property
def url_expected(self):
return '/event/{}/status'.format(self.e.pk)
def setUp(self):
super().setUp()
self.e = Event.objects.create()
self.cf1 = self.e.commentfields.create(name='Comment Field 1')
self.cf2 = self.e.commentfields.create(
name='Comment Field 2', fieldtype='char',
)
self.o1 = self.e.options.create(name='Option 1')
self.o2 = self.e.options.create(name='Option 2', multi_choices=True)
self.oc1 = self.o1.choices.create(value='O1 - Choice 1')
self.oc2 = self.o1.choices.create(value='O1 - Choice 2')
self.oc3 = self.o2.choices.create(value='O2 - Choice 1')
self.oc4 = self.o2.choices.create(value='O2 - Choice 2')
self.er1 = self.e.eventregistration_set.create(user=self.users['user'])
self.er1.options.add(self.oc1)
self.er2 = self.e.eventregistration_set.create(
user=self.users['member'],
)
def _get_oc_filter_name(self, oc):
return 'option_{}_choice_{}'.format(oc.event_option.pk, oc.pk)
def _test_filters(self, filters, expected):
r = self.client.post(self.url, {
self._get_oc_filter_name(oc): v for oc, v in filters
})
self.assertEqual(r.status_code, 200)
self.assertQuerysetEqual(
r.context['user_choices'], map(repr, expected),
ordered=False,
)
def test_filter_none(self):
self._test_filters([(self.oc1, 'none')], [self.er1, self.er2])
def test_filter_yes(self):
self._test_filters([(self.oc1, 'yes')], [self.er1])
def test_filter_no(self):
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

@ -52,7 +52,8 @@ events_patterns = [
calendar_patterns = [ calendar_patterns = [
url(r'^subscription$', views.calendar, url(r'^subscription$', views.calendar,
name='calendar'), name='calendar'),
url(r'^(?P<token>[a-z0-9-]+)/calendar.ics$', views.calendar_ics) url(r'^(?P<token>[a-z0-9-]+)/calendar.ics$', views.calendar_ics,
name='calendar.ics'),
] ]
clubs_patterns = [ clubs_patterns = [

View file

@ -20,6 +20,7 @@ class TriStateCheckbox(Widget):
def render(self, name, value, attrs=None, choices=()): def render(self, name, value, attrs=None, choices=()):
if value is None: if value is None:
value = 'none' value = 'none'
final_attrs = self.build_attrs(attrs, value=value) attrs['value'] = value
final_attrs = self.build_attrs(self.attrs, attrs)
output = ["<span class=\"tristate\"%s></span>" % flatatt(final_attrs)] output = ["<span class=\"tristate\"%s></span>" % flatatt(final_attrs)]
return mark_safe('\n'.join(output)) return mark_safe('\n'.join(output))

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

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-04-05 21:47
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('kfet', '0062_delete_globalpermissions'),
]
operations = [
migrations.AlterField(
model_name='account',
name='promo',
field=models.IntegerField(blank=True, choices=[(1980, 1980), (1981, 1981), (1982, 1982), (1983, 1983), (1984, 1984), (1985, 1985), (1986, 1986), (1987, 1987), (1988, 1988), (1989, 1989), (1990, 1990), (1991, 1991), (1992, 1992), (1993, 1993), (1994, 1994), (1995, 1995), (1996, 1996), (1997, 1997), (1998, 1998), (1999, 1999), (2000, 2000), (2001, 2001), (2002, 2002), (2003, 2003), (2004, 2004), (2005, 2005), (2006, 2006), (2007, 2007), (2008, 2008), (2009, 2009), (2010, 2010), (2011, 2011), (2012, 2012), (2013, 2013), (2014, 2014), (2015, 2015), (2016, 2016), (2017, 2017), (2018, 2018)], default=2017, null=True),
),
]

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,12 +746,16 @@ class CheckoutReadViewTests(ViewTestCaseMixin, TestCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.checkout = Checkout.objects.create(
name='Checkout', with mock.patch('django.utils.timezone.now') as mock_now:
created_by=self.accounts['team'], mock_now.return_value = self.now
valid_from=self.now,
valid_to=self.now + timedelta(days=5), 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=1),
)
def test_ok(self): def test_ok(self):
r = self.client.get(self.url) r = self.client.get(self.url)
@ -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

View file

@ -8,6 +8,8 @@ from django.test import Client
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
import icalendar
User = get_user_model() User = get_user_model()
@ -92,6 +94,40 @@ class TestCaseMixin:
else: else:
self.assertEqual(actual, expected) self.assertEqual(actual, expected)
def _test_event_equal(self, event, exp):
for k, v_desc in exp.items():
if isinstance(v_desc, tuple):
v_getter = v_desc[0]
v = v_desc[1]
else:
v_getter = lambda v: v
v = v_desc
if v_getter(event[k.upper()]) != v:
return False
return True
def _find_event(self, ev, l):
for i, elt in enumerate(l):
if self._test_event_equal(ev, elt):
return elt, i
return False, -1
def assertCalEqual(self, ical_content, expected):
remaining = expected.copy()
unexpected = []
cal = icalendar.Calendar.from_ical(ical_content)
for ev in cal.walk('vevent'):
found, i_found = self._find_event(ev, remaining)
if found:
remaining.pop(i_found)
else:
unexpected.append(ev)
self.assertListEqual(unexpected, [])
self.assertListEqual(remaining, [])
class ViewTestCaseMixin(TestCaseMixin): class ViewTestCaseMixin(TestCaseMixin):
""" """