Newer
Older
import logging
Eliot Berriot
committed
import uuid
from django.core.paginator import Paginator
from django.db import transaction
from rest_framework import serializers
from funkwhale_api.common import utils as common_utils
from funkwhale_api.common import models as common_models
from funkwhale_api.music import models as music_models
from funkwhale_api.tags import models as tags_models
from . import activity, actors, contexts, jsonld, models, tasks, utils
logger = logging.getLogger(__name__)
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 MediaSerializer(jsonld.JsonLdSerializer):
mediaType = serializers.CharField()
def __init__(self, *args, **kwargs):
self.allowed_mimetypes = kwargs.pop("allowed_mimetypes", [])
super().__init__(*args, **kwargs)
def validate_mediaType(self, v):
if not self.allowed_mimetypes:
# no restrictions
return v
for mt in self.allowed_mimetypes:
if mt.endswith("/*"):
if v.startswith(mt.replace("*", "")):
return v
else:
if v == mt:
return v
raise serializers.ValidationError(
"Invalid mimetype {}. Allowed: {}".format(v, self.allowed_mimetypes)
)
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
class LinkSerializer(MediaSerializer):
type = serializers.ChoiceField(choices=[contexts.AS.Link])
href = serializers.URLField(max_length=500)
class Meta:
jsonld_mapping = {
"href": jsonld.first_id(contexts.AS.href),
"mediaType": jsonld.first_val(contexts.AS.mediaType),
}
class ImageSerializer(MediaSerializer):
type = serializers.ChoiceField(choices=[contexts.AS.Image, contexts.AS.Link])
href = serializers.URLField(max_length=500, required=False)
url = serializers.URLField(max_length=500, required=False)
class Meta:
jsonld_mapping = {
"url": jsonld.first_id(contexts.AS.url),
"href": jsonld.first_id(contexts.AS.href),
"mediaType": jsonld.first_val(contexts.AS.mediaType),
}
def validate(self, data):
validated_data = super().validate(data)
if "url" not in validated_data:
try:
validated_data["url"] = validated_data["href"]
except KeyError:
if self.required:
raise serializers.ValidationError(
"You need to provide a url or href"
)
return validated_data
class URLSerializer(jsonld.JsonLdSerializer):
href = serializers.URLField(max_length=500)
mediaType = serializers.CharField(required=False)
class Meta:
jsonld_mapping = {
"href": jsonld.first_id(contexts.AS.href, aliases=[jsonld.raw("@id")]),
"mediaType": jsonld.first_val(contexts.AS.mediaType),
}
class EndpointsSerializer(jsonld.JsonLdSerializer):
sharedInbox = serializers.URLField(max_length=500, required=False)
class Meta:
jsonld_mapping = {"sharedInbox": jsonld.first_id(contexts.AS.sharedInbox)}
class PublicKeySerializer(jsonld.JsonLdSerializer):
publicKeyPem = serializers.CharField(trim_whitespace=False)
class Meta:
jsonld_mapping = {"publicKeyPem": jsonld.first_val(contexts.SEC.publicKeyPem)}
def get_by_media_type(urls, media_type):
for url in urls:
if url.get("mediaType", "text/html") == media_type:
return url
class ActorSerializer(jsonld.JsonLdSerializer):
id = serializers.URLField(max_length=500)
outbox = serializers.URLField(max_length=500, required=False)
inbox = serializers.URLField(max_length=500, required=False)
url = serializers.ListField(
child=URLSerializer(jsonld_expand=False), required=False, min_length=0
)
type = serializers.ChoiceField(
choices=[getattr(contexts.AS, c[0]) for c in models.TYPE_CHOICES]
)
preferredUsername = serializers.CharField()
manuallyApprovesFollowers = serializers.NullBooleanField(required=False)
name = serializers.CharField(
required=False, max_length=200, allow_blank=True, allow_null=True
)
summary = TruncatedCharField(
truncate_length=common_models.CONTENT_TEXT_MAX_LENGTH,
required=False,
allow_null=True,
)
followers = serializers.URLField(max_length=500, required=False)
following = serializers.URLField(max_length=500, required=False, allow_null=True)
publicKey = PublicKeySerializer(required=False)
endpoints = EndpointsSerializer(required=False)
allowed_mimetypes=["image/*"], allow_null=True, required=False
)
class Meta:
# not strictly necessary because it's not a model serializer
# but used by tasks.py/fetch
model = models.Actor
jsonld_mapping = {
"outbox": jsonld.first_id(contexts.AS.outbox),
"inbox": jsonld.first_id(contexts.LDP.inbox),
"following": jsonld.first_id(contexts.AS.following),
"followers": jsonld.first_id(contexts.AS.followers),
"preferredUsername": jsonld.first_val(contexts.AS.preferredUsername),
"summary": jsonld.first_val(contexts.AS.summary),
"name": jsonld.first_val(contexts.AS.name),
"publicKey": jsonld.first_obj(contexts.SEC.publicKey),
"manuallyApprovesFollowers": jsonld.first_val(
contexts.AS.manuallyApprovesFollowers
),
"mediaType": jsonld.first_val(contexts.AS.mediaType),
"endpoints": jsonld.first_obj(contexts.AS.endpoints),
"url": jsonld.raw(contexts.AS.url),
def to_representation(self, instance):
"outbox": instance.outbox_url,
"inbox": instance.inbox_url,
"preferredUsername": instance.preferred_username,
"type": instance.type,
}
if instance.name:
if instance.followers_url:
if instance.following_url:
if instance.manually_approves_followers is not None:
ret["manuallyApprovesFollowers"] = instance.manually_approves_followers
if instance.summary_obj_id:
ret["summary"] = instance.summary_obj.rendered
urls = []
if instance.url:
urls.append(
{"type": "Link", "href": instance.url, "mediaType": "text/html"}
)
channel = instance.get_channel()
if channel:
ret["url"] = [
{
"type": "Link",
"href": instance.channel.get_absolute_url()
if instance.channel.artist.is_local
else instance.get_absolute_url(),
"mediaType": "text/html",
},
{
"type": "Link",
"href": instance.channel.get_rss_url(),
"mediaType": "application/rss+xml",
},
]
include_image(ret, channel.artist.attachment_cover, "icon")
if channel.artist.description_id:
ret["summary"] = channel.artist.description.rendered
else:
ret["url"] = [
{
"type": "Link",
"href": instance.get_absolute_url(),
"mediaType": "text/html",
}
]
include_image(ret, instance.attachment_icon, "icon")
"id": "{}#main-key".format(instance.fid),
ret["endpoints"]["sharedInbox"] = instance.shared_inbox_url
return ret
def prepare_missing_fields(self):
"fid": self.validated_data["id"],
"outbox_url": self.validated_data.get("outbox"),
"inbox_url": self.validated_data.get("inbox"),
"following_url": self.validated_data.get("following"),
"followers_url": self.validated_data.get("followers"),
"type": self.validated_data["type"],
"name": self.validated_data.get("name"),
"preferred_username": self.validated_data["preferredUsername"],
url = get_by_media_type(self.validated_data.get("url", []), "text/html")
if url:
kwargs["url"] = url["href"]
maf = self.validated_data.get("manuallyApprovesFollowers")
domain = urllib.parse.urlparse(kwargs["fid"]).netloc
domain, domain_created = models.Domain.objects.get_or_create(pk=domain)
if domain_created and not domain.is_local:
# first time we see the domain, we trigger nodeinfo fetching
tasks.update_domain_nodeinfo(domain_name=domain.name)
kwargs["domain"] = domain
for endpoint, url in self.validated_data.get("endpoints", {}).items():
if endpoint == "sharedInbox":
kwargs["shared_inbox_url"] = url
kwargs["public_key"] = self.validated_data["publicKey"]["publicKeyPem"]
except KeyError:
pass
return kwargs
def validate_type(self, v):
return v.split("#")[-1]
d = self.prepare_missing_fields()
return models.Actor(**d)
d = self.prepare_missing_fields()
d.update(kwargs)
actor = models.Actor.objects.update_or_create(fid=d["fid"], defaults=d)[0]
common_utils.attach_content(
actor, "summary_obj", self.validated_data["summary"]
)
if "icon" in self.validated_data:
new_value = self.validated_data["icon"]
common_utils.attach_file(
actor,
"attachment_icon",
{"url": new_value["url"], "mimetype": new_value["mediaType"]}
def validate(self, data):
validated_data = super().validate(data)
if "summary" in data:
validated_data["summary"] = {
"content_type": "text/html",
"text": data["summary"],
}
else:
validated_data["summary"] = None
return validated_data
class APIActorSerializer(serializers.ModelSerializer):
class Meta:
model = models.Actor
fields = [
"url",
"creation_date",
"summary",
"preferred_username",
"name",
"last_fetch_date",
"domain",
"type",
"manually_approves_followers",
class BaseActivitySerializer(serializers.Serializer):
id = serializers.URLField(max_length=500, required=False)
type = serializers.CharField(max_length=100)
actor = serializers.URLField(max_length=500)
def validate_actor(self, v):
expected = self.context.get("actor")
if expected and expected.fid != v:
raise serializers.ValidationError("Invalid actor")
if expected:
# avoid a DB lookup
return expected
return models.Actor.objects.get(fid=v)
except models.Actor.DoesNotExist:
raise serializers.ValidationError("Actor not found")
def create(self, validated_data):
return models.Activity.objects.create(
fid=validated_data.get("id"),
actor=validated_data["actor"],
payload=self.initial_data,
def validate(self, data):
data["recipients"] = self.validate_recipients(self.initial_data)
return super().validate(data)
def validate_recipients(self, payload):
"""
Ensure we have at least a to/cc field with valid actors
"""
to = payload.get("to", [])
cc = payload.get("cc", [])
if not to and not cc and not self.context.get("recipients"):
raise serializers.ValidationError(
"We cannot handle an activity with no recipient"
Eliot Berriot
committed
class FollowSerializer(serializers.Serializer):
id = serializers.URLField(max_length=500)
object = serializers.URLField(max_length=500)
actor = serializers.URLField(max_length=500)
Eliot Berriot
committed
def validate_object(self, v):
if self.parent:
# it's probably an accept, so everything is inverted, the actor
# the recipient does not matter
recipient = None
else:
recipient = self.context.get("recipient")
if expected and expected.fid != v:
Eliot Berriot
committed
try:
obj = models.Actor.objects.get(fid=v)
if recipient and recipient.fid != obj.fid:
raise serializers.ValidationError("Invalid target")
return obj
Eliot Berriot
committed
except models.Actor.DoesNotExist:
pass
try:
qs = music_models.Library.objects.filter(fid=v)
if recipient:
qs = qs.filter(actor=recipient)
return qs.get()
except music_models.Library.DoesNotExist:
pass
raise serializers.ValidationError("Target not found")
Eliot Berriot
committed
def validate_actor(self, v):
if expected and expected.fid != v:
Eliot Berriot
committed
try:
return models.Actor.objects.get(fid=v)
Eliot Berriot
committed
except models.Actor.DoesNotExist:
Eliot Berriot
committed
def save(self, **kwargs):
target = self.validated_data["object"]
if target._meta.label == "music.Library":
follow_class = models.LibraryFollow
else:
follow_class = models.Follow
defaults = kwargs
defaults["fid"] = self.validated_data["id"]
Eliot Berriot
committed
approved = kwargs.pop("approved", None)
follow, created = follow_class.objects.update_or_create(
actor=self.validated_data["actor"],
target=self.validated_data["object"],
Eliot Berriot
committed
)
if not created:
# We likely received a new follow when we had an existing one in database
# this can happen when two instances are out of sync, e.g because some
# messages are not delivered properly. In this case, we don't change
# the follow approved status and return the follow as is.
# We set a new UUID to ensure the follow urls are updated properly
# cf #830
follow.uuid = uuid.uuid4()
follow.save(update_fields=["uuid"])
return follow
# it's a brand new follow, we use the approved value stored earlier
if approved != follow.approved:
follow.approved = approved
follow.save(update_fields=["approved"])
return follow
Eliot Berriot
committed
return {
"actor": instance.actor.fid,
"id": instance.get_federation_id(),
"object": instance.target.fid,
Eliot Berriot
committed
}
class APIFollowSerializer(serializers.ModelSerializer):
actor = APIActorSerializer()
target = APIActorSerializer()
class Meta:
model = models.Follow
fields = [
"uuid",
"id",
"approved",
"creation_date",
"modification_date",
"actor",
"target",
Eliot Berriot
committed
class AcceptFollowSerializer(serializers.Serializer):
id = serializers.URLField(max_length=500, required=False)
actor = serializers.URLField(max_length=500)
Eliot Berriot
committed
object = FollowSerializer()
Eliot Berriot
committed
def validate_actor(self, v):
expected = self.context.get("actor")
if expected and expected.fid != v:
Eliot Berriot
committed
try:
return models.Actor.objects.get(fid=v)
Eliot Berriot
committed
except models.Actor.DoesNotExist:
Eliot Berriot
committed
def validate(self, validated_data):
# we ensure the accept actor actually match the follow target / library owner
target = validated_data["object"]["object"]
if target._meta.label == "music.Library":
expected = target.actor
follow_class = models.LibraryFollow
else:
expected = target
follow_class = models.Follow
if validated_data["actor"] != expected:
Eliot Berriot
committed
try:
follow_class.objects.filter(
target=target, actor=validated_data["object"]["actor"]
except follow_class.DoesNotExist:
raise serializers.ValidationError("No follow to accept")
Eliot Berriot
committed
return validated_data
def to_representation(self, instance):
if instance.target._meta.label == "music.Library":
actor = instance.target.actor
else:
actor = instance.target
Eliot Berriot
committed
return {
"id": instance.get_federation_id() + "/accept",
Eliot Berriot
committed
"type": "Accept",
Eliot Berriot
committed
}
def save(self):
follow = self.validated_data["follow"]
follow.approved = True
follow.save()
if follow.target._meta.label == "music.Library":
Eliot Berriot
committed
class UndoFollowSerializer(serializers.Serializer):
id = serializers.URLField(max_length=500)
actor = serializers.URLField(max_length=500)
Eliot Berriot
committed
object = FollowSerializer()
Eliot Berriot
committed
def validate_actor(self, v):
if expected and expected.fid != v:
Eliot Berriot
committed
try:
return models.Actor.objects.get(fid=v)
Eliot Berriot
committed
except models.Actor.DoesNotExist:
Eliot Berriot
committed
def validate(self, validated_data):
# we ensure the accept actor actually match the follow actor
if validated_data["actor"] != validated_data["object"]["actor"]:
raise serializers.ValidationError("Actor mismatch")
target = validated_data["object"]["object"]
if target._meta.label == "music.Library":
follow_class = models.LibraryFollow
else:
follow_class = models.Follow
Eliot Berriot
committed
try:
validated_data["follow"] = follow_class.objects.filter(
actor=validated_data["actor"], target=target
Eliot Berriot
committed
).get()
raise serializers.ValidationError("No follow to remove")
Eliot Berriot
committed
return validated_data
def to_representation(self, instance):
return {
"id": instance.get_federation_id() + "/undo",
Eliot Berriot
committed
"type": "Undo",
"actor": instance.actor.fid,
Eliot Berriot
committed
}
def save(self):
Eliot Berriot
committed
class ActorWebfingerSerializer(serializers.Serializer):
subject = serializers.CharField()
aliases = serializers.ListField(child=serializers.URLField(max_length=500))
actor_url = serializers.URLField(max_length=500, required=False)
validated_data["actor_url"] = None
for l in validated_data["links"]:
if validated_data["actor_url"] is None:
raise serializers.ValidationError("No valid actor url found")
def to_representation(self, instance):
data = {}
data["subject"] = "acct:{}".format(instance.webfinger_subject)
data["links"] = [
{"rel": "self", "href": instance.fid, "type": "application/activity+json"}
data["aliases"] = [instance.fid]
class ActivitySerializer(serializers.Serializer):
actor = serializers.URLField(max_length=500)
id = serializers.URLField(max_length=500, required=False)
type = serializers.ChoiceField(choices=[(c, c) for c in activity.ACTIVITY_TYPES])
object = serializers.JSONField(required=False)
target = serializers.JSONField(required=False)
def validate_object(self, value):
try:
raise serializers.ValidationError("Missing object type")
except TypeError:
# probably a URL
return value
try:
object_serializer = OBJECT_SERIALIZERS[type]
except KeyError:
raise serializers.ValidationError("Unsupported type {}".format(type))
serializer = object_serializer(data=value)
serializer.is_valid(raise_exception=True)
return serializer.data
def validate_actor(self, value):
if request_actor and request_actor.fid != value:
raise serializers.ValidationError(
"The actor making the request do not match" " the activity actor"
)
return value
def to_representation(self, conf):
d = {}
d.update(conf)
return d
class ObjectSerializer(serializers.Serializer):
id = serializers.URLField(max_length=500)
url = serializers.URLField(max_length=500, required=False, allow_null=True)
type = serializers.ChoiceField(choices=[(c, c) for c in activity.OBJECT_TYPES])
content = serializers.CharField(required=False, allow_null=True)
summary = serializers.CharField(required=False, allow_null=True)
name = serializers.CharField(required=False, allow_null=True)
published = serializers.DateTimeField(required=False, allow_null=True)
updated = serializers.DateTimeField(required=False, allow_null=True)
to = serializers.ListField(
child=serializers.URLField(max_length=500), required=False, allow_null=True
)
cc = serializers.ListField(
child=serializers.URLField(max_length=500), required=False, allow_null=True
)
bto = serializers.ListField(
child=serializers.URLField(max_length=500), required=False, allow_null=True
)
bcc = serializers.ListField(
child=serializers.URLField(max_length=500), required=False, allow_null=True
)
OBJECT_SERIALIZERS = {t: ObjectSerializer for t in activity.OBJECT_TYPES}
def get_additional_fields(data):
UNSET = object()
additional_fields = {}
for field in ["name", "summary"]:
v = data.get(field, UNSET)
if v == UNSET:
continue
additional_fields[field] = v
return additional_fields
PAGINATED_COLLECTION_JSONLD_MAPPING = {
"totalItems": jsonld.first_val(contexts.AS.totalItems),
"actor": jsonld.first_id(contexts.AS.actor),
"attributedTo": jsonld.first_id(contexts.AS.attributedTo),
"first": jsonld.first_id(contexts.AS.first),
"last": jsonld.first_id(contexts.AS.last),
"partOf": jsonld.first_id(contexts.AS.partOf),
}
class PaginatedCollectionSerializer(jsonld.JsonLdSerializer):
type = serializers.ChoiceField(choices=[contexts.AS.Collection])
totalItems = serializers.IntegerField(min_value=0)
actor = serializers.URLField(max_length=500, required=False)
attributedTo = serializers.URLField(max_length=500, required=False)
id = serializers.URLField(max_length=500)
first = serializers.URLField(max_length=500)
last = serializers.URLField(max_length=500)
class Meta:
jsonld_mapping = PAGINATED_COLLECTION_JSONLD_MAPPING
def validate(self, validated_data):
d = super().validate(validated_data)
actor = d.get("actor")
attributed_to = d.get("attributedTo")
if not actor and not attributed_to:
raise serializers.ValidationError(
"You need to provide at least actor or attributedTo"
)
d["attributedTo"] = attributed_to or actor
return d
def to_representation(self, conf):
paginator = Paginator(conf["items"], conf.get("page_size", 20))
first = common_utils.set_query_parameter(conf["id"], page=1)
last = common_utils.set_query_parameter(conf["id"], page=paginator.num_pages)
# XXX Stable release: remove the obsolete actor field
"actor": conf["actor"].fid,
"attributedTo": conf["actor"].fid,
"type": conf.get("type", "Collection"),
"current": current,
"first": first,
"last": last,
d.update(get_additional_fields(conf))
class LibrarySerializer(PaginatedCollectionSerializer):
type = serializers.ChoiceField(
choices=[contexts.AS.Collection, contexts.FW.Library]
)
name = serializers.CharField()
summary = serializers.CharField(allow_blank=True, allow_null=True, required=False)
audience = serializers.ChoiceField(
choices=["", "./", None, "https://www.w3.org/ns/activitystreams#Public"],
required=False,
allow_null=True,
allow_blank=True,
)
class Meta:
# not strictly necessary because it's not a model serializer
# but used by tasks.py/fetch
model = music_models.Library
jsonld_mapping = common_utils.concat_dicts(
PAGINATED_COLLECTION_JSONLD_MAPPING,
{
"name": jsonld.first_val(contexts.AS.name),
"summary": jsonld.first_val(contexts.AS.summary),
"audience": jsonld.first_id(contexts.AS.audience),
"followers": jsonld.first_id(contexts.AS.followers),
},
)
def to_representation(self, library):
conf = {
"id": library.fid,
"name": library.name,
"summary": library.description,
"page_size": 100,
# XXX Stable release: remove the obsolete actor field
"attributedTo": library.actor,
"type": "Library",
}
r = super().to_representation(conf)
r["audience"] = (
contexts.AS.Public if library.privacy_level == "everyone" else ""
return r
def create(self, validated_data):
if self.instance:
actor = self.instance.actor
else:
actor = utils.retrieve_ap_object(
validated_data["attributedTo"],
actor=self.context.get("fetch_actor"),
queryset=models.Actor,
serializer_class=ActorSerializer,
)
privacy = {"": "me", "./": "me", None: "me", contexts.AS.Public: "everyone"}
library, created = music_models.Library.objects.update_or_create(
fid=validated_data["id"],
actor=actor,
defaults={
"name": validated_data["name"],
"description": validated_data.get("summary"),
"privacy_level": privacy[validated_data["audience"]],
},
)
return library
def update(self, instance, validated_data):
return self.create(validated_data)
class CollectionPageSerializer(jsonld.JsonLdSerializer):
type = serializers.ChoiceField(choices=[contexts.AS.CollectionPage])
totalItems = serializers.IntegerField(min_value=0)
items = serializers.ListField()
actor = serializers.URLField(max_length=500, required=False)
attributedTo = serializers.URLField(max_length=500, required=False)
id = serializers.URLField(max_length=500)
first = serializers.URLField(max_length=500)
last = serializers.URLField(max_length=500)
next = serializers.URLField(max_length=500, required=False)
prev = serializers.URLField(max_length=500, required=False)
partOf = serializers.URLField(max_length=500)
class Meta:
jsonld_mapping = {
"totalItems": jsonld.first_val(contexts.AS.totalItems),
"items": jsonld.raw(contexts.AS.items),
"actor": jsonld.first_id(contexts.AS.actor),
"attributedTo": jsonld.first_id(contexts.AS.attributedTo),
"first": jsonld.first_id(contexts.AS.first),
"last": jsonld.first_id(contexts.AS.last),
"next": jsonld.first_id(contexts.AS.next),
"partOf": jsonld.first_id(contexts.AS.partOf),
}
if not item_serializer:
return v
raw_items = [item_serializer(data=i, context=self.context) for i in v]
valid_items = []
valid_items.append(i)
return valid_items
def to_representation(self, conf):
first = common_utils.set_query_parameter(conf["id"], page=1)
last = common_utils.set_query_parameter(
id = common_utils.set_query_parameter(conf["id"], page=page.number)
# XXX Stable release: remove the obsolete actor field
"actor": conf["actor"].fid,
"attributedTo": conf["actor"].fid,
"totalItems": page.paginator.count,
"type": "CollectionPage",
"first": first,
"last": last,
"items": [
conf["item_serializer"](
i, context={"actor": conf["actor"], "include_ap_context": False}
).data
for i in page.object_list
d["prev"] = common_utils.set_query_parameter(
d["next"] = common_utils.set_query_parameter(
d.update(get_additional_fields(conf))
MUSIC_ENTITY_JSONLD_MAPPING = {
"name": jsonld.first_val(contexts.AS.name),
"published": jsonld.first_val(contexts.AS.published),
"musicbrainzId": jsonld.first_val(contexts.FW.musicbrainzId),
"attributedTo": jsonld.first_id(contexts.AS.attributedTo),
"tags": jsonld.raw(contexts.AS.tag),
"mediaType": jsonld.first_val(contexts.AS.mediaType),
"content": jsonld.first_val(contexts.AS.content),
class TagSerializer(jsonld.JsonLdSerializer):
type = serializers.ChoiceField(choices=[contexts.AS.Hashtag])
name = serializers.CharField(max_length=100)
class Meta:
jsonld_mapping = {"name": jsonld.first_val(contexts.AS.name)}
def validate_name(self, value):
if value.startswith("#"):
# remove trailing #
value = value[1:]
return value
def repr_tag(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"
def include_image(repr, attachment, field="image"):
"url": attachment.download_url_original,
"mediaType": attachment.mimetype or "image/jpeg",
}
else:
class MusicEntitySerializer(jsonld.JsonLdSerializer):
id = serializers.URLField(max_length=500)
published = serializers.DateTimeField()
musicbrainzId = serializers.UUIDField(allow_null=True, required=False)
name = serializers.CharField(max_length=1000)
attributedTo = serializers.URLField(max_length=500, allow_null=True, required=False)
updateable_fields = []
tags = serializers.ListField(
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,
)
return self.update_or_create(validated_data)
@transaction.atomic
def update_or_create(self, validated_data):
instance = self.instance or self.Meta.model(fid=validated_data["id"])
creating = instance.pk is None
attributed_to_fid = validated_data.get("attributedTo")
if attributed_to_fid:
validated_data["attributedTo"] = actors.get_actor(attributed_to_fid)
updated_fields = common_utils.get_updated_fields(
updated_fields = self.validate_updated_data(instance, updated_fields)
if creating:
instance, created = self.Meta.model.objects.get_or_create(