Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
svfusion
funkwhale
Commits
9ecb2bdf
Verified
Commit
9ecb2bdf
authored
Jan 07, 2018
by
Eliot Berriot
Browse files
Merge branch 'release/0.3.3'
parents
3a19505c
56c22027
Changes
29
Hide whitespace changes
Inline
Side-by-side
.gitlab-ci.yml
View file @
9ecb2bdf
...
...
@@ -11,11 +11,14 @@ stages:
-
deploy
test_api
:
services
:
-
postgres:9.4
stage
:
test
image
:
funkwhale/funkwhale:base
variables
:
PIP_CACHE_DIR
:
"
$CI_PROJECT_DIR/pip-cache"
DATABASE_URL
:
"
sqlite://"
DATABASE_URL
:
"
postgresql://postgres@postgres/postgres"
before_script
:
-
python3 -m venv --copies virtualenv
-
source virtualenv/bin/activate
...
...
CHANGELOG
View file @
9ecb2bdf
...
...
@@ -2,10 +2,18 @@ Changelog
=========
0.3.
3
(Unreleased)
0.3.
4
(Unreleased)
------------------
0.3.4 (2018-01-07)
------------------
- Users can now create their own dynamic radios (#51)
0.3.2
------------------
...
...
api/config/settings/test.py
View file @
9ecb2bdf
from
.common
import
*
# noqa
SECRET_KEY
=
env
(
"DJANGO_SECRET_KEY"
,
default
=
'test'
)
DATABASES
=
{
'default'
:
{
'ENGINE'
:
'django.db.backends.sqlite3'
,
'NAME'
:
':memory:'
,
}
}
# Mail settings
# ------------------------------------------------------------------------------
...
...
api/funkwhale_api/__init__.py
View file @
9ecb2bdf
# -*- coding: utf-8 -*-
__version__
=
'0.3.
2
'
__version__
=
'0.3.
3
'
__version_info__
=
tuple
([
int
(
num
)
if
num
.
isdigit
()
else
num
for
num
in
__version__
.
replace
(
'-'
,
'.'
,
1
).
split
(
'.'
)])
api/funkwhale_api/music/models.py
View file @
9ecb2bdf
...
...
@@ -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
:
...
...
api/funkwhale_api/music/views.py
View file @
9ecb2bdf
...
...
@@ -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'
]
...
...
api/funkwhale_api/radios/factories.py
0 → 100644
View file @
9ecb2bdf
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'
api/funkwhale_api/radios/filters.py
0 → 100644
View file @
9ecb2bdf
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
)
api/funkwhale_api/radios/filtersets.py
0 → 100644
View file @
9ecb2bdf
import
django_filters
from
.
import
models
class
RadioFilter
(
django_filters
.
FilterSet
):
class
Meta
:
model
=
models
.
Radio
fields
=
{
'name'
:
[
'exact'
,
'iexact'
,
'startswith'
,
'icontains'
]
}
api/funkwhale_api/radios/migrations/0004_auto_20180107_1813.py
0 → 100644
View file @
9ecb2bdf
# Generated by Django 2.0 on 2018-01-07 18:13
from
django.conf
import
settings
import
django.contrib.postgres.fields.jsonb
from
django.db
import
migrations
,
models
import
django.db.models.deletion
import
django.utils.timezone
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
migrations
.
swappable_dependency
(
settings
.
AUTH_USER_MODEL
),
(
'radios'
,
'0003_auto_20160521_1708'
),
]
operations
=
[
migrations
.
CreateModel
(
name
=
'Radio'
,
fields
=
[
(
'id'
,
models
.
AutoField
(
auto_created
=
True
,
primary_key
=
True
,
serialize
=
False
,
verbose_name
=
'ID'
)),
(
'name'
,
models
.
CharField
(
max_length
=
100
)),
(
'description'
,
models
.
TextField
(
blank
=
True
)),
(
'creation_date'
,
models
.
DateTimeField
(
default
=
django
.
utils
.
timezone
.
now
)),
(
'is_public'
,
models
.
BooleanField
(
default
=
False
)),
(
'version'
,
models
.
PositiveIntegerField
(
default
=
0
)),
(
'config'
,
django
.
contrib
.
postgres
.
fields
.
jsonb
.
JSONField
()),
(
'user'
,
models
.
ForeignKey
(
blank
=
True
,
null
=
True
,
on_delete
=
django
.
db
.
models
.
deletion
.
CASCADE
,
related_name
=
'radios'
,
to
=
settings
.
AUTH_USER_MODEL
)),
],
),
migrations
.
AddField
(
model_name
=
'radiosession'
,
name
=
'custom_radio'
,
field
=
models
.
ForeignKey
(
blank
=
True
,
null
=
True
,
on_delete
=
django
.
db
.
models
.
deletion
.
CASCADE
,
related_name
=
'sessions'
,
to
=
'radios.Radio'
),
),
]
api/funkwhale_api/radios/models.py
View file @
9ecb2bdf
from
django.db
import
models
from
django.utils
import
timezone
from
django.core.exceptions
import
ValidationError
from
django.contrib.postgres.fields
import
JSONField
from
django.contrib.contenttypes.fields
import
GenericForeignKey
from
django.contrib.contenttypes.models
import
ContentType
from
funkwhale_api.music.models
import
Track
from
.
import
filters
class
Radio
(
models
.
Model
):
CONFIG_VERSION
=
0
user
=
models
.
ForeignKey
(
'users.User'
,
related_name
=
'radios'
,
null
=
True
,
blank
=
True
,
on_delete
=
models
.
CASCADE
)
name
=
models
.
CharField
(
max_length
=
100
)
description
=
models
.
TextField
(
blank
=
True
)
creation_date
=
models
.
DateTimeField
(
default
=
timezone
.
now
)
is_public
=
models
.
BooleanField
(
default
=
False
)
version
=
models
.
PositiveIntegerField
(
default
=
0
)
config
=
JSONField
()
def
get_candidates
(
self
):
return
filters
.
run
(
self
.
config
)
class
RadioSession
(
models
.
Model
):
user
=
models
.
ForeignKey
(
'users.User'
,
...
...
@@ -15,6 +38,12 @@ class RadioSession(models.Model):
on_delete
=
models
.
CASCADE
)
session_key
=
models
.
CharField
(
max_length
=
100
,
null
=
True
,
blank
=
True
)
radio_type
=
models
.
CharField
(
max_length
=
50
)
custom_radio
=
models
.
ForeignKey
(
Radio
,
related_name
=
'sessions'
,
null
=
True
,
blank
=
True
,
on_delete
=
models
.
CASCADE
)
creation_date
=
models
.
DateTimeField
(
default
=
timezone
.
now
)
related_object_content_type
=
models
.
ForeignKey
(
ContentType
,
...
...
@@ -51,6 +80,7 @@ class RadioSession(models.Model):
from
.
import
radios
return
registry
[
self
.
radio_type
](
session
=
self
)
class
RadioSessionTrack
(
models
.
Model
):
session
=
models
.
ForeignKey
(
RadioSession
,
related_name
=
'session_tracks'
,
on_delete
=
models
.
CASCADE
)
...
...
api/funkwhale_api/radios/radios.py
View file @
9ecb2bdf
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...)"""
...
...
api/funkwhale_api/radios/serializers.py
View file @
9ecb2bdf
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
)
api/funkwhale_api/radios/urls.py
View file @
9ecb2bdf
...
...
@@ -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'
)
...
...
api/funkwhale_api/radios/views.py
View file @
9ecb2bdf
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