Commit 23f02d94 by Daniel Friedman Committed by Andy Armstrong

Configurations for filesystem and s3 backends.

TNL-1789
parent 44c78c60
...@@ -40,8 +40,8 @@ from lms.envs.common import ( ...@@ -40,8 +40,8 @@ from lms.envs.common import (
# The following PROFILE_IMAGE_* settings are included as they are # The following PROFILE_IMAGE_* settings are included as they are
# 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_DEFAULT_FILENAME, PROFILE_IMAGE_DEFAULT_FILE_EXTENSION,
PROFILE_IMAGE_DEFAULT_FILE_EXTENSION, PROFILE_IMAGE_SECRET_KEY, PROFILE_IMAGE_SECRET_KEY,
) )
from path import path from path import path
from warnings import simplefilter from warnings import simplefilter
......
...@@ -131,6 +131,10 @@ if STATIC_URL_BASE: ...@@ -131,6 +131,10 @@ if STATIC_URL_BASE:
if not STATIC_URL.endswith("/"): if not STATIC_URL.endswith("/"):
STATIC_URL += "/" STATIC_URL += "/"
# MEDIA_ROOT specifies the directory where user-uploaded files are stored.
MEDIA_ROOT = ENV_TOKENS.get('MEDIA_ROOT', MEDIA_ROOT)
MEDIA_URL = ENV_TOKENS.get('MEDIA_URL', MEDIA_URL)
PLATFORM_NAME = ENV_TOKENS.get('PLATFORM_NAME', PLATFORM_NAME) PLATFORM_NAME = ENV_TOKENS.get('PLATFORM_NAME', PLATFORM_NAME)
# For displaying on the receipt. At Stanford PLATFORM_NAME != MERCHANT_NAME, but PLATFORM_NAME is a fine default # For displaying on the receipt. At Stanford PLATFORM_NAME != MERCHANT_NAME, but PLATFORM_NAME is a fine default
PLATFORM_TWITTER_ACCOUNT = ENV_TOKENS.get('PLATFORM_TWITTER_ACCOUNT', PLATFORM_TWITTER_ACCOUNT) PLATFORM_TWITTER_ACCOUNT = ENV_TOKENS.get('PLATFORM_TWITTER_ACCOUNT', PLATFORM_TWITTER_ACCOUNT)
...@@ -594,3 +598,7 @@ if FEATURES.get('INDIVIDUAL_DUE_DATES'): ...@@ -594,3 +598,7 @@ if FEATURES.get('INDIVIDUAL_DUE_DATES'):
FIELD_OVERRIDE_PROVIDERS += ( FIELD_OVERRIDE_PROVIDERS += (
'courseware.student_field_overrides.IndividualStudentOverrideProvider', 'courseware.student_field_overrides.IndividualStudentOverrideProvider',
) )
# PROFILE IMAGE CONFIG
PROFILE_IMAGE_BACKEND = ENV_TOKENS.get('PROFILE_IMAGE_BACKEND', PROFILE_IMAGE_BACKEND)
...@@ -780,6 +780,10 @@ STATICFILES_DIRS = [ ...@@ -780,6 +780,10 @@ STATICFILES_DIRS = [
FAVICON_PATH = 'images/favicon.ico' FAVICON_PATH = 'images/favicon.ico'
# User-uploaded content
MEDIA_ROOT = '/edx/var/edxapp/media/'
MEDIA_URL = '/media/'
# Locale/Internationalization # Locale/Internationalization
TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html
...@@ -2258,20 +2262,19 @@ CHECKPOINT_PATTERN = r'(?P<checkpoint_name>\w+)' ...@@ -2258,20 +2262,19 @@ CHECKPOINT_PATTERN = r'(?P<checkpoint_name>\w+)'
FIELD_OVERRIDE_PROVIDERS = () FIELD_OVERRIDE_PROVIDERS = ()
# PROFILE IMAGE CONFIG # PROFILE IMAGE CONFIG
# TODO: add these settings to aws.py as well
# WARNING: Certain django storage backends do not support atomic # WARNING: Certain django storage backends do not support atomic
# file overwrites (including the default, specified below) - instead # file overwrites (including the default, OverwriteStorage) - instead
# there are separate calls to delete and then write a new file in the # there are separate calls to delete and then write a new file in the
# storage backend. This introduces the risk of a race condition # storage backend. This introduces the risk of a race condition
# occurring when a user uploads a new profile image to replace an # occurring when a user uploads a new profile image to replace an
# earlier one (the file will temporarily be deleted). # earlier one (the file will temporarily be deleted).
PROFILE_IMAGE_BACKEND = 'storages.backends.overwrite.OverwriteStorage' PROFILE_IMAGE_BACKEND = {
# PROFILE_IMAGE_DOMAIN points to the domain from which we serve image 'class': 'storages.backends.overwrite.OverwriteStorage',
# files from. When this is '/', it refers to the same domain as the 'options': {
# app server. If serving from a different domain, specify that here 'location': os.path.join(MEDIA_ROOT, 'profile-images/'),
# i.e. 'http://www.example-image-server.com/' 'base_url': os.path.join(MEDIA_URL, 'profile-images/'),
PROFILE_IMAGE_DOMAIN = '/' },
PROFILE_IMAGE_URL_PATH = 'media/profile_images/' }
PROFILE_IMAGE_DEFAULT_FILENAME = ( PROFILE_IMAGE_DEFAULT_FILENAME = (
'images/edx-theme/default-profile' if FEATURES['IS_EDX_DOMAIN'] else 'images/default-theme/default-profile' 'images/edx-theme/default-profile' if FEATURES['IS_EDX_DOMAIN'] else 'images/default-theme/default-profile'
) )
......
...@@ -478,9 +478,13 @@ MIDDLEWARE_CLASSES += ('ccx.overrides.CcxMiddleware',) ...@@ -478,9 +478,13 @@ MIDDLEWARE_CLASSES += ('ccx.overrides.CcxMiddleware',)
FEATURES['CUSTOM_COURSES_EDX'] = True FEATURES['CUSTOM_COURSES_EDX'] = True
# Set dummy values for profile image settings. # Set dummy values for profile image settings.
PROFILE_IMAGE_BACKEND = 'django.core.files.storage.FileSystemStorage' PROFILE_IMAGE_BACKEND = {
PROFILE_IMAGE_DOMAIN = 'http://example-storage.com/' 'class': 'storages.backends.overwrite.OverwriteStorage',
PROFILE_IMAGE_URL_PATH = 'profile_images/' 'options': {
'location': MEDIA_ROOT,
'base_url': 'http://example-storage.com/profile-images/',
},
}
PROFILE_IMAGE_DEFAULT_FILENAME = 'default' PROFILE_IMAGE_DEFAULT_FILENAME = 'default'
PROFILE_IMAGE_DEFAULT_FILE_EXTENSION = 'png' PROFILE_IMAGE_DEFAULT_FILE_EXTENSION = 'png'
PROFILE_IMAGE_SECRET_KEY = 'secret' PROFILE_IMAGE_SECRET_KEY = 'secret'
......
...@@ -639,7 +639,8 @@ urlpatterns = patterns(*urlpatterns) ...@@ -639,7 +639,8 @@ urlpatterns = patterns(*urlpatterns)
if settings.DEBUG: if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static( urlpatterns += static(
settings.PROFILE_IMAGE_DOMAIN + settings.PROFILE_IMAGE_URL_PATH, document_root=settings.MEDIA_ROOT settings.PROFILE_IMAGE_BACKEND['options']['base_url'],
document_root=settings.PROFILE_IMAGE_BACKEND['options']['location']
) )
# in debug mode, allow any template to be rendered (most useful for UX reference templates) # in debug mode, allow any template to be rendered (most useful for UX reference templates)
......
...@@ -27,8 +27,9 @@ def get_profile_image_storage(): ...@@ -27,8 +27,9 @@ def get_profile_image_storage():
Configures and returns a django Storage instance that can be used Configures and returns a django Storage instance that can be used
to physically locate, read and write profile images. to physically locate, read and write profile images.
""" """
storage_class = get_storage_class(settings.PROFILE_IMAGE_BACKEND) config = settings.PROFILE_IMAGE_BACKEND
return storage_class(base_url=(settings.PROFILE_IMAGE_DOMAIN + settings.PROFILE_IMAGE_URL_PATH)) storage_class = get_storage_class(config['class'])
return storage_class(**config['options'])
def _make_profile_image_name(username): def _make_profile_image_name(username):
...@@ -75,7 +76,7 @@ def get_profile_image_urls_for_user(user): ...@@ -75,7 +76,7 @@ def get_profile_image_urls_for_user(user):
callers will use `_get_default_profile_image_urls` instead to provide callers will use `_get_default_profile_image_urls` instead to provide
a set of urls that point to placeholder images, when there are no user- a set of urls that point to placeholder images, when there are no user-
submitted images. submitted images.
- based on the value of django.conf.settings.PROFILE_IMAGE_DOMAIN, - based on the value of django.conf.settings.PROFILE_IMAGE_BACKEND,
the URL may be relative, and in that case the caller is responsible for the URL may be relative, and in that case the caller is responsible for
constructing the full URL if needed. constructing the full URL if needed.
......
...@@ -34,7 +34,7 @@ class ProfileImageUrlTestCase(TestCase): ...@@ -34,7 +34,7 @@ class ProfileImageUrlTestCase(TestCase):
""" """
self.assertEqual( self.assertEqual(
actual_url, actual_url,
'http://example-storage.com/profile_images/{name}_{size}.jpg'.format( 'http://example-storage.com/profile-images/{name}_{size}.jpg'.format(
name=expected_name, size=expected_pixels name=expected_name, size=expected_pixels
) )
) )
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import datetime import datetime
from copy import deepcopy
import ddt import ddt
import hashlib import hashlib
import json import json
...@@ -19,6 +20,12 @@ from openedx.core.djangoapps.user_api.preferences.api import set_user_preference ...@@ -19,6 +20,12 @@ from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
from .. import PRIVATE_VISIBILITY, ALL_USERS_VISIBILITY from .. import PRIVATE_VISIBILITY, ALL_USERS_VISIBILITY
# this is used in one test to check the behavior of profile image url
# generation with a relative url in the config.
TEST_PROFILE_IMAGE_BACKEND = deepcopy(settings.PROFILE_IMAGE_BACKEND)
TEST_PROFILE_IMAGE_BACKEND['options']['base_url'] = '/profile-images/'
class UserAPITestCase(APITestCase): class UserAPITestCase(APITestCase):
""" """
The base class for all tests of the User API The base class for all tests of the User API
...@@ -117,7 +124,7 @@ class TestAccountAPI(UserAPITestCase): ...@@ -117,7 +124,7 @@ class TestAccountAPI(UserAPITestCase):
image. image.
""" """
if has_profile_image: if has_profile_image:
url_root = 'http://example-storage.com/profile_images' 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' file_extension = 'jpg'
else: else:
...@@ -593,12 +600,12 @@ class TestAccountAPI(UserAPITestCase): ...@@ -593,12 +600,12 @@ class TestAccountAPI(UserAPITestCase):
) )
self.assertIsNone(error_response.data["user_message"]) self.assertIsNone(error_response.data["user_message"])
@override_settings(PROFILE_IMAGE_DOMAIN='/') @override_settings(PROFILE_IMAGE_BACKEND=TEST_PROFILE_IMAGE_BACKEND)
def test_convert_relative_profile_url(self): def test_convert_relative_profile_url(self):
""" """
Test that when PROFILE_IMAGE_DOMAIN is set to '/', the API Test that when TEST_PROFILE_IMAGE_BACKEND['base_url'] begins
generates the full URL to profile images based on the URL with a '/', the API generates the full URL to profile images based on
of the request. the URL of the request.
""" """
self.client.login(username=self.user.username, password=self.test_password) self.client.login(username=self.user.username, password=self.test_password)
response = self.send_get(self.client) response = self.send_get(self.client)
......
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