forked from DGNum/gestioCOF
Merge branch 'master' into aureplop/kfet-sticky_thead
This commit is contained in:
commit
fba105fdd1
78 changed files with 3716 additions and 371 deletions
|
@ -1,5 +1,7 @@
|
|||
# GestioCOF
|
||||
|
||||
![build_status](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/build.svg)
|
||||
|
||||
## Installation
|
||||
|
||||
### Vagrant
|
||||
|
|
15
bda/admin.py
15
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
|
||||
|
|
|
@ -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...'})
|
|
@ -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',
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
|
|
109
bda/models.py
109
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
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
</tr></thead>
|
||||
<tbody class="bda_formset_content">
|
||||
{% 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 %}
|
||||
{% if field.name != "DELETE" and field.name != "priority" %}
|
||||
<td class="bda-field-{{ field.name }}">
|
||||
|
|
|
@ -27,6 +27,14 @@ var django = {
|
|||
var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-');
|
||||
$(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++;
|
||||
$('#id_' + type + '-TOTAL_FORMS').val(total);
|
||||
$(selector).after(newElement);
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<h4 class="bda-prix">Total à payer : {{ total|floatformat }}€</h4>
|
||||
<br/>
|
||||
<p>Ne manque pas un spectacle avec le
|
||||
<a href="{% url "gestioncof.views.calendar" %}">calendrier
|
||||
<a href="{% url "calendar" %}">calendrier
|
||||
automatique !</a></p>
|
||||
{% else %}
|
||||
<h3>Vous n'avez aucune place :(</h3>
|
||||
|
|
0
bda/tests/__init__.py
Normal file
0
bda/tests/__init__.py
Normal file
100
bda/tests/test_models.py
Normal file
100
bda/tests/test_models.py
Normal file
|
@ -0,0 +1,100 @@
|
|||
from datetime import timedelta
|
||||
from unittest import mock
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core import mail
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
|
||||
from bda.models import (
|
||||
Attribution, Participant, Salle, Spectacle, SpectacleRevente, Tirage,
|
||||
)
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class SpectacleReventeTests(TestCase):
|
||||
fixtures = ['gestioncof/management/data/custommail.json']
|
||||
|
||||
def setUp(self):
|
||||
now = timezone.now()
|
||||
|
||||
self.t = Tirage.objects.create(
|
||||
title='Tirage',
|
||||
ouverture=now - timedelta(days=7),
|
||||
fermeture=now - timedelta(days=3),
|
||||
active=True,
|
||||
)
|
||||
self.s = Spectacle.objects.create(
|
||||
title='Spectacle',
|
||||
date=now + timedelta(days=20),
|
||||
location=Salle.objects.create(name='Salle', address='Address'),
|
||||
price=10.5,
|
||||
slots=5,
|
||||
tirage=self.t,
|
||||
listing=False,
|
||||
)
|
||||
|
||||
self.seller = Participant.objects.create(
|
||||
user=User.objects.create(
|
||||
username='seller', email='seller@mail.net'),
|
||||
tirage=self.t,
|
||||
)
|
||||
self.p1 = Participant.objects.create(
|
||||
user=User.objects.create(username='part1', email='part1@mail.net'),
|
||||
tirage=self.t,
|
||||
)
|
||||
self.p2 = Participant.objects.create(
|
||||
user=User.objects.create(username='part2', email='part2@mail.net'),
|
||||
tirage=self.t,
|
||||
)
|
||||
self.p3 = Participant.objects.create(
|
||||
user=User.objects.create(username='part3', email='part3@mail.net'),
|
||||
tirage=self.t,
|
||||
)
|
||||
|
||||
self.attr = Attribution.objects.create(
|
||||
participant=self.seller,
|
||||
spectacle=self.s,
|
||||
)
|
||||
|
||||
self.rev = SpectacleRevente.objects.create(
|
||||
attribution=self.attr,
|
||||
seller=self.seller,
|
||||
)
|
||||
|
||||
def test_tirage(self):
|
||||
revente = self.rev
|
||||
|
||||
wanted_by = [self.p1, self.p2, self.p3]
|
||||
revente.answered_mail = wanted_by
|
||||
|
||||
with mock.patch('bda.models.random.choice') as mc:
|
||||
# Set winner to self.p1.
|
||||
mc.return_value = self.p1
|
||||
|
||||
revente.tirage()
|
||||
|
||||
# Call to random.choice used participants in wanted_by.
|
||||
mc_args, _ = mc.call_args
|
||||
|
||||
self.assertEqual(set(mc_args[0]), set(wanted_by))
|
||||
|
||||
self.assertEqual(revente.soldTo, self.p1)
|
||||
self.assertTrue(revente.tirage_done)
|
||||
|
||||
mails = {m.to[0]: m for m in mail.outbox}
|
||||
|
||||
self.assertEqual(len(mails), 4)
|
||||
|
||||
m_seller = mails['seller@mail.net']
|
||||
self.assertListEqual(m_seller.to, ['seller@mail.net'])
|
||||
self.assertListEqual(m_seller.reply_to, ['part1@mail.net'])
|
||||
|
||||
m_winner = mails['part1@mail.net']
|
||||
self.assertListEqual(m_winner.to, ['part1@mail.net'])
|
||||
|
||||
self.assertCountEqual(
|
||||
[mails['part2@mail.net'].to, mails['part3@mail.net'].to],
|
||||
[['part2@mail.net'], ['part3@mail.net']],
|
||||
)
|
|
@ -4,7 +4,7 @@ from django.contrib.auth.models import User
|
|||
from django.test import TestCase, Client
|
||||
from django.utils import timezone
|
||||
|
||||
from .models import Tirage, Spectacle, Salle, CategorieSpectacle
|
||||
from bda.models import Tirage, Spectacle, Salle, CategorieSpectacle
|
||||
|
||||
|
||||
class TestBdAViews(TestCase):
|
|
@ -32,6 +32,12 @@ urlpatterns = [
|
|||
url(r'^spectacles/unpaid/(?P<tirage_id>\d+)$',
|
||||
views.unpaid,
|
||||
name="bda-unpaid"),
|
||||
url(r'^spectacles/autocomplete$',
|
||||
views.spectacle_autocomplete,
|
||||
name="bda-spectacle-autocomplete"),
|
||||
url(r'^participants/autocomplete$',
|
||||
views.participant_autocomplete,
|
||||
name="bda-participant-autocomplete"),
|
||||
url(r'^liste-revente/(?P<tirage_id>\d+)$',
|
||||
views.list_revente,
|
||||
name="bda-liste-revente"),
|
||||
|
|
25
bda/views.py
25
bda/views.py
|
@ -33,6 +33,8 @@ from bda.forms import (
|
|||
InscriptionInlineFormSet,
|
||||
)
|
||||
|
||||
from utils.views.autocomplete import Select2QuerySetView
|
||||
|
||||
|
||||
@cof_required
|
||||
def etat_places(request, tirage_id):
|
||||
|
@ -813,3 +815,26 @@ def catalogue(request, request_type):
|
|||
return JsonResponse(data_return, safe=False)
|
||||
# Si la requête n'est pas de la forme attendue, on quitte avec une erreur
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
##
|
||||
# Autocomplete views
|
||||
#
|
||||
# https://django-autocomplete-light.readthedocs.io/en/master/tutorial.html#create-an-autocomplete-view
|
||||
##
|
||||
|
||||
|
||||
class ParticipantAutocomplete(Select2QuerySetView):
|
||||
model = Participant
|
||||
search_fields = ('user__username', 'user__first_name', 'user__last_name')
|
||||
|
||||
|
||||
participant_autocomplete = buro_required(ParticipantAutocomplete.as_view())
|
||||
|
||||
|
||||
class SpectacleAutocomplete(Select2QuerySetView):
|
||||
model = Spectacle
|
||||
search_fields = ('title',)
|
||||
|
||||
|
||||
spectacle_autocomplete = buro_required(SpectacleAutocomplete.as_view())
|
||||
|
|
|
@ -7,6 +7,7 @@ the local development server should be here.
|
|||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
try:
|
||||
from . import secret
|
||||
|
@ -53,21 +54,30 @@ BASE_DIR = os.path.dirname(
|
|||
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
)
|
||||
|
||||
TESTING = sys.argv[1] == 'test'
|
||||
|
||||
|
||||
# Application definition
|
||||
INSTALLED_APPS = [
|
||||
'shared',
|
||||
|
||||
'gestioncof',
|
||||
|
||||
# Must be before 'django.contrib.admin'.
|
||||
# https://django-autocomplete-light.readthedocs.io/en/master/install.html
|
||||
'dal',
|
||||
'dal_select2',
|
||||
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.sites',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'grappelli',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.admindocs',
|
||||
|
||||
'bda',
|
||||
'autocomplete_light',
|
||||
'captcha',
|
||||
'django_cas_ng',
|
||||
'bootstrapform',
|
||||
|
@ -96,7 +106,7 @@ INSTALLED_APPS = [
|
|||
'kfet.cms',
|
||||
]
|
||||
|
||||
MIDDLEWARE_CLASSES = [
|
||||
MIDDLEWARE = [
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
|
@ -124,9 +134,9 @@ TEMPLATES = [
|
|||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
'django.core.context_processors.i18n',
|
||||
'django.core.context_processors.media',
|
||||
'django.core.context_processors.static',
|
||||
'django.template.context_processors.i18n',
|
||||
'django.template.context_processors.media',
|
||||
'django.template.context_processors.static',
|
||||
'wagtailmenus.context_processors.wagtailmenus',
|
||||
'djconfig.context_processors.config',
|
||||
'gestioncof.shared.context_processor',
|
||||
|
|
|
@ -4,13 +4,18 @@ The settings that are not listed here are imported from .common
|
|||
"""
|
||||
|
||||
from .common import * # NOQA
|
||||
from .common import INSTALLED_APPS, MIDDLEWARE_CLASSES
|
||||
from .common import INSTALLED_APPS, MIDDLEWARE, TESTING
|
||||
|
||||
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
|
||||
DEBUG = True
|
||||
|
||||
if TESTING:
|
||||
PASSWORD_HASHERS = [
|
||||
'django.contrib.auth.hashers.MD5PasswordHasher',
|
||||
]
|
||||
|
||||
|
||||
# ---
|
||||
# Apache static/media config
|
||||
|
@ -36,11 +41,13 @@ def show_toolbar(request):
|
|||
"""
|
||||
return DEBUG
|
||||
|
||||
INSTALLED_APPS += ["debug_toolbar", "debug_panel"]
|
||||
MIDDLEWARE_CLASSES = (
|
||||
["debug_panel.middleware.DebugPanelMiddleware"]
|
||||
+ MIDDLEWARE_CLASSES
|
||||
)
|
||||
DEBUG_TOOLBAR_CONFIG = {
|
||||
'SHOW_TOOLBAR_CALLBACK': show_toolbar,
|
||||
}
|
||||
if not TESTING:
|
||||
INSTALLED_APPS += ["debug_toolbar", "debug_panel"]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"debug_panel.middleware.DebugPanelMiddleware"
|
||||
] + MIDDLEWARE
|
||||
|
||||
DEBUG_TOOLBAR_CONFIG = {
|
||||
'SHOW_TOOLBAR_CALLBACK': show_toolbar,
|
||||
}
|
||||
|
|
31
cof/urls.py
31
cof/urls.py
|
@ -4,8 +4,6 @@
|
|||
Fichier principal de configuration des urls du projet GestioCOF
|
||||
"""
|
||||
|
||||
import autocomplete_light
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include, url
|
||||
from django.conf.urls.static import static
|
||||
|
@ -24,7 +22,6 @@ from gestioncof.urls import export_patterns, petitcours_patterns, \
|
|||
clubs_patterns
|
||||
from gestioncof.autocomplete import autocomplete
|
||||
|
||||
autocomplete_light.autodiscover()
|
||||
admin.autodiscover()
|
||||
|
||||
urlpatterns = [
|
||||
|
@ -49,18 +46,22 @@ urlpatterns = [
|
|||
name="cof-denied"),
|
||||
url(r'^cas/login$', django_cas_views.login, name="cas_login_view"),
|
||||
url(r'^cas/logout$', django_cas_views.logout),
|
||||
url(r'^outsider/login$', gestioncof_views.login_ext),
|
||||
url(r'^outsider/login$', gestioncof_views.login_ext,
|
||||
name="ext_login_view"),
|
||||
url(r'^outsider/logout$', django_views.logout, {'next_page': 'home'}),
|
||||
url(r'^login$', gestioncof_views.login, name="cof-login"),
|
||||
url(r'^logout$', gestioncof_views.logout, name="cof-logout"),
|
||||
# Infos persos
|
||||
url(r'^profile$', gestioncof_views.profile),
|
||||
url(r'^outsider/password-change$', django_views.password_change),
|
||||
url(r'^profile$', gestioncof_views.profile,
|
||||
name='profile'),
|
||||
url(r'^outsider/password-change$', django_views.password_change,
|
||||
name='password_change'),
|
||||
url(r'^outsider/password-change-done$',
|
||||
django_views.password_change_done,
|
||||
name='password_change_done'),
|
||||
# Inscription d'un nouveau membre
|
||||
url(r'^registration$', gestioncof_views.registration),
|
||||
url(r'^registration$', gestioncof_views.registration,
|
||||
name='registration'),
|
||||
url(r'^registration/clipper/(?P<login_clipper>[\w-]+)/'
|
||||
r'(?P<fullname>.*)$',
|
||||
gestioncof_views.registration_form2, name="clipper-registration"),
|
||||
|
@ -70,7 +71,8 @@ urlpatterns = [
|
|||
name="empty-registration"),
|
||||
# Autocompletion
|
||||
url(r'^autocomplete/registration$', autocomplete),
|
||||
url(r'^autocomplete/', include('autocomplete_light.urls')),
|
||||
url(r'^user/autocomplete$', gestioncof_views.user_autocomplete,
|
||||
name='cof-user-autocomplete'),
|
||||
# Interface admin
|
||||
url(r'^admin/logout/', gestioncof_views.logout),
|
||||
url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
|
||||
|
@ -78,18 +80,21 @@ urlpatterns = [
|
|||
csv_views.admin_list_export,
|
||||
{'fields': ['username', ]}),
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
url(r'^grappelli/', include('grappelli.urls')),
|
||||
# Liens utiles du COF et du BdA
|
||||
url(r'^utile_cof$', gestioncof_views.utile_cof),
|
||||
url(r'^utile_bda$', gestioncof_views.utile_bda),
|
||||
url(r'^utile_cof$', gestioncof_views.utile_cof,
|
||||
name='utile_cof'),
|
||||
url(r'^utile_bda$', gestioncof_views.utile_bda,
|
||||
name='utile_bda'),
|
||||
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'^k-fet/', include('kfet.urls')),
|
||||
url(r'^cms/', include(wagtailadmin_urls)),
|
||||
url(r'^documents/', include(wagtaildocs_urls)),
|
||||
# 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:
|
||||
|
|
|
@ -13,7 +13,7 @@ from django.core.urlresolvers import reverse
|
|||
from django.utils.safestring import mark_safe
|
||||
from django.db.models import Q
|
||||
|
||||
import autocomplete_light
|
||||
from dal.autocomplete import ModelSelect2
|
||||
|
||||
|
||||
def add_link_field(target_model='', field='', link_text=str,
|
||||
|
@ -217,8 +217,16 @@ def user_str(self):
|
|||
User.__str__ = user_str
|
||||
|
||||
|
||||
class EventRegistrationAdminForm(forms.ModelForm):
|
||||
class Meta:
|
||||
widgets = {
|
||||
'user': ModelSelect2(url='cof-user-autocomplete'),
|
||||
}
|
||||
|
||||
|
||||
class EventRegistrationAdmin(admin.ModelAdmin):
|
||||
form = autocomplete_light.modelform_factory(EventRegistration, exclude=[])
|
||||
form = EventRegistrationAdminForm
|
||||
|
||||
list_display = ('__str__', 'event', 'user', 'paid')
|
||||
list_filter = ('paid',)
|
||||
search_fields = ('user__username', 'user__first_name', 'user__last_name',
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import autocomplete_light
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
autocomplete_light.register(
|
||||
User, search_fields=('username', 'first_name', 'last_name'),
|
||||
attrs={'placeholder': 'membre...'}
|
||||
)
|
|
@ -351,10 +351,12 @@ EventFormset = formset_factory(AdminEventForm, BaseEventRegistrationFormset)
|
|||
class CalendarForm(forms.ModelForm):
|
||||
subscribe_to_events = forms.BooleanField(
|
||||
initial=True,
|
||||
label="Événements du COF")
|
||||
label="Événements du COF",
|
||||
required=False)
|
||||
subscribe_to_my_shows = forms.BooleanField(
|
||||
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(
|
||||
label="Spectacles supplémentaires",
|
||||
queryset=Spectacle.objects.filter(tirage__active=True),
|
||||
|
|
|
@ -63,8 +63,9 @@ class Command(BaseCommand):
|
|||
except CustomMail.DoesNotExist:
|
||||
mail = CustomMail.objects.create(**fields)
|
||||
status['synced'] += 1
|
||||
self.stdout.write(
|
||||
'SYNCED {:s}'.format(fields['shortname']))
|
||||
if options['verbosity']:
|
||||
self.stdout.write(
|
||||
'SYNCED {:s}'.format(fields['shortname']))
|
||||
assoc['mails'][obj['pk']] = mail
|
||||
|
||||
# Variables
|
||||
|
@ -79,8 +80,9 @@ class Command(BaseCommand):
|
|||
except Variable.DoesNotExist:
|
||||
Variable.objects.create(**fields)
|
||||
|
||||
# C'est agréable d'avoir le résultat affiché
|
||||
self.stdout.write(
|
||||
'{synced:d} mails synchronized {unchanged:d} unchanged'
|
||||
.format(**status)
|
||||
)
|
||||
if options['verbosity']:
|
||||
# C'est agréable d'avoir le résultat affiché
|
||||
self.stdout.write(
|
||||
'{synced:d} mails synchronized {unchanged:d} unchanged'
|
||||
.format(**status)
|
||||
)
|
||||
|
|
|
@ -48,7 +48,7 @@ class Migration(migrations.Migration):
|
|||
('is_buro', models.BooleanField(default=False, verbose_name=b'Membre du Bur\xc3\xb4')),
|
||||
('petits_cours_accept', models.BooleanField(default=False, verbose_name=b'Recevoir des petits cours')),
|
||||
('petits_cours_remarques', models.TextField(default=b'', verbose_name='Remarques et pr\xe9cisions pour les petits cours', blank=True)),
|
||||
('user', models.OneToOneField(related_name='profile', to=settings.AUTH_USER_MODEL)),
|
||||
('user', models.OneToOneField(related_name='profile', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Profil COF',
|
||||
|
@ -91,7 +91,7 @@ class Migration(migrations.Migration):
|
|||
('name', models.CharField(max_length=200, verbose_name=b'Champ')),
|
||||
('fieldtype', models.CharField(default=b'text', max_length=10, verbose_name=b'Type', choices=[(b'text', 'Texte long'), (b'char', 'Texte court')])),
|
||||
('default', models.TextField(verbose_name=b'Valeur par d\xc3\xa9faut', blank=True)),
|
||||
('event', models.ForeignKey(related_name='commentfields', to='gestioncof.Event')),
|
||||
('event', models.ForeignKey(related_name='commentfields', to='gestioncof.Event', on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Champ',
|
||||
|
@ -102,7 +102,7 @@ class Migration(migrations.Migration):
|
|||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('content', models.TextField(null=True, verbose_name=b'Contenu', blank=True)),
|
||||
('commentfield', models.ForeignKey(related_name='values', to='gestioncof.EventCommentField')),
|
||||
('commentfield', models.ForeignKey(related_name='values', to='gestioncof.EventCommentField', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
|
@ -111,7 +111,7 @@ class Migration(migrations.Migration):
|
|||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(max_length=200, verbose_name=b'Option')),
|
||||
('multi_choices', models.BooleanField(default=False, verbose_name=b'Choix multiples')),
|
||||
('event', models.ForeignKey(related_name='options', to='gestioncof.Event')),
|
||||
('event', models.ForeignKey(related_name='options', to='gestioncof.Event', on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Option',
|
||||
|
@ -122,7 +122,7 @@ class Migration(migrations.Migration):
|
|||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('value', models.CharField(max_length=200, verbose_name=b'Valeur')),
|
||||
('event_option', models.ForeignKey(related_name='choices', to='gestioncof.EventOption')),
|
||||
('event_option', models.ForeignKey(related_name='choices', to='gestioncof.EventOption', on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Choix',
|
||||
|
@ -133,10 +133,10 @@ class Migration(migrations.Migration):
|
|||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('paid', models.BooleanField(default=False, verbose_name=b'A pay\xc3\xa9')),
|
||||
('event', models.ForeignKey(to='gestioncof.Event')),
|
||||
('event', models.ForeignKey(to='gestioncof.Event', on_delete=models.CASCADE)),
|
||||
('filledcomments', models.ManyToManyField(to='gestioncof.EventCommentField', through='gestioncof.EventCommentValue')),
|
||||
('options', models.ManyToManyField(to='gestioncof.EventOptionChoice')),
|
||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
|
||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Inscription',
|
||||
|
@ -240,7 +240,7 @@ class Migration(migrations.Migration):
|
|||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('question', models.CharField(max_length=200, verbose_name=b'Question')),
|
||||
('multi_answers', models.BooleanField(default=False, verbose_name=b'Choix multiples')),
|
||||
('survey', models.ForeignKey(related_name='questions', to='gestioncof.Survey')),
|
||||
('survey', models.ForeignKey(related_name='questions', to='gestioncof.Survey', on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Question',
|
||||
|
@ -251,7 +251,7 @@ class Migration(migrations.Migration):
|
|||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('answer', models.CharField(max_length=200, verbose_name=b'R\xc3\xa9ponse')),
|
||||
('survey_question', models.ForeignKey(related_name='answers', to='gestioncof.SurveyQuestion')),
|
||||
('survey_question', models.ForeignKey(related_name='answers', to='gestioncof.SurveyQuestion', on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'R\xe9ponse',
|
||||
|
@ -265,12 +265,12 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name='surveyanswer',
|
||||
name='survey',
|
||||
field=models.ForeignKey(to='gestioncof.Survey'),
|
||||
field=models.ForeignKey(to='gestioncof.Survey', on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='surveyanswer',
|
||||
name='user',
|
||||
field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
|
||||
field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='petitcoursdemande',
|
||||
|
@ -280,47 +280,47 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name='petitcoursdemande',
|
||||
name='traitee_par',
|
||||
field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True),
|
||||
field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='petitcoursattributioncounter',
|
||||
name='matiere',
|
||||
field=models.ForeignKey(verbose_name='Matiere', to='gestioncof.PetitCoursSubject'),
|
||||
field=models.ForeignKey(verbose_name='Matiere', to='gestioncof.PetitCoursSubject', on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='petitcoursattributioncounter',
|
||||
name='user',
|
||||
field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
|
||||
field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='petitcoursattribution',
|
||||
name='demande',
|
||||
field=models.ForeignKey(verbose_name='Demande', to='gestioncof.PetitCoursDemande'),
|
||||
field=models.ForeignKey(verbose_name='Demande', to='gestioncof.PetitCoursDemande', on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='petitcoursattribution',
|
||||
name='matiere',
|
||||
field=models.ForeignKey(verbose_name='Mati\xe8re', to='gestioncof.PetitCoursSubject'),
|
||||
field=models.ForeignKey(verbose_name='Mati\xe8re', to='gestioncof.PetitCoursSubject', on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='petitcoursattribution',
|
||||
name='user',
|
||||
field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
|
||||
field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='petitcoursability',
|
||||
name='matiere',
|
||||
field=models.ForeignKey(verbose_name='Mati\xe8re', to='gestioncof.PetitCoursSubject'),
|
||||
field=models.ForeignKey(verbose_name='Mati\xe8re', to='gestioncof.PetitCoursSubject', on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='petitcoursability',
|
||||
name='user',
|
||||
field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
|
||||
field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='eventcommentvalue',
|
||||
name='registration',
|
||||
field=models.ForeignKey(related_name='comments', to='gestioncof.EventRegistration'),
|
||||
field=models.ForeignKey(related_name='comments', to='gestioncof.EventRegistration', on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='surveyanswer',
|
||||
|
|
|
@ -23,7 +23,8 @@ class Migration(migrations.Migration):
|
|||
('subscribe_to_events', models.BooleanField(default=True)),
|
||||
('subscribe_to_my_shows', models.BooleanField(default=True)),
|
||||
('other_shows', models.ManyToManyField(to='bda.Spectacle')),
|
||||
('user', models.OneToOneField(to=settings.AUTH_USER_MODEL)),
|
||||
('user', models.OneToOneField(to=settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
|
|
|
@ -49,7 +49,10 @@ class CofProfile(models.Model):
|
|||
(COTIZ_GRATIS, _("Gratuit")),
|
||||
)
|
||||
|
||||
user = models.OneToOneField(User, related_name="profile")
|
||||
user = models.OneToOneField(
|
||||
User, on_delete=models.CASCADE,
|
||||
related_name="profile",
|
||||
)
|
||||
login_clipper = models.CharField(
|
||||
"Login clipper", max_length=32, blank=True
|
||||
)
|
||||
|
@ -130,7 +133,10 @@ class Event(models.Model):
|
|||
|
||||
|
||||
class EventCommentField(models.Model):
|
||||
event = models.ForeignKey(Event, related_name="commentfields")
|
||||
event = models.ForeignKey(
|
||||
Event, on_delete=models.CASCADE,
|
||||
related_name="commentfields",
|
||||
)
|
||||
name = models.CharField("Champ", max_length=200)
|
||||
fieldtype = models.CharField("Type", max_length=10,
|
||||
choices=TYPE_COMMENT_FIELD, default="text")
|
||||
|
@ -144,9 +150,14 @@ class EventCommentField(models.Model):
|
|||
|
||||
|
||||
class EventCommentValue(models.Model):
|
||||
commentfield = models.ForeignKey(EventCommentField, related_name="values")
|
||||
registration = models.ForeignKey("EventRegistration",
|
||||
related_name="comments")
|
||||
commentfield = models.ForeignKey(
|
||||
EventCommentField, on_delete=models.CASCADE,
|
||||
related_name="values",
|
||||
)
|
||||
registration = models.ForeignKey(
|
||||
"EventRegistration", on_delete=models.CASCADE,
|
||||
related_name="comments",
|
||||
)
|
||||
content = models.TextField("Contenu", blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
|
@ -154,7 +165,10 @@ class EventCommentValue(models.Model):
|
|||
|
||||
|
||||
class EventOption(models.Model):
|
||||
event = models.ForeignKey(Event, related_name="options")
|
||||
event = models.ForeignKey(
|
||||
Event, on_delete=models.CASCADE,
|
||||
related_name="options",
|
||||
)
|
||||
name = models.CharField("Option", max_length=200)
|
||||
multi_choices = models.BooleanField("Choix multiples", default=False)
|
||||
|
||||
|
@ -166,7 +180,10 @@ class EventOption(models.Model):
|
|||
|
||||
|
||||
class EventOptionChoice(models.Model):
|
||||
event_option = models.ForeignKey(EventOption, related_name="choices")
|
||||
event_option = models.ForeignKey(
|
||||
EventOption, on_delete=models.CASCADE,
|
||||
related_name="choices",
|
||||
)
|
||||
value = models.CharField("Valeur", max_length=200)
|
||||
|
||||
class Meta:
|
||||
|
@ -178,8 +195,8 @@ class EventOptionChoice(models.Model):
|
|||
|
||||
|
||||
class EventRegistration(models.Model):
|
||||
user = models.ForeignKey(User)
|
||||
event = models.ForeignKey(Event)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
event = models.ForeignKey(Event, on_delete=models.CASCADE)
|
||||
options = models.ManyToManyField(EventOptionChoice)
|
||||
filledcomments = models.ManyToManyField(EventCommentField,
|
||||
through=EventCommentValue)
|
||||
|
@ -207,7 +224,10 @@ class Survey(models.Model):
|
|||
|
||||
|
||||
class SurveyQuestion(models.Model):
|
||||
survey = models.ForeignKey(Survey, related_name="questions")
|
||||
survey = models.ForeignKey(
|
||||
Survey, on_delete=models.CASCADE,
|
||||
related_name="questions",
|
||||
)
|
||||
question = models.CharField("Question", max_length=200)
|
||||
multi_answers = models.BooleanField("Choix multiples", default=False)
|
||||
|
||||
|
@ -219,7 +239,10 @@ class SurveyQuestion(models.Model):
|
|||
|
||||
|
||||
class SurveyQuestionAnswer(models.Model):
|
||||
survey_question = models.ForeignKey(SurveyQuestion, related_name="answers")
|
||||
survey_question = models.ForeignKey(
|
||||
SurveyQuestion, on_delete=models.CASCADE,
|
||||
related_name="answers",
|
||||
)
|
||||
answer = models.CharField("Réponse", max_length=200)
|
||||
|
||||
class Meta:
|
||||
|
@ -230,8 +253,8 @@ class SurveyQuestionAnswer(models.Model):
|
|||
|
||||
|
||||
class SurveyAnswer(models.Model):
|
||||
user = models.ForeignKey(User)
|
||||
survey = models.ForeignKey(Survey)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
survey = models.ForeignKey(Survey, on_delete=models.CASCADE)
|
||||
answers = models.ManyToManyField(SurveyQuestionAnswer,
|
||||
related_name="selected_by")
|
||||
|
||||
|
@ -247,7 +270,7 @@ class SurveyAnswer(models.Model):
|
|||
|
||||
class CalendarSubscription(models.Model):
|
||||
token = models.UUIDField()
|
||||
user = models.OneToOneField(User)
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
other_shows = models.ManyToManyField(Spectacle)
|
||||
subscribe_to_events = models.BooleanField(default=True)
|
||||
subscribe_to_my_shows = models.BooleanField(default=True)
|
||||
|
|
|
@ -35,8 +35,11 @@ class PetitCoursSubject(models.Model):
|
|||
|
||||
|
||||
class PetitCoursAbility(models.Model):
|
||||
user = models.ForeignKey(User)
|
||||
matiere = models.ForeignKey(PetitCoursSubject, verbose_name=_("Matière"))
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
matiere = models.ForeignKey(
|
||||
PetitCoursSubject, on_delete=models.CASCADE,
|
||||
verbose_name=_("Matière"),
|
||||
)
|
||||
niveau = models.CharField(_("Niveau"),
|
||||
choices=LEVELS_CHOICES,
|
||||
max_length=choices_length(LEVELS_CHOICES))
|
||||
|
@ -84,7 +87,10 @@ class PetitCoursDemande(models.Model):
|
|||
remarques = models.TextField(_("Remarques et précisions"), blank=True)
|
||||
|
||||
traitee = models.BooleanField(_("Traitée"), default=False)
|
||||
traitee_par = models.ForeignKey(User, blank=True, null=True)
|
||||
traitee_par = models.ForeignKey(
|
||||
User, on_delete=models.CASCADE,
|
||||
blank=True, null=True,
|
||||
)
|
||||
processed = models.DateTimeField(_("Date de traitement"),
|
||||
blank=True, null=True)
|
||||
created = models.DateTimeField(_("Date de création"), auto_now_add=True)
|
||||
|
@ -126,9 +132,15 @@ class PetitCoursDemande(models.Model):
|
|||
|
||||
|
||||
class PetitCoursAttribution(models.Model):
|
||||
user = models.ForeignKey(User)
|
||||
demande = models.ForeignKey(PetitCoursDemande, verbose_name=_("Demande"))
|
||||
matiere = models.ForeignKey(PetitCoursSubject, verbose_name=_("Matière"))
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
demande = models.ForeignKey(
|
||||
PetitCoursDemande, on_delete=models.CASCADE,
|
||||
verbose_name=_("Demande"),
|
||||
)
|
||||
matiere = models.ForeignKey(
|
||||
PetitCoursSubject, on_delete=models.CASCADE,
|
||||
verbose_name=_("Matière"),
|
||||
)
|
||||
date = models.DateTimeField(_("Date d'attribution"), auto_now_add=True)
|
||||
rank = models.IntegerField("Rang dans l'email")
|
||||
selected = models.BooleanField(_("Sélectionné par le demandeur"),
|
||||
|
@ -145,8 +157,11 @@ class PetitCoursAttribution(models.Model):
|
|||
|
||||
|
||||
class PetitCoursAttributionCounter(models.Model):
|
||||
user = models.ForeignKey(User)
|
||||
matiere = models.ForeignKey(PetitCoursSubject, verbose_name=_("Matiere"))
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
matiere = models.ForeignKey(
|
||||
PetitCoursSubject, on_delete=models.CASCADE,
|
||||
verbose_name=_("Matiere"),
|
||||
)
|
||||
count = models.IntegerField("Nombre d'envois", default=0)
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from custommail.shortcuts import render_custom_mail
|
||||
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
|
@ -13,6 +12,7 @@ from django.conf import settings
|
|||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from gestioncof.models import CofProfile
|
||||
from gestioncof.petits_cours_models import (
|
||||
|
@ -287,7 +287,7 @@ def _traitement_post(request, demande):
|
|||
attrib.save()
|
||||
demande.traitee = True
|
||||
demande.traitee_par = request.user
|
||||
demande.processed = datetime.now()
|
||||
demande.processed = timezone.now()
|
||||
demande.save()
|
||||
return render(request,
|
||||
"gestioncof/traitement_demande_petit_cours_success.html",
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block extrahead %}
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" type="text/javascript"></script>
|
||||
{% include 'autocomplete_light/static.html' %}
|
||||
{% endblock %}
|
|
@ -1,78 +0,0 @@
|
|||
{% extends "admin/base_site.html" %}
|
||||
|
||||
<!-- LOADING -->
|
||||
{% load i18n grp_tags log %}
|
||||
|
||||
<!-- JAVASCRIPTS -->
|
||||
{% block javascripts %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
<!-- COLTYPE/BODYCLASS-- >
|
||||
{% block bodyclass %}dashboard{% endblock %}
|
||||
{% block content-class %}content-grid{% endblock %}
|
||||
|
||||
<!-- BREADCRUMBS -->
|
||||
{% block breadcrumbs %}
|
||||
<ul class="grp-horizontal-list">
|
||||
<li>{% trans "Home" %}</li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
{% block content_title %}
|
||||
{% if title %}
|
||||
<header><h1>{{ title }}</h1></header>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
<!-- CONTENT -->
|
||||
{% block content %}
|
||||
<div class="g-d-c">
|
||||
<div class="g-d-12 g-d-f">
|
||||
|
||||
{% for app in app_list %}
|
||||
<div class="grp-module" id="app_{{ app.name|lower }}">
|
||||
<h2><a href="{{ app.app_url }}" class="grp-section">{% trans app.name %}</a></h2>
|
||||
{% for model in app.models %}
|
||||
<div class="grp-row">
|
||||
{% if model.perms.change %}<a href="{{ model.admin_url }}"><strong>{{ model.name }}</strong></a>{% else %}<span><strong>{{ model.name }}</strong></span>{% endif %}
|
||||
{% if model.perms.add or model.perms.change %}
|
||||
<ul class="grp-actions">
|
||||
{% if model.perms.add %}<li class="grp-add-link"><a href="{{ model.admin_url }}add/">{% trans 'Add' %}</a></li>{% endif %}
|
||||
{% if model.perms.change %}<li class="grp-change-link"><a href="{{ model.admin_url }}">{% trans 'Change' %}</a></li>{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<p>{% trans "You don´t have permission to edit anything." %}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="g-d-6 g-d-l">
|
||||
<div class="grp-module" id="grp-recent-actions-module">
|
||||
<h2>{% trans 'Recent Actions' %}</h2>
|
||||
<div class="grp-module">
|
||||
<h3>{% trans 'My Actions' %}</h3>
|
||||
{% get_admin_log 20 as admin_log for_user user %}
|
||||
{% if not admin_log %}
|
||||
<p>{% trans 'None available' %}</p>
|
||||
{% else %}
|
||||
<ul class="grp-listing-small">
|
||||
{% for entry in admin_log %}
|
||||
<li class="grp-row{% if entry.is_addition %} grp-add-link{% endif %}{% if entry.is_change %} grp-change-link{% endif %}{% if entry.is_deletion %} grp-delete-link{% endif %}">
|
||||
{% if entry.is_deletion %}
|
||||
<span>{{ entry.object_repr }}</span>
|
||||
{% else %}
|
||||
<a href="{{ entry.get_admin_url }}">{{ entry.object_repr }}</a>
|
||||
{% endif %}
|
||||
<span class="grp-font-color-quiet">{% filter capfirst %}{% trans entry.content_type.name %}{% endfilter %}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
{% endif %}
|
||||
{% include "tristate_js.html" %}
|
||||
<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 %}
|
||||
{{ form.as_p }}
|
||||
<input style="margin-top:10px;" type="submit" class="btn btn-primary" value="Filtrer" />
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
{% block content %}
|
||||
<header>
|
||||
<div class="container banner">
|
||||
<a href="{% url "gestioncof.views.home" %}">
|
||||
<a href="{% url "home" %}">
|
||||
<h1>GestioCOF</h1>
|
||||
{% block homelink %}
|
||||
<span class="glyphicon glyphicon-home" aria-hidden=true></span>
|
||||
|
@ -11,7 +11,7 @@
|
|||
</a>
|
||||
<div class="secondary">
|
||||
<span class="hidden-xxs"> | </span>
|
||||
<span><a href="{% url "gestioncof.views.logout" %}">Se déconnecter <span class="glyphicon glyphicon-log-out"></span></a></span>
|
||||
<span><a href="{% url "cof-logout" %}">Se déconnecter <span class="glyphicon glyphicon-log-out"></span></a></span>
|
||||
</div>
|
||||
<h2 class="member-status">{% if user.first_name %}{{ user.first_name }}{% else %}<tt>{{ user.username }}</tt>{% endif %}, {% if user.profile.is_cof %}<tt class="user-is-cof">au COF{% else %}<tt class="user-is-not-cof">non-COF{% endif %}</tt></h2>
|
||||
</div><!-- /.container -->
|
||||
|
|
|
@ -12,7 +12,7 @@ souscrire aux événements du COF et/ou aux spectacles BdA.
|
|||
|
||||
{% if token %}
|
||||
<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>
|
||||
<li>Pour l'ajouter à Thunderbird (lightning), il faut copier ce lien et aller
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
{% if event.details %}
|
||||
<p>{{ event.details }}</p>
|
||||
{% endif %}
|
||||
<form method="post" action="{% url 'gestioncof.views.event' event.id %}">
|
||||
<form method="post" action="{% url 'event.details' event.id %}">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" class="btn-submit" value="Enregistrer" />
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
{% block realcontent %}
|
||||
<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%;">
|
||||
{% csrf_token %}
|
||||
<fieldset"center-block">
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
{% if survey.details %}
|
||||
<p>{{ survey.details }}</p>
|
||||
{% 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 %}
|
||||
{{ form | bootstrap}}
|
||||
|
||||
|
|
|
@ -7,15 +7,15 @@
|
|||
<h2>Liens utiles du COF</h2>
|
||||
<h3>COF</h3>
|
||||
<ul>
|
||||
<li><a href="{% url 'gestioncof.views.export_members' %}">Export des membres du COF</a></li>
|
||||
<li><a href="{% url 'gestioncof.views.liste_diffcof' %}">Diffusion COF</a></li>
|
||||
<li><a href="{% url 'cof.membres_export' %}">Export des membres du COF</a></li>
|
||||
<li><a href="{% url 'ml_diffcof' %}">Diffusion COF</a></li>
|
||||
</ul>
|
||||
|
||||
<h3>Mega</h3>
|
||||
<ul>
|
||||
<li><a href="{% url 'gestioncof.views.export_mega_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 'gestioncof.views.export_mega' %}">Export de tout le monde</a></li>
|
||||
<li><a href="{% url 'cof.mega_export_participants' %}">Export des non-orgas uniquement</a></li>
|
||||
<li><a href="{% url 'cof.mega_export_orgas' %}">Export des orgas uniquement</a></li>
|
||||
<li><a href="{% url 'cof.mega_export' %}">Export de tout le monde</a></li>
|
||||
</ul>
|
||||
|
||||
<p>Note : pour ouvrir les fichiers .csv avec Excel, il faut
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<div class="hm-block">
|
||||
<ul>
|
||||
{% for event in open_events %}
|
||||
<li><a href="{% url "gestioncof.views.event" event.id %}">{{ event.title }}</a></li>
|
||||
<li><a href="{% url "event.details" event.id %}">{{ event.title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -24,7 +24,7 @@
|
|||
<div class="hm-block">
|
||||
<ul>
|
||||
{% for survey in open_surveys %}
|
||||
<li><a href="{% url "gestioncof.views.survey" survey.id %}">{{ survey.title }}</a></li>
|
||||
<li><a href="{% url "survey.details" survey.id %}">{{ survey.title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -69,11 +69,11 @@
|
|||
<h3 class="block-title">Divers<span class="pull-right glyphicon glyphicon-question-sign"></span></h3>
|
||||
<div class="hm-block">
|
||||
<ul>
|
||||
<li><a href="{% url "gestioncof.views.calendar" %}">Calendrier dynamique</a></li>
|
||||
<li><a href="{% url "calendar" %}">Calendrier dynamique</a></li>
|
||||
{% if user.profile.is_cof %}<li><a href="{% url "petits-cours-inscription" %}">Inscription pour donner des petits cours</a></li>{% endif %}
|
||||
|
||||
<li><a href="{% url "gestioncof.views.profile" %}">Éditer mon profil</a></li>
|
||||
{% if not user.profile.login_clipper %}<li><a href="{% url "django.contrib.auth.views.password_change" %}">Changer mon mot de passe</a></li>{% endif %}
|
||||
<li><a href="{% url "profile" %}">Éditer mon profil</a></li>
|
||||
{% if not user.profile.login_clipper %}<li><a href="{% url "password_change" %}">Changer mon mot de passe</a></li>{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -86,16 +86,16 @@
|
|||
<h4>Général</h4>
|
||||
<li><a href="{% url "admin:index" %}">Administration générale</a></li>
|
||||
<li><a href="{% url "petits-cours-demandes-list" %}">Demandes de petits cours</a></li>
|
||||
<li><a href="{% url "gestioncof.views.registration" %}">Inscription d'un nouveau membre</a></li>
|
||||
<li><a href="{% url "registration" %}">Inscription d'un nouveau membre</a></li>
|
||||
<li><a href="{% url "liste-clubs" %}">Gestion des clubs</a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<h4>Évènements & Sondages</h4>
|
||||
{% for event in events %}
|
||||
<li><a href="{% url "gestioncof.views.event_status" event.id %}">Événement : {{ event.title }}</a></li>
|
||||
<li><a href="{% url "event.details.status" event.id %}">Événement : {{ event.title }}</a></li>
|
||||
{% endfor %}
|
||||
{% for survey in surveys %}
|
||||
<li><a href="{% url "gestioncof.views.survey_status" survey.id %}">Sondage : {{ survey.title }}</a></li>
|
||||
<li><a href="{% url "survey.details.status" survey.id %}">Sondage : {{ survey.title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -120,8 +120,8 @@
|
|||
<h3 class="block-title">Liens utiles<span class="pull-right glyphicon glyphicon-link"></span></h3>
|
||||
<div class="hm-block">
|
||||
<ul>
|
||||
<li><a href="{% url "gestioncof.views.utile_cof" %}">Liens utiles du COF</a></li>
|
||||
<li><a href="{% url "gestioncof.views.utile_bda" %}">Liens utiles BdA</a></li>
|
||||
<li><a href="{% url "utile_cof" %}">Liens utiles du COF</a></li>
|
||||
<li><a href="{% url "utile_bda" %}">Liens utiles BdA</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
</tr></thead>
|
||||
<tbody class="bda_formset_content">
|
||||
{% 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 %}
|
||||
{% if field.name != "DELETE" and field.name != "priority" %}
|
||||
<td class="bda-field-{{ field.name }}">
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<p class="error">Identifiants incorrects.</p>
|
||||
{% endif %}
|
||||
<form class="form-horizontal" method="post"
|
||||
action="{% url 'gestioncof.views.login_ext' %}?next={{ next|urlencode }}">
|
||||
action="{% url 'ext_login_view' %}?next={{ next|urlencode }}">
|
||||
{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<input class="form-control" id="id_username" maxlength="254" name="username" type="text" placeholder="Nom d'utilisateur">
|
||||
|
|
|
@ -12,13 +12,13 @@
|
|||
<div class="container-fluid">
|
||||
<div class="row" style="margin:0;">
|
||||
<a aria-label="Compte clipper"
|
||||
href="{% url 'django_cas_ng.views.login' %}?next={{ next|urlencode }}">
|
||||
href="{% url 'cas_login_view' %}?next={{ next|urlencode }}">
|
||||
<div class="col-xs-12 col-sm-6" id="login_clipper">
|
||||
Compte clipper
|
||||
</div>
|
||||
</a>
|
||||
<a aria-label="Extérieur"
|
||||
href="{% url 'gestioncof.views.login_ext' %}?next={{ next|urlencode }}">
|
||||
href="{% url 'ext_login_view' %}?next={{ next|urlencode }}">
|
||||
<div class="col-xs-12 col-sm-6" id="login_outsider">
|
||||
Extérieur
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{% block page_size %}col-sm-8{% endblock %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% block realcontent %}
|
||||
|
|
|
@ -5,5 +5,5 @@
|
|||
|
||||
{% block realcontent %}
|
||||
<h2>Mot de passe modifié avec succès !</h2>
|
||||
<h3><a href="{% url "gestioncof.views.home" %}">Retour au menu principal</a></h3>
|
||||
<h3><a href="{% url "home" %}">Retour au menu principal</a></h3>
|
||||
{% endblock %}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
{% block realcontent %}
|
||||
<h2>Changement de mot de passe</h2>
|
||||
<form class="form-horizontal" method="post" action="{% url 'django.contrib.auth.views.password_change' %}">
|
||||
<form class="form-horizontal" method="post" action="{% url 'password_change' %}">
|
||||
{% csrf_token %}
|
||||
{{ form | bootstrap }}
|
||||
<input type="submit" class="btn btn-primary pull-right" value="Changer" />
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
{% endif %}
|
||||
<h3>Filtres</h3>
|
||||
{% 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 %}
|
||||
{{ form.as_p }}
|
||||
<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
|
||||
|
||||
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<type>.+)$', 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<survey_id>\d+)/status$', views.survey_status),
|
||||
url(r'^(?P<survey_id>\d+)$', views.survey),
|
||||
url(r'^(?P<survey_id>\d+)/status$', views.survey_status,
|
||||
name='survey.details.status'),
|
||||
url(r'^(?P<survey_id>\d+)$', views.survey,
|
||||
name='survey.details'),
|
||||
]
|
||||
|
||||
events_patterns = [
|
||||
url(r'^(?P<event_id>\d+)$', views.event),
|
||||
url(r'^(?P<event_id>\d+)/status$', views.event_status),
|
||||
url(r'^(?P<event_id>\d+)$', views.event,
|
||||
name='event.details'),
|
||||
url(r'^(?P<event_id>\d+)/status$', views.event_status,
|
||||
name='event.details.status'),
|
||||
]
|
||||
|
||||
calendar_patterns = [
|
||||
url(r'^subscription$', 'gestioncof.views.calendar'),
|
||||
url(r'^(?P<token>[a-z0-9-]+)/calendar.ics$',
|
||||
'gestioncof.views.calendar_ics')
|
||||
url(r'^subscription$', views.calendar,
|
||||
name='calendar'),
|
||||
url(r'^(?P<token>[a-z0-9-]+)/calendar.ics$', views.calendar_ics,
|
||||
name='calendar.ics'),
|
||||
]
|
||||
|
||||
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.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())
|
||||
|
|
|
@ -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 = ["<span class=\"tristate\"%s></span>" % flatatt(final_attrs)]
|
||||
return mark_safe('\n'.join(output))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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')),
|
||||
|
|
|
@ -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(
|
||||
|
|
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),
|
||||
),
|
||||
]
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
{% block header-title %}Création d'un compte{% endblock %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% block main %}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
{% load i18n static %}
|
||||
{% load wagtailcore_tags %}
|
||||
|
||||
{% slugurl "kfet" as kfet_home_url %}
|
||||
|
||||
<nav class="navbar navbar-fixed-top">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand" href="{% slugurl "k-fet" %}">
|
||||
<a class="navbar-brand" href="{{ kfet_home_url }}">
|
||||
<img src="{% static 'kfet/img/logo3.png' %}">
|
||||
</a>
|
||||
</div>
|
||||
|
@ -99,7 +101,7 @@
|
|||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li>
|
||||
<a href="{% url "cof-logout" %}?next={% slugurl "k-fet" %}">
|
||||
<a href="{% url "cof-logout" %}?next={{ kfet_home_url|urlencode }}">
|
||||
<span class="glyphicon glyphicon-log-out"></span><span>Déconnexion</span>
|
||||
</a>
|
||||
</li>
|
||||
|
@ -108,13 +110,13 @@
|
|||
{% endif %}
|
||||
{% if user.is_authenticated and not perms.kfet.is_team %}
|
||||
<li>
|
||||
<a href="{% url "cof-logout" %}?next={% slugurl "k-fet" %}" title="Déconnexion">
|
||||
<a href="{% url "cof-logout" %}?next={{ kfet_home_url|urlencode }}" title="Déconnexion">
|
||||
<span class="glyphicon glyphicon-log-out"></span>
|
||||
</a>
|
||||
</li>
|
||||
{% elif not user.is_authenticated %}
|
||||
<li>
|
||||
<a href="{% url "cof-login" %}?next={{ request.path }}" title="Connexion">
|
||||
<a href="{% url "cof-login" %}?next={{ request.path|urlencode }}" title="Connexion">
|
||||
<span>Connexion</span><!--
|
||||
--><span class="glyphicon glyphicon-log-in"></span>
|
||||
</a>
|
||||
|
|
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.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()
|
||||
|
||||
|
@ -23,3 +28,33 @@ class AccountTests(TestCase):
|
|||
|
||||
with self.assertRaises(Account.DoesNotExist):
|
||||
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)
|
||||
|
|
|
@ -452,7 +452,7 @@ class AccountGroupUpdateViewTests(ViewTestCaseMixin, TestCase):
|
|||
'kfet.manage_perms',
|
||||
)
|
||||
self.group = Group.objects.create(name='K-Fêt - Group')
|
||||
self.group.permissions = self.perms.values()
|
||||
self.group.permissions.set(self.perms.values())
|
||||
|
||||
def test_get_ok(self):
|
||||
r = self.client.get(self.url)
|
||||
|
@ -746,12 +746,16 @@ class CheckoutReadViewTests(ViewTestCaseMixin, TestCase):
|
|||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.checkout = Checkout.objects.create(
|
||||
name='Checkout',
|
||||
created_by=self.accounts['team'],
|
||||
valid_from=self.now,
|
||||
valid_to=self.now + timedelta(days=5),
|
||||
)
|
||||
|
||||
with mock.patch('django.utils.timezone.now') as mock_now:
|
||||
mock_now.return_value = self.now
|
||||
|
||||
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):
|
||||
r = self.client.get(self.url)
|
||||
|
@ -794,7 +798,7 @@ class CheckoutUpdateViewTests(ViewTestCaseMixin, TestCase):
|
|||
name='Checkout',
|
||||
valid_from=self.now,
|
||||
valid_to=self.now + timedelta(days=5),
|
||||
balance='3.14',
|
||||
balance=Decimal('3.14'),
|
||||
is_protected=False,
|
||||
created_by=self.accounts['team'],
|
||||
)
|
||||
|
@ -864,6 +868,7 @@ class CheckoutStatementListViewTests(ViewTestCaseMixin, TestCase):
|
|||
self.assertQuerysetEqual(
|
||||
r.context['checkoutstatements'],
|
||||
map(repr, expected_statements),
|
||||
ordered=False,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ class TestCaseMixin:
|
|||
'path': request.get_full_path(),
|
||||
'username': (
|
||||
"'{}'".format(request.user)
|
||||
if request.user.is_authenticated()
|
||||
if request.user.is_authenticated
|
||||
else 'anonymous'
|
||||
),
|
||||
'code': response.status_code,
|
||||
|
@ -96,7 +96,7 @@ class TestCaseMixin:
|
|||
'path': request.get_full_path(),
|
||||
'username': (
|
||||
"'%s'" % request.user
|
||||
if request.user.is_authenticated()
|
||||
if request.user.is_authenticated
|
||||
else 'anonymous'
|
||||
),
|
||||
'form_ctx': form_ctx,
|
||||
|
@ -245,13 +245,7 @@ class ViewTestCaseMixin(TestCaseMixin):
|
|||
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,
|
||||
)
|
||||
)
|
||||
self.client.force_login(self.users[self.auth_user])
|
||||
|
||||
def tearDown(self):
|
||||
del self.users_base
|
||||
|
|
|
@ -184,5 +184,5 @@ def user_add_perms(user, perms_labels):
|
|||
|
||||
# If permissions have already been fetched for this user, we need to reload
|
||||
# it to avoid using of the previous permissions cache.
|
||||
# https://docs.djangoproject.com/en/1.11/topics/auth/default/#permission-caching
|
||||
# https://docs.djangoproject.com/en/dev/topics/auth/default/#permission-caching
|
||||
return User.objects.get(pk=user.pk)
|
||||
|
|
|
@ -528,15 +528,7 @@ class CheckoutCreate(SuccessMessageMixin, CreateView):
|
|||
|
||||
# Creating
|
||||
form.instance.created_by = self.request.user.profile.account_kfet
|
||||
checkout = 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)
|
||||
form.save()
|
||||
|
||||
return super(CheckoutCreate, self).form_valid(form)
|
||||
|
||||
|
@ -861,30 +853,34 @@ def account_read_json(request):
|
|||
'trigramme': account.trigramme }
|
||||
return JsonResponse(data)
|
||||
|
||||
|
||||
@teamkfet_required
|
||||
def kpsul_checkout_data(request):
|
||||
pk = request.POST.get('pk', 0)
|
||||
if not pk:
|
||||
pk = 0
|
||||
data = (Checkout.objects
|
||||
|
||||
data = (
|
||||
Checkout.objects
|
||||
.annotate(
|
||||
last_statement_by_first_name=F('statements__by__cofprofile__user__first_name'),
|
||||
last_statement_by_last_name=F('statements__by__cofprofile__user__last_name'),
|
||||
last_statement_by_trigramme=F('statements__by__trigramme'),
|
||||
last_statement_balance=F('statements__balance_new'),
|
||||
last_statement_at=F('statements__at'))
|
||||
.values(
|
||||
'id', 'name', 'balance', 'valid_from', 'valid_to',
|
||||
'last_statement_balance', 'last_statement_at',
|
||||
'last_statement_by_trigramme', 'last_statement_by_last_name',
|
||||
'last_statement_by_first_name')
|
||||
.select_related(
|
||||
'statements'
|
||||
'statements__by',
|
||||
'statements__by__cofprofile__user')
|
||||
.filter(pk=pk)
|
||||
.order_by('statements__at')
|
||||
.last())
|
||||
.values(
|
||||
'id', 'name', 'balance', 'valid_from', 'valid_to',
|
||||
'last_statement_balance', 'last_statement_at',
|
||||
'last_statement_by_trigramme', 'last_statement_by_last_name',
|
||||
'last_statement_by_first_name')
|
||||
.last()
|
||||
)
|
||||
if data is None:
|
||||
raise Http404
|
||||
return JsonResponse(data)
|
||||
|
@ -1312,13 +1308,13 @@ def history_json(request):
|
|||
|
||||
# Construction de la requête (sur les opérations) pour le prefetch
|
||||
queryset_prefetch = Operation.objects.select_related(
|
||||
'canceled_by__trigramme', 'addcost_for__trigramme',
|
||||
'article__name')
|
||||
'article', 'canceled_by', 'addcost_for')
|
||||
|
||||
# Construction de la requête principale
|
||||
opegroups = (OperationGroup.objects
|
||||
.prefetch_related(Prefetch('opes', queryset = queryset_prefetch))
|
||||
.select_related('on_acc__trigramme', 'valid_by__trigramme')
|
||||
opegroups = (
|
||||
OperationGroup.objects
|
||||
.prefetch_related(Prefetch('opes', queryset=queryset_prefetch))
|
||||
.select_related('on_acc', 'valid_by')
|
||||
.order_by('at')
|
||||
)
|
||||
# Application des filtres
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
#!/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
|
||||
# pour une installation de dév locale qui ne sera accessible que depuis la
|
||||
# machine virtuelle.
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Stop if an error is encountered.
|
||||
set -e
|
||||
|
||||
python manage.py migrate
|
||||
python manage.py loaddata gestion sites articles
|
||||
python manage.py loaddevdata
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
-r requirements.txt
|
||||
-e git://github.com/jazzband/django-debug-toolbar.git@88ddc7bdf39c7ff660eac054eab225ac22926754#egg=django-debug-toolbar
|
||||
django-debug-toolbar
|
||||
django-debug-panel
|
||||
ipython
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
configparser==3.5.0
|
||||
Django==1.8.*
|
||||
django-autocomplete-light==2.3.3
|
||||
Django==1.11.*
|
||||
django-autocomplete-light==3.1.3
|
||||
django-autoslug==1.9.3
|
||||
django-cas-ng==3.5.7
|
||||
django-djconfig==0.5.3
|
||||
django-grappelli==2.8.1
|
||||
django-recaptcha==1.0.5
|
||||
django-recaptcha==1.2.1
|
||||
django-redis-cache==1.7.1
|
||||
icalendar
|
||||
psycopg2
|
||||
Pillow==3.3.0
|
||||
six==1.10.0
|
||||
unicodecsv==0.14.1
|
||||
icalendar==3.10
|
||||
django-bootstrap-form==3.2.1
|
||||
Pillow
|
||||
six
|
||||
unicodecsv
|
||||
django-bootstrap-form==3.3
|
||||
asgiref==1.1.1
|
||||
daphne==1.3.0
|
||||
asgi-redis==1.3.0
|
||||
|
@ -26,8 +25,5 @@ python-dateutil
|
|||
wagtail==1.10.*
|
||||
wagtailmenus==2.2.*
|
||||
|
||||
# Remove this when we switch to Django 1.11
|
||||
djangorestframework==3.6.4
|
||||
|
||||
# Production tools
|
||||
wheel
|
||||
|
|
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)
|
0
utils/__init__.py
Normal file
0
utils/__init__.py
Normal file
0
utils/views/__init__.py
Normal file
0
utils/views/__init__.py
Normal file
26
utils/views/autocomplete.py
Normal file
26
utils/views/autocomplete.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
from django.db.models import Q
|
||||
|
||||
from dal import autocomplete
|
||||
|
||||
|
||||
class Select2QuerySetView(autocomplete.Select2QuerySetView):
|
||||
model = None
|
||||
search_fields = []
|
||||
|
||||
def get_queryset_filter(self):
|
||||
q = self.q
|
||||
filter_q = Q()
|
||||
|
||||
if not q:
|
||||
return filter_q
|
||||
|
||||
words = q.split()
|
||||
|
||||
for word in words:
|
||||
for field in self.search_fields:
|
||||
filter_q |= Q(**{'{}__icontains'.format(field): word})
|
||||
|
||||
return filter_q
|
||||
|
||||
def get_queryset(self):
|
||||
return self.model.objects.filter(self.get_queryset_filter())
|
Loading…
Reference in a new issue