From 7a34c297ed2cc0eaa444e5549b2c4a7c0c954f08 Mon Sep 17 00:00:00 2001
From: jake <haekbreen@gmail.com>
Date: Wed, 13 Feb 2019 08:46:38 +0100
Subject: [PATCH] Resolve "add a view to list albums"

---
 api/funkwhale_api/music/filters.py       |   2 +-
 api/tests/music/test_views.py            |  21 +++
 changes/changelog.d/356.bugfix           |   1 +
 changes/changelog.d/356.feature          |   1 +
 front/src/components/library/Albums.vue  | 186 +++++++++++++++++++++++
 front/src/components/library/Library.vue |   3 +
 front/src/router/index.js                |  12 ++
 7 files changed, 225 insertions(+), 1 deletion(-)
 create mode 100644 changes/changelog.d/356.bugfix
 create mode 100644 changes/changelog.d/356.feature
 create mode 100644 front/src/components/library/Albums.vue

diff --git a/api/funkwhale_api/music/filters.py b/api/funkwhale_api/music/filters.py
index 76bc93b67..009f0088d 100644
--- a/api/funkwhale_api/music/filters.py
+++ b/api/funkwhale_api/music/filters.py
@@ -87,7 +87,7 @@ class UploadFilter(filters.FilterSet):
 
 class AlbumFilter(filters.FilterSet):
     playable = filters.BooleanFilter(field_name="_", method="filter_playable")
-    q = fields.SearchFilter(search_fields=["title", "artist__name" "source"])
+    q = fields.SearchFilter(search_fields=["title", "artist__name"])
 
     class Meta:
         model = models.Album
diff --git a/api/tests/music/test_views.py b/api/tests/music/test_views.py
index 85ba2955a..741fe9b29 100644
--- a/api/tests/music/test_views.py
+++ b/api/tests/music/test_views.py
@@ -108,6 +108,27 @@ def test_album_view_filter_playable(param, expected, factories, api_request):
     assert list(queryset) == expected
 
 
+@pytest.mark.parametrize(
+    "param", [("I've Got"), ("Français"), ("I've Got Everything : Spoken Word Poetry")]
+)
+def test_album_view_filter_query(param, factories, api_request):
+    # Test both partial and full search.
+    factories["music.Album"](title="I've Got Nothing : Original Soundtrack")
+    factories["music.Album"](title="I've Got Cake : Remix")
+    factories["music.Album"](title="Français Et Tu")
+    factories["music.Album"](title="I've Got Everything : Spoken Word Poetry")
+
+    request = api_request.get("/", {"q": param})
+    view = views.AlbumViewSet()
+    view.action_map = {"get": "list"}
+    view.request = view.initialize_request(request)
+    queryset = view.filter_queryset(view.get_queryset())
+
+    # Loop through our "expected list", and assert some string finds against our param.
+    for val in list(queryset):
+        assert val.title.find(param) != -1
+
+
 def test_can_serve_upload_as_remote_library(
     factories, authenticated_actor, logged_in_api_client, settings, preferences
 ):
diff --git a/changes/changelog.d/356.bugfix b/changes/changelog.d/356.bugfix
new file mode 100644
index 000000000..c99d36870
--- /dev/null
+++ b/changes/changelog.d/356.bugfix
@@ -0,0 +1 @@
+Fixed issue with querying the albums api endpoint (#356)
\ No newline at end of file
diff --git a/changes/changelog.d/356.feature b/changes/changelog.d/356.feature
new file mode 100644
index 000000000..cf0744c18
--- /dev/null
+++ b/changes/changelog.d/356.feature
@@ -0,0 +1 @@
+Added albums view. Similar to artists view, it's viewable by clicking on the "Albums" link on the top bar. (#356)
\ No newline at end of file
diff --git a/front/src/components/library/Albums.vue b/front/src/components/library/Albums.vue
new file mode 100644
index 000000000..4884cf0ee
--- /dev/null
+++ b/front/src/components/library/Albums.vue
@@ -0,0 +1,186 @@
+<template>
+  <main v-title="labels.title">
+    <section class="ui vertical stripe segment">
+      <h2 class="ui header">
+        <translate>Browsing albums</translate>
+      </h2>
+      <div :class="['ui', {'loading': isLoading}, 'form']">
+        <div class="fields">
+          <div class="field">
+            <label>
+              <translate>Search</translate>
+            </label>
+            <input type="text" name="search" v-model="query" :placeholder="labels.searchPlaceholder"/>
+          </div>
+          <div class="field">
+            <label><translate>Ordering</translate></label>
+            <select class="ui dropdown" v-model="ordering">
+              <option v-for="option in orderingOptions" :value="option[0]">
+                {{ sharedLabels.filters[option[1]] }}
+              </option>
+            </select>
+          </div>
+          <div class="field">
+            <label><translate>Ordering direction</translate></label>
+            <select class="ui dropdown" v-model="orderingDirection">
+              <option value="+"><translate>Ascending</translate></option>
+              <option value="-"><translate>Descending</translate></option>
+            </select>
+          </div>
+          <div class="field">
+            <label><translate>Results per page</translate></label>
+            <select class="ui dropdown" v-model="paginateBy">
+              <option :value="parseInt(12)">12</option>
+              <option :value="parseInt(25)">25</option>
+              <option :value="parseInt(50)">50</option>
+            </select>
+          </div>
+        </div>
+      </div>
+      <div class="ui hidden divider"></div>
+      <div
+        v-if="result"
+        transition-duration="0"
+        item-selector=".column"
+        percent-position="true"
+        stagger="0"
+        class="ui stackable three column doubling grid">
+        <div
+          v-if="result.results.length > 0"
+          class="ui cards">
+          <album-card
+            :mode="'simple'"
+            v-masonry-tile
+            v-for="album in result.results"
+            :key="album.id"
+            :album="album"></album-card>
+        </div>
+      </div>
+      <div class="ui center aligned basic segment">
+        <pagination
+          v-if="result && result.count > paginateBy"
+          @page-changed="selectPage"
+          :current="page"
+          :paginate-by="paginateBy"
+          :total="result.count"
+          ></pagination>
+      </div>
+    </section>
+  </main>
+</template>
+
+<script>
+import axios from "axios"
+import _ from "@/lodash"
+import $ from "jquery"
+
+import logger from "@/logging"
+
+import OrderingMixin from "@/components/mixins/Ordering"
+import PaginationMixin from "@/components/mixins/Pagination"
+import TranslationsMixin from "@/components/mixins/Translations"
+import AlbumCard from "@/components/audio/album/Card"
+import Pagination from "@/components/Pagination"
+
+const FETCH_URL = "albums/"
+
+export default {
+  mixins: [OrderingMixin, PaginationMixin, TranslationsMixin],
+  props: {
+    defaultQuery: { type: String, required: false, default: "" }
+  },
+  components: {
+    AlbumCard,
+    Pagination
+  },
+  data() {
+    let defaultOrdering = this.getOrderingFromString(
+      this.defaultOrdering || "-creation_date"
+    )
+    return {
+      isLoading: true,
+      result: null,
+      page: parseInt(this.defaultPage),
+      query: this.defaultQuery,
+      paginateBy: parseInt(this.defaultPaginateBy || 25),
+      orderingDirection: defaultOrdering.direction || "+",
+      ordering: defaultOrdering.field,
+      orderingOptions: [["creation_date", "creation_date"], ["title", "title"]]
+    }
+  },
+  created() {
+    this.fetchData()
+  },
+  mounted() {
+    $(".ui.dropdown").dropdown()
+  },
+  computed: {
+    labels() {
+      let searchPlaceholder = this.$gettext("Enter album title...")
+      let title = this.$gettext("Albums")
+      return {
+        searchPlaceholder,
+        title
+      }
+    }
+  },
+  methods: {
+    updateQueryString: _.debounce(function() {
+      this.$router.replace({
+        query: {
+          query: this.query,
+          page: this.page,
+          paginateBy: this.paginateBy,
+          ordering: this.getOrderingAsString()
+        }
+      })
+    }, 500),
+    fetchData: _.debounce(function() {
+      var self = this
+      this.isLoading = true
+      let url = FETCH_URL
+      let params = {
+        page: this.page,
+        page_size: this.paginateBy,
+        q: this.query,
+        ordering: this.getOrderingAsString(),
+        playable: "true"
+      }
+      logger.default.debug("Fetching albums")
+      axios.get(url, { params: params }).then(response => {
+        self.result = response.data
+        self.isLoading = false
+      })
+    }, 500),
+    selectPage: function(page) {
+      this.page = page
+    }
+  },
+  watch: {
+    page() {
+      this.updateQueryString()
+      this.fetchData()
+    },
+    paginateBy() {
+      this.updateQueryString()
+      this.fetchData()
+    },
+    ordering() {
+      this.updateQueryString()
+      this.fetchData()
+    },
+    orderingDirection() {
+      this.updateQueryString()
+      this.fetchData()
+    },
+    query() {
+      this.updateQueryString()
+      this.fetchData()
+    }
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+</style>
diff --git a/front/src/components/library/Library.vue b/front/src/components/library/Library.vue
index c0371fe58..0276c34d0 100644
--- a/front/src/components/library/Library.vue
+++ b/front/src/components/library/Library.vue
@@ -4,6 +4,9 @@
       <router-link class="ui item" to="/library" exact>
         <translate>Browse</translate>
       </router-link>
+      <router-link class="ui item" to="/library/albums" exact>
+        <translate>Albums</translate>
+      </router-link>
       <router-link class="ui item" to="/library/artists" exact>
         <translate>Artists</translate>
       </router-link>
diff --git a/front/src/router/index.js b/front/src/router/index.js
index 1b60a6813..e6e5b2870 100644
--- a/front/src/router/index.js
+++ b/front/src/router/index.js
@@ -15,6 +15,7 @@ import Library from '@/components/library/Library'
 import LibraryHome from '@/components/library/Home'
 import LibraryArtist from '@/components/library/Artist'
 import LibraryArtists from '@/components/library/Artists'
+import LibraryAlbums from '@/components/library/Albums'
 import LibraryAlbum from '@/components/library/Album'
 import LibraryTrack from '@/components/library/Track'
 import LibraryRadios from '@/components/library/Radios'
@@ -277,6 +278,17 @@ export default new Router({
             defaultPage: route.query.page
           })
         },
+        {
+          path: 'albums/',
+          name: 'library.albums.browse',
+          component: LibraryAlbums,
+          props: (route) => ({
+            defaultOrdering: route.query.ordering,
+            defaultQuery: route.query.query,
+            defaultPaginateBy: route.query.paginateBy,
+            defaultPage: route.query.page
+          })
+        },
         {
           path: 'radios/',
           name: 'library.radios.browse',
-- 
GitLab