From 2bbb2f32399829eb872c97bbb42b6d0c08428848 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Mon, 15 Jul 2019 12:12:22 +0200 Subject: [PATCH] See #432: can now suggest tags on tracks/albums/artists (API) --- api/funkwhale_api/common/mutations.py | 9 +++++++-- api/funkwhale_api/music/mutations.py | 29 +++++++++++++++++++++------ api/tests/common/test_mutations.py | 2 +- api/tests/music/test_mutations.py | 23 +++++++++++++++++++++ 4 files changed, 54 insertions(+), 9 deletions(-) diff --git a/api/funkwhale_api/common/mutations.py b/api/funkwhale_api/common/mutations.py index dfc8ba85e..13a5a97de 100644 --- a/api/funkwhale_api/common/mutations.py +++ b/api/funkwhale_api/common/mutations.py @@ -86,6 +86,7 @@ class MutationSerializer(serializers.Serializer): class UpdateMutationSerializer(serializers.ModelSerializer, MutationSerializer): serialized_relations = {} + previous_state_handlers = {} def __init__(self, *args, **kwargs): # we force partial mode, because update mutations are partial @@ -139,16 +140,20 @@ class UpdateMutationSerializer(serializers.ModelSerializer, MutationSerializer): return get_update_previous_state( obj, *list(validated_data.keys()), - serialized_relations=self.serialized_relations + serialized_relations=self.serialized_relations, + handlers=self.previous_state_handlers, ) -def get_update_previous_state(obj, *fields, serialized_relations={}): +def get_update_previous_state(obj, *fields, serialized_relations={}, handlers={}): if not fields: raise ValueError("You need to provide at least one field") state = {} for field in fields: + if field in handlers: + state[field] = handlers[field](obj) + continue value = getattr(obj, field) if isinstance(value, models.Model): # we store the related object id and repr for better UX diff --git a/api/funkwhale_api/music/mutations.py b/api/funkwhale_api/music/mutations.py index 9fd91fb50..ebbe00f8d 100644 --- a/api/funkwhale_api/music/mutations.py +++ b/api/funkwhale_api/music/mutations.py @@ -1,5 +1,7 @@ from funkwhale_api.common import mutations from funkwhale_api.federation import routes +from funkwhale_api.tags import models as tags_models +from funkwhale_api.tags import serializers as tags_serializers from . import models @@ -12,17 +14,32 @@ def can_approve(obj, actor): return obj.is_local and actor.user and actor.user.get_permissions()["library"] +class TagMutation(mutations.UpdateMutationSerializer): + tags = tags_serializers.TagsListField() + previous_state_handlers = { + "tags": lambda obj: list( + sorted(obj.tagged_items.values_list("tag__name", flat=True)) + ) + } + + def update(self, instance, validated_data): + tags = validated_data.pop("tags", []) + r = super().update(instance, validated_data) + tags_models.set_tags(instance, *tags) + return r + + @mutations.registry.connect( "update", models.Track, perm_checkers={"suggest": can_suggest, "approve": can_approve}, ) -class TrackMutationSerializer(mutations.UpdateMutationSerializer): +class TrackMutationSerializer(TagMutation): serialized_relations = {"license": "code"} class Meta: model = models.Track - fields = ["license", "title", "position", "copyright"] + fields = ["license", "title", "position", "copyright", "tags"] def post_apply(self, obj, validated_data): routes.outbox.dispatch( @@ -35,10 +52,10 @@ class TrackMutationSerializer(mutations.UpdateMutationSerializer): models.Artist, perm_checkers={"suggest": can_suggest, "approve": can_approve}, ) -class ArtistMutationSerializer(mutations.UpdateMutationSerializer): +class ArtistMutationSerializer(TagMutation): class Meta: model = models.Artist - fields = ["name"] + fields = ["name", "tags"] def post_apply(self, obj, validated_data): routes.outbox.dispatch( @@ -51,10 +68,10 @@ class ArtistMutationSerializer(mutations.UpdateMutationSerializer): models.Album, perm_checkers={"suggest": can_suggest, "approve": can_approve}, ) -class AlbumMutationSerializer(mutations.UpdateMutationSerializer): +class AlbumMutationSerializer(TagMutation): class Meta: model = models.Album - fields = ["title", "release_date"] + fields = ["title", "release_date", "tags"] def post_apply(self, obj, validated_data): routes.outbox.dispatch( diff --git a/api/tests/common/test_mutations.py b/api/tests/common/test_mutations.py index 3c0d869a1..877128fac 100644 --- a/api/tests/common/test_mutations.py +++ b/api/tests/common/test_mutations.py @@ -51,7 +51,7 @@ def test_apply_update_mutation(factories, mutations_registry, mocker): ) assert previous_state == get_update_previous_state.return_value get_update_previous_state.assert_called_once_with( - user, "username", serialized_relations={} + user, "username", serialized_relations={}, handlers={} ) user.refresh_from_db() diff --git a/api/tests/music/test_mutations.py b/api/tests/music/test_mutations.py index be3fb0d76..260916bfa 100644 --- a/api/tests/music/test_mutations.py +++ b/api/tests/music/test_mutations.py @@ -2,6 +2,7 @@ import datetime import pytest from funkwhale_api.music import licenses +from funkwhale_api.tags import models as tags_models @pytest.mark.parametrize( @@ -117,3 +118,25 @@ def test_track_mutation_apply_outbox(factories, mocker): dispatch.assert_called_once_with( {"type": "Update", "object": {"type": "Track"}}, context={"track": track} ) + + +@pytest.mark.parametrize("factory_name", ["music.Artist", "music.Album", "music.Track"]) +def test_mutation_set_tags(factory_name, factories, now, mocker): + tags = ["tag1", "tag2"] + dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch") + set_tags = mocker.spy(tags_models, "set_tags") + obj = factories[factory_name]() + assert obj.tagged_items.all().count() == 0 + mutation = factories["common.Mutation"]( + type="update", target=obj, payload={"tags": tags} + ) + mutation.apply() + obj.refresh_from_db() + + assert sorted(obj.tagged_items.all().values_list("tag__name", flat=True)) == tags + set_tags.assert_called_once_with(obj, *tags) + obj_type = factory_name.lstrip("music.") + dispatch.assert_called_once_with( + {"type": "Update", "object": {"type": obj_type}}, + context={obj_type.lower(): obj}, + )