add propositions back

This commit is contained in:
Lucie Galland 2022-03-22 15:05:51 +01:00
parent 5eb27e2171
commit aa430fa4d2
20 changed files with 639 additions and 0 deletions

View file

@ -35,6 +35,7 @@ ACCOUNT_CREATION_PASS = import_secret("ACCOUNT_CREATION_PASS")
BASE_DIR = os.path.join(os.path.dirname(__file__), "..", "..") BASE_DIR = os.path.join(os.path.dirname(__file__), "..", "..")
INSTALLED_APPS = [ INSTALLED_APPS = [
"propositions",
"trombonoscope", "trombonoscope",
"actu", "actu",
"colorful", "colorful",

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

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

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