From aa430fa4d2a7aad2e4ef111d46d121784cdc5789 Mon Sep 17 00:00:00 2001 From: Lucie Galland Date: Tue, 22 Mar 2022 15:05:51 +0100 Subject: [PATCH] add propositions back --- Ernestophone/settings/common.py | 1 + .../migrations/0008_auto_20220322_1454.py | 17 ++ propositions/__init__.py | 0 propositions/admin.py | 0 propositions/migrations/0001_initial.py | 38 ++++ .../migrations/0002_nom_verbose_name.py | 18 ++ .../0003_reponse_renaming_and_cleaning.py | 75 ++++++++ .../0004_prop_renaming_and_cleaning.py | 43 +++++ .../0005_remove_nb_yes_no_fields.py | 24 +++ .../0006_proposition_profile_to_user.py | 53 ++++++ .../migrations/0007_auto_20220322_1455.py | 19 ++ propositions/migrations/__init__.py | 0 propositions/models.py | 34 ++++ .../templates/propositions/create.html | 14 ++ .../templates/propositions/delete.html | 10 ++ .../templates/propositions/liste.html | 44 +++++ propositions/tests.py | 165 ++++++++++++++++++ propositions/urls.py | 13 ++ propositions/utils.py | 8 + propositions/views.py | 63 +++++++ 20 files changed, 639 insertions(+) create mode 100644 calendrier/migrations/0008_auto_20220322_1454.py create mode 100644 propositions/__init__.py create mode 100644 propositions/admin.py create mode 100644 propositions/migrations/0001_initial.py create mode 100644 propositions/migrations/0002_nom_verbose_name.py create mode 100644 propositions/migrations/0003_reponse_renaming_and_cleaning.py create mode 100644 propositions/migrations/0004_prop_renaming_and_cleaning.py create mode 100644 propositions/migrations/0005_remove_nb_yes_no_fields.py create mode 100644 propositions/migrations/0006_proposition_profile_to_user.py create mode 100644 propositions/migrations/0007_auto_20220322_1455.py create mode 100644 propositions/migrations/__init__.py create mode 100644 propositions/models.py create mode 100644 propositions/templates/propositions/create.html create mode 100644 propositions/templates/propositions/delete.html create mode 100644 propositions/templates/propositions/liste.html create mode 100644 propositions/tests.py create mode 100644 propositions/urls.py create mode 100644 propositions/utils.py create mode 100644 propositions/views.py diff --git a/Ernestophone/settings/common.py b/Ernestophone/settings/common.py index 05994aa..a75edd7 100644 --- a/Ernestophone/settings/common.py +++ b/Ernestophone/settings/common.py @@ -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", diff --git a/calendrier/migrations/0008_auto_20220322_1454.py b/calendrier/migrations/0008_auto_20220322_1454.py new file mode 100644 index 0000000..bbaadca --- /dev/null +++ b/calendrier/migrations/0008_auto_20220322_1454.py @@ -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'), + ), + ] diff --git a/propositions/__init__.py b/propositions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/propositions/admin.py b/propositions/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/propositions/migrations/0001_initial.py b/propositions/migrations/0001_initial.py new file mode 100644 index 0000000..344f68c --- /dev/null +++ b/propositions/migrations/0001_initial.py @@ -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)), + ], + ), + ] diff --git a/propositions/migrations/0002_nom_verbose_name.py b/propositions/migrations/0002_nom_verbose_name.py new file mode 100644 index 0000000..02d1ef9 --- /dev/null +++ b/propositions/migrations/0002_nom_verbose_name.py @@ -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'), + ), + ] diff --git a/propositions/migrations/0003_reponse_renaming_and_cleaning.py b/propositions/migrations/0003_reponse_renaming_and_cleaning.py new file mode 100644 index 0000000..6489374 --- /dev/null +++ b/propositions/migrations/0003_reponse_renaming_and_cleaning.py @@ -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"), + ] diff --git a/propositions/migrations/0004_prop_renaming_and_cleaning.py b/propositions/migrations/0004_prop_renaming_and_cleaning.py new file mode 100644 index 0000000..0a4efea --- /dev/null +++ b/propositions/migrations/0004_prop_renaming_and_cleaning.py @@ -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'), + ), + ] diff --git a/propositions/migrations/0005_remove_nb_yes_no_fields.py b/propositions/migrations/0005_remove_nb_yes_no_fields.py new file mode 100644 index 0000000..4f09c1e --- /dev/null +++ b/propositions/migrations/0005_remove_nb_yes_no_fields.py @@ -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", + ), + ), + ] diff --git a/propositions/migrations/0006_proposition_profile_to_user.py b/propositions/migrations/0006_proposition_profile_to_user.py new file mode 100644 index 0000000..d0ebd6a --- /dev/null +++ b/propositions/migrations/0006_proposition_profile_to_user.py @@ -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", + ), + ), + ] diff --git a/propositions/migrations/0007_auto_20220322_1455.py b/propositions/migrations/0007_auto_20220322_1455.py new file mode 100644 index 0000000..f5e278e --- /dev/null +++ b/propositions/migrations/0007_auto_20220322_1455.py @@ -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'), + ), + ] diff --git a/propositions/migrations/__init__.py b/propositions/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/propositions/models.py b/propositions/models.py new file mode 100644 index 0000000..1c6dcfa --- /dev/null +++ b/propositions/models.py @@ -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" diff --git a/propositions/templates/propositions/create.html b/propositions/templates/propositions/create.html new file mode 100644 index 0000000..0d72e5d --- /dev/null +++ b/propositions/templates/propositions/create.html @@ -0,0 +1,14 @@ +{% extends "gestion/base.html" %} + +{% block titre %}Proposition de morceau{% endblock %} + +{% block content %} + +

Retour aux propositions

+
+ {% csrf_token %} + {{ form.as_p }} + +
+ +{% endblock %} diff --git a/propositions/templates/propositions/delete.html b/propositions/templates/propositions/delete.html new file mode 100644 index 0000000..27645af --- /dev/null +++ b/propositions/templates/propositions/delete.html @@ -0,0 +1,10 @@ +{% extends "gestion/base.html" %} + +{% block titre %}Suppression d'une proposition{% endblock %} +{% block content %}
+ {% csrf_token %} +

Retour aux propositions

+

Voulez vous vraiment supprimer la proposition {{ object }}?

+ +
+{% endblock %} diff --git a/propositions/templates/propositions/liste.html b/propositions/templates/propositions/liste.html new file mode 100644 index 0000000..02e5ca1 --- /dev/null +++ b/propositions/templates/propositions/liste.html @@ -0,0 +1,44 @@ +{% extends "gestion/base.html" %} + +{% block titre %}Propositions de morceau{% endblock %} + +{% block content %} +

Liste des propositions

+ +

Proposer un morceau

+ + {% if propositions.exists %} + + + + + + + + + + + {% for p in propositions %} + + + + + + + + + + {% endfor %} +
OuiNon
+ {% if p.link %}{% endif %} + {{ p.name }}{% if p.artist %} - {{ p.artist }}{% endif %} + {% if p.link %}{% endif %} + {{ p.nb_yes }}{{ p.nb_no }}OuiNon{% if p.user_answer %}Vous avez voté {{ p.user_answer }}{% endif %} + {% if p.user == request.user or request.user.profile.is_chef %} + Supprimer + {% endif %} +
+ {% else %} + Pas de proposition pour le moment + {% endif %} +{% endblock %} diff --git a/propositions/tests.py b/propositions/tests.py new file mode 100644 index 0000000..087feeb --- /dev/null +++ b/propositions/tests.py @@ -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] + ) diff --git a/propositions/urls.py b/propositions/urls.py new file mode 100644 index 0000000..01d37c6 --- /dev/null +++ b/propositions/urls.py @@ -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("/oui", views.answer, {"ans": "oui"}, name=Answer.YES), + path("/non", views.answer, {"ans": "non"}, name=Answer.NO), + path("/supprimer", views.PropositionDelete.as_view(), name="delete"), +] diff --git a/propositions/utils.py b/propositions/utils.py new file mode 100644 index 0000000..fb81d5b --- /dev/null +++ b/propositions/utils.py @@ -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) diff --git a/propositions/views.py b/propositions/views.py new file mode 100644 index 0000000..d7f39e1 --- /dev/null +++ b/propositions/views.py @@ -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")