diff --git a/.gitignore b/.gitignore index 493df5a7fd..b13a128a63 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,5 @@ nosetests.xml cover_html/ .idea/ .redcar/ -chromedriver.log \ No newline at end of file +chromedriver.log +ghostdriver.log diff --git a/cms/djangoapps/contentstore/features/advanced-settings.feature b/cms/djangoapps/contentstore/features/advanced-settings.feature index 4708a60be1..779d44e4b2 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.feature +++ b/cms/djangoapps/contentstore/features/advanced-settings.feature @@ -7,6 +7,7 @@ Feature: Advanced (manual) course policy When I select the Advanced Settings Then I see only the display name + @skip-phantom Scenario: Test if there are no policy settings without existing UI controls Given I am on the Advanced Course Settings page in Studio When I delete the display name @@ -14,6 +15,7 @@ Feature: Advanced (manual) course policy And I reload the page Then there are no advanced policy settings + @skip-phantom Scenario: Test cancel editing key name Given I am on the Advanced Course Settings page in Studio When I edit the name of a policy key @@ -32,6 +34,7 @@ Feature: Advanced (manual) course policy And I press the "Cancel" notification button Then the policy key value is unchanged + @skip-phantom Scenario: Test editing key value Given I am on the Advanced Course Settings page in Studio When I edit the value of a policy key diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py index 91daf70718..ec58aeccc6 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.py +++ b/cms/djangoapps/contentstore/features/advanced-settings.py @@ -1,9 +1,10 @@ from lettuce import world, step from common import * import time +from selenium.common.exceptions import WebDriverException +from selenium.webdriver.support import expected_conditions as EC -from nose.tools import assert_equal -from nose.tools import assert_true +from nose.tools import assert_true, assert_false, assert_equal """ http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdriver.common.keys.html @@ -19,6 +20,7 @@ def i_select_advanced_settings(step): css_click(expand_icon_css) link_css = 'li.nav-course-settings-advanced a' css_click(link_css) + # world.browser.click_link_by_text('Advanced Settings') @step('I am on the Advanced Course Settings page in Studio$') @@ -42,8 +44,20 @@ def edit_the_name_of_a_policy_key(step): @step(u'I press the "([^"]*)" notification button$') def press_the_notification_button(step, name): - world.browser.click_link_by_text(name) + def is_visible(driver): + return EC.visibility_of_element_located((By.CSS_SELECTOR,css,)) + def is_invisible(driver): + return EC.invisibility_of_element_located((By.CSS_SELECTOR,css,)) + css = 'a.%s-button' % name.lower() + wait_for(is_visible) + + try: + css_click_at(css) + wait_for(is_invisible) + except WebDriverException, e: + css_click_at(css) + wait_for(is_invisible) @step(u'I edit the value of a policy key$') def edit_the_value_of_a_policy_key(step): @@ -99,29 +113,29 @@ def it_is_formatted(step): @step(u'the policy key name is unchanged$') def the_policy_key_name_is_unchanged(step): policy_key_css = 'input.policy-key' - e = css_find(policy_key_css).first - assert_equal(e.value, 'display_name') + val = css_find(policy_key_css).first.value + assert_equal(val, 'display_name') @step(u'the policy key name is changed$') def the_policy_key_name_is_changed(step): policy_key_css = 'input.policy-key' - e = css_find(policy_key_css).first - assert_equal(e.value, 'new') + val = css_find(policy_key_css).first.value + assert_equal(val, 'new') @step(u'the policy key value is unchanged$') def the_policy_key_value_is_unchanged(step): policy_value_css = 'li.course-advanced-policy-list-item div.value textarea' - e = css_find(policy_value_css).first - assert_equal(e.value, '"Robot Super Course"') + val = css_find(policy_value_css).first.value + assert_equal(val, '"Robot Super Course"') @step(u'the policy key value is changed$') def the_policy_key_value_is_unchanged(step): policy_value_css = 'li.course-advanced-policy-list-item div.value textarea' - e = css_find(policy_value_css).first - assert_equal(e.value, '"Robot Super Course X"') + val = css_find(policy_value_css).first.value + assert_equal(val, '"Robot Super Course X"') ############# HELPERS ############### @@ -132,19 +146,23 @@ def create_entry(key, value): new_key_css = 'div#__new_advanced_key__ input' new_key_element = css_find(new_key_css).first new_key_element.fill(key) -# For some reason have to get the instance for each command (get error that it is no longer attached to the DOM) -# Have to do all this because Selenium has a bug that fill does not remove existing text +# For some reason have to get the instance for each command +# (get error that it is no longer attached to the DOM) +# Have to do all this because Selenium fill does not remove existing text new_value_css = 'div.CodeMirror textarea' css_find(new_value_css).last.fill("") css_find(new_value_css).last._element.send_keys(Keys.DELETE, Keys.DELETE) css_find(new_value_css).last.fill(value) + # Add in a TAB key press because intermittently on ubuntu the + # last character of "value" above was not getting typed in + css_find(new_value_css).last._element.send_keys(Keys.TAB) def delete_entry(index): """ Delete the nth entry where index is 0-based """ - css = '.delete-button' + css = 'a.delete-button' assert_true(world.browser.is_element_present_by_css(css, 5)) delete_buttons = css_find(css) assert_true(len(delete_buttons) > index, "no delete button exists for entry " + str(index)) @@ -165,16 +183,16 @@ def assert_entries(css, expected_values): def click_save(): - css = ".save-button" + css = "a.save-button" - def is_shown(driver): - visible = css_find(css).first.visible - if visible: - # Even when waiting for visible, this fails sporadically. Adding in a small wait. - time.sleep(float(1)) - return visible - wait_for(is_shown) - css_click(css) + # def is_shown(driver): + # visible = css_find(css).first.visible + # if visible: + # # Even when waiting for visible, this fails sporadically. Adding in a small wait. + # time.sleep(float(1)) + # return visible + # wait_for(is_shown) + css_click_at(css) def fill_last_field(value): diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 61b4fee9f6..f7e76ecf7f 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -3,18 +3,20 @@ from lettuce.django import django_url from nose.tools import assert_true from nose.tools import assert_equal from selenium.webdriver.support.ui import WebDriverWait +from selenium.common.exceptions import WebDriverException, StaleElementReferenceException +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.common.by import By from terrain.factories import UserFactory, RegistrationFactory, UserProfileFactory from terrain.factories import CourseFactory, GroupFactory -import xmodule.modulestore.django +from xmodule.modulestore.django import _MODULESTORES, modulestore +from xmodule.templates import update_templates from auth.authz import get_user_by_email from logging import getLogger logger = getLogger(__name__) ########### STEP HELPERS ############## - - @step('I (?:visit|access|open) the Studio homepage$') def i_visit_the_studio_homepage(step): # To make this go to port 8001, put @@ -52,9 +54,8 @@ def i_have_opened_a_new_course(step): log_into_studio() create_a_course() + ####### HELPER FUNCTIONS ############## - - def create_studio_user( uname='robot', email='robot+studio@edx.org', @@ -83,9 +84,9 @@ def flush_xmodule_store(): # (though it shouldn't), do this manually # from the bash shell to drop it: # $ mongo test_xmodule --eval "db.dropDatabase()" - xmodule.modulestore.django._MODULESTORES = {} - xmodule.modulestore.django.modulestore().collection.drop() - xmodule.templates.update_templates() + _MODULESTORES = {} + modulestore().collection.drop() + update_templates() def assert_css_with_text(css, text): @@ -94,8 +95,16 @@ def assert_css_with_text(css, text): def css_click(css): - assert_true(world.browser.is_element_present_by_css(css, 5)) - world.browser.find_by_css(css).first.click() + ''' + First try to use the regular click method, + but if clicking in the middle of an element + doesn't work it might be that it thinks some other + element is on top of it there so click in the upper left + ''' + try: + css_find(css).first.click() + except WebDriverException, e: + css_click_at(css) def css_click_at(css, x=10, y=10): @@ -103,8 +112,7 @@ def css_click_at(css, x=10, y=10): A method to click at x,y coordinates of the element rather than in the center of the element ''' - assert_true(world.browser.is_element_present_by_css(css, 5)) - e = world.browser.find_by_css(css).first + e = css_find(css).first e.action_chains.move_to_element_with_offset(e._element, x, y) e.action_chains.click() e.action_chains.perform() @@ -115,11 +123,16 @@ def css_fill(css, value): def css_find(css): + def is_visible(driver): + return EC.visibility_of_element_located((By.CSS_SELECTOR,css,)) + + assert_true(world.browser.is_element_present_by_css(css, 5)) + wait_for(is_visible) return world.browser.find_by_css(css) def wait_for(func): - WebDriverWait(world.browser.driver, 10).until(func) + WebDriverWait(world.browser.driver, 5).until(func) def id_find(id): diff --git a/cms/djangoapps/contentstore/features/section.feature b/cms/djangoapps/contentstore/features/section.feature index 75e7a4af10..08d38367bc 100644 --- a/cms/djangoapps/contentstore/features/section.feature +++ b/cms/djangoapps/contentstore/features/section.feature @@ -26,9 +26,10 @@ Feature: Create Section And I save a new section release date Then the section release date is updated + @skip-phantom Scenario: Delete section Given I have opened a new course in Studio And I have added a new section When I press the "section" delete icon And I confirm the alert - Then the section does not exist \ No newline at end of file + Then the section does not exist diff --git a/cms/djangoapps/contentstore/features/section.py b/cms/djangoapps/contentstore/features/section.py index cfa4e4bb52..b5ddb48a09 100644 --- a/cms/djangoapps/contentstore/features/section.py +++ b/cms/djangoapps/contentstore/features/section.py @@ -1,6 +1,8 @@ from lettuce import world, step from common import * from nose.tools import assert_equal +from selenium.webdriver.common.keys import Keys +import time ############### ACTIONS #################### @@ -37,10 +39,14 @@ def i_save_a_new_section_release_date(step): date_css = 'input.start-date.date.hasDatepicker' time_css = 'input.start-time.time.ui-timepicker-input' css_fill(date_css, '12/25/2013') - # click here to make the calendar go away - css_click(time_css) + # hit TAB to get to the time field + e = css_find(date_css).first + e._element.send_keys(Keys.TAB) css_fill(time_css, '12:00am') - css_click('a.save-button') + e = css_find(time_css).first + e._element.send_keys(Keys.TAB) + time.sleep(float(1)) + world.browser.click_link_by_text('Save') ############ ASSERTIONS ################### @@ -106,7 +112,7 @@ def the_section_release_date_picker_not_visible(step): def the_section_release_date_is_updated(step): css = 'span.published-status' status_text = world.browser.find_by_css(css).text - assert status_text == 'Will Release: 12/25/2013 at 12:00am' + assert_equal(status_text,'Will Release: 12/25/2013 at 12:00am') ############ HELPER METHODS ################### @@ -120,4 +126,4 @@ def save_section_name(name): def see_my_section_on_the_courseware_page(name): section_css = 'span.section-name-span' - assert_css_with_text(section_css, name) \ No newline at end of file + assert_css_with_text(section_css, name) diff --git a/cms/djangoapps/contentstore/features/signup.py b/cms/djangoapps/contentstore/features/signup.py index a786225ead..e8d0dd8229 100644 --- a/cms/djangoapps/contentstore/features/signup.py +++ b/cms/djangoapps/contentstore/features/signup.py @@ -1,4 +1,5 @@ from lettuce import world, step +from common import * @step('I fill in the registration form$') @@ -13,10 +14,11 @@ def i_fill_in_the_registration_form(step): @step('I press the Create My Account button on the registration form$') def i_press_the_button_on_the_registration_form(step): - register_form = world.browser.find_by_css('form#register_form') - submit_css = 'button#submit' - register_form.find_by_css(submit_css).click() - + submit_css = 'form#register_form button#submit' + # Workaround for click not working on ubuntu + # for some unknown reason. + e = css_find(submit_css) + e.type(' ') @step('I should see be on the studio home page$') def i_should_see_be_on_the_studio_home_page(step): diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature b/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature index 5276b90d12..52c10e41a8 100644 --- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature +++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature @@ -21,6 +21,7 @@ Feature: Overview Toggle Section Then I see the "Collapse All Sections" link And all sections are expanded + @skip-phantom Scenario: Collapse link is not removed after last section of a course is deleted Given I have a course with 1 section And I navigate to the course overview page diff --git a/cms/djangoapps/contentstore/features/subsection.feature b/cms/djangoapps/contentstore/features/subsection.feature index 4b5f5b869d..1be5f4aeb9 100644 --- a/cms/djangoapps/contentstore/features/subsection.feature +++ b/cms/djangoapps/contentstore/features/subsection.feature @@ -17,6 +17,7 @@ Feature: Create Subsection And I click to edit the subsection name Then I see the complete subsection name with a quote in the editor + @skip-phantom Scenario: Delete a subsection Given I have opened a new course section in Studio And I have added a new subsection diff --git a/common/djangoapps/static_replace/__init__.py b/common/djangoapps/static_replace/__init__.py index fb1f48d143..b73a658c5f 100644 --- a/common/djangoapps/static_replace/__init__.py +++ b/common/djangoapps/static_replace/__init__.py @@ -84,12 +84,19 @@ def replace_static_urls(text, data_directory, course_namespace=None): if rest.endswith('?raw'): return original - # course_namespace is not None, then use studio style urls - if course_namespace is not None and not isinstance(modulestore(), XMLModuleStore): - url = StaticContent.convert_legacy_static_url(rest, course_namespace) # In debug mode, if we can find the url as is, - elif settings.DEBUG and finders.find(rest, True): + if settings.DEBUG and finders.find(rest, True): return original + # if we're running with a MongoBacked store course_namespace is not None, then use studio style urls + elif course_namespace is not None and not isinstance(modulestore(), XMLModuleStore): + # first look in the static file pipeline and see if we are trying to reference + # a piece of static content which is in the mitx repo (e.g. JS associated with an xmodule) + if staticfiles_storage.exists(rest): + url = staticfiles_storage.url(rest) + else: + # if not, then assume it's courseware specific content and then look in the + # Mongo-backed database + url = StaticContent.convert_legacy_static_url(rest, course_namespace) # Otherwise, look the file up in staticfiles_storage, and append the data directory if needed else: course_path = "/".join((data_directory, rest)) diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 8c2a8ba7a5..0881d86124 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -13,6 +13,7 @@ from django.core.management import call_command def initial_setup(server): # Launch the browser app (choose one of these below) world.browser = Browser('chrome') + # world.browser = Browser('phantomjs') # world.browser = Browser('firefox') diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 14c590a660..8b32686985 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -29,6 +29,7 @@ import sys from lxml import etree from xml.sax.saxutils import unescape +from copy import deepcopy import chem import chem.chemcalc @@ -497,11 +498,10 @@ class LoncapaProblem(object): Used by get_html. ''' - if (problemtree.tag == 'script' and problemtree.get('type') and 'javascript' in problemtree.get('type')): # leave javascript intact. - return problemtree + return deepcopy(problemtree) if problemtree.tag in html_problem_semantics: return diff --git a/common/lib/capa/capa/correctmap.py b/common/lib/capa/capa/correctmap.py index f246b406d5..ea56863a2f 100644 --- a/common/lib/capa/capa/correctmap.py +++ b/common/lib/capa/capa/correctmap.py @@ -111,15 +111,14 @@ class CorrectMap(object): return None def get_npoints(self, answer_id): - """ Return the number of points for an answer: - If the answer is correct, return the assigned - number of points (default: 1 point) - Otherwise, return 0 points """ - if self.is_correct(answer_id): - npoints = self.get_property(answer_id, 'npoints') - return npoints if npoints is not None else 1 - else: - return 0 + """Return the number of points for an answer, used for partial credit.""" + npoints = self.get_property(answer_id, 'npoints') + if npoints is not None: + return npoints + elif self.is_correct(answer_id): + return 1 + # if not correct and no points have been assigned, return 0 + return 0 def set_property(self, answer_id, property, value): if answer_id in self.cmap: diff --git a/common/lib/capa/capa/tests/test_correctmap.py b/common/lib/capa/capa/tests/test_correctmap.py index 23734467bb..270ba4d849 100644 --- a/common/lib/capa/capa/tests/test_correctmap.py +++ b/common/lib/capa/capa/tests/test_correctmap.py @@ -91,12 +91,12 @@ class CorrectMapTest(unittest.TestCase): npoints=0) # Assert that we get the expected points - # If points assigned and correct --> npoints + # If points assigned --> npoints # If no points assigned and correct --> 1 point - # Otherwise --> 0 points + # If no points assigned and incorrect --> 0 points self.assertEqual(self.cmap.get_npoints('1_2_1'), 5) self.assertEqual(self.cmap.get_npoints('2_2_1'), 1) - self.assertEqual(self.cmap.get_npoints('3_2_1'), 0) + self.assertEqual(self.cmap.get_npoints('3_2_1'), 5) self.assertEqual(self.cmap.get_npoints('4_2_1'), 0) self.assertEqual(self.cmap.get_npoints('5_2_1'), 0) diff --git a/common/lib/capa/capa/tests/test_html_render.py b/common/lib/capa/capa/tests/test_html_render.py index ca2a3c2e2c..6c74d06ef4 100644 --- a/common/lib/capa/capa/tests/test_html_render.py +++ b/common/lib/capa/capa/tests/test_html_render.py @@ -3,6 +3,7 @@ from lxml import etree import os import textwrap import json + import mock from capa.capa_problem import LoncapaProblem @@ -49,6 +50,8 @@ class CapaHtmlRenderTest(unittest.TestCase): self.assertEqual(test_element.text, "Test include") + + def test_process_outtext(self): # Generate some XML with and xml_str = textwrap.dedent(""" @@ -86,6 +89,25 @@ class CapaHtmlRenderTest(unittest.TestCase): script_element = rendered_html.find('script') self.assertEqual(None, script_element) + def test_render_javascript(self): + # Generate some XML with a + + """) + + # Create the problem + problem = LoncapaProblem(xml_str, '1', system=test_system) + + # Render the HTML + rendered_html = etree.XML(problem.get_html()) + + + # expect the javascript is still present in the rendered html + self.assertTrue("" in etree.tostring(rendered_html)) + + def test_render_response_xml(self): # Generate some XML for a string response kwargs = {'question_text': "Test question", diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index b0d3950f06..2597690572 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -582,7 +582,7 @@ class CapaModule(XModule): @staticmethod def make_dict_of_responses(get): '''Make dictionary of student responses (aka "answers") - get is POST dictionary. + get is POST dictionary (Djano QueryDict). The *get* dict has keys of the form 'x_y', which are mapped to key 'y' in the returned dict. For example, @@ -606,6 +606,7 @@ class CapaModule(XModule): to 'input_1' in the returned dict) ''' answers = dict() + for key in get: # e.g. input_resistor_1 ==> resistor_1 _, _, name = key.partition('_') @@ -613,7 +614,7 @@ class CapaModule(XModule): # If key has no underscores, then partition # will return (key, '', '') # We detect this and raise an error - if name is '': + if not name: raise ValueError("%s must contain at least one underscore" % str(key)) else: @@ -625,10 +626,7 @@ class CapaModule(XModule): name = name[:-2] if is_list_key else name if is_list_key: - if type(get[key]) is list: - val = get[key] - else: - val = [get[key]] + val = get.getlist(key) else: val = get[key] diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py index 6330511fc5..cb77921957 100644 --- a/common/lib/xmodule/xmodule/tests/test_capa_module.py +++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py @@ -11,6 +11,8 @@ from xmodule.capa_module import CapaModule from xmodule.modulestore import Location from lxml import etree +from django.http import QueryDict + from . import test_system @@ -326,14 +328,18 @@ class CapaModuleTest(unittest.TestCase): def test_parse_get_params(self): + # We have to set up Django settings in order to use QueryDict + from django.conf import settings + settings.configure() + # Valid GET param dict - valid_get_dict = {'input_1': 'test', - 'input_1_2': 'test', - 'input_1_2_3': 'test', - 'input_[]_3': 'test', - 'input_4': None, - 'input_5': [], - 'input_6': 5} + valid_get_dict = self._querydict_from_dict({'input_1': 'test', + 'input_1_2': 'test', + 'input_1_2_3': 'test', + 'input_[]_3': 'test', + 'input_4': None, + 'input_5': [], + 'input_6': 5}) result = CapaModule.make_dict_of_responses(valid_get_dict) @@ -347,20 +353,19 @@ class CapaModuleTest(unittest.TestCase): # Valid GET param dict with list keys - valid_get_dict = {'input_2[]': ['test1', 'test2']} + valid_get_dict = self._querydict_from_dict({'input_2[]': ['test1', 'test2']}) result = CapaModule.make_dict_of_responses(valid_get_dict) self.assertTrue('2' in result) - self.assertEqual(valid_get_dict['input_2[]'], result['2']) + self.assertEqual(['test1','test2'], result['2']) # If we use [] at the end of a key name, we should always # get a list, even if there's just one value - valid_get_dict = {'input_1[]': 'test'} + valid_get_dict = self._querydict_from_dict({'input_1[]': 'test'}) result = CapaModule.make_dict_of_responses(valid_get_dict) self.assertEqual(result['1'], ['test']) - # If we have no underscores in the name, then the key is invalid - invalid_get_dict = {'input': 'test'} + invalid_get_dict = self._querydict_from_dict({'input': 'test'}) with self.assertRaises(ValueError): result = CapaModule.make_dict_of_responses(invalid_get_dict) @@ -368,11 +373,32 @@ class CapaModuleTest(unittest.TestCase): # Two equivalent names (one list, one non-list) # One of the values would overwrite the other, so detect this # and raise an exception - invalid_get_dict = {'input_1[]': 'test 1', - 'input_1': 'test 2' } + invalid_get_dict = self._querydict_from_dict({'input_1[]': 'test 1', + 'input_1': 'test 2' }) with self.assertRaises(ValueError): result = CapaModule.make_dict_of_responses(invalid_get_dict) + def _querydict_from_dict(self, param_dict): + """ Create a Django QueryDict from a Python dictionary """ + + # QueryDict objects are immutable by default, so we make + # a copy that we can update. + querydict = QueryDict('') + copyDict = querydict.copy() + + for (key, val) in param_dict.items(): + + # QueryDicts handle lists differently from ordinary values, + # so we have to specifically tell the QueryDict that + # this is a list + if type(val) is list: + copyDict.setlist(key, val) + else: + copyDict[key] = val + + return copyDict + + def test_check_problem_correct(self): module = CapaFactory.create(attempts=1) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 23d27c72ac..ec2178f642 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -500,7 +500,7 @@ def modx_dispatch(request, dispatch, location, course_id): if instance is None: # Either permissions just changed, or someone is trying to be clever # and load something they shouldn't have access to. - log.debug("No module {0} for user {1}--access denied?".format(location, user)) + log.debug("No module {0} for user {1}--access denied?".format(location, request.user)) raise Http404 instance_module = get_instance_module(course_id, request.user, instance, student_module_cache)