From 6dbb1a54e01bb928dcb84591523fe72240d2601b Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sun, 15 Jun 2025 12:04:28 +0200 Subject: [PATCH 01/31] Add MusicGameAnswer model with unique constraints and ordering --- game/migrations/0018_musicgameanswer.py | 60 +++++++++++++++++++++++++ game/models.py | 17 +++++++ 2 files changed, 77 insertions(+) create mode 100644 game/migrations/0018_musicgameanswer.py diff --git a/game/migrations/0018_musicgameanswer.py b/game/migrations/0018_musicgameanswer.py new file mode 100644 index 0000000..c1d8503 --- /dev/null +++ b/game/migrations/0018_musicgameanswer.py @@ -0,0 +1,60 @@ +# Generated by Django 5.2.3 on 2025-06-15 09:56 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("game", "0017_youtubecredentials_title"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="MusicGameAnswer", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("order", models.PositiveIntegerField()), + ( + "answer", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "game", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="game.musikgame" + ), + ), + ( + "player", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "ordering": ["order"], + "constraints": [ + models.UniqueConstraint( + fields=("game", "player", "order"), name="unique_answer" + ) + ], + }, + ), + ] diff --git a/game/models.py b/game/models.py index 1f8ce2f..99a5d6b 100644 --- a/game/models.py +++ b/game/models.py @@ -111,3 +111,20 @@ class MusicGameOrder(models.Model): models.UniqueConstraint(fields=("game", "order"), name="unique_order"), ] ordering = ["order"] + + +class MusicGameAnswer(models.Model): + game = models.ForeignKey(MusikGame, on_delete=models.CASCADE) + player = models.ForeignKey(User, on_delete=models.CASCADE) + order = models.PositiveIntegerField() + answer = models.ForeignKey( + User, on_delete=models.SET_NULL, null=True, related_name="+" + ) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=("game", "player", "order"), name="unique_answer" + ), + ] + ordering = ["order"] From d03d3b48d41110b34816e3fdcc9d1b2c365afbb7 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sun, 15 Jun 2025 12:40:55 +0200 Subject: [PATCH 02/31] Add game answer functionality with form and view for user responses --- base/static/css/main.css | 4 +++ game/forms.py | 19 ++++++++++++ game/migrations/0018_musicgameanswer.py | 10 +++--- game/models.py | 9 ++---- .../templates/game/include/group_members.html | 2 +- game/templates/game/musikgame_answer.html | 26 ++++++++++++++++ game/templates/game/musikgame_detail.html | 3 ++ game/urls.py | 5 +++ game/views.py | 31 +++++++++++++++++++ 9 files changed, 97 insertions(+), 12 deletions(-) create mode 100644 game/templates/game/musikgame_answer.html diff --git a/base/static/css/main.css b/base/static/css/main.css index c6146bc..90e5032 100644 --- a/base/static/css/main.css +++ b/base/static/css/main.css @@ -130,3 +130,7 @@ td.c, th.c { margin: 0; } } + +table select { + margin-bottom: 0; +} diff --git a/game/forms.py b/game/forms.py index 76dec01..c90e286 100644 --- a/game/forms.py +++ b/game/forms.py @@ -39,3 +39,22 @@ class MusikGameForm(forms.ModelForm): kwargs["initial"].setdefault("players", players) super().__init__(*args, **kwargs) self.fields["players"].queryset = players + + +class AnswerForm(forms.Form): + def __init__(self, *args, **kwargs): + game = kwargs.pop("game") + user = kwargs.pop("user") + super().__init__(*args, **kwargs) + + for music in game.musicgameorder_set.all(): + self.fields[f"answer-{music.order}"] = forms.ChoiceField( + choices=[("", "")] + + list(game.players.all().values_list("id", "username")), + required=False, + label=music.order, + initial=ma.answer.id + if (ma := music.musicgameanswer_set.filter(player=user).first()) + and ma.answer + else "", + ) diff --git a/game/migrations/0018_musicgameanswer.py b/game/migrations/0018_musicgameanswer.py index c1d8503..1f760be 100644 --- a/game/migrations/0018_musicgameanswer.py +++ b/game/migrations/0018_musicgameanswer.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.3 on 2025-06-15 09:56 +# Generated by Django 5.2.3 on 2025-06-15 10:36 import django.db.models.deletion from django.conf import settings @@ -24,7 +24,6 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ("order", models.PositiveIntegerField()), ( "answer", models.ForeignKey( @@ -37,7 +36,8 @@ class Migration(migrations.Migration): ( "game", models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="game.musikgame" + on_delete=django.db.models.deletion.CASCADE, + to="game.musicgameorder", ), ), ( @@ -49,10 +49,10 @@ class Migration(migrations.Migration): ), ], options={ - "ordering": ["order"], + "ordering": ["game"], "constraints": [ models.UniqueConstraint( - fields=("game", "player", "order"), name="unique_answer" + fields=("game", "player"), name="unique_answer" ) ], }, diff --git a/game/models.py b/game/models.py index 99a5d6b..6f4e672 100644 --- a/game/models.py +++ b/game/models.py @@ -114,17 +114,14 @@ class MusicGameOrder(models.Model): class MusicGameAnswer(models.Model): - game = models.ForeignKey(MusikGame, on_delete=models.CASCADE) + game = models.ForeignKey(MusicGameOrder, on_delete=models.CASCADE) player = models.ForeignKey(User, on_delete=models.CASCADE) - order = models.PositiveIntegerField() answer = models.ForeignKey( User, on_delete=models.SET_NULL, null=True, related_name="+" ) class Meta: constraints = [ - models.UniqueConstraint( - fields=("game", "player", "order"), name="unique_answer" - ), + models.UniqueConstraint(fields=("game", "player"), name="unique_answer"), ] - ordering = ["order"] + ordering = ["game"] diff --git a/game/templates/game/include/group_members.html b/game/templates/game/include/group_members.html index 3b668e5..daf3059 100644 --- a/game/templates/game/include/group_members.html +++ b/game/templates/game/include/group_members.html @@ -12,7 +12,7 @@ - + diff --git a/game/templates/game/musikgame_answer.html b/game/templates/game/musikgame_answer.html new file mode 100644 index 0000000..932d55e --- /dev/null +++ b/game/templates/game/musikgame_answer.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} +{% block content %} +

Mes réponses

+
+ {% csrf_token %} + + + + + + + + + {% for field in form %} + + + + + {% endfor %} + +
+ + Réponse
{{ field.label }}{{ field }}
+ +
+{% endblock content %} diff --git a/game/templates/game/musikgame_detail.html b/game/templates/game/musikgame_detail.html index 444c7f1..77f424c 100644 --- a/game/templates/game/musikgame_detail.html +++ b/game/templates/game/musikgame_detail.html @@ -10,6 +10,9 @@ href="{% yt_playlist musikgame %}" role="button" {% if musikgame.playlist_loading %}aria-busy="true"{% endif %}> Playlist + Répondre

{% endif %}

diff --git a/game/urls.py b/game/urls.py index 6ec4cfa..15b8d3e 100644 --- a/game/urls.py +++ b/game/urls.py @@ -63,4 +63,9 @@ urlpatterns = [ views.GroupClearBlacklistView.as_view(), name="group_clear_blacklist", ), + path( + "group/game//answer/", + views.GameAnswerView.as_view(), + name="game_answer", + ), ] diff --git a/game/views.py b/game/views.py index 3716424..3ebb49b 100644 --- a/game/views.py +++ b/game/views.py @@ -421,3 +421,34 @@ class GroupClearBlacklistView(MemberFilterMixin, SingleObjectMixin, View): group.musicvideo_set.filter(blacklisted=True).update(blacklisted=False) messages.add_message(request, messages.SUCCESS, "La blacklist a été effacée.") return redirect(group) + + +class GameAnswerView(LoginRequiredMixin, DetailView): + model = models.MusikGame + template_name = "game/musikgame_answer.html" + + def get_queryset(self): + return super().get_queryset().filter(players=self.request.user) + + def get_context_data(self, **kwargs): + return super().get_context_data(**kwargs) | { + "form": forms.AnswerForm(game=self.object, user=self.request.user) + } + + def post(self, request, pk): + game = self.get_object() + for music in game.musicgameorder_set.all(): + answer = request.POST.get(f"answer-{music.order}") + if answer: + models.MusicGameAnswer.objects.update_or_create( + game=music, + player=request.user, + defaults={"answer": game.players.get(pk=answer)}, + ) + else: + models.MusicGameAnswer.objects.update_or_create( + game=music, + player=request.user, + defaults={"answer": None}, + ) + return redirect("game_answer", pk) From f9ed70d386ca4421d2cb12bbcb5733d085b6049b Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sun, 15 Jun 2025 13:02:31 +0200 Subject: [PATCH 03/31] Add game end functionality with view and URL routing, update models and templates for game state management --- base/static/css/main.css | 3 + ..._alter_musikgame_options_musikgame_over.py | 21 ++++ .../0020_alter_musikgame_options.py | 16 +++ game/models.py | 4 + game/templates/game/group_detail.html | 47 +------- game/templates/game/include/group_games.html | 57 ++++++++++ game/templates/game/musikgame_detail.html | 101 ++++++++++-------- game/urls.py | 1 + game/views.py | 20 +++- 9 files changed, 181 insertions(+), 89 deletions(-) create mode 100644 game/migrations/0019_alter_musikgame_options_musikgame_over.py create mode 100644 game/migrations/0020_alter_musikgame_options.py create mode 100644 game/templates/game/include/group_games.html diff --git a/base/static/css/main.css b/base/static/css/main.css index 90e5032..219394c 100644 --- a/base/static/css/main.css +++ b/base/static/css/main.css @@ -117,6 +117,9 @@ h6, i.i { margin-right: .5em; } +i.hl { + color: var(--pico-primary); +} footer { text-align: center; diff --git a/game/migrations/0019_alter_musikgame_options_musikgame_over.py b/game/migrations/0019_alter_musikgame_options_musikgame_over.py new file mode 100644 index 0000000..f5e4489 --- /dev/null +++ b/game/migrations/0019_alter_musikgame_options_musikgame_over.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.3 on 2025-06-15 10:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("game", "0018_musicgameanswer"), + ] + + operations = [ + migrations.AlterModelOptions( + name="musikgame", + options={"ordering": ["-over", "-date"]}, + ), + migrations.AddField( + model_name="musikgame", + name="over", + field=models.BooleanField(default=False), + ), + ] diff --git a/game/migrations/0020_alter_musikgame_options.py b/game/migrations/0020_alter_musikgame_options.py new file mode 100644 index 0000000..cd933d9 --- /dev/null +++ b/game/migrations/0020_alter_musikgame_options.py @@ -0,0 +1,16 @@ +# Generated by Django 5.2.3 on 2025-06-15 10:59 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("game", "0019_alter_musikgame_options_musikgame_over"), + ] + + operations = [ + migrations.AlterModelOptions( + name="musikgame", + options={"ordering": ["over", "-date"]}, + ), + ] diff --git a/game/models.py b/game/models.py index 6f4e672..a4e1f91 100644 --- a/game/models.py +++ b/game/models.py @@ -74,10 +74,14 @@ class MusikGame(models.Model): players = models.ManyToManyField(User, verbose_name="Joueurs") playlist = models.CharField(blank=True, verbose_name="Playlist YouTube") playlist_loading = models.BooleanField(default=False) + over = models.BooleanField(default=False) def get_absolute_url(self): return reverse("game_detail", kwargs={"pk": self.pk}) + class Meta: + ordering = ["over", "-date"] + @receiver(post_save, sender=MusikGame) def generateYoutubePlaylist(sender, instance, created, **kwargs): diff --git a/game/templates/game/group_detail.html b/game/templates/game/group_detail.html index d523f29..3812b6e 100644 --- a/game/templates/game/group_detail.html +++ b/game/templates/game/group_detail.html @@ -10,52 +10,7 @@ {{ group.name }}

{% include "game/include/group_buttons.html" %} - {% if group.musikgame_set.exists %} -

- Parties -

-
- {% csrf_token %} - - - {% if group.owner == user %}{% endif %} - - - - - - {% for game in group.musikgame_set.all %} - - {% if group.owner == user %} - - {% endif %} - - - - - {% endfor %} - -
Date - Playlists - Joueurs
- - - {{ game.date }} - - {% if game.playlist %} - Playlist - {% endif %} - {{ game.players.all|join:", " }}
- {% if group.owner == user %} - - {% endif %} -
- {% endif %} + {% include "game/include/group_games.html" %} {% include "game/include/group_members.html" %}

Mes musiques {{ musics.count }} diff --git a/game/templates/game/include/group_games.html b/game/templates/game/include/group_games.html new file mode 100644 index 0000000..09ad49a --- /dev/null +++ b/game/templates/game/include/group_games.html @@ -0,0 +1,57 @@ +{% load form youtube %} +{% if group.musikgame_set.exists %} +

+ Parties +

+
+ {% csrf_token %} + + + {% if group.owner == user %}{% endif %} + + + + + + + {% for game in group.musikgame_set.all %} + + {% if group.owner == user %} + + {% endif %} + + + + + + {% endfor %} + +
+ + Date + Playlists + Joueurs
+ + + {% if game.over %} + + {% else %} + + {% endif %} + + {{ game.date }} + + {% if game.playlist %} + Playlist + {% endif %} + {{ game.players.all|join:", " }}
+ {% if group.owner == user %} + + {% endif %} +
+{% endif %} diff --git a/game/templates/game/musikgame_detail.html b/game/templates/game/musikgame_detail.html index 77f424c..0d84cda 100644 --- a/game/templates/game/musikgame_detail.html +++ b/game/templates/game/musikgame_detail.html @@ -2,18 +2,33 @@ {% load youtube %} {% block content %}

- {{ musikgame.date }} + {% if musikgame.over %} + + {% else %} + + {% endif %} + {{ musikgame.date }}

{% if musikgame.playlist or musikgame.playlist_loading %} -

- Playlist - Répondre -

+
+ {% csrf_token %} +
+ Playlist + {% if not musikgame.over %} + Répondre + {% endif %} +
+ {% if is_leader and not musikgame.over %} +
+ +
+ {% endif %} +
{% endif %}

Joueurs @@ -34,38 +49,40 @@ {% endfor %} -

- Résultats -

-
- - Résultats - - - - - - - - - - - {% for music in musikgame.musicgameorder_set.all %} + {% if musikgame.over %} +

+ Résultats +

+
+ + Résultats + +
- - - Musique - - Joueur -
+ - - - + + + - {% endfor %} - -
{{ music.order }} - {{ music.music_video.title }} - {{ music.player }} + + + Musique + + Joueur +
-
+ + + {% for music in musikgame.musicgameorder_set.all %} + + {{ music.order }} + + {{ music.music_video.title }} + + {{ music.player }} + + {% endfor %} + + + + {% endif %} {% endblock content %} diff --git a/game/urls.py b/game/urls.py index 15b8d3e..927dc79 100644 --- a/game/urls.py +++ b/game/urls.py @@ -68,4 +68,5 @@ urlpatterns = [ views.GameAnswerView.as_view(), name="game_answer", ), + path("group/game//end/", views.GameEndView.as_view(), name="game_end"), ] diff --git a/game/views.py b/game/views.py index 3ebb49b..1e1cae0 100644 --- a/game/views.py +++ b/game/views.py @@ -344,6 +344,12 @@ class GameDetailView(LoginRequiredMixin, DetailView): .distinct() ) + def get_context_data(self, **kwargs): + data = super().get_context_data(**kwargs) + data["is_leader"] = data["musikgame"].group.is_leader(self.request.user) + data["is_owner"] = data["musikgame"].group.is_owner(self.request.user) + return data + class YoutubeCallbackView(LoginRequiredMixin, View): def get(self, request): @@ -428,7 +434,7 @@ class GameAnswerView(LoginRequiredMixin, DetailView): template_name = "game/musikgame_answer.html" def get_queryset(self): - return super().get_queryset().filter(players=self.request.user) + return super().get_queryset().filter(over=False, players=self.request.user) def get_context_data(self, **kwargs): return super().get_context_data(**kwargs) | { @@ -452,3 +458,15 @@ class GameAnswerView(LoginRequiredMixin, DetailView): defaults={"answer": None}, ) return redirect("game_answer", pk) + + +class GameEndView(LoginRequiredMixin, SingleObjectMixin, View): + model = models.MusikGame + + def post(self, request, pk): + game = self.get_object() + if not game.group.is_leader(request.user): + raise PermissionDenied() + game.over = True + game.save() + return redirect("game_detail", pk) From 33896916e6529c9647189d49e105e62f6a9c821d Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sun, 15 Jun 2025 13:04:05 +0200 Subject: [PATCH 04/31] Open results section by default in musikgame detail template --- game/templates/game/musikgame_detail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/templates/game/musikgame_detail.html b/game/templates/game/musikgame_detail.html index 0d84cda..84d998d 100644 --- a/game/templates/game/musikgame_detail.html +++ b/game/templates/game/musikgame_detail.html @@ -53,7 +53,7 @@

Résultats

-
+
Résultats From b1ec960dfa2a0a85659ca11f658211e76bf2a511 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sun, 15 Jun 2025 13:12:24 +0200 Subject: [PATCH 05/31] Add podium styling and conditional rendering for players in musikgame detail template --- base/static/css/main.css | 24 +++++++++++++++++++++++ game/templates/game/musikgame_detail.html | 8 +++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/base/static/css/main.css b/base/static/css/main.css index 219394c..f1133eb 100644 --- a/base/static/css/main.css +++ b/base/static/css/main.css @@ -137,3 +137,27 @@ td.c, th.c { table select { margin-bottom: 0; } + +.podium { + > :first-child { + font-weight: 900; + font-size: 1.25em; + &::marker { + color: var(--pico-color-amber-200); + } + } + > :nth-child(2) { + font-weight: 800; + font-size: 1.1em; + &::marker { + color: var(--pico-color-grey-300); + } + } + > :nth-child(3) { + font-weight: 600; + font-size: 1.1em; + &::marker { + color: var(--pico-color-sand-300); + } + } +} diff --git a/game/templates/game/musikgame_detail.html b/game/templates/game/musikgame_detail.html index 84d998d..f4720ef 100644 --- a/game/templates/game/musikgame_detail.html +++ b/game/templates/game/musikgame_detail.html @@ -33,7 +33,13 @@

Joueurs

-

{{ musikgame.players.all|join:", " }}

+ {% if musikgame.over %} +
    + {% for player in musikgame.players.all %}
  1. {{ player.username }}
  2. {% endfor %} +
+ {% else %} +

{{ musikgame.players.all|join:", " }}

+ {% endif %}

Musiques

From 303538bf487fa29a2756a5f4e93be8dc95425653 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sun, 15 Jun 2025 13:34:37 +0200 Subject: [PATCH 06/31] Add MusicGameResults model and score calculation logic in GameEndView --- base/static/css/main.css | 6 ++ .../0020_alter_musikgame_options.py | 16 ------ ...lter_musikgame_options_musicgameresults.py | 55 +++++++++++++++++++ .../0021_alter_musicgameresults_score.py | 17 ++++++ game/models.py | 12 ++++ game/templates/game/musikgame_detail.html | 6 +- game/views.py | 22 +++++++- 7 files changed, 116 insertions(+), 18 deletions(-) delete mode 100644 game/migrations/0020_alter_musikgame_options.py create mode 100644 game/migrations/0020_alter_musikgame_options_musicgameresults.py create mode 100644 game/migrations/0021_alter_musicgameresults_score.py diff --git a/base/static/css/main.css b/base/static/css/main.css index f1133eb..80a38b2 100644 --- a/base/static/css/main.css +++ b/base/static/css/main.css @@ -160,4 +160,10 @@ table select { color: var(--pico-color-sand-300); } } + + .score { + font-weight: 900; + color: var(--pico-color-zinc-500); + margin-left: .5em; + } } diff --git a/game/migrations/0020_alter_musikgame_options.py b/game/migrations/0020_alter_musikgame_options.py deleted file mode 100644 index cd933d9..0000000 --- a/game/migrations/0020_alter_musikgame_options.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 5.2.3 on 2025-06-15 10:59 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("game", "0019_alter_musikgame_options_musikgame_over"), - ] - - operations = [ - migrations.AlterModelOptions( - name="musikgame", - options={"ordering": ["over", "-date"]}, - ), - ] diff --git a/game/migrations/0020_alter_musikgame_options_musicgameresults.py b/game/migrations/0020_alter_musikgame_options_musicgameresults.py new file mode 100644 index 0000000..3af5916 --- /dev/null +++ b/game/migrations/0020_alter_musikgame_options_musicgameresults.py @@ -0,0 +1,55 @@ +# Generated by Django 5.2.3 on 2025-06-15 11:23 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("game", "0019_alter_musikgame_options_musikgame_over"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterModelOptions( + name="musikgame", + options={"ordering": ["over", "-date"]}, + ), + migrations.CreateModel( + name="MusicGameResults", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("score", models.PositiveIntegerField(default=0)), + ( + "game", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="game.musikgame" + ), + ), + ( + "player", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "ordering": ["-score"], + "constraints": [ + models.UniqueConstraint( + fields=("game", "player"), name="unique_result" + ) + ], + }, + ), + ] diff --git a/game/migrations/0021_alter_musicgameresults_score.py b/game/migrations/0021_alter_musicgameresults_score.py new file mode 100644 index 0000000..3166d4d --- /dev/null +++ b/game/migrations/0021_alter_musicgameresults_score.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.3 on 2025-06-15 11:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("game", "0020_alter_musikgame_options_musicgameresults"), + ] + + operations = [ + migrations.AlterField( + model_name="musicgameresults", + name="score", + field=models.IntegerField(default=0), + ), + ] diff --git a/game/models.py b/game/models.py index a4e1f91..bf57c6e 100644 --- a/game/models.py +++ b/game/models.py @@ -129,3 +129,15 @@ class MusicGameAnswer(models.Model): models.UniqueConstraint(fields=("game", "player"), name="unique_answer"), ] ordering = ["game"] + + +class MusicGameResults(models.Model): + game = models.ForeignKey(MusikGame, on_delete=models.CASCADE) + player = models.ForeignKey(User, on_delete=models.CASCADE) + score = models.IntegerField(default=0) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=("game", "player"), name="unique_result") + ] + ordering = ["-score"] diff --git a/game/templates/game/musikgame_detail.html b/game/templates/game/musikgame_detail.html index f4720ef..5bf75d5 100644 --- a/game/templates/game/musikgame_detail.html +++ b/game/templates/game/musikgame_detail.html @@ -35,7 +35,11 @@ {% if musikgame.over %}
    - {% for player in musikgame.players.all %}
  1. {{ player.username }}
  2. {% endfor %} + {% for player in musikgame.musicgameresults_set.all %} +
  3. + {{ player.player.username }} {{ player.score }} +
  4. + {% endfor %}
{% else %}

{{ musikgame.players.all|join:", " }}

diff --git a/game/views.py b/game/views.py index 1e1cae0..b381b61 100644 --- a/game/views.py +++ b/game/views.py @@ -9,7 +9,7 @@ from django.contrib.auth.models import User from django.contrib.messages.views import SuccessMessageMixin from django.core.exceptions import PermissionDenied from django.db import IntegrityError -from django.db.models import Count, Q +from django.db.models import Count, F, Q from django.shortcuts import get_object_or_404, redirect from django.views import View from django.views.generic import TemplateView @@ -468,5 +468,25 @@ class GameEndView(LoginRequiredMixin, SingleObjectMixin, View): if not game.group.is_leader(request.user): raise PermissionDenied() game.over = True + + for player in game.players.all(): + score = ( + 100 + * player.musicgameanswer_set.filter(game__game=game) + .exclude(game__player=player) + .filter(game__player=F("answer")) + .count() + ) + score -= ( + 50 + * player.musicgameanswer_set.filter(game__game=game) + .filter(game__player=player) + .exclude(game__player=F("answer")) + .count() + ) + models.MusicGameResults.objects.create( + game=game, player=player, score=score + ) + game.save() return redirect("game_detail", pk) From 3d585e1e14e6c4d8e974712f01995103d54b703c Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sun, 15 Jun 2025 13:59:22 +0200 Subject: [PATCH 07/31] Update game answer handling and UI for completed games --- game/forms.py | 1 + game/templates/game/musikgame_answer.html | 2 +- game/templates/game/musikgame_detail.html | 6 ++++- game/views.py | 30 +++++++++++++++++------ 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/game/forms.py b/game/forms.py index c90e286..a548455 100644 --- a/game/forms.py +++ b/game/forms.py @@ -57,4 +57,5 @@ class AnswerForm(forms.Form): if (ma := music.musicgameanswer_set.filter(player=user).first()) and ma.answer else "", + disabled=game.over, ) diff --git a/game/templates/game/musikgame_answer.html b/game/templates/game/musikgame_answer.html index 932d55e..84e055d 100644 --- a/game/templates/game/musikgame_answer.html +++ b/game/templates/game/musikgame_answer.html @@ -21,6 +21,6 @@ {% endfor %} - + {% if not musikgame.over %}{% endif %} {% endblock content %} diff --git a/game/templates/game/musikgame_detail.html b/game/templates/game/musikgame_detail.html index 5bf75d5..4130ef3 100644 --- a/game/templates/game/musikgame_detail.html +++ b/game/templates/game/musikgame_detail.html @@ -17,7 +17,11 @@ href="{% yt_playlist musikgame %}" role="button" {% if musikgame.playlist_loading %}aria-busy="true"{% endif %}> Playlist - {% if not musikgame.over %} + {% if musikgame.over %} + Mes réponses + {% else %} Répondre {% endif %} diff --git a/game/views.py b/game/views.py index b381b61..b96cb2b 100644 --- a/game/views.py +++ b/game/views.py @@ -434,7 +434,7 @@ class GameAnswerView(LoginRequiredMixin, DetailView): template_name = "game/musikgame_answer.html" def get_queryset(self): - return super().get_queryset().filter(over=False, players=self.request.user) + return super().get_queryset().filter(players=self.request.user) def get_context_data(self, **kwargs): return super().get_context_data(**kwargs) | { @@ -443,6 +443,8 @@ class GameAnswerView(LoginRequiredMixin, DetailView): def post(self, request, pk): game = self.get_object() + if game.over: + raise PermissionDenied() for music in game.musicgameorder_set.all(): answer = request.POST.get(f"answer-{music.order}") if answer: @@ -469,16 +471,28 @@ class GameEndView(LoginRequiredMixin, SingleObjectMixin, View): raise PermissionDenied() game.over = True + value = {} + for go in game.musicgameorder_set.all(): + value[go.pk] = 1000 / ( + 1 + + ( + (go.musicgameanswer_set.filter(game__player=F("answer")).count()) + - 1 / game.players.count() + ) + ** 2 + ) + for player in game.players.all(): - score = ( - 100 - * player.musicgameanswer_set.filter(game__game=game) - .exclude(game__player=player) - .filter(game__player=F("answer")) - .count() + score = sum( + [ + value[ga.game.pk] + for ga in player.musicgameanswer_set.filter(game__game=game) + .exclude(game__player=player) + .filter(game__player=F("answer")) + ] ) score -= ( - 50 + 500 * player.musicgameanswer_set.filter(game__game=game) .filter(game__player=player) .exclude(game__player=F("answer")) From 178a7cab0362fe6677e79c01fc5b434f724c99f8 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sun, 15 Jun 2025 14:01:02 +0200 Subject: [PATCH 08/31] Bump version to 0.3.0 in settings and pyproject files --- musik/settings.py | 2 +- pyproject.toml | 2 +- uv.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/musik/settings.py b/musik/settings.py index 764863f..230feaf 100644 --- a/musik/settings.py +++ b/musik/settings.py @@ -13,7 +13,7 @@ https://docs.djangoproject.com/en/5.2/ref/settings/ import os from pathlib import Path -VERSION = "0.2.0" +VERSION = "0.3.0" # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent diff --git a/pyproject.toml b/pyproject.toml index d29d983..f387aef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "musik" -version = "0.2.0" +version = "0.3.0" description = "Le jeu de Musik." readme = "README.md" requires-python = ">=3.12" diff --git a/uv.lock b/uv.lock index e30935f..3659c92 100644 --- a/uv.lock +++ b/uv.lock @@ -423,7 +423,7 @@ wheels = [ [[package]] name = "musik" -version = "0.2.0" +version = "0.3.0" source = { virtual = "." } dependencies = [ { name = "celery" }, From 92abcb584c07b537a4b379d03b7c5adc819d8fd6 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sun, 15 Jun 2025 15:09:48 +0200 Subject: [PATCH 09/31] Add value field to MusicGameOrder and update score calculation logic --- game/migrations/0022_musicgameorder_value.py | 17 ++++++++++++ game/models.py | 25 +++++++++++++++++ game/views.py | 28 +++----------------- 3 files changed, 45 insertions(+), 25 deletions(-) create mode 100644 game/migrations/0022_musicgameorder_value.py diff --git a/game/migrations/0022_musicgameorder_value.py b/game/migrations/0022_musicgameorder_value.py new file mode 100644 index 0000000..712353c --- /dev/null +++ b/game/migrations/0022_musicgameorder_value.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.3 on 2025-06-15 12:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("game", "0021_alter_musicgameresults_score"), + ] + + operations = [ + migrations.AddField( + model_name="musicgameorder", + name="value", + field=models.PositiveIntegerField(default=0), + ), + ] diff --git a/game/models.py b/game/models.py index bf57c6e..fa84cbd 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 import F from django.db.models.functions import Lower from django.db.models.signals import post_delete, post_save from django.dispatch import receiver @@ -106,6 +107,17 @@ class MusicGameOrder(models.Model): player = models.ForeignKey(User, on_delete=models.CASCADE) music_video = models.ForeignKey(MusicVideo, on_delete=models.CASCADE) order = models.PositiveIntegerField() + value = models.PositiveIntegerField(default=0) + + def update_value(self): + n_right = self.musicgameanswer_set.filter(game__player=F("answer")).count() + if n_right == 0: + self.value = 1000 + else: + self.value = 1000 / ( + 1 + ((n_right - 1) / (self.game.players.count() - 1)) ** 0.5 + ) + self.save() class Meta: constraints = [ @@ -117,6 +129,17 @@ class MusicGameOrder(models.Model): ordering = ["order"] +class AnswerManager(models.Manager): + def score(self, game, player): + qs = self.filter(game__game=game, game__player=player) + return ( + qs.filter(answer=F("game__player")) + .aggregate(score=models.Sum("game__value", default=0)) + .get("score") + - 500 * qs.exclude(game__player=F("answer")).count() + ) + + class MusicGameAnswer(models.Model): game = models.ForeignKey(MusicGameOrder, on_delete=models.CASCADE) player = models.ForeignKey(User, on_delete=models.CASCADE) @@ -124,6 +147,8 @@ class MusicGameAnswer(models.Model): User, on_delete=models.SET_NULL, null=True, related_name="+" ) + objects = AnswerManager() + class Meta: constraints = [ models.UniqueConstraint(fields=("game", "player"), name="unique_answer"), diff --git a/game/views.py b/game/views.py index b96cb2b..85e7348 100644 --- a/game/views.py +++ b/game/views.py @@ -9,7 +9,7 @@ from django.contrib.auth.models import User from django.contrib.messages.views import SuccessMessageMixin from django.core.exceptions import PermissionDenied from django.db import IntegrityError -from django.db.models import Count, F, Q +from django.db.models import Count, Q from django.shortcuts import get_object_or_404, redirect from django.views import View from django.views.generic import TemplateView @@ -471,33 +471,11 @@ class GameEndView(LoginRequiredMixin, SingleObjectMixin, View): raise PermissionDenied() game.over = True - value = {} for go in game.musicgameorder_set.all(): - value[go.pk] = 1000 / ( - 1 - + ( - (go.musicgameanswer_set.filter(game__player=F("answer")).count()) - - 1 / game.players.count() - ) - ** 2 - ) + go.update_value() for player in game.players.all(): - score = sum( - [ - value[ga.game.pk] - for ga in player.musicgameanswer_set.filter(game__game=game) - .exclude(game__player=player) - .filter(game__player=F("answer")) - ] - ) - score -= ( - 500 - * player.musicgameanswer_set.filter(game__game=game) - .filter(game__player=player) - .exclude(game__player=F("answer")) - .count() - ) + score = player.musicgameanswer_set.score(game, player) models.MusicGameResults.objects.create( game=game, player=player, score=score ) From e039889488fc3e02bc400526790834ba3d23add8 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sun, 15 Jun 2025 15:12:19 +0200 Subject: [PATCH 10/31] Filter queryset in GameEndView to exclude completed games --- game/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/game/views.py b/game/views.py index 85e7348..31c74f9 100644 --- a/game/views.py +++ b/game/views.py @@ -465,6 +465,9 @@ class GameAnswerView(LoginRequiredMixin, DetailView): class GameEndView(LoginRequiredMixin, SingleObjectMixin, View): model = models.MusikGame + def get_queryset(self): + return super().get_queryset().filter(over=False) + def post(self, request, pk): game = self.get_object() if not game.group.is_leader(request.user): From 2278345f327f755b9f77bc4bb0bff28c01d48098 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sun, 15 Jun 2025 15:46:35 +0200 Subject: [PATCH 11/31] Refactor game results display and scoring logic in templates and CSS --- base/static/css/main.css | 26 ++++++++-- game/templates/game/include/game_results.html | 49 +++++++++++++++++++ game/templates/game/musikgame_detail.html | 37 +------------- game/templates/tags/game/answer.html | 16 ++++++ game/templatetags/game.py | 30 ++++++++++++ 5 files changed, 118 insertions(+), 40 deletions(-) create mode 100644 game/templates/game/include/game_results.html create mode 100644 game/templates/tags/game/answer.html create mode 100644 game/templatetags/game.py diff --git a/base/static/css/main.css b/base/static/css/main.css index 80a38b2..3f13bbc 100644 --- a/base/static/css/main.css +++ b/base/static/css/main.css @@ -17,7 +17,7 @@ form a[role="button"] { width: 100%; } -i.owner { +i.owner, .gold { color: var(--pico-color-amber-200); } @@ -160,10 +160,28 @@ table select { color: var(--pico-color-sand-300); } } - .score { - font-weight: 900; - color: var(--pico-color-zinc-500); margin-left: .5em; } } + + +.score { + font-weight: 900; + color: var(--pico-color-zinc-500); +} +.correct { + .score, i { + color: var(--pico-color-lime-200);} +} +.wrong { + .score, i { + color: var(--pico-color-red-500);} +} + +table.results { + white-space: nowrap; +} +.sc i { + margin-right: .5em; +} diff --git a/game/templates/game/include/game_results.html b/game/templates/game/include/game_results.html new file mode 100644 index 0000000..0f03b99 --- /dev/null +++ b/game/templates/game/include/game_results.html @@ -0,0 +1,49 @@ +{% load game %} +{% if musikgame.over %} +

+ Résultats +

+
+ + Résultats + +
+ + + + + + + {% for player in musikgame.musicgameresults_set.all %} + + + {% endfor %} + + + + {% for music in musikgame.musicgameorder_set.all %} + + + + + {% for player in musikgame.musicgameresults_set.all %} + {% answer player music %} + {% endfor %} + + {% endfor %} + +
+ + + Musique + + Joueur + {{ player.player.username }} + {% if forloop.first %}{% endif %} + {{ player.score }} +
{{ music.order }} + {{ music.music_video.title }} + {{ music.player }}
+
+
+{% endif %} diff --git a/game/templates/game/musikgame_detail.html b/game/templates/game/musikgame_detail.html index 4130ef3..0df1510 100644 --- a/game/templates/game/musikgame_detail.html +++ b/game/templates/game/musikgame_detail.html @@ -63,40 +63,5 @@ {% endfor %}
- {% if musikgame.over %} -

- Résultats -

-
- - Résultats - - - - - - - - - - - {% for music in musikgame.musicgameorder_set.all %} - - - - - - {% endfor %} - -
- - - Musique - - Joueur -
{{ music.order }} - {{ music.music_video.title }} - {{ music.player }}
-
- {% endif %} + {% include "game/include/game_results.html" %} {% endblock content %} diff --git a/game/templates/tags/game/answer.html b/game/templates/tags/game/answer.html new file mode 100644 index 0000000..bfdeba5 --- /dev/null +++ b/game/templates/tags/game/answer.html @@ -0,0 +1,16 @@ +{% if empty %} + + + {{ score }} + +{% elif correct %} + {{ answer }} + + {{ score }} + +{% else %} + {{ answer }} + + {{ score }} + +{% endif %} diff --git a/game/templatetags/game.py b/game/templatetags/game.py new file mode 100644 index 0000000..eec1bd0 --- /dev/null +++ b/game/templatetags/game.py @@ -0,0 +1,30 @@ +from django import template + +from .. import models + +register = template.Library() + + +@register.inclusion_tag("tags/game/answer.html") +def answer(player, music): + res = { + "answer": "", + "correct": False, + "score": 0, + "empty": False, + } + + answer = models.MusicGameAnswer.objects.filter( + player=player.player, game=music + ).first() + if answer: + res["answer"] = answer.answer + res["correct"] = answer.answer == music.player + if music.player == player.player: + res["score"] = 0 if res["correct"] else "−500" + else: + res["score"] = music.value if res["correct"] else 0 + else: + res["empty"] = True + + return res From 993ed8963c703da2760d5f3b75cb6222dbd21248 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sun, 15 Jun 2025 15:56:30 +0200 Subject: [PATCH 12/31] Highlight current user in game results and score display --- base/static/css/main.css | 2 +- game/models.py | 8 +++++--- game/templates/game/include/game_results.html | 2 +- game/templates/game/musikgame_detail.html | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/base/static/css/main.css b/base/static/css/main.css index 3f13bbc..d81f86f 100644 --- a/base/static/css/main.css +++ b/base/static/css/main.css @@ -117,7 +117,7 @@ h6, i.i { margin-right: .5em; } -i.hl { +i.hl, .me { color: var(--pico-primary); } diff --git a/game/models.py b/game/models.py index fa84cbd..0affa8c 100644 --- a/game/models.py +++ b/game/models.py @@ -131,12 +131,14 @@ class MusicGameOrder(models.Model): class AnswerManager(models.Manager): def score(self, game, player): - qs = self.filter(game__game=game, game__player=player) + qs = self.filter(game__game=game, player=player) return ( - qs.filter(answer=F("game__player")) + qs.exclude(game__player=player) + .filter(game__player=F("answer")) .aggregate(score=models.Sum("game__value", default=0)) .get("score") - - 500 * qs.exclude(game__player=F("answer")).count() + - 500 + * qs.filter(game__player=player).exclude(game__player=F("answer")).count() ) diff --git a/game/templates/game/include/game_results.html b/game/templates/game/include/game_results.html index 0f03b99..ca2821c 100644 --- a/game/templates/game/include/game_results.html +++ b/game/templates/game/include/game_results.html @@ -21,7 +21,7 @@ Joueur {% for player in musikgame.musicgameresults_set.all %} - {{ player.player.username }} + {{ player.player.username }} {% if forloop.first %}{% endif %} {{ player.score }} diff --git a/game/templates/game/musikgame_detail.html b/game/templates/game/musikgame_detail.html index 0df1510..67b9921 100644 --- a/game/templates/game/musikgame_detail.html +++ b/game/templates/game/musikgame_detail.html @@ -40,7 +40,7 @@ {% if musikgame.over %}
    {% for player in musikgame.musicgameresults_set.all %} -
  1. +
  2. {{ player.player.username }} {{ player.score }}
  3. {% endfor %} From 302b884b2323ef7844250f5df72ba20959d404b4 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sun, 15 Jun 2025 15:57:01 +0200 Subject: [PATCH 13/31] Bump version to 0.4.0 in settings and pyproject files --- musik/settings.py | 2 +- pyproject.toml | 2 +- uv.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/musik/settings.py b/musik/settings.py index 230feaf..b42044a 100644 --- a/musik/settings.py +++ b/musik/settings.py @@ -13,7 +13,7 @@ https://docs.djangoproject.com/en/5.2/ref/settings/ import os from pathlib import Path -VERSION = "0.3.0" +VERSION = "0.4.0" # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent diff --git a/pyproject.toml b/pyproject.toml index f387aef..a2dc6b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "musik" -version = "0.3.0" +version = "0.4.0" description = "Le jeu de Musik." readme = "README.md" requires-python = ">=3.12" diff --git a/uv.lock b/uv.lock index 3659c92..8f1539b 100644 --- a/uv.lock +++ b/uv.lock @@ -423,7 +423,7 @@ wheels = [ [[package]] name = "musik" -version = "0.3.0" +version = "0.4.0" source = { virtual = "." } dependencies = [ { name = "celery" }, From 486f650ea6781200ac34de3292a3c4b354e9a8bd Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sun, 15 Jun 2025 16:11:24 +0200 Subject: [PATCH 14/31] Add GameManager for filtering active games and update group_games template --- base/static/css/main.css | 2 +- game/models.py | 7 +++++++ game/templates/game/include/group_games.html | 5 +++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/base/static/css/main.css b/base/static/css/main.css index d81f86f..23dffab 100644 --- a/base/static/css/main.css +++ b/base/static/css/main.css @@ -25,7 +25,7 @@ i.owner, .gold { display: none; } -a.group { +a.group, a.running { text-decoration: none; } diff --git a/game/models.py b/game/models.py index 0affa8c..ef85fd8 100644 --- a/game/models.py +++ b/game/models.py @@ -68,6 +68,11 @@ class MusicVideo(models.Model): ] +class GameManager(models.Manager): + def playing(self): + return self.filter(over=False) + + class MusikGame(models.Model): group = models.ForeignKey(Group, on_delete=models.CASCADE) date = models.DateTimeField(auto_now_add=True) @@ -77,6 +82,8 @@ class MusikGame(models.Model): playlist_loading = models.BooleanField(default=False) over = models.BooleanField(default=False) + objects = GameManager() + def get_absolute_url(self): return reverse("game_detail", kwargs={"pk": self.pk}) diff --git a/game/templates/game/include/group_games.html b/game/templates/game/include/group_games.html index 09ad49a..15016fb 100644 --- a/game/templates/game/include/group_games.html +++ b/game/templates/game/include/group_games.html @@ -1,5 +1,10 @@ {% load form youtube %} {% if group.musikgame_set.exists %} + {% for game in group.musikgame_set.playing %} + +
    {{ game.date }}
    +
    + {% endfor %}

    Parties

    From cc38d72df822d992f99df3349bd1caafea3ffd4b Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sun, 15 Jun 2025 16:20:53 +0200 Subject: [PATCH 15/31] Add migration to alter MusicVideo model options and update group detail form to use textarea for YouTube IDs --- .../0023_alter_musicvideo_options.py | 16 +++++++ game/models.py | 1 + game/templates/game/group_detail.html | 4 +- game/views.py | 43 ++++++++++--------- 4 files changed, 42 insertions(+), 22 deletions(-) create mode 100644 game/migrations/0023_alter_musicvideo_options.py diff --git a/game/migrations/0023_alter_musicvideo_options.py b/game/migrations/0023_alter_musicvideo_options.py new file mode 100644 index 0000000..ee96adb --- /dev/null +++ b/game/migrations/0023_alter_musicvideo_options.py @@ -0,0 +1,16 @@ +# Generated by Django 5.2.3 on 2025-06-15 14:19 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("game", "0022_musicgameorder_value"), + ] + + operations = [ + migrations.AlterModelOptions( + name="musicvideo", + options={"ordering": ["blacklisted", "-date_added"]}, + ), + ] diff --git a/game/models.py b/game/models.py index ef85fd8..18912a0 100644 --- a/game/models.py +++ b/game/models.py @@ -66,6 +66,7 @@ class MusicVideo(models.Model): fields=("yt_id", "owner", "group"), name="unique_music_in_group" ) ] + ordering = ["blacklisted", "-date_added"] class GameManager(models.Manager): diff --git a/game/templates/game/group_detail.html b/game/templates/game/group_detail.html index 3812b6e..c5165f0 100644 --- a/game/templates/game/group_detail.html +++ b/game/templates/game/group_detail.html @@ -71,8 +71,8 @@
{% csrf_token %} -
- +
+
diff --git a/game/views.py b/game/views.py index 31c74f9..9ee29ca 100644 --- a/game/views.py +++ b/game/views.py @@ -104,28 +104,31 @@ class GroupAddMusicView(MemberFilterMixin, SingleObjectMixin, View): def post(self, request, pk): group = self.get_object() - yt_id = request.POST.get("yt_id") - if not yt_id: - messages.add_message(request, messages.ERROR, "Aucun identifiant donné") - return redirect(group) - yt_id = utils.parse_musik(yt_id) + ids = request.POST.get("yt_id") + for yt_id in ids.split(): + if not yt_id: + 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: - 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}" - ) + title = utils.get_yt_title(yt_id) + if not title: + messages.add_message( + request, messages.ERROR, f"Vidéo Youtube invalide : {yt_id}" + ) + else: + 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}" - ) + messages.add_message( + request, messages.SUCCESS, f"Vidéo Youtube ajoutée : {yt_id}" + ) return redirect(group) From b30ee77132d93a16c607f6d3787c4693b91dfd6c Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sun, 15 Jun 2025 16:24:51 +0200 Subject: [PATCH 16/31] Enhance GroupAddMemberView to support adding multiple members and improve error handling for non-existent users --- game/views.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/game/views.py b/game/views.py index 9ee29ca..d5f3b7d 100644 --- a/game/views.py +++ b/game/views.py @@ -139,19 +139,24 @@ class GroupAddMemberView(MemberFilterMixin, SingleObjectMixin, View): group = self.get_object() if not group.is_leader(request.user): raise PermissionDenied() - 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." - ) + usernames = request.POST.get("username") + for username in usernames.split(): + user = User.objects.filter(username=username).first() + if not user: + messages.add_message( + request, messages.ERROR, f"{username} n'existe pas." + ) + elif user == group.owner: + messages.add_message( + request, messages.WARNING, f"{user} est le propriétaire du groupe." + ) + elif user in group.members.all(): + messages.add_message( + request, messages.WARNING, f"{user} est déjà membre du groupe." + ) + else: + group.members.add(user) - group.members.add(user) return redirect(group) From c7b907f1151925047d34f9e93a544e5a3cc4fd97 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sun, 15 Jun 2025 16:37:09 +0200 Subject: [PATCH 17/31] Refactor music management UI: separate group musics into its own template, enhance form structure, and improve responsiveness Fix #5 --- base/static/css/main.css | 18 ++++- base/templates/auth/user_form.html | 24 ++++++- base/templates/registration/login.html | 16 ++--- game/templates/game/group_detail.html | 65 +----------------- game/templates/game/include/group_musics.html | 66 +++++++++++++++++++ 5 files changed, 113 insertions(+), 76 deletions(-) create mode 100644 game/templates/game/include/group_musics.html diff --git a/base/static/css/main.css b/base/static/css/main.css index 23dffab..8120679 100644 --- a/base/static/css/main.css +++ b/base/static/css/main.css @@ -179,9 +179,25 @@ table select { color: var(--pico-color-red-500);} } -table.results { +table.results, table.musics { white-space: nowrap; } .sc i { margin-right: .5em; } + +@media (width < 576px) { + [role="group"] { + display: grid; + & > :first-child { + border-radius: var(--pico-border-radius) var(--pico-border-radius) 0 0 !important; + } + & > :not(:first-child) { + margin-left: 0 !important; + margin-top: calc(var(--pico-border-width) * -1); + } + & > :last-child { + border-radius: 0 0 var(--pico-border-radius) var(--pico-border-radius) !important; + } + } +} diff --git a/base/templates/auth/user_form.html b/base/templates/auth/user_form.html index 20c40a8..c895f16 100644 --- a/base/templates/auth/user_form.html +++ b/base/templates/auth/user_form.html @@ -1,6 +1,24 @@ {% extends "base.html" %} {% load form %} {% block content %} -

Créer un compte

- {% form form submit="Créer mon compte" %} - {% endblock content %} +

Créer mon compte

+ {% for error in form.non_field_errors %}
{{ error }}
{% endfor %} +
+ {% csrf_token %} +
+ {% for field in form %} + + {% endfor %} +
+ +
+ J'ai déjà un compte +
+
+{% endblock content %} diff --git a/base/templates/registration/login.html b/base/templates/registration/login.html index 8bbb8fd..4e0394e 100644 --- a/base/templates/registration/login.html +++ b/base/templates/registration/login.html @@ -5,9 +5,9 @@
{% csrf_token %} {% for field in form %} -
- {% if field.id_for_label %} - + {% if field.id_for_label %} +
+ {% endfor %} -

+

Créer mon compte -

-

+

+
J'ai oublié mon mot de passe -

+
{% endblock content %} diff --git a/game/templates/game/group_detail.html b/game/templates/game/group_detail.html index c5165f0..90e9901 100644 --- a/game/templates/game/group_detail.html +++ b/game/templates/game/group_detail.html @@ -12,68 +12,5 @@ {% include "game/include/group_buttons.html" %} {% include "game/include/group_games.html" %} {% include "game/include/group_members.html" %} -

- Mes musiques {{ musics.count }} -

-
- - Liste des musiques - -
- {% csrf_token %} - - - - - - - - - - - {% for music in musics %} - - - - - - - {% empty %} - - - - {% endfor %} - -
MusiqueID - -
- - {{ music.title }} - {{ music.yt_id }} - - -
Aucune musique.
- {% if musics %} -
- - -
- {% endif %} -
-
-
- {% csrf_token %} -
- - -
-
+ {% include "game/include/group_musics.html" %} {% endblock content %} diff --git a/game/templates/game/include/group_musics.html b/game/templates/game/include/group_musics.html new file mode 100644 index 0000000..56117cf --- /dev/null +++ b/game/templates/game/include/group_musics.html @@ -0,0 +1,66 @@ +

+ Mes musiques {{ musics.count }} +

+
+ + Liste des musiques + +
+ {% csrf_token %} +
+ + + + + + + + + + + {% for music in musics %} + + + + + + + {% empty %} + + + + {% endfor %} + +
MusiqueID + +
+ + {{ music.title }} + {{ music.yt_id }} + + +
Aucune musique.
+
+ {% if musics %} +
+ + +
+ {% endif %} +
+
+
+ {% csrf_token %} +
+ + +
+
From 98183575af5cc3c47494130f221570b4f4b6ecd7 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sun, 15 Jun 2025 16:42:39 +0200 Subject: [PATCH 18/31] Bump version to 0.4.1 in settings and pyproject files --- musik/settings.py | 2 +- pyproject.toml | 2 +- uv.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/musik/settings.py b/musik/settings.py index b42044a..b69b0cb 100644 --- a/musik/settings.py +++ b/musik/settings.py @@ -13,7 +13,7 @@ https://docs.djangoproject.com/en/5.2/ref/settings/ import os from pathlib import Path -VERSION = "0.4.0" +VERSION = "0.4.1" # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent diff --git a/pyproject.toml b/pyproject.toml index a2dc6b3..93cda42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "musik" -version = "0.4.0" +version = "0.4.1" description = "Le jeu de Musik." readme = "README.md" requires-python = ">=3.12" diff --git a/uv.lock b/uv.lock index 8f1539b..44c8b62 100644 --- a/uv.lock +++ b/uv.lock @@ -423,7 +423,7 @@ wheels = [ [[package]] name = "musik" -version = "0.4.0" +version = "0.4.1" source = { virtual = "." } dependencies = [ { name = "celery" }, From e6d757c0691414cd43e6e63fc2312d8bbf18b9bc Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Mon, 16 Jun 2025 15:18:56 +0200 Subject: [PATCH 19/31] Update button text in musikgame_answer template for clarity Fix #6 --- game/templates/game/musikgame_answer.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/templates/game/musikgame_answer.html b/game/templates/game/musikgame_answer.html index 84e055d..f947802 100644 --- a/game/templates/game/musikgame_answer.html +++ b/game/templates/game/musikgame_answer.html @@ -21,6 +21,6 @@ {% endfor %} - {% if not musikgame.over %}{% endif %} + {% if not musikgame.over %}{% endif %} {% endblock content %} From cb3518a5e517b68d967352e22006543dc65ab86a Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Mon, 16 Jun 2025 15:23:19 +0200 Subject: [PATCH 20/31] Change playlist privacy status from private to unlisted in generate_playlist task Fix #7 --- game/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/tasks.py b/game/tasks.py index 22a7dc0..61937d0 100644 --- a/game/tasks.py +++ b/game/tasks.py @@ -18,7 +18,7 @@ def generate_playlist(creds, game_pk): "description": "Playlist générée par Musik", }, "status": { - "privacyStatus": "private", + "privacyStatus": "unlisted", }, }, ) From 22bb6931e820fa924098dda747b20739a77d7722 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Mon, 16 Jun 2025 15:29:40 +0200 Subject: [PATCH 21/31] Remove individual blacklisting of music in GameCreateView and update all related music to blacklisted in GameEndView Fix #8 --- game/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/game/views.py b/game/views.py index d5f3b7d..20fedff 100644 --- a/game/views.py +++ b/game/views.py @@ -327,8 +327,6 @@ class GameCreateView(LoginRequiredMixin, CreateView): pm_list = list(zip(players, musics)) random.shuffle(pm_list) for (player, music), order in zip(pm_list, range(1, len(pm_list) + 1)): - music.blacklisted = True - music.save() models.MusicGameOrder.objects.create( game=form.instance, player=player, music_video=music, order=order ) @@ -481,6 +479,9 @@ class GameEndView(LoginRequiredMixin, SingleObjectMixin, View): if not game.group.is_leader(request.user): raise PermissionDenied() game.over = True + models.MusicVideo.objects.filter(musicgameorder__game=game).update( + blacklisted=True + ) for go in game.musicgameorder_set.all(): go.update_value() From 0b8ce65a0af6d3e6e8832c924cbb27f7ef753bae Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Mon, 16 Jun 2025 15:33:23 +0200 Subject: [PATCH 22/31] Fix playlist display logic in musikgame_detail template Fix #9 --- game/templates/game/musikgame_detail.html | 40 +++++++++++------------ 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/game/templates/game/musikgame_detail.html b/game/templates/game/musikgame_detail.html index 67b9921..66708c0 100644 --- a/game/templates/game/musikgame_detail.html +++ b/game/templates/game/musikgame_detail.html @@ -9,31 +9,31 @@ {% endif %} {{ musikgame.date }} - {% if musikgame.playlist or musikgame.playlist_loading %} -
- {% csrf_token %} -
+ + {% csrf_token %} +
+ {% if musikgame.playlist or musikgame.playlist_loading %} Playlist - {% if musikgame.over %} - Mes réponses - {% else %} - Répondre - {% endif %} -
- {% if is_leader and not musikgame.over %} -
- -
{% endif %} - - {% endif %} + {% if musikgame.over %} + Mes réponses + {% else %} + Répondre + {% endif %} +
+ {% if is_leader and not musikgame.over %} +
+ +
+ {% endif %} +

Joueurs

From 951128147cb561f223a5b20075a9ed32dd9dab11 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Mon, 16 Jun 2025 15:44:17 +0200 Subject: [PATCH 23/31] Fix playlist loading logic in MusikGame creation and update related template messages Fix #10 --- game/models.py | 3 +++ game/templates/game/musikgame_form.html | 8 +++++--- game/views.py | 5 ++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/game/models.py b/game/models.py index 18912a0..51eb410 100644 --- a/game/models.py +++ b/game/models.py @@ -99,6 +99,9 @@ def generateYoutubePlaylist(sender, instance, created, **kwargs): if creds := instance.group.owner.youtubecredentials: tasks.generate_playlist.delay_on_commit(creds.credentials, instance.pk) + else: + instance.playlist_loading = False + instance.save() @receiver(post_delete, sender=MusikGame) diff --git a/game/templates/game/musikgame_form.html b/game/templates/game/musikgame_form.html index 38ed5b6..655723a 100644 --- a/game/templates/game/musikgame_form.html +++ b/game/templates/game/musikgame_form.html @@ -5,10 +5,12 @@ {{ group.name }}

- {% if not user.youtubecredentials.credentials %} - Me connecter au compte Youtube + {% if group.owner.youtubecredentials.credentials %} + Une playlist sera générée automatiquement sur le compte Youtube de {{ group.owner }} ({{ group.owner.youtubecredentials.title }}). + {% elif user == group.owner %} + Connecter mon compte Youtube {% else %} - Une playlist sera générée automatiquement sur le compte Youtube {{ user.youtubecredentials.title }}. + Aucune playlist Youtube ne sera générée car {{ group.owner }} n'a pas lié son compte Youtube. {% endif %}

{% form form %} diff --git a/game/views.py b/game/views.py index 20fedff..45685aa 100644 --- a/game/views.py +++ b/game/views.py @@ -331,9 +331,8 @@ class GameCreateView(LoginRequiredMixin, CreateView): game=form.instance, player=player, music_video=music, order=order ) - if models.YoutubeCredentials.objects.filter(user=self.request.user).exists(): - form.instance.playlist_loading = True - form.instance.save() + form.instance.playlist_loading = True + form.instance.save() return res From c639307cfbe5ca0808ac85be8cbd8841a50a956c Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Mon, 16 Jun 2025 15:44:37 +0200 Subject: [PATCH 24/31] Add restart policy for RabbitMQ and Postgres services in Docker Compose Fix #11 --- compose.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compose.yaml b/compose.yaml index 04838ec..a6ee8b3 100644 --- a/compose.yaml +++ b/compose.yaml @@ -32,10 +32,12 @@ services: rabbitmq: image: rabbitmq container_name: musik_rabbitmq + restart: unless-stopped postgres: image: postgres:17 container_name: musik_postgres + restart: unless-stopped env_file: stack.env volumes: - /docker/musik/postgres:/var/lib/postgresql/data From 84c432c325bc48d02b8474d16241d01e88df66e9 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Mon, 16 Jun 2025 16:16:03 +0200 Subject: [PATCH 25/31] Refactor value calculation in MusicGameOrder to improve scoring logic Fix #13 Close #12 --- game/models.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/game/models.py b/game/models.py index 51eb410..153583f 100644 --- a/game/models.py +++ b/game/models.py @@ -121,13 +121,10 @@ class MusicGameOrder(models.Model): value = models.PositiveIntegerField(default=0) def update_value(self): - n_right = self.musicgameanswer_set.filter(game__player=F("answer")).count() - if n_right == 0: - self.value = 1000 - else: - self.value = 1000 / ( - 1 + ((n_right - 1) / (self.game.players.count() - 1)) ** 0.5 - ) + x = self.musicgameanswer_set.filter(game__player=F("answer")).count() + n = self.game.players.count() + n = max(3, n) + self.value = 1000 * 2 ** (-(x - 2) / (n - 2)) self.save() class Meta: From bd8529cd014f5cea9a3bb35683a273283b57fa91 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Mon, 16 Jun 2025 16:17:04 +0200 Subject: [PATCH 26/31] Bump version to 0.4.2 in settings and pyproject.toml --- musik/settings.py | 2 +- pyproject.toml | 2 +- uv.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/musik/settings.py b/musik/settings.py index b69b0cb..a712445 100644 --- a/musik/settings.py +++ b/musik/settings.py @@ -13,7 +13,7 @@ https://docs.djangoproject.com/en/5.2/ref/settings/ import os from pathlib import Path -VERSION = "0.4.1" +VERSION = "0.4.2" # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent diff --git a/pyproject.toml b/pyproject.toml index 93cda42..17df9a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "musik" -version = "0.4.1" +version = "0.4.2" description = "Le jeu de Musik." readme = "README.md" requires-python = ">=3.12" diff --git a/uv.lock b/uv.lock index 44c8b62..0e3ae81 100644 --- a/uv.lock +++ b/uv.lock @@ -423,7 +423,7 @@ wheels = [ [[package]] name = "musik" -version = "0.4.1" +version = "0.4.2" source = { virtual = "." } dependencies = [ { name = "celery" }, From b9711cbe9c75b122df5cdc6d11d3b6fcf32fccea Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Mon, 16 Jun 2025 22:04:18 +0200 Subject: [PATCH 27/31] Refactor hero section layout and update footer privacy notice for clarity --- base/static/css/main.css | 47 +++++++++++++++++++++++++++---------- base/templates/footer.html | 2 +- base/templates/hero.html | 24 +++++++++++++++---- base/templates/privacy.html | 3 ++- 4 files changed, 56 insertions(+), 20 deletions(-) diff --git a/base/static/css/main.css b/base/static/css/main.css index 8120679..74e4488 100644 --- a/base/static/css/main.css +++ b/base/static/css/main.css @@ -77,11 +77,6 @@ article.message { } #hero { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; background: radial-gradient( circle 50vh at calc(100vw - 4rem) 50%, var(--pico-primary-background), @@ -90,19 +85,41 @@ article.message { color-mix(in hsl, var(--pico-primary-background) 30%, var(--pico-background-color)) 60%, color-mix(in hsl, var(--pico-primary-background) 10%, var(--pico-background-color)) 80%, var(--pico-background-color)); - display: grid; - grid-template-rows: 1fr min-content; - align-items: center; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + height: 100%; + overflow-y: auto; padding: 4rem; - .big-logo { - font-size: 8rem; + main { + display: contents; } - - h1 { - font-size: 4rem; + section { + max-width: 20rem; } } +.full-page { + height: 100%; + display: grid; + grid-template-rows: 1fr; + align-items: center; + margin-bottom: 4rem; + + &.r { + -ms-grid-column-align: end; + } + .big-logo { + font-size: 8rem; + } + + h1 { + font-size: 4rem; + } +} + h1, h2, @@ -201,3 +218,7 @@ table.results, table.musics { } } } + +.brand-name { + color: var(--pico-primary); +} diff --git a/base/templates/footer.html b/base/templates/footer.html index 52895f6..6c57641 100644 --- a/base/templates/footer.html +++ b/base/templates/footer.html @@ -1 +1 @@ -Musik {{ VERSION }} – © Edgar P. BurkhartMentions légales +Musik {{ VERSION }} – © Edgar P. BurkhartMentions légales et confidentialité diff --git a/base/templates/hero.html b/base/templates/hero.html index 04c2aee..813c011 100644 --- a/base/templates/hero.html +++ b/base/templates/hero.html @@ -1,11 +1,25 @@ {% load static %}
- -

Musik

-

- Jouer -

+
+
+ +

Musik

+

+ Jouer +

+
+
+
+
+

+ Musik Le jeu où ta playlist devient ton arme secrète ! +

+

+ Invite ta bande, ajoute tes sons fétiches, et c’est parti ! Une playlist Youtube apparaît, mélangeant les coups de cœur de tout le monde. Le jeu ? Écoute, devine qui a choisi quoi, et découvre les secrets musicaux de tes potes. Entre pièges, révélations et fous rires, Musik c’est le jeu parfait pour tester vos oreilles… et vos amitiés. Prêt à jouer le DJ incognito ? +

+
+