diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 952a0dfd9b..b05af2a0fd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -75,6 +75,11 @@ Common: Allow instructors to input complicated expressions as answers to `NumericalResponse`s. Prior to the change only numbers were allowed, now any answer from '1/3' to 'sqrt(12)*(1-1/3^2+1/5/3^2)' are valid. +Studio/LMS: Allow for 'preview' and 'published' in a single LMS instance. Use +middlware components to retain the incoming Django request and put in thread +local storage. It is recommended that all developers define a 'preview.localhost' +which maps to the same IP address as localhost in his/her HOSTS file. + LMS: Enable beta instructor dashboard. The beta dashboard is a rearchitecture of the existing instructor dashboard and is available by clicking a link at the top right of the existing dashboard. diff --git a/cms/envs/dev_shared_preview.py b/cms/envs/dev_shared_preview.py new file mode 100644 index 0000000000..119558ba05 --- /dev/null +++ b/cms/envs/dev_shared_preview.py @@ -0,0 +1,12 @@ +""" +This configuration is have localdev use a preview.localhost hostname for the preview LMS so that we can share +the same process between preview and published +""" + +# 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 * + +MITX_FEATURES['PREVIEW_LMS_BASE'] = "preview.localhost:8000" diff --git a/common/lib/xmodule/xmodule/modulestore/django.py b/common/lib/xmodule/xmodule/modulestore/django.py index d033e3b89e..83b537d3aa 100644 --- a/common/lib/xmodule/xmodule/modulestore/django.py +++ b/common/lib/xmodule/xmodule/modulestore/django.py @@ -7,10 +7,13 @@ Passes settings.MODULESTORE as kwargs to MongoModuleStore from __future__ import absolute_import from importlib import import_module +import re + from django.conf import settings from django.core.cache import get_cache, InvalidCacheBackendError from django.dispatch import Signal from xmodule.modulestore.loc_mapper_store import LocMapperStore +from xmodule.util.django import get_current_request_hostname # We may not always have the request_cache module available try: @@ -68,11 +71,41 @@ def create_modulestore_instance(engine, doc_store_config, options): ) -def modulestore(name='default'): +def get_default_store_name_for_current_request(): + """ + This method will return the appropriate default store mapping for the current Django request, + else 'default' which is the system default + """ + store_name = 'default' + + # see what request we are currently processing - if any at all - and get hostname for the request + hostname = get_current_request_hostname() + + # get mapping information which is defined in configurations + mappings = getattr(settings, 'HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS', None) + + # compare hostname against the regex expressions set of mappings + # which will tell us which store name to use + if hostname and mappings: + for key in mappings.keys(): + if re.match(key, hostname): + store_name = mappings[key] + return store_name + + return store_name + + +def modulestore(name=None): """ This returns an instance of a modulestore of given name. This will wither return an existing modulestore or create a new one """ + + if not name: + # If caller did not specify name then we should + # determine what should be the default + name = get_default_store_name_for_current_request() + if name not in _MODULESTORES: _MODULESTORES[name] = create_modulestore_instance( settings.MODULESTORE[name]['ENGINE'], diff --git a/common/lib/xmodule/xmodule/tests/test_utils_django.py b/common/lib/xmodule/xmodule/tests/test_utils_django.py new file mode 100644 index 0000000000..1d5bf9d83e --- /dev/null +++ b/common/lib/xmodule/xmodule/tests/test_utils_django.py @@ -0,0 +1,20 @@ +"""Tests for methods defined in util/django.py""" +from xmodule.util.django import get_current_request, get_current_request_hostname +from nose.tools import assert_is_none +from unittest import TestCase + +class UtilDjangoTests(TestCase): + """ + Tests for methods exposed in util/django + """ + def test_get_current_request(self): + """ + Since we are running outside of Django assert that get_current_request returns None + """ + assert_is_none(get_current_request()) + + def test_get_current_request_hostname(self): + """ + Since we are running outside of Django assert that get_current_request_hostname returns None + """ + assert_is_none(get_current_request_hostname()) diff --git a/common/lib/xmodule/xmodule/util/django.py b/common/lib/xmodule/xmodule/util/django.py new file mode 100644 index 0000000000..20a7c3fa79 --- /dev/null +++ b/common/lib/xmodule/xmodule/util/django.py @@ -0,0 +1,20 @@ +""" +Exposes Django utilities for consumption in the xmodule library +NOTE: This file should only be imported into 'django-safe' code, i.e. known that this code runs int the Django +runtime environment with the djangoapps in common configured to load +""" + +# NOTE: we are importing this method so that any module that imports us has access to get_current_request +from crum import get_current_request + + +def get_current_request_hostname(): + """ + This method will return the hostname that was used in the current Django request + """ + hostname = None + request = get_current_request() + if request: + hostname = request.META.get('HTTP_HOST') + + return hostname diff --git a/lms/djangoapps/courseware/tests/test_courses.py b/lms/djangoapps/courseware/tests/test_courses.py index 20cb83a411..ee05a483a5 100644 --- a/lms/djangoapps/courseware/tests/test_courses.py +++ b/lms/djangoapps/courseware/tests/test_courses.py @@ -1,8 +1,11 @@ # -*- coding: utf-8 -*- +import mock + from django.test import TestCase from django.http import Http404 from django.test.utils import override_settings from courseware.courses import get_course_by_id, get_cms_course_link_by_id +from xmodule.modulestore.django import get_default_store_name_for_current_request CMS_BASE_TEST = 'testcms' @@ -26,3 +29,14 @@ class CoursesTest(TestCase): self.assertEqual("//{}/".format(CMS_BASE_TEST), get_cms_course_link_by_id("blah_bad_course_id")) self.assertEqual("//{}/".format(CMS_BASE_TEST), get_cms_course_link_by_id("too/too/many/slashes")) self.assertEqual("//{}/org/num/course/name".format(CMS_BASE_TEST), get_cms_course_link_by_id('org/num/name')) + + + @mock.patch('xmodule.modulestore.django.get_current_request_hostname', mock.Mock(return_value='preview.localhost')) + @override_settings(HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS={'preview\.': 'draft'}) + def test_default_modulestore_preview_mapping(self): + self.assertEqual(get_default_store_name_for_current_request(), 'draft') + + @mock.patch('xmodule.modulestore.django.get_current_request_hostname', mock.Mock(return_value='localhost')) + @override_settings(HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS={'preview\.': 'draft'}) + def test_default_modulestore_published_mapping(self): + self.assertEqual(get_default_store_name_for_current_request(), 'default') diff --git a/lms/envs/cms/aws.py b/lms/envs/cms/aws.py index baeaebca1c..62e3ed978b 100644 --- a/lms/envs/cms/aws.py +++ b/lms/envs/cms/aws.py @@ -12,3 +12,5 @@ with open(ENV_ROOT / "cms.auth.json") as auth_file: CMS_AUTH_TOKENS = json.load(auth_file) MODULESTORE = CMS_AUTH_TOKENS['MODULESTORE'] + +HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS = ENVS_TOKENS.get('HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS',{}) diff --git a/lms/envs/cms/dev.py b/lms/envs/cms/dev.py index 124ed83225..38d70ddab7 100644 --- a/lms/envs/cms/dev.py +++ b/lms/envs/cms/dev.py @@ -40,6 +40,10 @@ MODULESTORE = { 'DOC_STORE_CONFIG': DOC_STORE_CONFIG, 'OPTIONS': modulestore_options }, + 'draft': { + 'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore', + 'OPTIONS': modulestore_options + }, } CONTENTSTORE = { @@ -59,3 +63,10 @@ INSTALLED_APPS += ( DEBUG_TOOLBAR_PANELS += ( 'debug_toolbar_mongo.panel.MongoDebugPanel', ) + +# HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS defines, as dictionary of regex's, a set of mappings of HTTP request hostnames to +# what the 'default' modulestore to use while processing the request +# for example 'preview.edx.org' should use the draft modulestore +HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS = { + 'preview\.': 'draft' +} diff --git a/lms/envs/common.py b/lms/envs/common.py index 6a9be412e1..4e9c47ebf6 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -585,6 +585,7 @@ MIDDLEWARE_CLASSES = ( #'django.contrib.auth.middleware.AuthenticationMiddleware', 'cache_toolbox.middleware.CacheBackedAuthenticationMiddleware', 'contentserver.middleware.StaticContentServer', + 'crum.CurrentRequestUserMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'track.middleware.TrackMiddleware', diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 8da2504f2e..b2b8e91694 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -98,6 +98,7 @@ django_debug_toolbar django-debug-toolbar-mongo nose-ignore-docstring nose-exclude +django-crum==0.5 git+https://github.com/mfogel/django-settings-context-processor.git