Merge branch 'actor-avatar' into 'develop'
Federation of avatars See merge request funkwhale/funkwhale!1003
This commit is contained in:
commit
9f3ca7b4c5
|
@ -180,6 +180,7 @@ class AttachmentQuerySet(models.QuerySet):
|
|||
"mutation_attachment",
|
||||
"covered_track",
|
||||
"covered_artist",
|
||||
"iconed_actor",
|
||||
]
|
||||
query = None
|
||||
for field in related_fields:
|
||||
|
|
|
@ -5,13 +5,9 @@ Compute different sizes of image used for Album covers and User avatars
|
|||
from versatileimagefield.image_warmer import VersatileImageFieldWarmer
|
||||
|
||||
from funkwhale_api.common.models import Attachment
|
||||
from funkwhale_api.music.models import Album
|
||||
from funkwhale_api.users.models import User
|
||||
|
||||
|
||||
MODELS = [
|
||||
(Album, "cover", "square"),
|
||||
(User, "avatar", "square"),
|
||||
(Attachment, "file", "attachment_square"),
|
||||
]
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ class RelatedField(serializers.RelatedField):
|
|||
self.related_field_name = related_field_name
|
||||
self.serializer = serializer
|
||||
self.filters = kwargs.pop("filters", None)
|
||||
self.queryset_filter = kwargs.pop("queryset_filter", None)
|
||||
try:
|
||||
kwargs["queryset"] = kwargs.pop("queryset")
|
||||
except KeyError:
|
||||
|
@ -36,10 +37,16 @@ class RelatedField(serializers.RelatedField):
|
|||
filters.update(self.filters(self.context))
|
||||
return filters
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
if self.queryset_filter:
|
||||
queryset = self.queryset_filter(queryset, self.context)
|
||||
return queryset
|
||||
|
||||
def to_internal_value(self, data):
|
||||
try:
|
||||
queryset = self.get_queryset()
|
||||
filters = self.get_filters(data)
|
||||
queryset = self.filter_queryset(queryset)
|
||||
return queryset.get(**filters)
|
||||
except ObjectDoesNotExist:
|
||||
self.fail(
|
||||
|
@ -318,3 +325,16 @@ class ContentSerializer(serializers.Serializer):
|
|||
|
||||
def get_html(self, o):
|
||||
return utils.render_html(o.text, o.content_type)
|
||||
|
||||
|
||||
class NullToEmptDict(object):
|
||||
def get_attribute(self, o):
|
||||
attr = super().get_attribute(o)
|
||||
if attr is None:
|
||||
return {}
|
||||
return attr
|
||||
|
||||
def to_representation(self, v):
|
||||
if not v:
|
||||
return v
|
||||
return super().to_representation(v)
|
||||
|
|
|
@ -327,8 +327,11 @@ def attach_file(obj, field, file_data, fetch=False):
|
|||
extensions = {"image/jpeg": "jpg", "image/png": "png", "image/gif": "gif"}
|
||||
extension = extensions.get(file_data["mimetype"], "jpg")
|
||||
attachment = models.Attachment(mimetype=file_data["mimetype"])
|
||||
|
||||
filename = "cover-{}.{}".format(obj.uuid, extension)
|
||||
name_fields = ["uuid", "full_username", "pk"]
|
||||
name = [getattr(obj, field) for field in name_fields if getattr(obj, field, None)][
|
||||
0
|
||||
]
|
||||
filename = "{}-{}.{}".format(field, name, extension)
|
||||
if "url" in file_data:
|
||||
attachment.url = file_data["url"]
|
||||
else:
|
||||
|
|
|
@ -22,7 +22,9 @@ class TrackFavoriteViewSet(
|
|||
|
||||
filterset_class = filters.TrackFavoriteFilter
|
||||
serializer_class = serializers.UserTrackFavoriteSerializer
|
||||
queryset = models.TrackFavorite.objects.all().select_related("user__actor")
|
||||
queryset = models.TrackFavorite.objects.all().select_related(
|
||||
"user__actor__attachment_icon"
|
||||
)
|
||||
permission_classes = [
|
||||
oauth_permissions.ScopePermission,
|
||||
permissions.OwnerPermission,
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 2.2.9 on 2020-01-23 13:59
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('common', '0007_auto_20200116_1610'),
|
||||
('federation', '0023_actor_summary_obj'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='actor',
|
||||
name='attachment_icon',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='iconed_actor', to='common.Attachment'),
|
||||
),
|
||||
]
|
|
@ -205,6 +205,13 @@ class Actor(models.Model):
|
|||
through_fields=("target", "actor"),
|
||||
related_name="following",
|
||||
)
|
||||
attachment_icon = models.ForeignKey(
|
||||
"common.Attachment",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="iconed_actor",
|
||||
)
|
||||
|
||||
objects = ActorQuerySet.as_manager()
|
||||
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import logging
|
||||
import mimetypes
|
||||
import urllib.parse
|
||||
import uuid
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import transaction
|
||||
|
||||
|
@ -97,6 +95,9 @@ class ActorSerializer(jsonld.JsonLdSerializer):
|
|||
following = serializers.URLField(max_length=500, required=False, allow_null=True)
|
||||
publicKey = PublicKeySerializer(required=False)
|
||||
endpoints = EndpointsSerializer(required=False)
|
||||
icon = LinkSerializer(
|
||||
allowed_mimetypes=["image/*"], allow_null=True, required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
jsonld_mapping = {
|
||||
|
@ -113,6 +114,7 @@ class ActorSerializer(jsonld.JsonLdSerializer):
|
|||
),
|
||||
"mediaType": jsonld.first_val(contexts.AS.mediaType),
|
||||
"endpoints": jsonld.first_obj(contexts.AS.endpoints),
|
||||
"icon": jsonld.first_obj(contexts.AS.icon),
|
||||
}
|
||||
|
||||
def to_representation(self, instance):
|
||||
|
@ -143,17 +145,11 @@ class ActorSerializer(jsonld.JsonLdSerializer):
|
|||
"id": "{}#main-key".format(instance.fid),
|
||||
}
|
||||
ret["endpoints"] = {}
|
||||
|
||||
include_image(ret, instance.attachment_icon, "icon")
|
||||
|
||||
if instance.shared_inbox_url:
|
||||
ret["endpoints"]["sharedInbox"] = instance.shared_inbox_url
|
||||
try:
|
||||
if instance.user.avatar:
|
||||
ret["icon"] = {
|
||||
"type": "Image",
|
||||
"mediaType": mimetypes.guess_type(instance.user.avatar_path)[0],
|
||||
"url": utils.full_url(instance.user.avatar.crop["400x400"].url),
|
||||
}
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
return ret
|
||||
|
||||
def prepare_missing_fields(self):
|
||||
|
@ -201,6 +197,15 @@ class ActorSerializer(jsonld.JsonLdSerializer):
|
|||
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["href"], "mimetype": new_value["mediaType"]}
|
||||
if new_value
|
||||
else None,
|
||||
)
|
||||
return actor
|
||||
|
||||
def validate(self, data):
|
||||
|
@ -844,15 +849,15 @@ def include_content(repr, content_obj):
|
|||
repr["mediaType"] = "text/html"
|
||||
|
||||
|
||||
def include_image(repr, attachment):
|
||||
def include_image(repr, attachment, field="image"):
|
||||
if attachment:
|
||||
repr["image"] = {
|
||||
repr[field] = {
|
||||
"type": "Image",
|
||||
"href": attachment.download_url_original,
|
||||
"mediaType": attachment.mimetype or "image/jpeg",
|
||||
}
|
||||
else:
|
||||
repr["image"] = None
|
||||
repr[field] = None
|
||||
|
||||
|
||||
class MusicEntitySerializer(jsonld.JsonLdSerializer):
|
||||
|
|
|
@ -19,7 +19,9 @@ class ListeningViewSet(
|
|||
):
|
||||
|
||||
serializer_class = serializers.ListeningSerializer
|
||||
queryset = models.Listening.objects.all().select_related("user__actor")
|
||||
queryset = models.Listening.objects.all().select_related(
|
||||
"user__actor__attachment_icon"
|
||||
)
|
||||
|
||||
permission_classes = [
|
||||
oauth_permissions.ScopePermission,
|
||||
|
|
|
@ -61,7 +61,9 @@ class DescriptionMutation(mutations.UpdateMutationSerializer):
|
|||
|
||||
class CoverMutation(mutations.UpdateMutationSerializer):
|
||||
cover = common_serializers.RelatedField(
|
||||
"uuid", queryset=common_models.Attachment.objects.all().local(), serializer=None
|
||||
"uuid",
|
||||
queryset=common_models.Attachment.objects.all().local(),
|
||||
serializer=None,
|
||||
)
|
||||
|
||||
def get_serialized_relations(self):
|
||||
|
|
|
@ -18,20 +18,9 @@ from funkwhale_api.tags import serializers as tags_serializers
|
|||
from . import filters, models, tasks
|
||||
|
||||
|
||||
class NullToEmptDict(object):
|
||||
def get_attribute(self, o):
|
||||
attr = super().get_attribute(o)
|
||||
if attr is None:
|
||||
return {}
|
||||
return attr
|
||||
|
||||
def to_representation(self, v):
|
||||
if not v:
|
||||
return v
|
||||
return super().to_representation(v)
|
||||
|
||||
|
||||
class CoverField(NullToEmptDict, common_serializers.AttachmentSerializer):
|
||||
class CoverField(
|
||||
common_serializers.NullToEmptDict, common_serializers.AttachmentSerializer
|
||||
):
|
||||
# XXX: BACKWARD COMPATIBILITY
|
||||
pass
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ class PlaylistViewSet(
|
|||
serializer_class = serializers.PlaylistSerializer
|
||||
queryset = (
|
||||
models.Playlist.objects.all()
|
||||
.select_related("user__actor")
|
||||
.select_related("user__actor__attachment_icon")
|
||||
.annotate(tracks_count=Count("playlist_tracks", distinct=True))
|
||||
.with_covers()
|
||||
.with_duration()
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def create_attachments(apps, schema_editor):
|
||||
Actor = apps.get_model("federation", "Actor")
|
||||
User = apps.get_model("users", "User")
|
||||
Attachment = apps.get_model("common", "Attachment")
|
||||
|
||||
obj_attachment_mapping = {}
|
||||
def get_mimetype(path):
|
||||
if path.lower().endswith('.png'):
|
||||
return "image/png"
|
||||
return "image/jpeg"
|
||||
qs = User.objects.filter(actor__attachment_icon=None).exclude(avatar="").exclude(avatar=None).exclude(actor=None).select_related('actor')
|
||||
total = qs.count()
|
||||
print('Creating attachments for {} user avatars, this may take a while…'.format(total))
|
||||
from django.core.files.storage import FileSystemStorage
|
||||
for i, user in enumerate(qs):
|
||||
if isinstance(user.avatar.storage._wrapped, FileSystemStorage):
|
||||
try:
|
||||
size = user.avatar.size
|
||||
except FileNotFoundError:
|
||||
# can occur when file isn't found on disk or S3
|
||||
print(" Warning: avatar file wasn't found in storage: {}".format(e.__class__))
|
||||
size = None
|
||||
obj_attachment_mapping[user.actor] = Attachment(
|
||||
file=user.avatar,
|
||||
size=size,
|
||||
mimetype=get_mimetype(user.avatar.name),
|
||||
)
|
||||
print('Commiting changes…')
|
||||
Attachment.objects.bulk_create(obj_attachment_mapping.values(), batch_size=2000)
|
||||
# map each attachment to the corresponding obj
|
||||
# and bulk save
|
||||
for obj, attachment in obj_attachment_mapping.items():
|
||||
obj.attachment_icon = attachment
|
||||
|
||||
Actor.objects.bulk_update(obj_attachment_mapping.keys(), fields=['attachment_icon'], batch_size=2000)
|
||||
|
||||
|
||||
def rewind(apps, schema_editor):
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [("users", "0016_auto_20190920_0857"), ("federation", "0024_actor_attachment_icon")]
|
||||
|
||||
operations = [migrations.RunPython(create_attachments, rewind)]
|
|
@ -21,7 +21,6 @@ from django_auth_ldap.backend import populate_user as ldap_populate_user
|
|||
from oauth2_provider import models as oauth2_models
|
||||
from oauth2_provider import validators as oauth2_validators
|
||||
from versatileimagefield.fields import VersatileImageField
|
||||
from versatileimagefield.image_warmer import VersatileImageFieldWarmer
|
||||
|
||||
from funkwhale_api.common import fields, preferences
|
||||
from funkwhale_api.common import utils as common_utils
|
||||
|
@ -413,13 +412,3 @@ def create_actor(user):
|
|||
def init_ldap_user(sender, user, ldap_user, **kwargs):
|
||||
if not user.actor:
|
||||
user.actor = create_actor(user)
|
||||
|
||||
|
||||
@receiver(models.signals.post_save, sender=User)
|
||||
def warm_user_avatar(sender, instance, **kwargs):
|
||||
if not instance.avatar or not settings.CREATE_IMAGE_THUMBNAILS:
|
||||
return
|
||||
user_avatar_warmer = VersatileImageFieldWarmer(
|
||||
instance_or_queryset=instance, rendition_key_set="square", image_attr="avatar"
|
||||
)
|
||||
num_created, failed_to_create = user_avatar_warmer.warm()
|
||||
|
|
|
@ -7,9 +7,9 @@ from django.utils.translation import gettext_lazy as _
|
|||
from rest_auth.serializers import PasswordResetSerializer as PRS
|
||||
from rest_auth.registration.serializers import RegisterSerializer as RS, get_adapter
|
||||
from rest_framework import serializers
|
||||
from versatileimagefield.serializers import VersatileImageFieldSerializer
|
||||
|
||||
from funkwhale_api.activity import serializers as activity_serializers
|
||||
from funkwhale_api.common import models as common_models
|
||||
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
|
||||
|
@ -89,26 +89,30 @@ class UserActivitySerializer(activity_serializers.ModelSerializer):
|
|||
return "Person"
|
||||
|
||||
|
||||
class AvatarField(
|
||||
common_serializers.StripExifImageField, VersatileImageFieldSerializer
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
avatar_field = AvatarField(allow_null=True, sizes="square")
|
||||
|
||||
|
||||
class UserBasicSerializer(serializers.ModelSerializer):
|
||||
avatar = avatar_field
|
||||
avatar = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = ["id", "username", "name", "date_joined", "avatar"]
|
||||
|
||||
def get_avatar(self, o):
|
||||
return common_serializers.AttachmentSerializer(
|
||||
o.actor.attachment_icon if o.actor else None
|
||||
).data
|
||||
|
||||
|
||||
class UserWriteSerializer(serializers.ModelSerializer):
|
||||
avatar = avatar_field
|
||||
summary = common_serializers.ContentSerializer(required=False, allow_null=True)
|
||||
avatar = common_serializers.RelatedField(
|
||||
"uuid",
|
||||
queryset=common_models.Attachment.objects.all().local().attached(False),
|
||||
serializer=None,
|
||||
queryset_filter=lambda qs, context: qs.filter(
|
||||
actor=context["request"].user.actor
|
||||
),
|
||||
write_only=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = models.User
|
||||
|
@ -125,19 +129,30 @@ class UserWriteSerializer(serializers.ModelSerializer):
|
|||
if not obj.actor:
|
||||
obj.create_actor()
|
||||
summary = validated_data.pop("summary", NOOP)
|
||||
avatar = validated_data.pop("avatar", NOOP)
|
||||
|
||||
obj = super().update(obj, validated_data)
|
||||
|
||||
if summary != NOOP:
|
||||
common_utils.attach_content(obj.actor, "summary_obj", summary)
|
||||
|
||||
if avatar != NOOP:
|
||||
obj.actor.attachment_icon = avatar
|
||||
obj.actor.save(update_fields=["attachment_icon"])
|
||||
return obj
|
||||
|
||||
def to_representation(self, obj):
|
||||
repr = super().to_representation(obj)
|
||||
repr["avatar"] = common_serializers.AttachmentSerializer(
|
||||
obj.actor.attachment_icon
|
||||
).data
|
||||
return repr
|
||||
|
||||
|
||||
class UserReadSerializer(serializers.ModelSerializer):
|
||||
|
||||
permissions = serializers.SerializerMethodField()
|
||||
full_username = serializers.SerializerMethodField()
|
||||
avatar = avatar_field
|
||||
avatar = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = models.User
|
||||
|
@ -155,6 +170,9 @@ class UserReadSerializer(serializers.ModelSerializer):
|
|||
"avatar",
|
||||
]
|
||||
|
||||
def get_avatar(self, o):
|
||||
return common_serializers.AttachmentSerializer(o.actor.attachment_icon).data
|
||||
|
||||
def get_permissions(self, o):
|
||||
return o.get_permissions()
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ class PasswordResetConfirmView(rest_auth_views.PasswordResetConfirmView):
|
|||
|
||||
|
||||
class UserViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet):
|
||||
queryset = models.User.objects.all()
|
||||
queryset = models.User.objects.all().select_related("actor__attachment_icon")
|
||||
serializer_class = serializers.UserWriteSerializer
|
||||
lookup_field = "username"
|
||||
lookup_value_regex = r"[a-zA-Z0-9-_.]+"
|
||||
|
|
|
@ -36,6 +36,11 @@ def test_actor_serializer_from_ap(db):
|
|||
"id": actor_url + "#main-key",
|
||||
},
|
||||
"endpoints": {"sharedInbox": "https://noop.url/federation/shared/inbox"},
|
||||
"icon": {
|
||||
"type": "Image",
|
||||
"mediaType": "image/jpeg",
|
||||
"href": "https://image.example/image.png",
|
||||
},
|
||||
}
|
||||
|
||||
serializer = serializers.ActorSerializer(data=payload)
|
||||
|
@ -60,6 +65,8 @@ def test_actor_serializer_from_ap(db):
|
|||
assert actor.private_key is None
|
||||
assert actor.public_key == payload["publicKey"]["publicKeyPem"]
|
||||
assert actor.domain_id == "test.federation"
|
||||
assert actor.attachment_icon.url == payload["icon"]["href"]
|
||||
assert actor.attachment_icon.mimetype == payload["icon"]["mediaType"]
|
||||
|
||||
|
||||
def test_actor_serializer_only_mandatory_field_from_ap(db):
|
||||
|
@ -90,7 +97,7 @@ def test_actor_serializer_only_mandatory_field_from_ap(db):
|
|||
assert actor.manually_approves_followers is None
|
||||
|
||||
|
||||
def test_actor_serializer_to_ap(db):
|
||||
def test_actor_serializer_to_ap(factories):
|
||||
expected = {
|
||||
"@context": jsonld.get_default_context(),
|
||||
"id": "https://test.federation/user",
|
||||
|
@ -122,12 +129,18 @@ def test_actor_serializer_to_ap(db):
|
|||
domain=models.Domain.objects.create(pk="test.federation"),
|
||||
type="Person",
|
||||
manually_approves_followers=False,
|
||||
attachment_icon=factories["common.Attachment"](),
|
||||
)
|
||||
|
||||
content = common_utils.attach_content(
|
||||
ac, "summary_obj", {"text": "hello world", "content_type": "text/markdown"}
|
||||
)
|
||||
expected["summary"] = content.rendered
|
||||
expected["icon"] = {
|
||||
"type": "Image",
|
||||
"mediaType": "image/jpeg",
|
||||
"href": utils.full_url(ac.attachment_icon.file.url),
|
||||
}
|
||||
serializer = serializers.ActorSerializer(ac)
|
||||
|
||||
assert serializer.data == expected
|
||||
|
@ -1133,6 +1146,7 @@ def test_local_actor_serializer_to_ap(factories):
|
|||
domain=models.Domain.objects.create(pk="test.federation"),
|
||||
type="Person",
|
||||
manually_approves_followers=False,
|
||||
attachment_icon=factories["common.Attachment"](),
|
||||
)
|
||||
content = common_utils.attach_content(
|
||||
ac, "summary_obj", {"text": "hello world", "content_type": "text/markdown"}
|
||||
|
@ -1145,7 +1159,7 @@ def test_local_actor_serializer_to_ap(factories):
|
|||
expected["icon"] = {
|
||||
"type": "Image",
|
||||
"mediaType": "image/jpeg",
|
||||
"url": utils.full_url(user.avatar.crop["400x400"].url),
|
||||
"href": utils.full_url(ac.attachment_icon.file.url),
|
||||
}
|
||||
serializer = serializers.ActorSerializer(ac)
|
||||
|
||||
|
|
|
@ -110,7 +110,9 @@ def test_can_fetch_data_from_api(api_client, factories):
|
|||
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)
|
||||
|
||||
avatar = factories["common.Attachment"]()
|
||||
user.actor.attachment_icon = avatar
|
||||
user.actor.save()
|
||||
api_client.login(username=user.username, password="test")
|
||||
response = api_client.get(url)
|
||||
assert response.status_code == 200
|
||||
|
@ -120,6 +122,9 @@ def test_can_fetch_data_from_api(api_client, factories):
|
|||
assert response.data["email"] == user.email
|
||||
assert response.data["name"] == user.name
|
||||
assert response.data["permissions"] == user.get_permissions()
|
||||
assert (
|
||||
response.data["avatar"] == common_serializers.AttachmentSerializer(avatar).data
|
||||
)
|
||||
assert (
|
||||
response.data["summary"]
|
||||
== common_serializers.ContentSerializer(summary_obj).data
|
||||
|
@ -301,18 +306,18 @@ def test_user_cannot_patch_another_user(method, logged_in_api_client, factories)
|
|||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_user_can_patch_their_own_avatar(logged_in_api_client, avatar):
|
||||
def test_user_can_patch_their_own_avatar(logged_in_api_client, factories):
|
||||
user = logged_in_api_client.user
|
||||
actor = user.create_actor()
|
||||
attachment = factories["common.Attachment"](actor=actor)
|
||||
url = reverse("api:v1:users:users-detail", kwargs={"username": user.username})
|
||||
content = avatar.read()
|
||||
avatar.seek(0)
|
||||
payload = {"avatar": avatar}
|
||||
payload = {"avatar": attachment.uuid}
|
||||
response = logged_in_api_client.patch(url, payload)
|
||||
|
||||
assert response.status_code == 200
|
||||
user.refresh_from_db()
|
||||
|
||||
assert user.avatar.read() == content
|
||||
assert user.actor.attachment_icon == attachment
|
||||
|
||||
|
||||
def test_creating_user_creates_actor_as_well(
|
||||
|
|
|
@ -74,7 +74,7 @@
|
|||
</router-link>
|
||||
<div class="item">
|
||||
<div class="ui user-dropdown dropdown" >
|
||||
<img class="ui avatar image" v-if="$store.state.auth.profile.avatar.square_crop" v-lazy="$store.getters['instance/absoluteUrl']($store.state.auth.profile.avatar.square_crop)" />
|
||||
<img class="ui avatar image" v-if="$store.state.auth.profile.avatar.square_crop" :src="$store.getters['instance/absoluteUrl']($store.state.auth.profile.avatar.square_crop)" />
|
||||
<actor-avatar v-else :actor="{preferred_username: $store.state.auth.username, full_username: $store.state.auth.username}" />
|
||||
<div class="menu">
|
||||
<router-link class="item" :to="{name: 'profile', params: {username: $store.state.auth.username}}"><translate translate-context="*/*/*/Noun">Profile</translate></router-link>
|
||||
|
|
|
@ -41,25 +41,13 @@
|
|||
<li v-for="error in avatarErrors">{{ error }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ui stackable grid">
|
||||
<div class="ui ten wide column">
|
||||
<h3 class="ui header"><translate translate-context="Content/Settings/Title/Verb">Upload a new avatar</translate></h3>
|
||||
<p><translate translate-context="Content/Settings/Paragraph">PNG, GIF or JPG. At most 2MB. Will be downscaled to 400x400px.</translate></p>
|
||||
<input class="ui input" ref="avatar" type="file" />
|
||||
<div class="ui hidden divider"></div>
|
||||
<button @click="submitAvatar" :class="['ui', {'loading': isLoadingAvatar}, 'button']">
|
||||
<translate translate-context="Content/Settings/Button.Label/Verb">Update avatar</translate>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ui six wide column">
|
||||
<h3 class="ui header"><translate translate-context="Content/Settings/Title/Noun">Current avatar</translate></h3>
|
||||
<img class="ui circular image" v-if="currentAvatar && currentAvatar.square_crop" v-lazy="$store.getters['instance/absoluteUrl'](currentAvatar.medium_square_crop)" />
|
||||
<div class="ui hidden divider"></div>
|
||||
<button @click="removeAvatar" v-if="currentAvatar && currentAvatar.square_crop" :class="['ui', {'loading': isLoadingAvatar}, ,'yellow', 'button']">
|
||||
<translate translate-context="Content/Settings/Button.Label/Verb">Remove avatar</translate>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{ }}
|
||||
<attachment-input
|
||||
:value="avatar.uuid"
|
||||
@input="submitAvatar($event)"
|
||||
:initial-value="initialAvatar"
|
||||
:required="false"
|
||||
@delete="avatar = {uuid: null}"></attachment-input>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
@ -315,12 +303,14 @@ import logger from "@/logging"
|
|||
import PasswordInput from "@/components/forms/PasswordInput"
|
||||
import SubsonicTokenForm from "@/components/auth/SubsonicTokenForm"
|
||||
import TranslationsMixin from "@/components/mixins/Translations"
|
||||
import AttachmentInput from '@/components/common/AttachmentInput'
|
||||
|
||||
export default {
|
||||
mixins: [TranslationsMixin],
|
||||
components: {
|
||||
PasswordInput,
|
||||
SubsonicTokenForm
|
||||
SubsonicTokenForm,
|
||||
AttachmentInput
|
||||
},
|
||||
data() {
|
||||
let d = {
|
||||
|
@ -328,7 +318,7 @@ export default {
|
|||
// properties that will be used in it
|
||||
old_password: "",
|
||||
new_password: "",
|
||||
currentAvatar: this.$store.state.auth.profile.avatar,
|
||||
avatar: {...(this.$store.state.auth.profile.avatar || {uuid: null})},
|
||||
passwordError: "",
|
||||
password: "",
|
||||
isLoading: false,
|
||||
|
@ -336,7 +326,6 @@ export default {
|
|||
isDeletingAccount: false,
|
||||
accountDeleteErrors: [],
|
||||
avatarErrors: [],
|
||||
avatar: null,
|
||||
apps: [],
|
||||
ownedApps: [],
|
||||
settings: {
|
||||
|
@ -352,6 +341,7 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
d.initialAvatar = d.avatar.uuid
|
||||
d.settings.order.forEach(id => {
|
||||
d.settings.fields[id].value = d.settings.fields[id].initial
|
||||
d.settings.fields[id].id = id
|
||||
|
@ -437,44 +427,17 @@ export default {
|
|||
}
|
||||
)
|
||||
},
|
||||
submitAvatar() {
|
||||
submitAvatar(uuid) {
|
||||
this.isLoadingAvatar = true
|
||||
this.avatarErrors = []
|
||||
let self = this
|
||||
this.avatar = this.$refs.avatar.files[0]
|
||||
let formData = new FormData()
|
||||
formData.append("avatar", this.avatar)
|
||||
axios
|
||||
.patch(`users/users/${this.$store.state.auth.username}/`, formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
})
|
||||
.patch(`users/users/${this.$store.state.auth.username}/`, {avatar: uuid})
|
||||
.then(
|
||||
response => {
|
||||
this.isLoadingAvatar = false
|
||||
self.currentAvatar = response.data.avatar
|
||||
self.$store.commit("auth/avatar", self.currentAvatar)
|
||||
},
|
||||
error => {
|
||||
self.isLoadingAvatar = false
|
||||
self.avatarErrors = error.backendErrors
|
||||
}
|
||||
)
|
||||
},
|
||||
removeAvatar() {
|
||||
this.isLoadingAvatar = true
|
||||
let self = this
|
||||
this.avatar = null
|
||||
axios
|
||||
.patch(`users/users/${this.$store.state.auth.username}/`, {
|
||||
avatar: null
|
||||
})
|
||||
.then(
|
||||
response => {
|
||||
this.isLoadingAvatar = false
|
||||
self.currentAvatar = {}
|
||||
self.$store.commit("auth/avatar", self.currentAvatar)
|
||||
self.avatar = response.data.avatar
|
||||
self.$store.commit("auth/avatar", response.data.avatar)
|
||||
},
|
||||
error => {
|
||||
self.isLoadingAvatar = false
|
||||
|
|
Loading…
Reference in New Issue