From bfeb86865dde65004904e9f280f53fe13012c96a Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Wed, 7 Mar 2018 22:34:16 +0100
Subject: [PATCH] Fix #114: can now filter artists and albums with no
 listenable tracks

---
 api/funkwhale_api/music/filters.py       | 30 ++++++++++++++--
 api/funkwhale_api/music/views.py         |  3 ++
 api/tests/conftest.py                    |  6 ++++
 api/tests/music/test_views.py            | 45 ++++++++++++++++++++++++
 changes/changelog.d/114.feature          |  1 +
 front/src/components/library/Artists.vue |  3 +-
 6 files changed, 84 insertions(+), 4 deletions(-)
 create mode 100644 api/tests/music/test_views.py
 create mode 100644 changes/changelog.d/114.feature

diff --git a/api/funkwhale_api/music/filters.py b/api/funkwhale_api/music/filters.py
index ff937a0f..fbea3735 100644
--- a/api/funkwhale_api/music/filters.py
+++ b/api/funkwhale_api/music/filters.py
@@ -1,12 +1,36 @@
-import django_filters
+from django.db.models import Count
+
+from django_filters import rest_framework as filters
 
 from . import models
 
 
-class ArtistFilter(django_filters.FilterSet):
+class ListenableMixin(filters.FilterSet):
+    listenable = filters.BooleanFilter(name='_', method='filter_listenable')
+
+    def filter_listenable(self, queryset, name, value):
+        queryset = queryset.annotate(
+            files_count=Count('tracks__files')
+        )
+        if value:
+            return queryset.filter(files_count__gt=0)
+        else:
+            return queryset.filter(files_count=0)
+
+
+class ArtistFilter(ListenableMixin):
 
     class Meta:
         model = models.Artist
         fields = {
-            'name': ['exact', 'iexact', 'startswith', 'icontains']
+            'name': ['exact', 'iexact', 'startswith', 'icontains'],
+            'listenable': 'exact',
         }
+
+
+class AlbumFilter(ListenableMixin):
+    listenable = filters.BooleanFilter(name='_', method='filter_listenable')
+
+    class Meta:
+        model = models.Album
+        fields = ['listenable']
diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py
index d026c984..78a35887 100644
--- a/api/funkwhale_api/music/views.py
+++ b/api/funkwhale_api/music/views.py
@@ -54,6 +54,7 @@ class TagViewSetMixin(object):
             queryset = queryset.filter(tags__pk=tag)
         return queryset
 
+
 class ArtistViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet):
     queryset = (
         models.Artist.objects.all()
@@ -67,6 +68,7 @@ class ArtistViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet):
     filter_class = filters.ArtistFilter
     ordering_fields = ('id', 'name', 'creation_date')
 
+
 class AlbumViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet):
     queryset = (
         models.Album.objects.all()
@@ -78,6 +80,7 @@ class AlbumViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet):
     permission_classes = [ConditionalAuthentication]
     search_fields = ['title__unaccent']
     ordering_fields = ('creation_date',)
+    filter_class = filters.AlbumFilter
 
 
 class ImportBatchViewSet(
diff --git a/api/tests/conftest.py b/api/tests/conftest.py
index 2d655f23..4ff1a8ee 100644
--- a/api/tests/conftest.py
+++ b/api/tests/conftest.py
@@ -4,6 +4,7 @@ import pytest
 from django.core.cache import cache as django_cache
 from dynamic_preferences.registries import global_preferences_registry
 from rest_framework.test import APIClient
+from rest_framework.test import APIRequestFactory
 
 from funkwhale_api.activity import record
 from funkwhale_api.taskapp import celery
@@ -84,6 +85,11 @@ def superuser_client(db, factories, client):
     delattr(client, 'user')
 
 
+@pytest.fixture
+def api_request():
+    return APIRequestFactory()
+
+
 @pytest.fixture
 def activity_registry():
     r = record.registry
diff --git a/api/tests/music/test_views.py b/api/tests/music/test_views.py
new file mode 100644
index 00000000..29560461
--- /dev/null
+++ b/api/tests/music/test_views.py
@@ -0,0 +1,45 @@
+import pytest
+
+from funkwhale_api.music import views
+
+
+@pytest.mark.parametrize('param,expected', [
+    ('true', 'full'),
+    ('false', 'empty'),
+])
+def test_artist_view_filter_listenable(
+        param, expected, factories, api_request):
+    artists = {
+        'empty': factories['music.Artist'](),
+        'full': factories['music.TrackFile']().track.artist,
+    }
+
+    request = api_request.get('/', {'listenable': param})
+    view = views.ArtistViewSet()
+    view.action_map = {'get': 'list'}
+    expected = [artists[expected]]
+    view.request = view.initialize_request(request)
+    queryset = view.filter_queryset(view.get_queryset())
+
+    assert list(queryset) == expected
+
+
+@pytest.mark.parametrize('param,expected', [
+    ('true', 'full'),
+    ('false', 'empty'),
+])
+def test_album_view_filter_listenable(
+        param, expected, factories, api_request):
+    artists = {
+        'empty': factories['music.Album'](),
+        'full': factories['music.TrackFile']().track.album,
+    }
+
+    request = api_request.get('/', {'listenable': param})
+    view = views.AlbumViewSet()
+    view.action_map = {'get': 'list'}
+    expected = [artists[expected]]
+    view.request = view.initialize_request(request)
+    queryset = view.filter_queryset(view.get_queryset())
+
+    assert list(queryset) == expected
diff --git a/changes/changelog.d/114.feature b/changes/changelog.d/114.feature
new file mode 100644
index 00000000..88e3bfc1
--- /dev/null
+++ b/changes/changelog.d/114.feature
@@ -0,0 +1 @@
+Can now filter artists and albums with no listenable tracks (#114)
diff --git a/front/src/components/library/Artists.vue b/front/src/components/library/Artists.vue
index 3cf12344..52ccbdd7 100644
--- a/front/src/components/library/Artists.vue
+++ b/front/src/components/library/Artists.vue
@@ -129,7 +129,8 @@ export default {
         page: this.page,
         page_size: this.paginateBy,
         name__icontains: this.query,
-        ordering: this.getOrderingAsString()
+        ordering: this.getOrderingAsString(),
+        listenable: 'true'
       }
       logger.default.debug('Fetching artists')
       axios.get(url, {params: params}).then((response) => {
-- 
GitLab