Merge branch 'actor-description' into 'develop'
Actor description/summary See merge request funkwhale/funkwhale!1002
This commit is contained in:
commit
b86971c305
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 2.2.9 on 2020-01-22 11:01
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('common', '0007_auto_20200116_1610'),
|
||||||
|
('federation', '0022_auto_20191204_1539'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='actor',
|
||||||
|
name='summary_obj',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.Content'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -189,6 +189,9 @@ class Actor(models.Model):
|
||||||
name = models.CharField(max_length=200, null=True, blank=True)
|
name = models.CharField(max_length=200, null=True, blank=True)
|
||||||
domain = models.ForeignKey(Domain, on_delete=models.CASCADE, related_name="actors")
|
domain = models.ForeignKey(Domain, on_delete=models.CASCADE, related_name="actors")
|
||||||
summary = models.CharField(max_length=500, null=True, blank=True)
|
summary = models.CharField(max_length=500, null=True, blank=True)
|
||||||
|
summary_obj = models.ForeignKey(
|
||||||
|
"common.Content", null=True, blank=True, on_delete=models.SET_NULL
|
||||||
|
)
|
||||||
preferred_username = models.CharField(max_length=200, null=True, blank=True)
|
preferred_username = models.CharField(max_length=200, null=True, blank=True)
|
||||||
public_key = models.TextField(max_length=5000, null=True, blank=True)
|
public_key = models.TextField(max_length=5000, null=True, blank=True)
|
||||||
private_key = models.TextField(max_length=5000, null=True, blank=True)
|
private_key = models.TextField(max_length=5000, null=True, blank=True)
|
||||||
|
|
|
@ -21,6 +21,18 @@ from . import activity, actors, contexts, jsonld, models, tasks, utils
|
||||||
logger = logging.getLogger(__name__)
|
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 LinkSerializer(jsonld.JsonLdSerializer):
|
class LinkSerializer(jsonld.JsonLdSerializer):
|
||||||
type = serializers.ChoiceField(choices=[contexts.AS.Link, contexts.AS.Image])
|
type = serializers.ChoiceField(choices=[contexts.AS.Link, contexts.AS.Image])
|
||||||
href = serializers.URLField(max_length=500)
|
href = serializers.URLField(max_length=500)
|
||||||
|
@ -76,7 +88,11 @@ class ActorSerializer(jsonld.JsonLdSerializer):
|
||||||
preferredUsername = serializers.CharField()
|
preferredUsername = serializers.CharField()
|
||||||
manuallyApprovesFollowers = serializers.NullBooleanField(required=False)
|
manuallyApprovesFollowers = serializers.NullBooleanField(required=False)
|
||||||
name = serializers.CharField(required=False, max_length=200)
|
name = serializers.CharField(required=False, max_length=200)
|
||||||
summary = serializers.CharField(max_length=None, required=False)
|
summary = TruncatedCharField(
|
||||||
|
truncate_length=common_models.CONTENT_TEXT_MAX_LENGTH,
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
)
|
||||||
followers = serializers.URLField(max_length=500, required=False)
|
followers = serializers.URLField(max_length=500, required=False)
|
||||||
following = serializers.URLField(max_length=500, required=False, allow_null=True)
|
following = serializers.URLField(max_length=500, required=False, allow_null=True)
|
||||||
publicKey = PublicKeySerializer(required=False)
|
publicKey = PublicKeySerializer(required=False)
|
||||||
|
@ -113,11 +129,12 @@ class ActorSerializer(jsonld.JsonLdSerializer):
|
||||||
ret["followers"] = instance.followers_url
|
ret["followers"] = instance.followers_url
|
||||||
if instance.following_url:
|
if instance.following_url:
|
||||||
ret["following"] = instance.following_url
|
ret["following"] = instance.following_url
|
||||||
if instance.summary:
|
|
||||||
ret["summary"] = instance.summary
|
|
||||||
if instance.manually_approves_followers is not None:
|
if instance.manually_approves_followers is not None:
|
||||||
ret["manuallyApprovesFollowers"] = instance.manually_approves_followers
|
ret["manuallyApprovesFollowers"] = instance.manually_approves_followers
|
||||||
|
|
||||||
|
if instance.summary_obj_id:
|
||||||
|
ret["summary"] = instance.summary_obj.rendered
|
||||||
|
|
||||||
ret["@context"] = jsonld.get_default_context()
|
ret["@context"] = jsonld.get_default_context()
|
||||||
if instance.public_key:
|
if instance.public_key:
|
||||||
ret["publicKey"] = {
|
ret["publicKey"] = {
|
||||||
|
@ -146,7 +163,6 @@ class ActorSerializer(jsonld.JsonLdSerializer):
|
||||||
"inbox_url": self.validated_data.get("inbox"),
|
"inbox_url": self.validated_data.get("inbox"),
|
||||||
"following_url": self.validated_data.get("following"),
|
"following_url": self.validated_data.get("following"),
|
||||||
"followers_url": self.validated_data.get("followers"),
|
"followers_url": self.validated_data.get("followers"),
|
||||||
"summary": self.validated_data.get("summary"),
|
|
||||||
"type": self.validated_data["type"],
|
"type": self.validated_data["type"],
|
||||||
"name": self.validated_data.get("name"),
|
"name": self.validated_data.get("name"),
|
||||||
"preferred_username": self.validated_data["preferredUsername"],
|
"preferred_username": self.validated_data["preferredUsername"],
|
||||||
|
@ -181,11 +197,22 @@ class ActorSerializer(jsonld.JsonLdSerializer):
|
||||||
def save(self, **kwargs):
|
def save(self, **kwargs):
|
||||||
d = self.prepare_missing_fields()
|
d = self.prepare_missing_fields()
|
||||||
d.update(kwargs)
|
d.update(kwargs)
|
||||||
return models.Actor.objects.update_or_create(fid=d["fid"], defaults=d)[0]
|
actor = models.Actor.objects.update_or_create(fid=d["fid"], defaults=d)[0]
|
||||||
|
common_utils.attach_content(
|
||||||
|
actor, "summary_obj", self.validated_data["summary"]
|
||||||
|
)
|
||||||
|
return actor
|
||||||
|
|
||||||
def validate_summary(self, value):
|
def validate(self, data):
|
||||||
if value:
|
validated_data = super().validate(data)
|
||||||
return value[:500]
|
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 APIActorSerializer(serializers.ModelSerializer):
|
||||||
|
@ -828,18 +855,6 @@ def include_image(repr, attachment):
|
||||||
repr["image"] = None
|
repr["image"] = None
|
||||||
|
|
||||||
|
|
||||||
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()
|
||||||
|
|
|
@ -11,6 +11,7 @@ from versatileimagefield.serializers import VersatileImageFieldSerializer
|
||||||
|
|
||||||
from funkwhale_api.activity import serializers as activity_serializers
|
from funkwhale_api.activity import serializers as activity_serializers
|
||||||
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 models as federation_models
|
from funkwhale_api.federation import models as federation_models
|
||||||
from . import adapters
|
from . import adapters
|
||||||
from . import models
|
from . import models
|
||||||
|
@ -27,6 +28,7 @@ class ASCIIUsernameValidator(validators.RegexValidator):
|
||||||
|
|
||||||
|
|
||||||
username_validators = [ASCIIUsernameValidator()]
|
username_validators = [ASCIIUsernameValidator()]
|
||||||
|
NOOP = object()
|
||||||
|
|
||||||
|
|
||||||
class RegisterSerializer(RS):
|
class RegisterSerializer(RS):
|
||||||
|
@ -106,6 +108,7 @@ class UserBasicSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class UserWriteSerializer(serializers.ModelSerializer):
|
class UserWriteSerializer(serializers.ModelSerializer):
|
||||||
avatar = avatar_field
|
avatar = avatar_field
|
||||||
|
summary = common_serializers.ContentSerializer(required=False, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.User
|
model = models.User
|
||||||
|
@ -115,8 +118,20 @@ class UserWriteSerializer(serializers.ModelSerializer):
|
||||||
"avatar",
|
"avatar",
|
||||||
"instance_support_message_display_date",
|
"instance_support_message_display_date",
|
||||||
"funkwhale_support_message_display_date",
|
"funkwhale_support_message_display_date",
|
||||||
|
"summary",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def update(self, obj, validated_data):
|
||||||
|
if not obj.actor:
|
||||||
|
obj.create_actor()
|
||||||
|
summary = validated_data.pop("summary", NOOP)
|
||||||
|
obj = super().update(obj, validated_data)
|
||||||
|
|
||||||
|
if summary != NOOP:
|
||||||
|
common_utils.attach_content(obj.actor, "summary_obj", summary)
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
class UserReadSerializer(serializers.ModelSerializer):
|
class UserReadSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
@ -150,17 +165,24 @@ class UserReadSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class MeSerializer(UserReadSerializer):
|
class MeSerializer(UserReadSerializer):
|
||||||
quota_status = serializers.SerializerMethodField()
|
quota_status = serializers.SerializerMethodField()
|
||||||
|
summary = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta(UserReadSerializer.Meta):
|
class Meta(UserReadSerializer.Meta):
|
||||||
fields = UserReadSerializer.Meta.fields + [
|
fields = UserReadSerializer.Meta.fields + [
|
||||||
"quota_status",
|
"quota_status",
|
||||||
"instance_support_message_display_date",
|
"instance_support_message_display_date",
|
||||||
"funkwhale_support_message_display_date",
|
"funkwhale_support_message_display_date",
|
||||||
|
"summary",
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_quota_status(self, o):
|
def get_quota_status(self, o):
|
||||||
return o.get_quota_status() if o.actor else 0
|
return o.get_quota_status() if o.actor else 0
|
||||||
|
|
||||||
|
def get_summary(self, o):
|
||||||
|
if not o.actor or not o.actor.summary_obj:
|
||||||
|
return
|
||||||
|
return common_serializers.ContentSerializer(o.actor.summary_obj).data
|
||||||
|
|
||||||
|
|
||||||
class PasswordResetSerializer(PRS):
|
class PasswordResetSerializer(PRS):
|
||||||
def get_email_options(self):
|
def get_email_options(self):
|
||||||
|
|
|
@ -53,7 +53,8 @@ def test_actor_serializer_from_ap(db):
|
||||||
assert actor.type == "Person"
|
assert actor.type == "Person"
|
||||||
assert actor.preferred_username == payload["preferredUsername"]
|
assert actor.preferred_username == payload["preferredUsername"]
|
||||||
assert actor.name == payload["name"]
|
assert actor.name == payload["name"]
|
||||||
assert actor.summary == payload["summary"]
|
assert actor.summary_obj.text == payload["summary"]
|
||||||
|
assert actor.summary_obj.content_type == "text/html"
|
||||||
assert actor.fid == actor_url
|
assert actor.fid == actor_url
|
||||||
assert actor.manually_approves_followers is True
|
assert actor.manually_approves_followers is True
|
||||||
assert actor.private_key is None
|
assert actor.private_key is None
|
||||||
|
@ -89,7 +90,7 @@ def test_actor_serializer_only_mandatory_field_from_ap(db):
|
||||||
assert actor.manually_approves_followers is None
|
assert actor.manually_approves_followers is None
|
||||||
|
|
||||||
|
|
||||||
def test_actor_serializer_to_ap():
|
def test_actor_serializer_to_ap(db):
|
||||||
expected = {
|
expected = {
|
||||||
"@context": jsonld.get_default_context(),
|
"@context": jsonld.get_default_context(),
|
||||||
"id": "https://test.federation/user",
|
"id": "https://test.federation/user",
|
||||||
|
@ -100,7 +101,6 @@ def test_actor_serializer_to_ap():
|
||||||
"outbox": "https://test.federation/user/outbox",
|
"outbox": "https://test.federation/user/outbox",
|
||||||
"preferredUsername": "user",
|
"preferredUsername": "user",
|
||||||
"name": "Real User",
|
"name": "Real User",
|
||||||
"summary": "Hello world",
|
|
||||||
"manuallyApprovesFollowers": False,
|
"manuallyApprovesFollowers": False,
|
||||||
"publicKey": {
|
"publicKey": {
|
||||||
"id": "https://test.federation/user#main-key",
|
"id": "https://test.federation/user#main-key",
|
||||||
|
@ -109,7 +109,7 @@ def test_actor_serializer_to_ap():
|
||||||
},
|
},
|
||||||
"endpoints": {"sharedInbox": "https://test.federation/inbox"},
|
"endpoints": {"sharedInbox": "https://test.federation/inbox"},
|
||||||
}
|
}
|
||||||
ac = models.Actor(
|
ac = models.Actor.objects.create(
|
||||||
fid=expected["id"],
|
fid=expected["id"],
|
||||||
inbox_url=expected["inbox"],
|
inbox_url=expected["inbox"],
|
||||||
outbox_url=expected["outbox"],
|
outbox_url=expected["outbox"],
|
||||||
|
@ -119,11 +119,15 @@ def test_actor_serializer_to_ap():
|
||||||
public_key=expected["publicKey"]["publicKeyPem"],
|
public_key=expected["publicKey"]["publicKeyPem"],
|
||||||
preferred_username=expected["preferredUsername"],
|
preferred_username=expected["preferredUsername"],
|
||||||
name=expected["name"],
|
name=expected["name"],
|
||||||
domain=models.Domain(pk="test.federation"),
|
domain=models.Domain.objects.create(pk="test.federation"),
|
||||||
summary=expected["summary"],
|
|
||||||
type="Person",
|
type="Person",
|
||||||
manually_approves_followers=False,
|
manually_approves_followers=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
content = common_utils.attach_content(
|
||||||
|
ac, "summary_obj", {"text": "hello world", "content_type": "text/markdown"}
|
||||||
|
)
|
||||||
|
expected["summary"] = content.rendered
|
||||||
serializer = serializers.ActorSerializer(ac)
|
serializer = serializers.ActorSerializer(ac)
|
||||||
|
|
||||||
assert serializer.data == expected
|
assert serializer.data == expected
|
||||||
|
@ -1127,14 +1131,17 @@ def test_local_actor_serializer_to_ap(factories):
|
||||||
preferred_username=expected["preferredUsername"],
|
preferred_username=expected["preferredUsername"],
|
||||||
name=expected["name"],
|
name=expected["name"],
|
||||||
domain=models.Domain.objects.create(pk="test.federation"),
|
domain=models.Domain.objects.create(pk="test.federation"),
|
||||||
summary=expected["summary"],
|
|
||||||
type="Person",
|
type="Person",
|
||||||
manually_approves_followers=False,
|
manually_approves_followers=False,
|
||||||
)
|
)
|
||||||
|
content = common_utils.attach_content(
|
||||||
|
ac, "summary_obj", {"text": "hello world", "content_type": "text/markdown"}
|
||||||
|
)
|
||||||
user = factories["users.User"]()
|
user = factories["users.User"]()
|
||||||
user.actor = ac
|
user.actor = ac
|
||||||
user.save()
|
user.save()
|
||||||
ac.refresh_from_db()
|
ac.refresh_from_db()
|
||||||
|
expected["summary"] = content.rendered
|
||||||
expected["icon"] = {
|
expected["icon"] = {
|
||||||
"type": "Image",
|
"type": "Image",
|
||||||
"mediaType": "image/jpeg",
|
"mediaType": "image/jpeg",
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import pytest
|
import pytest
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from funkwhale_api.common import serializers as common_serializers
|
||||||
|
from funkwhale_api.common import utils as common_utils
|
||||||
from funkwhale_api.users.models import User
|
from funkwhale_api.users.models import User
|
||||||
|
|
||||||
|
|
||||||
|
@ -105,7 +107,10 @@ def test_can_fetch_data_from_api(api_client, factories):
|
||||||
# login required
|
# login required
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
|
|
||||||
user = factories["users.User"](permission_library=True)
|
user = factories["users.User"](permission_library=True, with_actor=True)
|
||||||
|
summary = {"content_type": "text/plain", "text": "Hello"}
|
||||||
|
summary_obj = common_utils.attach_content(user.actor, "summary_obj", summary)
|
||||||
|
|
||||||
api_client.login(username=user.username, password="test")
|
api_client.login(username=user.username, password="test")
|
||||||
response = api_client.get(url)
|
response = api_client.get(url)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
@ -115,6 +120,10 @@ def test_can_fetch_data_from_api(api_client, factories):
|
||||||
assert response.data["email"] == user.email
|
assert response.data["email"] == user.email
|
||||||
assert response.data["name"] == user.name
|
assert response.data["name"] == user.name
|
||||||
assert response.data["permissions"] == user.get_permissions()
|
assert response.data["permissions"] == user.get_permissions()
|
||||||
|
assert (
|
||||||
|
response.data["summary"]
|
||||||
|
== common_serializers.ContentSerializer(summary_obj).data
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_can_get_token_via_api(api_client, factories):
|
def test_can_get_token_via_api(api_client, factories):
|
||||||
|
@ -202,6 +211,20 @@ def test_user_can_patch_his_own_settings(logged_in_api_client):
|
||||||
assert user.privacy_level == "me"
|
assert user.privacy_level == "me"
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_can_patch_description(logged_in_api_client):
|
||||||
|
user = logged_in_api_client.user
|
||||||
|
payload = {"summary": {"content_type": "text/markdown", "text": "hello"}}
|
||||||
|
url = reverse("api:v1:users:users-detail", kwargs={"username": user.username})
|
||||||
|
|
||||||
|
response = logged_in_api_client.patch(url, payload, format="json")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
user.refresh_from_db()
|
||||||
|
|
||||||
|
assert user.actor.summary_obj.content_type == payload["summary"]["content_type"]
|
||||||
|
assert user.actor.summary_obj.text == payload["summary"]["text"]
|
||||||
|
|
||||||
|
|
||||||
def test_user_can_request_new_subsonic_token(logged_in_api_client):
|
def test_user_can_request_new_subsonic_token(logged_in_api_client):
|
||||||
user = logged_in_api_client.user
|
user = logged_in_api_client.user
|
||||||
user.subsonic_api_token = "test"
|
user.subsonic_api_token = "test"
|
||||||
|
|
Loading…
Reference in New Issue