Commit c898e413 by Andy Armstrong

Add default profile images

TNL-1796
parent 20410a9c
...@@ -41,7 +41,7 @@ from lms.envs.common import ( ...@@ -41,7 +41,7 @@ from lms.envs.common import (
# indirectly accessed through the email opt-in API, which is # indirectly accessed through the email opt-in API, which is
# technically accessible through the CMS via legacy URLs. # technically accessible through the CMS via legacy URLs.
PROFILE_IMAGE_BACKEND, PROFILE_IMAGE_DOMAIN, PROFILE_IMAGE_URL_PATH, PROFILE_IMAGE_DEFAULT_FILENAME, PROFILE_IMAGE_BACKEND, PROFILE_IMAGE_DOMAIN, PROFILE_IMAGE_URL_PATH, PROFILE_IMAGE_DEFAULT_FILENAME,
PROFILE_IMAGE_SECRET_KEY PROFILE_IMAGE_DEFAULT_FILE_EXTENSION, PROFILE_IMAGE_SECRET_KEY,
) )
from path import path from path import path
from warnings import simplefilter from warnings import simplefilter
......
...@@ -2272,7 +2272,10 @@ PROFILE_IMAGE_BACKEND = 'storages.backends.overwrite.OverwriteStorage' ...@@ -2272,7 +2272,10 @@ PROFILE_IMAGE_BACKEND = 'storages.backends.overwrite.OverwriteStorage'
# i.e. 'http://www.example-image-server.com/' # i.e. 'http://www.example-image-server.com/'
PROFILE_IMAGE_DOMAIN = '/' PROFILE_IMAGE_DOMAIN = '/'
PROFILE_IMAGE_URL_PATH = 'media/profile_images/' PROFILE_IMAGE_URL_PATH = 'media/profile_images/'
PROFILE_IMAGE_DEFAULT_FILENAME = 'default_profile_image' # TODO: determine final name PROFILE_IMAGE_DEFAULT_FILENAME = (
'images/edx-theme/default-profile' if FEATURES['IS_EDX_DOMAIN'] else 'images/default-theme/default-profile'
)
PROFILE_IMAGE_DEFAULT_FILE_EXTENSION = 'png'
# This secret key is used in generating unguessable URLs to users' # This secret key is used in generating unguessable URLs to users'
# profile images. Once it has been set, changing it will make the # profile images. Once it has been set, changing it will make the
# platform unaware of current image URLs, resulting in reverting all # platform unaware of current image URLs, resulting in reverting all
......
...@@ -482,6 +482,7 @@ PROFILE_IMAGE_BACKEND = 'django.core.files.storage.FileSystemStorage' ...@@ -482,6 +482,7 @@ PROFILE_IMAGE_BACKEND = 'django.core.files.storage.FileSystemStorage'
PROFILE_IMAGE_DOMAIN = 'http://example-storage.com/' PROFILE_IMAGE_DOMAIN = 'http://example-storage.com/'
PROFILE_IMAGE_URL_PATH = 'profile_images/' PROFILE_IMAGE_URL_PATH = 'profile_images/'
PROFILE_IMAGE_DEFAULT_FILENAME = 'default' PROFILE_IMAGE_DEFAULT_FILENAME = 'default'
PROFILE_IMAGE_DEFAULT_FILE_EXTENSION = 'png'
PROFILE_IMAGE_SECRET_KEY = 'secret' PROFILE_IMAGE_SECRET_KEY = 'secret'
PROFILE_IMAGE_MAX_BYTES = 1024 * 1024 PROFILE_IMAGE_MAX_BYTES = 1024 * 1024
PROFILE_IMAGE_MIN_BYTES = 100 PROFILE_IMAGE_MIN_BYTES = 100
...@@ -6,11 +6,13 @@ import hashlib ...@@ -6,11 +6,13 @@ import hashlib
from django.conf import settings from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.files.storage import get_storage_class from django.core.files.storage import get_storage_class
from staticfiles.storage import staticfiles_storage
from student.models import UserProfile from student.models import UserProfile
from ..errors import UserNotFound from ..errors import UserNotFound
PROFILE_IMAGE_FILE_EXTENSION = 'jpg' # All processed profile images are converted to JPEGs
PROFILE_IMAGE_SIZES_MAP = { PROFILE_IMAGE_SIZES_MAP = {
'full': 500, 'full': 500,
'large': 120, 'large': 120,
...@@ -37,21 +39,20 @@ def _make_profile_image_name(username): ...@@ -37,21 +39,20 @@ def _make_profile_image_name(username):
return hashlib.md5(settings.PROFILE_IMAGE_SECRET_KEY + username).hexdigest() return hashlib.md5(settings.PROFILE_IMAGE_SECRET_KEY + username).hexdigest()
def _get_profile_image_filename(name, size): def _get_profile_image_filename(name, size, file_extension=PROFILE_IMAGE_FILE_EXTENSION):
""" """
Returns the full filename for a profile image, given the name and size. Returns the full filename for a profile image, given the name and size.
""" """
return '{name}_{size}.jpg'.format(name=name, size=size) return '{name}_{size}.{file_extension}'.format(name=name, size=size, file_extension=file_extension)
def _get_profile_image_urls(name): def _get_profile_image_urls(name, storage, file_extension=PROFILE_IMAGE_FILE_EXTENSION):
""" """
Returns a dict containing the urls for a complete set of profile images, Returns a dict containing the urls for a complete set of profile images,
keyed by "friendly" name (e.g. "full", "large", "medium", "small"). keyed by "friendly" name (e.g. "full", "large", "medium", "small").
""" """
storage = get_profile_image_storage()
return { return {
size_display_name: storage.url(_get_profile_image_filename(name, size)) size_display_name: storage.url(_get_profile_image_filename(name, size, file_extension=file_extension))
for size_display_name, size in PROFILE_IMAGE_SIZES_MAP.items() for size_display_name, size in PROFILE_IMAGE_SIZES_MAP.items()
} }
...@@ -86,7 +87,7 @@ def get_profile_image_urls_for_user(user): ...@@ -86,7 +87,7 @@ def get_profile_image_urls_for_user(user):
""" """
if user.profile.has_profile_image: if user.profile.has_profile_image:
return _get_profile_image_urls(_make_profile_image_name(user.username)) return _get_profile_image_urls(_make_profile_image_name(user.username), get_profile_image_storage())
else: else:
return _get_default_profile_image_urls() return _get_default_profile_image_urls()
...@@ -98,7 +99,11 @@ def _get_default_profile_image_urls(): ...@@ -98,7 +99,11 @@ def _get_default_profile_image_urls():
TODO The result of this function should be memoized, but not in tests. TODO The result of this function should be memoized, but not in tests.
""" """
return _get_profile_image_urls(settings.PROFILE_IMAGE_DEFAULT_FILENAME) return _get_profile_image_urls(
settings.PROFILE_IMAGE_DEFAULT_FILENAME,
staticfiles_storage,
file_extension=settings.PROFILE_IMAGE_DEFAULT_FILE_EXTENSION
)
def set_has_profile_image(username, has_profile_image=True): def set_has_profile_image(username, has_profile_image=True):
......
...@@ -231,8 +231,8 @@ class AccountSettingsOnCreationTest(TestCase): ...@@ -231,8 +231,8 @@ class AccountSettingsOnCreationTest(TestCase):
'bio': None, 'bio': None,
'profile_image': { 'profile_image': {
'has_image': False, 'has_image': False,
'image_url_full': 'http://example-storage.com/profile_images/default_50.jpg', 'image_url_full': '/static/default_50.png',
'image_url_small': 'http://example-storage.com/profile_images/default_10.jpg', 'image_url_small': '/static/default_10.png',
}, },
'requires_parental_consent': True, 'requires_parental_consent': True,
'language_proficiencies': [], 'language_proficiencies': [],
......
...@@ -34,16 +34,30 @@ class ProfileImageUrlTestCase(TestCase): ...@@ -34,16 +34,30 @@ class ProfileImageUrlTestCase(TestCase):
""" """
self.assertEqual( self.assertEqual(
actual_url, actual_url,
'http://example-storage.com/profile_images/{0}_{1}.jpg'.format(expected_name, expected_pixels), 'http://example-storage.com/profile_images/{name}_{size}.jpg'.format(
name=expected_name, size=expected_pixels
)
) )
def verify_urls(self, expected_name, actual_urls): def verify_default_url(self, actual_url, expected_pixels):
"""
Verify correct url structure for a default profile image.
"""
self.assertEqual(
actual_url,
'/static/default_{size}.png'.format(size=expected_pixels)
)
def verify_urls(self, expected_name, actual_urls, is_default=False):
""" """
Verify correct url dictionary structure. Verify correct url dictionary structure.
""" """
self.assertEqual(set(TEST_SIZES.keys()), set(actual_urls.keys())) self.assertEqual(set(TEST_SIZES.keys()), set(actual_urls.keys()))
for size_display_name, url in actual_urls.items(): for size_display_name, url in actual_urls.items():
self.verify_url(url, expected_name, TEST_SIZES[size_display_name]) if is_default:
self.verify_default_url(url, TEST_SIZES[size_display_name])
else:
self.verify_url(url, expected_name, TEST_SIZES[size_display_name])
def test_get_profile_image_urls(self): def test_get_profile_image_urls(self):
""" """
...@@ -57,4 +71,4 @@ class ProfileImageUrlTestCase(TestCase): ...@@ -57,4 +71,4 @@ class ProfileImageUrlTestCase(TestCase):
self.user.profile.has_profile_image = False self.user.profile.has_profile_image = False
self.user.profile.save() self.user.profile.save()
self.verify_urls('default', get_profile_image_urls_for_user(self.user)) self.verify_urls('default', get_profile_image_urls_for_user(self.user), is_default=True)
...@@ -117,15 +117,23 @@ class TestAccountAPI(UserAPITestCase): ...@@ -117,15 +117,23 @@ class TestAccountAPI(UserAPITestCase):
image. image.
""" """
if has_profile_image: if has_profile_image:
url_root = 'http://example-storage.com/profile_images'
filename = hashlib.md5('secret' + self.user.username).hexdigest() filename = hashlib.md5('secret' + self.user.username).hexdigest()
file_extension = 'jpg'
else: else:
url_root = 'http://testserver/static'
filename = 'default' filename = 'default'
file_extension = 'png'
self.assertEqual( self.assertEqual(
data['profile_image'], data['profile_image'],
{ {
'has_image': has_profile_image, 'has_image': has_profile_image,
'image_url_full': 'http://example-storage.com/profile_images/{}_50.jpg'.format(filename), 'image_url_full': '{root}/{filename}_50.{extension}'.format(
'image_url_small': 'http://example-storage.com/profile_images/{}_10.jpg'.format(filename) root=url_root, filename=filename, extension=file_extension,
),
'image_url_small': '{root}/{filename}_10.{extension}'.format(
root=url_root, filename=filename, extension=file_extension,
)
} }
) )
...@@ -584,7 +592,7 @@ class TestAccountAPI(UserAPITestCase): ...@@ -584,7 +592,7 @@ class TestAccountAPI(UserAPITestCase):
error_response.data["developer_message"] error_response.data["developer_message"]
) )
self.assertIsNone(error_response.data["user_message"]) self.assertIsNone(error_response.data["user_message"])
@override_settings(PROFILE_IMAGE_DOMAIN='/') @override_settings(PROFILE_IMAGE_DOMAIN='/')
def test_convert_relative_profile_url(self): def test_convert_relative_profile_url(self):
""" """
...@@ -599,8 +607,8 @@ class TestAccountAPI(UserAPITestCase): ...@@ -599,8 +607,8 @@ class TestAccountAPI(UserAPITestCase):
response.data["profile_image"], response.data["profile_image"],
{ {
"has_image": False, "has_image": False,
"image_url_full": "http://testserver/profile_images/default_50.jpg", "image_url_full": "http://testserver/static/default_50.png",
"image_url_small": "http://testserver/profile_images/default_10.jpg" "image_url_small": "http://testserver/static/default_10.png"
} }
) )
...@@ -647,7 +655,7 @@ class TestAccountAPI(UserAPITestCase): ...@@ -647,7 +655,7 @@ class TestAccountAPI(UserAPITestCase):
response = self.send_get(client, query_parameters='view=shared') response = self.send_get(client, query_parameters='view=shared')
self._verify_private_account_response(response, requires_parental_consent=True) self._verify_private_account_response(response, requires_parental_consent=True)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class TestAccountAPITransactions(TransactionTestCase): class TestAccountAPITransactions(TransactionTestCase):
""" """
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment