Browse Source

Initial commit that merge both the front end and the API in the same repository

merge-requests/1/head
Eliot Berriot 5 years ago
commit
76f98b74dd
  1. 69
      .dockerignore
  2. 29
      .editorconfig
  3. 3
      .env.dev
  4. 1
      .gitattributes
  5. 84
      .gitignore
  6. 22
      .gitlab-ci.yml
  7. 1
      CONTRIBUTORS.txt
  8. 27
      LICENSE
  9. 50
      README.rst
  10. 5
      api/.coveragerc
  11. 11
      api/.pylintrc
  12. 21
      api/Dockerfile
  13. 18
      api/compose/django/entrypoint.sh
  14. 3
      api/compose/django/gunicorn.sh
  15. 2
      api/compose/nginx/Dockerfile
  16. 53
      api/compose/nginx/nginx.conf
  17. 0
      api/config/__init__.py
  18. 26
      api/config/api_urls.py
  19. 1
      api/config/settings/__init__.py
  20. 307
      api/config/settings/common.py
  21. 85
      api/config/settings/local.py
  22. 167
      api/config/settings/production.py
  23. 34
      api/config/settings/test.py
  24. 33
      api/config/urls.py
  25. 41
      api/config/wsgi.py
  26. 6
      api/demo/demo-user.py
  27. 13
      api/demo/load-demo-data.sh
  28. 42
      api/docker-compose.yml
  29. 10
      api/docker/Dockerfile.base
  30. 12
      api/docker/Dockerfile.local
  31. 13
      api/docker/Dockerfile.test
  32. 3
      api/funkwhale_api/__init__.py
  33. 0
      api/funkwhale_api/common/__init__.py
  34. 11
      api/funkwhale_api/common/permissions.py
  35. 19
      api/funkwhale_api/common/utils.py
  36. 1
      api/funkwhale_api/contrib/__init__.py
  37. 1
      api/funkwhale_api/contrib/sites/__init__.py
  38. 31
      api/funkwhale_api/contrib/sites/migrations/0001_initial.py
  39. 40
      api/funkwhale_api/contrib/sites/migrations/0002_set_site_domain_and_name.py
  40. 1
      api/funkwhale_api/contrib/sites/migrations/__init__.py
  41. 2
      api/funkwhale_api/downloader/__init__.py
  42. 27
      api/funkwhale_api/downloader/downloader.py
  43. 0
      api/funkwhale_api/downloader/tests/__init__.py
  44. 14
      api/funkwhale_api/downloader/tests/test_downloader.py
  45. 0
      api/funkwhale_api/favorites/__init__.py
  46. 33
      api/funkwhale_api/favorites/migrations/0001_initial.py
  47. 0
      api/funkwhale_api/favorites/migrations/__init__.py
  48. 18
      api/funkwhale_api/favorites/models.py
  49. 12
      api/funkwhale_api/favorites/serializers.py
  50. 0
      api/funkwhale_api/favorites/tests/__init__.py
  51. 113
      api/funkwhale_api/favorites/tests/test_favorites.py
  52. 8
      api/funkwhale_api/favorites/urls.py
  53. 54
      api/funkwhale_api/favorites/views.py
  54. 0
      api/funkwhale_api/history/__init__.py
  55. 8
      api/funkwhale_api/history/admin.py
  56. 30
      api/funkwhale_api/history/migrations/0001_initial.py
  57. 0
      api/funkwhale_api/history/migrations/__init__.py
  58. 21
      api/funkwhale_api/history/models.py
  59. 20
      api/funkwhale_api/history/serializers.py
  60. 0
      api/funkwhale_api/history/tests/__init__.py
  61. 49
      api/funkwhale_api/history/tests/test_history.py
  62. 8
      api/funkwhale_api/history/urls.py
  63. 36
      api/funkwhale_api/history/views.py
  64. 0
      api/funkwhale_api/music/__init__.py
  65. 47
      api/funkwhale_api/music/admin.py
  66. 42
      api/funkwhale_api/music/importers.py
  67. 31
      api/funkwhale_api/music/lyrics.py
  68. 34
      api/funkwhale_api/music/metadata.py
  69. 89
      api/funkwhale_api/music/migrations/0001_initial.py
  70. 40
      api/funkwhale_api/music/migrations/0002_auto_20151215_1645.py
  71. 19
      api/funkwhale_api/music/migrations/0003_auto_20151222_2233.py
  72. 21
      api/funkwhale_api/music/migrations/0004_track_tags.py
  73. 40
      api/funkwhale_api/music/migrations/0005_deduplicate.py
  74. 28
      api/funkwhale_api/music/migrations/0006_unique_mbid.py
  75. 19
      api/funkwhale_api/music/migrations/0007_track_position.py
  76. 29
      api/funkwhale_api/music/migrations/0008_auto_20160529_1456.py
  77. 49
      api/funkwhale_api/music/migrations/0009_auto_20160920_1614.py
  78. 20
      api/funkwhale_api/music/migrations/0010_auto_20160920_1742.py
  79. 61
      api/funkwhale_api/music/migrations/0011_rename_files.py
  80. 20
      api/funkwhale_api/music/migrations/0012_auto_20161122_1905.py
  81. 0
      api/funkwhale_api/music/migrations/__init__.py
  82. 408
      api/funkwhale_api/music/models.py
  83. 96
      api/funkwhale_api/music/serializers.py
  84. 0
      api/funkwhale_api/music/tests/__init__.py
  85. 1
      api/funkwhale_api/music/tests/cover.py
  86. 502
      api/funkwhale_api/music/tests/data.py
  87. 32
      api/funkwhale_api/music/tests/mocking/lyricswiki.py
  88. BIN
      api/funkwhale_api/music/tests/test.ogg
  89. 216
      api/funkwhale_api/music/tests/test_api.py
  90. 75
      api/funkwhale_api/music/tests/test_lyrics.py
  91. 27
      api/funkwhale_api/music/tests/test_metadata.py
  92. 115
      api/funkwhale_api/music/tests/test_music.py
  93. 66
      api/funkwhale_api/music/tests/test_works.py
  94. 37
      api/funkwhale_api/music/utils.py
  95. 254
      api/funkwhale_api/music/views.py
  96. 1
      api/funkwhale_api/musicbrainz/__init__.py
  97. 46
      api/funkwhale_api/musicbrainz/client.py
  98. 0
      api/funkwhale_api/musicbrainz/tests/__init__.py
  99. 478
      api/funkwhale_api/musicbrainz/tests/data.py
  100. 87
      api/funkwhale_api/musicbrainz/tests/test_api.py

69
.dockerignore

@ -0,0 +1,69 @@
### OSX ###
.DS_Store
.AppleDouble
.LSOverride
### SublimeText ###
# cache files for sublime text
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache
# workspace files are user-specific
*.sublime-workspace
# project files should be checked into the repository, unless a significant
# proportion of contributors will probably not be using SublimeText
# *.sublime-project
# sftp configuration file
sftp-config.json
# Basics
*.py[cod]
__pycache__
# Logs
*.log
api/pip-log.txt
# Unit test / coverage reports
.coverage
.tox
nosetests.xml
htmlcov
# Translations
*.mo
*.pot
# Pycharm
.idea
# Vim
*~
*.swp
*.swo
# npm
front/node_modules/
# Compass
.sass-cache
# virtual environments
.env
# User-uploaded media
api/funkwhale_api/media/
# Hitch directory
api/tests/.hitch
# MailHog binary
mailhog
*.sqlite3
api/music
api/media

29
.editorconfig

@ -0,0 +1,29 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{py,rst,ini}]
indent_style = space
indent_size = 4
[*.py]
line_length=120
known_first_party=funkwhale_api
multi_line_output=3
default_section=THIRDPARTY
[*.{html,js,vue,css,scss,json,yml}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

3
.env.dev

@ -0,0 +1,3 @@
BACKEND_URL=http://localhost:6001
YOUTUBE_API_KEY=
API_AUTHENTICATION_REQUIRED=False

1
.gitattributes

@ -0,0 +1 @@
* text=auto

84
.gitignore

@ -0,0 +1,84 @@
### OSX ###
.DS_Store
.AppleDouble
.LSOverride
### SublimeText ###
# cache files for sublime text
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache
# workspace files are user-specific
*.sublime-workspace
# project files should be checked into the repository, unless a significant
# proportion of contributors will probably not be using SublimeText
# *.sublime-project
# sftp configuration file
sftp-config.json
# Basics
*.py[cod]
__pycache__
# Logs
*.log
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
nosetests.xml
htmlcov
# Translations
*.mo
*.pot
# Pycharm
.idea
# Vim
*~
*.swp
*.swo
# npm
front/node_modules/
# Compass
.sass-cache
# virtual environments
.env
# User-uploaded media
api/funkwhale_api/media/
# Hitch directory
tests/.hitch
# MailHog binary
mailhog
*.sqlite3
# Api
api/music
api/media
api/staticfiles
api/static
# Front
front/node_modules/
front/dist/
front/npm-debug.log*
front/yarn-debug.log*
front/yarn-error.log*
front/test/unit/coverage
front/test/e2e/reports
front/selenium-debug.log

22
.gitlab-ci.yml

@ -0,0 +1,22 @@
image: docker:latest
# When using dind, it's wise to use the overlayfs driver for
# improved performance.
# variables:
# DOCKER_DRIVER: overlay
#
# services:
# - docker:dind
#
#
# # build:
# # stage: build
# # script:
# # - docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
# # - docker build -t funkwhale/front .
# # - docker push
# #
# # tags:
# # - dind
# # only:
# # - master

1
CONTRIBUTORS.txt

@ -0,0 +1 @@
Eliot Berriot

27
LICENSE

@ -0,0 +1,27 @@
Copyright (c) 2015, Eliot Berriot
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
* Neither the name of funkwhale_api nor the names of its
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
OF THE POSSIBILITY OF SUCH DAMAGE.

50
README.rst

@ -0,0 +1,50 @@
Funkwhale
=============
A self-hosted tribute to Grooveshark.com.
LICENSE: BSD
Setting up a development environment (docker)
----------------------------------------------
First of all, pull the repository.
Then, pull and build all the containers::
docker-compose -f dev.yml build
docker-compose -f dev.yml pull
API setup
^^^^^^^^^^
You'll have apply database migrations::
docker-compose -f dev.yml run celeryworker python manage.py migrate
And to create an admin user::
docker-compose -f dev.yml run celeryworker python manage.py createsuperuser
Launch all services
^^^^^^^^^^^^^^^^^^^
Then you can run everything with::
docker-compose up
The API server will be accessible at http://localhost:6001, and the front-end at http://localhost:8080.
Running API tests
------------------
Everything is managed using docker and docker-compose, just run::
./api/runtests
This bash script invoke `python manage.py test` in a docker container under the hood, so you can use
traditional django test arguments and options, such as::
./api/runtests funkwhale_api.music # run a specific app test

5
api/.coveragerc

@ -0,0 +1,5 @@
[run]
include = funkwhale_api/*
omit = *migrations*, *tests*
plugins =
django_coverage_plugin

11
api/.pylintrc

@ -0,0 +1,11 @@
[MASTER]
load-plugins=pylint_common, pylint_django, pylint_celery
[FORMAT]
max-line-length=120
[MESSAGES CONTROL]
disable=missing-docstring,invalid-name
[DESIGN]
max-parents=13

21
api/Dockerfile

@ -0,0 +1,21 @@
FROM python:3.5
ENV PYTHONUNBUFFERED 1
# Requirements have to be pulled and installed here, otherwise caching won't work
COPY ./requirements.apt /requirements.apt
RUN apt-get update -qq && grep "^[^#;]" requirements.apt | xargs apt-get install -y
COPY ./requirements /requirements
RUN pip install -r /requirements/production.txt
COPY . /app
# Since youtube-dl code is updated fairly often, we split it here
RUN pip install --upgrade youtube-dl
WORKDIR /app
ENTRYPOINT ["./compose/django/entrypoint.sh"]

18
api/compose/django/entrypoint.sh

@ -0,0 +1,18 @@
#!/bin/bash
set -e
# This entrypoint is used to play nicely with the current cookiecutter configuration.
# Since docker-compose relies heavily on environment variables itself for configuration, we'd have to define multiple
# environment variables just to support cookiecutter out of the box. That makes no sense, so this little entrypoint
# does all this for us.
export REDIS_URL=redis://redis:6379/0
# the official postgres image uses 'postgres' as default user if not set explictly.
if [ -z "$POSTGRES_ENV_POSTGRES_USER" ]; then
export POSTGRES_ENV_POSTGRES_USER=postgres
fi
export DATABASE_URL=postgres://$POSTGRES_ENV_POSTGRES_USER:$POSTGRES_ENV_POSTGRES_PASSWORD@postgres:5432/$POSTGRES_ENV_POSTGRES_USER
export CELERY_BROKER_URL=$REDIS_URL
exec "$@"

3
api/compose/django/gunicorn.sh

@ -0,0 +1,3 @@
#!/bin/sh
python /app/manage.py collectstatic --noinput
/usr/local/bin/gunicorn config.wsgi -w 4 -b 0.0.0.0:5000 --chdir=/app

2
api/compose/nginx/Dockerfile

@ -0,0 +1,2 @@
FROM nginx:latest
ADD nginx.conf /etc/nginx/nginx.conf

53
api/compose/nginx/nginx.conf

@ -0,0 +1,53 @@
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
upstream app {
server django:12081;
}
server {
listen 80;
charset utf-8;
root /staticfiles;
location / {
# checks for static file, if not found proxy to app
try_files $uri @proxy_to_app;
}
location @proxy_to_app {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://app;
}
}
}

0
api/config/__init__.py

26
api/config/api_urls.py

@ -0,0 +1,26 @@
from rest_framework import routers
from django.conf.urls import include, url
from funkwhale_api.music import views
from funkwhale_api.playlists import views as playlists_views
router = routers.SimpleRouter()
router.register(r'tags', views.TagViewSet, 'tags')
router.register(r'tracks', views.TrackViewSet, 'tracks')
router.register(r'artists', views.ArtistViewSet, 'artists')
router.register(r'albums', views.AlbumViewSet, 'albums')
router.register(r'import-batches', views.ImportBatchViewSet, 'import-batches')
router.register(r'submit', views.SubmitViewSet, 'submit')
router.register(r'playlists', playlists_views.PlaylistViewSet, 'playlists')
router.register(r'playlist-tracks', playlists_views.PlaylistTrackViewSet, 'playlist-tracks')
urlpatterns = router.urls
urlpatterns += [
url(r'^providers/', include('funkwhale_api.providers.urls', namespace='providers')),
url(r'^favorites/', include('funkwhale_api.favorites.urls', namespace='favorites')),
url(r'^search$', views.Search.as_view(), name='search'),
url(r'^radios/', include('funkwhale_api.radios.urls', namespace='radios')),
url(r'^history/', include('funkwhale_api.history.urls', namespace='history')),
url(r'^users/', include('funkwhale_api.users.api_urls', namespace='users')),
url(r'^token/', 'rest_framework_jwt.views.obtain_jwt_token'),
url(r'^token/refresh/', 'rest_framework_jwt.views.refresh_jwt_token'),
]

1
api/config/settings/__init__.py

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

307
api/config/settings/common.py

@ -0,0 +1,307 @@
# -*- coding: utf-8 -*-
"""
Django settings for funkwhale_api project.
For more information on this file, see
https://docs.djangoproject.com/en/dev/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/dev/ref/settings/
"""
from __future__ import absolute_import, unicode_literals
import os
import environ
ROOT_DIR = environ.Path(__file__) - 3 # (/a/b/myfile.py - 3 = /)
APPS_DIR = ROOT_DIR.path('funkwhale_api')
env = environ.Env()
try:
env.read_env(ROOT_DIR.file('.env'))
except FileNotFoundError:
pass
# APP CONFIGURATION
# ------------------------------------------------------------------------------
DJANGO_APPS = (
# Default Django apps:
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
# Useful template tags:
# 'django.contrib.humanize',
# Admin
'django.contrib.admin',
)
THIRD_PARTY_APPS = (
# 'crispy_forms', # Form layouts
'allauth', # registration
'allauth.account', # registration
'allauth.socialaccount', # registration
'corsheaders',
'rest_framework',
'rest_framework.authtoken',
'djcelery',
'taggit',
'cachalot',
'rest_auth',
'rest_auth.registration',
'mptt',
)
# Apps specific for this project go here.
LOCAL_APPS = (
'funkwhale_api.users', # custom users app
# Your stuff: custom apps go here
'funkwhale_api.music',
'funkwhale_api.favorites',
'funkwhale_api.radios',
'funkwhale_api.history',
'funkwhale_api.playlists',
'funkwhale_api.providers.audiofile',
)
# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
# MIDDLEWARE CONFIGURATION
# ------------------------------------------------------------------------------
MIDDLEWARE_CLASSES = (
# Make sure djangosecure.middleware.SecurityMiddleware is listed first
'django.contrib.sessions.middleware.SessionMiddleware',
'funkwhale_api.users.middleware.AnonymousSessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
# MIGRATIONS CONFIGURATION
# ------------------------------------------------------------------------------
MIGRATION_MODULES = {
'sites': 'funkwhale_api.contrib.sites.migrations'
}
# DEBUG
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#debug
DEBUG = env.bool("DJANGO_DEBUG", False)
# FIXTURE CONFIGURATION
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FIXTURE_DIRS
FIXTURE_DIRS = (
str(APPS_DIR.path('fixtures')),
)
# EMAIL CONFIGURATION
# ------------------------------------------------------------------------------
EMAIL_BACKEND = env('DJANGO_EMAIL_BACKEND', default='django.core.mail.backends.smtp.EmailBackend')
# MANAGER CONFIGURATION
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#admins
ADMINS = (
("""Eliot Berriot""", 'contact@eliotberriot.om'),
)
# See: https://docs.djangoproject.com/en/dev/ref/settings/#managers
MANAGERS = ADMINS
# DATABASE CONFIGURATION
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#databases
DATABASES = {
# Raises ImproperlyConfigured exception if DATABASE_URL not in os.environ
'default': env.db("DATABASE_URL", default="postgresql://postgres@postgres/postgres"),
}
DATABASES['default']['ATOMIC_REQUESTS'] = True
#
# DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.sqlite3',
# 'NAME': 'db.sqlite3',
# }
# }
# GENERAL CONFIGURATION
# ------------------------------------------------------------------------------
# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
# In a Windows environment this must be set to your system time zone.
TIME_ZONE = 'UTC'
# See: https://docs.djangoproject.com/en/dev/ref/settings/#language-code
LANGUAGE_CODE = 'en-us'
# See: https://docs.djangoproject.com/en/dev/ref/settings/#site-id
SITE_ID = 1
# See: https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n
USE_I18N = True
# See: https://docs.djangoproject.com/en/dev/ref/settings/#use-l10n
USE_L10N = True
# See: https://docs.djangoproject.com/en/dev/ref/settings/#use-tz
USE_TZ = True
# TEMPLATE CONFIGURATION
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#templates
TEMPLATES = [
{
# See: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-TEMPLATES-BACKEND
'BACKEND': 'django.template.backends.django.DjangoTemplates',
# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs
'DIRS': [
str(APPS_DIR.path('templates')),
],
'OPTIONS': {
# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-debug
'debug': DEBUG,
# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders
# https://docs.djangoproject.com/en/dev/ref/templates/api/#loader-types
'loaders': [
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
],
# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.template.context_processors.i18n',
'django.template.context_processors.media',
'django.template.context_processors.static',
'django.template.context_processors.tz',
'django.contrib.messages.context_processors.messages',
# Your stuff: custom template context processors go here
],
},
},
]
# See: http://django-crispy-forms.readthedocs.org/en/latest/install.html#template-packs
CRISPY_TEMPLATE_PACK = 'bootstrap3'
# STATIC FILE CONFIGURATION
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-root
STATIC_ROOT = str(ROOT_DIR('staticfiles'))
# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url
STATIC_URL = env("STATIC_URL", default='/static/')
# See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS
STATICFILES_DIRS = (
str(APPS_DIR.path('static')),
)
# See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
)
# MEDIA CONFIGURATION
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#media-root
MEDIA_ROOT = str(APPS_DIR('media'))
USE_SAMPLE_TRACK = env.bool("USE_SAMPLE_TRACK", False)
# See: https://docs.djangoproject.com/en/dev/ref/settings/#media-url
MEDIA_URL = '/media/'
# URL Configuration
# ------------------------------------------------------------------------------
ROOT_URLCONF = 'config.urls'
# See: https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application
WSGI_APPLICATION = 'config.wsgi.application'
# AUTHENTICATION CONFIGURATION
# ------------------------------------------------------------------------------
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
)
# Some really nice defaults
ACCOUNT_AUTHENTICATION_METHOD = 'username'
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_EMAIL_VERIFICATION = 'mandatory'
# Custom user app defaults
# Select the correct user model
AUTH_USER_MODEL = 'users.User'
LOGIN_REDIRECT_URL = 'users:redirect'
LOGIN_URL = 'account_login'
# SLUGLIFIER
AUTOSLUG_SLUGIFY_FUNCTION = 'slugify.slugify'
########## CELERY
INSTALLED_APPS += ('funkwhale_api.taskapp.celery.CeleryConfig',)
# if you are not using the django database broker (e.g. rabbitmq, redis, memcached), you can remove the next line.
INSTALLED_APPS += ('kombu.transport.django',)
BROKER_URL = env("CELERY_BROKER_URL", default='django://')
########## END CELERY
# Location of root django.contrib.admin URL, use {% url 'admin:index' %}
ADMIN_URL = r'^admin/'
SESSION_SAVE_EVERY_REQUEST = True
# Your common stuff: Below this line define 3rd party library settings
CELERY_DEFAULT_RATE_LIMIT = 1
CELERYD_TASK_TIME_LIMIT = 300
import datetime
JWT_AUTH = {
'JWT_ALLOW_REFRESH': True,
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=30),
'JWT_AUTH_HEADER_PREFIX': 'JWT',
}
ACCOUNT_ADAPTER = 'funkwhale_api.users.adapters.FunkwhaleAccountAdapter'
CORS_ORIGIN_ALLOW_ALL = True
# CORS_ORIGIN_WHITELIST = (
# 'localhost',
# 'funkwhale.localhost',
# )
CORS_ALLOW_CREDENTIALS = True
API_AUTHENTICATION_REQUIRED = env.bool("API_AUTHENTICATION_REQUIRED", True)
REGISTRATION_MODE = env('REGISTRATION_MODE', default='disabled')
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 25,
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
'DEFAULT_FILTER_BACKENDS': (
'rest_framework.filters.OrderingFilter',
)
}
FUNKWHALE_PROVIDERS = {
'youtube': {
'api_key': env('YOUTUBE_API_KEY', default='REPLACE_ME')
}
}
ATOMIC_REQUESTS = False

85
api/config/settings/local.py

@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
'''
Local settings
- Run in Debug mode
- Use console backend for emails
- Add Django Debug Toolbar
- Add django-extensions as app
'''
from .common import * # noqa
# DEBUG
# ------------------------------------------------------------------------------
DEBUG = env.bool('DJANGO_DEBUG', default=True)
TEMPLATES[0]['OPTIONS']['debug'] = DEBUG
# SECRET CONFIGURATION
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
# Note: This key only used for development and testing.
SECRET_KEY = env("DJANGO_SECRET_KEY", default='mc$&b=5j#6^bv7tld1gyjp2&+^-qrdy=0sw@r5sua*1zp4fmxc')
# Mail settings
# ------------------------------------------------------------------------------
EMAIL_HOST = 'localhost'
EMAIL_PORT = 1025
EMAIL_BACKEND = env('DJANGO_EMAIL_BACKEND',
default='django.core.mail.backends.console.EmailBackend')
# CACHING
# ------------------------------------------------------------------------------
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': ''
}
}
# django-debug-toolbar
# ------------------------------------------------------------------------------
MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
# INTERNAL_IPS = ('127.0.0.1', '10.0.2.2',)
DEBUG_TOOLBAR_CONFIG = {
'DISABLE_PANELS': [
'debug_toolbar.panels.redirects.RedirectsPanel',
],
'SHOW_TEMPLATE_CONTEXT': True,
'SHOW_TOOLBAR_CALLBACK': lambda request: True,
}
# django-extensions
# ------------------------------------------------------------------------------
# INSTALLED_APPS += ('django_extensions', )
INSTALLED_APPS += ('debug_toolbar', )
# TESTING
# ------------------------------------------------------------------------------
TEST_RUNNER = 'django.test.runner.DiscoverRunner'
########## CELERY
# In development, all tasks will be executed locally by blocking until the task returns
CELERY_ALWAYS_EAGER = False
########## END CELERY
# Your local stuff: Below this line define 3rd party library settings
LOGGING = {
'version': 1,
'handlers': {
'console':{
'level':'DEBUG',
'class':'logging.StreamHandler',
},
},
'loggers': {
'django.request': {
'handlers':['console'],
'propagate': True,
'level':'DEBUG',
}
},
}

167
api/config/settings/production.py

@ -0,0 +1,167 @@
# -*- coding: utf-8 -*-
'''
Production Configurations
- Use djangosecure
- Use Amazon's S3 for storing static files and uploaded media
- Use mailgun to send emails
- Use Redis on Heroku
'''
from __future__ import absolute_import, unicode_literals
from django.utils import six
from .common import * # noqa
# SECRET CONFIGURATION
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
# Raises ImproperlyConfigured exception if DJANGO_SECRET_KEY not in os.environ
SECRET_KEY = env("DJANGO_SECRET_KEY")
# This ensures that Django will be able to detect a secure connection
# properly on Heroku.
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# django-secure
# ------------------------------------------------------------------------------
# INSTALLED_APPS += ("djangosecure", )
#
# SECURITY_MIDDLEWARE = (
# 'djangosecure.middleware.SecurityMiddleware',
# )
#
#
# # Make sure djangosecure.middleware.SecurityMiddleware is listed first
# MIDDLEWARE_CLASSES = SECURITY_MIDDLEWARE + MIDDLEWARE_CLASSES
#
# # set this to 60 seconds and then to 518400 when you can prove it works
# SECURE_HSTS_SECONDS = 60
# SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool(
# "DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True)
# SECURE_FRAME_DENY = env.bool("DJANGO_SECURE_FRAME_DENY", default=True)
# SECURE_CONTENT_TYPE_NOSNIFF = env.bool(
# "DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", default=True)
# SECURE_BROWSER_XSS_FILTER = True
# SESSION_COOKIE_SECURE = False
# SESSION_COOKIE_HTTPONLY = True
# SECURE_SSL_REDIRECT = env.bool("DJANGO_SECURE_SSL_REDIRECT", default=True)
# SITE CONFIGURATION
# ------------------------------------------------------------------------------
# Hosts/domain names that are valid for this site
# See https://docs.djangoproject.com/en/1.6/ref/settings/#allowed-hosts
ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS', default=['funkwhale.io'])
# END SITE CONFIGURATION
INSTALLED_APPS += ("gunicorn", )
# STORAGE CONFIGURATION
# ------------------------------------------------------------------------------
# Uploaded Media Files
# ------------------------
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
# URL that handles the media served from MEDIA_ROOT, used for managing
# stored files.
MEDIA_URL = '/media/'
# Static Assets
# ------------------------
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage'
# EMAIL
# ------------------------------------------------------------------------------
DEFAULT_FROM_EMAIL = env('DJANGO_DEFAULT_FROM_EMAIL',
default='funkwhale_api <noreply@funkwhale.io>')
EMAIL_SUBJECT_PREFIX = env("DJANGO_EMAIL_SUBJECT_PREFIX", default='[funkwhale_api] ')
SERVER_EMAIL = env('DJANGO_SERVER_EMAIL', default=DEFAULT_FROM_EMAIL)
# TEMPLATE CONFIGURATION
# ------------------------------------------------------------------------------
# See:
# https://docs.djangoproject.com/en/dev/ref/templates/api/#django.template.loaders.cached.Loader
TEMPLATES[0]['OPTIONS']['loaders'] = [
('django.template.loaders.cached.Loader', [
'django.template.loaders.filesystem.Loader', 'django.template.loaders.app_directories.Loader', ]),
]
# DATABASE CONFIGURATION
# ------------------------------------------------------------------------------
# Raises ImproperlyConfigured exception if DATABASE_URL not in os.environ
DATABASES['default'] = env.db("DATABASE_URL")
# CACHING
# ------------------------------------------------------------------------------
# Heroku URL does not pass the DB number, so we parse it in
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "{0}/{1}".format(env.cache_url('REDIS_URL', default="redis://127.0.0.1:6379"), 0),
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"IGNORE_EXCEPTIONS": True, # mimics memcache behavior.
# http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior
}
}
}
# LOGGING CONFIGURATION
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#logging
# A sample logging configuration. The only tangible logging
# performed by this configuration is to send an email to
# the site admins on every HTTP 500 error when DEBUG=False.
# See http://docs.djangoproject.com/en/dev/topics/logging for
# more details on how to customize your logging configuration.
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse'
}
},
'formatters': {
'verbose': {
'format': '%(levelname)s %(asctime)s %(module)s '
'%(process)d %(thread)d %(message)s'
},
},
'handlers': {
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler'
},
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
},
'loggers': {
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': True
},
'django.security.DisallowedHost': {
'level': 'ERROR',
'handlers': ['console', 'mail_admins'],
'propagate': True
}
}
}
# Custom Admin URL, use {% url 'admin:index' %}
ADMIN_URL = env('DJANGO_ADMIN_URL')
# Your production stuff: Below this line define 3rd party library settings

34
api/config/settings/test.py

@ -0,0 +1,34 @@
from .common import * # noqa
SECRET_KEY = env("DJANGO_SECRET_KEY", default='test')
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
}
# Mail settings
# ------------------------------------------------------------------------------
EMAIL_HOST = 'localhost'
EMAIL_PORT = 1025
EMAIL_BACKEND = env('DJANGO_EMAIL_BACKEND',
default='django.core.mail.backends.console.EmailBackend')
# CACHING
# ------------------------------------------------------------------------------
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': ''
}
}
# TESTING
# ------------------------------------------------------------------------------
TEST_RUNNER = 'django.test.runner.DiscoverRunner'
########## CELERY
# In development, all tasks will be executed locally by blocking until the task returns
CELERY_ALWAYS_EAGER = True
########## END CELERY
# Your local stuff: Below this line define 3rd party library settings

33
api/config/urls.py

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.conf import settings
from django.conf.urls import include, url
from django.conf.urls.static import static
from django.contrib import admin
from django.views.generic import TemplateView
from django.views import defaults as default_views
urlpatterns = [
# Django Admin, use {% url 'admin:index' %}
url(settings.ADMIN_URL, include(admin.site.urls)),
url(r'^api/', include("config.api_urls", namespace="api")),
url(r'^api/auth/', include('rest_auth.urls')),
url(r'^api/auth/registration/', include('funkwhale_api.users.rest_auth_urls')),
url(r'^accounts/', include('allauth.urls')),
# Your stuff: custom urls includes go here
]
if settings.DEBUG:
# This allows the error pages to be debugged during development, just visit
# these url in browser to see how these error pages look like.
urlpatterns += [
url(r'^400/$', default_views.bad_request),
url(r'^403/$', default_views.permission_denied),
url(r'^404/$', default_views.page_not_found),
url(r'^500/$', default_views.server_error),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

41
api/config/wsgi.py

@ -0,0 +1,41 @@
"""
WSGI config for funkwhale_api project.
This module contains the WSGI application used by Django's development server
and any production WSGI deployments. It should expose a module-level variable
named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
this application via the ``WSGI_APPLICATION`` setting.
Usually you will have the standard Django WSGI application here, but it also
might make sense to replace the whole Django WSGI application with a custom one
that later delegates to the Django one. For example, you could introduce WSGI
middleware here, or combine a Django application with an application of another
framework.
"""
import os
from django.core.wsgi import get_wsgi_application
from whitenoise.django import DjangoWhiteNoise
# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
# if running multiple sites in the same mod_wsgi process. To fix this, use
# mod_wsgi daemon mode with each site in its own daemon process, or use
# os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.production"
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")
# This application object is used by any WSGI server configured to use this
# file. This includes Django's development server, if the WSGI_APPLICATION
# setting points here.
application = get_wsgi_application()
# Use Whitenoise to serve static files
# See: https://whitenoise.readthedocs.org/
application = DjangoWhiteNoise(application)
# Apply WSGI middleware here.
# from helloworld.wsgi import HelloWorldApplication
# application = HelloWorldApplication(application)

6
api/demo/demo-user.py

@ -0,0 +1,6 @@
from funkwhale_api.users.models import User
u = User.objects.create(email='demo@demo.com', username='demo', is_staff=True)
u.set_password('demo')
u.save()

13
api/demo/load-demo-data.sh

@ -0,0 +1,13 @@
#! /bin/bash
echo "Loading demo data..."
python manage.py migrate --noinput
echo "Creating demo user..."
cat demo/demo-user.py | python manage.py shell --plain
echo "Importing demo tracks..."
python manage.py import_files "/music/**/*.ogg" --recursive --noinput

42
api/docker-compose.yml

@ -0,0 +1,42 @@
version: '2'
services:
postgres:
image: postgres:9.5
api:
build: .
links:
- postgres
- redis
command: ./compose/django/gunicorn.sh
env_file: .env
volumes:
- ./media:/app/funkwhale_api/media
- ./staticfiles:/app/staticfiles
- ./music:/music
ports:
- "127.0.0.1:6001:5000"
redis:
image: redis:3.0
celeryworker:
build: .
env_file: .env
links:
- postgres
- redis
command: celery -A funkwhale_api.taskapp worker -l INFO
volumes:
- ./media:/app/funkwhale_api/media
- ./music:/music
environment:
- C_FORCE_ROOT=True
celerybeat:
build: .
env_file: .env
links:
- postgres
- redis
command: celery -A funkwhale_api.taskapp beat -l INFO

10
api/docker/Dockerfile.base

@ -0,0 +1,10 @@
FROM python:3.5
ENV PYTHONUNBUFFERED 1
# Requirements have to be pulled and installed here, otherwise caching won't work
COPY ./requirements.apt /requirements.apt
COPY ./install_os_dependencies.sh /install_os_dependencies.sh
RUN bash install_os_dependencies.sh install
COPY ./requirements /requirements
RUN pip install -r /requirements/base.txt

12
api/docker/Dockerfile.local

@ -0,0 +1,12 @@
FROM python:3.5
ENV PYTHONUNBUFFERED 1
# Requirements have to be pulled and installed here, otherwise caching won't work
COPY ./requirements.apt /requirements.apt
COPY ./install_os_dependencies.sh /install_os_dependencies.sh
RUN bash install_os_dependencies.sh install
COPY ./requirements /requirements
RUN pip install -r /requirements/local.txt
WORKDIR /app

13
api/docker/Dockerfile.test

@ -0,0 +1,13 @@
FROM funkwhale/apibase
ENV PYTHONUNBUFFERED 1
# Requirements have to be pulled and installed here, otherwise caching won't work
COPY ./requirements.apt /requirements.apt
COPY ./install_os_dependencies.sh /install_os_dependencies.sh
RUN bash install_os_dependencies.sh install
COPY ./requirements /requirements
RUN pip install -r /requirements/local.txt
RUN pip install -r /requirements/test.txt
WORKDIR /app

3
api/funkwhale_api/__init__.py

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
__version__ = '0.1.0'
__version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')])

0
api/funkwhale_api/common/__init__.py

11
api/funkwhale_api/common/permissions.py

@ -0,0 +1,11 @@
from django.conf import settings
from rest_framework.permissions import BasePermission
class ConditionalAuthentication(BasePermission):
def has_permission(self, request, view):
if settings.API_AUTHENTICATION_REQUIRED:
return request.user and request.user.is_authenticated()
return True

19
api/funkwhale_api/common/utils.py

@ -0,0 +1,19 @@
import os
import shutil
def rename_file(instance, field_name, new_name, allow_missing_file=False):
field = getattr(instance, field_name)
current_name, extension = os.path.splitext(field.name)
new_name_with_extension = '{}{}'.format(new_name, extension)
try:
shutil.move(field.path, new_name_with_extension)
except FileNotFoundError:
if not allow_missing_file:
raise
print('Skipped missing file', field.path)
initial_path = os.path.dirname(field.name)
field.name = os.path.join(initial_path, new_name_with_extension)
instance.save()
return new_name_with_extension

1
api/funkwhale_api/contrib/__init__.py

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

1
api/funkwhale_api/contrib/sites/__init__.py

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

31
api/funkwhale_api/contrib/sites/migrations/0001_initial.py

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.contrib.sites.models
class Migration(migrations.Migration):
dependencies = [
]
operations = [
migrations.CreateModel(
name='Site',
fields=[
('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)),
('domain', models.CharField(verbose_name='domain name', max_length=100, validators=[django.contrib.sites.models._simple_domain_name_validator])),
('name', models.CharField(verbose_name='display name', max_length=50)),
],
options={
'verbose_name_plural': 'sites',
'verbose_name': 'site',
'db_table': 'django_site',
'ordering': ('domain',),
},
managers=[
(b'objects', django.contrib.sites.models.SiteManager()),
],
),
]

40
api/funkwhale_api/contrib/sites/migrations/0002_set_site_domain_and_name.py

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations
def update_site_forward(apps, schema_editor):
"""Set site domain and name."""
Site = apps.get_model("sites", "Site")
Site.objects.update_or_create(
id=settings.SITE_ID,
defaults={
"domain": "funkwhale.io",
"name": "funkwhale_api"
}
)
def update_site_backward(apps, schema_editor):
"""Revert site domain and name to default."""
Site = apps.get_model("sites", "Site")
Site.objects.update_or_create(
id=settings.SITE_ID,
defaults={
"domain": "example.com",
"name": "example.com"
}
)
class Migration(migrations.Migration):
dependencies = [
('sites', '0001_initial'),
]
operations = [
migrations.RunPython(update_site_forward, update_site_backward),
]

1
api/funkwhale_api/contrib/sites/migrations/__init__.py

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

2
api/funkwhale_api/downloader/__init__.py

@ -0,0 +1,2 @@
from .downloader import download

27
api/funkwhale_api/downloader/downloader.py

@ -0,0 +1,27 @@
import os
import requests
import json
from urllib.parse import quote_plus
import youtube_dl
from django.conf import settings
import glob
def download(
url,
target_directory=settings.MEDIA_ROOT,
name="%(id)s.%(ext)s",
bitrate=192):
target_path = os.path.join(target_directory, name)
ydl_opts = {
'quiet': True,
'outtmpl': target_path,
'postprocessors': [{
'key': 'FFmpegExtractAudio',
'preferredcodec': 'vorbis',
}],
}
_downloader = youtube_dl.YoutubeDL(ydl_opts)
info = _downloader.extract_info(url)
info['audio_file_path'] = target_path % {'id': info['id'], 'ext': 'ogg'}
return info

0
api/funkwhale_api/downloader/tests/__init__.py

14
api/funkwhale_api/downloader/tests/test_downloader.py

@ -0,0 +1,14 @@
import os
from test_plus.test import TestCase
from .. import downloader
from funkwhale_api.utils.tests import TMPDirTestCaseMixin
class TestDownloader(TMPDirTestCaseMixin, TestCase):
def test_can_download_audio_from_youtube_url_to_vorbis(self):
data = downloader.download('https://www.youtube.com/watch?v=tPEE9ZwTmy0', target_directory=self.download_dir)
self.assertEqual(
data['audio_file_path'],
os.path.join(self.download_dir, 'tPEE9ZwTmy0.ogg'))
self.assertTrue(os.path.exists(data['audio_file_path']))

0
api/funkwhale_api/favorites/__init__.py

33
api/funkwhale_api/favorites/migrations/0001_initial.py

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.utils.timezone
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
('music', '0003_auto_20151222_2233'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='TrackFavorite',
fields=[
('id', models.AutoField(serialize=False, auto_created=True, verbose_name='ID', primary_key=True)),
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
('track', models.ForeignKey(related_name='track_favorites', to='music.Track')),
('user', models.ForeignKey(related_name='track_favorites', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ('-creation_date',),
},
),
migrations.AlterUniqueTogether(
name='trackfavorite',
unique_together=set([('track', 'user')]),
),
]

0
api/funkwhale_api/favorites/migrations/__init__.py

18
api/funkwhale_api/favorites/models.py

@ -0,0 +1,18 @@
from django.db import models
from django.utils import timezone
from funkwhale_api.music.models import Track
class TrackFavorite(models.Model):
creation_date = models.DateTimeField(default=timezone.now)
user = models.ForeignKey('users.User', related_name='track_favorites')
track = models.ForeignKey(Track, related_name='track_favorites')
class Meta:
unique_together = ('track', 'user')
ordering = ('-creation_date',)
@classmethod
def add(cls, track, user):
favorite, created = cls.objects.get_or_create(user=user, track=track)
return favorite

12
api/funkwhale_api/favorites/serializers.py

@ -0,0 +1,12 @@
from rest_framework import serializers
from funkwhale_api.music.serializers import TrackSerializerNested
from . import models
class UserTrackFavoriteSerializer(serializers.ModelSerializer):
# track = TrackSerializerNested(read_only=True)
class Meta:
model = models.TrackFavorite
fields = ('id', 'track', 'creation_date')

0
api/funkwhale_api/favorites/tests/__init__.py

113
api/funkwhale_api/favorites/tests/test_favorites.py

@ -0,0 +1,113 @@
import json
from test_plus.test import TestCase
from django.core.urlresolvers import reverse
from funkwhale_api.music.models import Track, Artist
from funkwhale_api.favorites.models import TrackFavorite
from funkwhale_api.users.models import User
class TestFavorites(TestCase):
def setUp(self):
super().setUp()
self.artist = Artist.objects.create(name='test')
self.track = Track.objects.create(title='test', artist=self.artist)
self.user = User.objects.create_user(username='test', email='test@test.com', password='test')
def test_user_can_add_favorite(self):
TrackFavorite.add(self.track, self.user)
favorite = TrackFavorite.objects.latest('id')
self.assertEqual(favorite.track, self.track)
self.assertEqual(favorite.user, self.user)
def test_user_can_get_his_favorites(self):
favorite = TrackFavorite.add(self.track, self.user)
url = reverse('api:favorites:tracks-list')
self.client.login(username=self.user.username, password='test')
response = self.client.get(url)
expected = [
{
'track': self.track.pk,
'id': favorite.id,
'creation_date': favorite.creation_date.isoformat().replace('+00:00', 'Z'),
}
]
parsed_json = json.loads(response.content.decode('utf-8'))
self.assertEqual(expected, parsed_json['results'])
def test_user_can_add_favorite_via_api(self):
url = reverse('api:favorites:tracks-list')
self.client.login(username=self.user.username, password='test')
response = self.client.post(url, {'track': self.track.pk})
favorite = TrackFavorite.objects.latest('id')
expected = {
'track': self.track.pk,
'id': favorite.id,
'creation_date': favorite.creation_date.isoformat().replace('+00:00', 'Z'),
}
parsed_json = json.loads(response.content.decode('utf-8'))
self.assertEqual(expected, parsed_json)
self.assertEqual(favorite.track, self.track)
self.assertEqual(favorite.user, self.user)
def test_user_can_remove_favorite_via_api(self):
favorite = TrackFavorite.add(self.track, self.user)
url = reverse('api:favorites:tracks-detail', kwargs={'pk': favorite.pk})
self.client.login(username=self.user.username, password='test')
response = self.client.delete(url, {'track': self.track.pk})
self.assertEqual(response.status_code, 204)
self.assertEqual(TrackFavorite.objects.count(), 0)
def test_user_can_remove_favorite_via_api_using_track_id(self):
favorite = TrackFavorite.add(self.track, self.user)
url = reverse('api:favorites:tracks-remove')
self.client.login(username=self.user.username, password='test')
response = self.client.delete(
url, json.dumps({'track': self.track.pk}),
content_type='application/json'
)
self.assertEqual(response.status_code, 204)
self.assertEqual(TrackFavorite.objects.count(), 0)
from funkwhale_api.users.models import User
def test_can_restrict_api_views_to_authenticated_users(self):
urls