Add default profile images
TNL-1796
@@ -41,7 +41,7 @@ from lms.envs.common import (
|
||||
# indirectly accessed through the email opt-in API, which is
|
||||
# technically accessible through the CMS via legacy URLs.
|
||||
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 warnings import simplefilter
|
||||
|
||||
@@ -2272,7 +2272,10 @@ PROFILE_IMAGE_BACKEND = 'storages.backends.overwrite.OverwriteStorage'
|
||||
# i.e. 'http://www.example-image-server.com/'
|
||||
PROFILE_IMAGE_DOMAIN = '/'
|
||||
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'
|
||||
# profile images. Once it has been set, changing it will make the
|
||||
# platform unaware of current image URLs, resulting in reverting all
|
||||
|
||||
@@ -482,6 +482,7 @@ PROFILE_IMAGE_BACKEND = 'django.core.files.storage.FileSystemStorage'
|
||||
PROFILE_IMAGE_DOMAIN = 'http://example-storage.com/'
|
||||
PROFILE_IMAGE_URL_PATH = 'profile_images/'
|
||||
PROFILE_IMAGE_DEFAULT_FILENAME = 'default'
|
||||
PROFILE_IMAGE_DEFAULT_FILE_EXTENSION = 'png'
|
||||
PROFILE_IMAGE_SECRET_KEY = 'secret'
|
||||
PROFILE_IMAGE_MAX_BYTES = 1024 * 1024
|
||||
PROFILE_IMAGE_MIN_BYTES = 100
|
||||
|
||||
BIN
lms/static/images/default-theme/default-profile_120.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
lms/static/images/default-theme/default-profile_30.png
Normal file
|
After Width: | Height: | Size: 576 B |
BIN
lms/static/images/default-theme/default-profile_50.png
Normal file
|
After Width: | Height: | Size: 1023 B |
BIN
lms/static/images/default-theme/default-profile_500.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
lms/static/images/edx-theme/default-profile_120.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
lms/static/images/edx-theme/default-profile_30.png
Normal file
|
After Width: | Height: | Size: 993 B |
BIN
lms/static/images/edx-theme/default-profile_50.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
lms/static/images/edx-theme/default-profile_500.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
@@ -6,11 +6,13 @@ import hashlib
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.files.storage import get_storage_class
|
||||
from staticfiles.storage import staticfiles_storage
|
||||
|
||||
from student.models import UserProfile
|
||||
from ..errors import UserNotFound
|
||||
|
||||
|
||||
PROFILE_IMAGE_FILE_EXTENSION = 'jpg' # All processed profile images are converted to JPEGs
|
||||
PROFILE_IMAGE_SIZES_MAP = {
|
||||
'full': 500,
|
||||
'large': 120,
|
||||
@@ -37,21 +39,20 @@ def _make_profile_image_name(username):
|
||||
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.
|
||||
"""
|
||||
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,
|
||||
keyed by "friendly" name (e.g. "full", "large", "medium", "small").
|
||||
"""
|
||||
storage = get_profile_image_storage()
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -86,7 +87,7 @@ def get_profile_image_urls_for_user(user):
|
||||
|
||||
"""
|
||||
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:
|
||||
return _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.
|
||||
"""
|
||||
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):
|
||||
|
||||
@@ -231,8 +231,8 @@ class AccountSettingsOnCreationTest(TestCase):
|
||||
'bio': None,
|
||||
'profile_image': {
|
||||
'has_image': False,
|
||||
'image_url_full': 'http://example-storage.com/profile_images/default_50.jpg',
|
||||
'image_url_small': 'http://example-storage.com/profile_images/default_10.jpg',
|
||||
'image_url_full': '/static/default_50.png',
|
||||
'image_url_small': '/static/default_10.png',
|
||||
},
|
||||
'requires_parental_consent': True,
|
||||
'language_proficiencies': [],
|
||||
|
||||
@@ -34,16 +34,30 @@ class ProfileImageUrlTestCase(TestCase):
|
||||
"""
|
||||
self.assertEqual(
|
||||
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.
|
||||
"""
|
||||
self.assertEqual(set(TEST_SIZES.keys()), set(actual_urls.keys()))
|
||||
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):
|
||||
"""
|
||||
@@ -57,4 +71,4 @@ class ProfileImageUrlTestCase(TestCase):
|
||||
|
||||
self.user.profile.has_profile_image = False
|
||||
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):
|
||||
image.
|
||||
"""
|
||||
if has_profile_image:
|
||||
url_root = 'http://example-storage.com/profile_images'
|
||||
filename = hashlib.md5('secret' + self.user.username).hexdigest()
|
||||
file_extension = 'jpg'
|
||||
else:
|
||||
url_root = 'http://testserver/static'
|
||||
filename = 'default'
|
||||
file_extension = 'png'
|
||||
self.assertEqual(
|
||||
data['profile_image'],
|
||||
{
|
||||
'has_image': has_profile_image,
|
||||
'image_url_full': 'http://example-storage.com/profile_images/{}_50.jpg'.format(filename),
|
||||
'image_url_small': 'http://example-storage.com/profile_images/{}_10.jpg'.format(filename)
|
||||
'image_url_full': '{root}/{filename}_50.{extension}'.format(
|
||||
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):
|
||||
error_response.data["developer_message"]
|
||||
)
|
||||
self.assertIsNone(error_response.data["user_message"])
|
||||
|
||||
|
||||
@override_settings(PROFILE_IMAGE_DOMAIN='/')
|
||||
def test_convert_relative_profile_url(self):
|
||||
"""
|
||||
@@ -599,8 +607,8 @@ class TestAccountAPI(UserAPITestCase):
|
||||
response.data["profile_image"],
|
||||
{
|
||||
"has_image": False,
|
||||
"image_url_full": "http://testserver/profile_images/default_50.jpg",
|
||||
"image_url_small": "http://testserver/profile_images/default_10.jpg"
|
||||
"image_url_full": "http://testserver/static/default_50.png",
|
||||
"image_url_small": "http://testserver/static/default_10.png"
|
||||
}
|
||||
)
|
||||
|
||||
@@ -647,7 +655,7 @@ class TestAccountAPI(UserAPITestCase):
|
||||
response = self.send_get(client, query_parameters='view=shared')
|
||||
self._verify_private_account_response(response, requires_parental_consent=True)
|
||||
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class TestAccountAPITransactions(TransactionTestCase):
|
||||
"""
|
||||
|
||||