Merge branch 'release/0.1'
This commit is contained in:
commit
8335b1acc1
2
.env.dev
2
.env.dev
|
@ -1,3 +1,3 @@
|
||||||
BACKEND_URL=http://localhost:6001
|
BACKEND_URL=http://localhost:12081
|
||||||
YOUTUBE_API_KEY=
|
YOUTUBE_API_KEY=
|
||||||
API_AUTHENTICATION_REQUIRED=False
|
API_AUTHENTICATION_REQUIRED=False
|
||||||
|
|
|
@ -1,18 +1,26 @@
|
||||||
|
variables:
|
||||||
|
IMAGE_NAME: funkwhale/funkwhale
|
||||||
|
IMAGE: $IMAGE_NAME:$CI_BUILD_REF
|
||||||
|
IMAGE_LATEST: $IMAGE_NAME:latest
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
- build
|
- build
|
||||||
|
- deploy
|
||||||
|
|
||||||
test_api:
|
test_api:
|
||||||
stage: test
|
stage: test
|
||||||
|
image: funkwhale/funkwhale:base
|
||||||
before_script:
|
before_script:
|
||||||
- docker-compose -f api/test.yml build
|
- cd api
|
||||||
|
- pip install -r requirements/test.txt
|
||||||
script:
|
script:
|
||||||
- docker-compose -f api/test.yml run test
|
- pytest
|
||||||
after_script:
|
|
||||||
- docker-compose -f api/test.yml run test rm -rf funkwhale_api/media/
|
|
||||||
|
|
||||||
tags:
|
tags:
|
||||||
- dind
|
- docker
|
||||||
|
|
||||||
build_front:
|
build_front:
|
||||||
stage: build
|
stage: build
|
||||||
|
@ -53,6 +61,33 @@ pages:
|
||||||
paths:
|
paths:
|
||||||
- public
|
- public
|
||||||
only:
|
only:
|
||||||
- master
|
- develop
|
||||||
tags:
|
tags:
|
||||||
- docker
|
- docker
|
||||||
|
|
||||||
|
docker_develop:
|
||||||
|
stage: deploy
|
||||||
|
before_script:
|
||||||
|
- docker login -u $DOCKER_LOGIN -p $DOCKER_PASSWORD
|
||||||
|
- cd api
|
||||||
|
script:
|
||||||
|
- docker build -t $IMAGE .
|
||||||
|
- docker push $IMAGE
|
||||||
|
only:
|
||||||
|
- develop
|
||||||
|
tags:
|
||||||
|
- dind
|
||||||
|
|
||||||
|
docker_release:
|
||||||
|
stage: deploy
|
||||||
|
before_script:
|
||||||
|
- docker login -u $DOCKER_LOGIN -p $DOCKER_PASSWORD
|
||||||
|
- cd api
|
||||||
|
script:
|
||||||
|
- docker build -t $IMAGE -t $IMAGE_LATEST .
|
||||||
|
- docker push $IMAGE
|
||||||
|
- docker push $IMAGE_LATEST
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
tags:
|
||||||
|
- dind
|
||||||
|
|
|
@ -2,6 +2,8 @@ from rest_framework import routers
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
from funkwhale_api.music import views
|
from funkwhale_api.music import views
|
||||||
from funkwhale_api.playlists import views as playlists_views
|
from funkwhale_api.playlists import views as playlists_views
|
||||||
|
from rest_framework_jwt import views as jwt_views
|
||||||
|
|
||||||
|
|
||||||
router = routers.SimpleRouter()
|
router = routers.SimpleRouter()
|
||||||
router.register(r'tags', views.TagViewSet, 'tags')
|
router.register(r'tags', views.TagViewSet, 'tags')
|
||||||
|
@ -12,15 +14,19 @@ router.register(r'import-batches', views.ImportBatchViewSet, 'import-batches')
|
||||||
router.register(r'submit', views.SubmitViewSet, 'submit')
|
router.register(r'submit', views.SubmitViewSet, 'submit')
|
||||||
router.register(r'playlists', playlists_views.PlaylistViewSet, 'playlists')
|
router.register(r'playlists', playlists_views.PlaylistViewSet, 'playlists')
|
||||||
router.register(r'playlist-tracks', playlists_views.PlaylistTrackViewSet, 'playlist-tracks')
|
router.register(r'playlist-tracks', playlists_views.PlaylistTrackViewSet, 'playlist-tracks')
|
||||||
urlpatterns = router.urls
|
v1_patterns = router.urls
|
||||||
|
|
||||||
urlpatterns += [
|
v1_patterns += [
|
||||||
url(r'^providers/', include('funkwhale_api.providers.urls', namespace='providers')),
|
url(r'^providers/', include('funkwhale_api.providers.urls', namespace='providers')),
|
||||||
url(r'^favorites/', include('funkwhale_api.favorites.urls', namespace='favorites')),
|
url(r'^favorites/', include('funkwhale_api.favorites.urls', namespace='favorites')),
|
||||||
url(r'^search$', views.Search.as_view(), name='search'),
|
url(r'^search$', views.Search.as_view(), name='search'),
|
||||||
url(r'^radios/', include('funkwhale_api.radios.urls', namespace='radios')),
|
url(r'^radios/', include('funkwhale_api.radios.urls', namespace='radios')),
|
||||||
url(r'^history/', include('funkwhale_api.history.urls', namespace='history')),
|
url(r'^history/', include('funkwhale_api.history.urls', namespace='history')),
|
||||||
url(r'^users/', include('funkwhale_api.users.api_urls', namespace='users')),
|
url(r'^users/', include('funkwhale_api.users.api_urls', namespace='users')),
|
||||||
url(r'^token/', 'rest_framework_jwt.views.obtain_jwt_token'),
|
url(r'^token/', jwt_views.obtain_jwt_token),
|
||||||
url(r'^token/refresh/', 'rest_framework_jwt.views.refresh_jwt_token'),
|
url(r'^token/refresh/', jwt_views.refresh_jwt_token),
|
||||||
|
]
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^v1/', include(v1_patterns, namespace='v1'))
|
||||||
]
|
]
|
||||||
|
|
|
@ -199,7 +199,7 @@ CRISPY_TEMPLATE_PACK = 'bootstrap3'
|
||||||
STATIC_ROOT = str(ROOT_DIR('staticfiles'))
|
STATIC_ROOT = str(ROOT_DIR('staticfiles'))
|
||||||
|
|
||||||
# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url
|
# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url
|
||||||
STATIC_URL = env("STATIC_URL", default='/static/')
|
STATIC_URL = env("STATIC_URL", default='/staticfiles/')
|
||||||
|
|
||||||
# See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS
|
# See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS
|
||||||
STATICFILES_DIRS = (
|
STATICFILES_DIRS = (
|
||||||
|
|
|
@ -24,7 +24,7 @@ class TestFavorites(TestCase):
|
||||||
def test_user_can_get_his_favorites(self):
|
def test_user_can_get_his_favorites(self):
|
||||||
favorite = TrackFavorite.add(self.track, self.user)
|
favorite = TrackFavorite.add(self.track, self.user)
|
||||||
|
|
||||||
url = reverse('api:favorites:tracks-list')
|
url = reverse('api:v1:favorites:tracks-list')
|
||||||
self.client.login(username=self.user.username, password='test')
|
self.client.login(username=self.user.username, password='test')
|
||||||
|
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
|
@ -41,7 +41,7 @@ class TestFavorites(TestCase):
|
||||||
self.assertEqual(expected, parsed_json['results'])
|
self.assertEqual(expected, parsed_json['results'])
|
||||||
|
|
||||||
def test_user_can_add_favorite_via_api(self):
|
def test_user_can_add_favorite_via_api(self):
|
||||||
url = reverse('api:favorites:tracks-list')
|
url = reverse('api:v1:favorites:tracks-list')
|
||||||
self.client.login(username=self.user.username, password='test')
|
self.client.login(username=self.user.username, password='test')
|
||||||
response = self.client.post(url, {'track': self.track.pk})
|
response = self.client.post(url, {'track': self.track.pk})
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ class TestFavorites(TestCase):
|
||||||
def test_user_can_remove_favorite_via_api(self):
|
def test_user_can_remove_favorite_via_api(self):
|
||||||
favorite = TrackFavorite.add(self.track, self.user)
|
favorite = TrackFavorite.add(self.track, self.user)
|
||||||
|
|
||||||
url = reverse('api:favorites:tracks-detail', kwargs={'pk': favorite.pk})
|
url = reverse('api:v1:favorites:tracks-detail', kwargs={'pk': favorite.pk})
|
||||||
self.client.login(username=self.user.username, password='test')
|
self.client.login(username=self.user.username, password='test')
|
||||||
response = self.client.delete(url, {'track': self.track.pk})
|
response = self.client.delete(url, {'track': self.track.pk})
|
||||||
self.assertEqual(response.status_code, 204)
|
self.assertEqual(response.status_code, 204)
|
||||||
|
@ -69,7 +69,7 @@ class TestFavorites(TestCase):
|
||||||
def test_user_can_remove_favorite_via_api_using_track_id(self):
|
def test_user_can_remove_favorite_via_api_using_track_id(self):
|
||||||
favorite = TrackFavorite.add(self.track, self.user)
|
favorite = TrackFavorite.add(self.track, self.user)
|
||||||
|
|
||||||
url = reverse('api:favorites:tracks-remove')
|
url = reverse('api:v1:favorites:tracks-remove')
|
||||||
self.client.login(username=self.user.username, password='test')
|
self.client.login(username=self.user.username, password='test')
|
||||||
response = self.client.delete(
|
response = self.client.delete(
|
||||||
url, json.dumps({'track': self.track.pk}),
|
url, json.dumps({'track': self.track.pk}),
|
||||||
|
@ -83,7 +83,7 @@ class TestFavorites(TestCase):
|
||||||
|
|
||||||
def test_can_restrict_api_views_to_authenticated_users(self):
|
def test_can_restrict_api_views_to_authenticated_users(self):
|
||||||
urls = [
|
urls = [
|
||||||
('api:favorites:tracks-list', 'get'),
|
('api:v1:favorites:tracks-list', 'get'),
|
||||||
]
|
]
|
||||||
|
|
||||||
for route_name, method in urls:
|
for route_name, method in urls:
|
||||||
|
@ -103,7 +103,7 @@ class TestFavorites(TestCase):
|
||||||
def test_can_filter_tracks_by_favorites(self):
|
def test_can_filter_tracks_by_favorites(self):
|
||||||
favorite = TrackFavorite.add(self.track, self.user)
|
favorite = TrackFavorite.add(self.track, self.user)
|
||||||
|
|
||||||
url = reverse('api:tracks-list')
|
url = reverse('api:v1:tracks-list')
|
||||||
self.client.login(username=self.user.username, password='test')
|
self.client.login(username=self.user.username, password='test')
|
||||||
|
|
||||||
response = self.client.get(url, data={'favorites': True})
|
response = self.client.get(url, data={'favorites': True})
|
||||||
|
|
|
@ -23,7 +23,7 @@ class TestHistory(TestCase):
|
||||||
|
|
||||||
def test_anonymous_user_can_create_listening_via_api(self):
|
def test_anonymous_user_can_create_listening_via_api(self):
|
||||||
track = mommy.make('music.Track')
|
track = mommy.make('music.Track')
|
||||||
url = self.reverse('api:history:listenings-list')
|
url = self.reverse('api:v1:history:listenings-list')
|
||||||
response = self.client.post(url, {
|
response = self.client.post(url, {
|
||||||
'track': track.pk,
|
'track': track.pk,
|
||||||
})
|
})
|
||||||
|
@ -38,7 +38,7 @@ class TestHistory(TestCase):
|
||||||
|
|
||||||
self.client.login(username=self.user.username, password='test')
|
self.client.login(username=self.user.username, password='test')
|
||||||
|
|
||||||
url = self.reverse('api:history:listenings-list')
|
url = self.reverse('api:v1:history:listenings-list')
|
||||||
response = self.client.post(url, {
|
response = self.client.post(url, {
|
||||||
'track': track.pk,
|
'track': track.pk,
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,34 +1,130 @@
|
||||||
import mutagen
|
import mutagen
|
||||||
|
import arrow
|
||||||
|
|
||||||
NODEFAULT = object()
|
NODEFAULT = object()
|
||||||
|
|
||||||
class Metadata(object):
|
|
||||||
ALIASES = {
|
class TagNotFound(KeyError):
|
||||||
'release': 'musicbrainz_albumid',
|
pass
|
||||||
'artist': 'musicbrainz_artistid',
|
|
||||||
'recording': 'musicbrainz_trackid',
|
|
||||||
|
def get_id3_tag(f, k):
|
||||||
|
# First we try to grab the standard key
|
||||||
|
try:
|
||||||
|
return f.tags[k].text[0]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
# then we fallback on parsing non standard tags
|
||||||
|
all_tags = f.tags.getall('TXXX')
|
||||||
|
try:
|
||||||
|
matches = [
|
||||||
|
t
|
||||||
|
for t in all_tags
|
||||||
|
if t.desc.lower() == k.lower()
|
||||||
|
]
|
||||||
|
return matches[0].text[0]
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
raise TagNotFound(k)
|
||||||
|
|
||||||
|
|
||||||
|
def get_mp3_recording_id(f, k):
|
||||||
|
try:
|
||||||
|
return [
|
||||||
|
t
|
||||||
|
for t in f.tags.getall('UFID')
|
||||||
|
if 'musicbrainz.org' in t.owner
|
||||||
|
][0].data.decode('utf-8')
|
||||||
|
except IndexError:
|
||||||
|
raise TagNotFound(k)
|
||||||
|
|
||||||
|
CONF = {
|
||||||
|
'OggVorbis': {
|
||||||
|
'getter': lambda f, k: f[k][0],
|
||||||
|
'fields': {
|
||||||
|
'track_number': {
|
||||||
|
'field': 'TRACKNUMBER',
|
||||||
|
'to_application': int
|
||||||
|
},
|
||||||
|
'title': {
|
||||||
|
'field': 'title'
|
||||||
|
},
|
||||||
|
'artist': {
|
||||||
|
'field': 'artist'
|
||||||
|
},
|
||||||
|
'album': {
|
||||||
|
'field': 'album'
|
||||||
|
},
|
||||||
|
'date': {
|
||||||
|
'field': 'date',
|
||||||
|
'to_application': lambda v: arrow.get(v).date()
|
||||||
|
},
|
||||||
|
'musicbrainz_albumid': {
|
||||||
|
'field': 'musicbrainz_albumid'
|
||||||
|
},
|
||||||
|
'musicbrainz_artistid': {
|
||||||
|
'field': 'musicbrainz_artistid'
|
||||||
|
},
|
||||||
|
'musicbrainz_recordingid': {
|
||||||
|
'field': 'musicbrainz_trackid'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'MP3': {
|
||||||
|
'getter': get_id3_tag,
|
||||||
|
'fields': {
|
||||||
|
'track_number': {
|
||||||
|
'field': 'TPOS',
|
||||||
|
'to_application': lambda v: int(v.split('/')[0])
|
||||||
|
},
|
||||||
|
'title': {
|
||||||
|
'field': 'TIT2'
|
||||||
|
},
|
||||||
|
'artist': {
|
||||||
|
'field': 'TPE1'
|
||||||
|
},
|
||||||
|
'album': {
|
||||||
|
'field': 'TALB'
|
||||||
|
},
|
||||||
|
'date': {
|
||||||
|
'field': 'TDRC',
|
||||||
|
'to_application': lambda v: arrow.get(str(v)).date()
|
||||||
|
},
|
||||||
|
'musicbrainz_albumid': {
|
||||||
|
'field': 'MusicBrainz Album Id'
|
||||||
|
},
|
||||||
|
'musicbrainz_artistid': {
|
||||||
|
'field': 'MusicBrainz Artist Id'
|
||||||
|
},
|
||||||
|
'musicbrainz_recordingid': {
|
||||||
|
'field': 'UFID',
|
||||||
|
'getter': get_mp3_recording_id,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Metadata(object):
|
||||||
|
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
self._file = mutagen.File(path)
|
self._file = mutagen.File(path)
|
||||||
|
self._conf = CONF[self.get_file_type(self._file)]
|
||||||
|
|
||||||
def get(self, key, default=NODEFAULT, single=True):
|
def get_file_type(self, f):
|
||||||
|
return f.__class__.__name__
|
||||||
|
|
||||||
|
def get(self, key, default=NODEFAULT):
|
||||||
|
field_conf = self._conf['fields'][key]
|
||||||
|
real_key = field_conf['field']
|
||||||
try:
|
try:
|
||||||
v = self._file[key]
|
getter = field_conf.get('getter', self._conf['getter'])
|
||||||
|
v = getter(self._file, real_key)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
if default == NODEFAULT:
|
if default == NODEFAULT:
|
||||||
raise
|
raise TagNotFound(real_key)
|
||||||
return default
|
return default
|
||||||
|
|
||||||
# Some tags are returned as lists of string
|
converter = field_conf.get('to_application')
|
||||||
if single:
|
if converter:
|
||||||
return v[0]
|
v = converter(v)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
def __getattr__(self, key):
|
|
||||||
try:
|
|
||||||
alias = self.ALIASES[key]
|
|
||||||
except KeyError:
|
|
||||||
raise ValueError('Invalid alias {}'.format(key))
|
|
||||||
|
|
||||||
return self.get(alias, single=True)
|
|
||||||
|
|
|
@ -314,7 +314,7 @@ class Track(APIModelMixin):
|
||||||
return work
|
return work
|
||||||
|
|
||||||
def get_lyrics_url(self):
|
def get_lyrics_url(self):
|
||||||
return reverse('api:tracks-lyrics', kwargs={'pk': self.pk})
|
return reverse('api:v1:tracks-lyrics', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def full_name(self):
|
def full_name(self):
|
||||||
|
|
Binary file not shown.
|
@ -20,7 +20,7 @@ class TestAPI(TMPDirTestCaseMixin, TestCase):
|
||||||
def test_can_submit_youtube_url_for_track_import(self, *mocks):
|
def test_can_submit_youtube_url_for_track_import(self, *mocks):
|
||||||
mbid = '9968a9d6-8d92-4051-8f76-674e157b6eed'
|
mbid = '9968a9d6-8d92-4051-8f76-674e157b6eed'
|
||||||
video_id = 'tPEE9ZwTmy0'
|
video_id = 'tPEE9ZwTmy0'
|
||||||
url = reverse('api:submit-single')
|
url = reverse('api:v1:submit-single')
|
||||||
user = User.objects.create_superuser(username='test', email='test@test.com', password='test')
|
user = User.objects.create_superuser(username='test', email='test@test.com', password='test')
|
||||||
self.client.login(username=user.username, password='test')
|
self.client.login(username=user.username, password='test')
|
||||||
response = self.client.post(url, {'import_url': 'https://www.youtube.com/watch?v={0}'.format(video_id), 'mbid': mbid})
|
response = self.client.post(url, {'import_url': 'https://www.youtube.com/watch?v={0}'.format(video_id), 'mbid': mbid})
|
||||||
|
@ -33,7 +33,7 @@ class TestAPI(TMPDirTestCaseMixin, TestCase):
|
||||||
user = User.objects.create_superuser(username='test', email='test@test.com', password='test')
|
user = User.objects.create_superuser(username='test', email='test@test.com', password='test')
|
||||||
mbid = '9968a9d6-8d92-4051-8f76-674e157b6eed'
|
mbid = '9968a9d6-8d92-4051-8f76-674e157b6eed'
|
||||||
video_id = 'tPEE9ZwTmy0'
|
video_id = 'tPEE9ZwTmy0'
|
||||||
url = reverse('api:submit-single')
|
url = reverse('api:v1:submit-single')
|
||||||
self.client.login(username=user.username, password='test')
|
self.client.login(username=user.username, password='test')
|
||||||
with self.settings(CELERY_ALWAYS_EAGER=False):
|
with self.settings(CELERY_ALWAYS_EAGER=False):
|
||||||
response = self.client.post(url, {'import_url': 'https://www.youtube.com/watch?v={0}'.format(video_id), 'mbid': mbid})
|
response = self.client.post(url, {'import_url': 'https://www.youtube.com/watch?v={0}'.format(video_id), 'mbid': mbid})
|
||||||
|
@ -69,7 +69,7 @@ class TestAPI(TMPDirTestCaseMixin, TestCase):
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
url = reverse('api:submit-album')
|
url = reverse('api:v1:submit-album')
|
||||||
self.client.login(username=user.username, password='test')
|
self.client.login(username=user.username, password='test')
|
||||||
with self.settings(CELERY_ALWAYS_EAGER=False):
|
with self.settings(CELERY_ALWAYS_EAGER=False):
|
||||||
response = self.client.post(url, json.dumps(payload), content_type="application/json")
|
response = self.client.post(url, json.dumps(payload), content_type="application/json")
|
||||||
|
@ -123,7 +123,7 @@ class TestAPI(TMPDirTestCaseMixin, TestCase):
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
url = reverse('api:submit-artist')
|
url = reverse('api:v1:submit-artist')
|
||||||
self.client.login(username=user.username, password='test')
|
self.client.login(username=user.username, password='test')
|
||||||
with self.settings(CELERY_ALWAYS_EAGER=False):
|
with self.settings(CELERY_ALWAYS_EAGER=False):
|
||||||
response = self.client.post(url, json.dumps(payload), content_type="application/json")
|
response = self.client.post(url, json.dumps(payload), content_type="application/json")
|
||||||
|
@ -159,7 +159,7 @@ class TestAPI(TMPDirTestCaseMixin, TestCase):
|
||||||
batch = models.ImportBatch.objects.create(submitted_by=user1)
|
batch = models.ImportBatch.objects.create(submitted_by=user1)
|
||||||
job = models.ImportJob.objects.create(batch=batch, mbid=mbid, source=source)
|
job = models.ImportJob.objects.create(batch=batch, mbid=mbid, source=source)
|
||||||
|
|
||||||
url = reverse('api:import-batches-list')
|
url = reverse('api:v1:import-batches-list')
|
||||||
|
|
||||||
self.client.login(username=user2.username, password='test')
|
self.client.login(username=user2.username, password='test')
|
||||||
response2 = self.client.get(url)
|
response2 = self.client.get(url)
|
||||||
|
@ -175,7 +175,7 @@ class TestAPI(TMPDirTestCaseMixin, TestCase):
|
||||||
artist2 = models.Artist.objects.create(name='Test2')
|
artist2 = models.Artist.objects.create(name='Test2')
|
||||||
query = 'test1'
|
query = 'test1'
|
||||||
expected = '[{0}]'.format(json.dumps(serializers.ArtistSerializerNested(artist1).data))
|
expected = '[{0}]'.format(json.dumps(serializers.ArtistSerializerNested(artist1).data))
|
||||||
url = self.reverse('api:artists-search')
|
url = self.reverse('api:v1:artists-search')
|
||||||
response = self.client.get(url + '?query={0}'.format(query))
|
response = self.client.get(url + '?query={0}'.format(query))
|
||||||
|
|
||||||
self.assertJSONEqual(expected, json.loads(response.content.decode('utf-8')))
|
self.assertJSONEqual(expected, json.loads(response.content.decode('utf-8')))
|
||||||
|
@ -187,17 +187,17 @@ class TestAPI(TMPDirTestCaseMixin, TestCase):
|
||||||
track2 = models.Track.objects.create(artist=artist2, title="test_track2")
|
track2 = models.Track.objects.create(artist=artist2, title="test_track2")
|
||||||
query = 'test track 1'
|
query = 'test track 1'
|
||||||
expected = '[{0}]'.format(json.dumps(serializers.TrackSerializerNested(track1).data))
|
expected = '[{0}]'.format(json.dumps(serializers.TrackSerializerNested(track1).data))
|
||||||
url = self.reverse('api:tracks-search')
|
url = self.reverse('api:v1:tracks-search')
|
||||||
response = self.client.get(url + '?query={0}'.format(query))
|
response = self.client.get(url + '?query={0}'.format(query))
|
||||||
|
|
||||||
self.assertJSONEqual(expected, json.loads(response.content.decode('utf-8')))
|
self.assertJSONEqual(expected, json.loads(response.content.decode('utf-8')))
|
||||||
|
|
||||||
def test_can_restrict_api_views_to_authenticated_users(self):
|
def test_can_restrict_api_views_to_authenticated_users(self):
|
||||||
urls = [
|
urls = [
|
||||||
('api:tags-list', 'get'),
|
('api:v1:tags-list', 'get'),
|
||||||
('api:tracks-list', 'get'),
|
('api:v1:tracks-list', 'get'),
|
||||||
('api:artists-list', 'get'),
|
('api:v1:artists-list', 'get'),
|
||||||
('api:albums-list', 'get'),
|
('api:v1:albums-list', 'get'),
|
||||||
]
|
]
|
||||||
|
|
||||||
for route_name, method in urls:
|
for route_name, method in urls:
|
||||||
|
|
|
@ -59,7 +59,7 @@ Is it me you're looking for?"""
|
||||||
work=None,
|
work=None,
|
||||||
mbid='07ca77cf-f513-4e9c-b190-d7e24bbad448')
|
mbid='07ca77cf-f513-4e9c-b190-d7e24bbad448')
|
||||||
|
|
||||||
url = reverse('api:tracks-lyrics', kwargs={'pk': track.pk})
|
url = reverse('api:v1:tracks-lyrics', kwargs={'pk': track.pk})
|
||||||
user = User.objects.create_user(
|
user = User.objects.create_user(
|
||||||
username='test', email='test@test.com', password='test')
|
username='test', email='test@test.com', password='test')
|
||||||
self.client.login(username=user.username, password='test')
|
self.client.login(username=user.username, password='test')
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import unittest
|
import unittest
|
||||||
import os
|
import os
|
||||||
|
import datetime
|
||||||
from test_plus.test import TestCase
|
from test_plus.test import TestCase
|
||||||
from funkwhale_api.music import metadata
|
from funkwhale_api.music import metadata
|
||||||
|
|
||||||
|
@ -8,20 +9,72 @@ DATA_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
class TestMetadata(TestCase):
|
class TestMetadata(TestCase):
|
||||||
|
|
||||||
def test_can_get_metadata_from_file(self, *mocks):
|
def test_can_get_metadata_from_ogg_file(self, *mocks):
|
||||||
path = os.path.join(DATA_DIR, 'test.ogg')
|
path = os.path.join(DATA_DIR, 'test.ogg')
|
||||||
data = metadata.Metadata(path)
|
data = metadata.Metadata(path)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
data.get('title'),
|
||||||
|
'Peer Gynt Suite no. 1, op. 46: I. Morning'
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
data.get('artist'),
|
||||||
|
'Edvard Grieg'
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
data.get('album'),
|
||||||
|
'Peer Gynt Suite no. 1, op. 46'
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
data.get('date'),
|
||||||
|
datetime.date(2012, 8, 15),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
data.get('track_number'),
|
||||||
|
1
|
||||||
|
)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
data.get('musicbrainz_albumid'),
|
data.get('musicbrainz_albumid'),
|
||||||
'a766da8b-8336-47aa-a3ee-371cc41ccc75')
|
'a766da8b-8336-47aa-a3ee-371cc41ccc75')
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
data.get('musicbrainz_trackid'),
|
data.get('musicbrainz_recordingid'),
|
||||||
'bd21ac48-46d8-4e78-925f-d9cc2a294656')
|
'bd21ac48-46d8-4e78-925f-d9cc2a294656')
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
data.get('musicbrainz_artistid'),
|
data.get('musicbrainz_artistid'),
|
||||||
'013c8e5b-d72a-4cd3-8dee-6c64d6125823')
|
'013c8e5b-d72a-4cd3-8dee-6c64d6125823')
|
||||||
|
|
||||||
self.assertEqual(data.release, data.get('musicbrainz_albumid'))
|
def test_can_get_metadata_from_id3_mp3_file(self, *mocks):
|
||||||
self.assertEqual(data.artist, data.get('musicbrainz_artistid'))
|
path = os.path.join(DATA_DIR, 'test.mp3')
|
||||||
self.assertEqual(data.recording, data.get('musicbrainz_trackid'))
|
data = metadata.Metadata(path)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
data.get('title'),
|
||||||
|
'Bend'
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
data.get('artist'),
|
||||||
|
'Binärpilot'
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
data.get('album'),
|
||||||
|
'You Can\'t Stop Da Funk'
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
data.get('date'),
|
||||||
|
datetime.date(2006, 2, 7),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
data.get('track_number'),
|
||||||
|
1
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
data.get('musicbrainz_albumid'),
|
||||||
|
'ce40cdb1-a562-4fd8-a269-9269f98d4124')
|
||||||
|
self.assertEqual(
|
||||||
|
data.get('musicbrainz_recordingid'),
|
||||||
|
'f269d497-1cc0-4ae4-a0c4-157ec7d73fcb')
|
||||||
|
self.assertEqual(
|
||||||
|
data.get('musicbrainz_artistid'),
|
||||||
|
'9c6bddde-6228-4d9f-ad0d-03f6fcb19e13')
|
||||||
|
|
|
@ -121,13 +121,14 @@ class TrackViewSet(TagViewSetMixin, SearchMixin, viewsets.ReadOnlyModelViewSet):
|
||||||
|
|
||||||
|
|
||||||
class TagViewSet(viewsets.ReadOnlyModelViewSet):
|
class TagViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
queryset = Tag.objects.all()
|
queryset = Tag.objects.all().order_by('name')
|
||||||
serializer_class = serializers.TagSerializer
|
serializer_class = serializers.TagSerializer
|
||||||
permission_classes = [ConditionalAuthentication]
|
permission_classes = [ConditionalAuthentication]
|
||||||
|
|
||||||
|
|
||||||
class Search(views.APIView):
|
class Search(views.APIView):
|
||||||
max_results = 3
|
max_results = 3
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
query = request.GET['query']
|
query = request.GET['query']
|
||||||
results = {
|
results = {
|
||||||
|
|
|
@ -13,7 +13,7 @@ class TestAPI(TestCase):
|
||||||
return_value=api_data.recordings['search']['brontide matador'])
|
return_value=api_data.recordings['search']['brontide matador'])
|
||||||
def test_can_search_recording_in_musicbrainz_api(self, *mocks):
|
def test_can_search_recording_in_musicbrainz_api(self, *mocks):
|
||||||
query = 'brontide matador'
|
query = 'brontide matador'
|
||||||
url = reverse('api:providers:musicbrainz:search-recordings')
|
url = reverse('api:v1:providers:musicbrainz:search-recordings')
|
||||||
expected = api_data.recordings['search']['brontide matador']
|
expected = api_data.recordings['search']['brontide matador']
|
||||||
response = self.client.get(url, data={'query': query})
|
response = self.client.get(url, data={'query': query})
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ class TestAPI(TestCase):
|
||||||
return_value=api_data.releases['search']['brontide matador'])
|
return_value=api_data.releases['search']['brontide matador'])
|
||||||
def test_can_search_release_in_musicbrainz_api(self, *mocks):
|
def test_can_search_release_in_musicbrainz_api(self, *mocks):
|
||||||
query = 'brontide matador'
|
query = 'brontide matador'
|
||||||
url = reverse('api:providers:musicbrainz:search-releases')
|
url = reverse('api:v1:providers:musicbrainz:search-releases')
|
||||||
expected = api_data.releases['search']['brontide matador']
|
expected = api_data.releases['search']['brontide matador']
|
||||||
response = self.client.get(url, data={'query': query})
|
response = self.client.get(url, data={'query': query})
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ class TestAPI(TestCase):
|
||||||
return_value=api_data.artists['search']['lost fingers'])
|
return_value=api_data.artists['search']['lost fingers'])
|
||||||
def test_can_search_artists_in_musicbrainz_api(self, *mocks):
|
def test_can_search_artists_in_musicbrainz_api(self, *mocks):
|
||||||
query = 'lost fingers'
|
query = 'lost fingers'
|
||||||
url = reverse('api:providers:musicbrainz:search-artists')
|
url = reverse('api:v1:providers:musicbrainz:search-artists')
|
||||||
expected = api_data.artists['search']['lost fingers']
|
expected = api_data.artists['search']['lost fingers']
|
||||||
response = self.client.get(url, data={'query': query})
|
response = self.client.get(url, data={'query': query})
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ class TestAPI(TestCase):
|
||||||
return_value=api_data.artists['get']['lost fingers'])
|
return_value=api_data.artists['get']['lost fingers'])
|
||||||
def test_can_get_artist_in_musicbrainz_api(self, *mocks):
|
def test_can_get_artist_in_musicbrainz_api(self, *mocks):
|
||||||
uuid = 'ac16bbc0-aded-4477-a3c3-1d81693d58c9'
|
uuid = 'ac16bbc0-aded-4477-a3c3-1d81693d58c9'
|
||||||
url = reverse('api:providers:musicbrainz:artist-detail', kwargs={
|
url = reverse('api:v1:providers:musicbrainz:artist-detail', kwargs={
|
||||||
'uuid': uuid,
|
'uuid': uuid,
|
||||||
})
|
})
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
|
@ -60,7 +60,7 @@ class TestAPI(TestCase):
|
||||||
def test_can_broswe_release_group_using_musicbrainz_api(self, *mocks):
|
def test_can_broswe_release_group_using_musicbrainz_api(self, *mocks):
|
||||||
uuid = 'ac16bbc0-aded-4477-a3c3-1d81693d58c9'
|
uuid = 'ac16bbc0-aded-4477-a3c3-1d81693d58c9'
|
||||||
url = reverse(
|
url = reverse(
|
||||||
'api:providers:musicbrainz:release-group-browse',
|
'api:v1:providers:musicbrainz:release-group-browse',
|
||||||
kwargs={
|
kwargs={
|
||||||
'artist_uuid': uuid,
|
'artist_uuid': uuid,
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ class TestAPI(TestCase):
|
||||||
def test_can_broswe_releases_using_musicbrainz_api(self, *mocks):
|
def test_can_broswe_releases_using_musicbrainz_api(self, *mocks):
|
||||||
uuid = 'f04ed607-11b7-3843-957e-503ecdd485d1'
|
uuid = 'f04ed607-11b7-3843-957e-503ecdd485d1'
|
||||||
url = reverse(
|
url = reverse(
|
||||||
'api:providers:musicbrainz:release-browse',
|
'api:v1:providers:musicbrainz:release-browse',
|
||||||
kwargs={
|
kwargs={
|
||||||
'release_group_uuid': uuid,
|
'release_group_uuid': uuid,
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ class TestPlayLists(TestCase):
|
||||||
|
|
||||||
def test_can_create_playlist_via_api(self):
|
def test_can_create_playlist_via_api(self):
|
||||||
self.client.login(username=self.user.username, password='test')
|
self.client.login(username=self.user.username, password='test')
|
||||||
url = reverse('api:playlists-list')
|
url = reverse('api:v1:playlists-list')
|
||||||
data = {
|
data = {
|
||||||
'name': 'test',
|
'name': 'test',
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ class TestPlayLists(TestCase):
|
||||||
|
|
||||||
self.client.login(username=self.user.username, password='test')
|
self.client.login(username=self.user.username, password='test')
|
||||||
|
|
||||||
url = reverse('api:playlist-tracks-list')
|
url = reverse('api:v1:playlist-tracks-list')
|
||||||
data = {
|
data = {
|
||||||
'playlist': playlist.pk,
|
'playlist': playlist.pk,
|
||||||
'track': tracks[0].pk
|
'track': tracks[0].pk
|
||||||
|
|
|
@ -9,41 +9,34 @@ from funkwhale_api.music import models, metadata
|
||||||
@celery.app.task(name='audiofile.from_path')
|
@celery.app.task(name='audiofile.from_path')
|
||||||
def from_path(path):
|
def from_path(path):
|
||||||
data = metadata.Metadata(path)
|
data = metadata.Metadata(path)
|
||||||
|
|
||||||
artist = models.Artist.objects.get_or_create(
|
artist = models.Artist.objects.get_or_create(
|
||||||
name__iexact=data.get('artist'),
|
name__iexact=data.get('artist'),
|
||||||
defaults={'name': data.get('artist')},
|
defaults={
|
||||||
|
'name': data.get('artist'),
|
||||||
|
'mbid': data.get('musicbrainz_artistid', None),
|
||||||
|
|
||||||
|
},
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
release_date = None
|
release_date = data.get('date', default=None)
|
||||||
try:
|
|
||||||
year, month, day = data.get('date', None).split('-')
|
|
||||||
release_date = datetime.date(
|
|
||||||
int(year), int(month), int(day)
|
|
||||||
)
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
album = models.Album.objects.get_or_create(
|
album = models.Album.objects.get_or_create(
|
||||||
title__iexact=data.get('album'),
|
title__iexact=data.get('album'),
|
||||||
artist=artist,
|
artist=artist,
|
||||||
defaults={
|
defaults={
|
||||||
'title': data.get('album'),
|
'title': data.get('album'),
|
||||||
'release_date': release_date,
|
'release_date': release_date,
|
||||||
|
'mbid': data.get('musicbrainz_albumid', None),
|
||||||
},
|
},
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
position = None
|
position = data.get('track_number', default=None)
|
||||||
try:
|
|
||||||
position = int(data.get('tracknumber', None))
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
track = models.Track.objects.get_or_create(
|
track = models.Track.objects.get_or_create(
|
||||||
title__iexact=data.get('title'),
|
title__iexact=data.get('title'),
|
||||||
album=album,
|
album=album,
|
||||||
defaults={
|
defaults={
|
||||||
'title': data.get('title'),
|
'title': data.get('title'),
|
||||||
'position': position,
|
'position': position,
|
||||||
|
'mbid': data.get('musicbrainz_recordingid', None),
|
||||||
},
|
},
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
|
|
|
@ -14,21 +14,37 @@ class TestAudioFile(TestCase):
|
||||||
'artist': ['Test artist'],
|
'artist': ['Test artist'],
|
||||||
'album': ['Test album'],
|
'album': ['Test album'],
|
||||||
'title': ['Test track'],
|
'title': ['Test track'],
|
||||||
'tracknumber': ['4'],
|
'TRACKNUMBER': ['4'],
|
||||||
'date': ['2012-08-15']
|
'date': ['2012-08-15'],
|
||||||
|
'musicbrainz_albumid': ['a766da8b-8336-47aa-a3ee-371cc41ccc75'],
|
||||||
|
'musicbrainz_trackid': ['bd21ac48-46d8-4e78-925f-d9cc2a294656'],
|
||||||
|
'musicbrainz_artistid': ['013c8e5b-d72a-4cd3-8dee-6c64d6125823'],
|
||||||
}
|
}
|
||||||
|
|
||||||
with unittest.mock.patch('mutagen.File', return_value=metadata):
|
m1 = unittest.mock.patch('mutagen.File', return_value=metadata)
|
||||||
|
m2 = unittest.mock.patch(
|
||||||
|
'funkwhale_api.music.metadata.Metadata.get_file_type',
|
||||||
|
return_value='OggVorbis',
|
||||||
|
)
|
||||||
|
with m1, m2:
|
||||||
track_file = importer.from_path(
|
track_file = importer.from_path(
|
||||||
os.path.join(DATA_DIR, 'dummy_file.ogg'))
|
os.path.join(DATA_DIR, 'dummy_file.ogg'))
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
track_file.track.title, metadata['title'][0])
|
track_file.track.title, metadata['title'][0])
|
||||||
|
self.assertEqual(
|
||||||
|
track_file.track.mbid, metadata['musicbrainz_trackid'][0])
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
track_file.track.position, 4)
|
track_file.track.position, 4)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
track_file.track.album.title, metadata['album'][0])
|
track_file.track.album.title, metadata['album'][0])
|
||||||
|
self.assertEqual(
|
||||||
|
track_file.track.album.mbid,
|
||||||
|
metadata['musicbrainz_albumid'][0])
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
track_file.track.album.release_date, datetime.date(2012, 8, 15))
|
track_file.track.album.release_date, datetime.date(2012, 8, 15))
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
track_file.track.artist.name, metadata['artist'][0])
|
track_file.track.artist.name, metadata['artist'][0])
|
||||||
|
self.assertEqual(
|
||||||
|
track_file.track.artist.mbid,
|
||||||
|
metadata['musicbrainz_artistid'][0])
|
||||||
|
|
|
@ -26,7 +26,7 @@ class TestAPI(TestCase):
|
||||||
def test_can_get_search_results_from_funkwhale(self, *mocks):
|
def test_can_get_search_results_from_funkwhale(self, *mocks):
|
||||||
query = '8 bit adventure'
|
query = '8 bit adventure'
|
||||||
expected = json.dumps(client.search(query))
|
expected = json.dumps(client.search(query))
|
||||||
url = self.reverse('api:providers:youtube:search')
|
url = self.reverse('api:v1:providers:youtube:search')
|
||||||
response = self.client.get(url + '?query={0}'.format(query))
|
response = self.client.get(url + '?query={0}'.format(query))
|
||||||
|
|
||||||
self.assertJSONEqual(expected, json.loads(response.content.decode('utf-8')))
|
self.assertJSONEqual(expected, json.loads(response.content.decode('utf-8')))
|
||||||
|
@ -67,7 +67,7 @@ class TestAPI(TestCase):
|
||||||
}
|
}
|
||||||
|
|
||||||
expected = json.dumps(client.search_multiple(queries))
|
expected = json.dumps(client.search_multiple(queries))
|
||||||
url = self.reverse('api:providers:youtube:searchs')
|
url = self.reverse('api:v1:providers:youtube:searchs')
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
url, json.dumps(queries), content_type='application/json')
|
url, json.dumps(queries), content_type='application/json')
|
||||||
|
|
||||||
|
|
|
@ -94,7 +94,7 @@ class TestRadios(TestCase):
|
||||||
self.assertEqual(radio.session, restarted_radio.session)
|
self.assertEqual(radio.session, restarted_radio.session)
|
||||||
|
|
||||||
def test_can_get_start_radio_from_api(self):
|
def test_can_get_start_radio_from_api(self):
|
||||||
url = reverse('api:radios:sessions-list')
|
url = reverse('api:v1:radios:sessions-list')
|
||||||
response = self.client.post(url, {'radio_type': 'random'})
|
response = self.client.post(url, {'radio_type': 'random'})
|
||||||
session = models.RadioSession.objects.latest('id')
|
session = models.RadioSession.objects.latest('id')
|
||||||
self.assertEqual(session.radio_type, 'random')
|
self.assertEqual(session.radio_type, 'random')
|
||||||
|
@ -107,7 +107,7 @@ class TestRadios(TestCase):
|
||||||
self.assertEqual(session.user, self.user)
|
self.assertEqual(session.user, self.user)
|
||||||
|
|
||||||
def test_can_start_radio_for_anonymous_user(self):
|
def test_can_start_radio_for_anonymous_user(self):
|
||||||
url = reverse('api:radios:sessions-list')
|
url = reverse('api:v1:radios:sessions-list')
|
||||||
response = self.client.post(url, {'radio_type': 'random'})
|
response = self.client.post(url, {'radio_type': 'random'})
|
||||||
session = models.RadioSession.objects.latest('id')
|
session = models.RadioSession.objects.latest('id')
|
||||||
|
|
||||||
|
@ -118,11 +118,11 @@ class TestRadios(TestCase):
|
||||||
tracks = mommy.make('music.Track', _quantity=1)
|
tracks = mommy.make('music.Track', _quantity=1)
|
||||||
|
|
||||||
self.client.login(username=self.user.username, password='test')
|
self.client.login(username=self.user.username, password='test')
|
||||||
url = reverse('api:radios:sessions-list')
|
url = reverse('api:v1:radios:sessions-list')
|
||||||
response = self.client.post(url, {'radio_type': 'random'})
|
response = self.client.post(url, {'radio_type': 'random'})
|
||||||
session = models.RadioSession.objects.latest('id')
|
session = models.RadioSession.objects.latest('id')
|
||||||
|
|
||||||
url = reverse('api:radios:tracks-list')
|
url = reverse('api:v1:radios:tracks-list')
|
||||||
response = self.client.post(url, {'session': session.pk})
|
response = self.client.post(url, {'session': session.pk})
|
||||||
data = json.loads(response.content.decode('utf-8'))
|
data = json.loads(response.content.decode('utf-8'))
|
||||||
|
|
||||||
|
@ -173,7 +173,7 @@ class TestRadios(TestCase):
|
||||||
|
|
||||||
def test_can_start_artist_radio_from_api(self):
|
def test_can_start_artist_radio_from_api(self):
|
||||||
artist = mommy.make('music.Artist')
|
artist = mommy.make('music.Artist')
|
||||||
url = reverse('api:radios:sessions-list')
|
url = reverse('api:v1:radios:sessions-list')
|
||||||
|
|
||||||
response = self.client.post(url, {'radio_type': 'artist', 'related_object_id': artist.id})
|
response = self.client.post(url, {'radio_type': 'artist', 'related_object_id': artist.id})
|
||||||
session = models.RadioSession.objects.latest('id')
|
session = models.RadioSession.objects.latest('id')
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
{% block title %}{% block head_title %}{% endblock head_title %}{% endblock title %}
|
|
|
@ -1,80 +0,0 @@
|
||||||
{% extends "account/base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
{% load crispy_forms_tags %}
|
|
||||||
|
|
||||||
{% block head_title %}{% trans "Account" %}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-5">
|
|
||||||
<h2>{% trans "E-mail Addresses" %}</h2>
|
|
||||||
{% if user.emailaddress_set.all %}
|
|
||||||
<p>{% trans 'The following e-mail addresses are associated with your account:' %}</p>
|
|
||||||
|
|
||||||
<form action="{% url 'account_email' %}" class="email_list" method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<fieldset class="blockLabels">
|
|
||||||
|
|
||||||
{% for emailaddress in user.emailaddress_set.all %}
|
|
||||||
<div class="ctrlHolder">
|
|
||||||
<label for="email_radio_{{ forloop.counter }}" class="{% if emailaddress.primary %}primary_email{% endif %}">
|
|
||||||
|
|
||||||
<input id="email_radio_{{ forloop.counter }}" type="radio" name="email" {% if emailaddress.primary %}checked="checked"{% endif %} value="{{ emailaddress.email }}"/>
|
|
||||||
|
|
||||||
{{ emailaddress.email }}
|
|
||||||
{% if emailaddress.verified %}
|
|
||||||
<span class="verified">{% trans "Verified" %}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="unverified">{% trans "Unverified" %}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if emailaddress.primary %}<span class="primary">{% trans "Primary" %}</span>{% endif %}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<div class="buttonHolder">
|
|
||||||
<button class="secondaryAction" type="submit" name="action_primary" >{% trans 'Make Primary' %}</button>
|
|
||||||
<button class="secondaryAction" type="submit" name="action_send" >{% trans 'Re-send Verification' %}</button>
|
|
||||||
<button class="primaryAction" type="submit" name="action_remove" >{% trans 'Remove' %}</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{% else %}
|
|
||||||
<p><strong>{% trans 'Warning:'%}</strong> {% trans "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." %}</p>
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
|
||||||
<h2>{% trans "Add E-mail Address" %}</h2>
|
|
||||||
|
|
||||||
<form method="post" action="." class="add_email">
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ form|crispy }}
|
|
||||||
<button class="btn" name="action_add" type="submit">{% trans "Add E-mail" %}</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block extra_body %}
|
|
||||||
<script type="text/javascript">
|
|
||||||
(function() {
|
|
||||||
var message = "{% trans 'Do you really want to remove the selected e-mail address?' %}";
|
|
||||||
var actions = document.getElementsByName('action_remove');
|
|
||||||
if (actions.length) {
|
|
||||||
actions[0].addEventListener("click", function(e) {
|
|
||||||
if (! confirm(message)) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
{% extends "account/base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
{% load account %}
|
|
||||||
|
|
||||||
{% block head_title %}{% trans "Confirm E-mail Address" %}{% endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-12">
|
|
||||||
<h2>{% trans "Confirm E-mail Address" %}</h2>
|
|
||||||
|
|
||||||
{% if confirmation %}
|
|
||||||
|
|
||||||
{% user_display confirmation.email_address.user as user_display %}
|
|
||||||
|
|
||||||
<p>{% blocktrans with confirmation.email_address.email as email %}Please confirm that <a href="mailto:{{ email }}">{{ email }}</a> is an e-mail address for user {{ user_display }}.{% endblocktrans %}</p>
|
|
||||||
|
|
||||||
<form method="post" action="{% url 'account_confirm_email' confirmation.key %}">
|
|
||||||
{% csrf_token %}
|
|
||||||
<button id="confirm-button" class="submit" type="submit">{% trans 'Confirm' %}</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{% else %}
|
|
||||||
|
|
||||||
{% url 'account_email' as email_url %}
|
|
||||||
|
|
||||||
<p>{% blocktrans %}This e-mail confirmation link expired or is invalid. Please <a href="{{ email_url }}">issue a new e-mail confirmation request</a>.{% endblocktrans %}</p>
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
{% extends "account/base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
{% load account %}
|
|
||||||
|
|
||||||
{% block head_title %}{% trans "Confirm E-mail Address" %}{% endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-12">
|
|
||||||
<h2>{% trans "Confirm E-mail Address" %}</h2>
|
|
||||||
|
|
||||||
{% user_display email_address.user as user_display %}
|
|
||||||
|
|
||||||
<p>{% blocktrans with email_address.email as email %}You have confirmed that <a href="mailto:{{ email }}">{{ email }}</a> is an e-mail address for user {{ user_display }}.{% endblocktrans %}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
|
@ -1,48 +0,0 @@
|
||||||
{% extends "account/base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
{% load account %}
|
|
||||||
{% load socialaccount %}
|
|
||||||
{% load crispy_forms_tags %}
|
|
||||||
|
|
||||||
{% block head_title %}{% trans "Sign In" %}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-5">
|
|
||||||
<h2>{% trans "Sign In" %}</h2>
|
|
||||||
{% get_providers as socialaccount_providers %}
|
|
||||||
{% if socialaccount_providers %}
|
|
||||||
<p>{% blocktrans with site.name as site_name %}Please sign in with one
|
|
||||||
of your existing third party accounts. Or, <a href="{{ signup_url }}">sign up</a>
|
|
||||||
for a {{ site_name }} account and sign in below:{% endblocktrans %}</p>
|
|
||||||
|
|
||||||
<div class="socialaccount_ballot">
|
|
||||||
|
|
||||||
<ul class="socialaccount_providers">
|
|
||||||
{% include "socialaccount/snippets/provider_list.html" with process="login" %}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div class="login-or">{% trans 'or' %}</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% include "socialaccount/snippets/login_extra.html" %}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<form class="login" method="POST" action="{% url 'account_login' %}">
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ form|crispy }}
|
|
||||||
{% if redirect_field_value %}
|
|
||||||
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" />
|
|
||||||
{% endif %}
|
|
||||||
<button id="sign-in-button" class="btn btn-primary" type="submit">{% trans "Sign In" %}</button>
|
|
||||||
<a class="button secondaryAction" href="{% url 'account_reset_password' %}">{% trans "Forgot Password?" %}</a>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
{% extends "account/base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block head_title %}{% trans "Sign Out" %}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-5">
|
|
||||||
|
|
||||||
<h2>{% trans "Sign Out" %}</h2>
|
|
||||||
|
|
||||||
<p>{% trans 'Are you sure you want to sign out?' %}</p>
|
|
||||||
|
|
||||||
<form method="post" action="{% url 'account_logout' %}">
|
|
||||||
{% csrf_token %}
|
|
||||||
{% if redirect_field_value %}
|
|
||||||
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}"/>
|
|
||||||
{% endif %}
|
|
||||||
<button class="btn btn-danger" type="submit">{% trans 'Sign Out' %}</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
{% extends "account/base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
{% load crispy_forms_tags %}
|
|
||||||
{% block head_title %}{% trans "Change Password" %}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-5">
|
|
||||||
<h2>{% trans "Change Password" %}</h2>
|
|
||||||
|
|
||||||
<form method="POST" action="./" class="password_change">
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ form|crispy }}
|
|
||||||
<button class="btn" type="submit" name="action">{% trans "Change Password" %}</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
{% extends "account/base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
{% load account %}
|
|
||||||
{% load crispy_forms_tags %}
|
|
||||||
|
|
||||||
{% block head_title %}{% trans "Password Reset" %}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-5">
|
|
||||||
|
|
||||||
<h2>{% trans "Password Reset" %}</h2>
|
|
||||||
{% if user.is_authenticated %}
|
|
||||||
{% include "account/snippets/already_logged_in.html" %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<p>{% trans "Forgotten your password? Enter your e-mail address below, and we'll send you an e-mail allowing you to reset it." %}</p>
|
|
||||||
|
|
||||||
<form method="POST" action="./" class="password_reset">
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ form|crispy }}
|
|
||||||
<button class="btn" type="submit">{% trans "Reset My Password" %}</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<p>{% blocktrans %}Please contact us if you have any trouble resetting your password.{% endblocktrans %}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block javascript %}
|
|
||||||
{{ block.super }}
|
|
||||||
<script>
|
|
||||||
$("#id_email").focus();
|
|
||||||
</script>
|
|
||||||
{% endblock javascript %}
|
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
{% extends "account/base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
{% load account %}
|
|
||||||
|
|
||||||
{% block head_title %}{% trans "Password Reset" %}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-12">
|
|
||||||
<h2>{% trans "Password Reset" %}</h2>
|
|
||||||
|
|
||||||
{% if user.is_authenticated %}
|
|
||||||
{% include "account/snippets/already_logged_in.html" %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<p>{% blocktrans %}We have sent you an e-mail. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
|
@ -1,32 +0,0 @@
|
||||||
{% extends "account/base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
{% load crispy_forms_tags %}
|
|
||||||
|
|
||||||
{% block head_title %}{% trans "Change Password" %}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-12">
|
|
||||||
<h2>{% if token_fail %}{% trans "Bad Token" %}{% else %}{% trans "Change Password" %}{% endif %}</h2>
|
|
||||||
|
|
||||||
{% if token_fail %}
|
|
||||||
{% url 'account_reset_password' as passwd_reset_url %}
|
|
||||||
<p>{% blocktrans %}The password reset link was invalid, possibly because it has already been used. Please request a <a href="{{ passwd_reset_url }}">new password reset</a>.{% endblocktrans %}</p>
|
|
||||||
{% else %}
|
|
||||||
{% if form %}
|
|
||||||
<form method="POST" action="./">
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ form|crispy }}
|
|
||||||
<button type="submit" name="action">{% trans "change password" %}</button>
|
|
||||||
</form>
|
|
||||||
{% else %}
|
|
||||||
<p>{% trans 'Your password is now changed.' %}</p>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
{% extends "account/base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
{% block head_title %}{% trans "Change Password" %}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-5">
|
|
||||||
<h2>{% trans "Change Password" %}</h2>
|
|
||||||
<p>{% trans 'Your password is now changed.' %}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
|
|
||||||
{% extends "account/base.html" %}
|
|
||||||
|
|
||||||
{% load i18n crispy_forms_tags %}
|
|
||||||
|
|
||||||
{% block head_title %}{% trans "Set Password" %}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-5">
|
|
||||||
<h2>{% trans "Set Password" %}</h2>
|
|
||||||
|
|
||||||
<form method="POST" action="./" class="password_set">
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ form|crispy }}
|
|
||||||
<input type="submit" name="action" value="{% trans "Set Password" %}"/>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
{% extends "account/base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
{% load crispy_forms_tags %}
|
|
||||||
|
|
||||||
{% block title %}{% trans "Signup" %}{% endblock title %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-5">
|
|
||||||
<h1>{% trans "Sign Up" %}</h1>
|
|
||||||
|
|
||||||
<p>{% blocktrans %}Already have an account? Then please <a href="{{ login_url }}">sign in</a>.{% endblocktrans %}</p>
|
|
||||||
|
|
||||||
<form class="signup" id="signup_form" method="post" action="{% url 'account_signup' %}">
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ form|crispy }}
|
|
||||||
{% if redirect_field_value %}
|
|
||||||
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" />
|
|
||||||
{% endif %}
|
|
||||||
<button id="sign-up-button" class="btn btn-primary" type="submit">{% trans "Sign Up" %} »</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock content %}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
{% extends "account/base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block head_title %}{% trans "Sign Up Closed" %}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-5">
|
|
||||||
<h2>{% trans "Sign Up Closed" %}</h2>
|
|
||||||
|
|
||||||
<p>{% trans "We are sorry, but the sign up is currently closed." %}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
{% extends "account/base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-5">
|
|
||||||
<h2>{% trans "Verify Your E-mail Address" %}</h2>
|
|
||||||
|
|
||||||
<p>{% blocktrans %}We have sent an e-mail to <a href="mailto:{{ email }}">{{ email }}</a> for verification. Follow the link provided to finalize the signup process. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -1,28 +0,0 @@
|
||||||
{% extends "account/base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-5">
|
|
||||||
<h2>{% trans "Verify Your E-mail Address" %}</h2>
|
|
||||||
|
|
||||||
{% url 'account_email' as email_url %}
|
|
||||||
|
|
||||||
<p>{% blocktrans %}This part of the site requires us to verify that
|
|
||||||
you are who you claim to be. For this purpose, we require that you
|
|
||||||
verify ownership of your e-mail address. {% endblocktrans %}</p>
|
|
||||||
|
|
||||||
<p>{% blocktrans %}We have sent an e-mail to you for
|
|
||||||
verification. Please click on the link inside this e-mail. Please
|
|
||||||
contact us if you do not receive it within a few minutes.{% endblocktrans %}</p>
|
|
||||||
|
|
||||||
<p>{% blocktrans %}<strong>Note:</strong> you can still <a href="{{ email_url }}">change your e-mail address</a>.{% endblocktrans %}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block title %}User: {{ object.username }}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container">
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
|
|
||||||
<h2>{{ object.username }}</h2>
|
|
||||||
{% if object.name %}
|
|
||||||
<p>{{ object.name }}</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if object == request.user %}
|
|
||||||
<!-- Action buttons -->
|
|
||||||
<div class="row">
|
|
||||||
|
|
||||||
<div class="col-sm-12 ">
|
|
||||||
<a class="btn btn-primary" href="{% url 'users:update' %}">My Info</a>
|
|
||||||
<a class="btn btn-primary" href="{% url 'account_email' %}">E-Mail</a>
|
|
||||||
<!-- Your Stuff: Custom user template urls -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<!-- End Action buttons -->
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{% endblock content %}
|
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
{% load crispy_forms_tags %}
|
|
||||||
|
|
||||||
{% block title %}{{ user.username }}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<h1>{{ user.username }}</h1>
|
|
||||||
<form class="form-horizontal" method="post" action="{% url 'users:update' %}">
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ form|crispy }}
|
|
||||||
<div class="control-group">
|
|
||||||
<div class="controls">
|
|
||||||
<button type="submit" class="btn">Update</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
|
@ -1,20 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
{% load static %}{% load i18n %}
|
|
||||||
{% block title %}Members{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
|
|
||||||
<h2>Users</h2>
|
|
||||||
|
|
||||||
<div class="list-group">
|
|
||||||
{% for user in user_list %}
|
|
||||||
<a href="{% url 'users:detail' user.username %}" class="list-group-item">
|
|
||||||
<h4 class="list-group-item-heading">{{ user.username }}</h4>
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock content %}
|
|
|
@ -42,7 +42,7 @@ class UserTestCase(TestCase):
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
def test_can_fetch_data_from_api(self):
|
def test_can_fetch_data_from_api(self):
|
||||||
url = self.reverse('api:users:users-me')
|
url = self.reverse('api:v1:users:users-me')
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
# login required
|
# login required
|
||||||
self.assertEqual(response.status_code, 401)
|
self.assertEqual(response.status_code, 401)
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
|
|
||||||
from django.conf.urls import url
|
|
||||||
|
|
||||||
from . import views
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
|
|
||||||
]
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
[pytest]
|
||||||
|
DJANGO_SETTINGS_MODULE=config.settings.test
|
||||||
|
|
||||||
|
# -- recommended but optional:
|
||||||
|
python_files = tests.py test_*.py *_tests.py
|
|
@ -1,17 +1,11 @@
|
||||||
# Bleeding edge Django
|
# Bleeding edge Django
|
||||||
django==1.8.7
|
django==1.11
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
django-environ==0.4.0
|
django-environ==0.4.0
|
||||||
django-secure==1.0.1
|
django-secure==1.0.1
|
||||||
whitenoise==2.0.6
|
whitenoise==2.0.6
|
||||||
|
|
||||||
|
|
||||||
# Forms
|
|
||||||
django-braces==1.8.1
|
|
||||||
# django-crispy-forms==1.5.2
|
|
||||||
# django-floppyforms==1.5.2
|
|
||||||
|
|
||||||
# Models
|
# Models
|
||||||
django-model-utils==2.3.1
|
django-model-utils==2.3.1
|
||||||
|
|
||||||
|
@ -26,10 +20,6 @@ django-allauth==0.24.1
|
||||||
# Python-PostgreSQL Database Adapter
|
# Python-PostgreSQL Database Adapter
|
||||||
psycopg2==2.6.1
|
psycopg2==2.6.1
|
||||||
|
|
||||||
# Unicode slugification
|
|
||||||
unicode-slugify==0.1.3
|
|
||||||
django-autoslug==1.9.3
|
|
||||||
|
|
||||||
# Time zones support
|
# Time zones support
|
||||||
pytz==2015.7
|
pytz==2015.7
|
||||||
|
|
||||||
|
@ -42,21 +32,21 @@ celery==3.1.19
|
||||||
|
|
||||||
|
|
||||||
# Your custom requirements go here
|
# Your custom requirements go here
|
||||||
django-cors-headers
|
django-cors-headers==2.1.0
|
||||||
musicbrainzngs
|
musicbrainzngs==0.6
|
||||||
youtube_dl>=2015.12.21
|
youtube_dl>=2015.12.21
|
||||||
djangorestframework
|
djangorestframework==3.6.3
|
||||||
djangorestframework-jwt
|
djangorestframework-jwt==1.11.0
|
||||||
django-celery
|
django-celery==3.2.1
|
||||||
django-mptt
|
django-mptt==0.8.7
|
||||||
google-api-python-client
|
google-api-python-client==1.6.2
|
||||||
arrow
|
arrow==0.10.0
|
||||||
django-taggit
|
django-taggit==0.22.1
|
||||||
persisting_theory
|
persisting-theory==0.2.1
|
||||||
django-versatileimagefield
|
django-versatileimagefield==1.7.1
|
||||||
django-cachalot
|
django-cachalot==1.5.0
|
||||||
django-rest-auth
|
django-rest-auth==0.9.1
|
||||||
beautifulsoup4
|
beautifulsoup4==4.6.0
|
||||||
markdown
|
Markdown==2.6.8
|
||||||
ipython
|
ipython==6.1.0
|
||||||
mutagen
|
mutagen==1.38
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
-r base.txt
|
-r base.txt
|
||||||
coverage==4.0.3
|
coverage==4.0.3
|
||||||
django_coverage_plugin==1.1
|
django_coverage_plugin==1.1
|
||||||
Sphinx
|
Sphinx==1.6.2
|
||||||
django-extensions==1.5.9
|
django-extensions==1.5.9
|
||||||
Werkzeug==0.11.2
|
Werkzeug==0.11.2
|
||||||
django-test-plus==1.0.11
|
django-test-plus==1.0.11
|
||||||
factory_boy==2.6.0
|
factory_boy>=2.8.1
|
||||||
|
|
||||||
# django-debug-toolbar that works with Django 1.5+
|
# django-debug-toolbar that works with Django 1.5+
|
||||||
django-debug-toolbar>=1.5,<1.6
|
django-debug-toolbar>=1.5,<1.6
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
# Test dependencies go here.
|
# Test dependencies go here.
|
||||||
-r base.txt
|
-r local.txt
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
coverage==4.0.3
|
|
||||||
django_coverage_plugin==1.1
|
|
||||||
flake8==2.5.0
|
flake8==2.5.0
|
||||||
django-test-plus==1.0.11
|
model-mommy==1.3.2
|
||||||
factory_boy>=2.8.1
|
tox==2.7.0
|
||||||
model_mommy
|
pytest
|
||||||
tox
|
pytest-django
|
||||||
|
pytest-sugar
|
||||||
|
pytest-xdist
|
||||||
|
|
|
@ -2,4 +2,4 @@
|
||||||
|
|
||||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
|
||||||
docker-compose -f $DIR/test.yml run test python manage.py test "$@"
|
docker-compose -f $DIR/test.yml run test pytest "$@"
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
test:
|
test:
|
||||||
dockerfile: docker/Dockerfile.test
|
dockerfile: docker/Dockerfile.test
|
||||||
build: .
|
build: .
|
||||||
command: python manage.py test
|
command: pytest
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
environment:
|
|
||||||
- DJANGO_SETTINGS_MODULE=config.settings.test
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ DJANGO_SETTINGS_MODULE=config.settings.production
|
||||||
DJANGO_SECRET_KEY=
|
DJANGO_SECRET_KEY=
|
||||||
|
|
||||||
# You don't have to edit this
|
# You don't have to edit this
|
||||||
DJANGO_ADMIN_URL=^admin/
|
DJANGO_ADMIN_URL=^api/admin/
|
||||||
|
|
||||||
# Update it to match the domain that will be used to reach your funkwhale
|
# Update it to match the domain that will be used to reach your funkwhale
|
||||||
# instance
|
# instance
|
||||||
|
|
|
@ -40,7 +40,7 @@ server {
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto https;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_redirect off;
|
proxy_redirect off;
|
||||||
proxy_pass http://funkwhale-api/api/;
|
proxy_pass http://funkwhale-api/api/;
|
||||||
}
|
}
|
||||||
|
@ -48,6 +48,6 @@ server {
|
||||||
alias /srv/funkwhale/data/media/;
|
alias /srv/funkwhale/data/media/;
|
||||||
}
|
}
|
||||||
location /staticfiles/ {
|
location /staticfiles/ {
|
||||||
alias /srv/funkwhale/data/staticfiles/;
|
alias /srv/funkwhale/data/static/;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
7
dev.yml
7
dev.yml
|
@ -36,17 +36,18 @@ services:
|
||||||
- C_FORCE_ROOT=true
|
- C_FORCE_ROOT=true
|
||||||
volumes:
|
volumes:
|
||||||
- ./api:/app
|
- ./api:/app
|
||||||
|
- ./data/music:/music
|
||||||
api:
|
api:
|
||||||
env_file: .env.dev
|
env_file: .env.dev
|
||||||
build:
|
build:
|
||||||
context: ./api
|
context: ./api
|
||||||
dockerfile: docker/Dockerfile.local
|
dockerfile: docker/Dockerfile.test
|
||||||
command: python /app/manage.py runserver 0.0.0.0:12081
|
command: python /app/manage.py runserver 0.0.0.0:12081
|
||||||
volumes:
|
volumes:
|
||||||
- ./api:/app
|
- ./api:/app
|
||||||
|
- ./data/music:/music
|
||||||
ports:
|
ports:
|
||||||
- "12081"
|
- "12081:12081"
|
||||||
links:
|
links:
|
||||||
- postgres
|
- postgres
|
||||||
- redis
|
- redis
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
Changelog
|
||||||
|
=========
|
||||||
|
|
||||||
|
|
||||||
|
0.1
|
||||||
|
-------
|
||||||
|
|
||||||
|
2017-06-26
|
||||||
|
|
||||||
|
Initial release
|
|
@ -14,14 +14,18 @@ least an ``artist``, ``album`` and ``title`` tag, you can import those tracks as
|
||||||
|
|
||||||
docker-compose run --rm api python manage.py import_files "/music/**/*.ogg" --recursive --noinput
|
docker-compose run --rm api python manage.py import_files "/music/**/*.ogg" --recursive --noinput
|
||||||
|
|
||||||
|
For the best results, we recommand tagging your music collection through
|
||||||
|
`Picard <http://picard.musicbrainz.org/>`_ in order to have the best quality metadata.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
This command is idempotent, meaning you can run it multiple times on the same
|
This command is idempotent, meaning you can run it multiple times on the same
|
||||||
files and already imported files will simply be skipped.
|
files and already imported files will simply be skipped.
|
||||||
|
|
||||||
.. warning::
|
.. note::
|
||||||
|
|
||||||
|
At the moment, only OGG/Vorbis and MP3 files with ID3 tags are supported
|
||||||
|
|
||||||
At the moment, only ogg files are supported. MP3 support will be implemented soon.
|
|
||||||
|
|
||||||
Getting demo tracks
|
Getting demo tracks
|
||||||
^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
|
@ -14,7 +14,7 @@ Funkwhale is a self-hosted, modern free and open-source music server, heavily in
|
||||||
features
|
features
|
||||||
installation/index
|
installation/index
|
||||||
importing-music
|
importing-music
|
||||||
|
changelog
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
==================
|
==================
|
||||||
|
|
|
@ -4,7 +4,7 @@ class Config {
|
||||||
if (!this.BACKEND_URL.endsWith('/')) {
|
if (!this.BACKEND_URL.endsWith('/')) {
|
||||||
this.BACKEND_URL += '/'
|
this.BACKEND_URL += '/'
|
||||||
}
|
}
|
||||||
this.API_URL = this.BACKEND_URL + 'api/'
|
this.API_URL = this.BACKEND_URL + 'api/v1/'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue