Commit 34373d02 authored by Eliot Berriot's avatar Eliot Berriot

Fixed unplayable playlists

parent f595b852
......@@ -38,6 +38,23 @@ class PlaylistQuerySet(models.QuerySet):
)
return self.prefetch_related(plt_prefetch)
def annotate_playable_by_actor(self, actor):
plts = (
PlaylistTrack.objects.playable_by(actor)
.filter(playlist=models.OuterRef("id"))
.order_by("id")
.values("id")[:1]
)
subquery = models.Subquery(plts)
return self.annotate(is_playable_by_actor=subquery)
def playable_by(self, actor, include=True):
plts = PlaylistTrack.objects.playable_by(actor, include)
if include:
return self.filter(playlist_tracks__in=plts)
else:
return self.exclude(playlist_tracks__in=plts)
class Playlist(models.Model):
name = models.CharField(max_length=50)
......@@ -139,6 +156,23 @@ class PlaylistTrackQuerySet(models.QuerySet):
)
)
def annotate_playable_by_actor(self, actor):
tracks = (
music_models.Track.objects.playable_by(actor)
.filter(pk=models.OuterRef("track"))
.order_by("id")
.values("id")[:1]
)
subquery = models.Subquery(tracks)
return self.annotate(is_playable_by_actor=subquery)
def playable_by(self, actor, include=True):
tracks = music_models.Track.objects.playable_by(actor, include)
if include:
return self.filter(track__pk__in=tracks)
else:
return self.exclude(track__pk__in=tracks)
class PlaylistTrack(models.Model):
track = models.ForeignKey(
......
......@@ -11,10 +11,17 @@ from . import models
class PlaylistTrackSerializer(serializers.ModelSerializer):
track = TrackSerializer()
is_playable = serializers.SerializerMethodField()
class Meta:
model = models.PlaylistTrack
fields = ("id", "track", "playlist", "index", "creation_date")
fields = ("id", "track", "playlist", "index", "creation_date", "is_playable")
def get_is_playable(self, obj):
try:
return bool(obj.is_playable_by_actor)
except AttributeError:
return None
class PlaylistTrackWriteSerializer(serializers.ModelSerializer):
......@@ -68,6 +75,7 @@ class PlaylistSerializer(serializers.ModelSerializer):
duration = serializers.SerializerMethodField(read_only=True)
album_covers = serializers.SerializerMethodField(read_only=True)
user = UserBasicSerializer(read_only=True)
is_playable = serializers.SerializerMethodField()
class Meta:
model = models.Playlist
......@@ -81,9 +89,16 @@ class PlaylistSerializer(serializers.ModelSerializer):
"tracks_count",
"album_covers",
"duration",
"is_playable",
)
read_only_fields = ["id", "modification_date", "creation_date"]
def get_is_playable(self, obj):
try:
return bool(obj.is_playable_by_actor)
except AttributeError:
return None
def get_tracks_count(self, obj):
try:
return obj.tracks_count
......
......@@ -6,7 +6,7 @@ from rest_framework.permissions import IsAuthenticatedOrReadOnly
from rest_framework.response import Response
from funkwhale_api.common import fields, permissions
from funkwhale_api.music import utils as music_utils
from . import filters, models, serializers
......@@ -74,7 +74,9 @@ class PlaylistViewSet(
return Response(status=204)
def get_queryset(self):
return self.queryset.filter(fields.privacy_level_query(self.request.user))
return self.queryset.filter(
fields.privacy_level_query(self.request.user)
).annotate_playable_by_actor(music_utils.get_actor_from_request(self.request))
def perform_create(self, serializer):
return serializer.save(
......@@ -116,7 +118,7 @@ class PlaylistTrackViewSet(
lookup_field="playlist__privacy_level",
user_field="playlist__user",
)
)
).annotate_playable_by_actor(music_utils.get_actor_from_request(self.request))
def perform_destroy(self, instance):
instance.delete(update_indexes=True)
......@@ -122,3 +122,38 @@ def test_insert_many_honor_max_tracks(preferences, factories):
track = factories["music.Track"]()
with pytest.raises(exceptions.ValidationError):
playlist.insert_many([track, track, track])
@pytest.mark.parametrize(
"privacy_level,expected", [("me", False), ("instance", False), ("everyone", True)]
)
def test_playlist_track_playable_by_anonymous(privacy_level, expected, factories):
plt = factories["playlists.PlaylistTrack"]()
track = plt.track
factories["music.Upload"](
track=track, library__privacy_level=privacy_level, import_status="finished"
)
queryset = plt.__class__.objects.playable_by(None).annotate_playable_by_actor(None)
match = plt in list(queryset)
assert match is expected
if expected:
assert bool(queryset.first().is_playable_by_actor) is expected
@pytest.mark.parametrize(
"privacy_level,expected", [("me", False), ("instance", False), ("everyone", True)]
)
def test_playlist_playable_by_anonymous(privacy_level, expected, factories):
plt = factories["playlists.PlaylistTrack"]()
playlist = plt.playlist
track = plt.track
factories["music.Upload"](
track=track, library__privacy_level=privacy_level, import_status="finished"
)
queryset = playlist.__class__.objects.playable_by(None).annotate_playable_by_actor(
None
)
match = playlist in list(queryset)
assert match is expected
if expected:
assert bool(queryset.first().is_playable_by_actor) is expected
......@@ -25,6 +25,16 @@ def test_serializer_includes_tracks_count(factories, logged_in_api_client):
assert response.data["tracks_count"] == 1
def test_serializer_includes_is_playable(factories, logged_in_api_client):
playlist = factories["playlists.Playlist"]()
factories["playlists.PlaylistTrack"](playlist=playlist)
url = reverse("api:v1:playlists-detail", kwargs={"pk": playlist.pk})
response = logged_in_api_client.get(url)
assert response.data["is_playable"] is False
def test_playlist_inherits_user_privacy(logged_in_api_client):
url = reverse("api:v1:playlists-list")
user = logged_in_api_client.user
......
......@@ -5,8 +5,8 @@
<div class="content">
<div class="header">
<div class="right floated">
<play-button :icon-only="true" class="ui inline" :button-classes="['ui', 'circular', 'large', {orange: playlist.tracks_count > 0}, 'icon', 'button', {disabled: playlist.tracks_count === 0}]" :playlist="playlist"></play-button>
<play-button class="basic inline icon" :dropdown-only="true" :dropdown-icon-classes="['ellipsis', 'vertical', 'large', {disabled: playlist.tracks_count === 0}, 'grey']" :playlist="playlist"></play-button>
<play-button :is-playable="playlist.is_playable" :icon-only="true" class="ui inline" :button-classes="['ui', 'circular', 'large', {orange: playlist.tracks_count > 0}, 'icon', 'button', {disabled: playlist.tracks_count === 0}]" :playlist="playlist"></play-button>
<play-button :is-playable="playlist.is_playable" class="basic inline icon" :dropdown-only="true" :dropdown-icon-classes="['ellipsis', 'vertical', 'large', {disabled: playlist.tracks_count === 0}, 'grey']" :playlist="playlist"></play-button>
</div>
<router-link :title="playlist.name" class="discrete link" :to="{name: 'library.playlists.detail', params: {id: playlist.id }}">
{{ playlist.name | truncate(30) }}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment