Compare commits
15 commits
mdebray/re
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
98a6f98fda | ||
|
0aae767a3e | ||
|
d88768311e | ||
|
abc6c62a89 | ||
|
3e065f30c0 | ||
|
0ee798a64e | ||
|
1e8b993739 | ||
|
8398805b54 | ||
|
56d7490879 | ||
|
aa430fa4d2 | ||
|
5eb27e2171 | ||
|
31f6dc0961 | ||
|
764ffea61f | ||
|
7122c1c0c4 | ||
|
14e8d963d6 |
32 changed files with 773 additions and 21 deletions
|
@ -35,6 +35,7 @@ ACCOUNT_CREATION_PASS = import_secret("ACCOUNT_CREATION_PASS")
|
|||
BASE_DIR = os.path.join(os.path.dirname(__file__), "..", "..")
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"propositions",
|
||||
"trombonoscope",
|
||||
"actu",
|
||||
"colorful",
|
||||
|
|
|
@ -2,5 +2,27 @@ from django.contrib import admin
|
|||
|
||||
from .models import Event, Participants
|
||||
|
||||
|
||||
class ParticipantsAdmin(admin.ModelAdmin):
|
||||
fields = [
|
||||
"event",
|
||||
"participant",
|
||||
"reponse",
|
||||
"instrument",
|
||||
"instrument_autre",
|
||||
"dont_play_main",
|
||||
"details",
|
||||
"creationDate",
|
||||
"updateDate",
|
||||
]
|
||||
readonly_fields = ["creationDate", "updateDate"]
|
||||
list_display = ["participant", "event", "reponse", "creationDate", "updateDate"]
|
||||
def has_add_permission(self, req):
|
||||
return False
|
||||
def has_change_permission(self,obj, change=False):
|
||||
return False
|
||||
|
||||
|
||||
# Add event by admin page return a 502 error
|
||||
admin.site.register(Event)
|
||||
admin.site.register(Participants, ParticipantsAdmin)
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from gestion.models import ErnestoUser
|
||||
|
||||
from calendrier.models import Event, Participants
|
||||
from gestion.models import ErnestoUser
|
||||
|
||||
|
||||
class ModifEventForm(forms.ModelForm):
|
||||
|
|
31
calendrier/migrations/0007_auto_20220314_2320.py
Normal file
31
calendrier/migrations/0007_auto_20220314_2320.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
# Generated by Django 2.2.25 on 2022-03-14 23:20
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("calendrier", "0006_auto_20210929_1629"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="participants",
|
||||
name="creationDate",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
default=django.utils.timezone.now,
|
||||
verbose_name="Date de création",
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="participants",
|
||||
name="updateDate",
|
||||
field=models.DateTimeField(
|
||||
auto_now=True, verbose_name="Dernière mise à jour"
|
||||
),
|
||||
),
|
||||
]
|
17
calendrier/migrations/0008_auto_20220322_1454.py
Normal file
17
calendrier/migrations/0008_auto_20220322_1454.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 2.2.27 on 2022-03-22 13:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('calendrier', '0007_auto_20220314_2320'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddConstraint(
|
||||
model_name='participants',
|
||||
constraint=models.UniqueConstraint(fields=('event', 'participant'), name='reponse unique aux event'),
|
||||
),
|
||||
]
|
|
@ -2,7 +2,6 @@ import uuid
|
|||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from gestion.models import INSTRU_CHOICES, ErnestoUser
|
||||
|
||||
ANSWERS = (
|
||||
|
@ -73,3 +72,11 @@ class Participants(models.Model):
|
|||
choices=[("Non", _("Non")), ("Oui", _("Oui"))],
|
||||
)
|
||||
details = models.CharField(max_length=50, blank=True)
|
||||
creationDate = models.DateTimeField(
|
||||
auto_now_add=True, verbose_name=_("Date de création")
|
||||
)
|
||||
updateDate = models.DateTimeField(
|
||||
auto_now=True, verbose_name=_("Dernière mise à jour")
|
||||
)
|
||||
class Meta:
|
||||
constraints = [ models.UniqueConstraint(fields=['event', 'participant'], name='reponse unique aux event') ]
|
||||
|
|
|
@ -223,6 +223,9 @@
|
|||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% if event.id == 573 %}
|
||||
<div class="fireworks" style="pointer-events: none; position:fixed; top: 0; left: 0; right: 0; bottom: 0; height: 100%; weight: 100%;"></div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block script %}
|
||||
<script>
|
||||
|
@ -264,5 +267,13 @@ singleEvent.setOption({ lang: 'fr' });
|
|||
singleEvent.setOption({ lang: 'en' });
|
||||
{% endifequal %}
|
||||
</script>
|
||||
|
||||
{% if event.id == 573 %}
|
||||
<script src="https://unpkg.com/fireworks-js@2.x/dist/index.umd.js"></script>
|
||||
<script>
|
||||
const container = document.querySelector('.fireworks');
|
||||
const fireworks = new Fireworks.default(container);
|
||||
fireworks.updateOptions({ acceleration: 1.01, traceSpeed: 5 });
|
||||
fireworks.start();
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -4,7 +4,6 @@ from django.contrib.auth import get_user_model
|
|||
from django.template.defaultfilters import urlencode
|
||||
from django.test import Client, TestCase
|
||||
from django.utils import timezone
|
||||
|
||||
from gestion.models import ErnestoUser
|
||||
|
||||
from ..models import Event
|
||||
|
|
|
@ -4,20 +4,20 @@ from calendar import monthrange
|
|||
from collections import defaultdict
|
||||
from datetime import date, datetime
|
||||
|
||||
from actu.models import Actu
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.views.generic import DeleteView, TemplateView, UpdateView
|
||||
from gestion.mixins import ChefEventRequiredMixin
|
||||
from gestion.models import Photo
|
||||
|
||||
from actu.models import Actu
|
||||
from calendrier.calend import EventCalendar
|
||||
from calendrier.forms import (ChangeDoodleName, EventForm, ModifEventForm,
|
||||
ParticipantsForm)
|
||||
from calendrier.models import Event, Participants
|
||||
from gestion.mixins import ChefEventRequiredMixin, ChefRequiredMixin
|
||||
from gestion.models import Photo
|
||||
|
||||
|
||||
def generer(*args):
|
||||
|
@ -212,6 +212,7 @@ class ViewEvent(LoginRequiredMixin, TemplateView):
|
|||
else:
|
||||
instru = participant.instrument
|
||||
|
||||
instru = "" if instru is None else instru
|
||||
sure, maybe, namesoui, namespe, namesnon = instrument_count[instru]
|
||||
|
||||
if participant.reponse == "oui":
|
||||
|
@ -373,16 +374,16 @@ class ReponseEvent(LoginRequiredMixin, TemplateView):
|
|||
return context
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.form_class(request.POST)
|
||||
ev = get_object_or_404(Event, id=self.kwargs["id"])
|
||||
part = request.user.profile
|
||||
try:
|
||||
p = Participants.objects.get(event=ev, participant=part)
|
||||
except Participants.DoesNotExist:
|
||||
p = None
|
||||
form = self.form_class(request.POST, instance=p)
|
||||
if form.is_valid():
|
||||
try:
|
||||
p = Participants.objects.get(event=ev, participant=part)
|
||||
p.delete()
|
||||
except Participants.DoesNotExist:
|
||||
pass
|
||||
obj = form.save(commit=False)
|
||||
# Si la participation existe déjà, ces 2 ligne sont redondantes
|
||||
obj.event = ev
|
||||
obj.participant = part
|
||||
obj.save()
|
||||
|
|
BIN
gestion/static/images/cvec.png
Normal file
BIN
gestion/static/images/cvec.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
|
@ -145,6 +145,10 @@
|
|||
<!-- Footer -->
|
||||
<footer id="footer" style="background-color:rgb(228, 82, 47);">
|
||||
<div class="copyright">
|
||||
<ul class="icons">
|
||||
<li><a target="_blank" href="https://cvec.etudiant.gouv.fr/"><img alt="Logo de la CVEC" src='{% static "images/cvec.png" %}' width="100px"/></a></li>
|
||||
|
||||
</ul>
|
||||
<ul class="icons">
|
||||
<li><a target="_blank" href="https://www.facebook.com/ernestophone"
|
||||
|
||||
|
|
0
propositions/__init__.py
Normal file
0
propositions/__init__.py
Normal file
0
propositions/admin.py
Normal file
0
propositions/admin.py
Normal file
38
propositions/migrations/0001_initial.py
Normal file
38
propositions/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('gestion', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Prop',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, verbose_name='ID', auto_created=True, serialize=False)),
|
||||
('nom', models.CharField(max_length=100)),
|
||||
('artiste', models.CharField(max_length=100, blank=True)),
|
||||
('lien', models.URLField(blank=True)),
|
||||
('nboui', models.IntegerField(verbose_name='oui', default=0)),
|
||||
('nbnon', models.IntegerField(verbose_name='non', default=0)),
|
||||
('user', models.ForeignKey(verbose_name='Proposé par', to='gestion.ErnestoUser', on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Proposition',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Reponses',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, verbose_name='ID', auto_created=True, serialize=False)),
|
||||
('reponse', models.CharField(verbose_name='Réponse', choices=[('oui', 'Oui'), ('non', 'Non')], max_length=20, blank=True)),
|
||||
('part', models.ForeignKey(to='gestion.ErnestoUser', on_delete=models.CASCADE)),
|
||||
('prop', models.ForeignKey(to='propositions.Prop', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
]
|
18
propositions/migrations/0002_nom_verbose_name.py
Normal file
18
propositions/migrations/0002_nom_verbose_name.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.9 on 2020-01-04 23:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('propositions', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='prop',
|
||||
name='nom',
|
||||
field=models.CharField(max_length=100, verbose_name='nom du morceau'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,75 @@
|
|||
# Generated by Django 2.2.9 on 2020-01-05 13:32
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def move_profile_to_user(apps, schema_editor):
|
||||
Reponses = apps.get_model("propositions", "reponses")
|
||||
for answer in Reponses.objects.all():
|
||||
answer.user = answer.part.user
|
||||
answer.save()
|
||||
|
||||
|
||||
def move_user_to_profile(apps, schema_editor):
|
||||
# One should do something similar to ``move_profile_to_user`` AND make the
|
||||
# ``part`` field temporarily nullable in the operations below.
|
||||
# => Grosse flemme
|
||||
raise NotImplementedError("Who uses migrations backwards anyway?")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("gestion", "0001_initial"),
|
||||
("propositions", "0002_nom_verbose_name"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="reponses",
|
||||
options={
|
||||
"verbose_name": "Réponse à une proposition",
|
||||
"verbose_name_plural": "Réponses à une proposition",
|
||||
},
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="reponses", old_name="prop", new_name="proposition",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="reponses",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
on_delete=models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.RunPython(move_profile_to_user, move_user_to_profile),
|
||||
migrations.AlterField(
|
||||
model_name="reponses",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
on_delete=models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
null=False,
|
||||
),
|
||||
),
|
||||
migrations.RemoveField(model_name="reponses", name="part"),
|
||||
migrations.AddField(
|
||||
model_name="reponses",
|
||||
name="answer",
|
||||
field=models.CharField(
|
||||
choices=[("oui", "Oui"), ("non", "Non")],
|
||||
default="non",
|
||||
max_length=3,
|
||||
verbose_name="Réponse",
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="reponses", unique_together={("proposition", "user")},
|
||||
),
|
||||
migrations.RemoveField(model_name="reponses", name="reponse",),
|
||||
migrations.RenameModel(old_name="reponses", new_name="answer"),
|
||||
]
|
43
propositions/migrations/0004_prop_renaming_and_cleaning.py
Normal file
43
propositions/migrations/0004_prop_renaming_and_cleaning.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
# Generated by Django 2.2.9 on 2020-01-05 14:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("propositions", "0003_reponse_renaming_and_cleaning"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="prop",
|
||||
options={
|
||||
"verbose_name": "Proposition de morceau",
|
||||
"verbose_name_plural": "Propositions de morceaux",
|
||||
},
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="prop", old_name="artiste", new_name="artist",
|
||||
),
|
||||
migrations.RenameField(model_name="prop", old_name="lien", new_name="link"),
|
||||
migrations.RenameField(model_name="prop", old_name="nom", new_name="name"),
|
||||
migrations.RenameField(model_name="prop", old_name="nbnon", new_name="nb_no"),
|
||||
migrations.RenameField(model_name="prop", old_name="nboui", new_name="nb_yes"),
|
||||
migrations.AlterField(
|
||||
model_name="prop",
|
||||
name="nb_no",
|
||||
field=models.IntegerField(default=0, verbose_name="nombre de réponses non"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="prop",
|
||||
name="nb_yes",
|
||||
field=models.IntegerField(default=0, verbose_name="nombre de réponses oui"),
|
||||
),
|
||||
migrations.RenameModel(old_name="prop", new_name="proposition"),
|
||||
migrations.AlterField(
|
||||
model_name='answer',
|
||||
name='proposition',
|
||||
field=models.ForeignKey(on_delete=models.deletion.CASCADE, to='propositions.Proposition'),
|
||||
),
|
||||
]
|
24
propositions/migrations/0005_remove_nb_yes_no_fields.py
Normal file
24
propositions/migrations/0005_remove_nb_yes_no_fields.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 2.2.9 on 2020-01-05 15:26
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("propositions", "0004_prop_renaming_and_cleaning"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(model_name="proposition", name="nb_no",),
|
||||
migrations.RemoveField(model_name="proposition", name="nb_yes",),
|
||||
migrations.AlterField(
|
||||
model_name="answer",
|
||||
name="proposition",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="propositions.Proposition",
|
||||
),
|
||||
),
|
||||
]
|
53
propositions/migrations/0006_proposition_profile_to_user.py
Normal file
53
propositions/migrations/0006_proposition_profile_to_user.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
# Generated by Django 2.2.9 on 2020-01-05 16:28
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def move_profile_to_user(apps, schema_editor):
|
||||
Proposition = apps.get_model("propositions", "Proposition")
|
||||
for proposition in Proposition.objects.all():
|
||||
proposition.user = proposition.profile.user
|
||||
proposition.save()
|
||||
|
||||
|
||||
def move_user_to_profile(apps, schema_editor):
|
||||
# One should do something similar to ``move_profile_to_user`` AND make the
|
||||
# ``profile`` field temporarily nullable in the operations below.
|
||||
# => Grosse flemme
|
||||
raise NotImplementedError("Who uses migrations backwards anyway?")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("propositions", "0005_remove_nb_yes_no_fields"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="proposition", old_name="user", new_name="profile"
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="proposition",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Proposé par",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.RunPython(move_profile_to_user, move_user_to_profile),
|
||||
migrations.RemoveField(model_name="proposition", name="profile"),
|
||||
migrations.AlterField(
|
||||
model_name="proposition",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Proposé par",
|
||||
),
|
||||
),
|
||||
]
|
19
propositions/migrations/0007_auto_20220322_1455.py
Normal file
19
propositions/migrations/0007_auto_20220322_1455.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 2.2.27 on 2022-03-22 13:55
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('propositions', '0006_proposition_profile_to_user'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='answer',
|
||||
name='proposition',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='propositions.Proposition'),
|
||||
),
|
||||
]
|
19
propositions/migrations/0008_auto_20220914_1244.py
Normal file
19
propositions/migrations/0008_auto_20220914_1244.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 2.2.27 on 2022-09-14 10:44
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('propositions', '0007_auto_20220322_1455'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='answer',
|
||||
name='proposition',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='propositions.Proposition'),
|
||||
),
|
||||
]
|
19
propositions/migrations/0009_auto_20240615_1303.py
Normal file
19
propositions/migrations/0009_auto_20240615_1303.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 2.2.28 on 2024-06-15 13:03
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('propositions', '0008_auto_20220914_1244'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='answer',
|
||||
name='proposition',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='propositions.Proposition'),
|
||||
),
|
||||
]
|
0
propositions/migrations/__init__.py
Normal file
0
propositions/migrations/__init__.py
Normal file
34
propositions/models.py
Normal file
34
propositions/models.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.db import models
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class Proposition(models.Model):
|
||||
name = models.CharField(max_length=100, verbose_name="nom du morceau")
|
||||
artist = models.CharField(blank=True, max_length=100)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Proposé par")
|
||||
link = models.URLField(blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Proposition de morceau"
|
||||
verbose_name_plural = "Propositions de morceaux"
|
||||
|
||||
|
||||
class Answer(models.Model):
|
||||
YES = "oui"
|
||||
NO = "non"
|
||||
|
||||
REP_CHOICES = [(YES, "Oui"), (NO, "Non")]
|
||||
|
||||
proposition = models.ForeignKey(Proposition, on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
answer = models.CharField("Réponse", max_length=3, choices=REP_CHOICES)
|
||||
|
||||
class Meta:
|
||||
unique_together = ("proposition", "user")
|
||||
verbose_name = "Réponse à une proposition"
|
||||
verbose_name_plural = "Réponses à une proposition"
|
14
propositions/templates/propositions/create.html
Normal file
14
propositions/templates/propositions/create.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
{% extends "gestion/base.html" %}
|
||||
|
||||
{% block titre %}Proposition de morceau{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<p><a href="{% url "propositions:list" %}">Retour aux propositions</a></p>
|
||||
<form action="{% url "propositions:create" %}" method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="Enregistrer" />
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
10
propositions/templates/propositions/delete.html
Normal file
10
propositions/templates/propositions/delete.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
{% extends "gestion/base.html" %}
|
||||
|
||||
{% block titre %}Suppression d'une proposition{% endblock %}
|
||||
{% block content %}<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<p><a href="{% url "propositions:list" %}">Retour aux propositions</a></p>
|
||||
<p>Voulez vous vraiment supprimer la proposition {{ object }}?</p>
|
||||
<input type="submit" value="Oui" />
|
||||
</form>
|
||||
{% endblock %}
|
44
propositions/templates/propositions/liste.html
Normal file
44
propositions/templates/propositions/liste.html
Normal file
|
@ -0,0 +1,44 @@
|
|||
{% extends "gestion/base.html" %}
|
||||
|
||||
{% block titre %}Propositions de morceau{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Liste des propositions</h1>
|
||||
|
||||
<p><a href="{% url "propositions:create" %}">Proposer un morceau</a></p>
|
||||
|
||||
{% if propositions.exists %}
|
||||
<table>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Oui</th>
|
||||
<th>Non</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% for p in propositions %}
|
||||
<tr class="prop">
|
||||
<td>
|
||||
{% if p.link %}<a href={{ p.link }}>{% endif %}
|
||||
<b>{{ p.name }}</b>{% if p.artist %} - {{ p.artist }}{% endif %}
|
||||
{% if p.link %}</a>{% endif %}
|
||||
</td>
|
||||
<td>{{ p.nb_yes }}</td>
|
||||
<td>{{ p.nb_no }}</td>
|
||||
<td><a href="{% url "propositions:oui" p.id %}">Oui</a></td>
|
||||
<td><a href="{% url "propositions:non" p.id %}">Non</a></td>
|
||||
<td>{% if p.user_answer %}Vous avez voté {{ p.user_answer }}{% endif %}</td>
|
||||
<td>
|
||||
{% if p.user == request.user or request.user.profile.is_chef %}
|
||||
<a class="supprimer" href="{% url "propositions:delete" p.id %}">Supprimer</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
Pas de proposition pour le moment
|
||||
{% endif %}
|
||||
{% endblock %}
|
165
propositions/tests.py
Normal file
165
propositions/tests.py
Normal file
|
@ -0,0 +1,165 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import reverse_lazy, reverse
|
||||
|
||||
from gestion.models import ErnestoUser
|
||||
from propositions.models import Answer, Proposition
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
def new_user(username):
|
||||
u = User.objects.create_user(username=username)
|
||||
ErnestoUser.objects.create(user=u, slug=username, is_ernesto=True)
|
||||
return u
|
||||
|
||||
|
||||
class PropositionCreateTest(TestCase):
|
||||
url = reverse_lazy("propositions:create")
|
||||
|
||||
def test_anonymous_cannot_get(self):
|
||||
response = Client().get(self.url)
|
||||
self.assertRedirects(response, "/login?next={}".format(self.url))
|
||||
|
||||
def test_ernesto_user_can_get(self):
|
||||
user = new_user("toto")
|
||||
client = Client()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(self.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_ernesto_user_can_post(self):
|
||||
user = new_user("toto")
|
||||
client = Client()
|
||||
client.force_login(user)
|
||||
|
||||
data = {"name": "foo", "artist": "bar", "link": "example.com"}
|
||||
client.post(self.url, data)
|
||||
proposition = Proposition.objects.all()
|
||||
self.assertEqual(1, proposition.count())
|
||||
|
||||
proposition = proposition.get()
|
||||
self.assertEqual(proposition.name, "foo")
|
||||
self.assertEqual(proposition.artist, "bar")
|
||||
self.assertEqual(proposition.link, "http://example.com")
|
||||
self.assertEqual(proposition.user, user)
|
||||
|
||||
|
||||
class PropositionDeleteTest(TestCase):
|
||||
def setUp(self):
|
||||
self.owner = new_user("owner")
|
||||
self.random_user = new_user("toto")
|
||||
self.chef = new_user("chef")
|
||||
self.chef.profile.is_chef = True
|
||||
self.chef.profile.save()
|
||||
|
||||
proposition = Proposition.objects.create(name="prop", user=self.owner)
|
||||
self.url = reverse("propositions:delete", args=(proposition.id,))
|
||||
|
||||
def test_anonymous_cannot_get(self):
|
||||
response = Client().get(self.url)
|
||||
self.assertRedirects(response, "/login?next={}".format(self.url))
|
||||
|
||||
def test_anonymous_cannot_post(self):
|
||||
response = Client().post(self.url, {})
|
||||
self.assertRedirects(response, "/login?next={}".format(self.url))
|
||||
self.assertTrue(Proposition.objects.exists())
|
||||
|
||||
def test_random_user_cannot_get(self):
|
||||
client = Client()
|
||||
client.force_login(self.random_user)
|
||||
response = client.get(self.url)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_not_owner_cannot_post(self):
|
||||
client = Client()
|
||||
client.force_login(self.random_user)
|
||||
response = client.post(self.url, {})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertTrue(Proposition.objects.exists())
|
||||
|
||||
def test_chef_can_get(self):
|
||||
client = Client()
|
||||
client.force_login(self.chef)
|
||||
response = client.get(self.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_chef_can_post(self):
|
||||
client = Client()
|
||||
client.force_login(self.chef)
|
||||
client.post(self.url, {})
|
||||
self.assertFalse(Proposition.objects.exists())
|
||||
|
||||
def test_owner_can_get(self):
|
||||
client = Client()
|
||||
client.force_login(self.owner)
|
||||
response = client.get(self.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_owner_can_post(self):
|
||||
client = Client()
|
||||
client.force_login(self.owner)
|
||||
client.post(self.url, {})
|
||||
self.assertFalse(Proposition.objects.exists())
|
||||
|
||||
|
||||
class PropositionListTest(TestCase):
|
||||
url = reverse_lazy("propositions:list")
|
||||
|
||||
def setUp(self):
|
||||
self.user = new_user("toto")
|
||||
for name in ["foo", "bar", "baz"]:
|
||||
p = Proposition.objects.create(name=name, user=self.user)
|
||||
Answer.objects.create(proposition=p, user=self.user, answer=Answer.YES)
|
||||
for name in ["oof", "rab", "zab"]:
|
||||
p = Proposition.objects.create(name=name, user=self.user)
|
||||
Answer.objects.create(proposition=p, user=self.user, answer=Answer.NO)
|
||||
|
||||
def test_anonymous_cannot_get(self):
|
||||
response = Client().get(self.url)
|
||||
self.assertRedirects(response, "/login?next={}".format(self.url))
|
||||
|
||||
def test_ernesto_user_can_get(self):
|
||||
client = Client()
|
||||
client.force_login(self.user)
|
||||
|
||||
response = client.get(self.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class ReponseTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = new_user("toto")
|
||||
self.prop = Proposition.objects.create(name="foo", user=self.user)
|
||||
|
||||
def _url(self, rep):
|
||||
assert rep in Answer.REP_CHOICES
|
||||
return reverse("propositions:{}".format(rep), args=(self.prop.id,))
|
||||
|
||||
def test_anonymous_cannot_get(self):
|
||||
client = Client()
|
||||
|
||||
url = reverse("propositions:oui", args=(self.prop.id,))
|
||||
response = client.get(url)
|
||||
self.assertRedirects(response, "/login?next={}".format(url))
|
||||
|
||||
url = reverse("propositions:non", args=(self.prop.id,))
|
||||
response = client.get(url)
|
||||
self.assertRedirects(response, "/login?next={}".format(url))
|
||||
|
||||
def test_ernesto_user_can_get(self):
|
||||
client = Client()
|
||||
client.force_login(self.user)
|
||||
|
||||
client.get(reverse("propositions:oui", args=(self.prop.id,)))
|
||||
self.prop.refresh_from_db()
|
||||
self.assertEqual(
|
||||
list(self.prop.answer_set.values_list("answer", flat=True)), [Answer.YES],
|
||||
)
|
||||
|
||||
client.get(reverse("propositions:non", args=(self.prop.id,)))
|
||||
self.prop.refresh_from_db()
|
||||
self.assertEqual(
|
||||
list(self.prop.answer_set.values_list("answer", flat=True)), [Answer.NO]
|
||||
)
|
13
propositions/urls.py
Normal file
13
propositions/urls.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from django.urls import path
|
||||
|
||||
from propositions import views
|
||||
from propositions.models import Answer
|
||||
|
||||
app_name = "propositions"
|
||||
urlpatterns = [
|
||||
path("", views.PropositionList.as_view(), name="list"),
|
||||
path("new", views.PropositionCreate.as_view(), name="create"),
|
||||
path("<int:id>/oui", views.answer, {"ans": "oui"}, name=Answer.YES),
|
||||
path("<int:id>/non", views.answer, {"ans": "non"}, name=Answer.NO),
|
||||
path("<int:pk>/supprimer", views.PropositionDelete.as_view(), name="delete"),
|
||||
]
|
8
propositions/utils.py
Normal file
8
propositions/utils.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
import string
|
||||
import random
|
||||
|
||||
|
||||
def generer(*args):
|
||||
caracteres = string.ascii_letters + string.digits
|
||||
aleatoire = [random.choice(caracteres) for _ in range(6)]
|
||||
return ''.join(aleatoire)
|
63
propositions/views.py
Normal file
63
propositions/views.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
from django.shortcuts import redirect, get_object_or_404
|
||||
from django.urls import reverse_lazy
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||
from django.db.models import Count, OuterRef, Q, Subquery
|
||||
from django.views.generic import CreateView, DeleteView, ListView
|
||||
from django.http import HttpResponseRedirect
|
||||
|
||||
from propositions.models import Answer, Proposition
|
||||
|
||||
|
||||
class PropositionCreate(LoginRequiredMixin, CreateView):
|
||||
template_name = "propositions/create.html"
|
||||
success_url = reverse_lazy("propositions:list")
|
||||
model = Proposition
|
||||
fields = ["name", "artist", "link"]
|
||||
|
||||
def form_valid(self, form):
|
||||
proposition = form.save(commit=False)
|
||||
proposition.user = self.request.user
|
||||
proposition.save()
|
||||
return HttpResponseRedirect(self.success_url)
|
||||
|
||||
|
||||
class PropositionList(LoginRequiredMixin, ListView):
|
||||
template_name = "propositions/liste.html"
|
||||
context_object_name = "propositions"
|
||||
model = Proposition
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
user_answers = (
|
||||
Answer.objects
|
||||
.filter(proposition=OuterRef("id"), user=user)
|
||||
.values_list("answer", flat=True)
|
||||
)
|
||||
return (
|
||||
Proposition.objects
|
||||
.annotate(nb_yes=Count("answer", filter=Q(answer__answer=Answer.YES)))
|
||||
.annotate(nb_no=Count("answer", filter=Q(answer__answer=Answer.NO)))
|
||||
.annotate(user_answer=Subquery(user_answers[:1]))
|
||||
.order_by("-nb_yes", "nb_no", "name")
|
||||
)
|
||||
|
||||
|
||||
class PropositionDelete(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
|
||||
model = Proposition
|
||||
template_name = "propositions/delete.html"
|
||||
success_url = reverse_lazy("propositions:list")
|
||||
|
||||
def test_func(self):
|
||||
proposition = self.get_object()
|
||||
user = self.request.user
|
||||
return (proposition.user == user or user.profile.is_chef)
|
||||
|
||||
|
||||
@login_required
|
||||
def answer(request, id, ans):
|
||||
proposition = get_object_or_404(Proposition, id=id)
|
||||
user = request.user
|
||||
Answer.objects.filter(proposition=proposition, user=user).delete()
|
||||
Answer.objects.create(proposition=proposition, user=user, answer=ans)
|
||||
return redirect("propositions:list")
|
|
@ -1,8 +1,8 @@
|
|||
Django==2.2.*
|
||||
|
||||
# Pour la prod
|
||||
#psycopg2
|
||||
gunicorn
|
||||
django-colorful
|
||||
Pillow
|
||||
django-avatar==5.0.*
|
||||
Django==2.2.28
|
||||
django-appconf==1.0.4
|
||||
django-avatar==5.0.0
|
||||
django-colorful==1.3
|
||||
gunicorn==20.1.0
|
||||
Pillow==9.2.0
|
||||
#Pour le prod
|
||||
#psycopg2==2.8.6
|
||||
|
|
Loading…
Reference in a new issue