diff --git a/api/funkwhale_api/manage/filters.py b/api/funkwhale_api/manage/filters.py index de12ab1ab..af9ded746 100644 --- a/api/funkwhale_api/manage/filters.py +++ b/api/funkwhale_api/manage/filters.py @@ -13,6 +13,7 @@ from funkwhale_api.federation import utils as federation_utils from funkwhale_api.moderation import models as moderation_models from funkwhale_api.music import models as music_models from funkwhale_api.users import models as users_models +from funkwhale_api.tags import models as tags_models class ActorField(forms.CharField): @@ -61,6 +62,7 @@ class ManageArtistFilterSet(filters.FilterSet): "field": forms.IntegerField(), "distinct": True, }, + "tag": {"to": "tagged_items__tag__name", "distinct": True}, }, ) ) @@ -90,6 +92,7 @@ class ManageAlbumFilterSet(filters.FilterSet): "field": forms.IntegerField(), "distinct": True, }, + "tag": {"to": "tagged_items__tag__name", "distinct": True}, }, ) ) @@ -128,6 +131,7 @@ class ManageTrackFilterSet(filters.FilterSet): "field": forms.IntegerField(), "distinct": True, }, + "tag": {"to": "tagged_items__tag__name", "distinct": True}, }, ) ) @@ -340,3 +344,11 @@ class ManageInstancePolicyFilterSet(filters.FilterSet): "silence_notifications", "reject_media", ] + + +class ManageTagFilterSet(filters.FilterSet): + q = fields.SearchFilter(search_fields=["name"]) + + class Meta: + model = tags_models.Tag + fields = ["q"] diff --git a/api/funkwhale_api/manage/serializers.py b/api/funkwhale_api/manage/serializers.py index 67e0178e0..3ea390b87 100644 --- a/api/funkwhale_api/manage/serializers.py +++ b/api/funkwhale_api/manage/serializers.py @@ -10,6 +10,7 @@ from funkwhale_api.federation import tasks as federation_tasks from funkwhale_api.moderation import models as moderation_models from funkwhale_api.music import models as music_models from funkwhale_api.music import serializers as music_serializers +from funkwhale_api.tags import models as tags_models from funkwhale_api.users import models as users_models from . import filters @@ -376,6 +377,7 @@ class ManageArtistSerializer(ManageBaseArtistSerializer): albums = ManageNestedAlbumSerializer(many=True) tracks = ManageNestedTrackSerializer(many=True) attributed_to = ManageBaseActorSerializer() + tags = serializers.SerializerMethodField() class Meta: model = music_models.Artist @@ -383,8 +385,13 @@ class ManageArtistSerializer(ManageBaseArtistSerializer): "albums", "tracks", "attributed_to", + "tags", ] + def get_tags(self, obj): + tagged_items = getattr(obj, "_prefetched_tagged_items", []) + return [ti.tag.name for ti in tagged_items] + class ManageNestedArtistSerializer(ManageBaseArtistSerializer): pass @@ -394,6 +401,7 @@ class ManageAlbumSerializer(ManageBaseAlbumSerializer): tracks = ManageNestedTrackSerializer(many=True) attributed_to = ManageBaseActorSerializer() artist = ManageNestedArtistSerializer() + tags = serializers.SerializerMethodField() class Meta: model = music_models.Album @@ -401,8 +409,13 @@ class ManageAlbumSerializer(ManageBaseAlbumSerializer): "artist", "tracks", "attributed_to", + "tags", ] + def get_tags(self, obj): + tagged_items = getattr(obj, "_prefetched_tagged_items", []) + return [ti.tag.name for ti in tagged_items] + class ManageTrackAlbumSerializer(ManageBaseAlbumSerializer): artist = ManageNestedArtistSerializer() @@ -417,6 +430,7 @@ class ManageTrackSerializer(ManageNestedTrackSerializer): album = ManageTrackAlbumSerializer() attributed_to = ManageBaseActorSerializer() uploads_count = serializers.SerializerMethodField() + tags = serializers.SerializerMethodField() class Meta: model = music_models.Track @@ -425,11 +439,16 @@ class ManageTrackSerializer(ManageNestedTrackSerializer): "album", "attributed_to", "uploads_count", + "tags", ] def get_uploads_count(self, obj): return getattr(obj, "uploads_count", None) + def get_tags(self, obj): + tagged_items = getattr(obj, "_prefetched_tagged_items", []) + return [ti.tag.name for ti in tagged_items] + class ManageTrackActionSerializer(common_serializers.ActionSerializer): actions = [common_serializers.Action("delete", allow_all=False)] @@ -564,3 +583,40 @@ class ManageUploadSerializer(serializers.ModelSerializer): "track", "library", ) + + +class ManageTagSerializer(ManageBaseAlbumSerializer): + + tracks_count = serializers.SerializerMethodField() + albums_count = serializers.SerializerMethodField() + artists_count = serializers.SerializerMethodField() + + class Meta: + model = tags_models.Tag + fields = [ + "id", + "name", + "creation_date", + "tracks_count", + "albums_count", + "artists_count", + ] + + def get_tracks_count(self, obj): + return getattr(obj, "_tracks_count", None) + + def get_albums_count(self, obj): + return getattr(obj, "_albums_count", None) + + def get_artists_count(self, obj): + return getattr(obj, "_artists_count", None) + + +class ManageTagActionSerializer(common_serializers.ActionSerializer): + actions = [common_serializers.Action("delete", allow_all=False)] + filterset_class = filters.ManageTagFilterSet + pk_field = "name" + + @transaction.atomic + def handle_delete(self, objects): + return objects.delete() diff --git a/api/funkwhale_api/manage/urls.py b/api/funkwhale_api/manage/urls.py index 2af18f5b7..b830f0023 100644 --- a/api/funkwhale_api/manage/urls.py +++ b/api/funkwhale_api/manage/urls.py @@ -24,6 +24,7 @@ users_router.register(r"invitations", views.ManageInvitationViewSet, "invitation other_router = routers.OptionalSlashRouter() other_router.register(r"accounts", views.ManageActorViewSet, "accounts") +other_router.register(r"tags", views.ManageTagViewSet, "tags") urlpatterns = [ url( diff --git a/api/funkwhale_api/manage/views.py b/api/funkwhale_api/manage/views.py index c788dd96b..ab6c0c9f2 100644 --- a/api/funkwhale_api/manage/views.py +++ b/api/funkwhale_api/manage/views.py @@ -2,7 +2,7 @@ from rest_framework import mixins, response, viewsets from rest_framework import decorators as rest_decorators from django.db.models import Count, Prefetch, Q, Sum, OuterRef, Subquery -from django.db.models.functions import Coalesce +from django.db.models.functions import Coalesce, Length from django.shortcuts import get_object_or_404 from funkwhale_api.common import models as common_models @@ -12,8 +12,10 @@ from funkwhale_api.federation import models as federation_models from funkwhale_api.federation import tasks as federation_tasks from funkwhale_api.history import models as history_models from funkwhale_api.music import models as music_models +from funkwhale_api.music import views as music_views from funkwhale_api.moderation import models as moderation_models from funkwhale_api.playlists import models as playlists_models +from funkwhale_api.tags import models as tags_models from funkwhale_api.users import models as users_models @@ -70,6 +72,7 @@ class ManageArtistViewSet( tracks_count=Count("tracks") ), ), + music_views.TAG_PREFETCH, ) ) serializer_class = serializers.ManageArtistSerializer @@ -107,7 +110,7 @@ class ManageAlbumViewSet( music_models.Album.objects.all() .order_by("-id") .select_related("attributed_to", "artist") - .prefetch_related("tracks") + .prefetch_related("tracks", music_views.TAG_PREFETCH) ) serializer_class = serializers.ManageAlbumSerializer filterset_class = filters.ManageAlbumFilterSet @@ -151,6 +154,7 @@ class ManageTrackViewSet( .order_by("-id") .select_related("attributed_to", "artist", "album__artist") .annotate(uploads_count=Coalesce(Subquery(uploads_subquery), 0)) + .prefetch_related(music_views.TAG_PREFETCH) ) serializer_class = serializers.ManageTrackSerializer filterset_class = filters.ManageTrackFilterSet @@ -452,3 +456,53 @@ class ManageInstancePolicyViewSet( def perform_create(self, serializer): serializer.save(actor=self.request.user.actor) + + +class ManageTagViewSet( + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + mixins.DestroyModelMixin, + mixins.CreateModelMixin, + viewsets.GenericViewSet, +): + lookup_field = "name" + queryset = ( + tags_models.Tag.objects.all() + .order_by("-creation_date") + .annotate(items_count=Count("tagged_items")) + .annotate(length=Length("name")) + ) + serializer_class = serializers.ManageTagSerializer + filterset_class = filters.ManageTagFilterSet + required_scope = "instance:libraries" + ordering_fields = ["id", "creation_date", "name", "items_count", "length"] + + def get_queryset(self): + queryset = super().get_queryset() + from django.contrib.contenttypes.models import ContentType + + album_ct = ContentType.objects.get_for_model(music_models.Album) + track_ct = ContentType.objects.get_for_model(music_models.Track) + artist_ct = ContentType.objects.get_for_model(music_models.Artist) + queryset = queryset.annotate( + _albums_count=Count( + "tagged_items", filter=Q(tagged_items__content_type=album_ct) + ), + _tracks_count=Count( + "tagged_items", filter=Q(tagged_items__content_type=track_ct) + ), + _artists_count=Count( + "tagged_items", filter=Q(tagged_items__content_type=artist_ct) + ), + ) + return queryset + + @rest_decorators.action(methods=["post"], detail=False) + def action(self, request, *args, **kwargs): + queryset = self.get_queryset() + serializer = serializers.ManageTagActionSerializer( + request.data, queryset=queryset + ) + serializer.is_valid(raise_exception=True) + result = serializer.save() + return response.Response(result, status=200) diff --git a/api/tests/conftest.py b/api/tests/conftest.py index d76b1665e..bf9922377 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -1,5 +1,4 @@ import contextlib -import datetime import io import os import PIL @@ -22,7 +21,6 @@ from django.db.models import QuerySet from aioresponses import aioresponses from dynamic_preferences.registries import global_preferences_registry -from rest_framework import fields as rest_fields from rest_framework.test import APIClient, APIRequestFactory from funkwhale_api.activity import record @@ -30,6 +28,7 @@ from funkwhale_api.federation import actors from funkwhale_api.moderation import mrf from funkwhale_api.music import licenses +from . import utils as test_utils pytest_plugins = "aiohttp.pytest_plugin" @@ -297,18 +296,9 @@ def authenticated_actor(factories, mocker): yield actor -@pytest.fixture +@pytest.fixture(scope="session") def to_api_date(): - def inner(value): - if isinstance(value, datetime.datetime): - f = rest_fields.DateTimeField() - return f.to_representation(value) - if isinstance(value, datetime.date): - f = rest_fields.DateField() - return f.to_representation(value) - raise ValueError("Invalid value: {}".format(value)) - - return inner + return test_utils.to_api_date @pytest.fixture() diff --git a/api/tests/federation/test_api_serializers.py b/api/tests/federation/test_api_serializers.py index df55df7ae..d7c5836ca 100644 --- a/api/tests/federation/test_api_serializers.py +++ b/api/tests/federation/test_api_serializers.py @@ -4,7 +4,7 @@ from funkwhale_api.federation import api_serializers from funkwhale_api.federation import serializers -def test_library_serializer(factories): +def test_library_serializer(factories, to_api_date): library = factories["music.Library"](uploads_count=5678) expected = { "fid": library.fid, @@ -12,7 +12,7 @@ def test_library_serializer(factories): "actor": serializers.APIActorSerializer(library.actor).data, "name": library.name, "description": library.description, - "creation_date": library.creation_date.isoformat().split("+")[0] + "Z", + "creation_date": to_api_date(library.creation_date), "uploads_count": library.uploads_count, "privacy_level": library.privacy_level, "follow": None, @@ -34,7 +34,7 @@ def test_library_serializer_latest_scan(factories): assert serializer.data["latest_scan"] == expected -def test_library_serializer_with_follow(factories): +def test_library_serializer_with_follow(factories, to_api_date): library = factories["music.Library"](uploads_count=5678) follow = factories["federation.LibraryFollow"](target=library) @@ -45,7 +45,7 @@ def test_library_serializer_with_follow(factories): "actor": serializers.APIActorSerializer(library.actor).data, "name": library.name, "description": library.description, - "creation_date": library.creation_date.isoformat().split("+")[0] + "Z", + "creation_date": to_api_date(library.creation_date), "uploads_count": library.uploads_count, "privacy_level": library.privacy_level, "follow": api_serializers.NestedLibraryFollowSerializer(follow).data, diff --git a/api/tests/manage/test_serializers.py b/api/tests/manage/test_serializers.py index e621bf14c..16fd9ce58 100644 --- a/api/tests/manage/test_serializers.py +++ b/api/tests/manage/test_serializers.py @@ -39,13 +39,13 @@ def test_user_update_permission(factories): assert user.permission_settings is False -def test_manage_domain_serializer(factories, now): +def test_manage_domain_serializer(factories, now, to_api_date): domain = factories["federation.Domain"](nodeinfo_fetch_date=None) setattr(domain, "actors_count", 42) setattr(domain, "outbox_activities_count", 23) expected = { "name": domain.name, - "creation_date": domain.creation_date.isoformat().split("+")[0] + "Z", + "creation_date": to_api_date(domain.creation_date), "actors_count": 42, "outbox_activities_count": 23, "nodeinfo": {}, @@ -65,14 +65,14 @@ def test_manage_domain_serializer_validates_hostname(db): s.is_valid(raise_exception=True) -def test_manage_actor_serializer(factories, now): +def test_manage_actor_serializer(factories, now, to_api_date): actor = factories["federation.Actor"]() setattr(actor, "uploads_count", 66) expected = { "id": actor.id, "name": actor.name, - "creation_date": actor.creation_date.isoformat().split("+")[0] + "Z", - "last_fetch_date": actor.last_fetch_date.isoformat().split("+")[0] + "Z", + "creation_date": to_api_date(actor.creation_date), + "last_fetch_date": to_api_date(actor.last_fetch_date), "uploads_count": 66, "fid": actor.fid, "url": actor.url, @@ -110,13 +110,15 @@ def test_manage_actor_serializer(factories, now): ), ], ) -def test_instance_policy_serializer_repr(factories, factory_kwargs, expected): +def test_instance_policy_serializer_repr( + factories, factory_kwargs, expected, to_api_date +): policy = factories["moderation.InstancePolicy"](block_all=True, **factory_kwargs) e = { "id": policy.id, "uuid": str(policy.uuid), - "creation_date": policy.creation_date.isoformat().split("+")[0] + "Z", + "creation_date": to_api_date(policy.creation_date), "actor": policy.actor.full_username, "block_all": True, "silence_activity": False, @@ -280,7 +282,7 @@ def test_instance_policy_serializer_purges_target_actor( assert on_commit.call_count == 0 -def test_manage_artist_serializer(factories, now): +def test_manage_artist_serializer(factories, now, to_api_date): artist = factories["music.Artist"](attributed=True) track = factories["music.Track"](artist=artist) album = factories["music.Album"](artist=artist) @@ -291,19 +293,20 @@ def test_manage_artist_serializer(factories, now): "fid": artist.fid, "name": artist.name, "mbid": artist.mbid, - "creation_date": artist.creation_date.isoformat().split("+")[0] + "Z", + "creation_date": to_api_date(artist.creation_date), "albums": [serializers.ManageNestedAlbumSerializer(album).data], "tracks": [serializers.ManageNestedTrackSerializer(track).data], "attributed_to": serializers.ManageBaseActorSerializer( artist.attributed_to ).data, + "tags": [], } s = serializers.ManageArtistSerializer(artist) assert s.data == expected -def test_manage_nested_track_serializer(factories, now): +def test_manage_nested_track_serializer(factories, now, to_api_date): track = factories["music.Track"]() expected = { "id": track.id, @@ -312,7 +315,7 @@ def test_manage_nested_track_serializer(factories, now): "fid": track.fid, "title": track.title, "mbid": track.mbid, - "creation_date": track.creation_date.isoformat().split("+")[0] + "Z", + "creation_date": to_api_date(track.creation_date), "position": track.position, "disc_number": track.disc_number, "copyright": track.copyright, @@ -323,7 +326,7 @@ def test_manage_nested_track_serializer(factories, now): assert s.data == expected -def test_manage_nested_album_serializer(factories, now): +def test_manage_nested_album_serializer(factories, now, to_api_date): album = factories["music.Album"]() setattr(album, "tracks_count", 44) expected = { @@ -333,7 +336,7 @@ def test_manage_nested_album_serializer(factories, now): "fid": album.fid, "title": album.title, "mbid": album.mbid, - "creation_date": album.creation_date.isoformat().split("+")[0] + "Z", + "creation_date": to_api_date(album.creation_date), "release_date": album.release_date.isoformat(), "cover": { "original": album.cover.url, @@ -348,7 +351,7 @@ def test_manage_nested_album_serializer(factories, now): assert s.data == expected -def test_manage_nested_artist_serializer(factories, now): +def test_manage_nested_artist_serializer(factories, now, to_api_date): artist = factories["music.Artist"]() expected = { "id": artist.id, @@ -357,14 +360,14 @@ def test_manage_nested_artist_serializer(factories, now): "fid": artist.fid, "name": artist.name, "mbid": artist.mbid, - "creation_date": artist.creation_date.isoformat().split("+")[0] + "Z", + "creation_date": to_api_date(artist.creation_date), } s = serializers.ManageNestedArtistSerializer(artist) assert s.data == expected -def test_manage_album_serializer(factories, now): +def test_manage_album_serializer(factories, now, to_api_date): album = factories["music.Album"](attributed=True) track = factories["music.Track"](album=album) expected = { @@ -374,7 +377,7 @@ def test_manage_album_serializer(factories, now): "fid": album.fid, "title": album.title, "mbid": album.mbid, - "creation_date": album.creation_date.isoformat().split("+")[0] + "Z", + "creation_date": to_api_date(album.creation_date), "release_date": album.release_date.isoformat(), "cover": { "original": album.cover.url, @@ -387,13 +390,14 @@ def test_manage_album_serializer(factories, now): "attributed_to": serializers.ManageBaseActorSerializer( album.attributed_to ).data, + "tags": [], } s = serializers.ManageAlbumSerializer(album) assert s.data == expected -def test_manage_track_serializer(factories, now): +def test_manage_track_serializer(factories, now, to_api_date): track = factories["music.Track"](attributed=True) setattr(track, "uploads_count", 44) expected = { @@ -407,20 +411,21 @@ def test_manage_track_serializer(factories, now): "position": track.position, "copyright": track.copyright, "license": track.license, - "creation_date": track.creation_date.isoformat().split("+")[0] + "Z", + "creation_date": to_api_date(track.creation_date), "artist": serializers.ManageNestedArtistSerializer(track.artist).data, "album": serializers.ManageTrackAlbumSerializer(track.album).data, "attributed_to": serializers.ManageBaseActorSerializer( track.attributed_to ).data, "uploads_count": 44, + "tags": [], } s = serializers.ManageTrackSerializer(track) assert s.data == expected -def test_manage_library_serializer(factories, now): +def test_manage_library_serializer(factories, now, to_api_date): library = factories["music.Library"]() setattr(library, "followers_count", 42) setattr(library, "_uploads_count", 44) @@ -435,7 +440,7 @@ def test_manage_library_serializer(factories, now): "name": library.name, "description": library.description, "privacy_level": library.privacy_level, - "creation_date": library.creation_date.isoformat().split("+")[0] + "Z", + "creation_date": to_api_date(library.creation_date), "actor": serializers.ManageBaseActorSerializer(library.actor).data, "uploads_count": 44, "followers_count": 42, @@ -445,7 +450,7 @@ def test_manage_library_serializer(factories, now): assert s.data == expected -def test_manage_upload_serializer(factories, now): +def test_manage_upload_serializer(factories, now, to_api_date): upload = factories["music.Upload"]() expected = { @@ -463,8 +468,8 @@ def test_manage_upload_serializer(factories, now): "source": upload.source, "filename": upload.filename, "metadata": upload.metadata, - "creation_date": upload.creation_date.isoformat().split("+")[0] + "Z", - "modification_date": upload.modification_date.isoformat().split("+")[0] + "Z", + "creation_date": to_api_date(upload.creation_date), + "modification_date": to_api_date(upload.modification_date), "accessed_date": None, "import_date": None, "import_metadata": upload.import_metadata, @@ -487,6 +492,7 @@ def test_manage_upload_serializer(factories, now): ("music.Artist", serializers.ManageArtistActionSerializer), ("music.Library", serializers.ManageLibraryActionSerializer), ("music.Upload", serializers.ManageUploadActionSerializer), + ("tags.Tag", serializers.ManageTagActionSerializer), ], ) def test_action_serializer_delete(factory, serializer_class, factories): @@ -496,3 +502,22 @@ def test_action_serializer_delete(factory, serializer_class, factories): s.handle_delete(objects[0].__class__.objects.all()) assert objects[0].__class__.objects.count() == 0 + + +def test_manage_tag_serializer(factories, to_api_date): + tag = factories["tags.Tag"]() + + setattr(tag, "_tracks_count", 42) + setattr(tag, "_albums_count", 54) + setattr(tag, "_artists_count", 66) + expected = { + "id": tag.id, + "name": tag.name, + "creation_date": to_api_date(tag.creation_date), + "tracks_count": 42, + "albums_count": 54, + "artists_count": 66, + } + s = serializers.ManageTagSerializer(tag) + + assert s.data == expected diff --git a/api/tests/manage/test_views.py b/api/tests/manage/test_views.py index 72394052c..710722b90 100644 --- a/api/tests/manage/test_views.py +++ b/api/tests/manage/test_views.py @@ -377,3 +377,31 @@ def test_upload_delete(factories, superuser_api_client): response = superuser_api_client.delete(url) assert response.status_code == 204 + + +def test_tag_detail(factories, superuser_api_client): + tag = factories["tags.Tag"]() + url = reverse("api:v1:manage:tags-detail", kwargs={"name": tag.name}) + response = superuser_api_client.get(url) + + assert response.status_code == 200 + assert response.data["name"] == tag.name + + +def test_tag_list(factories, superuser_api_client, settings): + tag = factories["tags.Tag"]() + url = reverse("api:v1:manage:tags-list") + response = superuser_api_client.get(url) + + assert response.status_code == 200 + + assert response.data["count"] == 1 + assert response.data["results"][0]["name"] == tag.name + + +def test_tag_delete(factories, superuser_api_client): + tag = factories["tags.Tag"]() + url = reverse("api:v1:manage:tags-detail", kwargs={"name": tag.name}) + response = superuser_api_client.delete(url) + + assert response.status_code == 204 diff --git a/api/tests/music/test_serializers.py b/api/tests/music/test_serializers.py index 8582a761c..fd0c312e5 100644 --- a/api/tests/music/test_serializers.py +++ b/api/tests/music/test_serializers.py @@ -118,7 +118,7 @@ def test_upload_serializer(factories, to_api_date): "bitrate": upload.bitrate, "size": upload.size, "library": serializers.LibraryForOwnerSerializer(upload.library).data, - "creation_date": upload.creation_date.isoformat().split("+")[0] + "Z", + "creation_date": to_api_date(upload.creation_date), "import_date": None, "import_status": "pending", } @@ -145,7 +145,7 @@ def test_upload_owner_serializer(factories, to_api_date): "bitrate": upload.bitrate, "size": upload.size, "library": serializers.LibraryForOwnerSerializer(upload.library).data, - "creation_date": upload.creation_date.isoformat().split("+")[0] + "Z", + "creation_date": to_api_date(upload.creation_date), "metadata": {"test": "metadata"}, "import_metadata": {"import": "metadata"}, "import_date": None, diff --git a/api/tests/radios/test_serializers.py b/api/tests/radios/test_serializers.py index 54019d5e1..748bd993a 100644 --- a/api/tests/radios/test_serializers.py +++ b/api/tests/radios/test_serializers.py @@ -29,7 +29,7 @@ def test_create_artist_radio(factories): assert session.related_object == artist -def test_tag_radio_repr(factories): +def test_tag_radio_repr(factories, to_api_date): tag = factories["tags.Tag"]() session = factories["radios.RadioSession"](related_object=tag, radio_type="tag") @@ -39,6 +39,6 @@ def test_tag_radio_repr(factories): "custom_radio": None, "user": session.user.pk, "related_object_id": tag.name, - "creation_date": session.creation_date.isoformat().split("+")[0] + "Z", + "creation_date": to_api_date(session.creation_date), } assert serializers.RadioSessionSerializer(session).data == expected diff --git a/api/tests/tags/test_serializers.py b/api/tests/tags/test_serializers.py index f962e93e4..dc3a64ae0 100644 --- a/api/tests/tags/test_serializers.py +++ b/api/tests/tags/test_serializers.py @@ -3,15 +3,12 @@ import pytest from funkwhale_api.tags import serializers -def test_tag_serializer(factories): +def test_tag_serializer(factories, to_api_date): tag = factories["tags.Tag"]() serializer = serializers.TagSerializer(tag) - expected = { - "name": tag.name, - "creation_date": tag.creation_date.isoformat().split("+")[0] + "Z", - } + expected = {"name": tag.name, "creation_date": to_api_date(tag.creation_date)} assert serializer.data == expected diff --git a/api/tests/test_test_utils.py b/api/tests/test_test_utils.py new file mode 100644 index 000000000..a17b21856 --- /dev/null +++ b/api/tests/test_test_utils.py @@ -0,0 +1,6 @@ +from . import utils as test_utils + + +def test_to_api_date(now): + + assert test_utils.to_api_date(now) == now.isoformat().split("+")[0] + "Z" diff --git a/api/tests/utils.py b/api/tests/utils.py new file mode 100644 index 000000000..868972aa4 --- /dev/null +++ b/api/tests/utils.py @@ -0,0 +1,12 @@ +import datetime +from rest_framework import fields as rest_fields + + +def to_api_date(value): + if isinstance(value, datetime.datetime): + f = rest_fields.DateTimeField() + return f.to_representation(value) + if isinstance(value, datetime.date): + f = rest_fields.DateField() + return f.to_representation(value) + raise ValueError("Invalid value: {}".format(value)) diff --git a/front/src/components/library/TagDetail.vue b/front/src/components/library/TagDetail.vue index 904bce7ae..e928caa29 100644 --- a/front/src/components/library/TagDetail.vue +++ b/front/src/components/library/TagDetail.vue @@ -7,6 +7,11 @@ + + + Open in moderation interface + +
diff --git a/front/src/components/manage/library/TagsTable.vue b/front/src/components/manage/library/TagsTable.vue new file mode 100644 index 000000000..d75670454 --- /dev/null +++ b/front/src/components/manage/library/TagsTable.vue @@ -0,0 +1,209 @@ + + + diff --git a/front/src/components/mixins/Translations.vue b/front/src/components/mixins/Translations.vue index 56ea3ed15..e6e809252 100644 --- a/front/src/components/mixins/Translations.vue +++ b/front/src/components/mixins/Translations.vue @@ -52,6 +52,8 @@ export default { album_title: this.$pgettext('Content/*/Dropdown/Noun', 'Album name'), artist_name: this.$pgettext('Content/*/Dropdown/Noun', 'Artist name'), name: this.$pgettext('*/*/*/Noun', 'Name'), + length: this.$pgettext('*/*/*/Noun', 'Length'), + items_count: this.$pgettext('*/*/*/Noun', 'Items'), size: this.$pgettext('Content/Library/*/in MB', 'Size'), bitrate: this.$pgettext('Content/Track/*/Noun', 'Bitrate'), duration: this.$pgettext('Content/*/*', 'Duration'), diff --git a/front/src/components/tags/List.vue b/front/src/components/tags/List.vue index 6c280822b..e5bba5b58 100644 --- a/front/src/components/tags/List.vue +++ b/front/src/components/tags/List.vue @@ -1,7 +1,7 @@