diff --git a/README.md b/README.md index 01f4ead2..b9d736ae 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # GestioCOF +![build_status](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/build.svg) + ## Installation ### Vagrant diff --git a/bda/admin.py b/bda/admin.py index 60d3c1ba..6638ad45 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -import autocomplete_light from datetime import timedelta from custommail.shortcuts import send_mass_custom_mail @@ -9,6 +8,9 @@ from django.db.models import Sum, Count from django.template.defaultfilters import pluralize from django.utils import timezone from django import forms + +from dal.autocomplete import ModelSelect2 + from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\ Attribution, Tirage, Quote, CategorieSpectacle, SpectacleRevente @@ -24,8 +26,17 @@ class ReadOnlyMixin(object): return readonly_fields + self.readonly_fields_update +class ChoixSpectacleAdminForm(forms.ModelForm): + class Meta: + widgets = { + 'participant': ModelSelect2(url='bda-participant-autocomplete'), + 'spectacle': ModelSelect2(url='bda-spectacle-autocomplete'), + } + + class ChoixSpectacleInline(admin.TabularInline): model = ChoixSpectacle + form = ChoixSpectacleAdminForm sortable_field_name = "priority" @@ -180,7 +191,7 @@ class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin): class ChoixSpectacleAdmin(admin.ModelAdmin): - form = autocomplete_light.modelform_factory(ChoixSpectacle, exclude=[]) + form = ChoixSpectacleAdminForm def tirage(self, obj): return obj.participant.tirage diff --git a/bda/autocomplete_light_registry.py b/bda/autocomplete_light_registry.py deleted file mode 100644 index 6c2f3ea6..00000000 --- a/bda/autocomplete_light_registry.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- - -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - -import autocomplete_light - -from bda.models import Participant, Spectacle - -autocomplete_light.register( - Participant, search_fields=('user__username', 'user__first_name', - 'user__last_name'), - autocomplete_js_attributes={'placeholder': 'participant...'}) - -autocomplete_light.register( - Spectacle, search_fields=('title', ), - autocomplete_js_attributes={'placeholder': 'spectacle...'}) diff --git a/bda/migrations/0001_initial.py b/bda/migrations/0001_initial.py index aa2cb252..c4494413 100644 --- a/bda/migrations/0001_initial.py +++ b/bda/migrations/0001_initial.py @@ -59,7 +59,7 @@ class Migration(migrations.Migration): ('price', models.FloatField(verbose_name=b"Prix d'une place", blank=True)), ('slots', models.IntegerField(verbose_name=b'Places')), ('priority', models.IntegerField(default=1000, verbose_name=b'Priorit\xc3\xa9')), - ('location', models.ForeignKey(to='bda.Salle')), + ('location', models.ForeignKey(to='bda.Salle', on_delete=models.CASCADE)), ], options={ 'ordering': ('priority', 'date', 'title'), @@ -79,27 +79,27 @@ class Migration(migrations.Migration): migrations.AddField( model_name='participant', name='user', - field=models.OneToOneField(to=settings.AUTH_USER_MODEL), + field=models.OneToOneField(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), ), migrations.AddField( model_name='choixspectacle', name='participant', - field=models.ForeignKey(to='bda.Participant'), + field=models.ForeignKey(to='bda.Participant', on_delete=models.CASCADE), ), migrations.AddField( model_name='choixspectacle', name='spectacle', - field=models.ForeignKey(related_name='participants', to='bda.Spectacle'), + field=models.ForeignKey(related_name='participants', to='bda.Spectacle', on_delete=models.CASCADE), ), migrations.AddField( model_name='attribution', name='participant', - field=models.ForeignKey(to='bda.Participant'), + field=models.ForeignKey(to='bda.Participant', on_delete=models.CASCADE), ), migrations.AddField( model_name='attribution', name='spectacle', - field=models.ForeignKey(related_name='attribues', to='bda.Spectacle'), + field=models.ForeignKey(related_name='attribues', to='bda.Spectacle', on_delete=models.CASCADE), ), migrations.AlterUniqueTogether( name='choixspectacle', diff --git a/bda/migrations/0002_add_tirage.py b/bda/migrations/0002_add_tirage.py index 22c387a0..79f79a57 100644 --- a/bda/migrations/0002_add_tirage.py +++ b/bda/migrations/0002_add_tirage.py @@ -55,7 +55,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='participant', name='user', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), ), # Create fields `spectacle` for `Participant` and `Spectacle` models. # These fields are not nullable, but we first create them as nullable @@ -63,22 +63,22 @@ class Migration(migrations.Migration): migrations.AddField( model_name='participant', name='tirage', - field=models.ForeignKey(to='bda.Tirage', null=True), + field=models.ForeignKey(to='bda.Tirage', null=True, on_delete=models.CASCADE), ), migrations.AddField( model_name='spectacle', name='tirage', - field=models.ForeignKey(to='bda.Tirage', null=True), + field=models.ForeignKey(to='bda.Tirage', null=True, on_delete=models.CASCADE), ), migrations.RunPython(fill_tirage_fields, migrations.RunPython.noop), migrations.AlterField( model_name='participant', name='tirage', - field=models.ForeignKey(to='bda.Tirage'), + field=models.ForeignKey(to='bda.Tirage', on_delete=models.CASCADE), ), migrations.AlterField( model_name='spectacle', name='tirage', - field=models.ForeignKey(to='bda.Tirage'), + field=models.ForeignKey(to='bda.Tirage', on_delete=models.CASCADE), ), ] diff --git a/bda/migrations/0007_extends_spectacle.py b/bda/migrations/0007_extends_spectacle.py index b95c18de..6ea11dc0 100644 --- a/bda/migrations/0007_extends_spectacle.py +++ b/bda/migrations/0007_extends_spectacle.py @@ -73,6 +73,7 @@ class Migration(migrations.Migration): model_name='spectacle', name='category', field=models.ForeignKey(blank=True, to='bda.CategorieSpectacle', + on_delete=models.CASCADE, null=True), ), migrations.AddField( @@ -84,6 +85,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='quote', name='spectacle', - field=models.ForeignKey(to='bda.Spectacle'), + field=models.ForeignKey(to='bda.Spectacle', + on_delete=models.CASCADE), ), ] diff --git a/bda/migrations/0009_revente.py b/bda/migrations/0009_revente.py index 1cca4e86..70d6f338 100644 --- a/bda/migrations/0009_revente.py +++ b/bda/migrations/0009_revente.py @@ -47,12 +47,14 @@ class Migration(migrations.Migration): model_name='spectaclerevente', name='attribution', field=models.OneToOneField(to='bda.Attribution', + on_delete=models.CASCADE, related_name='revente'), ), migrations.AddField( model_name='spectaclerevente', name='seller', field=models.ForeignKey(to='bda.Participant', + on_delete=models.CASCADE, verbose_name='Vendeur', related_name='original_shows'), ), @@ -60,6 +62,7 @@ class Migration(migrations.Migration): model_name='spectaclerevente', name='soldTo', field=models.ForeignKey(to='bda.Participant', + on_delete=models.CASCADE, verbose_name='Vendue à', null=True, blank=True), ), diff --git a/bda/models.py b/bda/models.py index 41462d70..73356038 100644 --- a/bda/models.py +++ b/bda/models.py @@ -6,12 +6,15 @@ from datetime import timedelta from custommail.shortcuts import send_mass_custom_mail from django.contrib.sites.models import Site +from django.core import mail from django.db import models from django.db.models import Count from django.contrib.auth.models import User from django.conf import settings from django.utils import timezone, formats +from custommail.models import CustomMail + def get_generic_user(): generic, _ = User.objects.get_or_create( @@ -59,9 +62,12 @@ class CategorieSpectacle(models.Model): class Spectacle(models.Model): title = models.CharField("Titre", max_length=300) - category = models.ForeignKey(CategorieSpectacle, blank=True, null=True) + category = models.ForeignKey( + CategorieSpectacle, on_delete=models.CASCADE, + blank=True, null=True, + ) date = models.DateTimeField("Date & heure") - location = models.ForeignKey(Salle) + location = models.ForeignKey(Salle, on_delete=models.CASCADE) vips = models.TextField('Personnalités', blank=True) description = models.TextField("Description", blank=True) slots_description = models.TextField("Description des places", blank=True) @@ -71,7 +77,7 @@ class Spectacle(models.Model): max_length=500) price = models.FloatField("Prix d'une place") slots = models.IntegerField("Places") - tirage = models.ForeignKey(Tirage) + tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE) listing = models.BooleanField("Les places sont sur listing") rappel_sent = models.DateTimeField("Mail de rappel envoyé", blank=True, null=True) @@ -135,7 +141,7 @@ class Spectacle(models.Model): class Quote(models.Model): - spectacle = models.ForeignKey(Spectacle) + spectacle = models.ForeignKey(Spectacle, on_delete=models.CASCADE) text = models.TextField('Citation') author = models.CharField('Auteur', max_length=200) @@ -149,7 +155,7 @@ PAYMENT_TYPES = ( class Participant(models.Model): - user = models.ForeignKey(User) + user = models.ForeignKey(User, on_delete=models.CASCADE) choices = models.ManyToManyField(Spectacle, through="ChoixSpectacle", related_name="chosen_by") @@ -160,7 +166,7 @@ class Participant(models.Model): paymenttype = models.CharField("Moyen de paiement", max_length=6, choices=PAYMENT_TYPES, blank=True) - tirage = models.ForeignKey(Tirage) + tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE) choicesrevente = models.ManyToManyField(Spectacle, related_name="subscribed", blank=True) @@ -176,8 +182,11 @@ DOUBLE_CHOICES = ( class ChoixSpectacle(models.Model): - participant = models.ForeignKey(Participant) - spectacle = models.ForeignKey(Spectacle, related_name="participants") + participant = models.ForeignKey(Participant, on_delete=models.CASCADE) + spectacle = models.ForeignKey( + Spectacle, on_delete=models.CASCADE, + related_name="participants", + ) priority = models.PositiveIntegerField("Priorité") double_choice = models.CharField("Nombre de places", default="1", choices=DOUBLE_CHOICES, @@ -204,8 +213,11 @@ class ChoixSpectacle(models.Model): class Attribution(models.Model): - participant = models.ForeignKey(Participant) - spectacle = models.ForeignKey(Spectacle, related_name="attribues") + participant = models.ForeignKey(Participant, on_delete=models.CASCADE) + spectacle = models.ForeignKey( + Spectacle, on_delete=models.CASCADE, + related_name="attribues", + ) given = models.BooleanField("Donnée", default=False) def __str__(self): @@ -214,18 +226,25 @@ class Attribution(models.Model): class SpectacleRevente(models.Model): - attribution = models.OneToOneField(Attribution, - related_name="revente") + attribution = models.OneToOneField( + Attribution, on_delete=models.CASCADE, + related_name="revente", + ) date = models.DateTimeField("Date de mise en vente", default=timezone.now) answered_mail = models.ManyToManyField(Participant, related_name="wanted", blank=True) - seller = models.ForeignKey(Participant, - related_name="original_shows", - verbose_name="Vendeur") - soldTo = models.ForeignKey(Participant, blank=True, null=True, - verbose_name="Vendue à") + seller = models.ForeignKey( + Participant, on_delete=models.CASCADE, + verbose_name="Vendeur", + related_name="original_shows", + ) + soldTo = models.ForeignKey( + Participant, on_delete=models.CASCADE, + verbose_name="Vendue à", + blank=True, null=True, + ) notif_sent = models.BooleanField("Notification envoyée", default=False) @@ -312,37 +331,55 @@ class SpectacleRevente(models.Model): # Envoie un mail au gagnant et au vendeur winner = random.choice(inscrits) self.soldTo = winner - datatuple = [] + + mails = [] + context = { 'acheteur': winner.user, 'vendeur': seller.user, 'show': spectacle, } - datatuple.append(( - 'bda-revente-winner', - context, - settings.MAIL_DATA['revente']['FROM'], - [winner.user.email], - )) - datatuple.append(( + + c_mails_qs = CustomMail.objects.filter(shortname__in=[ + 'bda-revente-winner', 'bda-revente-loser', 'bda-revente-seller', - context, - settings.MAIL_DATA['revente']['FROM'], - [seller.user.email] - )) + ]) + + c_mails = {cm.shortname: cm for cm in c_mails_qs} + + mails.append( + c_mails['bda-revente-winner'].get_message( + context, + from_email=settings.MAIL_DATA['revente']['FROM'], + to=[winner.user.email], + ) + ) + + mails.append( + c_mails['bda-revente-seller'].get_message( + context, + from_email=settings.MAIL_DATA['revente']['FROM'], + to=[seller.user.email], + reply_to=[winner.user.email], + ) + ) # Envoie un mail aux perdants for inscrit in inscrits: if inscrit != winner: new_context = dict(context) new_context['acheteur'] = inscrit.user - datatuple.append(( - 'bda-revente-loser', - new_context, - settings.MAIL_DATA['revente']['FROM'], - [inscrit.user.email] - )) - send_mass_custom_mail(datatuple) + + mails.append( + c_mails['bda-revente-loser'].get_message( + new_context, + from_email=settings.MAIL_DATA['revente']['FROM'], + to=[inscrit.user.email], + ) + ) + + mail_conn = mail.get_connection() + mail_conn.send_messages(mails) # Si personne ne veut de la place, elle part au shotgun else: self.shotgun = True diff --git a/bda/templates/bda/inscription-formset.html b/bda/templates/bda/inscription-formset.html index 65ef389b..88b65600 100644 --- a/bda/templates/bda/inscription-formset.html +++ b/bda/templates/bda/inscription-formset.html @@ -14,7 +14,7 @@ {% endif %} - + {% for field in form.visible_fields %} {% if field.name != "DELETE" and field.name != "priority" %} diff --git a/bda/templates/bda/inscription-tirage.html b/bda/templates/bda/inscription-tirage.html index d56b4229..3fd81378 100644 --- a/bda/templates/bda/inscription-tirage.html +++ b/bda/templates/bda/inscription-tirage.html @@ -27,6 +27,14 @@ var django = { var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-'); $(this).attr('for', newFor); }); + // Cloning diff --git a/gestioncof/templates/gestioncof/base_header.html b/gestioncof/templates/gestioncof/base_header.html index 21441875..e5f757a7 100644 --- a/gestioncof/templates/gestioncof/base_header.html +++ b/gestioncof/templates/gestioncof/base_header.html @@ -3,7 +3,7 @@ {% block content %}
diff --git a/gestioncof/templates/gestioncof/calendar_subscription.html b/gestioncof/templates/gestioncof/calendar_subscription.html index b13cb7f2..4b9e3cbb 100644 --- a/gestioncof/templates/gestioncof/calendar_subscription.html +++ b/gestioncof/templates/gestioncof/calendar_subscription.html @@ -12,7 +12,7 @@ souscrire aux événements du COF et/ou aux spectacles BdA. {% if token %}

Votre calendrier (compatible avec toutes les applications d'agenda) se trouve à -cette adresse.

+cette adresse.

@@ -120,8 +120,8 @@

Liens utiles

diff --git a/gestioncof/templates/inscription-petit-cours-formset.html b/gestioncof/templates/inscription-petit-cours-formset.html index ec8979f5..40311772 100644 --- a/gestioncof/templates/inscription-petit-cours-formset.html +++ b/gestioncof/templates/inscription-petit-cours-formset.html @@ -16,7 +16,7 @@ {% endif %} - + {% for field in form.visible_fields %} {% if field.name != "DELETE" and field.name != "priority" %} diff --git a/gestioncof/templates/login.html b/gestioncof/templates/login.html index 1cd1d25d..bfc2dbb8 100644 --- a/gestioncof/templates/login.html +++ b/gestioncof/templates/login.html @@ -15,7 +15,7 @@

Identifiants incorrects.

{% endif %} + action="{% url 'ext_login_view' %}?next={{ next|urlencode }}"> {% csrf_token %}
diff --git a/gestioncof/templates/login_switch.html b/gestioncof/templates/login_switch.html index aa8a68c6..d361493b 100644 --- a/gestioncof/templates/login_switch.html +++ b/gestioncof/templates/login_switch.html @@ -12,13 +12,13 @@
+ href="{% url 'cas_login_view' %}?next={{ next|urlencode }}">
Compte clipper
+ href="{% url 'ext_login_view' %}?next={{ next|urlencode }}">
Extérieur
diff --git a/gestioncof/templates/registration.html b/gestioncof/templates/registration.html index 8f05dfb0..99ab3e73 100644 --- a/gestioncof/templates/registration.html +++ b/gestioncof/templates/registration.html @@ -4,7 +4,7 @@ {% block page_size %}col-sm-8{% endblock %} {% block extra_head %} - + {% endblock %} {% block realcontent %} diff --git a/gestioncof/templates/registration/password_change_done.html b/gestioncof/templates/registration/password_change_done.html index f83a781b..9f2c4a60 100644 --- a/gestioncof/templates/registration/password_change_done.html +++ b/gestioncof/templates/registration/password_change_done.html @@ -5,5 +5,5 @@ {% block realcontent %}

Mot de passe modifié avec succès !

-

Retour au menu principal

+

Retour au menu principal

{% endblock %} diff --git a/gestioncof/templates/registration/password_change_form.html b/gestioncof/templates/registration/password_change_form.html index f579fb31..d9a3f66a 100644 --- a/gestioncof/templates/registration/password_change_form.html +++ b/gestioncof/templates/registration/password_change_form.html @@ -5,7 +5,7 @@ {% block realcontent %}

Changement de mot de passe

- + {% csrf_token %} {{ form | bootstrap }} diff --git a/gestioncof/templates/survey_status.html b/gestioncof/templates/survey_status.html index 831a07bb..0e630c6e 100644 --- a/gestioncof/templates/survey_status.html +++ b/gestioncof/templates/survey_status.html @@ -11,7 +11,7 @@ {% endif %}

Filtres

{% include "tristate_js.html" %} - + {% csrf_token %} {{ form.as_p }} diff --git a/gestioncof/tests/__init__.py b/gestioncof/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gestioncof/tests.py b/gestioncof/tests/test_legacy.py similarity index 100% rename from gestioncof/tests.py rename to gestioncof/tests/test_legacy.py diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py new file mode 100644 index 00000000..47139327 --- /dev/null +++ b/gestioncof/tests/test_views.py @@ -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]) diff --git a/gestioncof/tests/testcases.py b/gestioncof/tests/testcases.py new file mode 100644 index 00000000..b53f2866 --- /dev/null +++ b/gestioncof/tests/testcases.py @@ -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'), + } diff --git a/gestioncof/tests/utils.py b/gestioncof/tests/utils.py new file mode 100644 index 00000000..7ba361b7 --- /dev/null +++ b/gestioncof/tests/utils.py @@ -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) diff --git a/gestioncof/urls.py b/gestioncof/urls.py index 57c2e8f2..7dc72092 100644 --- a/gestioncof/urls.py +++ b/gestioncof/urls.py @@ -6,12 +6,17 @@ from gestioncof import views, petits_cours_views from gestioncof.decorators import buro_required export_patterns = [ - url(r'^members$', views.export_members), - url(r'^mega/avecremarques$', views.export_mega_remarksonly), - url(r'^mega/participants$', views.export_mega_participants), - url(r'^mega/orgas$', views.export_mega_orgas), + url(r'^members$', views.export_members, + name='cof.membres_export'), + url(r'^mega/avecremarques$', views.export_mega_remarksonly, + 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.+)$', views.export_mega_bytype), - url(r'^mega$', views.export_mega), + url(r'^mega$', views.export_mega, + name='cof.mega_export'), ] petitcours_patterns = [ @@ -36,19 +41,24 @@ petitcours_patterns = [ ] surveys_patterns = [ - url(r'^(?P\d+)/status$', views.survey_status), - url(r'^(?P\d+)$', views.survey), + url(r'^(?P\d+)/status$', views.survey_status, + name='survey.details.status'), + url(r'^(?P\d+)$', views.survey, + name='survey.details'), ] events_patterns = [ - url(r'^(?P\d+)$', views.event), - url(r'^(?P\d+)/status$', views.event_status), + url(r'^(?P\d+)$', views.event, + name='event.details'), + url(r'^(?P\d+)/status$', views.event_status, + name='event.details.status'), ] calendar_patterns = [ - url(r'^subscription$', 'gestioncof.views.calendar'), - url(r'^(?P[a-z0-9-]+)/calendar.ics$', - 'gestioncof.views.calendar_ics') + url(r'^subscription$', views.calendar, + name='calendar'), + url(r'^(?P[a-z0-9-]+)/calendar.ics$', views.calendar_ics, + name='calendar.ics'), ] clubs_patterns = [ diff --git a/gestioncof/views.py b/gestioncof/views.py index ec9f6efd..6126eb10 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -9,6 +9,7 @@ from django.http import Http404, HttpResponse, HttpResponseForbidden from django.contrib.auth.decorators import login_required from django.contrib.auth.views import ( login as django_login_view, logout as django_logout_view, + redirect_to_login, ) from django.contrib.auth.models import User from django.contrib.sites.models import Site @@ -20,6 +21,8 @@ from django.contrib import messages from django_cas_ng.views import logout as cas_logout_view +from utils.views.autocomplete import Select2QuerySetView + from gestioncof.models import Survey, SurveyAnswer, SurveyQuestion, \ SurveyQuestionAnswer from gestioncof.models import Event, EventRegistration, EventOption, \ @@ -54,8 +57,8 @@ def home(request): def login(request): - if request.user.is_authenticated(): - return redirect("gestioncof.views.home") + if request.user.is_authenticated: + return redirect("home") context = {} if request.method == "GET" and 'next' in request.GET: context['next'] = request.GET['next'] @@ -336,7 +339,7 @@ def profile(request): if form.is_valid(): form.save() messages.success(request, - "Votre profil a été mis à jour avec succès !") + "Votre profil a été mis à jour avec succès !") else: form = UserProfileForm(instance=request.user.profile) return render(request, "gestioncof/profile.html", {"form": form}) @@ -564,7 +567,7 @@ def liste_clubs(request): if request.user.profile.is_buro: data = {'owned_clubs': clubs.all()} else: - data = {'owned_clubs': request.user.clubs_geres, + data = {'owned_clubs': request.user.clubs_geres.all(), 'other_clubs': clubs.exclude(respos=request.user)} return render(request, 'liste_clubs.html', data) @@ -780,9 +783,24 @@ class ConfigUpdate(FormView): def dispatch(self, request, *args, **kwargs): 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) def form_valid(self, form): form.save() return super().form_valid(form) + + +## +# Autocomplete views +# +# https://django-autocomplete-light.readthedocs.io/en/master/tutorial.html#create-an-autocomplete-view +## + + +class UserAutocomplete(Select2QuerySetView): + model = User + search_fields = ('username', 'first_name', 'last_name') + + +user_autocomplete = buro_required(UserAutocomplete.as_view()) diff --git a/gestioncof/widgets.py b/gestioncof/widgets.py index 758fc4ad..cbc9cd93 100644 --- a/gestioncof/widgets.py +++ b/gestioncof/widgets.py @@ -20,6 +20,7 @@ class TriStateCheckbox(Widget): def render(self, name, value, attrs=None, choices=()): if value is None: value = 'none' - final_attrs = self.build_attrs(attrs, value=value) + attrs['value'] = value + final_attrs = self.build_attrs(self.attrs, attrs) output = ["" % flatatt(final_attrs)] return mark_safe('\n'.join(output)) diff --git a/kfet/auth/middleware.py b/kfet/auth/middleware.py index 748ce4dd..48d9c4ee 100644 --- a/kfet/auth/middleware.py +++ b/kfet/auth/middleware.py @@ -13,8 +13,11 @@ class TemporaryAuthMiddleware: values from CofProfile and Account of this user. """ - def process_request(self, request): - if request.user.is_authenticated(): + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + if request.user.is_authenticated: # avoid multiple db accesses in views and templates request.user = ( User.objects @@ -31,6 +34,8 @@ class TemporaryAuthMiddleware: request.real_user = request.user request.user = temp_request_user + return self.get_response(request) + def get_kfet_password(self, request): return ( request.META.get('HTTP_KFETPASSWORD') or diff --git a/kfet/auth/tests.py b/kfet/auth/tests.py index c2f183cd..0c8b25d3 100644 --- a/kfet/auth/tests.py +++ b/kfet/auth/tests.py @@ -286,6 +286,8 @@ class TemporaryAuthTests(TestCase): self.factory = RequestFactory() + self.middleware = TemporaryAuthMiddleware(mock.Mock()) + user1_acc = Account(trigramme='000') user1_acc.change_pwd('kfet_user1') user1_acc.save({'username': 'user1'}) @@ -312,7 +314,7 @@ class TemporaryAuthTests(TestCase): request = self.factory.get('/', HTTP_KFETPASSWORD='kfet_user2') request.user = self.user1 - TemporaryAuthMiddleware().process_request(request) + self.middleware(request) self.assertEqual(request.user, self.user2) self.assertEqual(request.real_user, self.user1) @@ -325,7 +327,7 @@ class TemporaryAuthTests(TestCase): request = self.factory.post('/', {'KFETPASSWORD': 'kfet_user2'}) request.user = self.user1 - TemporaryAuthMiddleware().process_request(request) + self.middleware(request) self.assertEqual(request.user, self.user2) self.assertEqual(request.real_user, self.user1) @@ -337,7 +339,7 @@ class TemporaryAuthTests(TestCase): request = self.factory.post('/', {'KFETPASSWORD': 'invalid'}) request.user = self.user1 - TemporaryAuthMiddleware().process_request(request) + self.middleware(request) self.assertEqual(request.user, self.user1) self.assertFalse(hasattr(request, 'real_user')) diff --git a/kfet/cms/migrations/0001_initial.py b/kfet/cms/migrations/0001_initial.py index 951637c7..ed0b0948 100644 --- a/kfet/cms/migrations/0001_initial.py +++ b/kfet/cms/migrations/0001_initial.py @@ -20,7 +20,7 @@ class Migration(migrations.Migration): migrations.CreateModel( name='KFetPage', fields=[ - ('page_ptr', models.OneToOneField(serialize=False, primary_key=True, parent_link=True, auto_created=True, to='wagtailcore.Page')), + ('page_ptr', models.OneToOneField(serialize=False, primary_key=True, parent_link=True, auto_created=True, to='wagtailcore.Page', on_delete=models.CASCADE)), ('no_header', models.BooleanField(verbose_name='Sans en-tête', help_text="Coché, l'en-tête (avec le titre) de la page n'est pas affiché.", default=False)), ('content', wagtail.wagtailcore.fields.StreamField((('rich', wagtail.wagtailcore.blocks.RichTextBlock(label='Éditeur')), ('carte', kfet.cms.models.MenuBlock()), ('group_team', wagtail.wagtailcore.blocks.StructBlock((('show_only', wagtail.wagtailcore.blocks.IntegerBlock(help_text='Nombre initial de membres affichés. Laisser vide pour tou-te-s les afficher.', required=False, label='Montrer seulement')), ('members', wagtail.wagtailcore.blocks.ListBlock(wagtail.wagtailsnippets.blocks.SnippetChooserBlock(kfet.cms.models.MemberTeam), classname='team-group', label='K-Fêt-eux-ses'))))), ('group', wagtail.wagtailcore.blocks.StreamBlock((('rich', wagtail.wagtailcore.blocks.RichTextBlock(label='Éditeur')), ('carte', kfet.cms.models.MenuBlock()), ('group_team', wagtail.wagtailcore.blocks.StructBlock((('show_only', wagtail.wagtailcore.blocks.IntegerBlock(help_text='Nombre initial de membres affichés. Laisser vide pour tou-te-s les afficher.', required=False, label='Montrer seulement')), ('members', wagtail.wagtailcore.blocks.ListBlock(wagtail.wagtailsnippets.blocks.SnippetChooserBlock(kfet.cms.models.MemberTeam), classname='team-group', label='K-Fêt-eux-ses')))))), label='Contenu groupé'))), verbose_name='Contenu')), ('layout', models.CharField(max_length=255, choices=[('kfet/base_col_1.html', 'Une colonne : centrée sur la page'), ('kfet/base_col_2.html', 'Deux colonnes : fixe à gauche, contenu à droite'), ('kfet/base_col_mult.html', 'Contenu scindé sur plusieurs colonnes')], help_text='Comment cette page devrait être affichée ?', verbose_name='Template', default='kfet/base_col_mult.html')), diff --git a/kfet/forms.py b/kfet/forms.py index 963e4254..26774b1c 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -296,17 +296,17 @@ class KPsulAccountForm(forms.ModelForm): class KPsulCheckoutForm(forms.Form): checkout = forms.ModelChoiceField( - queryset=( - Checkout.objects - .filter( - is_protected=False, - valid_from__lte=timezone.now(), - valid_to__gte=timezone.now(), - ) - ), + queryset=None, 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): article = forms.ModelChoiceField( diff --git a/kfet/migrations/0063_promo.py b/kfet/migrations/0063_promo.py new file mode 100644 index 00000000..3fac5a8a --- /dev/null +++ b/kfet/migrations/0063_promo.py @@ -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), + ), + ] diff --git a/kfet/models.py b/kfet/models.py index b1e351d5..ecac77ca 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- from django.db import models -from django.core.urlresolvers import reverse from django.core.validators import RegexValidator from django.contrib.auth.models import User from gestioncof.models import CofProfile +from django.urls import reverse from django.utils.six.moves import reduce from django.utils import timezone from django.utils.encoding import python_2_unicode_compatible @@ -341,6 +341,13 @@ class AccountNegative(models.Model): 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): created_by = models.ForeignKey( Account, on_delete = models.PROTECT, @@ -353,6 +360,8 @@ class Checkout(models.Model): default = 0) is_protected = models.BooleanField(default = False) + objects = CheckoutQuerySet.as_manager() + def get_absolute_url(self): return reverse('kfet.checkout.read', kwargs={'pk': self.pk}) @@ -362,6 +371,22 @@ class Checkout(models.Model): def __str__(self): 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): from_checkout = models.ForeignKey( Checkout, on_delete = models.PROTECT, diff --git a/kfet/open/open.py b/kfet/open/open.py index 7fd90c21..82d6217a 100644 --- a/kfet/open/open.py +++ b/kfet/open/open.py @@ -85,7 +85,7 @@ class OpenKfet(CachedMixin, object): 'admin_status': self.admin_status(status), 'force_close': self.force_close, } - return base, {**base, **restrict} + return base, dict(base, **restrict) def export(self, user): """Export internal state for a given user. diff --git a/kfet/templates/kfet/account_create.html b/kfet/templates/kfet/account_create.html index 59fc1d56..b09713df 100644 --- a/kfet/templates/kfet/account_create.html +++ b/kfet/templates/kfet/account_create.html @@ -5,7 +5,7 @@ {% block header-title %}Création d'un compte{% endblock %} {% block extra_head %} - + {% endblock %} {% block main %} diff --git a/kfet/templates/kfet/base_nav.html b/kfet/templates/kfet/base_nav.html index dda6c1ef..f4c07e05 100644 --- a/kfet/templates/kfet/base_nav.html +++ b/kfet/templates/kfet/base_nav.html @@ -1,10 +1,12 @@ {% load i18n static %} {% load wagtailcore_tags %} +{% slugurl "kfet" as kfet_home_url %} +