diff --git a/.ruby-version b/.ruby-version
index 8880b79..311baaf 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-1.9.3-p374
\ No newline at end of file
+1.9.3-p374
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index db953cd..d44eac1 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -10,10 +10,8 @@ from datetime import timedelta
 import json
 from fs.osfs import OSFS
 import copy
-from mock import Mock
-from json import dumps, loads
+from json import loads
 
-from student.models import Registration
 from django.contrib.auth.models import User
 from cms.djangoapps.contentstore.utils import get_modulestore
 
@@ -23,13 +21,12 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
 from xmodule.modulestore import Location
 from xmodule.modulestore.store_utilities import clone_course
 from xmodule.modulestore.store_utilities import delete_course
-from xmodule.modulestore.django import modulestore, _MODULESTORES
+from xmodule.modulestore.django import modulestore
 from xmodule.contentstore.django import contentstore
 from xmodule.templates import update_templates
 from xmodule.modulestore.xml_exporter import export_to_xml
 from xmodule.modulestore.xml_importer import import_from_xml
 from xmodule.modulestore.inheritance import own_metadata
-from xmodule.templates import update_templates
 
 from xmodule.capa_module import CapaDescriptor
 from xmodule.course_module import CourseDescriptor
@@ -65,7 +62,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
         self.client = Client()
         self.client.login(username=uname, password=password)
 
-
     def check_edit_unit(self, test_course_name):
         import_from_xml(modulestore(), 'common/test/data/', [test_course_name])
 
@@ -84,8 +80,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
     def test_static_tab_reordering(self):
         import_from_xml(modulestore(), 'common/test/data/', ['full'])
 
-        ms = modulestore('direct')
-        course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
+        module_store = modulestore('direct')
+        course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
 
         # reverse the ordering
         reverse_tabs = []
@@ -93,9 +89,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
             if tab['type'] == 'static_tab':
                 reverse_tabs.insert(0, 'i4x://edX/full/static_tab/{0}'.format(tab['url_slug']))
 
-        resp = self.client.post(reverse('reorder_static_tabs'), json.dumps({'tabs': reverse_tabs}), "application/json")
+        self.client.post(reverse('reorder_static_tabs'), json.dumps({'tabs': reverse_tabs}), "application/json")
 
-        course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
+        course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
 
         # compare to make sure that the tabs information is in the expected order after the server call
         course_tabs = []
@@ -105,28 +101,60 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
 
         self.assertEqual(reverse_tabs, course_tabs)
 
+    def test_delete(self):
+        import_from_xml(modulestore(), 'common/test/data/', ['full'])
+
+        module_store = modulestore('direct')
+
+        sequential = module_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
+
+        chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None]))
+
+        # make sure the parent no longer points to the child object which was deleted
+        self.assertTrue(sequential.location.url() in chapter.definition['children'])
+
+        self.client.post(reverse('delete_item'),
+            json.dumps({'id': sequential.location.url(), 'delete_children':'true'}),
+            "application/json")
+
+        found = False
+        try:
+            module_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
+            found = True
+        except ItemNotFoundError:
+            pass
+
+        self.assertFalse(found)
+
+        chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None]))
+
+        # make sure the parent no longer points to the child object which was deleted
+        self.assertFalse(sequential.location.url() in chapter.definition['children'])
+
+
+
     def test_about_overrides(self):
         '''
         This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html
         while there is a base definition in /about/effort.html
         '''
         import_from_xml(modulestore(), 'common/test/data/', ['full'])
-        ms = modulestore('direct')
-        effort = ms.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None]))
+        module_store = modulestore('direct')
+        effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None]))
         self.assertEqual(effort.data, '6 hours')
 
         # this one should be in a non-override folder
-        effort = ms.get_item(Location(['i4x', 'edX', 'full', 'about', 'end_date', None]))
+        effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'end_date', None]))
         self.assertEqual(effort.data, 'TBD')
 
     def test_remove_hide_progress_tab(self):
         import_from_xml(modulestore(), 'common/test/data/', ['full'])
 
-        ms = modulestore('direct')
-        cs = contentstore()
+        module_store = modulestore('direct')
+        content_store = contentstore()
 
         source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
-        course = ms.get_item(source_location)
+        course = module_store.get_item(source_location)
         self.assertFalse(course.hide_progress_tab)
 
     def test_clone_course(self):
@@ -145,19 +173,19 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
         data = parse_json(resp)
         self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course')
 
-        ms = modulestore('direct')
-        cs = contentstore()
+        module_store = modulestore('direct')
+        content_store = contentstore()
 
         source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
         dest_location = CourseDescriptor.id_to_location('MITx/999/Robot_Super_Course')
 
-        clone_course(ms, cs, source_location, dest_location)
+        clone_course(module_store, content_store, source_location, dest_location)
 
         # now loop through all the units in the course and verify that the clone can render them, which
         # means the objects are at least present
-        items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
+        items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
         self.assertGreater(len(items), 0)
-        clone_items = ms.get_items(Location(['i4x', 'MITx', '999', 'vertical', None]))
+        clone_items = module_store.get_items(Location(['i4x', 'MITx', '999', 'vertical', None]))
         self.assertGreater(len(clone_items), 0)
         for descriptor in items:
             new_loc = descriptor.location._replace(org='MITx', course='999')
@@ -168,14 +196,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
     def test_delete_course(self):
         import_from_xml(modulestore(), 'common/test/data/', ['full'])
 
-        ms = modulestore('direct')
-        cs = contentstore()
+        module_store = modulestore('direct')
+        content_store = contentstore()
 
         location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
 
-        delete_course(ms, cs, location, commit=True)
+        delete_course(module_store, content_store, location, commit=True)
 
-        items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
+        items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
         self.assertEqual(len(items), 0)
 
     def verify_content_existence(self, modulestore, root_dir, location, dirname, category_name, filename_suffix=''):
@@ -190,10 +218,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
             self.assertTrue(fs.exists(item.location.name + filename_suffix))
 
     def test_export_course(self):
-        ms = modulestore('direct')
-        cs = contentstore()
+        module_store = modulestore('direct')
+        content_store = contentstore()
 
-        import_from_xml(ms, 'common/test/data/', ['full'])
+        import_from_xml(module_store, 'common/test/data/', ['full'])
         location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
 
         root_dir = path(mkdtemp_clean())
@@ -201,24 +229,24 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
         print 'Exporting to tempdir = {0}'.format(root_dir)
 
         # export out to a tempdir
-        export_to_xml(ms, cs, location, root_dir, 'test_export')
+        export_to_xml(module_store, content_store, location, root_dir, 'test_export')
 
         # check for static tabs
-        self.verify_content_existence(ms, root_dir, location, 'tabs', 'static_tab', '.html')
+        self.verify_content_existence(module_store, root_dir, location, 'tabs', 'static_tab', '.html')
 
         # check for custom_tags
-        self.verify_content_existence(ms, root_dir, location, 'info', 'course_info', '.html')
+        self.verify_content_existence(module_store, root_dir, location, 'info', 'course_info', '.html')
 
         # check for custom_tags
-        self.verify_content_existence(ms, root_dir, location, 'custom_tags', 'custom_tag_template')
+        self.verify_content_existence(module_store, root_dir, location, 'custom_tags', 'custom_tag_template')
 
         # check for graiding_policy.json
         fs = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012')
         self.assertTrue(fs.exists('grading_policy.json'))
 
-        course = ms.get_item(location)
+        course = module_store.get_item(location)
         # compare what's on disk compared to what we have in our course
-        with fs.open('grading_policy.json','r') as grading_policy:
+        with fs.open('grading_policy.json', 'r') as grading_policy:
             on_disk = loads(grading_policy.read())
             self.assertEqual(on_disk, course.grading_policy)
 
@@ -226,18 +254,18 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
         self.assertTrue(fs.exists('policy.json'))
 
         # compare what's on disk to what we have in the course module
-        with fs.open('policy.json','r') as course_policy:
+        with fs.open('policy.json', 'r') as course_policy:
             on_disk = loads(course_policy.read())
             self.assertIn('course/6.002_Spring_2012', on_disk)
             self.assertEqual(on_disk['course/6.002_Spring_2012'], own_metadata(course))
 
         # remove old course
-        delete_course(ms, cs, location)
+        delete_course(module_store, content_store, location)
 
         # reimport
-        import_from_xml(ms, root_dir, ['test_export'])
+        import_from_xml(module_store, root_dir, ['test_export'])
 
-        items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
+        items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
         self.assertGreater(len(items), 0)
         for descriptor in items:
             print "Checking {0}....".format(descriptor.location.url())
@@ -247,11 +275,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
         shutil.rmtree(root_dir)
 
     def test_course_handouts_rewrites(self):
-        ms = modulestore('direct')
-        cs = contentstore()
+        module_store = modulestore('direct')
+        content_store = contentstore()
 
         # import a test course
-        import_from_xml(ms, 'common/test/data/', ['full'])
+        import_from_xml(module_store, 'common/test/data/', ['full'])
 
         handout_location = Location(['i4x', 'edX', 'full', 'course_info', 'handouts'])
 
@@ -266,33 +294,33 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
         self.assertContains(resp, '/c4x/edX/full/asset/handouts_schematic_tutorial.pdf')
 
     def test_export_course_with_unknown_metadata(self):
-        ms = modulestore('direct')
-        cs = contentstore()
+        module_store = modulestore('direct')
+        content_store = contentstore()
 
-        import_from_xml(ms, 'common/test/data/', ['full'])
+        import_from_xml(module_store, 'common/test/data/', ['full'])
         location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
 
         root_dir = path(mkdtemp_clean())
 
-        course = ms.get_item(location)
+        course = module_store.get_item(location)
 
         metadata = own_metadata(course)
         # add a bool piece of unknown metadata so we can verify we don't throw an exception
         metadata['new_metadata'] = True
 
-        ms.update_metadata(location, metadata)
+        module_store.update_metadata(location, metadata)
 
         print 'Exporting to tempdir = {0}'.format(root_dir)
 
         # export out to a tempdir
-        bExported = False
+        exported = False
         try:
-            export_to_xml(ms, cs, location, root_dir, 'test_export')
-            bExported = True
+            export_to_xml(module_store, content_store, location, root_dir, 'test_export')
+            exported = True
         except Exception:
             pass
 
-        self.assertTrue(bExported)
+        self.assertTrue(exported)
 
 class ContentStoreTest(ModuleStoreTestCase):
     """
@@ -431,7 +459,7 @@ class ContentStoreTest(ModuleStoreTestCase):
 
     def test_capa_module(self):
         """Test that a problem treats markdown specially."""
-        course = CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
+        CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
 
         problem_data = {
             'parent_location': 'i4x://MITx/999/course/Robot_Super_Course',
@@ -452,10 +480,10 @@ class ContentStoreTest(ModuleStoreTestCase):
 
     def test_import_metadata_with_attempts_empty_string(self):
         import_from_xml(modulestore(), 'common/test/data/', ['simple'])
-        ms = modulestore('direct')
+        module_store = modulestore('direct')
         did_load_item = False
         try:
-            ms.get_item(Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None]))
+            module_store.get_item(Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None]))
             did_load_item = True
         except ItemNotFoundError:
             pass
@@ -466,10 +494,10 @@ class ContentStoreTest(ModuleStoreTestCase):
     def test_metadata_inheritance(self):
         import_from_xml(modulestore(), 'common/test/data/', ['full'])
 
-        ms = modulestore('direct')
-        course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
+        module_store = modulestore('direct')
+        course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
 
-        verticals = ms.get_items(['i4x', 'edX', 'full', 'vertical', None, None])
+        verticals = module_store.get_items(['i4x', 'edX', 'full', 'vertical', None, None])
 
         # let's assert on the metadata_inheritance on an existing vertical
         for vertical in verticals:
@@ -481,13 +509,13 @@ class ContentStoreTest(ModuleStoreTestCase):
         source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page')
 
         # crate a new module and add it as a child to a vertical
-        ms.clone_item(source_template_location, new_component_location)
+        module_store.clone_item(source_template_location, new_component_location)
         parent = verticals[0]
-        ms.update_children(parent.location, parent.children + [new_component_location.url()])
+        module_store.update_children(parent.location, parent.children + [new_component_location.url()])
 
         # flush the cache
-        ms.get_cached_metadata_inheritance_tree(new_component_location, -1)
-        new_module = ms.get_item(new_component_location)
+        module_store.get_cached_metadata_inheritance_tree(new_component_location, -1)
+        new_module = module_store.get_item(new_component_location)
 
         # check for grace period definition which should be defined at the course level
         self.assertEqual(parent.lms.graceperiod, new_module.lms.graceperiod)
@@ -498,11 +526,11 @@ class ContentStoreTest(ModuleStoreTestCase):
         # now let's define an override at the leaf node level
         #
         new_module.lms.graceperiod = timedelta(1)
-        ms.update_metadata(new_module.location, own_metadata(new_module))
+        module_store.update_metadata(new_module.location, own_metadata(new_module))
 
         # flush the cache and refetch
-        ms.get_cached_metadata_inheritance_tree(new_component_location, -1)
-        new_module = ms.get_item(new_component_location)
+        module_store.get_cached_metadata_inheritance_tree(new_component_location, -1)
+        new_module = module_store.get_item(new_component_location)
 
         self.assertEqual(timedelta(1), new_module.lms.graceperiod)
 
@@ -510,15 +538,15 @@ class ContentStoreTest(ModuleStoreTestCase):
 class TemplateTestCase(ModuleStoreTestCase):
 
     def test_template_cleanup(self):
-        ms = modulestore('direct')
+        module_store = modulestore('direct')
 
         # insert a bogus template in the store
         bogus_template_location = Location('i4x', 'edx', 'templates', 'html', 'bogus')
         source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page')
 
-        ms.clone_item(source_template_location, bogus_template_location)
+        module_store.clone_item(source_template_location, bogus_template_location)
 
-        verify_create = ms.get_item(bogus_template_location)
+        verify_create = module_store.get_item(bogus_template_location)
         self.assertIsNotNone(verify_create)
 
         # now run cleanup
@@ -527,10 +555,8 @@ class TemplateTestCase(ModuleStoreTestCase):
         # now try to find dangling template, it should not be in DB any longer
         asserted = False
         try:
-            verify_create = ms.get_item(bogus_template_location)
+            verify_create = module_store.get_item(bogus_template_location)
         except ItemNotFoundError:
             asserted = True
 
         self.assertTrue(asserted)
-
-
diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index 358c8e2..0a87306 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -90,12 +90,14 @@ def signup(request):
     csrf_token = csrf(request)['csrf_token']
     return render_to_response('signup.html', {'csrf': csrf_token})
 
+
 def old_login_redirect(request):
     '''
     Redirect to the active login url.
     '''
     return redirect('login', permanent=True)
 
+
 @ssl_login_shortcut
 @ensure_csrf_cookie
 def login_page(request):
@@ -108,6 +110,7 @@ def login_page(request):
         'forgot_password_link': "//{base}/#forgot-password-modal".format(base=settings.LMS_BASE),
     })
 
+
 def howitworks(request):
     if request.user.is_authenticated():
         return index(request)
@@ -116,6 +119,7 @@ def howitworks(request):
 
 # ==== Views for any logged-in user ==================================
 
+
 @login_required
 @ensure_csrf_cookie
 def index(request):
@@ -149,6 +153,7 @@ def index(request):
 
 # ==== Views with per-item permissions================================
 
+
 def has_access(user, location, role=STAFF_ROLE_NAME):
     '''
     Return True if user allowed to access this piece of data
@@ -396,6 +401,7 @@ def preview_component(request, location):
         'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(),
     })
 
+
 @expect_json
 @login_required
 @ensure_csrf_cookie
@@ -636,6 +642,17 @@ def delete_item(request):
     if item.location.revision is None and item.location.category == 'vertical' and delete_all_versions:
         modulestore('direct').delete_item(item.location)
 
+    # cdodge: we need to remove our parent's pointer to us so that it is no longer dangling
+
+    parent_locs = modulestore('direct').get_parent_locations(item_loc, None)
+
+    for parent_loc in parent_locs:
+        parent = modulestore('direct').get_item(parent_loc)
+        item_url = item_loc.url()
+        if item_url in parent.definition["children"]:
+            parent.definition["children"].remove(item_url)
+            modulestore('direct').update_children(parent.location, parent.definition["children"])
+
     return HttpResponse()
 
 
@@ -709,6 +726,7 @@ def create_draft(request):
 
     return HttpResponse()
 
+
 @login_required
 @expect_json
 def publish_draft(request):
@@ -738,6 +756,7 @@ def unpublish_unit(request):
 
     return HttpResponse()
 
+
 @login_required
 @expect_json
 def clone_item(request):
@@ -765,8 +784,7 @@ def clone_item(request):
 
     return HttpResponse(json.dumps({'id': dest_location.url()}))
 
-#@login_required
-#@ensure_csrf_cookie
+
 def upload_asset(request, org, course, coursename):
     '''
     cdodge: this method allows for POST uploading of files into the course asset library, which will
@@ -828,6 +846,7 @@ def upload_asset(request, org, course, coursename):
     response['asset_url'] = StaticContent.get_url_path_from_location(content.location)
     return response
 
+
 '''
 This view will return all CMS users who are editors for the specified course
 '''
@@ -860,6 +879,7 @@ def create_json_response(errmsg = None):
 
     return resp
 
+
 '''
 This POST-back view will add a user - specified by email - to the list of editors for
 the specified course
@@ -892,6 +912,7 @@ def add_user(request, location):
 
     return create_json_response()
 
+
 '''
 This POST-back view will remove a user - specified by email - from the list of editors for
 the specified course
@@ -923,6 +944,7 @@ def remove_user(request, location):
 def landing(request, org, course, coursename):
     return render_to_response('temp-course-landing.html', {})
 
+
 @login_required
 @ensure_csrf_cookie
 def static_pages(request, org, course, coursename):
@@ -1026,6 +1048,7 @@ def edit_tabs(request, org, course, coursename):
         'components': components
         })
 
+
 def not_found(request):
     return render_to_response('error.html', {'error': '404'})
 
@@ -1061,6 +1084,7 @@ def course_info(request, org, course, name, provided_id=None):
         'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url()
     })
 
+
 @expect_json
 @login_required
 @ensure_csrf_cookie
@@ -1158,6 +1182,7 @@ def get_course_settings(request, org, course, name):
                                        "section": "details"})
     })
 
+
 @login_required
 @ensure_csrf_cookie
 def course_config_graders_page(request, org, course, name):
@@ -1181,6 +1206,7 @@ def course_config_graders_page(request, org, course, name):
         'course_details': json.dumps(course_details, cls=CourseSettingsEncoder)
     })
 
+
 @login_required
 @ensure_csrf_cookie
 def course_config_advanced_page(request, org, course, name):
@@ -1204,6 +1230,7 @@ def course_config_advanced_page(request, org, course, name):
         'advanced_dict' : json.dumps(CourseMetadata.fetch(location)),
     })
 
+
 @expect_json
 @login_required
 @ensure_csrf_cookie
@@ -1235,6 +1262,7 @@ def course_settings_updates(request, org, course, name, section):
         return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
                             mimetype="application/json")
 
+
 @expect_json
 @login_required
 @ensure_csrf_cookie
@@ -1360,6 +1388,7 @@ def asset_index(request, org, course, name):
 def edge(request):
     return render_to_response('university_profiles/edge.html', {})
 
+
 @login_required
 @expect_json
 def create_new_course(request):
@@ -1412,6 +1441,7 @@ def create_new_course(request):
 
     return HttpResponse(json.dumps({'id': new_course.location.url()}))
 
+
 def initialize_course_tabs(course):
     # set up the default tabs
     # I've added this because when we add static tabs, the LMS either expects a None for the tabs list or
@@ -1429,6 +1459,7 @@ def initialize_course_tabs(course):
 
     modulestore('direct').update_metadata(course.location.url(), own_metadata(course))
 
+
 @ensure_csrf_cookie
 @login_required
 def import_course(request, org, course, name):
@@ -1506,6 +1537,7 @@ def import_course(request, org, course, name):
                         course_module.location.name])
         })
 
+
 @ensure_csrf_cookie
 @login_required
 def generate_export_course(request, org, course, name):
@@ -1557,6 +1589,7 @@ def export_course(request, org, course, name):
         'successful_import_redirect_url': ''
     })
 
+
 def event(request):
     '''
     A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at
diff --git a/common/lib/capa/capa/calc.py b/common/lib/capa/capa/calc.py
index 0f062d1..c3fe6b6 100644
--- a/common/lib/capa/capa/calc.py
+++ b/common/lib/capa/capa/calc.py
@@ -183,7 +183,7 @@ def evaluator(variables, functions, string, cs=False):
 
     # 0.33k or -17
     number = (Optional(minus | plus) + inner_number
-              + Optional(CaselessLiteral("E") + Optional("-") + number_part)
+              + Optional(CaselessLiteral("E") + Optional((plus | minus)) + number_part)
               + Optional(number_suffix))
     number = number.setParseAction(number_parse_action)  # Convert to number
 
diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py
index 3993eea..5f594b1 100644
--- a/common/lib/capa/capa/inputtypes.py
+++ b/common/lib/capa/capa/inputtypes.py
@@ -366,6 +366,12 @@ class ChoiceGroup(InputTypeBase):
 
         self.choices = self.extract_choices(self.xml)
 
+    @classmethod
+    def get_attributes(cls):
+        return [Attribute("show_correctness", "always"),
+                Attribute("submitted_message", "Answer received.")]
+
+
     def _extra_context(self):
         return {'input_type': self.html_input_type,
                 'choices': self.choices,
diff --git a/common/lib/capa/capa/templates/choicegroup.html b/common/lib/capa/capa/templates/choicegroup.html
index e4a3f1d..e1ff40b 100644
--- a/common/lib/capa/capa/templates/choicegroup.html
+++ b/common/lib/capa/capa/templates/choicegroup.html
@@ -1,7 +1,7 @@
 <form class="choicegroup capa_inputtype" id="inputtype_${id}">
     <div class="indicator_container">
     % if input_type == 'checkbox' or not value:
-        % if status == 'unsubmitted':
+        % if status == 'unsubmitted' or show_correctness == 'never':
         <span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
         % elif status == 'correct': 
         <span class="correct" id="status_${id}"></span>
@@ -26,7 +26,7 @@
                 else:
                     correctness = None
             %>
-            % if correctness:
+            % if correctness and not show_correctness=='never':
             class="choicegroup_${correctness}"
             % endif
             % endif
@@ -41,4 +41,7 @@
         <span id="answer_${id}"></span>
     </fieldset>
 
+    % if show_correctness == "never" and (value or status not in ['unsubmitted']):
+    <div class="capa_alert">${submitted_message}</div>
+    %endif
 </form>
diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py
index 8bfcb81..360fd9f 100644
--- a/common/lib/capa/capa/tests/test_inputtypes.py
+++ b/common/lib/capa/capa/tests/test_inputtypes.py
@@ -102,6 +102,8 @@ class ChoiceGroupTest(unittest.TestCase):
                     'choices': [('foil1', '<text>This is foil One.</text>'),
                                 ('foil2', '<text>This is foil Two.</text>'),
                                 ('foil3', 'This is foil Three.'), ],
+                    'show_correctness': 'always',
+                    'submitted_message': 'Answer received.',
                     'name_array_suffix': expected_suffix,   # what is this for??
                     }
 
diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py
index 93a7e96..e024909 100644
--- a/common/lib/capa/capa/tests/test_responsetypes.py
+++ b/common/lib/capa/capa/tests/test_responsetypes.py
@@ -19,7 +19,7 @@ from capa.xqueue_interface import dateformat
 
 class ResponseTest(unittest.TestCase):
     """ Base class for tests of capa responses."""
-    
+
     xml_factory_class = None
 
     def setUp(self):
@@ -43,7 +43,7 @@ class ResponseTest(unittest.TestCase):
 
         for input_str in incorrect_answers:
             result = problem.grade_answers({'1_2_1': input_str}).get_correctness('1_2_1')
-            self.assertEqual(result, 'incorrect', 
+            self.assertEqual(result, 'incorrect',
                             msg="%s should be marked incorrect" % str(input_str))
 
 class MultiChoiceResponseTest(ResponseTest):
@@ -61,7 +61,7 @@ class MultiChoiceResponseTest(ResponseTest):
     def test_named_multiple_choice_grade(self):
         problem = self.build_problem(choices=[False, True, False],
                                     choice_names=["foil_1", "foil_2", "foil_3"])
-        
+
         # Ensure that we get the expected grades
         self.assert_grade(problem, 'choice_foil_1', 'incorrect')
         self.assert_grade(problem, 'choice_foil_2', 'correct')
@@ -117,7 +117,7 @@ class ImageResponseTest(ResponseTest):
 
         # Anything inside the rectangle (and along the borders) is correct
         # Everything else is incorrect
-        correct_inputs = ["[12,19]", "[10,10]", "[20,20]", 
+        correct_inputs = ["[12,19]", "[10,10]", "[20,20]",
                             "[10,15]", "[20,15]", "[15,10]", "[15,20]"]
         incorrect_inputs = ["[4,6]", "[25,15]", "[15,40]", "[15,4]"]
         self.assert_multiple_grade(problem, correct_inputs, incorrect_inputs)
@@ -259,7 +259,7 @@ class OptionResponseTest(ResponseTest):
     xml_factory_class = OptionResponseXMLFactory
 
     def test_grade(self):
-        problem = self.build_problem(options=["first", "second", "third"], 
+        problem = self.build_problem(options=["first", "second", "third"],
                                     correct_option="second")
 
         # Assert that we get the expected grades
@@ -374,8 +374,8 @@ class StringResponseTest(ResponseTest):
         hints = [("wisconsin", "wisc", "The state capital of Wisconsin is Madison"),
                 ("minnesota", "minn", "The state capital of Minnesota is St. Paul")]
 
-        problem = self.build_problem(answer="Michigan", 
-                                    case_sensitive=False, 
+        problem = self.build_problem(answer="Michigan",
+                                    case_sensitive=False,
                                     hints=hints)
 
         # We should get a hint for Wisconsin
@@ -543,7 +543,7 @@ class ChoiceResponseTest(ResponseTest):
     xml_factory_class = ChoiceResponseXMLFactory
 
     def test_radio_group_grade(self):
-        problem = self.build_problem(choice_type='radio', 
+        problem = self.build_problem(choice_type='radio',
                                         choices=[False, True, False])
 
         # Check that we get the expected results
@@ -601,17 +601,17 @@ class NumericalResponseTest(ResponseTest):
         correct_responses = ["4", "4.0", "4.00"]
         incorrect_responses = ["", "3.9", "4.1", "0"]
         self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
-        
+
 
     def test_grade_decimal_tolerance(self):
         problem = self.build_problem(question_text="What is 2 + 2 approximately?",
                                         explanation="The answer is 4",
                                         answer=4,
                                         tolerance=0.1)
-        correct_responses = ["4.0", "4.00", "4.09", "3.91"] 
+        correct_responses = ["4.0", "4.00", "4.09", "3.91"]
         incorrect_responses = ["", "4.11", "3.89", "0"]
         self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
-                        
+
     def test_grade_percent_tolerance(self):
         problem = self.build_problem(question_text="What is 2 + 2 approximately?",
                                         explanation="The answer is 4",
@@ -642,6 +642,15 @@ class NumericalResponseTest(ResponseTest):
         incorrect_responses = ["", "2.11", "1.89", "0"]
         self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
 
+    def test_exponential_answer(self):
+        problem = self.build_problem(question_text="What 5 * 10?",
+                                     explanation="The answer is 50",
+                                     answer="5e+1")
+        correct_responses = ["50", "50.0", "5e1", "5e+1", "50e0", "500e-1"]
+        incorrect_responses = ["", "3.9", "4.1", "0", "5.01e1"]
+        self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
+
+
 
 class CustomResponseTest(ResponseTest):
     from response_xml_factory import CustomResponseXMLFactory
@@ -667,7 +676,7 @@ class CustomResponseTest(ResponseTest):
         # The code can also set the global overall_message (str)
         # to pass a message that applies to the whole response
         inline_script = textwrap.dedent("""
-        messages[0] = "Test Message" 
+        messages[0] = "Test Message"
         overall_message = "Overall message"
         """)
         problem = self.build_problem(answer=inline_script)
@@ -687,14 +696,14 @@ class CustomResponseTest(ResponseTest):
     def test_function_code_single_input(self):
 
         # For function code, we pass in these arguments:
-        # 
+        #
         #   'expect' is the expect attribute of the <customresponse>
         #
         #   'answer_given' is the answer the student gave (if there is just one input)
         #       or an ordered list of answers (if there are multiple inputs)
-        #   
         #
-        # The function should return a dict of the form 
+        #
+        # The function should return a dict of the form
         # { 'ok': BOOL, 'msg': STRING }
         #
         script = textwrap.dedent("""
@@ -727,7 +736,7 @@ class CustomResponseTest(ResponseTest):
     def test_function_code_multiple_input_no_msg(self):
 
         # Check functions also have the option of returning
-        # a single boolean value 
+        # a single boolean value
         # If true, mark all the inputs correct
         # If false, mark all the inputs incorrect
         script = textwrap.dedent("""
@@ -736,7 +745,7 @@ class CustomResponseTest(ResponseTest):
                         answer_given[1] == expect)
         """)
 
-        problem = self.build_problem(script=script, cfn="check_func", 
+        problem = self.build_problem(script=script, cfn="check_func",
                                     expect="42", num_inputs=2)
 
         # Correct answer -- expect both inputs marked correct
@@ -764,10 +773,10 @@ class CustomResponseTest(ResponseTest):
 
         # If the <customresponse> has multiple inputs associated with it,
         # the check function can return a dict of the form:
-        # 
+        #
         # {'overall_message': STRING,
         #  'input_list': [{'ok': BOOL, 'msg': STRING}, ...] }
-        # 
+        #
         # 'overall_message' is displayed at the end of the response
         #
         # 'input_list' contains dictionaries representing the correctness
@@ -784,7 +793,7 @@ class CustomResponseTest(ResponseTest):
                             {'ok': check3,  'msg': 'Feedback 3'} ] }
             """)
 
-        problem = self.build_problem(script=script, 
+        problem = self.build_problem(script=script,
                                     cfn="check_func", num_inputs=3)
 
         # Grade the inputs (one input incorrect)
@@ -821,11 +830,11 @@ class CustomResponseTest(ResponseTest):
                 check1 = (int(answer_given[0]) == 1)
                 check2 = (int(answer_given[1]) == 2)
                 check3 = (int(answer_given[2]) == 3)
-                return {'ok': (check1 and check2 and check3),  
+                return {'ok': (check1 and check2 and check3),
                         'msg': 'Message text'}
             """)
 
-        problem = self.build_problem(script=script, 
+        problem = self.build_problem(script=script,
                                     cfn="check_func", num_inputs=3)
 
         # Grade the inputs (one input incorrect)
@@ -862,7 +871,7 @@ class CustomResponseTest(ResponseTest):
         # Expect that an exception gets raised when we check the answer
         with self.assertRaises(Exception):
             problem.grade_answers({'1_2_1': '42'})
-    
+
     def test_invalid_dict_exception(self):
 
         # Construct a script that passes back an invalid dict format
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index f3634d8..627e9ab 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -174,7 +174,8 @@ class CourseDescriptor(SequenceDescriptor):
     is_new = Boolean(help="Whether this course should be flagged as new", scope=Scope.settings)
     no_grade = Boolean(help="True if this course isn't graded", default=False, scope=Scope.settings)
     disable_progress_graph = Boolean(help="True if this course shouldn't display the progress graph", default=False, scope=Scope.settings)
-    pdf_textbooks = List(help="List of dictionaries containing pdf_textbook configuration", default=None, scope=Scope.settings)
+    pdf_textbooks = List(help="List of dictionaries containing pdf_textbook configuration", scope=Scope.settings)
+    html_textbooks = List(help="List of dictionaries containing html_textbook configuration", scope=Scope.settings)
     remote_gradebook = Object(scope=Scope.settings)
     allow_anonymous = Boolean(scope=Scope.settings, default=True)
     allow_anonymous_to_peers = Boolean(scope=Scope.settings, default=False)
diff --git a/common/lib/xmodule/xmodule/foldit_module.py b/common/lib/xmodule/xmodule/foldit_module.py
index 43a345e..222f3dc 100644
--- a/common/lib/xmodule/xmodule/foldit_module.py
+++ b/common/lib/xmodule/xmodule/foldit_module.py
@@ -85,7 +85,10 @@ class FolditModule(XModule):
         """
         from foldit.models import Score
 
-        return [(e['username'], e['score']) for e in Score.get_tops_n(10)]
+        leaders = [(e['username'], e['score']) for e in Score.get_tops_n(10)]
+        leaders.sort(key=lambda x: x[1])
+
+        return leaders
 
     def get_html(self):
         """
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/factories.py b/common/lib/xmodule/xmodule/modulestore/tests/factories.py
index 10f63d5..b842ffe 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/factories.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/factories.py
@@ -46,10 +46,10 @@ class XModuleCourseFactory(Factory):
         new_course.start = gmtime()
 
         new_course.tabs = [{"type": "courseware"},
-            {"type": "course_info", "name": "Course Info"},
-            {"type": "discussion", "name": "Discussion"},
-            {"type": "wiki", "name": "Wiki"},
-            {"type": "progress", "name": "Progress"}]
+                           {"type": "course_info", "name": "Course Info"},
+                           {"type": "discussion", "name": "Discussion"},
+                           {"type": "wiki", "name": "Wiki"},
+                           {"type": "progress", "name": "Progress"}]
 
         # Update the data in the mongo datastore
         store.update_metadata(new_course.location.url(), own_metadata(new_course))
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_location.py b/common/lib/xmodule/xmodule/modulestore/tests/test_location.py
index 0772951..f0f0e8b 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/test_location.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/test_location.py
@@ -119,11 +119,11 @@ def test_equality():
 
 # All the cleaning functions should do the same thing with these
 general_pairs = [('', ''),
-                  (' ', '_'),
-                  ('abc,', 'abc_'),
-                  ('ab    fg!@//\\aj', 'ab_fg_aj'),
-                  (u"ab\xA9", "ab_"),  # no unicode allowed for now
-                  ]
+                 (' ', '_'),
+                 ('abc,', 'abc_'),
+                 ('ab    fg!@//\\aj', 'ab_fg_aj'),
+                 (u"ab\xA9", "ab_"),  # no unicode allowed for now
+                 ]
 
 
 def test_clean():
@@ -131,7 +131,7 @@ def test_clean():
         ('a:b', 'a_b'),  # no colons in non-name components
         ('a-b', 'a-b'),  # dashes ok
         ('a.b', 'a.b'),  # dot ok
-        ]
+    ]
     for input, output in pairs:
         assert_equals(Location.clean(input), output)
 
@@ -141,17 +141,17 @@ def test_clean_for_url_name():
         ('a:b', 'a:b'),  # colons ok in names
         ('a-b', 'a-b'),  # dashes ok in names
         ('a.b', 'a.b'),  # dot ok in names
-        ]
+    ]
     for input, output in pairs:
         assert_equals(Location.clean_for_url_name(input), output)
 
 
 def test_clean_for_html():
     pairs = general_pairs + [
-              ("a:b", "a_b"),   # no colons for html use
-              ("a-b", "a-b"),   # dashes ok (though need to be replaced in various use locations. ugh.)
-              ('a.b', 'a_b'),   # no dots.
-              ]
+        ("a:b", "a_b"),   # no colons for html use
+        ("a-b", "a-b"),   # dashes ok (though need to be replaced in various use locations. ugh.)
+        ('a.b', 'a_b'),   # no dots.
+    ]
     for input, output in pairs:
         assert_equals(Location.clean_for_html(input), output)
 
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_modulestore.py b/common/lib/xmodule/xmodule/modulestore/tests/test_modulestore.py
index 94ea622..469eeda 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/test_modulestore.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/test_modulestore.py
@@ -12,7 +12,7 @@ def check_path_to_location(modulestore):
          ("edX/toy/2012_Fall", "Overview", "Welcome", None)),
         ("i4x://edX/toy/chapter/Overview",
          ("edX/toy/2012_Fall", "Overview", None, None)),
-        )
+    )
     course_id = "edX/toy/2012_Fall"
 
     for location, expected in should_work:
@@ -20,6 +20,6 @@ def check_path_to_location(modulestore):
 
     not_found = (
         "i4x://edX/toy/video/WelcomeX", "i4x://edX/toy/course/NotHome"
-        )
+    )
     for location in not_found:
         assert_raises(ItemNotFoundError, path_to_location, modulestore, course_id, location)
diff --git a/jenkins/test.sh b/jenkins/test.sh
index 5b9a5ed..3a572c9 100755
--- a/jenkins/test.sh
+++ b/jenkins/test.sh
@@ -38,12 +38,15 @@ pip install -q -r test-requirements.txt
 yes w | pip install -q -r requirements.txt
 
 rake clobber
+rake pep8
+rake pylint
+
 TESTS_FAILED=0
 rake test_cms[false] || TESTS_FAILED=1
 rake test_lms[false] || TESTS_FAILED=1
 rake test_common/lib/capa || TESTS_FAILED=1
 rake test_common/lib/xmodule || TESTS_FAILED=1
-# Don't run the lms jasmine tests for now because 
+# Don't run the lms jasmine tests for now because
 # they mostly all fail anyhow
 # rake phantomjs_jasmine_lms || true
 rake phantomjs_jasmine_cms || TESTS_FAILED=1
diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py
index dcddb26..b1b9dee 100644
--- a/lms/djangoapps/courseware/tabs.py
+++ b/lms/djangoapps/courseware/tabs.py
@@ -131,6 +131,17 @@ def _pdf_textbooks(tab, user, course, active_page):
                 for index, textbook in enumerate(course.pdf_textbooks)]
     return []
 
+def _html_textbooks(tab, user, course, active_page):
+    """
+    Generates one tab per textbook.  Only displays if user is authenticated.
+    """
+    if user.is_authenticated():
+        # since there can be more than one textbook, active_page is e.g. "book/0".
+        return [CourseTab(textbook['tab_title'], reverse('html_book', args=[course.id, index]),
+                          active_page == "htmltextbook/{0}".format(index))
+                for index, textbook in enumerate(course.html_textbooks)]
+    return []
+
 def _staff_grading(tab, user, course, active_page):
     if has_access(user, course, 'staff'):
         link = reverse('staff_grading', args=[course.id])
@@ -210,6 +221,7 @@ VALID_TAB_TYPES = {
     'external_link': TabImpl(key_checker(['name', 'link']), _external_link),
     'textbooks': TabImpl(null_validator, _textbooks),
     'pdf_textbooks': TabImpl(null_validator, _pdf_textbooks),
+    'html_textbooks': TabImpl(null_validator, _html_textbooks),
     'progress': TabImpl(need_name, _progress),
     'static_tab': TabImpl(key_checker(['name', 'url_slug']), _static_tab),
     'peer_grading': TabImpl(null_validator, _peer_grading),
diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py
index 5eafa95..47dce86 100644
--- a/lms/djangoapps/django_comment_client/utils.py
+++ b/lms/djangoapps/django_comment_client/utils.py
@@ -22,7 +22,6 @@ import pystache_custom as pystache
 
 from xmodule.modulestore import Location
 from xmodule.modulestore.django import modulestore
-from xmodule.modulestore.search import path_to_location
 
 log = logging.getLogger(__name__)
 
@@ -170,7 +169,6 @@ def initialize_discussion_info(course):
     # get all discussion models within this course_id
     all_modules = modulestore().get_items(['i4x', course.location.org, course.location.course, 'discussion', None], course_id=course_id)
 
-    path_to_locations = {}
     for module in all_modules:
         skip_module = False
         for key in ('discussion_id', 'discussion_category', 'discussion_target'):
@@ -178,14 +176,6 @@ def initialize_discussion_info(course):
                 log.warning("Required key '%s' not in discussion %s, leaving out of category map" % (key, module.location))
                 skip_module = True
 
-        # cdodge: pre-compute the path_to_location. Note this can throw an exception for any
-        # dangling discussion modules
-        try:
-            path_to_locations[module.location] = path_to_location(modulestore(), course.id, module.location)
-        except NoPathToItem:
-            log.warning("Could not compute path_to_location for {0}. Perhaps this is an orphaned discussion module?!? Skipping...".format(module.location))
-            skip_module = True
-
         if skip_module:
             continue
 
@@ -248,7 +238,6 @@ def initialize_discussion_info(course):
     _DISCUSSIONINFO[course.id]['id_map'] = discussion_id_map
     _DISCUSSIONINFO[course.id]['category_map'] = category_map
     _DISCUSSIONINFO[course.id]['timestamp'] = datetime.now()
-    _DISCUSSIONINFO[course.id]['path_to_location'] = path_to_locations
 
 
 class JsonResponse(HttpResponse):
@@ -405,21 +394,8 @@ def get_courseware_context(content, course):
         location = id_map[id]["location"].url()
         title = id_map[id]["title"]
 
-        # cdodge: did we pre-compute, if so, then let's use that rather than recomputing
-        if 'path_to_location' in _DISCUSSIONINFO[course.id] and location in _DISCUSSIONINFO[course.id]['path_to_location']:
-            (course_id, chapter, section, position) = _DISCUSSIONINFO[course.id]['path_to_location'][location]
-        else:
-            try:
-                (course_id, chapter, section, position) = path_to_location(modulestore(), course.id, location)
-            except NoPathToItem:
-                # Object is not in the graph any longer, let's just get path to the base of the course
-                # so that we can at least return something to the caller
-                (course_id, chapter, section, position) = path_to_location(modulestore(), course.id, course.location)
-
-        url = reverse('courseware_position', kwargs={"course_id":course_id,
-                                                     "chapter":chapter,
-                                                     "section":section,
-                                                     "position":position})
+        url = reverse('jump_to', kwargs={"course_id":course.location.course_id, 
+                                                    "location": location})
 
         content_info = {"courseware_url": url, "courseware_title": title}
     return content_info
diff --git a/lms/djangoapps/foldit/models.py b/lms/djangoapps/foldit/models.py
index 7041be1..0dce956 100644
--- a/lms/djangoapps/foldit/models.py
+++ b/lms/djangoapps/foldit/models.py
@@ -59,7 +59,7 @@ class Score(models.Model):
         scores = Score.objects \
             .filter(puzzle_id__in=puzzles) \
             .annotate(total_score=models.Sum('best_score')) \
-            .order_by('-total_score')[:n]
+            .order_by('total_score')[:n]
         num = len(puzzles)
 
         return [{'username': s.user.username,
diff --git a/lms/djangoapps/foldit/tests.py b/lms/djangoapps/foldit/tests.py
index 7127651..afdd678 100644
--- a/lms/djangoapps/foldit/tests.py
+++ b/lms/djangoapps/foldit/tests.py
@@ -143,11 +143,12 @@ class FolditTestCase(TestCase):
     def test_SetPlayerPuzzleScores_manyplayers(self):
         """
         Check that when we send scores from multiple users, the correct order
-        of scores is displayed.
+        of scores is displayed. Note that, before being processed by
+        display_score, lower scores are better.
         """
         puzzle_id = ['1']
-        player1_score = 0.07
-        player2_score = 0.08
+        player1_score = 0.08
+        player2_score = 0.02
         response1 = self.make_puzzle_score_request(puzzle_id, player1_score,
                 self.user)
 
@@ -164,8 +165,12 @@ class FolditTestCase(TestCase):
         self.assertEqual(len(top_10), 2)
 
         # Top score should be player2_score. Second should be player1_score
-        self.assertEqual(top_10[0]['score'], Score.display_score(player2_score))
-        self.assertEqual(top_10[1]['score'], Score.display_score(player1_score))
+        self.assertAlmostEqual(top_10[0]['score'],
+                Score.display_score(player2_score),
+                delta=0.5)
+        self.assertAlmostEqual(top_10[1]['score'],
+                Score.display_score(player1_score),
+                delta=0.5)
 
         # Top score user should be self.user2.username
         self.assertEqual(top_10[0]['username'], self.user2.username)
diff --git a/lms/djangoapps/staticbook/views.py b/lms/djangoapps/staticbook/views.py
index 281bfbf..17e24b4 100644
--- a/lms/djangoapps/staticbook/views.py
+++ b/lms/djangoapps/staticbook/views.py
@@ -1,7 +1,7 @@
 from lxml import etree
 
-# from django.conf import settings
 from django.contrib.auth.decorators import login_required
+from django.http import Http404
 from mitxmako.shortcuts import render_to_response
 
 from courseware.access import has_access
@@ -15,6 +15,8 @@ def index(request, course_id, book_index, page=None):
     staff_access = has_access(request.user, course, 'staff')
 
     book_index = int(book_index)
+    if book_index < 0 or book_index >= len(course.textbooks):
+        raise Http404("Invalid book index value: {0}".format(book_index))
     textbook = course.textbooks[book_index]
     table_of_contents = textbook.table_of_contents
 
@@ -40,6 +42,8 @@ def pdf_index(request, course_id, book_index, chapter=None, page=None):
     staff_access = has_access(request.user, course, 'staff')
 
     book_index = int(book_index)
+    if book_index < 0 or book_index >= len(course.pdf_textbooks):
+        raise Http404("Invalid book index value: {0}".format(book_index))
     textbook = course.pdf_textbooks[book_index]
 
     def remap_static_url(original_url, course):
@@ -67,3 +71,39 @@ def pdf_index(request, course_id, book_index, chapter=None, page=None):
                                'chapter': chapter,
                                'page': page,
                                'staff_access': staff_access})
+
+@login_required
+def html_index(request, course_id, book_index, chapter=None, anchor_id=None):
+    course = get_course_with_access(request.user, course_id, 'load')
+    staff_access = has_access(request.user, course, 'staff')
+
+    book_index = int(book_index)
+    if book_index < 0 or book_index >= len(course.html_textbooks):
+        raise Http404("Invalid book index value: {0}".format(book_index))
+    textbook = course.html_textbooks[book_index]
+
+    def remap_static_url(original_url, course):
+        input_url = "'" + original_url + "'"
+        output_url = replace_static_urls(
+                    input_url,
+                    course.metadata['data_dir'],
+                    course_namespace=course.location
+                )
+        # strip off the quotes again...
+        return output_url[1:-1]
+
+    if 'url' in textbook:
+        textbook['url'] = remap_static_url(textbook['url'], course)
+    # then remap all the chapter URLs as well, if they are provided.
+    if 'chapters' in textbook:
+        for entry in textbook['chapters']:
+            entry['url'] = remap_static_url(entry['url'], course)
+
+
+    return render_to_response('static_htmlbook.html',
+                              {'book_index': book_index,
+                               'course': course,
+                               'textbook': textbook,
+                               'chapter': chapter,
+                               'anchor_id': anchor_id,
+                               'staff_access': staff_access})
diff --git a/lms/static/sass/course/_textbook.scss b/lms/static/sass/course/_textbook.scss
index af9c249..b1f3a86 100644
--- a/lms/static/sass/course/_textbook.scss
+++ b/lms/static/sass/course/_textbook.scss
@@ -158,6 +158,19 @@ div.book-wrapper {
       img {
         max-width: 100%;
       }
+
+      div {
+        text-align: left;
+	line-height: 1.6em;
+	margin-left: 5px;
+	margin-right: 5px;
+	margin-top: 5px;
+	margin-bottom: 5px;
+	
+	.Paragraph, h2 {
+	   margin-top: 10px;
+	}	   
+      }
     }
   }
 
diff --git a/lms/templates/static_htmlbook.html b/lms/templates/static_htmlbook.html
new file mode 100644
index 0000000..9500a37
--- /dev/null
+++ b/lms/templates/static_htmlbook.html
@@ -0,0 +1,135 @@
+<%inherit file="main.html" />
+<%namespace name='static' file='static_content.html'/>
+<%block name="title"><title>${course.number} Textbook</title>
+</%block>
+
+<%block name="headextra">
+<%static:css group='course'/>
+<%static:js group='courseware'/>
+</%block>
+
+<%block name="js_extra">
+  <script type="text/javascript">
+(function($) {
+    $.fn.myHTMLViewer = function(options) {
+        var urlToLoad = null;
+        if (options.url) {
+            urlToLoad = options.url;
+        }
+        var chapterUrls = null;
+        if (options.chapters) {
+            chapterUrls = options.chapters;
+        }
+        var chapterToLoad = 1;
+        if (options.chapterNum) {
+            // TODO: this should only be specified if there are 
+            // chapters, and it should be in-bounds.
+            chapterToLoad = options.chapterNum;
+        }
+        var anchorToLoad = null;
+        if (options.chapters) {
+            anchorToLoad = options.anchor_id;
+        }
+
+        loadUrl = function htmlViewLoadUrl(url, anchorId) {
+            // clear out previous load, if any:
+            parentElement = document.getElementById('bookpage');
+            while (parentElement.hasChildNodes())
+                parentElement.removeChild(parentElement.lastChild);
+	    // load new URL in:
+            $('#bookpage').load(url);
+
+	    // if there is an anchor set, then go to that location:
+            if (anchorId != null) {
+		// TODO: add implementation....
+            } 
+
+        }; 
+
+        loadChapterUrl = function htmlViewLoadChapterUrl(chapterNum, anchorId) {
+            if (chapterNum < 1 || chapterNum > chapterUrls.length) {
+                return;
+            }
+            var chapterUrl = chapterUrls[chapterNum-1];
+            loadUrl(chapterUrl, anchorId);
+        };
+
+        // define navigation links for chapters:  
+        if (chapterUrls != null) {
+            var loadChapterUrlHelper = function(i) {
+                return function(event) {
+                    // when opening a new chapter, always open to the top:
+                    loadChapterUrl(i, null);
+                };
+            };
+            for (var index = 1; index <= chapterUrls.length; index += 1) {
+                $("#htmlchapter-" + index).click(loadChapterUrlHelper(index));
+            }   
+        }
+
+        // finally, load the appropriate url/page
+        if (urlToLoad != null) {
+            loadUrl(urlToLoad, anchorToLoad);
+        } else {
+            loadChapterUrl(chapterToLoad, anchorToLoad);
+        }       
+
+    }
+})(jQuery);
+
+    $(document).ready(function() {
+        var options = {};
+        %if 'url' in textbook:
+            options.url = "${textbook['url']}";
+        %endif
+        %if 'chapters' in textbook:
+            var chptrs = [];
+            %for chap in textbook['chapters']:
+                chptrs.push("${chap['url']}");
+            %endfor
+            options.chapters = chptrs;
+        %endif
+        %if chapter is not None:
+            options.chapterNum = ${chapter};
+        %endif
+        %if anchor_id is not None:
+            options.anchor_id = ${anchor_id};
+        %endif
+
+        $('#outerContainer').myHTMLViewer(options);
+    });
+  </script>  
+</%block>
+
+<%include file="/courseware/course_navigation.html" args="active_page='htmltextbook/{0}'.format(book_index)" />
+
+    <div id="outerContainer">
+      <div id="mainContainer" class="book-wrapper">
+
+        %if 'chapters' in textbook:
+        <section aria-label="Textbook Navigation" class="book-sidebar">
+          <ul id="booknav" class="treeview-booknav">
+            <%def name="print_entry(entry, index_value)">
+              <li id="htmlchapter-${index_value}">
+                <a class="chapter">
+                  ${entry.get('title')}
+                </a>
+              </li>
+            </%def>
+
+            %for (index, entry) in enumerate(textbook['chapters']):
+              ${print_entry(entry, index+1)}
+            % endfor
+          </ul>
+        </section>
+        %endif
+
+        <section id="viewerContainer" class="book">
+          <section class="page"> 
+            <div id="bookpage" />
+          </section> 
+        </section>
+
+      </div>
+    </div>
+
diff --git a/lms/urls.py b/lms/urls.py
index f2ec45f..2c73891 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -280,6 +280,15 @@ if settings.COURSEWARE_ENABLED:
         url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/pdfbook/(?P<book_index>[^/]*)/chapter/(?P<chapter>[^/]*)/(?P<page>[^/]*)$',
             'staticbook.views.pdf_index'),
 
+        url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/htmlbook/(?P<book_index>[^/]*)/$',
+            'staticbook.views.html_index', name="html_book"),
+        url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/htmlbook/(?P<book_index>[^/]*)/chapter/(?P<chapter>[^/]*)/$',
+            'staticbook.views.html_index'),
+        url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/htmlbook/(?P<book_index>[^/]*)/chapter/(?P<chapter>[^/]*)/(?P<anchor_id>[^/]*)/$',
+            'staticbook.views.html_index'),
+        url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/htmlbook/(?P<book_index>[^/]*)/(?P<anchor_id>[^/]*)/$',
+            'staticbook.views.html_index'),
+
         url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/?$',
             'courseware.views.index', name="courseware"),
         url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/(?P<chapter>[^/]*)/$',