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