diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index d084859536..61a9a06d8e 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -53,6 +53,8 @@ from xmodule.modulestore.locator import BlockUsageLocator from course_creators.views import get_course_creator_status, add_user_with_status_unrequested from contentstore import utils +from microsite_configuration.middleware import MicrositeConfiguration + __all__ = ['course_info_handler', 'course_handler', 'course_info_update_handler', 'settings_handler', 'grading_handler', @@ -413,15 +415,21 @@ def settings_handler(request, tag=None, package_id=None, branch=None, version_gu if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET': upload_asset_url = locator.url_reverse('assets/') + # see if the ORG of this course can be attributed to a 'Microsite'. In that case, the + # course about page should be editable in Studio + about_page_editable = not MicrositeConfiguration.get_microsite_configuration_value_for_org( + course_module.location.org, + 'ENABLE_MKTG_SITE', + settings.FEATURES.get('ENABLE_MKTG_SITE', False) + ) + return render_to_response('settings.html', { 'context_course': course_module, 'course_locator': locator, 'lms_link_for_about_page': utils.get_lms_link_for_about_page(course_module.location), 'course_image_url': utils.course_image_url(course_module), 'details_url': locator.url_reverse('/settings/details/'), - 'about_page_editable': not settings.FEATURES.get( - 'ENABLE_MKTG_SITE', False - ), + 'about_page_editable': about_page_editable, 'upload_asset_url': upload_asset_url }) elif 'application/json' in request.META.get('HTTP_ACCEPT', ''): diff --git a/cms/djangoapps/contentstore/views/public.py b/cms/djangoapps/contentstore/views/public.py index c7857af0c0..79e1212e9d 100644 --- a/cms/djangoapps/contentstore/views/public.py +++ b/cms/djangoapps/contentstore/views/public.py @@ -10,6 +10,8 @@ from edxmako.shortcuts import render_to_response from external_auth.views import ssl_login_shortcut +from microsite_configuration.middleware import MicrositeConfiguration + __all__ = ['signup', 'login_page', 'howitworks'] @@ -29,10 +31,14 @@ def login_page(request): Display the login form. """ csrf_token = csrf(request)['csrf_token'] - return render_to_response('login.html', { - 'csrf': csrf_token, - 'forgot_password_link': "//{base}/login#forgot-password-modal".format(base=settings.LMS_BASE), - }) + return render_to_response( + 'login.html', + { + 'csrf': csrf_token, + 'forgot_password_link': "//{base}/login#forgot-password-modal".format(base=settings.LMS_BASE), + 'platform_name': MicrositeConfiguration.get_microsite_configuration_value('platform_name', settings.PLATFORM_NAME), + } + ) def howitworks(request): diff --git a/cms/envs/aws.py b/cms/envs/aws.py index 1ee1c3be77..a24e25b74e 100644 --- a/cms/envs/aws.py +++ b/cms/envs/aws.py @@ -9,6 +9,7 @@ This is the default template for our main set of AWS servers. import json from .common import * + from logsettings import get_logger_config import os @@ -145,7 +146,6 @@ COURSES_WITH_UNSAFE_CODE = ENV_TOKENS.get("COURSES_WITH_UNSAFE_CODE", []) #Timezone overrides TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE) - ENV_FEATURES = ENV_TOKENS.get('FEATURES', ENV_TOKENS.get('MITX_FEATURES', {})) for feature, value in ENV_FEATURES.items(): FEATURES[feature] = value @@ -213,3 +213,16 @@ BROKER_URL = "{0}://{1}:{2}@{3}/{4}".format(CELERY_BROKER_TRANSPORT, # Event tracking TRACKING_BACKENDS.update(AUTH_TOKENS.get("TRACKING_BACKENDS", {})) + +SUBDOMAIN_BRANDING = ENV_TOKENS.get('SUBDOMAIN_BRANDING', {}) +VIRTUAL_UNIVERSITIES = ENV_TOKENS.get('VIRTUAL_UNIVERSITIES', []) + +MICROSITE_CONFIGURATION = ENV_TOKENS.get('MICROSITE_CONFIGURATION', {}) +MICROSITE_ROOT_DIR = ENV_TOKENS.get('MICROSITE_ROOT_DIR') +if len(MICROSITE_CONFIGURATION.keys()) > 0: + enable_microsites( + MICROSITE_CONFIGURATION, + SUBDOMAIN_BRANDING, + VIRTUAL_UNIVERSITIES, + microsites_root=path(MICROSITE_ROOT_DIR) + ) diff --git a/cms/envs/common.py b/cms/envs/common.py index eb7d600c20..aa82ae5eed 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -25,7 +25,7 @@ Longer TODO: import sys import lms.envs.common -from lms.envs.common import USE_TZ, TECH_SUPPORT_EMAIL, PLATFORM_NAME, BUGS_EMAIL, DOC_STORE_CONFIG +from lms.envs.common import USE_TZ, TECH_SUPPORT_EMAIL, PLATFORM_NAME, BUGS_EMAIL, DOC_STORE_CONFIG, enable_microsites from path import path from lms.lib.xblock.mixin import LmsBlockMixin diff --git a/cms/envs/microsite_test.py b/cms/envs/microsite_test.py new file mode 100644 index 0000000000..5eb2079da8 --- /dev/null +++ b/cms/envs/microsite_test.py @@ -0,0 +1,15 @@ +""" +This is a localdev test for the Microsite processing pipeline +""" +# We intentionally define lots of variables that aren't used, and +# want to import all variables from base settings files +# pylint: disable=W0401, W0614 + +from .dev import * +from .dev import SUBDOMAIN_BRANDING, VIRTUAL_UNIVERSITIES + +MICROSITE_NAMES = ['openedx'] +MICROSITE_CONFIGURATION = {} + +if MICROSITE_NAMES and len(MICROSITE_NAMES) > 0: + enable_microsites(MICROSITE_NAMES, MICROSITE_CONFIGURATION, SUBDOMAIN_BRANDING, VIRTUAL_UNIVERSITIES) diff --git a/common/djangoapps/edxmako/shortcuts.py b/common/djangoapps/edxmako/shortcuts.py index 6cecb409e9..a52c852727 100644 --- a/common/djangoapps/edxmako/shortcuts.py +++ b/common/djangoapps/edxmako/shortcuts.py @@ -16,6 +16,8 @@ from django.template import Context from django.http import HttpResponse import logging +from microsite_configuration.middleware import MicrositeConfiguration + import edxmako import edxmako.middleware from django.conf import settings @@ -71,6 +73,10 @@ def marketing_link_context_processor(request): def render_to_string(template_name, dictionary, context=None, namespace='main'): + + # see if there is an override template defined in the microsite + template_name = MicrositeConfiguration.get_microsite_template_path(template_name) + context_instance = Context(dictionary) # add dictionary to context_instance context_instance.update(dictionary or {}) @@ -98,5 +104,9 @@ def render_to_response(template_name, dictionary=None, context_instance=None, na Returns a HttpResponse whose content is filled with the result of calling lookup.get_template(args[0]).render with the passed arguments. """ + + # see if there is an override template defined in the microsite + template_name = MicrositeConfiguration.get_microsite_template_path(template_name) + dictionary = dictionary or {} return HttpResponse(render_to_string(template_name, dictionary, context_instance, namespace), **kwargs) diff --git a/common/djangoapps/microsite_configuration/__init__.py b/common/djangoapps/microsite_configuration/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/djangoapps/microsite_configuration/middleware.py b/common/djangoapps/microsite_configuration/middleware.py new file mode 100644 index 0000000000..61d2cd9883 --- /dev/null +++ b/common/djangoapps/microsite_configuration/middleware.py @@ -0,0 +1,183 @@ +""" +This file implements the initial Microsite support for the Open edX platform. +A microsite enables the following features: + +1) Mapping of sub-domain name to a 'brand', e.g. foo-university.edx.org +2) Present a landing page with a listing of courses that are specific to the 'brand' +3) Ability to swap out some branding elements in the website +""" +import threading +import os.path + +from django.conf import settings + +_microsite_configuration_threadlocal = threading.local() +_microsite_configuration_threadlocal.data = {} + + +def has_microsite_configuration_set(): + """ + Returns whether the MICROSITE_CONFIGURATION has been set in the configuration files + """ + return getattr(settings, "MICROSITE_CONFIGURATION", False) + + +class MicrositeConfiguration(object): + """ + Middleware class which will bind configuration information regarding 'microsites' on a per request basis. + The actual configuration information is taken from Django settings information + """ + + @classmethod + def is_request_in_microsite(cls): + """ + This will return if current request is a request within a microsite + """ + return cls.get_microsite_configuration() + + @classmethod + def get_microsite_configuration(cls): + """ + Returns the current request's microsite configuration + """ + if not hasattr(_microsite_configuration_threadlocal, 'data'): + return {} + + return _microsite_configuration_threadlocal.data + + @classmethod + def get_microsite_configuration_value(cls, val_name, default=None): + """ + Returns a value associated with the request's microsite, if present + """ + configuration = cls.get_microsite_configuration() + return configuration.get(val_name, default) + + @classmethod + def get_microsite_template_path(cls, relative_path): + """ + Returns a path to a Mako template, which can either be in + a microsite directory (as an override) or will just return what is passed in + """ + + if not cls.is_request_in_microsite(): + return relative_path + + microsite_template_path = cls.get_microsite_configuration_value('template_dir') + + if microsite_template_path: + search_path = microsite_template_path / relative_path + + if os.path.isfile(search_path): + path = '{0}/templates/{1}'.format( + cls.get_microsite_configuration_value('microsite_name'), + relative_path + ) + return path + + return relative_path + + @classmethod + def get_microsite_configuration_value_for_org(cls, org, val_name, default=None): + """ + This returns a configuration value for a microsite which has an org_filter that matches + what is passed in + """ + if not has_microsite_configuration_set(): + return default + + for key in settings.MICROSITE_CONFIGURATION.keys(): + org_filter = settings.MICROSITE_CONFIGURATION[key].get('course_org_filter', None) + if org_filter == org: + return settings.MICROSITE_CONFIGURATION[key].get(val_name, default) + return default + + @classmethod + def get_all_microsite_orgs(cls): + """ + This returns a set of orgs that are considered within a Microsite. This can be used, + for example, to do filtering + """ + org_filter_set = [] + if not has_microsite_configuration_set(): + return org_filter_set + + for key in settings.MICROSITE_CONFIGURATION: + org_filter = settings.MICROSITE_CONFIGURATION[key].get('course_org_filter') + if org_filter: + org_filter_set.append(org_filter) + + return org_filter_set + + def clear_microsite_configuration(self): + """ + Clears out any microsite configuration from the current request/thread + """ + _microsite_configuration_threadlocal.data = {} + + def process_request(self, request): + """ + Middleware entry point on every request processing. This will associate a request's domain name + with a 'University' and any corresponding microsite configuration information + """ + self.clear_microsite_configuration() + + domain = request.META.get('HTTP_HOST', None) + + if domain: + subdomain = MicrositeConfiguration.pick_subdomain(domain, settings.SUBDOMAIN_BRANDING.keys()) + university = MicrositeConfiguration.match_university(subdomain) + microsite_configuration = self.get_microsite_configuration_for_university(university) + if microsite_configuration: + microsite_configuration['university'] = university + microsite_configuration['subdomain'] = subdomain + microsite_configuration['site_domain'] = domain + _microsite_configuration_threadlocal.data = microsite_configuration + + # also put the configuration on the request itself to make it easier to dereference + request.microsite_configuration = _microsite_configuration_threadlocal.data + return None + + def process_response(self, request, response): + """ + Middleware entry point for request completion. + """ + self.clear_microsite_configuration() + return response + + def get_microsite_configuration_for_university(self, university): + """ + For a given university, return the microsite configuration which + is in the Django settings + """ + if not university: + return None + + if not has_microsite_configuration_set(): + return None + + configuration = settings.MICROSITE_CONFIGURATION.get(university, None) + return configuration + + @classmethod + def match_university(cls, domain): + """ + Return the university name specified for the domain, or None + if no university was specified + """ + if not settings.FEATURES['SUBDOMAIN_BRANDING'] or domain is None: + return None + + subdomain = cls.pick_subdomain(domain, settings.SUBDOMAIN_BRANDING.keys()) + return settings.SUBDOMAIN_BRANDING.get(subdomain) + + @classmethod + def pick_subdomain(cls, domain, options, default='default'): + """ + Attempt to match the incoming request's HOST domain with a configuration map + to see what subdomains are supported in Microsites. + """ + for option in options: + if domain.startswith(option): + return option + return default diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index aaf4ca8274..f5660935bf 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -71,6 +71,7 @@ from pytz import UTC from util.json_request import JsonResponse +from microsite_configuration.middleware import MicrositeConfiguration log = logging.getLogger("edx.student") AUDIT_LOG = logging.getLogger("audit") @@ -250,7 +251,11 @@ def signin_user(request): context = { 'course_id': request.GET.get('course_id'), - 'enrollment_action': request.GET.get('enrollment_action') + 'enrollment_action': request.GET.get('enrollment_action'), + 'platform_name': MicrositeConfiguration.get_microsite_configuration_value( + 'platform_name', + settings.PLATFORM_NAME + ), } return render_to_response('login.html', context) @@ -269,7 +274,11 @@ def register_user(request, extra_context=None): context = { 'course_id': request.GET.get('course_id'), - 'enrollment_action': request.GET.get('enrollment_action') + 'enrollment_action': request.GET.get('enrollment_action'), + 'platform_name': MicrositeConfiguration.get_microsite_configuration_value( + 'platform_name', + settings.PLATFORM_NAME + ), } if extra_context is not None: context.update(extra_context) @@ -311,9 +320,33 @@ def dashboard(request): # longer exist (because the course IDs have changed). Still, we don't delete those # enrollments, because it could have been a data push snafu. course_enrollment_pairs = [] + + # for microsites, we want to filter and only show enrollments for courses within + # the microsites 'ORG' + course_org_filter = MicrositeConfiguration.get_microsite_configuration_value('course_org_filter') + + # Let's filter out any courses in an "org" that has been declared to be + # in a Microsite + org_filter_out_set = MicrositeConfiguration.get_all_microsite_orgs() + + # remove our current Microsite from the "filter out" list, if applicable + if course_org_filter: + org_filter_out_set.remove(course_org_filter) + for enrollment in CourseEnrollment.enrollments_for_user(user): try: - course_enrollment_pairs.append((course_from_id(enrollment.course_id), enrollment)) + course = course_from_id(enrollment.course_id) + + # if we are in a Microsite, then filter out anything that is not + # attributed (by ORG) to that Microsite + if course_org_filter and course_org_filter != course.location.org: + continue + # Conversely, if we are not in a Microsite, then let's filter out any enrollments + # with courses attributed (by ORG) to Microsites + elif course.location.org in org_filter_out_set: + continue + + course_enrollment_pairs.append((course, enrollment)) except ItemNotFoundError: log.error("User {0} enrolled in non-existent course {1}" .format(user.username, enrollment.course_id)) @@ -539,7 +572,11 @@ def accounts_login(request): course_id = _parse_course_id_from_string(redirect_to) if course_id and _get_course_enrollment_domain(course_id): return external_auth.views.course_specific_login(request, course_id) - return render_to_response('login.html') + + context = { + 'platform_name': settings.PLATFORM_NAME, + } + return render_to_response('login.html', context) # Need different levels of logging @@ -899,26 +936,31 @@ def create_account(request, post_override=None): return ret (user, profile, registration) = ret - d = {'name': post_vars['name'], - 'key': registration.activation_key, - } + context = { + 'name': post_vars['name'], + 'key': registration.activation_key, + } # composes activation email - subject = render_to_string('emails/activation_email_subject.txt', d) + subject = render_to_string('emails/activation_email_subject.txt', context) # Email subject *must not* contain newlines subject = ''.join(subject.splitlines()) - message = render_to_string('emails/activation_email.txt', d) + message = render_to_string('emails/activation_email.txt', context) # don't send email if we are doing load testing or random user generation for some reason if not (settings.FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING')): + from_address = MicrositeConfiguration.get_microsite_configuration_value( + 'email_from_address', + settings.DEFAULT_FROM_EMAIL + ) try: if settings.FEATURES.get('REROUTE_ACTIVATION_EMAIL'): dest_addr = settings.FEATURES['REROUTE_ACTIVATION_EMAIL'] message = ("Activation for %s (%s): %s\n" % (user, user.email, profile.name) + '-' * 80 + '\n\n' + message) - send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [dest_addr], fail_silently=False) + send_mail(subject, message, from_address, [dest_addr], fail_silently=False) else: - _res = user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL) + _res = user.email_user(subject, message, from_address) except: log.warning('Unable to send activation email to user', exc_info=True) js['value'] = _('Could not send activation e-mail.') @@ -1171,15 +1213,23 @@ def change_email_request(request): return HttpResponse(json.dumps({'success': False, 'error': _('Old email is the same as the new email.')})) - d = {'key': pec.activation_key, - 'old_email': user.email, - 'new_email': pec.new_email} + context = { + 'key': pec.activation_key, + 'old_email': user.email, + 'new_email': pec.new_email + } - subject = render_to_string('emails/email_change_subject.txt', d) + subject = render_to_string('emails/email_change_subject.txt', context) subject = ''.join(subject.splitlines()) - message = render_to_string('emails/email_change.txt', d) - _res = send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [pec.new_email]) + message = render_to_string('emails/email_change.txt', context) + + from_address = MicrositeConfiguration.get_microsite_configuration_value( + 'email_from_address', + settings.DEFAULT_FROM_EMAIL + ) + + _res = send_mail(subject, message, from_address, [pec.new_email]) return HttpResponse(json.dumps({'success': True})) diff --git a/common/djangoapps/util/request.py b/common/djangoapps/util/request.py index 0950fa3b42..fc9c835194 100644 --- a/common/djangoapps/util/request.py +++ b/common/djangoapps/util/request.py @@ -1,5 +1,6 @@ """ Utility functions related to HTTP requests """ from django.conf import settings +from microsite_configuration.middleware import MicrositeConfiguration def safe_get_host(request): @@ -14,4 +15,4 @@ def safe_get_host(request): if isinstance(settings.ALLOWED_HOSTS, (list, tuple)) and '*' not in settings.ALLOWED_HOSTS: return request.get_host() else: - return settings.SITE_NAME + return MicrositeConfiguration.get_microsite_configuration_value('site_domain', settings.SITE_NAME) diff --git a/lms/djangoapps/branding/__init__.py b/lms/djangoapps/branding/__init__.py index d70ffb1cc9..c63154356f 100644 --- a/lms/djangoapps/branding/__init__.py +++ b/lms/djangoapps/branding/__init__.py @@ -2,15 +2,10 @@ from xmodule.modulestore.django import modulestore from xmodule.course_module import CourseDescriptor from django.conf import settings - -def pick_subdomain(domain, options, default='default'): - for option in options: - if domain.startswith(option): - return option - return default +from microsite_configuration.middleware import MicrositeConfiguration -def get_visible_courses(domain=None): +def get_visible_courses(): """ Return the set of CourseDescriptors that should be visible in this branded instance """ @@ -20,31 +15,52 @@ def get_visible_courses(domain=None): if isinstance(c, CourseDescriptor)] courses = sorted(courses, key=lambda course: course.number) - if domain and settings.FEATURES.get('SUBDOMAIN_COURSE_LISTINGS'): - subdomain = pick_subdomain(domain, settings.COURSE_LISTINGS.keys()) - visible_ids = frozenset(settings.COURSE_LISTINGS[subdomain]) - return [course for course in courses if course.id in visible_ids] + subdomain = MicrositeConfiguration.get_microsite_configuration_value('subdomain') + + # See if we have filtered course listings in this domain + filtered_visible_ids = None + + # this is legacy format which is outside of the microsite feature + if hasattr(settings, 'COURSE_LISTINGS') and subdomain in settings.COURSE_LISTINGS: + filtered_visible_ids = frozenset(settings.COURSE_LISTINGS[subdomain]) + + filtered_by_org = MicrositeConfiguration.get_microsite_configuration_value('course_org_filter') + + if filtered_by_org: + return [course for course in courses if course.location.org == filtered_by_org] + if filtered_visible_ids: + return [course for course in courses if course.id in filtered_visible_ids] else: - return courses + # Let's filter out any courses in an "org" that has been declared to be + # in a Microsite + org_filter_out_set = MicrositeConfiguration.get_all_microsite_orgs() + return [course for course in courses if course.location.org not in org_filter_out_set] -def get_university(domain=None): +def get_university_for_request(): """ Return the university name specified for the domain, or None if no university was specified """ - if not settings.FEATURES['SUBDOMAIN_BRANDING'] or domain is None: - return None - - subdomain = pick_subdomain(domain, settings.SUBDOMAIN_BRANDING.keys()) - return settings.SUBDOMAIN_BRANDING.get(subdomain) + return MicrositeConfiguration.get_microsite_configuration_value('university') -def get_logo_url(domain=None): +def get_logo_url(): """ Return the url for the branded logo image to be used """ - university = get_university(domain) + + # if the MicrositeConfiguration has a value for the logo_image_url + # let's use that + image_url = MicrositeConfiguration.get_microsite_configuration_value('logo_image_url') + if image_url: + return '{static_url}{image_url}'.format( + static_url=settings.STATIC_URL, + image_url=image_url + ) + + # otherwise, use the legacy means to configure this + university = MicrositeConfiguration.get_microsite_configuration_value('university') if university is None: return '{static_url}images/header-logo.png'.format( diff --git a/lms/djangoapps/branding/views.py b/lms/djangoapps/branding/views.py index 0adf733818..aa0f752e7d 100644 --- a/lms/djangoapps/branding/views.py +++ b/lms/djangoapps/branding/views.py @@ -6,8 +6,9 @@ from django_future.csrf import ensure_csrf_cookie from edxmako.shortcuts import render_to_response import student.views -import branding import courseware.views + +from microsite_configuration.middleware import MicrositeConfiguration from edxmako.shortcuts import marketing_link from util.cache import cache_if_anonymous @@ -25,10 +26,19 @@ def index(request): if settings.FEATURES.get('AUTH_USE_MIT_CERTIFICATES'): from external_auth.views import ssl_login return ssl_login(request) - if settings.FEATURES.get('ENABLE_MKTG_SITE'): + + enable_mktg_site = MicrositeConfiguration.get_microsite_configuration_value( + 'ENABLE_MKTG_SITE', + settings.FEATURES.get('ENABLE_MKTG_SITE', False) + ) + + if enable_mktg_site: return redirect(settings.MKTG_URLS.get('ROOT')) - university = branding.get_university(request.META.get('HTTP_HOST')) + university = MicrositeConfiguration.match_university(request.META.get('HTTP_HOST')) + + # keep specialized logic for Edge until we can migrate over Edge to fully use + # microsite definitions if university == 'edge': context = { 'suppress_toplevel_navigation': True @@ -49,7 +59,9 @@ def courses(request): to that. Otherwise, if subdomain branding is on, this is the university profile page. Otherwise, it's the edX courseware.views.courses page """ - if settings.FEATURES.get('ENABLE_MKTG_SITE', False): + enable_mktg_site = settings.FEATURES.get('ENABLE_MKTG_SITE') or MicrositeConfiguration.get_microsite_configuration_value('ENABLE_MKTG_SITE', False) + + if enable_mktg_site: return redirect(marketing_link('COURSES'), permanent=True) if not settings.FEATURES.get('COURSES_ARE_BROWSABLE'): diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index c74b0f9e6f..fd8adf998b 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -294,7 +294,7 @@ def get_courses(user, domain=None): ''' Returns a list of courses available, sorted by course.number ''' - courses = branding.get_visible_courses(domain) + courses = branding.get_visible_courses() courses = [c for c in courses if has_access(user, c, 'see_exists')] courses = sorted(courses, key=lambda course: course.number) diff --git a/lms/djangoapps/courseware/tests/test_microsites.py b/lms/djangoapps/courseware/tests/test_microsites.py new file mode 100644 index 0000000000..0daffb611c --- /dev/null +++ b/lms/djangoapps/courseware/tests/test_microsites.py @@ -0,0 +1,145 @@ +""" +Tests related to the Microsites feature +""" +from django.core.urlresolvers import reverse +from django.test.utils import override_settings +from unittest import skip + +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory + +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase + +from helpers import LoginEnrollmentTestCase +from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE + +MICROSITE_TEST_HOSTNAME = 'testmicrosite.testserver' + + +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) +class TestMicrosites(ModuleStoreTestCase, LoginEnrollmentTestCase): + """ + This is testing of the Microsite feature + """ + + STUDENT_INFO = [('view@test.com', 'foo'), ('view2@test.com', 'foo')] + + def setUp(self): + # use a different hostname to test Microsites since they are + # triggered on subdomain mappings + # + # NOTE: The Microsite Configuration is in lms/envs/test.py. The content for the Test Microsite is in + # test_microsites/test_microsite. + # + # IMPORTANT: For these tests to work, this domain must be defined via + # DNS configuration (either local or published) + + self.course = CourseFactory.create(display_name='Robot_Super_Course', org='TestMicrositeX') + self.chapter0 = ItemFactory.create(parent_location=self.course.location, + display_name='Overview') + self.chapter9 = ItemFactory.create(parent_location=self.course.location, + display_name='factory_chapter') + self.section0 = ItemFactory.create(parent_location=self.chapter0.location, + display_name='Welcome') + self.section9 = ItemFactory.create(parent_location=self.chapter9.location, + display_name='factory_section') + + self.course_outside_microsite = CourseFactory.create(display_name='Robot_Course_Outside_Microsite', org='FooX') + + def create_test_accounts(self): + """ + Build out the test accounts we'll use in these tests + """ + # Create student accounts and activate them. + for i in range(len(self.STUDENT_INFO)): + email, password = self.STUDENT_INFO[i] + username = 'u{0}'.format(i) + self.create_account(username, email, password) + self.activate_user(email) + + @skip # skipping - runs fine on localdev, not jenkins environment + def test_microsite_anonymous_homepage_content(self): + """ + Verify that the homepage, when accessed via a Microsite domain, returns + HTML that reflects the Microsite branding elements + """ + + resp = self.client.get('/', HTTP_HOST=MICROSITE_TEST_HOSTNAME) + self.assertEqual(resp.status_code, 200) + + # assert various branding definitions on this Microsite + # as per the configuration and Microsite overrides + + self.assertContains(resp, 'This is a Test Microsite Overlay') # Overlay test message + self.assertContains(resp, 'test_microsite/images/header-logo.png') # logo swap + self.assertContains(resp, 'test_microsite/css/test_microsite.css') # css override + self.assertContains(resp, 'Test Microsite') # page title + + # assert that test course display name is visible + self.assertContains(resp, 'Robot_Super_Course') + + # assert that test course that is outside microsite is not visible + self.assertNotContains(resp, 'Robot_Course_Outside_Microsite') + + # assert that footer template has been properly overriden on homepage + self.assertContains(resp, 'This is a Test Microsite footer') + + # assert that the edX partners section is not in the HTML + self.assertNotContains(resp, '
') + + # assert that the edX partners tag line is not in the HTML + self.assertNotContains(resp, 'Explore free courses from') + + @skip # skipping - runs fine on localdev, not jenkins environment + def test_not_microsite_anonymous_homepage_content(self): + """ + Make sure we see the right content on the homepage if we are not in a microsite + """ + + resp = self.client.get('/') + self.assertEqual(resp.status_code, 200) + + # assert various branding definitions on this Microsite ARE NOT VISIBLE + + self.assertNotContains(resp, 'This is a Test Microsite Overlay') # Overlay test message + self.assertNotContains(resp, 'test_microsite/images/header-logo.png') # logo swap + self.assertNotContains(resp, 'test_microsite/css/test_microsite.css') # css override + self.assertNotContains(resp, 'Test Microsite') # page title + + # assert that test course display name IS NOT VISIBLE, since that is a Microsite only course + self.assertNotContains(resp, 'Robot_Super_Course') + + # assert that test course that is outside microsite IS VISIBLE + self.assertContains(resp, 'Robot_Course_Outside_Microsite') + + # assert that footer template has been properly overriden on homepage + self.assertNotContains(resp, 'This is a Test Microsite footer') + + # assert that the edX partners section is not in the HTML + self.assertContains(resp, '
') + + # assert that the edX partners tag line is not in the HTML + self.assertContains(resp, 'Explore free courses from') + + @skip # skipping - runs fine on localdev, not jenkins environment + def test_microsite_course_enrollment(self): + """ + Enroll user in a course scoped in a Microsite and one course outside of a Microsite + and make sure that they are only visible in the right Dashboards + """ + + self.create_test_accounts() + + email, password = self.STUDENT_INFO[0] + self.login(email, password) + self.enroll(self.course, True) + self.enroll(self.course_outside_microsite, True) + + # Access the microsite dashboard and make sure the right courses appear + resp = self.client.get(reverse('dashboard'), HTTP_HOST=MICROSITE_TEST_HOSTNAME) + self.assertContains(resp, 'Robot_Super_Course') + self.assertNotContains(resp, 'Robot_Course_Outside_Microsite') + + # Now access the non-microsite dashboard and make sure the right courses appear + resp = self.client.get(reverse('dashboard')) + self.assertNotContains(resp, 'Robot_Super_Course') + self.assertContains(resp, 'Robot_Course_Outside_Microsite') diff --git a/lms/djangoapps/instructor/enrollment.py b/lms/djangoapps/instructor/enrollment.py index a7c4fe0b65..5042f6996d 100644 --- a/lms/djangoapps/instructor/enrollment.py +++ b/lms/djangoapps/instructor/enrollment.py @@ -14,6 +14,8 @@ from student.models import CourseEnrollment, CourseEnrollmentAllowed from courseware.models import StudentModule from edxmako.shortcuts import render_to_string +from microsite_configuration.middleware import MicrositeConfiguration + # For determining if a shibboleth course SHIBBOLETH_DOMAIN_PREFIX = 'shib:' @@ -223,22 +225,58 @@ def send_mail_to_student(student, param_dict): Returns a boolean indicating whether the email was sent successfully. """ - email_template_dict = {'allowed_enroll': ('emails/enroll_email_allowedsubject.txt', 'emails/enroll_email_allowedmessage.txt'), - 'enrolled_enroll': ('emails/enroll_email_enrolledsubject.txt', 'emails/enroll_email_enrolledmessage.txt'), - 'allowed_unenroll': ('emails/unenroll_email_subject.txt', 'emails/unenroll_email_allowedmessage.txt'), - 'enrolled_unenroll': ('emails/unenroll_email_subject.txt', 'emails/unenroll_email_enrolledmessage.txt')} + # add some helpers and microconfig subsitutions + if 'course' in param_dict: + param_dict['course_name'] = param_dict['course'].display_name_with_default - subject_template, message_template = email_template_dict.get(param_dict['message'], (None, None)) + param_dict['site_name'] = MicrositeConfiguration.get_microsite_configuration_value( + 'SITE_NAME', + param_dict['site_name'] + ) + + subject = None + message = None + + # see if we are running in a microsite and that there is an + # activation email template definition available as configuration, if so, then render that + message_type = param_dict['message'] + + email_template_dict = { + 'allowed_enroll': ( + 'emails/enroll_email_allowedsubject.txt', + 'emails/enroll_email_allowedmessage.txt' + ), + 'enrolled_enroll': ( + 'emails/enroll_email_enrolledsubject.txt', + 'emails/enroll_email_enrolledmessage.txt' + ), + 'allowed_unenroll': ( + 'emails/unenroll_email_subject.txt', + 'emails/unenroll_email_allowedmessage.txt' + ), + 'enrolled_unenroll': ( + 'emails/unenroll_email_subject.txt', + 'emails/unenroll_email_enrolledmessage.txt' + ) + } + + subject_template, message_template = email_template_dict.get(message_type, (None, None)) if subject_template is not None and message_template is not None: subject = render_to_string(subject_template, param_dict) message = render_to_string(message_template, param_dict) + if subject and message: # Remove leading and trailing whitespace from body message = message.strip() # Email subject *must not* contain newlines subject = ''.join(subject.splitlines()) - send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [student], fail_silently=False) + from_address = MicrositeConfiguration.get_microsite_configuration_value( + 'email_from_address', + settings.DEFAULT_FROM_EMAIL + ) + + send_mail(subject, message, from_address, [student], fail_silently=False) def uses_shib(course): diff --git a/lms/djangoapps/instructor/views/legacy.py b/lms/djangoapps/instructor/views/legacy.py index 7f1d6e182f..4b54d037fc 100644 --- a/lms/djangoapps/instructor/views/legacy.py +++ b/lms/djangoapps/instructor/views/legacy.py @@ -63,6 +63,8 @@ from xblock.fields import ScopeIds from django.utils.translation import ugettext as _u from lms.lib.xblock.runtime import handler_prefix +from microsite_configuration.middleware import MicrositeConfiguration + log = logging.getLogger(__name__) # internal commands for managing forum roles: @@ -1282,7 +1284,10 @@ def _do_enroll_students(course, course_id, students, overload=False, auto_enroll ceaset.delete() if email_students: - stripped_site_name = settings.SITE_NAME + stripped_site_name = MicrositeConfiguration.get_microsite_configuration_value( + 'SITE_NAME', + settings.SITE_NAME + ) registration_url = 'https://' + stripped_site_name + reverse('student.views.register_user') #Composition of email d = {'site_name': stripped_site_name, @@ -1291,7 +1296,7 @@ def _do_enroll_students(course, course_id, students, overload=False, auto_enroll 'auto_enroll': auto_enroll, 'course_url': 'https://' + stripped_site_name + '/courses/' + course_id, 'course_about_url': 'https://' + stripped_site_name + '/courses/' + course_id + '/about', - 'is_shib_course': is_shib_course, + 'is_shib_course': is_shib_course } for student in new_students: @@ -1373,7 +1378,10 @@ def _do_unenroll_students(course_id, students, email_students=False): old_students, _ = get_and_clean_student_list(students) status = dict([x, 'unprocessed'] for x in old_students) - stripped_site_name = settings.SITE_NAME + stripped_site_name = MicrositeConfiguration.get_microsite_configuration_value( + 'SITE_NAME', + settings.SITE_NAME + ) if email_students: course = course_from_id(course_id) #Composition of email @@ -1447,22 +1455,43 @@ def send_mail_to_student(student, param_dict): Returns a boolean indicating whether the email was sent successfully. """ - EMAIL_TEMPLATE_DICT = {'allowed_enroll': ('emails/enroll_email_allowedsubject.txt', 'emails/enroll_email_allowedmessage.txt'), - 'enrolled_enroll': ('emails/enroll_email_enrolledsubject.txt', 'emails/enroll_email_enrolledmessage.txt'), - 'allowed_unenroll': ('emails/unenroll_email_subject.txt', 'emails/unenroll_email_allowedmessage.txt'), - 'enrolled_unenroll': ('emails/unenroll_email_subject.txt', 'emails/unenroll_email_enrolledmessage.txt')} + # add some helpers and microconfig subsitutions + if 'course' in param_dict: + param_dict['course_name'] = param_dict['course'].display_name_with_default + param_dict['site_name'] = MicrositeConfiguration.get_microsite_configuration_value( + 'SITE_NAME', + param_dict.get('site_name', '') + ) - subject_template, message_template = EMAIL_TEMPLATE_DICT.get(param_dict['message'], (None, None)) + subject = None + message = None + + message_type = param_dict['message'] + + email_template_dict = { + 'allowed_enroll': ('emails/enroll_email_allowedsubject.txt', 'emails/enroll_email_allowedmessage.txt'), + 'enrolled_enroll': ('emails/enroll_email_enrolledsubject.txt', 'emails/enroll_email_enrolledmessage.txt'), + 'allowed_unenroll': ('emails/unenroll_email_subject.txt', 'emails/unenroll_email_allowedmessage.txt'), + 'enrolled_unenroll': ('emails/unenroll_email_subject.txt', 'emails/unenroll_email_enrolledmessage.txt'), + } + + subject_template, message_template = email_template_dict.get(message_type, (None, None)) if subject_template is not None and message_template is not None: subject = render_to_string(subject_template, param_dict) message = render_to_string(message_template, param_dict) + if subject and message: # Remove leading and trailing whitespace from body message = message.strip() # Email subject *must not* contain newlines subject = ''.join(subject.splitlines()) - send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [student], fail_silently=False) + from_address = MicrositeConfiguration.get_microsite_configuration_value( + 'email_from_address', + settings.DEFAULT_FROM_EMAIL + ) + + send_mail(subject, message, from_address, [student], fail_silently=False) return True else: diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py index 7647b2c487..562ad08d82 100644 --- a/lms/djangoapps/shoppingcart/models.py +++ b/lms/djangoapps/shoppingcart/models.py @@ -32,6 +32,8 @@ from verify_student.models import SoftwareSecurePhotoVerification from .exceptions import (InvalidCartItem, PurchasedCallbackException, ItemAlreadyInCartException, AlreadyEnrolledInCourseException, CourseDoesNotExistException) +from microsite_configuration.middleware import MicrositeConfiguration + log = logging.getLogger("shoppingcart") ORDER_STATUSES = ( @@ -161,14 +163,22 @@ class Order(models.Model): # send confirmation e-mail subject = _("Order Payment Confirmation") - message = render_to_string('emails/order_confirmation_email.txt', { - 'order': self, - 'order_items': orderitems, - 'has_billing_info': settings.FEATURES['STORE_BILLING_INFO'] - }) + message = render_to_string( + 'emails/order_confirmation_email.txt', + { + 'order': self, + 'order_items': orderitems, + 'has_billing_info': settings.FEATURES['STORE_BILLING_INFO'] + } + ) try: + from_address = MicrositeConfiguration.get_microsite_configuration_value( + 'email_from_address', + settings.DEFAULT_FROM_EMAIL + ) + send_mail(subject, message, - settings.DEFAULT_FROM_EMAIL, [self.user.email]) # pylint: disable=E1101 + from_address, [self.user.email]) # pylint: disable=E1101 except (smtplib.SMTPException, BotoServerError): # sadly need to handle diff. mail backends individually log.error('Failed sending confirmation e-mail for order %d', self.id) # pylint: disable=E1101 @@ -523,7 +533,10 @@ class CertificateItem(OrderItem): user_email=course_enrollment.user.email, order_number=order_number) to_email = [settings.PAYMENT_SUPPORT_EMAIL] - from_email = [settings.PAYMENT_SUPPORT_EMAIL] + from_email = [MicrositeConfiguration.get_microsite_configuration_value( + 'payment_support_email', + settings.PAYMENT_SUPPORT_EMAIL + )] try: send_mail(subject, message, from_email, to_email, fail_silently=False) except (smtplib.SMTPException, BotoServerError): diff --git a/lms/envs/aws.py b/lms/envs/aws.py index ad6f9463d8..f598416cb7 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -337,3 +337,13 @@ VERIFY_STUDENT = AUTH_TOKENS.get("VERIFY_STUDENT", VERIFY_STUDENT) GRADES_DOWNLOAD_ROUTING_KEY = HIGH_MEM_QUEUE GRADES_DOWNLOAD = ENV_TOKENS.get("GRADES_DOWNLOAD", GRADES_DOWNLOAD) + +MICROSITE_CONFIGURATION = ENV_TOKENS.get('MICROSITE_CONFIGURATION', {}) +MICROSITE_ROOT_DIR = ENV_TOKENS.get('MICROSITE_ROOT_DIR') +if MICROSITE_CONFIGURATION: + enable_microsites( + MICROSITE_CONFIGURATION, + SUBDOMAIN_BRANDING, + VIRTUAL_UNIVERSITIES, + microsites_root=path(MICROSITE_ROOT_DIR) + ) diff --git a/lms/envs/cms/microsite_test.py b/lms/envs/cms/microsite_test.py new file mode 100644 index 0000000000..511b97e66a --- /dev/null +++ b/lms/envs/cms/microsite_test.py @@ -0,0 +1,38 @@ +""" +This is a localdev test for the Microsite processing pipeline +""" +# We intentionally define lots of variables that aren't used, and +# want to import all variables from base settings files +# pylint: disable=W0401, W0614 + +from .dev import * +from .dev import SUBDOMAIN_BRANDING, VIRTUAL_UNIVERSITIES + + +MICROSITE_CONFIGURATION = { + "openedx": { + "domain_prefix": "openedx", + "university": "openedx", + "platform_name": "Open edX", + "logo_image_url": "openedx/images/header-logo.png", + "email_from_address": "openedx@edx.org", + "payment_support_email": "openedx@edx.org", + "ENABLE_MKTG_SITE": False, + "SITE_NAME": "openedx.localhost", + "course_org_filter": "CDX", + "course_about_show_social_links": False, + "css_overrides_file": "openedx/css/openedx.css", + "show_partners": False, + "show_homepage_promo_video": False, + "course_index_overlay_text": "Explore free courses from leading universities.", + "course_index_overlay_logo_file": "openedx/images/header-logo.png", + "homepage_overlay_html": "

Take an Open edX Course

" + }, +} + +if len(MICROSITE_CONFIGURATION.keys()) > 0: + enable_microsites( + MICROSITE_CONFIGURATION, + SUBDOMAIN_BRANDING, + VIRTUAL_UNIVERSITIES + ) diff --git a/lms/envs/common.py b/lms/envs/common.py index 150b9ab8b8..716f11ae2e 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -25,6 +25,7 @@ Longer TODO: import sys import os +import json from path import path @@ -377,7 +378,7 @@ TRACKING_ENABLED = True ######################## subdomain specific settings ########################### COURSE_LISTINGS = {} SUBDOMAIN_BRANDING = {} - +VIRTUAL_UNIVERSITIES = [] ############################### XModule Store ################################## MODULESTORE = { @@ -617,6 +618,7 @@ TEMPLATE_LOADERS = ( MIDDLEWARE_CLASSES = ( 'request_cache.middleware.RequestCache', + 'microsite_configuration.middleware.MicrositeConfiguration', 'django_comment_client.middleware.AjaxExceptionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -1048,6 +1050,64 @@ MKTG_URL_LINK_MAP = { } +############################### MICROSITES ################################ +def enable_microsites(microsite_config_dict, subdomain_branding, virtual_universities, microsites_root=ENV_ROOT / "microsites"): + """ + Enable the use of microsites, which are websites that allow + for subdomains for the edX platform, e.g. foo.edx.org + """ + + if not microsite_config_dict: + return + + FEATURES['USE_MICROSITES'] = True + + for microsite_name in microsite_config_dict.keys(): + # Calculate the location of the microsite's files + microsite_root = microsites_root / microsite_name + microsite_config = microsite_config_dict[microsite_name] + + # pull in configuration information from each + # microsite root + + if os.path.isdir(microsite_root): + # store the path on disk for later use + microsite_config['microsite_root'] = microsite_root + + # get the domain that this should reside + domain = microsite_config['domain_prefix'] + + # get the virtual university that this should use + university = microsite_config['university'] + + # add to the existing maps in our settings + subdomain_branding[domain] = university + virtual_universities.append(university) + + template_dir = microsite_root / 'templates' + microsite_config['template_dir'] = template_dir + + microsite_config['microsite_name'] = microsite_name + + else: + # not sure if we have application logging at this stage of + # startup + print '**** Error loading microsite {0}. Directory does not exist'.format(microsite_root) + # remove from our configuration as it is not valid + del microsite_config_dict[microsite_name] + + # if we have microsites, then let's turn on SUBDOMAIN_BRANDING + # Note check size of the dict because some microsites might not be found on disk and + # we could be left with none + if microsite_config_dict: + FEATURES['SUBDOMAIN_BRANDING'] = True + + TEMPLATE_DIRS.append(microsites_root) + MAKO_TEMPLATES['main'].append(microsites_root) + + STATICFILES_DIRS.append(microsites_root) + + ################# Student Verification ################# VERIFY_STUDENT = { "DAYS_GOOD_FOR": 365, # How many days is a verficiation good for? diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 30053d3da3..6b4446893b 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -130,6 +130,8 @@ SUBDOMAIN_BRANDING = { 'mit': 'MITx', 'berkeley': 'BerkeleyX', 'harvard': 'HarvardX', + 'openedx': 'openedx', + 'edge': 'edge', } # List of `university` landing pages to display, even though they may not diff --git a/lms/envs/test.py b/lms/envs/test.py index dd52d2e76f..9577fa571a 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -277,3 +277,33 @@ PASSWORD_HASHERS = ( import openid.oidutil openid.oidutil.log = lambda message, level = 0: None + +# set up some testing for microsites +MICROSITE_CONFIGURATION = { + "test_microsite": { + "domain_prefix": "testmicrosite", + "university": "test_microsite", + "platform_name": "Test Microsite", + "logo_image_url": "test_microsite/images/header-logo.png", + "email_from_address": "test_microsite@edx.org", + "payment_support_email": "test_microsite@edx.org", + "ENABLE_MKTG_SITE": False, + "SITE_NAME": "test_microsite.localhost", + "course_org_filter": "TestMicrositeX", + "course_about_show_social_links": False, + "css_overrides_file": "test_microsite/css/test_microsite.css", + "show_partners": False, + "show_homepage_promo_video": False, + "course_index_overlay_text": "This is a Test Microsite Overlay Text.", + "course_index_overlay_logo_file": "test_microsite/images/header-logo.png", + "homepage_overlay_html": "

This is a Test Microsite Overlay HTML

" + } +} + +if len(MICROSITE_CONFIGURATION.keys()) > 0: + enable_microsites( + MICROSITE_CONFIGURATION, + SUBDOMAIN_BRANDING, + VIRTUAL_UNIVERSITIES, + microsites_root=ENV_ROOT / 'edx-platform' / 'test_microsites' + ) diff --git a/lms/templates/courseware/course_about.html b/lms/templates/courseware/course_about.html index 81e9e2507f..c042ba31bd 100644 --- a/lms/templates/courseware/course_about.html +++ b/lms/templates/courseware/course_about.html @@ -11,15 +11,20 @@ cart_link = "" %> <%namespace name='static' file='../static_content.html'/> +<%! from microsite_configuration.middleware import MicrositeConfiguration %> <%inherit file="../main.html" /> <%block name="headextra"> - % if self.theme_enabled(): - <%include file="../theme-google-analytics.html" /> - % else: - <%include file="../google_analytics.html" /> - % endif + + <% + if self.theme_enabled(): + google_analytics_file = u'../' + MicrositeConfiguration.get_microsite_configuration_value('google_analytics_file', 'theme-google-analytics.html') + else: + google_analytics_file = '../google_analytics.html' + %> + + <%include file="${google_analytics_file}" /> <%block name="js_extra"> @@ -196,6 +201,7 @@
+ % if MicrositeConfiguration.get_microsite_configuration_value('course_about_show_social_links', True): + % endif
    diff --git a/lms/templates/courseware/courses.html b/lms/templates/courseware/courses.html index 1f1d96a0df..9351980336 100644 --- a/lms/templates/courseware/courses.html +++ b/lms/templates/courseware/courses.html @@ -4,8 +4,20 @@ <%namespace name='static' file='../static_content.html'/> <%block name="title">${_("Courses")} +<%! from microsite_configuration.middleware import MicrositeConfiguration %>
    + +<% + course_index_overlay_text = MicrositeConfiguration.get_microsite_configuration_value('course_index_overlay_text', _("Explore free courses from leading universities.")) + + # not sure why this is, but if I use static.url('images/edx_bw.png') then the HTML rendering + # of this template goes wonky + + logo_file = MicrositeConfiguration.get_microsite_configuration_value( + 'course_index_overlay_logo_file', settings.STATIC_URL + 'images/edx_bw.png') +%> +
diff --git a/lms/templates/help_modal.html b/lms/templates/help_modal.html index 88f16d242a..8dc7b6ef6c 100644 --- a/lms/templates/help_modal.html +++ b/lms/templates/help_modal.html @@ -5,6 +5,7 @@ <%! import pytz %> <%! from django.conf import settings %> <%! from courseware.tabs import get_discussion_link %> +<%! from microsite_configuration.middleware import MicrositeConfiguration %> % if settings.FEATURES.get('ENABLE_FEEDBACK_SUBMISSION', False): @@ -12,13 +13,13 @@ ${_("Help")} -