diff --git a/cms/djangoapps/contentstore/features/advanced-settings.feature b/cms/djangoapps/contentstore/features/advanced-settings.feature
index af97709ad0..db7294c14c 100644
--- a/cms/djangoapps/contentstore/features/advanced-settings.feature
+++ b/cms/djangoapps/contentstore/features/advanced-settings.feature
@@ -1,6 +1,6 @@
Feature: Advanced (manual) course policy
In order to specify course policy settings for which no custom user interface exists
- I want to be able to manually enter JSON key/value pairs
+ I want to be able to manually enter JSON key /value pairs
Scenario: A course author sees default advanced settings
Given I have opened a new course in Studio
diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py
index 7e86e94a31..16562b6b15 100644
--- a/cms/djangoapps/contentstore/features/advanced-settings.py
+++ b/cms/djangoapps/contentstore/features/advanced-settings.py
@@ -1,9 +1,10 @@
+#pylint: disable=C0111
+#pylint: disable=W0621
+
from lettuce import world, step
from common import *
import time
from terrain.steps import reload_the_page
-from selenium.common.exceptions import WebDriverException
-from selenium.webdriver.support import expected_conditions as EC
from nose.tools import assert_true, assert_false, assert_equal
@@ -18,13 +19,14 @@ DISPLAY_NAME_KEY = "display_name"
DISPLAY_NAME_VALUE = '"Robot Super Course"'
############### ACTIONS ####################
+
@step('I select the Advanced Settings$')
def i_select_advanced_settings(step):
expand_icon_css = 'li.nav-course-settings i.icon-expand'
if world.browser.is_element_present_by_css(expand_icon_css):
- css_click(expand_icon_css)
+ world.css_click(expand_icon_css)
link_css = 'li.nav-course-settings-advanced a'
- css_click(link_css)
+ world.css_click(link_css)
@step('I am on the Advanced Course Settings page in Studio$')
@@ -35,24 +37,8 @@ def i_am_on_advanced_course_settings(step):
@step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(step, 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)
- time.sleep(float(1))
- css_click_at(css)
-
-# is_invisible is not returning a boolean, not working
-# try:
-# css_click_at(css)
-# wait_for(is_invisible)
-# except WebDriverException, e:
-# css_click_at(css)
-# wait_for(is_invisible)
+ world.css_click_at(css)
@step(u'I edit the value of a policy key$')
@@ -61,7 +47,7 @@ def edit_the_value_of_a_policy_key(step):
It is hard to figure out how to get into the CodeMirror
area, so cheat and do it from the policy key field :)
"""
- e = css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
+ e = world.css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
e._element.send_keys(Keys.TAB, Keys.END, Keys.ARROW_LEFT, ' ', 'X')
@@ -85,7 +71,7 @@ def i_see_default_advanced_settings(step):
@step('the settings are alphabetized$')
def they_are_alphabetized(step):
- key_elements = css_find(KEY_CSS)
+ key_elements = world.css_find(KEY_CSS)
all_keys = []
for key in key_elements:
all_keys.append(key.value)
@@ -118,13 +104,13 @@ def assert_policy_entries(expected_keys, expected_values):
for counter in range(len(expected_keys)):
index = get_index_of(expected_keys[counter])
assert_false(index == -1, "Could not find key: " + expected_keys[counter])
- assert_equal(expected_values[counter], css_find(VALUE_CSS)[index].value, "value is incorrect")
+ assert_equal(expected_values[counter], world.css_find(VALUE_CSS)[index].value, "value is incorrect")
def get_index_of(expected_key):
- for counter in range(len(css_find(KEY_CSS))):
+ for counter in range(len(world.css_find(KEY_CSS))):
# Sometimes get stale reference if I hold on to the array of elements
- key = css_find(KEY_CSS)[counter].value
+ key = world.css_find(KEY_CSS)[counter].value
if key == expected_key:
return counter
@@ -133,14 +119,14 @@ def get_index_of(expected_key):
def get_display_name_value():
index = get_index_of(DISPLAY_NAME_KEY)
- return css_find(VALUE_CSS)[index].value
+ return world.css_find(VALUE_CSS)[index].value
def change_display_name_value(step, new_value):
- e = css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
+ e = world.css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
display_name = get_display_name_value()
for count in range(len(display_name)):
e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE)
# Must delete "" before typing the JSON value
e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE, Keys.BACK_SPACE, new_value)
- press_the_notification_button(step, "Save")
\ No newline at end of file
+ press_the_notification_button(step, "Save")
diff --git a/cms/djangoapps/contentstore/features/checklists.py b/cms/djangoapps/contentstore/features/checklists.py
index 9ef66c8096..dc399f5fac 100644
--- a/cms/djangoapps/contentstore/features/checklists.py
+++ b/cms/djangoapps/contentstore/features/checklists.py
@@ -1,15 +1,19 @@
+#pylint: disable=C0111
+#pylint: disable=W0621
+
from lettuce import world, step
-from common import *
+from nose.tools import assert_true, assert_equal
from terrain.steps import reload_the_page
+from selenium.common.exceptions import StaleElementReferenceException
############### ACTIONS ####################
@step('I select Checklists from the Tools menu$')
def i_select_checklists(step):
expand_icon_css = 'li.nav-course-tools i.icon-expand'
if world.browser.is_element_present_by_css(expand_icon_css):
- css_click(expand_icon_css)
+ world.css_click(expand_icon_css)
link_css = 'li.nav-course-tools-checklists a'
- css_click(link_css)
+ world.css_click(link_css)
@step('I have opened Checklists$')
@@ -20,7 +24,7 @@ def i_have_opened_checklists(step):
@step('I see the four default edX checklists$')
def i_see_default_checklists(step):
- checklists = css_find('.checklist-title')
+ checklists = world.css_find('.checklist-title')
assert_equal(4, len(checklists))
assert_true(checklists[0].text.endswith('Getting Started With Studio'))
assert_true(checklists[1].text.endswith('Draft a Rough Course Outline'))
@@ -58,7 +62,7 @@ def i_select_a_link_to_the_course_outline(step):
@step('I am brought to the course outline page$')
def i_am_brought_to_course_outline(step):
- assert_equal('Course Outline', css_find('.outline .title-1')[0].text)
+ assert_equal('Course Outline', world.css_find('.outline .title-1')[0].text)
assert_equal(1, len(world.browser.windows))
@@ -90,30 +94,30 @@ def i_am_brought_to_help_page_in_new_window(step):
def verifyChecklist2Status(completed, total, percentage):
def verify_count(driver):
try:
- statusCount = css_find('#course-checklist1 .status-count').first
+ statusCount = world.css_find('#course-checklist1 .status-count').first
return statusCount.text == str(completed)
except StaleElementReferenceException:
return False
- wait_for(verify_count)
- assert_equal(str(total), css_find('#course-checklist1 .status-amount').first.text)
+ world.wait_for(verify_count)
+ assert_equal(str(total), world.css_find('#course-checklist1 .status-amount').first.text)
# Would like to check the CSS width, but not sure how to do that.
- assert_equal(str(percentage), css_find('#course-checklist1 .viz-checklist-status-value .int').first.text)
+ assert_equal(str(percentage), world.css_find('#course-checklist1 .viz-checklist-status-value .int').first.text)
def toggleTask(checklist, task):
- css_click('#course-checklist' + str(checklist) +'-task' + str(task))
+ world.css_click('#course-checklist' + str(checklist) +'-task' + str(task))
def clickActionLink(checklist, task, actionText):
# toggle checklist item to make sure that the link button is showing
toggleTask(checklist, task)
- action_link = css_find('#course-checklist' + str(checklist) + ' a')[task]
+ action_link = world.css_find('#course-checklist' + str(checklist) + ' a')[task]
# text will be empty initially, wait for it to populate
def verify_action_link_text(driver):
return action_link.text == actionText
- wait_for(verify_action_link_text)
+ world.wait_for(verify_action_link_text)
action_link.click()
diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py
index 820b60123b..3878340af3 100644
--- a/cms/djangoapps/contentstore/features/common.py
+++ b/cms/djangoapps/contentstore/features/common.py
@@ -1,11 +1,9 @@
+#pylint: disable=C0111
+#pylint: disable=W0621
+
from lettuce import world, step
-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 xmodule.modulestore.django import _MODULESTORES, modulestore
from xmodule.templates import update_templates
@@ -15,14 +13,15 @@ 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
# LETTUCE_SERVER_PORT = 8001
# in your settings.py file.
- world.browser.visit(django_url('/'))
+ world.visit('/')
signin_css = 'a.action-signin'
- assert world.browser.is_element_present_by_css(signin_css, 10)
+ assert world.is_css_present(signin_css)
@step('I am logged into Studio$')
@@ -43,12 +42,12 @@ def i_press_the_category_delete_icon(step, category):
css = 'a.delete-button.delete-subsection-button span.delete-icon'
else:
assert False, 'Invalid category: %s' % category
- css_click(css)
+ world.css_click(css)
@step('I have opened a new course in Studio$')
def i_have_opened_a_new_course(step):
- clear_courses()
+ world.clear_courses()
log_into_studio()
create_a_course()
@@ -74,80 +73,13 @@ def create_studio_user(
user_profile = world.UserProfileFactory(user=studio_user)
-def flush_xmodule_store():
- # Flush and initialize the module store
- # It needs the templates because it creates new records
- # by cloning from the template.
- # Note that if your test module gets in some weird state
- # (though it shouldn't), do this manually
- # from the bash shell to drop it:
- # $ mongo test_xmodule --eval "db.dropDatabase()"
- _MODULESTORES = {}
- modulestore().collection.drop()
- update_templates()
-
-
-def assert_css_with_text(css, text):
- assert_true(world.browser.is_element_present_by_css(css, 5))
- assert_equal(world.browser.find_by_css(css).text, text)
-
-
-def css_click(css):
- '''
- 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):
- '''
- A method to click at x,y coordinates of the element
- rather than in the center of the element
- '''
- 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()
-
-
-def css_fill(css, value):
- world.browser.find_by_css(css).first.fill(value)
-
-
-def css_find(css):
- def is_visible(driver):
- return EC.visibility_of_element_located((By.CSS_SELECTOR,css,))
-
- 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, 5).until(func)
-
-
-def id_find(id):
- return world.browser.find_by_id(id)
-
-
-def clear_courses():
- flush_xmodule_store()
-
-
def fill_in_course_info(
name='Robot Super Course',
org='MITx',
num='101'):
- css_fill('.new-course-name', name)
- css_fill('.new-course-org', org)
- css_fill('.new-course-number', num)
+ world.css_fill('.new-course-name', name)
+ world.css_fill('.new-course-org', org)
+ world.css_fill('.new-course-number', num)
def log_into_studio(
@@ -155,21 +87,22 @@ def log_into_studio(
email='robot+studio@edx.org',
password='test',
is_staff=False):
- create_studio_user(uname=uname, email=email, is_staff=is_staff)
- world.browser.cookies.delete()
- world.browser.visit(django_url('/'))
- signin_css = 'a.action-signin'
- world.browser.is_element_present_by_css(signin_css, 10)
- # click the signin button
- css_click(signin_css)
+ create_studio_user(uname=uname, email=email, is_staff=is_staff)
+
+ world.browser.cookies.delete()
+ world.visit('/')
+
+ signin_css = 'a.action-signin'
+ world.is_css_present(signin_css)
+ world.css_click(signin_css)
login_form = world.browser.find_by_css('form#login_form')
login_form.find_by_name('email').fill(email)
login_form.find_by_name('password').fill(password)
login_form.find_by_name('submit').click()
- assert_true(world.browser.is_element_present_by_css('.new-course-button', 5))
+ assert_true(world.is_css_present('.new-course-button'))
def create_a_course():
@@ -184,26 +117,26 @@ def create_a_course():
world.browser.reload()
course_link_css = 'span.class-name'
- css_click(course_link_css)
+ world.css_click(course_link_css)
course_title_css = 'span.course-title'
- assert_true(world.browser.is_element_present_by_css(course_title_css, 5))
+ assert_true(world.is_css_present(course_title_css))
def add_section(name='My Section'):
link_css = 'a.new-courseware-section-button'
- css_click(link_css)
+ world.css_click(link_css)
name_css = 'input.new-section-name'
save_css = 'input.new-section-name-save'
- css_fill(name_css, name)
- css_click(save_css)
+ world.css_fill(name_css, name)
+ world.css_click(save_css)
span_css = 'span.section-name-span'
- assert_true(world.browser.is_element_present_by_css(span_css, 5))
+ assert_true(world.is_css_present(span_css))
def add_subsection(name='Subsection One'):
css = 'a.new-subsection-item'
- css_click(css)
+ world.css_click(css)
name_css = 'input.new-subsection-name-input'
save_css = 'input.new-subsection-name-save'
- css_fill(name_css, name)
- css_click(save_css)
+ world.css_fill(name_css, name)
+ world.css_click(save_css)
diff --git a/cms/djangoapps/contentstore/features/course-settings.py b/cms/djangoapps/contentstore/features/course-settings.py
index a0c25045f2..9eb5b0951d 100644
--- a/cms/djangoapps/contentstore/features/course-settings.py
+++ b/cms/djangoapps/contentstore/features/course-settings.py
@@ -1,5 +1,7 @@
+#pylint: disable=C0111
+#pylint: disable=W0621
+
from lettuce import world, step
-from common import *
from terrain.steps import reload_the_page
from selenium.webdriver.common.keys import Keys
import time
@@ -25,9 +27,9 @@ DEFAULT_TIME = "12:00am"
def test_i_select_schedule_and_details(step):
expand_icon_css = 'li.nav-course-settings i.icon-expand'
if world.browser.is_element_present_by_css(expand_icon_css):
- css_click(expand_icon_css)
+ world.css_click(expand_icon_css)
link_css = 'li.nav-course-settings-schedule a'
- css_click(link_css)
+ world.css_click(link_css)
@step('I have set course dates$')
@@ -97,9 +99,9 @@ def test_i_clear_the_course_start_date(step):
@step('I receive a warning about course start date$')
def test_i_receive_a_warning_about_course_start_date(step):
- assert_css_with_text('.message-error', 'The course must have an assigned start date.')
- assert_true('error' in css_find(COURSE_START_DATE_CSS).first._element.get_attribute('class'))
- assert_true('error' in css_find(COURSE_START_TIME_CSS).first._element.get_attribute('class'))
+ assert_true(world.css_has_text('.message-error', 'The course must have an assigned start date.'))
+ assert_true('error' in world.css_find(COURSE_START_DATE_CSS).first._element.get_attribute('class'))
+ assert_true('error' in world.css_find(COURSE_START_TIME_CSS).first._element.get_attribute('class'))
@step('The previously set start date is shown on refresh$')
@@ -124,9 +126,9 @@ def test_i_have_entered_a_new_course_start_date(step):
@step('The warning about course start date goes away$')
def test_the_warning_about_course_start_date_goes_away(step):
- assert_equal(0, len(css_find('.message-error')))
- assert_false('error' in css_find(COURSE_START_DATE_CSS).first._element.get_attribute('class'))
- assert_false('error' in css_find(COURSE_START_TIME_CSS).first._element.get_attribute('class'))
+ assert_equal(0, len(world.css_find('.message-error')))
+ assert_false('error' in world.css_find(COURSE_START_DATE_CSS).first._element.get_attribute('class'))
+ assert_false('error' in world.css_find(COURSE_START_TIME_CSS).first._element.get_attribute('class'))
@step('My new course start date is shown on refresh$')
@@ -142,8 +144,8 @@ def set_date_or_time(css, date_or_time):
"""
Sets date or time field.
"""
- css_fill(css, date_or_time)
- e = css_find(css).first
+ world.css_fill(css, date_or_time)
+ e = world.css_find(css).first
# hit Enter to apply the changes
e._element.send_keys(Keys.ENTER)
@@ -152,7 +154,7 @@ def verify_date_or_time(css, date_or_time):
"""
Verifies date or time field.
"""
- assert_equal(date_or_time, css_find(css).first.value)
+ assert_equal(date_or_time, world.css_find(css).first.value)
def pause():
diff --git a/cms/djangoapps/contentstore/features/courses.feature b/cms/djangoapps/contentstore/features/courses.feature
index 39d39b50aa..455313b0e2 100644
--- a/cms/djangoapps/contentstore/features/courses.feature
+++ b/cms/djangoapps/contentstore/features/courses.feature
@@ -10,4 +10,4 @@ Feature: Create Course
And I fill in the new course information
And I press the "Save" button
Then the Courseware page has loaded in Studio
- And I see a link for adding a new section
\ No newline at end of file
+ And I see a link for adding a new section
diff --git a/cms/djangoapps/contentstore/features/courses.py b/cms/djangoapps/contentstore/features/courses.py
index e394165f08..5da7720945 100644
--- a/cms/djangoapps/contentstore/features/courses.py
+++ b/cms/djangoapps/contentstore/features/courses.py
@@ -1,3 +1,6 @@
+#pylint: disable=C0111
+#pylint: disable=W0621
+
from lettuce import world, step
from common import *
@@ -6,12 +9,12 @@ from common import *
@step('There are no courses$')
def no_courses(step):
- clear_courses()
+ world.clear_courses()
@step('I click the New Course button$')
def i_click_new_course(step):
- css_click('.new-course-button')
+ world.css_click('.new-course-button')
@step('I fill in the new course information$')
@@ -27,7 +30,7 @@ def i_create_a_course(step):
@step('I click the course link in My Courses$')
def i_click_the_course_link_in_my_courses(step):
course_css = 'span.class-name'
- css_click(course_css)
+ world.css_click(course_css)
############ ASSERTIONS ###################
@@ -35,28 +38,28 @@ def i_click_the_course_link_in_my_courses(step):
@step('the Courseware page has loaded in Studio$')
def courseware_page_has_loaded_in_studio(step):
course_title_css = 'span.course-title'
- assert world.browser.is_element_present_by_css(course_title_css)
+ assert world.is_css_present(course_title_css)
@step('I see the course listed in My Courses$')
def i_see_the_course_in_my_courses(step):
course_css = 'span.class-name'
- assert_css_with_text(course_css, 'Robot Super Course')
+ assert world.css_has_text(course_css, 'Robot Super Course')
@step('the course is loaded$')
def course_is_loaded(step):
class_css = 'a.class-name'
- assert_css_with_text(class_css, 'Robot Super Course')
+ assert world.css_has_text(course_css, 'Robot Super Cousre')
@step('I am on the "([^"]*)" tab$')
def i_am_on_tab(step, tab_name):
header_css = 'div.inner-wrapper h1'
- assert_css_with_text(header_css, tab_name)
+ assert world.css_has_text(header_css, tab_name)
@step('I see a link for adding a new section$')
def i_see_new_section_link(step):
link_css = 'a.new-courseware-section-button'
- assert_css_with_text(link_css, '+ New Section')
+ assert world.css_has_text(link_css, '+ New Section')
diff --git a/cms/djangoapps/contentstore/features/section.py b/cms/djangoapps/contentstore/features/section.py
index b5ddb48a09..0c0f5536a0 100644
--- a/cms/djangoapps/contentstore/features/section.py
+++ b/cms/djangoapps/contentstore/features/section.py
@@ -1,3 +1,6 @@
+#pylint: disable=C0111
+#pylint: disable=W0621
+
from lettuce import world, step
from common import *
from nose.tools import assert_equal
@@ -10,7 +13,7 @@ import time
@step('I click the new section link$')
def i_click_new_section_link(step):
link_css = 'a.new-courseware-section-button'
- css_click(link_css)
+ world.css_click(link_css)
@step('I enter the section name and click save$')
@@ -31,19 +34,19 @@ def i_have_added_new_section(step):
@step('I click the Edit link for the release date$')
def i_click_the_edit_link_for_the_release_date(step):
button_css = 'div.section-published-date a.edit-button'
- css_click(button_css)
+ world.css_click(button_css)
@step('I save a new section release date$')
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')
+ world.css_fill(date_css, '12/25/2013')
# hit TAB to get to the time field
- e = css_find(date_css).first
+ e = world.css_find(date_css).first
e._element.send_keys(Keys.TAB)
- css_fill(time_css, '12:00am')
- e = css_find(time_css).first
+ world.css_fill(time_css, '12:00am')
+ e = world.css_find(time_css).first
e._element.send_keys(Keys.TAB)
time.sleep(float(1))
world.browser.click_link_by_text('Save')
@@ -64,13 +67,13 @@ def i_see_my_section_name_with_quote_on_the_courseware_page(step):
@step('I click to edit the section name$')
def i_click_to_edit_section_name(step):
- css_click('span.section-name-span')
+ world.css_click('span.section-name-span')
@step('I see the complete section name with a quote in the editor$')
def i_see_complete_section_name_with_quote_in_editor(step):
css = '.edit-section-name'
- assert world.browser.is_element_present_by_css(css, 5)
+ assert world.is_css_present(css)
assert_equal(world.browser.find_by_css(css).value, 'Section with "Quote"')
@@ -85,7 +88,7 @@ def i_see_a_release_date_for_my_section(step):
import re
css = 'span.published-status'
- assert world.browser.is_element_present_by_css(css)
+ assert world.is_css_present(css)
status_text = world.browser.find_by_css(css).text
# e.g. 11/06/2012 at 16:25
@@ -99,20 +102,20 @@ def i_see_a_release_date_for_my_section(step):
@step('I see a link to create a new subsection$')
def i_see_a_link_to_create_a_new_subsection(step):
css = 'a.new-subsection-item'
- assert world.browser.is_element_present_by_css(css)
+ assert world.is_css_present(css)
@step('the section release date picker is not visible$')
def the_section_release_date_picker_not_visible(step):
css = 'div.edit-subsection-publish-settings'
- assert False, world.browser.find_by_css(css).visible
+ assert not world.css_visible(css)
@step('the section release date is updated$')
def the_section_release_date_is_updated(step):
css = 'span.published-status'
- status_text = world.browser.find_by_css(css).text
- assert_equal(status_text,'Will Release: 12/25/2013 at 12:00am')
+ status_text = world.css_text(css)
+ assert_equal(status_text, 'Will Release: 12/25/2013 at 12:00am')
############ HELPER METHODS ###################
@@ -120,10 +123,10 @@ def the_section_release_date_is_updated(step):
def save_section_name(name):
name_css = '.new-section-name'
save_css = '.new-section-name-save'
- css_fill(name_css, name)
- css_click(save_css)
+ world.css_fill(name_css, name)
+ world.css_click(save_css)
def see_my_section_on_the_courseware_page(name):
section_css = 'span.section-name-span'
- assert_css_with_text(section_css, name)
+ assert world.css_has_text(section_css, name)
diff --git a/cms/djangoapps/contentstore/features/signup.py b/cms/djangoapps/contentstore/features/signup.py
index e8d0dd8229..6ca358183b 100644
--- a/cms/djangoapps/contentstore/features/signup.py
+++ b/cms/djangoapps/contentstore/features/signup.py
@@ -1,3 +1,6 @@
+#pylint: disable=C0111
+#pylint: disable=W0621
+
from lettuce import world, step
from common import *
@@ -17,9 +20,10 @@ def i_press_the_button_on_the_registration_form(step):
submit_css = 'form#register_form button#submit'
# Workaround for click not working on ubuntu
# for some unknown reason.
- e = css_find(submit_css)
+ e = world.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):
assert world.browser.find_by_css('div.inner-wrapper')
diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature b/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature
index 52c10e41a8..762dea6838 100644
--- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature
+++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature
@@ -1,30 +1,30 @@
Feature: Overview Toggle Section
In order to quickly view the details of a course's section or to scan the inventory of sections
- As a course author
- I want to toggle the visibility of each section's subsection details in the overview listing
+ As a course author
+ I want to toggle the visibility of each section's subsection details in the overview listing
Scenario: The default layout for the overview page is to show sections in expanded view
Given I have a course with multiple sections
- When I navigate to the course overview page
- Then I see the "Collapse All Sections" link
- And all sections are expanded
+ When I navigate to the course overview page
+ Then I see the "Collapse All Sections" link
+ And all sections are expanded
- Scenario: Expand/collapse for a course with no sections
+ Scenario: Expand /collapse for a course with no sections
Given I have a course with no sections
- When I navigate to the course overview page
- Then I do not see the "Collapse All Sections" link
+ When I navigate to the course overview page
+ Then I do not see the "Collapse All Sections" link
Scenario: Collapse link appears after creating first section of a course
Given I have a course with no sections
- When I navigate to the course overview page
- And I add a section
- Then I see the "Collapse All Sections" link
- And all sections are expanded
+ When I navigate to the course overview page
+ And I add a 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
+ And I navigate to the course overview page
When I press the "section" delete icon
And I confirm the alert
Then I see the "Collapse All Sections" link
diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py
index 060d592cfd..7f717b731c 100644
--- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py
+++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py
@@ -1,3 +1,6 @@
+#pylint: disable=C0111
+#pylint: disable=W0621
+
from lettuce import world, step
from common import *
from nose.tools import assert_true, assert_false, assert_equal
@@ -8,13 +11,13 @@ logger = getLogger(__name__)
@step(u'I have a course with no sections$')
def have_a_course(step):
- clear_courses()
+ world.clear_courses()
course = world.CourseFactory.create()
@step(u'I have a course with 1 section$')
def have_a_course_with_1_section(step):
- clear_courses()
+ world.clear_courses()
course = world.CourseFactory.create()
section = world.ItemFactory.create(parent_location=course.location)
subsection1 = world.ItemFactory.create(
@@ -25,7 +28,7 @@ def have_a_course_with_1_section(step):
@step(u'I have a course with multiple sections$')
def have_a_course_with_two_sections(step):
- clear_courses()
+ world.clear_courses()
course = world.CourseFactory.create()
section = world.ItemFactory.create(parent_location=course.location)
subsection1 = world.ItemFactory.create(
@@ -49,7 +52,7 @@ def have_a_course_with_two_sections(step):
def navigate_to_the_course_overview_page(step):
log_into_studio(is_staff=True)
course_locator = '.class-name'
- css_click(course_locator)
+ world.css_click(course_locator)
@step(u'I navigate to the courseware page of a course with multiple sections')
@@ -66,44 +69,44 @@ def i_add_a_section(step):
@step(u'I click the "([^"]*)" link$')
def i_click_the_text_span(step, text):
span_locator = '.toggle-button-sections span'
- assert_true(world.browser.is_element_present_by_css(span_locator, 5))
+ assert_true(world.browser.is_element_present_by_css(span_locator))
# first make sure that the expand/collapse text is the one you expected
assert_equal(world.browser.find_by_css(span_locator).value, text)
- css_click(span_locator)
+ world.css_click(span_locator)
@step(u'I collapse the first section$')
def i_collapse_a_section(step):
collapse_locator = 'section.courseware-section a.collapse'
- css_click(collapse_locator)
+ world.css_click(collapse_locator)
@step(u'I expand the first section$')
def i_expand_a_section(step):
expand_locator = 'section.courseware-section a.expand'
- css_click(expand_locator)
+ world.css_click(expand_locator)
@step(u'I see the "([^"]*)" link$')
def i_see_the_span_with_text(step, text):
span_locator = '.toggle-button-sections span'
- assert_true(world.browser.is_element_present_by_css(span_locator, 5))
- assert_equal(world.browser.find_by_css(span_locator).value, text)
- assert_true(world.browser.find_by_css(span_locator).visible)
+ assert_true(world.is_css_present(span_locator))
+ assert_equal(world.css_find(span_locator).value, text)
+ assert_true(world.css_visible(span_locator))
@step(u'I do not see the "([^"]*)" link$')
def i_do_not_see_the_span_with_text(step, text):
# Note that the span will exist on the page but not be visible
span_locator = '.toggle-button-sections span'
- assert_true(world.browser.is_element_present_by_css(span_locator))
- assert_false(world.browser.find_by_css(span_locator).visible)
+ assert_true(world.is_css_present(span_locator))
+ assert_false(world.css_visible(span_locator))
@step(u'all sections are expanded$')
def all_sections_are_expanded(step):
subsection_locator = 'div.subsection-list'
- subsections = world.browser.find_by_css(subsection_locator)
+ subsections = world.css_find(subsection_locator)
for s in subsections:
assert_true(s.visible)
@@ -111,6 +114,6 @@ def all_sections_are_expanded(step):
@step(u'all sections are collapsed$')
def all_sections_are_expanded(step):
subsection_locator = 'div.subsection-list'
- subsections = world.browser.find_by_css(subsection_locator)
+ subsections = world.css_find(subsection_locator)
for s in subsections:
assert_false(s.visible)
diff --git a/cms/djangoapps/contentstore/features/subsection.feature b/cms/djangoapps/contentstore/features/subsection.feature
index 1be5f4aeb9..e913c6a4bf 100644
--- a/cms/djangoapps/contentstore/features/subsection.feature
+++ b/cms/djangoapps/contentstore/features/subsection.feature
@@ -17,6 +17,14 @@ Feature: Create Subsection
And I click to edit the subsection name
Then I see the complete subsection name with a quote in the editor
+ Scenario: Assign grading type to a subsection and verify it is still shown after refresh (bug #258)
+ Given I have opened a new course section in Studio
+ And I have added a new subsection
+ And I mark it as Homework
+ Then I see it marked as Homework
+ And I reload the page
+ Then I see it marked as Homework
+
@skip-phantom
Scenario: Delete a subsection
Given I have opened a new course section in Studio
diff --git a/cms/djangoapps/contentstore/features/subsection.py b/cms/djangoapps/contentstore/features/subsection.py
index 88e1424898..4ab27fcb49 100644
--- a/cms/djangoapps/contentstore/features/subsection.py
+++ b/cms/djangoapps/contentstore/features/subsection.py
@@ -1,3 +1,6 @@
+#pylint: disable=C0111
+#pylint: disable=W0621
+
from lettuce import world, step
from common import *
from nose.tools import assert_equal
@@ -7,7 +10,7 @@ from nose.tools import assert_equal
@step('I have opened a new course section in Studio$')
def i_have_opened_a_new_course_section(step):
- clear_courses()
+ world.clear_courses()
log_into_studio()
create_a_course()
add_section()
@@ -15,8 +18,7 @@ def i_have_opened_a_new_course_section(step):
@step('I click the New Subsection link')
def i_click_the_new_subsection_link(step):
- css = 'a.new-subsection-item'
- css_click(css)
+ world.css_click('a.new-subsection-item')
@step('I enter the subsection name and click save$')
@@ -31,14 +33,14 @@ def i_save_subsection_name_with_quote(step):
@step('I click to edit the subsection name$')
def i_click_to_edit_subsection_name(step):
- css_click('span.subsection-name-value')
+ world.css_click('span.subsection-name-value')
@step('I see the complete subsection name with a quote in the editor$')
def i_see_complete_subsection_name_with_quote_in_editor(step):
css = '.subsection-display-name-input'
- assert world.browser.is_element_present_by_css(css, 5)
- assert_equal(world.browser.find_by_css(css).value, 'Subsection With "Quote"')
+ assert world.is_css_present(css)
+ assert_equal(world.css_find(css).value, 'Subsection With "Quote"')
@step('I have added a new subsection$')
@@ -46,6 +48,17 @@ def i_have_added_a_new_subsection(step):
add_subsection()
+@step('I mark it as Homework$')
+def i_mark_it_as_homework(step):
+ world.css_click('a.menu-toggle')
+ world.browser.click_link_by_text('Homework')
+
+
+@step('I see it marked as Homework$')
+def i_see_it_marked__as_homework(step):
+ assert_equal(world.css_find(".status-label").value, 'Homework')
+
+
############ ASSERTIONS ###################
@@ -70,11 +83,12 @@ def the_subsection_does_not_exist(step):
def save_subsection_name(name):
name_css = 'input.new-subsection-name-input'
save_css = 'input.new-subsection-name-save'
- css_fill(name_css, name)
- css_click(save_css)
+ world.css_fill(name_css, name)
+ world.css_click(save_css)
+
def see_subsection_name(name):
css = 'span.subsection-name'
- assert world.browser.is_element_present_by_css(css)
+ assert world.is_css_present(css)
css = 'span.subsection-name-value'
- assert_css_with_text(css, name)
+ assert world.css_has_text(css, name)
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index edb20561bc..49a609a879 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -25,7 +25,7 @@ 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.xml_importer import import_from_xml, perform_xlint
from xmodule.modulestore.inheritance import own_metadata
from xmodule.capa_module import CapaDescriptor
@@ -85,6 +85,43 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_edit_unit_full(self):
self.check_edit_unit('full')
+ def _get_draft_counts(self, item):
+ cnt = 1 if getattr(item, 'is_draft', False) else 0
+ for child in item.get_children():
+ cnt = cnt + self._get_draft_counts(child)
+
+ return cnt
+
+ def test_get_depth_with_drafts(self):
+ import_from_xml(modulestore(), 'common/test/data/', ['simple'])
+
+ course = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple',
+ 'course', '2012_Fall', None]), depth=None)
+
+ # make sure no draft items have been returned
+ num_drafts = self._get_draft_counts(course)
+ self.assertEqual(num_drafts, 0)
+
+ problem = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple',
+ 'problem', 'ps01-simple', None]))
+
+ # put into draft
+ modulestore('draft').clone_item(problem.location, problem.location)
+
+ # make sure we can query that item and verify that it is a draft
+ draft_problem = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple',
+ 'problem', 'ps01-simple', None]))
+ self.assertTrue(getattr(draft_problem,'is_draft', False))
+
+ #now requery with depth
+ course = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple',
+ 'course', '2012_Fall', None]), depth=None)
+
+ # make sure just one draft item have been returned
+ num_drafts = self._get_draft_counts(course)
+ self.assertEqual(num_drafts, 1)
+
+
def test_static_tab_reordering(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
@@ -123,6 +160,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# check that there's actually content in the 'question' field
self.assertGreater(len(items[0].question),0)
+ def test_xlint_fails(self):
+ err_cnt = perform_xlint('common/test/data', ['full'])
+ self.assertGreater(err_cnt, 0)
+
def test_delete(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
@@ -211,7 +252,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
new_loc = descriptor.location._replace(org='MITx', course='999')
print "Checking {0} should now also be at {1}".format(descriptor.location.url(), new_loc.url())
resp = self.client.get(reverse('edit_unit', kwargs={'location': new_loc.url()}))
- self.assertEqual(resp.status_code, 200)
+ self.assertEqual(resp.status_code, 200)
+
+ def test_bad_contentstore_request(self):
+ resp = self.client.get('http://localhost:8001/c4x/CDX/123123/asset/&images_circuits_Lab7Solution2.png')
+ self.assertEqual(resp.status_code, 400)
def test_delete_course(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
@@ -328,11 +373,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertEqual(wrapper.counter, 4)
# make sure we pre-fetched a known sequential which should be at depth=2
- self.assertTrue(Location(['i4x', 'edX', 'full', 'sequential',
+ self.assertTrue(Location(['i4x', 'edX', 'full', 'sequential',
'Administrivia_and_Circuit_Elements', None]) in course.system.module_data)
# make sure we don't have a specific vertical which should be at depth=3
- self.assertFalse(Location(['i4x', 'edX', 'full', 'vertical', 'vertical_58',
+ self.assertFalse(Location(['i4x', 'edX', 'full', 'vertical', 'vertical_58',
None]) in course.system.module_data)
def test_export_course_with_unknown_metadata(self):
@@ -556,7 +601,7 @@ class ContentStoreTest(ModuleStoreTestCase):
module_store.update_children(parent.location, parent.children + [new_component_location.url()])
# flush the cache
- module_store.get_cached_metadata_inheritance_tree(new_component_location, -1)
+ module_store.refresh_cached_metadata_inheritance_tree(new_component_location)
new_module = module_store.get_item(new_component_location)
# check for grace period definition which should be defined at the course level
@@ -571,7 +616,7 @@ class ContentStoreTest(ModuleStoreTestCase):
module_store.update_metadata(new_module.location, own_metadata(new_module))
# flush the cache and refetch
- module_store.get_cached_metadata_inheritance_tree(new_component_location, -1)
+ module_store.refresh_cached_metadata_inheritance_tree(new_component_location)
new_module = module_store.get_item(new_component_location)
self.assertEqual(timedelta(1), new_module.lms.graceperiod)
diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py
index 2e7bc5db83..fe90ad18aa 100644
--- a/cms/djangoapps/contentstore/tests/test_course_settings.py
+++ b/cms/djangoapps/contentstore/tests/test_course_settings.py
@@ -1,8 +1,6 @@
import datetime
import json
import copy
-from util import converters
-from util.converters import jsdate_to_time
from django.contrib.auth.models import User
from django.test.client import Client
@@ -15,69 +13,13 @@ from models.settings.course_details import (CourseDetails,
from models.settings.course_grading import CourseGradingModel
from contentstore.utils import get_modulestore
-from django.test import TestCase
from .utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from models.settings.course_metadata import CourseMetadata
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.django import modulestore
-import time
-
-
-# YYYY-MM-DDThh:mm:ss.s+/-HH:MM
-class ConvertersTestCase(TestCase):
- @staticmethod
- def struct_to_datetime(struct_time):
- return datetime.datetime(struct_time.tm_year, struct_time.tm_mon,
- struct_time.tm_mday, struct_time.tm_hour,
- struct_time.tm_min, struct_time.tm_sec, tzinfo=UTC())
-
- def compare_dates(self, date1, date2, expected_delta):
- dt1 = ConvertersTestCase.struct_to_datetime(date1)
- dt2 = ConvertersTestCase.struct_to_datetime(date2)
- self.assertEqual(dt1 - dt2, expected_delta, str(date1) + "-"
- + str(date2) + "!=" + str(expected_delta))
-
- def test_iso_to_struct(self):
- '''Test conversion from iso compatible date strings to struct_time'''
- self.compare_dates(converters.jsdate_to_time("2013-01-01"),
- converters.jsdate_to_time("2012-12-31"),
- datetime.timedelta(days=1))
- self.compare_dates(converters.jsdate_to_time("2013-01-01T00"),
- converters.jsdate_to_time("2012-12-31T23"),
- datetime.timedelta(hours=1))
- self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00"),
- converters.jsdate_to_time("2012-12-31T23:59"),
- datetime.timedelta(minutes=1))
- self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00:00"),
- converters.jsdate_to_time("2012-12-31T23:59:59"),
- datetime.timedelta(seconds=1))
- self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00:00Z"),
- converters.jsdate_to_time("2012-12-31T23:59:59Z"),
- datetime.timedelta(seconds=1))
- self.compare_dates(
- converters.jsdate_to_time("2012-12-31T23:00:01-01:00"),
- converters.jsdate_to_time("2013-01-01T00:00:00+01:00"),
- datetime.timedelta(hours=1, seconds=1))
-
- def test_struct_to_iso(self):
- '''
- Test converting time reprs to iso dates
- '''
- self.assertEqual(
- converters.time_to_isodate(
- time.strptime("2012-12-31T23:59:59Z", "%Y-%m-%dT%H:%M:%SZ")),
- "2012-12-31T23:59:59Z")
- self.assertEqual(
- converters.time_to_isodate(
- jsdate_to_time("2012-12-31T23:59:59Z")),
- "2012-12-31T23:59:59Z")
- self.assertEqual(
- converters.time_to_isodate(
- jsdate_to_time("2012-12-31T23:00:01-01:00")),
- "2013-01-01T00:00:01Z")
-
+from xmodule.fields import Date
class CourseTestCase(ModuleStoreTestCase):
def setUp(self):
@@ -206,17 +148,24 @@ class CourseDetailsViewTest(CourseTestCase):
self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==")
self.assertEqual(details['effort'], encoded['effort'], context + " efforts not ==")
+ @staticmethod
+ def struct_to_datetime(struct_time):
+ return datetime.datetime(struct_time.tm_year, struct_time.tm_mon,
+ struct_time.tm_mday, struct_time.tm_hour,
+ struct_time.tm_min, struct_time.tm_sec, tzinfo=UTC())
+
def compare_date_fields(self, details, encoded, context, field):
if details[field] is not None:
+ date = Date()
if field in encoded and encoded[field] is not None:
- encoded_encoded = jsdate_to_time(encoded[field])
- dt1 = ConvertersTestCase.struct_to_datetime(encoded_encoded)
+ encoded_encoded = date.from_json(encoded[field])
+ dt1 = CourseDetailsViewTest.struct_to_datetime(encoded_encoded)
if isinstance(details[field], datetime.datetime):
dt2 = details[field]
else:
- details_encoded = jsdate_to_time(details[field])
- dt2 = ConvertersTestCase.struct_to_datetime(details_encoded)
+ details_encoded = date.from_json(details[field])
+ dt2 = CourseDetailsViewTest.struct_to_datetime(details_encoded)
expected_delta = datetime.timedelta(0)
self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "!=" + str(dt2) + " at " + context)
diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py
index b6b8cd5023..bb7ac2bf06 100644
--- a/cms/djangoapps/contentstore/tests/utils.py
+++ b/cms/djangoapps/contentstore/tests/utils.py
@@ -1,3 +1,9 @@
+'''
+Utilities for contentstore tests
+'''
+
+#pylint: disable=W0603
+
import json
import copy
from uuid import uuid4
@@ -17,36 +23,89 @@ class ModuleStoreTestCase(TestCase):
collection with templates before running the TestCase
and drops it they are finished. """
- def _pre_setup(self):
- super(ModuleStoreTestCase, self)._pre_setup()
+ @staticmethod
+ def flush_mongo_except_templates():
+ '''
+ Delete everything in the module store except templates
+ '''
+ modulestore = xmodule.modulestore.django.modulestore()
+
+ # This query means: every item in the collection
+ # that is not a template
+ query = {"_id.course": {"$ne": "templates"}}
+
+ # Remove everything except templates
+ modulestore.collection.remove(query)
+
+ @staticmethod
+ def load_templates_if_necessary():
+ '''
+ Load templates into the modulestore only if they do not already exist.
+ We need the templates, because they are copied to create
+ XModules such as sections and problems
+ '''
+ modulestore = xmodule.modulestore.django.modulestore()
+
+ # Count the number of templates
+ query = {"_id.course": "templates"}
+ num_templates = modulestore.collection.find(query).count()
+
+ if num_templates < 1:
+ update_templates()
+
+ @classmethod
+ def setUpClass(cls):
+ '''
+ Flush the mongo store and set up templates
+ '''
# Use a uuid to differentiate
# the mongo collections on jenkins.
- self.orig_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
- self.test_MODULESTORE = self.orig_MODULESTORE
- self.test_MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
- self.test_MODULESTORE['direct']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
- settings.MODULESTORE = self.test_MODULESTORE
-
- # Flush and initialize the module store
- # It needs the templates because it creates new records
- # by cloning from the template.
- # Note that if your test module gets in some weird state
- # (though it shouldn't), do this manually
- # from the bash shell to drop it:
- # $ mongo test_xmodule --eval "db.dropDatabase()"
+ cls.orig_modulestore = copy.deepcopy(settings.MODULESTORE)
+ test_modulestore = cls.orig_modulestore
+ test_modulestore['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
+ test_modulestore['direct']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
xmodule.modulestore.django._MODULESTORES = {}
- update_templates()
+
+ settings.MODULESTORE = test_modulestore
+
+ TestCase.setUpClass()
+
+ @classmethod
+ def tearDownClass(cls):
+ '''
+ Revert to the old modulestore settings
+ '''
+
+ # Clean up by dropping the collection
+ modulestore = xmodule.modulestore.django.modulestore()
+ modulestore.collection.drop()
+
+ # Restore the original modulestore settings
+ settings.MODULESTORE = cls.orig_modulestore
+
+ def _pre_setup(self):
+ '''
+ Remove everything but the templates before each test
+ '''
+
+ # Flush anything that is not a template
+ ModuleStoreTestCase.flush_mongo_except_templates()
+
+ # Check that we have templates loaded; if not, load them
+ ModuleStoreTestCase.load_templates_if_necessary()
+
+ # Call superclass implementation
+ super(ModuleStoreTestCase, self)._pre_setup()
def _post_teardown(self):
- # Make sure you flush out the modulestore.
- # Drop the collection at the end of the test,
- # otherwise there will be lingering collections leftover
- # from executing the tests.
- xmodule.modulestore.django._MODULESTORES = {}
- xmodule.modulestore.django.modulestore().collection.drop()
- settings.MODULESTORE = self.orig_MODULESTORE
+ '''
+ Flush everything we created except the templates
+ '''
+ # Flush anything that is not a template
+ ModuleStoreTestCase.flush_mongo_except_templates()
+ # Call superclass implementation
super(ModuleStoreTestCase, self)._post_teardown()
diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py
index 63dfe5bf5f..d38918d6b0 100644
--- a/cms/djangoapps/contentstore/utils.py
+++ b/cms/djangoapps/contentstore/utils.py
@@ -1,11 +1,15 @@
+import logging
from django.conf import settings
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
from django.core.urlresolvers import reverse
+import copy
DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info']
+#In order to instantiate an open ended tab automatically, need to have this data
+OPEN_ENDED_PANEL = {"name" : "Open Ended Panel", "type" : "open_ended"}
def get_modulestore(location):
"""
@@ -137,7 +141,7 @@ def compute_unit_state(unit):
'private' content is editabled and not visible in the LMS
"""
- if unit.cms.is_draft:
+ if getattr(unit, 'is_draft', False):
try:
modulestore('direct').get_item(unit.location)
return UnitState.draft
@@ -191,3 +195,35 @@ class CoursePageNames:
SettingsGrading = "settings_grading"
CourseOutline = "course_index"
Checklists = "checklists"
+
+def add_open_ended_panel_tab(course):
+ """
+ Used to add the open ended panel tab to a course if it does not exist.
+ @param course: A course object from the modulestore.
+ @return: Boolean indicating whether or not a tab was added and a list of tabs for the course.
+ """
+ #Copy course tabs
+ course_tabs = copy.copy(course.tabs)
+ changed = False
+ #Check to see if open ended panel is defined in the course
+ if OPEN_ENDED_PANEL not in course_tabs:
+ #Add panel to the tabs if it is not defined
+ course_tabs.append(OPEN_ENDED_PANEL)
+ changed = True
+ return changed, course_tabs
+
+def remove_open_ended_panel_tab(course):
+ """
+ Used to remove the open ended panel tab from a course if it exists.
+ @param course: A course object from the modulestore.
+ @return: Boolean indicating whether or not a tab was added and a list of tabs for the course.
+ """
+ #Copy course tabs
+ course_tabs = copy.copy(course.tabs)
+ changed = False
+ #Check to see if open ended panel is defined in the course
+ if OPEN_ENDED_PANEL in course_tabs:
+ #Add panel to the tabs if it is not defined
+ course_tabs = [ct for ct in course_tabs if ct!=OPEN_ENDED_PANEL]
+ changed = True
+ return changed, course_tabs
diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index 561708c833..9681f54350 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -42,7 +42,7 @@ from xmodule.modulestore.mongo import MongoUsage
from mitxmako.shortcuts import render_to_response, render_to_string
from xmodule.modulestore.django import modulestore
from xmodule_modifiers import replace_static_urls, wrap_xmodule
-from xmodule.exceptions import NotFoundError
+from xmodule.exceptions import NotFoundError, ProcessingError
from functools import partial
from xmodule.contentstore.django import contentstore
@@ -52,7 +52,8 @@ from auth.authz import is_user_in_course_group_role, get_users_in_course_group_b
from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups
from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, \
- get_date_display, UnitState, get_course_for_item, get_url_reverse
+ get_date_display, UnitState, get_course_for_item, get_url_reverse, add_open_ended_panel_tab, \
+ remove_open_ended_panel_tab
from xmodule.modulestore.xml_importer import import_from_xml
from contentstore.course_info_model import get_course_updates, \
@@ -73,7 +74,8 @@ log = logging.getLogger(__name__)
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
-ADVANCED_COMPONENT_TYPES = ['annotatable', 'combinedopenended', 'peergrading']
+OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
+ADVANCED_COMPONENT_TYPES = ['annotatable'] + OPEN_ENDED_COMPONENT_TYPES
ADVANCED_COMPONENT_CATEGORY = 'advanced'
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
@@ -188,7 +190,7 @@ def course_index(request, org, course, name):
'coursename': name
})
- course = modulestore().get_item(location)
+ course = modulestore().get_item(location, depth=3)
sections = course.get_children()
return render_to_response('overview.html', {
@@ -208,19 +210,14 @@ def course_index(request, org, course, name):
@login_required
def edit_subsection(request, location):
# check that we have permissions to edit this item
- if not has_access(request.user, location):
+ course = get_course_for_item(location)
+ if not has_access(request.user, course.location):
raise PermissionDenied()
- item = modulestore().get_item(location)
+ item = modulestore().get_item(location, depth=1)
- # TODO: we need a smarter way to figure out what course an item is in
- for course in modulestore().get_courses():
- if (course.location.org == item.location.org and
- course.location.course == item.location.course):
- break
-
- lms_link = get_lms_link_for_item(location)
- preview_link = get_lms_link_for_item(location, preview=True)
+ lms_link = get_lms_link_for_item(location, course_id=course.location.course_id)
+ preview_link = get_lms_link_for_item(location, course_id=course.location.course_id, preview=True)
# make sure that location references a 'sequential', otherwise return BadRequest
if item.location.category != 'sequential':
@@ -277,19 +274,13 @@ def edit_unit(request, location):
id: A Location URL
"""
- # check that we have permissions to edit this item
- if not has_access(request.user, location):
+ course = get_course_for_item(location)
+ if not has_access(request.user, course.location):
raise PermissionDenied()
- item = modulestore().get_item(location)
+ item = modulestore().get_item(location, depth=1)
- # TODO: we need a smarter way to figure out what course an item is in
- for course in modulestore().get_courses():
- if (course.location.org == item.location.org and
- course.location.course == item.location.course):
- break
-
- lms_link = get_lms_link_for_item(item.location)
+ lms_link = get_lms_link_for_item(item.location, course_id=course.location.course_id)
component_templates = defaultdict(list)
@@ -448,9 +439,16 @@ def preview_dispatch(request, preview_id, location, dispatch=None):
# Let the module handle the AJAX
try:
ajax_return = instance.handle_ajax(dispatch, request.POST)
+
except NotFoundError:
log.exception("Module indicating to user that request doesn't exist")
raise Http404
+
+ except ProcessingError:
+ log.warning("Module raised an error while processing AJAX request",
+ exc_info=True)
+ return HttpResponseBadRequest()
+
except:
log.exception("error processing ajax call")
raise
@@ -1273,15 +1271,48 @@ def course_advanced_updates(request, org, course, name):
location = get_location_and_verify_access(request, org, course, name)
real_method = get_request_method(request)
-
+
if real_method == 'GET':
return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json")
elif real_method == 'DELETE':
- return HttpResponse(json.dumps(CourseMetadata.delete_key(location, json.loads(request.body))), mimetype="application/json")
+ return HttpResponse(json.dumps(CourseMetadata.delete_key(location, json.loads(request.body))),
+ mimetype="application/json")
elif real_method == 'POST' or real_method == 'PUT':
# NOTE: request.POST is messed up because expect_json cloned_request.POST.copy() is creating a defective entry w/ the whole payload as the key
- return HttpResponse(json.dumps(CourseMetadata.update_from_json(location, json.loads(request.body))), mimetype="application/json")
-
+ request_body = json.loads(request.body)
+ #Whether or not to filter the tabs key out of the settings metadata
+ filter_tabs = True
+ #Check to see if the user instantiated any advanced components. This is a hack to add the open ended panel tab
+ #to a course automatically if the user has indicated that they want to edit the combinedopenended or peergrading
+ #module, and to remove it if they have removed the open ended elements.
+ if ADVANCED_COMPONENT_POLICY_KEY in request_body:
+ #Check to see if the user instantiated any open ended components
+ found_oe_type = False
+ #Get the course so that we can scrape current tabs
+ course_module = modulestore().get_item(location)
+ for oe_type in OPEN_ENDED_COMPONENT_TYPES:
+ if oe_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]:
+ #Add an open ended tab to the course if needed
+ changed, new_tabs = add_open_ended_panel_tab(course_module)
+ #If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json
+ if changed:
+ request_body.update({'tabs': new_tabs})
+ #Indicate that tabs should not be filtered out of the metadata
+ filter_tabs = False
+ #Set this flag to avoid the open ended tab removal code below.
+ found_oe_type = True
+ break
+ #If we did not find an open ended module type in the advanced settings,
+ # we may need to remove the open ended tab from the course.
+ if not found_oe_type:
+ #Remove open ended tab to the course if needed
+ changed, new_tabs = remove_open_ended_panel_tab(course_module)
+ if changed:
+ request_body.update({'tabs': new_tabs})
+ #Indicate that tabs should not be filtered out of the metadata
+ filter_tabs = False
+ response_json = json.dumps(CourseMetadata.update_from_json(location, request_body, filter_tabs=filter_tabs))
+ return HttpResponse(response_json, mimetype="application/json")
@ensure_csrf_cookie
@login_required
diff --git a/cms/djangoapps/models/settings/course_details.py b/cms/djangoapps/models/settings/course_details.py
index d3cd5fe164..876000c7fe 100644
--- a/cms/djangoapps/models/settings/course_details.py
+++ b/cms/djangoapps/models/settings/course_details.py
@@ -1,4 +1,3 @@
-from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.inheritance import own_metadata
@@ -6,9 +5,9 @@ import json
from json.encoder import JSONEncoder
import time
from contentstore.utils import get_modulestore
-from util.converters import jsdate_to_time, time_to_date
from models.settings import course_grading
from contentstore.utils import update_item
+from xmodule.fields import Date
import re
import logging
@@ -81,8 +80,14 @@ class CourseDetails(object):
dirty = False
+ # In the descriptor's setter, the date is converted to JSON using Date's to_json method.
+ # Calling to_json on something that is already JSON doesn't work. Since reaching directly
+ # into the model is nasty, convert the JSON Date to a Python date, which is what the
+ # setter expects as input.
+ date = Date()
+
if 'start_date' in jsondict:
- converted = jsdate_to_time(jsondict['start_date'])
+ converted = date.from_json(jsondict['start_date'])
else:
converted = None
if converted != descriptor.start:
@@ -90,7 +95,7 @@ class CourseDetails(object):
descriptor.start = converted
if 'end_date' in jsondict:
- converted = jsdate_to_time(jsondict['end_date'])
+ converted = date.from_json(jsondict['end_date'])
else:
converted = None
@@ -99,7 +104,7 @@ class CourseDetails(object):
descriptor.end = converted
if 'enrollment_start' in jsondict:
- converted = jsdate_to_time(jsondict['enrollment_start'])
+ converted = date.from_json(jsondict['enrollment_start'])
else:
converted = None
@@ -108,7 +113,7 @@ class CourseDetails(object):
descriptor.enrollment_start = converted
if 'enrollment_end' in jsondict:
- converted = jsdate_to_time(jsondict['enrollment_end'])
+ converted = date.from_json(jsondict['enrollment_end'])
else:
converted = None
@@ -178,6 +183,6 @@ class CourseSettingsEncoder(json.JSONEncoder):
elif isinstance(obj, Location):
return obj.dict()
elif isinstance(obj, time.struct_time):
- return time_to_date(obj)
+ return Date().to_json(obj)
else:
return JSONEncoder.default(self, obj)
diff --git a/cms/djangoapps/models/settings/course_grading.py b/cms/djangoapps/models/settings/course_grading.py
index b20fb71f66..ee9b4ac0eb 100644
--- a/cms/djangoapps/models/settings/course_grading.py
+++ b/cms/djangoapps/models/settings/course_grading.py
@@ -1,7 +1,5 @@
from xmodule.modulestore import Location
from contentstore.utils import get_modulestore
-import re
-from util import converters
from datetime import timedelta
diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py
index 563dd16524..70f69315ff 100644
--- a/cms/djangoapps/models/settings/course_metadata.py
+++ b/cms/djangoapps/models/settings/course_metadata.py
@@ -4,7 +4,7 @@ from xmodule.x_module import XModuleDescriptor
from xmodule.modulestore.inheritance import own_metadata
from xblock.core import Scope
from xmodule.course_module import CourseDescriptor
-
+import copy
class CourseMetadata(object):
'''
@@ -39,7 +39,7 @@ class CourseMetadata(object):
return course
@classmethod
- def update_from_json(cls, course_location, jsondict):
+ def update_from_json(cls, course_location, jsondict, filter_tabs=True):
"""
Decode the json into CourseMetadata and save any changed attrs to the db.
@@ -48,10 +48,16 @@ class CourseMetadata(object):
descriptor = get_modulestore(course_location).get_item(course_location)
dirty = False
+
+ #Copy the filtered list to avoid permanently changing the class attribute
+ filtered_list = copy.copy(cls.FILTERED_LIST)
+ #Don't filter on the tab attribute if filter_tabs is False
+ if not filter_tabs:
+ filtered_list.remove("tabs")
for k, v in jsondict.iteritems():
# should it be an error if one of the filtered list items is in the payload?
- if k in cls.FILTERED_LIST:
+ if k in filtered_list:
continue
if hasattr(descriptor, k) and getattr(descriptor, k) != v:
diff --git a/cms/envs/common.py b/cms/envs/common.py
index a83f61d8f9..12fa09947a 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -113,6 +113,7 @@ TEMPLATE_LOADERS = (
MIDDLEWARE_CLASSES = (
'contentserver.middleware.StaticContentServer',
+ 'request_cache.middleware.RequestCache',
'django.middleware.cache.UpdateCacheMiddleware',
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
diff --git a/cms/envs/dev.py b/cms/envs/dev.py
index 5612db1396..c4465a0e06 100644
--- a/cms/envs/dev.py
+++ b/cms/envs/dev.py
@@ -112,6 +112,10 @@ CACHE_TIMEOUT = 0
# Dummy secret key for dev
SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
+################################ PIPELINE #################################
+
+PIPELINE_SASS_ARGUMENTS = '--debug-info --require {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT)
+
################################ DEBUG TOOLBAR #################################
INSTALLED_APPS += ('debug_toolbar', 'debug_toolbar_mongo')
MIDDLEWARE_CLASSES += ('django_comment_client.utils.QueryCountDebugMiddleware',
@@ -142,4 +146,4 @@ DEBUG_TOOLBAR_CONFIG = {
# To see stacktraces for MongoDB queries, set this to True.
# Stacktraces slow down page loads drastically (for pages with lots of queries).
-DEBUG_TOOLBAR_MONGO_STACKTRACES = False
+DEBUG_TOOLBAR_MONGO_STACKTRACES = True
diff --git a/cms/envs/test.py b/cms/envs/test.py
index d7992cb471..59664bfd40 100644
--- a/cms/envs/test.py
+++ b/cms/envs/test.py
@@ -58,6 +58,10 @@ MODULESTORE = {
'direct': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': modulestore_options
+ },
+ 'draft': {
+ 'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
+ 'OPTIONS': modulestore_options
}
}
diff --git a/cms/one_time_startup.py b/cms/one_time_startup.py
index 38a2fef847..6e88fed439 100644
--- a/cms/one_time_startup.py
+++ b/cms/one_time_startup.py
@@ -1,13 +1,15 @@
from dogapi import dog_http_api, dog_stats_api
from django.conf import settings
from xmodule.modulestore.django import modulestore
+from request_cache.middleware import RequestCache
from django.core.cache import get_cache, InvalidCacheBackendError
cache = get_cache('mongo_metadata_inheritance')
for store_name in settings.MODULESTORE:
store = modulestore(store_name)
- store.metadata_inheritance_cache = cache
+ store.metadata_inheritance_cache_subsystem = cache
+ store.request_cache = RequestCache.get_request_cache()
if hasattr(settings, 'DATADOG_API'):
dog_http_api.api_key = settings.DATADOG_API
diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss
index 10c046d22a..28415b8e8a 100644
--- a/cms/static/sass/_base.scss
+++ b/cms/static/sass/_base.scss
@@ -660,7 +660,7 @@ hr.divide {
position: absolute;
top: 0;
left: 0;
- z-index: 99999;
+ z-index: 10000;
padding: 0 10px;
border-radius: 3px;
background: rgba(0, 0, 0, 0.85);
diff --git a/cms/static/sass/elements/_jquery-ui-calendar.scss b/cms/static/sass/elements/_jquery-ui-calendar.scss
index 3d20bde642..d7d7f093e5 100644
--- a/cms/static/sass/elements/_jquery-ui-calendar.scss
+++ b/cms/static/sass/elements/_jquery-ui-calendar.scss
@@ -8,6 +8,7 @@
font-family: $sans-serif;
font-size: 12px;
@include box-shadow(0 5px 10px rgba(0, 0, 0, 0.1));
+ z-index: 100000 !important;
.ui-widget-header {
background: $darkGrey;
diff --git a/cms/templates/overview.html b/cms/templates/overview.html
index 904f654717..d45a90093e 100644
--- a/cms/templates/overview.html
+++ b/cms/templates/overview.html
@@ -200,7 +200,7 @@
-
+
diff --git a/cms/xmodule_namespace.py b/cms/xmodule_namespace.py
index cad3110574..c9bb8f4c6e 100644
--- a/cms/xmodule_namespace.py
+++ b/cms/xmodule_namespace.py
@@ -40,7 +40,6 @@ class CmsNamespace(Namespace):
"""
Namespace with fields common to all blocks in Studio
"""
- is_draft = Boolean(help="Whether this module is a draft", default=False, scope=Scope.settings)
published_date = DateTuple(help="Date when the module was published", scope=Scope.settings)
published_by = String(help="Id of the user who published this module", scope=Scope.settings)
empty = StringyBoolean(help="Whether this is an empty template", scope=Scope.settings, default=False)
diff --git a/common/djangoapps/contentserver/middleware.py b/common/djangoapps/contentserver/middleware.py
index c5e887801e..8e9e70046d 100644
--- a/common/djangoapps/contentserver/middleware.py
+++ b/common/djangoapps/contentserver/middleware.py
@@ -5,6 +5,7 @@ from django.http import HttpResponse, Http404, HttpResponseNotModified
from xmodule.contentstore.django import contentstore
from xmodule.contentstore.content import StaticContent, XASSET_LOCATION_TAG
+from xmodule.modulestore import InvalidLocationError
from cache_toolbox.core import get_cached_content, set_cached_content
from xmodule.exceptions import NotFoundError
@@ -13,7 +14,14 @@ class StaticContentServer(object):
def process_request(self, request):
# look to see if the request is prefixed with 'c4x' tag
if request.path.startswith('/' + XASSET_LOCATION_TAG + '/'):
- loc = StaticContent.get_location_from_path(request.path)
+ try:
+ loc = StaticContent.get_location_from_path(request.path)
+ except InvalidLocationError:
+ # return a 'Bad Request' to browser as we have a malformed Location
+ response = HttpResponse()
+ response.status_code = 400
+ return response
+
# first look in our cache so we don't have to round-trip to the DB
content = get_cached_content(loc)
if content is None:
diff --git a/common/djangoapps/request_cache/__init__.py b/common/djangoapps/request_cache/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/common/djangoapps/request_cache/middleware.py b/common/djangoapps/request_cache/middleware.py
new file mode 100644
index 0000000000..9d3dffdf27
--- /dev/null
+++ b/common/djangoapps/request_cache/middleware.py
@@ -0,0 +1,20 @@
+import threading
+
+_request_cache_threadlocal = threading.local()
+_request_cache_threadlocal.data = {}
+
+class RequestCache(object):
+ @classmethod
+ def get_request_cache(cls):
+ return _request_cache_threadlocal
+
+ def clear_request_cache(self):
+ _request_cache_threadlocal.data = {}
+
+ def process_request(self, request):
+ self.clear_request_cache()
+ return None
+
+ def process_response(self, request, response):
+ self.clear_request_cache()
+ return response
\ No newline at end of file
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index 5dbaf5d2c2..8267816e2c 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -325,7 +325,12 @@ def change_enrollment(request):
"course:{0}".format(course_num),
"run:{0}".format(run)])
- enrollment, created = CourseEnrollment.objects.get_or_create(user=user, course_id=course.id)
+ try:
+ enrollment, created = CourseEnrollment.objects.get_or_create(user=user, course_id=course.id)
+ except IntegrityError:
+ # If we've already created this enrollment in a separate transaction,
+ # then just continue
+ pass
return {'success': True}
elif action == "unenroll":
@@ -369,14 +374,14 @@ def login_user(request, error=""):
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
- log.warning("Login failed - Unknown user email: {0}".format(email))
+ log.warning(u"Login failed - Unknown user email: {0}".format(email))
return HttpResponse(json.dumps({'success': False,
'value': 'Email or password is incorrect.'})) # TODO: User error message
username = user.username
user = authenticate(username=username, password=password)
if user is None:
- log.warning("Login failed - password for {0} is invalid".format(email))
+ log.warning(u"Login failed - password for {0} is invalid".format(email))
return HttpResponse(json.dumps({'success': False,
'value': 'Email or password is incorrect.'}))
@@ -392,7 +397,7 @@ def login_user(request, error=""):
log.critical("Login failed - Could not create session. Is memcached running?")
log.exception(e)
- log.info("Login success - {0} ({1})".format(username, email))
+ log.info(u"Login success - {0} ({1})".format(username, email))
try_change_enrollment(request)
@@ -400,7 +405,7 @@ def login_user(request, error=""):
return HttpResponse(json.dumps({'success': True}))
- log.warning("Login failed - Account not active for user {0}, resending activation".format(username))
+ log.warning(u"Login failed - Account not active for user {0}, resending activation".format(username))
reactivation_email_for_user(user)
not_activated_msg = "This account has not been activated. We have " + \
diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py
new file mode 100644
index 0000000000..f0df456c80
--- /dev/null
+++ b/common/djangoapps/terrain/course_helpers.py
@@ -0,0 +1,140 @@
+#pylint: disable=C0111
+#pylint: disable=W0621
+
+from lettuce import world, step
+from .factories import *
+from django.conf import settings
+from django.http import HttpRequest
+from django.contrib.auth.models import User
+from django.contrib.auth import authenticate, login
+from django.contrib.auth.middleware import AuthenticationMiddleware
+from django.contrib.sessions.middleware import SessionMiddleware
+from student.models import CourseEnrollment
+from xmodule.modulestore.django import _MODULESTORES, modulestore
+from xmodule.templates import update_templates
+from bs4 import BeautifulSoup
+import os.path
+from urllib import quote_plus
+from lettuce.django import django_url
+
+
+@world.absorb
+def create_user(uname):
+
+ # If the user already exists, don't try to create it again
+ if len(User.objects.filter(username=uname)) > 0:
+ return
+
+ portal_user = UserFactory.build(username=uname, email=uname + '@edx.org')
+ portal_user.set_password('test')
+ portal_user.save()
+
+ registration = world.RegistrationFactory(user=portal_user)
+ registration.register(portal_user)
+ registration.activate()
+
+ user_profile = world.UserProfileFactory(user=portal_user)
+
+
+@world.absorb
+def log_in(username, password):
+ '''
+ Log the user in programatically
+ '''
+
+ # Authenticate the user
+ user = authenticate(username=username, password=password)
+ assert(user is not None and user.is_active)
+
+ # Send a fake HttpRequest to log the user in
+ # We need to process the request using
+ # Session middleware and Authentication middleware
+ # to ensure that session state can be stored
+ request = HttpRequest()
+ SessionMiddleware().process_request(request)
+ AuthenticationMiddleware().process_request(request)
+ login(request, user)
+
+ # Save the session
+ request.session.save()
+
+ # Retrieve the sessionid and add it to the browser's cookies
+ cookie_dict = {settings.SESSION_COOKIE_NAME: request.session.session_key}
+ try:
+ world.browser.cookies.add(cookie_dict)
+
+ # WebDriver has an issue where we cannot set cookies
+ # before we make a GET request, so if we get an error,
+ # we load the '/' page and try again
+ except:
+ world.browser.visit(django_url('/'))
+ world.browser.cookies.add(cookie_dict)
+
+
+@world.absorb
+def register_by_course_id(course_id, is_staff=False):
+ create_user('robot')
+ u = User.objects.get(username='robot')
+ if is_staff:
+ u.is_staff = True
+ u.save()
+ CourseEnrollment.objects.get_or_create(user=u, course_id=course_id)
+
+
+
+@world.absorb
+def save_the_course_content(path='/tmp'):
+ html = world.browser.html.encode('ascii', 'ignore')
+ soup = BeautifulSoup(html)
+
+ # get rid of the header, we only want to compare the body
+ soup.head.decompose()
+
+ # for now, remove the data-id attributes, because they are
+ # causing mismatches between cms-master and master
+ for item in soup.find_all(attrs={'data-id': re.compile('.*')}):
+ del item['data-id']
+
+ # we also need to remove them from unrendered problems,
+ # where they are contained in the text of divs instead of
+ # in attributes of tags
+ # Be careful of whether or not it was the last attribute
+ # and needs a trailing space
+ for item in soup.find_all(text=re.compile(' data-id=".*?" ')):
+ s = unicode(item.string)
+ item.string.replace_with(re.sub(' data-id=".*?" ', ' ', s))
+
+ for item in soup.find_all(text=re.compile(' data-id=".*?"')):
+ s = unicode(item.string)
+ item.string.replace_with(re.sub(' data-id=".*?"', ' ', s))
+
+ # prettify the html so it will compare better, with
+ # each HTML tag on its own line
+ output = soup.prettify()
+
+ # use string slicing to grab everything after 'courseware/' in the URL
+ u = world.browser.url
+ section_url = u[u.find('courseware/') + 11:]
+
+
+ if not os.path.exists(path):
+ os.makedirs(path)
+
+ filename = '%s.html' % (quote_plus(section_url))
+ f = open('%s/%s' % (path, filename), 'w')
+ f.write(output)
+ f.close
+
+
+@world.absorb
+def clear_courses():
+ # Flush and initialize the module store
+ # It needs the templates because it creates new records
+ # by cloning from the template.
+ # Note that if your test module gets in some weird state
+ # (though it shouldn't), do this manually
+ # from the bash shell to drop it:
+ # $ mongo test_xmodule --eval "db.dropDatabase()"
+ _MODULESTORES = {}
+ modulestore().collection.drop()
+ update_templates()
diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py
index 3bc838a6af..a8a32db173 100644
--- a/common/djangoapps/terrain/steps.py
+++ b/common/djangoapps/terrain/steps.py
@@ -1,20 +1,12 @@
+#pylint: disable=C0111
+#pylint: disable=W0621
+
from lettuce import world, step
-from .factories import *
+from .course_helpers import *
+from .ui_helpers import *
from lettuce.django import django_url
-from django.conf import settings
-from django.http import HttpRequest
-from django.contrib.auth.models import User
-from django.contrib.auth import authenticate, login
-from django.contrib.auth.middleware import AuthenticationMiddleware
-from django.contrib.sessions.middleware import SessionMiddleware
-from student.models import CourseEnrollment
-from urllib import quote_plus
-from nose.tools import assert_equals
-from bs4 import BeautifulSoup
+from nose.tools import assert_equals, assert_in
import time
-import re
-import os.path
-from selenium.common.exceptions import WebDriverException
from logging import getLogger
logger = getLogger(__name__)
@@ -22,7 +14,7 @@ logger = getLogger(__name__)
@step(u'I wait (?:for )?"(\d+)" seconds?$')
def wait(step, seconds):
- time.sleep(float(seconds))
+ world.wait(seconds)
@step('I reload the page$')
@@ -37,42 +29,42 @@ def browser_back(step):
@step('I (?:visit|access|open) the homepage$')
def i_visit_the_homepage(step):
- world.browser.visit(django_url('/'))
- assert world.browser.is_element_present_by_css('header.global', 10)
+ world.visit('/')
+ assert world.is_css_present('header.global')
@step(u'I (?:visit|access|open) the dashboard$')
def i_visit_the_dashboard(step):
- world.browser.visit(django_url('/dashboard'))
- assert world.browser.is_element_present_by_css('section.container.dashboard', 5)
+ world.visit('/dashboard')
+ assert world.is_css_present('section.container.dashboard')
@step('I should be on the dashboard page$')
def i_should_be_on_the_dashboard(step):
- assert world.browser.is_element_present_by_css('section.container.dashboard', 5)
+ assert world.is_css_present('section.container.dashboard')
assert world.browser.title == 'Dashboard'
@step(u'I (?:visit|access|open) the courses page$')
def i_am_on_the_courses_page(step):
- world.browser.visit(django_url('/courses'))
- assert world.browser.is_element_present_by_css('section.courses')
+ world.visit('/courses')
+ assert world.is_css_present('section.courses')
@step(u'I press the "([^"]*)" button$')
def and_i_press_the_button(step, value):
button_css = 'input[value="%s"]' % value
- world.browser.find_by_css(button_css).first.click()
+ world.css_click(button_css)
@step(u'I click the link with the text "([^"]*)"$')
def click_the_link_with_the_text_group1(step, linktext):
- world.browser.find_link_by_text(linktext).first.click()
+ world.click_link(linktext)
@step('I should see that the path is "([^"]*)"$')
def i_should_see_that_the_path_is(step, path):
- assert world.browser.url == django_url(path)
+ assert world.url_equals(path)
@step(u'the page title should be "([^"]*)"$')
@@ -85,10 +77,15 @@ def the_page_title_should_contain(step, title):
assert(title in world.browser.title)
+@step('I log in$')
+def i_log_in(step):
+ world.log_in('robot', 'test')
+
+
@step('I am a logged in user$')
def i_am_logged_in_user(step):
- create_user('robot')
- log_in('robot', 'test')
+ world.create_user('robot')
+ world.log_in('robot', 'test')
@step('I am not logged in$')
@@ -98,151 +95,46 @@ def i_am_not_logged_in(step):
@step('I am staff for course "([^"]*)"$')
def i_am_staff_for_course_by_id(step, course_id):
- register_by_course_id(course_id, True)
+ world.register_by_course_id(course_id, True)
-@step('I log in$')
-def i_log_in(step):
- log_in('robot', 'test')
+@step(r'click (?:the|a) link (?:called|with the text) "([^"]*)"$')
+def click_the_link_called(step, text):
+ world.click_link(text)
+
+
+@step(r'should see that the url is "([^"]*)"$')
+def should_have_the_url(step, url):
+ assert_equals(world.browser.url, url)
+
+
+@step(r'should see (?:the|a) link (?:called|with the text) "([^"]*)"$')
+def should_see_a_link_called(step, text):
+ assert len(world.browser.find_link_by_text(text)) > 0
+
+
+@step(r'should see "(.*)" (?:somewhere|anywhere) in (?:the|this) page')
+def should_see_in_the_page(step, text):
+ assert_in(text, world.css_text('body'))
+
+
+@step('I am logged in$')
+def i_am_logged_in(step):
+ world.create_user('robot')
+ world.log_in('robot', 'test')
+ world.browser.visit(django_url('/'))
+
+
+@step('I am not logged in$')
+def i_am_not_logged_in(step):
+ world.browser.cookies.delete()
@step(u'I am an edX user$')
def i_am_an_edx_user(step):
- create_user('robot')
-
-#### helper functions
+ world.create_user('robot')
-@world.absorb
-def scroll_to_bottom():
- # Maximize the browser
- world.browser.execute_script("window.scrollTo(0, screen.height);")
-
-
-@world.absorb
-def create_user(uname):
-
- # If the user already exists, don't try to create it again
- if len(User.objects.filter(username=uname)) > 0:
- return
-
- portal_user = UserFactory.build(username=uname, email=uname + '@edx.org')
- portal_user.set_password('test')
- portal_user.save()
-
- registration = world.RegistrationFactory(user=portal_user)
- registration.register(portal_user)
- registration.activate()
-
- user_profile = world.UserProfileFactory(user=portal_user)
-
-
-@world.absorb
-def log_in(username, password):
- '''
- Log the user in programatically
- '''
-
- # Authenticate the user
- user = authenticate(username=username, password=password)
- assert(user is not None and user.is_active)
-
- # Send a fake HttpRequest to log the user in
- # We need to process the request using
- # Session middleware and Authentication middleware
- # to ensure that session state can be stored
- request = HttpRequest()
- SessionMiddleware().process_request(request)
- AuthenticationMiddleware().process_request(request)
- login(request, user)
-
- # Save the session
- request.session.save()
-
- # Retrieve the sessionid and add it to the browser's cookies
- cookie_dict = {settings.SESSION_COOKIE_NAME: request.session.session_key}
- try:
- world.browser.cookies.add(cookie_dict)
-
- # WebDriver has an issue where we cannot set cookies
- # before we make a GET request, so if we get an error,
- # we load the '/' page and try again
- except:
- world.browser.visit(django_url('/'))
- world.browser.cookies.add(cookie_dict)
-
-
-@world.absorb
-def register_by_course_id(course_id, is_staff=False):
- create_user('robot')
- u = User.objects.get(username='robot')
- if is_staff:
- u.is_staff = True
- u.save()
- CourseEnrollment.objects.get_or_create(user=u, course_id=course_id)
-
-
-@world.absorb
-def save_the_html(path='/tmp'):
- u = world.browser.url
- html = world.browser.html.encode('ascii', 'ignore')
- filename = '%s.html' % quote_plus(u)
- f = open('%s/%s' % (path, filename), 'w')
- f.write(html)
- f.close
-
-
-@world.absorb
-def save_the_course_content(path='/tmp'):
- html = world.browser.html.encode('ascii', 'ignore')
- soup = BeautifulSoup(html)
-
- # get rid of the header, we only want to compare the body
- soup.head.decompose()
-
- # for now, remove the data-id attributes, because they are
- # causing mismatches between cms-master and master
- for item in soup.find_all(attrs={'data-id': re.compile('.*')}):
- del item['data-id']
-
- # we also need to remove them from unrendered problems,
- # where they are contained in the text of divs instead of
- # in attributes of tags
- # Be careful of whether or not it was the last attribute
- # and needs a trailing space
- for item in soup.find_all(text=re.compile(' data-id=".*?" ')):
- s = unicode(item.string)
- item.string.replace_with(re.sub(' data-id=".*?" ', ' ', s))
-
- for item in soup.find_all(text=re.compile(' data-id=".*?"')):
- s = unicode(item.string)
- item.string.replace_with(re.sub(' data-id=".*?"', ' ', s))
-
- # prettify the html so it will compare better, with
- # each HTML tag on its own line
- output = soup.prettify()
-
- # use string slicing to grab everything after 'courseware/' in the URL
- u = world.browser.url
- section_url = u[u.find('courseware/') + 11:]
-
-
- if not os.path.exists(path):
- os.makedirs(path)
-
- filename = '%s.html' % (quote_plus(section_url))
- f = open('%s/%s' % (path, filename), 'w')
- f.write(output)
- f.close
-
-@world.absorb
-def css_click(css_selector):
- try:
- world.browser.find_by_css(css_selector).click()
-
- except WebDriverException:
- # Occassionally, MathJax or other JavaScript can cover up
- # an element temporarily.
- # If this happens, wait a second, then try again
- time.sleep(1)
- world.browser.find_by_css(css_selector).click()
+@step(u'User "([^"]*)" is an edX user$')
+def registered_edx_user(step, uname):
+ world.create_user(uname)
diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py
new file mode 100644
index 0000000000..d4d99e17b5
--- /dev/null
+++ b/common/djangoapps/terrain/ui_helpers.py
@@ -0,0 +1,117 @@
+#pylint: disable=C0111
+#pylint: disable=W0621
+
+from lettuce import world, step
+import time
+from urllib import quote_plus
+from selenium.common.exceptions import WebDriverException
+from selenium.webdriver.support import expected_conditions as EC
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+from lettuce.django import django_url
+
+
+@world.absorb
+def wait(seconds):
+ time.sleep(float(seconds))
+
+
+@world.absorb
+def wait_for(func):
+ WebDriverWait(world.browser.driver, 5).until(func)
+
+
+@world.absorb
+def visit(url):
+ world.browser.visit(django_url(url))
+
+
+@world.absorb
+def url_equals(url):
+ return world.browser.url == django_url(url)
+
+
+@world.absorb
+def is_css_present(css_selector):
+ return world.browser.is_element_present_by_css(css_selector, wait_time=4)
+
+
+@world.absorb
+def css_has_text(css_selector, text):
+ return world.css_text(css_selector) == text
+
+
+@world.absorb
+def css_find(css):
+ def is_visible(driver):
+ return EC.visibility_of_element_located((By.CSS_SELECTOR, css,))
+
+ world.browser.is_element_present_by_css(css, 5)
+ wait_for(is_visible)
+ return world.browser.find_by_css(css)
+
+
+@world.absorb
+def css_click(css_selector):
+ '''
+ 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:
+ world.browser.find_by_css(css_selector).click()
+
+ except WebDriverException:
+ # Occassionally, MathJax or other JavaScript can cover up
+ # an element temporarily.
+ # If this happens, wait a second, then try again
+ time.sleep(1)
+ world.browser.find_by_css(css_selector).click()
+
+
+@world.absorb
+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
+ '''
+ 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()
+
+
+@world.absorb
+def css_fill(css_selector, text):
+ world.browser.find_by_css(css_selector).first.fill(text)
+
+
+@world.absorb
+def click_link(partial_text):
+ world.browser.find_link_by_partial_text(partial_text).first.click()
+
+
+@world.absorb
+def css_text(css_selector):
+
+ # Wait for the css selector to appear
+ if world.is_css_present(css_selector):
+ return world.browser.find_by_css(css_selector).first.text
+ else:
+ return ""
+
+
+@world.absorb
+def css_visible(css_selector):
+ return world.browser.find_by_css(css_selector).visible
+
+
+@world.absorb
+def save_the_html(path='/tmp'):
+ u = world.browser.url
+ html = world.browser.html.encode('ascii', 'ignore')
+ filename = '%s.html' % quote_plus(u)
+ f = open('%s/%s' % (path, filename), 'w')
+ f.write(html)
+ f.close
diff --git a/common/djangoapps/util/converters.py b/common/djangoapps/util/converters.py
deleted file mode 100644
index 212cceb77d..0000000000
--- a/common/djangoapps/util/converters.py
+++ /dev/null
@@ -1,37 +0,0 @@
-import time
-import datetime
-import calendar
-import dateutil.parser
-
-
-def time_to_date(time_obj):
- """
- Convert a time.time_struct to a true universal time (can pass to js Date
- constructor)
- """
- return calendar.timegm(time_obj) * 1000
-
-
-def time_to_isodate(source):
- '''Convert to an iso date'''
- if isinstance(source, time.struct_time):
- return time.strftime('%Y-%m-%dT%H:%M:%SZ', source)
- elif isinstance(source, datetime):
- return source.isoformat() + 'Z'
-
-
-def jsdate_to_time(field):
- """
- Convert a universal time (iso format) or msec since epoch to a time obj
- """
- if field is None:
- return field
- elif isinstance(field, basestring):
- d = dateutil.parser.parse(field)
- return d.utctimetuple()
- elif isinstance(field, (int, long, float)):
- return time.gmtime(field / 1000)
- elif isinstance(field, time.struct_time):
- return field
- else:
- raise ValueError("Couldn't convert %r to time" % field)
diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py
index 68f80006f6..6580114bcc 100644
--- a/common/lib/capa/capa/capa_problem.py
+++ b/common/lib/capa/capa/capa_problem.py
@@ -32,6 +32,8 @@ from copy import deepcopy
import chem
import chem.miller
+import chem.chemcalc
+import chem.chemtools
import verifiers
import verifiers.draganddrop
@@ -67,6 +69,9 @@ global_context = {'random': random,
'scipy': scipy,
'calc': calc,
'eia': eia,
+ 'chemcalc': chem.chemcalc,
+ 'chemtools': chem.chemtools,
+ 'miller': chem.miller,
'draganddrop': verifiers.draganddrop}
# These should be removed from HTML output, including all subelements
@@ -118,7 +123,7 @@ class LoncapaProblem(object):
# 3. Assign from the OS's random number generator
self.seed = state.get('seed', seed)
if self.seed is None:
- self.seed = struct.unpack('i', os.urandom(4))
+ self.seed = struct.unpack('i', os.urandom(4))[0]
self.student_answers = state.get('student_answers', {})
if 'correct_map' in state:
self.correct_map.set_dict(state['correct_map'])
diff --git a/common/lib/capa/capa/correctmap.py b/common/lib/capa/capa/correctmap.py
index b726f765d8..950cd199fc 100644
--- a/common/lib/capa/capa/correctmap.py
+++ b/common/lib/capa/capa/correctmap.py
@@ -80,16 +80,17 @@ class CorrectMap(object):
Special migration case:
If correct_map is a one-level dict, then convert it to the new dict of dicts format.
- '''
- if correct_map and not (type(correct_map[correct_map.keys()[0]]) == dict):
- # empty current dict
- self.__init__()
- # create new dict entries
+ '''
+ # empty current dict
+ self.__init__()
+
+ # create new dict entries
+ if correct_map and not isinstance(correct_map.values()[0], dict):
+ # special migration
for k in correct_map:
- self.set(k, correct_map[k])
+ self.set(k, correctness=correct_map[k])
else:
- self.__init__()
for k in correct_map:
self.set(k, **correct_map[k])
diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py
index 8ab716735c..5b1b46d858 100644
--- a/common/lib/capa/capa/responsetypes.py
+++ b/common/lib/capa/capa/responsetypes.py
@@ -17,6 +17,7 @@ import logging
import numbers
import numpy
import os
+import sys
import random
import re
import requests
@@ -52,12 +53,17 @@ class LoncapaProblemError(Exception):
class ResponseError(Exception):
'''
- Error for failure in processing a response
+ Error for failure in processing a response, including
+ exceptions that occur when executing a custom script.
'''
pass
class StudentInputError(Exception):
+ '''
+ Error for an invalid student input.
+ For example, submitting a string when the problem expects a number
+ '''
pass
#-----------------------------------------------------------------------------
@@ -833,7 +839,7 @@ class NumericalResponse(LoncapaResponse):
import sys
type, value, traceback = sys.exc_info()
- raise StudentInputError, ("Invalid input: could not interpret '%s' as a number" %
+ raise StudentInputError, ("Could not interpret '%s' as a number" %
cgi.escape(student_answer)), traceback
if correct:
@@ -1072,13 +1078,10 @@ def sympy_check2():
correct = self.context['correct']
messages = self.context['messages']
overall_message = self.context['overall_message']
+
except Exception as err:
- print "oops in customresponse (code) error %s" % err
- print "context = ", self.context
- print traceback.format_exc()
- # Notify student
- raise StudentInputError(
- "Error: Problem could not be evaluated with your input")
+ self._handle_exec_exception(err)
+
else:
# self.code is not a string; assume its a function
@@ -1105,13 +1108,9 @@ def sympy_check2():
nargs, args, kwargs))
ret = fn(*args[:nargs], **kwargs)
+
except Exception as err:
- log.error("oops in customresponse (cfn) error %s" % err)
- # print "context = ",self.context
- log.error(traceback.format_exc())
- raise Exception("oops in customresponse (cfn) error %s" % err)
- log.debug(
- "[courseware.capa.responsetypes.customresponse.get_score] ret = %s" % ret)
+ self._handle_exec_exception(err)
if type(ret) == dict:
@@ -1147,9 +1146,9 @@ def sympy_check2():
correct = []
messages = []
for input_dict in input_list:
- correct.append('correct'
+ correct.append('correct'
if input_dict['ok'] else 'incorrect')
- msg = (self.clean_message_html(input_dict['msg'])
+ msg = (self.clean_message_html(input_dict['msg'])
if 'msg' in input_dict else None)
messages.append(msg)
@@ -1157,7 +1156,7 @@ def sympy_check2():
# Raise an exception
else:
log.error(traceback.format_exc())
- raise Exception(
+ raise ResponseError(
"CustomResponse: check function returned an invalid dict")
# The check function can return a boolean value,
@@ -1174,7 +1173,7 @@ def sympy_check2():
correct_map.set_overall_message(overall_message)
for k in range(len(idset)):
- npoints = (self.maxpoints[idset[k]]
+ npoints = (self.maxpoints[idset[k]]
if correct[k] == 'correct' else 0)
correct_map.set(idset[k], correct[k], msg=messages[k],
npoints=npoints)
@@ -1227,6 +1226,22 @@ def sympy_check2():
return {self.answer_ids[0]: self.expect}
return self.default_answer_map
+ def _handle_exec_exception(self, err):
+ '''
+ Handle an exception raised during the execution of
+ custom Python code.
+
+ Raises a ResponseError
+ '''
+
+ # Log the error if we are debugging
+ msg = 'Error occurred while evaluating CustomResponse'
+ log.warning(msg, exc_info=True)
+
+ # Notify student with a student input error
+ _, _, traceback_obj = sys.exc_info()
+ raise ResponseError, err.message, traceback_obj
+
#-----------------------------------------------------------------------------
@@ -1901,7 +1916,14 @@ class SchematicResponse(LoncapaResponse):
submission = [json.loads(student_answers[
k]) for k in sorted(self.answer_ids)]
self.context.update({'submission': submission})
- exec self.code in global_context, self.context
+
+ try:
+ exec self.code in global_context, self.context
+
+ except Exception as err:
+ _, _, traceback_obj = sys.exc_info()
+ raise ResponseError, ResponseError(err.message), traceback_obj
+
cmap = CorrectMap()
cmap.set_dict(dict(zip(sorted(
self.answer_ids), self.context['correct'])))
@@ -1961,9 +1983,10 @@ class ImageResponse(LoncapaResponse):
self.ielements = self.inputfields
self.answer_ids = [ie.get('id') for ie in self.ielements]
+
def get_score(self, student_answers):
correct_map = CorrectMap()
- expectedset = self.get_answers()
+ expectedset = self.get_mapped_answers()
for aid in self.answer_ids: # loop through IDs of
# fields in our stanza
given = student_answers[
@@ -2018,11 +2041,42 @@ class ImageResponse(LoncapaResponse):
break
return correct_map
- def get_answers(self):
- return (
+ def get_mapped_answers(self):
+ '''
+ Returns the internal representation of the answers
+
+ Input:
+ None
+ Returns:
+ tuple (dict, dict) -
+ rectangles (dict) - a map of inputs to the defined rectangle for that input
+ regions (dict) - a map of inputs to the defined region for that input
+ '''
+ answers = (
dict([(ie.get('id'), ie.get(
'rectangle')) for ie in self.ielements]),
dict([(ie.get('id'), ie.get('regions')) for ie in self.ielements]))
+ return answers
+
+ def get_answers(self):
+ '''
+ Returns the external representation of the answers
+
+ Input:
+ None
+ Returns:
+ dict (str, (str, str)) - a map of inputs to a tuple of their rectange
+ and their regions
+ '''
+ answers = {}
+ for ie in self.ielements:
+ ie_id = ie.get('id')
+ answers[ie_id] = (ie.get('rectangle'), ie.get('regions'))
+
+ return answers
+
+
+
#-----------------------------------------------------------------------------
@@ -2074,7 +2128,7 @@ class AnnotationResponse(LoncapaResponse):
option_scoring = dict([(option['id'], {
'correctness': choices.get(option['choice']),
'points': scoring.get(option['choice'])
- }) for option in self._find_options(inputfield) ])
+ }) for option in self._find_options(inputfield)])
scoring_map[inputfield.get('id')] = option_scoring
@@ -2087,8 +2141,8 @@ class AnnotationResponse(LoncapaResponse):
correct_option = self._find_option_with_choice(
inputfield, 'correct')
if correct_option is not None:
- answer_map[inputfield.get(
- 'id')] = correct_option.get('description')
+ input_id = inputfield.get('id')
+ answer_map[input_id] = correct_option.get('description')
return answer_map
def _get_max_points(self):
diff --git a/common/lib/capa/capa/templates/choicegroup.html b/common/lib/capa/capa/templates/choicegroup.html
index 758e2ffba1..c9cc3fd28d 100644
--- a/common/lib/capa/capa/templates/choicegroup.html
+++ b/common/lib/capa/capa/templates/choicegroup.html
@@ -17,7 +17,7 @@
% for choice_id, choice_description in choices: