See #170: add a description field on tracks, albums, tracks

This commit is contained in:
Eliot Berriot 2020-01-14 14:00:08 +01:00
parent 424b9f133a
commit 2bc71eecfd
38 changed files with 653 additions and 59 deletions

View File

@ -26,3 +26,12 @@ class AttachmentFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
class Meta: class Meta:
model = "common.Attachment" model = "common.Attachment"
@registry.register
class CommonFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
text = factory.Faker("paragraph")
content_type = "text/plain"
class Meta:
model = "common.Content"

View File

@ -0,0 +1,21 @@
# Generated by Django 2.2.7 on 2020-01-13 10:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0005_auto_20191125_1421'),
]
operations = [
migrations.CreateModel(
name='Content',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('text', models.CharField(blank=True, max_length=5000, null=True)),
('content_type', models.CharField(max_length=100)),
],
),
]

View File

@ -24,6 +24,14 @@ from . import utils
from . import validators from . import validators
CONTENT_TEXT_MAX_LENGTH = 5000
CONTENT_TEXT_SUPPORTED_TYPES = [
"text/html",
"text/markdown",
"text/plain",
]
@Field.register_lookup @Field.register_lookup
class NotEqual(Lookup): class NotEqual(Lookup):
lookup_name = "ne" lookup_name = "ne"
@ -273,6 +281,15 @@ class MutationAttachment(models.Model):
unique_together = ("attachment", "mutation") unique_together = ("attachment", "mutation")
class Content(models.Model):
"""
A text content that can be associated to other models, like a description, a summary, etc.
"""
text = models.CharField(max_length=CONTENT_TEXT_MAX_LENGTH, blank=True, null=True)
content_type = models.CharField(max_length=100)
@receiver(models.signals.post_save, sender=Attachment) @receiver(models.signals.post_save, sender=Attachment)
def warm_attachment_thumbnails(sender, instance, **kwargs): def warm_attachment_thumbnails(sender, instance, **kwargs):
if not instance.file or not settings.CREATE_IMAGE_THUMBNAILS: if not instance.file or not settings.CREATE_IMAGE_THUMBNAILS:
@ -302,3 +319,18 @@ def trigger_mutation_post_init(sender, instance, created, **kwargs):
except AttributeError: except AttributeError:
return return
handler(instance) handler(instance)
CONTENT_FKS = {
"music.Track": ["description"],
"music.Album": ["description"],
"music.Artist": ["description"],
}
@receiver(models.signals.post_delete, sender=None)
def remove_attached_content(sender, instance, **kwargs):
fk_fields = CONTENT_FKS.get(instance._meta.label, [])
for field in fk_fields:
if getattr(instance, "{}_id".format(field)):
getattr(instance, field).delete()

View File

@ -86,7 +86,6 @@ class MutationSerializer(serializers.Serializer):
class UpdateMutationSerializer(serializers.ModelSerializer, MutationSerializer): class UpdateMutationSerializer(serializers.ModelSerializer, MutationSerializer):
serialized_relations = {} serialized_relations = {}
previous_state_handlers = {}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# we force partial mode, because update mutations are partial # we force partial mode, because update mutations are partial
@ -141,9 +140,12 @@ class UpdateMutationSerializer(serializers.ModelSerializer, MutationSerializer):
obj, obj,
*list(validated_data.keys()), *list(validated_data.keys()),
serialized_relations=self.serialized_relations, serialized_relations=self.serialized_relations,
handlers=self.previous_state_handlers, handlers=self.get_previous_state_handlers(),
) )
def get_previous_state_handlers(self):
return {}
def get_update_previous_state(obj, *fields, serialized_relations={}, handlers={}): def get_update_previous_state(obj, *fields, serialized_relations={}, handlers={}):
if not fields: if not fields:

View File

@ -11,6 +11,7 @@ from django.utils.encoding import smart_text
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from . import models from . import models
from . import utils
class RelatedField(serializers.RelatedField): class RelatedField(serializers.RelatedField):
@ -308,3 +309,12 @@ class AttachmentSerializer(serializers.Serializer):
return models.Attachment.objects.create( return models.Attachment.objects.create(
file=validated_data["file"], actor=validated_data["actor"] file=validated_data["file"], actor=validated_data["actor"]
) )
class ContentSerializer(serializers.Serializer):
text = serializers.CharField(max_length=models.CONTENT_TEXT_MAX_LENGTH)
content_type = serializers.ChoiceField(choices=models.CONTENT_TEXT_SUPPORTED_TYPES,)
html = serializers.SerializerMethodField()
def get_html(self, o):
return utils.render_html(o.text, o.content_type)

View File

@ -1,5 +1,7 @@
from django.utils.deconstruct import deconstructible from django.utils.deconstruct import deconstructible
import bleach.sanitizer
import markdown
import os import os
import shutil import shutil
import uuid import uuid
@ -241,3 +243,65 @@ def join_queries_or(left, right):
return left | right return left | right
else: else:
return right return right
def render_markdown(text):
return markdown.markdown(text, extensions=["nl2br"])
HTMl_CLEANER = bleach.sanitizer.Cleaner(
strip=True,
tags=[
"p",
"a",
"abbr",
"acronym",
"b",
"blockquote",
"code",
"em",
"i",
"li",
"ol",
"strong",
"ul",
],
)
HTML_LINKER = bleach.linkifier.Linker()
def clean_html(html):
return HTMl_CLEANER.clean(html)
def render_html(text, content_type):
rendered = render_markdown(text)
if content_type == "text/html":
rendered = text
elif content_type == "text/markdown":
rendered = render_markdown(text)
else:
rendered = render_markdown(text)
rendered = HTML_LINKER.linkify(rendered)
return clean_html(rendered).strip().replace("\n", "")
@transaction.atomic
def attach_content(obj, field, content_data):
from . import models
existing = getattr(obj, "{}_id".format(field))
if existing:
getattr(obj, field).delete()
if not content_data:
return
content_obj = models.Content.objects.create(
text=content_data["text"][: models.CONTENT_TEXT_MAX_LENGTH],
content_type=content_data["content_type"],
)
setattr(obj, field, content_obj)
obj.save(update_fields=[field])

View File

@ -9,7 +9,7 @@ from django.db import transaction
from rest_framework import serializers from rest_framework import serializers
from funkwhale_api.common import utils as funkwhale_utils from funkwhale_api.common import utils as common_utils
from funkwhale_api.common import models as common_models from funkwhale_api.common import models as common_models
from funkwhale_api.music import licenses from funkwhale_api.music import licenses
from funkwhale_api.music import models as music_models from funkwhale_api.music import models as music_models
@ -611,9 +611,9 @@ class PaginatedCollectionSerializer(jsonld.JsonLdSerializer):
def to_representation(self, conf): def to_representation(self, conf):
paginator = Paginator(conf["items"], conf.get("page_size", 20)) paginator = Paginator(conf["items"], conf.get("page_size", 20))
first = funkwhale_utils.set_query_parameter(conf["id"], page=1) first = common_utils.set_query_parameter(conf["id"], page=1)
current = first current = first
last = funkwhale_utils.set_query_parameter(conf["id"], page=paginator.num_pages) last = common_utils.set_query_parameter(conf["id"], page=paginator.num_pages)
d = { d = {
"id": conf["id"], "id": conf["id"],
# XXX Stable release: remove the obsolete actor field # XXX Stable release: remove the obsolete actor field
@ -646,7 +646,7 @@ class LibrarySerializer(PaginatedCollectionSerializer):
) )
class Meta: class Meta:
jsonld_mapping = funkwhale_utils.concat_dicts( jsonld_mapping = common_utils.concat_dicts(
PAGINATED_COLLECTION_JSONLD_MAPPING, PAGINATED_COLLECTION_JSONLD_MAPPING,
{ {
"name": jsonld.first_val(contexts.AS.name), "name": jsonld.first_val(contexts.AS.name),
@ -740,11 +740,11 @@ class CollectionPageSerializer(jsonld.JsonLdSerializer):
def to_representation(self, conf): def to_representation(self, conf):
page = conf["page"] page = conf["page"]
first = funkwhale_utils.set_query_parameter(conf["id"], page=1) first = common_utils.set_query_parameter(conf["id"], page=1)
last = funkwhale_utils.set_query_parameter( last = common_utils.set_query_parameter(
conf["id"], page=page.paginator.num_pages conf["id"], page=page.paginator.num_pages
) )
id = funkwhale_utils.set_query_parameter(conf["id"], page=page.number) id = common_utils.set_query_parameter(conf["id"], page=page.number)
d = { d = {
"id": id, "id": id,
"partOf": conf["id"], "partOf": conf["id"],
@ -764,12 +764,12 @@ class CollectionPageSerializer(jsonld.JsonLdSerializer):
} }
if page.has_previous(): if page.has_previous():
d["prev"] = funkwhale_utils.set_query_parameter( d["prev"] = common_utils.set_query_parameter(
conf["id"], page=page.previous_page_number() conf["id"], page=page.previous_page_number()
) )
if page.has_next(): if page.has_next():
d["next"] = funkwhale_utils.set_query_parameter( d["next"] = common_utils.set_query_parameter(
conf["id"], page=page.next_page_number() conf["id"], page=page.next_page_number()
) )
d.update(get_additional_fields(conf)) d.update(get_additional_fields(conf))
@ -784,6 +784,8 @@ MUSIC_ENTITY_JSONLD_MAPPING = {
"musicbrainzId": jsonld.first_val(contexts.FW.musicbrainzId), "musicbrainzId": jsonld.first_val(contexts.FW.musicbrainzId),
"attributedTo": jsonld.first_id(contexts.AS.attributedTo), "attributedTo": jsonld.first_id(contexts.AS.attributedTo),
"tags": jsonld.raw(contexts.AS.tag), "tags": jsonld.raw(contexts.AS.tag),
"mediaType": jsonld.first_val(contexts.AS.mediaType),
"content": jsonld.first_val(contexts.AS.content),
} }
@ -805,6 +807,28 @@ def repr_tag(tag_name):
return {"type": "Hashtag", "name": "#{}".format(tag_name)} return {"type": "Hashtag", "name": "#{}".format(tag_name)}
def include_content(repr, content_obj):
if not content_obj:
return
repr["content"] = common_utils.render_html(
content_obj.text, content_obj.content_type
)
repr["mediaType"] = "text/html"
class TruncatedCharField(serializers.CharField):
def __init__(self, *args, **kwargs):
self.truncate_length = kwargs.pop("truncate_length")
super().__init__(*args, **kwargs)
def to_internal_value(self, v):
v = super().to_internal_value(v)
if v:
v = v[: self.truncate_length]
return v
class MusicEntitySerializer(jsonld.JsonLdSerializer): class MusicEntitySerializer(jsonld.JsonLdSerializer):
id = serializers.URLField(max_length=500) id = serializers.URLField(max_length=500)
published = serializers.DateTimeField() published = serializers.DateTimeField()
@ -815,13 +839,23 @@ class MusicEntitySerializer(jsonld.JsonLdSerializer):
tags = serializers.ListField( tags = serializers.ListField(
child=TagSerializer(), min_length=0, required=False, allow_null=True child=TagSerializer(), min_length=0, required=False, allow_null=True
) )
mediaType = serializers.ChoiceField(
choices=common_models.CONTENT_TEXT_SUPPORTED_TYPES,
default="text/html",
required=False,
)
content = TruncatedCharField(
truncate_length=common_models.CONTENT_TEXT_MAX_LENGTH,
required=False,
allow_null=True,
)
@transaction.atomic @transaction.atomic
def update(self, instance, validated_data): def update(self, instance, validated_data):
attributed_to_fid = validated_data.get("attributedTo") attributed_to_fid = validated_data.get("attributedTo")
if attributed_to_fid: if attributed_to_fid:
validated_data["attributedTo"] = actors.get_actor(attributed_to_fid) validated_data["attributedTo"] = actors.get_actor(attributed_to_fid)
updated_fields = funkwhale_utils.get_updated_fields( updated_fields = common_utils.get_updated_fields(
self.updateable_fields, validated_data, instance self.updateable_fields, validated_data, instance
) )
updated_fields = self.validate_updated_data(instance, updated_fields) updated_fields = self.validate_updated_data(instance, updated_fields)
@ -831,6 +865,9 @@ class MusicEntitySerializer(jsonld.JsonLdSerializer):
tags = [t["name"] for t in validated_data.get("tags", []) or []] tags = [t["name"] for t in validated_data.get("tags", []) or []]
tags_models.set_tags(instance, *tags) tags_models.set_tags(instance, *tags)
common_utils.attach_content(
instance, "description", validated_data.get("description")
)
return instance return instance
def get_tags_repr(self, instance): def get_tags_repr(self, instance):
@ -842,6 +879,15 @@ class MusicEntitySerializer(jsonld.JsonLdSerializer):
def validate_updated_data(self, instance, validated_data): def validate_updated_data(self, instance, validated_data):
return validated_data return validated_data
def validate(self, data):
validated_data = super().validate(data)
if data.get("content"):
validated_data["description"] = {
"content_type": data["mediaType"],
"text": data["content"],
}
return validated_data
class ArtistSerializer(MusicEntitySerializer): class ArtistSerializer(MusicEntitySerializer):
updateable_fields = [ updateable_fields = [
@ -866,7 +912,7 @@ class ArtistSerializer(MusicEntitySerializer):
else None, else None,
"tag": self.get_tags_repr(instance), "tag": self.get_tags_repr(instance),
} }
include_content(d, instance.description)
if self.context.get("include_ap_context", self.parent is None): if self.context.get("include_ap_context", self.parent is None):
d["@context"] = jsonld.get_default_context() d["@context"] = jsonld.get_default_context()
return d return d
@ -888,7 +934,7 @@ class AlbumSerializer(MusicEntitySerializer):
class Meta: class Meta:
model = music_models.Album model = music_models.Album
jsonld_mapping = funkwhale_utils.concat_dicts( jsonld_mapping = common_utils.concat_dicts(
MUSIC_ENTITY_JSONLD_MAPPING, MUSIC_ENTITY_JSONLD_MAPPING,
{ {
"released": jsonld.first_val(contexts.FW.released), "released": jsonld.first_val(contexts.FW.released),
@ -917,6 +963,7 @@ class AlbumSerializer(MusicEntitySerializer):
else None, else None,
"tag": self.get_tags_repr(instance), "tag": self.get_tags_repr(instance),
} }
include_content(d, instance.description)
if instance.attachment_cover: if instance.attachment_cover:
d["cover"] = { d["cover"] = {
"type": "Link", "type": "Link",
@ -968,7 +1015,7 @@ class TrackSerializer(MusicEntitySerializer):
class Meta: class Meta:
model = music_models.Track model = music_models.Track
jsonld_mapping = funkwhale_utils.concat_dicts( jsonld_mapping = common_utils.concat_dicts(
MUSIC_ENTITY_JSONLD_MAPPING, MUSIC_ENTITY_JSONLD_MAPPING,
{ {
"album": jsonld.first_obj(contexts.FW.album), "album": jsonld.first_obj(contexts.FW.album),
@ -1006,7 +1053,7 @@ class TrackSerializer(MusicEntitySerializer):
else None, else None,
"tag": self.get_tags_repr(instance), "tag": self.get_tags_repr(instance),
} }
include_content(d, instance.description)
if self.context.get("include_ap_context", self.parent is None): if self.context.get("include_ap_context", self.parent is None):
d["@context"] = jsonld.get_default_context() d["@context"] = jsonld.get_default_context()
return d return d
@ -1017,23 +1064,21 @@ class TrackSerializer(MusicEntitySerializer):
references = {} references = {}
actors_to_fetch = set() actors_to_fetch = set()
actors_to_fetch.add( actors_to_fetch.add(
funkwhale_utils.recursive_getattr( common_utils.recursive_getattr(
validated_data, "attributedTo", permissive=True validated_data, "attributedTo", permissive=True
) )
) )
actors_to_fetch.add( actors_to_fetch.add(
funkwhale_utils.recursive_getattr( common_utils.recursive_getattr(
validated_data, "album.attributedTo", permissive=True validated_data, "album.attributedTo", permissive=True
) )
) )
artists = ( artists = (
funkwhale_utils.recursive_getattr( common_utils.recursive_getattr(validated_data, "artists", permissive=True)
validated_data, "artists", permissive=True
)
or [] or []
) )
album_artists = ( album_artists = (
funkwhale_utils.recursive_getattr( common_utils.recursive_getattr(
validated_data, "album.artists", permissive=True validated_data, "album.artists", permissive=True
) )
or [] or []
@ -1244,6 +1289,7 @@ class ChannelUploadSerializer(serializers.Serializer):
}, },
], ],
} }
include_content(data, upload.track.description)
tags = [item.tag.name for item in upload.get_all_tagged_items()] tags = [item.tag.name for item in upload.get_all_tagged_items()]
if tags: if tags:
data["tag"] = [repr_tag(name) for name in tags] data["tag"] = [repr_tag(name) for name in tags]

View File

@ -225,11 +225,14 @@ class MusicLibraryViewSet(
"album__attributed_to", "album__attributed_to",
"attributed_to", "attributed_to",
"album__attachment_cover", "album__attachment_cover",
"description",
).prefetch_related( ).prefetch_related(
"tagged_items__tag", "tagged_items__tag",
"album__tagged_items__tag", "album__tagged_items__tag",
"album__artist__tagged_items__tag", "album__artist__tagged_items__tag",
"artist__tagged_items__tag", "artist__tagged_items__tag",
"artist__description",
"album__description",
), ),
) )
), ),
@ -278,6 +281,7 @@ class MusicUploadViewSet(
"library__actor", "library__actor",
"track__artist", "track__artist",
"track__album__artist", "track__album__artist",
"track__description",
"track__album__attachment_cover", "track__album__attachment_cover",
) )
serializer_class = serializers.UploadSerializer serializer_class = serializers.UploadSerializer
@ -299,7 +303,7 @@ class MusicArtistViewSet(
): ):
authentication_classes = [authentication.SignatureAuthentication] authentication_classes = [authentication.SignatureAuthentication]
renderer_classes = renderers.get_ap_renderers() renderer_classes = renderers.get_ap_renderers()
queryset = music_models.Artist.objects.local() queryset = music_models.Artist.objects.local().select_related("description")
serializer_class = serializers.ArtistSerializer serializer_class = serializers.ArtistSerializer
lookup_field = "uuid" lookup_field = "uuid"
@ -309,7 +313,9 @@ class MusicAlbumViewSet(
): ):
authentication_classes = [authentication.SignatureAuthentication] authentication_classes = [authentication.SignatureAuthentication]
renderer_classes = renderers.get_ap_renderers() renderer_classes = renderers.get_ap_renderers()
queryset = music_models.Album.objects.local().select_related("artist") queryset = music_models.Album.objects.local().select_related(
"artist__description", "description"
)
serializer_class = serializers.AlbumSerializer serializer_class = serializers.AlbumSerializer
lookup_field = "uuid" lookup_field = "uuid"
@ -320,7 +326,7 @@ class MusicTrackViewSet(
authentication_classes = [authentication.SignatureAuthentication] authentication_classes = [authentication.SignatureAuthentication]
renderer_classes = renderers.get_ap_renderers() renderer_classes = renderers.get_ap_renderers()
queryset = music_models.Track.objects.local().select_related( queryset = music_models.Track.objects.local().select_related(
"album__artist", "artist" "album__artist", "album__description", "artist__description", "description"
) )
serializer_class = serializers.TrackSerializer serializer_class = serializers.TrackSerializer
lookup_field = "uuid" lookup_field = "uuid"

View File

@ -383,7 +383,9 @@ class ManageNestedAlbumSerializer(ManageBaseAlbumSerializer):
return getattr(obj, "tracks_count", None) return getattr(obj, "tracks_count", None)
class ManageArtistSerializer(ManageBaseArtistSerializer): class ManageArtistSerializer(
music_serializers.OptionalDescriptionMixin, ManageBaseArtistSerializer
):
albums = ManageNestedAlbumSerializer(many=True) albums = ManageNestedAlbumSerializer(many=True)
tracks = ManageNestedTrackSerializer(many=True) tracks = ManageNestedTrackSerializer(many=True)
attributed_to = ManageBaseActorSerializer() attributed_to = ManageBaseActorSerializer()
@ -407,7 +409,9 @@ class ManageNestedArtistSerializer(ManageBaseArtistSerializer):
pass pass
class ManageAlbumSerializer(ManageBaseAlbumSerializer): class ManageAlbumSerializer(
music_serializers.OptionalDescriptionMixin, ManageBaseAlbumSerializer
):
tracks = ManageNestedTrackSerializer(many=True) tracks = ManageNestedTrackSerializer(many=True)
attributed_to = ManageBaseActorSerializer() attributed_to = ManageBaseActorSerializer()
artist = ManageNestedArtistSerializer() artist = ManageNestedArtistSerializer()
@ -435,7 +439,9 @@ class ManageTrackAlbumSerializer(ManageBaseAlbumSerializer):
fields = ManageBaseAlbumSerializer.Meta.fields + ["artist"] fields = ManageBaseAlbumSerializer.Meta.fields + ["artist"]
class ManageTrackSerializer(ManageNestedTrackSerializer): class ManageTrackSerializer(
music_serializers.OptionalDescriptionMixin, ManageNestedTrackSerializer
):
artist = ManageNestedArtistSerializer() artist = ManageNestedArtistSerializer()
album = ManageTrackAlbumSerializer() album = ManageTrackAlbumSerializer()
attributed_to = ManageBaseActorSerializer() attributed_to = ManageBaseActorSerializer()

View File

@ -100,6 +100,11 @@ class ManageArtistViewSet(
result = serializer.save() result = serializer.save()
return response.Response(result, status=200) return response.Response(result, status=200)
def get_serializer_context(self):
context = super().get_serializer_context()
context["description"] = self.action in ["retrieve", "create", "update"]
return context
class ManageAlbumViewSet( class ManageAlbumViewSet(
mixins.ListModelMixin, mixins.ListModelMixin,
@ -134,6 +139,11 @@ class ManageAlbumViewSet(
result = serializer.save() result = serializer.save()
return response.Response(result, status=200) return response.Response(result, status=200)
def get_serializer_context(self):
context = super().get_serializer_context()
context["description"] = self.action in ["retrieve", "create", "update"]
return context
uploads_subquery = ( uploads_subquery = (
music_models.Upload.objects.filter(track_id=OuterRef("pk")) music_models.Upload.objects.filter(track_id=OuterRef("pk"))
@ -186,6 +196,11 @@ class ManageTrackViewSet(
result = serializer.save() result = serializer.save()
return response.Response(result, status=200) return response.Response(result, status=200)
def get_serializer_context(self):
context = super().get_serializer_context()
context["description"] = self.action in ["retrieve", "create", "update"]
return context
uploads_subquery = ( uploads_subquery = (
music_models.Upload.objects.filter(library_id=OuterRef("pk")) music_models.Upload.objects.filter(library_id=OuterRef("pk"))

View File

@ -168,6 +168,17 @@ def get_mp3_recording_id(f, k):
raise TagNotFound(k) raise TagNotFound(k)
def get_mp3_comment(f, k):
keys_to_try = ["COMM", "COMM::eng"]
for key in keys_to_try:
try:
return get_id3_tag(f, key)
except TagNotFound:
pass
raise TagNotFound("COMM")
VALIDATION = {} VALIDATION = {}
CONF = { CONF = {
@ -192,6 +203,7 @@ CONF = {
"field": "metadata_block_picture", "field": "metadata_block_picture",
"to_application": clean_ogg_pictures, "to_application": clean_ogg_pictures,
}, },
"comment": {"field": "comment"},
}, },
}, },
"OggVorbis": { "OggVorbis": {
@ -215,6 +227,7 @@ CONF = {
"field": "metadata_block_picture", "field": "metadata_block_picture",
"to_application": clean_ogg_pictures, "to_application": clean_ogg_pictures,
}, },
"comment": {"field": "comment"},
}, },
}, },
"OggTheora": { "OggTheora": {
@ -234,6 +247,7 @@ CONF = {
"license": {}, "license": {},
"copyright": {}, "copyright": {},
"genre": {}, "genre": {},
"comment": {"field": "comment"},
}, },
}, },
"MP3": { "MP3": {
@ -255,6 +269,7 @@ CONF = {
"pictures": {}, "pictures": {},
"license": {"field": "WCOP"}, "license": {"field": "WCOP"},
"copyright": {"field": "TCOP"}, "copyright": {"field": "TCOP"},
"comment": {"field": "COMM", "getter": get_mp3_comment},
}, },
}, },
"MP4": { "MP4": {
@ -282,6 +297,7 @@ CONF = {
"pictures": {}, "pictures": {},
"license": {"field": "----:com.apple.iTunes:LICENSE"}, "license": {"field": "----:com.apple.iTunes:LICENSE"},
"copyright": {"field": "cprt"}, "copyright": {"field": "cprt"},
"comment": {"field": "©cmt"},
}, },
}, },
"FLAC": { "FLAC": {
@ -304,6 +320,7 @@ CONF = {
"pictures": {}, "pictures": {},
"license": {}, "license": {},
"copyright": {}, "copyright": {},
"comment": {},
}, },
}, },
} }
@ -322,6 +339,7 @@ ALL_FIELDS = [
"mbid", "mbid",
"license", "license",
"copyright", "copyright",
"comment",
] ]
@ -657,6 +675,21 @@ class PositionField(serializers.CharField):
pass pass
class DescriptionField(serializers.CharField):
def get_value(self, data):
return data
def to_internal_value(self, data):
try:
value = data.get("comment") or None
except TagNotFound:
return None
if not value:
return None
value = super().to_internal_value(value)
return {"text": value, "content_type": "text/plain"}
class TrackMetadataSerializer(serializers.Serializer): class TrackMetadataSerializer(serializers.Serializer):
title = serializers.CharField() title = serializers.CharField()
position = PositionField(allow_blank=True, allow_null=True, required=False) position = PositionField(allow_blank=True, allow_null=True, required=False)
@ -665,6 +698,7 @@ class TrackMetadataSerializer(serializers.Serializer):
license = serializers.CharField(allow_blank=True, allow_null=True, required=False) license = serializers.CharField(allow_blank=True, allow_null=True, required=False)
mbid = MBIDField() mbid = MBIDField()
tags = TagsField(allow_blank=True, allow_null=True, required=False) tags = TagsField(allow_blank=True, allow_null=True, required=False)
description = DescriptionField(allow_null=True, allow_blank=True, required=False)
album = AlbumField() album = AlbumField()
artists = ArtistField() artists = ArtistField()
@ -672,6 +706,7 @@ class TrackMetadataSerializer(serializers.Serializer):
remove_blank_null_fields = [ remove_blank_null_fields = [
"copyright", "copyright",
"description",
"license", "license",
"position", "position",
"disc_number", "disc_number",

View File

@ -0,0 +1,30 @@
# Generated by Django 2.2.7 on 2020-01-13 10:18
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('common', '0006_content'),
('music', '0045_full_text_search_stop_words'),
]
operations = [
migrations.AddField(
model_name='album',
name='description',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.Content'),
),
migrations.AddField(
model_name='artist',
name='description',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.Content'),
),
migrations.AddField(
model_name='track',
name='description',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.Content'),
),
]

View File

@ -227,6 +227,9 @@ class Artist(APIModelMixin):
content_type_field="object_content_type", content_type_field="object_content_type",
object_id_field="object_id", object_id_field="object_id",
) )
description = models.ForeignKey(
"common.Content", null=True, blank=True, on_delete=models.SET_NULL
)
api = musicbrainz.api.artists api = musicbrainz.api.artists
objects = ArtistQuerySet.as_manager() objects = ArtistQuerySet.as_manager()
@ -327,6 +330,10 @@ class Album(APIModelMixin):
object_id_field="object_id", object_id_field="object_id",
) )
description = models.ForeignKey(
"common.Content", null=True, blank=True, on_delete=models.SET_NULL
)
api_includes = ["artist-credits", "recordings", "media", "release-groups"] api_includes = ["artist-credits", "recordings", "media", "release-groups"]
api = musicbrainz.api.releases api = musicbrainz.api.releases
federation_namespace = "albums" federation_namespace = "albums"
@ -508,6 +515,10 @@ class Track(APIModelMixin):
copyright = models.CharField( copyright = models.CharField(
max_length=MAX_LENGTHS["COPYRIGHT"], null=True, blank=True max_length=MAX_LENGTHS["COPYRIGHT"], null=True, blank=True
) )
description = models.ForeignKey(
"common.Content", null=True, blank=True, on_delete=models.SET_NULL
)
federation_namespace = "tracks" federation_namespace = "tracks"
musicbrainz_model = "recording" musicbrainz_model = "recording"
api = musicbrainz.api.recordings api = musicbrainz.api.recordings

View File

@ -1,6 +1,8 @@
from funkwhale_api.common import models as common_models from funkwhale_api.common import models as common_models
from funkwhale_api.common import mutations from funkwhale_api.common import mutations
from funkwhale_api.common import serializers as common_serializers from funkwhale_api.common import serializers as common_serializers
from funkwhale_api.common import utils as common_utils
from funkwhale_api.federation import routes from funkwhale_api.federation import routes
from funkwhale_api.tags import models as tags_models from funkwhale_api.tags import models as tags_models
from funkwhale_api.tags import serializers as tags_serializers from funkwhale_api.tags import serializers as tags_serializers
@ -23,11 +25,13 @@ def can_approve(obj, actor):
class TagMutation(mutations.UpdateMutationSerializer): class TagMutation(mutations.UpdateMutationSerializer):
tags = tags_serializers.TagsListField() tags = tags_serializers.TagsListField()
previous_state_handlers = {
"tags": lambda obj: list( def get_previous_state_handlers(self):
handlers = super().get_previous_state_handlers()
handlers["tags"] = lambda obj: list(
sorted(obj.tagged_items.values_list("tag__name", flat=True)) sorted(obj.tagged_items.values_list("tag__name", flat=True))
) )
} return handlers
def update(self, instance, validated_data): def update(self, instance, validated_data):
tags = validated_data.pop("tags", []) tags = validated_data.pop("tags", [])
@ -36,17 +40,36 @@ class TagMutation(mutations.UpdateMutationSerializer):
return r return r
class DescriptionMutation(mutations.UpdateMutationSerializer):
description = common_serializers.ContentSerializer()
def get_previous_state_handlers(self):
handlers = super().get_previous_state_handlers()
handlers["description"] = (
lambda obj: common_serializers.ContentSerializer(obj.description).data
if obj.description_id
else None
)
return handlers
def update(self, instance, validated_data):
description = validated_data.pop("description", None)
r = super().update(instance, validated_data)
common_utils.attach_content(instance, "description", description)
return r
@mutations.registry.connect( @mutations.registry.connect(
"update", "update",
models.Track, models.Track,
perm_checkers={"suggest": can_suggest, "approve": can_approve}, perm_checkers={"suggest": can_suggest, "approve": can_approve},
) )
class TrackMutationSerializer(TagMutation): class TrackMutationSerializer(TagMutation, DescriptionMutation):
serialized_relations = {"license": "code"} serialized_relations = {"license": "code"}
class Meta: class Meta:
model = models.Track model = models.Track
fields = ["license", "title", "position", "copyright", "tags"] fields = ["license", "title", "position", "copyright", "tags", "description"]
def post_apply(self, obj, validated_data): def post_apply(self, obj, validated_data):
routes.outbox.dispatch( routes.outbox.dispatch(
@ -59,10 +82,10 @@ class TrackMutationSerializer(TagMutation):
models.Artist, models.Artist,
perm_checkers={"suggest": can_suggest, "approve": can_approve}, perm_checkers={"suggest": can_suggest, "approve": can_approve},
) )
class ArtistMutationSerializer(TagMutation): class ArtistMutationSerializer(TagMutation, DescriptionMutation):
class Meta: class Meta:
model = models.Artist model = models.Artist
fields = ["name", "tags"] fields = ["name", "tags", "description"]
def post_apply(self, obj, validated_data): def post_apply(self, obj, validated_data):
routes.outbox.dispatch( routes.outbox.dispatch(
@ -75,27 +98,23 @@ class ArtistMutationSerializer(TagMutation):
models.Album, models.Album,
perm_checkers={"suggest": can_suggest, "approve": can_approve}, perm_checkers={"suggest": can_suggest, "approve": can_approve},
) )
class AlbumMutationSerializer(TagMutation): class AlbumMutationSerializer(TagMutation, DescriptionMutation):
cover = common_serializers.RelatedField( cover = common_serializers.RelatedField(
"uuid", queryset=common_models.Attachment.objects.all().local(), serializer=None "uuid", queryset=common_models.Attachment.objects.all().local(), serializer=None
) )
serialized_relations = {"cover": "uuid"} serialized_relations = {"cover": "uuid"}
previous_state_handlers = dict(
list(TagMutation.previous_state_handlers.items())
+ [
(
"cover",
lambda obj: str(obj.attachment_cover.uuid)
if obj.attachment_cover
else None,
),
]
)
class Meta: class Meta:
model = models.Album model = models.Album
fields = ["title", "release_date", "tags", "cover"] fields = ["title", "release_date", "tags", "cover", "description"]
def get_previous_state_handlers(self):
handlers = super().get_previous_state_handlers()
handlers["cover"] = (
lambda obj: str(obj.attachment_cover.uuid) if obj.attachment_cover else None
)
return handlers
def post_apply(self, obj, validated_data): def post_apply(self, obj, validated_data):
routes.outbox.dispatch( routes.outbox.dispatch(

View File

@ -49,6 +49,20 @@ def serialize_attributed_to(self, obj):
return federation_serializers.APIActorSerializer(obj.attributed_to).data return federation_serializers.APIActorSerializer(obj.attributed_to).data
class OptionalDescriptionMixin(object):
def to_representation(self, obj):
repr = super().to_representation(obj)
if self.context.get("description", False):
description = obj.description
repr["description"] = (
common_serializers.ContentSerializer(description).data
if description
else None
)
return repr
class LicenseSerializer(serializers.Serializer): class LicenseSerializer(serializers.Serializer):
id = serializers.SerializerMethodField() id = serializers.SerializerMethodField()
url = serializers.URLField() url = serializers.URLField()
@ -96,7 +110,7 @@ class ArtistAlbumSerializer(serializers.Serializer):
DATETIME_FIELD = serializers.DateTimeField() DATETIME_FIELD = serializers.DateTimeField()
class ArtistWithAlbumsSerializer(serializers.Serializer): class ArtistWithAlbumsSerializer(OptionalDescriptionMixin, serializers.Serializer):
albums = ArtistAlbumSerializer(many=True) albums = ArtistAlbumSerializer(many=True)
tags = serializers.SerializerMethodField() tags = serializers.SerializerMethodField()
attributed_to = serializers.SerializerMethodField() attributed_to = serializers.SerializerMethodField()
@ -152,7 +166,7 @@ def serialize_album_track(track):
} }
class AlbumSerializer(serializers.Serializer): class AlbumSerializer(OptionalDescriptionMixin, serializers.Serializer):
tracks = serializers.SerializerMethodField() tracks = serializers.SerializerMethodField()
artist = serializers.SerializerMethodField() artist = serializers.SerializerMethodField()
cover = cover_field cover = cover_field
@ -225,7 +239,7 @@ def serialize_upload(upload):
} }
class TrackSerializer(serializers.Serializer): class TrackSerializer(OptionalDescriptionMixin, serializers.Serializer):
artist = serializers.SerializerMethodField() artist = serializers.SerializerMethodField()
album = TrackAlbumSerializer(read_only=True) album = TrackAlbumSerializer(read_only=True)
uploads = serializers.SerializerMethodField() uploads = serializers.SerializerMethodField()

View File

@ -12,6 +12,7 @@ from musicbrainzngs import ResponseError
from requests.exceptions import RequestException from requests.exceptions import RequestException
from funkwhale_api.common import channels, preferences from funkwhale_api.common import channels, preferences
from funkwhale_api.common import utils as common_utils
from funkwhale_api.federation import routes from funkwhale_api.federation import routes
from funkwhale_api.federation import library as lb from funkwhale_api.federation import library as lb
from funkwhale_api.federation import utils as federation_utils from funkwhale_api.federation import utils as federation_utils
@ -309,6 +310,7 @@ def federation_audio_track_to_metadata(payload, references):
"disc_number": payload.get("disc"), "disc_number": payload.get("disc"),
"license": payload.get("license"), "license": payload.get("license"),
"copyright": payload.get("copyright"), "copyright": payload.get("copyright"),
"description": payload.get("description"),
"attributed_to": references.get(payload.get("attributedTo")), "attributed_to": references.get(payload.get("attributedTo")),
"mbid": str(payload.get("musicbrainzId")) "mbid": str(payload.get("musicbrainzId"))
if payload.get("musicbrainzId") if payload.get("musicbrainzId")
@ -317,6 +319,7 @@ def federation_audio_track_to_metadata(payload, references):
"title": payload["album"]["name"], "title": payload["album"]["name"],
"fdate": payload["album"]["published"], "fdate": payload["album"]["published"],
"fid": payload["album"]["id"], "fid": payload["album"]["id"],
"description": payload["album"].get("description"),
"attributed_to": references.get(payload["album"].get("attributedTo")), "attributed_to": references.get(payload["album"].get("attributedTo")),
"mbid": str(payload["album"]["musicbrainzId"]) "mbid": str(payload["album"]["musicbrainzId"])
if payload["album"].get("musicbrainzId") if payload["album"].get("musicbrainzId")
@ -328,6 +331,7 @@ def federation_audio_track_to_metadata(payload, references):
"fid": a["id"], "fid": a["id"],
"name": a["name"], "name": a["name"],
"fdate": a["published"], "fdate": a["published"],
"description": a.get("description"),
"attributed_to": references.get(a.get("attributedTo")), "attributed_to": references.get(a.get("attributedTo")),
"mbid": str(a["musicbrainzId"]) if a.get("musicbrainzId") else None, "mbid": str(a["musicbrainzId"]) if a.get("musicbrainzId") else None,
"tags": [t["name"] for t in a.get("tags", []) or []], "tags": [t["name"] for t in a.get("tags", []) or []],
@ -340,6 +344,7 @@ def federation_audio_track_to_metadata(payload, references):
"fid": a["id"], "fid": a["id"],
"name": a["name"], "name": a["name"],
"fdate": a["published"], "fdate": a["published"],
"description": a.get("description"),
"attributed_to": references.get(a.get("attributedTo")), "attributed_to": references.get(a.get("attributedTo")),
"mbid": str(a["musicbrainzId"]) if a.get("musicbrainzId") else None, "mbid": str(a["musicbrainzId"]) if a.get("musicbrainzId") else None,
"tags": [t["name"] for t in a.get("tags", []) or []], "tags": [t["name"] for t in a.get("tags", []) or []],
@ -505,6 +510,9 @@ def _get_track(data, attributed_to=None, **forced_values):
) )
if created: if created:
tags_models.add_tags(artist, *artist_data.get("tags", [])) tags_models.add_tags(artist, *artist_data.get("tags", []))
common_utils.attach_content(
artist, "description", artist_data.get("description")
)
if "album" in forced_values: if "album" in forced_values:
album = forced_values["album"] album = forced_values["album"]
@ -539,6 +547,9 @@ def _get_track(data, attributed_to=None, **forced_values):
) )
if created: if created:
tags_models.add_tags(album_artist, *album_artist_data.get("tags", [])) tags_models.add_tags(album_artist, *album_artist_data.get("tags", []))
common_utils.attach_content(
album_artist, "description", album_artist_data.get("description")
)
# get / create album # get / create album
album_data = data["album"] album_data = data["album"]
@ -569,6 +580,9 @@ def _get_track(data, attributed_to=None, **forced_values):
) )
if created: if created:
tags_models.add_tags(album, *album_data.get("tags", [])) tags_models.add_tags(album, *album_data.get("tags", []))
common_utils.attach_content(
album, "description", album_data.get("description")
)
# get / create track # get / create track
track_title = ( track_title = (
@ -602,6 +616,7 @@ def _get_track(data, attributed_to=None, **forced_values):
query |= Q(mbid=track_mbid) query |= Q(mbid=track_mbid)
if track_fid: if track_fid:
query |= Q(fid=track_fid) query |= Q(fid=track_fid)
defaults = { defaults = {
"title": track_title, "title": track_title,
"album": album, "album": album,
@ -627,6 +642,8 @@ def _get_track(data, attributed_to=None, **forced_values):
forced_values["tags"] if "tags" in forced_values else data.get("tags", []) forced_values["tags"] if "tags" in forced_values else data.get("tags", [])
) )
tags_models.add_tags(track, *tags) tags_models.add_tags(track, *tags)
common_utils.attach_content(track, "description", data.get("description"))
return track return track

View File

@ -143,6 +143,11 @@ class ArtistViewSet(
obj = refetch_obj(obj, self.get_queryset()) obj = refetch_obj(obj, self.get_queryset())
return obj return obj
def get_serializer_context(self):
context = super().get_serializer_context()
context["description"] = self.action in ["retrieve", "create", "update"]
return context
def get_queryset(self): def get_queryset(self):
queryset = super().get_queryset() queryset = super().get_queryset()
albums = models.Album.objects.with_tracks_count().select_related( albums = models.Album.objects.with_tracks_count().select_related(
@ -194,6 +199,11 @@ class AlbumViewSet(
obj = refetch_obj(obj, self.get_queryset()) obj = refetch_obj(obj, self.get_queryset())
return obj return obj
def get_serializer_context(self):
context = super().get_serializer_context()
context["description"] = self.action in ["retrieve", "create", "update"]
return context
def get_queryset(self): def get_queryset(self):
queryset = super().get_queryset() queryset = super().get_queryset()
tracks = ( tracks = (
@ -332,6 +342,11 @@ class TrackViewSet(
get_libraries(filter_uploads=lambda o, uploads: uploads.filter(track=o)) get_libraries(filter_uploads=lambda o, uploads: uploads.filter(track=o))
) )
def get_serializer_context(self):
context = super().get_serializer_context()
context["description"] = self.action in ["retrieve", "create", "update"]
return context
def strip_absolute_media_url(path): def strip_absolute_media_url(path):
if ( if (

View File

@ -76,3 +76,5 @@ django-cacheops==4.2
click>=7,<8 click>=7,<8
service_identity==18.1.0 service_identity==18.1.0
markdown>=3,<4
bleach>=3,<4

View File

@ -71,3 +71,17 @@ def test_attachment_queryset_attached(args, expected, factories, queryset_equal_
queryset = attachments[0].__class__.objects.attached(*args).order_by("id") queryset = attachments[0].__class__.objects.attached(*args).order_by("id")
expected_objs = [attachments[i] for i in expected] expected_objs = [attachments[i] for i in expected]
assert queryset == expected_objs assert queryset == expected_objs
def test_removing_obj_removes_content(factories):
kept_content = factories["common.Content"]()
removed_content = factories["common.Content"]()
track1 = factories["music.Track"](description=removed_content)
factories["music.Track"](description=kept_content)
track1.delete()
with pytest.raises(removed_content.DoesNotExist):
removed_content.refresh_from_db()
kept_content.refresh_from_db()

View File

@ -7,6 +7,7 @@ from django.urls import reverse
import django_filters import django_filters
from funkwhale_api.common import serializers from funkwhale_api.common import serializers
from funkwhale_api.common import utils
from funkwhale_api.users import models from funkwhale_api.users import models
from funkwhale_api.federation import utils as federation_utils from funkwhale_api.federation import utils as federation_utils
@ -252,3 +253,17 @@ def test_attachment_serializer_remote_file(factories, to_api_date):
serializer = serializers.AttachmentSerializer(attachment) serializer = serializers.AttachmentSerializer(attachment)
assert serializer.data == expected assert serializer.data == expected
def test_content_serializer(factories):
content = factories["common.Content"]()
expected = {
"text": content.text,
"content_type": content.content_type,
"html": utils.render_html(content.text, content.content_type),
}
serializer = serializers.ContentSerializer(content)
assert serializer.data == expected

View File

@ -99,3 +99,28 @@ def test_get_updated_fields(conf, mock_args, data, expected, mocker):
) )
def test_join_url(start, end, expected): def test_join_url(start, end, expected):
assert utils.join_url(start, end) == expected assert utils.join_url(start, end) == expected
@pytest.mark.parametrize(
"text, content_type, expected",
[
("hello world", "text/markdown", "<p>hello world</p>"),
("hello world", "text/plain", "<p>hello world</p>"),
("<strong>hello world</strong>", "text/html", "<strong>hello world</strong>"),
# images and other non whitelisted html should be removed
("hello world\n![img](src)", "text/markdown", "<p>hello world</p>"),
(
"hello world\n\n<script></script>\n\n<style></style>",
"text/markdown",
"<p>hello world</p>",
),
(
"<p>hello world</p><script></script>\n\n<style></style>",
"text/html",
"<p>hello world</p>",
),
],
)
def test_render_html(text, content_type, expected):
result = utils.render_html(text, content_type)
assert result == expected

View File

@ -5,6 +5,7 @@ import uuid
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.utils import timezone from django.utils import timezone
from funkwhale_api.common import utils as common_utils
from funkwhale_api.federation import contexts from funkwhale_api.federation import contexts
from funkwhale_api.federation import keys from funkwhale_api.federation import keys
from funkwhale_api.federation import jsonld from funkwhale_api.federation import jsonld
@ -560,7 +561,10 @@ def test_music_library_serializer_from_private(factories, mocker):
def test_activity_pub_artist_serializer_to_ap(factories): def test_activity_pub_artist_serializer_to_ap(factories):
artist = factories["music.Artist"](attributed=True, set_tags=["Punk", "Rock"]) content = factories["common.Content"]()
artist = factories["music.Artist"](
description=content, attributed=True, set_tags=["Punk", "Rock"]
)
expected = { expected = {
"@context": jsonld.get_default_context(), "@context": jsonld.get_default_context(),
"type": "Artist", "type": "Artist",
@ -569,6 +573,8 @@ def test_activity_pub_artist_serializer_to_ap(factories):
"musicbrainzId": artist.mbid, "musicbrainzId": artist.mbid,
"published": artist.creation_date.isoformat(), "published": artist.creation_date.isoformat(),
"attributedTo": artist.attributed_to.fid, "attributedTo": artist.attributed_to.fid,
"mediaType": "text/html",
"content": common_utils.render_html(content.text, content.content_type),
"tag": [ "tag": [
{"type": "Hashtag", "name": "#Punk"}, {"type": "Hashtag", "name": "#Punk"},
{"type": "Hashtag", "name": "#Rock"}, {"type": "Hashtag", "name": "#Rock"},
@ -580,7 +586,10 @@ def test_activity_pub_artist_serializer_to_ap(factories):
def test_activity_pub_album_serializer_to_ap(factories): def test_activity_pub_album_serializer_to_ap(factories):
album = factories["music.Album"](attributed=True, set_tags=["Punk", "Rock"]) content = factories["common.Content"]()
album = factories["music.Album"](
description=content, attributed=True, set_tags=["Punk", "Rock"]
)
expected = { expected = {
"@context": jsonld.get_default_context(), "@context": jsonld.get_default_context(),
@ -601,6 +610,8 @@ def test_activity_pub_album_serializer_to_ap(factories):
).data ).data
], ],
"attributedTo": album.attributed_to.fid, "attributedTo": album.attributed_to.fid,
"mediaType": "text/html",
"content": common_utils.render_html(content.text, content.content_type),
"tag": [ "tag": [
{"type": "Hashtag", "name": "#Punk"}, {"type": "Hashtag", "name": "#Punk"},
{"type": "Hashtag", "name": "#Rock"}, {"type": "Hashtag", "name": "#Rock"},
@ -653,7 +664,9 @@ def test_activity_pub_album_serializer_from_ap_update(factories, faker):
def test_activity_pub_track_serializer_to_ap(factories): def test_activity_pub_track_serializer_to_ap(factories):
content = factories["common.Content"]()
track = factories["music.Track"]( track = factories["music.Track"](
description=content,
license="cc-by-4.0", license="cc-by-4.0",
copyright="test", copyright="test",
disc_number=3, disc_number=3,
@ -680,6 +693,8 @@ def test_activity_pub_track_serializer_to_ap(factories):
track.album, context={"include_ap_context": False} track.album, context={"include_ap_context": False}
).data, ).data,
"attributedTo": track.attributed_to.fid, "attributedTo": track.attributed_to.fid,
"mediaType": "text/html",
"content": common_utils.render_html(content.text, content.content_type),
"tag": [ "tag": [
{"type": "Hashtag", "name": "#Punk"}, {"type": "Hashtag", "name": "#Punk"},
{"type": "Hashtag", "name": "#Rock"}, {"type": "Hashtag", "name": "#Rock"},
@ -709,6 +724,7 @@ def test_activity_pub_track_serializer_from_ap(factories, r_mock, mocker):
"name": "Black in back", "name": "Black in back",
"position": 5, "position": 5,
"disc": 1, "disc": 1,
"content": "Hello there",
"attributedTo": track_attributed_to.fid, "attributedTo": track_attributed_to.fid,
"album": { "album": {
"type": "Album", "type": "Album",
@ -717,6 +733,8 @@ def test_activity_pub_track_serializer_from_ap(factories, r_mock, mocker):
"musicbrainzId": str(uuid.uuid4()), "musicbrainzId": str(uuid.uuid4()),
"published": published.isoformat(), "published": published.isoformat(),
"released": released.isoformat(), "released": released.isoformat(),
"content": "Album summary",
"mediaType": "text/markdown",
"attributedTo": album_attributed_to.fid, "attributedTo": album_attributed_to.fid,
"cover": { "cover": {
"type": "Link", "type": "Link",
@ -727,6 +745,8 @@ def test_activity_pub_track_serializer_from_ap(factories, r_mock, mocker):
"artists": [ "artists": [
{ {
"type": "Artist", "type": "Artist",
"mediaType": "text/plain",
"content": "Artist summary",
"id": "http://hello.artist", "id": "http://hello.artist",
"name": "John Smith", "name": "John Smith",
"musicbrainzId": str(uuid.uuid4()), "musicbrainzId": str(uuid.uuid4()),
@ -741,6 +761,8 @@ def test_activity_pub_track_serializer_from_ap(factories, r_mock, mocker):
"type": "Artist", "type": "Artist",
"id": "http://hello.trackartist", "id": "http://hello.trackartist",
"name": "Bob Smith", "name": "Bob Smith",
"mediaType": "text/plain",
"content": "Other artist summary",
"musicbrainzId": str(uuid.uuid4()), "musicbrainzId": str(uuid.uuid4()),
"attributedTo": artist_attributed_to.fid, "attributedTo": artist_attributed_to.fid,
"published": published.isoformat(), "published": published.isoformat(),
@ -769,6 +791,8 @@ def test_activity_pub_track_serializer_from_ap(factories, r_mock, mocker):
assert track.creation_date == published assert track.creation_date == published
assert track.attributed_to == track_attributed_to assert track.attributed_to == track_attributed_to
assert str(track.mbid) == data["musicbrainzId"] assert str(track.mbid) == data["musicbrainzId"]
assert track.description.text == data["content"]
assert track.description.content_type == "text/html"
assert album.from_activity == activity assert album.from_activity == activity
assert album.attachment_cover.file.read() == b"coucou" assert album.attachment_cover.file.read() == b"coucou"
@ -779,6 +803,8 @@ def test_activity_pub_track_serializer_from_ap(factories, r_mock, mocker):
assert album.creation_date == published assert album.creation_date == published
assert album.release_date == released assert album.release_date == released
assert album.attributed_to == album_attributed_to assert album.attributed_to == album_attributed_to
assert album.description.text == data["album"]["content"]
assert album.description.content_type == data["album"]["mediaType"]
assert artist.from_activity == activity assert artist.from_activity == activity
assert artist.name == data["artists"][0]["name"] assert artist.name == data["artists"][0]["name"]
@ -786,6 +812,8 @@ def test_activity_pub_track_serializer_from_ap(factories, r_mock, mocker):
assert str(artist.mbid) == data["artists"][0]["musicbrainzId"] assert str(artist.mbid) == data["artists"][0]["musicbrainzId"]
assert artist.creation_date == published assert artist.creation_date == published
assert artist.attributed_to == artist_attributed_to assert artist.attributed_to == artist_attributed_to
assert artist.description.text == data["artists"][0]["content"]
assert artist.description.content_type == data["artists"][0]["mediaType"]
assert album_artist.from_activity == activity assert album_artist.from_activity == activity
assert album_artist.name == data["album"]["artists"][0]["name"] assert album_artist.name == data["album"]["artists"][0]["name"]
@ -793,6 +821,11 @@ def test_activity_pub_track_serializer_from_ap(factories, r_mock, mocker):
assert str(album_artist.mbid) == data["album"]["artists"][0]["musicbrainzId"] assert str(album_artist.mbid) == data["album"]["artists"][0]["musicbrainzId"]
assert album_artist.creation_date == published assert album_artist.creation_date == published
assert album_artist.attributed_to == album_artist_attributed_to assert album_artist.attributed_to == album_artist_attributed_to
assert album_artist.description.text == data["album"]["artists"][0]["content"]
assert (
album_artist.description.content_type
== data["album"]["artists"][0]["mediaType"]
)
add_tags.assert_any_call(track, *["Hello", "World"]) add_tags.assert_any_call(track, *["Hello", "World"])
add_tags.assert_any_call(album, *["AlbumTag"]) add_tags.assert_any_call(album, *["AlbumTag"])
@ -802,8 +835,9 @@ def test_activity_pub_track_serializer_from_ap(factories, r_mock, mocker):
def test_activity_pub_track_serializer_from_ap_update(factories, r_mock, mocker): def test_activity_pub_track_serializer_from_ap_update(factories, r_mock, mocker):
set_tags = mocker.patch("funkwhale_api.tags.models.set_tags") set_tags = mocker.patch("funkwhale_api.tags.models.set_tags")
content = factories["common.Content"]()
track_attributed_to = factories["federation.Actor"]() track_attributed_to = factories["federation.Actor"]()
track = factories["music.Track"]() track = factories["music.Track"](description=content)
published = timezone.now() published = timezone.now()
data = { data = {
@ -815,6 +849,7 @@ def test_activity_pub_track_serializer_from_ap_update(factories, r_mock, mocker)
"name": "Black in back", "name": "Black in back",
"position": 5, "position": 5,
"disc": 2, "disc": 2,
"content": "hello there",
"attributedTo": track_attributed_to.fid, "attributedTo": track_attributed_to.fid,
"album": serializers.AlbumSerializer(track.album).data, "album": serializers.AlbumSerializer(track.album).data,
"artists": [serializers.ArtistSerializer(track.artist).data], "artists": [serializers.ArtistSerializer(track.artist).data],
@ -835,10 +870,15 @@ def test_activity_pub_track_serializer_from_ap_update(factories, r_mock, mocker)
assert track.position == data["position"] assert track.position == data["position"]
assert track.disc_number == data["disc"] assert track.disc_number == data["disc"]
assert track.attributed_to == track_attributed_to assert track.attributed_to == track_attributed_to
assert track.description.content_type == "text/html"
assert track.description.text == "hello there"
assert str(track.mbid) == data["musicbrainzId"] assert str(track.mbid) == data["musicbrainzId"]
set_tags.assert_called_once_with(track, *["Hello", "World"]) set_tags.assert_called_once_with(track, *["Hello", "World"])
with pytest.raises(content.DoesNotExist):
content.refresh_from_db()
def test_activity_pub_upload_serializer_from_ap(factories, mocker, r_mock): def test_activity_pub_upload_serializer_from_ap(factories, mocker, r_mock):
activity = factories["federation.Activity"]() activity = factories["federation.Activity"]()
@ -1083,11 +1123,13 @@ def test_channel_actor_outbox_serializer(factories):
def test_channel_upload_serializer(factories): def test_channel_upload_serializer(factories):
channel = factories["audio.Channel"]() channel = factories["audio.Channel"]()
content = factories["common.Content"]()
upload = factories["music.Upload"]( upload = factories["music.Upload"](
playable=True, playable=True,
library=channel.library, library=channel.library,
import_status="finished", import_status="finished",
track__set_tags=["Punk"], track__set_tags=["Punk"],
track__description=content,
track__album__set_tags=["Rock"], track__album__set_tags=["Rock"],
track__artist__set_tags=["Indie"], track__artist__set_tags=["Indie"],
) )
@ -1100,6 +1142,8 @@ def test_channel_upload_serializer(factories):
"summary": "#Indie #Punk #Rock", "summary": "#Indie #Punk #Rock",
"attributedTo": channel.actor.fid, "attributedTo": channel.actor.fid,
"published": upload.creation_date.isoformat(), "published": upload.creation_date.isoformat(),
"mediaType": "text/html",
"content": common_utils.render_html(content.text, content.content_type),
"to": "https://www.w3.org/ns/activitystreams#Public", "to": "https://www.w3.org/ns/activitystreams#Public",
"url": [ "url": [
{ {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -30,6 +30,7 @@ DATA_DIR = os.path.dirname(os.path.abspath(__file__))
), ),
("license", "Dummy license: http://creativecommons.org/licenses/by-sa/4.0/"), ("license", "Dummy license: http://creativecommons.org/licenses/by-sa/4.0/"),
("copyright", "Someone"), ("copyright", "Someone"),
("comment", "Hello there"),
], ],
) )
def test_can_get_metadata_from_ogg_file(field, value): def test_can_get_metadata_from_ogg_file(field, value):
@ -58,6 +59,7 @@ def test_can_get_metadata_all():
"license": "Dummy license: http://creativecommons.org/licenses/by-sa/4.0/", "license": "Dummy license: http://creativecommons.org/licenses/by-sa/4.0/",
"copyright": "Someone", "copyright": "Someone",
"genre": "Classical", "genre": "Classical",
"comment": "Hello there",
} }
assert data.all() == expected assert data.all() == expected
@ -81,6 +83,7 @@ def test_can_get_metadata_all():
), ),
("license", "Dummy license: http://creativecommons.org/licenses/by-sa/4.0/"), ("license", "Dummy license: http://creativecommons.org/licenses/by-sa/4.0/"),
("copyright", "Someone"), ("copyright", "Someone"),
("comment", "Hello there"),
], ],
) )
def test_can_get_metadata_from_opus_file(field, value): def test_can_get_metadata_from_opus_file(field, value):
@ -104,6 +107,7 @@ def test_can_get_metadata_from_opus_file(field, value):
("mbid", "124d0150-8627-46bc-bc14-789a3bc960c8"), ("mbid", "124d0150-8627-46bc-bc14-789a3bc960c8"),
("musicbrainz_artistid", "c3bc80a6-1f4a-4e17-8cf0-6b1efe8302f1"), ("musicbrainz_artistid", "c3bc80a6-1f4a-4e17-8cf0-6b1efe8302f1"),
("musicbrainz_albumartistid", "c3bc80a6-1f4a-4e17-8cf0-6b1efe8302f1"), ("musicbrainz_albumartistid", "c3bc80a6-1f4a-4e17-8cf0-6b1efe8302f1"),
("comment", "Hello there"),
# somehow, I cannot successfully create an ogg theora file # somehow, I cannot successfully create an ogg theora file
# with the proper license field # with the proper license field
# ("license", "Dummy license: http://creativecommons.org/licenses/by-sa/4.0/"), # ("license", "Dummy license: http://creativecommons.org/licenses/by-sa/4.0/"),
@ -132,6 +136,7 @@ def test_can_get_metadata_from_ogg_theora_file(field, value):
("musicbrainz_albumartistid", "9c6bddde-6228-4d9f-ad0d-03f6fcb19e13"), ("musicbrainz_albumartistid", "9c6bddde-6228-4d9f-ad0d-03f6fcb19e13"),
("license", "https://creativecommons.org/licenses/by-nc-nd/2.5/"), ("license", "https://creativecommons.org/licenses/by-nc-nd/2.5/"),
("copyright", "Someone"), ("copyright", "Someone"),
("comment", "Hello there"),
], ],
) )
def test_can_get_metadata_from_id3_mp3_file(field, value): def test_can_get_metadata_from_id3_mp3_file(field, value):
@ -181,6 +186,7 @@ def test_can_get_pictures(name):
("musicbrainz_albumartistid", "b7ffd2af-418f-4be2-bdd1-22f8b48613da"), ("musicbrainz_albumartistid", "b7ffd2af-418f-4be2-bdd1-22f8b48613da"),
("license", "http://creativecommons.org/licenses/by-nc-sa/3.0/us/"), ("license", "http://creativecommons.org/licenses/by-nc-sa/3.0/us/"),
("copyright", "2008 nin"), ("copyright", "2008 nin"),
("comment", "Hello there"),
], ],
) )
def test_can_get_metadata_from_flac_file(field, value): def test_can_get_metadata_from_flac_file(field, value):
@ -210,6 +216,7 @@ def test_can_get_metadata_from_flac_file(field, value):
("license", "Dummy license: http://creativecommons.org/licenses/by-sa/4.0/"), ("license", "Dummy license: http://creativecommons.org/licenses/by-sa/4.0/"),
("copyright", "Someone"), ("copyright", "Someone"),
("genre", "Dubstep"), ("genre", "Dubstep"),
("comment", "Hello there"),
], ],
) )
def test_can_get_metadata_from_m4a_file(field, value): def test_can_get_metadata_from_m4a_file(field, value):
@ -294,6 +301,7 @@ def test_metadata_fallback_ogg_theora(mocker):
"license": "https://creativecommons.org/licenses/by-nc-nd/2.5/", "license": "https://creativecommons.org/licenses/by-nc-nd/2.5/",
"copyright": "Someone", "copyright": "Someone",
"tags": ["Funk"], "tags": ["Funk"],
"description": {"text": "Hello there", "content_type": "text/plain"},
}, },
), ),
( (
@ -327,6 +335,7 @@ def test_metadata_fallback_ogg_theora(mocker):
"license": "Dummy license: http://creativecommons.org/licenses/by-sa/4.0/", "license": "Dummy license: http://creativecommons.org/licenses/by-sa/4.0/",
"copyright": "Someone", "copyright": "Someone",
"tags": ["Classical"], "tags": ["Classical"],
"description": {"text": "Hello there", "content_type": "text/plain"},
}, },
), ),
( (
@ -360,6 +369,7 @@ def test_metadata_fallback_ogg_theora(mocker):
"license": "Dummy license: http://creativecommons.org/licenses/by-sa/4.0/", "license": "Dummy license: http://creativecommons.org/licenses/by-sa/4.0/",
"copyright": "Someone", "copyright": "Someone",
"tags": ["Classical"], "tags": ["Classical"],
"description": {"text": "Hello there", "content_type": "text/plain"},
}, },
), ),
( (
@ -391,6 +401,7 @@ def test_metadata_fallback_ogg_theora(mocker):
# with the proper license field # with the proper license field
# ("license", "Dummy license: http://creativecommons.org/licenses/by-sa/4.0/"), # ("license", "Dummy license: http://creativecommons.org/licenses/by-sa/4.0/"),
"copyright": "℗ 2012 JKP GmbH & Co. KG", "copyright": "℗ 2012 JKP GmbH & Co. KG",
"description": {"text": "Hello there", "content_type": "text/plain"},
}, },
), ),
( (
@ -420,6 +431,7 @@ def test_metadata_fallback_ogg_theora(mocker):
"license": "http://creativecommons.org/licenses/by-nc-sa/3.0/us/", "license": "http://creativecommons.org/licenses/by-nc-sa/3.0/us/",
"copyright": "2008 nin", "copyright": "2008 nin",
"tags": ["Industrial"], "tags": ["Industrial"],
"description": {"text": "Hello there", "content_type": "text/plain"},
}, },
), ),
], ],
@ -528,10 +540,12 @@ def test_fake_metadata_with_serializer():
"musicbrainz_albumartistid": "013c8e5b-d72a-4cd3-8dee-6c64d6125823;5b4d7d2d-36df-4b38-95e3-a964234f520f", "musicbrainz_albumartistid": "013c8e5b-d72a-4cd3-8dee-6c64d6125823;5b4d7d2d-36df-4b38-95e3-a964234f520f",
"license": "Dummy license: http://creativecommons.org/licenses/by-sa/4.0/", "license": "Dummy license: http://creativecommons.org/licenses/by-sa/4.0/",
"copyright": "Someone", "copyright": "Someone",
"comment": "hello there",
} }
expected = { expected = {
"title": "Peer Gynt Suite no. 1, op. 46: I. Morning", "title": "Peer Gynt Suite no. 1, op. 46: I. Morning",
"description": {"text": "hello there", "content_type": "text/plain"},
"artists": [ "artists": [
{ {
"name": "Edvard Grieg", "name": "Edvard Grieg",

View File

@ -1,6 +1,7 @@
import datetime import datetime
import pytest import pytest
from funkwhale_api.common import serializers as common_serializers
from funkwhale_api.music import licenses from funkwhale_api.music import licenses
from funkwhale_api.music import mutations from funkwhale_api.music import mutations
@ -195,3 +196,26 @@ def test_mutation_set_attachment_cover(factories, now, mocker):
assert obj.attachment_cover == new_attachment assert obj.attachment_cover == new_attachment
assert mutation.previous_state["cover"] == old_attachment.uuid assert mutation.previous_state["cover"] == old_attachment.uuid
@pytest.mark.parametrize(
"factory_name", ["music.Track", "music.Album", "music.Artist"],
)
def test_album_mutation_description(factory_name, factories, mocker):
mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
content = factories["common.Content"]()
obj = factories[factory_name](description=content)
mutation = factories["common.Mutation"](
type="update",
target=obj,
payload={"description": {"content_type": "text/plain", "text": "hello there"}},
)
mutation.apply()
obj.refresh_from_db()
assert obj.description.content_type == "text/plain"
assert obj.description.text == "hello there"
assert (
mutation.previous_state["description"]
== common_serializers.ContentSerializer(content).data
)

View File

@ -501,3 +501,21 @@ def test_upload_with_channel_validates_import_metadata(factories, uploaded_audio
) )
with pytest.raises(serializers.serializers.ValidationError): with pytest.raises(serializers.serializers.ValidationError):
assert serializer.is_valid(raise_exception=True) assert serializer.is_valid(raise_exception=True)
@pytest.mark.parametrize(
"factory_name, serializer_class",
[
("music.Artist", serializers.ArtistWithAlbumsSerializer),
("music.Album", serializers.AlbumSerializer),
("music.Track", serializers.TrackSerializer),
],
)
def test_detail_serializers_with_description_description(
factory_name, serializer_class, factories
):
content = factories["common.Content"]()
obj = factories[factory_name](description=content)
expected = common_serializers.ContentSerializer(content).data
serializer = serializer_class(obj, context={"description": True})
assert serializer.data["description"] == expected

View File

@ -128,6 +128,21 @@ def test_can_create_track_from_file_metadata_featuring(factories):
assert track.artist.name == "Santana feat. Chris Cornell" assert track.artist.name == "Santana feat. Chris Cornell"
def test_can_create_track_from_file_metadata_description(factories):
metadata = {
"title": "Whole Lotta Love",
"position": 1,
"disc_number": 1,
"description": {"text": "hello there", "content_type": "text/plain"},
"album": {"title": "Test album"},
"artists": [{"name": "Santana"}],
}
track = tasks.get_track_from_import_metadata(metadata)
assert track.description.text == "hello there"
assert track.description.content_type == "text/plain"
def test_can_create_track_from_file_metadata_mbid(factories, mocker): def test_can_create_track_from_file_metadata_mbid(factories, mocker):
metadata = { metadata = {
"title": "Test track", "title": "Test track",
@ -607,6 +622,7 @@ def test_federation_audio_track_to_metadata(now, mocker):
"copyright": "2018 Someone", "copyright": "2018 Someone",
"attributedTo": "http://track.attributed", "attributedTo": "http://track.attributed",
"tag": [{"type": "Hashtag", "name": "TrackTag"}], "tag": [{"type": "Hashtag", "name": "TrackTag"}],
"content": "hello there",
"album": { "album": {
"published": published.isoformat(), "published": published.isoformat(),
"type": "Album", "type": "Album",
@ -616,12 +632,16 @@ def test_federation_audio_track_to_metadata(now, mocker):
"released": released.isoformat(), "released": released.isoformat(),
"tag": [{"type": "Hashtag", "name": "AlbumTag"}], "tag": [{"type": "Hashtag", "name": "AlbumTag"}],
"attributedTo": "http://album.attributed", "attributedTo": "http://album.attributed",
"content": "album desc",
"mediaType": "text/plain",
"artists": [ "artists": [
{ {
"type": "Artist", "type": "Artist",
"published": published.isoformat(), "published": published.isoformat(),
"id": "http://hello.artist", "id": "http://hello.artist",
"name": "John Smith", "name": "John Smith",
"content": "album artist desc",
"mediaType": "text/markdown",
"musicbrainzId": str(uuid.uuid4()), "musicbrainzId": str(uuid.uuid4()),
"attributedTo": "http://album-artist.attributed", "attributedTo": "http://album-artist.attributed",
"tag": [{"type": "Hashtag", "name": "AlbumArtistTag"}], "tag": [{"type": "Hashtag", "name": "AlbumArtistTag"}],
@ -639,6 +659,8 @@ def test_federation_audio_track_to_metadata(now, mocker):
"type": "Artist", "type": "Artist",
"id": "http://hello.trackartist", "id": "http://hello.trackartist",
"name": "Bob Smith", "name": "Bob Smith",
"content": "artist desc",
"mediaType": "text/html",
"musicbrainzId": str(uuid.uuid4()), "musicbrainzId": str(uuid.uuid4()),
"attributedTo": "http://artist.attributed", "attributedTo": "http://artist.attributed",
"tag": [{"type": "Hashtag", "name": "ArtistTag"}], "tag": [{"type": "Hashtag", "name": "ArtistTag"}],
@ -658,6 +680,7 @@ def test_federation_audio_track_to_metadata(now, mocker):
"fid": payload["id"], "fid": payload["id"],
"attributed_to": references["http://track.attributed"], "attributed_to": references["http://track.attributed"],
"tags": ["TrackTag"], "tags": ["TrackTag"],
"description": {"content_type": "text/html", "text": "hello there"},
"album": { "album": {
"title": payload["album"]["name"], "title": payload["album"]["name"],
"attributed_to": references["http://album.attributed"], "attributed_to": references["http://album.attributed"],
@ -666,6 +689,7 @@ def test_federation_audio_track_to_metadata(now, mocker):
"fid": payload["album"]["id"], "fid": payload["album"]["id"],
"fdate": serializer.validated_data["album"]["published"], "fdate": serializer.validated_data["album"]["published"],
"tags": ["AlbumTag"], "tags": ["AlbumTag"],
"description": {"content_type": "text/plain", "text": "album desc"},
"artists": [ "artists": [
{ {
"name": a["name"], "name": a["name"],
@ -675,6 +699,10 @@ def test_federation_audio_track_to_metadata(now, mocker):
"fdate": serializer.validated_data["album"]["artists"][i][ "fdate": serializer.validated_data["album"]["artists"][i][
"published" "published"
], ],
"description": {
"content_type": "text/markdown",
"text": "album artist desc",
},
"tags": ["AlbumArtistTag"], "tags": ["AlbumArtistTag"],
} }
for i, a in enumerate(payload["album"]["artists"]) for i, a in enumerate(payload["album"]["artists"])
@ -690,6 +718,7 @@ def test_federation_audio_track_to_metadata(now, mocker):
"fdate": serializer.validated_data["artists"][i]["published"], "fdate": serializer.validated_data["artists"][i]["published"],
"attributed_to": references["http://artist.attributed"], "attributed_to": references["http://artist.attributed"],
"tags": ["ArtistTag"], "tags": ["ArtistTag"],
"description": {"content_type": "text/html", "text": "artist desc"},
} }
for i, a in enumerate(payload["artists"]) for i, a in enumerate(payload["artists"])
], ],

Binary file not shown.

View File

@ -1256,3 +1256,22 @@ def test_search_get_fts_stop_words(settings, logged_in_api_client, factories):
assert response.status_code == 200 assert response.status_code == 200
assert response.data == expected assert response.data == expected
@pytest.mark.parametrize(
"route, factory_name",
[
("api:v1:artists-detail", "music.Artist"),
("api:v1:albums-detail", "music.Album"),
("api:v1:tracks-detail", "music.Track"),
],
)
def test_detail_includes_description_key(
route, factory_name, logged_in_api_client, factories
):
obj = factories[factory_name]()
url = reverse(route, kwargs={"pk": obj.pk})
response = logged_in_api_client.get(url)
assert response.data["description"] is None

View File

@ -77,6 +77,10 @@
</button> </button>
</template> </template>
<template v-else-if="fieldConfig.type === 'content'">
<label :for="fieldConfig.id">{{ fieldConfig.label }}</label>
<textarea v-model="values[fieldConfig.id].text" :name="fieldConfig.id" :id="fieldConfig.id" rows="3"></textarea>
</template>
<template v-else-if="fieldConfig.type === 'attachment'"> <template v-else-if="fieldConfig.type === 'attachment'">
<label :for="fieldConfig.id">{{ fieldConfig.label }}</label> <label :for="fieldConfig.id">{{ fieldConfig.label }}</label>
<attachment-input <attachment-input
@ -100,8 +104,8 @@
<translate translate-context="Content/Library/Button.Label">Clear</translate> <translate translate-context="Content/Library/Button.Label">Clear</translate>
</button> </button>
</template> </template>
<div v-if="values[fieldConfig.id] != initialValues[fieldConfig.id]"> <div v-if="!lodash.isEqual(values[fieldConfig.id], initialValues[fieldConfig.id])">
<button class="ui tiny basic right floated reset button" form="noop" @click.prevent="values[fieldConfig.id] = initialValues[fieldConfig.id]"> <button class="ui tiny basic right floated reset button" form="noop" @click.prevent="values[fieldConfig.id] = lodash.clone(initialValues[fieldConfig.id])">
<i class="undo icon"></i> <i class="undo icon"></i>
<translate translate-context="Content/Library/Button.Label">Reset to initial value</translate> <translate translate-context="Content/Library/Button.Label">Reset to initial value</translate>
</button> </button>
@ -156,6 +160,7 @@ export default {
summary: '', summary: '',
submittedMutation: null, submittedMutation: null,
showPendingReview: true, showPendingReview: true,
lodash,
} }
}, },
created () { created () {
@ -216,8 +221,8 @@ export default {
setValues () { setValues () {
let self = this let self = this
this.config.fields.forEach(f => { this.config.fields.forEach(f => {
self.$set(self.values, f.id, f.getValue(self.object)) self.$set(self.values, f.id, lodash.clone(f.getValue(self.object)))
self.$set(self.initialValues, f.id, self.values[f.id]) self.$set(self.initialValues, f.id, lodash.clone(self.values[f.id]))
}) })
}, },
submit() { submit() {

View File

@ -5,8 +5,20 @@ function getTagsValueRepr (val) {
return val.slice().sort().join('\n') return val.slice().sort().join('\n')
} }
function getContentValueRepr (val) {
return val.text
}
export default { export default {
getConfigs () { getConfigs () {
const description = {
id: 'description',
type: 'content',
required: true,
label: this.$pgettext('*/*/*/Noun', 'Description'),
getValue: (obj) => { return obj.description || {text: null, content_type: 'text/markdown'}},
getValueRepr: getContentValueRepr
}
return { return {
artist: { artist: {
fields: [ fields: [
@ -17,6 +29,7 @@ export default {
label: this.$pgettext('*/*/*/Noun', 'Name'), label: this.$pgettext('*/*/*/Noun', 'Name'),
getValue: (obj) => { return obj.name } getValue: (obj) => { return obj.name }
}, },
description,
{ {
id: 'tags', id: 'tags',
type: 'tags', type: 'tags',
@ -24,7 +37,7 @@ export default {
label: this.$pgettext('*/*/*/Noun', 'Tags'), label: this.$pgettext('*/*/*/Noun', 'Tags'),
getValue: (obj) => { return obj.tags }, getValue: (obj) => { return obj.tags },
getValueRepr: getTagsValueRepr getValueRepr: getTagsValueRepr
} },
] ]
}, },
album: { album: {
@ -36,6 +49,7 @@ export default {
label: this.$pgettext('*/*/*/Noun', 'Title'), label: this.$pgettext('*/*/*/Noun', 'Title'),
getValue: (obj) => { return obj.title } getValue: (obj) => { return obj.title }
}, },
description,
{ {
id: 'release_date', id: 'release_date',
type: 'text', type: 'text',
@ -75,6 +89,7 @@ export default {
label: this.$pgettext('*/*/*/Noun', 'Title'), label: this.$pgettext('*/*/*/Noun', 'Title'),
getValue: (obj) => { return obj.title } getValue: (obj) => { return obj.title }
}, },
description,
{ {
id: 'position', id: 'position',
type: 'text', type: 'text',

View File

@ -129,6 +129,12 @@
{{ object.domain }} {{ object.domain }}
</td> </td>
</tr> </tr>
<tr v-if="object.description">
<td>
<translate translate-context="'*/*/*/Noun">Description</translate>
</td>
<td v-html="object.description.html"></td>
</tr>
</tbody> </tbody>
</table> </table>
</section> </section>

View File

@ -117,6 +117,12 @@
{{ object.domain }} {{ object.domain }}
</td> </td>
</tr> </tr>
<tr v-if="object.description">
<td>
<translate translate-context="'*/*/*/Noun">Description</translate>
</td>
<td v-html="object.description.html"></td>
</tr>
</tbody> </tbody>
</table> </table>
</section> </section>

View File

@ -181,6 +181,12 @@
{{ object.domain }} {{ object.domain }}
</td> </td>
</tr> </tr>
<tr v-if="object.description">
<td>
<translate translate-context="'*/*/*/Noun">Description</translate>
</td>
<td v-html="object.description.html"></td>
</tr>
</tbody> </tbody>
</table> </table>
</section> </section>