diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 0b68d19c005c9ed446a0ee7e321db7b736b50b7d..d77d91f17284c753c9087de78b840213765da43e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -13,14 +13,22 @@ stages:
 test_api:
   stage: test
   image: funkwhale/funkwhale:base
+  variables:
+    PIP_CACHE_DIR: "$CI_PROJECT_DIR/pip-cache"
+    DATABASE_URL: "sqlite://"
   before_script:
+    - python3 -m venv --copies virtualenv
+    - source virtualenv/bin/activate
     - cd api
+    - pip install -r requirements/base.txt
+    - pip install -r requirements/local.txt
     - pip install -r requirements/test.txt
   script:
     - pytest
-  variables:
-    DATABASE_URL: "sqlite://"
-
+  cache:
+    key: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME"
+    paths:
+      - "$CI_PROJECT_DIR/pip-cache"
   tags:
     - docker
 
diff --git a/CHANGELOG b/CHANGELOG
index fdaa931b5527a6de7fd1c526a5a5cc71d1e13fb9..0888c77e5269f1bd193e7ef27011a37fed61e58c 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -16,6 +16,10 @@ Bugfixes:
 - Player: better handling of errors when fetching the audio file (#46)
 - Csrf: default CSRF_TRUSTED_ORIGINS to ALLOWED_HOSTS to avoid Csrf issues on admin (#49)
 
+Tech:
+
+- Django 2 compatibility, lot of packages upgrades (#47)
+
 
 0.2.4 (2017-12-14)
 ------------------
diff --git a/api/Dockerfile b/api/Dockerfile
index bb1942c22770eac242bfede554fd957de3fe835f..def9a43162df4e33faacaae1205cfa2cc1c8f7df 100644
--- a/api/Dockerfile
+++ b/api/Dockerfile
@@ -8,7 +8,9 @@ COPY ./requirements.apt /requirements.apt
 RUN apt-get update -qq && grep "^[^#;]" requirements.apt | xargs apt-get install -y
 
 
-COPY ./requirements /requirements
+COPY ./requirements/base.txt /requirements
+RUN pip install -r /requirements/base.txt
+COPY ./requirements/production.txt /requirements
 RUN pip install -r /requirements/production.txt
 
 COPY . /app
diff --git a/api/config/api_urls.py b/api/config/api_urls.py
index 5ed4cffdd52b462480306176ed988798fcf99aa4..13205fe3de7bf93b9c46add6a71970da6ac68d4f 100644
--- a/api/config/api_urls.py
+++ b/api/config/api_urls.py
@@ -25,22 +25,32 @@ v1_patterns = router.urls
 
 v1_patterns += [
     url(r'^providers/',
-        include('funkwhale_api.providers.urls', namespace='providers')),
+        include(
+            ('funkwhale_api.providers.urls', 'providers'),
+            namespace='providers')),
     url(r'^favorites/',
-        include('funkwhale_api.favorites.urls', namespace='favorites')),
+        include(
+            ('funkwhale_api.favorites.urls', 'favorites'),
+            namespace='favorites')),
     url(r'^search$',
         views.Search.as_view(), name='search'),
     url(r'^radios/',
-        include('funkwhale_api.radios.urls', namespace='radios')),
+        include(
+            ('funkwhale_api.radios.urls', 'radios'),
+            namespace='radios')),
     url(r'^history/',
-        include('funkwhale_api.history.urls', namespace='history')),
+        include(
+            ('funkwhale_api.history.urls', 'history'),
+            namespace='history')),
     url(r'^users/',
-        include('funkwhale_api.users.api_urls', namespace='users')),
+        include(
+            ('funkwhale_api.users.api_urls', 'users'),
+            namespace='users')),
     url(r'^token/',
         jwt_views.obtain_jwt_token),
     url(r'^token/refresh/', jwt_views.refresh_jwt_token),
 ]
 
 urlpatterns = [
-    url(r'^v1/', include(v1_patterns, namespace='v1'))
+    url(r'^v1/', include((v1_patterns, 'v1'), namespace='v1'))
 ]
diff --git a/api/config/settings/common.py b/api/config/settings/common.py
index b10a0310c162a1b94df427de5b17ea2f2d5b56d8..9804bb9c08d133b74bfd08440f5d207314ded2cf 100644
--- a/api/config/settings/common.py
+++ b/api/config/settings/common.py
@@ -75,7 +75,7 @@ INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
 
 # MIDDLEWARE CONFIGURATION
 # ------------------------------------------------------------------------------
-MIDDLEWARE_CLASSES = (
+MIDDLEWARE = (
     # Make sure djangosecure.middleware.SecurityMiddleware is listed first
     'django.contrib.sessions.middleware.SessionMiddleware',
     'funkwhale_api.users.middleware.AnonymousSessionMiddleware',
diff --git a/api/config/settings/local.py b/api/config/settings/local.py
index e8108e98bd63bc2fc3b51c111e268b260f258dc6..e0d497e793f90a2ab3b5ba7a9d52def7aba0d429 100644
--- a/api/config/settings/local.py
+++ b/api/config/settings/local.py
@@ -31,7 +31,7 @@ EMAIL_BACKEND = env('DJANGO_EMAIL_BACKEND',
 
 # django-debug-toolbar
 # ------------------------------------------------------------------------------
-MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
+MIDDLEWARE += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
 
 # INTERNAL_IPS = ('127.0.0.1', '10.0.2.2',)
 
diff --git a/api/config/settings/production.py b/api/config/settings/production.py
index ba02b5fd5cd4e2009c1b7149e872400867942e25..e009833050ead06ea7d62089f4c8182b3bbd172c 100644
--- a/api/config/settings/production.py
+++ b/api/config/settings/production.py
@@ -36,7 +36,7 @@ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
 #
 #
 # # Make sure djangosecure.middleware.SecurityMiddleware is listed first
-# MIDDLEWARE_CLASSES = SECURITY_MIDDLEWARE + MIDDLEWARE_CLASSES
+# MIDDLEWARE = SECURITY_MIDDLEWARE + MIDDLEWARE
 #
 # # set this to 60 seconds and then to 518400 when you can prove it works
 # SECURE_HSTS_SECONDS = 60
diff --git a/api/config/settings/test.py b/api/config/settings/test.py
index 7d02c417b39c675a4f7afef254fb43fdcecec3cc..1056454968cc468ff05c5cf93bcd20c936d24c8e 100644
--- a/api/config/settings/test.py
+++ b/api/config/settings/test.py
@@ -22,8 +22,8 @@ CACHES = {
         'LOCATION': ''
     }
 }
-INSTALLED_APPS += ('kombu.transport.django',)
-BROKER_URL = 'django://'
+
+BROKER_URL = 'memory://'
 
 # TESTING
 # ------------------------------------------------------------------------------
diff --git a/api/config/urls.py b/api/config/urls.py
index 8764640d1565faff010387aab3c8be91423486e1..8c490a5e6599e2f44bb60bb110a6a362f4153f93 100644
--- a/api/config/urls.py
+++ b/api/config/urls.py
@@ -10,9 +10,9 @@ from django.views import defaults as default_views
 
 urlpatterns = [
     # Django Admin, use {% url 'admin:index' %}
-    url(settings.ADMIN_URL, include(admin.site.urls)),
+    url(settings.ADMIN_URL, admin.site.urls),
 
-    url(r'^api/', include("config.api_urls", namespace="api")),
+    url(r'^api/', include(("config.api_urls", 'api'), 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')),
diff --git a/api/funkwhale_api/common/permissions.py b/api/funkwhale_api/common/permissions.py
index 3f13b2005cafc3d9ffcd1f098b5d10e6f080e720..6b61d9839d5dd7c14eb4c495b912e35e440cb68c 100644
--- a/api/funkwhale_api/common/permissions.py
+++ b/api/funkwhale_api/common/permissions.py
@@ -7,5 +7,5 @@ class ConditionalAuthentication(BasePermission):
 
     def has_permission(self, request, view):
         if settings.API_AUTHENTICATION_REQUIRED:
-            return request.user and request.user.is_authenticated()
+            return request.user and request.user.is_authenticated
         return True
diff --git a/api/funkwhale_api/contrib/sites/migrations/0001_initial.py b/api/funkwhale_api/contrib/sites/migrations/0001_initial.py
index 555d02c42434d51c6f379fc0854caefece4e0adf..cf95cec6586d758a216749f15d46053f0c05658c 100644
--- a/api/funkwhale_api/contrib/sites/migrations/0001_initial.py
+++ b/api/funkwhale_api/contrib/sites/migrations/0001_initial.py
@@ -25,7 +25,7 @@ class Migration(migrations.Migration):
                 'ordering': ('domain',),
             },
             managers=[
-                (b'objects', django.contrib.sites.models.SiteManager()),
+                ('objects', django.contrib.sites.models.SiteManager()),
             ],
         ),
     ]
diff --git a/api/funkwhale_api/favorites/migrations/0001_initial.py b/api/funkwhale_api/favorites/migrations/0001_initial.py
index 0a6f0e5fc58e2dba299ccf0d645388f5b3e19056..c2bd03182d5c6791eda77c23fc593d38ef99e22a 100644
--- a/api/funkwhale_api/favorites/migrations/0001_initial.py
+++ b/api/funkwhale_api/favorites/migrations/0001_initial.py
@@ -19,8 +19,8 @@ class Migration(migrations.Migration):
             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)),
+                ('track', models.ForeignKey(related_name='track_favorites', to='music.Track', on_delete=models.CASCADE)),
+                ('user', models.ForeignKey(related_name='track_favorites', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
             ],
             options={
                 'ordering': ('-creation_date',),
diff --git a/api/funkwhale_api/favorites/models.py b/api/funkwhale_api/favorites/models.py
index f9c6426e697471d9b520952e59a6409e7767266c..899ed9cff7f403bc2089e34cb3ff313ebaceff55 100644
--- a/api/funkwhale_api/favorites/models.py
+++ b/api/funkwhale_api/favorites/models.py
@@ -5,8 +5,10 @@ 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')
+    user = models.ForeignKey(
+        'users.User', related_name='track_favorites', on_delete=models.CASCADE)
+    track = models.ForeignKey(
+        Track, related_name='track_favorites', on_delete=models.CASCADE)
 
     class Meta:
         unique_together = ('track', 'user')
diff --git a/api/funkwhale_api/favorites/tests/test_favorites.py b/api/funkwhale_api/favorites/tests/test_favorites.py
index 230f030fe6b84f7f5dc3fd298e48cc8b0cd6ffb2..78c64a41338607a11830a8fdb64df758d9214ab6 100644
--- a/api/funkwhale_api/favorites/tests/test_favorites.py
+++ b/api/funkwhale_api/favorites/tests/test_favorites.py
@@ -1,6 +1,6 @@
 import json
 from test_plus.test import TestCase
-from django.core.urlresolvers import reverse
+from django.urls import reverse
 
 from funkwhale_api.music.models import Track, Artist
 from funkwhale_api.favorites.models import TrackFavorite
diff --git a/api/funkwhale_api/history/migrations/0001_initial.py b/api/funkwhale_api/history/migrations/0001_initial.py
index 5ddfc26f3b1ebf8bd46da81d76d319df7fda11e8..7b6f950edf38d474e5fa6488f2c42be8b3dbdb85 100644
--- a/api/funkwhale_api/history/migrations/0001_initial.py
+++ b/api/funkwhale_api/history/migrations/0001_initial.py
@@ -20,8 +20,8 @@ class Migration(migrations.Migration):
                 ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)),
                 ('end_date', models.DateTimeField(null=True, blank=True, default=django.utils.timezone.now)),
                 ('session_key', models.CharField(null=True, blank=True, max_length=100)),
-                ('track', models.ForeignKey(related_name='listenings', to='music.Track')),
-                ('user', models.ForeignKey(blank=True, null=True, related_name='listenings', to=settings.AUTH_USER_MODEL)),
+                ('track', models.ForeignKey(related_name='listenings', to='music.Track', on_delete=models.CASCADE)),
+                ('user', models.ForeignKey(blank=True, null=True, related_name='listenings', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
             ],
             options={
                 'ordering': ('-end_date',),
diff --git a/api/funkwhale_api/history/models.py b/api/funkwhale_api/history/models.py
index 0810ecf81d0bc7c82638958d6c26d73e4af3809c..f7f62de62a2f59a08edd9f0a9be28bed65c102fb 100644
--- a/api/funkwhale_api/history/models.py
+++ b/api/funkwhale_api/history/models.py
@@ -7,8 +7,14 @@ from funkwhale_api.music.models import Track
 
 class Listening(models.Model):
     end_date = models.DateTimeField(default=timezone.now, null=True, blank=True)
-    track = models.ForeignKey(Track, related_name="listenings")
-    user = models.ForeignKey('users.User', related_name="listenings", null=True, blank=True)
+    track = models.ForeignKey(
+        Track, related_name="listenings", on_delete=models.CASCADE)
+    user = models.ForeignKey(
+        'users.User',
+        related_name="listenings",
+        null=True,
+        blank=True,
+        on_delete=models.CASCADE)
     session_key = models.CharField(max_length=100, null=True, blank=True)
 
     class Meta:
diff --git a/api/funkwhale_api/history/tests/factories.py b/api/funkwhale_api/history/tests/factories.py
new file mode 100644
index 0000000000000000000000000000000000000000..0a411adf0ce02467e41fe8dd94958755606f0b95
--- /dev/null
+++ b/api/funkwhale_api/history/tests/factories.py
@@ -0,0 +1,12 @@
+import factory
+from funkwhale_api.music.tests import factories
+
+from funkwhale_api.users.tests.factories import UserFactory
+
+
+class ListeningFactory(factory.django.DjangoModelFactory):
+    user = factory.SubFactory(UserFactory)
+    track = factory.SubFactory(factories.TrackFactory)
+
+    class Meta:
+        model = 'history.Listening'
diff --git a/api/funkwhale_api/history/tests/test_history.py b/api/funkwhale_api/history/tests/test_history.py
index 61009615a607ac7c3f6a0dcd1876d2413cddb1e3..5cb45c946d092d8e0d58d357397581b117bb616f 100644
--- a/api/funkwhale_api/history/tests/test_history.py
+++ b/api/funkwhale_api/history/tests/test_history.py
@@ -1,15 +1,16 @@
 import random
 import json
 from test_plus.test import TestCase
-from django.core.urlresolvers import reverse
+from django.urls import reverse
 from django.core.exceptions import ValidationError
 from django.utils import timezone
 
-from model_mommy import mommy
+from funkwhale_api.music.tests.factories import TrackFactory
 
 from funkwhale_api.users.models import User
 from funkwhale_api.history import models
 
+
 class TestHistory(TestCase):
 
     def setUp(self):
@@ -17,12 +18,12 @@ class TestHistory(TestCase):
         self.user = User.objects.create_user(username='test', email='test@test.com', password='test')
 
     def test_can_create_listening(self):
-        track = mommy.make('music.Track')
+        track = TrackFactory()
         now = timezone.now()
         l = models.Listening.objects.create(user=self.user, track=track)
 
     def test_anonymous_user_can_create_listening_via_api(self):
-        track = mommy.make('music.Track')
+        track = TrackFactory()
         url = self.reverse('api:v1:history:listenings-list')
         response = self.client.post(url, {
             'track': track.pk,
@@ -34,7 +35,7 @@ class TestHistory(TestCase):
         self.assertIsNotNone(listening.session_key)
 
     def test_logged_in_user_can_create_listening_via_api(self):
-        track = mommy.make('music.Track')
+        track = TrackFactory()
 
         self.client.login(username=self.user.username, password='test')
 
diff --git a/api/funkwhale_api/history/views.py b/api/funkwhale_api/history/views.py
index d65a70f8742ee958d728738dc1c9aa88c0c457c0..32bad6060273e53abad7de942c285b48b16c252e 100644
--- a/api/funkwhale_api/history/views.py
+++ b/api/funkwhale_api/history/views.py
@@ -22,14 +22,14 @@ class ListeningViewSet(mixins.CreateModelMixin,
 
     def get_queryset(self):
         queryset = super().get_queryset()
-        if self.request.user.is_authenticated():
+        if self.request.user.is_authenticated:
             return queryset.filter(user=self.request.user)
         else:
             return queryset.filter(session_key=self.request.session.session_key)
 
     def get_serializer_context(self):
         context = super().get_serializer_context()
-        if self.request.user.is_authenticated():
+        if self.request.user.is_authenticated:
             context['user'] = self.request.user
         else:
             context['session_key'] = self.request.session.session_key
diff --git a/api/funkwhale_api/music/migrations/0001_initial.py b/api/funkwhale_api/music/migrations/0001_initial.py
index e78647948a3cb2062cbc7b2163f3872602b1bbea..265b81577f7ca52a4cb37e48f499b96cc4622d5b 100644
--- a/api/funkwhale_api/music/migrations/0001_initial.py
+++ b/api/funkwhale_api/music/migrations/0001_initial.py
@@ -44,7 +44,7 @@ class Migration(migrations.Migration):
             fields=[
                 ('id', models.AutoField(primary_key=True, auto_created=True, serialize=False, verbose_name='ID')),
                 ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
-                ('submitted_by', models.ForeignKey(related_name='imports', to=settings.AUTH_USER_MODEL)),
+                ('submitted_by', models.ForeignKey(related_name='imports', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
             ],
         ),
         migrations.CreateModel(
@@ -54,7 +54,7 @@ class Migration(migrations.Migration):
                 ('source', models.URLField()),
                 ('mbid', models.UUIDField(editable=False)),
                 ('status', models.CharField(default='pending', choices=[('pending', 'Pending'), ('finished', 'finished')], max_length=30)),
-                ('batch', models.ForeignKey(related_name='jobs', to='music.ImportBatch')),
+                ('batch', models.ForeignKey(related_name='jobs', to='music.ImportBatch', on_delete=models.CASCADE)),
             ],
         ),
         migrations.CreateModel(
@@ -64,8 +64,8 @@ class Migration(migrations.Migration):
                 ('mbid', models.UUIDField(editable=False, blank=True, null=True)),
                 ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
                 ('title', models.CharField(max_length=255)),
-                ('album', models.ForeignKey(related_name='tracks', blank=True, null=True, to='music.Album')),
-                ('artist', models.ForeignKey(related_name='tracks', to='music.Artist')),
+                ('album', models.ForeignKey(related_name='tracks', blank=True, null=True, to='music.Album', on_delete=models.CASCADE)),
+                ('artist', models.ForeignKey(related_name='tracks', to='music.Artist', on_delete=models.CASCADE)),
             ],
             options={
                 'abstract': False,
@@ -78,12 +78,12 @@ class Migration(migrations.Migration):
                 ('audio_file', models.FileField(upload_to='tracks')),
                 ('source', models.URLField(blank=True, null=True)),
                 ('duration', models.IntegerField(blank=True, null=True)),
-                ('track', models.ForeignKey(related_name='files', to='music.Track')),
+                ('track', models.ForeignKey(related_name='files', to='music.Track', on_delete=models.CASCADE)),
             ],
         ),
         migrations.AddField(
             model_name='album',
             name='artist',
-            field=models.ForeignKey(related_name='albums', to='music.Artist'),
+            field=models.ForeignKey(related_name='albums', to='music.Artist', on_delete=models.CASCADE),
         ),
     ]
diff --git a/api/funkwhale_api/music/migrations/0009_auto_20160920_1614.py b/api/funkwhale_api/music/migrations/0009_auto_20160920_1614.py
index 2046a712765303f4044b627abed3813bf784a539..3a3d93989cdd436a6c9cbed010d1140e7bd933e1 100644
--- a/api/funkwhale_api/music/migrations/0009_auto_20160920_1614.py
+++ b/api/funkwhale_api/music/migrations/0009_auto_20160920_1614.py
@@ -39,11 +39,11 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='lyrics',
             name='work',
-            field=models.ForeignKey(related_name='lyrics', to='music.Work', blank=True, null=True),
+            field=models.ForeignKey(related_name='lyrics', to='music.Work', blank=True, null=True, on_delete=models.CASCADE),
         ),
         migrations.AddField(
             model_name='track',
             name='work',
-            field=models.ForeignKey(related_name='tracks', to='music.Work', blank=True, null=True),
+            field=models.ForeignKey(related_name='tracks', to='music.Work', blank=True, null=True, on_delete=models.CASCADE),
         ),
     ]
diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py
index 95a47fd4a7ac6fabe2cd842d6ed2d9994e26b1bd..b7cf0f25c82d8b6335b1fd30af7d96288f495de0 100644
--- a/api/funkwhale_api/music/models.py
+++ b/api/funkwhale_api/music/models.py
@@ -10,7 +10,7 @@ from django.conf import settings
 from django.db import models
 from django.core.files.base import ContentFile
 from django.core.files import File
-from django.core.urlresolvers import reverse
+from django.urls import reverse
 from django.utils import timezone
 from taggit.managers import TaggableManager
 from versatileimagefield.fields import VersatileImageField
@@ -108,7 +108,8 @@ def import_tracks(instance, cleaned_data, raw_data):
 
 class Album(APIModelMixin):
     title = models.CharField(max_length=255)
-    artist = models.ForeignKey(Artist, related_name='albums')
+    artist = models.ForeignKey(
+        Artist, related_name='albums', on_delete=models.CASCADE)
     release_date = models.DateField(null=True)
     release_group_id = models.UUIDField(null=True, blank=True)
     cover = VersatileImageField(upload_to='albums/covers/%Y/%m/%d', null=True, blank=True)
@@ -245,7 +246,12 @@ class Work(APIModelMixin):
 
 
 class Lyrics(models.Model):
-    work = models.ForeignKey(Work, related_name='lyrics', null=True, blank=True)
+    work = models.ForeignKey(
+        Work,
+        related_name='lyrics',
+        null=True,
+        blank=True,
+        on_delete=models.CASCADE)
     url = models.URLField(unique=True)
     content = models.TextField(null=True, blank=True)
 
@@ -268,10 +274,21 @@ class Lyrics(models.Model):
 
 class Track(APIModelMixin):
     title = models.CharField(max_length=255)
-    artist = models.ForeignKey(Artist, related_name='tracks')
+    artist = models.ForeignKey(
+        Artist, related_name='tracks', on_delete=models.CASCADE)
     position = models.PositiveIntegerField(null=True, blank=True)
-    album = models.ForeignKey(Album, related_name='tracks', null=True, blank=True)
-    work = models.ForeignKey(Work, related_name='tracks', null=True, blank=True)
+    album = models.ForeignKey(
+        Album,
+        related_name='tracks',
+        null=True,
+        blank=True,
+        on_delete=models.CASCADE)
+    work = models.ForeignKey(
+        Work,
+        related_name='tracks',
+        null=True,
+        blank=True,
+        on_delete=models.CASCADE)
 
     musicbrainz_model = 'recording'
     api = musicbrainz.api.recordings
@@ -340,7 +357,8 @@ class Track(APIModelMixin):
 
 
 class TrackFile(models.Model):
-    track = models.ForeignKey(Track, related_name='files')
+    track = models.ForeignKey(
+        Track, related_name='files', on_delete=models.CASCADE)
     audio_file = models.FileField(upload_to='tracks/%Y/%m/%d', max_length=255)
     source = models.URLField(null=True, blank=True)
     duration = models.IntegerField(null=True, blank=True)
@@ -376,7 +394,8 @@ class TrackFile(models.Model):
 
 class ImportBatch(models.Model):
     creation_date = models.DateTimeField(default=timezone.now)
-    submitted_by = models.ForeignKey('users.User', related_name='imports')
+    submitted_by = models.ForeignKey(
+        'users.User', related_name='imports', on_delete=models.CASCADE)
 
     class Meta:
         ordering = ['-creation_date']
@@ -392,9 +411,14 @@ class ImportBatch(models.Model):
         return 'finished'
 
 class ImportJob(models.Model):
-    batch = models.ForeignKey(ImportBatch, related_name='jobs')
+    batch = models.ForeignKey(
+        ImportBatch, related_name='jobs', on_delete=models.CASCADE)
     track_file = models.ForeignKey(
-        TrackFile, related_name='jobs', null=True, blank=True)
+        TrackFile,
+        related_name='jobs',
+        null=True,
+        blank=True,
+        on_delete=models.CASCADE)
     source = models.URLField()
     mbid = models.UUIDField(editable=False)
     STATUS_CHOICES = (
diff --git a/api/funkwhale_api/music/tests/factories.py b/api/funkwhale_api/music/tests/factories.py
index b554e3e14a934bc26281130dc869ec09907dc80f..567e2a765f426e8ca9ce4359bf6644431ef97dec 100644
--- a/api/funkwhale_api/music/tests/factories.py
+++ b/api/funkwhale_api/music/tests/factories.py
@@ -59,3 +59,30 @@ class ImportJobFactory(factory.django.DjangoModelFactory):
 
     class Meta:
         model = 'music.ImportJob'
+
+
+class WorkFactory(factory.django.DjangoModelFactory):
+    mbid = factory.Faker('uuid4')
+    language = 'eng'
+    nature = 'song'
+    title = factory.Faker('sentence', nb_words=3)
+
+    class Meta:
+        model = 'music.Work'
+
+
+class LyricsFactory(factory.django.DjangoModelFactory):
+    work = factory.SubFactory(WorkFactory)
+    url = factory.Faker('url')
+    content = factory.Faker('paragraphs', nb=4)
+
+    class Meta:
+        model = 'music.Lyrics'
+
+
+class TagFactory(factory.django.DjangoModelFactory):
+    name = factory.SelfAttribute('slug')
+    slug = factory.Faker('slug')
+
+    class Meta:
+        model = 'taggit.Tag'
diff --git a/api/funkwhale_api/music/tests/test_api.py b/api/funkwhale_api/music/tests/test_api.py
index b7c25424f322605bc47662743d921e0419ed3d3e..2460fa97d0830a8b66ceaeef80f680def65b02cc 100644
--- a/api/funkwhale_api/music/tests/test_api.py
+++ b/api/funkwhale_api/music/tests/test_api.py
@@ -1,7 +1,7 @@
 import json
 import unittest
 from test_plus.test import TestCase
-from django.core.urlresolvers import reverse
+from django.urls import reverse
 
 from funkwhale_api.music import models
 from funkwhale_api.utils.tests import TMPDirTestCaseMixin
diff --git a/api/funkwhale_api/music/tests/test_lyrics.py b/api/funkwhale_api/music/tests/test_lyrics.py
index 0ea22371bad091ed17796db24b58f414191979f4..9a05e5eb87b88752ea9aa81ac081a0e2981d9570 100644
--- a/api/funkwhale_api/music/tests/test_lyrics.py
+++ b/api/funkwhale_api/music/tests/test_lyrics.py
@@ -1,25 +1,26 @@
 import json
 import unittest
 from test_plus.test import TestCase
-from django.core.urlresolvers import reverse
-from model_mommy import mommy
+from django.urls import reverse
 
 from funkwhale_api.music import models
 from funkwhale_api.musicbrainz import api
 from funkwhale_api.music import serializers
 from funkwhale_api.users.models import User
+from funkwhale_api.music import lyrics as lyrics_utils
 
 from .mocking import lyricswiki
+from . import factories
 from . import data as api_data
-from funkwhale_api.music import lyrics as lyrics_utils
+
+
 
 class TestLyrics(TestCase):
 
     @unittest.mock.patch('funkwhale_api.music.lyrics._get_html',
                          return_value=lyricswiki.content)
     def test_works_import_lyrics_if_any(self, *mocks):
-        lyrics = mommy.make(
-            models.Lyrics,
+        lyrics = factories.LyricsFactory(
             url='http://lyrics.wikia.com/System_Of_A_Down:Chop_Suey!')
 
         lyrics.fetch_content()
@@ -42,7 +43,7 @@ Is it me you're looking for?
         content = """Hello
 Is it me you're looking for?"""
 
-        l = mommy.make(models.Lyrics, content=content)
+        l = factories.LyricsFactory(content=content)
 
         expected = "<p>Hello<br />Is it me you're looking for?</p>"
         self.assertHTMLEqual(expected, l.content_rendered)
@@ -54,8 +55,7 @@ Is it me you're looking for?"""
     @unittest.mock.patch('funkwhale_api.music.lyrics._get_html',
                          return_value=lyricswiki.content)
     def test_works_import_lyrics_if_any(self, *mocks):
-        track = mommy.make(
-            models.Track,
+        track = factories.TrackFactory(
             work=None,
             mbid='07ca77cf-f513-4e9c-b190-d7e24bbad448')
 
diff --git a/api/funkwhale_api/music/tests/test_music.py b/api/funkwhale_api/music/tests/test_music.py
index cc01bc9edcacbae05c0e9cf4280d78df4d49a193..5cf9d0cf96d53c6060759b6a8c65cf1aaef6917f 100644
--- a/api/funkwhale_api/music/tests/test_music.py
+++ b/api/funkwhale_api/music/tests/test_music.py
@@ -2,13 +2,11 @@ from test_plus.test import TestCase
 import unittest.mock
 from funkwhale_api.music import models
 import datetime
-from model_mommy import mommy
+
+from . import factories
 from . import data as api_data
 from .cover import binary_data
 
-def prettyprint(d):
-    import json
-    print(json.dumps(d, sort_keys=True, indent=4))
 
 class TestMusic(TestCase):
 
@@ -79,9 +77,9 @@ class TestMusic(TestCase):
         self.assertEqual(track, track2)
 
     def test_album_tags_deduced_from_tracks_tags(self):
-        tag = mommy.make('taggit.Tag')
-        album = mommy.make('music.Album')
-        tracks = mommy.make('music.Track', album=album, _quantity=5)
+        tag = factories.TagFactory()
+        album = factories.AlbumFactory()
+        tracks = factories.TrackFactory.create_batch(album=album, size=5)
 
         for track in tracks:
             track.tags.add(tag)
@@ -92,10 +90,10 @@ class TestMusic(TestCase):
             self.assertIn(tag, album.tags)
 
     def test_artist_tags_deduced_from_album_tags(self):
-        tag = mommy.make('taggit.Tag')
-        artist = mommy.make('music.Artist')
-        album = mommy.make('music.Album', artist=artist)
-        tracks = mommy.make('music.Track', album=album, _quantity=5)
+        tag = factories.TagFactory()
+        artist = factories.ArtistFactory()
+        album = factories.AlbumFactory(artist=artist)
+        tracks = factories.TrackFactory.create_batch(album=album, size=5)
 
         for track in tracks:
             track.tags.add(tag)
@@ -108,7 +106,7 @@ class TestMusic(TestCase):
     @unittest.mock.patch('funkwhale_api.musicbrainz.api.images.get_front', return_value=binary_data)
     def test_can_download_image_file_for_album(self, *mocks):
         # client._api.get_image_front('55ea4f82-b42b-423e-a0e5-290ccdf443ed')
-        album = mommy.make('music.Album', mbid='55ea4f82-b42b-423e-a0e5-290ccdf443ed')
+        album = factories.AlbumFactory(mbid='55ea4f82-b42b-423e-a0e5-290ccdf443ed')
         album.get_image()
         album.save()
 
diff --git a/api/funkwhale_api/music/tests/test_works.py b/api/funkwhale_api/music/tests/test_works.py
index 84cb51cde09a54c131f76636f6d13545381cd063..55714bce2c142c36b86f52c9361292df79689695 100644
--- a/api/funkwhale_api/music/tests/test_works.py
+++ b/api/funkwhale_api/music/tests/test_works.py
@@ -1,23 +1,24 @@
 import json
 import unittest
 from test_plus.test import TestCase
-from django.core.urlresolvers import reverse
-from model_mommy import mommy
+from django.urls import reverse
 
 from funkwhale_api.music import models
 from funkwhale_api.musicbrainz import api
 from funkwhale_api.music import serializers
+from funkwhale_api.music.tests import factories
 from funkwhale_api.users.models import User
 
 from . import data as api_data
 
+
 class TestWorks(TestCase):
 
     @unittest.mock.patch('funkwhale_api.musicbrainz.api.works.get',
                          return_value=api_data.works['get']['chop_suey'])
     def test_can_import_work(self, *mocks):
-        recording = mommy.make(
-            models.Track, mbid='07ca77cf-f513-4e9c-b190-d7e24bbad448')
+        recording = factories.TrackFactory(
+            mbid='07ca77cf-f513-4e9c-b190-d7e24bbad448')
         mbid = 'e2ecabc4-1b9d-30b2-8f30-3596ec423dc5'
         work = models.Work.create_from_api(id=mbid)
 
@@ -36,8 +37,7 @@ class TestWorks(TestCase):
     @unittest.mock.patch('funkwhale_api.musicbrainz.api.recordings.get',
                          return_value=api_data.tracks['get']['chop_suey'])
     def test_can_get_work_from_recording(self, *mocks):
-        recording = mommy.make(
-            models.Track,
+        recording = factories.TrackFactory(
             work=None,
             mbid='07ca77cf-f513-4e9c-b190-d7e24bbad448')
         mbid = 'e2ecabc4-1b9d-30b2-8f30-3596ec423dc5'
diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py
index 72982e4c5b916360e6f02d811507757c51dcb61a..c32fa8f7ff49caa1ef9fbfff533e12a32835373e 100644
--- a/api/funkwhale_api/music/views.py
+++ b/api/funkwhale_api/music/views.py
@@ -2,7 +2,7 @@ import os
 import json
 import unicodedata
 import urllib
-from django.core.urlresolvers import reverse
+from django.urls import reverse
 from django.db import models, transaction
 from django.db.models.functions import Length
 from django.conf import settings
@@ -102,7 +102,7 @@ class TrackViewSet(TagViewSetMixin, SearchMixin, viewsets.ReadOnlyModelViewSet):
         queryset = super().get_queryset()
         filter_favorites = self.request.GET.get('favorites', None)
         user = self.request.user
-        if user.is_authenticated() and filter_favorites == 'true':
+        if user.is_authenticated and filter_favorites == 'true':
             queryset = queryset.filter(track_favorites__user=user)
 
         return queryset
diff --git a/api/funkwhale_api/musicbrainz/tests/test_api.py b/api/funkwhale_api/musicbrainz/tests/test_api.py
index f962e0f781e515716b808c7697fd0b63692cf174..b0911f1c54983fd0f41eda12027ea0c836d587a0 100644
--- a/api/funkwhale_api/musicbrainz/tests/test_api.py
+++ b/api/funkwhale_api/musicbrainz/tests/test_api.py
@@ -1,7 +1,7 @@
 import json
 import unittest
 from test_plus.test import TestCase
-from django.core.urlresolvers import reverse
+from django.urls import reverse
 
 from funkwhale_api.musicbrainz import api
 from . import data as api_data
diff --git a/api/funkwhale_api/playlists/migrations/0001_initial.py b/api/funkwhale_api/playlists/migrations/0001_initial.py
index f42ca154e6064cb9de8f2f8dbb28c114e37dc5bd..bc97d81227ea49f8b46c0e0344fca6978a57b959 100644
--- a/api/funkwhale_api/playlists/migrations/0001_initial.py
+++ b/api/funkwhale_api/playlists/migrations/0001_initial.py
@@ -22,7 +22,7 @@ class Migration(migrations.Migration):
                 ('name', models.CharField(max_length=50)),
                 ('is_public', models.BooleanField(default=False)),
                 ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
-                ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='playlists')),
+                ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='playlists', on_delete=models.CASCADE)),
             ],
         ),
         migrations.CreateModel(
@@ -33,9 +33,9 @@ class Migration(migrations.Migration):
                 ('rght', models.PositiveIntegerField(db_index=True, editable=False)),
                 ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
                 ('position', models.PositiveIntegerField(db_index=True, editable=False)),
-                ('playlist', models.ForeignKey(to='playlists.Playlist', related_name='playlist_tracks')),
-                ('previous', mptt.fields.TreeOneToOneField(null=True, to='playlists.PlaylistTrack', related_name='next', blank=True)),
-                ('track', models.ForeignKey(to='music.Track', related_name='playlist_tracks')),
+                ('playlist', models.ForeignKey(to='playlists.Playlist', related_name='playlist_tracks', on_delete=models.CASCADE)),
+                ('previous', mptt.fields.TreeOneToOneField(null=True, to='playlists.PlaylistTrack', related_name='next', blank=True, on_delete=models.CASCADE)),
+                ('track', models.ForeignKey(to='music.Track', related_name='playlist_tracks', on_delete=models.CASCADE)),
             ],
             options={
                 'ordering': ('-playlist', 'position'),
diff --git a/api/funkwhale_api/playlists/models.py b/api/funkwhale_api/playlists/models.py
index 35a30a704a06a494d0b37c322150cc6ee81cc660..e89dce81c93d23a4827e2b812e5894abcc948485 100644
--- a/api/funkwhale_api/playlists/models.py
+++ b/api/funkwhale_api/playlists/models.py
@@ -7,7 +7,8 @@ from mptt.models import MPTTModel, TreeOneToOneField
 class Playlist(models.Model):
     name = models.CharField(max_length=50)
     is_public = models.BooleanField(default=False)
-    user = models.ForeignKey('users.User', related_name="playlists")
+    user = models.ForeignKey(
+        'users.User', related_name="playlists", on_delete=models.CASCADE)
     creation_date = models.DateTimeField(default=timezone.now)
 
     def __str__(self):
@@ -21,9 +22,18 @@ class Playlist(models.Model):
 
 
 class PlaylistTrack(MPTTModel):
-    track = models.ForeignKey('music.Track', related_name='playlist_tracks')
-    previous = TreeOneToOneField('self', blank=True, null=True, related_name='next')
-    playlist = models.ForeignKey(Playlist, related_name='playlist_tracks')
+    track = models.ForeignKey(
+        'music.Track',
+        related_name='playlist_tracks',
+        on_delete=models.CASCADE)
+    previous = TreeOneToOneField(
+        'self',
+        blank=True,
+        null=True,
+        related_name='next',
+        on_delete=models.CASCADE)
+    playlist = models.ForeignKey(
+        Playlist, related_name='playlist_tracks', on_delete=models.CASCADE)
 
     class MPTTMeta:
         level_attr = 'position'
diff --git a/api/funkwhale_api/playlists/tests/test_playlists.py b/api/funkwhale_api/playlists/tests/test_playlists.py
index 49025cd11b078e072e196325ce32506b448e142f..2f61889ee13d5afbc949ba36a06e9b30b6dbfad7 100644
--- a/api/funkwhale_api/playlists/tests/test_playlists.py
+++ b/api/funkwhale_api/playlists/tests/test_playlists.py
@@ -1,11 +1,10 @@
 import json
 from test_plus.test import TestCase
-from django.core.urlresolvers import reverse
+from django.urls import reverse
 from django.core.exceptions import ValidationError
 from django.utils import timezone
 
-from model_mommy import mommy
-
+from funkwhale_api.music.tests import factories
 from funkwhale_api.users.models import User
 from funkwhale_api.playlists import models
 from funkwhale_api.playlists.serializers import PlaylistSerializer
@@ -18,7 +17,7 @@ class TestPlayLists(TestCase):
         self.user = User.objects.create_user(username='test', email='test@test.com', password='test')
 
     def test_can_create_playlist(self):
-        tracks = list(mommy.make('music.Track', _quantity=5))
+        tracks = factories.TrackFactory.create_batch(size=5)
         playlist = models.Playlist.objects.create(user=self.user, name="test")
 
         previous = None
@@ -49,7 +48,7 @@ class TestPlayLists(TestCase):
         self.assertEqual(playlist.name, 'test')
 
     def test_can_add_playlist_track_via_api(self):
-        tracks = list(mommy.make('music.Track', _quantity=5))
+        tracks = factories.TrackFactory.create_batch(size=5)
         playlist = models.Playlist.objects.create(user=self.user, name="test")
 
         self.client.login(username=self.user.username, password='test')
diff --git a/api/funkwhale_api/providers/urls.py b/api/funkwhale_api/providers/urls.py
index a6d417e5b588245f14c22afae73102b29a07a130..10975da53b6db28599d61d8d1e75d19ea136a0a5 100644
--- a/api/funkwhale_api/providers/urls.py
+++ b/api/funkwhale_api/providers/urls.py
@@ -1,8 +1,11 @@
 from django.conf.urls import include, url
 from funkwhale_api.music import views
 
-
 urlpatterns = [
-    url(r'^youtube/', include('funkwhale_api.providers.youtube.urls', namespace='youtube')),
-    url(r'^musicbrainz/', include('funkwhale_api.musicbrainz.urls', namespace='musicbrainz')),
+    url(r'^youtube/', include(
+        ('funkwhale_api.providers.youtube.urls', 'youtube'),
+        namespace='youtube')),
+    url(r'^musicbrainz/', include(
+        ('funkwhale_api.musicbrainz.urls', 'musicbrainz'),
+        namespace='musicbrainz')),
 ]
diff --git a/api/funkwhale_api/providers/youtube/tests/test_youtube.py b/api/funkwhale_api/providers/youtube/tests/test_youtube.py
index db6bd8413f24bc77035737c936591701f14dd80a..8a1dd1eb70a601e56082ec7dce8fe3cae32c7095 100644
--- a/api/funkwhale_api/providers/youtube/tests/test_youtube.py
+++ b/api/funkwhale_api/providers/youtube/tests/test_youtube.py
@@ -2,7 +2,7 @@ import json
 from collections import OrderedDict
 import unittest
 from test_plus.test import TestCase
-from django.core.urlresolvers import reverse
+from django.urls import reverse
 from funkwhale_api.providers.youtube.client import client
 
 from . import data as api_data
diff --git a/api/funkwhale_api/radios/migrations/0001_initial.py b/api/funkwhale_api/radios/migrations/0001_initial.py
index 9ec25805db927845610d9f3f071e574b8d450904..46faf749edaf0d0baa5e98ce320ad4636b8426a4 100644
--- a/api/funkwhale_api/radios/migrations/0001_initial.py
+++ b/api/funkwhale_api/radios/migrations/0001_initial.py
@@ -20,7 +20,7 @@ class Migration(migrations.Migration):
                 ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)),
                 ('radio_type', models.CharField(max_length=50)),
                 ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
-                ('user', models.ForeignKey(related_name='radio_sessions', blank=True, to=settings.AUTH_USER_MODEL, null=True)),
+                ('user', models.ForeignKey(related_name='radio_sessions', blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE)),
             ],
         ),
         migrations.CreateModel(
@@ -28,8 +28,8 @@ class Migration(migrations.Migration):
             fields=[
                 ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)),
                 ('position', models.IntegerField(default=1)),
-                ('session', models.ForeignKey(to='radios.RadioSession', related_name='session_tracks')),
-                ('track', models.ForeignKey(to='music.Track', related_name='radio_session_tracks')),
+                ('session', models.ForeignKey(to='radios.RadioSession', related_name='session_tracks', on_delete=models.CASCADE)),
+                ('track', models.ForeignKey(to='music.Track', related_name='radio_session_tracks', on_delete=models.CASCADE)),
             ],
             options={
                 'ordering': ('session', 'position'),
diff --git a/api/funkwhale_api/radios/migrations/0003_auto_20160521_1708.py b/api/funkwhale_api/radios/migrations/0003_auto_20160521_1708.py
index 4629d68fefb121b0ed5e2f975cd7c1e08b53e3b3..7c70abc2e12ed6309de4bf274ff94a26b2ba20e6 100644
--- a/api/funkwhale_api/radios/migrations/0003_auto_20160521_1708.py
+++ b/api/funkwhale_api/radios/migrations/0003_auto_20160521_1708.py
@@ -15,7 +15,7 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='radiosession',
             name='related_object_content_type',
-            field=models.ForeignKey(null=True, to='contenttypes.ContentType', blank=True),
+            field=models.ForeignKey(null=True, to='contenttypes.ContentType', blank=True, on_delete=models.CASCADE),
         ),
         migrations.AddField(
             model_name='radiosession',
diff --git a/api/funkwhale_api/radios/models.py b/api/funkwhale_api/radios/models.py
index a3a3531328a0565bd3c287377c40b855a8f1ec64..984b34a1f7d86230c6e9186c123246286f0c6eac 100644
--- a/api/funkwhale_api/radios/models.py
+++ b/api/funkwhale_api/radios/models.py
@@ -7,11 +7,20 @@ from django.contrib.contenttypes.models import ContentType
 from funkwhale_api.music.models import Track
 
 class RadioSession(models.Model):
-    user = models.ForeignKey('users.User', related_name='radio_sessions', null=True, blank=True)
+    user = models.ForeignKey(
+        'users.User',
+        related_name='radio_sessions',
+        null=True,
+        blank=True,
+        on_delete=models.CASCADE)
     session_key = models.CharField(max_length=100, null=True, blank=True)
     radio_type = models.CharField(max_length=50)
     creation_date = models.DateTimeField(default=timezone.now)
-    related_object_content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, blank=True, null=True)
+    related_object_content_type = models.ForeignKey(
+        ContentType,
+        blank=True,
+        null=True,
+        on_delete=models.CASCADE)
     related_object_id = models.PositiveIntegerField(blank=True, null=True)
     related_object = GenericForeignKey('related_object_content_type', 'related_object_id')
 
@@ -43,9 +52,11 @@ class RadioSession(models.Model):
         return registry[self.radio_type](session=self)
 
 class RadioSessionTrack(models.Model):
-    session = models.ForeignKey(RadioSession, related_name='session_tracks')
+    session = models.ForeignKey(
+        RadioSession, related_name='session_tracks', on_delete=models.CASCADE)
     position = models.IntegerField(default=1)
-    track = models.ForeignKey(Track, related_name='radio_session_tracks')
+    track = models.ForeignKey(
+        Track, related_name='radio_session_tracks', on_delete=models.CASCADE)
 
     class Meta:
         ordering = ('session', 'position')
diff --git a/api/funkwhale_api/radios/tests/test_radios.py b/api/funkwhale_api/radios/tests/test_radios.py
index 5729b4412104d906e208674177b893414ea9a44b..ab27d45166a343c3bb731608db750312b934d241 100644
--- a/api/funkwhale_api/radios/tests/test_radios.py
+++ b/api/funkwhale_api/radios/tests/test_radios.py
@@ -1,16 +1,18 @@
 import random
 import json
 from test_plus.test import TestCase
-from django.core.urlresolvers import reverse
+from django.urls import reverse
 from django.core.exceptions import ValidationError
 
-from model_mommy import mommy
 
 from funkwhale_api.radios import radios
 from funkwhale_api.radios import models
 from funkwhale_api.favorites.models import TrackFavorite
 from funkwhale_api.users.models import User
 from funkwhale_api.music.models import Artist
+from funkwhale_api.music.tests import factories
+from funkwhale_api.history.tests.factories import ListeningFactory
+
 
 class TestRadios(TestCase):
 
@@ -55,7 +57,7 @@ class TestRadios(TestCase):
         self.assertTrue(picks[2] > picks[1])
 
     def test_can_get_choices_for_favorites_radio(self):
-        tracks = mommy.make('music.Track', _quantity=100)
+        tracks = factories.TrackFactory.create_batch(size=100)
 
         for i in range(20):
             TrackFavorite.add(track=random.choice(tracks), user=self.user)
@@ -73,7 +75,7 @@ class TestRadios(TestCase):
             self.assertIn(pick, choices)
 
     def test_can_use_radio_session_to_filter_choices(self):
-        tracks = mommy.make('music.Track', _quantity=30)
+        tracks = factories.TrackFactory.create_batch(size=30)
         radio = radios.RandomRadio()
         session = radio.start_session(self.user)
 
@@ -85,7 +87,7 @@ class TestRadios(TestCase):
         self.assertEqual(len(set(tracks_id)), 30)
 
     def test_can_restore_radio_from_previous_session(self):
-        tracks = mommy.make('music.Track', _quantity=30)
+        tracks = factories.TrackFactory.create_batch(size=30)
 
         radio = radios.RandomRadio()
         session = radio.start_session(self.user)
@@ -115,7 +117,7 @@ class TestRadios(TestCase):
         self.assertIsNotNone(session.session_key)
 
     def test_can_get_track_for_session_from_api(self):
-        tracks = mommy.make('music.Track', _quantity=1)
+        tracks = factories.TrackFactory.create_batch(size=1)
 
         self.client.login(username=self.user.username, password='test')
         url = reverse('api:v1:radios:sessions-list')
@@ -129,7 +131,7 @@ class TestRadios(TestCase):
         self.assertEqual(data['track']['id'], tracks[0].id)
         self.assertEqual(data['position'], 1)
 
-        next_track = mommy.make('music.Track')
+        next_track = factories.TrackFactory()
         response = self.client.post(url, {'session': session.pk})
         data = json.loads(response.content.decode('utf-8'))
 
@@ -148,9 +150,10 @@ class TestRadios(TestCase):
             radio.start_session(self.user, related_object=self.user)
 
     def test_can_start_artist_radio(self):
-        artist = mommy.make('music.Artist')
-        wrong_tracks = mommy.make('music.Track', _quantity=30)
-        good_tracks = mommy.make('music.Track', artist=artist, _quantity=5)
+        artist = factories.ArtistFactory()
+        wrong_tracks = factories.TrackFactory.create_batch(size=30)
+        good_tracks = factories.TrackFactory.create_batch(
+            artist=artist, size=5)
 
         radio = radios.ArtistRadio()
         session = radio.start_session(self.user, related_object=artist)
@@ -159,9 +162,9 @@ class TestRadios(TestCase):
             self.assertIn(radio.pick(), good_tracks)
 
     def test_can_start_tag_radio(self):
-        tag = mommy.make('taggit.Tag')
-        wrong_tracks = mommy.make('music.Track', _quantity=30)
-        good_tracks = mommy.make('music.Track', _quantity=5)
+        tag = factories.TagFactory()
+        wrong_tracks = factories.TrackFactory.create_batch(size=30)
+        good_tracks = factories.TrackFactory.create_batch(size=5)
         for track in good_tracks:
             track.tags.add(tag)
 
@@ -172,7 +175,7 @@ class TestRadios(TestCase):
             self.assertIn(radio.pick(), good_tracks)
 
     def test_can_start_artist_radio_from_api(self):
-        artist = mommy.make('music.Artist')
+        artist = factories.ArtistFactory()
         url = reverse('api:v1:radios:sessions-list')
 
         response = self.client.post(url, {'radio_type': 'artist', 'related_object_id': artist.id})
@@ -181,10 +184,10 @@ class TestRadios(TestCase):
         self.assertEqual(session.related_object, artist)
 
     def test_can_start_less_listened_radio(self):
-        history = mommy.make('history.Listening', _quantity=5, user=self.user)
+        history = ListeningFactory.create_batch(size=5, user=self.user)
         wrong_tracks = [h.track for h in history]
 
-        good_tracks = mommy.make('music.Track', _quantity=30)
+        good_tracks = factories.TrackFactory.create_batch(size=30)
 
         radio = radios.LessListenedRadio()
         session = radio.start_session(self.user)
diff --git a/api/funkwhale_api/radios/urls.py b/api/funkwhale_api/radios/urls.py
index 57e47f06371c60fe09f54c7a43efe81fb3e07b6d..8c31df0937c748639fcf524bd234ec7a966309b0 100644
--- a/api/funkwhale_api/radios/urls.py
+++ b/api/funkwhale_api/radios/urls.py
@@ -6,4 +6,5 @@ router = routers.SimpleRouter()
 router.register(r'sessions', views.RadioSessionViewSet, 'sessions')
 router.register(r'tracks', views.RadioSessionTrackViewSet, 'tracks')
 
+
 urlpatterns = router.urls
diff --git a/api/funkwhale_api/radios/views.py b/api/funkwhale_api/radios/views.py
index 1ae788fcb0aad9c2d787f8f4862ff8bf3782d899..9243d6a90cc5c2ed7a8c420d2f1cc5cea5a9d41d 100644
--- a/api/funkwhale_api/radios/views.py
+++ b/api/funkwhale_api/radios/views.py
@@ -19,14 +19,14 @@ class RadioSessionViewSet(mixins.CreateModelMixin,
 
     def get_queryset(self):
         queryset = super().get_queryset()
-        if self.request.user.is_authenticated():
+        if self.request.user.is_authenticated:
             return queryset.filter(user=self.request.user)
         else:
             return queryset.filter(session_key=self.request.session.session_key)
 
     def get_serializer_context(self):
         context = super().get_serializer_context()
-        if self.request.user.is_authenticated():
+        if self.request.user.is_authenticated:
             context['user'] = self.request.user
         else:
             context['session_key'] = self.request.session.session_key
@@ -44,7 +44,7 @@ class RadioSessionTrackViewSet(mixins.CreateModelMixin,
         serializer.is_valid(raise_exception=True)
         session = serializer.validated_data['session']
         try:
-            if request.user.is_authenticated():
+            if request.user.is_authenticated:
                 assert request.user == session.user
             else:
                 assert request.session.session_key == session.session_key
diff --git a/api/funkwhale_api/users/middleware.py b/api/funkwhale_api/users/middleware.py
index 0f572c20388043225b714d3e5f1a8891fdc046dd..e3eba95f3ff5813b7eab16a633ccb1121e8851f3 100644
--- a/api/funkwhale_api/users/middleware.py
+++ b/api/funkwhale_api/users/middleware.py
@@ -1,6 +1,11 @@
 
 
-class AnonymousSessionMiddleware(object):
-    def process_request(self, request):
+class AnonymousSessionMiddleware:
+    def __init__(self, get_response):
+        self.get_response = get_response
+
+    def __call__(self, request):
         if not request.session.session_key:
             request.session.save()
+        response = self.get_response(request)
+        return response
diff --git a/api/funkwhale_api/users/migrations/0001_initial.py b/api/funkwhale_api/users/migrations/0001_initial.py
index 8327d2890036add7862845f93c21e7f1251c71d2..ef9240c9158d795b0e1201f176ec14e8df5b5e86 100644
--- a/api/funkwhale_api/users/migrations/0001_initial.py
+++ b/api/funkwhale_api/users/migrations/0001_initial.py
@@ -38,7 +38,7 @@ class Migration(migrations.Migration):
                 'verbose_name_plural': 'users',
             },
             managers=[
-                (b'objects', django.contrib.auth.models.UserManager()),
+                ('objects', django.contrib.auth.models.UserManager()),
             ],
         ),
     ]
diff --git a/api/funkwhale_api/users/models.py b/api/funkwhale_api/users/models.py
index c5ca896ab667e6e69f588dd4446643844003382e..c8d0b534c8b84706fcd050eee819e889e186f525 100644
--- a/api/funkwhale_api/users/models.py
+++ b/api/funkwhale_api/users/models.py
@@ -2,7 +2,7 @@
 from __future__ import unicode_literals, absolute_import
 
 from django.contrib.auth.models import AbstractUser
-from django.core.urlresolvers import reverse
+from django.urls import reverse
 from django.db import models
 from django.utils.encoding import python_2_unicode_compatible
 from django.utils.translation import ugettext_lazy as _
diff --git a/api/requirements/base.txt b/api/requirements/base.txt
index 3a11afadf509cfa04aae62a9ca566d23f28f2646..aee1222591882c83062e190a2a7c8b36a10dc7d7 100644
--- a/api/requirements/base.txt
+++ b/api/requirements/base.txt
@@ -1,59 +1,59 @@
 # Bleeding edge Django
-django==1.11
+django>=2.0,<2.1
 
 # Configuration
-django-environ==0.4.0
-django-secure==1.0.1
-whitenoise==2.0.6
-
-# Models
-django-model-utils==2.3.1
+django-environ>=0.4,<0.5
+whitenoise>=3.3,<3.4
 
 # Images
-Pillow==3.0.0
+Pillow>=4.3,<4.4
 
 # For user registration, either via email or social
 # Well-built with regular release cycles!
-django-allauth==0.24.1
+django-allauth>=0.34,<0.35
 
 
 # Python-PostgreSQL Database Adapter
-psycopg2==2.6.1
+psycopg2>=2.7,<=2.8
 
 # Time zones support
-pytz==2015.7
+pytz==2017.3
 
 # Redis support
-django-redis==4.3.0
-redis>=2.10.0
+django-redis>=4.5,<4.6
+redis>=2.10,<2.11
 
 
-celery==3.1.19
+celery>=3.1,<3.2
 
 
 # Your custom requirements go here
-django-cors-headers==2.1.0
+django-cors-headers>=2.1,<2.2
 musicbrainzngs==0.6
-youtube_dl>=2015.12.21
-djangorestframework==3.6.3
-djangorestframework-jwt==1.11.0
-django-celery==3.2.1
-django-mptt==0.8.7
-google-api-python-client==1.6.2
-arrow==0.10.0
-django-taggit==0.22.1
-persisting-theory==0.2.1
-django-versatileimagefield==1.7.1
-django-cachalot==1.5.0
-django-filter==1.1
-django-rest-auth==0.9.1
-beautifulsoup4==4.6.0
-Markdown==2.6.8
-ipython==6.1.0
-mutagen==1.38
+youtube_dl>=2017.12.14
+djangorestframework>=3.7,<3.8
+djangorestframework-jwt>=1.11,<1.12
+django-celery>=3.2,<3.3
+django-mptt>=0.9,<0.10
+google-api-python-client>=1.6,<1.7
+arrow>=0.12,<0.13
+persisting-theory>=0.2,<0.3
+django-versatileimagefield>=1.8,<1.9
+django-filter>=1.1,<1.2
+django-rest-auth>=0.9,<0.10
+beautifulsoup4>=4.6,<4.7
+Markdown>=2.6,<2.7
+ipython>=6,<7
+mutagen>=1.39,<1.40
 
 
+# Until this is merged
+#django-taggit>=0.22,<0.23
+git+https://github.com/jdufresne/django-taggit.git@e8f7f216f04c9781bebc84363ab24d575f948ede
 # Until this is merged
 git+https://github.com/EliotBerriot/PyMemoize.git@django
+# Until this is merged
+#django-cachalot==1.5.0
+git+https://github.com/EliotBerriot/django-cachalot.git@django-2
 
-django-dynamic-preferences>=1.3,<1.4
+django-dynamic-preferences>=1.5,<1.6
diff --git a/api/requirements/local.txt b/api/requirements/local.txt
index 3f681b26d59d131468b54bda896477a3f6191624..d8a1561e0a31705150d00bdf48ffeaf3431ee4da 100644
--- a/api/requirements/local.txt
+++ b/api/requirements/local.txt
@@ -1,15 +1,15 @@
 # Local development dependencies go here
--r base.txt
-coverage==4.0.3
-django_coverage_plugin==1.1
-Sphinx==1.6.2
-django-extensions==1.5.9
-Werkzeug==0.11.2
-django-test-plus==1.0.11
+
+coverage>=4.4,<4.5
+django_coverage_plugin>=1.5,<1.6
+Sphinx>=1.6,<1.7
+django-extensions>=1.9,<1.10
+Werkzeug>=0.13,<0.14
+django-test-plus>=1.0.20
 factory_boy>=2.8.1
 
 # django-debug-toolbar that works with Django 1.5+
-django-debug-toolbar>=1.5,<1.6
+django-debug-toolbar>=1.9,<1.10
 
 # improved REPL
 ipdb==0.8.1
diff --git a/api/requirements/production.txt b/api/requirements/production.txt
index 10d05fd3413c3362c0c2cbebcfbbb3fbd42995ba..42b66eb1535792ae0bb5a8407c0c140df1b90049 100644
--- a/api/requirements/production.txt
+++ b/api/requirements/production.txt
@@ -1,8 +1,5 @@
 # Pro-tip: Try not to put anything here. There should be no dependency in
 #	production that isn't in development.
--r base.txt
-
-
 
 # WSGI Handler
 # ------------------------------------------------
diff --git a/api/requirements/test.txt b/api/requirements/test.txt
index 7c304bbdb430bb46647c713a6e0cd1d85ed4111e..bde5a2df97b8b21f2a1d7dfe20ad4e18d38eea1c 100644
--- a/api/requirements/test.txt
+++ b/api/requirements/test.txt
@@ -1,9 +1,6 @@
 # Test dependencies go here.
--r local.txt
 
-
-flake8==2.5.0
-model-mommy==1.3.2
+flake8
 pytest
 pytest-django
 pytest-mock