diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index d0848595367d..61a9a06d8e37 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -53,6 +53,8 @@ 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 c7857af0c0a9..79e1212e9dd6 100644 --- a/cms/djangoapps/contentstore/views/public.py +++ b/cms/djangoapps/contentstore/views/public.py @@ -10,6 +10,8 @@ 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 1ee1c3be7775..a24e25b74e01 100644 --- a/cms/envs/aws.py +++ b/cms/envs/aws.py @@ -9,6 +9,7 @@ import json from .common import * + from logsettings import get_logger_config import os @@ -145,7 +146,6 @@ #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 @@ # 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 eb7d600c209d..aa82ae5eed50 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -25,7 +25,7 @@ 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 000000000000..5eb2079da8de --- /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 6cecb409e948..d70e2145ddcc 100644 --- a/common/djangoapps/edxmako/shortcuts.py +++ b/common/djangoapps/edxmako/shortcuts.py @@ -16,6 +16,8 @@ from django.http import HttpResponse import logging +from microsite_configuration.middleware import MicrositeConfiguration + import edxmako import edxmako.middleware from django.conf import settings @@ -35,13 +37,18 @@ def marketing_link(name): # link_map maps URLs from the marketing site to the old equivalent on # the Django site link_map = settings.MKTG_URL_LINK_MAP - if settings.FEATURES.get('ENABLE_MKTG_SITE') and name in settings.MKTG_URLS: + enable_mktg_site = MicrositeConfiguration.get_microsite_configuration_value( + 'ENABLE_MKTG_SITE', + settings.FEATURES.get('ENABLE_MKTG_SITE', False) + ) + + if enable_mktg_site and name in settings.MKTG_URLS: # special case for when we only want the root marketing URL if name == 'ROOT': return settings.MKTG_URLS.get('ROOT') return settings.MKTG_URLS.get('ROOT') + settings.MKTG_URLS.get(name) # only link to the old pages when the marketing site isn't on - elif not settings.FEATURES.get('ENABLE_MKTG_SITE') and name in link_map: + elif not enable_mktg_site and name in link_map: # don't try to reverse disabled marketing links if link_map[name] is not None: return reverse(link_map[name]) @@ -71,6 +78,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 +109,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 000000000000..e69de29bb2d1 diff --git a/common/djangoapps/microsite_configuration/middleware.py b/common/djangoapps/microsite_configuration/middleware.py new file mode 100644 index 000000000000..61d2cd98839b --- /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 aaf4ca827424..f5660935bf6d 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -71,6 +71,7 @@ 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 0950fa3b424e..fc9c83519490 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/common/test/test_microsites/test_microsite/css/test_microsite.css b/common/test/test_microsites/test_microsite/css/test_microsite.css new file mode 100644 index 000000000000..0ad6d4247703 --- /dev/null +++ b/common/test/test_microsites/test_microsite/css/test_microsite.css @@ -0,0 +1,15 @@ +.find-courses header.search, .university-profile header.search { + background-image: url("../images/background-image.jpg"); +} + +.course-info header.course-profile { + background: url("../images/background-image.jpg") repeat scroll 0 0 / cover #F5F5F5; +} + +.view-login .introduction header { + background-image: url("../images/login-and-register-banner.png"); +} + +.view-register .introduction header { + background-image: url("../images/login-and-register-banner.png"); +} \ No newline at end of file diff --git a/common/test/test_microsites/test_microsite/images/background-image.jpg b/common/test/test_microsites/test_microsite/images/background-image.jpg new file mode 100644 index 000000000000..61da5545c1e5 Binary files /dev/null and b/common/test/test_microsites/test_microsite/images/background-image.jpg differ diff --git a/common/test/test_microsites/test_microsite/images/header-logo.png b/common/test/test_microsites/test_microsite/images/header-logo.png new file mode 100644 index 000000000000..df8cb1323366 Binary files /dev/null and b/common/test/test_microsites/test_microsite/images/header-logo.png differ diff --git a/common/test/test_microsites/test_microsite/images/login-and-register-banner.png b/common/test/test_microsites/test_microsite/images/login-and-register-banner.png new file mode 100644 index 000000000000..13aa7fd9a6e2 Binary files /dev/null and b/common/test/test_microsites/test_microsite/images/login-and-register-banner.png differ diff --git a/common/test/test_microsites/test_microsite/templates/emails/activation_email.txt b/common/test/test_microsites/test_microsite/templates/emails/activation_email.txt new file mode 100644 index 000000000000..97b3c4f5e06b --- /dev/null +++ b/common/test/test_microsites/test_microsite/templates/emails/activation_email.txt @@ -0,0 +1,16 @@ +<%! from django.utils.translation import ugettext as _ %> + +${_("Thank you for signing up for Open edX! To activate " +"your account, please copy and paste this address into your web " +"browser's address bar:")} + +% if is_secure: + https://${ site }/activate/${ key } +% else: + http://${ site }/activate/${ key } +% endif + +${_("If you didn't request this, you don't need to do anything; you won't " + "receive any more email from us. Please do not reply to this e-mail; " + "if you require assistance, check the help section of the " + "Open edX web site.")} diff --git a/common/test/test_microsites/test_microsite/templates/emails/activation_email_subject.txt b/common/test/test_microsites/test_microsite/templates/emails/activation_email_subject.txt new file mode 100644 index 000000000000..fc4ef4425c08 --- /dev/null +++ b/common/test/test_microsites/test_microsite/templates/emails/activation_email_subject.txt @@ -0,0 +1,3 @@ +<%! from django.utils.translation import ugettext as _ %> + +${_("Your account for Open edX")} diff --git a/common/test/test_microsites/test_microsite/templates/emails/confirm_email_change.txt b/common/test/test_microsites/test_microsite/templates/emails/confirm_email_change.txt new file mode 100644 index 000000000000..406175d1cd26 --- /dev/null +++ b/common/test/test_microsites/test_microsite/templates/emails/confirm_email_change.txt @@ -0,0 +1,15 @@ +<%! from django.core.urlresolvers import reverse %> + +This is to confirm that you changed the e-mail associated with +Open edX from ${old_email} to ${new_email}. If you +did not make this request, please contact us immediately. Contact +information is listed at: + +% if is_secure: + https://${ site }${reverse('contact')} +% else: + http://${ site }${reverse('contact')} +% endif + +We keep a log of old e-mails, so if this request was unintentional, we +can investigate. diff --git a/common/test/test_microsites/test_microsite/templates/emails/email_change.txt b/common/test/test_microsites/test_microsite/templates/emails/email_change.txt new file mode 100644 index 000000000000..e55dbde03970 --- /dev/null +++ b/common/test/test_microsites/test_microsite/templates/emails/email_change.txt @@ -0,0 +1,15 @@ +We received a request to change the e-mail associated with your +Open edX account from ${old_email} to ${new_email}. +If this is correct, please confirm your new e-mail address by +visiting: + +% if is_secure: + https://${ site }/email_confirm/${ key } +% else: + http://${ site }/email_confirm/${ key } +% endif + +If you didn't request this, you don't need to do anything; you won't +receive any more email from us. Please do not reply to this e-mail; +if you require assistance, check the help section of the +Open edX web site. diff --git a/common/test/test_microsites/test_microsite/templates/emails/email_change_subject.txt b/common/test/test_microsites/test_microsite/templates/emails/email_change_subject.txt new file mode 100644 index 000000000000..1d3e3c0aa905 --- /dev/null +++ b/common/test/test_microsites/test_microsite/templates/emails/email_change_subject.txt @@ -0,0 +1 @@ +Request to change Open edX account e-mail diff --git a/common/test/test_microsites/test_microsite/templates/emails/enroll_email_allowedmessage.txt b/common/test/test_microsites/test_microsite/templates/emails/enroll_email_allowedmessage.txt new file mode 100644 index 000000000000..f6a729cfc152 --- /dev/null +++ b/common/test/test_microsites/test_microsite/templates/emails/enroll_email_allowedmessage.txt @@ -0,0 +1,30 @@ +<%! from django.utils.translation import ugettext as _ %> + +${_("Dear student,")} + +${_("You have been invited to join {course_name} at {site_name} by a " + "member of the course staff.").format( + course_name=course.display_name_with_default, + site_name=site_name + )} + +${_("To finish your registration, please visit {registration_url} and fill " + "out the registration form making sure to use {email_address} in the E-mail field.").format( + registration_url=registration_url, + email_address=email_address + )} +% if auto_enroll: +${_("Once you have registered and activated your account, you will see " + "{course_name} listed on your dashboard.").format( + course_name=course.display_name_with_default + )} +% else: +${_("Once you have registered and activated your account, visit {course_about_url} " + "to join the course.").format(course_about_url=course_about_url)} +% endif + +---- +${_("This email was automatically sent from {site_name} to " + "{email_address}").format( + site_name=site_name, email_address=email_address + )} \ No newline at end of file diff --git a/common/test/test_microsites/test_microsite/templates/emails/enroll_email_allowedsubject.txt b/common/test/test_microsites/test_microsite/templates/emails/enroll_email_allowedsubject.txt new file mode 100644 index 000000000000..6ed7ce61b5ba --- /dev/null +++ b/common/test/test_microsites/test_microsite/templates/emails/enroll_email_allowedsubject.txt @@ -0,0 +1,5 @@ +<%! from django.utils.translation import ugettext as _ %> + +${_("You have been invited to register for {course_name}").format( + course_name=course.display_name_with_default + )} \ No newline at end of file diff --git a/common/test/test_microsites/test_microsite/templates/emails/enroll_email_enrolledmessage.txt b/common/test/test_microsites/test_microsite/templates/emails/enroll_email_enrolledmessage.txt new file mode 100644 index 000000000000..a0a817473e08 --- /dev/null +++ b/common/test/test_microsites/test_microsite/templates/emails/enroll_email_enrolledmessage.txt @@ -0,0 +1,20 @@ +<%! from django.utils.translation import ugettext as _ %> + +${_("Dear {full_name}").format(full_name=full_name)} + +${_("You have been enrolled in {course_name} at {site_name} by a member " + "of the course staff. The course should now appear on your {site_name} " + "dashboard.").format( + course_name=course.display_name_with_default, + site_name=site_name + )} + +${_("To start accessing course materials, please visit {course_url}").format( + course_url=course_url + )} + +---- +${_("This email was automatically sent from {site_name} to " + "{full_name}").format( + site_name=site_name, full_name=full_name + )} \ No newline at end of file diff --git a/common/test/test_microsites/test_microsite/templates/emails/enroll_email_enrolledsubject.txt b/common/test/test_microsites/test_microsite/templates/emails/enroll_email_enrolledsubject.txt new file mode 100644 index 000000000000..f13675f99af0 --- /dev/null +++ b/common/test/test_microsites/test_microsite/templates/emails/enroll_email_enrolledsubject.txt @@ -0,0 +1,5 @@ +<%! from django.utils.translation import ugettext as _ %> + +${_("You have been enrolled in {course_name}").format( + course_name=course.display_name_with_default + )} \ No newline at end of file diff --git a/common/test/test_microsites/test_microsite/templates/emails/unenroll_email_allowedmessage.txt b/common/test/test_microsites/test_microsite/templates/emails/unenroll_email_allowedmessage.txt new file mode 100644 index 000000000000..6e3386738e2c --- /dev/null +++ b/common/test/test_microsites/test_microsite/templates/emails/unenroll_email_allowedmessage.txt @@ -0,0 +1,13 @@ +<%! from django.utils.translation import ugettext as _ %> + +${_("Dear Student,")} + +${_("You have been un-enrolled from course {course_name} by a member " + "of the course staff. Please disregard the invitation " + "previously sent.").format(course_name=course.display_name_with_default)} + +---- +${_("This email was automatically sent from {site_name} " + "to {email_address}").format( + site_name=site_name, email_address=email_address + )} \ No newline at end of file diff --git a/common/test/test_microsites/test_microsite/templates/emails/unenroll_email_enrolledmessage.txt b/common/test/test_microsites/test_microsite/templates/emails/unenroll_email_enrolledmessage.txt new file mode 100644 index 000000000000..9a6e5d916147 --- /dev/null +++ b/common/test/test_microsites/test_microsite/templates/emails/unenroll_email_enrolledmessage.txt @@ -0,0 +1,17 @@ +<%! from django.utils.translation import ugettext as _ %> + +${_("Dear {full_name}").format(full_name=full_name)} + +${_("You have been un-enrolled in {course_name} at {site_name} by a member " + "of the course staff. The course will no longer appear on your " + "{site_name} dashboard.").format( + course_name=course.display_name_with_default, site_name=site_name + )} + +${_("Your other courses have not been affected.")} + +---- +${_("This email was automatically sent from {site_name} to " + "{full_name}").format( + full_name=full_name, site_name=site_name + )} \ No newline at end of file diff --git a/common/test/test_microsites/test_microsite/templates/emails/unenroll_email_subject.txt b/common/test/test_microsites/test_microsite/templates/emails/unenroll_email_subject.txt new file mode 100644 index 000000000000..9dd348e2b6fa --- /dev/null +++ b/common/test/test_microsites/test_microsite/templates/emails/unenroll_email_subject.txt @@ -0,0 +1,5 @@ +<%! from django.utils.translation import ugettext as _ %> + +${_("You have been un-enrolled from {course_name}").format( + course_name=course.display_name_with_default +)} \ No newline at end of file diff --git a/common/test/test_microsites/test_microsite/templates/footer.html b/common/test/test_microsites/test_microsite/templates/footer.html new file mode 100644 index 000000000000..bb7dd0de2887 --- /dev/null +++ b/common/test/test_microsites/test_microsite/templates/footer.html @@ -0,0 +1,17 @@ +## mako +<%! from django.core.urlresolvers import reverse %> +<%! from django.utils.translation import ugettext as _ %> +<%! from microsite_configuration.middleware import MicrositeConfiguration %> + +<%namespace name='static' file='static_content.html'/> + + diff --git a/common/test/test_microsites/test_microsite/templates/login-sidebar.html b/common/test/test_microsites/test_microsite/templates/login-sidebar.html new file mode 100644 index 000000000000..79ebfa76a33d --- /dev/null +++ b/common/test/test_microsites/test_microsite/templates/login-sidebar.html @@ -0,0 +1,18 @@ +<%! +from django.utils.translation import ugettext as _ +from django.core.urlresolvers import reverse +%> + +
+

${_("Helpful Information")}

+
+ +
+

${_("Not Enrolled?")}

+

${_("Sign up for {platform_name} today!").format(platform_name=platform_name)}

+ +

${_("Need Help?")}

+

+ Custom text +

+
\ No newline at end of file diff --git a/common/test/test_microsites/test_microsite/templates/register-sidebar.html b/common/test/test_microsites/test_microsite/templates/register-sidebar.html new file mode 100644 index 000000000000..cbfb28dc1a2b --- /dev/null +++ b/common/test/test_microsites/test_microsite/templates/register-sidebar.html @@ -0,0 +1,37 @@ +<%! +from django.utils.translation import ugettext as _ +from django.core.urlresolvers import reverse +%> +<%namespace file='../../main.html' import="login_query"/> +<%namespace name='static' file='../../static_content.html'/> + + +
+

${_("Registration Help")}

+
+ +
+

${_("Already registered?")}

+

+ + ${_("Click here to log in.")} + +

+
+ +
+

${_("Welcome to {platform_name}").format(platform_name=platform_name)}

+

${_("Registering with {platform_name} gives you access to all of our current and future free courses. Not ready to take a course just yet? Registering puts you on our mailing list - we will update you as courses are added.").format(platform_name=platform_name)}

+
+ +
+

${_("Next Steps")}

+

${_("As part of joining {platform_name}, you will receive an activation email. You must click on the activation link to complete the process. Don't see the email? Check your spam folder and mark {platform_name} emails as 'not spam'. At {platform_name}, we communicate mostly through email.").format(platform_name=platform_name)}

+
+ +
+

${_("Need Help?")}

+

+ This is custom area +

+
diff --git a/common/test/test_microsites/test_microsite/templates/static_templates/about.html b/common/test/test_microsites/test_microsite/templates/static_templates/about.html new file mode 100644 index 000000000000..a326281b6d75 --- /dev/null +++ b/common/test/test_microsites/test_microsite/templates/static_templates/about.html @@ -0,0 +1,64 @@ +<%! from django.utils.translation import ugettext as _ %> +<%! from django.core.urlresolvers import reverse %> +<%namespace name='static' file='../../../static_content.html'/> + +<%inherit file="../../../main.html" /> + +<%block name="title">${_("About {edX}").format(edX="edX")} + +
+ + +
+ ##
+ ## + ##

“${_('The mission of {edX} is to enhance human fulfillment worldwide through online ## learning, transforming education in quality, efficiency and scale through technology and research, for the benefit of campus-based ## students and the worldwide community of online learners.').format(edX="edX")}”

+ ##
+ +
+
+ +
+
+

${_('About {span_start}{edX}{span_end}'.format(span_start='', span_end='', edX="edX"))}

+

${_("Open edX is a not-for-profit enterprise of its founding partners {harvard_university} and the {massachusetts_institute_of_technology} that features learning designed specifically for interactive study via the web. Based on a long history of collaboration and their shared educational missions, the founders are creating a new online-learning experience with online courses that reflect their disciplinary breadth. Along with offering online courses, the institutions will use {edX} to research how students learn and how technology can transform learning–both on-campus and worldwide. Anant Agarwal, former Director of {MIT}'s Computer Science and Artificial Intelligence Laboratory, serves as the first president of {edX}. {EdX}'s goals combine the desire to reach out to students of all ages, means, and nations, and to deliver these teachings from a faculty who reflect the diversity of its audience. {EdX} is based in Cambridge, Massachusetts and is governed by {MIT} and {Harvard}.").format(edX="edX", EdX="EdX", harvard_university="Harvard University", Harvard="Harvard", MIT="MIT", massachusetts_institute_of_technology="Massachusetts Institute of Technology")}

+
+
+
+ +
+
+ +
+
+

${_("{harvard_university}").format(harvard_university="Harvard University")}

+

${_("{harvard_university} is devoted to excellence in teaching, learning, and research, and to developing leaders in many disciplines who make a difference globally. {Harvard} faculty are engaged with teaching and research to push the boundaries of human knowledge. The University has twelve degree-granting Schools in addition to the {radcliffe_institute_for_advanced_study}.").format(harvard_university="Harvard University", Harvard="Harvard", radcliffe_institute_for_advanced_study="Radcliffe Institute for Advanced Study")}

+

${_("Established in 1636, {Harvard} is the oldest institution of higher education in the United States. The University, which is based in Cambridge and Boston, Massachusetts, has an enrollment of over 20,000 degree candidates, including undergraduate, graduate, and professional students. {Harvard} has more than 360,000 alumni around the world.").format(Harvard="Harvard")}

+
+
+
+ +
+
+ +
+
+

${_("{massachusetts_institute_of_technology}").format(massachusetts_institute_of_technology="Massachusetts Institute of Technology")}

+

${_("The {massachusetts_institute_of_technology} — a coeducational, privately endowed research university founded in 1861 — is dedicated to advancing knowledge and educating students in science, technology, and other areas of scholarship that will best serve the nation and the world in the 21st century. The Institute has close to 1,000 faculty and 10,000 undergraduate and graduate students. It is organized into five Schools: Architecture and Urban Planning; Engineering; Humanities, Arts, and Social Sciences; {Sloan} School of Management; and Science.").format(massachusetts_institute_of_technology="Massachusetts Institute of Technology", Sloan="Sloan")}

+

${_("{MIT}'s commitment to innovation has led to a host of scientific breakthroughs and technological advances. Achievements of the Institute's faculty and graduates have included the first chemical synthesis of penicillin and vitamin A, the development of inertial guidance systems, modern technologies for artificial limbs, and the magnetic core memory that made possible the development of digital computers. 78 alumni, faculty, researchers and staff have won Nobel Prizes.").format(MIT="MIT")}

+

${_("Current areas of research and education include neuroscience and the study of the brain and mind, bioengineering, cancer, energy, the environment and sustainable development, information sciences and technology, new media, financial technology, and entrepreneurship.")}

+
+
+ +
+
+
+
+ diff --git a/common/test/test_microsites/test_microsite/templates/static_templates/contact.html b/common/test/test_microsites/test_microsite/templates/static_templates/contact.html new file mode 100644 index 000000000000..dcdc953f86c6 --- /dev/null +++ b/common/test/test_microsites/test_microsite/templates/static_templates/contact.html @@ -0,0 +1,62 @@ +<%! from django.utils.translation import ugettext as _ %> +<%! from django.core.urlresolvers import reverse %> +<%namespace name='static' file='../static_content.html'/> + +<%inherit file="../main.html" /> + +<%block name="title">${_("Contact {platform_name}").format(platform_name=settings.PLATFORM_NAME)} + +
+ + +
+
+ +
+
+

${_("Class Feedback")}

+

${_("We are always seeking feedback to improve our courses. If you are an enrolled student and have any questions, feedback, suggestions, or any other issues specific to a particular class, please post on the discussion forums of that class.")}

+ +

${_("General Inquiries and Feedback")}

+

${_('If you have a general question about {platform_name} please email {email}. To see if your question has already been answered, visit our {faq_link_start}FAQ page{faq_link_end}. You can also join the discussion on our {fb_link_start}facebook page{fb_link_end}. Though we may not have a chance to respond to every email, we take all feedback into consideration.').format( + platform_name=settings.PLATFORM_NAME, + email='{contact_email}'.format(contact_email=settings.CONTACT_EMAIL), + faq_link_start=''.format(url=reverse('faq_edx')), + faq_link_end='', + fb_link_start='', + fb_link_end='' + )}

+ +

${_("Technical Inquiries and Feedback")}

+

${_('If you have suggestions/feedback about the overall {platform_name} platform, or are facing general technical issues with the platform (e.g., issues with email addresses and passwords), you can reach us at {tech_email}. For technical questions, please make sure you are using a current version of Firefox or Chrome, and include browser and version in your e-mail, as well as screenshots or other pertinent details. If you find a bug or other issues, you can reach us at the following: {bug_email}.').format( + platform_name=settings.PLATFORM_NAME, + tech_email='{tech_support_email}'.format(tech_support_email=settings.TECH_SUPPORT_EMAIL), + bug_email='{bugs_email}'.format(bugs_email=settings.BUGS_EMAIL) + )}

+ +

${_("Media")}

+

${_('Please visit our {link_start}media/press page{link_end} for more information. For any media or press inquiries, please email {email}.').format( + link_start=''.format(url=reverse('faq_edx')), + link_end='', + email='{email}'.format(email="press@edx.org"), + )}

+ +

${_("Universities")}

+

${_('If you are a university wishing to collaborate or you have questions about {platform_name}, please email {email}.'.format( + platform_name="edX", + email='{email}'.format( + email="university@edx.org" + ) + ))}

+ +

${_("Accessibility")}

+

${_('{platform_name} strives to create an innovative online-learning platform that promotes accessibility for everyone, including students with disabilities. We are dedicated to improving the accessibility of the platform and welcome your comments or questions at {email}.'.format(platform_name="EdX", email='{email}'.format(email="accessibility@edx.org")))}

+
+
+
+ diff --git a/common/test/test_microsites/test_microsite/templates/static_templates/faq.html b/common/test/test_microsites/test_microsite/templates/static_templates/faq.html new file mode 100644 index 000000000000..4621b30323c1 --- /dev/null +++ b/common/test/test_microsites/test_microsite/templates/static_templates/faq.html @@ -0,0 +1,139 @@ +<%! from django.utils.translation import ugettext as _ %> +<%! from django.core.urlresolvers import reverse %> +<%namespace name='static' file='../static_content.html'/> + +<%inherit file="../main.html" /> + +<%block name="title">${_("FAQ")} + +
+ + + +
+
+
+

${_("Organization")}

+
+

${_("What is {edX}?").format(edX="edX")}

+

${_('{EdX} is a not-for-profit enterprise of its founding partners, the {MIT_long} ({MIT}) and {harvard_u} that offers online learning to on-campus students and to millions of people around the world. To do so, {edX} is building an open-source online learning platform and hosts an online web portal at www.edx.org for online education.').format(EdX="EdX", edX="edX", MIT_long="Massachusetts Institute of Technology", MIT="MIT", harvard_u="Harvard University")}

+

${_("{EdX} currently offers {HarvardX}, {MITx} and {BerkeleyX} classes online for free. Beginning in fall 2013, {edX} will offer {WellesleyX} , {GeorgetownX} and the {UTexas} classes online for free. The {UT} System includes nine universities and six health institutions. In 2014, {edX} will further expand its consortium, including several international schools, when it begins offering courses from {EPFL}, {McGill}, {Toronto}, {ANU}, {Delft}, and {Rice}. The {edX} institutions aim to extend their collective reach to build a global community of online students. Along with offering online courses, the three universities undertake research on how students learn and how technology can transform learning both on-campus and online throughout the world.").format( + EdX="EdX", + edX="edX", + HarvardX="HarvardX", + MITx="MITx", + BerkeleyX="BerkeleyX", + WellesleyX="WellesleyX", + GeorgetownX="GeorgetownX", + UTexas="University of Texas System", + UT="UT", + EPFL=u"École Polytechnique Fédérale de Lausanne", + McGill="McGill University", + Toronto="University of Toronto", + ANU="Australian National University", + Delft="Delft University of Technology", + Rice="Rice University", + )} +

+
+ +
+

${_("Will {edX} be adding additional X Universities?").format(edX="edX")}

+

${_("More than 200 institutions from around the world have expressed interest in collaborating with {edX} since {Harvard} and {MIT} announced its creation in May. {EdX} is focused above all on quality and developing the best not-for-profit model for online education. In addition to providing online courses on the {edX} platform, the {x_consortium} will be a forum in which members can share experiences around online learning. {Harvard}, {MIT}, {Berkeley}, the {UTexas} and the other {consortium} members will work collaboratively to establish the {x_consortium}, whose membership will expand to include additional \"{X_Universities}.\" As noted above, {edX}'s newest {consortium} members include {Wellesley}, {Georgetown}, {EPFL}, {McGill}, {Toronto}, {ANU}, {Delft}, and {Rice}. Each member of the {consortium} will offer courses on the {edX} platform as an \"{X_University}\". The gathering of many universities' educational content together on one site will enable learners worldwide to access the offered course content of any participating university from a single website, and to use a set of online educational tools shared by all participating universities.").format( + EdX="EdX", + edX="edX", + Harvard="Harvard", + MIT="MIT", + x_consortium="\"X University\" Consortium", + consortium="consortium", + X_Universities="X Universities", + X_University="X University", + Berkeley="UC Berkeley", + Wellesley="Wellesley", + Georgetown="Georgetown", + UTexas="University of Texas System", + EPFL=u"École Polytechnique Fédérale de Lausanne", + McGill="McGill University", + Toronto="University of Toronto", + ANU="Australian National University", + Delft="Delft University of Technology", + Rice="Rice University", + )}

+

${_("{EdX} will actively explore the addition of other institutions from around the world to the {edX} platform, and looks forward to adding more \"{X_Universities}\".").format(EdX="EdX", edX="edX", X_Universities="X Universities")} +

+
+
+ +
+

${_("Students")}

+
+

${_("Who can take {edX} courses? Will there be an admissions process?").format(edX="edX")}

+

${_("{EdX} will be available to anyone in the world with an internet connection, and in general, there will not be an admissions process.").format(EdX="EdX")}

+
+
+

${_("Will certificates be awarded?")}

+

${_("Yes. Online learners who demonstrate mastery of subjects can earn a certificate " + "of mastery. Certificates will be issued at the discretion of {edX} and the underlying " + "\"{X_University}\" that offered the course under the name of the underlying \"{X_University}\" from where the course originated, i.e. {HarvardX}, {MITx} or {BerkeleyX}. " + "For the courses in Fall 2012, those certificates will be free. There is a plan to " + "charge a modest fee for certificates in the future. Note: At this time, {edX} is " + "holding certificates for learners connected with Cuba, Iran, Syria and Sudan " + "pending confirmation that the issuance is in compliance with U.S. embargoes.").format(edX="edX", X_University="X University", HarvardX="HarvardX", MITx="MITx", BerkeleyX="BerkeleyX")}

+
+
+

${_("What will the scope of the online courses be? How many? Which faculty?")}

+

${_('Our goal is to offer a wide variety of courses across disciplines. There are currently {link_start}fifteen{link_end} offered on the {edX} platform.').format(link_start='', link_end='', edX="edX")}

+
+
+

${_("Who is the learner? Domestic or international? Age range?")}

+

${_("Improving teaching and learning for students on our campuses is one of our primary goals. Beyond that, we don't have a target group of potential learners, as the goal is to make these courses available to anyone in the world - from any demographic - who has interest in advancing their own knowledge. The only requirement is to have a computer with an internet connection. More than 150,000 students from over 160 countries registered for {MITx}'s first course, 6.002x: Circuits and Electronics. The age range of students certified in this course was from 14 to 74 years-old.").format(MITx="MITx")}

+
+
+

${_("Will participating universities' standards apply to all courses offered on the edX platform?")}

+

${_("Yes: the reach changes exponentially, but the rigor remains the same.")}

+
+
+

${_("How do you intend to test whether this approach is improving learning?")}

+

${_("{EdX} institutions have assembled faculty members who will collect and analyze data to assess results and the impact {edX} is having on learning.").format(EdX="EdX", edX="edX")}

+
+
+

${_("How may I apply to study with {edX}?").format(edX="edX")}

+

${_('Simply complete the online {link_start}signup form{link_end}. Enrolling will create your unique student record in the {edX} database, allow you to register for classes, and to receive a certificate on successful completion.').format(link_start='', link_end='', edX="edX")}

+
+
+

${_("How may another university participate in {edX}? ").format(edX="edX")}

+

${_('If you are from a university interested in discussing {edX}, please email {email}').format(email='university@edx.org', edX="edX")}

+
+
+ +
+

${_("Technology Platform")}

+
+

${_("What technology will {edX} use?").format(edX="edX")}

+

${_("The {edX} open-source online learning platform will feature interactive learning designed specifically for the web. Features will include: self-paced learning, online discussion groups, wiki-based collaborative learning, assessment of learning as a student progresses through a course, and online laboratories and other interactive learning tools. The platform will also serve as a laboratory from which data will be gathered to better understand how students learn. Because it is open source, the platform will be continuously improved by a worldwide community of collaborators, with new features added as needs arise.").format(edX="edX")}

+

${_("The first version of the technology was used in the first {MITx} course, 6.002x Circuits and Electronics, which launched in Spring, 2012.").format(MITx="MITx")}

+
+
+

${_("How is this different from what other universities are doing online?")}

+

${_("{EdX} is a not-for-profit enterprise built upon the shared educational missions of its founding partners, {Harvard_long} and {MIT}. The {edX} platform will be available as open source. Also, a primary goal of {edX} is to improve teaching and learning on campus by experimenting with blended models of learning and by supporting faculty in conducting significant research on how students learn.").format(edX="edX", EdX="EdX", Harvard_long="Harvard University", MIT="MIT")}

+
+
+ +
+ + +
+
+ +%if user.is_authenticated(): +<%include file="../signup_modal.html" /> +%endif diff --git a/common/test/test_microsites/test_microsite/templates/static_templates/tos.html b/common/test/test_microsites/test_microsite/templates/static_templates/tos.html new file mode 100644 index 000000000000..414754d82b27 --- /dev/null +++ b/common/test/test_microsites/test_microsite/templates/static_templates/tos.html @@ -0,0 +1,122 @@ +<%! from django.utils.translation import ugettext as _ %> +<%! from django.core.urlresolvers import reverse %> +<%inherit file="../main.html" /> + +<%namespace name='static' file='../static_content.html'/> + +<%block name="title">Terms of Service + +
+

edX Terms of Service

+
+ +
+

NOTICE: on September 26, 2012 edX adopted amended Terms of Service, providing as follows:

+ +

Welcome to edX. Please read these Terms of Service ("TOS") and edX's Privacy Policy and Honor Code prior to registering for edX.org or using any portion of the edX website (the "Site," which consists of all content and pages located within the edX.org web domain), including accessing any course material, chat rooms, or other electronic services. These TOS, the Privacy Policy and the Honor Code are agreements (the "Agreements") between you and edX. By using the Site, you accept and agree to be legally bound by the Agreements, whether or not you are a registered user. If you do not understand or do not wish to be bound by the terms of the Agreements, you should not use the Site.")

+ +

EdX reserves the right to modify these TOS at any time without advance notice. Any changes to these TOS will be effective immediately upon posting on this page, with an updated effective date. By accessing the Site after any changes have been made, you signify your agreement on a prospective basis to the modified TOS and all of the changes. Be sure to return to this page periodically to ensure familiarity with the most current version of these TOS.

+ +

Description of edX

+

EdX offers online courses that include opportunities for professor-to-student and student-to-student interactivity, individual assessment of a student's work and for students who demonstrate their mastery of subjects, a certificate of mastery.

+ +

Rules for Online Conduct

+

You agree that you are responsible for your own use of the Site and for your User Postings. "User Postings" include all content submitted, posted, published or distributed on the Site by you or other users of the Site, including but not limited to all forum posts, wiki edits, notes, questions, comments, videos and file uploads. You agree that you will use the Site in compliance with these TOS, the Honor Code and all applicable local, state, national and international laws, rules and regulations, including copyright laws, any laws regarding the transmission of technical data exported from your country of residence, and all United States export control laws.

+ +

As a condition of your use of the edX services, you will not use the Site in any manner intended to damage, disable, overburden or impair any edX server or the network(s) connected to any edX server or to interfere with any other party's use and enjoyment of the Site. You may not attempt to gain unauthorized access to the Site, other accounts, computer systems or networks connected to any edX server through hacking, password mining or any other means. You may not obtain or attempt to obtain any materials or information stored on the Site, its servers or associated computers through any means not intentionally made available through the Site.

+ +

The following list of items is strictly prohibited on the Site:

+
    +
  1. Content that defames, harasses or threatens others;
  2. +
  3. Content that discusses illegal activities with the intent to commit them;
  4. +
  5. Content that infringes another's intellectual property, including, but not limited to, copyrights or trademarks;
  6. +
  7. Profane, pornographic, obscene, indecent or unlawful content;
  8. +
  9. Advertising or any form of commercial solicitation;
  10. +
  11. Content related to partisan political activities;
  12. +
  13. Viruses, trojan horses, worms, time bombs, corrupted files, malware, spyware or any other similar software that may damage the operation of another's computer or property; and
  14. +
  15. Content that contains intentionally inaccurate information or that is posted with the intent of misleading others.
  16. +
+ +

Furthermore, you agree not to scrape, or otherwise download in bulk, any Site content, including but not limited to a list or directory of users on the system, on-line textbooks, User Postings or user information. You agree not to misrepresent or attempt to misrepresent your identity while using the Sites (although you are welcome and encouraged to use an anonymous username in the forums and to act in a manner that keeps your identity concealed).

+ +

User Accounts and Authority

+

In order to participate fully in Site activities, you must provide your name, an email address and a user password in order to create a user account ("User Account"). You agree that you will never divulge or share access or access information to your User Account with any third party for any reason. In setting up your User Account, you may be prompted to enter additional optional information (e.g., your address). You represent that all information provided by you is accurate and current. You agree to maintain and update your information to keep it accurate and current.

+ +

We care about the confidentiality and security of your personal information. Please see our Privacy Policy for more information about what information about you edX collects and how edX uses that information."

+ +

Your Right to Use Content on the Site

+

Unless indicated as being in the public domain, the content on the Site is protected by United States and foreign copyright laws. Unless otherwise expressly stated on the Site, the texts, exams, video, images and other instructional materials provided with the courses offered on this Site are for your personal use in connection with those courses only. MIT and Harvard aim to make much of the edX course content available under more open license terms that will help create a vibrant ecosystem of contributors and further edX's goal of making education accessible and affordable to the world.

+ +

Certain reference documents, digital textbooks, articles and other information on the Site are used with the permission of third parties, and use of that information is subject to certain rules and conditions, which will be posted along with the information. By using this Site you agree to abide by all such rules and conditions.

+ +

You agree to retain all copyright and other notices on any content you obtain from the Site. All rights in the Site and its content, if not expressly granted, are reserved.

+ +

User Postings

+ +

User Postings Representations and Warranties. By submitting or distributing your User Postings, you affirm, represent and warrant (1) that you have the necessary rights, licenses, consents and/or permissions to reproduce and publish the User Postings and to authorize edX and its users to reproduce, modify, publish and otherwise use and distribute your User Postings in a manner consistent with the licenses granted by you below, and (2) that neither your submission of your User Postings nor the exercise of the licenses granted below will infringe or violate the rights of any third party. You, and not edX, are solely responsible for your User Postings and the consequences of posting or publishing them.

+ +

License Grant to edX. By submitting or distributing User Postings to the Site, you hereby grant to edX a worldwide, non-exclusive, transferable, assignable, sublicensable, fully paid-up, royalty-free, perpetual, irrevocable right and license to host, transfer, display, perform, reproduce, modify, distribute, re-distribute, relicense and otherwise use, make available and exploit your User Postings, in whole or in part, in any form and in any media formats and through any media channels (now known or hereafter developed).

+ +

License Grant to edX Users. By submitting or distributing User Postings to the Site, you hereby grant to each user of the Site a non-exclusive license to access and use your User Postings in connection with their use of the Site for their own personal purposes.

+ +

Certificates, etc.

+ +

EdX and/or the colleges and universities providing courses on the Site may offer a certificate of mastery or other acknowledgment (a "Certificate") for students who, in their judgment, have satisfactorily demonstrated mastery of the course material. Certificates will be issued by edX under the name of the underlying "X University" from where the course originated, i.e. HarvardX, MITx. etc. The decision whether a Certificate will be awarded to a given student will be solely within the discretion of the awarding entity, as will the name and form of any such Certificate. EdX and/or the institutions providing courses on the Site may choose not to offer a Certificate for some courses.

+ +

When you take a course through edX, you will not be an applicant for admission to, or enrolled in, any degree program of the institution as a result of registering for or completing a course through edX. You will not be entitled to use any of the resources of the institution beyond the online courses provided on the Site, nor will you be eligible to receive student privileges or benefits provided to students enrolled in degree programs of the institution.

+ +

Trademarks

+ +

Use of edX, MIT, Harvard University and X University Names, Trademarks and Service Marks. The "edX", "MIT", and "Harvard University" names, logos and seals are trademarks ("Trademarks") of the respective entities. Likewise, the names, logos, and seals of the other colleges and universities providing courses on the Site (the "X Universities") are Trademarks owned by the X Universities. You may not use any of these Trademarks, or any variations thereof, without the owner's prior written consent. You may not use any of these Trademarks, or any variations thereof, for promotional purposes, or in any way that deliberately or inadvertently claims, suggests or, in these institutions' sole judgment, gives the appearance or impression of a relationship with or endorsement by these institutions.

+ +

All Trademarks not owned by these institutions that appear on the Site or on or through the services made available on or through the Site, if any, are the property of their respective owners.

+ +

Nothing contained on the Site should be construed as granting, by implication, estoppel or otherwise, any license or right to use any Trademark displayed on the Site without the written permission of the owner of the applicable Trademark.

+ +

Digital Millennium Copyright Act

+ +

Copyright owners who believe their material has been infringed on the Site should contact edX's designated copyright agent at dcma-agent@mit.edu or at 77 Massachusetts Avenue, Cambridge, MA 02138-4307 Attention: MIT DCMA Agent, W92-263A.

+ +

Notification must include:

+ + +

Disclaimers of Warranty / Limitations of Liabilities

+

THE SITE AND ANY INFORMATION, CONTENT OR SERVICES MADE AVAILABLE ON OR THROUGH THE SITE ARE PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTY OF ANY KIND (EXPRESS, IMPLIED OR OTHERWISE), INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT, EXCEPT INSOFAR AS ANY SUCH IMPLIED WARRANTIES MAY NOT BE DISCLAIMED UNDER APPLICABLE LAW.

+ +

EDX AND THE EDX PARTICIPANTS (AS HERINAFTER DEFINED) DO NOT WARRANT THAT THE SITE WILL OPERATE IN AN UNINTERRUPTED OR ERROR-FREE MANNER, THAT THE SITE IS FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS, OR THAT THE COURSES OR CONTENT PROVIDED WILL MEET YOUR NEEDS OR EXPECTATIONS. EDX AND THE EDX PARTICIPANTS ALSO MAKE NO WARRANTY ABOUT THE ACCURACY, COMPLETENESS, TIMELINESS, OR QUALITY OF THE SITE OR ANY COURSES OR CONTENT, OR THAT ANY PARTICULAR COURSES OR CONTENT WILL CONTINUE TO BE MADE AVAILABLE. “EDX PARTICIPANTS” MEANS MIT, HARVARD, X UNIVERSITIES, THE ENTITIES PROVIDING INFORMATION, CONTENT OR SERVICES FOR THE SITE, THE COURSE INSTRUCTORS AND THEIR STAFFS.

+ +

USE OF THE SITE, AND THE CONTENT AND SERVICES OBTAINED FROM OR THROUGH THE SITE, IS AT YOUR OWN RISK. YOUR ACCESS TO OR DOWNLOAD OF INFORMATION, MATERIALS OR DATA THROUGH THE SITE OR ANY REFERENCE SITES IS AT YOUR OWN DISCRETION AND RISK, AND YOU WILL BE SOLELY RESPONSIBLE FOR ANY DAMAGE TO YOUR PROPERTY (INCLUDING YOUR COMPUTER SYSTEM) OR LOSS OF DATA THAT RESULTS FROM THE DOWNLOAD OR USE OF SUCH MATERIAL OR DATA.

+ +

User Postings Disclaimer. You understand that when using the Site you will be exposed to User Postings from a variety of sources and that neither edX nor the edX Participants are responsible for the accuracy, usefulness, reliability or intellectual property rights of or relating to such User Postings. You further understand and acknowledge that you may be exposed to User Postings that are inaccurate, offensive, defamatory, indecent or objectionable and you agree to waive, and hereby do waive, any legal or equitable rights or remedies you have or may have against edX or any of the edX Participants with respect thereto. Neither edX nor the edX Participants endorse any User Postings or any opinion, recommendation or advice expressed therein. Neither edX nor the edX Participants have any obligation to monitor any User Postings or any other user communications through the Site.

+ +

However, edX reserves the right to review User Postings and to exercise its sole discretion to edit or remove, in whole or in part, any User Posting at any time or for any reason, or to allow the edX Participants to do so. Without limiting the foregoing, upon receiving notice from a user or a content owner that a User Posting allegedly does not conform to these TOS, edX may investigate the allegation and determine in its sole discretion whether to remove the User Posting, which it reserves the right to do at any time and without notice.

+ +

Links to Other Sites. The Site may include hyperlinks to sites maintained or controlled by others. EdX and the edX Participants are not responsible for and do not routinely screen, approve, review or endorse the contents of or use of any of the products or services that may be offered at these sites. If you decide to access linked third party web sites, you do so at your own risk.

+ +

TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, YOU AGREE THAT NEITHER EDX NOR ANY OF THE EDX PARTICIPANTS WILL BE LIABLE TO YOU FOR ANY LOSS OR DAMAGES, EITHER ACTUAL OR CONSEQUENTIAL, ARISING OUT OF OR RELATING TO THESE TERMS OF SERVICE, OR YOUR (OR ANY THIRD PARTY'S) USE OF OR INABILITY TO USE THE SITE, OR YOUR PLACEMENT OF CONTENT ON THE SITE, OR YOUR RELIANCE UPON INFORMATION OBTAINED FROM OR THROUGH THE SITE, WHETHER YOUR CLAIM IS BASED IN CONTRACT, TORT, STATUTORY OR OTHER LAW.

+ +

IN PARTICULAR, TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, NEITHER EDX NOR ANY OF THE EDX PARTICIPANTS WILL HAVE ANY LIABILITY FOR ANY CONSEQUENTIAL, INDIRECT, PUNITIVE, SPECIAL, EXEMPLARY OR INCIDENTAL DAMAGES, WHETHER FORESEEABLE OR UNFORESEEABLE AND WHETHER OR NOT EDX OR ANY OF THE EDX PARTICIPANTS HAS BEEN NEGLIGENT OR OTHERWISE AT FAULT (INCLUDING, BUT NOT LIMITED TO, CLAIMS FOR DEFAMATION, ERRORS, LOSS OF PROFITS, LOSS OF DATA OR INTERRUPTION IN AVAILABILITY OF DATA).

+ +

Indemnification

+ +

You agree to defend, hold harmless and indemnify edX and the edX Participants, and their respective subsidiaries, affiliates, officers, faculty, students, fellows, governing board members, agents and employees from and against any third-party claims, actions or demands arising out of, resulting from or in any way related to your use of the Site, including any liability or expense arising from any and all claims, losses, damages (actual and consequential), suits, judgments, litigation costs and attorneys' fees, of every kind and nature. In such a case, edX or one of the edX Participants will provide you with written notice of such claim, suit or action.

+ +

Miscellaneous

+ +

Termination Rights; Discontinuation of Courses and Content. You agree that edX, in its sole discretion, may terminate your use of the Site or your participation in it, for any reason or no reason, upon notice to you. It is edX's policy to terminate in appropriate circumstances users of the Site who are repeat copyright infringers. EdX and the edX Participants reserve the right at any time in their sole discretion to cancel, delay, reschedule or alter the format of any course offered through edX, or to cease providing any part or all of the Site content or related services, and you agree that neither edX nor any of the edX Participants will have any liability to you for such an action. If you no longer desire to participate in the Site, you may terminate your participation at any time. The rights granted to you hereunder will terminate upon any termination of your right to use the Site, but the other provisions of these TOS will survive any such termination.

+ +

Entire Agreement. These TOS, the Honor Code, and the Privacy Policy together constitute the entire agreement between you and edX with respect to your use of the Site, superseding any prior agreements between you and edX regarding your use of the Site.

+ +

Waiver and Severability of TOS. The failure of edX to exercise or enforce any right or provision of these TOS shall not constitute a waiver of such right or provision. If any provision of these TOS is found by a court of competent jurisdiction to be invalid, the parties nevertheless agree that the court should endeavor to give effect to the parties' intentions as reflected in the provision and the other provisions of these TOS shall remain in full force and effect.

+ +

Choice of Law/Forum Selection. You agree that these TOS and any claim or dispute arising out of or relating to these TOS or any content or service obtained from or through the Site will be governed by the laws of the Commonwealth of Massachusetts, excluding its conflicts of law provisions. You agree that all such claims and disputes will be heard and resolved exclusively in the federal or state courts located in and serving Cambridge, Massachusetts, U.S.A. You consent to the personal jurisdiction of those courts over you for this purpose, and you waive and agree not to assert any objection to such proceedings in those courts (including any defense or objection of lack of proper jurisdiction or venue or inconvenience of forum).

+ +

Effective Date: September 26, 2012

+
+
diff --git a/lms/djangoapps/branding/__init__.py b/lms/djangoapps/branding/__init__.py index d70ffb1cc990..c63154356fc1 100644 --- a/lms/djangoapps/branding/__init__.py +++ b/lms/djangoapps/branding/__init__.py @@ -2,15 +2,10 @@ from xmodule.course_module import CourseDescriptor from django.conf import settings +from microsite_configuration.middleware import MicrositeConfiguration -def pick_subdomain(domain, options, default='default'): - for option in options: - if domain.startswith(option): - return option - return default - -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 + return MicrositeConfiguration.get_microsite_configuration_value('university') - subdomain = pick_subdomain(domain, settings.SUBDOMAIN_BRANDING.keys()) - return settings.SUBDOMAIN_BRANDING.get(subdomain) - -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 0adf73381825..fdbacdec53ac 100644 --- a/lms/djangoapps/branding/views.py +++ b/lms/djangoapps/branding/views.py @@ -6,8 +6,9 @@ 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,12 @@ 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 = MicrositeConfiguration.get_microsite_configuration_value( + 'ENABLE_MKTG_SITE', + settings.FEATURES.get('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 c74b0f9e6f01..fd8adf998b6b 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 000000000000..e9cd776376af --- /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_student_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) + + + 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') + + + 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') + + + 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_student_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/courseware/views.py b/lms/djangoapps/courseware/views.py index 137b52916905..e60b6c5394fc 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -38,6 +38,8 @@ from xmodule.course_module import CourseDescriptor import shoppingcart +from microsite_configuration.middleware import MicrositeConfiguration + log = logging.getLogger("edx.courseware") template_imports = {'urllib': urllib} @@ -514,7 +516,11 @@ def registered_for_course(course, user): @ensure_csrf_cookie @cache_if_anonymous def course_about(request, course_id): - if settings.FEATURES.get('ENABLE_MKTG_SITE', False): + + if MicrositeConfiguration.get_microsite_configuration_value( + 'ENABLE_MKTG_SITE', + settings.FEATURES.get('ENABLE_MKTG_SITE', False) + ): raise Http404 course = get_course_with_access(request.user, course_id, 'see_exists') diff --git a/lms/djangoapps/instructor/enrollment.py b/lms/djangoapps/instructor/enrollment.py index a7c4fe0b6554..5042f6996dad 100644 --- a/lms/djangoapps/instructor/enrollment.py +++ b/lms/djangoapps/instructor/enrollment.py @@ -14,6 +14,8 @@ 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 + + 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(param_dict['message'], (None, None)) + 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 7f1d6e182ff0..4b54d037fc15 100644 --- a/lms/djangoapps/instructor/views/legacy.py +++ b/lms/djangoapps/instructor/views/legacy.py @@ -63,6 +63,8 @@ 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 = None + message = None + + message_type = param_dict['message'] - subject_template, message_template = EMAIL_TEMPLATE_DICT.get(param_dict['message'], (None, None)) + 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 7647b2c48708..562ad08d82d0 100644 --- a/lms/djangoapps/shoppingcart/models.py +++ b/lms/djangoapps/shoppingcart/models.py @@ -32,6 +32,8 @@ from .exceptions import (InvalidCartItem, PurchasedCallbackException, ItemAlreadyInCartException, AlreadyEnrolledInCourseException, CourseDoesNotExistException) +from microsite_configuration.middleware import MicrositeConfiguration + log = logging.getLogger("shoppingcart") ORDER_STATUSES = ( @@ -161,14 +163,22 @@ def purchase(self, first='', last='', street1='', street2='', city='', state='', # 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 @@ def refund_cert_callback(sender, course_enrollment=None, **kwargs): 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 ad6f9463d8a5..f598416cb7f0 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -337,3 +337,13 @@ 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 000000000000..700e7c6c166b --- /dev/null +++ b/lms/envs/cms/microsite_test.py @@ -0,0 +1,42 @@ +""" +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 + ) + +# pretend we are behind some marketing site, we want to be able to assert that the Microsite config values override +# this global setting +FEATURES['ENABLE_MKTG_SITE'] = True diff --git a/lms/envs/common.py b/lms/envs/common.py index 150b9ab8b846..716f11ae2e2b 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -25,6 +25,7 @@ import sys import os +import json from path import path @@ -377,7 +378,7 @@ ######################## subdomain specific settings ########################### COURSE_LISTINGS = {} SUBDOMAIN_BRANDING = {} - +VIRTUAL_UNIVERSITIES = [] ############################### XModule Store ################################## MODULESTORE = { @@ -617,6 +618,7 @@ 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 @@ } +############################### 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 30053d3da3e5..6b4446893bd1 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -130,6 +130,8 @@ '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 dd52d2e76f62..3c09a999e7ba 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -277,3 +277,33 @@ 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=COMMON_ROOT / "test" / 'test_microsites' + ) diff --git a/lms/templates/courseware/course_about.html b/lms/templates/courseware/course_about.html index 81e9e2507f2e..c042ba31bd26 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 1f1d96a0dffd..9351980336aa 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 9bf489295b74..8dc7b6ef6c26 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")} -