Merge branch 'master' into develop
This commit is contained in:
commit
21f147c0c4
|
@ -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
|
||||||
|
|
|
@ -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})
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Can now filter subscribed content through API (#1116)
|
|
@ -0,0 +1 @@
|
||||||
|
Fix embedded player not working on channel serie/album (#1175)
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue