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 %} + +
+ {% csrf_token %} +
+ {% for field in form %} + + {% if field.errors %} +
    + {% for error in field.errors %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} + {% endfor %} +
+ +
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 %} -
    - {% for error in form.non_field_errors %} -
  • {{ error }}
  • - {% endfor %} -
+
    + {% for error in form.non_field_errors %}
  • {{ error }}
  • {% endfor %} +
{% endif %} - -
+ {% csrf_token %}
{% for field in form %} @@ -16,9 +13,7 @@ {% if field.errors %}
    - {% for error in field.errors %} -
  • {{ error }}
  • - {% endfor %} + {% for error in field.errors %}
  • {{ error }}
  • {% endfor %}
{% endif %} {% endfor %} diff --git a/base/templates/index.html b/base/templates/index.html index 64383d8..42c83ec 100644 --- a/base/templates/index.html +++ b/base/templates/index.html @@ -1,5 +1,7 @@ {% extends "base.html" %} - {% block content %} -

Musik

+

Musik

+ {% if user.is_authenticated %} + {% include "game/home.html" %} + {% endif %} {% endblock content %} diff --git a/base/templates/registration/login.html b/base/templates/registration/login.html index f699914..a41b5aa 100644 --- a/base/templates/registration/login.html +++ b/base/templates/registration/login.html @@ -1,7 +1,6 @@ {% extends "base.html" %} {% load form %} - {% block content %} -

Connexion

-{% form form submit="Se connecter" %} +

Connexion

+ {% form form submit="Se connecter" %} {% endblock content %} diff --git a/game/__init__.py b/game/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/game/apps.py b/game/apps.py new file mode 100644 index 0000000..3ee5205 --- /dev/null +++ b/game/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class GameConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "game" diff --git a/game/migrations/0001_initial.py b/game/migrations/0001_initial.py new file mode 100644 index 0000000..5cf8035 --- /dev/null +++ b/game/migrations/0001_initial.py @@ -0,0 +1,72 @@ +# Generated by Django 5.2.3 on 2025-06-13 14:12 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Group", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField()), + ( + "members", + models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL), + ), + ( + "owner", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="owned_group_set", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="MusicVideo", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("yt_id", models.CharField(max_length=16)), + ("blacklisted", models.BooleanField(default=False)), + ( + "group", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="game.group" + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/game/migrations/0002_alter_group_name_alter_group_unique_together.py b/game/migrations/0002_alter_group_name_alter_group_unique_together.py new file mode 100644 index 0000000..abcb37b --- /dev/null +++ b/game/migrations/0002_alter_group_name_alter_group_unique_together.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.3 on 2025-06-13 15:04 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("game", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name="group", + name="name", + field=models.CharField(verbose_name="Nom du groupe"), + ), + migrations.AlterUniqueTogether( + name="group", + unique_together={("name", "owner")}, + ), + ] diff --git a/game/migrations/__init__.py b/game/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/game/models.py b/game/models.py new file mode 100644 index 0000000..ea80626 --- /dev/null +++ b/game/models.py @@ -0,0 +1,24 @@ +from django.contrib.auth.models import User +from django.db import models +from django.urls import reverse + + +class Group(models.Model): + name = models.CharField(verbose_name="Nom du groupe") + owner = models.ForeignKey( + User, on_delete=models.CASCADE, related_name="owned_group_set" + ) + members = models.ManyToManyField(User, blank=True) + + def get_absolute_url(self): + return reverse("group_detail", kwargs={"pk": self.pk}) + + class Meta: + unique_together = ["name", "owner"] + + +class MusicVideo(models.Model): + yt_id = models.CharField(max_length=16) + user = models.ForeignKey(User, on_delete=models.CASCADE) + group = models.ForeignKey(Group, on_delete=models.CASCADE) + blacklisted = models.BooleanField(default=False) diff --git a/game/templates/game/group_confirm_delete.html b/game/templates/game/group_confirm_delete.html new file mode 100644 index 0000000..696227f --- /dev/null +++ b/game/templates/game/group_confirm_delete.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{% load form %} +{% block content %} +

+ +
+
+

+ {{ group.name }} +

+
+

+ Confirmer la supression du groupe {{ group.name }} ? +

+ + {% csrf_token %} + + Annuler + +
+
+{% endblock content %} diff --git a/game/templates/game/group_detail.html b/game/templates/game/group_detail.html new file mode 100644 index 0000000..8654554 --- /dev/null +++ b/game/templates/game/group_detail.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} +{% block content %} +

+ {{ group.name }} +

+

+ Modifier le groupe +

+

Membres

+
    +
  • + {{ group.owner }} +
  • + {% for member in group.members.all %}
  • {{ member }}
  • {% endfor %} +
+{% endblock content %} diff --git a/game/templates/game/group_form.html b/game/templates/game/group_form.html new file mode 100644 index 0000000..67f1f05 --- /dev/null +++ b/game/templates/game/group_form.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} +{% load form %} +{% block content %} + {% if group %} +

+ {{ group.name }} +

+

+ Supprimer le groupe +

+ {% else %} +

Créer un groupe

+ {% endif %} + {% form form %} +{% endblock content %} diff --git a/game/templates/game/home.html b/game/templates/game/home.html new file mode 100644 index 0000000..d2c045b --- /dev/null +++ b/game/templates/game/home.html @@ -0,0 +1,19 @@ +

Bienvenue {{ user.username }} !

+

Mes groupes

+

+ Créer un groupe +

+{% if user.owned_group_set.exists or user.group_set.exists %} +
    + {% for group in user.owned_group_set.all %} +
  • + {{ group.name }} +
  • + {% endfor %} + {% for group in user.group_set.all %} +
  • + {{ group.name }} +
  • + {% endfor %} +
+{% endif %} diff --git a/game/urls.py b/game/urls.py new file mode 100644 index 0000000..28bc1d6 --- /dev/null +++ b/game/urls.py @@ -0,0 +1,14 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + path("group/create/", views.GroupCreateView.as_view(), name="group_create"), + path( + "group//update/", views.GroupUpdateView.as_view(), name="group_update" + ), + path( + "group//delete/", views.GroupDeleteView.as_view(), name="group_delete" + ), + path("group//", views.GroupDetailView.as_view(), name="group_detail"), +] diff --git a/game/views.py b/game/views.py new file mode 100644 index 0000000..05fdfe7 --- /dev/null +++ b/game/views.py @@ -0,0 +1,33 @@ +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 + + +class OwnerFilterMixin(LoginRequiredMixin): + def get_queryset(self): + return super().get_queryset().filter(owner=self.request.user) + + +class GroupMixin: + model = models.Group + fields = ["name"] + + +class GroupCreateView(LoginRequiredMixin, GroupMixin, CreateView): + def form_valid(self, form): + form.instance.owner = self.request.user + return super().form_valid(form) + + +class GroupUpdateView(OwnerFilterMixin, GroupMixin, UpdateView): + pass + + +class GroupDeleteView(OwnerFilterMixin, GroupMixin, DeleteView): + success_url = "/" + + +class GroupDetailView(OwnerFilterMixin, GroupMixin, DetailView): + pass diff --git a/musik/settings.py b/musik/settings.py index 9c3c277..10439ce 100644 --- a/musik/settings.py +++ b/musik/settings.py @@ -32,6 +32,7 @@ ALLOWED_HOSTS = [] INSTALLED_APPS = [ "base", + "game", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", diff --git a/musik/urls.py b/musik/urls.py index b10d442..946e05d 100644 --- a/musik/urls.py +++ b/musik/urls.py @@ -20,5 +20,6 @@ from django.urls import include, path urlpatterns = [ path("", include("base.urls")), + path("game/", include("game.urls")), path("admin/", admin.site.urls), ] From 8ed39c78b8061497420925cb34fe5d750820332b Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Fri, 13 Jun 2025 17:32:17 +0200 Subject: [PATCH 04/44] Add GroupAddMembersForm and view for editing group members --- .pre-commit-config.yaml | 5 +++++ base/templates/base.html | 6 +++++- base/templates/registration/login.html | 2 +- game/forms.py | 15 +++++++++++++++ game/templates/game/group_confirm_delete.html | 1 - game/templates/game/group_detail.html | 3 +++ game/templates/game/group_form.html | 2 +- game/urls.py | 5 +++++ game/views.py | 7 ++++++- 9 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 game/forms.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4220f94..fa834b3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,3 +15,8 @@ repos: hooks: - id: ruff - id: ruff-format + - repo: https://github.com/djlint/djLint + rev: v1.23.3 + hooks: + - id: djlint-django + args: ["--reformat", "--lint", "--quiet"] diff --git a/base/templates/base.html b/base/templates/base.html index 49f52bf..d4ea3bb 100644 --- a/base/templates/base.html +++ b/base/templates/base.html @@ -4,8 +4,12 @@ + + - {% block title %}Musik{% endblock %} + {% block title %} + Musik + {% endblock title %} Connexion {% form form submit="Se connecter" %} -{% endblock content %} + {% endblock content %} diff --git a/game/forms.py b/game/forms.py new file mode 100644 index 0000000..f305315 --- /dev/null +++ b/game/forms.py @@ -0,0 +1,15 @@ +from django import forms + +from . import models + + +class GroupAddMembersForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["members"].queryset = self.fields["members"].queryset.exclude( + id=self.instance.owner.id + ) + + class Meta: + model = models.Group + fields = ["members"] diff --git a/game/templates/game/group_confirm_delete.html b/game/templates/game/group_confirm_delete.html index 696227f..f8d7e5e 100644 --- a/game/templates/game/group_confirm_delete.html +++ b/game/templates/game/group_confirm_delete.html @@ -1,7 +1,6 @@ {% extends "base.html" %} {% load form %} {% block content %} -

diff --git a/game/templates/game/group_detail.html b/game/templates/game/group_detail.html index 8654554..d710c37 100644 --- a/game/templates/game/group_detail.html +++ b/game/templates/game/group_detail.html @@ -7,6 +7,9 @@ Modifier le groupe

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 }})

    + + + + + + + + + + + {% for music in musics %} + + + + + + {% empty %} + + + + {% endfor %} + +
    MusiqueID + + + +
    {{ music.title }} + {{ music.yt_id }} + + + + +
    Aucune musique.
    +
    + {% csrf_token %} +
    + + +
    +
    + {% 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 %} +
    1. + {{ music.music_video.title }} +
    2. + {% 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 @@
    + + + + + + + {% for game in group.musikgame_set.all %} + + + + + + {% endfor %} + +
    DateJoueurs + +
    + {{ game.date }} + {{ game.players.all|join:", " }} + +
    {% endif %}

    Membres diff --git a/game/urls.py b/game/urls.py index a995581..1b76d14 100644 --- a/game/urls.py +++ b/game/urls.py @@ -26,6 +26,11 @@ urlpatterns = [ views.GroupRemoveMusicView.as_view(), name="group_remove_music", ), + path( + "group/remove_game//", + views.GroupRemoveGameView.as_view(), + name="group_remove_game", + ), path( "group//start_game/", views.GameCreateView.as_view(), name="start_game" ), diff --git a/game/views.py b/game/views.py index e84fe56..b13bf5a 100644 --- a/game/views.py +++ b/game/views.py @@ -96,6 +96,19 @@ class GroupRemoveMusicView(OwnerFilterMixin, SingleObjectMixin, View): return redirect(group) +class GroupRemoveGameView(SingleObjectMixin, View): + model = models.MusikGame + + def get_queryset(self): + return super().get_queryset().filter(group__owner=self.request.user) + + def get(self, request, pk): + game = self.get_object() + group = game.group + game.delete() + return redirect(group) + + class GameCreateView(LoginRequiredMixin, CreateView): model = models.MusikGame form_class = forms.MusikGameForm From c55861c439854065a53ce84626db4bddb1fb5df2 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Fri, 13 Jun 2025 23:20:58 +0200 Subject: [PATCH 11/44] Update GroupDetailView to exclude blacklisted music videos and mark music as blacklisted in GameCreateView --- game/views.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/game/views.py b/game/views.py index b13bf5a..86f7b6f 100644 --- a/game/views.py +++ b/game/views.py @@ -54,10 +54,15 @@ class GroupDetailView(MemberFilterMixin, GroupMixin, DetailView): 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["group"] + .musicvideo_set.filter(owner=data["group"].owner, blacklisted=False) + .count() ) data["members"] = data["group"].members.annotate( - count=Count("musicvideo", filter=Q(group=data["group"])) + count=Count( + "musicvideo", + filter=Q(group=data["group"], musicvideo__blacklisted=False), + ) ) return data @@ -143,6 +148,8 @@ class GameCreateView(LoginRequiredMixin, CreateView): pm_list = list(zip(players, musics)) random.shuffle(pm_list) for (player, music), order in zip(pm_list, range(len(pm_list))): + music.blacklisted = True + music.save() models.MusicGameOrder.objects.create( game=form.instance, player=player, music_video=music, order=order ) From e64bb8b95a34758c3162d61e35086348355d5784 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Fri, 13 Jun 2025 23:24:21 +0200 Subject: [PATCH 12/44] Add functionality to clear blacklisted music videos in GroupClearBlacklistView and update group detail template --- game/templates/game/group_detail.html | 1 + game/urls.py | 5 +++++ game/views.py | 9 +++++++++ 3 files changed, 15 insertions(+) diff --git a/game/templates/game/group_detail.html b/game/templates/game/group_detail.html index 15f1243..1ef8b50 100644 --- a/game/templates/game/group_detail.html +++ b/game/templates/game/group_detail.html @@ -10,6 +10,7 @@

    Lancer une partie + Effacer la blacklist

    {% endif %} {% if group.musikgame_set %} diff --git a/game/urls.py b/game/urls.py index 1b76d14..8b8f9d3 100644 --- a/game/urls.py +++ b/game/urls.py @@ -41,4 +41,9 @@ urlpatterns = [ views.YoutubeCallbackView.as_view(), name="youtube_callback", ), + path( + "group//clear_blacklist/", + views.GroupClearBlacklistView.as_view(), + name="group_clear_blacklist", + ), ] diff --git a/game/views.py b/game/views.py index 86f7b6f..5cf1c33 100644 --- a/game/views.py +++ b/game/views.py @@ -246,3 +246,12 @@ class YoutubeCallbackView(LoginRequiredMixin, View): }, ) return redirect("/") + + +class GroupClearBlacklistView(OwnerFilterMixin, SingleObjectMixin, View): + model = models.Group + + def get(self, request, pk): + group = self.get_object() + group.musicvideo_set.filter(blacklisted=True).update(blacklisted=False) + return redirect(group) From f211b9af506d9660be200a4a65c05394fd9a4fd5 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Fri, 13 Jun 2025 23:26:54 +0200 Subject: [PATCH 13/44] Add collapsible section for music list in group detail template --- game/templates/game/group_detail.html | 67 ++++++++++++++------------- 1 file changed, 36 insertions(+), 31 deletions(-) diff --git a/game/templates/game/group_detail.html b/game/templates/game/group_detail.html index 1ef8b50..70cdfd5 100644 --- a/game/templates/game/group_detail.html +++ b/game/templates/game/group_detail.html @@ -57,40 +57,45 @@

    Mes musiques ({{ musics.count }})

    - - - - - - - - - - - {% for music in musics %} +
    + + Liste des musiques + +
    MusiqueID - - - -
    + - - - + + + + + + {% for music in musics %} + + + - - {% empty %} - - - - {% endfor %} - -
    {{ music.title }} - {{ music.yt_id }} - - Musique + ID + + + +
    {{ music.title }} + {{ music.yt_id }} - -
    Aucune musique.
    + + + + + + {% empty %} + + Aucune musique. + + {% endfor %} + + +
    {% csrf_token %}
    From 6ada3290c846cb72dedd173507fe863b273afe54 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Fri, 13 Jun 2025 23:51:10 +0200 Subject: [PATCH 14/44] Add member management functionality in group views and templates --- base/static/css/main.css | 2 +- game/templates/game/group_detail.html | 55 ++++++++++++++++++++++++--- game/templates/game/home.html | 2 +- game/urls.py | 10 +++++ game/views.py | 26 ++++++++++++- 5 files changed, 86 insertions(+), 9 deletions(-) diff --git a/base/static/css/main.css b/base/static/css/main.css index e7247c2..a353d2e 100644 --- a/base/static/css/main.css +++ b/base/static/css/main.css @@ -12,7 +12,7 @@ form a[role="button"] { width: 100%; } -i.ri-vip-crown-fill { +i.owner { color: var(--pico-color-amber-200); } diff --git a/game/templates/game/group_detail.html b/game/templates/game/group_detail.html index 70cdfd5..d305efa 100644 --- a/game/templates/game/group_detail.html +++ b/game/templates/game/group_detail.html @@ -48,12 +48,55 @@ Modifier les membres

    {% endif %} -
      -
    • - {{ group.owner }} ({{ owner_count }}) -
    • - {% for member in members.all %}
    • {{ member }} ({{ member.count }})
    • {% endfor %} -
    + + + + + + + + + + + + + + + + + {% for member in members.all %} + + + + + + + {% endfor %} + +
    Membre + + + + + +
    {{ group.owner }} + + {{ owner_count }}
    {{ member }}{{ member.count }} + + + +
    + + {% csrf_token %} +
    + + +
    +

    Mes musiques ({{ musics.count }})

    diff --git a/game/templates/game/home.html b/game/templates/game/home.html index 74352e1..b726da8 100644 --- a/game/templates/game/home.html +++ b/game/templates/game/home.html @@ -10,7 +10,7 @@
      {% for group in user.owned_group_set.all %}
    • - {{ group.name }} + {{ group.name }}
    • {% endfor %} {% for group in user.group_set.all %} diff --git a/game/urls.py b/game/urls.py index 8b8f9d3..d55639b 100644 --- a/game/urls.py +++ b/game/urls.py @@ -21,6 +21,11 @@ urlpatterns = [ views.GroupAddMusicView.as_view(), name="group_add_music", ), + path( + "group//add_member/", + views.GroupAddMemberView.as_view(), + name="group_add_member", + ), path( "group/remove_music//", views.GroupRemoveMusicView.as_view(), @@ -31,6 +36,11 @@ urlpatterns = [ views.GroupRemoveGameView.as_view(), name="group_remove_game", ), + path( + "group//remove_membrer//", + views.GroupRemoveMemberView.as_view(), + name="group_remove_member", + ), path( "group//start_game/", views.GameCreateView.as_view(), name="start_game" ), diff --git a/game/views.py b/game/views.py index 5cf1c33..7713d26 100644 --- a/game/views.py +++ b/game/views.py @@ -5,9 +5,10 @@ import google_auth_oauthlib import googleapiclient.discovery from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.auth.models import User from django.db.models import Count, Q from django.http import JsonResponse -from django.shortcuts import redirect +from django.shortcuts import get_object_or_404, redirect from django.views import View from django.views.generic.detail import DetailView, SingleObjectMixin from django.views.generic.edit import CreateView, DeleteView, UpdateView @@ -26,6 +27,7 @@ class MemberFilterMixin(LoginRequiredMixin): super() .get_queryset() .filter(Q(members=self.request.user) | Q(owner=self.request.user)) + .distinct() ) @@ -91,6 +93,18 @@ class GroupAddMusicView(MemberFilterMixin, SingleObjectMixin, View): return redirect(group) +class GroupAddMemberView(OwnerFilterMixin, SingleObjectMixin, View): + model = models.Group + + def post(self, request, pk): + group = self.get_object() + username = request.POST.get("username") + user = User.objects.get(username=username) + + group.members.add(user) + return redirect(group) + + class GroupRemoveMusicView(OwnerFilterMixin, SingleObjectMixin, View): model = models.MusicVideo @@ -101,6 +115,16 @@ 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 = relation.group + relation.delete() + return redirect(group) + + class GroupRemoveGameView(SingleObjectMixin, View): model = models.MusikGame From 43ba52f31e1544c2878a1867960154fe4183bd72 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Fri, 13 Jun 2025 23:58:24 +0200 Subject: [PATCH 15/44] Refactor group display in home template to use articles for better structure and styling --- base/static/css/main.css | 12 ++++++++++++ game/templates/game/home.html | 26 ++++++++++++++------------ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/base/static/css/main.css b/base/static/css/main.css index a353d2e..372715c 100644 --- a/base/static/css/main.css +++ b/base/static/css/main.css @@ -19,3 +19,15 @@ i.owner { .template { display: none; } + +a.group { + text-decoration: none; +} + +.group-owner, .group i { + margin-left: .5rem; +} + +.group-owner { + color: var(--pico-color-zinc-500); +} diff --git a/game/templates/game/home.html b/game/templates/game/home.html index b726da8..20c310b 100644 --- a/game/templates/game/home.html +++ b/game/templates/game/home.html @@ -7,16 +7,18 @@ {% endif %}

      {% if user.owned_group_set.exists or user.group_set.exists %} -
        - {% for group in user.owned_group_set.all %} -
      • - {{ group.name }} -
      • - {% endfor %} - {% for group in user.group_set.all %} -
      • - {{ group.name }} -
      • - {% endfor %} -
      + {% for group in user.owned_group_set.all %} + +
      + {{ group.name }} +
      +
      + {% endfor %} + {% for group in user.group_set.all %} + +
      + {{ group.name }} {{ group.owner }} +
      +
      + {% endfor %} {% endif %} From 78e5be580b296bc1daee707297552d165df04f69 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sat, 14 Jun 2025 00:11:39 +0200 Subject: [PATCH 16/44] Refactor group detail and musikgame detail templates for improved owner visibility and styling; update CSS for music count display --- base/static/css/main.css | 8 ++- game/templates/game/group_detail.html | 76 +++++++++++++---------- game/templates/game/musikgame_detail.html | 4 +- game/urls.py | 5 -- game/views.py | 20 +++--- 5 files changed, 64 insertions(+), 49 deletions(-) diff --git a/base/static/css/main.css b/base/static/css/main.css index 372715c..1fb8b6e 100644 --- a/base/static/css/main.css +++ b/base/static/css/main.css @@ -25,9 +25,15 @@ a.group { } .group-owner, .group i { - margin-left: .5rem; + margin-left: .5em; } .group-owner { color: var(--pico-color-zinc-500); } + +.music-count { + font-weight: 900; + color: var(--pico-color-zinc-500); + margin-left: .5em; +} diff --git a/game/templates/game/group_detail.html b/game/templates/game/group_detail.html index d305efa..19b4121 100644 --- a/game/templates/game/group_detail.html +++ b/game/templates/game/group_detail.html @@ -2,7 +2,12 @@ {% load form %} {% block content %}

      - {{ group.name }} + {% if group.owner == user %} + + {% else %} + + {% endif %} + {{ group.name }}

      {% if group.owner == user %}

      @@ -21,9 +26,11 @@ Date Joueurs - - - + {% if group.owner == user %} + + + + {% endif %} {% for game in group.musikgame_set.all %} @@ -32,9 +39,11 @@ {{ game.date }} {{ game.players.all|join:", " }} - - - + {% if group.owner == user %} + + + + {% endif %} {% endfor %} @@ -43,11 +52,6 @@

      Membres

      - {% if group.owner == user %} -

      - Modifier les membres -

      - {% endif %} @@ -58,9 +62,11 @@ - + {% if group.owner == user %} + + {% endif %} @@ -70,35 +76,39 @@ - + {% if group.owner == user %}{% endif %} {% for member in members.all %} - + {% if group.owner == user %} + + {% endif %} {% endfor %}
      - - + +
      {{ owner_count }}
      {{ member }} {{ member.count }} - - - - + + + +
      -
      - {% csrf_token %} -
      - - -
      -
      + {% if group.owner == user %} +
      + {% csrf_token %} +
      + + +
      +
      + {% endif %}

      - Mes musiques ({{ musics.count }}) + Mes musiques {{ musics.count }}

      diff --git a/game/templates/game/musikgame_detail.html b/game/templates/game/musikgame_detail.html index 4d08293..91674ef 100644 --- a/game/templates/game/musikgame_detail.html +++ b/game/templates/game/musikgame_detail.html @@ -13,9 +13,7 @@

      Joueurs

      -
        - {% for member in musikgame.players.all %}
      • {{ member }}
      • {% endfor %} -
      +

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

      Musiques

      diff --git a/game/urls.py b/game/urls.py index d55639b..04c64e6 100644 --- a/game/urls.py +++ b/game/urls.py @@ -10,11 +10,6 @@ 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"), path( "group//add_music/", diff --git a/game/views.py b/game/views.py index 7713d26..84b99cd 100644 --- a/game/views.py +++ b/game/views.py @@ -70,11 +70,6 @@ class GroupDetailView(MemberFilterMixin, GroupMixin, DetailView): return data -class GroupAddMembersView(OwnerFilterMixin, GroupMixin, UpdateView): - fields = None - form_class = forms.GroupAddMembersForm - - class GroupAddMusicView(MemberFilterMixin, SingleObjectMixin, View): model = models.Group @@ -118,8 +113,12 @@ class GroupRemoveMusicView(OwnerFilterMixin, SingleObjectMixin, View): 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 + models.Group.members.through, + group_id=pk, + user_id=user_pk, + group__owner=request.user, ) + group = relation.group relation.delete() return redirect(group) @@ -220,7 +219,14 @@ class GameDetailView(LoginRequiredMixin, DetailView): model = models.MusikGame def get_queryset(self): - return super().get_queryset().filter(group__owner=self.request.user) + return ( + super() + .get_queryset() + .filter( + Q(group__members=self.request.user) | Q(group__owner=self.request.user) + ) + .distinct() + ) class YoutubeLoginView(LoginRequiredMixin, View): From c320015205004cc23984828cfa6cbe73e7df4370 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sat, 14 Jun 2025 00:19:44 +0200 Subject: [PATCH 17/44] Fix condition check for musikgame existence in group detail template; add results section in musikgame detail template; update order range in GameCreateView --- game/templates/game/group_detail.html | 2 +- game/templates/game/musikgame_detail.html | 34 +++++++++++++++++++++++ game/views.py | 2 +- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/game/templates/game/group_detail.html b/game/templates/game/group_detail.html index 19b4121..6db239b 100644 --- a/game/templates/game/group_detail.html +++ b/game/templates/game/group_detail.html @@ -18,7 +18,7 @@ Effacer la blacklist

      {% endif %} - {% if group.musikgame_set %} + {% if group.musikgame_set.exists %}

      Parties

      diff --git a/game/templates/game/musikgame_detail.html b/game/templates/game/musikgame_detail.html index 91674ef..9a673ca 100644 --- a/game/templates/game/musikgame_detail.html +++ b/game/templates/game/musikgame_detail.html @@ -24,4 +24,38 @@ {% endfor %} +

      + Résultats +

      +
      + + Résultats + + + + + + + + + + + {% for music in musikgame.musicgameorder_set.all %} + + + + + + {% endfor %} + +
      + + + Musique + + Joueur +
      {{ music.order }} + {{ music.music_video.title }} + {{ music.player }}
      +
      {% endblock content %} diff --git a/game/views.py b/game/views.py index 84b99cd..312e051 100644 --- a/game/views.py +++ b/game/views.py @@ -170,7 +170,7 @@ class GameCreateView(LoginRequiredMixin, CreateView): pm_list = list(zip(players, musics)) random.shuffle(pm_list) - for (player, music), order in zip(pm_list, range(len(pm_list))): + for (player, music), order in zip(pm_list, range(1, len(pm_list) + 1)): music.blacklisted = True music.save() models.MusicGameOrder.objects.create( From 20b90f410ee9ce296659a10445a2dd43158a1ca1 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sat, 14 Jun 2025 00:22:04 +0200 Subject: [PATCH 18/44] Remove out-of-date README sections --- README.md | 40 ---------------------------------------- 1 file changed, 40 deletions(-) diff --git a/README.md b/README.md index bd41b1e..7164040 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,3 @@ # Musik Script pour créer un jeu de Musik. - -## Installation -Pour utiliser la création automatisée de Playlist youtube, les bibliothèques Python -suivantes doivent être installées : -``` -pip install --upgrade google-api-python-client -pip install --upgrade google-auth-oauthlib google-auth-httplib2 -``` - -## Configuration -Pour utiliser l'API Youtube, une clé API est nécessaire. Elle doit être stockée dans le -fichier `secret.json`. - -## Utilisation -Créer un dossier `lists` qui contient les listes de musiques pour chaque joueur (une url -youtube ou un identifiant de vidéo par ligne). Le nom des fichiers correspondra au nom -des joueurs. - -Lancer le script à l'aide de la commande `python -m musik`. -``` -usage: python -m musik [-h] [-a] [-c] [-b] [-n NUMBER] [--lists LISTS] [--blacklists BLACKLISTS] [--results RESULTS] - [-v] - -Lancer une partie de Musik - -options: - -h, --help show this help message and exit - -a, --no-api Désactiver l'API Youtube ; affiche la liste des liens (default: False) - -c, --no-save-creds Désactiver l'enregistrement de la connexion Youtube (default: False) - -b, --no-blacklist Désactiver le méchanisme de blacklist en lecture et écriture (default: False) - -n NUMBER, --number NUMBER - Modifier le nombre de musiques par joueur (default: 2) - --lists LISTS Sélectionner le dossier contenant les listes de musiques (default: lists) - --blacklists BLACKLISTS - Sélectionner le dossier contenant les blacklist (default: blacklists) - --results RESULTS Sélectionner le dossier pour stocker les résultats (default: results) - -v, --verbose -``` - -Stocker les fichiers des joueurs absents dans un dossier séparé. From 6ab5748cbc4f066e76acd64459f50eb7fdc67f44 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sat, 14 Jun 2025 09:31:10 +0200 Subject: [PATCH 19/44] Add favicon link to base template --- base/templates/base.html | 1 + 1 file changed, 1 insertion(+) diff --git a/base/templates/base.html b/base/templates/base.html index 663166c..da110fe 100644 --- a/base/templates/base.html +++ b/base/templates/base.html @@ -11,6 +11,7 @@ Musik {% endblock title %} + From 700ab7eccab59c2b2a504ca5e22cc10a4299beb1 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sat, 14 Jun 2025 10:01:45 +0200 Subject: [PATCH 20/44] Add user signup form and implement signup view; enhance message display in templates --- base/forms.py | 11 +++++++++++ base/static/css/main.css | 27 ++++++++++++++++++++++++++ base/templates/auth/user_form.html | 6 ++++++ base/templates/base.html | 1 + base/templates/registration/login.html | 3 +++ base/urls.py | 1 + base/views.py | 12 ++++++++++++ game/views.py | 7 +++++-- 8 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 base/forms.py create mode 100644 base/templates/auth/user_form.html diff --git a/base/forms.py b/base/forms.py new file mode 100644 index 0000000..024ee98 --- /dev/null +++ b/base/forms.py @@ -0,0 +1,11 @@ +from django import forms +from django.contrib.auth.forms import UserCreationForm +from django.contrib.auth.models import User + + +class UserSignupForm(UserCreationForm): + email = forms.EmailField() + + class Meta: + model = User + fields = ["username", "email"] diff --git a/base/static/css/main.css b/base/static/css/main.css index 1fb8b6e..60f2420 100644 --- a/base/static/css/main.css +++ b/base/static/css/main.css @@ -37,3 +37,30 @@ a.group { color: var(--pico-color-zinc-500); margin-left: .5em; } + +article.message { + &::before { + margin-right: .5em; + font-family: remixicon; + } + &.debug::before { + content: "\eb06"; + color: var(--pico-color-zinc-500); + } + &.info::before { + content: "\ee58"; + color: var(--pico-color-indigo-600); + } + &.success::before { + content: "\eb80"; + color: var(--pico-color-green-500); + } + &.warning::before { + content: "\eca0"; + color: var(--pico-color-amber-200); + } + &.error::before { + content: "\eca0"; + color: var(--pico-color-red-500); + } +} diff --git a/base/templates/auth/user_form.html b/base/templates/auth/user_form.html new file mode 100644 index 0000000..257d1dc --- /dev/null +++ b/base/templates/auth/user_form.html @@ -0,0 +1,6 @@ +{% extends "base.html" %} +{% load form %} +{% block content %} +

      Créer un compte

      + {% form form %} + {% endblock content %} diff --git a/base/templates/base.html b/base/templates/base.html index da110fe..9fe6763 100644 --- a/base/templates/base.html +++ b/base/templates/base.html @@ -57,6 +57,7 @@

+ {% for message in messages %}
{{ message }}
{% endfor %} {% block content %} {% endblock content %}
diff --git a/base/templates/registration/login.html b/base/templates/registration/login.html index 50e7ccb..1a78811 100644 --- a/base/templates/registration/login.html +++ b/base/templates/registration/login.html @@ -2,5 +2,8 @@ {% load form %} {% block content %}

Connexion

+

+ Créer un compte +

{% form form submit="Se connecter" %} {% endblock content %} diff --git a/base/urls.py b/base/urls.py index 8468c3e..2a6b2d5 100644 --- a/base/urls.py +++ b/base/urls.py @@ -4,5 +4,6 @@ from . import views urlpatterns = [ path("", views.HomePageView.as_view(), name="index"), + path("accounts/signup/", views.SignupView.as_view(), name="signup"), path("accounts/", include("django.contrib.auth.urls")), ] diff --git a/base/views.py b/base/views.py index d1762b8..e3618a0 100644 --- a/base/views.py +++ b/base/views.py @@ -1,5 +1,17 @@ +from django.contrib.auth.models import User +from django.contrib.messages.views import SuccessMessageMixin from django.views.generic.base import TemplateView +from django.views.generic.edit import CreateView + +from . import forms class HomePageView(TemplateView): template_name = "index.html" + + +class SignupView(SuccessMessageMixin, CreateView): + model = User + form_class = forms.UserSignupForm + success_url = "/" + success_message = "Le compte %(username)s a été créé avec succès." diff --git a/game/views.py b/game/views.py index 312e051..1b188e5 100644 --- a/game/views.py +++ b/game/views.py @@ -6,6 +6,7 @@ import googleapiclient.discovery 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.models import Count, Q from django.http import JsonResponse from django.shortcuts import get_object_or_404, redirect @@ -46,8 +47,9 @@ class GroupUpdateView(OwnerFilterMixin, GroupMixin, UpdateView): pass -class GroupDeleteView(OwnerFilterMixin, GroupMixin, DeleteView): +class GroupDeleteView(OwnerFilterMixin, GroupMixin, SuccessMessageMixin, DeleteView): success_url = "/" + success_message = "Le groupe a été supprimé avec succès." class GroupDetailView(MemberFilterMixin, GroupMixin, DetailView): @@ -124,8 +126,9 @@ class GroupRemoveMemberView(View): return redirect(group) -class GroupRemoveGameView(SingleObjectMixin, View): +class GroupRemoveGameView(SingleObjectMixin, SuccessMessageMixin, View): model = models.MusikGame + success_message = "Le jeu du %(date)s a été supprimé avec succès." def get_queryset(self): return super().get_queryset().filter(group__owner=self.request.user) From 43ec6aafc496466cdbd991a21be6f33d8322eee1 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sat, 14 Jun 2025 10:02:32 +0200 Subject: [PATCH 21/44] Update music display in group detail template to expand empty message cell --- game/templates/game/group_detail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/templates/game/group_detail.html b/game/templates/game/group_detail.html index 6db239b..529a236 100644 --- a/game/templates/game/group_detail.html +++ b/game/templates/game/group_detail.html @@ -143,7 +143,7 @@ {% 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 %} -
    - {% for error in form.non_field_errors %}
  • {{ error }}
  • {% endfor %} -
-{% endif %} +{% for error in form.non_field_errors %}
{{ error }}
{% endfor %}
{% csrf_token %}
@@ -10,12 +6,10 @@ - {% if field.errors %} -
    - {% for error in field.errors %}
  • {{ error }}
  • {% endfor %} -
- {% endif %} {% endfor %}
diff --git a/game/migrations/0013_alter_group_unique_together_and_more.py b/game/migrations/0013_alter_group_unique_together_and_more.py new file mode 100644 index 0000000..438a287 --- /dev/null +++ b/game/migrations/0013_alter_group_unique_together_and_more.py @@ -0,0 +1,53 @@ +# Generated by Django 5.2.3 on 2025-06-14 08:13 + +import django.db.models.functions.text +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("game", "0012_alter_musikgame_playlist"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterUniqueTogether( + name="group", + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name="musicgameorder", + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name="musicvideo", + unique_together=set(), + ), + migrations.AddConstraint( + model_name="group", + constraint=models.UniqueConstraint( + django.db.models.functions.text.Lower("name"), + models.F("owner"), + name="unique_group_name", + ), + ), + migrations.AddConstraint( + model_name="musicgameorder", + constraint=models.UniqueConstraint( + fields=("game", "player", "music_video"), name="unique_music_in_game" + ), + ), + migrations.AddConstraint( + model_name="musicgameorder", + constraint=models.UniqueConstraint( + fields=("game", "order"), name="unique_order" + ), + ), + migrations.AddConstraint( + model_name="musicvideo", + constraint=models.UniqueConstraint( + fields=("yt_id", "owner", "group"), name="unique_music_in_group" + ), + ), + ] diff --git a/game/models.py b/game/models.py index 8383915..886bd21 100644 --- a/game/models.py +++ b/game/models.py @@ -1,5 +1,6 @@ from django.contrib.auth.models import User from django.db import models +from django.db.models.functions import Lower from django.urls import reverse @@ -19,7 +20,9 @@ class Group(models.Model): return reverse("group_detail", kwargs={"pk": self.pk}) class Meta: - unique_together = ["name", "owner"] + constraints = [ + models.UniqueConstraint(Lower("name"), "owner", name="unique_group_name") + ] class MusicVideo(models.Model): @@ -31,7 +34,11 @@ class MusicVideo(models.Model): blacklisted = models.BooleanField(default=False) class Meta: - unique_together = ["yt_id", "owner", "group"] + constraints = [ + models.UniqueConstraint( + fields=("yt_id", "owner", "group"), name="unique_music_in_group" + ) + ] class MusikGame(models.Model): @@ -52,5 +59,10 @@ class MusicGameOrder(models.Model): order = models.PositiveIntegerField() class Meta: - unique_together = [["game", "player", "music_video"], ["game", "order"]] + constraints = [ + models.UniqueConstraint( + fields=("game", "player", "music_video"), name="unique_music_in_game" + ), + models.UniqueConstraint(fields=("game", "order"), name="unique_order"), + ] ordering = ["order"] diff --git a/game/views.py b/game/views.py index 1b188e5..0c0dad3 100644 --- a/game/views.py +++ b/game/views.py @@ -7,6 +7,7 @@ from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.models import User from django.contrib.messages.views import SuccessMessageMixin +from django.db import IntegrityError from django.db.models import Count, Q from django.http import JsonResponse from django.shortcuts import get_object_or_404, redirect @@ -40,7 +41,11 @@ class GroupMixin: class GroupCreateView(LoginRequiredMixin, GroupMixin, CreateView): def form_valid(self, form): form.instance.owner = self.request.user - return super().form_valid(form) + try: + return super().form_valid(form) + except IntegrityError: + form.add_error("name", "Ce nom de groupe existe déjà.") + return super().form_invalid(form) class GroupUpdateView(OwnerFilterMixin, GroupMixin, UpdateView): From 0859b36f9849445fb0de2f9e6243e2a5eb5337bb Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sat, 14 Jun 2025 11:01:48 +0200 Subject: [PATCH 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 %} -
  1. - {{ music.music_video.title }} -
  2. - {% endfor %} -
+
+ + Musiques + +
    + {% for music in musikgame.musicgameorder_set.all %} +
  1. + {{ music.music_video.title }} +
  2. + {% 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 - - - - - - - - - - - {% for music in musics %} + + {% csrf_token %} +
MusiqueID - - - -
+ - - - + + + + + + {% for music in musics %} + + + + {% empty %} @@ -148,12 +148,18 @@ {% endfor %}
{{ music.title }} - {{ music.yt_id }} - - + MusiqueID + +
+ + {{ music.title }} + {{ music.yt_id }} - +
- - - {% 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 group.owner == user %} - - {% endif %} - - - - - - - - {% if group.owner == user %}{% endif %} - - {% for member in members.all %} + + {% csrf_token %} +
Membre - - - - - -
{{ group.owner }} - - {{ owner_count }}
+ - - - - {% if group.owner == user %} - - {% endif %} + {% if group.owner == user %}{% endif %} + + + - {% endfor %} - -
{{ member }}{{ member.count }} - - - - Membre + + + +
+ + + + {% if group.owner == user %}{% endif %} + {{ group.owner }} + + + + {{ owner_count }} + + {% for member in members.all %} + + {% if group.owner == user %} + + + + {% endif %} + {{ member }} + + {{ member.count }} + + {% endfor %} + + + {% 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

- - - - - {% if group.owner == user %} - - {% endif %} - - - {% for game in group.musikgame_set.all %} - - - - {% if group.owner == user %} + + {% csrf_token %} +
DateJoueurs - -
- {{ game.date }} - {{ game.players.all|join:", " }}
+ + + + + + + {% for game in group.musikgame_set.all %} + + {% if group.owner == user %} + + {% endif %} - {% endif %} - - {% endfor %} - -
DateJoueurs
+ + - + {{ game.date }}
+ {{ 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 %} @@ -46,9 +46,11 @@ {% endfor %}
Date Joueurs
- + {% if group.owner == user %} + + {% endif %} {% endif %}

@@ -92,7 +94,7 @@ {% endfor %} - {% if musics %} + {% if group.owner == user %}

{% if group.owner == user %}

- Modifier le groupe -

-

- Lancer une partie - Effacer la blacklist +

+ {% csrf_token %} +
+ Jouer + Renommer + +
+

{% endif %} {% if group.musikgame_set.exists %} diff --git a/game/views.py b/game/views.py index c564b8c..453694f 100644 --- a/game/views.py +++ b/game/views.py @@ -338,7 +338,7 @@ class YoutubeCallbackView(LoginRequiredMixin, View): class GroupClearBlacklistView(OwnerFilterMixin, SingleObjectMixin, View): model = models.Group - def get(self, request, pk): + def post(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é effacée.") From 5e394a8c034722a8bff6d7c075e230ca8d91d09b Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sat, 14 Jun 2025 12:34:14 +0200 Subject: [PATCH 32/44] Refactor group detail view: change button style for clearing blacklist to secondary --- game/templates/game/group_detail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/templates/game/group_detail.html b/game/templates/game/group_detail.html index 1f7f810..639385a 100644 --- a/game/templates/game/group_detail.html +++ b/game/templates/game/group_detail.html @@ -19,7 +19,7 @@ class="secondary" role="button"> Renommer From 0a930e575c405abd1c5a3c7f1be9c22248ae74e1 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sat, 14 Jun 2025 12:39:12 +0200 Subject: [PATCH 33/44] Add unblacklist functionality for group music and update URLs --- game/templates/game/group_detail.html | 13 ++++++++++--- game/urls.py | 7 ++++++- game/views.py | 18 ++++++++++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/game/templates/game/group_detail.html b/game/templates/game/group_detail.html index 639385a..d8fe44d 100644 --- a/game/templates/game/group_detail.html +++ b/game/templates/game/group_detail.html @@ -165,9 +165,16 @@ {% if musics %} - +
+ + +
{% 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 %} diff --git a/base/templates/hero.html b/base/templates/hero.html new file mode 100644 index 0000000..7dcdbe0 --- /dev/null +++ b/base/templates/hero.html @@ -0,0 +1,10 @@ +{% load static %} +
+
+ +

Musik

+

+ Jouer +

+
+
diff --git a/base/templates/index.html b/base/templates/index.html index 42c83ec..5235a9f 100644 --- a/base/templates/index.html +++ b/base/templates/index.html @@ -1,7 +1,11 @@ {% extends "base.html" %} {% block content %} -

Musik

- {% if user.is_authenticated %} - {% include "game/home.html" %} - {% endif %} + {% include "game/home.html" %} {% endblock content %} +{% block body %} + {% if user.is_authenticated %} + {{ block.super }} + {% else %} + {% include "hero.html" %} + {% endif %} +{% endblock body %} From 5446175cad4b3af65ab6d89356f68fae3bccd502 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sat, 14 Jun 2025 14:39:10 +0200 Subject: [PATCH 37/44] Fix hero section background gradient positioning for improved responsiveness --- base/static/css/main.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/static/css/main.css b/base/static/css/main.css index 1cb4daf..1bb1f28 100644 --- a/base/static/css/main.css +++ b/base/static/css/main.css @@ -78,7 +78,7 @@ article.message { right: 0; bottom: 0; background: radial-gradient( - circle 50vh at 4rem 50%, + circle 50vh at calc(100vw - 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%, From 04b0a30e76de86dac50babb1923cb197ff45e957 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sat, 14 Jun 2025 15:05:50 +0200 Subject: [PATCH 38/44] Enhance UI by updating header styles and improving group detail layout --- base/static/css/main.css | 21 ++++++++++++++++++++- base/templates/base.html | 22 ++++++++++++++-------- game/templates/game/group_detail.html | 13 +++++++++---- game/templates/game/home.html | 8 +++++--- 4 files changed, 48 insertions(+), 16 deletions(-) diff --git a/base/static/css/main.css b/base/static/css/main.css index 1bb1f28..c329a18 100644 --- a/base/static/css/main.css +++ b/base/static/css/main.css @@ -1,3 +1,8 @@ +:root { + --pico-font-family-sans-serif: Exo, sans-serif; + --pico-font-weight: 450; +} + img.logo { border-radius: var(--pico-border-radius); } @@ -92,8 +97,22 @@ article.message { .big-logo { font-size: 8rem; } + h1 { font-size: 4rem; - font-weight: 900; } } + +h1, +h2, +h3, +h4, +h5, +h6, +.header-title { + font-weight: 900; +} + +i.i { + margin-right: .5em; +} diff --git a/base/templates/base.html b/base/templates/base.html index 60315f0..8e677eb 100644 --- a/base/templates/base.html +++ b/base/templates/base.html @@ -12,27 +12,29 @@ {% endblock title %} - + + + + {% block body %}
- - - +

{% csrf_token %} - +
{% if group.owner == user %}{% endif %} @@ -66,7 +66,9 @@
Date
{% if group.owner == user %} - {% endif %} @@ -115,6 +117,7 @@ {% if group.owner == user %} @@ -142,7 +145,7 @@ {% csrf_token %} - +
@@ -176,7 +179,9 @@
{% if musics %}
-