diff --git a/base/static/css/main.css b/base/static/css/main.css index 74e4488..c329a18 100644 --- a/base/static/css/main.css +++ b/base/static/css/main.css @@ -17,7 +17,7 @@ form a[role="button"] { width: 100%; } -i.owner, .gold { +i.owner { color: var(--pico-color-amber-200); } @@ -25,7 +25,7 @@ i.owner, .gold { display: none; } -a.group, a.running { +a.group { text-decoration: none; } @@ -77,6 +77,11 @@ article.message { } #hero { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; background: radial-gradient( circle 50vh at calc(100vw - 4rem) 50%, var(--pico-primary-background), @@ -85,41 +90,18 @@ article.message { color-mix(in hsl, var(--pico-primary-background) 30%, var(--pico-background-color)) 60%, color-mix(in hsl, var(--pico-primary-background) 10%, var(--pico-background-color)) 80%, var(--pico-background-color)); - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - height: 100%; - overflow-y: auto; + display: grid; + align-items: center; padding: 4rem; - main { - display: contents; + .big-logo { + font-size: 8rem; } - section { - max-width: 20rem; + + h1 { + font-size: 4rem; } } -.full-page { - height: 100%; - display: grid; - grid-template-rows: 1fr; - align-items: center; - margin-bottom: 4rem; - - &.r { - -ms-grid-column-align: end; - } - .big-logo { - font-size: 8rem; - } - - h1 { - font-size: 4rem; - } -} - h1, h2, @@ -134,91 +116,3 @@ h6, i.i { margin-right: .5em; } -i.hl, .me { - color: var(--pico-primary); -} - -footer { - text-align: center; - font-weight: 350; -} - -td.c, th.c { - text-align: center; - - input { - margin: 0; - } -} - -table select { - margin-bottom: 0; -} - -.podium { - > :first-child { - font-weight: 900; - font-size: 1.25em; - &::marker { - color: var(--pico-color-amber-200); - } - } - > :nth-child(2) { - font-weight: 800; - font-size: 1.1em; - &::marker { - color: var(--pico-color-grey-300); - } - } - > :nth-child(3) { - font-weight: 600; - font-size: 1.1em; - &::marker { - color: var(--pico-color-sand-300); - } - } - .score { - margin-left: .5em; - } -} - - -.score { - font-weight: 900; - color: var(--pico-color-zinc-500); -} -.correct { - .score, i { - color: var(--pico-color-lime-200);} -} -.wrong { - .score, i { - color: var(--pico-color-red-500);} -} - -table.results, table.musics { - white-space: nowrap; -} -.sc i { - margin-right: .5em; -} - -@media (width < 576px) { - [role="group"] { - display: grid; - & > :first-child { - border-radius: var(--pico-border-radius) var(--pico-border-radius) 0 0 !important; - } - & > :not(:first-child) { - margin-left: 0 !important; - margin-top: calc(var(--pico-border-width) * -1); - } - & > :last-child { - border-radius: 0 0 var(--pico-border-radius) var(--pico-border-radius) !important; - } - } -} - -.brand-name { - color: var(--pico-primary); -} diff --git a/base/static/favicon/apple-touch-icon.png b/base/static/favicon/apple-touch-icon.png deleted file mode 100644 index abb8a48..0000000 Binary files a/base/static/favicon/apple-touch-icon.png and /dev/null differ diff --git a/base/static/favicon/favicon-96x96.png b/base/static/favicon/favicon-96x96.png deleted file mode 100644 index beab288..0000000 Binary files a/base/static/favicon/favicon-96x96.png and /dev/null differ diff --git a/base/static/favicon/favicon.ico b/base/static/favicon/favicon.ico deleted file mode 100644 index 178d60c..0000000 Binary files a/base/static/favicon/favicon.ico and /dev/null differ diff --git a/base/static/favicon/favicon.svg b/base/static/favicon/favicon.svg deleted file mode 100644 index 63c10e0..0000000 --- a/base/static/favicon/favicon.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/base/static/favicon/site.webmanifest b/base/static/favicon/site.webmanifest deleted file mode 100644 index af6d059..0000000 --- a/base/static/favicon/site.webmanifest +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "MyWebSite", - "short_name": "MySite", - "icons": [ - { - "src": "/web-app-manifest-192x192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "/web-app-manifest-512x512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ], - "theme_color": "#aa40bf", - "background_color": "#ffffff", - "display": "standalone" -} diff --git a/base/static/favicon/web-app-manifest-192x192.png b/base/static/favicon/web-app-manifest-192x192.png deleted file mode 100644 index f7f97da..0000000 Binary files a/base/static/favicon/web-app-manifest-192x192.png and /dev/null differ diff --git a/base/static/favicon/web-app-manifest-512x512.png b/base/static/favicon/web-app-manifest-512x512.png deleted file mode 100644 index 9e4e381..0000000 Binary files a/base/static/favicon/web-app-manifest-512x512.png and /dev/null differ diff --git a/base/templates/auth/user_confirm_delete.html b/base/templates/auth/user_confirm_delete.html deleted file mode 100644 index 8840f15..0000000 --- a/base/templates/auth/user_confirm_delete.html +++ /dev/null @@ -1,25 +0,0 @@ -{% extends "base.html" %} -{% block content %} - -
-
-

- {{ user.username }} -

-
-

- Confirmer la supression du compte {{ user.username }} ? -

-

- Toute suppression est immédiate et définitive. La suppression du compte entraîne la suppression des groupes dont celui-ci est propriétaire. -

-
- {% csrf_token %} - - Annuler -
-
-
-{% endblock content %} diff --git a/base/templates/auth/user_form.html b/base/templates/auth/user_form.html index c895f16..257d1dc 100644 --- a/base/templates/auth/user_form.html +++ b/base/templates/auth/user_form.html @@ -1,24 +1,6 @@ {% extends "base.html" %} {% load form %} {% block content %} -

Créer mon compte

- {% for error in form.non_field_errors %}
{{ error }}
{% endfor %} -
- {% csrf_token %} -
- {% for field in form %} - - {% endfor %} -
- -
- J'ai déjà un compte -
-
-{% endblock content %} +

Créer un compte

+ {% form form %} + {% endblock content %} diff --git a/base/templates/auth/user_settings.html b/base/templates/auth/user_settings.html deleted file mode 100644 index c69e952..0000000 --- a/base/templates/auth/user_settings.html +++ /dev/null @@ -1,45 +0,0 @@ -{% extends "base.html" %} -{% load form %} -{% block content %} -

- Mon compte -

- {% for error in form.non_field_errors %}
{{ error }}
{% endfor %} -
- {% csrf_token %} -
- -
-
- {% if not user.youtubecredentials.credentials %} - Me connecter au compte Youtube - {% else %} - - {% endif %} -
-
- {% for field in form %} - - {% endfor %} - -
-
- Changer mon mot de passe -
-
- Supprimer mon compte -
-
-{% endblock content %} diff --git a/base/templates/base.html b/base/templates/base.html index 504228f..8e677eb 100644 --- a/base/templates/base.html +++ b/base/templates/base.html @@ -11,7 +11,7 @@ Musik {% endblock title %} - {% include "favicon.html" %} + @@ -60,9 +68,6 @@ {% block content %} {% endblock content %} - {% endblock body %} diff --git a/base/templates/base/inline_form.html b/base/templates/base/inline_form.html deleted file mode 100644 index 75769ae..0000000 --- a/base/templates/base/inline_form.html +++ /dev/null @@ -1,11 +0,0 @@ -{% for error in form.non_field_errors %}
{{ error }}
{% endfor %} -
- {% csrf_token %} -
- {% for field in form %}{{ field }}{% endfor %} - - {% if field.errors %} - {{ field.errors|join:", " }} - {% endif %} -
-
diff --git a/base/templates/favicon.html b/base/templates/favicon.html deleted file mode 100644 index dad76d7..0000000 --- a/base/templates/favicon.html +++ /dev/null @@ -1,8 +0,0 @@ -{% load static %} - - - - - - - diff --git a/base/templates/footer.html b/base/templates/footer.html deleted file mode 100644 index 6c57641..0000000 --- a/base/templates/footer.html +++ /dev/null @@ -1 +0,0 @@ -Musik {{ VERSION }} – © Edgar P. BurkhartMentions légales et confidentialité diff --git a/base/templates/hero.html b/base/templates/hero.html index 813c011..7dcdbe0 100644 --- a/base/templates/hero.html +++ b/base/templates/hero.html @@ -1,27 +1,10 @@ {% load static %}
-
-
-
- -

Musik

-

- Jouer -

-
-
-
-
-

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

-

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

-
-
-
- +
+ +

Musik

+

+ Jouer +

+
diff --git a/base/templates/index.html b/base/templates/index.html index e026c32..5235a9f 100644 --- a/base/templates/index.html +++ b/base/templates/index.html @@ -1,4 +1,11 @@ {% extends "base.html" %} +{% block content %} + {% include "game/home.html" %} +{% endblock content %} {% block body %} - {% include "hero.html" %} + {% if user.is_authenticated %} + {{ block.super }} + {% else %} + {% include "hero.html" %} + {% endif %} {% endblock body %} diff --git a/base/templates/privacy.html b/base/templates/privacy.html deleted file mode 100644 index 1549403..0000000 --- a/base/templates/privacy.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends "base.html" %} -{% block content %} -

Mentions légales

-

Éditeur

-

- Ce site est réalisé et hébergé par Edgar P. Burkhart. -

-

Données Personnelles

-

- Dans le cadre de la création d'un compte, un nom d'utilisateur, une adresse email et un mot de passe sont requis. - Ces données sont stockées dans le seul but de permettre la connexion et la réinitialisation du mot de passe de l'utilisateur (dans le cas de l'adresse mail). -

-

- La connexion à un compte Youtube est utilisée pour permettre la génération automatique de playlists, et sa suppression le cas échéant. - Elle n'est pas requise pour l'utilisation de Musik. - Youtube est une marque de Google LLC. -

-

- Les données saisies dans Musik (groupes créés, listes de musiques) sont conservées jusqu'à demande de suppression. La suppression du compte entraîne la suppression de l'ensemble des données qui y sont liées. - La suppression du compte peut être demandée dans les paramètres du compte. La suppression des données est immédiate et définitive. -

-{% endblock content %} diff --git a/base/templates/registration/login.html b/base/templates/registration/login.html index 4e0394e..1a78811 100644 --- a/base/templates/registration/login.html +++ b/base/templates/registration/login.html @@ -1,30 +1,9 @@ {% extends "base.html" %} +{% load form %} {% block content %}

Connexion

- {% for error in form.non_field_errors %}
{{ error }}
{% endfor %} -
- {% csrf_token %} - {% for field in form %} - {% if field.id_for_label %} - - {% endfor %} - -
- Créer mon compte -
-
- J'ai oublié mon mot de passe -
-
-{% endblock content %} +

+ Créer un compte +

+ {% form form submit="Se connecter" %} + {% endblock content %} diff --git a/base/templates/registration/password_change_form.html b/base/templates/registration/password_change_form.html deleted file mode 100644 index 25304fe..0000000 --- a/base/templates/registration/password_change_form.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "base.html" %} -{% load form %} -{% block content %} -

- Changer mon mot de passe -

- {% form form submit="Changer mon mot de passe" %} - {% endblock content %} diff --git a/base/templates/registration/password_reset_confirm.html b/base/templates/registration/password_reset_confirm.html deleted file mode 100644 index 2f7f317..0000000 --- a/base/templates/registration/password_reset_confirm.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "base.html" %} -{% load form %} -{% block content %} -

- Réinitialiser mon mot de passe -

- {% form form submit="Réinitialiser mon mot de passe" %} - {% endblock content %} diff --git a/base/templates/registration/password_reset_form.html b/base/templates/registration/password_reset_form.html deleted file mode 100644 index 2f7f317..0000000 --- a/base/templates/registration/password_reset_form.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "base.html" %} -{% load form %} -{% block content %} -

- Réinitialiser mon mot de passe -

- {% form form submit="Réinitialiser mon mot de passe" %} - {% endblock content %} diff --git a/base/templatetags/form.py b/base/templatetags/form.py index 66532a8..9ab40ce 100644 --- a/base/templatetags/form.py +++ b/base/templatetags/form.py @@ -4,13 +4,8 @@ register = template.Library() @register.inclusion_tag("base/form.html") -def form(f, **kwargs): +def form(form, **kwargs): return kwargs | { - "form": f, - "errors": f.errors, + "form": form, + "errors": form.errors, } - - -@register.inclusion_tag("base/inline_form.html") -def inline_form(f, **kwargs): - return form(f, **kwargs) diff --git a/base/urls.py b/base/urls.py index f240d91..2a6b2d5 100644 --- a/base/urls.py +++ b/base/urls.py @@ -1,30 +1,9 @@ -from django.contrib.auth import views as auth_views -from django.urls import path -from django.views.generic import TemplateView +from django.urls import include, path from . import views urlpatterns = [ path("", views.HomePageView.as_view(), name="index"), path("accounts/signup/", views.SignupView.as_view(), name="signup"), - path("accounts/login/", auth_views.LoginView.as_view(), name="login"), - path("accounts/logout/", auth_views.LogoutView.as_view(), name="logout"), - path( - "accounts/password_change/", - views.PasswordChangeView.as_view(), - name="password_change", - ), - path( - "accounts/password_reset/", - views.PasswordResetView.as_view(), - name="password_reset", - ), - path( - "accounts/reset///", - views.PasswordResetConfirmView.as_view(), - name="password_reset_confirm", - ), - path("accounts/settings/", views.AccountView.as_view(), name="account_settings"), - path("accounts/delete/", views.AccountDeleteView.as_view(), name="account_delete"), - path("legal/", TemplateView.as_view(template_name="privacy.html"), name="legal"), + path("accounts/", include("django.contrib.auth.urls")), ] diff --git a/base/views.py b/base/views.py index 7ad30e5..e3618a0 100644 --- a/base/views.py +++ b/base/views.py @@ -1,10 +1,7 @@ -from django.contrib.auth import views as auth_views from django.contrib.auth.models import User from django.contrib.messages.views import SuccessMessageMixin -from django.shortcuts import redirect -from django.urls import reverse_lazy from django.views.generic.base import TemplateView -from django.views.generic.edit import CreateView, DeleteView, UpdateView +from django.views.generic.edit import CreateView from . import forms @@ -12,53 +9,9 @@ from . import forms class HomePageView(TemplateView): template_name = "index.html" - def dispatch(self, request, *args, **kwargs): - if request.user.is_authenticated: - return redirect("home") - return super().dispatch(request, *args, **kwargs) - class SignupView(SuccessMessageMixin, CreateView): model = User form_class = forms.UserSignupForm - success_url = reverse_lazy("login") + success_url = "/" success_message = "Le compte %(username)s a été créé avec succès." - - -class PasswordChangeView(SuccessMessageMixin, auth_views.PasswordChangeView): - success_message = "Le mot de passe a été changé avec succès." - success_url = reverse_lazy("index") - - -class PasswordResetView(SuccessMessageMixin, auth_views.PasswordResetView): - success_message = "Un courriel a été envoyé avec les instructions pour réinitialiser votre mot de passe." - success_url = reverse_lazy("login") - - -class PasswordResetConfirmView( - SuccessMessageMixin, auth_views.PasswordResetConfirmView -): - success_message = "Le mot de passe a été réinitialisé avec succès." - success_url = reverse_lazy("login") - - -class AccountView(UpdateView): - model = User - fields = ["username", "email"] - success_url = reverse_lazy("index") - template_name = "auth/user_settings.html" - - def get_object(self, queryset=None): - if queryset is None: - queryset = self.get_queryset() - return queryset.get(pk=self.request.user.pk) - - -class AccountDeleteView(DeleteView): - model = User - success_url = reverse_lazy("index") - - def get_object(self, queryset=None): - if queryset is None: - queryset = self.get_queryset() - return queryset.get(pk=self.request.user.pk) diff --git a/compose.yaml b/compose.yaml deleted file mode 100644 index a6ee8b3..0000000 --- a/compose.yaml +++ /dev/null @@ -1,43 +0,0 @@ -services: - musik: - image: code.edgarpierre.fr/edpibu/musik - container_name: musik - restart: unless-stopped - ports: - - 35001:8000 - volumes: - - /docker/musik/config:/config:ro - - /data/srv/musik/static:/app/static - environment: - CELERY_BROKER_URL: amqp://rabbitmq:5672// - POSTGRES_HOST: postgres - env_file: stack.env - depends_on: - - rabbitmq - - celery - - postgres - - celery: - image: code.edgarpierre.fr/edpibu/musik - container_name: musik_celery - restart: unless-stopped - command: uv run celery -A musik worker - environment: - CELERY_BROKER_URL: amqp://rabbitmq:5672// - POSTGRES_HOST: postgres - env_file: stack.env - depends_on: - - rabbitmq - - rabbitmq: - image: rabbitmq - container_name: musik_rabbitmq - restart: unless-stopped - - postgres: - image: postgres:17 - container_name: musik_postgres - restart: unless-stopped - env_file: stack.env - volumes: - - /docker/musik/postgres:/var/lib/postgresql/data diff --git a/game/forms.py b/game/forms.py index a548455..99a18cf 100644 --- a/game/forms.py +++ b/game/forms.py @@ -3,16 +3,6 @@ from django import forms from . import models -class GroupForm(forms.ModelForm): - class Meta: - model = models.Group - fields = ["name"] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields["name"].widget.attrs["placeholder"] = self.fields["name"].label - - class GroupAddMembersForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -39,23 +29,3 @@ class MusikGameForm(forms.ModelForm): kwargs["initial"].setdefault("players", players) super().__init__(*args, **kwargs) self.fields["players"].queryset = players - - -class AnswerForm(forms.Form): - def __init__(self, *args, **kwargs): - game = kwargs.pop("game") - user = kwargs.pop("user") - super().__init__(*args, **kwargs) - - for music in game.musicgameorder_set.all(): - self.fields[f"answer-{music.order}"] = forms.ChoiceField( - choices=[("", "")] - + list(game.players.all().values_list("id", "username")), - required=False, - label=music.order, - initial=ma.answer.id - if (ma := music.musicgameanswer_set.filter(player=user).first()) - and ma.answer - else "", - disabled=game.over, - ) diff --git a/game/migrations/0015_groupleader.py b/game/migrations/0015_groupleader.py deleted file mode 100644 index a9e5dc2..0000000 --- a/game/migrations/0015_groupleader.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 5.2.3 on 2025-06-14 18:50 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("game", "0014_musikgame_playlist_loading"), - ] - - operations = [ - migrations.CreateModel( - name="GroupLeader", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("is_leader", models.BooleanField(default=False)), - ( - "member", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - to="game.group_members", - ), - ), - ], - ), - ] diff --git a/game/migrations/0016_alter_groupleader_member.py b/game/migrations/0016_alter_groupleader_member.py deleted file mode 100644 index 1561a34..0000000 --- a/game/migrations/0016_alter_groupleader_member.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 5.2.3 on 2025-06-14 18:50 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("game", "0015_groupleader"), - ] - - operations = [ - migrations.AlterField( - model_name="groupleader", - name="member", - field=models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - related_name="lead", - to="game.group_members", - ), - ), - ] diff --git a/game/migrations/0017_youtubecredentials_title.py b/game/migrations/0017_youtubecredentials_title.py deleted file mode 100644 index 89870e4..0000000 --- a/game/migrations/0017_youtubecredentials_title.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 5.2.3 on 2025-06-15 08:24 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("game", "0016_alter_groupleader_member"), - ] - - operations = [ - migrations.AddField( - model_name="youtubecredentials", - name="title", - field=models.CharField(blank=True), - ), - ] diff --git a/game/migrations/0018_musicgameanswer.py b/game/migrations/0018_musicgameanswer.py deleted file mode 100644 index 1f760be..0000000 --- a/game/migrations/0018_musicgameanswer.py +++ /dev/null @@ -1,60 +0,0 @@ -# Generated by Django 5.2.3 on 2025-06-15 10:36 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("game", "0017_youtubecredentials_title"), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name="MusicGameAnswer", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "answer", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="+", - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "game", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="game.musicgameorder", - ), - ), - ( - "player", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - ), - ), - ], - options={ - "ordering": ["game"], - "constraints": [ - models.UniqueConstraint( - fields=("game", "player"), name="unique_answer" - ) - ], - }, - ), - ] diff --git a/game/migrations/0019_alter_musikgame_options_musikgame_over.py b/game/migrations/0019_alter_musikgame_options_musikgame_over.py deleted file mode 100644 index f5e4489..0000000 --- a/game/migrations/0019_alter_musikgame_options_musikgame_over.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 5.2.3 on 2025-06-15 10:44 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("game", "0018_musicgameanswer"), - ] - - operations = [ - migrations.AlterModelOptions( - name="musikgame", - options={"ordering": ["-over", "-date"]}, - ), - migrations.AddField( - model_name="musikgame", - name="over", - field=models.BooleanField(default=False), - ), - ] diff --git a/game/migrations/0020_alter_musikgame_options_musicgameresults.py b/game/migrations/0020_alter_musikgame_options_musicgameresults.py deleted file mode 100644 index 3af5916..0000000 --- a/game/migrations/0020_alter_musikgame_options_musicgameresults.py +++ /dev/null @@ -1,55 +0,0 @@ -# Generated by Django 5.2.3 on 2025-06-15 11:23 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("game", "0019_alter_musikgame_options_musikgame_over"), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AlterModelOptions( - name="musikgame", - options={"ordering": ["over", "-date"]}, - ), - migrations.CreateModel( - name="MusicGameResults", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("score", models.PositiveIntegerField(default=0)), - ( - "game", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="game.musikgame" - ), - ), - ( - "player", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - ), - ), - ], - options={ - "ordering": ["-score"], - "constraints": [ - models.UniqueConstraint( - fields=("game", "player"), name="unique_result" - ) - ], - }, - ), - ] diff --git a/game/migrations/0021_alter_musicgameresults_score.py b/game/migrations/0021_alter_musicgameresults_score.py deleted file mode 100644 index 3166d4d..0000000 --- a/game/migrations/0021_alter_musicgameresults_score.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 5.2.3 on 2025-06-15 11:33 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("game", "0020_alter_musikgame_options_musicgameresults"), - ] - - operations = [ - migrations.AlterField( - model_name="musicgameresults", - name="score", - field=models.IntegerField(default=0), - ), - ] diff --git a/game/migrations/0022_musicgameorder_value.py b/game/migrations/0022_musicgameorder_value.py deleted file mode 100644 index 712353c..0000000 --- a/game/migrations/0022_musicgameorder_value.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 5.2.3 on 2025-06-15 12:49 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("game", "0021_alter_musicgameresults_score"), - ] - - operations = [ - migrations.AddField( - model_name="musicgameorder", - name="value", - field=models.PositiveIntegerField(default=0), - ), - ] diff --git a/game/migrations/0023_alter_musicvideo_options.py b/game/migrations/0023_alter_musicvideo_options.py deleted file mode 100644 index ee96adb..0000000 --- a/game/migrations/0023_alter_musicvideo_options.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 5.2.3 on 2025-06-15 14:19 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("game", "0022_musicgameorder_value"), - ] - - operations = [ - migrations.AlterModelOptions( - name="musicvideo", - options={"ordering": ["blacklisted", "-date_added"]}, - ), - ] diff --git a/game/models.py b/game/models.py index 153583f..88fe6cc 100644 --- a/game/models.py +++ b/game/models.py @@ -1,6 +1,5 @@ from django.contrib.auth.models import User from django.db import models -from django.db.models import F from django.db.models.functions import Lower from django.db.models.signals import post_delete, post_save from django.dispatch import receiver @@ -12,7 +11,6 @@ from . import tasks class YoutubeCredentials(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) credentials = models.JSONField() - title = models.CharField(blank=True) class Group(models.Model): @@ -25,33 +23,12 @@ class Group(models.Model): def get_absolute_url(self): return reverse("group_detail", kwargs={"pk": self.pk}) - def is_owner(self, user): - return user == self.owner - - def is_leader(self, user): - return ( - self.is_owner(user) - or self.members.through.objects.filter( - lead__is_leader=True, user=user - ).exists() - ) - - def is_member(self, user): - return self.is_owner(user) or self.members.filter(user=user).exists() - class Meta: constraints = [ models.UniqueConstraint(Lower("name"), "owner", name="unique_group_name") ] -class GroupLeader(models.Model): - member = models.OneToOneField( - Group.members.through, on_delete=models.CASCADE, related_name="lead" - ) - is_leader = models.BooleanField(default=False) - - class MusicVideo(models.Model): yt_id = models.CharField(max_length=16) title = models.CharField(blank=True) @@ -66,12 +43,6 @@ class MusicVideo(models.Model): fields=("yt_id", "owner", "group"), name="unique_music_in_group" ) ] - ordering = ["blacklisted", "-date_added"] - - -class GameManager(models.Manager): - def playing(self): - return self.filter(over=False) class MusikGame(models.Model): @@ -81,16 +52,10 @@ class MusikGame(models.Model): players = models.ManyToManyField(User, verbose_name="Joueurs") playlist = models.CharField(blank=True, verbose_name="Playlist YouTube") playlist_loading = models.BooleanField(default=False) - over = models.BooleanField(default=False) - - objects = GameManager() def get_absolute_url(self): return reverse("game_detail", kwargs={"pk": self.pk}) - class Meta: - ordering = ["over", "-date"] - @receiver(post_save, sender=MusikGame) def generateYoutubePlaylist(sender, instance, created, **kwargs): @@ -99,9 +64,6 @@ def generateYoutubePlaylist(sender, instance, created, **kwargs): if creds := instance.group.owner.youtubecredentials: tasks.generate_playlist.delay_on_commit(creds.credentials, instance.pk) - else: - instance.playlist_loading = False - instance.save() @receiver(post_delete, sender=MusikGame) @@ -118,14 +80,6 @@ class MusicGameOrder(models.Model): player = models.ForeignKey(User, on_delete=models.CASCADE) music_video = models.ForeignKey(MusicVideo, on_delete=models.CASCADE) order = models.PositiveIntegerField() - value = models.PositiveIntegerField(default=0) - - def update_value(self): - x = self.musicgameanswer_set.filter(game__player=F("answer")).count() - n = self.game.players.count() - n = max(3, n) - self.value = 1000 * 2 ** (-(x - 2) / (n - 2)) - self.save() class Meta: constraints = [ @@ -135,44 +89,3 @@ class MusicGameOrder(models.Model): models.UniqueConstraint(fields=("game", "order"), name="unique_order"), ] ordering = ["order"] - - -class AnswerManager(models.Manager): - def score(self, game, player): - qs = self.filter(game__game=game, player=player) - return ( - qs.exclude(game__player=player) - .filter(game__player=F("answer")) - .aggregate(score=models.Sum("game__value", default=0)) - .get("score") - - 500 - * qs.filter(game__player=player).exclude(game__player=F("answer")).count() - ) - - -class MusicGameAnswer(models.Model): - game = models.ForeignKey(MusicGameOrder, on_delete=models.CASCADE) - player = models.ForeignKey(User, on_delete=models.CASCADE) - answer = models.ForeignKey( - User, on_delete=models.SET_NULL, null=True, related_name="+" - ) - - objects = AnswerManager() - - class Meta: - constraints = [ - models.UniqueConstraint(fields=("game", "player"), name="unique_answer"), - ] - ordering = ["game"] - - -class MusicGameResults(models.Model): - game = models.ForeignKey(MusikGame, on_delete=models.CASCADE) - player = models.ForeignKey(User, on_delete=models.CASCADE) - score = models.IntegerField(default=0) - - class Meta: - constraints = [ - models.UniqueConstraint(fields=("game", "player"), name="unique_result") - ] - ordering = ["-score"] diff --git a/game/tasks.py b/game/tasks.py index 61937d0..22a7dc0 100644 --- a/game/tasks.py +++ b/game/tasks.py @@ -18,7 +18,7 @@ def generate_playlist(creds, game_pk): "description": "Playlist générée par Musik", }, "status": { - "privacyStatus": "unlisted", + "privacyStatus": "private", }, }, ) diff --git a/game/templates/game/group_confirm_delete.html b/game/templates/game/group_confirm_delete.html index 26c5411..f8d7e5e 100644 --- a/game/templates/game/group_confirm_delete.html +++ b/game/templates/game/group_confirm_delete.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% load form %} {% block content %}
diff --git a/game/templates/game/group_detail.html b/game/templates/game/group_detail.html index 90e9901..9931d35 100644 --- a/game/templates/game/group_detail.html +++ b/game/templates/game/group_detail.html @@ -9,8 +9,196 @@ {% endif %} {{ group.name }} - {% include "game/include/group_buttons.html" %} - {% include "game/include/group_games.html" %} - {% include "game/include/group_members.html" %} - {% include "game/include/group_musics.html" %} + {% if group.owner == user %} +

+

+ {% csrf_token %} +

+ Jouer +

+
+ Renommer + +
+
+

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

+ Parties +

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

+ Membres +

+
+ {% csrf_token %} + + + + {% if group.owner == user %}{% endif %} + + + + + + + + {% if group.owner == user %}{% endif %} + + + + + {% for member in members.all %} + + {% if group.owner == user %} + + {% endif %} + + + + + {% endfor %} + +
Membre + + + +
{{ group.owner }} + + {{ owner_count }}
+ + {{ member }}{{ member.count }}
+ {% if group.owner == user %} + + {% endif %} +
+ {% if group.owner == user %} +
+ {% csrf_token %} +
+ + +
+
+ {% endif %} +

+ Mes musiques {{ musics.count }} +

+
+ + Liste des musiques + +
+ {% csrf_token %} + + + + + + + + + + + {% for music in musics %} + + + + + + + {% empty %} + + + + {% endfor %} + +
MusiqueID + +
+ + {{ music.title }} + {{ music.yt_id }} + + +
Aucune musique.
+ {% if musics %} +
+ + +
+ {% endif %} +
+
+
+ {% csrf_token %} +
+ + +
+
{% endblock content %} diff --git a/game/templates/game/group_form.html b/game/templates/game/group_form.html index ef6da8c..72ae8d9 100644 --- a/game/templates/game/group_form.html +++ b/game/templates/game/group_form.html @@ -5,6 +5,9 @@

{{ group.name }}

+

+ Supprimer le groupe +

{% else %}

Créer un groupe

{% endif %} diff --git a/game/templates/game/home.html b/game/templates/game/home.html index b1d2399..9aee9fe 100644 --- a/game/templates/game/home.html +++ b/game/templates/game/home.html @@ -1,30 +1,26 @@ -{% extends "base.html" %} -{% load form %} -{% block content %} -

- Musik -

-

- Bienvenue {{ user.username }} ! -

-

- Mes groupes -

- {% 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 }} {{ group.owner }} -
-
- {% endfor %} +

Bienvenue {{ user.username }} !

+

+ Mes groupes +

+

+ Créer un groupe + {% if not user.youtubecredentials.credentials %} + Me connecter au compte Youtube {% endif %} - {% inline_form group_form action="group_create" submit="Créer" %} -{% endblock content %} +

+{% 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 }} {{ group.owner }} +
+
+ {% endfor %} +{% endif %} diff --git a/game/templates/game/include/game_results.html b/game/templates/game/include/game_results.html deleted file mode 100644 index ca2821c..0000000 --- a/game/templates/game/include/game_results.html +++ /dev/null @@ -1,49 +0,0 @@ -{% load game %} -{% if musikgame.over %} -

- Résultats -

-
- - Résultats - -
- - - - - - - {% for player in musikgame.musicgameresults_set.all %} - - - {% endfor %} - - - - {% for music in musikgame.musicgameorder_set.all %} - - - - - {% for player in musikgame.musicgameresults_set.all %} - {% answer player music %} - {% endfor %} - - {% endfor %} - -
- - - Musique - - Joueur - {{ player.player.username }} - {% if forloop.first %}{% endif %} - {{ player.score }} -
{{ music.order }} - {{ music.music_video.title }} - {{ music.player }}
-
-
-{% endif %} diff --git a/game/templates/game/include/group_buttons.html b/game/templates/game/include/group_buttons.html deleted file mode 100644 index 065545f..0000000 --- a/game/templates/game/include/group_buttons.html +++ /dev/null @@ -1,28 +0,0 @@ -{% if is_leader %} -

-

- {% csrf_token %} -

- Jouer -

-
- {% if is_owner %} - Renommer - {% endif %} - - {% if is_owner %} - - Supprimer - {% endif %} -
-
-

-{% endif %} diff --git a/game/templates/game/include/group_games.html b/game/templates/game/include/group_games.html deleted file mode 100644 index 15016fb..0000000 --- a/game/templates/game/include/group_games.html +++ /dev/null @@ -1,62 +0,0 @@ -{% load form youtube %} -{% if group.musikgame_set.exists %} - {% for game in group.musikgame_set.playing %} - -
{{ game.date }}
-
- {% endfor %} -

- Parties -

-
- {% csrf_token %} - - - {% if group.owner == user %}{% endif %} - - - - - - - {% for game in group.musikgame_set.all %} - - {% if group.owner == user %} - - {% endif %} - - - - - - {% endfor %} - -
- - Date - Playlists - Joueurs
- - - {% if game.over %} - - {% else %} - - {% endif %} - - {{ game.date }} - - {% if game.playlist %} - Playlist - {% endif %} - {{ game.players.all|join:", " }}
- {% if group.owner == user %} - - {% endif %} -
-{% endif %} diff --git a/game/templates/game/include/group_members.html b/game/templates/game/include/group_members.html deleted file mode 100644 index daf3059..0000000 --- a/game/templates/game/include/group_members.html +++ /dev/null @@ -1,81 +0,0 @@ -

- Membres -

-
- {% csrf_token %} - - - - {% if is_leader %}{% endif %} - - - - - - - - {% if is_leader %}{% endif %} - - - - - {% for member in members.all %} - - {% if is_leader %} - - {% endif %} - - - - - {% endfor %} - -
Membre - - - -
{{ group.owner }} - - {{ owner_count }}
- - {{ member.user }} - {% if is_owner %} - - {% elif member.lead.is_leader %} - - {% endif %} - {{ member.count }}
- {% if is_leader %} -
- {% if is_owner %} - - {% endif %} - -
- {% endif %} -
-{% if is_leader %} -
- {% csrf_token %} -
- - -
-
-{% endif %} diff --git a/game/templates/game/include/group_musics.html b/game/templates/game/include/group_musics.html deleted file mode 100644 index 56117cf..0000000 --- a/game/templates/game/include/group_musics.html +++ /dev/null @@ -1,66 +0,0 @@ -

- Mes musiques {{ musics.count }} -

-
- - Liste des musiques - -
- {% csrf_token %} -
- - - - - - - - - - - {% for music in musics %} - - - - - - - {% empty %} - - - - {% endfor %} - -
MusiqueID - -
- - {{ music.title }} - {{ music.yt_id }} - - -
Aucune musique.
-
- {% if musics %} -
- - -
- {% endif %} -
-
-
- {% csrf_token %} -
- - -
-
diff --git a/game/templates/game/musikgame_answer.html b/game/templates/game/musikgame_answer.html deleted file mode 100644 index f947802..0000000 --- a/game/templates/game/musikgame_answer.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends "base.html" %} -{% block content %} -

Mes réponses

-
- {% csrf_token %} - - - - - - - - - {% for field in form %} - - - - - {% endfor %} - -
- - Réponse
{{ field.label }}{{ field }}
- {% if not musikgame.over %}{% endif %} -
-{% endblock content %} diff --git a/game/templates/game/musikgame_detail.html b/game/templates/game/musikgame_detail.html index 66708c0..444c7f1 100644 --- a/game/templates/game/musikgame_detail.html +++ b/game/templates/game/musikgame_detail.html @@ -2,52 +2,20 @@ {% load youtube %} {% block content %}

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

-
- {% csrf_token %} -
- {% if musikgame.playlist or musikgame.playlist_loading %} - Playlist - {% endif %} - {% if musikgame.over %} - Mes réponses - {% else %} - Répondre - {% endif %} -
- {% if is_leader and not musikgame.over %} -
- -
- {% endif %} -
+ {% if musikgame.playlist or musikgame.playlist_loading %} +

+ Playlist +

+ {% endif %}

Joueurs

- {% if musikgame.over %} -
    - {% for player in musikgame.musicgameresults_set.all %} -
  1. - {{ player.player.username }} {{ player.score }} -
  2. - {% endfor %} -
- {% else %} -

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

- {% endif %} +

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

Musiques

@@ -63,5 +31,38 @@ {% endfor %} - {% include "game/include/game_results.html" %} +

+ 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/templates/game/musikgame_form.html b/game/templates/game/musikgame_form.html index 655723a..3e6d4ed 100644 --- a/game/templates/game/musikgame_form.html +++ b/game/templates/game/musikgame_form.html @@ -4,14 +4,5 @@

{{ group.name }}

-

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

{% form form %} {% endblock content %} diff --git a/game/templates/tags/game/answer.html b/game/templates/tags/game/answer.html deleted file mode 100644 index bfdeba5..0000000 --- a/game/templates/tags/game/answer.html +++ /dev/null @@ -1,16 +0,0 @@ -{% if empty %} - - - {{ score }} - -{% elif correct %} - {{ answer }} - - {{ score }} - -{% else %} - {{ answer }} - - {{ score }} - -{% endif %} diff --git a/game/templatetags/game.py b/game/templatetags/game.py deleted file mode 100644 index eec1bd0..0000000 --- a/game/templatetags/game.py +++ /dev/null @@ -1,30 +0,0 @@ -from django import template - -from .. import models - -register = template.Library() - - -@register.inclusion_tag("tags/game/answer.html") -def answer(player, music): - res = { - "answer": "", - "correct": False, - "score": 0, - "empty": False, - } - - answer = models.MusicGameAnswer.objects.filter( - player=player.player, game=music - ).first() - if answer: - res["answer"] = answer.answer - res["correct"] = answer.answer == music.player - if music.player == player.player: - res["score"] = 0 if res["correct"] else "−500" - else: - res["score"] = music.value if res["correct"] else 0 - else: - res["empty"] = True - - return res diff --git a/game/urls.py b/game/urls.py index 927dc79..763d277 100644 --- a/game/urls.py +++ b/game/urls.py @@ -3,7 +3,6 @@ from django.urls import path from . import views urlpatterns = [ - path("home", views.HomeView.as_view(), name="home"), path("group/create/", views.GroupCreateView.as_view(), name="group_create"), path( "group//update/", views.GroupUpdateView.as_view(), name="group_update" @@ -42,17 +41,11 @@ urlpatterns = [ views.GroupRemoveMemberView.as_view(), name="group_remove_member", ), - path( - "group//set_leader/", - views.GroupSetLead.as_view(), - name="group_set_leader", - ), path( "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_logout/", views.YoutubeLogoutView.as_view(), name="youtube_logout"), path( "youtube_callback/", views.YoutubeCallbackView.as_view(), @@ -63,10 +56,4 @@ urlpatterns = [ views.GroupClearBlacklistView.as_view(), name="group_clear_blacklist", ), - path( - "group/game//answer/", - views.GameAnswerView.as_view(), - name="game_answer", - ), - path("group/game//end/", views.GameEndView.as_view(), name="game_end"), ] diff --git a/game/views.py b/game/views.py index 45685aa..3c434a4 100644 --- a/game/views.py +++ b/game/views.py @@ -1,33 +1,21 @@ import random import google_auth_oauthlib -import googleapiclient from django.conf import settings from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.models import User from django.contrib.messages.views import SuccessMessageMixin -from django.core.exceptions import PermissionDenied from django.db import IntegrityError from django.db.models import Count, Q from django.shortcuts import get_object_or_404, redirect from django.views import View -from django.views.generic import TemplateView from django.views.generic.detail import DetailView, SingleObjectMixin from django.views.generic.edit import CreateView, DeleteView, UpdateView from . import forms, models, utils -class HomeView(LoginRequiredMixin, TemplateView): - template_name = "game/home.html" - - def get_context_data(self, **kwargs): - data = super().get_context_data(**kwargs) - data["group_form"] = forms.GroupForm() - return data - - class OwnerFilterMixin(LoginRequiredMixin): def get_queryset(self): return super().get_queryset().filter(owner=self.request.user) @@ -82,19 +70,14 @@ class GroupDetailView(MemberFilterMixin, GroupMixin, DetailView): .musicvideo_set.filter(owner=data["group"].owner, blacklisted=False) .count() ) - data["members"] = models.Group.members.through.objects.filter( - group=data["group"] - ).annotate( + data["members"] = data["group"].members.annotate( count=Count( - "user__musicvideo", + "musicvideo", filter=Q( - user__musicvideo__group=data["group"], - user__musicvideo__blacklisted=False, + musicvideo__group=data["group"], musicvideo__blacklisted=False ), ) ) - data["is_leader"] = data["group"].is_leader(self.request.user) - data["is_owner"] = data["group"].is_owner(self.request.user) return data @@ -104,59 +87,49 @@ class GroupAddMusicView(MemberFilterMixin, SingleObjectMixin, View): def post(self, request, pk): group = self.get_object() - ids = request.POST.get("yt_id") - for yt_id in ids.split(): - if not yt_id: - messages.add_message(request, messages.ERROR, "Aucun identifiant donné") - return redirect(group) - yt_id = utils.parse_musik(yt_id) + yt_id = request.POST.get("yt_id") + if not yt_id: + messages.add_message(request, messages.ERROR, "Aucun identifiant donné") + return redirect(group) + yt_id = utils.parse_musik(yt_id) - title = utils.get_yt_title(yt_id) - if not title: - messages.add_message( - request, messages.ERROR, f"Vidéo Youtube invalide : {yt_id}" - ) - else: - try: - group.musicvideo_set.create( - yt_id=yt_id, title=title, owner=request.user - ) - except IntegrityError: - messages.add_message( - request, messages.ERROR, f"Vidéo Youtube déjà ajoutée : {yt_id}" - ) + title = utils.get_yt_title(yt_id) + if not title: + messages.add_message( + request, messages.ERROR, f"Vidéo Youtube invalide : {yt_id}" + ) + return redirect(group) + try: + group.musicvideo_set.create(yt_id=yt_id, title=title, owner=request.user) + except IntegrityError: + messages.add_message( + request, messages.ERROR, f"Vidéo Youtube déjà ajoutée : {yt_id}" + ) - messages.add_message( - request, messages.SUCCESS, f"Vidéo Youtube ajoutée : {yt_id}" - ) + messages.add_message( + request, messages.SUCCESS, f"Vidéo Youtube ajoutée : {yt_id}" + ) return redirect(group) -class GroupAddMemberView(MemberFilterMixin, SingleObjectMixin, View): +class GroupAddMemberView(OwnerFilterMixin, SingleObjectMixin, View): model = models.Group def post(self, request, pk): group = self.get_object() - if not group.is_leader(request.user): - raise PermissionDenied() - usernames = request.POST.get("username") - for username in usernames.split(): - user = User.objects.filter(username=username).first() - if not user: - messages.add_message( - request, messages.ERROR, f"{username} n'existe pas." - ) - elif user == group.owner: - messages.add_message( - request, messages.WARNING, f"{user} est le propriétaire du groupe." - ) - elif user in group.members.all(): - messages.add_message( - request, messages.WARNING, f"{user} est déjà membre du groupe." - ) - else: - group.members.add(user) + 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) @@ -207,16 +180,14 @@ class GroupUnblacklistMusicView(MemberFilterMixin, SingleObjectMixin, View): return redirect(group) -class GroupRemoveMemberView(MemberFilterMixin, SingleObjectMixin, View): +class GroupRemoveMemberView(OwnerFilterMixin, SingleObjectMixin, View): model = models.Group def post(self, request, pk): group = self.get_object() - if not group.is_leader(request.user): - raise PermissionDenied() relations = models.Group.members.through.objects.filter( - group=group, pk__in=request.POST.getlist("member") + group=group, user__id__in=request.POST.getlist("member") ) if relations.count() == 0: messages.add_message(request, messages.INFO, "Aucun membre supprimé.") @@ -239,25 +210,6 @@ class GroupRemoveMemberView(MemberFilterMixin, SingleObjectMixin, View): return redirect(group) -class GroupSetLead(OwnerFilterMixin, SingleObjectMixin, View): - model = models.Group - - def post(self, request, pk): - group = self.get_object() - - members = models.Group.members.through.objects.filter(group=group) - for member in members.filter(pk__in=request.POST.getlist("leader")): - models.GroupLeader.objects.update_or_create( - member=member, defaults={"is_leader": True} - ) - for member in members.exclude(pk__in=request.POST.getlist("leader")): - models.GroupLeader.objects.update_or_create( - member=member, defaults={"is_leader": False} - ) - - return redirect(group) - - class GroupRemoveGameView(OwnerFilterMixin, SingleObjectMixin, View): model = models.Group @@ -298,15 +250,15 @@ class GameCreateView(LoginRequiredMixin, CreateView): def get_context_data(self, **kwargs): data = super().get_context_data(**kwargs) - data["group"] = get_object_or_404(models.Group, pk=self.kwargs["pk"]) - if not data["group"].is_leader(self.request.user): - raise PermissionDenied() + data["group"] = get_object_or_404( + models.Group, owner=self.request.user, pk=self.kwargs["pk"] + ) return data def form_valid(self, form): - group = get_object_or_404(models.Group, pk=self.kwargs["pk"]) - if not group.is_leader(self.request.user): - return super().form_invalid(form) + group = get_object_or_404( + models.Group, owner=self.request.user, pk=self.kwargs["pk"] + ) form.instance.group = group res = super().form_valid(form) players = [] @@ -327,12 +279,15 @@ class GameCreateView(LoginRequiredMixin, CreateView): pm_list = list(zip(players, musics)) random.shuffle(pm_list) for (player, music), order in zip(pm_list, range(1, len(pm_list) + 1)): + music.blacklisted = True + music.save() models.MusicGameOrder.objects.create( game=form.instance, player=player, music_video=music, order=order ) - form.instance.playlist_loading = True - form.instance.save() + if self.request.user.youtubecredentials: + form.instance.playlist_loading = True + form.instance.save() return res @@ -349,11 +304,21 @@ class GameDetailView(LoginRequiredMixin, DetailView): .distinct() ) - def get_context_data(self, **kwargs): - data = super().get_context_data(**kwargs) - data["is_leader"] = data["musikgame"].group.is_leader(self.request.user) - data["is_owner"] = data["musikgame"].group.is_owner(self.request.user) - return data + +class 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): @@ -370,18 +335,11 @@ class YoutubeCallbackView(LoginRequiredMixin, View): ["https://www.googleapis.com/auth/youtube.force-ssl"], state=state, ) - flow.redirect_uri = request.build_absolute_uri("/youtube_callback/") + flow.redirect_uri = "https://localhost/youtube_callback/" flow.fetch_token(code=request.GET.get("code")) credentials = flow.credentials - - yt_api = googleapiclient.discovery.build( - "youtube", "v3", credentials=credentials - ) - channel_request = yt_api.channels().list(part="snippet", mine=True) - res = channel_request.execute() - models.YoutubeCredentials.objects.update_or_create( user=request.user, defaults={ @@ -392,104 +350,19 @@ class YoutubeCallbackView(LoginRequiredMixin, View): "client_id": credentials.client_id, "client_secret": credentials.client_secret, "granted_scopes": credentials.granted_scopes, - }, - "title": res["items"][0]["snippet"]["title"], + } }, ) + messages.add_message(request, messages.SUCCESS, "Connexion à Youtube réussie.") return redirect("/") -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 = request.build_absolute_uri("/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 YoutubeLogoutView(LoginRequiredMixin, View): - def post(self, request): - request.user.youtubecredentials.delete() - return redirect("account_settings") - - -class GroupClearBlacklistView(MemberFilterMixin, SingleObjectMixin, View): +class GroupClearBlacklistView(OwnerFilterMixin, SingleObjectMixin, View): model = models.Group def post(self, request, pk): group = self.get_object() - if not group.is_leader(request.user): - raise PermissionDenied() group.musicvideo_set.filter(blacklisted=True).update(blacklisted=False) messages.add_message(request, messages.SUCCESS, "La blacklist a été effacée.") return redirect(group) - - -class GameAnswerView(LoginRequiredMixin, DetailView): - model = models.MusikGame - template_name = "game/musikgame_answer.html" - - def get_queryset(self): - return super().get_queryset().filter(players=self.request.user) - - def get_context_data(self, **kwargs): - return super().get_context_data(**kwargs) | { - "form": forms.AnswerForm(game=self.object, user=self.request.user) - } - - def post(self, request, pk): - game = self.get_object() - if game.over: - raise PermissionDenied() - for music in game.musicgameorder_set.all(): - answer = request.POST.get(f"answer-{music.order}") - if answer: - models.MusicGameAnswer.objects.update_or_create( - game=music, - player=request.user, - defaults={"answer": game.players.get(pk=answer)}, - ) - else: - models.MusicGameAnswer.objects.update_or_create( - game=music, - player=request.user, - defaults={"answer": None}, - ) - return redirect("game_answer", pk) - - -class GameEndView(LoginRequiredMixin, SingleObjectMixin, View): - model = models.MusikGame - - def get_queryset(self): - return super().get_queryset().filter(over=False) - - def post(self, request, pk): - game = self.get_object() - if not game.group.is_leader(request.user): - raise PermissionDenied() - game.over = True - models.MusicVideo.objects.filter(musicgameorder__game=game).update( - blacklisted=True - ) - - for go in game.musicgameorder_set.all(): - go.update_value() - - for player in game.players.all(): - score = player.musicgameanswer_set.score(game, player) - models.MusicGameResults.objects.create( - game=game, player=player, score=score - ) - - game.save() - return redirect("game_detail", pk) diff --git a/musik/context_processors.py b/musik/context_processors.py deleted file mode 100644 index de709cd..0000000 --- a/musik/context_processors.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.conf import settings - - -def version(request): - return {"VERSION": settings.VERSION} diff --git a/musik/settings.py b/musik/settings.py index 0f097d7..370562b 100644 --- a/musik/settings.py +++ b/musik/settings.py @@ -13,8 +13,6 @@ https://docs.djangoproject.com/en/5.2/ref/settings/ import os from pathlib import Path -VERSION = "0.4.4" - # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -23,19 +21,13 @@ BASE_DIR = Path(__file__).resolve().parent.parent # See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.getenv( - "MUSIK_SECRET_KEY", - "django-insecure-&z*xu$^w8btr(%1!y#+0a98)l_q*+*6z54611pi678mdpsar_=", -) +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 = "DEBUG" in os.environ +DEBUG = True -HOST = os.getenv("MUSIK_HOST", "localhost") -ALLOWED_HOSTS = [HOST] -CSRF_TRUSTED_ORIGINS = [f"https://{HOST}"] -USE_X_FORWARDED_HOST = True -SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") +ALLOWED_HOSTS = [] +CSRF_TRUSTED_ORIGINS = ["https://localhost"] # Application definition @@ -73,7 +65,6 @@ TEMPLATES = [ "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", - "musik.context_processors.version", ], }, }, @@ -87,14 +78,12 @@ WSGI_APPLICATION = "musik.wsgi.application" DATABASES = { "default": { - "ENGINE": "django.db.backends.postgresql", - "HOST": os.getenv("POSTGRES_HOST", "localhost"), - "NAME": os.getenv("POSTGRES_DB", os.getenv("POSTGRES_USER", "musik")), - "USER": os.getenv("POSTGRES_USER", "musik"), - "PASSWORD": os.getenv("POSTGRES_PASSWORD"), + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", } } + # Password validation # https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators @@ -142,14 +131,3 @@ LOGIN_REDIRECT_URL = "/" YOUTUBE_API_KEY = os.getenv("YOUTUBE_API_KEY", "") YOUTUBE_OAUTH_SECRETS = os.getenv("YOUTUBE_OAUTH_SECRETS", "") - -CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL", None) - -EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" -EMAIL_HOST = os.getenv("EMAIL_HOST") -EMAIL_PORT = os.getenv("EMAIL_PORT", 587) -EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER") -EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD") -EMAIL_USE_SSL = os.getenv("EMAIL_USE_SSL", False) -EMAIL_USE_TLS = os.getenv("EMAIL_USE_TLS", not EMAIL_USE_SSL) -DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", EMAIL_HOST_USER) diff --git a/pyproject.toml b/pyproject.toml index 0be7cd8..204fa92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "musik" -version = "0.4.4" +version = "0.1.0" description = "Le jeu de Musik." readme = "README.md" requires-python = ">=3.12" @@ -12,7 +12,6 @@ dependencies = [ "google-auth-httplib2>=0.2.0", "google-auth-oauthlib>=1.2.2", "gunicorn>=23.0.0", - "psycopg[binary]>=3.2.9", "requests>=2.32.4", ] diff --git a/stack.env b/stack.env deleted file mode 100644 index e69de29..0000000 diff --git a/uv.lock b/uv.lock index 21e0387..d4175cd 100644 --- a/uv.lock +++ b/uv.lock @@ -66,11 +66,11 @@ wheels = [ [[package]] name = "certifi" -version = "2025.6.15" +version = "2025.4.26" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } +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/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, + { 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]] @@ -423,7 +423,7 @@ wheels = [ [[package]] name = "musik" -version = "0.4.4" +version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "celery" }, @@ -433,7 +433,6 @@ dependencies = [ { name = "google-auth-httplib2" }, { name = "google-auth-oauthlib" }, { name = "gunicorn" }, - { name = "psycopg", extra = ["binary"] }, { name = "requests" }, ] @@ -452,7 +451,6 @@ requires-dist = [ { name = "google-auth-httplib2", specifier = ">=0.2.0" }, { name = "google-auth-oauthlib", specifier = ">=1.2.2" }, { name = "gunicorn", specifier = ">=23.0.0" }, - { name = "psycopg", extras = ["binary"], specifier = ">=3.2.9" }, { name = "requests", specifier = ">=2.32.4" }, ] @@ -561,53 +559,6 @@ wheels = [ { 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 = "psycopg" -version = "3.2.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, - { name = "tzdata", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/27/4a/93a6ab570a8d1a4ad171a1f4256e205ce48d828781312c0bbaff36380ecb/psycopg-3.2.9.tar.gz", hash = "sha256:2fbb46fcd17bc81f993f28c47f1ebea38d66ae97cc2dbc3cad73b37cefbff700", size = 158122, upload-time = "2025-05-13T16:11:15.533Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/b0/a73c195a56eb6b92e937a5ca58521a5c3346fb233345adc80fd3e2f542e2/psycopg-3.2.9-py3-none-any.whl", hash = "sha256:01a8dadccdaac2123c916208c96e06631641c0566b22005493f09663c7a8d3b6", size = 202705, upload-time = "2025-05-13T16:06:26.584Z" }, -] - -[package.optional-dependencies] -binary = [ - { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, -] - -[[package]] -name = "psycopg-binary" -version = "3.2.9" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/29/6f/ec9957e37a606cd7564412e03f41f1b3c3637a5be018d0849914cb06e674/psycopg_binary-3.2.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be7d650a434921a6b1ebe3fff324dbc2364393eb29d7672e638ce3e21076974e", size = 4022205, upload-time = "2025-05-13T16:07:48.195Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ba/497b8bea72b20a862ac95a94386967b745a472d9ddc88bc3f32d5d5f0d43/psycopg_binary-3.2.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6a76b4722a529390683c0304501f238b365a46b1e5fb6b7249dbc0ad6fea51a0", size = 4083795, upload-time = "2025-05-13T16:07:50.917Z" }, - { url = "https://files.pythonhosted.org/packages/42/07/af9503e8e8bdad3911fd88e10e6a29240f9feaa99f57d6fac4a18b16f5a0/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96a551e4683f1c307cfc3d9a05fec62c00a7264f320c9962a67a543e3ce0d8ff", size = 4655043, upload-time = "2025-05-13T16:07:54.857Z" }, - { url = "https://files.pythonhosted.org/packages/28/ed/aff8c9850df1648cc6a5cc7a381f11ee78d98a6b807edd4a5ae276ad60ad/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:61d0a6ceed8f08c75a395bc28cb648a81cf8dee75ba4650093ad1a24a51c8724", size = 4477972, upload-time = "2025-05-13T16:07:57.925Z" }, - { url = "https://files.pythonhosted.org/packages/5c/bd/8e9d1b77ec1a632818fe2f457c3a65af83c68710c4c162d6866947d08cc5/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad280bbd409bf598683dda82232f5215cfc5f2b1bf0854e409b4d0c44a113b1d", size = 4737516, upload-time = "2025-05-13T16:08:01.616Z" }, - { url = "https://files.pythonhosted.org/packages/46/ec/222238f774cd5a0881f3f3b18fb86daceae89cc410f91ef6a9fb4556f236/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76eddaf7fef1d0994e3d536ad48aa75034663d3a07f6f7e3e601105ae73aeff6", size = 4436160, upload-time = "2025-05-13T16:08:04.278Z" }, - { url = "https://files.pythonhosted.org/packages/37/78/af5af2a1b296eeca54ea7592cd19284739a844974c9747e516707e7b3b39/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:52e239cd66c4158e412318fbe028cd94b0ef21b0707f56dcb4bdc250ee58fd40", size = 3753518, upload-time = "2025-05-13T16:08:07.567Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ac/8a3ed39ea069402e9e6e6a2f79d81a71879708b31cc3454283314994b1ae/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:08bf9d5eabba160dd4f6ad247cf12f229cc19d2458511cab2eb9647f42fa6795", size = 3313598, upload-time = "2025-05-13T16:08:09.999Z" }, - { url = "https://files.pythonhosted.org/packages/da/43/26549af068347c808fbfe5f07d2fa8cef747cfff7c695136172991d2378b/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1b2cf018168cad87580e67bdde38ff5e51511112f1ce6ce9a8336871f465c19a", size = 3407289, upload-time = "2025-05-13T16:08:12.66Z" }, - { url = "https://files.pythonhosted.org/packages/67/55/ea8d227c77df8e8aec880ded398316735add8fda5eb4ff5cc96fac11e964/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:14f64d1ac6942ff089fc7e926440f7a5ced062e2ed0949d7d2d680dc5c00e2d4", size = 3472493, upload-time = "2025-05-13T16:08:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/3c/02/6ff2a5bc53c3cd653d281666728e29121149179c73fddefb1e437024c192/psycopg_binary-3.2.9-cp312-cp312-win_amd64.whl", hash = "sha256:7a838852e5afb6b4126f93eb409516a8c02a49b788f4df8b6469a40c2157fa21", size = 2927400, upload-time = "2025-05-13T16:08:18.652Z" }, - { url = "https://files.pythonhosted.org/packages/28/0b/f61ff4e9f23396aca674ed4d5c9a5b7323738021d5d72d36d8b865b3deaf/psycopg_binary-3.2.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:98bbe35b5ad24a782c7bf267596638d78aa0e87abc7837bdac5b2a2ab954179e", size = 4017127, upload-time = "2025-05-13T16:08:21.391Z" }, - { url = "https://files.pythonhosted.org/packages/bc/00/7e181fb1179fbfc24493738b61efd0453d4b70a0c4b12728e2b82db355fd/psycopg_binary-3.2.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:72691a1615ebb42da8b636c5ca9f2b71f266be9e172f66209a361c175b7842c5", size = 4080322, upload-time = "2025-05-13T16:08:24.049Z" }, - { url = "https://files.pythonhosted.org/packages/58/fd/94fc267c1d1392c4211e54ccb943be96ea4032e761573cf1047951887494/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25ab464bfba8c401f5536d5aa95f0ca1dd8257b5202eede04019b4415f491351", size = 4655097, upload-time = "2025-05-13T16:08:27.376Z" }, - { url = "https://files.pythonhosted.org/packages/41/17/31b3acf43de0b2ba83eac5878ff0dea5a608ca2a5c5dd48067999503a9de/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e8aeefebe752f46e3c4b769e53f1d4ad71208fe1150975ef7662c22cca80fab", size = 4482114, upload-time = "2025-05-13T16:08:30.781Z" }, - { url = "https://files.pythonhosted.org/packages/85/78/b4d75e5fd5a85e17f2beb977abbba3389d11a4536b116205846b0e1cf744/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7e4e4dd177a8665c9ce86bc9caae2ab3aa9360b7ce7ec01827ea1baea9ff748", size = 4737693, upload-time = "2025-05-13T16:08:34.625Z" }, - { url = "https://files.pythonhosted.org/packages/3b/95/7325a8550e3388b00b5e54f4ced5e7346b531eb4573bf054c3dbbfdc14fe/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fc2915949e5c1ea27a851f7a472a7da7d0a40d679f0a31e42f1022f3c562e87", size = 4437423, upload-time = "2025-05-13T16:08:37.444Z" }, - { url = "https://files.pythonhosted.org/packages/1a/db/cef77d08e59910d483df4ee6da8af51c03bb597f500f1fe818f0f3b925d3/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a1fa38a4687b14f517f049477178093c39c2a10fdcced21116f47c017516498f", size = 3758667, upload-time = "2025-05-13T16:08:40.116Z" }, - { url = "https://files.pythonhosted.org/packages/95/3e/252fcbffb47189aa84d723b54682e1bb6d05c8875fa50ce1ada914ae6e28/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5be8292d07a3ab828dc95b5ee6b69ca0a5b2e579a577b39671f4f5b47116dfd2", size = 3320576, upload-time = "2025-05-13T16:08:43.243Z" }, - { url = "https://files.pythonhosted.org/packages/1c/cd/9b5583936515d085a1bec32b45289ceb53b80d9ce1cea0fef4c782dc41a7/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:778588ca9897b6c6bab39b0d3034efff4c5438f5e3bd52fda3914175498202f9", size = 3411439, upload-time = "2025-05-13T16:08:47.321Z" }, - { url = "https://files.pythonhosted.org/packages/45/6b/6f1164ea1634c87956cdb6db759e0b8c5827f989ee3cdff0f5c70e8331f2/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f0d5b3af045a187aedbd7ed5fc513bd933a97aaff78e61c3745b330792c4345b", size = 3477477, upload-time = "2025-05-13T16:08:51.166Z" }, - { url = "https://files.pythonhosted.org/packages/7b/1d/bf54cfec79377929da600c16114f0da77a5f1670f45e0c3af9fcd36879bc/psycopg_binary-3.2.9-cp313-cp313-win_amd64.whl", hash = "sha256:2290bc146a1b6a9730350f695e8b670e1d1feb8446597bed0bbe7c3c30e0abcb", size = 2928009, upload-time = "2025-05-13T16:08:53.67Z" }, -] - [[package]] name = "pyasn1" version = "0.6.1" @@ -784,15 +735,6 @@ 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 = "typing-extensions" -version = "4.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, -] - [[package]] name = "tzdata" version = "2025.2"