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 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 routers as common_routers
|
||||
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"listen", views.ListenViewSet, "listen")
|
||||
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"licenses", views.LicenseViewSet, "licenses")
|
||||
router.register(r"playlists", playlists_views.PlaylistViewSet, "playlists")
|
||||
|
|
|
@ -191,6 +191,7 @@ LOCAL_APPS = (
|
|||
"funkwhale_api.users.oauth",
|
||||
# Your stuff: custom apps go here
|
||||
"funkwhale_api.instance",
|
||||
"funkwhale_api.audio",
|
||||
"funkwhale_api.music",
|
||||
"funkwhale_api.requests",
|
||||
"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
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.http import Http404
|
||||
|
||||
from rest_framework.permissions import BasePermission
|
||||
|
||||
from funkwhale_api.common import preferences
|
||||
|
@ -46,7 +48,12 @@ class OwnerPermission(BasePermission):
|
|||
return True
|
||||
|
||||
owner_field = getattr(view, "owner_field", "user")
|
||||
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:
|
||||
raise Http404
|
||||
raise owner_exception
|
||||
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 = {
|
||||
"ARTIST": {"target_artist": ["pk"]},
|
||||
"CHANNEL": {"target_artist": ["artist__pk"]},
|
||||
"ALBUM": {"target_artist": ["artist__pk"]},
|
||||
"TRACK": {"target_artist": ["artist__pk", "album__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 funkwhale_api.audio import filters as audio_filters
|
||||
from funkwhale_api.common import fields
|
||||
from funkwhale_api.common import filters as common_filters
|
||||
from funkwhale_api.common import search
|
||||
|
@ -19,7 +20,9 @@ def filter_tags(queryset, name, value):
|
|||
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"])
|
||||
playable = filters.BooleanFilter(field_name="_", method="filter_playable")
|
||||
tag = TAG_FILTER
|
||||
|
@ -36,13 +39,16 @@ class ArtistFilter(moderation_filters.HiddenContentFilterSet):
|
|||
"mbid": ["exact"],
|
||||
}
|
||||
hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG["ARTIST"]
|
||||
include_channels_field = "channel"
|
||||
|
||||
def filter_playable(self, queryset, name, value):
|
||||
actor = utils.get_actor_from_request(self.request)
|
||||
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"])
|
||||
playable = filters.BooleanFilter(field_name="_", method="filter_playable")
|
||||
tag = TAG_FILTER
|
||||
|
@ -64,13 +70,14 @@ class TrackFilter(moderation_filters.HiddenContentFilterSet):
|
|||
"mbid": ["exact"],
|
||||
}
|
||||
hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG["TRACK"]
|
||||
include_channels_field = "artist__channel"
|
||||
|
||||
def filter_playable(self, queryset, name, value):
|
||||
actor = utils.get_actor_from_request(self.request)
|
||||
return queryset.playable_by(actor, value)
|
||||
|
||||
|
||||
class UploadFilter(filters.FilterSet):
|
||||
class UploadFilter(audio_filters.IncludeChannelsFilterSet):
|
||||
library = filters.CharFilter("library__uuid")
|
||||
track = filters.UUIDFilter("track__uuid")
|
||||
track_artist = filters.UUIDFilter("track__artist__uuid")
|
||||
|
@ -109,13 +116,16 @@ class UploadFilter(filters.FilterSet):
|
|||
"import_reference",
|
||||
"scope",
|
||||
]
|
||||
include_channels_field = "track__artist__channel"
|
||||
|
||||
def filter_playable(self, queryset, name, value):
|
||||
actor = utils.get_actor_from_request(self.request)
|
||||
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")
|
||||
q = fields.SearchFilter(search_fields=["title", "artist__name"])
|
||||
tag = TAG_FILTER
|
||||
|
@ -127,6 +137,7 @@ class AlbumFilter(moderation_filters.HiddenContentFilterSet):
|
|||
model = models.Album
|
||||
fields = ["playable", "q", "artist", "scope", "mbid"]
|
||||
hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG["ALBUM"]
|
||||
include_channels_field = "artist__channel"
|
||||
|
||||
def filter_playable(self, queryset, name, value):
|
||||
actor = utils.get_actor_from_request(self.request)
|
||||
|
|
|
@ -362,7 +362,8 @@ def get_actor_data(username, **kwargs):
|
|||
"preferred_username": slugified_username,
|
||||
"domain": domain,
|
||||
"type": "Person",
|
||||
"name": username,
|
||||
"name": kwargs.get("name", username),
|
||||
"summary": kwargs.get("summary"),
|
||||
"manually_approves_followers": False,
|
||||
"fid": federation_utils.full_url(
|
||||
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",
|
||||
[
|
||||
("music.Track", filters.TrackFilter),
|
||||
("music.Artist", filters.TrackFilter),
|
||||
("music.Album", filters.TrackFilter),
|
||||
("music.Artist", filters.ArtistFilter),
|
||||
("music.Album", filters.AlbumFilter),
|
||||
],
|
||||
)
|
||||
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())
|
||||
fetch = obj.fetches.filter(actor=service_actor).order_by("-creation_date").first()
|
||||
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