From 7f773491f97fd257b1f5c569c7fc69958a33e816 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Mon, 9 Dec 2013 22:42:45 -0500 Subject: [PATCH 01/13] initial WIP --- .../microsite_configuration/__init__.py | 0 .../microsite_configuration/middleware.py | 104 ++++++++++++++++++ common/djangoapps/student/views.py | 46 ++++++-- lms/djangoapps/branding/__init__.py | 50 +++++---- lms/djangoapps/branding/views.py | 19 +++- lms/djangoapps/courseware/courses.py | 2 +- lms/djangoapps/instructor/enrollment.py | 19 +++- lms/djangoapps/shoppingcart/models.py | 10 +- lms/envs/common.py | 1 + lms/envs/dev.py | 47 ++++++++ lms/static/images/open_edX_logo.png | Bin 0 -> 14216 bytes lms/templates/courseware/course_about.html | 15 ++- lms/templates/index.html | 10 +- lms/templates/main.html | 58 +++++++--- lms/templates/navigation.html | 2 +- lms/templates/openedx-footer.html | 31 ++++++ 16 files changed, 349 insertions(+), 65 deletions(-) create mode 100644 common/djangoapps/microsite_configuration/__init__.py create mode 100644 common/djangoapps/microsite_configuration/middleware.py create mode 100644 lms/static/images/open_edX_logo.png create mode 100644 lms/templates/openedx-footer.html 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..503114387588 --- /dev/null +++ b/common/djangoapps/microsite_configuration/middleware.py @@ -0,0 +1,104 @@ +""" +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 + +from django.conf import settings + +_microsite_configuration_threadlocal = threading.local() +_microsite_configuration_threadlocal.data = {} + +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 get_microsite_configuration(cls): + """ + Returns the current request's microsite configuration + """ + return _microsite_configuration_threadlocal.data + + @classmethod + def get_microsite_configuration_value(cls, name, default=None): + """ + Returns a value associated with the request's microsite, if present + """ + configuration = cls.get_microsite_configuration() + return configuration.get(name, default) + + 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 'Univserity' and any corresponding microsite configuration information + """ + self.clear_microsite_configuration() + + domain = request.META.get('HTTP_HOST', None) + + if settings.FEATURES['SUBDOMAIN_BRANDING'] and domain: + subdomain = self.pick_subdomain(domain, settings.SUBDOMAIN_BRANDING.keys()) + university = self.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_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 hasattr(settings, 'MICROSITE_CONFIGURATION'): + return None + + configuration = settings.MICROSITE_CONFIGURATION.get(university, None) + return configuration + + def match_university(self, 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 = self.pick_subdomain(domain, settings.SUBDOMAIN_BRANDING.keys()) + return settings.SUBDOMAIN_BRANDING.get(subdomain) + + def pick_subdomain(self, 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 a0cf27d786df..38dd17baaaab 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") @@ -903,22 +904,37 @@ def create_account(request, post_override=None): 'key': registration.activation_key, } + # see if there are email templates defined for this microsite + email_templates = MicrositeConfiguration.get_microsite_configuration_value( + 'email_templates' + ) + + if email_templates: + subject_template, message_template = email_templates['activation_email'] + else: + # fallback to default system templates + subject_template = 'emails/activation_email_subject.txt' + message_template = 'emails/activation_email.txt' + # composes activation email - subject = render_to_string('emails/activation_email_subject.txt', d) + subject = render_to_string(subject_template, d) # Email subject *must not* contain newlines subject = ''.join(subject.splitlines()) - message = render_to_string('emails/activation_email.txt', d) + + message = render_to_string(message_template, d) # 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.') @@ -1175,11 +1191,27 @@ def change_email_request(request): 'old_email': user.email, 'new_email': pec.new_email} - subject = render_to_string('emails/email_change_subject.txt', d) + # see if there are email templates defined for this microsite + email_templates = MicrositeConfiguration.get_microsite_configuration_value( + 'email_templates' + ) + + if email_templates: + subject_template, message_template = email_templates['email_change'] + else: + # fallback to default system templates + subject_template = 'emails/email_change_subject.txt' + message_template = 'emails/email_change.txt' + + subject = render_to_string(subject_template, d) 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(message_template, d) + + 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/lms/djangoapps/branding/__init__.py b/lms/djangoapps/branding/__init__.py index d70ffb1cc990..90675ea5777f 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,48 @@ 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 -def get_university(domain=None): +def get_university_for_request(): """ Return the university name specified for the domain, or None if no university was specified """ - if not settings.FEATURES['SUBDOMAIN_BRANDING'] or domain is None: - return None - - subdomain = pick_subdomain(domain, settings.SUBDOMAIN_BRANDING.keys()) - return settings.SUBDOMAIN_BRANDING.get(subdomain) + return MicrositeConfiguration.get_microsite_configuration_value('university') -def get_logo_url(domain=None): +def get_logo_url(): """ Return the url for the branded logo image to be used """ - university = get_university(domain) + + # see if a logo file has been specified + image_file = MicrositeConfiguration.get_microsite_configuration_value('logo_image_file') + + if image_file: + return '{static_url}images/{image_file}'.format( + static_url=settings.STATIC_URL, + image_file=image_file + ) + + 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 b725f7e1996a..32c2aab518d4 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,12 +26,16 @@ 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 = settings.FEATURES.get('ENABLE_MKTG_SITE') or MicrositeConfiguration.get_microsite_configuration_value('ENABLE_MKTG_SITE', False) + + if enable_mktg_site: return redirect(settings.MKTG_URLS.get('ROOT')) - university = branding.get_university(request.META.get('HTTP_HOST')) - if university == 'edge': - return render_to_response('university_profile/edge.html', {}) + custom_landing_page_template = MicrositeConfiguration.get_microsite_configuration_value('university_profile_template') + + if custom_landing_page_template: + return render_to_response(custom_landing_page_template, {}) # we do not expect this case to be reached in cases where # marketing and edge are enabled @@ -46,7 +51,9 @@ def courses(request): to that. Otherwise, if subdomain branding is on, this is the university profile page. Otherwise, it's the edX courseware.views.courses page """ - if settings.FEATURES.get('ENABLE_MKTG_SITE', False): + enable_mktg_site = settings.FEATURES.get('ENABLE_MKTG_SITE') or MicrositeConfiguration.get_microsite_configuration_value('ENABLE_MKTG_SITE', False) + + if enable_mktg_site: return redirect(marketing_link('COURSES'), permanent=True) if not settings.FEATURES.get('COURSES_ARE_BROWSABLE'): diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 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/instructor/enrollment.py b/lms/djangoapps/instructor/enrollment.py index a7c4fe0b6554..b92dfb95f932 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,10 +225,14 @@ 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')} + email_template_dict_default = {'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') + } + + email_template_dict = MicrositeConfiguration.get_microsite_configuration_value('email_templates', + email_template_dict_default) subject_template, message_template = email_template_dict.get(param_dict['message'], (None, None)) if subject_template is not None and message_template is not None: @@ -238,7 +244,10 @@ def send_mail_to_student(student, param_dict): # 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/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py index 7647b2c48708..f3cff1c766e2 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 = ( @@ -167,8 +169,11 @@ def purchase(self, first='', last='', street1='', street2='', city='', state='', '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 +528,8 @@ 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/common.py b/lms/envs/common.py index 893e537f770c..9d737a23201c 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -603,6 +603,7 @@ MIDDLEWARE_CLASSES = ( 'request_cache.middleware.RequestCache', 'django_comment_client.middleware.AjaxExceptionMiddleware', + 'microsite_configuration.middleware.MicrositeConfiguration', 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', diff --git a/lms/envs/dev.py b/lms/envs/dev.py index bfff68157e1b..467f4e19f3f2 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -126,6 +126,8 @@ 'mit': 'MITx', 'berkeley': 'BerkeleyX', 'harvard': 'HarvardX', + 'openedx': 'OpenedX', + 'edge': 'Edge' } # List of `university` landing pages to display, even though they may not @@ -135,6 +137,51 @@ # Organization that contain other organizations META_UNIVERSITIES = {'UTx': ['UTAustinX']} +# Configuration data to support 'microsites' that are bound - in middleware - +# based on incoming hostname domain +# For local test purposes, let's define a "OpenedX" subdomain brand +MICROSITE_CONFIGURATION = { + 'OpenedX': { + # what logo file to display, note that we shouldn't check in trademarked logos + # into the repository, so these images will have to be deployed/managed outside of + # code pushes + 'logo_image_file': 'open_edX_logo.png', + # what filter to use when displaying the course catalog on the landing page + 'course_org_filter': 'CDX', + # email from field on outbound emails + 'email_from_address': 'openedx@edx.org', + 'payment_support_email': 'openedx@edx.org', + 'email_templates': { + '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') + }, + # override for ENABLE_MKTG_SITE set otherwhere in config + 'ENABLE_MKTG_SITE': False, + # setting to hide the "university partner" list on the landing page + 'show_university_partners': False, + # override for USE_CUSTOM_THEME set otherwhere in config + 'USE_CUSTOM_THEME': True, + # override for THEME_NAME set otherwhere in config + 'THEME_NAME': 'openedx', + # These 4 following items define the template substitutions to use in the courseware pages + 'header_extra_file': None, + 'header_file': 'navigation.html', + 'google_analytics_file': 'google_analytics.html', + 'footer_file': 'openedx-footer.html', + # this control whether the home header (the overlay) shows on the homepage + # for example the overlay which says "The Future of Online Education", has the background image, as well as + # the top-level promo video + 'show_home_header': False + }, + 'Edge': { + # if set will render to different template in the index page + # if not set, then the default index page will be rendered + 'university_profile_template': 'university_profile/edge.html', + } +} + COMMENTS_SERVICE_KEY = "PUT_YOUR_API_KEY_HERE" ############################## Course static files ########################## diff --git a/lms/static/images/open_edX_logo.png b/lms/static/images/open_edX_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..4573e9e0d6e9232596f40daebee2326465bf03e7 GIT binary patch literal 14216 zcmV;3H+RU1P)4Tx07wm;mUmPX*B8g%%xo{TU6vwc>AklFq%OTkl_mFQv@x1^BM1TV}0C2duqR=S6Xn?LjUp6xrb&~O43j*Nv zEr418u3H3zGns$s|L;SQD-ufpfWpxLJ03rmi*g~#S@{x?OrJ!Vo{}kJ7$ajbnjp%m zGEV!%=70KpVow?KvV}a4moSaFCQKV= zXBIPnpP$8-NG!rR+)R#`$7JVZi#Wn10DSspSrkx`)s~4C+0n+?(b2-z5-tDd^^cpM zz5W?wz5V3zGUCskL5!X++LzcbT23thtSPiMTfS&1I{|204}j|3FPi>70OSh+Xzlyz zdl<5LNtZ}OE>>3g`T3RtKG#xK(9i3CI(+v0d-&=+OWAp!Ysd8Ar*foO5~i%E+?=c& zshF87;&Ay)i~kOm zCIB-Z!^JGdti+UJsxgN!t(Y#%b<8kk67vyD#cE*9urAm@Y#cTXn~yERR$}Y1E!Yd# zo7hq8Ya9;8z!~A3Z~?e@Tn26#t`xT$*Ni)h>&K1Yrto;Y8r}@=h7ZGY@Dh9xekcA2 z{tSKqKZ<`tAQQ9+wgf*y0zpVvOQ<9qCY&Y=5XJ~ILHOG0j2XwBQ%7jM`P2tv~{#P+6CGu9Y;5!2hua>CG_v;z4S?CC1rc%807-x z8s$^ULkxsr$OvR)G0GUn7`GVjR5Vq*RQM{JRGL%DRgX~5SKp(4L49HleU9rK?wsN|$L8GCfHh1tA~lw29MI^|n9|hJ z^w$(=?$kW5IibbS^3=-Es?a*EHLgw5cGnhYS7@Kne#%s4dNH$@Rm?8tq>hG8fR0pW zzfP~tjINRHeBHIW&AJctNO~;2RJ{tlPQ6KeZT(RF<@$~KcMXUJEQ54|9R}S7(}qTd zv4$HA+YFx=sTu_uEj4O1x^GN1_Ap*-Tx)#81ZToB$u!w*a?KPrbudjgtugI0gUuYx z1ZKO<`pvQC&gMe%TJu2*iiMX&o<*a@uqDGX#B!}=o8@yWeX9hktybMuAFUm%v#jf^ z@7XBX1lg>$>9G0T*3_13TVs2}j%w#;x5}>F?uEUXJ>Pzh{cQ)DL#V?BhfaqNj!uqZ z$0o;dCw-@6r(I5iEIKQkRm!^LjCJ;QUgdn!`K^nii^S!a%Wtk0u9>cfU7yS~n#-SC zH+RHM*Nx-0-)+d9>7MMq&wa>4$AjZh>+#4_&y(j_?>XjW;+5fb#Ot}YwYS*2#e16V z!d}5X>x20C`xN{1`YQR(_pSDQ=%?$K=GW*q>F?mb%>QfvHXt})YrtTjW*|4PA#gIt zDQHDdS1=_wD!4lMQHW`XIHV&K4h;(37J7f4!93x-wlEMD7`83!LAX));_x3Ma1r4V zH4%>^Z6cRPc1O{olA;bry^i*dE{nc5-*~=serJq)Okzw!%yg_zYWi`#ol25V;v^kU#wN!mA5MPH z3FFjqrcwe^cBM>m+1wr6XFN|{1#g`1#xLiOrMjh-r#?w@OWT$Wgg6&&5F%x&L(6hXP*!%2{VOVIa)adIsGCtQITk9vCHD^izmgw;`&@D zcVTY3gpU49^+=7S>!rha?s+wNZ}MaEj~6Hw2n%|am@e70WNfM5(r=exmT{MLF4tMU zX8G_6uNC`OLMu~NcCOM}Rk&(&wg2ivYe;J{*Zj2BdTsgISLt?eJQu}$~QLORDCnMIdyYynPb_W zEx0YhEw{FMY&}%2SiZD;WLxOA)(U1tamB0cN!u@1+E?z~LE0hRF;o>&)xJ}I=a!xC ztJAA*)_B)6@6y<{Y1i~_-tK`to_m`1YVIxB`);3L-|hYW`&(-bYby`n4&)tpTo+T< z{VnU;hI;k-lKKw^g$IWYMIP#EaB65ctZ}%k5pI+=jvq-pa_u{x@7kLzn)Wv{noEv? zqtc^Kzfb=D*0JDYoyS?nn|?6(VOI;SrMMMpUD7()mfkkh9^c-7BIrbChiga6kCs0k zJgIZC=9KcOveTr~g{NoFEIl)IR&;jaT-v#j&ZN$J=i|=b=!)p-y%2oi(nY_E=exbS z&s=i5bn>#xz3Ke>~2=f&N;yEFGz-^boBexUH6@}b7V+Mi8+ZXR+R zIyLMw-18{v(Y+Dw$g^K^e|bMz_?Y^*a!h-y;fd{&ljDBl*PbqTI{HlXY-Xb9SH)j< zJvV;-!*8Cy^-RW1j=m7TnEk!PbXFRCodHoe7j)M|J1#xAeZMyVc!Nx71oKOR^=|mMm|w4K|B`!NDeH1`Z(^hhqZF zWEhrA63B!El9K}&Aju&thlFJqmLZs!CD>-Mjcs|?YT4SQmeeivqV87j>-Ww7SMSx= zuRncXcT4zWFh^H<-+Qm>)~)T{y0>msy^f^?b_p)Ez{}7Auu?1*v+nM0``Z8hrcF#t zSTdP3&th;|K_YFZhAZsT?d4V-OIcF+rl+Q?di4q$uWPia$w@~eUvXg8!kAfH4@XB+ z_LE!c?e*(wTpr||QYLZTM<-ME7e8;azd15!SC=R4)KuCE@)6Awu54^f^y&)}7SDro zDfm(gTxx+x3tSSENMl@jd}Xu%BmIQ}X)ZKfG|eSJxzHBBgg<8scw2tjV|Ox{uv98( z$%O9lU_74GTm*=TWs72ItI&|0(0eKz>gQr)gsNJ&68r&4BFWExD^%@*i zf;FM%qPQ)S(4bwWF-aS|qeT$jUZEZx4TM_W0jE+f35si`OFu7X3*hN$S-Qj~R82@gyFQEDXjpAcL!`3+)Zf#nxP$umTCo!NE!U>F}hr=s9Y-krE-P)Wc!fX{Cz)y~ZMy zPk1VCk@AblordsTf>&}2%+ZC8g&4|T2AGRoaM7Yc#>U3nAP;_FVS#macG-8n`#l?% zmPr;A+N1;n_K!;wOofr#HWrxEK$8qjNaM&&TqgJKiQX~$!(X)91O1b>x*%pliVsQrAdHkDP z&bO?twh6Qqc&d{uxJ&SgZ2`E!_4Yh^(F@bX3QU+6Ofx%3OK46~T2e9tt-QRtFA1$}Vdt zvk$MYw=bL-v|XB8tQV7ZDUZT1118~h;`=VSp>Fua8QW8ouq&I&oi)*xDRW43c((*AUHxxM|GdKZRVM9C(fI5p@ZF6Jki*#)Qf z9XCh9+0gRkpDR2Uoxra#U4Zk_DS$ATCp(V!Oqd?>jkFmY@WU5w*?B|E;|BN0Zx|61 zW5!_qLa`6egTofnDtPR4huP^av+f?VVOjYwu$VhmUTRifYq3S@{6&oxTUcYU(h`?X zdO|G2=q`HEe0>>-i;Gj3Z@^mPqoX!>uFuZ(oV9^-y*4s9U=uQ<(qWm3ii)kQqSC4s z)L3v8S<~@|vLokj6@4q z(gT-yN~6;49G zBlMoH3^XcnFgW1w^yBd}j4$FoEWn(`@d=~x+0U7GJw)r13HNzJ3>-(4dtigRau67e zb4b(Wa!)WcwUK}@3dc{JvDn`I7TdntVh`-s3{*nmj0jkY%L9f5>>%S}qXgx3>n(o6 zMvGsu#$t5~ltkvwxVDVMgET?b%18MXCJU68WUsT^PMHMq0DVd+JEb)dmLGPH+y8pvjQz#!%~mSrM#FhwK}*qY``U->wkOm!nqc7W z^%_c9y4I>w*lu~l%H$3Et8EQdUm>AWfpTf6WD#zM3j{JERnSBim^egt3@}I5J{n;m z&nWn~XqbPdQM19}XC=xmLnb<`U?xqNc_v=OdCt7+!7&X1!p($dczi!7 z%!m^MI2%BtbG)a|5>M{1`1c<%`@RGO0l1>l?8e0bw?Qu~E039iXhLf}qn7^U!wP)Z z(!aOSVt2pV;+rKjWzsNXVnvrZBYn&YB#^@c{dWAoi+1d#z1G*=WpN2WK~a%3Mdge^ z!Q~VAK^P|G;Uewv120*_;wHQ1`Wvjgyj(&g(#VhZ+&+G2 z&^DAM?X22>YK6^fJ$k>gEMb3pWXM)kb=tdcXmmmXlLMH5XO#YrA3NjR!kbDG)-C~K zE&>MPS}tPeXj{#NesoKNt!*r0e8^k3B)As~ze3XKTF;U>^y$B_{xdLL+>DUv7aGDd zUnbPIawZ@|%2x@WLB9AXo%!H;oM^hap6MJ+Etx-weFrV^^&eR5%g{&5JBGI_4(f2`v=1w;r<> zo_@kQ+fF#4DV0e@N1#d28goHHscE@^|l?q4DN2Db_Ep79o zv*T_-!MCPE)Wn#k70TKogn$z!UBMQ=b5nZodq{swpb81FEnkhwe(;9#3uJ1xAgz` zT`O2Gb4>G=Lwk1Gp6!oHD~!822kBEOgfK!2(z5`9z%F3QUM?Y<)blBsZ-rWLL{Rbp zRmOZ89iOsAV*ZcZ*kC&zYO_5Op7rv0b*sKGIZQjC>M#js5%CRAcG>e41$On)3RlXn zJ=sXOGw-ghZO;9gB+Xz1KI$dyEGz*qZMM=lMZ6FTt-<3&7C0 zf@HoVAK7k+cYay(5Sa$m#b*6taI^^{sS^<0%OxBvjCIJ{b#zoUfw7SW#WDV_)p|y) z(u$c(o+UTmVf9QVm}bxxyGe_8IEsLhE&0K(o2{w{gaj=@?*lMBngW4&{Hy{bKv!$zedjW{gC$hBbTPBqipswDEOG};bOlk5yBzN&ZU$0!q=j0YHb`$@Mu!+Wn0yV`=iwEk>SJv4l&yCw3 z?d#WKWYW42M8$)Hz~Dl@OsnlbJU4+QW~d0a|E*UQW7|_OyLPgTu;&CCKx< zwcN`lC=}Y2T$uymEqM72_;vNmSC3gE^Cij=&P2JW(Bfx%EcumxRnHE~Jdyy&q;Nt( z5*Sz`p);Sf+HCLGVCmOx6{jpWtC7pMgpG&d3S7SgDonS$(-JY+U5# z9tGN(f`g|XG;fw8~ zwn-Y{j0OvY1fcl`)hq}L`ZOve2o$@4fj$ii-@UPJ#;X2&yY5#K6t5zYOa?jbWK6h| zW?l%Vl)oal3cZZo0CGK2oHX7 znY0O8PsoRHosz-OCEBvGpr*!}m$uj{;q89*X*(hpaY?Dlp!kaw7BmX;m0H#Nk2Ja8 z{a~9tH8g1(q*1*mjHaO+2#YHZ0RmbA2Cr29yR|*#Q|l}3EgR=Ic_)1oE=o`+48W!| z1cX&gIVFP8Md5Do$aR?dQ*pE_e2@!3^ZxJi;+2j?FMyE&aP)M~MQ%!&kPrD~07OqG z8cwjo4a%3n4WYAzOdr`q!7~L0D30b4TA#F-v_fn}i^bpb zc1tZ@Xp`^xmKADaVQ*qgj^;#)>psInwPC` zWYI2uM5KPMee^RGPWoAJY>=Scvf%YrQB~;#rlfRwpO+(Yfj@Y{##Fu)btU%qGMlb_ z=(O!o+pN)Ct6%kWgR*Ewnlh6^r6PAru-?6-#O{?gr9YyJlNrQ09xhl=Xc$aAn5a%G zq>HtItyV`gx{@lkfvHjaSLk{SwLI#YGQd(tIRyl3KWq1bx-WvXR$|Hr2&?( z{WSJV<(lnwB~PC*LL@WHN{TIh_Z^nh0%Q99 z-?NijV>+{OP6wfi)n>wpDGzz4CJr65vE?mRSX*OTZ+pEpFJEa>Y*!JE+wvm1zDg83 z@OFhlBb+g<$rNgyvgO8YZqmPZ$8$~-U9482?Z|Ob9lfQc((b=*!tUJOWuxLLzjE)( zh!i;G$C&;{s_YY{zWT&MQ zvh8VTlF{SN1*&)LoK1b>UQ2IUr;$S*E|?nuapZ;Q)JK?x47~sb+^jTXmevY?!Ee3R z1`nOElb`zsORQgLu>lDiG~|MgEv~Z3=UZ*~%G0*_O?TSjmgP1ffh5}PGlT9dfY5NU z53EQl`B!hfUfWGq$`zT9h~d!|!~wx?VAc(3BOP&3sF$s}NgfL{3P~szK-u~vjL?ai zay(O};qXG!p~k@nAn$1eA;CZ}COSd6=$-(4nkor*vos;JsWX#CPpVu+G{rO zre&HniS`cd-8yXApAni}VM^s^&-PgQeOoPcp^Cb4un{| zrrHMHe3PBElk$2^SxQ>iff^&Bjg-TaM`bD0T1(3cS4Re-Kx;&a<$m^Uz}m^g_?T5z zSK5YaHoJMv6b^>D8YDMa7ph`7{^`@;ds4f7dGURcoeZw=gWsvXVBHBJfyTu^?jbV| zr3vM-QJSR5nIzLlC*`_3rbVm~jj#-G4HAmg!Yk282s0JyzDdIMMB9+o%XG3$6{Nh( zH!w|Fb*&eEi&kSjv=2_$zO$q9g2uG-v%m=^ar8gJw@6y(c)z|IB^opUrrR@ThMhTh zRReCzfYO-C^qj(BUJcq}{Wp^yj6qHBPYf!Ez^iI+MoU9bF<7v3_~M_`KOXU42xc+95Cv{0Cf z_PoN>FA_c`_=}pFt^MRlr&TV-1cQ$X#YYTYpL_C*edgGZt*b~_|IBeTQ!4352UYTf z-K8~{N5nH6#sX;83$4?<1ckPS(P}j(!Q670xrQ}6CfDWjow9%Rj)lOs%onBN2Q`{* zktRVHR7b1OsJK!`Gk155Iy?%>#cC;Lls^~aD}YG^jd6KqUXq$!sEKT~@`o1X!DNfY zCW3bgBp6W{M|DMkHyU?T^a`c6$!k0^3^{ty1bU_n2;Q)Sn)!+c+Q6BQ3SdsQOoF{n zW+1}o%MUJP$1KCAk8sTnR7i+o(Adc{YDOh+X~KLdpxVK%t+e>TV^W*46UQ}o5#d~< zXN0Mt)O?{3=q6fmgh9FV9N6jBOOh4Z3eT#)@DP;Iks+&JcAZr}+ig?5eVP-6hnQxs zM9x@*6fJ_u)!eeoPRVtAF+xHUcm0P_bQ5eU_U@N(Ij8ay%$>WGCu z3_3J6%n!{T?Q_~8EkhnKbgpgH2qR?##{=`DN(m?zut=-oozg0&<*W_@a1wMZ2U;m@|<}?ZSmx2bH-hlM%CQz5+wPkvoF&@WDQN zhqm9>Ym+2(VIM-!6G7d4s}Kk9-lNXnsLt3e&Hs^Kp0UrrzB$-VFDCZe@*VOVKl2h4 z1cQ#pHjhHD7>+px485~4-tvJO=*#F ztpBVf&T+P6Ix~M>gf*3fTHfs_4hs#IN_JsDg0SqRWwI``nIAo z9?`H_pJ@;Zldn<`B#&_PpOw~Zk)o5*TF9hMLdMwY5(1irB5*Izxe@dZtM})3lut4L zJ9N<|tg&E<%@<%~`oB$c;J?s`#&$VtHp$E7&z3-kCP84>_jOPohQLI*T&6{zGWEkc z`MWbLGS~j<@Sv@z?y`HfHaH=fudqzFl!(3DaGo7ViRS^J)o8NG6$LjDRpcQZLh2L4 zW3ojC#3*X;tj2>D{&##=g;MzlFCc{{W>CEQA!KfVhnc6eYI9;`ksJo372t%x`7qx` z4^n0QLJJi&TZNo$sqUX=J7(peS0&UuB$h6;#JEi2!TxZRLr5edltC!cCAw)2i1#C` z<3F_A-#(#XO!@k=wW>;z3{Asm+RFl9VG|OS({q5RHJ0#Qo zi>F6zy)+01zKEx<@q3Mk=5h(z@9*f<;8|d=xxB^+3*(ckyKwTsOis*!B^r()FkKkQ z0e-14+5u+q??EgW3eyYo3$ING^E47)3>dms*`lUeQ%p~Ykw_j9YvrgqI0&=mR7 zN9iCKyS1~H2|UgQMt*2>*T>YIiRc$EeY)B1Xf{l(29=>vngrl;I8FK}73G{aC%BaU zM+Sx%IIX=?=hX_D^fYbp6muAFna@>wq2>Stf|0h!1|<-kdMZ~tLY$&&;IU!FbnC9Q z6I_QO2hk@N4N5#RJnYt1IMg*4&_78HYOve?-PvV-EiG_`HbZtRfTr+(ZfaD2?2xPY zPga)O`*kp__3Vg!@rNh1(3r9-v|R)f4Ktry@sPPQERw9(>WJ_jSq6Nd0`rc#&jxds zTB#8Rv@q)wOvpVf(1>3q?!!A(Ec*-BzL!iY;TP;wR!4*Nh5=ow+ccBwnIdvU;f$OC zhvZRX@{NEok`FmkFPM;L zyZAMjD1J04o#C1zER53|5=iA_)kpSN)#+naTBL0&d^$)u;}I*Il(VHoTV&Q;W65PY zm8g!NL36IUMB|mOORIaYK1#u#fnc7J4=5}Bw`KRs<{)#ugfb3Q2H%`=3}P430ZAAGFCzIsQ2 zE!NJ8xvYVC35xP@^N>?AFYsao^Mzn?zNV(gp3{guB0q0n^9L^47Oj1-)&Tg1KBM$Z zh!HAi5>CV5f`wP200XnW!sMR@1eZAungm>*KAJ{t;O0cpFrI1ngt&^dc)4mcued@J za-dy#?CKhEWp@GL59Pv2ipOy=| zqS~E3aWwQ(pwVtf^PrJeZLylBMOu}Y>)$U*LEr*_k(znQsFn-bPMlCbBDf5wRP219 z8QkPugV>G}1NO$Jx*g+{irM+hDVmVFU`C-yI9pPqvnQX>*%Ces!F(7Vxk+bOzO8}m zKRwxPHJo7)K5i1Ukw3$7l}xjr_Ke%79zSEBculjj3TE2_M%p>S^`i=a$LFZr!2tb7 zr?AiII-NvgZxjN+=Ps|*0^QZ}S0gw`DxGjnX^*JzjOqcszC2~A`Czn*-HjJ zOg!F68f9FE82Ryd8BS3g4U1MHJ%R!bcMm-JJbf7N2v>=4qoyOCJj_YTin(@D6MM`- z2iZhCvH9H9Ml1rRTAEZAbt^RW?h3B~!<~26LS*9Mr?uTgTkpl_I;*S3P#lx{d>0dE zhod=%zbjo_`BhZufR-9_QrqLx3gb-4lvdJN_#D&OlGgqEbU;h4e+h~AhRvrcQ5-yK ze5|{py3B^}iPqXrop5P&X$YvCZz&_LR%T ze=4?f_rh<$U7PCcZ`M`XJ{{;E2*fm)))7>9m6fF*SNi2Y0(Mv(|Ng^7eKaX z4L+qhGBRo}?AqbBvy_I%i*qGF8?Zpzt#S6!7K%>b|7E6>(JoniUs(<3^=lALjdMJ=ZV$+WGMS@&P}^f`wNd&pdW(;pCv zKrY^kL{C3bNRPZjEAe=q*yrSdk(WtrR7i+rk?tvdU+JuDLT|G9H1sb%a?%h7_yY1y zICF`BTU_+{KVgMn`YLH4(s`jHif3Gm{wfob_?^xWL#C%|5s-)B5opTB9XedNl!LHf zqDg14*mzcxdw??ddp6A=W|m(ueY(2JQnzl>VJmr~RAwgt&_rN34_}|O_~-Al*wI$K zOZaIlGzIru7X{+-crx#ppidq>VWn*yRyQTvzpKk8bWn_IvZvR^yL8Y?3yr|;ryjGO z&JMQ|m`0jS8@L4F58oaRKAabGLUQcT0ji`{ zC7nx`_$XAXJ}*g2xDP{IJ$<%>Dtd65L7lijR?bKDy{@Y?=+BDiP)(Fc&WT8fGOcL6H5qU_Ri+2b{ zqJd!G>~>->#LdeNV;1Zpb#=ICCl)PX)o5I_0(2m;q(80E2!SBKC_d%R2I8U5R(RcJ z0CmJcLAkO4)Ips$Cr!YAX}8ZUsYJvP&SGdb&1^a|-a|&_2=L^V4VJk4A@?0FcSs9u z0x^#8UZ#osk55{>>g$&Jz&mv=r6tfT+Wz7o!!49VwN_GhCtSORaU-U5C~WG(Uvp^?td>p z+!>e(RS!(P7j`~x&pq+Djs^$w!SfhErDIS^V$%wKzQ5l-pwWGmgoM$aTKQoT4bY%= z*rmz)+x20H_vma1CbgfJkUk2`&)|dQwI%lXZS{8DL+!R#{jx^=X3qEs0C7+sy(QaG+$v<-xV5k43k^_qc9 zBP)b51qVQ&>>egBN7)S3$H}OTw485X7?<7e}@`rp+zju#QAt=(Y5}a zct$b89kOm7JPz+Jp1ZrIV}f+bD^zcV-@Maz(eI|9|_1l~&7R)nb!jV^4S=x|oDV&5Ps{x&x ze^KVvp68x%ADh4>?C^a92*(MH-Z7Jk;uDrwwavaXw8&l<8r6~BU|z!d2=vH_Gw&~H z^1eY7Zl1tVRBtmh$|mtM+Gzx z*dSe$W_B3wlp#tZ`=)K{2C<%Olz9@qS_KxgQ+Te{@Vo2+>C%7C$jQ=o4O z4-XC6$s-4CpkEs@1*GHRJUw5ocl|Giz`5QYJ9==xJ@@#dc39`;i*-EMeYpjpa+P$+ z-D5)h!QjGQ3GSOHwy*U!+Ka^%woX+T)G$=6ID`OUU~9@*ReMBf?tfLIUAb8G7kC~D zsLuYoM$V;0a&`Zowh?QT^@Hck16qp@RIHX|^y98^tJP;_x9A&TSR5WMN~4f?nnd`l z0K$}?`*C-k^FXwcQCjDr&k~s(<=?sc=L|gsoF6tbWagXj!!*=|I3J(=P9Ai6E{KMD zUI}Q0F}iu2;4bR&4i#*F&XRY2R$o`q+DehQ$A4;wOpr*91&>G&c1oC%GHvc!Cey4* zTN<<$3}*wAquUkrq*4|J8=JUJb19b-+JX3-~-;^7t28aeXc0rrth6B zwZnt#Y>+!wl|C{MxBZ1x)>u*Ec5DYGe4x!I>$N^oTxv&U?%t}2_gh=aoF@{ig|)HU zbu4-#i-yXE;`%!l&A(|K=5UXkPts6WHte4Gv({m25*|Z|vrNOnQA)o;*s3LjKVPOD z&sySOn^u7|{mUXK(T14sb5u}fOXHa<7mn(i69~_a%S9)<40EN4#%Jf@KDZE;CL7Ne z(w8Af8)E6^@m;)nnltW6|=3)fkt7@1ZM#V3yuyhzW23XZexTaG>04x z>`*5Yi}NjPDn+nJhfwQ=`!>!3a__k9X+LI%?%8BjEiKx1&+!Ez_yUUH+ z7RE~!2m$3$nYCDniN$xl&>|TxT2K)83&}!hkk+9J`|K%s_1GE_RNCrgV6j6y7;g|_ z7N+HhxnjLdYON=cnv}VuT@fK6dB73kD~mRTLM~;&E!P zG{BfPpnmhuHFA#G0eQMg^(Pyc+oW(3gfs%}ASZ^rW<}WV(1r5>Xa!G>VP^^nIG9<| zN}5OUh9(i`Tb%@j?Uxi;TU?z-&S68$3v@`^zpf~WyFdCv9rvm(xc_qjp|!ZQG^e{k znury71SH=dJ0)KYInE?Qv#ixZCRL|-t3>BFQczUSS2l>L+&YQoD--|mO_tWTx)T58 z+j0-f#9AwXzzhq`Cs$GCEsTfstm7Mg6h`kmX4ZSCMH~YnSZJb0bo5vs*o*(&?_1Hf zt+ruQLMdiGu;Xb<%5zm%qzSVYGlK<3N7xBMwmC%}WR(ltf~6w(Q*~&ZfC|3|h!>I| zW#Bd;hImY`nE)em&`*a0f;g&}FJs}-Dj*;r%s~(Uv_(FE6!B_Z=NUsdNL!@qlAx$D zek}oE!IZ*feAgY8ZfdaP-~WsLYQr&|sc(=QNb?AUg(1(eX~~a_xI|yT1C^tFJ-oY>F5hGwniF=+PjU zLZkW!2CM!xI=IDiDkbwT1WKM&dKcrgm!7hPfU2p+Zsd2KJ%^qD$EPO`(#<9$Q7T?Q zQM@>(NPi(4XQQ;vllB>r^LTNE@gS|F$yGVe8;&fkE5sRea)p6Lcmy(O!tiK* zJZIkZka>=VN1r1&;O4^jVg4?+i^VX1bWd%&)+V*ZCGpT>7XRWSnxvo5{vl}>b{)X{ znslZSoM;`?5HW-UGfi=6z6sXZXR&LSSo}-(Sp2pdEWS`5!;?Vz?+wS5*QzTvT5Wx! z9e!b_wH|!QhWiJdc1bY*kY<7b&)cu1EZ9^@+Y`rM=;GsfW%_$6n{T<*7Sz_+!w=}| z`4gi~fYbyIs4Lfe!Q=Y-tm#6edEw?Gq{O68vMcvu;pHg6TzSu6`Yhyp`R5ura?!~S z%ce&>VYCo;AJ02^_;()i(aPX}AHHB(WXy~Rf{TY%q)Xf+nmGn%4)nRx`0w0palQ_z zFFM8_++p^!R#6tCn?3@uL0@rXQHvo^xAd46a-V0lSsNR*hb(sI3jNisw^&TuVA4yQ zY)V3r))o^A=JF91e|M3&MD;?gXm7jCmaJZDCypGlcKyXIJX*{@3M4QrDxx(!kZxED zm+s>f(oATO<*P2!waOOj@2~Xe&*tGd^KCFwqCJ&g-+OYAmiaFRBK>Bnsw<@sqR3QG*wz(cZ_N603o5$0TYv#o|RWZZt1EO%WZkfQhiH!i`>8Z z+lso*cIi7^9i498L0U|uGA(+pT)D#P7S>yhT)H)N3-x_s39jOuqfXv3*I>SpBKJ%m eMZ$jF9{(Rp0O%97AB <%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"> diff --git a/lms/templates/index.html b/lms/templates/index.html index 56d08ce1108f..0d5f58d5257c 100644 --- a/lms/templates/index.html +++ b/lms/templates/index.html @@ -5,7 +5,15 @@ <%inherit file="main.html" /> <%namespace name='static' file='static_content.html'/> +<%! from microsite_configuration.middleware import MicrositeConfiguration %> + +<% + show_home_header = MicrositeConfiguration.get_microsite_configuration_value('show_home_header', True) +%> + +
+ % if show_home_header:
@@ -51,7 +59,7 @@

${_("For anyone, anywhere, anytime")}

- + % endif
## Disable university partner logos and sites for non-edX sites diff --git a/lms/templates/main.html b/lms/templates/main.html index a2395db4ed39..e707ca8de18a 100644 --- a/lms/templates/main.html +++ b/lms/templates/main.html @@ -1,4 +1,5 @@ <%! from django.utils.translation import ugettext as _ %> +<%! from microsite_configuration.middleware import MicrositeConfiguration %> <%namespace name='static' file='static_content.html'/> <%! from django.utils import html %> @@ -8,11 +9,26 @@ ## templates have access to these functions, and we can import these ## into non-inheriting templates via the %namespace tag. <%def name="theme_enabled()"> - <% return settings.FEATURES["USE_CUSTOM_THEME"] %> + <% + + theme_enabled = MicrositeConfiguration.get_microsite_configuration_value('USE_CUSTOM_THEME') or settings.FEATURES["USE_CUSTOM_THEME"] + return theme_enabled + + %> <%def name="stanford_theme_enabled()"> - <% return theme_enabled() and getattr(settings, "THEME_NAME") == "stanford" %> + <% + if not theme_enabled(): + return False + + theme_name = MicrositeConfiguration.get_microsite_configuration_value('THEME_NAME') + + if hasattr(settings, "THEME_NAME"): + theme_name = getattr(settings, "THEME_NAME") + + return theme_enabled and theme_name == "stanford" + %> @@ -54,8 +70,22 @@ <%static:js group='main_vendor'/> <%block name="headextra"/> - % if theme_enabled(): - <%include file="theme-head-extra.html" /> + +<% + if theme_enabled(): + header_extra_file = MicrositeConfiguration.get_microsite_configuration_value('header_extra_file', 'theme-head-extra.html') + header_file = MicrositeConfiguration.get_microsite_configuration_value('header_file', 'navigation.html') + google_analytics_file = MicrositeConfiguration.get_microsite_configuration_value('google_analytics_file', 'theme-google-analytics.html') + footer_file = MicrositeConfiguration.get_microsite_configuration_value('footer_file', 'theme-footer.html') + else: + header_extra_file = None + header_file = "navigation.html" + google_analytics_file = "google_analytics.html" + footer_file = "footer.html" +%> + + % if header_extra_file: + <%include file="${header_extra_file}" /> % endif