Compare commits

...

15 commits

Author SHA1 Message Date
Sinavir
98a6f98fda Fireworks hotfix 2024-11-13 10:50:46 +01:00
sinavir
0aae767a3e
fireworks 2024-11-13 10:35:37 +01:00
sinavir
d88768311e
cvec 2024-11-08 14:33:16 +01:00
sinavir
abc6c62a89 make forgotten migration 2024-06-15 15:03:37 +02:00
sinavir
3e065f30c0 Bugfix 2024-06-15 14:57:39 +02:00
sinavir
0ee798a64e Improve requirements.txt 2024-06-15 14:28:41 +02:00
sinavir
1e8b993739 Revert "add poll for 10y anniversary"
This reverts commit 56d7490879.
2022-10-26 21:57:01 +02:00
Lucie Galland
8398805b54 proposition migration 2022-09-14 12:45:30 +02:00
sinavir
56d7490879 add poll for 10y anniversary 2022-09-14 10:24:32 +02:00
Lucie Galland
aa430fa4d2 add propositions back 2022-03-22 15:05:51 +01:00
Lucie Galland
5eb27e2171 Merge branch 'mdebray/date_des_reponses_aux_sondages' into 'master'
Date des reponses aux sondages

See merge request klub-dev-ens/Ernesto!22
2022-03-15 11:29:37 +01:00
Lucie Galland
31f6dc0961 bug fix 2022-03-15 11:28:44 +01:00
Maurice Debray
764ffea61f on est moins laxiste 2022-03-15 11:10:55 +01:00
Maurice Debray
7122c1c0c4 black puis isort 2022-03-15 01:08:18 +01:00
Maurice Debray
14e8d963d6 Ajout de la date de création/mise à jour dans le modèle Participants 2022-03-15 01:04:01 +01:00
32 changed files with 773 additions and 21 deletions

View file

@ -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",

View file

@ -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)

View file

@ -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):

View 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"
),
),
]

View 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'),
),
]

View file

@ -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') ]

View file

@ -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 %}

View file

@ -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

View file

@ -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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View file

@ -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
View file

0
propositions/admin.py Normal file
View file

View 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)),
],
),
]

View 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'),
),
]

View file

@ -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"),
]

View 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'),
),
]

View 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",
),
),
]

View 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",
),
),
]

View 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'),
),
]

View 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'),
),
]

View 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'),
),
]

View file

34
propositions/models.py Normal file
View 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"

View 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 %}

View 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 %}

View 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
View 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
View 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
View 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
View 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")

View file

@ -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