Add group leader functionality and update group member management

- Create GroupLeader model and migration
- Alter GroupLeader member field to include related_name
- Implement is_leader and is_owner methods in Group model
- Update GroupDetailView to pass leader and owner status to template
- Refactor group buttons and members display into separate templates
- Add view and URL for setting group leaders
- Update permissions for adding/removing members and clearing blacklist
- Bump version to 0.1.1 in uv.lock
This commit is contained in:
Edgar P. Burkhart 2025-06-14 21:58:33 +02:00
parent 6cd9c0c841
commit 7409b4cd8f
Signed by: edpibu
GPG key ID: 9833D3C5A25BD227
10 changed files with 244 additions and 101 deletions

View file

@ -122,3 +122,11 @@ footer {
text-align: center; text-align: center;
font-weight: 350; font-weight: 350;
} }
td.c, th.c {
text-align: center;
input {
margin: 0;
}
}

View file

@ -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",
),
),
],
),
]

View file

@ -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",
),
),
]

View file

@ -23,12 +23,33 @@ class Group(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return reverse("group_detail", kwargs={"pk": self.pk}) 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: class Meta:
constraints = [ constraints = [
models.UniqueConstraint(Lower("name"), "owner", name="unique_group_name") 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): class MusicVideo(models.Model):
yt_id = models.CharField(max_length=16) yt_id = models.CharField(max_length=16)
title = models.CharField(blank=True) title = models.CharField(blank=True)

View file

@ -9,30 +9,7 @@
{% endif %} {% endif %}
{{ group.name }} {{ group.name }}
</h1> </h1>
{% if group.owner == user %} {% include "game/include/group_buttons.html" %}
<p>
<form method="post">
{% csrf_token %}
<p>
<a href="{% url "start_game" pk=group.pk %}" role="button"><i class="ri-play-fill"></i> Jouer</a>
</p>
<div role="group">
<a href="{% url "group_update" pk=group.pk %}"
class="secondary"
role="button"><i class="ri-edit-line"></i> Renommer</a>
<button type="submit"
class="secondary"
formaction="{% url "group_clear_blacklist" pk=group.pk %}">
<i class="ri-history-fill"></i> Effacer la blacklist
</button>
<a href="{% url "group_delete" group.pk %}"
role="button"
class="secondary">
<i class="ri-delete-bin-fill"></i> Supprimer</a>
</div>
</form>
</p>
{% endif %}
{% if group.musikgame_set.exists %} {% if group.musikgame_set.exists %}
<h2> <h2>
<i class="ri-play-circle-fill"></i> Parties <i class="ri-play-circle-fill"></i> Parties
@ -79,68 +56,7 @@
{% endif %} {% endif %}
</form> </form>
{% endif %} {% endif %}
<h2> {% include "game/include/group_members.html" %}
<i class="ri-group-2-fill"></i> Membres
</h2>
<form method="post">
{% csrf_token %}
<table>
<thead>
<tr>
{% if group.owner == user %}<th></th>{% endif %}
<th>Membre</th>
<th>
<i class="ri-vip-crown-fill"></i>
</th>
<th>
<i class="ri-mv-line"></i>
</th>
</tr>
</thead>
<tbody>
<tr>
{% if group.owner == user %}<td></td>{% endif %}
<td>{{ group.owner }}</td>
<td>
<i class="ri-vip-crown-fill owner"></i>
</td>
<td>{{ owner_count }}</td>
</tr>
{% for member in members.all %}
<tr>
{% if group.owner == user %}
<td>
<input type="checkbox" name="member" value="{{ member.pk }}">
</td>
{% endif %}
<td>{{ member }}</td>
<td></td>
<td>{{ member.count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if group.owner == user %}
<button type="submit"
class="secondary"
formaction="{% url "group_remove_member" pk=group.pk %}">
<i class="ri-delete-bin-fill"></i> Supprimer les membres sélectionnés
</button>
{% endif %}
</form>
{% if group.owner == user %}
<form method="post" action="{% url "group_add_member" pk=group.pk %}">
{% csrf_token %}
<fieldset role="group">
<input type="string"
name="username"
id="username"
placeholder="Membre"
required>
<button type="submit">Ajouter</button>
</fieldset>
</form>
{% endif %}
<h2> <h2>
<i class="ri-music-2-fill"></i> Mes musiques <span class="music-count">{{ musics.count }}</span> <i class="ri-music-2-fill"></i> Mes musiques <span class="music-count">{{ musics.count }}</span>
</h2> </h2>

View file

@ -0,0 +1,28 @@
{% if is_leader %}
<p>
<form method="post">
{% csrf_token %}
<p>
<a href="{% url "start_game" pk=group.pk %}" role="button"><i class="ri-play-fill"></i> Jouer</a>
</p>
<div role="group">
{% if is_owner %}
<a href="{% url "group_update" pk=group.pk %}"
class="secondary"
role="button"><i class="ri-edit-line"></i> Renommer</a>
{% endif %}
<button type="submit"
class="secondary"
formaction="{% url "group_clear_blacklist" pk=group.pk %}">
<i class="ri-history-fill"></i> Effacer la blacklist
</button>
{% if is_owner %}
<a href="{% url "group_delete" group.pk %}"
role="button"
class="secondary">
<i class="ri-delete-bin-fill"></i> Supprimer</a>
{% endif %}
</div>
</form>
</p>
{% endif %}

View file

@ -0,0 +1,79 @@
<h2>
<i class="ri-group-2-fill"></i> Membres
</h2>
<form method="post">
{% csrf_token %}
<table>
<thead>
<tr>
{% if is_leader %}<th></th>{% endif %}
<th>Membre</th>
<th class="c">
<i class="ri-vip-crown-fill"></i>
</th>
<th>
<i class="ri-mv-line"></i>
</th>
</tr>
</thead>
<tbody>
<tr>
{% if is_leader %}<td></td>{% endif %}
<td>{{ group.owner }}</td>
<td class="c">
<i class="ri-vip-crown-fill owner"></i>
</td>
<td>{{ owner_count }}</td>
</tr>
{% for member in members.all %}
<tr>
{% if is_leader %}
<td>
<input type="checkbox" name="member" value="{{ member.pk }}">
</td>
{% endif %}
<td>{{ member.user }}</td>
<td class="c">
{% if is_owner %}
<input type="checkbox"
name="leader"
value="{{ member.pk }}"
role="switch"
{% if member.lead.is_leader %}checked{% endif %}>
{% endif %}
</td>
<td>{{ member.count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if is_leader %}
<div role="group">
{% if is_owner %}
<button type="submit"
class="secondary"
formaction="{% url "group_set_leader" pk=group.pk %}">
<i class="ri-vip-crown-fill"></i> Mettre à jour meneurs
</button>
{% endif %}
<button type="submit"
class="secondary"
formaction="{% url "group_remove_member" pk=group.pk %}">
<i class="ri-delete-bin-fill"></i> Supprimer
</button>
</div>
{% endif %}
</form>
{% if is_leader %}
<form method="post" action="{% url "group_add_member" pk=group.pk %}">
{% csrf_token %}
<fieldset role="group">
<input type="string"
name="username"
id="username"
placeholder="Membre"
required>
<button type="submit">Ajouter</button>
</fieldset>
</form>
{% endif %}

View file

@ -41,6 +41,11 @@ urlpatterns = [
views.GroupRemoveMemberView.as_view(), views.GroupRemoveMemberView.as_view(),
name="group_remove_member", name="group_remove_member",
), ),
path(
"group/<int:pk>/set_leader/",
views.GroupSetLead.as_view(),
name="group_set_leader",
),
path( path(
"group/<int:pk>/start_game/", views.GameCreateView.as_view(), name="start_game" "group/<int:pk>/start_game/", views.GameCreateView.as_view(), name="start_game"
), ),

View file

@ -6,6 +6,7 @@ from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.core.exceptions import PermissionDenied
from django.db import IntegrityError from django.db import IntegrityError
from django.db.models import Count, Q from django.db.models import Count, Q
from django.shortcuts import get_object_or_404, redirect 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) .musicvideo_set.filter(owner=data["group"].owner, blacklisted=False)
.count() .count()
) )
data["members"] = data["group"].members.annotate( data["members"] = data["group"].members.through.objects.annotate(
count=Count( count=Count(
"musicvideo", "user__musicvideo",
filter=Q( 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 return data
@ -112,11 +116,13 @@ class GroupAddMusicView(MemberFilterMixin, SingleObjectMixin, View):
return redirect(group) return redirect(group)
class GroupAddMemberView(OwnerFilterMixin, SingleObjectMixin, View): class GroupAddMemberView(MemberFilterMixin, SingleObjectMixin, View):
model = models.Group model = models.Group
def post(self, request, pk): def post(self, request, pk):
group = self.get_object() group = self.get_object()
if not group.is_leader(request.user):
raise PermissionDenied()
username = request.POST.get("username") username = request.POST.get("username")
user = User.objects.get(username=username) user = User.objects.get(username=username)
if user == group.owner: if user == group.owner:
@ -180,14 +186,16 @@ class GroupUnblacklistMusicView(MemberFilterMixin, SingleObjectMixin, View):
return redirect(group) return redirect(group)
class GroupRemoveMemberView(OwnerFilterMixin, SingleObjectMixin, View): class GroupRemoveMemberView(MemberFilterMixin, SingleObjectMixin, View):
model = models.Group model = models.Group
def post(self, request, pk): def post(self, request, pk):
group = self.get_object() group = self.get_object()
if not group.is_leader(request.user):
raise PermissionDenied()
relations = models.Group.members.through.objects.filter( 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: if relations.count() == 0:
messages.add_message(request, messages.INFO, "Aucun membre supprimé.") messages.add_message(request, messages.INFO, "Aucun membre supprimé.")
@ -210,6 +218,25 @@ class GroupRemoveMemberView(OwnerFilterMixin, SingleObjectMixin, View):
return redirect(group) 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): class GroupRemoveGameView(OwnerFilterMixin, SingleObjectMixin, View):
model = models.Group model = models.Group
@ -250,15 +277,15 @@ class GameCreateView(LoginRequiredMixin, CreateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs) data = super().get_context_data(**kwargs)
data["group"] = get_object_or_404( data["group"] = get_object_or_404(models.Group, pk=self.kwargs["pk"])
models.Group, owner=self.request.user, pk=self.kwargs["pk"] if not data["group"].is_leader(self.request.user):
) raise PermissionDenied()
return data return data
def form_valid(self, form): def form_valid(self, form):
group = get_object_or_404( group = get_object_or_404(models.Group, pk=self.kwargs["pk"])
models.Group, owner=self.request.user, pk=self.kwargs["pk"] if not group.is_leader(self.request.user):
) return super().form_invalid(form)
form.instance.group = group form.instance.group = group
res = super().form_valid(form) res = super().form_valid(form)
players = [] players = []
@ -285,7 +312,7 @@ class GameCreateView(LoginRequiredMixin, CreateView):
game=form.instance, player=player, music_video=music, order=order 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.playlist_loading = True
form.instance.save() form.instance.save()
return res return res
@ -358,11 +385,13 @@ class YoutubeLoginView(LoginRequiredMixin, View):
return redirect(auth_url) return redirect(auth_url)
class GroupClearBlacklistView(OwnerFilterMixin, SingleObjectMixin, View): class GroupClearBlacklistView(MemberFilterMixin, SingleObjectMixin, View):
model = models.Group model = models.Group
def post(self, request, pk): def post(self, request, pk):
group = self.get_object() group = self.get_object()
if not group.is_leader(request.user):
raise PermissionDenied()
group.musicvideo_set.filter(blacklisted=True).update(blacklisted=False) group.musicvideo_set.filter(blacklisted=True).update(blacklisted=False)
messages.add_message(request, messages.SUCCESS, "La blacklist a été effacée.") messages.add_message(request, messages.SUCCESS, "La blacklist a été effacée.")
return redirect(group) return redirect(group)

2
uv.lock generated
View file

@ -423,7 +423,7 @@ wheels = [
[[package]] [[package]]
name = "musik" name = "musik"
version = "0.1.0" version = "0.1.1"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "celery" }, { name = "celery" },