mirror of
https://github.com/zulip/zulip.git
synced 2025-11-09 16:37:23 +00:00
registration: Allow users to import profile picture.
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
from django.contrib.auth.models import UserManager
|
from django.contrib.auth.models import UserManager
|
||||||
from django.utils.timezone import now as timezone_now
|
from django.utils.timezone import now as timezone_now
|
||||||
from zerver.models import UserProfile, Recipient, Subscription, Realm, Stream
|
from zerver.models import UserProfile, Recipient, Subscription, Realm, Stream
|
||||||
|
from zerver.lib.upload import copy_avatar
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import ujson
|
import ujson
|
||||||
import os
|
import os
|
||||||
@@ -25,6 +27,12 @@ def copy_user_settings(source_profile: UserProfile, target_profile: UserProfile)
|
|||||||
setattr(target_profile, settings_name, value)
|
setattr(target_profile, settings_name, value)
|
||||||
|
|
||||||
setattr(target_profile, "full_name", source_profile.full_name)
|
setattr(target_profile, "full_name", source_profile.full_name)
|
||||||
|
target_profile.save()
|
||||||
|
|
||||||
|
if source_profile.avatar_source == UserProfile.AVATAR_FROM_USER:
|
||||||
|
from zerver.lib.actions import do_change_avatar_fields
|
||||||
|
do_change_avatar_fields(target_profile, UserProfile.AVATAR_FROM_USER)
|
||||||
|
copy_avatar(source_profile, target_profile)
|
||||||
|
|
||||||
# create_user_profile is based on Django's User.objects.create_user,
|
# create_user_profile is based on Django's User.objects.create_user,
|
||||||
# except that we don't save to the database so it can used in
|
# except that we don't save to the database so it can used in
|
||||||
@@ -92,8 +100,10 @@ def create_user(email: str, password: Optional[str], realm: Realm,
|
|||||||
# than the guess. As we decide on details like avatars and full
|
# than the guess. As we decide on details like avatars and full
|
||||||
# names for this feature, we may want to move it.
|
# names for this feature, we may want to move it.
|
||||||
if source_profile is not None:
|
if source_profile is not None:
|
||||||
|
# copy_user_settings saves the attribute values so a secondary
|
||||||
|
# save is not required.
|
||||||
copy_user_settings(source_profile, user_profile)
|
copy_user_settings(source_profile, user_profile)
|
||||||
|
else:
|
||||||
user_profile.save()
|
user_profile.save()
|
||||||
|
|
||||||
recipient = Recipient.objects.create(type_id=user_profile.id,
|
recipient = Recipient.objects.create(type_id=user_profile.id,
|
||||||
|
|||||||
@@ -187,11 +187,13 @@ def get_test_image_file(filename: str) -> IO[Any]:
|
|||||||
test_avatar_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../tests/images'))
|
test_avatar_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../tests/images'))
|
||||||
return open(os.path.join(test_avatar_dir, filename), 'rb')
|
return open(os.path.join(test_avatar_dir, filename), 'rb')
|
||||||
|
|
||||||
def avatar_disk_path(user_profile: UserProfile, medium: bool=False) -> str:
|
def avatar_disk_path(user_profile: UserProfile, medium: bool=False, original: bool=False) -> str:
|
||||||
avatar_url_path = avatar_url(user_profile, medium)
|
avatar_url_path = avatar_url(user_profile, medium)
|
||||||
avatar_disk_path = os.path.join(settings.LOCAL_UPLOADS_DIR, "avatars",
|
avatar_disk_path = os.path.join(settings.LOCAL_UPLOADS_DIR, "avatars",
|
||||||
avatar_url_path.split("/")[-2],
|
avatar_url_path.split("/")[-2],
|
||||||
avatar_url_path.split("/")[-1].split("?")[0])
|
avatar_url_path.split("/")[-1].split("?")[0])
|
||||||
|
if original:
|
||||||
|
avatar_disk_path.replace(".png", ".original")
|
||||||
return avatar_disk_path
|
return avatar_disk_path
|
||||||
|
|
||||||
def make_client(name: str) -> Client:
|
def make_client(name: str) -> Client:
|
||||||
|
|||||||
@@ -159,6 +159,9 @@ class ZulipUploadBackend:
|
|||||||
def get_avatar_url(self, hash_key: str, medium: bool=False) -> str:
|
def get_avatar_url(self, hash_key: str, medium: bool=False) -> str:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def copy_avatar(self, source_profile: UserProfile, target_profile: UserProfile) -> None:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
def ensure_medium_avatar_image(self, user_profile: UserProfile) -> None:
|
def ensure_medium_avatar_image(self, user_profile: UserProfile) -> None:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@@ -341,6 +344,24 @@ class S3UploadBackend(ZulipUploadBackend):
|
|||||||
self.write_avatar_images(s3_file_name, target_user_profile,
|
self.write_avatar_images(s3_file_name, target_user_profile,
|
||||||
image_data, content_type)
|
image_data, content_type)
|
||||||
|
|
||||||
|
def get_avatar_key(self, file_name: str) -> Key:
|
||||||
|
conn = S3Connection(settings.S3_KEY, settings.S3_SECRET_KEY)
|
||||||
|
bucket_name = settings.S3_AVATAR_BUCKET
|
||||||
|
bucket = get_bucket(conn, bucket_name)
|
||||||
|
|
||||||
|
key = bucket.get_key(file_name)
|
||||||
|
return key
|
||||||
|
|
||||||
|
def copy_avatar(self, source_profile: UserProfile, target_profile: UserProfile) -> None:
|
||||||
|
s3_source_file_name = user_avatar_path(source_profile)
|
||||||
|
s3_target_file_name = user_avatar_path(target_profile)
|
||||||
|
|
||||||
|
key = self.get_avatar_key(s3_source_file_name + ".original")
|
||||||
|
image_data = key.get_contents_as_string() # type: ignore # https://github.com/python/typeshed/issues/1552
|
||||||
|
content_type = key.content_type
|
||||||
|
|
||||||
|
self.write_avatar_images(s3_target_file_name, target_profile, image_data, content_type) # type: ignore # image_data is `bytes`, boto subs are wrong
|
||||||
|
|
||||||
def get_avatar_url(self, hash_key: str, medium: bool=False) -> str:
|
def get_avatar_url(self, hash_key: str, medium: bool=False) -> str:
|
||||||
bucket = settings.S3_AVATAR_BUCKET
|
bucket = settings.S3_AVATAR_BUCKET
|
||||||
medium_suffix = "-medium.png" if medium else ""
|
medium_suffix = "-medium.png" if medium else ""
|
||||||
@@ -437,6 +458,11 @@ def write_local_file(type: str, path: str, file_data: bytes) -> None:
|
|||||||
with open(file_path, 'wb') as f:
|
with open(file_path, 'wb') as f:
|
||||||
f.write(file_data)
|
f.write(file_data)
|
||||||
|
|
||||||
|
def read_local_file(type: str, path: str) -> bytes:
|
||||||
|
file_path = os.path.join(settings.LOCAL_UPLOADS_DIR, type, path)
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
def get_local_file_path(path_id: str) -> Optional[str]:
|
def get_local_file_path(path_id: str) -> Optional[str]:
|
||||||
local_path = os.path.join(settings.LOCAL_UPLOADS_DIR, 'files', path_id)
|
local_path = os.path.join(settings.LOCAL_UPLOADS_DIR, 'files', path_id)
|
||||||
if os.path.isfile(local_path):
|
if os.path.isfile(local_path):
|
||||||
@@ -493,6 +519,13 @@ class LocalUploadBackend(ZulipUploadBackend):
|
|||||||
medium_suffix = "-medium" if medium else ""
|
medium_suffix = "-medium" if medium else ""
|
||||||
return "/user_avatars/%s%s.png?x=x" % (hash_key, medium_suffix)
|
return "/user_avatars/%s%s.png?x=x" % (hash_key, medium_suffix)
|
||||||
|
|
||||||
|
def copy_avatar(self, source_profile: UserProfile, target_profile: UserProfile) -> None:
|
||||||
|
source_file_path = user_avatar_path(source_profile)
|
||||||
|
target_file_path = user_avatar_path(target_profile)
|
||||||
|
|
||||||
|
image_data = read_local_file('avatars', source_file_path + '.original')
|
||||||
|
self.write_avatar_images(target_file_path, image_data)
|
||||||
|
|
||||||
def upload_realm_icon_image(self, icon_file: File, user_profile: UserProfile) -> None:
|
def upload_realm_icon_image(self, icon_file: File, user_profile: UserProfile) -> None:
|
||||||
upload_path = os.path.join('avatars', str(user_profile.realm.id), 'realm')
|
upload_path = os.path.join('avatars', str(user_profile.realm.id), 'realm')
|
||||||
|
|
||||||
@@ -557,6 +590,9 @@ def upload_avatar_image(user_file: File, acting_user_profile: UserProfile,
|
|||||||
target_user_profile: UserProfile) -> None:
|
target_user_profile: UserProfile) -> None:
|
||||||
upload_backend.upload_avatar_image(user_file, acting_user_profile, target_user_profile)
|
upload_backend.upload_avatar_image(user_file, acting_user_profile, target_user_profile)
|
||||||
|
|
||||||
|
def copy_avatar(source_profile: UserProfile, target_profile: UserProfile) -> None:
|
||||||
|
upload_backend.copy_avatar(source_profile, target_profile)
|
||||||
|
|
||||||
def upload_icon_image(user_file: File, user_profile: UserProfile) -> None:
|
def upload_icon_image(user_file: File, user_profile: UserProfile) -> None:
|
||||||
upload_backend.upload_realm_icon_image(user_file, user_profile)
|
upload_backend.upload_realm_icon_image(user_file, user_profile)
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from django.core.exceptions import ValidationError
|
|||||||
from two_factor.utils import default_device
|
from two_factor.utils import default_device
|
||||||
|
|
||||||
from mock import patch, MagicMock
|
from mock import patch, MagicMock
|
||||||
from zerver.lib.test_helpers import MockLDAP
|
from zerver.lib.test_helpers import MockLDAP, get_test_image_file, avatar_disk_path
|
||||||
|
|
||||||
from confirmation.models import Confirmation, create_confirmation_link, MultiuseInvite, \
|
from confirmation.models import Confirmation, create_confirmation_link, MultiuseInvite, \
|
||||||
generate_key, confirmation_url, get_object_from_key, ConfirmationKeyException
|
generate_key, confirmation_url, get_object_from_key, ConfirmationKeyException
|
||||||
@@ -48,6 +48,7 @@ from zerver.lib.actions import (
|
|||||||
do_set_realm_property,
|
do_set_realm_property,
|
||||||
add_new_user_history,
|
add_new_user_history,
|
||||||
)
|
)
|
||||||
|
from zerver.lib.avatar import avatar_url
|
||||||
from zerver.lib.mobile_auth_otp import xor_hex_strings, ascii_to_hex, \
|
from zerver.lib.mobile_auth_otp import xor_hex_strings, ascii_to_hex, \
|
||||||
otp_encrypt_api_key, is_valid_otp, hex_to_ascii, otp_decrypt_api_key
|
otp_encrypt_api_key, is_valid_otp, hex_to_ascii, otp_decrypt_api_key
|
||||||
from zerver.lib.notifications import enqueue_welcome_emails, \
|
from zerver.lib.notifications import enqueue_welcome_emails, \
|
||||||
@@ -2045,6 +2046,9 @@ class UserSignUpTest(ZulipTestCase):
|
|||||||
lear_realm = get_realm("lear")
|
lear_realm = get_realm("lear")
|
||||||
zulip_realm = get_realm("zulip")
|
zulip_realm = get_realm("zulip")
|
||||||
|
|
||||||
|
self.login(self.example_email("hamlet"))
|
||||||
|
with get_test_image_file('img.png') as image_file:
|
||||||
|
self.client_post("/json/users/me/avatar", {'file': image_file})
|
||||||
hamlet_in_zulip = get_user(self.example_email("hamlet"), zulip_realm)
|
hamlet_in_zulip = get_user(self.example_email("hamlet"), zulip_realm)
|
||||||
hamlet_in_zulip.left_side_userlist = True
|
hamlet_in_zulip.left_side_userlist = True
|
||||||
hamlet_in_zulip.default_language = "de"
|
hamlet_in_zulip.default_language = "de"
|
||||||
@@ -2068,6 +2072,9 @@ class UserSignUpTest(ZulipTestCase):
|
|||||||
self.assertEqual(hamlet_in_lear.emojiset, "twitter")
|
self.assertEqual(hamlet_in_lear.emojiset, "twitter")
|
||||||
self.assertEqual(hamlet_in_lear.high_contrast_mode, True)
|
self.assertEqual(hamlet_in_lear.high_contrast_mode, True)
|
||||||
self.assertEqual(hamlet_in_lear.enable_stream_sounds, False)
|
self.assertEqual(hamlet_in_lear.enable_stream_sounds, False)
|
||||||
|
zulip_path_id = avatar_disk_path(hamlet_in_zulip)
|
||||||
|
hamlet_path_id = avatar_disk_path(hamlet_in_zulip)
|
||||||
|
self.assertEqual(open(zulip_path_id, "rb").read(), open(hamlet_path_id, "rb").read())
|
||||||
|
|
||||||
def test_signup_invalid_subdomain(self) -> None:
|
def test_signup_invalid_subdomain(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ from zerver.lib.actions import (
|
|||||||
do_delete_old_unclaimed_attachments,
|
do_delete_old_unclaimed_attachments,
|
||||||
internal_send_private_message,
|
internal_send_private_message,
|
||||||
)
|
)
|
||||||
|
from zerver.lib.create_user import copy_user_settings
|
||||||
from zerver.lib.request import JsonableError
|
from zerver.lib.request import JsonableError
|
||||||
from zerver.views.upload import upload_file_backend, serve_local
|
from zerver.views.upload import upload_file_backend, serve_local
|
||||||
|
|
||||||
@@ -945,6 +946,29 @@ class AvatarTest(UploadSerializeMixin, ZulipTestCase):
|
|||||||
self.assertEqual(user_profile.avatar_version, version)
|
self.assertEqual(user_profile.avatar_version, version)
|
||||||
version += 1
|
version += 1
|
||||||
|
|
||||||
|
def test_copy_avatar_image(self) -> None:
|
||||||
|
self.login(self.example_email("hamlet"))
|
||||||
|
with get_test_image_file('img.png') as image_file:
|
||||||
|
self.client_post("/json/users/me/avatar", {'file': image_file})
|
||||||
|
|
||||||
|
source_user_profile = self.example_user('hamlet')
|
||||||
|
target_user_profile = self.example_user('iago')
|
||||||
|
|
||||||
|
copy_user_settings(source_user_profile, target_user_profile)
|
||||||
|
|
||||||
|
source_path_id = avatar_disk_path(source_user_profile)
|
||||||
|
target_path_id = avatar_disk_path(target_user_profile)
|
||||||
|
self.assertNotEqual(source_path_id, target_path_id)
|
||||||
|
self.assertEqual(open(source_path_id, "rb").read(), open(target_path_id, "rb").read())
|
||||||
|
|
||||||
|
source_original_path_id = avatar_disk_path(source_user_profile, original=True)
|
||||||
|
target_original_path_id = avatar_disk_path(target_user_profile, original=True)
|
||||||
|
self.assertEqual(open(source_original_path_id, "rb").read(), open(target_original_path_id, "rb").read())
|
||||||
|
|
||||||
|
source_medium_path_id = avatar_disk_path(source_user_profile, medium=True)
|
||||||
|
target_medium_path_id = avatar_disk_path(target_user_profile, medium=True)
|
||||||
|
self.assertEqual(open(source_medium_path_id, "rb").read(), open(target_medium_path_id, "rb").read())
|
||||||
|
|
||||||
def test_invalid_avatars(self) -> None:
|
def test_invalid_avatars(self) -> None:
|
||||||
"""
|
"""
|
||||||
A PUT request to /json/users/me/avatar with an invalid file should fail.
|
A PUT request to /json/users/me/avatar with an invalid file should fail.
|
||||||
@@ -1326,6 +1350,52 @@ class S3Test(ZulipTestCase):
|
|||||||
medium_image_key = bucket.get_key(medium_path_id)
|
medium_image_key = bucket.get_key(medium_path_id)
|
||||||
self.assertEqual(medium_image_key.key, medium_path_id)
|
self.assertEqual(medium_image_key.key, medium_path_id)
|
||||||
|
|
||||||
|
@use_s3_backend
|
||||||
|
def test_copy_avatar_image(self) -> None:
|
||||||
|
conn = S3Connection(settings.S3_KEY, settings.S3_SECRET_KEY)
|
||||||
|
bucket = conn.create_bucket(settings.S3_AVATAR_BUCKET)
|
||||||
|
|
||||||
|
self.login(self.example_email("hamlet"))
|
||||||
|
with get_test_image_file('img.png') as image_file:
|
||||||
|
self.client_post("/json/users/me/avatar", {'file': image_file})
|
||||||
|
|
||||||
|
source_user_profile = self.example_user('hamlet')
|
||||||
|
target_user_profile = self.example_user('othello')
|
||||||
|
|
||||||
|
copy_user_settings(source_user_profile, target_user_profile)
|
||||||
|
|
||||||
|
source_path_id = user_avatar_path(source_user_profile)
|
||||||
|
target_path_id = user_avatar_path(target_user_profile)
|
||||||
|
self.assertNotEqual(source_path_id, target_path_id)
|
||||||
|
|
||||||
|
source_image_key = bucket.get_key(source_path_id)
|
||||||
|
target_image_key = bucket.get_key(target_path_id)
|
||||||
|
self.assertEqual(target_image_key.key, target_path_id)
|
||||||
|
self.assertEqual(source_image_key.content_type, target_image_key.content_type)
|
||||||
|
source_image_data = source_image_key.get_contents_as_string()
|
||||||
|
target_image_data = target_image_key.get_contents_as_string()
|
||||||
|
self.assertEqual(source_image_data, target_image_data)
|
||||||
|
|
||||||
|
source_original_image_path_id = source_path_id + ".original"
|
||||||
|
target_original_image_path_id = target_path_id + ".original"
|
||||||
|
target_original_image_key = bucket.get_key(target_original_image_path_id)
|
||||||
|
self.assertEqual(target_original_image_key.key, target_original_image_path_id)
|
||||||
|
source_original_image_key = bucket.get_key(source_original_image_path_id)
|
||||||
|
self.assertEqual(source_original_image_key.content_type, target_original_image_key.content_type)
|
||||||
|
source_image_data = source_original_image_key.get_contents_as_string()
|
||||||
|
target_image_data = target_original_image_key.get_contents_as_string()
|
||||||
|
self.assertEqual(source_image_data, target_image_data)
|
||||||
|
|
||||||
|
target_medium_path_id = target_path_id + "-medium.png"
|
||||||
|
source_medium_path_id = source_path_id + "-medium.png"
|
||||||
|
source_medium_image_key = bucket.get_key(source_medium_path_id)
|
||||||
|
target_medium_image_key = bucket.get_key(target_medium_path_id)
|
||||||
|
self.assertEqual(target_medium_image_key.key, target_medium_path_id)
|
||||||
|
self.assertEqual(source_medium_image_key.content_type, target_medium_image_key.content_type)
|
||||||
|
source_medium_image_data = source_medium_image_key.get_contents_as_string()
|
||||||
|
target_medium_image_data = target_medium_image_key.get_contents_as_string()
|
||||||
|
self.assertEqual(source_medium_image_data, target_medium_image_data)
|
||||||
|
|
||||||
@use_s3_backend
|
@use_s3_backend
|
||||||
def test_get_realm_for_filename(self) -> None:
|
def test_get_realm_for_filename(self) -> None:
|
||||||
conn = S3Connection(settings.S3_KEY, settings.S3_SECRET_KEY)
|
conn = S3Connection(settings.S3_KEY, settings.S3_SECRET_KEY)
|
||||||
|
|||||||
Reference in New Issue
Block a user