See #170: exclude by default all channels-related entities from /artists, /albums and /tracks endpoints results, for backward compatibility

environments/review-front-340-9n9j9v/deployments/3368
Eliot Berriot 2019-11-25 09:49:49 +01:00
rodzic 32c0afab4f
commit 6bbe48598e
23 zmienionych plików z 649 dodań i 9 usunięć

Wyświetl plik

@ -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")

Wyświetl plik

@ -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",

Wyświetl plik

@ -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",
]

Wyświetl plik

@ -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."
)

Wyświetl plik

@ -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,
)

Wyświetl plik

@ -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)

Wyświetl plik

@ -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')),
],
),
]

Wyświetl plik

@ -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)

Wyświetl plik

@ -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)

Wyświetl plik

@ -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)

Wyświetl plik

@ -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 = 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:
raise Http404
raise owner_exception
return True

Wyświetl plik

@ -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),
),
]

Wyświetl plik

@ -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"]},

Wyświetl plik

@ -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)

Wyświetl plik

@ -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(

Wyświetl plik

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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(

Wyświetl plik

@ -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