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 %}
-
-
-
- {% endif %}
+ {% include "game/include/group_buttons.html" %}
{% if group.musikgame_set.exists %}
Parties
@@ -79,68 +56,7 @@
{% endif %}
{% endif %}
-
- Membres
-
-
- {% if group.owner == user %}
-
- {% 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 %}
+
+
+
+{% 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
+
+
+{% if is_leader %}
+
+{% 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" },