Merge branch 'master' into develop

This commit is contained in:
Agate 2020-07-21 14:47:07 +02:00
commit 21f147c0c4
15 changed files with 91 additions and 37 deletions

View File

@ -136,7 +136,7 @@ test_api:
- branches - branches
before_script: before_script:
- apk add make git gcc python3-dev musl-dev - apk add make git gcc python3-dev musl-dev
# - apk add postgresql-dev py3-psycopg2 libldap libffi-dev make zlib-dev jpeg-dev openldap-dev - apk add postgresql-dev py3-psycopg2 libldap libffi-dev make zlib-dev jpeg-dev openldap-dev
- cd api - cd api
- pip3 install -r requirements/base.txt - pip3 install -r requirements/base.txt
- pip3 install -r requirements/local.txt - pip3 install -r requirements/local.txt

View File

@ -7,6 +7,7 @@ from django_filters import rest_framework as filters
from . import fields from . import fields
from . import models from . import models
from . import search from . import search
from . import utils
class NoneObject(object): class NoneObject(object):
@ -169,13 +170,17 @@ class MutationFilter(filters.FilterSet):
fields = ["is_approved", "is_applied", "type"] fields = ["is_approved", "is_applied", "type"]
class EmptyQuerySet(ValueError):
pass
class ActorScopeFilter(filters.CharFilter): class ActorScopeFilter(filters.CharFilter):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.actor_field = kwargs.pop("actor_field") self.actor_field = kwargs.pop("actor_field")
self.library_field = kwargs.pop("library_field", None)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def filter(self, queryset, value): def filter(self, queryset, value):
from funkwhale_api.federation import models as federation_models
if not value: if not value:
return queryset return queryset
@ -185,35 +190,57 @@ class ActorScopeFilter(filters.CharFilter):
return queryset.none() return queryset.none()
user = getattr(request, "user", None) user = getattr(request, "user", None)
qs = queryset actor = getattr(user, "actor", None)
if value.lower() == "me": scopes = [v.strip().lower() for v in value.split(",")]
qs = self.filter_me(user=user, queryset=queryset) query = None
elif value.lower() == "all": for scope in scopes:
return queryset try:
elif value.lower().startswith("actor:"): right_query = self.get_query(scope, user, actor)
full_username = value.split("actor:", 1)[1] except ValueError:
return queryset.none()
query = utils.join_queries_or(query, right_query)
return queryset.filter(query).distinct()
def get_query(self, scope, user, actor):
from funkwhale_api.federation import models as federation_models
if scope == "me":
return self.filter_me(actor)
elif scope == "all":
return Q(pk__gte=0)
elif scope == "subscribed":
if not actor or self.library_field is None:
raise EmptyQuerySet()
followed_libraries = federation_models.LibraryFollow.objects.filter(
approved=True, actor=user.actor
).values_list("target_id", flat=True)
if not self.library_field:
predicate = "pk__in"
else:
predicate = "{}__in".format(self.library_field)
return Q(**{predicate: followed_libraries})
elif scope.startswith("actor:"):
full_username = scope.split("actor:", 1)[1]
username, domain = full_username.split("@") username, domain = full_username.split("@")
try: try:
actor = federation_models.Actor.objects.get( actor = federation_models.Actor.objects.get(
preferred_username=username, domain_id=domain, preferred_username=username, domain_id=domain,
) )
except federation_models.Actor.DoesNotExist: except federation_models.Actor.DoesNotExist:
return queryset.none() raise EmptyQuerySet()
return queryset.filter(**{self.actor_field: actor}) return Q(**{self.actor_field: actor})
elif value.lower().startswith("domain:"): elif scope.startswith("domain:"):
domain = value.split("domain:", 1)[1] domain = scope.split("domain:", 1)[1]
return queryset.filter(**{"{}__domain_id".format(self.actor_field): domain}) return Q(**{"{}__domain_id".format(self.actor_field): domain})
else: else:
return queryset.none() raise EmptyQuerySet()
if self.distinct: def filter_me(self, actor):
qs = qs.distinct()
return qs
def filter_me(self, user, queryset):
actor = getattr(user, "actor", None)
if not actor: if not actor:
return queryset.none() raise EmptyQuerySet()
return queryset.filter(**{self.actor_field: actor}) return Q(**{self.actor_field: actor})

View File

@ -146,7 +146,9 @@ class TrackFilter(
tag = TAG_FILTER tag = TAG_FILTER
id = common_filters.MultipleQueryFilter(coerce=int) id = common_filters.MultipleQueryFilter(coerce=int)
scope = common_filters.ActorScopeFilter( scope = common_filters.ActorScopeFilter(
actor_field="uploads__library__actor", distinct=True actor_field="uploads__library__actor",
library_field="uploads__library",
distinct=True,
) )
artist = filters.ModelChoiceFilter( artist = filters.ModelChoiceFilter(
field_name="_", method="filter_artist", queryset=models.Artist.objects.all() field_name="_", method="filter_artist", queryset=models.Artist.objects.all()

View File

@ -177,6 +177,8 @@ def serialize_artist_simple(artist):
if artist.attachment_cover if artist.attachment_cover
else None else None
) )
if "channel" in artist._state.fields_cache and artist.get_channel():
data["channel"] = str(artist.channel.uuid)
if getattr(artist, "_tracks_count", None) is not None: if getattr(artist, "_tracks_count", None) is not None:
data["tracks_count"] = artist._tracks_count data["tracks_count"] = artist._tracks_count
@ -833,4 +835,5 @@ class AlbumCreateSerializer(serializers.Serializer):
instance, "description", validated_data.get("description") instance, "description", validated_data.get("description")
) )
tag_models.set_tags(instance, *(validated_data.get("tags", []) or [])) tag_models.set_tags(instance, *(validated_data.get("tags", []) or []))
instance.artist.get_channel()
return instance return instance

View File

@ -181,10 +181,11 @@ class AlbumViewSet(
viewsets.ReadOnlyModelViewSet, viewsets.ReadOnlyModelViewSet,
): ):
queryset = ( queryset = (
models.Album.objects.all().order_by("-creation_date") models.Album.objects.all()
# we do a prefetech related on tracks instead of a count because it's more efficient .order_by("-creation_date")
# db-wise .prefetch_related(
.prefetch_related("artist", "attributed_to", "attachment_cover", "tracks") "artist__channel", "attributed_to", "attachment_cover", "tracks"
)
) )
serializer_class = serializers.AlbumSerializer serializer_class = serializers.AlbumSerializer
permission_classes = [oauth_permissions.ScopePermission] permission_classes = [oauth_permissions.ScopePermission]

View File

@ -44,15 +44,20 @@ def test_mutation_filter_is_approved(value, expected, factories):
("me", 0, [0]), ("me", 0, [0]),
("me", 1, [1]), ("me", 1, [1]),
("me", 2, []), ("me", 2, []),
("all", 0, [0, 1, 2]), ("all", 0, [0, 1, 2, 3]),
("all", 1, [0, 1, 2]), ("all", 1, [0, 1, 2, 3]),
("all", 2, [0, 1, 2]), ("all", 2, [0, 1, 2, 3]),
("noop", 0, []), ("noop", 0, []),
("noop", 1, []), ("noop", 1, []),
("noop", 2, []), ("noop", 2, []),
("actor:actor1@domain.test", 0, [0]), ("actor:actor1@domain.test", 0, [0]),
("actor:actor2@domain.test", 0, [1]), ("actor:actor2@domain.test", 0, [1]),
("domain:domain.test", 0, [0, 1]), ("domain:domain.test", 0, [0, 1]),
("subscribed", 0, [3]),
("subscribed", 1, []),
("subscribed", 2, []),
("me,subscribed", 0, [0, 3]),
("me,subscribed", 1, [1]),
], ],
) )
def test_actor_scope_filter( def test_actor_scope_filter(
@ -72,15 +77,23 @@ def test_actor_scope_filter(
preferred_username="actor2", domain=domain preferred_username="actor2", domain=domain
) )
users = [actor1.user, actor2.user, anonymous_user] users = [actor1.user, actor2.user, anonymous_user]
followed_library = factories["music.Library"]()
tracks = [ tracks = [
factories["music.Upload"](library__actor=actor1, playable=True).track, factories["music.Upload"](library__actor=actor1, playable=True).track,
factories["music.Upload"](library__actor=actor2, playable=True).track, factories["music.Upload"](library__actor=actor2, playable=True).track,
factories["music.Upload"](playable=True).track, factories["music.Upload"](playable=True).track,
factories["music.Upload"](playable=True, library=followed_library).track,
] ]
factories["federation.LibraryFollow"](
actor=actor1, target=followed_library, approved=True
)
class FS(filters.filters.FilterSet): class FS(filters.filters.FilterSet):
scope = filters.ActorScopeFilter( scope = filters.ActorScopeFilter(
actor_field="uploads__library__actor", distinct=True actor_field="uploads__library__actor",
library_field="uploads__library",
distinct=True,
) )
class Meta: class Meta:

View File

@ -0,0 +1 @@
Can now filter subscribed content through API (#1116)

View File

@ -0,0 +1 @@
Fix embedded player not working on channel serie/album (#1175)

View File

@ -94,14 +94,19 @@ Scope:
Limit the results to a given user or pod: Limit the results to a given user or pod:
- Use `all` (or do not specify the property to disable scope filtering) - Use `all` (or do not specify the property to disable scope filtering)
- Use `me` to retrieve content relative to the current user - Use `me` to retrieve content relative to the current user
- Use `subscribed` to retrieve content in libraries you follow
- Use `actor:alice@example.com` to retrieve content relative to the account `alice@example.com - Use `actor:alice@example.com` to retrieve content relative to the account `alice@example.com
- Use `domain:example.com` to retrieve content relative to the domain `example.com - Use `domain:example.com` to retrieve content relative to the domain `example.com
You can specify multiple coma separated scopes, e.g `scope=me,subscribed` to retrieve content matching either scopes.
schema: schema:
required: false required: false
type: "string" type: "string"
enum: enum:
- "me" - "me"
- "all" - "all"
- "subscribed"
- "actor:alice@example.com" - "actor:alice@example.com"
- "domain:example.com" - "domain:example.com"

View File

@ -279,6 +279,7 @@ export default {
}, },
fetchTracks (filters, path) { fetchTracks (filters, path) {
path = path || "/api/v1/tracks/" path = path || "/api/v1/tracks/"
filters.include_channels = "true"
let self = this let self = this
let url = `${this.baseUrl}${path}` let url = `${this.baseUrl}${path}`
axios.get(url, {params: filters}).then(response => { axios.get(url, {params: filters}).then(response => {

View File

@ -5,7 +5,7 @@
<h4 class="header"> <h4 class="header">
<translate translate-context="Popup/Album/Title/Verb">Embed this album on your website</translate> <translate translate-context="Popup/Album/Title/Verb">Embed this album on your website</translate>
</h4> </h4>
<div class="content"> <div class="scrolling content">
<div class="description"> <div class="description">
<embed-wizard type="album" :id="object.id" /> <embed-wizard type="album" :id="object.id" />

View File

@ -38,7 +38,7 @@
<h4 class="header"> <h4 class="header">
<translate translate-context="Popup/Artist/Title/Verb">Embed this artist work on your website</translate> <translate translate-context="Popup/Artist/Title/Verb">Embed this artist work on your website</translate>
</h4> </h4>
<div class="content"> <div class="scrolling content">
<div class="description"> <div class="description">
<embed-wizard type="artist" :id="object.id" /> <embed-wizard type="artist" :id="object.id" />

View File

@ -30,7 +30,7 @@
<h4 class="header"> <h4 class="header">
<translate translate-context="Popup/Track/Title">Embed this track on your website</translate> <translate translate-context="Popup/Track/Title">Embed this track on your website</translate>
</h4> </h4>
<div class="content"> <div class="scrolling content">
<div class="description"> <div class="description">
<embed-wizard type="track" :id="track.id" /> <embed-wizard type="track" :id="track.id" />
</div> </div>

View File

@ -159,7 +159,7 @@
<h4 class="header"> <h4 class="header">
<translate translate-context="Popup/Artist/Title/Verb">Embed this artist work on your website</translate> <translate translate-context="Popup/Artist/Title/Verb">Embed this artist work on your website</translate>
</h4> </h4>
<div class="content"> <div class="scrolling content">
<div class="description"> <div class="description">
<embed-wizard type="artist" :id="object.artist.id" /> <embed-wizard type="artist" :id="object.artist.id" />
</div> </div>

View File

@ -52,7 +52,7 @@
<h4 class="header"> <h4 class="header">
<translate translate-context="Popup/Album/Title/Verb">Embed this playlist on your website</translate> <translate translate-context="Popup/Album/Title/Verb">Embed this playlist on your website</translate>
</h4> </h4>
<div class="content"> <div class="scrolling content">
<div class="description"> <div class="description">
<embed-wizard type="playlist" :id="playlist.id" /> <embed-wizard type="playlist" :id="playlist.id" />
</div> </div>