diff --git a/base/static/css/main.css b/base/static/css/main.css index 99b0900..c6146bc 100644 --- a/base/static/css/main.css +++ b/base/static/css/main.css @@ -122,3 +122,11 @@ footer { text-align: center; font-weight: 350; } + +td.c, th.c { + text-align: center; + + input { + margin: 0; + } +} diff --git a/game/migrations/0015_groupleader.py b/game/migrations/0015_groupleader.py new file mode 100644 index 0000000..a9e5dc2 --- /dev/null +++ b/game/migrations/0015_groupleader.py @@ -0,0 +1,35 @@ +# 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 new file mode 100644 index 0000000..1561a34 --- /dev/null +++ b/game/migrations/0016_alter_groupleader_member.py @@ -0,0 +1,22 @@ +# 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/models.py b/game/models.py index 88fe6cc..a3f104d 100644 --- a/game/models.py +++ b/game/models.py @@ -23,12 +23,33 @@ 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) diff --git a/game/templates/game/group_detail.html b/game/templates/game/group_detail.html index b6f48d8..d523f29 100644 --- a/game/templates/game/group_detail.html +++ b/game/templates/game/group_detail.html @@ -9,30 +9,7 @@ {% endif %} {{ group.name }} - {% if group.owner == user %} -

-

- {% csrf_token %} -

- Jouer -

-
- Renommer - - - Supprimer -
-
-

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

Parties @@ -79,68 +56,7 @@ {% 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 %} + {% include "game/include/group_members.html" %}

Mes musiques {{ musics.count }}

diff --git a/game/templates/game/include/group_buttons.html b/game/templates/game/include/group_buttons.html new file mode 100644 index 0000000..065545f --- /dev/null +++ b/game/templates/game/include/group_buttons.html @@ -0,0 +1,28 @@ +{% 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_members.html b/game/templates/game/include/group_members.html new file mode 100644 index 0000000..f49131c --- /dev/null +++ b/game/templates/game/include/group_members.html @@ -0,0 +1,79 @@ +

+ 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 %} + + {% endif %} + {{ member.count }}
+ {% if is_leader %} +
+ {% if is_owner %} + + {% endif %} + +
+ {% endif %} +
+{% if is_leader %} +
+ {% csrf_token %} +
+ + +
+
+{% endif %} diff --git a/game/urls.py b/game/urls.py index 763d277..2f4c3fa 100644 --- a/game/urls.py +++ b/game/urls.py @@ -41,6 +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" ), diff --git a/game/views.py b/game/views.py index f6632f0..ef10245 100644 --- a/game/views.py +++ b/game/views.py @@ -6,6 +6,7 @@ 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 @@ -70,14 +71,17 @@ class GroupDetailView(MemberFilterMixin, GroupMixin, DetailView): .musicvideo_set.filter(owner=data["group"].owner, blacklisted=False) .count() ) - data["members"] = data["group"].members.annotate( + data["members"] = data["group"].members.through.objects.annotate( count=Count( - "musicvideo", + "user__musicvideo", filter=Q( - musicvideo__group=data["group"], musicvideo__blacklisted=False + user__musicvideo__group=data["group"], + user__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 @@ -112,11 +116,13 @@ class GroupAddMusicView(MemberFilterMixin, SingleObjectMixin, View): return redirect(group) -class GroupAddMemberView(OwnerFilterMixin, SingleObjectMixin, View): +class GroupAddMemberView(MemberFilterMixin, SingleObjectMixin, View): model = models.Group def post(self, request, pk): group = self.get_object() + if not group.is_leader(request.user): + raise PermissionDenied() username = request.POST.get("username") user = User.objects.get(username=username) if user == group.owner: @@ -180,14 +186,16 @@ class GroupUnblacklistMusicView(MemberFilterMixin, SingleObjectMixin, View): return redirect(group) -class GroupRemoveMemberView(OwnerFilterMixin, SingleObjectMixin, View): +class GroupRemoveMemberView(MemberFilterMixin, 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, user__id__in=request.POST.getlist("member") + group=group, pk__in=request.POST.getlist("member") ) if relations.count() == 0: messages.add_message(request, messages.INFO, "Aucun membre supprimé.") @@ -210,6 +218,25 @@ class GroupRemoveMemberView(OwnerFilterMixin, 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 @@ -250,15 +277,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, owner=self.request.user, pk=self.kwargs["pk"] - ) + data["group"] = get_object_or_404(models.Group, pk=self.kwargs["pk"]) + if not data["group"].is_leader(self.request.user): + raise PermissionDenied() return data def form_valid(self, form): - group = get_object_or_404( - models.Group, owner=self.request.user, pk=self.kwargs["pk"] - ) + group = get_object_or_404(models.Group, pk=self.kwargs["pk"]) + if not group.is_leader(self.request.user): + return super().form_invalid(form) form.instance.group = group res = super().form_valid(form) players = [] @@ -285,7 +312,7 @@ class GameCreateView(LoginRequiredMixin, CreateView): game=form.instance, player=player, music_video=music, order=order ) - if self.request.user.youtubecredentials: + if models.YoutubeCredentials.objects.filter(user=self.request.user).exists(): form.instance.playlist_loading = True form.instance.save() return res @@ -358,11 +385,13 @@ class YoutubeLoginView(LoginRequiredMixin, View): return redirect(auth_url) -class GroupClearBlacklistView(OwnerFilterMixin, SingleObjectMixin, View): +class GroupClearBlacklistView(MemberFilterMixin, 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) diff --git a/uv.lock b/uv.lock index ce6f258..72a1f51 100644 --- a/uv.lock +++ b/uv.lock @@ -423,7 +423,7 @@ wheels = [ [[package]] name = "musik" -version = "0.1.0" +version = "0.1.1" source = { virtual = "." } dependencies = [ { name = "celery" },