From d875f0d07064fdcf118f0fc3a888ea0567ed76f3 Mon Sep 17 00:00:00 2001 From: Eliot Berriot <contact@eliotberriot.com> Date: Tue, 27 Feb 2018 22:38:55 +0100 Subject: [PATCH] Fixed #82: Basic instance states are now available on /about --- api/funkwhale_api/instance/stats.py | 51 ++++++++++++ api/funkwhale_api/instance/urls.py | 4 + api/funkwhale_api/instance/views.py | 11 +++ api/tests/instance/test_stats.py | 84 +++++++++++++++++++ changes/changelog.d/82.feature | 0 front/src/components/About.vue | 5 ++ front/src/components/instance/Stats.vue | 104 ++++++++++++++++++++++++ 7 files changed, 259 insertions(+) create mode 100644 api/funkwhale_api/instance/stats.py create mode 100644 api/tests/instance/test_stats.py create mode 100644 changes/changelog.d/82.feature create mode 100644 front/src/components/instance/Stats.vue diff --git a/api/funkwhale_api/instance/stats.py b/api/funkwhale_api/instance/stats.py new file mode 100644 index 00000000..167b333d --- /dev/null +++ b/api/funkwhale_api/instance/stats.py @@ -0,0 +1,51 @@ +from django.db.models import Sum + +from funkwhale_api.favorites.models import TrackFavorite +from funkwhale_api.history.models import Listening +from funkwhale_api.music import models +from funkwhale_api.users.models import User + + +def get(): + return { + 'users': get_users(), + 'tracks': get_tracks(), + 'albums': get_albums(), + 'artists': get_artists(), + 'track_favorites': get_track_favorites(), + 'listenings': get_listenings(), + 'music_duration': get_music_duration(), + } + + +def get_users(): + return User.objects.count() + + +def get_listenings(): + return Listening.objects.count() + + +def get_track_favorites(): + return TrackFavorite.objects.count() + + +def get_tracks(): + return models.Track.objects.count() + + +def get_albums(): + return models.Album.objects.count() + + +def get_artists(): + return models.Artist.objects.count() + + +def get_music_duration(): + seconds = models.TrackFile.objects.aggregate( + d=Sum('duration'), + )['d'] + if seconds: + return seconds / 3600 + return 0 diff --git a/api/funkwhale_api/instance/urls.py b/api/funkwhale_api/instance/urls.py index 2f2b46b8..af23e7e0 100644 --- a/api/funkwhale_api/instance/urls.py +++ b/api/funkwhale_api/instance/urls.py @@ -1,7 +1,11 @@ from django.conf.urls import url +from django.views.decorators.cache import cache_page + from . import views urlpatterns = [ url(r'^settings/$', views.InstanceSettings.as_view(), name='settings'), + url(r'^stats/$', + cache_page(60 * 5)(views.InstanceStats.as_view()), name='stats'), ] diff --git a/api/funkwhale_api/instance/views.py b/api/funkwhale_api/instance/views.py index 44ee2287..7f8f393c 100644 --- a/api/funkwhale_api/instance/views.py +++ b/api/funkwhale_api/instance/views.py @@ -4,6 +4,8 @@ from rest_framework.response import Response from dynamic_preferences.api import serializers from dynamic_preferences.registries import global_preferences_registry +from . import stats + class InstanceSettings(views.APIView): permission_classes = [] @@ -23,3 +25,12 @@ class InstanceSettings(views.APIView): data = serializers.GlobalPreferenceSerializer( api_preferences, many=True).data return Response(data, status=200) + + +class InstanceStats(views.APIView): + permission_classes = [] + authentication_classes = [] + + def get(self, request, *args, **kwargs): + data = stats.get() + return Response(data, status=200) diff --git a/api/tests/instance/test_stats.py b/api/tests/instance/test_stats.py new file mode 100644 index 00000000..6eaad76f --- /dev/null +++ b/api/tests/instance/test_stats.py @@ -0,0 +1,84 @@ +from django.urls import reverse + +from funkwhale_api.instance import stats + + +def test_can_get_stats_via_api(db, api_client, mocker): + stats = { + 'foo': 'bar' + } + mocker.patch('funkwhale_api.instance.stats.get', return_value=stats) + url = reverse('api:v1:instance:stats') + response = api_client.get(url) + assert response.data == stats + + +def test_get_users(mocker): + mocker.patch( + 'funkwhale_api.users.models.User.objects.count', return_value=42) + + assert stats.get_users() == 42 + + +def test_get_music_duration(factories): + factories['music.TrackFile'].create_batch(size=5, duration=360) + + # duration is in hours + assert stats.get_music_duration() == 0.5 + + +def test_get_listenings(mocker): + mocker.patch( + 'funkwhale_api.history.models.Listening.objects.count', + return_value=42) + assert stats.get_listenings() == 42 + + +def test_get_track_favorites(mocker): + mocker.patch( + 'funkwhale_api.favorites.models.TrackFavorite.objects.count', + return_value=42) + assert stats.get_track_favorites() == 42 + + +def test_get_tracks(mocker): + mocker.patch( + 'funkwhale_api.music.models.Track.objects.count', + return_value=42) + assert stats.get_tracks() == 42 + + +def test_get_albums(mocker): + mocker.patch( + 'funkwhale_api.music.models.Album.objects.count', + return_value=42) + assert stats.get_albums() == 42 + + +def test_get_artists(mocker): + mocker.patch( + 'funkwhale_api.music.models.Artist.objects.count', + return_value=42) + assert stats.get_artists() == 42 + + +def test_get(mocker): + keys = [ + 'users', + 'tracks', + 'albums', + 'artists', + 'track_favorites', + 'listenings', + 'music_duration', + ] + mocks = [ + mocker.patch.object(stats, 'get_{}'.format(k), return_value=i) + for i, k in enumerate(keys) + ] + + expected = { + k: i for i, k in enumerate(keys) + } + + assert stats.get() == expected diff --git a/changes/changelog.d/82.feature b/changes/changelog.d/82.feature new file mode 100644 index 00000000..e69de29b diff --git a/front/src/components/About.vue b/front/src/components/About.vue index 01ce6a29..92bafd7a 100644 --- a/front/src/components/About.vue +++ b/front/src/components/About.vue @@ -6,6 +6,7 @@ <template v-if="instance.name.value">About {{ instance.name.value }}</template> <template v-else="instance.name.value">About this instance</template> </h1> + <stats></stats> </div> </div> <div class="ui vertical stripe segment"> @@ -27,8 +28,12 @@ <script> import {mapState} from 'vuex' +import Stats from '@/components/instance/Stats' export default { + components: { + Stats + }, created () { this.$store.dispatch('instance/fetchSettings') }, diff --git a/front/src/components/instance/Stats.vue b/front/src/components/instance/Stats.vue new file mode 100644 index 00000000..884809f3 --- /dev/null +++ b/front/src/components/instance/Stats.vue @@ -0,0 +1,104 @@ +<template> + <div> + <div v-if="stats" class="ui stackable two column grid"> + <div class="column"> + <h3 class="ui left aligned header">User activity</h3> + <div class="ui mini horizontal statistics"> + <div class="statistic"> + <div class="value"> + <i class="green user icon"></i> + {{ stats.users }} + </div> + <div class="label"> + Users + </div> + </div> + <div class="statistic"> + <div class="value"> + <i class="orange sound icon"></i> {{ stats.listenings }} + </div> + <div class="label"> + tracks listened + </div> + </div> + <div class="statistic"> + <div class="value"> + <i class="pink heart icon"></i> {{ stats.track_favorites }} + </div> + <div class="label"> + Tracks favorited + </div> + </div> + </div> + </div> + <div class="column"> + <h3 class="ui left aligned header">Library</h3> + <div class="ui mini horizontal statistics"> + <div class="statistic"> + <div class="value"> + {{ parseInt(stats.music_duration) }} + </div> + <div class="label"> + hours of music + </div> + </div> + <div class="statistic"> + <div class="value"> + {{ stats.artists }} + </div> + <div class="label"> + Artists + </div> + </div> + <div class="statistic"> + <div class="value"> + {{ stats.albums }} + </div> + <div class="label"> + Albums + </div> + </div> + <div class="statistic"> + <div class="value"> + {{ stats.tracks }} + </div> + <div class="label"> + tracks + </div> + </div> + </div> + </div> + </div> + </div> +</template> + +<script> +import axios from 'axios' +import logger from '@/logging' + +export default { + data () { + return { + stats: null + } + }, + created () { + this.fetchData() + }, + methods: { + fetchData () { + var self = this + this.isLoading = true + logger.default.debug('Fetching instance stats...') + axios.get('instance/stats/').then((response) => { + self.stats = response.data + self.isLoading = false + }) + } + } +} +</script> + +<!-- Add "scoped" attribute to limit CSS to this component only --> +<style scoped> +</style> -- GitLab