Skip to content
Snippets Groups Projects
Verified Commit e7f0c1b8 authored by Eliot Berriot's avatar Eliot Berriot
Browse files

Views and logic for custom radios

parent df632521
No related branches found
No related tags found
No related merge requests found
......@@ -262,6 +262,16 @@ class Lyrics(models.Model):
extensions=['markdown.extensions.nl2br'])
class TrackQuerySet(models.QuerySet):
def for_nested_serialization(self):
return (self.select_related()
.select_related('album__artist')
.prefetch_related(
'tags',
'files',
'artist__albums__tracks__tags'))
class Track(APIModelMixin):
title = models.CharField(max_length=255)
artist = models.ForeignKey(
......@@ -302,6 +312,7 @@ class Track(APIModelMixin):
import_hooks = [
import_tags
]
objects = TrackQuerySet.as_manager()
tags = TaggableManager()
class Meta:
......
......@@ -116,13 +116,7 @@ class TrackViewSet(TagViewSetMixin, SearchMixin, viewsets.ReadOnlyModelViewSet):
"""
A simple ViewSet for viewing and editing accounts.
"""
queryset = (models.Track.objects.all()
.select_related()
.select_related('album__artist')
.prefetch_related(
'tags',
'files',
'artist__albums__tracks__tags'))
queryset = (models.Track.objects.all().for_nested_serialization())
serializer_class = serializers.TrackSerializerNested
permission_classes = [ConditionalAuthentication]
search_fields = ['title', 'artist__name']
......
import factory
from funkwhale_api.factories import registry
from funkwhale_api.users.factories import UserFactory
@registry.register
class RadioFactory(factory.django.DjangoModelFactory):
name = factory.Faker('name')
description = factory.Faker('paragraphs')
user = factory.SubFactory(UserFactory)
config = []
class Meta:
model = 'radios.Radio'
@registry.register
class RadioSessionFactory(factory.django.DjangoModelFactory):
user = factory.SubFactory(UserFactory)
class Meta:
model = 'radios.RadioSession'
@registry.register(name='radios.CustomRadioSession')
class RadioSessionFactory(factory.django.DjangoModelFactory):
user = factory.SubFactory(UserFactory)
radio_type = 'custom'
custom_radio = factory.SubFactory(
RadioFactory, user=factory.SelfAttribute('..user'))
class Meta:
model = 'radios.RadioSession'
import collections
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.urls import reverse_lazy
import persisting_theory
from funkwhale_api.music import models
from funkwhale_api.taskapp.celery import require_instance
class RadioFilterRegistry(persisting_theory.Registry):
def prepare_data(self, data):
return data()
def prepare_name(self, data, name=None):
return data.code
@property
def exposed_filters(self):
return [
f for f in self.values() if f.expose_in_api
]
registry = RadioFilterRegistry()
def run(filters, **kwargs):
candidates = kwargs.pop('candidates', models.Track.objects.all())
final_query = None
final_query = registry['group'].get_query(
candidates, filters=filters, **kwargs)
if final_query:
candidates = candidates.filter(final_query)
return candidates.order_by('pk')
def validate(filter_config):
try:
f = registry[filter_config['type']]
except KeyError:
raise ValidationError(
'Invalid type "{}"'.format(filter_config['type']))
f.validate(filter_config)
return True
def test(filter_config, **kwargs):
"""
Run validation and also gather the candidates for the given config
"""
data = {
'errors': [],
'candidates': {
'count': None,
'sample': None,
}
}
try:
validate(filter_config)
except ValidationError as e:
data['errors'] = [e.message]
return data
candidates = run([filter_config], **kwargs)
data['candidates']['count'] = candidates.count()
data['candidates']['sample'] = candidates[:10]
return data
def clean_config(filter_config):
f = registry[filter_config['type']]
return f.clean_config(filter_config)
class RadioFilter(object):
help_text = None
label = None
fields = []
expose_in_api = True
def get_query(self, candidates, **kwargs):
return candidates
def clean_config(self, filter_config):
return filter_config
def validate(self, config):
operator = config.get('operator', 'and')
try:
assert operator in ['or', 'and']
except AssertionError:
raise ValidationError(
'Invalid operator "{}"'.format(config['operator']))
@registry.register
class GroupFilter(RadioFilter):
code = 'group'
expose_in_api = False
def get_query(self, candidates, filters, **kwargs):
if not filters:
return
final_query = None
for filter_config in filters:
f = registry[filter_config['type']]
conf = collections.ChainMap(filter_config, kwargs)
query = f.get_query(candidates, **conf)
if filter_config.get('not', False):
query = ~query
if not final_query:
final_query = query
else:
operator = filter_config.get('operator', 'and')
if operator == 'and':
final_query &= query
elif operator == 'or':
final_query |= query
else:
raise ValueError(
'Invalid query operator "{}"'.format(operator))
return final_query
def validate(self, config):
super().validate(config)
for fc in config['filters']:
registry[fc['type']].validate(fc)
@registry.register
class ArtistFilter(RadioFilter):
code = 'artist'
label = 'Artist'
help_text = 'Select tracks for a given artist'
fields = [
{
'name': 'ids',
'type': 'list',
'subtype': 'number',
'autocomplete': reverse_lazy('api:v1:artists-search'),
'autocomplete_qs': 'query={query}',
'autocomplete_fields': {'name': 'name', 'value': 'id'},
'label': 'Artist',
'placeholder': 'Select artists'
}
]
def clean_config(self, filter_config):
filter_config = super().clean_config(filter_config)
filter_config['ids'] = sorted(filter_config['ids'])
names = models.Artist.objects.filter(
pk__in=filter_config['ids']
).order_by('id').values_list('name', flat=True)
filter_config['names'] = list(names)
return filter_config
def get_query(self, candidates, ids, **kwargs):
return Q(artist__pk__in=ids)
def validate(self, config):
super().validate(config)
try:
pks = models.Artist.objects.filter(
pk__in=config['ids']).values_list('pk', flat=True)
diff = set(config['ids']) - set(pks)
assert len(diff) == 0
except KeyError:
raise ValidationError('You must provide an id')
except AssertionError:
raise ValidationError(
'No artist matching ids "{}"'.format(diff))
@registry.register
class TagFilter(RadioFilter):
code = 'tag'
fields = [
{
'name': 'names',
'type': 'list',
'subtype': 'string',
'autocomplete': reverse_lazy('api:v1:tags-list'),
'autocomplete_qs': '',
'autocomplete_fields': {'remoteValues': 'results', 'name': 'name', 'value': 'slug'},
'autocomplete_qs': 'query={query}',
'label': 'Tags',
'placeholder': 'Select tags'
}
]
help_text = 'Select tracks with a given tag'
label = 'Tag'
def get_query(self, candidates, names, **kwargs):
return Q(tags__slug__in=names)
import django_filters
from . import models
class RadioFilter(django_filters.FilterSet):
class Meta:
model = models.Radio
fields = {
'name': ['exact', 'iexact', 'startswith', 'icontains']
}
import random
from rest_framework import serializers
from django.core.exceptions import ValidationError
from taggit.models import Tag
from funkwhale_api.users.models import User
from funkwhale_api.music.models import Track, Artist
from . import filters
from . import models
from .registries import registry
class SimpleRadio(object):
def clean(self, instance):
......@@ -50,7 +54,7 @@ class SessionRadio(SimpleRadio):
def filter_from_session(self, queryset):
already_played = self.session.session_tracks.all().values_list('track', flat=True)
queryset = queryset.exclude(pk__in=list(already_played))
queryset = queryset.exclude(pk__in=already_played)
return queryset
def pick(self, **kwargs):
......@@ -64,6 +68,10 @@ class SessionRadio(SimpleRadio):
self.session.add(choice)
return picked_choices
def validate_session(self, data, **context):
return data
@registry.register(name='random')
class RandomRadio(SessionRadio):
def get_queryset(self, **kwargs):
......@@ -83,6 +91,37 @@ class FavoritesRadio(SessionRadio):
return Track.objects.filter(pk__in=track_ids)
@registry.register(name='custom')
class CustomRadio(SessionRadio):
def get_queryset_kwargs(self):
kwargs = super().get_queryset_kwargs()
kwargs['user'] = self.session.user
kwargs['custom_radio'] = self.session.custom_radio
return kwargs
def get_queryset(self, **kwargs):
return filters.run(kwargs['custom_radio'].config)
def validate_session(self, data, **context):
data = super().validate_session(data, **context)
try:
user = data['user']
except KeyError:
user = context['user']
try:
assert (
data['custom_radio'].user == user or
data['custom_radio'].is_public)
except KeyError:
raise serializers.ValidationError(
'You must provide a custom radio')
except AssertionError:
raise serializers.ValidationError(
"You don't have access to this radio")
return data
class RelatedObjectRadio(SessionRadio):
"""Abstract radio related to an object (tag, artist, user...)"""
......
from rest_framework import serializers
from funkwhale_api.music.serializers import TrackSerializerNested
from . import filters
from . import models
from .radios import registry
class FilterSerializer(serializers.Serializer):
type = serializers.CharField(source='code')
label = serializers.CharField()
help_text = serializers.CharField()
fields = serializers.ReadOnlyField()
class RadioSerializer(serializers.ModelSerializer):
class Meta:
model = models.Radio
fields = (
'id',
'is_public',
'name',
'creation_date',
'user',
'config',
'description')
read_only_fields = ('user', 'creation_date')
def save(self, **kwargs):
kwargs['config'] = [
filters.registry[f['type']].clean_config(f)
for f in self.validated_data['config']
]
return super().save(**kwargs)
class RadioSessionTrackSerializerCreate(serializers.ModelSerializer):
class Meta:
......@@ -21,7 +52,18 @@ class RadioSessionTrackSerializer(serializers.ModelSerializer):
class RadioSessionSerializer(serializers.ModelSerializer):
class Meta:
model = models.RadioSession
fields = ('id', 'radio_type', 'related_object_id', 'user', 'creation_date', 'session_key')
fields = (
'id',
'radio_type',
'related_object_id',
'user',
'creation_date',
'custom_radio',
'session_key')
def validate(self, data):
registry[data['radio_type']]().validate_session(data, **self.context)
return data
def create(self, validated_data):
if self.context.get('user'):
......@@ -29,7 +71,6 @@ class RadioSessionSerializer(serializers.ModelSerializer):
else:
validated_data['session_key'] = self.context['session_key']
if validated_data.get('related_object_id'):
from . import radios
radio = radios.registry[validated_data['radio_type']]()
radio = registry[validated_data['radio_type']]()
validated_data['related_object'] = radio.get_related_object(validated_data['related_object_id'])
return super().create(validated_data)
......@@ -4,6 +4,7 @@ from . import views
from rest_framework import routers
router = routers.SimpleRouter()
router.register(r'sessions', views.RadioSessionViewSet, 'sessions')
router.register(r'radios', views.RadioViewSet, 'radios')
router.register(r'tracks', views.RadioSessionTrackViewSet, 'tracks')
......
from django.db.models import Q
from django.http import Http404
from rest_framework import generics, mixins, viewsets
from rest_framework import status
from rest_framework.response import Response
from rest_framework.decorators import detail_route
from rest_framework.decorators import detail_route, list_route
from funkwhale_api.music.serializers import TrackSerializerNested
from funkwhale_api.common.permissions import ConditionalAuthentication
from . import models
from . import filters
from . import filtersets
from . import serializers
class RadioViewSet(
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.ListModelMixin,
viewsets.GenericViewSet):
serializer_class = serializers.RadioSerializer
permission_classes = [ConditionalAuthentication]
filter_class = filtersets.RadioFilter
def get_queryset(self):
query = Q(is_public=True)
if self.request.user.is_authenticated:
query |= Q(user=self.request.user)
return models.Radio.objects.filter(query)
def perform_create(self, serializer):
return serializer.save(user=self.request.user)
def perform_update(self, serializer):
if serializer.instance.user != self.request.user:
raise Http404
return serializer.save(user=self.request.user)
@list_route(methods=['get'])
def filters(self, request, *args, **kwargs):
serializer = serializers.FilterSerializer(
filters.registry.exposed_filters, many=True)
return Response(serializer.data)
@list_route(methods=['post'])
def validate(self, request, *args, **kwargs):
try:
f_list = request.data['filters']
except KeyError:
return Response(
{'error': 'You must provide a filters list'}, status=400)
data = {
'filters': []
}
for f in f_list:
results = filters.test(f)
if results['candidates']['sample']:
qs = results['candidates']['sample'].for_nested_serialization()
results['candidates']['sample'] = TrackSerializerNested(
qs, many=True).data
data['filters'].append(results)
return Response(data)
class RadioSessionViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet):
......
......@@ -27,11 +27,12 @@ class CeleryConfig(AppConfig):
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS, force=True)
def require_instance(model_or_qs, parameter_name):
def require_instance(model_or_qs, parameter_name, id_kwarg_name=None):
def decorator(function):
@functools.wraps(function)
def inner(*args, **kwargs):
pk = kwargs.pop('_'.join([parameter_name, 'id']))
kw = id_kwarg_name or '_'.join([parameter_name, 'id'])
pk = kwargs.pop(kw)
try:
instance = model_or_qs.get(pk=pk)
except AttributeError:
......
import json
import pytest
from django.urls import reverse
from funkwhale_api.music.serializers import TrackSerializerNested
from funkwhale_api.radios import filters
from funkwhale_api.radios import serializers
def test_can_list_config_options(logged_in_client):
url = reverse('api:v1:radios:radios-filters')
response = logged_in_client.get(url)
assert response.status_code == 200
payload = json.loads(response.content.decode('utf-8'))
expected = [f for f in filters.registry.values() if f.expose_in_api]
assert len(payload) == len(expected)
def test_can_validate_config(logged_in_client, factories):
artist1 = factories['music.Artist']()
artist2 = factories['music.Artist']()
factories['music.Track'].create_batch(3, artist=artist1)
factories['music.Track'].create_batch(3, artist=artist2)
candidates = artist1.tracks.order_by('pk')
f = {
'filters': [
{'type': 'artist', 'ids': [artist1.pk]}
]
}
url = reverse('api:v1:radios:radios-validate')
response = logged_in_client.post(
url,
json.dumps(f),
content_type="application/json")
assert response.status_code == 200
payload = json.loads(response.content.decode('utf-8'))
expected = {
'count': candidates.count(),
'sample': TrackSerializerNested(candidates, many=True).data
}
assert payload['filters'][0]['candidates'] == expected
assert payload['filters'][0]['errors'] == []
def test_can_validate_config_with_wrong_config(logged_in_client, factories):
f = {
'filters': [
{'type': 'artist', 'ids': [999]}
]
}
url = reverse('api:v1:radios:radios-validate')
response = logged_in_client.post(
url,
json.dumps(f),
content_type="application/json")
assert response.status_code == 200
payload = json.loads(response.content.decode('utf-8'))
expected = {
'count': None,
'sample': None
}
assert payload['filters'][0]['candidates'] == expected
assert len(payload['filters'][0]['errors']) == 1
def test_saving_radio_sets_user(logged_in_client, factories):
artist = factories['music.Artist']()
f = {
'name': 'Test',
'config': [
{'type': 'artist', 'ids': [artist.pk]}
]
}
url = reverse('api:v1:radios:radios-list')
response = logged_in_client.post(
url,
json.dumps(f),
content_type="application/json")
assert response.status_code == 201
radio = logged_in_client.user.radios.latest('id')
assert radio.name == 'Test'
assert radio.user == logged_in_client.user
def test_user_can_detail_his_radio(logged_in_client, factories):
radio = factories['radios.Radio'](user=logged_in_client.user)
url = reverse('api:v1:radios:radios-detail', kwargs={'pk': radio.pk})
response = logged_in_client.get(url)
assert response.status_code == 200
def test_user_can_detail_public_radio(logged_in_client, factories):
radio = factories['radios.Radio'](is_public=True)
url = reverse('api:v1:radios:radios-detail', kwargs={'pk': radio.pk})
response = logged_in_client.get(url)
assert response.status_code == 200
def test_user_cannot_detail_someone_else_radio(logged_in_client, factories):
radio = factories['radios.Radio'](is_public=False)
url = reverse('api:v1:radios:radios-detail', kwargs={'pk': radio.pk})
response = logged_in_client.get(url)
assert response.status_code == 404
def test_user_can_edit_his_radio(logged_in_client, factories):
radio = factories['radios.Radio'](user=logged_in_client.user)
url = reverse('api:v1:radios:radios-detail', kwargs={'pk': radio.pk})
response = logged_in_client.put(
url,
json.dumps({'name': 'new', 'config': []}),
content_type="application/json")
radio.refresh_from_db()
assert response.status_code == 200
assert radio.name == 'new'
def test_user_cannot_edit_someone_else_radio(logged_in_client, factories):
radio = factories['radios.Radio']()
url = reverse('api:v1:radios:radios-detail', kwargs={'pk': radio.pk})
response = logged_in_client.put(
url,
json.dumps({'name': 'new', 'config': []}),
content_type="application/json")
assert response.status_code == 404
def test_clean_config_is_called_on_serializer_save(mocker, factories):
user = factories['users.User']()
artist = factories['music.Artist']()
data= {
'name': 'Test',
'config': [
{'type': 'artist', 'ids': [artist.pk]}
]
}
spied = mocker.spy(filters.registry['artist'], 'clean_config')
serializer = serializers.RadioSerializer(data=data)
assert serializer.is_valid()
instance = serializer.save(user=user)
spied.assert_called_once_with(data['config'][0])
assert instance.config[0]['names'] == [artist.name]
import pytest
from django.core.exceptions import ValidationError
from funkwhale_api.music.models import Track
from funkwhale_api.radios import filters
@filters.registry.register
class NoopFilter(filters.RadioFilter):
code = 'noop'
def get_query(self, candidates, **kwargs):
return
def test_most_simple_radio_does_not_filter_anything(factories):
tracks = factories['music.Track'].create_batch(3)
radio = factories['radios.Radio'](config=[{'type': 'noop'}])
assert radio.version == 0
assert radio.get_candidates().count() == 3
def test_filter_can_use_custom_queryset(factories):
tracks = factories['music.Track'].create_batch(3)
candidates = Track.objects.filter(pk=tracks[0].pk)
qs = filters.run([{'type': 'noop'}], candidates=candidates)
assert qs.count() == 1
assert qs.first() == tracks[0]
def test_filter_on_tag(factories):
tracks = factories['music.Track'].create_batch(3, tags=['metal'])
factories['music.Track'].create_batch(3, tags=['pop'])
expected = tracks
f = [
{'type': 'tag', 'names': ['metal']}
]
candidates = filters.run(f)
assert list(candidates.order_by('pk')) == expected
def test_filter_on_artist(factories):
artist1 = factories['music.Artist']()
artist2 = factories['music.Artist']()
factories['music.Track'].create_batch(3, artist=artist1)
factories['music.Track'].create_batch(3, artist=artist2)
expected = list(artist1.tracks.order_by('pk'))
f = [
{'type': 'artist', 'ids': [artist1.pk]}
]
candidates = filters.run(f)
assert list(candidates.order_by('pk')) == expected
def test_can_combine_with_or(factories):
artist1 = factories['music.Artist']()
artist2 = factories['music.Artist']()
artist3 = factories['music.Artist']()
factories['music.Track'].create_batch(3, artist=artist1)
factories['music.Track'].create_batch(3, artist=artist2)
factories['music.Track'].create_batch(3, artist=artist3)
expected = Track.objects.exclude(artist=artist3).order_by('pk')
f = [
{'type': 'artist', 'ids': [artist1.pk]},
{'type': 'artist', 'ids': [artist2.pk], 'operator': 'or'},
]
candidates = filters.run(f)
assert list(candidates.order_by('pk')) == list(expected)
def test_can_combine_with_and(factories):
artist1 = factories['music.Artist']()
artist2 = factories['music.Artist']()
metal_tracks = factories['music.Track'].create_batch(
2, artist=artist1, tags=['metal'])
factories['music.Track'].create_batch(2, artist=artist1, tags=['pop'])
factories['music.Track'].create_batch(3, artist=artist2)
expected = metal_tracks
f = [
{'type': 'artist', 'ids': [artist1.pk]},
{'type': 'tag', 'names': ['metal'], 'operator': 'and'},
]
candidates = filters.run(f)
assert list(candidates.order_by('pk')) == list(expected)
def test_can_negate(factories):
artist1 = factories['music.Artist']()
artist2 = factories['music.Artist']()
factories['music.Track'].create_batch(3, artist=artist1)
factories['music.Track'].create_batch(3, artist=artist2)
expected = artist2.tracks.order_by('pk')
f = [
{'type': 'artist', 'ids': [artist1.pk], 'not': True},
]
candidates = filters.run(f)
assert list(candidates.order_by('pk')) == list(expected)
def test_can_group(factories):
artist1 = factories['music.Artist']()
artist2 = factories['music.Artist']()
factories['music.Track'].create_batch(2, artist=artist1)
t1 = factories['music.Track'].create_batch(
2, artist=artist1, tags=['metal'])
factories['music.Track'].create_batch(2, artist=artist2)
t2 = factories['music.Track'].create_batch(
2, artist=artist2, tags=['metal'])
factories['music.Track'].create_batch(2, tags=['metal'])
expected = t1 + t2
f = [
{'type': 'tag', 'names': ['metal']},
{'type': 'group', 'operator': 'and', 'filters': [
{'type': 'artist', 'ids': [artist1.pk], 'operator': 'or'},
{'type': 'artist', 'ids': [artist2.pk], 'operator': 'or'},
]}
]
candidates = filters.run(f)
assert list(candidates.order_by('pk')) == list(expected)
def test_artist_filter_clean_config(factories):
artist1 = factories['music.Artist']()
artist2 = factories['music.Artist']()
config = filters.clean_config(
{'type': 'artist', 'ids': [artist2.pk, artist1.pk]})
expected = {
'type': 'artist',
'ids': [artist1.pk, artist2.pk],
'names': [artist1.name, artist2.name]
}
assert filters.clean_config(config) == expected
def test_can_check_artist_filter(factories):
artist = factories['music.Artist']()
assert filters.validate({'type': 'artist', 'ids': [artist.pk]})
with pytest.raises(ValidationError):
filters.validate({'type': 'artist', 'ids': [artist.pk + 1]})
def test_can_check_operator():
assert filters.validate(
{'type': 'group', 'operator': 'or', 'filters': []})
assert filters.validate(
{'type': 'group', 'operator': 'and', 'filters': []})
with pytest.raises(ValidationError):
assert filters.validate(
{'type': 'group', 'operator': 'nope', 'filters': []})
......@@ -8,6 +8,7 @@ from django.core.exceptions import ValidationError
from funkwhale_api.radios import radios
from funkwhale_api.radios import models
from funkwhale_api.radios import serializers
from funkwhale_api.favorites.models import TrackFavorite
......@@ -50,9 +51,9 @@ def test_can_pick_by_weight():
def test_can_get_choices_for_favorites_radio(factories):
tracks = factories['music.Track'].create_batch(100)
tracks = factories['music.Track'].create_batch(10)
user = factories['users.User']()
for i in range(20):
for i in range(5):
TrackFavorite.add(track=random.choice(tracks), user=user)
radio = radios.FavoritesRadio()
......@@ -63,11 +64,54 @@ def test_can_get_choices_for_favorites_radio(factories):
for favorite in user.track_favorites.all():
assert favorite.track in choices
for i in range(20):
for i in range(5):
pick = radio.pick(user=user)
assert pick in choices
def test_can_get_choices_for_custom_radio(factories):
artist = factories['music.Artist']()
tracks = factories['music.Track'].create_batch(5, artist=artist)
wrong_tracks = factories['music.Track'].create_batch(5)
session = factories['radios.CustomRadioSession'](
custom_radio__config=[{'type': 'artist', 'ids': [artist.pk]}]
)
choices = session.radio.get_choices()
expected = [t.pk for t in tracks]
assert list(choices.values_list('id', flat=True)) == expected
def test_cannot_start_custom_radio_if_not_owner_or_not_public(factories):
user = factories['users.User']()
artist = factories['music.Artist']()
radio = factories['radios.Radio'](
config=[{'type': 'artist', 'ids': [artist.pk]}]
)
serializer = serializers.RadioSessionSerializer(
data={
'radio_type': 'custom', 'custom_radio': radio.pk, 'user': user.pk}
)
message = "You don't have access to this radio"
assert not serializer.is_valid()
assert message in serializer.errors['non_field_errors']
def test_can_start_custom_radio_from_api(logged_in_client, factories):
artist = factories['music.Artist']()
radio = factories['radios.Radio'](
config=[{'type': 'artist', 'ids': [artist.pk]}],
user=logged_in_client.user
)
url = reverse('api:v1:radios:sessions-list')
response = logged_in_client.post(
url, {'radio_type': 'custom', 'custom_radio': radio.pk})
assert response.status_code == 201
session = radio.sessions.latest('id')
assert session.radio_type == 'custom'
assert session.user == logged_in_client.user
def test_can_use_radio_session_to_filter_choices(factories):
tracks = factories['music.Track'].create_batch(30)
user = factories['users.User']()
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment