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>[^/]*)/$',