From da29634e579d32c797adbe56398ddb5d7d1a1dee Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Sun, 15 Jun 2025 10:17:08 +0200
Subject: [PATCH 01/41] Update YouTube credentials handling to include channel
title in home template
---
game/templates/game/home.html | 2 ++
game/views.py | 10 +++++++++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/game/templates/game/home.html b/game/templates/game/home.html
index fe97624..eb7e220 100644
--- a/game/templates/game/home.html
+++ b/game/templates/game/home.html
@@ -11,6 +11,8 @@
{% if not user.youtubecredentials.credentials %}
Me connecter au compte Youtube
+ {% else %}
+ Connecté au compte Youtube {{ user.youtubecredentials.credentials.channel_title }} .
{% endif %}
{% if user.owned_group_set.exists or user.group_set.exists %}
diff --git a/game/views.py b/game/views.py
index cec8875..cbcbdb8 100644
--- a/game/views.py
+++ b/game/views.py
@@ -1,6 +1,7 @@
import random
import google_auth_oauthlib
+import googleapiclient
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
@@ -363,6 +364,13 @@ class YoutubeCallbackView(LoginRequiredMixin, View):
flow.fetch_token(code=request.GET.get("code"))
credentials = flow.credentials
+
+ yt_api = googleapiclient.discovery.build(
+ "youtube", "v3", credentials=credentials
+ )
+ channel_request = yt_api.channels().list(part="snippet", mine=True)
+ res = channel_request.execute()
+
models.YoutubeCredentials.objects.update_or_create(
user=request.user,
defaults={
@@ -373,10 +381,10 @@ class YoutubeCallbackView(LoginRequiredMixin, View):
"client_id": credentials.client_id,
"client_secret": credentials.client_secret,
"granted_scopes": credentials.granted_scopes,
+ "channel_title": res["items"][0]["snippet"]["title"],
}
},
)
-
messages.add_message(request, messages.SUCCESS, "Connexion à Youtube réussie.")
return redirect("/")
From 28203bd630a2995ec1898a1a14ed15c65f582cb9 Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Sun, 15 Jun 2025 10:19:46 +0200
Subject: [PATCH 02/41] Reorganize YouTube credentials display in home and form
templates for consistency
---
game/templates/game/home.html | 6 +++---
game/templates/game/musikgame_form.html | 7 +++++++
2 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/game/templates/game/home.html b/game/templates/game/home.html
index eb7e220..27511b5 100644
--- a/game/templates/game/home.html
+++ b/game/templates/game/home.html
@@ -5,9 +5,6 @@
Musik
Bienvenue {{ user.username }} !
-
- Mes groupes
-
{% if not user.youtubecredentials.credentials %}
Me connecter au compte Youtube
@@ -15,6 +12,9 @@
Connecté au compte Youtube {{ user.youtubecredentials.credentials.channel_title }} .
{% endif %}
+
+ Mes groupes
+
{% if user.owned_group_set.exists or user.group_set.exists %}
{% for group in user.owned_group_set.all %}
diff --git a/game/templates/game/musikgame_form.html b/game/templates/game/musikgame_form.html
index 3e6d4ed..03820a3 100644
--- a/game/templates/game/musikgame_form.html
+++ b/game/templates/game/musikgame_form.html
@@ -4,5 +4,12 @@
{{ group.name }}
+
+ {% if not user.youtubecredentials.credentials %}
+ Me connecter au compte Youtube
+ {% else %}
+ Une playlist sera générée automatiquement sur le compte Youtube {{ user.youtubecredentials.credentials.channel_title }} .
+ {% endif %}
+
{% form form %}
{% endblock content %}
From f36fe8ea84bf9facdc843edf67039bdc7ccfad90 Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Sun, 15 Jun 2025 10:22:27 +0200
Subject: [PATCH 03/41] Remove channel title from credentials in playlist
generation and deletion tasks
---
game/tasks.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/game/tasks.py b/game/tasks.py
index 22a7dc0..ceb5777 100644
--- a/game/tasks.py
+++ b/game/tasks.py
@@ -8,6 +8,7 @@ from . import models
@shared_task
def generate_playlist(creds, game_pk):
game = models.MusikGame.objects.get(pk=game_pk)
+ creds.pop("channel_title")
credentials = google.oauth2.credentials.Credentials(**creds)
yt_api = googleapiclient.discovery.build("youtube", "v3", credentials=credentials)
pl_request = yt_api.playlists().insert(
@@ -47,6 +48,7 @@ def generate_playlist(creds, game_pk):
@shared_task
def delete_playlist(creds, playlist_id):
+ creds.pop("channel_title")
credentials = google.oauth2.credentials.Credentials(**creds)
yt_api = googleapiclient.discovery.build("youtube", "v3", credentials=credentials)
From 83404e2ed5b969678c0cf7f0fd6a39fecd31eb7e Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Sun, 15 Jun 2025 10:25:40 +0200
Subject: [PATCH 04/41] Add title field to YoutubeCredentials model and update
related templates and tasks
---
.../migrations/0017_youtubecredentials_title.py | 17 +++++++++++++++++
game/models.py | 1 +
game/tasks.py | 2 --
game/templates/game/home.html | 2 +-
game/templates/game/musikgame_form.html | 2 +-
game/views.py | 4 ++--
6 files changed, 22 insertions(+), 6 deletions(-)
create mode 100644 game/migrations/0017_youtubecredentials_title.py
diff --git a/game/migrations/0017_youtubecredentials_title.py b/game/migrations/0017_youtubecredentials_title.py
new file mode 100644
index 0000000..89870e4
--- /dev/null
+++ b/game/migrations/0017_youtubecredentials_title.py
@@ -0,0 +1,17 @@
+# Generated by Django 5.2.3 on 2025-06-15 08:24
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("game", "0016_alter_groupleader_member"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="youtubecredentials",
+ name="title",
+ field=models.CharField(blank=True),
+ ),
+ ]
diff --git a/game/models.py b/game/models.py
index a3f104d..1f8ce2f 100644
--- a/game/models.py
+++ b/game/models.py
@@ -11,6 +11,7 @@ from . import tasks
class YoutubeCredentials(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
credentials = models.JSONField()
+ title = models.CharField(blank=True)
class Group(models.Model):
diff --git a/game/tasks.py b/game/tasks.py
index ceb5777..22a7dc0 100644
--- a/game/tasks.py
+++ b/game/tasks.py
@@ -8,7 +8,6 @@ from . import models
@shared_task
def generate_playlist(creds, game_pk):
game = models.MusikGame.objects.get(pk=game_pk)
- creds.pop("channel_title")
credentials = google.oauth2.credentials.Credentials(**creds)
yt_api = googleapiclient.discovery.build("youtube", "v3", credentials=credentials)
pl_request = yt_api.playlists().insert(
@@ -48,7 +47,6 @@ def generate_playlist(creds, game_pk):
@shared_task
def delete_playlist(creds, playlist_id):
- creds.pop("channel_title")
credentials = google.oauth2.credentials.Credentials(**creds)
yt_api = googleapiclient.discovery.build("youtube", "v3", credentials=credentials)
diff --git a/game/templates/game/home.html b/game/templates/game/home.html
index 27511b5..5a290a5 100644
--- a/game/templates/game/home.html
+++ b/game/templates/game/home.html
@@ -9,7 +9,7 @@
{% if not user.youtubecredentials.credentials %}
Me connecter au compte Youtube
{% else %}
- Connecté au compte Youtube {{ user.youtubecredentials.credentials.channel_title }} .
+ Connecté au compte Youtube {{ user.youtubecredentials.title }} .
{% endif %}
diff --git a/game/templates/game/musikgame_form.html b/game/templates/game/musikgame_form.html
index 03820a3..38ed5b6 100644
--- a/game/templates/game/musikgame_form.html
+++ b/game/templates/game/musikgame_form.html
@@ -8,7 +8,7 @@
{% if not user.youtubecredentials.credentials %}
Me connecter au compte Youtube
{% else %}
- Une playlist sera générée automatiquement sur le compte Youtube {{ user.youtubecredentials.credentials.channel_title }} .
+ Une playlist sera générée automatiquement sur le compte Youtube {{ user.youtubecredentials.title }} .
{% endif %}
{% form form %}
diff --git a/game/views.py b/game/views.py
index cbcbdb8..096bc12 100644
--- a/game/views.py
+++ b/game/views.py
@@ -381,8 +381,8 @@ class YoutubeCallbackView(LoginRequiredMixin, View):
"client_id": credentials.client_id,
"client_secret": credentials.client_secret,
"granted_scopes": credentials.granted_scopes,
- "channel_title": res["items"][0]["snippet"]["title"],
- }
+ },
+ "title": res["items"][0]["snippet"]["title"],
},
)
messages.add_message(request, messages.SUCCESS, "Connexion à Youtube réussie.")
From 7259046916ced4d9d2188de3d2ed004729826347 Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Sun, 15 Jun 2025 10:37:55 +0200
Subject: [PATCH 05/41] Add email configuration settings to Django settings
---
musik/settings.py | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/musik/settings.py b/musik/settings.py
index b12b987..aeb5f91 100644
--- a/musik/settings.py
+++ b/musik/settings.py
@@ -141,3 +141,12 @@ YOUTUBE_API_KEY = os.getenv("YOUTUBE_API_KEY", "")
YOUTUBE_OAUTH_SECRETS = os.getenv("YOUTUBE_OAUTH_SECRETS", "")
CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL", None)
+
+EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
+EMAIL_HOST = os.getenv("EMAIL_HOST")
+EMAIL_PORT = os.getenv("EMAIL_PORT", 587)
+EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER")
+EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD")
+EMAIL_USE_SSL = os.getenv("EMAIL_USE_SSL", False)
+EMAIL_USE_TLS = os.getenv("EMAIL_USE_TLS", not EMAIL_USE_SSL)
+DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", EMAIL_HOST_USER)
From a24fb897a31c9a57e4a1fab585ef7fee4d1c97f0 Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Sun, 15 Jun 2025 10:42:16 +0200
Subject: [PATCH 06/41] Refactor user registration and login forms for improved
structure and error handling
---
base/templates/auth/user_form.html | 2 +-
base/templates/registration/login.html | 33 +++++++++++++++++++++-----
2 files changed, 28 insertions(+), 7 deletions(-)
diff --git a/base/templates/auth/user_form.html b/base/templates/auth/user_form.html
index 257d1dc..20c40a8 100644
--- a/base/templates/auth/user_form.html
+++ b/base/templates/auth/user_form.html
@@ -2,5 +2,5 @@
{% load form %}
{% block content %}
Créer un compte
- {% form form %}
+ {% form form submit="Créer mon compte" %}
{% endblock content %}
diff --git a/base/templates/registration/login.html b/base/templates/registration/login.html
index 1a78811..8bbb8fd 100644
--- a/base/templates/registration/login.html
+++ b/base/templates/registration/login.html
@@ -1,9 +1,30 @@
{% extends "base.html" %}
-{% load form %}
{% block content %}
Connexion
-
- Créer un compte
-
- {% form form submit="Se connecter" %}
- {% endblock content %}
+ {% for error in form.non_field_errors %}{{ error }} {% endfor %}
+
+{% endblock content %}
From da1c7507710cc77a60029ff101b40bcea4fb28e4 Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Sun, 15 Jun 2025 11:02:35 +0200
Subject: [PATCH 07/41] Add password change and reset functionality with
corresponding views and templates
---
.../registration/password_change_form.html | 8 +++++++
.../registration/password_reset_confirm.html | 8 +++++++
.../registration/password_reset_form.html | 8 +++++++
base/urls.py | 21 +++++++++++++++++--
base/views.py | 18 ++++++++++++++++
5 files changed, 61 insertions(+), 2 deletions(-)
create mode 100644 base/templates/registration/password_change_form.html
create mode 100644 base/templates/registration/password_reset_confirm.html
create mode 100644 base/templates/registration/password_reset_form.html
diff --git a/base/templates/registration/password_change_form.html b/base/templates/registration/password_change_form.html
new file mode 100644
index 0000000..25304fe
--- /dev/null
+++ b/base/templates/registration/password_change_form.html
@@ -0,0 +1,8 @@
+{% extends "base.html" %}
+{% load form %}
+{% block content %}
+
+ Changer mon mot de passe
+
+ {% form form submit="Changer mon mot de passe" %}
+ {% endblock content %}
diff --git a/base/templates/registration/password_reset_confirm.html b/base/templates/registration/password_reset_confirm.html
new file mode 100644
index 0000000..2f7f317
--- /dev/null
+++ b/base/templates/registration/password_reset_confirm.html
@@ -0,0 +1,8 @@
+{% extends "base.html" %}
+{% load form %}
+{% block content %}
+
+ Réinitialiser mon mot de passe
+
+ {% form form submit="Réinitialiser mon mot de passe" %}
+ {% endblock content %}
diff --git a/base/templates/registration/password_reset_form.html b/base/templates/registration/password_reset_form.html
new file mode 100644
index 0000000..2f7f317
--- /dev/null
+++ b/base/templates/registration/password_reset_form.html
@@ -0,0 +1,8 @@
+{% extends "base.html" %}
+{% load form %}
+{% block content %}
+
+ Réinitialiser mon mot de passe
+
+ {% form form submit="Réinitialiser mon mot de passe" %}
+ {% endblock content %}
diff --git a/base/urls.py b/base/urls.py
index ea1f6ba..6723389 100644
--- a/base/urls.py
+++ b/base/urls.py
@@ -1,4 +1,5 @@
-from django.urls import include, path
+from django.contrib.auth import views as auth_views
+from django.urls import path
from django.views.generic import TemplateView
from . import views
@@ -6,6 +7,22 @@ from . import views
urlpatterns = [
path("", views.HomePageView.as_view(), name="index"),
path("accounts/signup/", views.SignupView.as_view(), name="signup"),
- path("accounts/", include("django.contrib.auth.urls")),
+ path("accounts/login/", auth_views.LoginView.as_view(), name="login"),
+ path("accounts/logout/", auth_views.LogoutView.as_view(), name="logout"),
+ path(
+ "accounts/password_change/",
+ views.PasswordChangeView.as_view(),
+ name="password_change",
+ ),
+ path(
+ "accounts/password_reset/",
+ views.PasswordResetView.as_view(),
+ name="password_reset",
+ ),
+ path(
+ "accounts/reset///",
+ views.PasswordResetConfirmView.as_view(),
+ name="password_reset_confirm",
+ ),
path("legal/", TemplateView.as_view(template_name="privacy.html"), name="legal"),
]
diff --git a/base/views.py b/base/views.py
index 32dd5bb..26f9163 100644
--- a/base/views.py
+++ b/base/views.py
@@ -1,3 +1,4 @@
+from django.contrib.auth import views as auth_views
from django.contrib.auth.models import User
from django.contrib.messages.views import SuccessMessageMixin
from django.shortcuts import redirect
@@ -22,3 +23,20 @@ class SignupView(SuccessMessageMixin, CreateView):
form_class = forms.UserSignupForm
success_url = reverse_lazy("login")
success_message = "Le compte %(username)s a été créé avec succès."
+
+
+class PasswordChangeView(SuccessMessageMixin, auth_views.PasswordChangeView):
+ success_message = "Le mot de passe a été changé avec succès."
+ success_url = reverse_lazy("index")
+
+
+class PasswordResetView(SuccessMessageMixin, auth_views.PasswordResetView):
+ success_message = "Un courriel a été envoyé avec les instructions pour réinitialiser votre mot de passe."
+ success_url = reverse_lazy("login")
+
+
+class PasswordResetConfirmView(
+ SuccessMessageMixin, auth_views.PasswordResetConfirmView
+):
+ success_message = "Le mot de passe a été réinitialisé avec succès."
+ success_url = reverse_lazy("login")
From cd0ca2f5ea3e1834763261047f4908b7b451abf1 Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Sun, 15 Jun 2025 11:19:12 +0200
Subject: [PATCH 08/41] Add account settings page with YouTube connection
management and user update functionality
---
base/templates/auth/user_settings.html | 37 ++++++++++++++++++++++++++
base/templates/base.html | 4 ++-
base/urls.py | 1 +
base/views.py | 14 +++++++++-
game/templates/game/home.html | 7 +----
game/urls.py | 1 +
game/views.py | 6 +++++
7 files changed, 62 insertions(+), 8 deletions(-)
create mode 100644 base/templates/auth/user_settings.html
diff --git a/base/templates/auth/user_settings.html b/base/templates/auth/user_settings.html
new file mode 100644
index 0000000..9fa4218
--- /dev/null
+++ b/base/templates/auth/user_settings.html
@@ -0,0 +1,37 @@
+{% extends "base.html" %}
+{% load form %}
+{% block content %}
+
+ Mon compte
+
+ {% for error in form.non_field_errors %}{{ error }} {% endfor %}
+
+{% endblock content %}
diff --git a/base/templates/base.html b/base/templates/base.html
index 8c84913..4622abb 100644
--- a/base/templates/base.html
+++ b/base/templates/base.html
@@ -47,7 +47,9 @@
{% if user.is_authenticated %}
- {{ user.username }}
+
+ {{ user.username }}
+
{% 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 18/41] 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 19/41] 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 20/41] 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 21/41] 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
+
+
+
+
+
+
+
+
+
+ Musique
+
+
+ Joueur
+
+ {% for player in musikgame.musicgameresults_set.all %}
+ {{ player.player.username }}
+
+ {% if forloop.first %} {% endif %}
+ {{ player.score }}
+
+ {% endfor %}
+
+
+
+ {% for music in musikgame.musicgameorder_set.all %}
+
+ {{ music.order }}
+
+ {{ music.music_video.title }}
+
+ {{ music.player }}
+ {% for player in musikgame.musicgameresults_set.all %}
+ {% answer player music %}
+ {% endfor %}
+
+ {% endfor %}
+
+
+
+
+{% 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
-
-
-
-
-
-
-
-
- Musique
-
-
- Joueur
-
-
-
-
- {% for music in musikgame.musicgameorder_set.all %}
-
- {{ music.order }}
-
- {{ music.music_video.title }}
-
- {{ music.player }}
-
- {% endfor %}
-
-
-
- {% 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 22/41] 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 %}
-
+
{{ player.player.username }} {{ player.score }}
{% 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 23/41] 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 24/41] 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 25/41] 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 %}
-
-
+
+
Ajouter
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 26/41] 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 27/41] 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 %}
+
+ {{ field.label }}
+ {{ field }}
+ {% if field.errors %}
+ {{ field.errors|join:", " }}
+ {% endif %}
+
+ {% endfor %}
+
+ Créer mon compte
+
+ 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 %}
- {{ field.label }}
+ {% if field.id_for_label %}
+
+ {{ field.label }}
{% else %}
{{ field.label }}
{% endif %}
@@ -15,16 +15,16 @@
{% if field.errors %}
{{ field.errors|join:", " }}
{% endif %}
-
+
{% endfor %}
Me connecter
-
+
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 %}
-
- {% if musics %}
-
-
- Supprimer
-
-
- Retirer de la blacklist
-
-
- {% endif %}
-
-
-
- {% csrf_token %}
-
-
- Ajouter
-
-
+ {% 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 %}
+
+ {% if musics %}
+
+
+ Supprimer
+
+
+ Retirer de la blacklist
+
+
+ {% endif %}
+
+
+
+ {% csrf_token %}
+
+
+ Ajouter
+
+
From 98183575af5cc3c47494130f221570b4f4b6ecd7 Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Sun, 15 Jun 2025 16:42:39 +0200
Subject: [PATCH 28/41] 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 29/41] 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 %}Valider mes réponses {% endif %}
+ {% if not musikgame.over %}Sauvegarder mes réponses {% 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 30/41] 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 31/41] 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 32/41] 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 %}
-
-
- Finir la partie
-
-
{% endif %}
-
- {% endif %}
+ {% if musikgame.over %}
+ Mes réponses
+ {% else %}
+ Répondre
+ {% endif %}
+
+ {% if is_leader and not musikgame.over %}
+
+
+ Finir la partie
+
+
+ {% 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 33/41] 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 34/41] 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 35/41] 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 36/41] 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 37/41] 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. Burkhart – Mentions légales
+Musik {{ VERSION }} – © Edgar P. Burkhart – Mentions 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 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 ?
+
+
+
{% include "footer.html" %}
diff --git a/base/templates/privacy.html b/base/templates/privacy.html
index 8e78465..1549403 100644
--- a/base/templates/privacy.html
+++ b/base/templates/privacy.html
@@ -16,6 +16,7 @@
Youtube est une marque de Google LLC.
- La suppression des données stockée par le service Musik pour son utilisation peut être demandée par email à Edgar P. Burkhart .
+ Les données saisies dans Musik (groupes créés, listes de musiques) sont conservées jusqu'à demande de suppression. La suppression du compte entraîne la suppression de l'ensemble des données qui y sont liées.
+ La suppression du compte peut être demandée dans les paramètres du compte. La suppression des données est immédiate et définitive.
{% endblock content %}
From a462fabde4aaa431b5b7cec6be408dbdb3a768e4 Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Mon, 16 Jun 2025 22:04:51 +0200
Subject: [PATCH 38/41] Bump version to 0.4.3 in settings, pyproject.toml, and
uv.lock
---
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 a712445..b89d791 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.2"
+VERSION = "0.4.3"
# 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 17df9a9..e6d684f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "musik"
-version = "0.4.2"
+version = "0.4.3"
description = "Le jeu de Musik."
readme = "README.md"
requires-python = ">=3.12"
diff --git a/uv.lock b/uv.lock
index 0e3ae81..580e3aa 100644
--- a/uv.lock
+++ b/uv.lock
@@ -423,7 +423,7 @@ wheels = [
[[package]]
name = "musik"
-version = "0.4.2"
+version = "0.4.3"
source = { virtual = "." }
dependencies = [
{ name = "celery" },
From b20eee8cfbdc68cb45fc33f03f203b1fcc2f37c1 Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Mon, 16 Jun 2025 23:59:02 +0200
Subject: [PATCH 39/41] Add favicon assets and update HTML references for
improved branding
---
base/static/favicon/apple-touch-icon.png | Bin 0 -> 4968 bytes
base/static/favicon/favicon-96x96.png | Bin 0 -> 3680 bytes
base/static/favicon/favicon.ico | Bin 0 -> 15086 bytes
base/static/favicon/favicon.svg | 25 ++++++++++++++++++
base/static/favicon/site.webmanifest | 21 +++++++++++++++
.../favicon/web-app-manifest-192x192.png | Bin 0 -> 9627 bytes
.../favicon/web-app-manifest-512x512.png | Bin 0 -> 47864 bytes
base/templates/base.html | 2 +-
base/templates/favicon.html | 8 ++++++
9 files changed, 55 insertions(+), 1 deletion(-)
create mode 100644 base/static/favicon/apple-touch-icon.png
create mode 100644 base/static/favicon/favicon-96x96.png
create mode 100644 base/static/favicon/favicon.ico
create mode 100644 base/static/favicon/favicon.svg
create mode 100644 base/static/favicon/site.webmanifest
create mode 100644 base/static/favicon/web-app-manifest-192x192.png
create mode 100644 base/static/favicon/web-app-manifest-512x512.png
create mode 100644 base/templates/favicon.html
diff --git a/base/static/favicon/apple-touch-icon.png b/base/static/favicon/apple-touch-icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..abb8a48a10741bd4022025fbe2c0489e8b560a22
GIT binary patch
literal 4968
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
mS*s3-)rVvgT506A%Krfr(Fb8t)7e!30000w`
literal 0
HcmV?d00001
diff --git a/base/static/favicon/favicon-96x96.png b/base/static/favicon/favicon-96x96.png
new file mode 100644
index 0000000000000000000000000000000000000000..beab2883d963ef589f46455d76865600bcc756d7
GIT binary patch
literal 3680
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/base/static/favicon/site.webmanifest b/base/static/favicon/site.webmanifest
new file mode 100644
index 0000000..af6d059
--- /dev/null
+++ b/base/static/favicon/site.webmanifest
@@ -0,0 +1,21 @@
+{
+ "name": "MyWebSite",
+ "short_name": "MySite",
+ "icons": [
+ {
+ "src": "/web-app-manifest-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "/web-app-manifest-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ }
+ ],
+ "theme_color": "#aa40bf",
+ "background_color": "#ffffff",
+ "display": "standalone"
+}
diff --git a/base/static/favicon/web-app-manifest-192x192.png b/base/static/favicon/web-app-manifest-192x192.png
new file mode 100644
index 0000000000000000000000000000000000000000..f7f97dad8a34c5cf2a48053e79f92ce1ccacdf79
GIT binary patch
literal 9627
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
R;{X5v002ovPDHLkV1g0R48Q;Y
literal 0
HcmV?d00001
diff --git a/base/static/favicon/web-app-manifest-512x512.png b/base/static/favicon/web-app-manifest-512x512.png
new file mode 100644
index 0000000000000000000000000000000000000000..9e4e3813ff18a65002ade69a10d67f0c7a3b687a
GIT binary patch
literal 47864
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
z