From 3294ef2a82aef1b9aeb6bfa279dd7381a17552d1 Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Fri, 13 Jun 2025 14:21:31 +0200
Subject: [PATCH 01/44] Create base structure for Django project
---
.pre-commit-config.yaml | 17 ++++
.python-version | 1 +
manage.py | 23 ++++++
musik/__init__.py | 0
musik/__main__.py | 118 ---------------------------
musik/asgi.py | 16 ++++
musik/list.py | 72 -----------------
musik/settings.py | 122 ++++++++++++++++++++++++++++
musik/urls.py | 23 ++++++
musik/wsgi.py | 16 ++++
musik/youtube.py | 68 ----------------
pyproject.toml | 14 ++++
uv.lock | 173 ++++++++++++++++++++++++++++++++++++++++
13 files changed, 405 insertions(+), 258 deletions(-)
create mode 100644 .pre-commit-config.yaml
create mode 100644 .python-version
create mode 100755 manage.py
create mode 100644 musik/__init__.py
delete mode 100644 musik/__main__.py
create mode 100644 musik/asgi.py
delete mode 100644 musik/list.py
create mode 100644 musik/settings.py
create mode 100644 musik/urls.py
create mode 100644 musik/wsgi.py
delete mode 100644 musik/youtube.py
create mode 100644 pyproject.toml
create mode 100644 uv.lock
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..4220f94
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,17 @@
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v5.0.0
+ hooks:
+ - id: check-yaml
+ - id: end-of-file-fixer
+ - id: trailing-whitespace
+ - repo: https://github.com/pycqa/isort
+ rev: 6.0.1
+ hooks:
+ - id: isort
+ name: isort (python)
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.9.10
+ hooks:
+ - id: ruff
+ - id: ruff-format
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000..e4fba21
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.12
diff --git a/manage.py b/manage.py
new file mode 100755
index 0000000..c03444d
--- /dev/null
+++ b/manage.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+
+import os
+import sys
+
+
+def main():
+ """Run administrative tasks."""
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "musik.settings")
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/musik/__init__.py b/musik/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/musik/__main__.py b/musik/__main__.py
deleted file mode 100644
index 8547fdd..0000000
--- a/musik/__main__.py
+++ /dev/null
@@ -1,118 +0,0 @@
-import argparse
-import logging
-from pathlib import Path
-
-from .list import generate_list, write_blacklist, write_results
-
-ROOT_PATH = Path("./lists")
-BLACKLIST = Path("./blacklists")
-RESULTS = Path("./results")
-NUM_MUS = 2
-
-
-def main():
- # Lecture arguments console
- parser = argparse.ArgumentParser(
- prog="python -m musik",
- description="Lancer une partie de Musik",
- formatter_class=argparse.ArgumentDefaultsHelpFormatter,
- )
- parser.add_argument(
- "-a",
- "--no-api",
- action="store_true",
- help="Désactiver l'API Youtube ; affiche la liste des liens",
- )
- parser.add_argument(
- "-c",
- "--no-save-creds",
- action="store_true",
- help="Désactiver l'enregistrement de la connexion Youtube",
- )
- parser.add_argument(
- "-b",
- "--no-blacklist",
- action="store_true",
- help="Désactiver le méchanisme de blacklist en lecture et écriture",
- )
- parser.add_argument(
- "-n",
- "--number",
- type=int,
- default=NUM_MUS,
- help="Modifier le nombre de musiques par joueur",
- )
- parser.add_argument(
- "--lists",
- type=Path,
- default=ROOT_PATH,
- help="Sélectionner le dossier contenant les listes de musiques",
- )
- parser.add_argument(
- "--blacklists",
- type=Path,
- default=BLACKLIST,
- help="Sélectionner le dossier contenant les blacklist",
- )
- parser.add_argument(
- "--results",
- type=Path,
- default=RESULTS,
- help="Sélectionner le dossier pour stocker les résultats",
- )
- parser.add_argument(
- "-v",
- "--verbose",
- action="store_const",
- const=logging.DEBUG,
- default=logging.INFO,
- )
-
- args = parser.parse_args()
- if not args.no_api:
- from .youtube import create_playlist
-
- # Configuration logs
- logger = logging.getLogger("musik")
- logging.basicConfig(
- level=args.verbose, format="%(asctime)s\t%(levelname)s\t%(message)s"
- )
-
- logger.info("Vérification")
- if not args.lists.is_dir():
- logger.error(f"Le dossier <{args.lists}> n'existe pas.")
- return
- if not args.blacklists.is_dir():
- logger.warning(f"Le dossier <{args.blacklists}> n'existe pas, il va être créé.")
- args.blacklists.mkdir()
- if not args.results.is_dir():
- logger.warning(f"Le dossier <{args.results}> n'existe pas, il va être créé.")
- args.results.mkdir()
- if args.number < 1:
- logger.error("Le nombre de musiques est inférieur à 1.")
- return
-
- # Lecture des fichiers musique dans ROOT_PATH
- # Faire un dossier différent pour les gens qui ne jouent pas
- logger.info("Génération de la liste de musiques")
- musik_list = generate_list(args)
-
- if not args.no_api:
- create_playlist(musik_list, args)
- else:
- logger.info("Liste des musiques :")
- for _, musik in musik_list:
- logger.info(f"# https://www.youtube.com/watch?v={musik}")
-
- # Écriture des résultats
- logger.info("Écriture des résultats")
- write_results(musik_list, args)
-
- # Écriture de la blacklist
- if not args.no_blacklist:
- logger.info("Écriture de la blacklist")
- write_blacklist(musik_list, args)
-
-
-if __name__ == "__main__":
- main()
diff --git a/musik/asgi.py b/musik/asgi.py
new file mode 100644
index 0000000..5456555
--- /dev/null
+++ b/musik/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for musik project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "musik.settings")
+
+application = get_asgi_application()
diff --git a/musik/list.py b/musik/list.py
deleted file mode 100644
index e5fc992..0000000
--- a/musik/list.py
+++ /dev/null
@@ -1,72 +0,0 @@
-import logging
-import random
-import sys
-from datetime import datetime
-from urllib.parse import parse_qs, urlparse
-
-logger = logging.getLogger("musik.list")
-
-
-def bl_path(bl, user):
- return bl.joinpath(user).with_suffix(".txt")
-
-
-def parse_musik(raw):
- if "youtube.com" in raw:
- return parse_qs(urlparse(raw).query).get("v", [None])[0]
- elif "youtu.be" in raw:
- return urlparse(raw).path[1:]
-
- return raw
-
-
-def generate_list(args):
- musik_list = []
- user_list = []
-
- for q in args.lists.iterdir():
- _u = q.stem
- logger.info(f"Musiques de {_u}")
- if (not args.no_blacklist) and bl_path(args.blacklists, _u).exists():
- logger.debug("Blacklist")
- with bl_path(args.blacklists, _u).open("r") as blf:
- blacklist = blf.read().splitlines()
- else:
- blacklist = []
-
- logger.debug("Lecture de la liste")
- with q.open() as f:
- _raw_musiks = [parse_musik(_musik) for _musik in f.read().splitlines()]
- _musiks = list(set(filter(lambda _m: _m not in blacklist, _raw_musiks)))
-
- if len(_musiks) < args.number:
- logger.error(
- f"{_u} a {len(_musiks)} musique(s) non black-listée(s) "
- f"au lieu de {args.number}"
- )
- sys.exit()
-
- logger.debug("Ajout des musiques à la liste")
- musik_list += random.sample(_musiks, args.number)
- user_list += [_u] * args.number
-
- # Shuffle musics
- logger.info("Classement aléatoire des musiques")
- user_musik_list = list(zip(user_list, musik_list))
- random.shuffle(user_musik_list)
- return user_musik_list
-
-
-def write_blacklist(musik_list, args):
- for user, musik in musik_list:
- with bl_path(args.blacklists, user).open("a") as f:
- f.write("\n")
- f.write(musik)
-
-
-def write_results(musik_list, args):
- with args.results.joinpath(datetime.now().strftime("%Y%m%d %H%M%S")).with_suffix(
- ".txt"
- ).open("a") as f:
- f.write(f"Résultats {datetime.now()}\n\n")
- f.write("\n".join("\t".join(um) for um in musik_list))
diff --git a/musik/settings.py b/musik/settings.py
new file mode 100644
index 0000000..1248f4e
--- /dev/null
+++ b/musik/settings.py
@@ -0,0 +1,122 @@
+"""
+Django settings for musik project.
+
+Generated by 'django-admin startproject' using Django 5.2.3.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.2/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/5.2/ref/settings/
+"""
+
+from pathlib import Path
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = "django-insecure-&z*xu$^w8btr(%1!y#+0a98)l_q*+*6z54611pi678mdpsar_="
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ "django.contrib.admin",
+ "django.contrib.auth",
+ "django.contrib.contenttypes",
+ "django.contrib.sessions",
+ "django.contrib.messages",
+ "django.contrib.staticfiles",
+]
+
+MIDDLEWARE = [
+ "django.middleware.security.SecurityMiddleware",
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.middleware.common.CommonMiddleware",
+ "django.middleware.csrf.CsrfViewMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "django.contrib.messages.middleware.MessageMiddleware",
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
+]
+
+ROOT_URLCONF = "musik.urls"
+
+TEMPLATES = [
+ {
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "DIRS": [],
+ "APP_DIRS": True,
+ "OPTIONS": {
+ "context_processors": [
+ "django.template.context_processors.request",
+ "django.contrib.auth.context_processors.auth",
+ "django.contrib.messages.context_processors.messages",
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = "musik.wsgi.application"
+
+
+# Database
+# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
+
+DATABASES = {
+ "default": {
+ "ENGINE": "django.db.backends.sqlite3",
+ "NAME": BASE_DIR / "db.sqlite3",
+ }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/5.2/topics/i18n/
+
+LANGUAGE_CODE = "en-us"
+
+TIME_ZONE = "UTC"
+
+USE_I18N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/5.2/howto/static-files/
+
+STATIC_URL = "static/"
+
+# Default primary key field type
+# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
+
+DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
diff --git a/musik/urls.py b/musik/urls.py
new file mode 100644
index 0000000..1e00b4b
--- /dev/null
+++ b/musik/urls.py
@@ -0,0 +1,23 @@
+"""
+URL configuration for musik project.
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/5.2/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.urls import include, path
+ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
+"""
+
+from django.contrib import admin
+from django.urls import path
+
+urlpatterns = [
+ path("admin/", admin.site.urls),
+]
diff --git a/musik/wsgi.py b/musik/wsgi.py
new file mode 100644
index 0000000..5103bbd
--- /dev/null
+++ b/musik/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for musik project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "musik.settings")
+
+application = get_wsgi_application()
diff --git a/musik/youtube.py b/musik/youtube.py
deleted file mode 100644
index 4c690e3..0000000
--- a/musik/youtube.py
+++ /dev/null
@@ -1,68 +0,0 @@
-import logging
-import pickle
-from datetime import date
-from pathlib import Path
-
-import google_auth_oauthlib.flow
-import googleapiclient.discovery
-import googleapiclient.errors
-
-logger = logging.getLogger("musik.youtube")
-
-
-def create_playlist(musik_list, args):
- pickle_path = Path("./youtube.pickle")
-
- # Connexion à l'API youtube, obtention d'un jeton OAuth
- logger.info("Connexion à l'API Youtube")
- flow = google_auth_oauthlib.flow.InstalledAppFlow.from_client_secrets_file(
- "./secret.json", ["https://www.googleapis.com/auth/youtube.force-ssl"]
- )
- if (not args.no_save_creds) and pickle_path.is_file():
- with pickle_path.open("rb") as f:
- credentials = pickle.load(f)
- else:
- credentials = flow.run_local_server(port=0)
- if not args.no_save_creds:
- with pickle_path.open("wb") as f:
- pickle.dump(credentials, f)
-
- youtube = googleapiclient.discovery.build("youtube", "v3", credentials=credentials)
-
- # Création d'une playlist
- logger.info("Création de la playlist")
- pl_request = youtube.playlists().insert(
- part="snippet,status",
- body={
- "snippet": {
- "title": f"Musik {date.today().strftime('%x')}",
- },
- "status": {
- "privacyStatus": "private",
- },
- },
- )
- pl_response = pl_request.execute()
- logger.info(
- "Playlist créée : "
- f"https://www.youtube.com/playlist?list={pl_response['id']}",
- )
-
- # Insertion des musiques dans la playlist
- logger.info("Insertion des musiques dans la playlist")
- for _, musik in musik_list:
- logger.debug(musik)
- request = youtube.playlistItems().insert(
- part="snippet",
- body={
- "snippet": {
- "playlistId": pl_response.get("id"),
- "position": 0,
- "resourceId": {
- "kind": "youtube#video",
- "videoId": musik,
- },
- }
- },
- )
- request.execute()
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..026e134
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,14 @@
+[project]
+name = "musik"
+version = "0.1.0"
+description = "Le jeu de Musik."
+readme = "README.md"
+requires-python = ">=3.12"
+dependencies = [
+ "django>=5.2.3",
+]
+
+[dependency-groups]
+dev = [
+ "pre-commit>=4.2.0",
+]
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..f4a2ee2
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,173 @@
+version = 1
+revision = 2
+requires-python = ">=3.12"
+
+[[package]]
+name = "asgiref"
+version = "3.8.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186, upload-time = "2024-03-22T14:39:36.863Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828, upload-time = "2024-03-22T14:39:34.521Z" },
+]
+
+[[package]]
+name = "cfgv"
+version = "3.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" },
+]
+
+[[package]]
+name = "distlib"
+version = "0.3.9"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923, upload-time = "2024-10-09T18:35:47.551Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973, upload-time = "2024-10-09T18:35:44.272Z" },
+]
+
+[[package]]
+name = "django"
+version = "5.2.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "asgiref" },
+ { name = "sqlparse" },
+ { name = "tzdata", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c6/af/77b403926025dc6f7fd7b31256394d643469418965eb528eab45d0505358/django-5.2.3.tar.gz", hash = "sha256:335213277666ab2c5cac44a792a6d2f3d58eb79a80c14b6b160cd4afc3b75684", size = 10850303, upload-time = "2025-06-10T10:14:05.174Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1b/11/7aff961db37e1ea501a2bb663d27a8ce97f3683b9e5b83d3bfead8b86fa4/django-5.2.3-py3-none-any.whl", hash = "sha256:c517a6334e0fd940066aa9467b29401b93c37cec2e61365d663b80922542069d", size = 8301935, upload-time = "2025-06-10T10:13:58.993Z" },
+]
+
+[[package]]
+name = "filelock"
+version = "3.18.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" },
+]
+
+[[package]]
+name = "identify"
+version = "2.6.12"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/88/d193a27416618628a5eea64e3223acd800b40749a96ffb322a9b55a49ed1/identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6", size = 99254, upload-time = "2025-05-23T20:37:53.3Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145, upload-time = "2025-05-23T20:37:51.495Z" },
+]
+
+[[package]]
+name = "musik"
+version = "0.1.0"
+source = { virtual = "." }
+dependencies = [
+ { name = "django" },
+]
+
+[package.dev-dependencies]
+dev = [
+ { name = "pre-commit" },
+]
+
+[package.metadata]
+requires-dist = [{ name = "django", specifier = ">=5.2.3" }]
+
+[package.metadata.requires-dev]
+dev = [{ name = "pre-commit", specifier = ">=4.2.0" }]
+
+[[package]]
+name = "nodeenv"
+version = "1.9.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.3.8"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" },
+]
+
+[[package]]
+name = "pre-commit"
+version = "4.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cfgv" },
+ { name = "identify" },
+ { name = "nodeenv" },
+ { name = "pyyaml" },
+ { name = "virtualenv" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" },
+ { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" },
+ { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
+ { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
+ { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
+ { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
+]
+
+[[package]]
+name = "sqlparse"
+version = "0.5.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999, upload-time = "2024-12-10T12:05:30.728Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415, upload-time = "2024-12-10T12:05:27.824Z" },
+]
+
+[[package]]
+name = "tzdata"
+version = "2025.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
+]
+
+[[package]]
+name = "virtualenv"
+version = "20.31.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "distlib" },
+ { name = "filelock" },
+ { name = "platformdirs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c3a104fd0c495184c4f2336d65baf398e3c75d72ea94/virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af", size = 6076316, upload-time = "2025-05-08T17:58:23.811Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" },
+]
From d7545ab43caf6dbdabf9f621765d46fd61c28c1e Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Fri, 13 Jun 2025 15:51:32 +0200
Subject: [PATCH 02/44] Setup basic auth
---
base/__init__.py | 0
base/apps.py | 6 +
base/migrations/__init__.py | 0
base/static/css/main.css | 9 ++
base/static/logo.svg | 25 ++++
base/templates/base.html | 36 ++++++
base/templates/base/form.html | 27 ++++
base/templates/index.html | 5 +
base/templates/registration/login.html | 7 ++
base/templatetags/__init__.py | 0
base/templatetags/form.py | 11 ++
base/urls.py | 8 ++
base/views.py | 5 +
musik/settings.py | 8 +-
musik/urls.py | 3 +-
pyproject.toml | 1 +
uv.lock | 168 ++++++++++++++++++++++++-
17 files changed, 315 insertions(+), 4 deletions(-)
create mode 100644 base/__init__.py
create mode 100644 base/apps.py
create mode 100644 base/migrations/__init__.py
create mode 100644 base/static/css/main.css
create mode 100644 base/static/logo.svg
create mode 100644 base/templates/base.html
create mode 100644 base/templates/base/form.html
create mode 100644 base/templates/index.html
create mode 100644 base/templates/registration/login.html
create mode 100644 base/templatetags/__init__.py
create mode 100644 base/templatetags/form.py
create mode 100644 base/urls.py
create mode 100644 base/views.py
diff --git a/base/__init__.py b/base/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/base/apps.py b/base/apps.py
new file mode 100644
index 0000000..bca3fb0
--- /dev/null
+++ b/base/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class BaseConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "base"
diff --git a/base/migrations/__init__.py b/base/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/base/static/css/main.css b/base/static/css/main.css
new file mode 100644
index 0000000..0b47044
--- /dev/null
+++ b/base/static/css/main.css
@@ -0,0 +1,9 @@
+img.logo {
+ border-radius: var(--pico-border-radius);
+}
+
+header .container {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
diff --git a/base/static/logo.svg b/base/static/logo.svg
new file mode 100644
index 0000000..472e617
--- /dev/null
+++ b/base/static/logo.svg
@@ -0,0 +1,25 @@
+
+
diff --git a/base/templates/base.html b/base/templates/base.html
new file mode 100644
index 0000000..5d38174
--- /dev/null
+++ b/base/templates/base.html
@@ -0,0 +1,36 @@
+{% load static %}
+
+
+
+
+
+ {% block title %}Musik{% endblock %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% block content %}
+ {% endblock content %}
+
+
+
diff --git a/base/templates/base/form.html b/base/templates/base/form.html
new file mode 100644
index 0000000..57d5486
--- /dev/null
+++ b/base/templates/base/form.html
@@ -0,0 +1,27 @@
+{% if form.non_field_errors %}
+
+{% endif %}
+
+
diff --git a/base/templates/index.html b/base/templates/index.html
new file mode 100644
index 0000000..64383d8
--- /dev/null
+++ b/base/templates/index.html
@@ -0,0 +1,5 @@
+{% extends "base.html" %}
+
+{% block content %}
+Musik
+{% endblock content %}
diff --git a/base/templates/registration/login.html b/base/templates/registration/login.html
new file mode 100644
index 0000000..f699914
--- /dev/null
+++ b/base/templates/registration/login.html
@@ -0,0 +1,7 @@
+{% extends "base.html" %}
+{% load form %}
+
+{% block content %}
+Connexion
+{% form form submit="Se connecter" %}
+{% endblock content %}
diff --git a/base/templatetags/__init__.py b/base/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/base/templatetags/form.py b/base/templatetags/form.py
new file mode 100644
index 0000000..9ab40ce
--- /dev/null
+++ b/base/templatetags/form.py
@@ -0,0 +1,11 @@
+from django import template
+
+register = template.Library()
+
+
+@register.inclusion_tag("base/form.html")
+def form(form, **kwargs):
+ return kwargs | {
+ "form": form,
+ "errors": form.errors,
+ }
diff --git a/base/urls.py b/base/urls.py
new file mode 100644
index 0000000..8468c3e
--- /dev/null
+++ b/base/urls.py
@@ -0,0 +1,8 @@
+from django.urls import include, path
+
+from . import views
+
+urlpatterns = [
+ path("", views.HomePageView.as_view(), name="index"),
+ path("accounts/", include("django.contrib.auth.urls")),
+]
diff --git a/base/views.py b/base/views.py
new file mode 100644
index 0000000..d1762b8
--- /dev/null
+++ b/base/views.py
@@ -0,0 +1,5 @@
+from django.views.generic.base import TemplateView
+
+
+class HomePageView(TemplateView):
+ template_name = "index.html"
diff --git a/musik/settings.py b/musik/settings.py
index 1248f4e..9c3c277 100644
--- a/musik/settings.py
+++ b/musik/settings.py
@@ -31,6 +31,7 @@ ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
+ "base",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
@@ -102,9 +103,9 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/
-LANGUAGE_CODE = "en-us"
+LANGUAGE_CODE = "fr-fr"
-TIME_ZONE = "UTC"
+TIME_ZONE = "Europe/Paris"
USE_I18N = True
@@ -120,3 +121,6 @@ STATIC_URL = "static/"
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
+
+LOGOUT_REDIRECT_URL = "/"
+LOGIN_REDIRECT_URL = "/"
diff --git a/musik/urls.py b/musik/urls.py
index 1e00b4b..b10d442 100644
--- a/musik/urls.py
+++ b/musik/urls.py
@@ -16,8 +16,9 @@ Including another URLconf
"""
from django.contrib import admin
-from django.urls import path
+from django.urls import include, path
urlpatterns = [
+ path("", include("base.urls")),
path("admin/", admin.site.urls),
]
diff --git a/pyproject.toml b/pyproject.toml
index 026e134..bb7c6ae 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,5 +10,6 @@ dependencies = [
[dependency-groups]
dev = [
+ "djlint>=1.36.4",
"pre-commit>=4.2.0",
]
diff --git a/uv.lock b/uv.lock
index f4a2ee2..45dc5a5 100644
--- a/uv.lock
+++ b/uv.lock
@@ -20,6 +20,41 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" },
]
+[[package]]
+name = "click"
+version = "8.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "cssbeautifier"
+version = "1.15.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "editorconfig" },
+ { name = "jsbeautifier" },
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f7/01/fdf41c1e5f93d359681976ba10410a04b299d248e28ecce1d4e88588dde4/cssbeautifier-1.15.4.tar.gz", hash = "sha256:9bb08dc3f64c101a01677f128acf01905914cf406baf87434dcde05b74c0acf5", size = 25376, upload-time = "2025-02-27T17:53:51.341Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/63/51/ef6c5628e46092f0a54c7cee69acc827adc6b6aab57b55d344fefbdf28f1/cssbeautifier-1.15.4-py3-none-any.whl", hash = "sha256:78c84d5e5378df7d08622bbd0477a1abdbd209680e95480bf22f12d5701efc98", size = 123667, upload-time = "2025-02-27T17:53:43.594Z" },
+]
+
[[package]]
name = "distlib"
version = "0.3.9"
@@ -43,6 +78,43 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1b/11/7aff961db37e1ea501a2bb663d27a8ce97f3683b9e5b83d3bfead8b86fa4/django-5.2.3-py3-none-any.whl", hash = "sha256:c517a6334e0fd940066aa9467b29401b93c37cec2e61365d663b80922542069d", size = 8301935, upload-time = "2025-06-10T10:13:58.993Z" },
]
+[[package]]
+name = "djlint"
+version = "1.36.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "colorama" },
+ { name = "cssbeautifier" },
+ { name = "jsbeautifier" },
+ { name = "json5" },
+ { name = "pathspec" },
+ { name = "pyyaml" },
+ { name = "regex" },
+ { name = "tqdm" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/74/89/ecf5be9f5c59a0c53bcaa29671742c5e269cc7d0e2622e3f65f41df251bf/djlint-1.36.4.tar.gz", hash = "sha256:17254f218b46fe5a714b224c85074c099bcb74e3b2e1f15c2ddc2cf415a408a1", size = 47849, upload-time = "2024-12-24T13:06:36.36Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/53/f5/9ae02b875604755d4d00cebf96b218b0faa3198edc630f56a139581aed87/djlint-1.36.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff9faffd7d43ac20467493fa71d5355b5b330a00ade1c4d1e859022f4195223b", size = 354886, upload-time = "2024-12-24T13:06:11.571Z" },
+ { url = "https://files.pythonhosted.org/packages/97/51/284443ff2f2a278f61d4ae6ae55eaf820ad9f0fd386d781cdfe91f4de495/djlint-1.36.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:79489e262b5ac23a8dfb7ca37f1eea979674cfc2d2644f7061d95bea12c38f7e", size = 323237, upload-time = "2024-12-24T13:06:13.057Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/5e/791f4c5571f3f168ad26fa3757af8f7a05c623fde1134a9c4de814ee33b7/djlint-1.36.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e58c5fa8c6477144a0be0a87273706a059e6dd0d6efae01146ae8c29cdfca675", size = 411719, upload-time = "2024-12-24T13:06:15.672Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/11/894425add6f84deffcc6e373f2ce250f2f7b01aa58c7f230016ebe7a0085/djlint-1.36.4-cp312-cp312-win_amd64.whl", hash = "sha256:bb6903777bf3124f5efedcddf1f4716aef097a7ec4223fc0fa54b865829a6e08", size = 362076, upload-time = "2024-12-24T13:06:17.517Z" },
+ { url = "https://files.pythonhosted.org/packages/da/83/88b4c885812921739f5529a29085c3762705154d41caf7eb9a8886a3380c/djlint-1.36.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ead475013bcac46095b1bbc8cf97ed2f06e83422335734363f8a76b4ba7e47c2", size = 354384, upload-time = "2024-12-24T13:06:20.809Z" },
+ { url = "https://files.pythonhosted.org/packages/32/38/67695f7a150b3d9d62fadb65242213d96024151570c3cf5d966effa68b0e/djlint-1.36.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6c601dfa68ea253311deb4a29a7362b7a64933bdfcfb5a06618f3e70ad1fa835", size = 322971, upload-time = "2024-12-24T13:06:22.185Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/7a/cd851393291b12e7fe17cf5d4d8874b8ea133aebbe9235f5314aabc96a52/djlint-1.36.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bda5014f295002363381969864addeb2db13955f1b26e772657c3b273ed7809f", size = 410972, upload-time = "2024-12-24T13:06:24.077Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/31/56469120394b970d4f079a552fde21ed27702ca729595ab0ed459eb6d240/djlint-1.36.4-cp313-cp313-win_amd64.whl", hash = "sha256:16ce37e085afe5a30953b2bd87cbe34c37843d94c701fc68a2dda06c1e428ff4", size = 362053, upload-time = "2024-12-24T13:06:25.432Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/67/f7aeea9be6fb3bd984487af8d0d80225a0b1e5f6f7126e3332d349fb13fe/djlint-1.36.4-py3-none-any.whl", hash = "sha256:e9699b8ac3057a6ed04fb90835b89bee954ed1959c01541ce4f8f729c938afdd", size = 52290, upload-time = "2024-12-24T13:06:33.76Z" },
+]
+
+[[package]]
+name = "editorconfig"
+version = "0.17.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/88/3a/a61d9a1f319a186b05d14df17daea42fcddea63c213bcd61a929fb3a6796/editorconfig-0.17.1.tar.gz", hash = "sha256:23c08b00e8e08cc3adcddb825251c497478df1dada6aefeb01e626ad37303745", size = 14695, upload-time = "2025-06-09T08:21:37.097Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/96/fd/a40c621ff207f3ce8e484aa0fc8ba4eb6e3ecf52e15b42ba764b457a9550/editorconfig-0.17.1-py3-none-any.whl", hash = "sha256:1eda9c2c0db8c16dbd50111b710572a5e6de934e39772de1959d41f64fc17c82", size = 16360, upload-time = "2025-06-09T08:21:35.654Z" },
+]
+
[[package]]
name = "filelock"
version = "3.18.0"
@@ -61,6 +133,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145, upload-time = "2025-05-23T20:37:51.495Z" },
]
+[[package]]
+name = "jsbeautifier"
+version = "1.15.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "editorconfig" },
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ea/98/d6cadf4d5a1c03b2136837a435682418c29fdeb66be137128544cecc5b7a/jsbeautifier-1.15.4.tar.gz", hash = "sha256:5bb18d9efb9331d825735fbc5360ee8f1aac5e52780042803943aa7f854f7592", size = 75257, upload-time = "2025-02-27T17:53:53.252Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2d/14/1c65fccf8413d5f5c6e8425f84675169654395098000d8bddc4e9d3390e1/jsbeautifier-1.15.4-py3-none-any.whl", hash = "sha256:72f65de312a3f10900d7685557f84cb61a9733c50dcc27271a39f5b0051bf528", size = 94707, upload-time = "2025-02-27T17:53:46.152Z" },
+]
+
+[[package]]
+name = "json5"
+version = "0.12.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/12/be/c6c745ec4c4539b25a278b70e29793f10382947df0d9efba2fa09120895d/json5-0.12.0.tar.gz", hash = "sha256:0b4b6ff56801a1c7dc817b0241bca4ce474a0e6a163bfef3fc594d3fd263ff3a", size = 51907, upload-time = "2025-04-03T16:33:13.201Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/9f/3500910d5a98549e3098807493851eeef2b89cdd3032227558a104dfe926/json5-0.12.0-py3-none-any.whl", hash = "sha256:6d37aa6c08b0609f16e1ec5ff94697e2cbbfbad5ac112afa05794da9ab7810db", size = 36079, upload-time = "2025-04-03T16:33:11.927Z" },
+]
+
[[package]]
name = "musik"
version = "0.1.0"
@@ -71,6 +165,7 @@ dependencies = [
[package.dev-dependencies]
dev = [
+ { name = "djlint" },
{ name = "pre-commit" },
]
@@ -78,7 +173,10 @@ dev = [
requires-dist = [{ name = "django", specifier = ">=5.2.3" }]
[package.metadata.requires-dev]
-dev = [{ name = "pre-commit", specifier = ">=4.2.0" }]
+dev = [
+ { name = "djlint", specifier = ">=1.36.4" },
+ { name = "pre-commit", specifier = ">=4.2.0" },
+]
[[package]]
name = "nodeenv"
@@ -89,6 +187,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
]
+[[package]]
+name = "pathspec"
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
+]
+
[[package]]
name = "platformdirs"
version = "4.3.8"
@@ -140,6 +247,53 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
]
+[[package]]
+name = "regex"
+version = "2024.11.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494, upload-time = "2024-11-06T20:12:31.635Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781, upload-time = "2024-11-06T20:10:07.07Z" },
+ { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455, upload-time = "2024-11-06T20:10:09.117Z" },
+ { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759, upload-time = "2024-11-06T20:10:11.155Z" },
+ { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976, upload-time = "2024-11-06T20:10:13.24Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077, upload-time = "2024-11-06T20:10:15.37Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160, upload-time = "2024-11-06T20:10:19.027Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896, upload-time = "2024-11-06T20:10:21.85Z" },
+ { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997, upload-time = "2024-11-06T20:10:24.329Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725, upload-time = "2024-11-06T20:10:28.067Z" },
+ { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481, upload-time = "2024-11-06T20:10:31.612Z" },
+ { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896, upload-time = "2024-11-06T20:10:34.054Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138, upload-time = "2024-11-06T20:10:36.142Z" },
+ { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692, upload-time = "2024-11-06T20:10:38.394Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135, upload-time = "2024-11-06T20:10:40.367Z" },
+ { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567, upload-time = "2024-11-06T20:10:43.467Z" },
+ { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525, upload-time = "2024-11-06T20:10:45.19Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324, upload-time = "2024-11-06T20:10:47.177Z" },
+ { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617, upload-time = "2024-11-06T20:10:49.312Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023, upload-time = "2024-11-06T20:10:51.102Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072, upload-time = "2024-11-06T20:10:52.926Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130, upload-time = "2024-11-06T20:10:54.828Z" },
+ { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857, upload-time = "2024-11-06T20:10:56.634Z" },
+ { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006, upload-time = "2024-11-06T20:10:59.369Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650, upload-time = "2024-11-06T20:11:02.042Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545, upload-time = "2024-11-06T20:11:03.933Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045, upload-time = "2024-11-06T20:11:06.497Z" },
+ { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182, upload-time = "2024-11-06T20:11:09.06Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733, upload-time = "2024-11-06T20:11:11.256Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122, upload-time = "2024-11-06T20:11:13.161Z" },
+ { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545, upload-time = "2024-11-06T20:11:15Z" },
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+]
+
[[package]]
name = "sqlparse"
version = "0.5.3"
@@ -149,6 +303,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415, upload-time = "2024-12-10T12:05:27.824Z" },
]
+[[package]]
+name = "tqdm"
+version = "4.67.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
+]
+
[[package]]
name = "tzdata"
version = "2025.2"
From ba746c9cae4dabd4d616692ea4011a9be19e7353 Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Fri, 13 Jun 2025 17:10:56 +0200
Subject: [PATCH 03/44] Add game app with group and music video models, views,
and templates
---
.gitignore | 1 -
base/static/css/main.css | 8 ++
base/templates/base.html | 80 +++++++++++--------
base/templates/base/form.html | 15 ++--
base/templates/index.html | 6 +-
base/templates/registration/login.html | 5 +-
game/__init__.py | 0
game/apps.py | 6 ++
game/migrations/0001_initial.py | 72 +++++++++++++++++
..._group_name_alter_group_unique_together.py | 23 ++++++
game/migrations/__init__.py | 0
game/models.py | 24 ++++++
game/templates/game/group_confirm_delete.html | 24 ++++++
game/templates/game/group_detail.html | 16 ++++
game/templates/game/group_form.html | 15 ++++
game/templates/game/home.html | 19 +++++
game/urls.py | 14 ++++
game/views.py | 33 ++++++++
musik/settings.py | 1 +
musik/urls.py | 1 +
20 files changed, 315 insertions(+), 48 deletions(-)
create mode 100644 game/__init__.py
create mode 100644 game/apps.py
create mode 100644 game/migrations/0001_initial.py
create mode 100644 game/migrations/0002_alter_group_name_alter_group_unique_together.py
create mode 100644 game/migrations/__init__.py
create mode 100644 game/models.py
create mode 100644 game/templates/game/group_confirm_delete.html
create mode 100644 game/templates/game/group_detail.html
create mode 100644 game/templates/game/group_form.html
create mode 100644 game/templates/game/home.html
create mode 100644 game/urls.py
create mode 100644 game/views.py
diff --git a/.gitignore b/.gitignore
index c6f515c..1af379d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -166,4 +166,3 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
-
diff --git a/base/static/css/main.css b/base/static/css/main.css
index 0b47044..e8527d1 100644
--- a/base/static/css/main.css
+++ b/base/static/css/main.css
@@ -7,3 +7,11 @@ header .container {
align-items: center;
justify-content: space-between;
}
+
+form a[role="button"] {
+ width: 100%;
+}
+
+i.ri-vip-crown-fill {
+ color: var(--pico-color-amber-200);
+}
diff --git a/base/templates/base.html b/base/templates/base.html
index 5d38174..49f52bf 100644
--- a/base/templates/base.html
+++ b/base/templates/base.html
@@ -1,36 +1,52 @@
{% load static %}
-
-
-
- {% block title %}Musik{% endblock %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% block content %}
- {% endblock content %}
-
-
+
+
+
+
+ {% block title %}Musik{% endblock %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% block content %}
+ {% endblock content %}
+
+
diff --git a/base/templates/base/form.html b/base/templates/base/form.html
index 57d5486..df40bc7 100644
--- a/base/templates/base/form.html
+++ b/base/templates/base/form.html
@@ -1,12 +1,9 @@
{% if form.non_field_errors %}
-
+
{% endif %}
-
-
Membres
+
+ Modifier les membres
+
-
{{ group.owner }}
diff --git a/game/templates/game/group_form.html b/game/templates/game/group_form.html
index 67f1f05..72ae8d9 100644
--- a/game/templates/game/group_form.html
+++ b/game/templates/game/group_form.html
@@ -12,4 +12,4 @@
Créer un groupe
{% endif %}
{% form form %}
-{% endblock content %}
+ {% endblock content %}
diff --git a/game/urls.py b/game/urls.py
index 28bc1d6..5b176f5 100644
--- a/game/urls.py
+++ b/game/urls.py
@@ -10,5 +10,10 @@ urlpatterns = [
path(
"group//delete/", views.GroupDeleteView.as_view(), name="group_delete"
),
+ path(
+ "group//edit_members/",
+ views.GroupAddMembersView.as_view(),
+ name="group_edit_members",
+ ),
path("group//", views.GroupDetailView.as_view(), name="group_detail"),
]
diff --git a/game/views.py b/game/views.py
index 05fdfe7..8a36bbd 100644
--- a/game/views.py
+++ b/game/views.py
@@ -2,7 +2,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView, DeleteView, UpdateView
-from . import models
+from . import forms, models
class OwnerFilterMixin(LoginRequiredMixin):
@@ -31,3 +31,8 @@ class GroupDeleteView(OwnerFilterMixin, GroupMixin, DeleteView):
class GroupDetailView(OwnerFilterMixin, GroupMixin, DetailView):
pass
+
+
+class GroupAddMembersView(OwnerFilterMixin, GroupMixin, UpdateView):
+ fields = None
+ form_class = forms.GroupAddMembersForm
From 4e28311b1cc2a07b9f52507b048593854399b341 Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Fri, 13 Jun 2025 18:55:50 +0200
Subject: [PATCH 05/44] Refactor music video model and views: rename user to
owner, add title field, and implement music management in group detail view
---
base/static/css/main.css | 4 +
...3_rename_user_musicvideo_owner_and_more.py | 23 +++++
game/migrations/0004_musicvideo_title.py | 17 ++++
game/models.py | 6 +-
game/templates/game/group_detail.html | 49 ++++++++++-
game/urls.py | 10 +++
game/utils.py | 35 ++++++++
game/views.py | 49 ++++++++++-
musik/settings.py | 3 +
pyproject.toml | 1 +
uv.lock | 83 ++++++++++++++++++-
11 files changed, 272 insertions(+), 8 deletions(-)
create mode 100644 game/migrations/0003_rename_user_musicvideo_owner_and_more.py
create mode 100644 game/migrations/0004_musicvideo_title.py
create mode 100644 game/utils.py
diff --git a/base/static/css/main.css b/base/static/css/main.css
index e8527d1..e7247c2 100644
--- a/base/static/css/main.css
+++ b/base/static/css/main.css
@@ -15,3 +15,7 @@ form a[role="button"] {
i.ri-vip-crown-fill {
color: var(--pico-color-amber-200);
}
+
+.template {
+ display: none;
+}
diff --git a/game/migrations/0003_rename_user_musicvideo_owner_and_more.py b/game/migrations/0003_rename_user_musicvideo_owner_and_more.py
new file mode 100644
index 0000000..91c3a04
--- /dev/null
+++ b/game/migrations/0003_rename_user_musicvideo_owner_and_more.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.2.3 on 2025-06-13 16:21
+
+from django.conf import settings
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("game", "0002_alter_group_name_alter_group_unique_together"),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name="musicvideo",
+ old_name="user",
+ new_name="owner",
+ ),
+ migrations.AlterUniqueTogether(
+ name="musicvideo",
+ unique_together={("yt_id", "owner", "group")},
+ ),
+ ]
diff --git a/game/migrations/0004_musicvideo_title.py b/game/migrations/0004_musicvideo_title.py
new file mode 100644
index 0000000..4a8ea7e
--- /dev/null
+++ b/game/migrations/0004_musicvideo_title.py
@@ -0,0 +1,17 @@
+# Generated by Django 5.2.3 on 2025-06-13 16:44
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("game", "0003_rename_user_musicvideo_owner_and_more"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="musicvideo",
+ name="title",
+ field=models.CharField(blank=True),
+ ),
+ ]
diff --git a/game/models.py b/game/models.py
index ea80626..1bb21d4 100644
--- a/game/models.py
+++ b/game/models.py
@@ -19,6 +19,10 @@ class Group(models.Model):
class MusicVideo(models.Model):
yt_id = models.CharField(max_length=16)
- user = models.ForeignKey(User, on_delete=models.CASCADE)
+ title = models.CharField(blank=True)
+ owner = models.ForeignKey(User, on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
blacklisted = models.BooleanField(default=False)
+
+ class Meta:
+ unique_together = ["yt_id", "owner", "group"]
diff --git a/game/templates/game/group_detail.html b/game/templates/game/group_detail.html
index d710c37..08cb55c 100644
--- a/game/templates/game/group_detail.html
+++ b/game/templates/game/group_detail.html
@@ -1,4 +1,5 @@
{% extends "base.html" %}
+{% load form %}
{% block content %}
{{ group.name }}
@@ -12,8 +13,50 @@
-
- {{ group.owner }}
+ {{ group.owner }} ({{ owner_count }})
- {% for member in group.members.all %}- {{ member }}
{% endfor %}
+ {% for member in members.all %}- {{ member }} ({{ member.count }})
{% endfor %}
-{% endblock content %}
+ Mes musiques ({{ musics.count }})
+
+
+ {% endblock content %}
diff --git a/game/urls.py b/game/urls.py
index 5b176f5..383c6c3 100644
--- a/game/urls.py
+++ b/game/urls.py
@@ -16,4 +16,14 @@ urlpatterns = [
name="group_edit_members",
),
path("group//", views.GroupDetailView.as_view(), name="group_detail"),
+ path(
+ "group//add_music/",
+ views.GroupAddMusicView.as_view(),
+ name="group_add_music",
+ ),
+ path(
+ "group/remove_music//",
+ views.GroupRemoveMusicView.as_view(),
+ name="group_remove_music",
+ ),
]
diff --git a/game/utils.py b/game/utils.py
new file mode 100644
index 0000000..e937983
--- /dev/null
+++ b/game/utils.py
@@ -0,0 +1,35 @@
+from urllib.parse import parse_qs, urlparse
+
+import requests
+from django.conf import settings
+
+
+def parse_musik(raw):
+ if "youtube.com" in raw:
+ return parse_qs(urlparse(raw).query).get("v", [None])[0]
+ elif "youtu.be" in raw:
+ return urlparse(raw).path[1:]
+
+ return raw
+
+
+def get_yt_title(yt_id):
+ if not yt_id:
+ return None
+ req = requests.get(
+ "https://www.googleapis.com/youtube/v3/videos",
+ params={
+ "part": "snippet",
+ "id": yt_id,
+ "key": settings.YOUTUBE_API_KEY,
+ },
+ )
+
+ if req.status_code != 200:
+ raise Exception(f"Error fetching YouTube video: {req.status_code} {req.text}")
+
+ data = req.json()
+ if not data.get("items"):
+ return None
+
+ return data["items"][0]["snippet"]["title"]
diff --git a/game/views.py b/game/views.py
index 8a36bbd..28d0929 100644
--- a/game/views.py
+++ b/game/views.py
@@ -1,8 +1,12 @@
from django.contrib.auth.mixins import LoginRequiredMixin
-from django.views.generic.detail import DetailView
+from django.db.models import Count, Q
+from django.http import JsonResponse
+from django.shortcuts import redirect
+from django.views import View
+from django.views.generic.detail import DetailView, SingleObjectMixin
from django.views.generic.edit import CreateView, DeleteView, UpdateView
-from . import forms, models
+from . import forms, models, utils
class OwnerFilterMixin(LoginRequiredMixin):
@@ -30,9 +34,48 @@ class GroupDeleteView(OwnerFilterMixin, GroupMixin, DeleteView):
class GroupDetailView(OwnerFilterMixin, GroupMixin, DetailView):
- pass
+ def get_context_data(self, **kwargs):
+ data = super().get_context_data(**kwargs)
+
+ data["musics"] = data["group"].musicvideo_set.filter(owner=self.request.user)
+ data["owner_count"] = (
+ data["group"].musicvideo_set.filter(owner=data["group"].owner).count()
+ )
+ data["members"] = data["group"].members.annotate(
+ count=Count("musicvideo", filter=Q(group=data["group"]))
+ )
+
+ return data
class GroupAddMembersView(OwnerFilterMixin, GroupMixin, UpdateView):
fields = None
form_class = forms.GroupAddMembersForm
+
+
+class GroupAddMusicView(OwnerFilterMixin, SingleObjectMixin, View):
+ model = models.Group
+
+ def post(self, request, pk):
+ group = self.get_object()
+ yt_id = request.POST.get("yt_id")
+ if not yt_id:
+ return JsonResponse({"error": "You must provide a YouTube ID."}, status=400)
+ yt_id = utils.parse_musik(yt_id)
+
+ title = utils.get_yt_title(yt_id)
+ if not title:
+ return JsonResponse({"error": "Invalid YouTube ID."}, status=400)
+ group.musicvideo_set.create(yt_id=yt_id, title=title, owner=request.user)
+ group.save()
+ return redirect(group)
+
+
+class GroupRemoveMusicView(OwnerFilterMixin, SingleObjectMixin, View):
+ model = models.MusicVideo
+
+ def get(self, request, pk):
+ music = self.get_object()
+ group = music.group
+ music.delete()
+ return redirect(group)
diff --git a/musik/settings.py b/musik/settings.py
index 10439ce..d775f24 100644
--- a/musik/settings.py
+++ b/musik/settings.py
@@ -10,6 +10,7 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.2/ref/settings/
"""
+import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
@@ -125,3 +126,5 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
LOGOUT_REDIRECT_URL = "/"
LOGIN_REDIRECT_URL = "/"
+
+YOUTUBE_API_KEY = os.getenv("YOUTUBE_API_KEY", "")
diff --git a/pyproject.toml b/pyproject.toml
index bb7c6ae..9ef4d67 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,6 +6,7 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"django>=5.2.3",
+ "requests>=2.32.4",
]
[dependency-groups]
diff --git a/uv.lock b/uv.lock
index 45dc5a5..4922fdc 100644
--- a/uv.lock
+++ b/uv.lock
@@ -11,6 +11,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828, upload-time = "2024-03-22T14:39:34.521Z" },
]
+[[package]]
+name = "certifi"
+version = "2025.4.26"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" },
+]
+
[[package]]
name = "cfgv"
version = "3.4.0"
@@ -20,6 +29,41 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" },
]
+[[package]]
+name = "charset-normalizer"
+version = "3.4.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" },
+ { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" },
+ { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" },
+ { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" },
+ { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" },
+ { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" },
+ { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
+ { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
+ { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
+ { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
+ { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
+ { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
+ { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
+ { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
+]
+
[[package]]
name = "click"
version = "8.2.1"
@@ -133,6 +177,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145, upload-time = "2025-05-23T20:37:51.495Z" },
]
+[[package]]
+name = "idna"
+version = "3.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
+]
+
[[package]]
name = "jsbeautifier"
version = "1.15.4"
@@ -161,6 +214,7 @@ version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "django" },
+ { name = "requests" },
]
[package.dev-dependencies]
@@ -170,7 +224,10 @@ dev = [
]
[package.metadata]
-requires-dist = [{ name = "django", specifier = ">=5.2.3" }]
+requires-dist = [
+ { name = "django", specifier = ">=5.2.3" },
+ { name = "requests", specifier = ">=2.32.4" },
+]
[package.metadata.requires-dev]
dev = [
@@ -285,6 +342,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545, upload-time = "2024-11-06T20:11:15Z" },
]
+[[package]]
+name = "requests"
+version = "2.32.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
+]
+
[[package]]
name = "six"
version = "1.17.0"
@@ -324,6 +396,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
]
+[[package]]
+name = "urllib3"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" },
+]
+
[[package]]
name = "virtualenv"
version = "20.31.2"
From 19e6eb32c875c5bd9d2e6f7048dc7e8c6ad95cf2 Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Fri, 13 Jun 2025 19:11:23 +0200
Subject: [PATCH 06/44] Refactor group detail and music views to use
MemberFilterMixin for member access control
---
game/templates/game/group_detail.html | 16 ++++++++++------
game/views.py | 13 +++++++++++--
2 files changed, 21 insertions(+), 8 deletions(-)
diff --git a/game/templates/game/group_detail.html b/game/templates/game/group_detail.html
index 08cb55c..9245ca6 100644
--- a/game/templates/game/group_detail.html
+++ b/game/templates/game/group_detail.html
@@ -4,13 +4,17 @@
{{ group.name }}
-
- Modifier le groupe
-
+ {% if group.owner == user %}
+
+ Modifier le groupe
+
+ {% endif %}
Membres
-
- Modifier les membres
-
+ {% if group.owner == user %}
+
+ Modifier les membres
+
+ {% endif %}
-
{{ group.owner }} ({{ owner_count }})
diff --git a/game/views.py b/game/views.py
index 28d0929..1574493 100644
--- a/game/views.py
+++ b/game/views.py
@@ -14,6 +14,15 @@ class OwnerFilterMixin(LoginRequiredMixin):
return super().get_queryset().filter(owner=self.request.user)
+class MemberFilterMixin(LoginRequiredMixin):
+ def get_queryset(self):
+ return (
+ super()
+ .get_queryset()
+ .filter(Q(members=self.request.user) | Q(owner=self.request.user))
+ )
+
+
class GroupMixin:
model = models.Group
fields = ["name"]
@@ -33,7 +42,7 @@ class GroupDeleteView(OwnerFilterMixin, GroupMixin, DeleteView):
success_url = "/"
-class GroupDetailView(OwnerFilterMixin, GroupMixin, DetailView):
+class GroupDetailView(MemberFilterMixin, GroupMixin, DetailView):
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
@@ -53,7 +62,7 @@ class GroupAddMembersView(OwnerFilterMixin, GroupMixin, UpdateView):
form_class = forms.GroupAddMembersForm
-class GroupAddMusicView(OwnerFilterMixin, SingleObjectMixin, View):
+class GroupAddMusicView(MemberFilterMixin, SingleObjectMixin, View):
model = models.Group
def post(self, request, pk):
From f7baa911324f60a82c55562478a32f76676d4ab9 Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Fri, 13 Jun 2025 21:06:23 +0200
Subject: [PATCH 07/44] Add game management features: create MusikGame model,
implement game creation and detail views, and update group detail template
---
game/forms.py | 13 ++++
.../0005_musicvideo_date_added_musikgame.py | 47 ++++++++++++++
...006_musikgame_n_alter_musikgame_players.py | 28 +++++++++
...e_musikgame_music_videos_musicgameorder.py | 61 +++++++++++++++++++
game/models.py | 22 +++++++
game/templates/game/group_detail.html | 23 ++++++-
game/templates/game/musikgame_detail.html | 22 +++++++
game/templates/game/musikgame_form.html | 8 +++
game/urls.py | 4 ++
game/views.py | 49 +++++++++++++++
10 files changed, 275 insertions(+), 2 deletions(-)
create mode 100644 game/migrations/0005_musicvideo_date_added_musikgame.py
create mode 100644 game/migrations/0006_musikgame_n_alter_musikgame_players.py
create mode 100644 game/migrations/0007_remove_musikgame_music_videos_musicgameorder.py
create mode 100644 game/templates/game/musikgame_detail.html
create mode 100644 game/templates/game/musikgame_form.html
diff --git a/game/forms.py b/game/forms.py
index f305315..24d2b38 100644
--- a/game/forms.py
+++ b/game/forms.py
@@ -13,3 +13,16 @@ class GroupAddMembersForm(forms.ModelForm):
class Meta:
model = models.Group
fields = ["members"]
+
+
+class MusikGameForm(forms.ModelForm):
+ class Meta:
+ model = models.MusikGame
+ fields = ["players", "n"]
+
+ def __init__(self, *args, **kwargs):
+ group = models.Group.objects.get(pk=kwargs.pop("group", None))
+ super().__init__(*args, **kwargs)
+ self.fields["players"].queryset = (
+ group.members.all() | models.User.objects.filter(id=group.owner.id)
+ )
diff --git a/game/migrations/0005_musicvideo_date_added_musikgame.py b/game/migrations/0005_musicvideo_date_added_musikgame.py
new file mode 100644
index 0000000..d6bf7b6
--- /dev/null
+++ b/game/migrations/0005_musicvideo_date_added_musikgame.py
@@ -0,0 +1,47 @@
+# Generated by Django 5.2.3 on 2025-06-13 17:23
+
+import django.db.models.deletion
+import django.utils.timezone
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("game", "0004_musicvideo_title"),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="musicvideo",
+ name="date_added",
+ field=models.DateTimeField(
+ auto_now_add=True, default=django.utils.timezone.now
+ ),
+ preserve_default=False,
+ ),
+ migrations.CreateModel(
+ name="MusikGame",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("date", models.DateTimeField(auto_now_add=True)),
+ (
+ "group",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE, to="game.group"
+ ),
+ ),
+ ("music_videos", models.ManyToManyField(to="game.musicvideo")),
+ ("players", models.ManyToManyField(to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ ]
diff --git a/game/migrations/0006_musikgame_n_alter_musikgame_players.py b/game/migrations/0006_musikgame_n_alter_musikgame_players.py
new file mode 100644
index 0000000..b40d843
--- /dev/null
+++ b/game/migrations/0006_musikgame_n_alter_musikgame_players.py
@@ -0,0 +1,28 @@
+# Generated by Django 5.2.3 on 2025-06-13 18:41
+
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("game", "0005_musicvideo_date_added_musikgame"),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="musikgame",
+ name="n",
+ field=models.PositiveIntegerField(
+ default=2, verbose_name="Nombre de musiques"
+ ),
+ ),
+ migrations.AlterField(
+ model_name="musikgame",
+ name="players",
+ field=models.ManyToManyField(
+ to=settings.AUTH_USER_MODEL, verbose_name="Joueurs"
+ ),
+ ),
+ ]
diff --git a/game/migrations/0007_remove_musikgame_music_videos_musicgameorder.py b/game/migrations/0007_remove_musikgame_music_videos_musicgameorder.py
new file mode 100644
index 0000000..6c88755
--- /dev/null
+++ b/game/migrations/0007_remove_musikgame_music_videos_musicgameorder.py
@@ -0,0 +1,61 @@
+# Generated by Django 5.2.3 on 2025-06-13 18:57
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("game", "0006_musikgame_n_alter_musikgame_players"),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name="musikgame",
+ name="music_videos",
+ ),
+ migrations.CreateModel(
+ name="MusicGameOrder",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("order", models.PositiveIntegerField()),
+ (
+ "game",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE, to="game.musikgame"
+ ),
+ ),
+ (
+ "music_video",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="game.musicvideo",
+ ),
+ ),
+ (
+ "player",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ ],
+ options={
+ "ordering": ["order"],
+ "unique_together": {
+ ("game", "order"),
+ ("game", "player", "music_video"),
+ },
+ },
+ ),
+ ]
diff --git a/game/models.py b/game/models.py
index 1bb21d4..5b3c947 100644
--- a/game/models.py
+++ b/game/models.py
@@ -20,9 +20,31 @@ class Group(models.Model):
class MusicVideo(models.Model):
yt_id = models.CharField(max_length=16)
title = models.CharField(blank=True)
+ date_added = models.DateTimeField(auto_now_add=True)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
blacklisted = models.BooleanField(default=False)
class Meta:
unique_together = ["yt_id", "owner", "group"]
+
+
+class MusikGame(models.Model):
+ group = models.ForeignKey(Group, on_delete=models.CASCADE)
+ date = models.DateTimeField(auto_now_add=True)
+ n = models.PositiveIntegerField(default=2, verbose_name="Nombre de musiques")
+ players = models.ManyToManyField(User, verbose_name="Joueurs")
+
+ def get_absolute_url(self):
+ return reverse("game_detail", kwargs={"pk": self.pk})
+
+
+class MusicGameOrder(models.Model):
+ game = models.ForeignKey(MusikGame, on_delete=models.CASCADE)
+ player = models.ForeignKey(User, on_delete=models.CASCADE)
+ music_video = models.ForeignKey(MusicVideo, on_delete=models.CASCADE)
+ order = models.PositiveIntegerField()
+
+ class Meta:
+ unique_together = [["game", "player", "music_video"], ["game", "order"]]
+ ordering = ["order"]
diff --git a/game/templates/game/group_detail.html b/game/templates/game/group_detail.html
index 9245ca6..1365394 100644
--- a/game/templates/game/group_detail.html
+++ b/game/templates/game/group_detail.html
@@ -8,8 +8,25 @@
Modifier le groupe
+
+ Lancer une partie
+
{% endif %}
- Membres
+ {% if group.musikgame_set %}
+
+ Parties
+
+
+ {% for game in group.musikgame_set.all %}
+ -
+ {{ game.date }}
+
+ {% endfor %}
+
+ {% endif %}
+
+ Membres
+
{% if group.owner == user %}
Modifier les membres
@@ -21,7 +38,9 @@
{% for member in members.all %}- {{ member }} ({{ member.count }})
{% endfor %}
- Mes musiques ({{ musics.count }})
+
+ Mes musiques ({{ musics.count }})
+
diff --git a/game/templates/game/musikgame_detail.html b/game/templates/game/musikgame_detail.html
new file mode 100644
index 0000000..8a2d195
--- /dev/null
+++ b/game/templates/game/musikgame_detail.html
@@ -0,0 +1,22 @@
+{% extends "base.html" %}
+{% block content %}
+
+ {{ musikgame.date }}
+
+
+ Joueurs
+
+
+ {% for member in musikgame.players.all %}- {{ member }}
{% endfor %}
+
+
+ Musiques
+
+
+ {% for music in musikgame.musicgameorder_set.all %}
+ -
+ {{ music.music_video.title }}
+
+ {% endfor %}
+
+{% endblock content %}
diff --git a/game/templates/game/musikgame_form.html b/game/templates/game/musikgame_form.html
new file mode 100644
index 0000000..3e6d4ed
--- /dev/null
+++ b/game/templates/game/musikgame_form.html
@@ -0,0 +1,8 @@
+{% extends "base.html" %}
+{% load form %}
+{% block content %}
+
+ {{ group.name }}
+
+ {% form form %}
+ {% endblock content %}
diff --git a/game/urls.py b/game/urls.py
index 383c6c3..6ad87f0 100644
--- a/game/urls.py
+++ b/game/urls.py
@@ -26,4 +26,8 @@ urlpatterns = [
views.GroupRemoveMusicView.as_view(),
name="group_remove_music",
),
+ path(
+ "group//start_game/", views.GameCreateView.as_view(), name="start_game"
+ ),
+ path("group/game//", views.GameDetailView.as_view(), name="game_detail"),
]
diff --git a/game/views.py b/game/views.py
index 1574493..f55e95b 100644
--- a/game/views.py
+++ b/game/views.py
@@ -1,3 +1,5 @@
+import random
+
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Count, Q
from django.http import JsonResponse
@@ -88,3 +90,50 @@ class GroupRemoveMusicView(OwnerFilterMixin, SingleObjectMixin, View):
group = music.group
music.delete()
return redirect(group)
+
+
+class GameCreateView(LoginRequiredMixin, CreateView):
+ model = models.MusikGame
+ form_class = forms.MusikGameForm
+
+ def get_form_kwargs(self):
+ kwargs = super().get_form_kwargs()
+ kwargs["group"] = self.kwargs["pk"]
+ return kwargs
+
+ def get_context_data(self, **kwargs):
+ data = super().get_context_data(**kwargs)
+ data["group"] = models.Group.objects.filter(owner=self.request.user).get(
+ pk=self.kwargs["pk"]
+ )
+ return data
+
+ def form_valid(self, form):
+ group = models.Group.objects.get(pk=self.kwargs["pk"])
+ form.instance.group = group
+ res = super().form_valid(form)
+ players = []
+ musics = []
+ for player in form.instance.players.all():
+ players += 2 * [player]
+ musics += random.sample(
+ list(
+ player.musicvideo_set.filter(group=group, blacklisted=False).all()
+ ),
+ form.instance.n,
+ )
+
+ pm_list = list(zip(players, musics))
+ random.shuffle(pm_list)
+ for (player, music), order in zip(pm_list, range(len(pm_list))):
+ models.MusicGameOrder.objects.create(
+ game=form.instance, player=player, music_video=music, order=order
+ )
+ return res
+
+
+class GameDetailView(LoginRequiredMixin, DetailView):
+ model = models.MusikGame
+
+ def get_queryset(self):
+ return super().get_queryset().filter(group__owner=self.request.user)
From 4b2f695afb50745fdc8172bd84c5b82ce29e4bde Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Fri, 13 Jun 2025 22:23:18 +0200
Subject: [PATCH 08/44] Add YoutubeCredentials model and implement YouTube
OAuth login functionality
---
game/migrations/0008_youtubecredentials.py | 37 ++++
game/models.py | 5 +
game/templates/game/home.html | 3 +
game/urls.py | 6 +
game/views.py | 41 ++++
musik/settings.py | 2 +
musik/urls.py | 2 +-
pyproject.toml | 4 +
uv.lock | 216 +++++++++++++++++++++
9 files changed, 315 insertions(+), 1 deletion(-)
create mode 100644 game/migrations/0008_youtubecredentials.py
diff --git a/game/migrations/0008_youtubecredentials.py b/game/migrations/0008_youtubecredentials.py
new file mode 100644
index 0000000..5967308
--- /dev/null
+++ b/game/migrations/0008_youtubecredentials.py
@@ -0,0 +1,37 @@
+# Generated by Django 5.2.3 on 2025-06-13 20:16
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("game", "0007_remove_musikgame_music_videos_musicgameorder"),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="YoutubeCredentials",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("credentials", models.JSONField()),
+ (
+ "user",
+ models.OneToOneField(
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ ],
+ ),
+ ]
diff --git a/game/models.py b/game/models.py
index 5b3c947..25e905c 100644
--- a/game/models.py
+++ b/game/models.py
@@ -3,6 +3,11 @@ from django.db import models
from django.urls import reverse
+class YoutubeCredentials(models.Model):
+ user = models.OneToOneField(User, on_delete=models.CASCADE)
+ credentials = models.JSONField()
+
+
class Group(models.Model):
name = models.CharField(verbose_name="Nom du groupe")
owner = models.ForeignKey(
diff --git a/game/templates/game/home.html b/game/templates/game/home.html
index d2c045b..6c78861 100644
--- a/game/templates/game/home.html
+++ b/game/templates/game/home.html
@@ -2,6 +2,9 @@
Mes groupes
Créer un groupe
+ {% if not user.youtubecredentials %}
+ Me connecter au compte Youtube
+ {% endif %}
{% if user.owned_group_set.exists or user.group_set.exists %}
diff --git a/game/urls.py b/game/urls.py
index 6ad87f0..a995581 100644
--- a/game/urls.py
+++ b/game/urls.py
@@ -30,4 +30,10 @@ urlpatterns = [
"group//start_game/", views.GameCreateView.as_view(), name="start_game"
),
path("group/game//", views.GameDetailView.as_view(), name="game_detail"),
+ path("youtube_login/", views.YoutubeLoginView.as_view(), name="youtube_login"),
+ path(
+ "youtube_callback/",
+ views.YoutubeCallbackView.as_view(),
+ name="youtube_callback",
+ ),
]
diff --git a/game/views.py b/game/views.py
index f55e95b..38ec076 100644
--- a/game/views.py
+++ b/game/views.py
@@ -1,5 +1,7 @@
import random
+import google_auth_oauthlib
+from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Count, Q
from django.http import JsonResponse
@@ -137,3 +139,42 @@ class GameDetailView(LoginRequiredMixin, DetailView):
def get_queryset(self):
return super().get_queryset().filter(group__owner=self.request.user)
+
+
+class YoutubeLoginView(LoginRequiredMixin, View):
+ def get(self, request):
+ flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
+ settings.YOUTUBE_OAUTH_SECRETS,
+ ["https://www.googleapis.com/auth/youtube.force-ssl"],
+ )
+ flow.redirect_uri = "https://localhost/youtube_callback/"
+ auth_url, state = flow.authorization_url(
+ access_type="offline",
+ include_granted_scopes="true",
+ prompt="consent",
+ )
+ self.request.session["state"] = state
+ return redirect(auth_url)
+
+
+class YoutubeCallbackView(LoginRequiredMixin, View):
+ def get(self, request):
+ if request.GET.get("error"):
+ return redirect("/")
+ print(request.GET)
+
+ state = self.request.session.get("state")
+ flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
+ settings.YOUTUBE_OAUTH_SECRETS,
+ ["https://www.googleapis.com/auth/youtube.force-ssl"],
+ state=state,
+ )
+ flow.redirect_uri = "https://localhost/youtube_callback/"
+
+ flow.fetch_token(code=request.GET.get("code"))
+
+ credentials = models.YoutubeCredentials(
+ user=request.user, credentials=flow.credentials.to_json()
+ )
+ credentials.save()
+ return redirect("/")
diff --git a/musik/settings.py b/musik/settings.py
index d775f24..9b1a167 100644
--- a/musik/settings.py
+++ b/musik/settings.py
@@ -27,6 +27,7 @@ SECRET_KEY = "django-insecure-&z*xu$^w8btr(%1!y#+0a98)l_q*+*6z54611pi678mdpsar_=
DEBUG = True
ALLOWED_HOSTS = []
+CSRF_TRUSTED_ORIGINS = ["https://localhost"]
# Application definition
@@ -128,3 +129,4 @@ LOGOUT_REDIRECT_URL = "/"
LOGIN_REDIRECT_URL = "/"
YOUTUBE_API_KEY = os.getenv("YOUTUBE_API_KEY", "")
+YOUTUBE_OAUTH_SECRETS = os.getenv("YOUTUBE_OAUTH_SECRETS", "")
diff --git a/musik/urls.py b/musik/urls.py
index 946e05d..641f479 100644
--- a/musik/urls.py
+++ b/musik/urls.py
@@ -20,6 +20,6 @@ from django.urls import include, path
urlpatterns = [
path("", include("base.urls")),
- path("game/", include("game.urls")),
+ path("", include("game.urls")),
path("admin/", admin.site.urls),
]
diff --git a/pyproject.toml b/pyproject.toml
index 9ef4d67..f45bbbc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,6 +6,10 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"django>=5.2.3",
+ "google-api-python-client>=2.172.0",
+ "google-auth>=2.40.3",
+ "google-auth-httplib2>=0.2.0",
+ "google-auth-oauthlib>=1.2.2",
"requests>=2.32.4",
]
diff --git a/uv.lock b/uv.lock
index 4922fdc..43900ae 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,6 +1,10 @@
version = 1
revision = 2
requires-python = ">=3.12"
+resolution-markers = [
+ "python_full_version >= '3.13'",
+ "python_full_version < '3.13'",
+]
[[package]]
name = "asgiref"
@@ -11,6 +15,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828, upload-time = "2024-03-22T14:39:34.521Z" },
]
+[[package]]
+name = "cachetools"
+version = "5.5.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" },
+]
+
[[package]]
name = "certifi"
version = "2025.4.26"
@@ -168,6 +181,102 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" },
]
+[[package]]
+name = "google-api-core"
+version = "2.25.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "google-auth" },
+ { name = "googleapis-common-protos" },
+ { name = "proto-plus" },
+ { name = "protobuf" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/dc/21/e9d043e88222317afdbdb567165fdbc3b0aad90064c7e0c9eb0ad9955ad8/google_api_core-2.25.1.tar.gz", hash = "sha256:d2aaa0b13c78c61cb3f4282c464c046e45fbd75755683c9c525e6e8f7ed0a5e8", size = 165443, upload-time = "2025-06-12T20:52:20.439Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/14/4b/ead00905132820b623732b175d66354e9d3e69fcf2a5dcdab780664e7896/google_api_core-2.25.1-py3-none-any.whl", hash = "sha256:8a2a56c1fef82987a524371f99f3bd0143702fecc670c72e600c1cda6bf8dbb7", size = 160807, upload-time = "2025-06-12T20:52:19.334Z" },
+]
+
+[[package]]
+name = "google-api-python-client"
+version = "2.172.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "google-api-core" },
+ { name = "google-auth" },
+ { name = "google-auth-httplib2" },
+ { name = "httplib2" },
+ { name = "uritemplate" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/02/69/c0cec6be5878d4de161f64096edb3d4a2d1a838f036b8425ea8358d0dfb3/google_api_python_client-2.172.0.tar.gz", hash = "sha256:dcb3b7e067154b2aa41f1776cf86584a5739c0ac74e6ff46fc665790dca0e6a6", size = 13074841, upload-time = "2025-06-10T16:58:41.181Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/15/fc/8850ccf21c5df43faeaf8bba8c4149ee880b41b8dc7066e3259bcfd921ca/google_api_python_client-2.172.0-py3-none-any.whl", hash = "sha256:9f1b9a268d5dc1228207d246c673d3a09ee211b41a11521d38d9212aeaa43af7", size = 13595800, upload-time = "2025-06-10T16:58:38.143Z" },
+]
+
+[[package]]
+name = "google-auth"
+version = "2.40.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cachetools" },
+ { name = "pyasn1-modules" },
+ { name = "rsa" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029, upload-time = "2025-06-04T18:04:57.577Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137, upload-time = "2025-06-04T18:04:55.573Z" },
+]
+
+[[package]]
+name = "google-auth-httplib2"
+version = "0.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "google-auth" },
+ { name = "httplib2" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/56/be/217a598a818567b28e859ff087f347475c807a5649296fb5a817c58dacef/google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05", size = 10842, upload-time = "2023-12-12T17:40:30.722Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/be/8a/fe34d2f3f9470a27b01c9e76226965863f153d5fbe276f83608562e49c04/google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d", size = 9253, upload-time = "2023-12-12T17:40:13.055Z" },
+]
+
+[[package]]
+name = "google-auth-oauthlib"
+version = "1.2.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "google-auth" },
+ { name = "requests-oauthlib" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fb/87/e10bf24f7bcffc1421b84d6f9c3377c30ec305d082cd737ddaa6d8f77f7c/google_auth_oauthlib-1.2.2.tar.gz", hash = "sha256:11046fb8d3348b296302dd939ace8af0a724042e8029c1b872d87fabc9f41684", size = 20955, upload-time = "2025-04-22T16:40:29.172Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ac/84/40ee070be95771acd2f4418981edb834979424565c3eec3cd88b6aa09d24/google_auth_oauthlib-1.2.2-py3-none-any.whl", hash = "sha256:fd619506f4b3908b5df17b65f39ca8d66ea56986e5472eb5978fd8f3786f00a2", size = 19072, upload-time = "2025-04-22T16:40:28.174Z" },
+]
+
+[[package]]
+name = "googleapis-common-protos"
+version = "1.70.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "protobuf" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/39/24/33db22342cf4a2ea27c9955e6713140fedd51e8b141b5ce5260897020f1a/googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257", size = 145903, upload-time = "2025-04-14T10:17:02.924Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/86/f1/62a193f0227cf15a920390abe675f386dec35f7ae3ffe6da582d3ade42c7/googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8", size = 294530, upload-time = "2025-04-14T10:17:01.271Z" },
+]
+
+[[package]]
+name = "httplib2"
+version = "0.22.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyparsing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3d/ad/2371116b22d616c194aa25ec410c9c6c37f23599dcd590502b74db197584/httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81", size = 351116, upload-time = "2023-03-21T22:29:37.214Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a8/6c/d2fbdaaa5959339d53ba38e94c123e4e84b8fbc4b84beb0e70d7c1608486/httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc", size = 96854, upload-time = "2023-03-21T22:29:35.683Z" },
+]
+
[[package]]
name = "identify"
version = "2.6.12"
@@ -214,6 +323,10 @@ version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "django" },
+ { name = "google-api-python-client" },
+ { name = "google-auth" },
+ { name = "google-auth-httplib2" },
+ { name = "google-auth-oauthlib" },
{ name = "requests" },
]
@@ -226,6 +339,10 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "django", specifier = ">=5.2.3" },
+ { name = "google-api-python-client", specifier = ">=2.172.0" },
+ { name = "google-auth", specifier = ">=2.40.3" },
+ { name = "google-auth-httplib2", specifier = ">=0.2.0" },
+ { name = "google-auth-oauthlib", specifier = ">=1.2.2" },
{ name = "requests", specifier = ">=2.32.4" },
]
@@ -244,6 +361,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
]
+[[package]]
+name = "oauthlib"
+version = "3.2.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6d/fa/fbf4001037904031639e6bfbfc02badfc7e12f137a8afa254df6c4c8a670/oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918", size = 177352, upload-time = "2022-10-17T20:04:27.471Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", size = 151688, upload-time = "2022-10-17T20:04:24.037Z" },
+]
+
[[package]]
name = "pathspec"
version = "0.12.1"
@@ -278,6 +404,62 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" },
]
+[[package]]
+name = "proto-plus"
+version = "1.26.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "protobuf" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f4/ac/87285f15f7cce6d4a008f33f1757fb5a13611ea8914eb58c3d0d26243468/proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012", size = 56142, upload-time = "2025-03-10T15:54:38.843Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4e/6d/280c4c2ce28b1593a19ad5239c8b826871fc6ec275c21afc8e1820108039/proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66", size = 50163, upload-time = "2025-03-10T15:54:37.335Z" },
+]
+
+[[package]]
+name = "protobuf"
+version = "6.31.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/52/f3/b9655a711b32c19720253f6f06326faf90580834e2e83f840472d752bc8b/protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a", size = 441797, upload-time = "2025-05-28T19:25:54.947Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f3/6f/6ab8e4bf962fd5570d3deaa2d5c38f0a363f57b4501047b5ebeb83ab1125/protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9", size = 423603, upload-time = "2025-05-28T19:25:41.198Z" },
+ { url = "https://files.pythonhosted.org/packages/44/3a/b15c4347dd4bf3a1b0ee882f384623e2063bb5cf9fa9d57990a4f7df2fb6/protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447", size = 435283, upload-time = "2025-05-28T19:25:44.275Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/c9/b9689a2a250264a84e66c46d8862ba788ee7a641cdca39bccf64f59284b7/protobuf-6.31.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402", size = 425604, upload-time = "2025-05-28T19:25:45.702Z" },
+ { url = "https://files.pythonhosted.org/packages/76/a1/7a5a94032c83375e4fe7e7f56e3976ea6ac90c5e85fac8576409e25c39c3/protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39", size = 322115, upload-time = "2025-05-28T19:25:47.128Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6", size = 321070, upload-time = "2025-05-28T19:25:50.036Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724, upload-time = "2025-05-28T19:25:53.926Z" },
+]
+
+[[package]]
+name = "pyasn1"
+version = "0.6.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" },
+]
+
+[[package]]
+name = "pyasn1-modules"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyasn1" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" },
+]
+
+[[package]]
+name = "pyparsing"
+version = "3.2.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" },
+]
+
[[package]]
name = "pyyaml"
version = "6.0.2"
@@ -357,6 +539,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
]
+[[package]]
+name = "requests-oauthlib"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "oauthlib" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" },
+]
+
+[[package]]
+name = "rsa"
+version = "4.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyasn1" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
+]
+
[[package]]
name = "six"
version = "1.17.0"
@@ -396,6 +603,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
]
+[[package]]
+name = "uritemplate"
+version = "4.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload-time = "2025-06-02T15:12:06.318Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" },
+]
+
[[package]]
name = "urllib3"
version = "2.4.0"
From 122ae40570ac76526adcf46d86d13de19c671ece Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Fri, 13 Jun 2025 23:08:41 +0200
Subject: [PATCH 09/44] Add playlist field to MusikGame model and update
YouTube credentials handling in views
---
...09_alter_youtubecredentials_credentials.py | 17 ++++++
...10_alter_youtubecredentials_credentials.py | 17 ++++++
game/migrations/0011_musikgame_playlist.py | 17 ++++++
.../0012_alter_musikgame_playlist.py | 17 ++++++
game/models.py | 1 +
game/templates/game/home.html | 2 +-
game/templates/game/musikgame_detail.html | 7 +++
game/views.py | 54 +++++++++++++++++--
8 files changed, 128 insertions(+), 4 deletions(-)
create mode 100644 game/migrations/0009_alter_youtubecredentials_credentials.py
create mode 100644 game/migrations/0010_alter_youtubecredentials_credentials.py
create mode 100644 game/migrations/0011_musikgame_playlist.py
create mode 100644 game/migrations/0012_alter_musikgame_playlist.py
diff --git a/game/migrations/0009_alter_youtubecredentials_credentials.py b/game/migrations/0009_alter_youtubecredentials_credentials.py
new file mode 100644
index 0000000..7c8ac8a
--- /dev/null
+++ b/game/migrations/0009_alter_youtubecredentials_credentials.py
@@ -0,0 +1,17 @@
+# Generated by Django 5.2.3 on 2025-06-13 20:50
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("game", "0008_youtubecredentials"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="youtubecredentials",
+ name="credentials",
+ field=models.CharField(),
+ ),
+ ]
diff --git a/game/migrations/0010_alter_youtubecredentials_credentials.py b/game/migrations/0010_alter_youtubecredentials_credentials.py
new file mode 100644
index 0000000..8565c40
--- /dev/null
+++ b/game/migrations/0010_alter_youtubecredentials_credentials.py
@@ -0,0 +1,17 @@
+# Generated by Django 5.2.3 on 2025-06-13 20:58
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("game", "0009_alter_youtubecredentials_credentials"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="youtubecredentials",
+ name="credentials",
+ field=models.JSONField(),
+ ),
+ ]
diff --git a/game/migrations/0011_musikgame_playlist.py b/game/migrations/0011_musikgame_playlist.py
new file mode 100644
index 0000000..49b1132
--- /dev/null
+++ b/game/migrations/0011_musikgame_playlist.py
@@ -0,0 +1,17 @@
+# Generated by Django 5.2.3 on 2025-06-13 21:01
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("game", "0010_alter_youtubecredentials_credentials"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="musikgame",
+ name="playlist",
+ field=models.URLField(blank=True, verbose_name="Playlist YouTube"),
+ ),
+ ]
diff --git a/game/migrations/0012_alter_musikgame_playlist.py b/game/migrations/0012_alter_musikgame_playlist.py
new file mode 100644
index 0000000..3c33e77
--- /dev/null
+++ b/game/migrations/0012_alter_musikgame_playlist.py
@@ -0,0 +1,17 @@
+# Generated by Django 5.2.3 on 2025-06-13 21:03
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("game", "0011_musikgame_playlist"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="musikgame",
+ name="playlist",
+ field=models.CharField(blank=True, verbose_name="Playlist YouTube"),
+ ),
+ ]
diff --git a/game/models.py b/game/models.py
index 25e905c..8383915 100644
--- a/game/models.py
+++ b/game/models.py
@@ -39,6 +39,7 @@ class MusikGame(models.Model):
date = models.DateTimeField(auto_now_add=True)
n = models.PositiveIntegerField(default=2, verbose_name="Nombre de musiques")
players = models.ManyToManyField(User, verbose_name="Joueurs")
+ playlist = models.CharField(blank=True, verbose_name="Playlist YouTube")
def get_absolute_url(self):
return reverse("game_detail", kwargs={"pk": self.pk})
diff --git a/game/templates/game/home.html b/game/templates/game/home.html
index 6c78861..74352e1 100644
--- a/game/templates/game/home.html
+++ b/game/templates/game/home.html
@@ -2,7 +2,7 @@
Mes groupes
Créer un groupe
- {% if not user.youtubecredentials %}
+ {% if not user.youtubecredentials.credentials %}
Me connecter au compte Youtube
{% endif %}
diff --git a/game/templates/game/musikgame_detail.html b/game/templates/game/musikgame_detail.html
index 8a2d195..4d08293 100644
--- a/game/templates/game/musikgame_detail.html
+++ b/game/templates/game/musikgame_detail.html
@@ -3,6 +3,13 @@
{{ musikgame.date }}
+ {% if musikgame.playlist %}
+
+ Playlist
+
+ {% endif %}
Joueurs
diff --git a/game/views.py b/game/views.py
index 38ec076..e84fe56 100644
--- a/game/views.py
+++ b/game/views.py
@@ -1,6 +1,8 @@
import random
+import google.oauth2.credentials
import google_auth_oauthlib
+import googleapiclient.discovery
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Count, Q
@@ -131,6 +133,42 @@ class GameCreateView(LoginRequiredMixin, CreateView):
models.MusicGameOrder.objects.create(
game=form.instance, player=player, music_video=music, order=order
)
+
+ if creds := self.request.user.youtubecredentials:
+ credentials = google.oauth2.credentials.Credentials(**creds.credentials)
+ yt_api = googleapiclient.discovery.build(
+ "youtube", "v3", credentials=credentials
+ )
+ pl_request = yt_api.playlists().insert(
+ part="snippet,status",
+ body={
+ "snippet": {
+ "title": f"Musik – {group.name} – {form.instance.date.strftime('%x')}",
+ "description": "Playlist générée par Musik",
+ },
+ "status": {
+ "privacyStatus": "private",
+ },
+ },
+ )
+ pl_response = pl_request.execute()
+ pl_id = pl_response.get("id")
+ form.instance.playlist = pl_id
+ form.instance.save()
+ for _, music in pm_list:
+ request = yt_api.playlistItems().insert(
+ part="snippet",
+ body={
+ "snippet": {
+ "playlistId": pl_id,
+ "resourceId": {
+ "kind": "youtube#video",
+ "videoId": music.yt_id,
+ },
+ }
+ },
+ )
+ request.execute()
return res
@@ -173,8 +211,18 @@ class YoutubeCallbackView(LoginRequiredMixin, View):
flow.fetch_token(code=request.GET.get("code"))
- credentials = models.YoutubeCredentials(
- user=request.user, credentials=flow.credentials.to_json()
+ credentials = flow.credentials
+ models.YoutubeCredentials.objects.update_or_create(
+ user=request.user,
+ defaults={
+ "credentials": {
+ "token": credentials.token,
+ "refresh_token": credentials.refresh_token,
+ "token_uri": credentials.token_uri,
+ "client_id": credentials.client_id,
+ "client_secret": credentials.client_secret,
+ "granted_scopes": credentials.granted_scopes,
+ }
+ },
)
- credentials.save()
return redirect("/")
From dfe312d47d837823f1608a1865839de6e3c7b167 Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Fri, 13 Jun 2025 23:16:53 +0200
Subject: [PATCH 10/44] Add group detail link and implement game removal
functionality in views
---
base/templates/base.html | 7 +++++++
game/templates/game/group_detail.html | 29 ++++++++++++++++++++-------
game/urls.py | 5 +++++
game/views.py | 13 ++++++++++++
4 files changed, 47 insertions(+), 7 deletions(-)
diff --git a/base/templates/base.html b/base/templates/base.html
index d4ea3bb..663166c 100644
--- a/base/templates/base.html
+++ b/base/templates/base.html
@@ -31,6 +31,13 @@
{% empty %}
- Aucune musique. |
+ Aucune musique. |
{% endfor %}
From 245a2503e2b421fa7ce777827e37c3ac7c5d639c Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Sat, 14 Jun 2025 10:35:42 +0200
Subject: [PATCH 22/44] Refactor group and music game models to use
UniqueConstraint; update form error handling in templates
---
base/static/css/main.css | 6 +++
base/templates/base/form.html | 14 ++---
...13_alter_group_unique_together_and_more.py | 53 +++++++++++++++++++
game/models.py | 18 +++++--
game/views.py | 7 ++-
5 files changed, 84 insertions(+), 14 deletions(-)
create mode 100644 game/migrations/0013_alter_group_unique_together_and_more.py
diff --git a/base/static/css/main.css b/base/static/css/main.css
index 60f2420..e916ab0 100644
--- a/base/static/css/main.css
+++ b/base/static/css/main.css
@@ -64,3 +64,9 @@ article.message {
color: var(--pico-color-red-500);
}
}
+
+.form-error::before {
+ margin-right: .5em;
+ font-family: remixicon;
+ content: "\eca0";
+}
diff --git a/base/templates/base/form.html b/base/templates/base/form.html
index df40bc7..979daa3 100644
--- a/base/templates/base/form.html
+++ b/base/templates/base/form.html
@@ -1,8 +1,4 @@
-{% if form.non_field_errors %}
-
-{% endif %}
+{% for error in form.non_field_errors %}{{ error }}{% endfor %}
{% csrf_token %}
diff --git a/game/migrations/0013_alter_group_unique_together_and_more.py b/game/migrations/0013_alter_group_unique_together_and_more.py
new file mode 100644
index 0000000..438a287
--- /dev/null
+++ b/game/migrations/0013_alter_group_unique_together_and_more.py
@@ -0,0 +1,53 @@
+# Generated by Django 5.2.3 on 2025-06-14 08:13
+
+import django.db.models.functions.text
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("game", "0012_alter_musikgame_playlist"),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.AlterUniqueTogether(
+ name="group",
+ unique_together=set(),
+ ),
+ migrations.AlterUniqueTogether(
+ name="musicgameorder",
+ unique_together=set(),
+ ),
+ migrations.AlterUniqueTogether(
+ name="musicvideo",
+ unique_together=set(),
+ ),
+ migrations.AddConstraint(
+ model_name="group",
+ constraint=models.UniqueConstraint(
+ django.db.models.functions.text.Lower("name"),
+ models.F("owner"),
+ name="unique_group_name",
+ ),
+ ),
+ migrations.AddConstraint(
+ model_name="musicgameorder",
+ constraint=models.UniqueConstraint(
+ fields=("game", "player", "music_video"), name="unique_music_in_game"
+ ),
+ ),
+ migrations.AddConstraint(
+ model_name="musicgameorder",
+ constraint=models.UniqueConstraint(
+ fields=("game", "order"), name="unique_order"
+ ),
+ ),
+ migrations.AddConstraint(
+ model_name="musicvideo",
+ constraint=models.UniqueConstraint(
+ fields=("yt_id", "owner", "group"), name="unique_music_in_group"
+ ),
+ ),
+ ]
diff --git a/game/models.py b/game/models.py
index 8383915..886bd21 100644
--- a/game/models.py
+++ b/game/models.py
@@ -1,5 +1,6 @@
from django.contrib.auth.models import User
from django.db import models
+from django.db.models.functions import Lower
from django.urls import reverse
@@ -19,7 +20,9 @@ class Group(models.Model):
return reverse("group_detail", kwargs={"pk": self.pk})
class Meta:
- unique_together = ["name", "owner"]
+ constraints = [
+ models.UniqueConstraint(Lower("name"), "owner", name="unique_group_name")
+ ]
class MusicVideo(models.Model):
@@ -31,7 +34,11 @@ class MusicVideo(models.Model):
blacklisted = models.BooleanField(default=False)
class Meta:
- unique_together = ["yt_id", "owner", "group"]
+ constraints = [
+ models.UniqueConstraint(
+ fields=("yt_id", "owner", "group"), name="unique_music_in_group"
+ )
+ ]
class MusikGame(models.Model):
@@ -52,5 +59,10 @@ class MusicGameOrder(models.Model):
order = models.PositiveIntegerField()
class Meta:
- unique_together = [["game", "player", "music_video"], ["game", "order"]]
+ constraints = [
+ models.UniqueConstraint(
+ fields=("game", "player", "music_video"), name="unique_music_in_game"
+ ),
+ models.UniqueConstraint(fields=("game", "order"), name="unique_order"),
+ ]
ordering = ["order"]
diff --git a/game/views.py b/game/views.py
index 1b188e5..0c0dad3 100644
--- a/game/views.py
+++ b/game/views.py
@@ -7,6 +7,7 @@ from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
from django.contrib.messages.views import SuccessMessageMixin
+from django.db import IntegrityError
from django.db.models import Count, Q
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect
@@ -40,7 +41,11 @@ class GroupMixin:
class GroupCreateView(LoginRequiredMixin, GroupMixin, CreateView):
def form_valid(self, form):
form.instance.owner = self.request.user
- return super().form_valid(form)
+ try:
+ return super().form_valid(form)
+ except IntegrityError:
+ form.add_error("name", "Ce nom de groupe existe déjà.")
+ return super().form_invalid(form)
class GroupUpdateView(OwnerFilterMixin, GroupMixin, UpdateView):
From 0859b36f9849445fb0de2f9e6243e2a5eb5337bb Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Sat, 14 Jun 2025 11:01:48 +0200
Subject: [PATCH 23/44] Enhance message handling in group views; add user
feedback for actions and errors Fix #4
---
game/views.py | 86 ++++++++++++++++++++++++++++++++++++++-------------
1 file changed, 65 insertions(+), 21 deletions(-)
diff --git a/game/views.py b/game/views.py
index 0c0dad3..a24c1c6 100644
--- a/game/views.py
+++ b/game/views.py
@@ -4,13 +4,13 @@ import google.oauth2.credentials
import google_auth_oauthlib
import googleapiclient.discovery
from django.conf import settings
+from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
from django.contrib.messages.views import SuccessMessageMixin
from django.db import IntegrityError
from django.db.models import Count, Q
-from django.http import JsonResponse
-from django.shortcuts import get_object_or_404, redirect
+from django.shortcuts import redirect
from django.views import View
from django.views.generic.detail import DetailView, SingleObjectMixin
from django.views.generic.edit import CreateView, DeleteView, UpdateView
@@ -38,9 +38,8 @@ class GroupMixin:
fields = ["name"]
-class GroupCreateView(LoginRequiredMixin, GroupMixin, CreateView):
+class GroupIntegrityMixin:
def form_valid(self, form):
- form.instance.owner = self.request.user
try:
return super().form_valid(form)
except IntegrityError:
@@ -48,7 +47,13 @@ class GroupCreateView(LoginRequiredMixin, GroupMixin, CreateView):
return super().form_invalid(form)
-class GroupUpdateView(OwnerFilterMixin, GroupMixin, UpdateView):
+class GroupCreateView(LoginRequiredMixin, GroupMixin, GroupIntegrityMixin, CreateView):
+ def form_valid(self, form):
+ form.instance.owner = self.request.user
+ return super().form_valid(form)
+
+
+class GroupUpdateView(OwnerFilterMixin, GroupMixin, GroupIntegrityMixin, UpdateView):
pass
@@ -84,14 +89,26 @@ class GroupAddMusicView(MemberFilterMixin, SingleObjectMixin, View):
group = self.get_object()
yt_id = request.POST.get("yt_id")
if not yt_id:
- return JsonResponse({"error": "You must provide a YouTube ID."}, status=400)
+ messages.add_message(request, messages.ERROR, "Aucun identifiant donné")
+ return redirect(group)
yt_id = utils.parse_musik(yt_id)
title = utils.get_yt_title(yt_id)
if not title:
- return JsonResponse({"error": "Invalid YouTube ID."}, status=400)
- group.musicvideo_set.create(yt_id=yt_id, title=title, owner=request.user)
- group.save()
+ messages.add_message(
+ request, messages.ERROR, f"Vidéo Youtube invalide : {yt_id}"
+ )
+ return redirect(group)
+ try:
+ group.musicvideo_set.create(yt_id=yt_id, title=title, owner=request.user)
+ except IntegrityError:
+ messages.add_message(
+ request, messages.ERROR, f"Vidéo Youtube déjà ajoutée : {yt_id}"
+ )
+
+ messages.add_message(
+ request, messages.SUCCESS, f"Vidéo Youtube ajoutée : {yt_id}"
+ )
return redirect(group)
@@ -102,6 +119,15 @@ class GroupAddMemberView(OwnerFilterMixin, SingleObjectMixin, View):
group = self.get_object()
username = request.POST.get("username")
user = User.objects.get(username=username)
+ if user == group.owner:
+ messages.add_message(
+ request, messages.WARNING, f"{user} est le propriétaire du groupe."
+ )
+ return redirect(group)
+ if user in group.members.all():
+ messages.add_message(
+ request, messages.WARNING, f"{user} est déjà membre du groupe."
+ )
group.members.add(user)
return redirect(group)
@@ -117,17 +143,25 @@ class GroupRemoveMusicView(OwnerFilterMixin, SingleObjectMixin, View):
return redirect(group)
-class GroupRemoveMemberView(View):
- def get(self, request, pk, user_pk):
- relation = get_object_or_404(
- models.Group.members.through,
- group_id=pk,
- user_id=user_pk,
- group__owner=request.user,
- )
+class GroupRemoveMemberView(OwnerFilterMixin, SingleObjectMixin, View):
+ model = models.Group
+
+ def get(self, request, pk, user_pk):
+ group = self.get_object()
+ user = User.objects.get(pk=user_pk)
+
+ relation = models.Group.members.through.objects.filter(
+ group=group, user=user
+ ).first()
+ if not relation:
+ messages.add_message(
+ request,
+ messages.ERROR,
+ f"L'utilisateur {user} n'est pas membre du groupe.",
+ )
+ else:
+ relation.delete()
- group = relation.group
- relation.delete()
return redirect(group)
@@ -142,6 +176,11 @@ class GroupRemoveGameView(SingleObjectMixin, SuccessMessageMixin, View):
game = self.get_object()
group = game.group
game.delete()
+ messages.add_message(
+ request,
+ messages.SUCCESS,
+ f"Le jeu du {game.date.strftime('%x')} a été supprimé avec succès.",
+ )
return redirect(group)
@@ -255,9 +294,11 @@ class YoutubeLoginView(LoginRequiredMixin, View):
class YoutubeCallbackView(LoginRequiredMixin, View):
def get(self, request):
- if request.GET.get("error"):
+ if err := request.GET.get("error"):
+ messages.add_message(
+ request, messages.ERROR, f"Échec de la connexion à Youtube : {err}"
+ )
return redirect("/")
- print(request.GET)
state = self.request.session.get("state")
flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
@@ -283,6 +324,8 @@ class YoutubeCallbackView(LoginRequiredMixin, View):
}
},
)
+
+ messages.add_message(request, messages.SUCCESS, "Connexion à Youtube réussie.")
return redirect("/")
@@ -292,4 +335,5 @@ class GroupClearBlacklistView(OwnerFilterMixin, SingleObjectMixin, View):
def get(self, request, pk):
group = self.get_object()
group.musicvideo_set.filter(blacklisted=True).update(blacklisted=False)
+ messages.add_message(request, messages.SUCCESS, "La blacklist a été vidée.")
return redirect(group)
From 95969b897bad7b06d685eb71cef42b897ee9de63 Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Sat, 14 Jun 2025 11:16:41 +0200
Subject: [PATCH 24/44] Update playlist link to direct to specific video; wrap
music list in details tag
---
game/templates/game/musikgame_detail.html | 21 +++++++++++++--------
1 file changed, 13 insertions(+), 8 deletions(-)
diff --git a/game/templates/game/musikgame_detail.html b/game/templates/game/musikgame_detail.html
index 9a673ca..7011066 100644
--- a/game/templates/game/musikgame_detail.html
+++ b/game/templates/game/musikgame_detail.html
@@ -6,7 +6,7 @@
{% if musikgame.playlist %}
Playlist
{% endif %}
@@ -17,13 +17,18 @@
Musiques
-
- {% for music in musikgame.musicgameorder_set.all %}
- -
- {{ music.music_video.title }}
-
- {% endfor %}
-
+
+
+ Musiques
+
+
+ {% for music in musikgame.musicgameorder_set.all %}
+ -
+ {{ music.music_video.title }}
+
+ {% endfor %}
+
+
Résultats
From 094c5c104d893925f770f83fc0171e6bd668777f Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Sat, 14 Jun 2025 11:49:04 +0200
Subject: [PATCH 25/44] Add playlist loading feature and integrate YouTube API
for playlist generation
---
.../0014_musikgame_playlist_loading.py | 17 ++
game/models.py | 1 +
game/tasks.py | 45 ++++++
game/templates/game/musikgame_detail.html | 5 +-
game/views.py | 41 +----
musik/__init__.py | 3 +
musik/celery.py | 11 ++
pyproject.toml | 1 +
uv.lock | 145 ++++++++++++++++++
9 files changed, 230 insertions(+), 39 deletions(-)
create mode 100644 game/migrations/0014_musikgame_playlist_loading.py
create mode 100644 game/tasks.py
create mode 100644 musik/celery.py
diff --git a/game/migrations/0014_musikgame_playlist_loading.py b/game/migrations/0014_musikgame_playlist_loading.py
new file mode 100644
index 0000000..bc9fe9f
--- /dev/null
+++ b/game/migrations/0014_musikgame_playlist_loading.py
@@ -0,0 +1,17 @@
+# Generated by Django 5.2.3 on 2025-06-14 09:44
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("game", "0013_alter_group_unique_together_and_more"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="musikgame",
+ name="playlist_loading",
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/game/models.py b/game/models.py
index 886bd21..d207cb6 100644
--- a/game/models.py
+++ b/game/models.py
@@ -47,6 +47,7 @@ class MusikGame(models.Model):
n = models.PositiveIntegerField(default=2, verbose_name="Nombre de musiques")
players = models.ManyToManyField(User, verbose_name="Joueurs")
playlist = models.CharField(blank=True, verbose_name="Playlist YouTube")
+ playlist_loading = models.BooleanField(default=False)
def get_absolute_url(self):
return reverse("game_detail", kwargs={"pk": self.pk})
diff --git a/game/tasks.py b/game/tasks.py
new file mode 100644
index 0000000..2daa551
--- /dev/null
+++ b/game/tasks.py
@@ -0,0 +1,45 @@
+import google.oauth2.credentials
+import googleapiclient.discovery
+from celery import shared_task
+
+from . import models
+
+
+@shared_task
+def generate_playlist(creds, game_pk):
+ game = models.MusikGame.objects.get(pk=game_pk)
+ credentials = google.oauth2.credentials.Credentials(**creds)
+ yt_api = googleapiclient.discovery.build("youtube", "v3", credentials=credentials)
+ pl_request = yt_api.playlists().insert(
+ part="snippet,status",
+ body={
+ "snippet": {
+ "title": f"Musik – {game.group.name} – {game.date.strftime('%x')}",
+ "description": "Playlist générée par Musik",
+ },
+ "status": {
+ "privacyStatus": "private",
+ },
+ },
+ )
+ pl_response = pl_request.execute()
+ pl_id = pl_response.get("id")
+ game.playlist = pl_id
+ game.save()
+ for music in game.musicgameorder_set.all():
+ request = yt_api.playlistItems().insert(
+ part="snippet",
+ body={
+ "snippet": {
+ "playlistId": pl_id,
+ "resourceId": {
+ "kind": "youtube#video",
+ "videoId": music.music_video.yt_id,
+ },
+ }
+ },
+ )
+ request.execute()
+
+ game.playlist_loading = False
+ game.save()
diff --git a/game/templates/game/musikgame_detail.html b/game/templates/game/musikgame_detail.html
index 7011066..7d16d64 100644
--- a/game/templates/game/musikgame_detail.html
+++ b/game/templates/game/musikgame_detail.html
@@ -3,11 +3,12 @@
{{ musikgame.date }}
- {% if musikgame.playlist %}
+ {% if musikgame.playlist or musikgame.playlist_loading %}
Playlist
+ role="button"
+ {% if musikgame.playlist_loading %}aria-busy="true"{% endif %}> Playlist
{% endif %}
diff --git a/game/views.py b/game/views.py
index a24c1c6..141b03b 100644
--- a/game/views.py
+++ b/game/views.py
@@ -1,8 +1,6 @@
import random
-import google.oauth2.credentials
import google_auth_oauthlib
-import googleapiclient.discovery
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
@@ -15,7 +13,7 @@ from django.views import View
from django.views.generic.detail import DetailView, SingleObjectMixin
from django.views.generic.edit import CreateView, DeleteView, UpdateView
-from . import forms, models, utils
+from . import forms, models, tasks, utils
class OwnerFilterMixin(LoginRequiredMixin):
@@ -207,7 +205,7 @@ class GameCreateView(LoginRequiredMixin, CreateView):
players = []
musics = []
for player in form.instance.players.all():
- players += 2 * [player]
+ players += form.instance.n * [player]
musics += random.sample(
list(
player.musicvideo_set.filter(group=group, blacklisted=False).all()
@@ -225,40 +223,9 @@ class GameCreateView(LoginRequiredMixin, CreateView):
)
if creds := self.request.user.youtubecredentials:
- credentials = google.oauth2.credentials.Credentials(**creds.credentials)
- yt_api = googleapiclient.discovery.build(
- "youtube", "v3", credentials=credentials
- )
- pl_request = yt_api.playlists().insert(
- part="snippet,status",
- body={
- "snippet": {
- "title": f"Musik – {group.name} – {form.instance.date.strftime('%x')}",
- "description": "Playlist générée par Musik",
- },
- "status": {
- "privacyStatus": "private",
- },
- },
- )
- pl_response = pl_request.execute()
- pl_id = pl_response.get("id")
- form.instance.playlist = pl_id
+ form.instance.playlist_loading = True
form.instance.save()
- for _, music in pm_list:
- request = yt_api.playlistItems().insert(
- part="snippet",
- body={
- "snippet": {
- "playlistId": pl_id,
- "resourceId": {
- "kind": "youtube#video",
- "videoId": music.yt_id,
- },
- }
- },
- )
- request.execute()
+ tasks.generate_playlist.delay_on_commit(creds.credentials, form.instance.pk)
return res
diff --git a/musik/__init__.py b/musik/__init__.py
index e69de29..53f4ccb 100644
--- a/musik/__init__.py
+++ b/musik/__init__.py
@@ -0,0 +1,3 @@
+from .celery import app as celery_app
+
+__all__ = ("celery_app",)
diff --git a/musik/celery.py b/musik/celery.py
new file mode 100644
index 0000000..4cf33a3
--- /dev/null
+++ b/musik/celery.py
@@ -0,0 +1,11 @@
+import os
+
+from celery import Celery
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "musik.settings")
+
+app = Celery("musik")
+
+app.config_from_object("django.conf:settings", namespace="CELERY")
+
+app.autodiscover_tasks()
diff --git a/pyproject.toml b/pyproject.toml
index f45bbbc..c289f43 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,6 +5,7 @@ description = "Le jeu de Musik."
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
+ "celery>=5.5.3",
"django>=5.2.3",
"google-api-python-client>=2.172.0",
"google-auth>=2.40.3",
diff --git a/uv.lock b/uv.lock
index 43900ae..c9bd1ae 100644
--- a/uv.lock
+++ b/uv.lock
@@ -6,6 +6,18 @@ resolution-markers = [
"python_full_version < '3.13'",
]
+[[package]]
+name = "amqp"
+version = "5.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "vine" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" },
+]
+
[[package]]
name = "asgiref"
version = "3.8.1"
@@ -15,6 +27,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828, upload-time = "2024-03-22T14:39:34.521Z" },
]
+[[package]]
+name = "billiard"
+version = "4.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7c/58/1546c970afcd2a2428b1bfafecf2371d8951cc34b46701bea73f4280989e/billiard-4.2.1.tar.gz", hash = "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f", size = 155031, upload-time = "2024-09-21T13:40:22.491Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/30/da/43b15f28fe5f9e027b41c539abc5469052e9d48fd75f8ff094ba2a0ae767/billiard-4.2.1-py3-none-any.whl", hash = "sha256:40b59a4ac8806ba2c2369ea98d876bc6108b051c227baffd928c644d15d8f3cb", size = 86766, upload-time = "2024-09-21T13:40:20.188Z" },
+]
+
[[package]]
name = "cachetools"
version = "5.5.2"
@@ -24,6 +45,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" },
]
+[[package]]
+name = "celery"
+version = "5.5.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "billiard" },
+ { name = "click" },
+ { name = "click-didyoumean" },
+ { name = "click-plugins" },
+ { name = "click-repl" },
+ { name = "kombu" },
+ { name = "python-dateutil" },
+ { name = "vine" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bb/7d/6c289f407d219ba36d8b384b42489ebdd0c84ce9c413875a8aae0c85f35b/celery-5.5.3.tar.gz", hash = "sha256:6c972ae7968c2b5281227f01c3a3f984037d21c5129d07bf3550cc2afc6b10a5", size = 1667144, upload-time = "2025-06-01T11:08:12.563Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c9/af/0dcccc7fdcdf170f9a1585e5e96b6fb0ba1749ef6be8c89a6202284759bd/celery-5.5.3-py3-none-any.whl", hash = "sha256:0b5761a07057acee94694464ca482416b959568904c9dfa41ce8413a7d65d525", size = 438775, upload-time = "2025-06-01T11:08:09.94Z" },
+]
+
[[package]]
name = "certifi"
version = "2025.4.26"
@@ -89,6 +129,43 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
]
+[[package]]
+name = "click-didyoumean"
+version = "0.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089, upload-time = "2024-03-24T08:22:07.499Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", size = 3631, upload-time = "2024-03-24T08:22:06.356Z" },
+]
+
+[[package]]
+name = "click-plugins"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5f/1d/45434f64ed749540af821fd7e42b8e4d23ac04b1eda7c26613288d6cd8a8/click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b", size = 8164, upload-time = "2019-04-04T04:27:04.82Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/da/824b92d9942f4e472702488857914bdd50f73021efea15b4cad9aca8ecef/click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8", size = 7497, upload-time = "2019-04-04T04:27:03.36Z" },
+]
+
+[[package]]
+name = "click-repl"
+version = "0.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "prompt-toolkit" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449, upload-time = "2023-06-15T12:43:51.141Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289, upload-time = "2023-06-15T12:43:48.626Z" },
+]
+
[[package]]
name = "colorama"
version = "0.4.6"
@@ -317,11 +394,27 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/41/9f/3500910d5a98549e3098807493851eeef2b89cdd3032227558a104dfe926/json5-0.12.0-py3-none-any.whl", hash = "sha256:6d37aa6c08b0609f16e1ec5ff94697e2cbbfbad5ac112afa05794da9ab7810db", size = 36079, upload-time = "2025-04-03T16:33:11.927Z" },
]
+[[package]]
+name = "kombu"
+version = "5.5.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "amqp" },
+ { name = "packaging" },
+ { name = "tzdata" },
+ { name = "vine" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0f/d3/5ff936d8319ac86b9c409f1501b07c426e6ad41966fedace9ef1b966e23f/kombu-5.5.4.tar.gz", hash = "sha256:886600168275ebeada93b888e831352fe578168342f0d1d5833d88ba0d847363", size = 461992, upload-time = "2025-06-01T10:19:22.281Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/70/a07dcf4f62598c8ad579df241af55ced65bed76e42e45d3c368a6d82dbc1/kombu-5.5.4-py3-none-any.whl", hash = "sha256:a12ed0557c238897d8e518f1d1fdf84bd1516c5e305af2dacd85c2015115feb8", size = 210034, upload-time = "2025-06-01T10:19:20.436Z" },
+]
+
[[package]]
name = "musik"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
+ { name = "celery" },
{ name = "django" },
{ name = "google-api-python-client" },
{ name = "google-auth" },
@@ -338,6 +431,7 @@ dev = [
[package.metadata]
requires-dist = [
+ { name = "celery", specifier = ">=5.5.3" },
{ name = "django", specifier = ">=5.2.3" },
{ name = "google-api-python-client", specifier = ">=2.172.0" },
{ name = "google-auth", specifier = ">=2.40.3" },
@@ -370,6 +464,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", size = 151688, upload-time = "2022-10-17T20:04:24.037Z" },
]
+[[package]]
+name = "packaging"
+version = "25.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
+]
+
[[package]]
name = "pathspec"
version = "0.12.1"
@@ -404,6 +507,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" },
]
+[[package]]
+name = "prompt-toolkit"
+version = "3.0.51"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "wcwidth" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" },
+]
+
[[package]]
name = "proto-plus"
version = "1.26.1"
@@ -460,6 +575,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" },
]
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
+]
+
[[package]]
name = "pyyaml"
version = "6.0.2"
@@ -621,6 +748,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" },
]
+[[package]]
+name = "vine"
+version = "5.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980, upload-time = "2023-11-05T08:46:53.857Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" },
+]
+
[[package]]
name = "virtualenv"
version = "20.31.2"
@@ -634,3 +770,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c
wheels = [
{ url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" },
]
+
+[[package]]
+name = "wcwidth"
+version = "0.2.13"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" },
+]
From 7ed5cfcb8330287a9f40e436eb33a954604bd213 Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Sat, 14 Jun 2025 12:09:07 +0200
Subject: [PATCH 26/44] Refactor group music management: update removal logic
and enhance user feedback in group views
---
game/templates/game/group_detail.html | 66 +++++++++++++++------------
game/urls.py | 2 +-
game/views.py | 31 ++++++++++---
3 files changed, 62 insertions(+), 37 deletions(-)
diff --git a/game/templates/game/group_detail.html b/game/templates/game/group_detail.html
index 529a236..a3458e6 100644
--- a/game/templates/game/group_detail.html
+++ b/game/templates/game/group_detail.html
@@ -114,31 +114,31 @@
Liste des musiques
-
-
-
- Musique |
- ID |
-
-
- |
-
-
- |
-
-
-
- {% for music in musics %}
+
+ {% csrf_token %}
+
-
-
- {% csrf_token %}
-
+ {% if musics %}
+
+ {% endif %}
- {% endblock content %}
+
+
+ {% csrf_token %}
+
+
+{% endblock content %}
diff --git a/game/urls.py b/game/urls.py
index 04c64e6..3d8d8ca 100644
--- a/game/urls.py
+++ b/game/urls.py
@@ -27,7 +27,7 @@ urlpatterns = [
name="group_remove_music",
),
path(
- "group/remove_game//",
+ "group//remove_game/",
views.GroupRemoveGameView.as_view(),
name="group_remove_game",
),
diff --git a/game/views.py b/game/views.py
index 141b03b..f70831d 100644
--- a/game/views.py
+++ b/game/views.py
@@ -131,13 +131,32 @@ class GroupAddMemberView(OwnerFilterMixin, SingleObjectMixin, View):
return redirect(group)
-class GroupRemoveMusicView(OwnerFilterMixin, SingleObjectMixin, View):
- model = models.MusicVideo
+class GroupRemoveMusicView(MemberFilterMixin, SingleObjectMixin, View):
+ model = models.Group
- def get(self, request, pk):
- music = self.get_object()
- group = music.group
- music.delete()
+ def post(self, request, pk):
+ group = self.get_object()
+ musics = group.musicvideo_set.filter(
+ owner=request.user, pk__in=request.POST.getlist("musics")
+ )
+
+ if musics.count() == 0:
+ messages.add_message(request, messages.INFO, "Aucune musique supprimée.")
+ return redirect(group)
+ if musics.count() != len(request.POST.getlist("musics")):
+ messages.add_message(
+ request,
+ messages.WARNING,
+ "Certaines musiques n'ont pas pu être supprimées.",
+ )
+ musics.delete()
+ else:
+ musics.delete()
+ messages.add_message(
+ request,
+ messages.SUCCESS,
+ "Les musiques sélectionnées ont été supprimées.",
+ )
return redirect(group)
From 16cd9056940afe6f3e16ccea9018d61e12ad2ea1 Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Sat, 14 Jun 2025 12:15:18 +0200
Subject: [PATCH 27/44] Implement member removal functionality with user
feedback in group views
---
game/templates/game/group_detail.html | 83 ++++++++++++++-------------
game/urls.py | 2 +-
game/views.py | 27 +++++----
3 files changed, 61 insertions(+), 51 deletions(-)
diff --git a/game/templates/game/group_detail.html b/game/templates/game/group_detail.html
index a3458e6..24f56e7 100644
--- a/game/templates/game/group_detail.html
+++ b/game/templates/game/group_detail.html
@@ -52,48 +52,51 @@
Membres
-
+ {% if musics %}
+
+ {% endif %}
+
{% if group.owner == user %}
{% csrf_token %}
diff --git a/game/urls.py b/game/urls.py
index 3d8d8ca..9ee8dd6 100644
--- a/game/urls.py
+++ b/game/urls.py
@@ -32,7 +32,7 @@ urlpatterns = [
name="group_remove_game",
),
path(
- "group//remove_membrer//",
+ "group//remove_membrer/",
views.GroupRemoveMemberView.as_view(),
name="group_remove_member",
),
diff --git a/game/views.py b/game/views.py
index f70831d..ce9dc20 100644
--- a/game/views.py
+++ b/game/views.py
@@ -163,21 +163,28 @@ class GroupRemoveMusicView(MemberFilterMixin, SingleObjectMixin, View):
class GroupRemoveMemberView(OwnerFilterMixin, SingleObjectMixin, View):
model = models.Group
- def get(self, request, pk, user_pk):
+ def post(self, request, pk):
group = self.get_object()
- user = User.objects.get(pk=user_pk)
- relation = models.Group.members.through.objects.filter(
- group=group, user=user
- ).first()
- if not relation:
+ relations = models.Group.members.through.objects.filter(
+ group=group, user__id__in=request.POST.getlist("member")
+ )
+ if relations.count() == 0:
+ messages.add_message(request, messages.INFO, "Aucun membre supprimé.")
+ if relations.count() != len(request.POST.getlist("member")):
messages.add_message(
request,
- messages.ERROR,
- f"L'utilisateur {user} n'est pas membre du groupe.",
+ messages.WARNING,
+ "Certains membres n'ont pas pu être supprimées.",
)
+ relations.delete()
else:
- relation.delete()
+ relations.delete()
+ messages.add_message(
+ request,
+ messages.SUCCESS,
+ "Les membres sélectionnés ont été supprimées.",
+ )
return redirect(group)
@@ -321,5 +328,5 @@ class GroupClearBlacklistView(OwnerFilterMixin, SingleObjectMixin, View):
def get(self, request, pk):
group = self.get_object()
group.musicvideo_set.filter(blacklisted=True).update(blacklisted=False)
- messages.add_message(request, messages.SUCCESS, "La blacklist a été vidée.")
+ messages.add_message(request, messages.SUCCESS, "La blacklist a été effacée.")
return redirect(group)
From 539abe11a1ed2cb0f65dea1c478915d416df92ed Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Sat, 14 Jun 2025 12:16:39 +0200
Subject: [PATCH 28/44] Fix queryset filter in GroupDetailView to correctly
reference group members
---
game/views.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/game/views.py b/game/views.py
index ce9dc20..e6f73a8 100644
--- a/game/views.py
+++ b/game/views.py
@@ -73,7 +73,9 @@ class GroupDetailView(MemberFilterMixin, GroupMixin, DetailView):
data["members"] = data["group"].members.annotate(
count=Count(
"musicvideo",
- filter=Q(group=data["group"], musicvideo__blacklisted=False),
+ filter=Q(
+ musicvideo__group=data["group"], musicvideo__blacklisted=False
+ ),
)
)
From 7e54c6e0ad3af6ed2a2530be1a135da7c25e0829 Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Sat, 14 Jun 2025 12:23:14 +0200
Subject: [PATCH 29/44] Implement game removal functionality with user feedback
in group views
---
game/templates/game/group_detail.html | 50 ++++++++++++++-------------
game/views.py | 43 ++++++++++++++---------
2 files changed, 53 insertions(+), 40 deletions(-)
diff --git a/game/templates/game/group_detail.html b/game/templates/game/group_detail.html
index 24f56e7..1395e85 100644
--- a/game/templates/game/group_detail.html
+++ b/game/templates/game/group_detail.html
@@ -22,32 +22,34 @@
Parties
-
-
- Date |
- Joueurs |
- {% if group.owner == user %}
-
-
- |
- {% endif %}
-
-
- {% for game in group.musikgame_set.all %}
-
-
- {{ game.date }}
- |
- {{ game.players.all|join:", " }} |
- {% if group.owner == user %}
+
+ {% csrf_token %}
+
+ {{ game.players.all|join:", " }} |
+
+ {% endfor %}
+
+
+
+
{% endif %}
Membres
diff --git a/game/views.py b/game/views.py
index e6f73a8..c564b8c 100644
--- a/game/views.py
+++ b/game/views.py
@@ -173,11 +173,12 @@ class GroupRemoveMemberView(OwnerFilterMixin, SingleObjectMixin, View):
)
if relations.count() == 0:
messages.add_message(request, messages.INFO, "Aucun membre supprimé.")
+ return redirect(group)
if relations.count() != len(request.POST.getlist("member")):
messages.add_message(
request,
messages.WARNING,
- "Certains membres n'ont pas pu être supprimées.",
+ "Certains membres n'ont pas pu être supprimés.",
)
relations.delete()
else:
@@ -185,28 +186,38 @@ class GroupRemoveMemberView(OwnerFilterMixin, SingleObjectMixin, View):
messages.add_message(
request,
messages.SUCCESS,
- "Les membres sélectionnés ont été supprimées.",
+ "Les membres sélectionnés ont été supprimés.",
)
return redirect(group)
-class GroupRemoveGameView(SingleObjectMixin, SuccessMessageMixin, View):
- model = models.MusikGame
- success_message = "Le jeu du %(date)s a été supprimé avec succès."
+class GroupRemoveGameView(OwnerFilterMixin, SingleObjectMixin, View):
+ model = models.Group
- def get_queryset(self):
- return super().get_queryset().filter(group__owner=self.request.user)
+ def post(self, request, pk):
+ group = self.get_object()
+
+ games = group.musikgame_set.filter(pk__in=request.POST.getlist("game"))
+
+ if games.count() == 0:
+ messages.add_message(request, messages.INFO, "Aucune partie supprimé.")
+ return redirect(group)
+ if games.count() != len(request.POST.getlist("game")):
+ messages.add_message(
+ request,
+ messages.WARNING,
+ "Certaines parties n'ont pas pu être supprimées.",
+ )
+ games.delete()
+ else:
+ games.delete()
+ messages.add_message(
+ request,
+ messages.SUCCESS,
+ "Les parties sélectionnées ont été supprimées.",
+ )
- def get(self, request, pk):
- game = self.get_object()
- group = game.group
- game.delete()
- messages.add_message(
- request,
- messages.SUCCESS,
- f"Le jeu du {game.date.strftime('%x')} a été supprimé avec succès.",
- )
return redirect(group)
From 637b4973623d57eb4ec1dcc5988ab149df735ddf Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Sat, 14 Jun 2025 12:27:24 +0200
Subject: [PATCH 30/44] Enhance group detail view: add conditional buttons for
game and member removal based on group ownership
---
game/templates/game/group_detail.html | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/game/templates/game/group_detail.html b/game/templates/game/group_detail.html
index 1395e85..8f12311 100644
--- a/game/templates/game/group_detail.html
+++ b/game/templates/game/group_detail.html
@@ -26,7 +26,7 @@
{% csrf_token %}
- |
+ {% if group.owner == user %} | {% endif %}
Date |
Joueurs |
@@ -46,9 +46,11 @@
{% endfor %}
-
+ {% if group.owner == user %}
+
+ {% endif %}
{% endif %}
@@ -92,7 +94,7 @@
{% endfor %}
- {% if musics %}
+ {% if group.owner == user %}
{% if musics %}
-
- Supprimer les musiques sélectionnées
-
+
{% endif %}
diff --git a/game/urls.py b/game/urls.py
index 9ee8dd6..763d277 100644
--- a/game/urls.py
+++ b/game/urls.py
@@ -22,10 +22,15 @@ urlpatterns = [
name="group_add_member",
),
path(
- "group/remove_music//",
+ "group//remove_music/",
views.GroupRemoveMusicView.as_view(),
name="group_remove_music",
),
+ path(
+ "group//unblacklist_music/",
+ views.GroupUnblacklistMusicView.as_view(),
+ name="group_unblacklist_music",
+ ),
path(
"group//remove_game/",
views.GroupRemoveGameView.as_view(),
diff --git a/game/views.py b/game/views.py
index 453694f..09bad7d 100644
--- a/game/views.py
+++ b/game/views.py
@@ -162,6 +162,24 @@ class GroupRemoveMusicView(MemberFilterMixin, SingleObjectMixin, View):
return redirect(group)
+class GroupUnblacklistMusicView(MemberFilterMixin, SingleObjectMixin, View):
+ model = models.Group
+
+ def post(self, request, pk):
+ group = self.get_object()
+ musics = group.musicvideo_set.filter(
+ owner=request.user, pk__in=request.POST.getlist("musics")
+ )
+
+ musics.update(blacklisted=False)
+ messages.add_message(
+ request,
+ messages.SUCCESS,
+ "Les musiques sélectionnées ont été enlevées de la blacklist.",
+ )
+ return redirect(group)
+
+
class GroupRemoveMemberView(OwnerFilterMixin, SingleObjectMixin, View):
model = models.Group
From 08e4d6e75bf9ab9dfa7f566f7c2466430db4e79f Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Sat, 14 Jun 2025 12:39:55 +0200
Subject: [PATCH 34/44] Add game start and rename buttons in group detail view
---
game/templates/game/group_detail.html | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/game/templates/game/group_detail.html b/game/templates/game/group_detail.html
index d8fe44d..8fc9186 100644
--- a/game/templates/game/group_detail.html
+++ b/game/templates/game/group_detail.html
@@ -13,8 +13,10 @@
{% csrf_token %}
-
+
Jouer
+
+
Renommer
From 3042382ae4dcd0e3b08548e5935a9b94ac563d87 Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Sat, 14 Jun 2025 12:53:46 +0200
Subject: [PATCH 35/44] Add YouTube playlist support in game detail views and
create custom template tag
---
game/templates/game/group_detail.html | 10 +++++++++-
game/templates/game/musikgame_detail.html | 3 ++-
game/templates/tags/youtube/playlist.html | 1 +
game/templatetags/__init__.py | 0
game/templatetags/youtube.py | 8 ++++++++
5 files changed, 20 insertions(+), 2 deletions(-)
create mode 100644 game/templates/tags/youtube/playlist.html
create mode 100644 game/templatetags/__init__.py
create mode 100644 game/templatetags/youtube.py
diff --git a/game/templates/game/group_detail.html b/game/templates/game/group_detail.html
index 8fc9186..e3b5f56 100644
--- a/game/templates/game/group_detail.html
+++ b/game/templates/game/group_detail.html
@@ -1,5 +1,5 @@
{% extends "base.html" %}
-{% load form %}
+{% load form youtube %}
{% block content %}
{% if group.owner == user %}
@@ -39,6 +39,9 @@
{% if group.owner == user %} | {% endif %}
Date |
+
+ Playlists
+ |
Joueurs |
@@ -52,6 +55,11 @@
{{ game.date }}
|
+
+ {% if game.playlist %}
+ Playlist
+ {% endif %}
+ |
{{ game.players.all|join:", " }} |
{% endfor %}
diff --git a/game/templates/game/musikgame_detail.html b/game/templates/game/musikgame_detail.html
index 7d16d64..444c7f1 100644
--- a/game/templates/game/musikgame_detail.html
+++ b/game/templates/game/musikgame_detail.html
@@ -1,4 +1,5 @@
{% extends "base.html" %}
+{% load youtube %}
{% block content %}
{{ musikgame.date }}
@@ -6,7 +7,7 @@
{% if musikgame.playlist or musikgame.playlist_loading %}
Playlist
diff --git a/game/templates/tags/youtube/playlist.html b/game/templates/tags/youtube/playlist.html
new file mode 100644
index 0000000..3270d81
--- /dev/null
+++ b/game/templates/tags/youtube/playlist.html
@@ -0,0 +1 @@
+{{ text }}
diff --git a/game/templatetags/__init__.py b/game/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/game/templatetags/youtube.py b/game/templatetags/youtube.py
new file mode 100644
index 0000000..6fc1ac1
--- /dev/null
+++ b/game/templatetags/youtube.py
@@ -0,0 +1,8 @@
+from django import template
+
+register = template.Library()
+
+
+@register.simple_tag
+def yt_playlist(game):
+ return f"https://youtube.com/watch?v={game.musicgameorder_set.first().music_video.yt_id}&list={game.playlist}"
From 9cb4f42d07fff69eb8a8d667241ddb049b2564f8 Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart"
Date: Sat, 14 Jun 2025 13:49:01 +0200
Subject: [PATCH 36/44] Add hero section with background gradient and logo to
enhance user interface
---
base/static/css/main.css | 27 +++++++++++++
base/templates/base.html | 82 ++++++++++++++++++++-------------------
base/templates/hero.html | 10 +++++
base/templates/index.html | 12 ++++--
4 files changed, 87 insertions(+), 44 deletions(-)
create mode 100644 base/templates/hero.html
diff --git a/base/static/css/main.css b/base/static/css/main.css
index e916ab0..1cb4daf 100644
--- a/base/static/css/main.css
+++ b/base/static/css/main.css
@@ -70,3 +70,30 @@ article.message {
font-family: remixicon;
content: "\eca0";
}
+
+#hero {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: radial-gradient(
+ circle 50vh at 4rem 50%,
+ var(--pico-primary-background),
+ color-mix(in hsl, var(--pico-primary-background), var(--pico-background-color) 10%) 20%,
+ color-mix(in hsl, var(--pico-primary-background), var(--pico-background-color) 30%) 40%,
+ 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;
+ align-items: center;
+ padding: 4rem;
+
+ .big-logo {
+ font-size: 8rem;
+ }
+ h1 {
+ font-size: 4rem;
+ font-weight: 900;
+ }
+}
diff --git a/base/templates/base.html b/base/templates/base.html
index 9fe6763..60315f0 100644
--- a/base/templates/base.html
+++ b/base/templates/base.html
@@ -21,45 +21,47 @@
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.colors.min.css">
-
-
- {% for message in messages %}{{ message }}{% endfor %}
- {% block content %}
- {% endblock content %}
-
+ {% block body %}
+
+
+ {% for message in messages %}{{ message }}{% endfor %}
+ {% block content %}
+ {% endblock content %}
+
+ {% endblock body %}