From 8f9d815f10044e0df289b644bb53f62b54c9b8a8 Mon Sep 17 00:00:00 2001
From: Chris Dodge <cdodge@edx.org>
Date: Thu, 26 Sep 2013 17:24:52 -0400
Subject: [PATCH] add some middleware to determine whether draft modulestore or
 non-draft modulestore should be used

change to use regex to do the domain mappings. Also add config to AWS to be able to set from configuration file.

handle cases where HTTP_HOST is none, like in unit tests

add linefeed at end

fix up regex matches

switch to use thread local storage to hold the request itself

.

.

convert over to use open source 3rd party library

convert over to use django-cum

add unit test

remove comment

.

add comment to config setting

fix comment

use better regex for localdev

no need to break

no need to wrap an imported function, it's visible to any file that is importing us

add comment

add unit test

clean up test

use a separate env file to set the preview hostname
---
 CHANGELOG.rst                                 |  5 +++
 cms/envs/dev_shared_preview.py                | 12 +++++++
 .../lib/xmodule/xmodule/modulestore/django.py | 35 ++++++++++++++++++-
 .../xmodule/tests/test_utils_django.py        | 20 +++++++++++
 common/lib/xmodule/xmodule/util/django.py     | 20 +++++++++++
 .../courseware/tests/test_courses.py          | 14 ++++++++
 lms/envs/cms/aws.py                           |  2 ++
 lms/envs/cms/dev.py                           | 11 ++++++
 lms/envs/common.py                            |  1 +
 requirements/edx/base.txt                     |  1 +
 10 files changed, 120 insertions(+), 1 deletion(-)
 create mode 100644 cms/envs/dev_shared_preview.py
 create mode 100644 common/lib/xmodule/xmodule/tests/test_utils_django.py
 create mode 100644 common/lib/xmodule/xmodule/util/django.py

diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 952a0dfd9bc..b05af2a0fd4 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 00000000000..119558ba055
--- /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 284f5e4eb78..f608c98619c 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:
@@ -67,11 +70,41 @@ def create_modulestore_instance(engine, 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'],
                                                           settings.MODULESTORE[name]['OPTIONS'])
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 00000000000..1d5bf9d83e6
--- /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 00000000000..20a7c3fa792
--- /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 20cb83a411b..ee05a483a57 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 baeaebca1c6..62e3ed978b6 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 7026d453667..6444ed569d9 100644
--- a/lms/envs/cms/dev.py
+++ b/lms/envs/cms/dev.py
@@ -39,6 +39,10 @@ MODULESTORE = {
         'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
         'OPTIONS': modulestore_options
     },
+    'draft': {
+        'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
+        'OPTIONS': modulestore_options    
+    },
 }
 
 CONTENTSTORE = {
@@ -58,3 +62,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 22047afb285..a28b30471e6 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -579,6 +579,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 8da2504f2e3..b2b8e91694b 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
 
-- 
GitLab