Merge branch 'master' into Aufinal/bda_fixes
This commit is contained in:
commit
87a6722143
59 changed files with 9629 additions and 225 deletions
|
@ -1,5 +1,7 @@
|
||||||
# GestioCOF
|
# GestioCOF
|
||||||
|
|
||||||
|
![build_status](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/build.svg)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Vagrant
|
### Vagrant
|
||||||
|
|
|
@ -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 }}">
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 !</a></p>
|
automatique !</a></p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<h3>Vous n'avez aucune place :(</h3>
|
<h3>Vous n'avez aucune place :(</h3>
|
||||||
|
|
|
@ -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'.
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,13 +86,15 @@ urlpatterns = [
|
||||||
url(r'^utile_bda$', gestioncof_views.utile_bda,
|
url(r'^utile_bda$', gestioncof_views.utile_bda,
|
||||||
name='utile_bda'),
|
name='utile_bda'),
|
||||||
url(r'^utile_bda/bda_diff$', gestioncof_views.liste_bdadiff),
|
url(r'^utile_bda/bda_diff$', gestioncof_views.liste_bdadiff),
|
||||||
url(r'^utile_cof/diff_cof$', gestioncof_views.liste_diffcof),
|
url(r'^utile_cof/diff_cof$', gestioncof_views.liste_diffcof,
|
||||||
|
name='ml_diffcof'),
|
||||||
url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente),
|
url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente),
|
||||||
url(r'^k-fet/', include('kfet.urls')),
|
url(r'^k-fet/', include('kfet.urls')),
|
||||||
url(r'^cms/', include(wagtailadmin_urls)),
|
url(r'^cms/', include(wagtailadmin_urls)),
|
||||||
url(r'^documents/', include(wagtaildocs_urls)),
|
url(r'^documents/', include(wagtaildocs_urls)),
|
||||||
# djconfig
|
# djconfig
|
||||||
url(r"^config", gestioncof_views.ConfigUpdate.as_view()),
|
url(r"^config", gestioncof_views.ConfigUpdate.as_view(),
|
||||||
|
name='config.edit'),
|
||||||
]
|
]
|
||||||
|
|
||||||
if 'debug_toolbar' in settings.INSTALLED_APPS:
|
if 'debug_toolbar' in settings.INSTALLED_APPS:
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -63,8 +63,9 @@ class Command(BaseCommand):
|
||||||
except CustomMail.DoesNotExist:
|
except CustomMail.DoesNotExist:
|
||||||
mail = CustomMail.objects.create(**fields)
|
mail = CustomMail.objects.create(**fields)
|
||||||
status['synced'] += 1
|
status['synced'] += 1
|
||||||
self.stdout.write(
|
if options['verbosity']:
|
||||||
'SYNCED {:s}'.format(fields['shortname']))
|
self.stdout.write(
|
||||||
|
'SYNCED {:s}'.format(fields['shortname']))
|
||||||
assoc['mails'][obj['pk']] = mail
|
assoc['mails'][obj['pk']] = mail
|
||||||
|
|
||||||
# Variables
|
# Variables
|
||||||
|
@ -79,8 +80,9 @@ class Command(BaseCommand):
|
||||||
except Variable.DoesNotExist:
|
except Variable.DoesNotExist:
|
||||||
Variable.objects.create(**fields)
|
Variable.objects.create(**fields)
|
||||||
|
|
||||||
# C'est agréable d'avoir le résultat affiché
|
if options['verbosity']:
|
||||||
self.stdout.write(
|
# C'est agréable d'avoir le résultat affiché
|
||||||
'{synced:d} mails synchronized {unchanged:d} unchanged'
|
self.stdout.write(
|
||||||
.format(**status)
|
'{synced:d} mails synchronized {unchanged:d} unchanged'
|
||||||
)
|
.format(**status)
|
||||||
|
)
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
{% block realcontent %}
|
{% block realcontent %}
|
||||||
<h2>Modifier mon profil</h2>
|
<h2>Modifier mon profil</h2>
|
||||||
<form id="profile form-horizontal" method="post" action="{% url 'gestioncof.views.profile' %}">
|
<form id="profile form-horizontal" method="post" action="{% url 'profile' %}">
|
||||||
<div class="row" style="margin: 0 15%;">
|
<div class="row" style="margin: 0 15%;">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<fieldset"center-block">
|
<fieldset"center-block">
|
||||||
|
|
|
@ -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}}
|
||||||
|
|
||||||
|
|
|
@ -7,15 +7,15 @@
|
||||||
<h2>Liens utiles du COF</h2>
|
<h2>Liens utiles du COF</h2>
|
||||||
<h3>COF</h3>
|
<h3>COF</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="{% url 'gestioncof.views.export_members' %}">Export des membres du COF</a></li>
|
<li><a href="{% url 'cof.membres_export' %}">Export des membres du COF</a></li>
|
||||||
<li><a href="{% url 'gestioncof.views.liste_diffcof' %}">Diffusion COF</a></li>
|
<li><a href="{% url 'ml_diffcof' %}">Diffusion COF</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3>Mega</h3>
|
<h3>Mega</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="{% url 'gestioncof.views.export_mega_participants' %}">Export des non-orgas uniquement</a></li>
|
<li><a href="{% url 'cof.mega_export_participants' %}">Export des non-orgas uniquement</a></li>
|
||||||
<li><a href="{% url 'gestioncof.views.export_mega_orgas' %}">Export des orgas uniquement</a></li>
|
<li><a href="{% url 'cof.mega_export_orgas' %}">Export des orgas uniquement</a></li>
|
||||||
<li><a href="{% url 'gestioncof.views.export_mega' %}">Export de tout le monde</a></li>
|
<li><a href="{% url 'cof.mega_export' %}">Export de tout le monde</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>Note : pour ouvrir les fichiers .csv avec Excel, il faut
|
<p>Note : pour ouvrir les fichiers .csv avec Excel, il faut
|
||||||
|
|
|
@ -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 }}">
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
0
gestioncof/tests/__init__.py
Normal file
0
gestioncof/tests/__init__.py
Normal file
874
gestioncof/tests/test_views.py
Normal file
874
gestioncof/tests/test_views.py
Normal file
|
@ -0,0 +1,874 @@
|
||||||
|
import csv
|
||||||
|
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.urls import reverse
|
||||||
|
|
||||||
|
from bda.models import Salle, Tirage
|
||||||
|
from gestioncof.models import (
|
||||||
|
CalendarSubscription, Club, Event, Survey, SurveyAnswer
|
||||||
|
)
|
||||||
|
from gestioncof.tests.testcases import ViewTestCaseMixin
|
||||||
|
|
||||||
|
from .utils import create_member, create_root, create_user
|
||||||
|
|
||||||
|
|
||||||
|
class HomeViewTests(ViewTestCaseMixin, TestCase):
|
||||||
|
url_name = 'home'
|
||||||
|
url_expected = '/'
|
||||||
|
|
||||||
|
auth_user = 'user'
|
||||||
|
auth_forbidden = [None]
|
||||||
|
|
||||||
|
def test(self):
|
||||||
|
r = self.client.get(self.url)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileViewTests(ViewTestCaseMixin, TestCase):
|
||||||
|
url_name = 'profile'
|
||||||
|
url_expected = '/profile'
|
||||||
|
|
||||||
|
http_methods = ['GET', 'POST']
|
||||||
|
|
||||||
|
auth_user = 'member'
|
||||||
|
auth_forbidden = [None, 'user']
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
r = self.client.get(self.url)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
|
def test_post(self):
|
||||||
|
u = self.users['member']
|
||||||
|
|
||||||
|
r = self.client.post(self.url, {
|
||||||
|
'first_name': 'First',
|
||||||
|
'last_name': 'Last',
|
||||||
|
'phone': '',
|
||||||
|
# 'mailing_cof': '1',
|
||||||
|
# 'mailing_bda': '1',
|
||||||
|
# 'mailing_bda_revente': '1',
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
expected_message = Message(messages.SUCCESS, (
|
||||||
|
"Votre profil a été mis à jour avec succès !"
|
||||||
|
))
|
||||||
|
self.assertIn(expected_message, get_messages(r.wsgi_request))
|
||||||
|
u.refresh_from_db()
|
||||||
|
self.assertEqual(u.first_name, 'First')
|
||||||
|
self.assertEqual(u.last_name, 'Last')
|
||||||
|
self.assertFalse(u.profile.mailing_cof)
|
||||||
|
self.assertFalse(u.profile.mailing_bda)
|
||||||
|
self.assertFalse(u.profile.mailing_bda_revente)
|
||||||
|
|
||||||
|
|
||||||
|
class UtilsViewTests(ViewTestCaseMixin, TestCase):
|
||||||
|
url_name = 'utile_cof'
|
||||||
|
url_expected = '/utile_cof'
|
||||||
|
|
||||||
|
auth_user = 'staff'
|
||||||
|
auth_forbidden = [None, 'user', 'member']
|
||||||
|
|
||||||
|
def test(self):
|
||||||
|
r = self.client.get(self.url)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
class MailingListDiffCof(ViewTestCaseMixin, TestCase):
|
||||||
|
url_name = 'ml_diffcof'
|
||||||
|
url_expected = '/utile_cof/diff_cof'
|
||||||
|
|
||||||
|
auth_user = 'staff'
|
||||||
|
auth_forbidden = [None, 'user', 'member']
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
self.u1 = create_member('u1', attrs={'mailing_cof': True})
|
||||||
|
self.u2 = create_member('u2', attrs={'mailing_cof': False})
|
||||||
|
self.u3 = create_user('u3', attrs={'mailing_cof': True})
|
||||||
|
|
||||||
|
def test(self):
|
||||||
|
r = self.client.get(self.url)
|
||||||
|
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(r.context['personnes'].get(), self.u1.profile)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigUpdateViewTests(ViewTestCaseMixin, TestCase):
|
||||||
|
url_name = 'config.edit'
|
||||||
|
url_expected = '/config'
|
||||||
|
|
||||||
|
http_methods = ['GET', 'POST']
|
||||||
|
|
||||||
|
auth_user = 'root'
|
||||||
|
auth_forbidden = [None, 'user', 'member', 'staff']
|
||||||
|
|
||||||
|
def get_users_extra(self):
|
||||||
|
return {
|
||||||
|
'root': create_root('root'),
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
r = self.client.get(self.url)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
|
def test_post(self):
|
||||||
|
r = self.client.post(self.url, {
|
||||||
|
'gestion_banner': 'Announcement !',
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertRedirects(r, reverse('home'))
|
||||||
|
|
||||||
|
|
||||||
|
class UserAutocompleteViewTests(ViewTestCaseMixin, TestCase):
|
||||||
|
url_name = 'cof-user-autocomplete'
|
||||||
|
url_expected = '/user/autocomplete'
|
||||||
|
|
||||||
|
auth_user = 'staff'
|
||||||
|
auth_forbidden = [None, 'user', 'member']
|
||||||
|
|
||||||
|
def test(self):
|
||||||
|
r = self.client.get(self.url, {'q': 'user'})
|
||||||
|
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
class ExportMembersViewTests(ViewTestCaseMixin, TestCase):
|
||||||
|
url_name = 'cof.membres_export'
|
||||||
|
url_expected = '/export/members'
|
||||||
|
|
||||||
|
auth_user = 'staff'
|
||||||
|
auth_forbidden = [None, 'user', 'member']
|
||||||
|
|
||||||
|
def test(self):
|
||||||
|
u1, u2 = self.users['member'], self.users['staff']
|
||||||
|
u1.first_name = 'first'
|
||||||
|
u1.last_name = 'last'
|
||||||
|
u1.email = 'user@mail.net'
|
||||||
|
u1.save()
|
||||||
|
u1.profile.phone = '0123456789'
|
||||||
|
u1.profile.departement = 'Dept'
|
||||||
|
u1.profile.save()
|
||||||
|
|
||||||
|
r = self.client.get(self.url)
|
||||||
|
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
data = list(csv.reader(r.content.decode('utf-8').split('\n')[:-1]))
|
||||||
|
expected = [
|
||||||
|
[
|
||||||
|
str(u1.pk), 'member', 'first', 'last', 'user@mail.net',
|
||||||
|
'0123456789', '1A', 'Dept', 'normalien',
|
||||||
|
],
|
||||||
|
[str(u2.pk), 'staff', '', '', '', '', '1A', '', 'normalien'],
|
||||||
|
]
|
||||||
|
# Sort before checking equality, the order of the output of csv.reader
|
||||||
|
# does not seem deterministic
|
||||||
|
expected.sort(key=lambda row: int(row[0]))
|
||||||
|
data.sort(key=lambda row: int(row[0]))
|
||||||
|
self.assertListEqual(data, expected)
|
||||||
|
|
||||||
|
|
||||||
|
class MegaHelpers:
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
u1 = create_user('u1')
|
||||||
|
u1.first_name = 'first'
|
||||||
|
u1.last_name = 'last'
|
||||||
|
u1.email = 'user@mail.net'
|
||||||
|
u1.save()
|
||||||
|
u1.profile.phone = '0123456789'
|
||||||
|
u1.profile.departement = 'Dept'
|
||||||
|
u1.profile.comments = 'profile.comments'
|
||||||
|
u1.profile.save()
|
||||||
|
|
||||||
|
u2 = create_user('u2')
|
||||||
|
u2.profile.save()
|
||||||
|
|
||||||
|
m = Event.objects.create(title='MEGA 2017')
|
||||||
|
|
||||||
|
cf1 = m.commentfields.create(name='Commentaire')
|
||||||
|
cf2 = m.commentfields.create(
|
||||||
|
name='Comment Field 2', fieldtype='char',
|
||||||
|
)
|
||||||
|
|
||||||
|
option_type = m.options.create(name='Conscrit/Orga ?')
|
||||||
|
choice_orga = option_type.choices.create(value='Orga')
|
||||||
|
choice_conscrit = option_type.choices.create(value='Conscrit')
|
||||||
|
|
||||||
|
mr1 = m.eventregistration_set.create(user=u1)
|
||||||
|
mr1.options.add(choice_orga)
|
||||||
|
mr1.comments.create(commentfield=cf1, content='Comment 1')
|
||||||
|
mr1.comments.create(commentfield=cf2, content='Comment 2')
|
||||||
|
|
||||||
|
mr2 = m.eventregistration_set.create(user=u2)
|
||||||
|
mr2.options.add(choice_conscrit)
|
||||||
|
|
||||||
|
self.u1 = u1
|
||||||
|
self.u2 = u2
|
||||||
|
self.m = m
|
||||||
|
self.choice_orga = choice_orga
|
||||||
|
self.choice_conscrit = choice_conscrit
|
||||||
|
|
||||||
|
|
||||||
|
class ExportMegaViewTests(MegaHelpers, ViewTestCaseMixin, TestCase):
|
||||||
|
url_name = 'cof.mega_export'
|
||||||
|
url_expected = '/export/mega'
|
||||||
|
|
||||||
|
auth_user = 'staff'
|
||||||
|
auth_forbidden = [None, 'user', 'member']
|
||||||
|
|
||||||
|
def test(self):
|
||||||
|
r = self.client.get(self.url)
|
||||||
|
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertListEqual(self.load_from_csv_response(r), [
|
||||||
|
[
|
||||||
|
'u1', 'first', 'last', 'user@mail.net', '0123456789',
|
||||||
|
str(self.u1.pk), 'profile.comments', 'Comment 1---Comment 2',
|
||||||
|
],
|
||||||
|
['u2', '', '', '', '', str(self.u2.pk), '', ''],
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
class ExportMegaOrgasViewTests(MegaHelpers, ViewTestCaseMixin, TestCase):
|
||||||
|
url_name = 'cof.mega_export_orgas'
|
||||||
|
url_expected = '/export/mega/orgas'
|
||||||
|
|
||||||
|
auth_user = 'staff'
|
||||||
|
auth_forbidden = [None, 'user', 'member']
|
||||||
|
|
||||||
|
def test(self):
|
||||||
|
|
||||||
|
r = self.client.get(self.url)
|
||||||
|
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertListEqual(self.load_from_csv_response(r), [
|
||||||
|
[
|
||||||
|
'u1', 'first', 'last', 'user@mail.net', '0123456789',
|
||||||
|
str(self.u1.pk), 'profile.comments', 'Comment 1---Comment 2',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
class ExportMegaParticipantsViewTests(
|
||||||
|
MegaHelpers, ViewTestCaseMixin, TestCase):
|
||||||
|
url_name = 'cof.mega_export_participants'
|
||||||
|
url_expected = '/export/mega/participants'
|
||||||
|
|
||||||
|
auth_user = 'staff'
|
||||||
|
auth_forbidden = [None, 'user', 'member']
|
||||||
|
|
||||||
|
def test(self):
|
||||||
|
r = self.client.get(self.url)
|
||||||
|
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertListEqual(self.load_from_csv_response(r), [
|
||||||
|
['u2', '', '', '', '', str(self.u2.pk), '', ''],
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
class ExportMegaRemarksViewTests(
|
||||||
|
MegaHelpers, ViewTestCaseMixin, TestCase):
|
||||||
|
url_name = 'cof.mega_export_remarks'
|
||||||
|
url_expected = '/export/mega/avecremarques'
|
||||||
|
|
||||||
|
auth_user = 'staff'
|
||||||
|
auth_forbidden = [None, 'user', 'member']
|
||||||
|
|
||||||
|
def test(self):
|
||||||
|
r = self.client.get(self.url)
|
||||||
|
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertListEqual(self.load_from_csv_response(r), [
|
||||||
|
[
|
||||||
|
'u1', 'first', 'last', 'user@mail.net', '0123456789',
|
||||||
|
str(self.u1.pk), 'profile.comments', 'Comment 1',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
class ClubListViewTests(ViewTestCaseMixin, TestCase):
|
||||||
|
url_name = 'liste-clubs'
|
||||||
|
url_expected = '/clubs/liste'
|
||||||
|
|
||||||
|
auth_user = 'member'
|
||||||
|
auth_forbidden = [None, 'user']
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
self.c1 = Club.objects.create(name='Club1')
|
||||||
|
self.c2 = Club.objects.create(name='Club2')
|
||||||
|
|
||||||
|
m = self.users['member']
|
||||||
|
self.c1.membres.add(m)
|
||||||
|
self.c1.respos.add(m)
|
||||||
|
|
||||||
|
def test_as_member(self):
|
||||||
|
r = self.client.get(self.url)
|
||||||
|
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(r.context['owned_clubs'].get(), self.c1)
|
||||||
|
self.assertEqual(r.context['other_clubs'].get(), self.c2)
|
||||||
|
|
||||||
|
def test_as_staff(self):
|
||||||
|
u = self.users['staff']
|
||||||
|
c = Client()
|
||||||
|
c.force_login(u)
|
||||||
|
|
||||||
|
r = c.get(self.url)
|
||||||
|
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertQuerysetEqual(
|
||||||
|
r.context['owned_clubs'], map(repr, [self.c1, self.c2]),
|
||||||
|
ordered=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ClubMembersViewTests(ViewTestCaseMixin, TestCase):
|
||||||
|
url_name = 'membres-club'
|
||||||
|
|
||||||
|
auth_user = 'staff'
|
||||||
|
auth_forbidden = [None, 'user', 'member']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_kwargs(self):
|
||||||
|
return {'name': self.c.name}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_expected(self):
|
||||||
|
return '/clubs/membres/{}'.format(self.c.name)
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
self.u1 = create_user('u1')
|
||||||
|
self.u2 = create_user('u2')
|
||||||
|
|
||||||
|
self.c = Club.objects.create(name='Club')
|
||||||
|
self.c.membres.add(self.u1, self.u2)
|
||||||
|
self.c.respos.add(self.u1)
|
||||||
|
|
||||||
|
def test_as_staff(self):
|
||||||
|
r = self.client.get(self.url)
|
||||||
|
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(r.context['members_no_respo'].get(), self.u2)
|
||||||
|
|
||||||
|
def test_as_respo(self):
|
||||||
|
u = self.users['user']
|
||||||
|
self.c.respos.add(u)
|
||||||
|
|
||||||
|
c = Client()
|
||||||
|
c.force_login(u)
|
||||||
|
r = c.get(self.url)
|
||||||
|
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
class ClubChangeRespoViewTests(ViewTestCaseMixin, TestCase):
|
||||||
|
url_name = 'change-respo'
|
||||||
|
|
||||||
|
auth_user = 'staff'
|
||||||
|
auth_forbidden = [None, 'user', 'member']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_kwargs(self):
|
||||||
|
return {'club_name': self.c.name, 'user_id': self.users['user'].pk}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_expected(self):
|
||||||
|
return '/clubs/change_respo/{}/{}'.format(
|
||||||
|
self.c.name, self.users['user'].pk,
|
||||||
|
)
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
self.c = Club.objects.create(name='Club')
|
||||||
|
|
||||||
|
def test(self):
|
||||||
|
u = self.users['user']
|
||||||
|
expected_redirect = reverse('membres-club', kwargs={
|
||||||
|
'name': self.c.name,
|
||||||
|
})
|
||||||
|
self.c.membres.add(u)
|
||||||
|
|
||||||
|
r = self.client.get(self.url)
|
||||||
|
self.assertRedirects(r, expected_redirect)
|
||||||
|
self.assertIn(u, self.c.respos.all())
|
||||||
|
|
||||||
|
self.client.get(self.url)
|
||||||
|
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])
|
24
gestioncof/tests/testcases.py
Normal file
24
gestioncof/tests/testcases.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
from shared.tests.testcases import ViewTestCaseMixin as BaseViewTestCaseMixin
|
||||||
|
|
||||||
|
from .utils import create_user, create_member, create_staff
|
||||||
|
|
||||||
|
|
||||||
|
class ViewTestCaseMixin(BaseViewTestCaseMixin):
|
||||||
|
"""
|
||||||
|
TestCase extension to ease testing of cof views.
|
||||||
|
|
||||||
|
Most information can be found in the base parent class doc.
|
||||||
|
This class performs some changes to users management, detailed below.
|
||||||
|
|
||||||
|
During setup, three users are created:
|
||||||
|
- 'user': a basic user without any permission,
|
||||||
|
- 'member': (profile.is_cof is True),
|
||||||
|
- 'staff': (profile.is_cof is True) && (profile.is_buro is True).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_users_base(self):
|
||||||
|
return {
|
||||||
|
'user': create_user('user'),
|
||||||
|
'member': create_member('member'),
|
||||||
|
'staff': create_staff('staff'),
|
||||||
|
}
|
61
gestioncof/tests/utils.py
Normal file
61
gestioncof/tests/utils.py
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
def _create_user(username, is_cof=False, is_staff=False, attrs=None):
|
||||||
|
if attrs is None:
|
||||||
|
attrs = {}
|
||||||
|
|
||||||
|
password = attrs.pop('password', username)
|
||||||
|
|
||||||
|
user_keys = [
|
||||||
|
'first_name', 'last_name', 'email', 'is_staff', 'is_superuser',
|
||||||
|
]
|
||||||
|
user_attrs = {k: v for k, v in attrs.items() if k in user_keys}
|
||||||
|
|
||||||
|
profile_keys = [
|
||||||
|
'is_cof', 'login_clipper', 'phone', 'occupation', 'departement',
|
||||||
|
'type_cotiz', 'mailing_cof', 'mailing_bda', 'mailing_bda_revente',
|
||||||
|
'comments', 'is_buro', 'petit_cours_accept',
|
||||||
|
'petit_cours_remarques',
|
||||||
|
]
|
||||||
|
profile_attrs = {k: v for k, v in attrs.items() if k in profile_keys}
|
||||||
|
|
||||||
|
if is_cof:
|
||||||
|
profile_attrs['is_cof'] = True
|
||||||
|
|
||||||
|
if is_staff:
|
||||||
|
# At the moment, admin is accessible by COF staff.
|
||||||
|
user_attrs['is_staff'] = True
|
||||||
|
profile_attrs['is_buro'] = True
|
||||||
|
|
||||||
|
user = User(username=username, **user_attrs)
|
||||||
|
user.set_password(password)
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
for k, v in profile_attrs.items():
|
||||||
|
setattr(user.profile, k, v)
|
||||||
|
user.profile.save()
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def create_user(username, attrs=None):
|
||||||
|
return _create_user(username, attrs=attrs)
|
||||||
|
|
||||||
|
|
||||||
|
def create_member(username, attrs=None):
|
||||||
|
return _create_user(username, is_cof=True, attrs=attrs)
|
||||||
|
|
||||||
|
|
||||||
|
def create_staff(username, attrs=None):
|
||||||
|
return _create_user(username, is_cof=True, is_staff=True, attrs=attrs)
|
||||||
|
|
||||||
|
|
||||||
|
def create_root(username, attrs=None):
|
||||||
|
if attrs is None:
|
||||||
|
attrs = {}
|
||||||
|
attrs.setdefault('is_staff', True)
|
||||||
|
attrs.setdefault('is_superuser', True)
|
||||||
|
return _create_user(username, attrs=attrs)
|
|
@ -6,12 +6,17 @@ from gestioncof import views, petits_cours_views
|
||||||
from gestioncof.decorators import buro_required
|
from gestioncof.decorators import buro_required
|
||||||
|
|
||||||
export_patterns = [
|
export_patterns = [
|
||||||
url(r'^members$', views.export_members),
|
url(r'^members$', views.export_members,
|
||||||
url(r'^mega/avecremarques$', views.export_mega_remarksonly),
|
name='cof.membres_export'),
|
||||||
url(r'^mega/participants$', views.export_mega_participants),
|
url(r'^mega/avecremarques$', views.export_mega_remarksonly,
|
||||||
url(r'^mega/orgas$', views.export_mega_orgas),
|
name='cof.mega_export_remarks'),
|
||||||
|
url(r'^mega/participants$', views.export_mega_participants,
|
||||||
|
name='cof.mega_export_participants'),
|
||||||
|
url(r'^mega/orgas$', views.export_mega_orgas,
|
||||||
|
name='cof.mega_export_orgas'),
|
||||||
# url(r'^mega/(?P<type>.+)$', views.export_mega_bytype),
|
# url(r'^mega/(?P<type>.+)$', views.export_mega_bytype),
|
||||||
url(r'^mega$', views.export_mega),
|
url(r'^mega$', views.export_mega,
|
||||||
|
name='cof.mega_export'),
|
||||||
]
|
]
|
||||||
|
|
||||||
petitcours_patterns = [
|
petitcours_patterns = [
|
||||||
|
@ -52,7 +57,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 = [
|
||||||
|
|
|
@ -9,6 +9,7 @@ from django.http import Http404, HttpResponse, HttpResponseForbidden
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.auth.views import (
|
from django.contrib.auth.views import (
|
||||||
login as django_login_view, logout as django_logout_view,
|
login as django_login_view, logout as django_logout_view,
|
||||||
|
redirect_to_login,
|
||||||
)
|
)
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.sites.models import Site
|
from django.contrib.sites.models import Site
|
||||||
|
@ -338,7 +339,7 @@ def profile(request):
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
form.save()
|
form.save()
|
||||||
messages.success(request,
|
messages.success(request,
|
||||||
"Votre profil a été mis à jour avec succès !")
|
"Votre profil a été mis à jour avec succès !")
|
||||||
else:
|
else:
|
||||||
form = UserProfileForm(instance=request.user.profile)
|
form = UserProfileForm(instance=request.user.profile)
|
||||||
return render(request, "gestioncof/profile.html", {"form": form})
|
return render(request, "gestioncof/profile.html", {"form": form})
|
||||||
|
@ -566,7 +567,7 @@ def liste_clubs(request):
|
||||||
if request.user.profile.is_buro:
|
if request.user.profile.is_buro:
|
||||||
data = {'owned_clubs': clubs.all()}
|
data = {'owned_clubs': clubs.all()}
|
||||||
else:
|
else:
|
||||||
data = {'owned_clubs': request.user.clubs_geres,
|
data = {'owned_clubs': request.user.clubs_geres.all(),
|
||||||
'other_clubs': clubs.exclude(respos=request.user)}
|
'other_clubs': clubs.exclude(respos=request.user)}
|
||||||
return render(request, 'liste_clubs.html', data)
|
return render(request, 'liste_clubs.html', data)
|
||||||
|
|
||||||
|
@ -782,7 +783,7 @@ class ConfigUpdate(FormView):
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
if request.user is None or not request.user.is_superuser:
|
if request.user is None or not request.user.is_superuser:
|
||||||
raise Http404
|
return redirect_to_login(request.get_full_path())
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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(
|
||||||
|
|
20
kfet/migrations/0063_promo.py
Normal file
20
kfet/migrations/0063_promo.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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,
|
||||||
|
|
|
@ -75,6 +75,10 @@ ul {
|
||||||
padding:8px !important;
|
padding:8px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table thead .sm-padding {
|
||||||
|
padding:3px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.table tr.section {
|
.table tr.section {
|
||||||
background: #c63b52 !important;
|
background: #c63b52 !important;
|
||||||
color:#fff;
|
color:#fff;
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
/* Libs customizations */
|
/* Libs customizations */
|
||||||
@import url("libs/jconfirm-kfet.css");
|
@import url("libs/jconfirm-kfet.css");
|
||||||
|
@import url("libs/jquery-tablesorter-kfet.css");
|
||||||
@import url("libs/multiple-select-kfet.css");
|
@import url("libs/multiple-select-kfet.css");
|
||||||
|
|
||||||
/* Base */
|
/* Base */
|
||||||
|
@ -54,6 +55,11 @@
|
||||||
color: #C81022;
|
color: #C81022;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table thead .glyphicon {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Pages tableaux seuls
|
* Pages tableaux seuls
|
||||||
|
@ -82,6 +88,11 @@
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table td.small-width {
|
||||||
|
/* Header still extends the width of the column, but it will be minimal. */
|
||||||
|
width: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
.auth-form {
|
.auth-form {
|
||||||
padding: 15px 0;
|
padding: 15px 0;
|
||||||
background: #d86c7e;
|
background: #d86c7e;
|
||||||
|
|
0
kfet/static/kfet/css/libs/jquery-tablesorter-kfet.css
Normal file
0
kfet/static/kfet/css/libs/jquery-tablesorter-kfet.css
Normal file
|
@ -235,3 +235,77 @@ function submit_url(el) {
|
||||||
let url = $(el).data('url');
|
let url = $(el).data('url');
|
||||||
create_form(url).appendTo($('body')).submit();
|
create_form(url).appendTo($('body')).submit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* jquery-tablesorter
|
||||||
|
* https://mottie.github.io/tablesorter/docs/
|
||||||
|
*
|
||||||
|
* Known bugs (v2.29.0):
|
||||||
|
* - Sort order icons in sticky headers are not updated.
|
||||||
|
* Status: Fixed in next release.
|
||||||
|
*
|
||||||
|
* TODO:
|
||||||
|
* - Handle i18n.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
function registerBoolParser(id, true_str, false_str) {
|
||||||
|
$.tablesorter.addParser({
|
||||||
|
id: id,
|
||||||
|
format: function(s) {
|
||||||
|
return s.toLowerCase()
|
||||||
|
.replace(true_str, 1)
|
||||||
|
.replace(false_str, 0);
|
||||||
|
},
|
||||||
|
type: 'numeric'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Parsers for the text representations of boolean.
|
||||||
|
registerBoolParser('yesno', 'oui', 'non');
|
||||||
|
|
||||||
|
registerBoolParser('article__is_sold', 'en vente', 'non vendu');
|
||||||
|
registerBoolParser('article__hidden', 'caché', 'affiché');
|
||||||
|
|
||||||
|
|
||||||
|
// https://mottie.github.io/tablesorter/docs/index.html#variable-defaults
|
||||||
|
$.extend(true, $.tablesorter.defaults, {
|
||||||
|
headerTemplate: '{content} {icon}',
|
||||||
|
|
||||||
|
cssIconAsc : 'glyphicon glyphicon-chevron-up',
|
||||||
|
cssIconDesc : 'glyphicon glyphicon-chevron-down',
|
||||||
|
cssIconNone : 'glyphicon glyphicon-resize-vertical',
|
||||||
|
|
||||||
|
// Only four-digits format year is handled by the builtin parser
|
||||||
|
// 'shortDate'.
|
||||||
|
dateFormat: 'ddmmyyyy',
|
||||||
|
|
||||||
|
// Accented characters are replaced with their non-accented one.
|
||||||
|
sortLocaleCompare: true,
|
||||||
|
// French format: 1 234,56
|
||||||
|
usNumberFormat: false,
|
||||||
|
|
||||||
|
widgets: ['stickyHeaders'],
|
||||||
|
widgetOptions: {
|
||||||
|
stickyHeaders_offset: '.navbar',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// https://mottie.github.io/tablesorter/docs/index.html#variable-language
|
||||||
|
$.extend($.tablesorter.language, {
|
||||||
|
sortAsc : 'Trié par ordre croissant, ',
|
||||||
|
sortDesc : 'Trié par ordre décroissant, ',
|
||||||
|
sortNone : 'Non trié, ',
|
||||||
|
sortDisabled : 'tri désactivé et/ou non-modifiable',
|
||||||
|
nextAsc : 'cliquer pour trier par ordre croissant',
|
||||||
|
nextDesc : 'cliquer pour trier par ordre décroissant',
|
||||||
|
nextNone : 'cliquer pour retirer le tri'
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$( function() {
|
||||||
|
$('.sortable').tablesorter();
|
||||||
|
});
|
||||||
|
|
6014
kfet/static/kfet/vendor/jquery-tablesorter/jquery.tablesorter.combined.js
vendored
Normal file
6014
kfet/static/kfet/vendor/jquery-tablesorter/jquery.tablesorter.combined.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
|
@ -37,13 +37,16 @@
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover table-condensed">
|
<table
|
||||||
|
class="table table-hover table-condensed sortable"
|
||||||
|
{# Initial sort: [(trigramme,asc)] #}
|
||||||
|
data-sortlist="[[0,0]]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center">Tri.</td>
|
<td class="text-center">Tri.</td>
|
||||||
<td>Nom</td>
|
<td>Nom</td>
|
||||||
<td class="text-right">Balance</td>
|
<td class="text-right">Balance</td>
|
||||||
<td class="text-center">COF</td>
|
<td class="text-center" data-sorter="yesno">COF</td>
|
||||||
<td>Dpt</td>
|
<td>Dpt</td>
|
||||||
<td class="text-center">Promo</td>
|
<td class="text-center">Promo</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -35,16 +35,19 @@
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover table-condensed">
|
<table
|
||||||
|
class="table table-hover table-condensed sortable"
|
||||||
|
{# Initial sort: [(trigramme,asc)] #}
|
||||||
|
data-sortlist="[[0,0]]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center">Tri.</td>
|
<td class="text-center">Tri.</td>
|
||||||
<td>Nom</td>
|
<td>Nom</td>
|
||||||
<td class="text-right">Balance</td>
|
<td class="text-right">Balance</td>
|
||||||
<td class="text-right">Réelle</td>
|
<td class="text-right">Réelle</td>
|
||||||
<td>Début</td>
|
<td data-sorter="shortDate">Début</td>
|
||||||
<td>Découvert autorisé</td>
|
<td>Découvert autorisé</td>
|
||||||
<td>Jusqu'au</td>
|
<td data-sorter="shortDate">Jusqu'au</td>
|
||||||
<td>Balance offset</td>
|
<td>Balance offset</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -63,9 +66,13 @@
|
||||||
{{ neg.account.real_balance|floatformat:2 }}€
|
{{ neg.account.real_balance|floatformat:2 }}€
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ neg.start|date:'d/m/Y H:i:s'}}</td>
|
<td title="{{ neg.start }}">
|
||||||
|
{{ neg.start|date:'d/m/Y H:i'}}
|
||||||
|
</td>
|
||||||
<td>{{ neg.authz_overdraft_amount|default_if_none:'' }}</td>
|
<td>{{ neg.authz_overdraft_amount|default_if_none:'' }}</td>
|
||||||
<td>{{ neg.authz_overdrafy_until|default_if_none:'' }}</td>
|
<td title="{{ neg.authz_overdraft_until }}">
|
||||||
|
{{ neg.authz_overdraft_until|date:'d/m/Y H:i' }}
|
||||||
|
</td>
|
||||||
<td>{{ neg.balance_offset|default_if_none:'' }}</td>
|
<td>{{ neg.balance_offset|default_if_none:'' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -7,8 +7,11 @@
|
||||||
|
|
||||||
<aside>
|
<aside>
|
||||||
<div class="heading">
|
<div class="heading">
|
||||||
{{ articles|length }}
|
{{ nb_articles }}
|
||||||
<span class="sub">article{{ articles|length|pluralize }}</span>
|
<span class="sub">article{{ nb_articles|pluralize }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="heading">
|
||||||
|
<span class="sub">dont {{ articles|length }} en vente</span>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
@ -25,39 +28,99 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
<h2>Article{{ articles|length|pluralize}} en vente</h2>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover table-condensed">
|
<table
|
||||||
|
class="table table-hover table-condensed sortable"
|
||||||
|
{# Initial sort: [(is_sold,desc), (name,asc)] #}
|
||||||
|
data-sortlist="[[3,1], [0,0]]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Nom</td>
|
<td>Nom</td>
|
||||||
<td class="text-right">Prix</td>
|
<td class="text-right">Prix</td>
|
||||||
<td class="text-right">Stock</td>
|
<td class="text-right">Stock</td>
|
||||||
<td class="text-right">En vente</td>
|
<td class="text-right" data-sorter="article__is_sold">En vente</td>
|
||||||
<td class="text-right">Affiché</td>
|
<td class="text-right" data-sorter="article__hidden">Affiché</td>
|
||||||
<td class="text-right">Dernier inventaire</td>
|
<td class="text-right" data-sorter="shortDate">Dernier inventaire</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
{% regroup articles by category as category_list %}
|
||||||
{% for article in articles %}
|
|
||||||
{% ifchanged article.category %}
|
{% for category in category_list %}
|
||||||
<tr class="section">
|
<tbody class="tablesorter-no-sort">
|
||||||
<td colspan="6">{{ article.category.name }}</td>
|
<tr class="section">
|
||||||
</tr>
|
<td colspan="6">{{ category.grouper }}</td>
|
||||||
{% endifchanged %}
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tbody>
|
||||||
|
{% for article in category.list %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'kfet.article.read' article.pk %}">
|
||||||
|
{{ article.name }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="text-right">{{ article.price }}€</td>
|
||||||
|
<td class="text-right">{{ article.stock }}</td>
|
||||||
|
<td class="text-right">{{ article.is_sold | yesno:"En vente,Non vendu"}}</td>
|
||||||
|
<td class="text-right">{{ article.hidden | yesno:"Caché,Affiché" }}</td>
|
||||||
|
{% with last_inventory=article.inventory.0 %}
|
||||||
|
<td class="text-right" title="{{ last_inventory.at }}">
|
||||||
|
{{ last_inventory.at|date:'d/m/Y H:i' }}
|
||||||
|
</td>
|
||||||
|
{% endwith %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Article{{ not_sold_articles|length|pluralize }} non vendu{{ nots_sold_article|length|pluralize }}</h2>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table
|
||||||
|
class="table table-hover table-condensed sortable"
|
||||||
|
{# Initial sort: [(is_sold,desc), (name,asc)] #}
|
||||||
|
data-sortlist="[[3,1], [0,0]]">
|
||||||
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>Nom</td>
|
||||||
<a href="{% url 'kfet.article.read' article.pk %}">
|
<td class="text-right">Prix</td>
|
||||||
{{ article.name }}
|
<td class="text-right">Stock</td>
|
||||||
</a>
|
<td class="text-right" data-sorter="article__is_sold">En vente</td>
|
||||||
</td>
|
<td class="text-right" data-sorter="article__hidden">Affiché</td>
|
||||||
<td class="text-right">{{ article.price }}€</td>
|
<td class="text-right" data-sorter="shortDate">Dernier inventaire</td>
|
||||||
<td class="text-right">{{ article.stock }}</td>
|
|
||||||
<td class="text-right">{{ article.is_sold | yesno:"En vente,Non vendu"}}</td>
|
|
||||||
<td class="text-right">{{ article.hidden | yesno:"Caché,Affiché" }}</td>
|
|
||||||
<td class="text-right">{{ article.inventory.0.at }}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
</thead>
|
||||||
</tbody>
|
{% regroup not_sold_articles by category as not_sold_category_list %}
|
||||||
|
|
||||||
|
{% for category in not_sold_category_list %}
|
||||||
|
<tbody class="tablesorter-no-sort">
|
||||||
|
<tr class="section">
|
||||||
|
<td colspan="6">{{ category.grouper }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tbody>
|
||||||
|
{% for article in category.list %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'kfet.article.read' article.pk %}">
|
||||||
|
{{ article.name }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="text-right">{{ article.price }}€</td>
|
||||||
|
<td class="text-right">{{ article.stock }}</td>
|
||||||
|
<td class="text-right">{{ article.is_sold | yesno:"En vente,Non vendu"}}</td>
|
||||||
|
<td class="text-right">{{ article.hidden | yesno:"Caché,Affiché" }}</td>
|
||||||
|
{% with last_inventory=article.inventory.0 %}
|
||||||
|
<td class="text-right" title="{{ last_inventory.at }}">
|
||||||
|
{{ last_inventory.at|date:'d/m/Y H:i' }}
|
||||||
|
</td>
|
||||||
|
{% endwith %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
<table class="table table-hover table-condensed">
|
<table
|
||||||
|
class="table table-hover table-condensed sortable"
|
||||||
|
{# Initial sort: [(inventory.at,desc)] #}
|
||||||
|
data-sortlist="[[0,1]]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Date</td>
|
<td data-sorter="shortDate">Date</td>
|
||||||
<td>Stock</td>
|
<td>Stock</td>
|
||||||
<td>Erreur</td>
|
<td>Erreur</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -9,9 +12,9 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for inventoryart in inventoryarts %}
|
{% for inventoryart in inventoryarts %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td title="{{ inventoryart.inventory.at }}">
|
||||||
<a href="{% url "kfet.inventory.read" inventoryart.inventory.pk %}">
|
<a href="{% url "kfet.inventory.read" inventoryart.inventory.pk %}">
|
||||||
{{ inventoryart.inventory.at }}
|
{{ inventoryart.inventory.at|date:'d/m/Y H:i' }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ inventoryart.stock_new }}</td>
|
<td>{{ inventoryart.stock_new }}</td>
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
<table class="table table-hover table-condensed">
|
<table
|
||||||
|
class="table table-hover table-condensed sortable"
|
||||||
|
{# Initial sort: [(at,desc)] #}
|
||||||
|
data-sortlist="[[0,1]]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Date</td>
|
<td data-sorter="shortDate">Date</td>
|
||||||
<td>Fournisseur</td>
|
<td>Fournisseur</td>
|
||||||
<td>HT</td>
|
<td>HT</td>
|
||||||
<td>TVA</td>
|
<td>TVA</td>
|
||||||
|
@ -11,7 +14,9 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for supplierart in supplierarts %}
|
{% for supplierart in supplierarts %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ supplierart.at }}</td>
|
<td title="{{ supplierart.at }}">
|
||||||
|
{{ supplierart.at|date:'d/m/Y' }}
|
||||||
|
</td>
|
||||||
<td>{{ supplierart.supplier.name }}</td>
|
<td>{{ supplierart.supplier.name }}</td>
|
||||||
<td>{{ supplierart.price_HT|default_if_none:"" }}</td>
|
<td>{{ supplierart.price_HT|default_if_none:"" }}</td>
|
||||||
<td>{{ supplierart.TVA|default_if_none:"" }}</td>
|
<td>{{ supplierart.TVA|default_if_none:"" }}</td>
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
|
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
|
||||||
<script type="text/javascript" src="{% static 'kfet/js/jquery-ui.min.js' %}"></script>
|
<script type="text/javascript" src="{% static 'kfet/js/jquery-ui.min.js' %}"></script>
|
||||||
<script type="text/javascript" src="{% static 'kfet/js/jquery-confirm.js' %}"></script>
|
<script type="text/javascript" src="{% static 'kfet/js/jquery-confirm.js' %}"></script>
|
||||||
|
<script type="text/javascript" src="{% static 'kfet/vendor/jquery-tablesorter/jquery.tablesorter.combined.js' %}"></script>
|
||||||
<script type="text/javascript" src="{% static 'kfet/js/reconnecting-websocket.js' %}"></script>
|
<script type="text/javascript" src="{% static 'kfet/js/reconnecting-websocket.js' %}"></script>
|
||||||
<script type="text/javascript" src="{% static 'kfet/js/moment.js' %}"></script>
|
<script type="text/javascript" src="{% static 'kfet/js/moment.js' %}"></script>
|
||||||
<script type="text/javascript" src="{% static 'kfet/js/moment-fr.js' %}"></script>
|
<script type="text/javascript" src="{% static 'kfet/js/moment-fr.js' %}"></script>
|
||||||
|
|
|
@ -17,12 +17,15 @@
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover table-condensed">
|
<table
|
||||||
|
class="table table-hover table-condensed sortable"
|
||||||
|
{# Initial sort: [(name,asc)] #}
|
||||||
|
data-sortlist="[[0,0]]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Nom</td>
|
<td>Nom</td>
|
||||||
<td class="text-right">Nombre d'articles</td>
|
<td class="text-right">Nombre d'articles</td>
|
||||||
<td class="text-right">Peut être majorée</td>
|
<td class="text-right" data-sorter="yesno">Peut être majorée</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
|
@ -24,13 +24,16 @@
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover table-condensed">
|
<table
|
||||||
|
class="table table-hover table-condensed sortable"
|
||||||
|
{# Initial sort: [(valid_to,desc)] #}
|
||||||
|
data-sortlist="[[3,1]]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Nom</td>
|
<td>Nom</td>
|
||||||
<td class="text-right">Balance</td>
|
<td class="text-right">Balance</td>
|
||||||
<td class="text-right">Déb. valid.</td>
|
<td class="text-right" data-parser="shortDate">Déb. valid.</td>
|
||||||
<td class="text-right">Fin valid.</td>
|
<td class="text-right" data-parser="shortDate">Fin valid.</td>
|
||||||
<td class="text-right">Protégée</td>
|
<td class="text-right">Protégée</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -43,8 +46,12 @@
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right">{{ checkout.balance}}€</td>
|
<td class="text-right">{{ checkout.balance}}€</td>
|
||||||
<td class="text-right">{{ checkout.valid_from }}</td>
|
<td class="text-right" title="{{ checkout.valid_from }}">
|
||||||
<td class="text-right">{{ checkout.valid_to }}</td>
|
{{ checkout.valid_from|date:'d/m/Y H:i' }}
|
||||||
|
</td>
|
||||||
|
<td class="text-right" title="{{ checkout.valid_to }}">
|
||||||
|
{{ checkout.valid_to|date:'d/m/Y H:i' }}
|
||||||
|
</td>
|
||||||
<td class="text-right">{{ checkout.is_protected|yesno }}</td>
|
<td class="text-right">{{ checkout.is_protected|yesno }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -14,10 +14,13 @@
|
||||||
{% if not statements %}
|
{% if not statements %}
|
||||||
Pas de relevé
|
Pas de relevé
|
||||||
{% else %}
|
{% else %}
|
||||||
<table class="table table-hover table-condensed">
|
<table
|
||||||
|
class="table table-hover table-condensed sortable"
|
||||||
|
{# Initial sort: [(at,desc)] #}
|
||||||
|
data-sortlist="[[0,1]]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Date/heure</td>
|
<td data-sorter="shortDate">Date/heure</td>
|
||||||
<td>Montant pris</td>
|
<td>Montant pris</td>
|
||||||
<td>Montant laissé</td>
|
<td>Montant laissé</td>
|
||||||
<td>Erreur</td>
|
<td>Erreur</td>
|
||||||
|
@ -25,9 +28,9 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for statement in statements %}
|
{% for statement in statements %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td title="{{ statement.at }}">
|
||||||
<a href="{% url 'kfet.checkoutstatement.update' checkout.pk statement.pk %}">
|
<a href="{% url 'kfet.checkoutstatement.update' checkout.pk statement.pk %}">
|
||||||
{{ statement.at }}
|
{{ statement.at|date:'d/m/Y H:i' }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ statement.amount_taken }}</td>
|
<td>{{ statement.amount_taken }}</td>
|
||||||
|
|
|
@ -17,10 +17,13 @@
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover table-condensed">
|
<table
|
||||||
|
class="table table-hover table-condensed sortable"
|
||||||
|
{# Initial sort: [(at,desc)] #}
|
||||||
|
data-sortlist="[[0,1]]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Date</td>
|
<td data-sorter="shortDate">Date</td>
|
||||||
<td>Par</td>
|
<td>Par</td>
|
||||||
<td>Nb articles</td>
|
<td>Nb articles</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -28,9 +31,9 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for inventory in inventories %}
|
{% for inventory in inventories %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td title="{{ inventory.at }}">
|
||||||
<a href="{% url 'kfet.inventory.read' inventory.pk %}">
|
<a href="{% url 'kfet.inventory.read' inventory.pk %}">
|
||||||
<span>{{ inventory.at }}</span>
|
{{ inventory.at|date:'d/m/Y H:i' }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ inventory.by }}</td>
|
<td>{{ inventory.by }}</td>
|
||||||
|
|
|
@ -27,7 +27,10 @@
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-condensed">
|
<table
|
||||||
|
class="table table-condensed table-hover table-striped sortable"
|
||||||
|
{# Initial sort: [(article.name,asc)] #}
|
||||||
|
data-sortlist="[[0,0]]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Article</td>
|
<td>Article</td>
|
||||||
|
@ -36,25 +39,28 @@
|
||||||
<td>Erreur</td>
|
<td>Erreur</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
{% regroup inventoryarts by article.category as category_list %}
|
||||||
{% for inventoryart in inventoryarts %}
|
{% for category in category_list %}
|
||||||
{% ifchanged inventoryart.article.category %}
|
<tbody class="tablesorter-no-sort">
|
||||||
<tr class="section">
|
<tr class="section">
|
||||||
<td colspan="4">{{ inventoryart.article.category.name }}</td>
|
<td colspan="4">{{ category.grouper.name }}</td>
|
||||||
</tr>
|
|
||||||
{% endifchanged %}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a href="{% url "kfet.article.read" inventoryart.article.id %}">
|
|
||||||
{{ inventoryart.article.name }}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>{{ inventoryart.stock_old }}</td>
|
|
||||||
<td>{{ inventoryart.stock_new }}</td>
|
|
||||||
<td>{{ inventoryart.stock_error }}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
</tbody>
|
||||||
</tbody>
|
<tbody>
|
||||||
|
{% for inventoryart in category.list %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{% url "kfet.article.read" inventoryart.article.id %}">
|
||||||
|
{{ inventoryart.article.name }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>{{ inventoryart.stock_old }}</td>
|
||||||
|
<td>{{ inventoryart.stock_new }}</td>
|
||||||
|
<td>{{ inventoryart.stock_error }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -55,11 +55,14 @@
|
||||||
<section>
|
<section>
|
||||||
<h2>Liste des commandes</h2>
|
<h2>Liste des commandes</h2>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover table-condensed">
|
<table
|
||||||
|
class="table table-hover table-condensed sortable"
|
||||||
|
{# Initial sort: [(at,desc)] #}
|
||||||
|
data-sortlist="[[1,1]]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td></td>
|
<td data-sorter="false"></td>
|
||||||
<td>Date</td>
|
<td data-parser="shortDate">Date</td>
|
||||||
<td>Fournisseur</td>
|
<td>Fournisseur</td>
|
||||||
<td>Inventaire</td>
|
<td>Inventaire</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -74,9 +77,9 @@
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td tile="{{ order.at }}">
|
||||||
<a href="{% url 'kfet.order.read' order.pk %}">
|
<a href="{% url 'kfet.order.read' order.pk %}">
|
||||||
{{ order.at }}
|
{{ order.at|date:'d/m/Y H:i' }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ order.supplier }}</td>
|
<td>{{ order.supplier }}</td>
|
||||||
|
|
|
@ -11,60 +11,79 @@
|
||||||
<form action="" method="post">
|
<form action="" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover table-condensed table-condensed-input text-center table-striped">
|
<table
|
||||||
|
class="table table-hover table-condensed table-condensed-input text-center table-striped sortable"
|
||||||
|
{# Initial sort: [(name,asc)] #}
|
||||||
|
data-sortlist="[[0,0]]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="2">Article</td>
|
<td rowspan="2">Article</td>
|
||||||
<td colspan="{{ scale|length }}">Ventes
|
<td colspan="{{ scale|length }}">
|
||||||
<span class='glyphicon glyphicon-question-sign' title="Ventes des 5 dernières semaines" data-placement="bottom"></span>
|
Ventes
|
||||||
</td>
|
<i class='glyphicon glyphicon-question-sign' title="Ventes des 5 dernières semaines" data-placement="bottom"></i>
|
||||||
<td rowspan="2">V. moy.<br>
|
</td>
|
||||||
<span class='glyphicon glyphicon-question-sign' title="Moyenne des ventes" data-placement="bottom"></span>
|
<td rowspan="2">
|
||||||
</td>
|
V. moy.
|
||||||
<td rowspan="2">E.T.<br>
|
<br>
|
||||||
<span class='glyphicon glyphicon-question-sign' title="Écart-type des ventes" data-placement="bottom"></span>
|
<i class='glyphicon glyphicon-question-sign' title="Moyenne des ventes" data-placement="bottom"></i>
|
||||||
</td>
|
</td>
|
||||||
<td rowspan="2">Prév.<br>
|
<td rowspan="2" data-sorter="false">
|
||||||
<span class='glyphicon glyphicon-question-sign' title="Prévision de ventes" data-placement="bottom"></span>
|
E.T.
|
||||||
</td>
|
<br>
|
||||||
|
<i class='glyphicon glyphicon-question-sign' title="Écart-type des ventes" data-placement="bottom"></i>
|
||||||
|
</td>
|
||||||
|
<td rowspan="2">
|
||||||
|
Prév.
|
||||||
|
<br>
|
||||||
|
<i class='glyphicon glyphicon-question-sign' title="Prévision de ventes" data-placement="bottom"></i>
|
||||||
|
</td>
|
||||||
<td rowspan="2">Stock</td>
|
<td rowspan="2">Stock</td>
|
||||||
<td rowspan="2">Box<br>
|
<td rowspan="2" data-sorter="false">
|
||||||
<span class='glyphicon glyphicon-question-sign' title="Capacité d'une boite" data-placement="bottom"></span>
|
Box
|
||||||
</td>
|
<br>
|
||||||
<td rowspan="2">Rec.<br>
|
<i class='glyphicon glyphicon-question-sign' title="Capacité d'une boite" data-placement="bottom"></i>
|
||||||
<span class='glyphicon glyphicon-question-sign' title="Quantité conseillée" data-placement="bottom"></span>
|
</td>
|
||||||
</td>
|
<td rowspan="2">
|
||||||
<td rowspan="2">Commande</td>
|
Rec.
|
||||||
|
<br>
|
||||||
|
<i class='glyphicon glyphicon-question-sign' title="Quantité conseillée" data-placement="bottom"></i>
|
||||||
|
</td>
|
||||||
|
<td rowspan="2" data-sorter="false" class="small-width">
|
||||||
|
Commande
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
{% for label in scale.get_labels %}
|
{% for label in scale.get_labels %}
|
||||||
<td>{{ label }}</td>
|
<td class="sm-padding">{{ label }}</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
{% regroup formset by category_name as category_list %}
|
||||||
{% for form in formset %}
|
{% for category in category_list %}
|
||||||
{% ifchanged form.category %}
|
<tbody class="tablesorter-no-sort">
|
||||||
<tr class='section text-left'>
|
<tr class='section text-left'>
|
||||||
<td colspan="{{ scale|length|add:'8' }}">{{ form.category_name }}</td>
|
<td colspan="{{ scale|length|add:'8' }}">{{ category.grouper }}</td>
|
||||||
</tr>
|
|
||||||
{% endifchanged %}
|
|
||||||
<tr>
|
|
||||||
{{ form.article }}
|
|
||||||
<td class="text-left">{{ form.name }}</td>
|
|
||||||
{% for v_chunk in form.v_all %}
|
|
||||||
<td>{{ v_chunk }}</td>
|
|
||||||
{% endfor %}
|
|
||||||
<td>{{ form.v_moy }}</td>
|
|
||||||
<td>{{ form.v_et }}</td>
|
|
||||||
<td>{{ form.v_prev }}</td>
|
|
||||||
<td>{{ form.stock }}</td>
|
|
||||||
<td>{{ form.box_capacity|default:"" }}</td>
|
|
||||||
<td>{{ form.c_rec }}</td>
|
|
||||||
<td class="nopadding">{{ form.quantity_ordered | add_class:"form-control" }}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
</tbody>
|
||||||
</tbody>
|
<tbody>
|
||||||
|
{% for form in category.list %}
|
||||||
|
<tr>
|
||||||
|
{{ form.article }}
|
||||||
|
<td class="text-left">{{ form.name }}</td>
|
||||||
|
{% for v_chunk in form.v_all %}
|
||||||
|
<td>{{ v_chunk }}</td>
|
||||||
|
{% endfor %}
|
||||||
|
<td>{{ form.v_moy }}</td>
|
||||||
|
<td>{{ form.v_et }}</td>
|
||||||
|
<td>{{ form.v_prev }}</td>
|
||||||
|
<td>{{ form.stock }}</td>
|
||||||
|
<td>{{ form.box_capacity|default:"" }}</td>
|
||||||
|
<td>{{ form.c_rec }}</td>
|
||||||
|
<td class="nopadding">{{ form.quantity_ordered|add_class:"form-control" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{{ formset.management_form }}
|
{{ formset.management_form }}
|
||||||
|
|
|
@ -42,7 +42,10 @@
|
||||||
<section>
|
<section>
|
||||||
<h2>Détails</h2>
|
<h2>Détails</h2>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-condensed">
|
<table
|
||||||
|
class="table table-condensed table-hover table-striped sortable"
|
||||||
|
{# Initial sort: [(article.name,asc)] #}
|
||||||
|
data-sortlist="[[0,0]]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Article</td>
|
<td>Article</td>
|
||||||
|
@ -51,32 +54,35 @@
|
||||||
<td>Reçu</td>
|
<td>Reçu</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
{% regroup orderarts by article.category as category_list %}
|
||||||
{% for orderart in orderarts %}
|
{% for category in category_list %}
|
||||||
{% ifchanged orderart.article.category %}
|
<tbody class="tablesorter-no-sort">
|
||||||
<tr class="section">
|
<tr class="section">
|
||||||
<td colspan="4">{{ orderart.article.category.name }}</td>
|
<td colspan="4">{{ category.grouper.name }}</td>
|
||||||
</tr>
|
|
||||||
{% endifchanged %}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a href="{% url "kfet.article.read" orderart.article.id %}">
|
|
||||||
{{ orderart.article.name }}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>{{ orderart.quantity_ordered }}</td>
|
|
||||||
<td>
|
|
||||||
{% if orderart.article.box_capacity %}
|
|
||||||
{# c'est une division ! #}
|
|
||||||
{% widthratio orderart.quantity_ordered orderart.article.box_capacity 1 %}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ orderart.quantity_received|default_if_none:'' }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
</tbody>
|
||||||
</tbody>
|
<tbody>
|
||||||
|
{% for orderart in category.list %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{% url "kfet.article.read" orderart.article.id %}">
|
||||||
|
{{ orderart.article.name }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>{{ orderart.quantity_ordered }}</td>
|
||||||
|
<td>
|
||||||
|
{% if orderart.article.box_capacity %}
|
||||||
|
{# c'est une division ! #}
|
||||||
|
{% widthratio orderart.quantity_ordered orderart.article.box_capacity 1 %}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ orderart.quantity_received|default_if_none:'' }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
48
kfet/tests/test_forms.py
Normal file
48
kfet/tests/test_forms.py
Normal 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), [('', '---------')])
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
@ -715,6 +707,14 @@ class ArticleList(ListView):
|
||||||
template_name = 'kfet/article.html'
|
template_name = 'kfet/article.html'
|
||||||
context_object_name = 'articles'
|
context_object_name = 'articles'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
articles = context[self.context_object_name]
|
||||||
|
context['nb_articles'] = len(articles)
|
||||||
|
context[self.context_object_name] = articles.filter(is_sold=True)
|
||||||
|
context['not_sold_articles'] = articles.filter(is_sold=False)
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
# Article - Create
|
# Article - Create
|
||||||
class ArticleCreate(SuccessMessageMixin, CreateView):
|
class ArticleCreate(SuccessMessageMixin, CreateView):
|
||||||
|
@ -1843,7 +1843,7 @@ def order_create(request, pk):
|
||||||
else:
|
else:
|
||||||
formset = cls_formset(initial=initial)
|
formset = cls_formset(initial=initial)
|
||||||
|
|
||||||
scale.label_fmt = "S -{rev_i}"
|
scale.label_fmt = "S-{rev_i}"
|
||||||
|
|
||||||
return render(request, 'kfet/order_create.html', {
|
return render(request, 'kfet/order_create.html', {
|
||||||
'supplier': supplier,
|
'supplier': supplier,
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
1698
shared/static/vendor/jquery.autocomplete-light/3.5.0/dist/jquery.autocomplete-light.js
vendored
Normal file
1698
shared/static/vendor/jquery.autocomplete-light/3.5.0/dist/jquery.autocomplete-light.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
9
shared/static/vendor/jquery.autocomplete-light/3.5.0/dist/jquery.autocomplete-light.min.js
vendored
Normal file
9
shared/static/vendor/jquery.autocomplete-light/3.5.0/dist/jquery.autocomplete-light.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
335
shared/tests/testcases.py
Normal file
335
shared/tests/testcases.py
Normal file
|
@ -0,0 +1,335 @@
|
||||||
|
import csv
|
||||||
|
from unittest import mock
|
||||||
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
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
|
||||||
|
|
||||||
|
import icalendar
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class TestCaseMixin:
|
||||||
|
|
||||||
|
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 = '{}?{}'.format(
|
||||||
|
reverse('cof-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 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)
|
||||||
|
|
||||||
|
def load_from_csv_response(self, r):
|
||||||
|
decoded = r.content.decode('utf-8')
|
||||||
|
return list(csv.reader(decoded.split('\n')[:-1]))
|
||||||
|
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
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, the following users are created:
|
||||||
|
- 'user': a basic user without any permission,
|
||||||
|
- 'root': a superuser, account trigramme: 200.
|
||||||
|
Their password is their username.
|
||||||
|
|
||||||
|
One can create additionnal users with 'get_users_extra' method, or prevent
|
||||||
|
these 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.
|
||||||
|
|
||||||
|
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 '<method(lowercase)>_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()
|
||||||
|
|
||||||
|
# Register of User instances.
|
||||||
|
self.users = {}
|
||||||
|
|
||||||
|
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 <label: user instance>.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'user': User.objects.create_user('user', '', 'user'),
|
||||||
|
'root': User.objects.create_superuser('root', '', 'root'),
|
||||||
|
}
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def users_base(self):
|
||||||
|
return self.get_users_base()
|
||||||
|
|
||||||
|
def get_users_extra(self):
|
||||||
|
"""
|
||||||
|
Dict of <label: user instance>.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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)
|
Loading…
Reference in a new issue