Merge branch 'master' into fix/btalbot/studio-sasscleanup
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -28,4 +28,5 @@ nosetests.xml
|
|||||||
cover_html/
|
cover_html/
|
||||||
.idea/
|
.idea/
|
||||||
.redcar/
|
.redcar/
|
||||||
chromedriver.log
|
chromedriver.log
|
||||||
|
ghostdriver.log
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Feature: Advanced (manual) course policy
|
|||||||
When I select the Advanced Settings
|
When I select the Advanced Settings
|
||||||
Then I see only the display name
|
Then I see only the display name
|
||||||
|
|
||||||
|
@skip-phantom
|
||||||
Scenario: Test if there are no policy settings without existing UI controls
|
Scenario: Test if there are no policy settings without existing UI controls
|
||||||
Given I am on the Advanced Course Settings page in Studio
|
Given I am on the Advanced Course Settings page in Studio
|
||||||
When I delete the display name
|
When I delete the display name
|
||||||
@@ -14,6 +15,7 @@ Feature: Advanced (manual) course policy
|
|||||||
And I reload the page
|
And I reload the page
|
||||||
Then there are no advanced policy settings
|
Then there are no advanced policy settings
|
||||||
|
|
||||||
|
@skip-phantom
|
||||||
Scenario: Test cancel editing key name
|
Scenario: Test cancel editing key name
|
||||||
Given I am on the Advanced Course Settings page in Studio
|
Given I am on the Advanced Course Settings page in Studio
|
||||||
When I edit the name of a policy key
|
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
|
And I press the "Cancel" notification button
|
||||||
Then the policy key value is unchanged
|
Then the policy key value is unchanged
|
||||||
|
|
||||||
|
@skip-phantom
|
||||||
Scenario: Test editing key value
|
Scenario: Test editing key value
|
||||||
Given I am on the Advanced Course Settings page in Studio
|
Given I am on the Advanced Course Settings page in Studio
|
||||||
When I edit the value of a policy key
|
When I edit the value of a policy key
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from lettuce import world, step
|
from lettuce import world, step
|
||||||
from common import *
|
from common import *
|
||||||
import time
|
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, assert_false, assert_equal
|
||||||
from nose.tools import assert_true
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdriver.common.keys.html
|
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)
|
css_click(expand_icon_css)
|
||||||
link_css = 'li.nav-course-settings-advanced a'
|
link_css = 'li.nav-course-settings-advanced a'
|
||||||
css_click(link_css)
|
css_click(link_css)
|
||||||
|
# world.browser.click_link_by_text('Advanced Settings')
|
||||||
|
|
||||||
|
|
||||||
@step('I am on the Advanced Course Settings page in Studio$')
|
@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$')
|
@step(u'I press the "([^"]*)" notification button$')
|
||||||
def press_the_notification_button(step, name):
|
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$')
|
@step(u'I edit the value of a policy key$')
|
||||||
def edit_the_value_of_a_policy_key(step):
|
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$')
|
@step(u'the policy key name is unchanged$')
|
||||||
def the_policy_key_name_is_unchanged(step):
|
def the_policy_key_name_is_unchanged(step):
|
||||||
policy_key_css = 'input.policy-key'
|
policy_key_css = 'input.policy-key'
|
||||||
e = css_find(policy_key_css).first
|
val = css_find(policy_key_css).first.value
|
||||||
assert_equal(e.value, 'display_name')
|
assert_equal(val, 'display_name')
|
||||||
|
|
||||||
|
|
||||||
@step(u'the policy key name is changed$')
|
@step(u'the policy key name is changed$')
|
||||||
def the_policy_key_name_is_changed(step):
|
def the_policy_key_name_is_changed(step):
|
||||||
policy_key_css = 'input.policy-key'
|
policy_key_css = 'input.policy-key'
|
||||||
e = css_find(policy_key_css).first
|
val = css_find(policy_key_css).first.value
|
||||||
assert_equal(e.value, 'new')
|
assert_equal(val, 'new')
|
||||||
|
|
||||||
|
|
||||||
@step(u'the policy key value is unchanged$')
|
@step(u'the policy key value is unchanged$')
|
||||||
def the_policy_key_value_is_unchanged(step):
|
def the_policy_key_value_is_unchanged(step):
|
||||||
policy_value_css = 'li.course-advanced-policy-list-item div.value textarea'
|
policy_value_css = 'li.course-advanced-policy-list-item div.value textarea'
|
||||||
e = css_find(policy_value_css).first
|
val = css_find(policy_value_css).first.value
|
||||||
assert_equal(e.value, '"Robot Super Course"')
|
assert_equal(val, '"Robot Super Course"')
|
||||||
|
|
||||||
|
|
||||||
@step(u'the policy key value is changed$')
|
@step(u'the policy key value is changed$')
|
||||||
def the_policy_key_value_is_unchanged(step):
|
def the_policy_key_value_is_unchanged(step):
|
||||||
policy_value_css = 'li.course-advanced-policy-list-item div.value textarea'
|
policy_value_css = 'li.course-advanced-policy-list-item div.value textarea'
|
||||||
e = css_find(policy_value_css).first
|
val = css_find(policy_value_css).first.value
|
||||||
assert_equal(e.value, '"Robot Super Course X"')
|
assert_equal(val, '"Robot Super Course X"')
|
||||||
|
|
||||||
|
|
||||||
############# HELPERS ###############
|
############# HELPERS ###############
|
||||||
@@ -132,19 +146,23 @@ def create_entry(key, value):
|
|||||||
new_key_css = 'div#__new_advanced_key__ input'
|
new_key_css = 'div#__new_advanced_key__ input'
|
||||||
new_key_element = css_find(new_key_css).first
|
new_key_element = css_find(new_key_css).first
|
||||||
new_key_element.fill(key)
|
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)
|
# For some reason have to get the instance for each command
|
||||||
# Have to do all this because Selenium has a bug that fill does not remove existing text
|
# (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'
|
new_value_css = 'div.CodeMirror textarea'
|
||||||
css_find(new_value_css).last.fill("")
|
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._element.send_keys(Keys.DELETE, Keys.DELETE)
|
||||||
css_find(new_value_css).last.fill(value)
|
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):
|
def delete_entry(index):
|
||||||
"""
|
"""
|
||||||
Delete the nth entry where index is 0-based
|
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))
|
assert_true(world.browser.is_element_present_by_css(css, 5))
|
||||||
delete_buttons = css_find(css)
|
delete_buttons = css_find(css)
|
||||||
assert_true(len(delete_buttons) > index, "no delete button exists for entry " + str(index))
|
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():
|
def click_save():
|
||||||
css = ".save-button"
|
css = "a.save-button"
|
||||||
|
|
||||||
def is_shown(driver):
|
# def is_shown(driver):
|
||||||
visible = css_find(css).first.visible
|
# visible = css_find(css).first.visible
|
||||||
if visible:
|
# if visible:
|
||||||
# Even when waiting for visible, this fails sporadically. Adding in a small wait.
|
# # Even when waiting for visible, this fails sporadically. Adding in a small wait.
|
||||||
time.sleep(float(1))
|
# time.sleep(float(1))
|
||||||
return visible
|
# return visible
|
||||||
wait_for(is_shown)
|
# wait_for(is_shown)
|
||||||
css_click(css)
|
css_click_at(css)
|
||||||
|
|
||||||
|
|
||||||
def fill_last_field(value):
|
def fill_last_field(value):
|
||||||
|
|||||||
@@ -3,18 +3,20 @@ from lettuce.django import django_url
|
|||||||
from nose.tools import assert_true
|
from nose.tools import assert_true
|
||||||
from nose.tools import assert_equal
|
from nose.tools import assert_equal
|
||||||
from selenium.webdriver.support.ui import WebDriverWait
|
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 UserFactory, RegistrationFactory, UserProfileFactory
|
||||||
from terrain.factories import CourseFactory, GroupFactory
|
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 auth.authz import get_user_by_email
|
||||||
|
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
########### STEP HELPERS ##############
|
########### STEP HELPERS ##############
|
||||||
|
|
||||||
|
|
||||||
@step('I (?:visit|access|open) the Studio homepage$')
|
@step('I (?:visit|access|open) the Studio homepage$')
|
||||||
def i_visit_the_studio_homepage(step):
|
def i_visit_the_studio_homepage(step):
|
||||||
# To make this go to port 8001, put
|
# To make this go to port 8001, put
|
||||||
@@ -52,9 +54,8 @@ def i_have_opened_a_new_course(step):
|
|||||||
log_into_studio()
|
log_into_studio()
|
||||||
create_a_course()
|
create_a_course()
|
||||||
|
|
||||||
|
|
||||||
####### HELPER FUNCTIONS ##############
|
####### HELPER FUNCTIONS ##############
|
||||||
|
|
||||||
|
|
||||||
def create_studio_user(
|
def create_studio_user(
|
||||||
uname='robot',
|
uname='robot',
|
||||||
email='robot+studio@edx.org',
|
email='robot+studio@edx.org',
|
||||||
@@ -83,9 +84,9 @@ def flush_xmodule_store():
|
|||||||
# (though it shouldn't), do this manually
|
# (though it shouldn't), do this manually
|
||||||
# from the bash shell to drop it:
|
# from the bash shell to drop it:
|
||||||
# $ mongo test_xmodule --eval "db.dropDatabase()"
|
# $ mongo test_xmodule --eval "db.dropDatabase()"
|
||||||
xmodule.modulestore.django._MODULESTORES = {}
|
_MODULESTORES = {}
|
||||||
xmodule.modulestore.django.modulestore().collection.drop()
|
modulestore().collection.drop()
|
||||||
xmodule.templates.update_templates()
|
update_templates()
|
||||||
|
|
||||||
|
|
||||||
def assert_css_with_text(css, text):
|
def assert_css_with_text(css, text):
|
||||||
@@ -94,8 +95,16 @@ def assert_css_with_text(css, text):
|
|||||||
|
|
||||||
|
|
||||||
def css_click(css):
|
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):
|
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
|
A method to click at x,y coordinates of the element
|
||||||
rather than in the center of the element
|
rather than in the center of the element
|
||||||
'''
|
'''
|
||||||
assert_true(world.browser.is_element_present_by_css(css, 5))
|
e = css_find(css).first
|
||||||
e = world.browser.find_by_css(css).first
|
|
||||||
e.action_chains.move_to_element_with_offset(e._element, x, y)
|
e.action_chains.move_to_element_with_offset(e._element, x, y)
|
||||||
e.action_chains.click()
|
e.action_chains.click()
|
||||||
e.action_chains.perform()
|
e.action_chains.perform()
|
||||||
@@ -115,11 +123,16 @@ def css_fill(css, value):
|
|||||||
|
|
||||||
|
|
||||||
def css_find(css):
|
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)
|
return world.browser.find_by_css(css)
|
||||||
|
|
||||||
|
|
||||||
def wait_for(func):
|
def wait_for(func):
|
||||||
WebDriverWait(world.browser.driver, 10).until(func)
|
WebDriverWait(world.browser.driver, 5).until(func)
|
||||||
|
|
||||||
|
|
||||||
def id_find(id):
|
def id_find(id):
|
||||||
|
|||||||
@@ -26,9 +26,10 @@ Feature: Create Section
|
|||||||
And I save a new section release date
|
And I save a new section release date
|
||||||
Then the section release date is updated
|
Then the section release date is updated
|
||||||
|
|
||||||
|
@skip-phantom
|
||||||
Scenario: Delete section
|
Scenario: Delete section
|
||||||
Given I have opened a new course in Studio
|
Given I have opened a new course in Studio
|
||||||
And I have added a new section
|
And I have added a new section
|
||||||
When I press the "section" delete icon
|
When I press the "section" delete icon
|
||||||
And I confirm the alert
|
And I confirm the alert
|
||||||
Then the section does not exist
|
Then the section does not exist
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from lettuce import world, step
|
from lettuce import world, step
|
||||||
from common import *
|
from common import *
|
||||||
from nose.tools import assert_equal
|
from nose.tools import assert_equal
|
||||||
|
from selenium.webdriver.common.keys import Keys
|
||||||
|
import time
|
||||||
|
|
||||||
############### ACTIONS ####################
|
############### ACTIONS ####################
|
||||||
|
|
||||||
@@ -37,10 +39,14 @@ def i_save_a_new_section_release_date(step):
|
|||||||
date_css = 'input.start-date.date.hasDatepicker'
|
date_css = 'input.start-date.date.hasDatepicker'
|
||||||
time_css = 'input.start-time.time.ui-timepicker-input'
|
time_css = 'input.start-time.time.ui-timepicker-input'
|
||||||
css_fill(date_css, '12/25/2013')
|
css_fill(date_css, '12/25/2013')
|
||||||
# click here to make the calendar go away
|
# hit TAB to get to the time field
|
||||||
css_click(time_css)
|
e = css_find(date_css).first
|
||||||
|
e._element.send_keys(Keys.TAB)
|
||||||
css_fill(time_css, '12:00am')
|
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 ###################
|
############ ASSERTIONS ###################
|
||||||
@@ -106,7 +112,7 @@ def the_section_release_date_picker_not_visible(step):
|
|||||||
def the_section_release_date_is_updated(step):
|
def the_section_release_date_is_updated(step):
|
||||||
css = 'span.published-status'
|
css = 'span.published-status'
|
||||||
status_text = world.browser.find_by_css(css).text
|
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 ###################
|
############ HELPER METHODS ###################
|
||||||
@@ -120,4 +126,4 @@ def save_section_name(name):
|
|||||||
|
|
||||||
def see_my_section_on_the_courseware_page(name):
|
def see_my_section_on_the_courseware_page(name):
|
||||||
section_css = 'span.section-name-span'
|
section_css = 'span.section-name-span'
|
||||||
assert_css_with_text(section_css, name)
|
assert_css_with_text(section_css, name)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from lettuce import world, step
|
from lettuce import world, step
|
||||||
|
from common import *
|
||||||
|
|
||||||
|
|
||||||
@step('I fill in the registration form$')
|
@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$')
|
@step('I press the Create My Account button on the registration form$')
|
||||||
def i_press_the_button_on_the_registration_form(step):
|
def i_press_the_button_on_the_registration_form(step):
|
||||||
register_form = world.browser.find_by_css('form#register_form')
|
submit_css = 'form#register_form button#submit'
|
||||||
submit_css = 'button#submit'
|
# Workaround for click not working on ubuntu
|
||||||
register_form.find_by_css(submit_css).click()
|
# for some unknown reason.
|
||||||
|
e = css_find(submit_css)
|
||||||
|
e.type(' ')
|
||||||
|
|
||||||
@step('I should see be on the studio home page$')
|
@step('I should see be on the studio home page$')
|
||||||
def i_should_see_be_on_the_studio_home_page(step):
|
def i_should_see_be_on_the_studio_home_page(step):
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ Feature: Overview Toggle Section
|
|||||||
Then I see the "Collapse All Sections" link
|
Then I see the "Collapse All Sections" link
|
||||||
And all sections are expanded
|
And all sections are expanded
|
||||||
|
|
||||||
|
@skip-phantom
|
||||||
Scenario: Collapse link is not removed after last section of a course is deleted
|
Scenario: Collapse link is not removed after last section of a course is deleted
|
||||||
Given I have a course with 1 section
|
Given I have a course with 1 section
|
||||||
And I navigate to the course overview page
|
And I navigate to the course overview page
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ Feature: Create Subsection
|
|||||||
And I click to edit the subsection name
|
And I click to edit the subsection name
|
||||||
Then I see the complete subsection name with a quote in the editor
|
Then I see the complete subsection name with a quote in the editor
|
||||||
|
|
||||||
|
@skip-phantom
|
||||||
Scenario: Delete a subsection
|
Scenario: Delete a subsection
|
||||||
Given I have opened a new course section in Studio
|
Given I have opened a new course section in Studio
|
||||||
And I have added a new subsection
|
And I have added a new subsection
|
||||||
|
|||||||
@@ -84,12 +84,19 @@ def replace_static_urls(text, data_directory, course_namespace=None):
|
|||||||
if rest.endswith('?raw'):
|
if rest.endswith('?raw'):
|
||||||
return original
|
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,
|
# 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
|
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
|
# Otherwise, look the file up in staticfiles_storage, and append the data directory if needed
|
||||||
else:
|
else:
|
||||||
course_path = "/".join((data_directory, rest))
|
course_path = "/".join((data_directory, rest))
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from django.core.management import call_command
|
|||||||
def initial_setup(server):
|
def initial_setup(server):
|
||||||
# Launch the browser app (choose one of these below)
|
# Launch the browser app (choose one of these below)
|
||||||
world.browser = Browser('chrome')
|
world.browser = Browser('chrome')
|
||||||
|
# world.browser = Browser('phantomjs')
|
||||||
# world.browser = Browser('firefox')
|
# world.browser = Browser('firefox')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import sys
|
|||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from xml.sax.saxutils import unescape
|
from xml.sax.saxutils import unescape
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
import chem
|
import chem
|
||||||
import chem.chemcalc
|
import chem.chemcalc
|
||||||
@@ -497,11 +498,10 @@ class LoncapaProblem(object):
|
|||||||
|
|
||||||
Used by get_html.
|
Used by get_html.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
if (problemtree.tag == 'script' and problemtree.get('type')
|
if (problemtree.tag == 'script' and problemtree.get('type')
|
||||||
and 'javascript' in problemtree.get('type')):
|
and 'javascript' in problemtree.get('type')):
|
||||||
# leave javascript intact.
|
# leave javascript intact.
|
||||||
return problemtree
|
return deepcopy(problemtree)
|
||||||
|
|
||||||
if problemtree.tag in html_problem_semantics:
|
if problemtree.tag in html_problem_semantics:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -111,15 +111,14 @@ class CorrectMap(object):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def get_npoints(self, answer_id):
|
def get_npoints(self, answer_id):
|
||||||
""" Return the number of points for an answer:
|
"""Return the number of points for an answer, used for partial credit."""
|
||||||
If the answer is correct, return the assigned
|
npoints = self.get_property(answer_id, 'npoints')
|
||||||
number of points (default: 1 point)
|
if npoints is not None:
|
||||||
Otherwise, return 0 points """
|
return npoints
|
||||||
if self.is_correct(answer_id):
|
elif self.is_correct(answer_id):
|
||||||
npoints = self.get_property(answer_id, 'npoints')
|
return 1
|
||||||
return npoints if npoints is not None else 1
|
# if not correct and no points have been assigned, return 0
|
||||||
else:
|
return 0
|
||||||
return 0
|
|
||||||
|
|
||||||
def set_property(self, answer_id, property, value):
|
def set_property(self, answer_id, property, value):
|
||||||
if answer_id in self.cmap:
|
if answer_id in self.cmap:
|
||||||
|
|||||||
@@ -91,12 +91,12 @@ class CorrectMapTest(unittest.TestCase):
|
|||||||
npoints=0)
|
npoints=0)
|
||||||
|
|
||||||
# Assert that we get the expected points
|
# 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
|
# 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('1_2_1'), 5)
|
||||||
self.assertEqual(self.cmap.get_npoints('2_2_1'), 1)
|
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('4_2_1'), 0)
|
||||||
self.assertEqual(self.cmap.get_npoints('5_2_1'), 0)
|
self.assertEqual(self.cmap.get_npoints('5_2_1'), 0)
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from lxml import etree
|
|||||||
import os
|
import os
|
||||||
import textwrap
|
import textwrap
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
from capa.capa_problem import LoncapaProblem
|
from capa.capa_problem import LoncapaProblem
|
||||||
@@ -49,6 +50,8 @@ class CapaHtmlRenderTest(unittest.TestCase):
|
|||||||
self.assertEqual(test_element.text, "Test include")
|
self.assertEqual(test_element.text, "Test include")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_process_outtext(self):
|
def test_process_outtext(self):
|
||||||
# Generate some XML with <startouttext /> and <endouttext />
|
# Generate some XML with <startouttext /> and <endouttext />
|
||||||
xml_str = textwrap.dedent("""
|
xml_str = textwrap.dedent("""
|
||||||
@@ -86,6 +89,25 @@ class CapaHtmlRenderTest(unittest.TestCase):
|
|||||||
script_element = rendered_html.find('script')
|
script_element = rendered_html.find('script')
|
||||||
self.assertEqual(None, script_element)
|
self.assertEqual(None, script_element)
|
||||||
|
|
||||||
|
def test_render_javascript(self):
|
||||||
|
# Generate some XML with a <script> tag
|
||||||
|
xml_str = textwrap.dedent("""
|
||||||
|
<problem>
|
||||||
|
<script type="text/javascript">function(){}</script>
|
||||||
|
</problem>
|
||||||
|
""")
|
||||||
|
|
||||||
|
# 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("<script type=\"text/javascript\">function(){}</script>" in etree.tostring(rendered_html))
|
||||||
|
|
||||||
|
|
||||||
def test_render_response_xml(self):
|
def test_render_response_xml(self):
|
||||||
# Generate some XML for a string response
|
# Generate some XML for a string response
|
||||||
kwargs = {'question_text': "Test question",
|
kwargs = {'question_text': "Test question",
|
||||||
|
|||||||
@@ -582,7 +582,7 @@ class CapaModule(XModule):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def make_dict_of_responses(get):
|
def make_dict_of_responses(get):
|
||||||
'''Make dictionary of student responses (aka "answers")
|
'''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
|
The *get* dict has keys of the form 'x_y', which are mapped
|
||||||
to key 'y' in the returned dict. For example,
|
to key 'y' in the returned dict. For example,
|
||||||
@@ -606,6 +606,7 @@ class CapaModule(XModule):
|
|||||||
to 'input_1' in the returned dict)
|
to 'input_1' in the returned dict)
|
||||||
'''
|
'''
|
||||||
answers = dict()
|
answers = dict()
|
||||||
|
|
||||||
for key in get:
|
for key in get:
|
||||||
# e.g. input_resistor_1 ==> resistor_1
|
# e.g. input_resistor_1 ==> resistor_1
|
||||||
_, _, name = key.partition('_')
|
_, _, name = key.partition('_')
|
||||||
@@ -613,7 +614,7 @@ class CapaModule(XModule):
|
|||||||
# If key has no underscores, then partition
|
# If key has no underscores, then partition
|
||||||
# will return (key, '', '')
|
# will return (key, '', '')
|
||||||
# We detect this and raise an error
|
# We detect this and raise an error
|
||||||
if name is '':
|
if not name:
|
||||||
raise ValueError("%s must contain at least one underscore" % str(key))
|
raise ValueError("%s must contain at least one underscore" % str(key))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -625,10 +626,7 @@ class CapaModule(XModule):
|
|||||||
name = name[:-2] if is_list_key else name
|
name = name[:-2] if is_list_key else name
|
||||||
|
|
||||||
if is_list_key:
|
if is_list_key:
|
||||||
if type(get[key]) is list:
|
val = get.getlist(key)
|
||||||
val = get[key]
|
|
||||||
else:
|
|
||||||
val = [get[key]]
|
|
||||||
else:
|
else:
|
||||||
val = get[key]
|
val = get[key]
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ from xmodule.capa_module import CapaModule
|
|||||||
from xmodule.modulestore import Location
|
from xmodule.modulestore import Location
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
from django.http import QueryDict
|
||||||
|
|
||||||
from . import test_system
|
from . import test_system
|
||||||
|
|
||||||
|
|
||||||
@@ -326,14 +328,18 @@ class CapaModuleTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_parse_get_params(self):
|
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 param dict
|
||||||
valid_get_dict = {'input_1': 'test',
|
valid_get_dict = self._querydict_from_dict({'input_1': 'test',
|
||||||
'input_1_2': 'test',
|
'input_1_2': 'test',
|
||||||
'input_1_2_3': 'test',
|
'input_1_2_3': 'test',
|
||||||
'input_[]_3': 'test',
|
'input_[]_3': 'test',
|
||||||
'input_4': None,
|
'input_4': None,
|
||||||
'input_5': [],
|
'input_5': [],
|
||||||
'input_6': 5}
|
'input_6': 5})
|
||||||
|
|
||||||
result = CapaModule.make_dict_of_responses(valid_get_dict)
|
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 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)
|
result = CapaModule.make_dict_of_responses(valid_get_dict)
|
||||||
self.assertTrue('2' in result)
|
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
|
# If we use [] at the end of a key name, we should always
|
||||||
# get a list, even if there's just one value
|
# 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)
|
result = CapaModule.make_dict_of_responses(valid_get_dict)
|
||||||
self.assertEqual(result['1'], ['test'])
|
self.assertEqual(result['1'], ['test'])
|
||||||
|
|
||||||
|
|
||||||
# If we have no underscores in the name, then the key is invalid
|
# 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):
|
with self.assertRaises(ValueError):
|
||||||
result = CapaModule.make_dict_of_responses(invalid_get_dict)
|
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)
|
# Two equivalent names (one list, one non-list)
|
||||||
# One of the values would overwrite the other, so detect this
|
# One of the values would overwrite the other, so detect this
|
||||||
# and raise an exception
|
# and raise an exception
|
||||||
invalid_get_dict = {'input_1[]': 'test 1',
|
invalid_get_dict = self._querydict_from_dict({'input_1[]': 'test 1',
|
||||||
'input_1': 'test 2' }
|
'input_1': 'test 2' })
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
result = CapaModule.make_dict_of_responses(invalid_get_dict)
|
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):
|
def test_check_problem_correct(self):
|
||||||
|
|
||||||
module = CapaFactory.create(attempts=1)
|
module = CapaFactory.create(attempts=1)
|
||||||
|
|||||||
@@ -500,7 +500,7 @@ def modx_dispatch(request, dispatch, location, course_id):
|
|||||||
if instance is None:
|
if instance is None:
|
||||||
# Either permissions just changed, or someone is trying to be clever
|
# Either permissions just changed, or someone is trying to be clever
|
||||||
# and load something they shouldn't have access to.
|
# 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
|
raise Http404
|
||||||
|
|
||||||
instance_module = get_instance_module(course_id, request.user, instance, student_module_cache)
|
instance_module = get_instance_module(course_id, request.user, instance, student_module_cache)
|
||||||
|
|||||||
Reference in New Issue
Block a user