Commit 769e2090 by Brian Talbot

resolving local merge

parents 57e5eb68 ac180ca2
Feature: Advanced (manual) course policy Feature: Advanced (manual) course policy
In order to specify course policy settings for which no custom user interface exists 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 Scenario: A course author sees default advanced settings
Given I have opened a new course in Studio Given I have opened a new course in Studio
......
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from common import * from common import *
import time import time
from terrain.steps import reload_the_page 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 from nose.tools import assert_true, assert_false, assert_equal
...@@ -18,13 +19,14 @@ DISPLAY_NAME_KEY = "display_name" ...@@ -18,13 +19,14 @@ DISPLAY_NAME_KEY = "display_name"
DISPLAY_NAME_VALUE = '"Robot Super Course"' DISPLAY_NAME_VALUE = '"Robot Super Course"'
############### ACTIONS #################### ############### ACTIONS ####################
@step('I select the Advanced Settings$') @step('I select the Advanced Settings$')
def i_select_advanced_settings(step): def i_select_advanced_settings(step):
expand_icon_css = 'li.nav-course-settings i.icon-expand' expand_icon_css = 'li.nav-course-settings i.icon-expand'
if world.browser.is_element_present_by_css(expand_icon_css): 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' 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$') @step('I am on the Advanced Course Settings page in Studio$')
...@@ -35,24 +37,8 @@ def i_am_on_advanced_course_settings(step): ...@@ -35,24 +37,8 @@ def i_am_on_advanced_course_settings(step):
@step(u'I press the "([^"]*)" notification button$') @step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(step, name): def press_the_notification_button(step, name):
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() css = 'a.%s-button' % name.lower()
wait_for(is_visible) world.css_click_at(css)
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)
@step(u'I edit the value of a policy key$') @step(u'I edit the value of a policy key$')
...@@ -61,7 +47,7 @@ def edit_the_value_of_a_policy_key(step): ...@@ -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 It is hard to figure out how to get into the CodeMirror
area, so cheat and do it from the policy key field :) 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') e._element.send_keys(Keys.TAB, Keys.END, Keys.ARROW_LEFT, ' ', 'X')
...@@ -85,7 +71,7 @@ def i_see_default_advanced_settings(step): ...@@ -85,7 +71,7 @@ def i_see_default_advanced_settings(step):
@step('the settings are alphabetized$') @step('the settings are alphabetized$')
def they_are_alphabetized(step): def they_are_alphabetized(step):
key_elements = css_find(KEY_CSS) key_elements = world.css_find(KEY_CSS)
all_keys = [] all_keys = []
for key in key_elements: for key in key_elements:
all_keys.append(key.value) all_keys.append(key.value)
...@@ -118,13 +104,13 @@ def assert_policy_entries(expected_keys, expected_values): ...@@ -118,13 +104,13 @@ def assert_policy_entries(expected_keys, expected_values):
for counter in range(len(expected_keys)): for counter in range(len(expected_keys)):
index = get_index_of(expected_keys[counter]) index = get_index_of(expected_keys[counter])
assert_false(index == -1, "Could not find key: " + 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): 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 # 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: if key == expected_key:
return counter return counter
...@@ -133,14 +119,14 @@ def get_index_of(expected_key): ...@@ -133,14 +119,14 @@ def get_index_of(expected_key):
def get_display_name_value(): def get_display_name_value():
index = get_index_of(DISPLAY_NAME_KEY) 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): 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() display_name = get_display_name_value()
for count in range(len(display_name)): for count in range(len(display_name)):
e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE) e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE)
# Must delete "" before typing the JSON value # Must delete "" before typing the JSON value
e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE, Keys.BACK_SPACE, new_value) e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE, Keys.BACK_SPACE, new_value)
press_the_notification_button(step, "Save") press_the_notification_button(step, "Save")
\ No newline at end of file
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from common import * from nose.tools import assert_true, assert_equal
from terrain.steps import reload_the_page from terrain.steps import reload_the_page
from selenium.common.exceptions import StaleElementReferenceException
############### ACTIONS #################### ############### ACTIONS ####################
@step('I select Checklists from the Tools menu$') @step('I select Checklists from the Tools menu$')
def i_select_checklists(step): def i_select_checklists(step):
expand_icon_css = 'li.nav-course-tools i.icon-expand' expand_icon_css = 'li.nav-course-tools i.icon-expand'
if world.browser.is_element_present_by_css(expand_icon_css): 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' link_css = 'li.nav-course-tools-checklists a'
css_click(link_css) world.css_click(link_css)
@step('I have opened Checklists$') @step('I have opened Checklists$')
...@@ -20,7 +24,7 @@ def i_have_opened_checklists(step): ...@@ -20,7 +24,7 @@ def i_have_opened_checklists(step):
@step('I see the four default edX checklists$') @step('I see the four default edX checklists$')
def i_see_default_checklists(step): def i_see_default_checklists(step):
checklists = css_find('.checklist-title') checklists = world.css_find('.checklist-title')
assert_equal(4, len(checklists)) assert_equal(4, len(checklists))
assert_true(checklists[0].text.endswith('Getting Started With Studio')) assert_true(checklists[0].text.endswith('Getting Started With Studio'))
assert_true(checklists[1].text.endswith('Draft a Rough Course Outline')) 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): ...@@ -58,7 +62,7 @@ def i_select_a_link_to_the_course_outline(step):
@step('I am brought to the course outline page$') @step('I am brought to the course outline page$')
def i_am_brought_to_course_outline(step): 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)) assert_equal(1, len(world.browser.windows))
...@@ -90,30 +94,30 @@ def i_am_brought_to_help_page_in_new_window(step): ...@@ -90,30 +94,30 @@ def i_am_brought_to_help_page_in_new_window(step):
def verifyChecklist2Status(completed, total, percentage): def verifyChecklist2Status(completed, total, percentage):
def verify_count(driver): def verify_count(driver):
try: try:
statusCount = css_find('#course-checklist1 .status-count').first statusCount = world.css_find('#course-checklist1 .status-count').first
return statusCount.text == str(completed) return statusCount.text == str(completed)
except StaleElementReferenceException: except StaleElementReferenceException:
return False return False
wait_for(verify_count) world.wait_for(verify_count)
assert_equal(str(total), css_find('#course-checklist1 .status-amount').first.text) 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. # 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): 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): def clickActionLink(checklist, task, actionText):
# toggle checklist item to make sure that the link button is showing # toggle checklist item to make sure that the link button is showing
toggleTask(checklist, task) 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 # text will be empty initially, wait for it to populate
def verify_action_link_text(driver): def verify_action_link_text(driver):
return action_link.text == actionText return action_link.text == actionText
wait_for(verify_action_link_text) world.wait_for(verify_action_link_text)
action_link.click() action_link.click()
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from lettuce.django import django_url
from nose.tools import assert_true from nose.tools import assert_true
from nose.tools import assert_equal from nose.tools import assert_equal
from selenium.webdriver.support.ui import WebDriverWait
from selenium.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.modulestore.django import _MODULESTORES, modulestore
from xmodule.templates import update_templates from xmodule.templates import update_templates
...@@ -15,14 +13,15 @@ from logging import getLogger ...@@ -15,14 +13,15 @@ from logging import getLogger
logger = getLogger(__name__) logger = getLogger(__name__)
########### STEP HELPERS ############## ########### STEP HELPERS ##############
@step('I (?:visit|access|open) the Studio homepage$') @step('I (?:visit|access|open) the Studio homepage$')
def i_visit_the_studio_homepage(step): def i_visit_the_studio_homepage(step):
# To make this go to port 8001, put # To make this go to port 8001, put
# LETTUCE_SERVER_PORT = 8001 # LETTUCE_SERVER_PORT = 8001
# in your settings.py file. # in your settings.py file.
world.browser.visit(django_url('/')) world.visit('/')
signin_css = 'a.action-signin' 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$') @step('I am logged into Studio$')
...@@ -43,12 +42,12 @@ def i_press_the_category_delete_icon(step, category): ...@@ -43,12 +42,12 @@ def i_press_the_category_delete_icon(step, category):
css = 'a.delete-button.delete-subsection-button span.delete-icon' css = 'a.delete-button.delete-subsection-button span.delete-icon'
else: else:
assert False, 'Invalid category: %s' % category assert False, 'Invalid category: %s' % category
css_click(css) world.css_click(css)
@step('I have opened a new course in Studio$') @step('I have opened a new course in Studio$')
def i_have_opened_a_new_course(step): def i_have_opened_a_new_course(step):
clear_courses() world.clear_courses()
log_into_studio() log_into_studio()
create_a_course() create_a_course()
...@@ -74,80 +73,13 @@ def create_studio_user( ...@@ -74,80 +73,13 @@ def create_studio_user(
user_profile = world.UserProfileFactory(user=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( def fill_in_course_info(
name='Robot Super Course', name='Robot Super Course',
org='MITx', org='MITx',
num='101'): num='101'):
css_fill('.new-course-name', name) world.css_fill('.new-course-name', name)
css_fill('.new-course-org', org) world.css_fill('.new-course-org', org)
css_fill('.new-course-number', num) world.css_fill('.new-course-number', num)
def log_into_studio( def log_into_studio(
...@@ -155,21 +87,22 @@ def log_into_studio( ...@@ -155,21 +87,22 @@ def log_into_studio(
email='robot+studio@edx.org', email='robot+studio@edx.org',
password='test', password='test',
is_staff=False): is_staff=False):
create_studio_user(uname=uname, email=email, is_staff=is_staff) create_studio_user(uname=uname, email=email, is_staff=is_staff)
world.browser.cookies.delete() world.browser.cookies.delete()
world.browser.visit(django_url('/')) world.visit('/')
signin_css = 'a.action-signin'
world.browser.is_element_present_by_css(signin_css, 10)
# click the signin button signin_css = 'a.action-signin'
css_click(signin_css) world.is_css_present(signin_css)
world.css_click(signin_css)
login_form = world.browser.find_by_css('form#login_form') login_form = world.browser.find_by_css('form#login_form')
login_form.find_by_name('email').fill(email) login_form.find_by_name('email').fill(email)
login_form.find_by_name('password').fill(password) login_form.find_by_name('password').fill(password)
login_form.find_by_name('submit').click() 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(): def create_a_course():
...@@ -184,26 +117,26 @@ def create_a_course(): ...@@ -184,26 +117,26 @@ def create_a_course():
world.browser.reload() world.browser.reload()
course_link_css = 'span.class-name' course_link_css = 'span.class-name'
css_click(course_link_css) world.css_click(course_link_css)
course_title_css = 'span.course-title' 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'): def add_section(name='My Section'):
link_css = 'a.new-courseware-section-button' link_css = 'a.new-courseware-section-button'
css_click(link_css) world.css_click(link_css)
name_css = 'input.new-section-name' name_css = 'input.new-section-name'
save_css = 'input.new-section-name-save' save_css = 'input.new-section-name-save'
css_fill(name_css, name) world.css_fill(name_css, name)
css_click(save_css) world.css_click(save_css)
span_css = 'span.section-name-span' 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'): def add_subsection(name='Subsection One'):
css = 'a.new-subsection-item' css = 'a.new-subsection-item'
css_click(css) world.css_click(css)
name_css = 'input.new-subsection-name-input' name_css = 'input.new-subsection-name-input'
save_css = 'input.new-subsection-name-save' save_css = 'input.new-subsection-name-save'
css_fill(name_css, name) world.css_fill(name_css, name)
css_click(save_css) world.css_click(save_css)
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from common import *
from terrain.steps import reload_the_page from terrain.steps import reload_the_page
from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.keys import Keys
import time import time
...@@ -25,9 +27,9 @@ DEFAULT_TIME = "12:00am" ...@@ -25,9 +27,9 @@ DEFAULT_TIME = "12:00am"
def test_i_select_schedule_and_details(step): def test_i_select_schedule_and_details(step):
expand_icon_css = 'li.nav-course-settings i.icon-expand' expand_icon_css = 'li.nav-course-settings i.icon-expand'
if world.browser.is_element_present_by_css(expand_icon_css): 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' link_css = 'li.nav-course-settings-schedule a'
css_click(link_css) world.css_click(link_css)
@step('I have set course dates$') @step('I have set course dates$')
...@@ -97,9 +99,9 @@ def test_i_clear_the_course_start_date(step): ...@@ -97,9 +99,9 @@ def test_i_clear_the_course_start_date(step):
@step('I receive a warning about course start date$') @step('I receive a warning about course start date$')
def test_i_receive_a_warning_about_course_start_date(step): 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(world.css_has_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 world.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('error' in world.css_find(COURSE_START_TIME_CSS).first._element.get_attribute('class'))
@step('The previously set start date is shown on refresh$') @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): ...@@ -124,9 +126,9 @@ def test_i_have_entered_a_new_course_start_date(step):
@step('The warning about course start date goes away$') @step('The warning about course start date goes away$')
def test_the_warning_about_course_start_date_goes_away(step): def test_the_warning_about_course_start_date_goes_away(step):
assert_equal(0, len(css_find('.message-error'))) assert_equal(0, len(world.css_find('.message-error')))
assert_false('error' in css_find(COURSE_START_DATE_CSS).first._element.get_attribute('class')) assert_false('error' in world.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_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$') @step('My new course start date is shown on refresh$')
...@@ -142,8 +144,8 @@ def set_date_or_time(css, date_or_time): ...@@ -142,8 +144,8 @@ def set_date_or_time(css, date_or_time):
""" """
Sets date or time field. Sets date or time field.
""" """
css_fill(css, date_or_time) world.css_fill(css, date_or_time)
e = css_find(css).first e = world.css_find(css).first
# hit Enter to apply the changes # hit Enter to apply the changes
e._element.send_keys(Keys.ENTER) e._element.send_keys(Keys.ENTER)
...@@ -152,7 +154,7 @@ def verify_date_or_time(css, date_or_time): ...@@ -152,7 +154,7 @@ def verify_date_or_time(css, date_or_time):
""" """
Verifies date or time field. 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(): def pause():
......
...@@ -10,4 +10,4 @@ Feature: Create Course ...@@ -10,4 +10,4 @@ Feature: Create Course
And I fill in the new course information And I fill in the new course information
And I press the "Save" button And I press the "Save" button
Then the Courseware page has loaded in Studio Then the Courseware page has loaded in Studio
And I see a link for adding a new section And I see a link for adding a new section
\ No newline at end of file
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from common import * from common import *
...@@ -6,12 +9,12 @@ from common import * ...@@ -6,12 +9,12 @@ from common import *
@step('There are no courses$') @step('There are no courses$')
def no_courses(step): def no_courses(step):
clear_courses() world.clear_courses()
@step('I click the New Course button$') @step('I click the New Course button$')
def i_click_new_course(step): 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$') @step('I fill in the new course information$')
...@@ -27,7 +30,7 @@ def i_create_a_course(step): ...@@ -27,7 +30,7 @@ def i_create_a_course(step):
@step('I click the course link in My Courses$') @step('I click the course link in My Courses$')
def i_click_the_course_link_in_my_courses(step): def i_click_the_course_link_in_my_courses(step):
course_css = 'span.class-name' course_css = 'span.class-name'
css_click(course_css) world.css_click(course_css)
############ ASSERTIONS ################### ############ ASSERTIONS ###################
...@@ -35,28 +38,28 @@ def i_click_the_course_link_in_my_courses(step): ...@@ -35,28 +38,28 @@ def i_click_the_course_link_in_my_courses(step):
@step('the Courseware page has loaded in Studio$') @step('the Courseware page has loaded in Studio$')
def courseware_page_has_loaded_in_studio(step): def courseware_page_has_loaded_in_studio(step):
course_title_css = 'span.course-title' 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$') @step('I see the course listed in My Courses$')
def i_see_the_course_in_my_courses(step): def i_see_the_course_in_my_courses(step):
course_css = 'span.class-name' 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$') @step('the course is loaded$')
def course_is_loaded(step): def course_is_loaded(step):
class_css = 'a.class-name' 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$') @step('I am on the "([^"]*)" tab$')
def i_am_on_tab(step, tab_name): def i_am_on_tab(step, tab_name):
header_css = 'div.inner-wrapper h1' 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$') @step('I see a link for adding a new section$')
def i_see_new_section_link(step): def i_see_new_section_link(step):
link_css = 'a.new-courseware-section-button' link_css = 'a.new-courseware-section-button'
assert_css_with_text(link_css, '+ New Section') assert world.css_has_text(link_css, '+ New Section')
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from common import * from common import *
from nose.tools import assert_equal from nose.tools import assert_equal
...@@ -10,7 +13,7 @@ import time ...@@ -10,7 +13,7 @@ import time
@step('I click the new section link$') @step('I click the new section link$')
def i_click_new_section_link(step): def i_click_new_section_link(step):
link_css = 'a.new-courseware-section-button' 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$') @step('I enter the section name and click save$')
...@@ -31,19 +34,19 @@ def i_have_added_new_section(step): ...@@ -31,19 +34,19 @@ def i_have_added_new_section(step):
@step('I click the Edit link for the release date$') @step('I click the Edit link for the release date$')
def i_click_the_edit_link_for_the_release_date(step): def i_click_the_edit_link_for_the_release_date(step):
button_css = 'div.section-published-date a.edit-button' 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$') @step('I save a new section release date$')
def i_save_a_new_section_release_date(step): def i_save_a_new_section_release_date(step):
date_css = 'input.start-date.date.hasDatepicker' date_css = 'input.start-date.date.hasDatepicker'
time_css = 'input.start-time.time.ui-timepicker-input' time_css = 'input.start-time.time.ui-timepicker-input'
css_fill(date_css, '12/25/2013') world.css_fill(date_css, '12/25/2013')
# hit TAB to get to the time field # 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) e._element.send_keys(Keys.TAB)
css_fill(time_css, '12:00am') world.css_fill(time_css, '12:00am')
e = css_find(time_css).first e = world.css_find(time_css).first
e._element.send_keys(Keys.TAB) e._element.send_keys(Keys.TAB)
time.sleep(float(1)) time.sleep(float(1))
world.browser.click_link_by_text('Save') world.browser.click_link_by_text('Save')
...@@ -64,13 +67,13 @@ def i_see_my_section_name_with_quote_on_the_courseware_page(step): ...@@ -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$') @step('I click to edit the section name$')
def i_click_to_edit_section_name(step): 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$') @step('I see the complete section name with a quote in the editor$')
def i_see_complete_section_name_with_quote_in_editor(step): def i_see_complete_section_name_with_quote_in_editor(step):
css = '.edit-section-name' 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"') 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): ...@@ -85,7 +88,7 @@ def i_see_a_release_date_for_my_section(step):
import re import re
css = 'span.published-status' 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 status_text = world.browser.find_by_css(css).text
# e.g. 11/06/2012 at 16:25 # e.g. 11/06/2012 at 16:25
...@@ -99,20 +102,20 @@ def i_see_a_release_date_for_my_section(step): ...@@ -99,20 +102,20 @@ def i_see_a_release_date_for_my_section(step):
@step('I see a link to create a new subsection$') @step('I see a link to create a new subsection$')
def i_see_a_link_to_create_a_new_subsection(step): def i_see_a_link_to_create_a_new_subsection(step):
css = 'a.new-subsection-item' 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$') @step('the section release date picker is not visible$')
def the_section_release_date_picker_not_visible(step): def the_section_release_date_picker_not_visible(step):
css = 'div.edit-subsection-publish-settings' 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$') @step('the section release date is updated$')
def the_section_release_date_is_updated(step): def the_section_release_date_is_updated(step):
css = 'span.published-status' css = 'span.published-status'
status_text = world.browser.find_by_css(css).text status_text = world.css_text(css)
assert_equal(status_text,'Will Release: 12/25/2013 at 12:00am') assert_equal(status_text, 'Will Release: 12/25/2013 at 12:00am')
############ HELPER METHODS ################### ############ HELPER METHODS ###################
...@@ -120,10 +123,10 @@ def the_section_release_date_is_updated(step): ...@@ -120,10 +123,10 @@ def the_section_release_date_is_updated(step):
def save_section_name(name): def save_section_name(name):
name_css = '.new-section-name' name_css = '.new-section-name'
save_css = '.new-section-name-save' save_css = '.new-section-name-save'
css_fill(name_css, name) world.css_fill(name_css, name)
css_click(save_css) world.css_click(save_css)
def see_my_section_on_the_courseware_page(name): def see_my_section_on_the_courseware_page(name):
section_css = 'span.section-name-span' section_css = 'span.section-name-span'
assert_css_with_text(section_css, name) assert world.css_has_text(section_css, name)
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from common import * from common import *
...@@ -17,9 +20,10 @@ def i_press_the_button_on_the_registration_form(step): ...@@ -17,9 +20,10 @@ def i_press_the_button_on_the_registration_form(step):
submit_css = 'form#register_form button#submit' submit_css = 'form#register_form button#submit'
# Workaround for click not working on ubuntu # Workaround for click not working on ubuntu
# for some unknown reason. # for some unknown reason.
e = css_find(submit_css) e = world.css_find(submit_css)
e.type(' ') e.type(' ')
@step('I should see be on the studio home page$') @step('I should see be on the studio home page$')
def i_should_see_be_on_the_studio_home_page(step): def i_should_see_be_on_the_studio_home_page(step):
assert world.browser.find_by_css('div.inner-wrapper') assert world.browser.find_by_css('div.inner-wrapper')
......
Feature: Overview Toggle Section Feature: Overview Toggle Section
In order to quickly view the details of a course's section or to scan the inventory of sections In order to quickly view the details of a course's section or to scan the inventory of sections
As a course author As a course author
I want to toggle the visibility of each section's subsection details in the overview listing 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 Scenario: The default layout for the overview page is to show sections in expanded view
Given I have a course with multiple sections Given I have a course with multiple sections
When I navigate to the course overview page When I navigate to the course overview page
Then I see the "Collapse All Sections" link Then I see the "Collapse All Sections" link
And all sections are expanded And all sections are expanded
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 Given I have a course with no sections
When I navigate to the course overview page When I navigate to the course overview page
Then I do not see the "Collapse All Sections" link Then I do not see the "Collapse All Sections" link
Scenario: Collapse link appears after creating first section of a course Scenario: Collapse link appears after creating first section of a course
Given I have a course with no sections Given I have a course with no sections
When I navigate to the course overview page When I navigate to the course overview page
And I add a section And I add a section
Then I see the "Collapse All Sections" link Then I see the "Collapse All Sections" link
And all sections are expanded And all sections are expanded
@skip-phantom @skip-phantom
Scenario: Collapse link is not removed after last section of a course is deleted Scenario: Collapse link is not removed after last section of a course is deleted
Given I have a course with 1 section Given I have a course with 1 section
And I navigate to the course overview page And I navigate to the course overview page
When I press the "section" delete icon When I press the "section" delete icon
And I confirm the alert And I confirm the alert
Then I see the "Collapse All Sections" link Then I see the "Collapse All Sections" link
......
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from common import * from common import *
from nose.tools import assert_true, assert_false, assert_equal from nose.tools import assert_true, assert_false, assert_equal
...@@ -8,13 +11,13 @@ logger = getLogger(__name__) ...@@ -8,13 +11,13 @@ logger = getLogger(__name__)
@step(u'I have a course with no sections$') @step(u'I have a course with no sections$')
def have_a_course(step): def have_a_course(step):
clear_courses() world.clear_courses()
course = world.CourseFactory.create() course = world.CourseFactory.create()
@step(u'I have a course with 1 section$') @step(u'I have a course with 1 section$')
def have_a_course_with_1_section(step): def have_a_course_with_1_section(step):
clear_courses() world.clear_courses()
course = world.CourseFactory.create() course = world.CourseFactory.create()
section = world.ItemFactory.create(parent_location=course.location) section = world.ItemFactory.create(parent_location=course.location)
subsection1 = world.ItemFactory.create( subsection1 = world.ItemFactory.create(
...@@ -25,7 +28,7 @@ def have_a_course_with_1_section(step): ...@@ -25,7 +28,7 @@ def have_a_course_with_1_section(step):
@step(u'I have a course with multiple sections$') @step(u'I have a course with multiple sections$')
def have_a_course_with_two_sections(step): def have_a_course_with_two_sections(step):
clear_courses() world.clear_courses()
course = world.CourseFactory.create() course = world.CourseFactory.create()
section = world.ItemFactory.create(parent_location=course.location) section = world.ItemFactory.create(parent_location=course.location)
subsection1 = world.ItemFactory.create( subsection1 = world.ItemFactory.create(
...@@ -49,7 +52,7 @@ def have_a_course_with_two_sections(step): ...@@ -49,7 +52,7 @@ def have_a_course_with_two_sections(step):
def navigate_to_the_course_overview_page(step): def navigate_to_the_course_overview_page(step):
log_into_studio(is_staff=True) log_into_studio(is_staff=True)
course_locator = '.class-name' 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') @step(u'I navigate to the courseware page of a course with multiple sections')
...@@ -66,44 +69,44 @@ def i_add_a_section(step): ...@@ -66,44 +69,44 @@ def i_add_a_section(step):
@step(u'I click the "([^"]*)" link$') @step(u'I click the "([^"]*)" link$')
def i_click_the_text_span(step, text): def i_click_the_text_span(step, text):
span_locator = '.toggle-button-sections span' 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 # first make sure that the expand/collapse text is the one you expected
assert_equal(world.browser.find_by_css(span_locator).value, text) 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$') @step(u'I collapse the first section$')
def i_collapse_a_section(step): def i_collapse_a_section(step):
collapse_locator = 'section.courseware-section a.collapse' collapse_locator = 'section.courseware-section a.collapse'
css_click(collapse_locator) world.css_click(collapse_locator)
@step(u'I expand the first section$') @step(u'I expand the first section$')
def i_expand_a_section(step): def i_expand_a_section(step):
expand_locator = 'section.courseware-section a.expand' expand_locator = 'section.courseware-section a.expand'
css_click(expand_locator) world.css_click(expand_locator)
@step(u'I see the "([^"]*)" link$') @step(u'I see the "([^"]*)" link$')
def i_see_the_span_with_text(step, text): def i_see_the_span_with_text(step, text):
span_locator = '.toggle-button-sections span' span_locator = '.toggle-button-sections span'
assert_true(world.browser.is_element_present_by_css(span_locator, 5)) assert_true(world.is_css_present(span_locator))
assert_equal(world.browser.find_by_css(span_locator).value, text) assert_equal(world.css_find(span_locator).value, text)
assert_true(world.browser.find_by_css(span_locator).visible) assert_true(world.css_visible(span_locator))
@step(u'I do not see the "([^"]*)" link$') @step(u'I do not see the "([^"]*)" link$')
def i_do_not_see_the_span_with_text(step, text): def i_do_not_see_the_span_with_text(step, text):
# Note that the span will exist on the page but not be visible # Note that the span will exist on the page but not be visible
span_locator = '.toggle-button-sections span' span_locator = '.toggle-button-sections span'
assert_true(world.browser.is_element_present_by_css(span_locator)) assert_true(world.is_css_present(span_locator))
assert_false(world.browser.find_by_css(span_locator).visible) assert_false(world.css_visible(span_locator))
@step(u'all sections are expanded$') @step(u'all sections are expanded$')
def all_sections_are_expanded(step): def all_sections_are_expanded(step):
subsection_locator = 'div.subsection-list' subsection_locator = 'div.subsection-list'
subsections = world.browser.find_by_css(subsection_locator) subsections = world.css_find(subsection_locator)
for s in subsections: for s in subsections:
assert_true(s.visible) assert_true(s.visible)
...@@ -111,6 +114,6 @@ def all_sections_are_expanded(step): ...@@ -111,6 +114,6 @@ def all_sections_are_expanded(step):
@step(u'all sections are collapsed$') @step(u'all sections are collapsed$')
def all_sections_are_expanded(step): def all_sections_are_expanded(step):
subsection_locator = 'div.subsection-list' subsection_locator = 'div.subsection-list'
subsections = world.browser.find_by_css(subsection_locator) subsections = world.css_find(subsection_locator)
for s in subsections: for s in subsections:
assert_false(s.visible) assert_false(s.visible)
...@@ -17,6 +17,14 @@ Feature: Create Subsection ...@@ -17,6 +17,14 @@ Feature: Create Subsection
And I click to edit the subsection name And I click to edit the subsection name
Then I see the complete subsection name with a quote in the editor Then I see the complete subsection name with a quote in the editor
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 @skip-phantom
Scenario: Delete a subsection Scenario: Delete a subsection
Given I have opened a new course section in Studio Given I have opened a new course section in Studio
......
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from common import * from common import *
from nose.tools import assert_equal from nose.tools import assert_equal
...@@ -7,7 +10,7 @@ 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$') @step('I have opened a new course section in Studio$')
def i_have_opened_a_new_course_section(step): def i_have_opened_a_new_course_section(step):
clear_courses() world.clear_courses()
log_into_studio() log_into_studio()
create_a_course() create_a_course()
add_section() add_section()
...@@ -15,8 +18,7 @@ def i_have_opened_a_new_course_section(step): ...@@ -15,8 +18,7 @@ def i_have_opened_a_new_course_section(step):
@step('I click the New Subsection link') @step('I click the New Subsection link')
def i_click_the_new_subsection_link(step): def i_click_the_new_subsection_link(step):
css = 'a.new-subsection-item' world.css_click('a.new-subsection-item')
css_click(css)
@step('I enter the subsection name and click save$') @step('I enter the subsection name and click save$')
...@@ -31,14 +33,14 @@ def i_save_subsection_name_with_quote(step): ...@@ -31,14 +33,14 @@ def i_save_subsection_name_with_quote(step):
@step('I click to edit the subsection name$') @step('I click to edit the subsection name$')
def i_click_to_edit_subsection_name(step): 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$') @step('I see the complete subsection name with a quote in the editor$')
def i_see_complete_subsection_name_with_quote_in_editor(step): def i_see_complete_subsection_name_with_quote_in_editor(step):
css = '.subsection-display-name-input' css = '.subsection-display-name-input'
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, 'Subsection With "Quote"') assert_equal(world.css_find(css).value, 'Subsection With "Quote"')
@step('I have added a new subsection$') @step('I have added a new subsection$')
...@@ -46,6 +48,17 @@ def i_have_added_a_new_subsection(step): ...@@ -46,6 +48,17 @@ def i_have_added_a_new_subsection(step):
add_subsection() 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 ################### ############ ASSERTIONS ###################
...@@ -70,11 +83,12 @@ def the_subsection_does_not_exist(step): ...@@ -70,11 +83,12 @@ def the_subsection_does_not_exist(step):
def save_subsection_name(name): def save_subsection_name(name):
name_css = 'input.new-subsection-name-input' name_css = 'input.new-subsection-name-input'
save_css = 'input.new-subsection-name-save' save_css = 'input.new-subsection-name-save'
css_fill(name_css, name) world.css_fill(name_css, name)
css_click(save_css) world.css_click(save_css)
def see_subsection_name(name): def see_subsection_name(name):
css = 'span.subsection-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' css = 'span.subsection-name-value'
assert_css_with_text(css, name) assert world.css_has_text(css, name)
...@@ -25,7 +25,7 @@ from xmodule.modulestore.django import modulestore ...@@ -25,7 +25,7 @@ from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.templates import update_templates from xmodule.templates import update_templates
from xmodule.modulestore.xml_exporter import export_to_xml 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.modulestore.inheritance import own_metadata
from xmodule.capa_module import CapaDescriptor from xmodule.capa_module import CapaDescriptor
...@@ -85,6 +85,43 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -85,6 +85,43 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_edit_unit_full(self): def test_edit_unit_full(self):
self.check_edit_unit('full') 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): def test_static_tab_reordering(self):
import_from_xml(modulestore(), 'common/test/data/', ['full']) import_from_xml(modulestore(), 'common/test/data/', ['full'])
...@@ -123,6 +160,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -123,6 +160,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# check that there's actually content in the 'question' field # check that there's actually content in the 'question' field
self.assertGreater(len(items[0].question),0) 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): def test_delete(self):
import_from_xml(modulestore(), 'common/test/data/', ['full']) import_from_xml(modulestore(), 'common/test/data/', ['full'])
...@@ -211,7 +252,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -211,7 +252,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
new_loc = descriptor.location._replace(org='MITx', course='999') 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()) 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()})) 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): def test_delete_course(self):
import_from_xml(modulestore(), 'common/test/data/', ['full']) import_from_xml(modulestore(), 'common/test/data/', ['full'])
...@@ -328,11 +373,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -328,11 +373,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertEqual(wrapper.counter, 4) self.assertEqual(wrapper.counter, 4)
# make sure we pre-fetched a known sequential which should be at depth=2 # 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) '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 # 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) None]) in course.system.module_data)
def test_export_course_with_unknown_metadata(self): def test_export_course_with_unknown_metadata(self):
...@@ -556,7 +601,7 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -556,7 +601,7 @@ class ContentStoreTest(ModuleStoreTestCase):
module_store.update_children(parent.location, parent.children + [new_component_location.url()]) module_store.update_children(parent.location, parent.children + [new_component_location.url()])
# flush the cache # 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) new_module = module_store.get_item(new_component_location)
# check for grace period definition which should be defined at the course level # check for grace period definition which should be defined at the course level
...@@ -571,7 +616,7 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -571,7 +616,7 @@ class ContentStoreTest(ModuleStoreTestCase):
module_store.update_metadata(new_module.location, own_metadata(new_module)) module_store.update_metadata(new_module.location, own_metadata(new_module))
# flush the cache and refetch # 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) new_module = module_store.get_item(new_component_location)
self.assertEqual(timedelta(1), new_module.lms.graceperiod) self.assertEqual(timedelta(1), new_module.lms.graceperiod)
......
import datetime import datetime
import json import json
import copy import copy
from util import converters
from util.converters import jsdate_to_time
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test.client import Client from django.test.client import Client
...@@ -15,69 +13,13 @@ from models.settings.course_details import (CourseDetails, ...@@ -15,69 +13,13 @@ from models.settings.course_details import (CourseDetails,
from models.settings.course_grading import CourseGradingModel from models.settings.course_grading import CourseGradingModel
from contentstore.utils import get_modulestore from contentstore.utils import get_modulestore
from django.test import TestCase
from .utils import ModuleStoreTestCase from .utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from models.settings.course_metadata import CourseMetadata from models.settings.course_metadata import CourseMetadata
from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
import time from xmodule.fields import Date
# 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")
class CourseTestCase(ModuleStoreTestCase): class CourseTestCase(ModuleStoreTestCase):
def setUp(self): def setUp(self):
...@@ -206,17 +148,24 @@ class CourseDetailsViewTest(CourseTestCase): ...@@ -206,17 +148,24 @@ class CourseDetailsViewTest(CourseTestCase):
self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==") self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==")
self.assertEqual(details['effort'], encoded['effort'], context + " efforts 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): def compare_date_fields(self, details, encoded, context, field):
if details[field] is not None: if details[field] is not None:
date = Date()
if field in encoded and encoded[field] is not None: if field in encoded and encoded[field] is not None:
encoded_encoded = jsdate_to_time(encoded[field]) encoded_encoded = date.from_json(encoded[field])
dt1 = ConvertersTestCase.struct_to_datetime(encoded_encoded) dt1 = CourseDetailsViewTest.struct_to_datetime(encoded_encoded)
if isinstance(details[field], datetime.datetime): if isinstance(details[field], datetime.datetime):
dt2 = details[field] dt2 = details[field]
else: else:
details_encoded = jsdate_to_time(details[field]) details_encoded = date.from_json(details[field])
dt2 = ConvertersTestCase.struct_to_datetime(details_encoded) dt2 = CourseDetailsViewTest.struct_to_datetime(details_encoded)
expected_delta = datetime.timedelta(0) expected_delta = datetime.timedelta(0)
self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "!=" + str(dt2) + " at " + context) self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "!=" + str(dt2) + " at " + context)
......
'''
Utilities for contentstore tests
'''
#pylint: disable=W0603
import json import json
import copy import copy
from uuid import uuid4 from uuid import uuid4
...@@ -17,36 +23,89 @@ class ModuleStoreTestCase(TestCase): ...@@ -17,36 +23,89 @@ class ModuleStoreTestCase(TestCase):
collection with templates before running the TestCase collection with templates before running the TestCase
and drops it they are finished. """ and drops it they are finished. """
def _pre_setup(self): @staticmethod
super(ModuleStoreTestCase, self)._pre_setup() 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 # Use a uuid to differentiate
# the mongo collections on jenkins. # the mongo collections on jenkins.
self.orig_MODULESTORE = copy.deepcopy(settings.MODULESTORE) cls.orig_modulestore = copy.deepcopy(settings.MODULESTORE)
self.test_MODULESTORE = self.orig_MODULESTORE test_modulestore = cls.orig_modulestore
self.test_MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex test_modulestore['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
self.test_MODULESTORE['direct']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex 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()"
xmodule.modulestore.django._MODULESTORES = {} 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): def _post_teardown(self):
# Make sure you flush out the modulestore. '''
# Drop the collection at the end of the test, Flush everything we created except the templates
# otherwise there will be lingering collections leftover '''
# from executing the tests. # Flush anything that is not a template
xmodule.modulestore.django._MODULESTORES = {} ModuleStoreTestCase.flush_mongo_except_templates()
xmodule.modulestore.django.modulestore().collection.drop()
settings.MODULESTORE = self.orig_MODULESTORE
# Call superclass implementation
super(ModuleStoreTestCase, self)._post_teardown() super(ModuleStoreTestCase, self)._post_teardown()
......
import logging
from django.conf import settings from django.conf import settings
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
import copy
DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info'] 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): def get_modulestore(location):
""" """
...@@ -137,7 +141,7 @@ def compute_unit_state(unit): ...@@ -137,7 +141,7 @@ def compute_unit_state(unit):
'private' content is editabled and not visible in the LMS 'private' content is editabled and not visible in the LMS
""" """
if unit.cms.is_draft: if getattr(unit, 'is_draft', False):
try: try:
modulestore('direct').get_item(unit.location) modulestore('direct').get_item(unit.location)
return UnitState.draft return UnitState.draft
...@@ -191,3 +195,35 @@ class CoursePageNames: ...@@ -191,3 +195,35 @@ class CoursePageNames:
SettingsGrading = "settings_grading" SettingsGrading = "settings_grading"
CourseOutline = "course_index" CourseOutline = "course_index"
Checklists = "checklists" 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
...@@ -42,7 +42,7 @@ from xmodule.modulestore.mongo import MongoUsage ...@@ -42,7 +42,7 @@ from xmodule.modulestore.mongo import MongoUsage
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule_modifiers import replace_static_urls, wrap_xmodule 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 functools import partial
from xmodule.contentstore.django import contentstore 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 ...@@ -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 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 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, \ 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 xmodule.modulestore.xml_importer import import_from_xml
from contentstore.course_info_model import get_course_updates, \ from contentstore.course_info_model import get_course_updates, \
...@@ -73,7 +74,8 @@ log = logging.getLogger(__name__) ...@@ -73,7 +74,8 @@ log = logging.getLogger(__name__)
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video'] 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_CATEGORY = 'advanced'
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules' ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
...@@ -188,7 +190,7 @@ def course_index(request, org, course, name): ...@@ -188,7 +190,7 @@ def course_index(request, org, course, name):
'coursename': name 'coursename': name
}) })
course = modulestore().get_item(location) course = modulestore().get_item(location, depth=3)
sections = course.get_children() sections = course.get_children()
return render_to_response('overview.html', { return render_to_response('overview.html', {
...@@ -208,19 +210,14 @@ def course_index(request, org, course, name): ...@@ -208,19 +210,14 @@ def course_index(request, org, course, name):
@login_required @login_required
def edit_subsection(request, location): def edit_subsection(request, location):
# check that we have permissions to edit this item # 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() 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 lms_link = get_lms_link_for_item(location, course_id=course.location.course_id)
for course in modulestore().get_courses(): preview_link = get_lms_link_for_item(location, course_id=course.location.course_id, preview=True)
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)
# make sure that location references a 'sequential', otherwise return BadRequest # make sure that location references a 'sequential', otherwise return BadRequest
if item.location.category != 'sequential': if item.location.category != 'sequential':
...@@ -277,19 +274,13 @@ def edit_unit(request, location): ...@@ -277,19 +274,13 @@ def edit_unit(request, location):
id: A Location URL id: A Location URL
""" """
# check that we have permissions to edit this item course = get_course_for_item(location)
if not has_access(request.user, location): if not has_access(request.user, course.location):
raise PermissionDenied() 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 lms_link = get_lms_link_for_item(item.location, course_id=course.location.course_id)
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)
component_templates = defaultdict(list) component_templates = defaultdict(list)
...@@ -448,9 +439,16 @@ def preview_dispatch(request, preview_id, location, dispatch=None): ...@@ -448,9 +439,16 @@ def preview_dispatch(request, preview_id, location, dispatch=None):
# Let the module handle the AJAX # Let the module handle the AJAX
try: try:
ajax_return = instance.handle_ajax(dispatch, request.POST) ajax_return = instance.handle_ajax(dispatch, request.POST)
except NotFoundError: except NotFoundError:
log.exception("Module indicating to user that request doesn't exist") log.exception("Module indicating to user that request doesn't exist")
raise Http404 raise Http404
except ProcessingError:
log.warning("Module raised an error while processing AJAX request",
exc_info=True)
return HttpResponseBadRequest()
except: except:
log.exception("error processing ajax call") log.exception("error processing ajax call")
raise raise
...@@ -1273,15 +1271,48 @@ def course_advanced_updates(request, org, course, name): ...@@ -1273,15 +1271,48 @@ def course_advanced_updates(request, org, course, name):
location = get_location_and_verify_access(request, org, course, name) location = get_location_and_verify_access(request, org, course, name)
real_method = get_request_method(request) real_method = get_request_method(request)
if real_method == 'GET': if real_method == 'GET':
return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json") return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json")
elif real_method == 'DELETE': 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': 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 # 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 @ensure_csrf_cookie
@login_required @login_required
......
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
...@@ -6,9 +5,9 @@ import json ...@@ -6,9 +5,9 @@ import json
from json.encoder import JSONEncoder from json.encoder import JSONEncoder
import time import time
from contentstore.utils import get_modulestore from contentstore.utils import get_modulestore
from util.converters import jsdate_to_time, time_to_date
from models.settings import course_grading from models.settings import course_grading
from contentstore.utils import update_item from contentstore.utils import update_item
from xmodule.fields import Date
import re import re
import logging import logging
...@@ -81,8 +80,14 @@ class CourseDetails(object): ...@@ -81,8 +80,14 @@ class CourseDetails(object):
dirty = False 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: if 'start_date' in jsondict:
converted = jsdate_to_time(jsondict['start_date']) converted = date.from_json(jsondict['start_date'])
else: else:
converted = None converted = None
if converted != descriptor.start: if converted != descriptor.start:
...@@ -90,7 +95,7 @@ class CourseDetails(object): ...@@ -90,7 +95,7 @@ class CourseDetails(object):
descriptor.start = converted descriptor.start = converted
if 'end_date' in jsondict: if 'end_date' in jsondict:
converted = jsdate_to_time(jsondict['end_date']) converted = date.from_json(jsondict['end_date'])
else: else:
converted = None converted = None
...@@ -99,7 +104,7 @@ class CourseDetails(object): ...@@ -99,7 +104,7 @@ class CourseDetails(object):
descriptor.end = converted descriptor.end = converted
if 'enrollment_start' in jsondict: if 'enrollment_start' in jsondict:
converted = jsdate_to_time(jsondict['enrollment_start']) converted = date.from_json(jsondict['enrollment_start'])
else: else:
converted = None converted = None
...@@ -108,7 +113,7 @@ class CourseDetails(object): ...@@ -108,7 +113,7 @@ class CourseDetails(object):
descriptor.enrollment_start = converted descriptor.enrollment_start = converted
if 'enrollment_end' in jsondict: if 'enrollment_end' in jsondict:
converted = jsdate_to_time(jsondict['enrollment_end']) converted = date.from_json(jsondict['enrollment_end'])
else: else:
converted = None converted = None
...@@ -178,6 +183,6 @@ class CourseSettingsEncoder(json.JSONEncoder): ...@@ -178,6 +183,6 @@ class CourseSettingsEncoder(json.JSONEncoder):
elif isinstance(obj, Location): elif isinstance(obj, Location):
return obj.dict() return obj.dict()
elif isinstance(obj, time.struct_time): elif isinstance(obj, time.struct_time):
return time_to_date(obj) return Date().to_json(obj)
else: else:
return JSONEncoder.default(self, obj) return JSONEncoder.default(self, obj)
from xmodule.modulestore import Location from xmodule.modulestore import Location
from contentstore.utils import get_modulestore from contentstore.utils import get_modulestore
import re
from util import converters
from datetime import timedelta from datetime import timedelta
......
...@@ -4,7 +4,7 @@ from xmodule.x_module import XModuleDescriptor ...@@ -4,7 +4,7 @@ from xmodule.x_module import XModuleDescriptor
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from xblock.core import Scope from xblock.core import Scope
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
import copy
class CourseMetadata(object): class CourseMetadata(object):
''' '''
...@@ -39,7 +39,7 @@ class CourseMetadata(object): ...@@ -39,7 +39,7 @@ class CourseMetadata(object):
return course return course
@classmethod @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. Decode the json into CourseMetadata and save any changed attrs to the db.
...@@ -48,10 +48,16 @@ class CourseMetadata(object): ...@@ -48,10 +48,16 @@ class CourseMetadata(object):
descriptor = get_modulestore(course_location).get_item(course_location) descriptor = get_modulestore(course_location).get_item(course_location)
dirty = False 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(): for k, v in jsondict.iteritems():
# should it be an error if one of the filtered list items is in the payload? # 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 continue
if hasattr(descriptor, k) and getattr(descriptor, k) != v: if hasattr(descriptor, k) and getattr(descriptor, k) != v:
......
...@@ -113,6 +113,7 @@ TEMPLATE_LOADERS = ( ...@@ -113,6 +113,7 @@ TEMPLATE_LOADERS = (
MIDDLEWARE_CLASSES = ( MIDDLEWARE_CLASSES = (
'contentserver.middleware.StaticContentServer', 'contentserver.middleware.StaticContentServer',
'request_cache.middleware.RequestCache',
'django.middleware.cache.UpdateCacheMiddleware', 'django.middleware.cache.UpdateCacheMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
......
...@@ -112,6 +112,10 @@ CACHE_TIMEOUT = 0 ...@@ -112,6 +112,10 @@ CACHE_TIMEOUT = 0
# Dummy secret key for dev # Dummy secret key for dev
SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd' 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 ################################# ################################ DEBUG TOOLBAR #################################
INSTALLED_APPS += ('debug_toolbar', 'debug_toolbar_mongo') INSTALLED_APPS += ('debug_toolbar', 'debug_toolbar_mongo')
MIDDLEWARE_CLASSES += ('django_comment_client.utils.QueryCountDebugMiddleware', MIDDLEWARE_CLASSES += ('django_comment_client.utils.QueryCountDebugMiddleware',
...@@ -142,4 +146,4 @@ DEBUG_TOOLBAR_CONFIG = { ...@@ -142,4 +146,4 @@ DEBUG_TOOLBAR_CONFIG = {
# To see stacktraces for MongoDB queries, set this to True. # To see stacktraces for MongoDB queries, set this to True.
# Stacktraces slow down page loads drastically (for pages with lots of queries). # Stacktraces slow down page loads drastically (for pages with lots of queries).
DEBUG_TOOLBAR_MONGO_STACKTRACES = False DEBUG_TOOLBAR_MONGO_STACKTRACES = True
...@@ -58,6 +58,10 @@ MODULESTORE = { ...@@ -58,6 +58,10 @@ MODULESTORE = {
'direct': { 'direct': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': modulestore_options 'OPTIONS': modulestore_options
},
'draft': {
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
'OPTIONS': modulestore_options
} }
} }
......
from dogapi import dog_http_api, dog_stats_api from dogapi import dog_http_api, dog_stats_api
from django.conf import settings from django.conf import settings
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from request_cache.middleware import RequestCache
from django.core.cache import get_cache, InvalidCacheBackendError from django.core.cache import get_cache, InvalidCacheBackendError
cache = get_cache('mongo_metadata_inheritance') cache = get_cache('mongo_metadata_inheritance')
for store_name in settings.MODULESTORE: for store_name in settings.MODULESTORE:
store = modulestore(store_name) 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'): if hasattr(settings, 'DATADOG_API'):
dog_http_api.api_key = settings.DATADOG_API dog_http_api.api_key = settings.DATADOG_API
......
...@@ -660,7 +660,7 @@ hr.divide { ...@@ -660,7 +660,7 @@ hr.divide {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
z-index: 99999; z-index: 10000;
padding: 0 10px; padding: 0 10px;
border-radius: 3px; border-radius: 3px;
background: rgba(0, 0, 0, 0.85); background: rgba(0, 0, 0, 0.85);
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
font-family: $sans-serif; font-family: $sans-serif;
font-size: 12px; font-size: 12px;
@include box-shadow(0 5px 10px rgba(0, 0, 0, 0.1)); @include box-shadow(0 5px 10px rgba(0, 0, 0, 0.1));
z-index: 100000 !important;
.ui-widget-header { .ui-widget-header {
background: $darkGrey; background: $darkGrey;
......
...@@ -200,7 +200,7 @@ ...@@ -200,7 +200,7 @@
</a> </a>
</div> </div>
<div class="gradable-status" data-initial-status="${subsection.lms.format if section.lms.format is not None else 'Not Graded'}"> <div class="gradable-status" data-initial-status="${subsection.lms.format if subsection.lms.format is not None else 'Not Graded'}">
</div> </div>
<div class="item-actions"> <div class="item-actions">
......
...@@ -40,7 +40,6 @@ class CmsNamespace(Namespace): ...@@ -40,7 +40,6 @@ class CmsNamespace(Namespace):
""" """
Namespace with fields common to all blocks in Studio 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_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) 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) empty = StringyBoolean(help="Whether this is an empty template", scope=Scope.settings, default=False)
...@@ -5,6 +5,7 @@ from django.http import HttpResponse, Http404, HttpResponseNotModified ...@@ -5,6 +5,7 @@ from django.http import HttpResponse, Http404, HttpResponseNotModified
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.contentstore.content import StaticContent, XASSET_LOCATION_TAG 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 cache_toolbox.core import get_cached_content, set_cached_content
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError
...@@ -13,7 +14,14 @@ class StaticContentServer(object): ...@@ -13,7 +14,14 @@ class StaticContentServer(object):
def process_request(self, request): def process_request(self, request):
# look to see if the request is prefixed with 'c4x' tag # look to see if the request is prefixed with 'c4x' tag
if request.path.startswith('/' + XASSET_LOCATION_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 # first look in our cache so we don't have to round-trip to the DB
content = get_cached_content(loc) content = get_cached_content(loc)
if content is None: if content is None:
......
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
...@@ -325,7 +325,12 @@ def change_enrollment(request): ...@@ -325,7 +325,12 @@ def change_enrollment(request):
"course:{0}".format(course_num), "course:{0}".format(course_num),
"run:{0}".format(run)]) "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} return {'success': True}
elif action == "unenroll": elif action == "unenroll":
...@@ -369,14 +374,14 @@ def login_user(request, error=""): ...@@ -369,14 +374,14 @@ def login_user(request, error=""):
try: try:
user = User.objects.get(email=email) user = User.objects.get(email=email)
except User.DoesNotExist: 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, return HttpResponse(json.dumps({'success': False,
'value': 'Email or password is incorrect.'})) # TODO: User error message 'value': 'Email or password is incorrect.'})) # TODO: User error message
username = user.username username = user.username
user = authenticate(username=username, password=password) user = authenticate(username=username, password=password)
if user is None: 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, return HttpResponse(json.dumps({'success': False,
'value': 'Email or password is incorrect.'})) 'value': 'Email or password is incorrect.'}))
...@@ -392,7 +397,7 @@ def login_user(request, error=""): ...@@ -392,7 +397,7 @@ def login_user(request, error=""):
log.critical("Login failed - Could not create session. Is memcached running?") log.critical("Login failed - Could not create session. Is memcached running?")
log.exception(e) 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) try_change_enrollment(request)
...@@ -400,7 +405,7 @@ def login_user(request, error=""): ...@@ -400,7 +405,7 @@ def login_user(request, error=""):
return HttpResponse(json.dumps({'success': True})) 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) reactivation_email_for_user(user)
not_activated_msg = "This account has not been activated. We have " + \ not_activated_msg = "This account has not been activated. We have " + \
......
#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()
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from .factories import * from .course_helpers import *
from .ui_helpers import *
from lettuce.django import django_url from lettuce.django import django_url
from django.conf import settings from nose.tools import assert_equals, assert_in
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
import time import time
import re
import os.path
from selenium.common.exceptions import WebDriverException
from logging import getLogger from logging import getLogger
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -22,7 +14,7 @@ logger = getLogger(__name__) ...@@ -22,7 +14,7 @@ logger = getLogger(__name__)
@step(u'I wait (?:for )?"(\d+)" seconds?$') @step(u'I wait (?:for )?"(\d+)" seconds?$')
def wait(step, seconds): def wait(step, seconds):
time.sleep(float(seconds)) world.wait(seconds)
@step('I reload the page$') @step('I reload the page$')
...@@ -37,42 +29,42 @@ def browser_back(step): ...@@ -37,42 +29,42 @@ def browser_back(step):
@step('I (?:visit|access|open) the homepage$') @step('I (?:visit|access|open) the homepage$')
def i_visit_the_homepage(step): def i_visit_the_homepage(step):
world.browser.visit(django_url('/')) world.visit('/')
assert world.browser.is_element_present_by_css('header.global', 10) assert world.is_css_present('header.global')
@step(u'I (?:visit|access|open) the dashboard$') @step(u'I (?:visit|access|open) the dashboard$')
def i_visit_the_dashboard(step): def i_visit_the_dashboard(step):
world.browser.visit(django_url('/dashboard')) world.visit('/dashboard')
assert world.browser.is_element_present_by_css('section.container.dashboard', 5) assert world.is_css_present('section.container.dashboard')
@step('I should be on the dashboard page$') @step('I should be on the dashboard page$')
def i_should_be_on_the_dashboard(step): 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' assert world.browser.title == 'Dashboard'
@step(u'I (?:visit|access|open) the courses page$') @step(u'I (?:visit|access|open) the courses page$')
def i_am_on_the_courses_page(step): def i_am_on_the_courses_page(step):
world.browser.visit(django_url('/courses')) world.visit('/courses')
assert world.browser.is_element_present_by_css('section.courses') assert world.is_css_present('section.courses')
@step(u'I press the "([^"]*)" button$') @step(u'I press the "([^"]*)" button$')
def and_i_press_the_button(step, value): def and_i_press_the_button(step, value):
button_css = 'input[value="%s"]' % 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 "([^"]*)"$') @step(u'I click the link with the text "([^"]*)"$')
def click_the_link_with_the_text_group1(step, linktext): 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 "([^"]*)"$') @step('I should see that the path is "([^"]*)"$')
def i_should_see_that_the_path_is(step, path): 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 "([^"]*)"$') @step(u'the page title should be "([^"]*)"$')
...@@ -85,10 +77,15 @@ def the_page_title_should_contain(step, title): ...@@ -85,10 +77,15 @@ def the_page_title_should_contain(step, title):
assert(title in world.browser.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$') @step('I am a logged in user$')
def i_am_logged_in_user(step): def i_am_logged_in_user(step):
create_user('robot') world.create_user('robot')
log_in('robot', 'test') world.log_in('robot', 'test')
@step('I am not logged in$') @step('I am not logged in$')
...@@ -98,151 +95,46 @@ def i_am_not_logged_in(step): ...@@ -98,151 +95,46 @@ def i_am_not_logged_in(step):
@step('I am staff for course "([^"]*)"$') @step('I am staff for course "([^"]*)"$')
def i_am_staff_for_course_by_id(step, course_id): 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$') @step(r'click (?:the|a) link (?:called|with the text) "([^"]*)"$')
def i_log_in(step): def click_the_link_called(step, text):
log_in('robot', 'test') world.click_link(text)
@step(u'I am an edX user$') @step(r'should see that the url is "([^"]*)"$')
def i_am_an_edx_user(step): def should_have_the_url(step, url):
create_user('robot') assert_equals(world.browser.url, url)
#### helper functions
@world.absorb
def scroll_to_bottom():
# Maximize the browser
world.browser.execute_script("window.scrollTo(0, screen.height);")
@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
@world.absorb
def create_user(uname):
# If the user already exists, don't try to create it again @step(r'should see "(.*)" (?:somewhere|anywhere) in (?:the|this) page')
if len(User.objects.filter(username=uname)) > 0: def should_see_in_the_page(step, text):
return assert_in(text, world.css_text('body'))
portal_user = UserFactory.build(username=uname, email=uname + '@edx.org')
portal_user.set_password('test')
portal_user.save()
registration = world.RegistrationFactory(user=portal_user) @step('I am logged in$')
registration.register(portal_user) def i_am_logged_in(step):
registration.activate() world.create_user('robot')
world.log_in('robot', 'test')
world.browser.visit(django_url('/'))
user_profile = world.UserProfileFactory(user=portal_user)
@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):
world.create_user('robot')
@world.absorb
def log_in(username, password):
'''
Log the user in programatically
'''
# Authenticate the user @step(u'User "([^"]*)" is an edX user$')
user = authenticate(username=username, password=password) def registered_edx_user(step, uname):
assert(user is not None and user.is_active) world.create_user(uname)
# 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()
#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
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)
...@@ -32,6 +32,8 @@ from copy import deepcopy ...@@ -32,6 +32,8 @@ from copy import deepcopy
import chem import chem
import chem.miller import chem.miller
import chem.chemcalc
import chem.chemtools
import verifiers import verifiers
import verifiers.draganddrop import verifiers.draganddrop
...@@ -67,6 +69,9 @@ global_context = {'random': random, ...@@ -67,6 +69,9 @@ global_context = {'random': random,
'scipy': scipy, 'scipy': scipy,
'calc': calc, 'calc': calc,
'eia': eia, 'eia': eia,
'chemcalc': chem.chemcalc,
'chemtools': chem.chemtools,
'miller': chem.miller,
'draganddrop': verifiers.draganddrop} 'draganddrop': verifiers.draganddrop}
# These should be removed from HTML output, including all subelements # These should be removed from HTML output, including all subelements
...@@ -118,7 +123,7 @@ class LoncapaProblem(object): ...@@ -118,7 +123,7 @@ class LoncapaProblem(object):
# 3. Assign from the OS's random number generator # 3. Assign from the OS's random number generator
self.seed = state.get('seed', seed) self.seed = state.get('seed', seed)
if self.seed is None: 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', {}) self.student_answers = state.get('student_answers', {})
if 'correct_map' in state: if 'correct_map' in state:
self.correct_map.set_dict(state['correct_map']) self.correct_map.set_dict(state['correct_map'])
......
...@@ -80,16 +80,17 @@ class CorrectMap(object): ...@@ -80,16 +80,17 @@ class CorrectMap(object):
Special migration case: Special migration case:
If correct_map is a one-level dict, then convert it to the new dict of dicts format. 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
# empty current dict self.__init__()
self.__init__()
# create new dict entries # create new dict entries
if correct_map and not isinstance(correct_map.values()[0], dict):
# special migration
for k in correct_map: for k in correct_map:
self.set(k, correct_map[k]) self.set(k, correctness=correct_map[k])
else: else:
self.__init__()
for k in correct_map: for k in correct_map:
self.set(k, **correct_map[k]) self.set(k, **correct_map[k])
......
...@@ -17,6 +17,7 @@ import logging ...@@ -17,6 +17,7 @@ import logging
import numbers import numbers
import numpy import numpy
import os import os
import sys
import random import random
import re import re
import requests import requests
...@@ -52,12 +53,17 @@ class LoncapaProblemError(Exception): ...@@ -52,12 +53,17 @@ class LoncapaProblemError(Exception):
class ResponseError(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 pass
class StudentInputError(Exception): class StudentInputError(Exception):
'''
Error for an invalid student input.
For example, submitting a string when the problem expects a number
'''
pass pass
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
...@@ -833,7 +839,7 @@ class NumericalResponse(LoncapaResponse): ...@@ -833,7 +839,7 @@ class NumericalResponse(LoncapaResponse):
import sys import sys
type, value, traceback = sys.exc_info() 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 cgi.escape(student_answer)), traceback
if correct: if correct:
...@@ -1072,13 +1078,10 @@ def sympy_check2(): ...@@ -1072,13 +1078,10 @@ def sympy_check2():
correct = self.context['correct'] correct = self.context['correct']
messages = self.context['messages'] messages = self.context['messages']
overall_message = self.context['overall_message'] overall_message = self.context['overall_message']
except Exception as err: except Exception as err:
print "oops in customresponse (code) error %s" % err self._handle_exec_exception(err)
print "context = ", self.context
print traceback.format_exc()
# Notify student
raise StudentInputError(
"Error: Problem could not be evaluated with your input")
else: else:
# self.code is not a string; assume its a function # self.code is not a string; assume its a function
...@@ -1105,13 +1108,9 @@ def sympy_check2(): ...@@ -1105,13 +1108,9 @@ def sympy_check2():
nargs, args, kwargs)) nargs, args, kwargs))
ret = fn(*args[:nargs], **kwargs) ret = fn(*args[:nargs], **kwargs)
except Exception as err: except Exception as err:
log.error("oops in customresponse (cfn) error %s" % err) self._handle_exec_exception(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)
if type(ret) == dict: if type(ret) == dict:
...@@ -1147,9 +1146,9 @@ def sympy_check2(): ...@@ -1147,9 +1146,9 @@ def sympy_check2():
correct = [] correct = []
messages = [] messages = []
for input_dict in input_list: for input_dict in input_list:
correct.append('correct' correct.append('correct'
if input_dict['ok'] else 'incorrect') 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) if 'msg' in input_dict else None)
messages.append(msg) messages.append(msg)
...@@ -1157,7 +1156,7 @@ def sympy_check2(): ...@@ -1157,7 +1156,7 @@ def sympy_check2():
# Raise an exception # Raise an exception
else: else:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise Exception( raise ResponseError(
"CustomResponse: check function returned an invalid dict") "CustomResponse: check function returned an invalid dict")
# The check function can return a boolean value, # The check function can return a boolean value,
...@@ -1174,7 +1173,7 @@ def sympy_check2(): ...@@ -1174,7 +1173,7 @@ def sympy_check2():
correct_map.set_overall_message(overall_message) correct_map.set_overall_message(overall_message)
for k in range(len(idset)): for k in range(len(idset)):
npoints = (self.maxpoints[idset[k]] npoints = (self.maxpoints[idset[k]]
if correct[k] == 'correct' else 0) if correct[k] == 'correct' else 0)
correct_map.set(idset[k], correct[k], msg=messages[k], correct_map.set(idset[k], correct[k], msg=messages[k],
npoints=npoints) npoints=npoints)
...@@ -1227,6 +1226,22 @@ def sympy_check2(): ...@@ -1227,6 +1226,22 @@ def sympy_check2():
return {self.answer_ids[0]: self.expect} return {self.answer_ids[0]: self.expect}
return self.default_answer_map 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): ...@@ -1901,7 +1916,14 @@ class SchematicResponse(LoncapaResponse):
submission = [json.loads(student_answers[ submission = [json.loads(student_answers[
k]) for k in sorted(self.answer_ids)] k]) for k in sorted(self.answer_ids)]
self.context.update({'submission': submission}) 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 = CorrectMap()
cmap.set_dict(dict(zip(sorted( cmap.set_dict(dict(zip(sorted(
self.answer_ids), self.context['correct']))) self.answer_ids), self.context['correct'])))
...@@ -1961,9 +1983,10 @@ class ImageResponse(LoncapaResponse): ...@@ -1961,9 +1983,10 @@ class ImageResponse(LoncapaResponse):
self.ielements = self.inputfields self.ielements = self.inputfields
self.answer_ids = [ie.get('id') for ie in self.ielements] self.answer_ids = [ie.get('id') for ie in self.ielements]
def get_score(self, student_answers): def get_score(self, student_answers):
correct_map = CorrectMap() correct_map = CorrectMap()
expectedset = self.get_answers() expectedset = self.get_mapped_answers()
for aid in self.answer_ids: # loop through IDs of <imageinput> for aid in self.answer_ids: # loop through IDs of <imageinput>
# fields in our stanza # fields in our stanza
given = student_answers[ given = student_answers[
...@@ -2018,11 +2041,42 @@ class ImageResponse(LoncapaResponse): ...@@ -2018,11 +2041,42 @@ class ImageResponse(LoncapaResponse):
break break
return correct_map return correct_map
def get_answers(self): def get_mapped_answers(self):
return ( '''
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( dict([(ie.get('id'), ie.get(
'rectangle')) for ie in self.ielements]), 'rectangle')) for ie in self.ielements]),
dict([(ie.get('id'), ie.get('regions')) 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): ...@@ -2074,7 +2128,7 @@ class AnnotationResponse(LoncapaResponse):
option_scoring = dict([(option['id'], { option_scoring = dict([(option['id'], {
'correctness': choices.get(option['choice']), 'correctness': choices.get(option['choice']),
'points': scoring.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 scoring_map[inputfield.get('id')] = option_scoring
...@@ -2087,8 +2141,8 @@ class AnnotationResponse(LoncapaResponse): ...@@ -2087,8 +2141,8 @@ class AnnotationResponse(LoncapaResponse):
correct_option = self._find_option_with_choice( correct_option = self._find_option_with_choice(
inputfield, 'correct') inputfield, 'correct')
if correct_option is not None: if correct_option is not None:
answer_map[inputfield.get( input_id = inputfield.get('id')
'id')] = correct_option.get('description') answer_map[input_id] = correct_option.get('description')
return answer_map return answer_map
def _get_max_points(self): def _get_max_points(self):
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
% for choice_id, choice_description in choices: % for choice_id, choice_description in choices:
<label for="input_${id}_${choice_id}" <label for="input_${id}_${choice_id}"
% if input_type == 'radio' and choice_id == value: % if input_type == 'radio' and ( (isinstance(value, basestring) and (choice_id == value)) or (not isinstance(value, basestring) and choice_id in value) ):
<% <%
if status == 'correct': if status == 'correct':
correctness = 'correct' correctness = 'correct'
...@@ -30,9 +30,9 @@ ...@@ -30,9 +30,9 @@
class="choicegroup_${correctness}" class="choicegroup_${correctness}"
% endif % endif
% endif % endif
> >
<input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" value="${choice_id}" <input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" value="${choice_id}"
% if input_type == 'radio' and choice_id == value: % if input_type == 'radio' and ( (isinstance(value, basestring) and (choice_id == value)) or (not isinstance(value, basestring) and choice_id in value) ):
checked="true" checked="true"
% elif input_type != 'radio' and choice_id in value: % elif input_type != 'radio' and choice_id in value:
checked="true" checked="true"
......
...@@ -13,6 +13,8 @@ import textwrap ...@@ -13,6 +13,8 @@ import textwrap
from . import test_system from . import test_system
import capa.capa_problem as lcp import capa.capa_problem as lcp
from capa.responsetypes import LoncapaProblemError, \
StudentInputError, ResponseError
from capa.correctmap import CorrectMap from capa.correctmap import CorrectMap
from capa.util import convert_files_to_filenames from capa.util import convert_files_to_filenames
from capa.xqueue_interface import dateformat from capa.xqueue_interface import dateformat
...@@ -36,6 +38,10 @@ class ResponseTest(unittest.TestCase): ...@@ -36,6 +38,10 @@ class ResponseTest(unittest.TestCase):
correct_map = problem.grade_answers(input_dict) correct_map = problem.grade_answers(input_dict)
self.assertEquals(correct_map.get_correctness('1_2_1'), expected_correctness) self.assertEquals(correct_map.get_correctness('1_2_1'), expected_correctness)
def assert_answer_format(self, problem):
answers = problem.get_question_answers()
self.assertTrue(answers['1_2_1'] is not None)
def assert_multiple_grade(self, problem, correct_answers, incorrect_answers): def assert_multiple_grade(self, problem, correct_answers, incorrect_answers):
for input_str in correct_answers: for input_str in correct_answers:
result = problem.grade_answers({'1_2_1': input_str}).get_correctness('1_2_1') result = problem.grade_answers({'1_2_1': input_str}).get_correctness('1_2_1')
...@@ -166,6 +172,13 @@ class ImageResponseTest(ResponseTest): ...@@ -166,6 +172,13 @@ class ImageResponseTest(ResponseTest):
incorrect_inputs = ["[0,0]", "[600,300]"] incorrect_inputs = ["[0,0]", "[600,300]"]
self.assert_multiple_grade(problem, correct_inputs, incorrect_inputs) self.assert_multiple_grade(problem, correct_inputs, incorrect_inputs)
def test_show_answer(self):
rectangle_str = "(100,100)-(200,200)"
region_str = "[[10,10], [20,10], [20, 30]]"
problem = self.build_problem(regions=region_str, rectangle=rectangle_str)
self.assert_answer_format(problem)
class SymbolicResponseTest(unittest.TestCase): class SymbolicResponseTest(unittest.TestCase):
def test_sr_grade(self): def test_sr_grade(self):
...@@ -853,7 +866,7 @@ class CustomResponseTest(ResponseTest): ...@@ -853,7 +866,7 @@ class CustomResponseTest(ResponseTest):
# Message is interpreted as an "overall message" # Message is interpreted as an "overall message"
self.assertEqual(correct_map.get_overall_message(), 'Message text') self.assertEqual(correct_map.get_overall_message(), 'Message text')
def test_script_exception(self): def test_script_exception_function(self):
# Construct a script that will raise an exception # Construct a script that will raise an exception
script = textwrap.dedent(""" script = textwrap.dedent("""
...@@ -864,7 +877,17 @@ class CustomResponseTest(ResponseTest): ...@@ -864,7 +877,17 @@ class CustomResponseTest(ResponseTest):
problem = self.build_problem(script=script, cfn="check_func") problem = self.build_problem(script=script, cfn="check_func")
# Expect that an exception gets raised when we check the answer # Expect that an exception gets raised when we check the answer
with self.assertRaises(Exception): with self.assertRaises(ResponseError):
problem.grade_answers({'1_2_1': '42'})
def test_script_exception_inline(self):
# Construct a script that will raise an exception
script = 'raise Exception("Test")'
problem = self.build_problem(answer=script)
# Expect that an exception gets raised when we check the answer
with self.assertRaises(ResponseError):
problem.grade_answers({'1_2_1': '42'}) problem.grade_answers({'1_2_1': '42'})
def test_invalid_dict_exception(self): def test_invalid_dict_exception(self):
...@@ -878,10 +901,70 @@ class CustomResponseTest(ResponseTest): ...@@ -878,10 +901,70 @@ class CustomResponseTest(ResponseTest):
problem = self.build_problem(script=script, cfn="check_func") problem = self.build_problem(script=script, cfn="check_func")
# Expect that an exception gets raised when we check the answer # Expect that an exception gets raised when we check the answer
with self.assertRaises(Exception): with self.assertRaises(ResponseError):
problem.grade_answers({'1_2_1': '42'}) problem.grade_answers({'1_2_1': '42'})
def test_module_imports_inline(self):
'''
Check that the correct modules are available to custom
response scripts
'''
for module_name in ['random', 'numpy', 'math', 'scipy',
'calc', 'eia', 'chemcalc', 'chemtools',
'miller', 'draganddrop']:
# Create a script that checks that the name is defined
# If the name is not defined, then the script
# will raise an exception
script = textwrap.dedent('''
correct[0] = 'correct'
assert('%s' in globals())''' % module_name)
# Create the problem
problem = self.build_problem(answer=script)
# Expect that we can grade an answer without
# getting an exception
try:
problem.grade_answers({'1_2_1': '42'})
except ResponseError:
self.fail("Could not use name '%s' in custom response"
% module_name)
def test_module_imports_function(self):
'''
Check that the correct modules are available to custom
response scripts
'''
for module_name in ['random', 'numpy', 'math', 'scipy',
'calc', 'eia', 'chemcalc', 'chemtools',
'miller', 'draganddrop']:
# Create a script that checks that the name is defined
# If the name is not defined, then the script
# will raise an exception
script = textwrap.dedent('''
def check_func(expect, answer_given):
assert('%s' in globals())
return True''' % module_name)
# Create the problem
problem = self.build_problem(script=script, cfn="check_func")
# Expect that we can grade an answer without
# getting an exception
try:
problem.grade_answers({'1_2_1': '42'})
except ResponseError:
self.fail("Could not use name '%s' in custom response"
% module_name)
class SchematicResponseTest(ResponseTest): class SchematicResponseTest(ResponseTest):
from response_xml_factory import SchematicResponseXMLFactory from response_xml_factory import SchematicResponseXMLFactory
xml_factory_class = SchematicResponseXMLFactory xml_factory_class = SchematicResponseXMLFactory
...@@ -911,6 +994,18 @@ class SchematicResponseTest(ResponseTest): ...@@ -911,6 +994,18 @@ class SchematicResponseTest(ResponseTest):
# is what we expect) # is what we expect)
self.assertEqual(correct_map.get_correctness('1_2_1'), 'correct') self.assertEqual(correct_map.get_correctness('1_2_1'), 'correct')
def test_script_exception(self):
# Construct a script that will raise an exception
script = "raise Exception('test')"
problem = self.build_problem(answer=script)
# Expect that an exception gets raised when we check the answer
with self.assertRaises(ResponseError):
submission_dict = {'test': 'test'}
input_dict = {'1_2_1': json.dumps(submission_dict)}
problem.grade_answers(input_dict)
class AnnotationResponseTest(ResponseTest): class AnnotationResponseTest(ResponseTest):
from response_xml_factory import AnnotationResponseXMLFactory from response_xml_factory import AnnotationResponseXMLFactory
......
...@@ -12,12 +12,13 @@ from lxml import etree ...@@ -12,12 +12,13 @@ from lxml import etree
from pkg_resources import resource_string from pkg_resources import resource_string
from capa.capa_problem import LoncapaProblem from capa.capa_problem import LoncapaProblem
from capa.responsetypes import StudentInputError from capa.responsetypes import StudentInputError, \
ResponseError, LoncapaProblemError
from capa.util import convert_files_to_filenames from capa.util import convert_files_to_filenames
from .progress import Progress from .progress import Progress
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError, ProcessingError
from xblock.core import Integer, Scope, BlockScope, ModelType, String, Boolean, Object, Float from xblock.core import Integer, Scope, BlockScope, ModelType, String, Boolean, Object, Float
from .fields import Timedelta from .fields import Timedelta
...@@ -93,7 +94,7 @@ class CapaFields(object): ...@@ -93,7 +94,7 @@ class CapaFields(object):
rerandomize = Randomization(help="When to rerandomize the problem", default="always", scope=Scope.settings) rerandomize = Randomization(help="When to rerandomize the problem", default="always", scope=Scope.settings)
data = String(help="XML data for the problem", scope=Scope.content) data = String(help="XML data for the problem", scope=Scope.content)
correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.student_state, default={}) correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.student_state, default={})
input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.student_state, default={}) input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.student_state)
student_answers = Object(help="Dictionary with the current student responses", scope=Scope.student_state) student_answers = Object(help="Dictionary with the current student responses", scope=Scope.student_state)
done = Boolean(help="Whether the student has answered the problem", scope=Scope.student_state) done = Boolean(help="Whether the student has answered the problem", scope=Scope.student_state)
display_name = String(help="Display name for this module", scope=Scope.settings) display_name = String(help="Display name for this module", scope=Scope.settings)
...@@ -150,6 +151,16 @@ class CapaModule(CapaFields, XModule): ...@@ -150,6 +151,16 @@ class CapaModule(CapaFields, XModule):
# TODO (vshnayder): move as much as possible of this work and error # TODO (vshnayder): move as much as possible of this work and error
# checking to descriptor load time # checking to descriptor load time
self.lcp = self.new_lcp(self.get_state_for_lcp()) self.lcp = self.new_lcp(self.get_state_for_lcp())
# At this point, we need to persist the randomization seed
# so that when the problem is re-loaded (to check/view/save)
# it stays the same.
# However, we do not want to write to the database
# every time the module is loaded.
# So we set the seed ONLY when there is not one set already
if self.seed is None:
self.seed = self.lcp.seed
except Exception as err: except Exception as err:
msg = 'cannot create LoncapaProblem {loc}: {err}'.format( msg = 'cannot create LoncapaProblem {loc}: {err}'.format(
loc=self.location.url(), err=err) loc=self.location.url(), err=err)
...@@ -454,7 +465,14 @@ class CapaModule(CapaFields, XModule): ...@@ -454,7 +465,14 @@ class CapaModule(CapaFields, XModule):
return 'Error' return 'Error'
before = self.get_progress() before = self.get_progress()
d = handlers[dispatch](get)
try:
d = handlers[dispatch](get)
except Exception as err:
_, _, traceback_obj = sys.exc_info()
raise ProcessingError, err.message, traceback_obj
after = self.get_progress() after = self.get_progress()
d.update({ d.update({
'progress_changed': after != before, 'progress_changed': after != before,
...@@ -576,7 +594,7 @@ class CapaModule(CapaFields, XModule): ...@@ -576,7 +594,7 @@ class CapaModule(CapaFields, XModule):
# save any state changes that may occur # save any state changes that may occur
self.set_state_from_lcp() self.set_state_from_lcp()
return response return response
def get_answer(self, get): def get_answer(self, get):
''' '''
...@@ -725,9 +743,24 @@ class CapaModule(CapaFields, XModule): ...@@ -725,9 +743,24 @@ class CapaModule(CapaFields, XModule):
try: try:
correct_map = self.lcp.grade_answers(answers) correct_map = self.lcp.grade_answers(answers)
self.set_state_from_lcp() self.set_state_from_lcp()
except StudentInputError as inst:
log.exception("StudentInputError in capa_module:problem_check") except (StudentInputError, ResponseError, LoncapaProblemError) as inst:
return {'success': inst.message} log.warning("StudentInputError in capa_module:problem_check",
exc_info=True)
# If the user is a staff member, include
# the full exception, including traceback,
# in the response
if self.system.user_is_staff:
msg = "Staff debug info: %s" % traceback.format_exc()
# Otherwise, display just an error message,
# without a stack trace
else:
msg = "Error: %s" % str(inst.message)
return {'success': msg}
except Exception, err: except Exception, err:
if self.system.DEBUG: if self.system.DEBUG:
msg = "Error checking problem: " + str(err) msg = "Error checking problem: " + str(err)
...@@ -778,7 +811,7 @@ class CapaModule(CapaFields, XModule): ...@@ -778,7 +811,7 @@ class CapaModule(CapaFields, XModule):
event_info['answers'] = answers event_info['answers'] = answers
# Too late. Cannot submit # Too late. Cannot submit
if self.closed() and not self.max_attempts ==0: if self.closed() and not self.max_attempts == 0:
event_info['failure'] = 'closed' event_info['failure'] = 'closed'
self.system.track_function('save_problem_fail', event_info) self.system.track_function('save_problem_fail', event_info)
return {'success': False, return {'success': False,
...@@ -798,7 +831,7 @@ class CapaModule(CapaFields, XModule): ...@@ -798,7 +831,7 @@ class CapaModule(CapaFields, XModule):
self.system.track_function('save_problem_success', event_info) self.system.track_function('save_problem_success', event_info)
msg = "Your answers have been saved" msg = "Your answers have been saved"
if not self.max_attempts ==0: if not self.max_attempts == 0:
msg += " but not graded. Hit 'Check' to grade them." msg += " but not graded. Hit 'Check' to grade them."
return {'success': True, return {'success': True,
'msg': msg} 'msg': msg}
......
...@@ -6,14 +6,15 @@ from pkg_resources import resource_string ...@@ -6,14 +6,15 @@ from pkg_resources import resource_string
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from .x_module import XModule from .x_module import XModule
from xblock.core import Integer, Scope, BlockScope, ModelType, String, Boolean, Object, Float, List from xblock.core import Integer, Scope, BlockScope, ModelType, String, Boolean, Object, List
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
from collections import namedtuple from collections import namedtuple
from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
V1_SETTINGS_ATTRIBUTES = ["display_name", "attempts", "is_graded", "accept_file_upload", V1_SETTINGS_ATTRIBUTES = ["display_name", "attempts", "is_graded", "accept_file_upload",
"skip_spelling_checks", "due", "graceperiod", "max_score"] "skip_spelling_checks", "due", "graceperiod"]
V1_STUDENT_ATTRIBUTES = ["current_task_number", "task_states", "state", V1_STUDENT_ATTRIBUTES = ["current_task_number", "task_states", "state",
"student_attempts", "ready_to_reset"] "student_attempts", "ready_to_reset"]
...@@ -66,9 +67,9 @@ class CombinedOpenEndedFields(object): ...@@ -66,9 +67,9 @@ class CombinedOpenEndedFields(object):
due = String(help="Date that this problem is due by", default=None, scope=Scope.settings) due = String(help="Date that this problem is due by", default=None, scope=Scope.settings)
graceperiod = String(help="Amount of time after the due date that submissions will be accepted", default=None, graceperiod = String(help="Amount of time after the due date that submissions will be accepted", default=None,
scope=Scope.settings) scope=Scope.settings)
max_score = Integer(help="Maximum score for the problem.", default=1, scope=Scope.settings)
version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings) version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings)
data = String(help="XML data for the problem", scope=Scope.content) data = String(help="XML data for the problem", scope=Scope.content)
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)
class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
...@@ -118,7 +119,7 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): ...@@ -118,7 +119,7 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
Definition file should have one or many task blocks, a rubric block, and a prompt block: Definition file should have one or many task blocks, a rubric block, and a prompt block:
Sample file: Sample file:
<combinedopenended attempts="10000" max_score="1"> <combinedopenended attempts="10000">
<rubric> <rubric>
Blah blah rubric. Blah blah rubric.
</rubric> </rubric>
...@@ -190,8 +191,8 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): ...@@ -190,8 +191,8 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
def get_score(self): def get_score(self):
return self.child_module.get_score() return self.child_module.get_score()
#def max_score(self): def max_score(self):
# return self.child_module.max_score() return self.child_module.max_score()
def get_progress(self): def get_progress(self):
return self.child_module.get_progress() return self.child_module.get_progress()
......
...@@ -635,8 +635,17 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -635,8 +635,17 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
@property @property
def start_date_text(self): def start_date_text(self):
def try_parse_iso_8601(text):
try:
result = datetime.strptime(text, "%Y-%m-%dT%H:%M")
result = result.strftime("%b %d, %Y")
except ValueError:
result = text.title()
return result
if isinstance(self.advertised_start, basestring): if isinstance(self.advertised_start, basestring):
return self.advertised_start return try_parse_iso_8601(self.advertised_start)
elif self.advertised_start is None and self.start is None: elif self.advertised_start is None and self.start is None:
return 'TBD' return 'TBD'
else: else:
......
class InvalidDefinitionError(Exception): class InvalidDefinitionError(Exception):
pass pass
class NotFoundError(Exception): class NotFoundError(Exception):
pass pass
class ProcessingError(Exception):
'''
An error occurred while processing a request to the XModule.
For example: if an exception occurs while checking a capa problem.
'''
pass
...@@ -14,7 +14,6 @@ class Date(ModelType): ...@@ -14,7 +14,6 @@ class Date(ModelType):
''' '''
Date fields know how to parse and produce json (iso) compatible formats. Date fields know how to parse and produce json (iso) compatible formats.
''' '''
# NB: these are copies of util.converters.*
def from_json(self, field): def from_json(self, field):
""" """
Parse an optional metadata key containing a time: if present, complain Parse an optional metadata key containing a time: if present, complain
......
...@@ -10,6 +10,7 @@ from collections import namedtuple ...@@ -10,6 +10,7 @@ from collections import namedtuple
from .exceptions import InvalidLocationError, InsufficientSpecificationError from .exceptions import InvalidLocationError, InsufficientSpecificationError
from xmodule.errortracker import ErrorLog, make_error_tracker from xmodule.errortracker import ErrorLog, make_error_tracker
from bson.son import SON
log = logging.getLogger('mitx.' + 'modulestore') log = logging.getLogger('mitx.' + 'modulestore')
...@@ -457,3 +458,13 @@ class ModuleStoreBase(ModuleStore): ...@@ -457,3 +458,13 @@ class ModuleStoreBase(ModuleStore):
if c.id == course_id: if c.id == course_id:
return c return c
return None return None
def namedtuple_to_son(namedtuple, prefix=''):
"""
Converts a namedtuple into a SON object with the same key order
"""
son = SON()
for idx, field_name in enumerate(namedtuple._fields):
son[prefix + field_name] = namedtuple[idx]
return son
from datetime import datetime from datetime import datetime
from . import ModuleStoreBase, Location from . import ModuleStoreBase, Location, namedtuple_to_son
from .exceptions import ItemNotFoundError from .exceptions import ItemNotFoundError
import logging
DRAFT = 'draft' DRAFT = 'draft'
...@@ -15,11 +16,11 @@ def as_draft(location): ...@@ -15,11 +16,11 @@ def as_draft(location):
def wrap_draft(item): def wrap_draft(item):
""" """
Sets `item.cms.is_draft` to `True` if the item is a Sets `item.is_draft` to `True` if the item is a
draft, and `False` otherwise. Sets the item's location to the draft, and `False` otherwise. Sets the item's location to the
non-draft location in either case non-draft location in either case
""" """
item.cms.is_draft = item.location.revision == DRAFT setattr(item, 'is_draft', item.location.revision == DRAFT)
item.location = item.location._replace(revision=None) item.location = item.location._replace(revision=None)
return item return item
...@@ -55,11 +56,10 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -55,11 +56,10 @@ class DraftModuleStore(ModuleStoreBase):
get_children() to cache. None indicates to cache all descendents get_children() to cache. None indicates to cache all descendents
""" """
# cdodge: we're forcing depth=0 here as the Draft store is not handling caching well
try: try:
return wrap_draft(super(DraftModuleStore, self).get_item(as_draft(location), depth=0)) return wrap_draft(super(DraftModuleStore, self).get_item(as_draft(location), depth=depth))
except ItemNotFoundError: except ItemNotFoundError:
return wrap_draft(super(DraftModuleStore, self).get_item(location, depth=0)) return wrap_draft(super(DraftModuleStore, self).get_item(location, depth=depth))
def get_instance(self, course_id, location, depth=0): def get_instance(self, course_id, location, depth=0):
""" """
...@@ -67,11 +67,10 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -67,11 +67,10 @@ class DraftModuleStore(ModuleStoreBase):
TODO (vshnayder): this may want to live outside the modulestore eventually TODO (vshnayder): this may want to live outside the modulestore eventually
""" """
# cdodge: we're forcing depth=0 here as the Draft store is not handling caching well
try: try:
return wrap_draft(super(DraftModuleStore, self).get_instance(course_id, as_draft(location), depth=0)) return wrap_draft(super(DraftModuleStore, self).get_instance(course_id, as_draft(location), depth=depth))
except ItemNotFoundError: except ItemNotFoundError:
return wrap_draft(super(DraftModuleStore, self).get_instance(course_id, location, depth=0)) return wrap_draft(super(DraftModuleStore, self).get_instance(course_id, location, depth=depth))
def get_items(self, location, course_id=None, depth=0): def get_items(self, location, course_id=None, depth=0):
""" """
...@@ -88,9 +87,8 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -88,9 +87,8 @@ class DraftModuleStore(ModuleStoreBase):
""" """
draft_loc = as_draft(location) draft_loc = as_draft(location)
# cdodge: we're forcing depth=0 here as the Draft store is not handling caching well draft_items = super(DraftModuleStore, self).get_items(draft_loc, course_id=course_id, depth=depth)
draft_items = super(DraftModuleStore, self).get_items(draft_loc, course_id=course_id, depth=0) items = super(DraftModuleStore, self).get_items(location, course_id=course_id, depth=depth)
items = super(DraftModuleStore, self).get_items(location, course_id=course_id, depth=0)
draft_locs_found = set(item.location._replace(revision=None) for item in draft_items) draft_locs_found = set(item.location._replace(revision=None) for item in draft_items)
non_draft_items = [ non_draft_items = [
...@@ -118,7 +116,7 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -118,7 +116,7 @@ class DraftModuleStore(ModuleStoreBase):
""" """
draft_loc = as_draft(location) draft_loc = as_draft(location)
draft_item = self.get_item(location) draft_item = self.get_item(location)
if not draft_item.cms.is_draft: if not getattr(draft_item, 'is_draft', False):
self.clone_item(location, draft_loc) self.clone_item(location, draft_loc)
return super(DraftModuleStore, self).update_item(draft_loc, data) return super(DraftModuleStore, self).update_item(draft_loc, data)
...@@ -133,7 +131,7 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -133,7 +131,7 @@ class DraftModuleStore(ModuleStoreBase):
""" """
draft_loc = as_draft(location) draft_loc = as_draft(location)
draft_item = self.get_item(location) draft_item = self.get_item(location)
if not draft_item.cms.is_draft: if not getattr(draft_item, 'is_draft', False):
self.clone_item(location, draft_loc) self.clone_item(location, draft_loc)
return super(DraftModuleStore, self).update_children(draft_loc, children) return super(DraftModuleStore, self).update_children(draft_loc, children)
...@@ -149,7 +147,7 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -149,7 +147,7 @@ class DraftModuleStore(ModuleStoreBase):
draft_loc = as_draft(location) draft_loc = as_draft(location)
draft_item = self.get_item(location) draft_item = self.get_item(location)
if not draft_item.cms.is_draft: if not getattr(draft_item, 'is_draft', False):
self.clone_item(location, draft_loc) self.clone_item(location, draft_loc)
if 'is_draft' in metadata: if 'is_draft' in metadata:
...@@ -192,3 +190,36 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -192,3 +190,36 @@ class DraftModuleStore(ModuleStoreBase):
""" """
super(DraftModuleStore, self).clone_item(location, as_draft(location)) super(DraftModuleStore, self).clone_item(location, as_draft(location))
super(DraftModuleStore, self).delete_item(location) super(DraftModuleStore, self).delete_item(location)
def _query_children_for_cache_children(self, items):
# first get non-draft in a round-trip
queried_children = []
to_process_non_drafts = super(DraftModuleStore, self)._query_children_for_cache_children(items)
to_process_dict = {}
for non_draft in to_process_non_drafts:
to_process_dict[Location(non_draft["_id"])] = non_draft
# now query all draft content in another round-trip
query = {
'_id': {'$in': [namedtuple_to_son(as_draft(Location(item))) for item in items]}
}
to_process_drafts = list(self.collection.find(query))
# now we have to go through all drafts and replace the non-draft
# with the draft. This is because the semantics of the DraftStore is to
# always return the draft - if available
for draft in to_process_drafts:
draft_loc = Location(draft["_id"])
draft_as_non_draft_loc = draft_loc._replace(revision=None)
# does non-draft exist in the collection
# if so, replace it
if draft_as_non_draft_loc in to_process_dict:
to_process_dict[draft_as_non_draft_loc] = draft
# convert the dict - which is used for look ups - back into a list
for key, value in to_process_dict.iteritems():
queried_children.append(value)
return queried_children
...@@ -136,3 +136,4 @@ def delete_course(modulestore, contentstore, source_location, commit = False): ...@@ -136,3 +136,4 @@ def delete_course(modulestore, contentstore, source_location, commit = False):
modulestore.delete_item(source_location) modulestore.delete_item(source_location)
return True return True
import pymongo import pymongo
from nose.tools import assert_equals, assert_raises, assert_not_equals, with_setup from mock import Mock
from nose.tools import assert_equals, assert_raises, assert_not_equals, with_setup, assert_false
from pprint import pprint from pprint import pprint
from xmodule.modulestore import Location from xmodule.modulestore import Location
......
...@@ -356,6 +356,26 @@ def remap_namespace(module, target_location_namespace): ...@@ -356,6 +356,26 @@ def remap_namespace(module, target_location_namespace):
return module return module
def validate_no_non_editable_metadata(module_store, course_id, category, allowed=[]):
'''
Assert that there is no metadata within a particular category that we can't support editing
However we always allow display_name and 'xml_attribtues'
'''
allowed = allowed + ['xml_attributes', 'display_name']
err_cnt = 0
for module_loc in module_store.modules[course_id]:
module = module_store.modules[course_id][module_loc]
if module.location.category == category:
my_metadata = dict(own_metadata(module))
for key in my_metadata.keys():
if key not in allowed:
err_cnt = err_cnt + 1
print ': found metadata on {0}. Studio will not support editing this piece of metadata, so it is not allowed. Metadata: {1} = {2}'. format(module.location.url(), key, my_metadata[key])
return err_cnt
def validate_category_hierarchy(module_store, course_id, parent_category, expected_child_category): def validate_category_hierarchy(module_store, course_id, parent_category, expected_child_category):
err_cnt = 0 err_cnt = 0
...@@ -440,6 +460,13 @@ def perform_xlint(data_dir, course_dirs, ...@@ -440,6 +460,13 @@ def perform_xlint(data_dir, course_dirs,
err_cnt += validate_category_hierarchy(module_store, course_id, "chapter", "sequential") err_cnt += validate_category_hierarchy(module_store, course_id, "chapter", "sequential")
# constrain that sequentials only have 'verticals' # constrain that sequentials only have 'verticals'
err_cnt += validate_category_hierarchy(module_store, course_id, "sequential", "vertical") err_cnt += validate_category_hierarchy(module_store, course_id, "sequential", "vertical")
# don't allow metadata on verticals, since we can't edit them in studio
err_cnt += validate_no_non_editable_metadata(module_store, course_id, "vertical")
# don't allow metadata on chapters, since we can't edit them in studio
err_cnt += validate_no_non_editable_metadata(module_store, course_id, "chapter",['start'])
# don't allow metadata on sequences that we can't edit
err_cnt += validate_no_non_editable_metadata(module_store, course_id, "sequential",
['due','format','start','graded'])
# check for a presence of a course marketing video # check for a presence of a course marketing video
location_elements = course_id.split('/') location_elements = course_id.split('/')
...@@ -456,3 +483,5 @@ def perform_xlint(data_dir, course_dirs, ...@@ -456,3 +483,5 @@ def perform_xlint(data_dir, course_dirs,
print "This course can be imported, but some errors may occur during the run of the course. It is recommend that you fix your courseware before importing" print "This course can be imported, but some errors may occur during the run of the course. It is recommend that you fix your courseware before importing"
else: else:
print "This course can be imported successfully." print "This course can be imported successfully."
return err_cnt
...@@ -19,12 +19,8 @@ log = logging.getLogger("mitx.courseware") ...@@ -19,12 +19,8 @@ log = logging.getLogger("mitx.courseware")
# attempts specified in xml definition overrides this. # attempts specified in xml definition overrides this.
MAX_ATTEMPTS = 1 MAX_ATTEMPTS = 1
# Set maximum available number of points.
# Overriden by max_score specified in xml.
MAX_SCORE = 1
#The highest score allowed for the overall xmodule and for each rubric point #The highest score allowed for the overall xmodule and for each rubric point
MAX_SCORE_ALLOWED = 3 MAX_SCORE_ALLOWED = 50
#If true, default behavior is to score module as a practice problem. Otherwise, no grade at all is shown in progress #If true, default behavior is to score module as a practice problem. Otherwise, no grade at all is shown in progress
#Metadata overrides this. #Metadata overrides this.
...@@ -88,7 +84,7 @@ class CombinedOpenEndedV1Module(): ...@@ -88,7 +84,7 @@ class CombinedOpenEndedV1Module():
Definition file should have one or many task blocks, a rubric block, and a prompt block: Definition file should have one or many task blocks, a rubric block, and a prompt block:
Sample file: Sample file:
<combinedopenended attempts="10000" max_score="1"> <combinedopenended attempts="10000">
<rubric> <rubric>
Blah blah rubric. Blah blah rubric.
</rubric> </rubric>
...@@ -153,13 +149,9 @@ class CombinedOpenEndedV1Module(): ...@@ -153,13 +149,9 @@ class CombinedOpenEndedV1Module():
raise raise
self.display_due_date = self.timeinfo.display_due_date self.display_due_date = self.timeinfo.display_due_date
# Used for progress / grading. Currently get credit just for
# completion (doesn't matter if you self-assessed correct/incorrect).
self._max_score = self.instance_state.get('max_score', MAX_SCORE)
self.rubric_renderer = CombinedOpenEndedRubric(system, True) self.rubric_renderer = CombinedOpenEndedRubric(system, True)
rubric_string = stringify_children(definition['rubric']) rubric_string = stringify_children(definition['rubric'])
self.rubric_renderer.check_if_rubric_is_parseable(rubric_string, location, MAX_SCORE_ALLOWED, self._max_score) self._max_score = self.rubric_renderer.check_if_rubric_is_parseable(rubric_string, location, MAX_SCORE_ALLOWED)
#Static data is passed to the child modules to render #Static data is passed to the child modules to render
self.static_data = { self.static_data = {
...@@ -363,7 +355,15 @@ class CombinedOpenEndedV1Module(): ...@@ -363,7 +355,15 @@ class CombinedOpenEndedV1Module():
""" """
self.update_task_states() self.update_task_states()
html = self.current_task.get_html(self.system) html = self.current_task.get_html(self.system)
return_html = rewrite_links(html, self.rewrite_content_links) return_html = html
try:
#Without try except block, get this error:
# File "/home/vik/mitx_all/mitx/common/lib/xmodule/xmodule/x_module.py", line 263, in rewrite_content_links
# if link.startswith(XASSET_SRCREF_PREFIX):
# Placing try except so that if the error is fixed, this code will start working again.
return_html = rewrite_links(html, self.rewrite_content_links)
except:
pass
return return_html return return_html
def get_current_attributes(self, task_number): def get_current_attributes(self, task_number):
...@@ -782,7 +782,7 @@ class CombinedOpenEndedV1Descriptor(): ...@@ -782,7 +782,7 @@ class CombinedOpenEndedV1Descriptor():
template_dir_name = "combinedopenended" template_dir_name = "combinedopenended"
def __init__(self, system): def __init__(self, system):
self.system =system self.system = system
@classmethod @classmethod
def definition_from_xml(cls, xml_object, system): def definition_from_xml(cls, xml_object, system):
......
...@@ -79,7 +79,7 @@ class CombinedOpenEndedRubric(object): ...@@ -79,7 +79,7 @@ class CombinedOpenEndedRubric(object):
raise RubricParsingError(error_message) raise RubricParsingError(error_message)
return {'success': success, 'html': html, 'rubric_scores': rubric_scores} return {'success': success, 'html': html, 'rubric_scores': rubric_scores}
def check_if_rubric_is_parseable(self, rubric_string, location, max_score_allowed, max_score): def check_if_rubric_is_parseable(self, rubric_string, location, max_score_allowed):
rubric_dict = self.render_rubric(rubric_string) rubric_dict = self.render_rubric(rubric_string)
success = rubric_dict['success'] success = rubric_dict['success']
rubric_feedback = rubric_dict['html'] rubric_feedback = rubric_dict['html']
...@@ -101,12 +101,7 @@ class CombinedOpenEndedRubric(object): ...@@ -101,12 +101,7 @@ class CombinedOpenEndedRubric(object):
log.error(error_message) log.error(error_message)
raise RubricParsingError(error_message) raise RubricParsingError(error_message)
if int(total) != int(max_score): return int(total)
#This is a staff_facing_error
error_msg = "The max score {0} for problem {1} does not match the total number of points in the rubric {2}. Contact the learning sciences group for assistance.".format(
max_score, location, total)
log.error(error_msg)
raise RubricParsingError(error_msg)
def extract_categories(self, element): def extract_categories(self, element):
''' '''
......
...@@ -36,7 +36,7 @@ ALLOWABLE_IMAGE_SUFFIXES = [ ...@@ -36,7 +36,7 @@ ALLOWABLE_IMAGE_SUFFIXES = [
] ]
#Maximum allowed dimensions (x and y) for an uploaded image #Maximum allowed dimensions (x and y) for an uploaded image
MAX_ALLOWED_IMAGE_DIM = 1500 MAX_ALLOWED_IMAGE_DIM = 2000
#Dimensions to which image is resized before it is evaluated for color count, etc #Dimensions to which image is resized before it is evaluated for color count, etc
MAX_IMAGE_DIM = 150 MAX_IMAGE_DIM = 150
...@@ -178,7 +178,7 @@ class URLProperties(object): ...@@ -178,7 +178,7 @@ class URLProperties(object):
Runs all available url tests Runs all available url tests
@return: True if URL passes tests, false if not. @return: True if URL passes tests, false if not.
""" """
url_is_okay = self.check_suffix() and self.check_if_parses() and self.check_domain() url_is_okay = self.check_suffix() and self.check_if_parses()
return url_is_okay return url_is_okay
def check_domain(self): def check_domain(self):
......
...@@ -357,10 +357,6 @@ class OpenEndedChild(object): ...@@ -357,10 +357,6 @@ class OpenEndedChild(object):
if get_data['can_upload_files'] in ['true', '1']: if get_data['can_upload_files'] in ['true', '1']:
has_file_to_upload = True has_file_to_upload = True
file = get_data['student_file'][0] file = get_data['student_file'][0]
if self.system.track_fuction:
self.system.track_function('open_ended_image_upload', {'filename': file.name})
else:
log.info("No tracking function found when uploading image.")
uploaded_to_s3, image_ok, s3_public_url = self.upload_image_to_s3(file) uploaded_to_s3, image_ok, s3_public_url = self.upload_image_to_s3(file)
if uploaded_to_s3: if uploaded_to_s3:
image_tag = self.generate_image_tag_from_url(s3_public_url, file.name) image_tag = self.generate_image_tag_from_url(s3_public_url, file.name)
......
from xblock.core import Integer, Float
class StringyFloat(Float):
"""
A model type that converts from string to floats when reading from json
"""
def from_json(self, value):
try:
return float(value)
except:
return None
...@@ -13,6 +13,7 @@ from xmodule.modulestore import Location ...@@ -13,6 +13,7 @@ from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from .timeinfo import TimeInfo from .timeinfo import TimeInfo
from xblock.core import Object, Integer, Boolean, String, Scope from xblock.core import Object, Integer, Boolean, String, Scope
from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
...@@ -28,13 +29,18 @@ EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please ...@@ -28,13 +29,18 @@ EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please
class PeerGradingFields(object): class PeerGradingFields(object):
use_for_single_location = Boolean(help="Whether to use this for a single location or as a panel.", default=USE_FOR_SINGLE_LOCATION, scope=Scope.settings) use_for_single_location = Boolean(help="Whether to use this for a single location or as a panel.",
link_to_location = String(help="The location this problem is linked to.", default=LINK_TO_LOCATION, scope=Scope.settings) default=USE_FOR_SINGLE_LOCATION, scope=Scope.settings)
is_graded = Boolean(help="Whether or not this module is scored.",default=IS_GRADED, scope=Scope.settings) link_to_location = String(help="The location this problem is linked to.", default=LINK_TO_LOCATION,
scope=Scope.settings)
is_graded = Boolean(help="Whether or not this module is scored.", default=IS_GRADED, scope=Scope.settings)
display_due_date_string = String(help="Due date that should be displayed.", default=None, scope=Scope.settings) display_due_date_string = String(help="Due date that should be displayed.", default=None, scope=Scope.settings)
grace_period_string = String(help="Amount of grace to give on the due date.", default=None, scope=Scope.settings) grace_period_string = String(help="Amount of grace to give on the due date.", default=None, scope=Scope.settings)
max_grade = Integer(help="The maximum grade that a student can receieve for this problem.", default=MAX_SCORE, scope=Scope.settings) max_grade = Integer(help="The maximum grade that a student can receieve for this problem.", default=MAX_SCORE,
student_data_for_location = Object(help="Student data for a given peer grading problem.", default=json.dumps({}),scope=Scope.student_state) scope=Scope.settings)
student_data_for_location = Object(help="Student data for a given peer grading problem.", default=json.dumps({}),
scope=Scope.student_state)
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)
class PeerGradingModule(PeerGradingFields, XModule): class PeerGradingModule(PeerGradingFields, XModule):
......
...@@ -2,12 +2,12 @@ ...@@ -2,12 +2,12 @@
metadata: metadata:
display_name: Open Ended Response display_name: Open Ended Response
max_attempts: 1 max_attempts: 1
max_score: 1
is_graded: False is_graded: False
version: 1 version: 1
display_name: Open Ended Response display_name: Open Ended Response
skip_spelling_checks: False skip_spelling_checks: False
accept_file_upload: False accept_file_upload: False
weight: ""
data: | data: |
<combinedopenended> <combinedopenended>
<rubric> <rubric>
......
...@@ -6,6 +6,7 @@ metadata: ...@@ -6,6 +6,7 @@ metadata:
link_to_location: None link_to_location: None
is_graded: False is_graded: False
max_grade: 1 max_grade: 1
weight: ""
data: | data: |
<peergrading> <peergrading>
</peergrading> </peergrading>
......
...@@ -5,11 +5,15 @@ import unittest ...@@ -5,11 +5,15 @@ import unittest
from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild
from xmodule.open_ended_grading_classes.open_ended_module import OpenEndedModule from xmodule.open_ended_grading_classes.open_ended_module import OpenEndedModule
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module
from xmodule.combined_open_ended_module import CombinedOpenEndedModule
from xmodule.modulestore import Location from xmodule.modulestore import Location
from lxml import etree from lxml import etree
import capa.xqueue_interface as xqueue_interface import capa.xqueue_interface as xqueue_interface
from datetime import datetime from datetime import datetime
import logging
log = logging.getLogger(__name__)
from . import test_system from . import test_system
...@@ -57,7 +61,7 @@ class OpenEndedChildTest(unittest.TestCase): ...@@ -57,7 +61,7 @@ class OpenEndedChildTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.test_system = test_system() self.test_system = test_system()
self.openendedchild = OpenEndedChild(self.test_system, self.location, self.openendedchild = OpenEndedChild(self.test_system, self.location,
self.definition, self.descriptor, self.static_data, self.metadata) self.definition, self.descriptor, self.static_data, self.metadata)
def test_latest_answer_empty(self): def test_latest_answer_empty(self):
...@@ -183,10 +187,12 @@ class OpenEndedModuleTest(unittest.TestCase): ...@@ -183,10 +187,12 @@ class OpenEndedModuleTest(unittest.TestCase):
self.test_system.location = self.location self.test_system.location = self.location
self.mock_xqueue = MagicMock() self.mock_xqueue = MagicMock()
self.mock_xqueue.send_to_queue.return_value = (None, "Message") self.mock_xqueue.send_to_queue.return_value = (None, "Message")
def constructed_callback(dispatch="score_update"): def constructed_callback(dispatch="score_update"):
return dispatch return dispatch
self.test_system.xqueue = {'interface': self.mock_xqueue, 'construct_callback': constructed_callback, 'default_queuename': 'testqueue', self.test_system.xqueue = {'interface': self.mock_xqueue, 'construct_callback': constructed_callback,
'default_queuename': 'testqueue',
'waittime': 1} 'waittime': 1}
self.openendedmodule = OpenEndedModule(self.test_system, self.location, self.openendedmodule = OpenEndedModule(self.test_system, self.location,
self.definition, self.descriptor, self.static_data, self.metadata) self.definition, self.descriptor, self.static_data, self.metadata)
...@@ -281,7 +287,18 @@ class OpenEndedModuleTest(unittest.TestCase): ...@@ -281,7 +287,18 @@ class OpenEndedModuleTest(unittest.TestCase):
class CombinedOpenEndedModuleTest(unittest.TestCase): class CombinedOpenEndedModuleTest(unittest.TestCase):
location = Location(["i4x", "edX", "open_ended", "combinedopenended", location = Location(["i4x", "edX", "open_ended", "combinedopenended",
"SampleQuestion"]) "SampleQuestion"])
definition_template = """
<combinedopenended attempts="10000">
{rubric}
{prompt}
<task>
{task1}
</task>
<task>
{task2}
</task>
</combinedopenended>
"""
prompt = "<prompt>This is a question prompt</prompt>" prompt = "<prompt>This is a question prompt</prompt>"
rubric = '''<rubric><rubric> rubric = '''<rubric><rubric>
<category> <category>
...@@ -335,10 +352,15 @@ class CombinedOpenEndedModuleTest(unittest.TestCase): ...@@ -335,10 +352,15 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
</openendedparam> </openendedparam>
</openended>''' </openended>'''
definition = {'prompt': etree.XML(prompt), 'rubric': etree.XML(rubric), 'task_xml': [task_xml1, task_xml2]} definition = {'prompt': etree.XML(prompt), 'rubric': etree.XML(rubric), 'task_xml': [task_xml1, task_xml2]}
descriptor = Mock() full_definition = definition_template.format(prompt=prompt, rubric=rubric, task1=task_xml1, task2=task_xml2)
descriptor = Mock(data=full_definition)
test_system = test_system()
combinedoe_container = CombinedOpenEndedModule(test_system,
location,
descriptor,
model_data={'data': full_definition, 'weight' : '1'})
def setUp(self): def setUp(self):
self.test_system = test_system()
# TODO: this constructor call is definitely wrong, but neither branch # TODO: this constructor call is definitely wrong, but neither branch
# of the merge matches the module constructor. Someone (Vik?) should fix this. # of the merge matches the module constructor. Someone (Vik?) should fix this.
self.combinedoe = CombinedOpenEndedV1Module(self.test_system, self.combinedoe = CombinedOpenEndedV1Module(self.test_system,
...@@ -368,3 +390,19 @@ class CombinedOpenEndedModuleTest(unittest.TestCase): ...@@ -368,3 +390,19 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
changed = self.combinedoe.update_task_states() changed = self.combinedoe.update_task_states()
self.assertTrue(changed) self.assertTrue(changed)
def test_get_max_score(self):
changed = self.combinedoe.update_task_states()
self.combinedoe.state = "done"
self.combinedoe.is_scored = True
max_score = self.combinedoe.max_score()
self.assertEqual(max_score, 1)
def test_container_get_max_score(self):
#The progress view requires that this function be exposed
max_score = self.combinedoe_container.max_score()
self.assertEqual(max_score, None)
def test_container_weight(self):
weight = self.combinedoe_container.weight
self.assertEqual(weight,1)
import unittest import unittest
from time import strptime from time import strptime
from fs.memoryfs import MemoryFS from fs.memoryfs import MemoryFS
from mock import Mock, patch from mock import Mock, patch
...@@ -108,7 +109,22 @@ class IsNewCourseTestCase(unittest.TestCase): ...@@ -108,7 +109,22 @@ class IsNewCourseTestCase(unittest.TestCase):
print "Comparing %s to %s" % (a, b) print "Comparing %s to %s" % (a, b)
assertion(a_score, b_score) assertion(a_score, b_score)
@patch('xmodule.course_module.time.gmtime')
def test_start_date_text(self, gmtime_mock):
gmtime_mock.return_value = NOW
settings = [
# start, advertized, result
('2012-12-02T12:00', None, 'Dec 02, 2012'),
('2012-12-02T12:00', '2011-11-01T12:00', 'Nov 01, 2011'),
('2012-12-02T12:00', 'Spring 2012', 'Spring 2012'),
('2012-12-02T12:00', 'November, 2011', 'November, 2011'),
]
for s in settings:
d = self.get_dummy_course(start=s[0], advertised_start=s[1])
print "Checking start=%s advertised=%s" % (s[0], s[1])
self.assertEqual(d.start_date_text, s[2])
@patch('xmodule.course_module.time.gmtime') @patch('xmodule.course_module.time.gmtime')
def test_is_newish(self, gmtime_mock): def test_is_newish(self, gmtime_mock):
......
"""Tests for Date class defined in fields.py."""
import datetime
import unittest
from django.utils.timezone import UTC
from xmodule.fields import Date
import time
class DateTest(unittest.TestCase):
date = Date()
@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 = DateTest.struct_to_datetime(date1)
dt2 = DateTest.struct_to_datetime(date2)
self.assertEqual(dt1 - dt2, expected_delta, str(date1) + "-"
+ str(date2) + "!=" + str(expected_delta))
def test_from_json(self):
'''Test conversion from iso compatible date strings to struct_time'''
self.compare_dates(
DateTest.date.from_json("2013-01-01"),
DateTest.date.from_json("2012-12-31"),
datetime.timedelta(days=1))
self.compare_dates(
DateTest.date.from_json("2013-01-01T00"),
DateTest.date.from_json("2012-12-31T23"),
datetime.timedelta(hours=1))
self.compare_dates(
DateTest.date.from_json("2013-01-01T00:00"),
DateTest.date.from_json("2012-12-31T23:59"),
datetime.timedelta(minutes=1))
self.compare_dates(
DateTest.date.from_json("2013-01-01T00:00:00"),
DateTest.date.from_json("2012-12-31T23:59:59"),
datetime.timedelta(seconds=1))
self.compare_dates(
DateTest.date.from_json("2013-01-01T00:00:00Z"),
DateTest.date.from_json("2012-12-31T23:59:59Z"),
datetime.timedelta(seconds=1))
self.compare_dates(
DateTest.date.from_json("2012-12-31T23:00:01-01:00"),
DateTest.date.from_json("2013-01-01T00:00:00+01:00"),
datetime.timedelta(hours=1, seconds=1))
def test_return_None(self):
self.assertIsNone(DateTest.date.from_json(""))
self.assertIsNone(DateTest.date.from_json(None))
self.assertIsNone(DateTest.date.from_json(['unknown value']))
def test_old_due_date_format(self):
current = datetime.datetime.today()
self.assertEqual(
time.struct_time((current.year, 3, 12, 12, 0, 0, 1, 71, 0)),
DateTest.date.from_json("March 12 12:00"))
self.assertEqual(
time.struct_time((current.year, 12, 4, 16, 30, 0, 2, 338, 0)),
DateTest.date.from_json("December 4 16:30"))
def test_to_json(self):
'''
Test converting time reprs to iso dates
'''
self.assertEqual(
DateTest.date.to_json(
time.strptime("2012-12-31T23:59:59Z", "%Y-%m-%dT%H:%M:%SZ")),
"2012-12-31T23:59:59Z")
self.assertEqual(
DateTest.date.to_json(
DateTest.date.from_json("2012-12-31T23:59:59Z")),
"2012-12-31T23:59:59Z")
self.assertEqual(
DateTest.date.to_json(
DateTest.date.from_json("2012-12-31T23:00:01-01:00")),
"2013-01-01T00:00:01Z")
...@@ -340,7 +340,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -340,7 +340,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
# cdodge: this is a list of metadata names which are 'system' metadata # cdodge: this is a list of metadata names which are 'system' metadata
# and should not be edited by an end-user # and should not be edited by an end-user
system_metadata_fields = ['data_dir', 'published_date', 'published_by', 'is_draft'] system_metadata_fields = ['data_dir', 'published_date', 'published_by', 'is_draft', 'xml_attributes']
# A list of descriptor attributes that must be equal for the descriptors to # A list of descriptor attributes that must be equal for the descriptors to
# be equal # be equal
......
/* This file defines a processor in between the student's math input
(AsciiMath) and what is read by MathJax. It allows for our own
customizations, such as use of the syntax "a_b__x" in superscripts, or
possibly coloring certain variables, etc&.
It is used in the <textline> definition like the following:
<symbolicresponse expect="a_b^c + b_x__d" size="30">
<textline math="1"
preprocessorClassName="SymbolicMathjaxPreprocessor"
preprocessorSrc="/static/js/capa/symbolic_mathjax_preprocessor.js"/>
</symbolicresponse>
*/
window.SymbolicMathjaxPreprocessor = function () {
this.fn = function (eqn) {
// flags and config
var superscriptsOn = true;
if (superscriptsOn) {
// find instances of "__" and make them superscripts ("^") and tag them
// as such. Specifcally replace instances of "__X" or "__{XYZ}" with
// "^{CHAR$1}", marking superscripts as different from powers
// a zero width space--this is an invisible character that no one would
// use, that gets passed through MathJax and to the server
var c = "\u200b";
eqn = eqn.replace(/__(?:([^\{])|\{([^\}]+)\})/g, '^{' + c + '$1$2}');
// NOTE: MathJax supports '\class{name}{mathcode}' but not for asciimath
// input, which is too bad. This would be preferable to this char tag
}
return eqn;
};
};
#################
Symbolic Response
#################
This document plans to document features that the current symbolic response
supports. In general it allows the input and validation of math expressions,
up to commutativity and some identities.
********
Features
********
This is a partial list of features, to be revised as we go along:
* sub and superscripts: an expression following the ``^`` character
indicates exponentiation. To use superscripts in variables, the syntax
is ``b_x__d`` for the variable ``b`` with subscript ``x`` and super
``d``.
An example of a problem::
<symbolicresponse expect="a_b^c + b_x__d" size="30">
<textline math="1"
preprocessorClassName="SymbolicMathjaxPreprocessor"
preprocessorSrc="/static/js/capa/symbolic_mathjax_preprocessor.js"/>
</symbolicresponse>
It's a bit of a pain to enter that.
* The script-style math variant. What would be outputted in latex if you
entered ``\mathcal{N}``. This is used in some variables.
An example::
<symbolicresponse expect="scriptN_B + x" size="30">
<textline math="1"/>
</symbolicresponse>
There is no fancy preprocessing needed, but if you had superscripts or
something, you would need to include that part.
...@@ -43,13 +43,15 @@ rake pep8 > pep8.log || cat pep8.log ...@@ -43,13 +43,15 @@ rake pep8 > pep8.log || cat pep8.log
rake pylint > pylint.log || cat pylint.log rake pylint > pylint.log || cat pylint.log
TESTS_FAILED=0 TESTS_FAILED=0
# Run the python unit tests
rake test_cms[false] || TESTS_FAILED=1 rake test_cms[false] || TESTS_FAILED=1
rake test_lms[false] || TESTS_FAILED=1 rake test_lms[false] || TESTS_FAILED=1
rake test_common/lib/capa || TESTS_FAILED=1 rake test_common/lib/capa || TESTS_FAILED=1
rake test_common/lib/xmodule || TESTS_FAILED=1 rake test_common/lib/xmodule || TESTS_FAILED=1
# Don't run the lms jasmine tests for now because
# they mostly all fail anyhow # Run the jaavascript unit tests
# rake phantomjs_jasmine_lms || true rake phantomjs_jasmine_lms || TESTS_FAILED=1
rake phantomjs_jasmine_cms || TESTS_FAILED=1 rake phantomjs_jasmine_cms || TESTS_FAILED=1
rake phantomjs_jasmine_common/lib/xmodule || TESTS_FAILED=1 rake phantomjs_jasmine_common/lib/xmodule || TESTS_FAILED=1
......
...@@ -73,6 +73,9 @@ class Command(BaseCommand): ...@@ -73,6 +73,9 @@ class Command(BaseCommand):
ended_courses.append(course_id) ended_courses.append(course_id)
for course_id in ended_courses: for course_id in ended_courses:
# prefetch all chapters/sequentials by saying depth=2
course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=2)
print "Fetching enrolled students for {0}".format(course_id) print "Fetching enrolled students for {0}".format(course_id)
enrolled_students = User.objects.filter( enrolled_students = User.objects.filter(
courseenrollment__course_id=course_id).prefetch_related( courseenrollment__course_id=course_id).prefetch_related(
...@@ -99,6 +102,6 @@ class Command(BaseCommand): ...@@ -99,6 +102,6 @@ class Command(BaseCommand):
student, course_id)['status'] in valid_statuses: student, course_id)['status'] in valid_statuses:
if not options['noop']: if not options['noop']:
# Add the certificate request to the queue # Add the certificate request to the queue
ret = xq.add_cert(student, course_id) ret = xq.add_cert(student, course_id, course=course)
if ret == 'generating': if ret == 'generating':
print '{0} - {1}'.format(student, ret) print '{0} - {1}'.format(student, ret)
...@@ -115,7 +115,7 @@ class XQueueCertInterface(object): ...@@ -115,7 +115,7 @@ class XQueueCertInterface(object):
raise NotImplementedError raise NotImplementedError
def add_cert(self, student, course_id): def add_cert(self, student, course_id, course=None):
""" """
Arguments: Arguments:
...@@ -151,9 +151,12 @@ class XQueueCertInterface(object): ...@@ -151,9 +151,12 @@ class XQueueCertInterface(object):
if cert_status in VALID_STATUSES: if cert_status in VALID_STATUSES:
# grade the student # grade the student
course = courses.get_course_by_id(course_id)
profile = UserProfile.objects.get(user=student)
# re-use the course passed in optionally so we don't have to re-fetch everything
# for every student
if course is None:
course = courses.get_course_by_id(course_id)
profile = UserProfile.objects.get(user=student)
cert, created = GeneratedCertificate.objects.get_or_create( cert, created = GeneratedCertificate.objects.get_or_create(
user=student, course_id=course_id) user=student, course_id=course_id)
......
...@@ -3,13 +3,11 @@ from django.test.utils import override_settings ...@@ -3,13 +3,11 @@ from django.test.utils import override_settings
import xmodule.modulestore.django import xmodule.modulestore.django
from courseware.tests.tests import PageLoader, TEST_DATA_XML_MODULESTORE from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml_importer import import_from_xml
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class WikiRedirectTestCase(PageLoader): class WikiRedirectTestCase(LoginEnrollmentTestCase):
def setUp(self): def setUp(self):
xmodule.modulestore.django._MODULESTORES = {} xmodule.modulestore.django._MODULESTORES = {}
courses = modulestore().get_courses() courses = modulestore().get_courses()
...@@ -30,8 +28,6 @@ class WikiRedirectTestCase(PageLoader): ...@@ -30,8 +28,6 @@ class WikiRedirectTestCase(PageLoader):
self.activate_user(self.student) self.activate_user(self.student)
self.activate_user(self.instructor) self.activate_user(self.instructor)
def test_wiki_redirect(self): def test_wiki_redirect(self):
""" """
Test that requesting wiki URLs redirect properly to or out of classes. Test that requesting wiki URLs redirect properly to or out of classes.
...@@ -69,7 +65,6 @@ class WikiRedirectTestCase(PageLoader): ...@@ -69,7 +65,6 @@ class WikiRedirectTestCase(PageLoader):
self.assertEqual(resp.status_code, 302) self.assertEqual(resp.status_code, 302)
self.assertEqual(resp['Location'], 'http://testserver' + destination) self.assertEqual(resp['Location'], 'http://testserver' + destination)
def create_course_page(self, course): def create_course_page(self, course):
""" """
Test that loading the course wiki page creates the wiki page. Test that loading the course wiki page creates the wiki page.
...@@ -98,7 +93,6 @@ class WikiRedirectTestCase(PageLoader): ...@@ -98,7 +93,6 @@ class WikiRedirectTestCase(PageLoader):
self.assertTrue("course info" in resp.content.lower()) self.assertTrue("course info" in resp.content.lower())
self.assertTrue("courseware" in resp.content.lower()) self.assertTrue("courseware" in resp.content.lower())
def test_course_navigator(self): def test_course_navigator(self):
"""" """"
Test that going from a course page to a wiki page contains the course navigator. Test that going from a course page to a wiki page contains the course navigator.
...@@ -108,7 +102,6 @@ class WikiRedirectTestCase(PageLoader): ...@@ -108,7 +102,6 @@ class WikiRedirectTestCase(PageLoader):
self.enroll(self.toy) self.enroll(self.toy)
self.create_course_page(self.toy) self.create_course_page(self.toy)
course_wiki_page = reverse('wiki:get', kwargs={'path': self.toy.wiki_slug + '/'}) course_wiki_page = reverse('wiki:get', kwargs={'path': self.toy.wiki_slug + '/'})
referer = reverse("courseware", kwargs={'course_id': self.toy.id}) referer = reverse("courseware", kwargs={'course_id': self.toy.id})
......
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from nose.tools import assert_equals, assert_in from nose.tools import assert_equals, assert_in
from lettuce.django import django_url from lettuce.django import django_url
...@@ -6,83 +9,13 @@ from student.models import CourseEnrollment ...@@ -6,83 +9,13 @@ from student.models import CourseEnrollment
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import _MODULESTORES, modulestore from xmodule.modulestore.django import _MODULESTORES, modulestore
from xmodule.templates import update_templates from xmodule.templates import update_templates
import time from xmodule.course_module import CourseDescriptor
from courseware.courses import get_course_by_id
from xmodule import seq_module, vertical_module
from logging import getLogger from logging import getLogger
logger = getLogger(__name__) logger = getLogger(__name__)
@step(u'I wait (?:for )?"(\d+)" seconds?$')
def wait(step, seconds):
time.sleep(float(seconds))
@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)
@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)
@step(r'click (?:the|a) link (?:called|with the text) "([^"]*)"$')
def click_the_link_called(step, text):
world.browser.find_link_by_text(text).click()
@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.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')
@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)
@step(u'the page title should be "([^"]*)"$')
def the_page_title_should_be(step, title):
assert world.browser.title == title
@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.browser.html)
@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()
TEST_COURSE_ORG = 'edx' TEST_COURSE_ORG = 'edx'
TEST_COURSE_NAME = 'Test Course' TEST_COURSE_NAME = 'Test Course'
TEST_SECTION_NAME = "Problem" TEST_SECTION_NAME = "Problem"
...@@ -94,7 +27,7 @@ def create_course(step, course): ...@@ -94,7 +27,7 @@ def create_course(step, course):
# First clear the modulestore so we don't try to recreate # First clear the modulestore so we don't try to recreate
# the same course twice # the same course twice
# This also ensures that the necessary templates are loaded # This also ensures that the necessary templates are loaded
flush_xmodule_store() world.clear_courses()
# Create the course # Create the course
# We always use the same org and display name, # We always use the same org and display name,
...@@ -135,29 +68,6 @@ def add_tab_to_course(step, course, extra_tab_name): ...@@ -135,29 +68,6 @@ def add_tab_to_course(step, course, extra_tab_name):
display_name=str(extra_tab_name)) display_name=str(extra_tab_name))
@step(u'I am an edX user$')
def i_am_an_edx_user(step):
world.create_user('robot')
@step(u'User "([^"]*)" is an edX user$')
def registered_edx_user(step, uname):
world.create_user(uname)
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 course_id(course_num): def course_id(course_num):
return "%s/%s/%s" % (TEST_COURSE_ORG, course_num, return "%s/%s/%s" % (TEST_COURSE_ORG, course_num,
TEST_COURSE_NAME.replace(" ", "_")) TEST_COURSE_NAME.replace(" ", "_"))
...@@ -177,3 +87,87 @@ def section_location(course_num): ...@@ -177,3 +87,87 @@ def section_location(course_num):
course=course_num, course=course_num,
category='sequential', category='sequential',
name=TEST_SECTION_NAME.replace(" ", "_")) name=TEST_SECTION_NAME.replace(" ", "_"))
def get_courses():
'''
Returns dict of lists of courses available, keyed by course.org (ie university).
Courses are sorted by course.number.
'''
courses = [c for c in modulestore().get_courses()
if isinstance(c, CourseDescriptor)]
courses = sorted(courses, key=lambda course: course.number)
return courses
def get_courseware_with_tabs(course_id):
"""
Given a course_id (string), return a courseware array of dictionaries for the
top three levels of navigation. Same as get_courseware() except include
the tabs on the right hand main navigation page.
This hides the appropriate courseware as defined by the hide_from_toc field:
chapter.lms.hide_from_toc
Example:
[{
'chapter_name': 'Overview',
'sections': [{
'clickable_tab_count': 0,
'section_name': 'Welcome',
'tab_classes': []
}, {
'clickable_tab_count': 1,
'section_name': 'System Usage Sequence',
'tab_classes': ['VerticalDescriptor']
}, {
'clickable_tab_count': 0,
'section_name': 'Lab0: Using the tools',
'tab_classes': ['HtmlDescriptor', 'HtmlDescriptor', 'CapaDescriptor']
}, {
'clickable_tab_count': 0,
'section_name': 'Circuit Sandbox',
'tab_classes': []
}]
}, {
'chapter_name': 'Week 1',
'sections': [{
'clickable_tab_count': 4,
'section_name': 'Administrivia and Circuit Elements',
'tab_classes': ['VerticalDescriptor', 'VerticalDescriptor', 'VerticalDescriptor', 'VerticalDescriptor']
}, {
'clickable_tab_count': 0,
'section_name': 'Basic Circuit Analysis',
'tab_classes': ['CapaDescriptor', 'CapaDescriptor', 'CapaDescriptor']
}, {
'clickable_tab_count': 0,
'section_name': 'Resistor Divider',
'tab_classes': []
}, {
'clickable_tab_count': 0,
'section_name': 'Week 1 Tutorials',
'tab_classes': []
}]
}, {
'chapter_name': 'Midterm Exam',
'sections': [{
'clickable_tab_count': 2,
'section_name': 'Midterm Exam',
'tab_classes': ['VerticalDescriptor', 'VerticalDescriptor']
}]
}]
"""
course = get_course_by_id(course_id)
chapters = [chapter for chapter in course.get_children() if not chapter.lms.hide_from_toc]
courseware = [{'chapter_name': c.display_name_with_default,
'sections': [{'section_name': s.display_name_with_default,
'clickable_tab_count': len(s.get_children()) if (type(s) == seq_module.SequenceDescriptor) else 0,
'tabs': [{'children_count': len(t.get_children()) if (type(t) == vertical_module.VerticalDescriptor) else 0,
'class': t.__class__.__name__}
for t in s.get_children()]}
for s in c.get_children() if not s.lms.hide_from_toc]}
for c in chapters]
return courseware
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from lettuce.django import django_url from lettuce.django import django_url
......
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from lettuce.django import django_url
@step('I click on View Courseware') @step('I click on View Courseware')
def i_click_on_view_courseware(step): def i_click_on_view_courseware(step):
css = 'a.enter-course' world.css_click('a.enter-course')
world.browser.find_by_css(css).first.click()
@step('I click on the "([^"]*)" tab$') @step('I click on the "([^"]*)" tab$')
def i_click_on_the_tab(step, tab): def i_click_on_the_tab(step, tab_text):
world.browser.find_link_by_partial_text(tab).first.click() world.click_link(tab_text)
world.save_the_html() world.save_the_html()
@step('I visit the courseware URL$') @step('I visit the courseware URL$')
def i_visit_the_course_info_url(step): def i_visit_the_course_info_url(step):
url = django_url('/courses/MITx/6.002x/2012_Fall/courseware') world.visit('/courses/MITx/6.002x/2012_Fall/courseware')
world.browser.visit(url)
@step(u'I do not see "([^"]*)" anywhere on the page') @step(u'I do not see "([^"]*)" anywhere on the page')
...@@ -27,18 +27,15 @@ def i_do_not_see_text_anywhere_on_the_page(step, text): ...@@ -27,18 +27,15 @@ def i_do_not_see_text_anywhere_on_the_page(step, text):
@step(u'I am on the dashboard page$') @step(u'I am on the dashboard page$')
def i_am_on_the_dashboard_page(step): def i_am_on_the_dashboard_page(step):
assert world.browser.is_element_present_by_css('section.courses') assert world.is_css_present('section.courses')
assert world.browser.url == django_url('/dashboard') assert world.url_equals('/dashboard')
@step('the "([^"]*)" tab is active$') @step('the "([^"]*)" tab is active$')
def the_tab_is_active(step, tab): def the_tab_is_active(step, tab_text):
css = '.course-tabs a.active' assert world.css_text('.course-tabs a.active') == tab_text
active_tab = world.browser.find_by_css(css)
assert (active_tab.text == tab)
@step('the login dialog is visible$') @step('the login dialog is visible$')
def login_dialog_visible(step): def login_dialog_visible(step):
css = 'form#login_form.login_form' assert world.css_visible('form#login_form.login_form')
assert world.browser.find_by_css(css).visible
...@@ -3,7 +3,7 @@ Feature: All the high level tabs should work ...@@ -3,7 +3,7 @@ Feature: All the high level tabs should work
As a student As a student
I want to navigate through the high level tabs I want to navigate through the high level tabs
Scenario: I can navigate to all high -level tabs in a course Scenario: I can navigate to all high - level tabs in a course
Given: I am registered for the course "6.002x" Given: I am registered for the course "6.002x"
And The course "6.002x" has extra tab "Custom Tab" And The course "6.002x" has extra tab "Custom Tab"
And I am logged in And I am logged in
......
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from nose.tools import assert_in from nose.tools import assert_in
......
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import step, world from lettuce import step, world
from django.contrib.auth.models import User from django.contrib.auth.models import User
...@@ -28,9 +31,7 @@ def i_should_see_the_login_error_message(step, msg): ...@@ -28,9 +31,7 @@ def i_should_see_the_login_error_message(step, msg):
@step(u'click the dropdown arrow$') @step(u'click the dropdown arrow$')
def click_the_dropdown(step): def click_the_dropdown(step):
css = ".dropdown" world.css_click('.dropdown')
e = world.browser.find_by_css(css)
e.click()
#### helper functions #### helper functions
......
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from lettuce.django import django_url from lettuce.django import django_url
from nose.tools import assert_equals, assert_in from nose.tools import assert_equals, assert_in
...@@ -12,7 +15,7 @@ def navigate_to_an_openended_question(step): ...@@ -12,7 +15,7 @@ def navigate_to_an_openended_question(step):
problem = '/courses/MITx/3.091x/2012_Fall/courseware/Week_10/Polymer_Synthesis/' problem = '/courses/MITx/3.091x/2012_Fall/courseware/Week_10/Polymer_Synthesis/'
world.browser.visit(django_url(problem)) world.browser.visit(django_url(problem))
tab_css = 'ol#sequence-list > li > a[data-element="5"]' tab_css = 'ol#sequence-list > li > a[data-element="5"]'
world.browser.find_by_css(tab_css).click() world.css_click(tab_css)
@step('I navigate to an openended question as staff$') @step('I navigate to an openended question as staff$')
...@@ -22,81 +25,69 @@ def navigate_to_an_openended_question_as_staff(step): ...@@ -22,81 +25,69 @@ def navigate_to_an_openended_question_as_staff(step):
problem = '/courses/MITx/3.091x/2012_Fall/courseware/Week_10/Polymer_Synthesis/' problem = '/courses/MITx/3.091x/2012_Fall/courseware/Week_10/Polymer_Synthesis/'
world.browser.visit(django_url(problem)) world.browser.visit(django_url(problem))
tab_css = 'ol#sequence-list > li > a[data-element="5"]' tab_css = 'ol#sequence-list > li > a[data-element="5"]'
world.browser.find_by_css(tab_css).click() world.css_click(tab_css)
@step(u'I enter the answer "([^"]*)"$') @step(u'I enter the answer "([^"]*)"$')
def enter_the_answer_text(step, text): def enter_the_answer_text(step, text):
textarea_css = 'textarea' world.css_fill('textarea', text)
world.browser.find_by_css(textarea_css).first.fill(text)
@step(u'I submit the answer "([^"]*)"$') @step(u'I submit the answer "([^"]*)"$')
def i_submit_the_answer_text(step, text): def i_submit_the_answer_text(step, text):
textarea_css = 'textarea' world.css_fill('textarea', text)
world.browser.find_by_css(textarea_css).first.fill(text) world.css_click('input.check')
check_css = 'input.check'
world.browser.find_by_css(check_css).click()
@step('I click the link for full output$') @step('I click the link for full output$')
def click_full_output_link(step): def click_full_output_link(step):
link_css = 'a.full' world.css_click('a.full')
world.browser.find_by_css(link_css).first.click()
@step(u'I visit the staff grading page$') @step(u'I visit the staff grading page$')
def i_visit_the_staff_grading_page(step): def i_visit_the_staff_grading_page(step):
# course_u = '/courses/MITx/3.091x/2012_Fall' world.click_link('Instructor')
# sg_url = '%s/staff_grading' % course_u world.click_link('Staff grading')
world.browser.click_link_by_text('Instructor')
world.browser.click_link_by_text('Staff grading')
# world.browser.visit(django_url(sg_url))
@step(u'I see the grader message "([^"]*)"$') @step(u'I see the grader message "([^"]*)"$')
def see_grader_message(step, msg): def see_grader_message(step, msg):
message_css = 'div.external-grader-message' message_css = 'div.external-grader-message'
grader_msg = world.browser.find_by_css(message_css).text assert_in(msg, world.css_text(message_css))
assert_in(msg, grader_msg)
@step(u'I see the grader status "([^"]*)"$') @step(u'I see the grader status "([^"]*)"$')
def see_the_grader_status(step, status): def see_the_grader_status(step, status):
status_css = 'div.grader-status' status_css = 'div.grader-status'
grader_status = world.browser.find_by_css(status_css).text assert_equals(status, world.css_text(status_css))
assert_equals(status, grader_status)
@step('I see the red X$') @step('I see the red X$')
def see_the_red_x(step): def see_the_red_x(step):
x_css = 'div.grader-status > span.incorrect' assert world.is_css_present('div.grader-status > span.incorrect')
assert world.browser.find_by_css(x_css)
@step(u'I see the grader score "([^"]*)"$') @step(u'I see the grader score "([^"]*)"$')
def see_the_grader_score(step, score): def see_the_grader_score(step, score):
score_css = 'div.result-output > p' score_css = 'div.result-output > p'
score_text = world.browser.find_by_css(score_css).text score_text = world.css_text(score_css)
assert_equals(score_text, 'Score: %s' % score) assert_equals(score_text, 'Score: %s' % score)
@step('I see the link for full output$') @step('I see the link for full output$')
def see_full_output_link(step): def see_full_output_link(step):
link_css = 'a.full' assert world.is_css_present('a.full')
assert world.browser.find_by_css(link_css)
@step('I see the spelling grading message "([^"]*)"$') @step('I see the spelling grading message "([^"]*)"$')
def see_spelling_msg(step, msg): def see_spelling_msg(step, msg):
spelling_css = 'div.spelling' spelling_msg = world.css_text('div.spelling')
spelling_msg = world.browser.find_by_css(spelling_css).text
assert_equals('Spelling: %s' % msg, spelling_msg) assert_equals('Spelling: %s' % msg, spelling_msg)
@step(u'my answer is queued for instructor grading$') @step(u'my answer is queued for instructor grading$')
def answer_is_queued_for_instructor_grading(step): def answer_is_queued_for_instructor_grading(step):
list_css = 'ul.problem-list > li > a' list_css = 'ul.problem-list > li > a'
actual_msg = world.browser.find_by_css(list_css).text actual_msg = world.css_text(list_css)
expected_msg = "(0 graded, 1 pending)" expected_msg = "(0 graded, 1 pending)"
assert_in(expected_msg, actual_msg) assert_in(expected_msg, actual_msg)
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
Steps for problem.feature lettuce tests Steps for problem.feature lettuce tests
''' '''
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from lettuce.django import django_url from lettuce.django import django_url
...@@ -339,7 +341,7 @@ def assert_answer_mark(step, problem_type, correctness): ...@@ -339,7 +341,7 @@ def assert_answer_mark(step, problem_type, correctness):
# At least one of the correct selectors should be present # At least one of the correct selectors should be present
for sel in selector_dict[problem_type]: for sel in selector_dict[problem_type]:
has_expected = world.browser.is_element_present_by_css(sel, wait_time=4) has_expected = world.is_css_present(sel)
# As soon as we find the selector, break out of the loop # As soon as we find the selector, break out of the loop
if has_expected: if has_expected:
...@@ -366,7 +368,7 @@ def inputfield(problem_type, choice=None, input_num=1): ...@@ -366,7 +368,7 @@ def inputfield(problem_type, choice=None, input_num=1):
# If the input element doesn't exist, fail immediately # If the input element doesn't exist, fail immediately
assert(world.browser.is_element_present_by_css(sel, wait_time=4)) assert world.is_css_present(sel)
# Retrieve the input element # Retrieve the input element
return world.browser.find_by_css(sel) return world.browser.find_by_css(sel)
......
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from lettuce.django import django_url from lettuce.django import django_url
from common import TEST_COURSE_ORG, TEST_COURSE_NAME from common import TEST_COURSE_ORG, TEST_COURSE_NAME
...@@ -13,17 +16,17 @@ def i_register_for_the_course(step, course): ...@@ -13,17 +16,17 @@ def i_register_for_the_course(step, course):
register_link = intro_section.find_by_css('a.register') register_link = intro_section.find_by_css('a.register')
register_link.click() register_link.click()
assert world.browser.is_element_present_by_css('section.container.dashboard') assert world.is_css_present('section.container.dashboard')
@step(u'I should see the course numbered "([^"]*)" in my dashboard$') @step(u'I should see the course numbered "([^"]*)" in my dashboard$')
def i_should_see_that_course_in_my_dashboard(step, course): def i_should_see_that_course_in_my_dashboard(step, course):
course_link_css = 'section.my-courses a[href*="%s"]' % course course_link_css = 'section.my-courses a[href*="%s"]' % course
assert world.browser.is_element_present_by_css(course_link_css) assert world.is_css_present(course_link_css)
@step(u'I press the "([^"]*)" button in the Unenroll dialog') @step(u'I press the "([^"]*)" button in the Unenroll dialog')
def i_press_the_button_in_the_unenroll_dialog(step, value): def i_press_the_button_in_the_unenroll_dialog(step, value):
button_css = 'section#unenroll-modal input[value="%s"]' % value button_css = 'section#unenroll-modal input[value="%s"]' % value
world.browser.find_by_css(button_css).click() world.css_click(button_css)
assert world.browser.is_element_present_by_css('section.container.dashboard') assert world.is_css_present('section.container.dashboard')
from lettuce import world, step #pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
@step('I fill in "([^"]*)" on the registration form with "([^"]*)"$') @step('I fill in "([^"]*)" on the registration form with "([^"]*)"$')
def when_i_fill_in_field_on_the_registration_form_with_value(step, field, value): def when_i_fill_in_field_on_the_registration_form_with_value(step, field, value):
...@@ -22,4 +24,4 @@ def i_check_checkbox(step, checkbox): ...@@ -22,4 +24,4 @@ def i_check_checkbox(step, checkbox):
@step('I should see "([^"]*)" in the dashboard banner$') @step('I should see "([^"]*)" in the dashboard banner$')
def i_should_see_text_in_the_dashboard_banner_section(step, text): def i_should_see_text_in_the_dashboard_banner_section(step, text):
css_selector = "section.dashboard-banner h2" css_selector = "section.dashboard-banner h2"
assert (text in world.browser.find_by_css(css_selector).text) assert (text in world.css_text(css_selector))
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from re import sub from re import sub
from nose.tools import assert_equals from nose.tools import assert_equals
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from courses import * from common import *
from logging import getLogger from logging import getLogger
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -32,20 +35,20 @@ def i_verify_all_the_content_of_each_course(step): ...@@ -32,20 +35,20 @@ def i_verify_all_the_content_of_each_course(step):
pass pass
for test_course in registered_courses: for test_course in registered_courses:
test_course.find_by_css('a').click() test_course.css_click('a')
check_for_errors() check_for_errors()
# Get the course. E.g. 'MITx/6.002x/2012_Fall' # Get the course. E.g. 'MITx/6.002x/2012_Fall'
current_course = sub('/info', '', sub('.*/courses/', '', world.browser.url)) current_course = sub('/info', '', sub('.*/courses/', '', world.browser.url))
validate_course(current_course, ids) validate_course(current_course, ids)
world.browser.find_link_by_text('Courseware').click() world.click_link('Courseware')
assert world.browser.is_element_present_by_id('accordion', wait_time=2) assert world.is_css_present('accordion')
check_for_errors() check_for_errors()
browse_course(current_course) browse_course(current_course)
# clicking the user link gets you back to the user's home page # clicking the user link gets you back to the user's home page
world.browser.find_by_css('.user-link').click() world.css_click('.user-link')
check_for_errors() check_for_errors()
...@@ -94,7 +97,7 @@ def browse_course(course_id): ...@@ -94,7 +97,7 @@ def browse_course(course_id):
world.browser.find_by_css('#accordion > nav > div')[chapter_it].find_by_tag('li')[section_it].find_by_tag('a').click() world.browser.find_by_css('#accordion > nav > div')[chapter_it].find_by_tag('li')[section_it].find_by_tag('a').click()
## sometimes the course-content takes a long time to load ## sometimes the course-content takes a long time to load
assert world.browser.is_element_present_by_css('.course-content', wait_time=5) assert world.is_css_present('.course-content')
## look for server error div ## look for server error div
check_for_errors() check_for_errors()
......
#pylint: disable=C0111
#pylint: disable=W0621
from courseware.mock_xqueue_server.mock_xqueue_server import MockXQueueServer from courseware.mock_xqueue_server.mock_xqueue_server import MockXQueueServer
from lettuce import before, after, world from lettuce import before, after, world
from django.conf import settings from django.conf import settings
......
...@@ -13,7 +13,6 @@ from xblock.core import Scope ...@@ -13,7 +13,6 @@ from xblock.core import Scope
from .module_render import get_module, get_module_for_descriptor from .module_render import get_module, get_module_for_descriptor
from xmodule import graders from xmodule import graders
from xmodule.capa_module import CapaModule from xmodule.capa_module import CapaModule
from xmodule.course_module import CourseDescriptor
from xmodule.graders import Score from xmodule.graders import Score
from .models import StudentModule from .models import StudentModule
...@@ -43,7 +42,6 @@ def yield_dynamic_descriptor_descendents(descriptor, module_creator): ...@@ -43,7 +42,6 @@ def yield_dynamic_descriptor_descendents(descriptor, module_creator):
else: else:
return descriptor.get_children() return descriptor.get_children()
stack = [descriptor] stack = [descriptor]
while len(stack) > 0: while len(stack) > 0:
...@@ -66,7 +64,7 @@ def yield_problems(request, course, student): ...@@ -66,7 +64,7 @@ def yield_problems(request, course, student):
).values_list('module_state_key', flat=True)) ).values_list('module_state_key', flat=True))
sections_to_list = [] sections_to_list = []
for section_format, sections in grading_context['graded_sections'].iteritems(): for _, sections in grading_context['graded_sections'].iteritems():
for section in sections: for section in sections:
section_descriptor = section['section_descriptor'] section_descriptor = section['section_descriptor']
...@@ -123,7 +121,7 @@ def answer_distributions(request, course): ...@@ -123,7 +121,7 @@ def answer_distributions(request, course):
def grade(student, request, course, model_data_cache=None, keep_raw_scores=False): def grade(student, request, course, model_data_cache=None, keep_raw_scores=False):
""" """
This grades a student as quickly as possible. It retuns the This grades a student as quickly as possible. It returns the
output from the course grader, augmented with the final letter output from the course grader, augmented with the final letter
grade. The keys in the output are: grade. The keys in the output are:
...@@ -158,6 +156,12 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False ...@@ -158,6 +156,12 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False
should_grade_section = False should_grade_section = False
# If we haven't seen a single problem in the section, we don't have to grade it at all! We can assume 0% # If we haven't seen a single problem in the section, we don't have to grade it at all! We can assume 0%
for moduledescriptor in section['xmoduledescriptors']: for moduledescriptor in section['xmoduledescriptors']:
# some problems have state that is updated independently of interaction
# with the LMS, so they need to always be scored. (E.g. foldit.)
if moduledescriptor.always_recalculate_grades:
should_grade_section = True
break
# Create a fake key to pull out a StudentModule object from the ModelDataCache # Create a fake key to pull out a StudentModule object from the ModelDataCache
key = LmsKeyValueStore.Key( key = LmsKeyValueStore.Key(
...@@ -174,7 +178,8 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False ...@@ -174,7 +178,8 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False
scores = [] scores = []
def create_module(descriptor): def create_module(descriptor):
# TODO: We need the request to pass into here. If we could forgo that, our arguments '''creates an XModule instance given a descriptor'''
# TODO: We need the request to pass into here. If we could forego that, our arguments
# would be simpler # would be simpler
return get_module_for_descriptor(student, request, descriptor, model_data_cache, course.id) return get_module_for_descriptor(student, request, descriptor, model_data_cache, course.id)
...@@ -197,18 +202,18 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False ...@@ -197,18 +202,18 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False
scores.append(Score(correct, total, graded, module_descriptor.display_name_with_default)) scores.append(Score(correct, total, graded, module_descriptor.display_name_with_default))
section_total, graded_total = graders.aggregate_scores(scores, section_name) _, graded_total = graders.aggregate_scores(scores, section_name)
if keep_raw_scores: if keep_raw_scores:
raw_scores += scores raw_scores += scores
else: else:
section_total = Score(0.0, 1.0, False, section_name)
graded_total = Score(0.0, 1.0, True, section_name) graded_total = Score(0.0, 1.0, True, section_name)
#Add the graded total to totaled_scores #Add the graded total to totaled_scores
if graded_total.possible > 0: if graded_total.possible > 0:
format_scores.append(graded_total) format_scores.append(graded_total)
else: else:
log.exception("Unable to grade a section with a total possible score of zero. " + str(section_descriptor.location)) log.exception("Unable to grade a section with a total possible score of zero. " +
str(section_descriptor.location))
totaled_scores[section_format] = format_scores totaled_scores[section_format] = format_scores
...@@ -274,12 +279,9 @@ def progress_summary(student, request, course, model_data_cache): ...@@ -274,12 +279,9 @@ def progress_summary(student, request, course, model_data_cache):
""" """
# TODO: We need the request to pass into here. If we could forego that, our arguments
# TODO: We need the request to pass into here. If we could forgo that, our arguments
# would be simpler # would be simpler
course_module = get_module(student, request, course_module = get_module(student, request, course.location, model_data_cache, course.id, depth=None)
course.location, model_data_cache,
course.id, depth=None)
if not course_module: if not course_module:
# This student must not have access to the course. # This student must not have access to the course.
return None return None
...@@ -310,20 +312,19 @@ def progress_summary(student, request, course, model_data_cache): ...@@ -310,20 +312,19 @@ def progress_summary(student, request, course, model_data_cache):
if correct is None and total is None: if correct is None and total is None:
continue continue
scores.append(Score(correct, total, graded, scores.append(Score(correct, total, graded, module_descriptor.display_name_with_default))
module_descriptor.display_name_with_default))
scores.reverse() scores.reverse()
section_total, graded_total = graders.aggregate_scores( section_total, _ = graders.aggregate_scores(
scores, section_module.display_name_with_default) scores, section_module.display_name_with_default)
format = section_module.lms.format if section_module.lms.format is not None else '' module_format = section_module.lms.format if section_module.lms.format is not None else ''
sections.append({ sections.append({
'display_name': section_module.display_name_with_default, 'display_name': section_module.display_name_with_default,
'url_name': section_module.url_name, 'url_name': section_module.url_name,
'scores': scores, 'scores': scores,
'section_total': section_total, 'section_total': section_total,
'format': format, 'format': module_format,
'due': section_module.lms.due, 'due': section_module.lms.due,
'graded': graded, 'graded': graded,
}) })
...@@ -353,11 +354,13 @@ def get_score(course_id, user, problem_descriptor, module_creator, model_data_ca ...@@ -353,11 +354,13 @@ def get_score(course_id, user, problem_descriptor, module_creator, model_data_ca
if not user.is_authenticated(): if not user.is_authenticated():
return (None, None) return (None, None)
# some problems have state that is updated independently of interaction
# with the LMS, so they need to always be scored. (E.g. foldit.)
if problem_descriptor.always_recalculate_grades: if problem_descriptor.always_recalculate_grades:
problem = module_creator(problem_descriptor) problem = module_creator(problem_descriptor)
d = problem.get_score() score = problem.get_score()
if d is not None: if score is not None:
return (d['score'], d['total']) return (score['score'], score['total'])
else: else:
return (None, None) return (None, None)
...@@ -394,7 +397,7 @@ def get_score(course_id, user, problem_descriptor, module_creator, model_data_ca ...@@ -394,7 +397,7 @@ def get_score(course_id, user, problem_descriptor, module_creator, model_data_ca
if total is None: if total is None:
return (None, None) return (None, None)
#Now we re-weight the problem, if specified # Now we re-weight the problem, if specified
weight = problem_descriptor.weight weight = problem_descriptor.weight
if weight is not None: if weight is not None:
if total == 0: if total == 0:
......
...@@ -3,10 +3,11 @@ import unittest ...@@ -3,10 +3,11 @@ import unittest
import threading import threading
import json import json
import urllib import urllib
import urlparse
import time import time
from mock_xqueue_server import MockXQueueServer, MockXQueueRequestHandler from mock_xqueue_server import MockXQueueServer, MockXQueueRequestHandler
from nose.plugins.skip import SkipTest
class MockXQueueServerTest(unittest.TestCase): class MockXQueueServerTest(unittest.TestCase):
''' '''
...@@ -22,11 +23,16 @@ class MockXQueueServerTest(unittest.TestCase): ...@@ -22,11 +23,16 @@ class MockXQueueServerTest(unittest.TestCase):
def setUp(self): def setUp(self):
# This is a test of the test setup,
# so it does not need to run as part of the unit test suite
# You can re-enable it by commenting out the line below
raise SkipTest
# Create the server # Create the server
server_port = 8034 server_port = 8034
self.server_url = 'http://127.0.0.1:%d' % server_port self.server_url = 'http://127.0.0.1:%d' % server_port
self.server = MockXQueueServer(server_port, self.server = MockXQueueServer(server_port,
{'correct': True, 'score': 1, 'msg': ''}) {'correct': True, 'score': 1, 'msg': ''})
# Start the server in a separate daemon thread # Start the server in a separate daemon thread
server_thread = threading.Thread(target=self.server.serve_forever) server_thread = threading.Thread(target=self.server.serve_forever)
...@@ -48,18 +54,18 @@ class MockXQueueServerTest(unittest.TestCase): ...@@ -48,18 +54,18 @@ class MockXQueueServerTest(unittest.TestCase):
callback_url = 'http://127.0.0.1:8000/test_callback' callback_url = 'http://127.0.0.1:8000/test_callback'
grade_header = json.dumps({'lms_callback_url': callback_url, grade_header = json.dumps({'lms_callback_url': callback_url,
'lms_key': 'test_queuekey', 'lms_key': 'test_queuekey',
'queue_name': 'test_queue'}) 'queue_name': 'test_queue'})
grade_body = json.dumps({'student_info': 'test', grade_body = json.dumps({'student_info': 'test',
'grader_payload': 'test', 'grader_payload': 'test',
'student_response': 'test'}) 'student_response': 'test'})
grade_request = {'xqueue_header': grade_header, grade_request = {'xqueue_header': grade_header,
'xqueue_body': grade_body} 'xqueue_body': grade_body}
response_handle = urllib.urlopen(self.server_url + '/xqueue/submit', response_handle = urllib.urlopen(self.server_url + '/xqueue/submit',
urllib.urlencode(grade_request)) urllib.urlencode(grade_request))
response_dict = json.loads(response_handle.read()) response_dict = json.loads(response_handle.read())
...@@ -71,8 +77,8 @@ class MockXQueueServerTest(unittest.TestCase): ...@@ -71,8 +77,8 @@ class MockXQueueServerTest(unittest.TestCase):
# Expect that the server tries to post back the grading info # Expect that the server tries to post back the grading info
xqueue_body = json.dumps({'correct': True, 'score': 1, xqueue_body = json.dumps({'correct': True, 'score': 1,
'msg': '<div></div>'}) 'msg': '<div></div>'})
expected_callback_dict = {'xqueue_header': grade_header, expected_callback_dict = {'xqueue_header': grade_header,
'xqueue_body': xqueue_body} 'xqueue_body': xqueue_body}
MockXQueueRequestHandler.post_to_url.assert_called_with(callback_url, MockXQueueRequestHandler.post_to_url.assert_called_with(callback_url,
expected_callback_dict) expected_callback_dict)
...@@ -8,9 +8,10 @@ from functools import partial ...@@ -8,9 +8,10 @@ from functools import partial
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import Http404 from django.http import Http404
from django.http import HttpResponse from django.http import HttpResponse, HttpResponseBadRequest
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth
...@@ -22,7 +23,7 @@ from .models import StudentModule ...@@ -22,7 +23,7 @@ from .models import StudentModule
from psychometrics.psychoanalyze import make_psychometrics_data_update_handler from psychometrics.psychoanalyze import make_psychometrics_data_update_handler
from student.models import unique_id_for_user from student.models import unique_id_for_user
from xmodule.errortracker import exc_info_to_str from xmodule.errortracker import exc_info_to_str
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError, ProcessingError
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.x_module import ModuleSystem from xmodule.x_module import ModuleSystem
...@@ -208,9 +209,6 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours ...@@ -208,9 +209,6 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS 'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS
} }
def get_or_default(key, default):
getattr(settings, key, default)
#This is a hacky way to pass settings to the combined open ended xmodule #This is a hacky way to pass settings to the combined open ended xmodule
#It needs an S3 interface to upload images to S3 #It needs an S3 interface to upload images to S3
#It needs the open ended grading interface in order to get peer grading to be done #It needs the open ended grading interface in order to get peer grading to be done
...@@ -226,12 +224,11 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours ...@@ -226,12 +224,11 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
open_ended_grading_interface['mock_staff_grading'] = settings.MOCK_STAFF_GRADING open_ended_grading_interface['mock_staff_grading'] = settings.MOCK_STAFF_GRADING
if is_descriptor_combined_open_ended: if is_descriptor_combined_open_ended:
s3_interface = { s3_interface = {
'access_key' : get_or_default('AWS_ACCESS_KEY_ID',''), 'access_key' : getattr(settings,'AWS_ACCESS_KEY_ID',''),
'secret_access_key' : get_or_default('AWS_SECRET_ACCESS_KEY',''), 'secret_access_key' : getattr(settings,'AWS_SECRET_ACCESS_KEY',''),
'storage_bucket_name' : get_or_default('AWS_STORAGE_BUCKET_NAME','') 'storage_bucket_name' : getattr(settings,'AWS_STORAGE_BUCKET_NAME','openended')
} }
def inner_get_module(descriptor): def inner_get_module(descriptor):
""" """
Delegate to get_module. It does an access check, so may return None Delegate to get_module. It does an access check, so may return None
...@@ -412,6 +409,9 @@ def modx_dispatch(request, dispatch, location, course_id): ...@@ -412,6 +409,9 @@ def modx_dispatch(request, dispatch, location, course_id):
if not Location.is_valid(location): if not Location.is_valid(location):
raise Http404("Invalid location") raise Http404("Invalid location")
if not request.user.is_authenticated():
raise PermissionDenied
# Check for submitted files and basic file size checks # Check for submitted files and basic file size checks
p = request.POST.copy() p = request.POST.copy()
if request.FILES: if request.FILES:
...@@ -443,9 +443,19 @@ def modx_dispatch(request, dispatch, location, course_id): ...@@ -443,9 +443,19 @@ def modx_dispatch(request, dispatch, location, course_id):
# Let the module handle the AJAX # Let the module handle the AJAX
try: try:
ajax_return = instance.handle_ajax(dispatch, p) ajax_return = instance.handle_ajax(dispatch, p)
# If we can't find the module, respond with a 404
except NotFoundError: except NotFoundError:
log.exception("Module indicating to user that request doesn't exist") log.exception("Module indicating to user that request doesn't exist")
raise Http404 raise Http404
# For XModule-specific errors, we respond with 400
except ProcessingError:
log.warning("Module encountered an error while prcessing AJAX call",
exc_info=True)
return HttpResponseBadRequest()
# If any other error occurred, re-raise it to trigger a 500 response
except: except:
log.exception("error processing ajax call") log.exception("error processing ajax call")
raise raise
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment