See #170: exclude by default all channels-related entities from /artists, /albums and /tracks endpoints results, for backward compatibility
This commit is contained in:
parent
32c0afab4f
commit
6bbe48598e
|
@ -4,6 +4,7 @@ from rest_framework import routers
|
||||||
from rest_framework.urlpatterns import format_suffix_patterns
|
from rest_framework.urlpatterns import format_suffix_patterns
|
||||||
|
|
||||||
from funkwhale_api.activity import views as activity_views
|
from funkwhale_api.activity import views as activity_views
|
||||||
|
from funkwhale_api.audio import views as audio_views
|
||||||
from funkwhale_api.common import views as common_views
|
from funkwhale_api.common import views as common_views
|
||||||
from funkwhale_api.common import routers as common_routers
|
from funkwhale_api.common import routers as common_routers
|
||||||
from funkwhale_api.music import views
|
from funkwhale_api.music import views
|
||||||
|
@ -21,6 +22,7 @@ router.register(r"uploads", views.UploadViewSet, "uploads")
|
||||||
router.register(r"libraries", views.LibraryViewSet, "libraries")
|
router.register(r"libraries", views.LibraryViewSet, "libraries")
|
||||||
router.register(r"listen", views.ListenViewSet, "listen")
|
router.register(r"listen", views.ListenViewSet, "listen")
|
||||||
router.register(r"artists", views.ArtistViewSet, "artists")
|
router.register(r"artists", views.ArtistViewSet, "artists")
|
||||||
|
router.register(r"channels", audio_views.ChannelViewSet, "channels")
|
||||||
router.register(r"albums", views.AlbumViewSet, "albums")
|
router.register(r"albums", views.AlbumViewSet, "albums")
|
||||||
router.register(r"licenses", views.LicenseViewSet, "licenses")
|
router.register(r"licenses", views.LicenseViewSet, "licenses")
|
||||||
router.register(r"playlists", playlists_views.PlaylistViewSet, "playlists")
|
router.register(r"playlists", playlists_views.PlaylistViewSet, "playlists")
|
||||||
|
|
|
@ -191,6 +191,7 @@ LOCAL_APPS = (
|
||||||
"funkwhale_api.users.oauth",
|
"funkwhale_api.users.oauth",
|
||||||
# Your stuff: custom apps go here
|
# Your stuff: custom apps go here
|
||||||
"funkwhale_api.instance",
|
"funkwhale_api.instance",
|
||||||
|
"funkwhale_api.audio",
|
||||||
"funkwhale_api.music",
|
"funkwhale_api.music",
|
||||||
"funkwhale_api.requests",
|
"funkwhale_api.requests",
|
||||||
"funkwhale_api.favorites",
|
"funkwhale_api.favorites",
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
from funkwhale_api.common import admin
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.Channel)
|
||||||
|
class ChannelAdmin(admin.ModelAdmin):
|
||||||
|
list_display = [
|
||||||
|
"uuid",
|
||||||
|
"artist",
|
||||||
|
"attributed_to",
|
||||||
|
"actor",
|
||||||
|
"library",
|
||||||
|
"creation_date",
|
||||||
|
]
|
|
@ -0,0 +1,16 @@
|
||||||
|
from dynamic_preferences import types
|
||||||
|
from dynamic_preferences.registries import global_preferences_registry
|
||||||
|
|
||||||
|
audio = types.Section("audio")
|
||||||
|
|
||||||
|
|
||||||
|
@global_preferences_registry.register
|
||||||
|
class ChannelsEnabled(types.BooleanPreference):
|
||||||
|
section = audio
|
||||||
|
name = "channels_enabled"
|
||||||
|
default = True
|
||||||
|
verbose_name = "Enable channels"
|
||||||
|
help_text = (
|
||||||
|
"If disabled, the channels feature will be completely switched off, "
|
||||||
|
"and users won't be able to create channels or subscribe to them."
|
||||||
|
)
|
|
@ -0,0 +1,32 @@
|
||||||
|
import factory
|
||||||
|
|
||||||
|
from funkwhale_api.factories import registry, NoUpdateOnCreate
|
||||||
|
from funkwhale_api.federation import factories as federation_factories
|
||||||
|
from funkwhale_api.music import factories as music_factories
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
def set_actor(o):
|
||||||
|
return models.generate_actor(str(o.uuid))
|
||||||
|
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
class ChannelFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
|
||||||
|
uuid = factory.Faker("uuid4")
|
||||||
|
attributed_to = factory.SubFactory(federation_factories.ActorFactory)
|
||||||
|
library = factory.SubFactory(
|
||||||
|
federation_factories.MusicLibraryFactory,
|
||||||
|
actor=factory.SelfAttribute("..attributed_to"),
|
||||||
|
)
|
||||||
|
actor = factory.LazyAttribute(set_actor)
|
||||||
|
artist = factory.SubFactory(music_factories.ArtistFactory)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = "audio.Channel"
|
||||||
|
|
||||||
|
class Params:
|
||||||
|
local = factory.Trait(
|
||||||
|
attributed_to__fid=factory.Faker("federation_url", local=True),
|
||||||
|
artist__local=True,
|
||||||
|
)
|
|
@ -0,0 +1,65 @@
|
||||||
|
import django_filters
|
||||||
|
|
||||||
|
from funkwhale_api.common import fields
|
||||||
|
from funkwhale_api.common import filters as common_filters
|
||||||
|
from funkwhale_api.moderation import filters as moderation_filters
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
def filter_tags(queryset, name, value):
|
||||||
|
non_empty_tags = [v.lower() for v in value if v]
|
||||||
|
for tag in non_empty_tags:
|
||||||
|
queryset = queryset.filter(artist__tagged_items__tag__name=tag).distinct()
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
TAG_FILTER = common_filters.MultipleQueryFilter(method=filter_tags)
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelFilter(moderation_filters.HiddenContentFilterSet):
|
||||||
|
q = fields.SearchFilter(
|
||||||
|
search_fields=["artist__name", "actor__summary", "actor__preferred_username"]
|
||||||
|
)
|
||||||
|
tag = TAG_FILTER
|
||||||
|
scope = common_filters.ActorScopeFilter(actor_field="attributed_to", distinct=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Channel
|
||||||
|
fields = ["q", "scope", "tag"]
|
||||||
|
hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG["CHANNEL"]
|
||||||
|
|
||||||
|
|
||||||
|
class IncludeChannelsFilterSet(django_filters.FilterSet):
|
||||||
|
"""
|
||||||
|
|
||||||
|
A filterset that include a "include_channels" param. Meant for compatibility
|
||||||
|
with clients that don't support channels yet:
|
||||||
|
|
||||||
|
- include_channels=false : exclude objects associated with a channel
|
||||||
|
- include_channels=true : don't exclude objects associated with a channel
|
||||||
|
- not specified: include_channels=false
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
class MyFilterSet(IncludeChannelsFilterSet):
|
||||||
|
class Meta:
|
||||||
|
include_channels_field = "album__artist__channel"
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
include_channels = django_filters.BooleanFilter(
|
||||||
|
field_name="_", method="filter_include_channels"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.data = self.data.copy()
|
||||||
|
self.data.setdefault("include_channels", False)
|
||||||
|
|
||||||
|
def filter_include_channels(self, queryset, name, value):
|
||||||
|
if value is True:
|
||||||
|
return queryset
|
||||||
|
else:
|
||||||
|
params = {self.__class__.Meta.include_channels_field: None}
|
||||||
|
return queryset.filter(**params)
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Generated by Django 2.2.6 on 2019-10-29 12:57
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('federation', '0021_auto_20191029_1257'),
|
||||||
|
('music', '0041_auto_20191021_1705'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Channel',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True)),
|
||||||
|
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
('actor', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='channel', to='federation.Actor')),
|
||||||
|
('artist', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='channel', to='music.Artist')),
|
||||||
|
('attributed_to', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owned_channels', to='federation.Actor')),
|
||||||
|
('library', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='channel', to='music.Library')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,39 @@
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from funkwhale_api.federation import keys
|
||||||
|
from funkwhale_api.federation import models as federation_models
|
||||||
|
from funkwhale_api.users import models as user_models
|
||||||
|
|
||||||
|
|
||||||
|
class Channel(models.Model):
|
||||||
|
uuid = models.UUIDField(default=uuid.uuid4, unique=True)
|
||||||
|
artist = models.OneToOneField(
|
||||||
|
"music.Artist", on_delete=models.CASCADE, related_name="channel"
|
||||||
|
)
|
||||||
|
# the owner of the channel
|
||||||
|
attributed_to = models.ForeignKey(
|
||||||
|
"federation.Actor", on_delete=models.CASCADE, related_name="owned_channels"
|
||||||
|
)
|
||||||
|
# the federation actor created for the channel
|
||||||
|
# (the one people can follow to receive updates)
|
||||||
|
actor = models.OneToOneField(
|
||||||
|
"federation.Actor", on_delete=models.CASCADE, related_name="channel"
|
||||||
|
)
|
||||||
|
|
||||||
|
library = models.OneToOneField(
|
||||||
|
"music.Library", on_delete=models.CASCADE, related_name="channel"
|
||||||
|
)
|
||||||
|
creation_date = models.DateTimeField(default=timezone.now)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_actor(username, **kwargs):
|
||||||
|
actor_data = user_models.get_actor_data(username, **kwargs)
|
||||||
|
private, public = keys.get_key_pair()
|
||||||
|
actor_data["private_key"] = private.decode("utf-8")
|
||||||
|
actor_data["public_key"] = public.decode("utf-8")
|
||||||
|
|
||||||
|
return federation_models.Actor.objects.create(**actor_data)
|
|
@ -0,0 +1,88 @@
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from funkwhale_api.federation import serializers as federation_serializers
|
||||||
|
from funkwhale_api.music import models as music_models
|
||||||
|
from funkwhale_api.music import serializers as music_serializers
|
||||||
|
from funkwhale_api.tags import models as tags_models
|
||||||
|
from funkwhale_api.tags import serializers as tags_serializers
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelCreateSerializer(serializers.Serializer):
|
||||||
|
name = serializers.CharField(max_length=music_models.MAX_LENGTHS["ARTIST_NAME"])
|
||||||
|
username = serializers.CharField(max_length=music_models.MAX_LENGTHS["ARTIST_NAME"])
|
||||||
|
summary = serializers.CharField(max_length=500, allow_blank=True, allow_null=True)
|
||||||
|
tags = tags_serializers.TagsListField()
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def create(self, validated_data):
|
||||||
|
artist = music_models.Artist.objects.create(
|
||||||
|
attributed_to=validated_data["attributed_to"], name=validated_data["name"]
|
||||||
|
)
|
||||||
|
if validated_data.get("tags", []):
|
||||||
|
tags_models.set_tags(artist, *validated_data["tags"])
|
||||||
|
|
||||||
|
channel = models.Channel(
|
||||||
|
artist=artist, attributed_to=validated_data["attributed_to"]
|
||||||
|
)
|
||||||
|
|
||||||
|
channel.actor = models.generate_actor(
|
||||||
|
validated_data["username"],
|
||||||
|
summary=validated_data["summary"],
|
||||||
|
name=validated_data["name"],
|
||||||
|
)
|
||||||
|
|
||||||
|
channel.library = music_models.Library.objects.create(
|
||||||
|
name=channel.actor.preferred_username,
|
||||||
|
privacy_level="public",
|
||||||
|
actor=validated_data["attributed_to"],
|
||||||
|
)
|
||||||
|
channel.save()
|
||||||
|
return channel
|
||||||
|
|
||||||
|
def to_representation(self, obj):
|
||||||
|
return ChannelSerializer(obj).data
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelUpdateSerializer(serializers.Serializer):
|
||||||
|
name = serializers.CharField(max_length=music_models.MAX_LENGTHS["ARTIST_NAME"])
|
||||||
|
summary = serializers.CharField(max_length=500, allow_blank=True, allow_null=True)
|
||||||
|
tags = tags_serializers.TagsListField()
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def update(self, obj, validated_data):
|
||||||
|
if validated_data.get("tags") is not None:
|
||||||
|
tags_models.set_tags(obj.artist, *validated_data["tags"])
|
||||||
|
actor_update_fields = []
|
||||||
|
|
||||||
|
if "summary" in validated_data:
|
||||||
|
actor_update_fields.append(("summary", validated_data["summary"]))
|
||||||
|
if "name" in validated_data:
|
||||||
|
obj.artist.name = validated_data["name"]
|
||||||
|
obj.artist.save(update_fields=["name"])
|
||||||
|
actor_update_fields.append(("name", validated_data["name"]))
|
||||||
|
|
||||||
|
if actor_update_fields:
|
||||||
|
for field, value in actor_update_fields:
|
||||||
|
setattr(obj.actor, field, value)
|
||||||
|
obj.actor.save(update_fields=[f for f, _ in actor_update_fields])
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def to_representation(self, obj):
|
||||||
|
return ChannelSerializer(obj).data
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelSerializer(serializers.ModelSerializer):
|
||||||
|
artist = serializers.SerializerMethodField()
|
||||||
|
actor = federation_serializers.APIActorSerializer()
|
||||||
|
attributed_to = federation_serializers.APIActorSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Channel
|
||||||
|
fields = ["uuid", "artist", "attributed_to", "actor", "creation_date"]
|
||||||
|
|
||||||
|
def get_artist(self, obj):
|
||||||
|
return music_serializers.serialize_artist_simple(obj.artist)
|
|
@ -0,0 +1,54 @@
|
||||||
|
from rest_framework import exceptions, mixins, viewsets
|
||||||
|
|
||||||
|
from django import http
|
||||||
|
|
||||||
|
from funkwhale_api.common import permissions
|
||||||
|
from funkwhale_api.common import preferences
|
||||||
|
from funkwhale_api.users.oauth import permissions as oauth_permissions
|
||||||
|
|
||||||
|
from . import filters, models, serializers
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelsMixin(object):
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
if not preferences.get("audio__channels_enabled"):
|
||||||
|
return http.HttpResponse(status=405)
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelViewSet(
|
||||||
|
ChannelsMixin,
|
||||||
|
mixins.CreateModelMixin,
|
||||||
|
mixins.RetrieveModelMixin,
|
||||||
|
mixins.UpdateModelMixin,
|
||||||
|
mixins.ListModelMixin,
|
||||||
|
mixins.DestroyModelMixin,
|
||||||
|
viewsets.GenericViewSet,
|
||||||
|
):
|
||||||
|
lookup_field = "uuid"
|
||||||
|
filterset_class = filters.ChannelFilter
|
||||||
|
serializer_class = serializers.ChannelSerializer
|
||||||
|
queryset = (
|
||||||
|
models.Channel.objects.all()
|
||||||
|
.prefetch_related("library", "attributed_to", "artist", "actor")
|
||||||
|
.order_by("-creation_date")
|
||||||
|
)
|
||||||
|
permission_classes = [
|
||||||
|
oauth_permissions.ScopePermission,
|
||||||
|
permissions.OwnerPermission,
|
||||||
|
]
|
||||||
|
required_scope = "libraries"
|
||||||
|
anonymous_policy = "setting"
|
||||||
|
owner_checks = ["write"]
|
||||||
|
owner_field = "attributed_to.user"
|
||||||
|
owner_exception = exceptions.PermissionDenied
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
if self.request.method.lower() in ["head", "get", "options"]:
|
||||||
|
return serializers.ChannelSerializer
|
||||||
|
elif self.action in ["update", "partial_update"]:
|
||||||
|
return serializers.ChannelUpdateSerializer
|
||||||
|
return serializers.ChannelCreateSerializer
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
return serializer.save(attributed_to=self.request.user.actor)
|
|
@ -1,6 +1,8 @@
|
||||||
import operator
|
import operator
|
||||||
|
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
|
|
||||||
from rest_framework.permissions import BasePermission
|
from rest_framework.permissions import BasePermission
|
||||||
|
|
||||||
from funkwhale_api.common import preferences
|
from funkwhale_api.common import preferences
|
||||||
|
@ -46,7 +48,12 @@ class OwnerPermission(BasePermission):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
owner_field = getattr(view, "owner_field", "user")
|
owner_field = getattr(view, "owner_field", "user")
|
||||||
owner = operator.attrgetter(owner_field)(obj)
|
owner_exception = getattr(view, "owner_exception", Http404)
|
||||||
|
try:
|
||||||
|
owner = operator.attrgetter(owner_field)(obj)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
raise owner_exception
|
||||||
|
|
||||||
if not owner or not request.user.is_authenticated or owner != request.user:
|
if not owner or not request.user.is_authenticated or owner != request.user:
|
||||||
raise Http404
|
raise owner_exception
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Generated by Django 2.2.6 on 2019-10-29 12:57
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('federation', '0020_auto_20190730_0846'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='actor',
|
||||||
|
options={'verbose_name': 'Account'},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='actor',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('Person', 'Person'), ('Tombstone', 'Tombstone'), ('Application', 'Application'), ('Group', 'Group'), ('Organization', 'Organization'), ('Service', 'Service')], default='Person', max_length=25),
|
||||||
|
),
|
||||||
|
]
|
|
@ -5,6 +5,7 @@ from django_filters import rest_framework as filters
|
||||||
|
|
||||||
USER_FILTER_CONFIG = {
|
USER_FILTER_CONFIG = {
|
||||||
"ARTIST": {"target_artist": ["pk"]},
|
"ARTIST": {"target_artist": ["pk"]},
|
||||||
|
"CHANNEL": {"target_artist": ["artist__pk"]},
|
||||||
"ALBUM": {"target_artist": ["artist__pk"]},
|
"ALBUM": {"target_artist": ["artist__pk"]},
|
||||||
"TRACK": {"target_artist": ["artist__pk", "album__artist__pk"]},
|
"TRACK": {"target_artist": ["artist__pk", "album__artist__pk"]},
|
||||||
"LISTENING": {"target_artist": ["track__album__artist__pk", "track__artist__pk"]},
|
"LISTENING": {"target_artist": ["track__album__artist__pk", "track__artist__pk"]},
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from django_filters import rest_framework as filters
|
from django_filters import rest_framework as filters
|
||||||
|
|
||||||
|
from funkwhale_api.audio import filters as audio_filters
|
||||||
from funkwhale_api.common import fields
|
from funkwhale_api.common import fields
|
||||||
from funkwhale_api.common import filters as common_filters
|
from funkwhale_api.common import filters as common_filters
|
||||||
from funkwhale_api.common import search
|
from funkwhale_api.common import search
|
||||||
|
@ -19,7 +20,9 @@ def filter_tags(queryset, name, value):
|
||||||
TAG_FILTER = common_filters.MultipleQueryFilter(method=filter_tags)
|
TAG_FILTER = common_filters.MultipleQueryFilter(method=filter_tags)
|
||||||
|
|
||||||
|
|
||||||
class ArtistFilter(moderation_filters.HiddenContentFilterSet):
|
class ArtistFilter(
|
||||||
|
audio_filters.IncludeChannelsFilterSet, moderation_filters.HiddenContentFilterSet
|
||||||
|
):
|
||||||
q = fields.SearchFilter(search_fields=["name"])
|
q = fields.SearchFilter(search_fields=["name"])
|
||||||
playable = filters.BooleanFilter(field_name="_", method="filter_playable")
|
playable = filters.BooleanFilter(field_name="_", method="filter_playable")
|
||||||
tag = TAG_FILTER
|
tag = TAG_FILTER
|
||||||
|
@ -36,13 +39,16 @@ class ArtistFilter(moderation_filters.HiddenContentFilterSet):
|
||||||
"mbid": ["exact"],
|
"mbid": ["exact"],
|
||||||
}
|
}
|
||||||
hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG["ARTIST"]
|
hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG["ARTIST"]
|
||||||
|
include_channels_field = "channel"
|
||||||
|
|
||||||
def filter_playable(self, queryset, name, value):
|
def filter_playable(self, queryset, name, value):
|
||||||
actor = utils.get_actor_from_request(self.request)
|
actor = utils.get_actor_from_request(self.request)
|
||||||
return queryset.playable_by(actor, value)
|
return queryset.playable_by(actor, value)
|
||||||
|
|
||||||
|
|
||||||
class TrackFilter(moderation_filters.HiddenContentFilterSet):
|
class TrackFilter(
|
||||||
|
audio_filters.IncludeChannelsFilterSet, moderation_filters.HiddenContentFilterSet
|
||||||
|
):
|
||||||
q = fields.SearchFilter(search_fields=["title", "album__title", "artist__name"])
|
q = fields.SearchFilter(search_fields=["title", "album__title", "artist__name"])
|
||||||
playable = filters.BooleanFilter(field_name="_", method="filter_playable")
|
playable = filters.BooleanFilter(field_name="_", method="filter_playable")
|
||||||
tag = TAG_FILTER
|
tag = TAG_FILTER
|
||||||
|
@ -64,13 +70,14 @@ class TrackFilter(moderation_filters.HiddenContentFilterSet):
|
||||||
"mbid": ["exact"],
|
"mbid": ["exact"],
|
||||||
}
|
}
|
||||||
hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG["TRACK"]
|
hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG["TRACK"]
|
||||||
|
include_channels_field = "artist__channel"
|
||||||
|
|
||||||
def filter_playable(self, queryset, name, value):
|
def filter_playable(self, queryset, name, value):
|
||||||
actor = utils.get_actor_from_request(self.request)
|
actor = utils.get_actor_from_request(self.request)
|
||||||
return queryset.playable_by(actor, value)
|
return queryset.playable_by(actor, value)
|
||||||
|
|
||||||
|
|
||||||
class UploadFilter(filters.FilterSet):
|
class UploadFilter(audio_filters.IncludeChannelsFilterSet):
|
||||||
library = filters.CharFilter("library__uuid")
|
library = filters.CharFilter("library__uuid")
|
||||||
track = filters.UUIDFilter("track__uuid")
|
track = filters.UUIDFilter("track__uuid")
|
||||||
track_artist = filters.UUIDFilter("track__artist__uuid")
|
track_artist = filters.UUIDFilter("track__artist__uuid")
|
||||||
|
@ -109,13 +116,16 @@ class UploadFilter(filters.FilterSet):
|
||||||
"import_reference",
|
"import_reference",
|
||||||
"scope",
|
"scope",
|
||||||
]
|
]
|
||||||
|
include_channels_field = "track__artist__channel"
|
||||||
|
|
||||||
def filter_playable(self, queryset, name, value):
|
def filter_playable(self, queryset, name, value):
|
||||||
actor = utils.get_actor_from_request(self.request)
|
actor = utils.get_actor_from_request(self.request)
|
||||||
return queryset.playable_by(actor, value)
|
return queryset.playable_by(actor, value)
|
||||||
|
|
||||||
|
|
||||||
class AlbumFilter(moderation_filters.HiddenContentFilterSet):
|
class AlbumFilter(
|
||||||
|
audio_filters.IncludeChannelsFilterSet, moderation_filters.HiddenContentFilterSet
|
||||||
|
):
|
||||||
playable = filters.BooleanFilter(field_name="_", method="filter_playable")
|
playable = filters.BooleanFilter(field_name="_", method="filter_playable")
|
||||||
q = fields.SearchFilter(search_fields=["title", "artist__name"])
|
q = fields.SearchFilter(search_fields=["title", "artist__name"])
|
||||||
tag = TAG_FILTER
|
tag = TAG_FILTER
|
||||||
|
@ -127,6 +137,7 @@ class AlbumFilter(moderation_filters.HiddenContentFilterSet):
|
||||||
model = models.Album
|
model = models.Album
|
||||||
fields = ["playable", "q", "artist", "scope", "mbid"]
|
fields = ["playable", "q", "artist", "scope", "mbid"]
|
||||||
hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG["ALBUM"]
|
hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG["ALBUM"]
|
||||||
|
include_channels_field = "artist__channel"
|
||||||
|
|
||||||
def filter_playable(self, queryset, name, value):
|
def filter_playable(self, queryset, name, value):
|
||||||
actor = utils.get_actor_from_request(self.request)
|
actor = utils.get_actor_from_request(self.request)
|
||||||
|
|
|
@ -362,7 +362,8 @@ def get_actor_data(username, **kwargs):
|
||||||
"preferred_username": slugified_username,
|
"preferred_username": slugified_username,
|
||||||
"domain": domain,
|
"domain": domain,
|
||||||
"type": "Person",
|
"type": "Person",
|
||||||
"name": username,
|
"name": kwargs.get("name", username),
|
||||||
|
"summary": kwargs.get("summary"),
|
||||||
"manually_approves_followers": False,
|
"manually_approves_followers": False,
|
||||||
"fid": federation_utils.full_url(
|
"fid": federation_utils.full_url(
|
||||||
reverse(
|
reverse(
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
def test_channel(factories, now):
|
||||||
|
channel = factories["audio.Channel"]()
|
||||||
|
assert channel.artist is not None
|
||||||
|
assert channel.actor is not None
|
||||||
|
assert channel.attributed_to is not None
|
||||||
|
assert channel.library is not None
|
||||||
|
assert channel.creation_date >= now
|
|
@ -0,0 +1,74 @@
|
||||||
|
from funkwhale_api.audio import serializers
|
||||||
|
from funkwhale_api.federation import serializers as federation_serializers
|
||||||
|
from funkwhale_api.music import serializers as music_serializers
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_serializer_create(factories):
|
||||||
|
attributed_to = factories["federation.Actor"](local=True)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
# TODO: cover
|
||||||
|
"name": "My channel",
|
||||||
|
"username": "mychannel",
|
||||||
|
"summary": "This is my channel",
|
||||||
|
"tags": ["hello", "world"],
|
||||||
|
}
|
||||||
|
|
||||||
|
serializer = serializers.ChannelCreateSerializer(data=data)
|
||||||
|
assert serializer.is_valid(raise_exception=True) is True
|
||||||
|
|
||||||
|
channel = serializer.save(attributed_to=attributed_to)
|
||||||
|
|
||||||
|
assert channel.artist.name == data["name"]
|
||||||
|
assert channel.artist.attributed_to == attributed_to
|
||||||
|
assert (
|
||||||
|
sorted(channel.artist.tagged_items.values_list("tag__name", flat=True))
|
||||||
|
== data["tags"]
|
||||||
|
)
|
||||||
|
assert channel.attributed_to == attributed_to
|
||||||
|
assert channel.actor.summary == data["summary"]
|
||||||
|
assert channel.actor.preferred_username == data["username"]
|
||||||
|
assert channel.actor.name == data["name"]
|
||||||
|
assert channel.library.privacy_level == "public"
|
||||||
|
assert channel.library.actor == attributed_to
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_serializer_update(factories):
|
||||||
|
channel = factories["audio.Channel"](artist__set_tags=["rock"])
|
||||||
|
|
||||||
|
data = {
|
||||||
|
# TODO: cover
|
||||||
|
"name": "My channel",
|
||||||
|
"summary": "This is my channel",
|
||||||
|
"tags": ["hello", "world"],
|
||||||
|
}
|
||||||
|
|
||||||
|
serializer = serializers.ChannelUpdateSerializer(channel, data=data)
|
||||||
|
assert serializer.is_valid(raise_exception=True) is True
|
||||||
|
|
||||||
|
serializer.save()
|
||||||
|
channel.refresh_from_db()
|
||||||
|
|
||||||
|
assert channel.artist.name == data["name"]
|
||||||
|
assert (
|
||||||
|
sorted(channel.artist.tagged_items.values_list("tag__name", flat=True))
|
||||||
|
== data["tags"]
|
||||||
|
)
|
||||||
|
assert channel.actor.summary == data["summary"]
|
||||||
|
assert channel.actor.name == data["name"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_serializer_representation(factories, to_api_date):
|
||||||
|
channel = factories["audio.Channel"]()
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
"artist": music_serializers.serialize_artist_simple(channel.artist),
|
||||||
|
"uuid": str(channel.uuid),
|
||||||
|
"creation_date": to_api_date(channel.creation_date),
|
||||||
|
"actor": federation_serializers.APIActorSerializer(channel.actor).data,
|
||||||
|
"attributed_to": federation_serializers.APIActorSerializer(
|
||||||
|
channel.attributed_to
|
||||||
|
).data,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert serializers.ChannelSerializer(channel).data == expected
|
|
@ -0,0 +1,128 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from funkwhale_api.audio import serializers
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_create(logged_in_api_client):
|
||||||
|
actor = logged_in_api_client.user.create_actor()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
# TODO: cover
|
||||||
|
"name": "My channel",
|
||||||
|
"username": "mychannel",
|
||||||
|
"summary": "This is my channel",
|
||||||
|
"tags": ["hello", "world"],
|
||||||
|
}
|
||||||
|
|
||||||
|
url = reverse("api:v1:channels-list")
|
||||||
|
response = logged_in_api_client.post(url, data)
|
||||||
|
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
channel = actor.owned_channels.latest("id")
|
||||||
|
expected = serializers.ChannelSerializer(channel).data
|
||||||
|
|
||||||
|
assert response.data == expected
|
||||||
|
assert channel.artist.name == data["name"]
|
||||||
|
assert channel.artist.attributed_to == actor
|
||||||
|
assert (
|
||||||
|
sorted(channel.artist.tagged_items.values_list("tag__name", flat=True))
|
||||||
|
== data["tags"]
|
||||||
|
)
|
||||||
|
assert channel.attributed_to == actor
|
||||||
|
assert channel.actor.summary == data["summary"]
|
||||||
|
assert channel.actor.preferred_username == data["username"]
|
||||||
|
assert channel.library.privacy_level == "public"
|
||||||
|
assert channel.library.actor == actor
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_detail(factories, logged_in_api_client):
|
||||||
|
channel = factories["audio.Channel"]()
|
||||||
|
url = reverse("api:v1:channels-detail", kwargs={"uuid": channel.uuid})
|
||||||
|
expected = serializers.ChannelSerializer(channel).data
|
||||||
|
response = logged_in_api_client.get(url)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_list(factories, logged_in_api_client):
|
||||||
|
channel = factories["audio.Channel"]()
|
||||||
|
url = reverse("api:v1:channels-list")
|
||||||
|
expected = serializers.ChannelSerializer(channel).data
|
||||||
|
response = logged_in_api_client.get(url)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == {
|
||||||
|
"results": [expected],
|
||||||
|
"count": 1,
|
||||||
|
"next": None,
|
||||||
|
"previous": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_update(logged_in_api_client, factories):
|
||||||
|
actor = logged_in_api_client.user.create_actor()
|
||||||
|
channel = factories["audio.Channel"](attributed_to=actor)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
# TODO: cover
|
||||||
|
"name": "new name"
|
||||||
|
}
|
||||||
|
|
||||||
|
url = reverse("api:v1:channels-detail", kwargs={"uuid": channel.uuid})
|
||||||
|
response = logged_in_api_client.patch(url, data)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
channel.refresh_from_db()
|
||||||
|
|
||||||
|
assert channel.artist.name == data["name"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_update_permission(logged_in_api_client, factories):
|
||||||
|
logged_in_api_client.user.create_actor()
|
||||||
|
channel = factories["audio.Channel"]()
|
||||||
|
|
||||||
|
data = {"name": "new name"}
|
||||||
|
|
||||||
|
url = reverse("api:v1:channels-detail", kwargs={"uuid": channel.uuid})
|
||||||
|
response = logged_in_api_client.patch(url, data)
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_delete(logged_in_api_client, factories):
|
||||||
|
actor = logged_in_api_client.user.create_actor()
|
||||||
|
channel = factories["audio.Channel"](attributed_to=actor)
|
||||||
|
|
||||||
|
url = reverse("api:v1:channels-detail", kwargs={"uuid": channel.uuid})
|
||||||
|
response = logged_in_api_client.delete(url)
|
||||||
|
|
||||||
|
assert response.status_code == 204
|
||||||
|
|
||||||
|
with pytest.raises(channel.DoesNotExist):
|
||||||
|
channel.refresh_from_db()
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_delete_permission(logged_in_api_client, factories):
|
||||||
|
logged_in_api_client.user.create_actor()
|
||||||
|
channel = factories["audio.Channel"]()
|
||||||
|
|
||||||
|
url = reverse("api:v1:channels-detail", kwargs={"uuid": channel.uuid})
|
||||||
|
response = logged_in_api_client.patch(url)
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
channel.refresh_from_db()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("url_name", ["api:v1:channels-list"])
|
||||||
|
def test_channel_views_disabled_via_feature_flag(
|
||||||
|
url_name, logged_in_api_client, preferences
|
||||||
|
):
|
||||||
|
preferences["audio__channels_enabled"] = False
|
||||||
|
url = reverse(url_name)
|
||||||
|
response = logged_in_api_client.get(url)
|
||||||
|
assert response.status_code == 405
|
|
@ -60,8 +60,8 @@ def test_artist_filter_track_album_artist(factories, mocker, queryset_equal_list
|
||||||
"factory_name, filterset_class",
|
"factory_name, filterset_class",
|
||||||
[
|
[
|
||||||
("music.Track", filters.TrackFilter),
|
("music.Track", filters.TrackFilter),
|
||||||
("music.Artist", filters.TrackFilter),
|
("music.Artist", filters.ArtistFilter),
|
||||||
("music.Album", filters.TrackFilter),
|
("music.Album", filters.AlbumFilter),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_track_filter_tag_single(
|
def test_track_filter_tag_single(
|
||||||
|
|
|
@ -997,3 +997,49 @@ def test_refetch_obj(mocker, factories, settings, service_actor):
|
||||||
views.refetch_obj(obj, obj.__class__.objects.all())
|
views.refetch_obj(obj, obj.__class__.objects.all())
|
||||||
fetch = obj.fetches.filter(actor=service_actor).order_by("-creation_date").first()
|
fetch = obj.fetches.filter(actor=service_actor).order_by("-creation_date").first()
|
||||||
fetch_task.assert_called_once_with(fetch_id=fetch.pk)
|
fetch_task.assert_called_once_with(fetch_id=fetch.pk)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"params, expected",
|
||||||
|
[({}, 0), ({"include_channels": "false"}, 0), ({"include_channels": "true"}, 1)],
|
||||||
|
)
|
||||||
|
def test_artist_list_exclude_channels(
|
||||||
|
params, expected, factories, logged_in_api_client
|
||||||
|
):
|
||||||
|
factories["audio.Channel"]()
|
||||||
|
|
||||||
|
url = reverse("api:v1:artists-list")
|
||||||
|
response = logged_in_api_client.get(url, params)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data["count"] == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"params, expected",
|
||||||
|
[({}, 0), ({"include_channels": "false"}, 0), ({"include_channels": "true"}, 1)],
|
||||||
|
)
|
||||||
|
def test_album_list_exclude_channels(params, expected, factories, logged_in_api_client):
|
||||||
|
channel_artist = factories["audio.Channel"]().artist
|
||||||
|
factories["music.Album"](artist=channel_artist)
|
||||||
|
|
||||||
|
url = reverse("api:v1:albums-list")
|
||||||
|
response = logged_in_api_client.get(url, params)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data["count"] == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"params, expected",
|
||||||
|
[({}, 0), ({"include_channels": "false"}, 0), ({"include_channels": "true"}, 1)],
|
||||||
|
)
|
||||||
|
def test_track_list_exclude_channels(params, expected, factories, logged_in_api_client):
|
||||||
|
channel_artist = factories["audio.Channel"]().artist
|
||||||
|
factories["music.Track"](artist=channel_artist)
|
||||||
|
|
||||||
|
url = reverse("api:v1:tracks-list")
|
||||||
|
response = logged_in_api_client.get(url, params)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data["count"] == expected
|
||||||
|
|
Loading…
Reference in New Issue