registration: Allow users to import profile picture.

This commit is contained in:
Vishnu Ks
2018-06-06 18:00:26 +05:30
committed by Tim Abbott
parent ca87cf4c97
commit 53237d39aa
5 changed files with 129 additions and 4 deletions

View File

@@ -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,9 +100,11 @@ 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,
type=Recipient.PERSONAL) type=Recipient.PERSONAL)

View File

@@ -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:

View File

@@ -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)

View File

@@ -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:
""" """

View File

@@ -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)