From 245a2503e2b421fa7ce777827e37c3ac7c5d639c Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sat, 14 Jun 2025 10:35:42 +0200 Subject: [PATCH 1/2] Refactor group and music game models to use UniqueConstraint; update form error handling in templates --- base/static/css/main.css | 6 +++ base/templates/base/form.html | 14 ++--- ...13_alter_group_unique_together_and_more.py | 53 +++++++++++++++++++ game/models.py | 18 +++++-- game/views.py | 7 ++- 5 files changed, 84 insertions(+), 14 deletions(-) create mode 100644 game/migrations/0013_alter_group_unique_together_and_more.py diff --git a/base/static/css/main.css b/base/static/css/main.css index 60f2420..e916ab0 100644 --- a/base/static/css/main.css +++ b/base/static/css/main.css @@ -64,3 +64,9 @@ article.message { color: var(--pico-color-red-500); } } + +.form-error::before { + margin-right: .5em; + font-family: remixicon; + content: "\eca0"; +} diff --git a/base/templates/base/form.html b/base/templates/base/form.html index df40bc7..979daa3 100644 --- a/base/templates/base/form.html +++ b/base/templates/base/form.html @@ -1,8 +1,4 @@ -{% if form.non_field_errors %} - -{% endif %} +{% for error in form.non_field_errors %}
{{ error }}
{% endfor %}
{% csrf_token %}
@@ -10,12 +6,10 @@ - {% if field.errors %} -
    - {% for error in field.errors %}
  • {{ error }}
  • {% endfor %} -
- {% endif %} {% endfor %}
diff --git a/game/migrations/0013_alter_group_unique_together_and_more.py b/game/migrations/0013_alter_group_unique_together_and_more.py new file mode 100644 index 0000000..438a287 --- /dev/null +++ b/game/migrations/0013_alter_group_unique_together_and_more.py @@ -0,0 +1,53 @@ +# Generated by Django 5.2.3 on 2025-06-14 08:13 + +import django.db.models.functions.text +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("game", "0012_alter_musikgame_playlist"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterUniqueTogether( + name="group", + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name="musicgameorder", + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name="musicvideo", + unique_together=set(), + ), + migrations.AddConstraint( + model_name="group", + constraint=models.UniqueConstraint( + django.db.models.functions.text.Lower("name"), + models.F("owner"), + name="unique_group_name", + ), + ), + migrations.AddConstraint( + model_name="musicgameorder", + constraint=models.UniqueConstraint( + fields=("game", "player", "music_video"), name="unique_music_in_game" + ), + ), + migrations.AddConstraint( + model_name="musicgameorder", + constraint=models.UniqueConstraint( + fields=("game", "order"), name="unique_order" + ), + ), + migrations.AddConstraint( + model_name="musicvideo", + constraint=models.UniqueConstraint( + fields=("yt_id", "owner", "group"), name="unique_music_in_group" + ), + ), + ] diff --git a/game/models.py b/game/models.py index 8383915..886bd21 100644 --- a/game/models.py +++ b/game/models.py @@ -1,5 +1,6 @@ from django.contrib.auth.models import User from django.db import models +from django.db.models.functions import Lower from django.urls import reverse @@ -19,7 +20,9 @@ class Group(models.Model): return reverse("group_detail", kwargs={"pk": self.pk}) class Meta: - unique_together = ["name", "owner"] + constraints = [ + models.UniqueConstraint(Lower("name"), "owner", name="unique_group_name") + ] class MusicVideo(models.Model): @@ -31,7 +34,11 @@ class MusicVideo(models.Model): blacklisted = models.BooleanField(default=False) class Meta: - unique_together = ["yt_id", "owner", "group"] + constraints = [ + models.UniqueConstraint( + fields=("yt_id", "owner", "group"), name="unique_music_in_group" + ) + ] class MusikGame(models.Model): @@ -52,5 +59,10 @@ class MusicGameOrder(models.Model): order = models.PositiveIntegerField() class Meta: - unique_together = [["game", "player", "music_video"], ["game", "order"]] + constraints = [ + models.UniqueConstraint( + fields=("game", "player", "music_video"), name="unique_music_in_game" + ), + models.UniqueConstraint(fields=("game", "order"), name="unique_order"), + ] ordering = ["order"] diff --git a/game/views.py b/game/views.py index 1b188e5..0c0dad3 100644 --- a/game/views.py +++ b/game/views.py @@ -7,6 +7,7 @@ from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.models import User from django.contrib.messages.views import SuccessMessageMixin +from django.db import IntegrityError from django.db.models import Count, Q from django.http import JsonResponse from django.shortcuts import get_object_or_404, redirect @@ -40,7 +41,11 @@ class GroupMixin: class GroupCreateView(LoginRequiredMixin, GroupMixin, CreateView): def form_valid(self, form): form.instance.owner = self.request.user - return super().form_valid(form) + try: + return super().form_valid(form) + except IntegrityError: + form.add_error("name", "Ce nom de groupe existe déjà.") + return super().form_invalid(form) class GroupUpdateView(OwnerFilterMixin, GroupMixin, UpdateView): From 0859b36f9849445fb0de2f9e6243e2a5eb5337bb Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sat, 14 Jun 2025 11:01:48 +0200 Subject: [PATCH 2/2] Enhance message handling in group views; add user feedback for actions and errors Fix #4 --- game/views.py | 86 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 65 insertions(+), 21 deletions(-) diff --git a/game/views.py b/game/views.py index 0c0dad3..a24c1c6 100644 --- a/game/views.py +++ b/game/views.py @@ -4,13 +4,13 @@ import google.oauth2.credentials import google_auth_oauthlib import googleapiclient.discovery from django.conf import settings +from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.models import User from django.contrib.messages.views import SuccessMessageMixin from django.db import IntegrityError from django.db.models import Count, Q -from django.http import JsonResponse -from django.shortcuts import get_object_or_404, redirect +from django.shortcuts import redirect from django.views import View from django.views.generic.detail import DetailView, SingleObjectMixin from django.views.generic.edit import CreateView, DeleteView, UpdateView @@ -38,9 +38,8 @@ class GroupMixin: fields = ["name"] -class GroupCreateView(LoginRequiredMixin, GroupMixin, CreateView): +class GroupIntegrityMixin: def form_valid(self, form): - form.instance.owner = self.request.user try: return super().form_valid(form) except IntegrityError: @@ -48,7 +47,13 @@ class GroupCreateView(LoginRequiredMixin, GroupMixin, CreateView): return super().form_invalid(form) -class GroupUpdateView(OwnerFilterMixin, GroupMixin, UpdateView): +class GroupCreateView(LoginRequiredMixin, GroupMixin, GroupIntegrityMixin, CreateView): + def form_valid(self, form): + form.instance.owner = self.request.user + return super().form_valid(form) + + +class GroupUpdateView(OwnerFilterMixin, GroupMixin, GroupIntegrityMixin, UpdateView): pass @@ -84,14 +89,26 @@ class GroupAddMusicView(MemberFilterMixin, SingleObjectMixin, View): group = self.get_object() yt_id = request.POST.get("yt_id") if not yt_id: - return JsonResponse({"error": "You must provide a YouTube ID."}, status=400) + messages.add_message(request, messages.ERROR, "Aucun identifiant donné") + return redirect(group) yt_id = utils.parse_musik(yt_id) title = utils.get_yt_title(yt_id) if not title: - return JsonResponse({"error": "Invalid YouTube ID."}, status=400) - group.musicvideo_set.create(yt_id=yt_id, title=title, owner=request.user) - group.save() + messages.add_message( + request, messages.ERROR, f"Vidéo Youtube invalide : {yt_id}" + ) + return redirect(group) + try: + group.musicvideo_set.create(yt_id=yt_id, title=title, owner=request.user) + except IntegrityError: + messages.add_message( + request, messages.ERROR, f"Vidéo Youtube déjà ajoutée : {yt_id}" + ) + + messages.add_message( + request, messages.SUCCESS, f"Vidéo Youtube ajoutée : {yt_id}" + ) return redirect(group) @@ -102,6 +119,15 @@ class GroupAddMemberView(OwnerFilterMixin, SingleObjectMixin, View): group = self.get_object() username = request.POST.get("username") user = User.objects.get(username=username) + if user == group.owner: + messages.add_message( + request, messages.WARNING, f"{user} est le propriétaire du groupe." + ) + return redirect(group) + if user in group.members.all(): + messages.add_message( + request, messages.WARNING, f"{user} est déjà membre du groupe." + ) group.members.add(user) return redirect(group) @@ -117,17 +143,25 @@ class GroupRemoveMusicView(OwnerFilterMixin, SingleObjectMixin, View): return redirect(group) -class GroupRemoveMemberView(View): - def get(self, request, pk, user_pk): - relation = get_object_or_404( - models.Group.members.through, - group_id=pk, - user_id=user_pk, - group__owner=request.user, - ) +class GroupRemoveMemberView(OwnerFilterMixin, SingleObjectMixin, View): + model = models.Group + + def get(self, request, pk, user_pk): + group = self.get_object() + user = User.objects.get(pk=user_pk) + + relation = models.Group.members.through.objects.filter( + group=group, user=user + ).first() + if not relation: + messages.add_message( + request, + messages.ERROR, + f"L'utilisateur {user} n'est pas membre du groupe.", + ) + else: + relation.delete() - group = relation.group - relation.delete() return redirect(group) @@ -142,6 +176,11 @@ class GroupRemoveGameView(SingleObjectMixin, SuccessMessageMixin, View): game = self.get_object() group = game.group game.delete() + messages.add_message( + request, + messages.SUCCESS, + f"Le jeu du {game.date.strftime('%x')} a été supprimé avec succès.", + ) return redirect(group) @@ -255,9 +294,11 @@ class YoutubeLoginView(LoginRequiredMixin, View): class YoutubeCallbackView(LoginRequiredMixin, View): def get(self, request): - if request.GET.get("error"): + if err := request.GET.get("error"): + messages.add_message( + request, messages.ERROR, f"Échec de la connexion à Youtube : {err}" + ) return redirect("/") - print(request.GET) state = self.request.session.get("state") flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file( @@ -283,6 +324,8 @@ class YoutubeCallbackView(LoginRequiredMixin, View): } }, ) + + messages.add_message(request, messages.SUCCESS, "Connexion à Youtube réussie.") return redirect("/") @@ -292,4 +335,5 @@ class GroupClearBlacklistView(OwnerFilterMixin, SingleObjectMixin, View): def get(self, request, pk): group = self.get_object() group.musicvideo_set.filter(blacklisted=True).update(blacklisted=False) + messages.add_message(request, messages.SUCCESS, "La blacklist a été vidée.") return redirect(group)