Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
jovuit
funkwhale
Commits
f1076565
Verified
Commit
f1076565
authored
Jan 23, 2020
by
Eliot Berriot
Browse files
Federation of avatars
parent
b86971c3
Changes
20
Hide whitespace changes
Inline
Side-by-side
api/funkwhale_api/common/models.py
View file @
f1076565
...
...
@@ -180,6 +180,7 @@ class AttachmentQuerySet(models.QuerySet):
"mutation_attachment"
,
"covered_track"
,
"covered_artist"
,
"iconed_actor"
,
]
query
=
None
for
field
in
related_fields
:
...
...
api/funkwhale_api/common/scripts/create_image_variations.py
View file @
f1076565
...
...
@@ -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"
),
]
...
...
api/funkwhale_api/common/serializers.py
View file @
f1076565
...
...
@@ -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
)
api/funkwhale_api/common/utils.py
View file @
f1076565
...
...
@@ -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
:
...
...
api/funkwhale_api/favorites/views.py
View file @
f1076565
...
...
@@ -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
,
...
...
api/funkwhale_api/federation/migrations/0024_actor_attachment_icon.py
0 → 100644
View file @
f1076565
# 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'
),
),
]
api/funkwhale_api/federation/models.py
View file @
f1076565
...
...
@@ -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
()
...
...
api/funkwhale_api/federation/serializers.py
View file @
f1076565
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
):
...
...
api/funkwhale_api/history/views.py
View file @
f1076565
...
...
@@ -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
,
...
...
api/funkwhale_api/music/mutations.py
View file @
f1076565
...
...
@@ -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
):
...
...
api/funkwhale_api/music/serializers.py
View file @
f1076565
...
...
@@ -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
...
...
api/funkwhale_api/playlists/views.py
View file @
f1076565
...
...
@@ -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
()
...
...
api/funkwhale_api/users/migrations/0017_actor_avatar.py
0 → 100644
View file @
f1076565
# -*- 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
)]
api/funkwhale_api/users/models.py
View file @
f1076565
...
...
@@ -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
()
api/funkwhale_api/users/serializers.py
View file @
f1076565
...
...
@@ -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_f
ield
avatar
=
serializers
.
SerializerMethodF
ield
()
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_f
ield
avatar
=
serializers
.
SerializerMethodF
ield
()
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
()
...
...
api/funkwhale_api/users/views.py
View file @
f1076565
...
...
@@ -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-_.]+"
...
...
api/tests/federation/test_serializers.py
View file @
f1076565
...
...
@@ -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
)
...
...
api/tests/users/test_views.py
View file @
f1076565
...
...
@@ -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
.
a
vatar
.
read
()
==
cont
ent
assert
user
.
a
ctor
.
attachment_icon
==
attachm
ent
def
test_creating_user_creates_actor_as_well
(
...
...
front/src/components/Sidebar.vue
View file @
f1076565
...
...
@@ -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>
...
...
front/src/components/auth/Settings.vue
View file @
f1076565
...
...
@@ -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>