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:
parent
6cd9c0c841
commit
7409b4cd8f
10 changed files with 244 additions and 101 deletions
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
35
game/migrations/0015_groupleader.py
Normal file
35
game/migrations/0015_groupleader.py
Normal 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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
22
game/migrations/0016_alter_groupleader_member.py
Normal file
22
game/migrations/0016_alter_groupleader_member.py
Normal 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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
28
game/templates/game/include/group_buttons.html
Normal file
28
game/templates/game/include/group_buttons.html
Normal 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 %}
|
79
game/templates/game/include/group_members.html
Normal file
79
game/templates/game/include/group_members.html
Normal 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 %}
|
|
@ -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"
|
||||||
),
|
),
|
||||||
|
|
|
@ -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
2
uv.lock
generated
|
@ -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" },
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue