Skip to content
Snippets Groups Projects
Commit 68540cf4 authored by Eliot Berriot's avatar Eliot Berriot
Browse files

Merge branch 'qa-017-round1' into 'develop'

Qa 017 round1

See merge request !421
parents 8bd1ed61 c641b8f5
No related branches found
No related tags found
No related merge requests found
Showing
with 103 additions and 33 deletions
from rest_framework import serializers from rest_framework import serializers
from funkwhale_api.activity import serializers as activity_serializers from funkwhale_api.activity import serializers as activity_serializers
......
from dynamic_preferences import types from dynamic_preferences import types
from dynamic_preferences.registries import global_preferences_registry from dynamic_preferences.registries import global_preferences_registry
......
...@@ -38,6 +38,23 @@ class PlaylistQuerySet(models.QuerySet): ...@@ -38,6 +38,23 @@ class PlaylistQuerySet(models.QuerySet):
) )
return self.prefetch_related(plt_prefetch) 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): class Playlist(models.Model):
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
...@@ -139,6 +156,23 @@ class PlaylistTrackQuerySet(models.QuerySet): ...@@ -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): class PlaylistTrack(models.Model):
track = models.ForeignKey( track = models.ForeignKey(
......
...@@ -11,10 +11,17 @@ from . import models ...@@ -11,10 +11,17 @@ from . import models
class PlaylistTrackSerializer(serializers.ModelSerializer): class PlaylistTrackSerializer(serializers.ModelSerializer):
track = TrackSerializer() track = TrackSerializer()
is_playable = serializers.SerializerMethodField()
class Meta: class Meta:
model = models.PlaylistTrack 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): class PlaylistTrackWriteSerializer(serializers.ModelSerializer):
...@@ -68,6 +75,7 @@ class PlaylistSerializer(serializers.ModelSerializer): ...@@ -68,6 +75,7 @@ class PlaylistSerializer(serializers.ModelSerializer):
duration = serializers.SerializerMethodField(read_only=True) duration = serializers.SerializerMethodField(read_only=True)
album_covers = serializers.SerializerMethodField(read_only=True) album_covers = serializers.SerializerMethodField(read_only=True)
user = UserBasicSerializer(read_only=True) user = UserBasicSerializer(read_only=True)
is_playable = serializers.SerializerMethodField()
class Meta: class Meta:
model = models.Playlist model = models.Playlist
...@@ -81,9 +89,16 @@ class PlaylistSerializer(serializers.ModelSerializer): ...@@ -81,9 +89,16 @@ class PlaylistSerializer(serializers.ModelSerializer):
"tracks_count", "tracks_count",
"album_covers", "album_covers",
"duration", "duration",
"is_playable",
) )
read_only_fields = ["id", "modification_date", "creation_date"] 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): def get_tracks_count(self, obj):
try: try:
return obj.tracks_count return obj.tracks_count
......
...@@ -6,7 +6,7 @@ from rest_framework.permissions import IsAuthenticatedOrReadOnly ...@@ -6,7 +6,7 @@ from rest_framework.permissions import IsAuthenticatedOrReadOnly
from rest_framework.response import Response from rest_framework.response import Response
from funkwhale_api.common import fields, permissions from funkwhale_api.common import fields, permissions
from funkwhale_api.music import utils as music_utils
from . import filters, models, serializers from . import filters, models, serializers
...@@ -74,7 +74,9 @@ class PlaylistViewSet( ...@@ -74,7 +74,9 @@ class PlaylistViewSet(
return Response(status=204) return Response(status=204)
def get_queryset(self): 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): def perform_create(self, serializer):
return serializer.save( return serializer.save(
...@@ -116,7 +118,7 @@ class PlaylistTrackViewSet( ...@@ -116,7 +118,7 @@ class PlaylistTrackViewSet(
lookup_field="playlist__privacy_level", lookup_field="playlist__privacy_level",
user_field="playlist__user", user_field="playlist__user",
) )
) ).annotate_playable_by_actor(music_utils.get_actor_from_request(self.request))
def perform_destroy(self, instance): def perform_destroy(self, instance):
instance.delete(update_indexes=True) instance.delete(update_indexes=True)
from __future__ import absolute_import from __future__ import absolute_import
import functools import functools
......
from django.db import models from django.db import models
from rest_framework import serializers from rest_framework import serializers
......
import pytest import pytest
import uuid import uuid
......
import funkwhale_api import funkwhale_api
from funkwhale_api.instance import nodeinfo from funkwhale_api.instance import nodeinfo
......
...@@ -122,3 +122,38 @@ def test_insert_many_honor_max_tracks(preferences, factories): ...@@ -122,3 +122,38 @@ def test_insert_many_honor_max_tracks(preferences, factories):
track = factories["music.Track"]() track = factories["music.Track"]()
with pytest.raises(exceptions.ValidationError): with pytest.raises(exceptions.ValidationError):
playlist.insert_many([track, track, track]) 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): ...@@ -25,6 +25,16 @@ def test_serializer_includes_tracks_count(factories, logged_in_api_client):
assert response.data["tracks_count"] == 1 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): def test_playlist_inherits_user_privacy(logged_in_api_client):
url = reverse("api:v1:playlists-list") url = reverse("api:v1:playlists-list")
user = logged_in_api_client.user user = logged_in_api_client.user
......
...@@ -13,14 +13,6 @@ ...@@ -13,14 +13,6 @@
<router-link class="ui item" to="/library/playlists" exact> <router-link class="ui item" to="/library/playlists" exact>
<translate>Playlists</translate> <translate>Playlists</translate>
</router-link> </router-link>
<div class="ui secondary right menu">
<router-link v-if="showImports" class="ui item" to="/library/import/launch" exact>
<translate>Import</translate>
</router-link>
<router-link v-if="showImports" class="ui item" to="/library/import/batches">
<translate>Import batches</translate>
</router-link>
</div>
</div> </div>
<router-view :key="$route.fullPath"></router-view> <router-view :key="$route.fullPath"></router-view>
</div> </div>
......
...@@ -5,8 +5,8 @@ ...@@ -5,8 +5,8 @@
<div class="content"> <div class="content">
<div class="header"> <div class="header">
<div class="right floated"> <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 :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 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" class="basic inline icon" :dropdown-only="true" :dropdown-icon-classes="['ellipsis', 'vertical', 'large', {disabled: playlist.tracks_count === 0}, 'grey']" :playlist="playlist"></play-button>
</div> </div>
<router-link :title="playlist.name" class="discrete link" :to="{name: 'library.playlists.detail', params: {id: playlist.id }}"> <router-link :title="playlist.name" class="discrete link" :to="{name: 'library.playlists.detail', params: {id: playlist.id }}">
{{ playlist.name | truncate(30) }} {{ playlist.name | truncate(30) }}
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
<div class="ui text loader"><translate>Loading usage data...</translate></div> <div class="ui text loader"><translate>Loading usage data...</translate></div>
</div> </div>
<div :class="['ui', {'success': progress < 60}, {'yellow': progress >= 60 && progress < 96}, {'error': progress >= 95}, 'progress']"> <div :class="['ui', {'success': progress < 60}, {'yellow': progress >= 60 && progress < 96}, {'error': progress >= 95}, 'progress']">
<div class="bar"> <div class="bar" :style="{width: `${progress}%`}">
<div class="progress">{{ progress }}%</div> <div class="progress">{{ progress }}%</div>
</div> </div>
<div class="label" v-if="quotaStatus"> <div class="label" v-if="quotaStatus">
...@@ -98,7 +98,6 @@ ...@@ -98,7 +98,6 @@
</template> </template>
<script> <script>
import axios from 'axios' import axios from 'axios'
import $ from 'jquery'
import {humanSize} from '@/filters' import {humanSize} from '@/filters'
import {compileTokens} from '@/search' import {compileTokens} from '@/search'
...@@ -145,12 +144,6 @@ export default { ...@@ -145,12 +144,6 @@ export default {
purgeErroredFiles () { purgeErroredFiles () {
this.purge('errored') this.purge('errored')
}, },
updateProgressBar () {
$(this.$el).find('.ui.progress').progress({
percent: this.progress,
showActivity: false
})
}
}, },
computed: { computed: {
progress () { progress () {
...@@ -159,11 +152,6 @@ export default { ...@@ -159,11 +152,6 @@ export default {
} }
return Math.min(parseInt(this.quotaStatus.current * 100 / this.quotaStatus.max), 100) return Math.min(parseInt(this.quotaStatus.current * 100 / this.quotaStatus.max), 100)
} }
},
watch: {
progress () {
this.updateProgressBar()
}
} }
} }
</script> </script>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment