diff --git a/.pylintrc b/.pylintrc
index 6690bb7df0..2f2be69eb0 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -34,6 +34,7 @@ load-plugins=
# multiple time (only on the command line, not in the configuration file where
# it should appear only once).
disable=
+# C0301: Line too long
# W0141: Used builtin function 'map'
# W0142: Used * or ** magic
# R0201: Method could be a function
@@ -41,7 +42,8 @@ disable=
# R0902: Too many instance attributes
# R0903: Too few public methods (1/2)
# R0904: Too many public methods
- W0141,W0142,R0201,R0901,R0902,R0903,R0904
+# R0913: Too many arguments
+ C0301,W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0913
[REPORTS]
@@ -137,7 +139,7 @@ bad-names=foo,bar,baz,toto,tutu,tata
# Regular expression which should only match functions or classes name which do
# not require a docstring
-no-docstring-rgx=__.*__
+no-docstring-rgx=(__.*__|test_.*)
[MISCELLANEOUS]
diff --git a/cms/djangoapps/contentstore/course_info_model.py b/cms/djangoapps/contentstore/course_info_model.py
index 8c8aed549d..589db4ac56 100644
--- a/cms/djangoapps/contentstore/course_info_model.py
+++ b/cms/djangoapps/contentstore/course_info_model.py
@@ -1,13 +1,15 @@
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
-from lxml import html, etree
+from lxml import html
import re
from django.http import HttpResponseBadRequest
import logging
+import django.utils
-## TODO store as array of { date, content } and override course_info_module.definition_from_xml
-## This should be in a class which inherits from XmlDescriptor
+# # TODO store as array of { date, content } and override course_info_module.definition_from_xml
+# # This should be in a class which inherits from XmlDescriptor
+log = logging.getLogger(__name__)
def get_course_updates(location):
@@ -26,9 +28,11 @@ def get_course_updates(location):
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try:
- course_html_parsed = etree.fromstring(course_updates.data)
- except etree.XMLSyntaxError:
- course_html_parsed = etree.fromstring("
subs and then rest of val
course_upd_collection = []
@@ -64,9 +68,11 @@ def update_course_updates(location, update, passed_id=None):
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try:
- course_html_parsed = etree.fromstring(course_updates.data)
- except etree.XMLSyntaxError:
- course_html_parsed = etree.fromstring("")
+ course_html_parsed = html.fromstring(course_updates.data)
+ except:
+ log.error("Cannot parse: " + course_updates.data)
+ escaped = django.utils.html.escape(course_updates.data)
+ course_html_parsed = html.fromstring("
" + escaped + "
")
# No try/catch b/c failure generates an error back to client
new_html_parsed = html.fromstring('
' + update['date'] + '
' + update['content'] + '
')
@@ -85,12 +91,19 @@ def update_course_updates(location, update, passed_id=None):
passed_id = course_updates.location.url() + "/" + str(idx)
# update db record
- course_updates.data = etree.tostring(course_html_parsed)
+ course_updates.data = html.tostring(course_html_parsed)
modulestore('direct').update_item(location, course_updates.data)
- return {"id" : passed_id,
- "date" : update['date'],
- "content" :update['content']}
+ if (len(new_html_parsed) == 1):
+ content = new_html_parsed[0].tail
+ else:
+ content = "\n".join([html.tostring(ele)
+ for ele in new_html_parsed[1:]])
+
+ return {"id": passed_id,
+ "date": update['date'],
+ "content": content}
+
def delete_course_update(location, update, passed_id):
"""
@@ -108,9 +121,11 @@ def delete_course_update(location, update, passed_id):
# TODO use delete_blank_text parser throughout and cache as a static var in a class
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try:
- course_html_parsed = etree.fromstring(course_updates.data)
- except etree.XMLSyntaxError:
- course_html_parsed = etree.fromstring("")
+ course_html_parsed = html.fromstring(course_updates.data)
+ except:
+ log.error("Cannot parse: " + course_updates.data)
+ escaped = django.utils.html.escape(course_updates.data)
+ course_html_parsed = html.fromstring("
" + escaped + "
")
if course_html_parsed.tag == 'ol':
# ??? Should this use the id in the json or in the url or does it matter?
@@ -121,7 +136,7 @@ def delete_course_update(location, update, passed_id):
course_html_parsed.remove(element_to_delete)
# update db record
- course_updates.data = etree.tostring(course_html_parsed)
+ course_updates.data = html.tostring(course_html_parsed)
store = modulestore('direct')
store.update_item(location, course_updates.data)
@@ -132,7 +147,6 @@ def get_idx(passed_id):
"""
From the url w/ idx appended, get the idx.
"""
- # TODO compile this regex into a class static and reuse for each call
- idx_matcher = re.search(r'.*/(\d+)$', passed_id)
+ idx_matcher = re.search(r'.*?/?(\d+)$', passed_id)
if idx_matcher:
return int(idx_matcher.group(1))
diff --git a/cms/djangoapps/contentstore/features/advanced-settings.feature b/cms/djangoapps/contentstore/features/advanced-settings.feature
index af97709ad0..558294e890 100644
--- a/cms/djangoapps/contentstore/features/advanced-settings.feature
+++ b/cms/djangoapps/contentstore/features/advanced-settings.feature
@@ -1,6 +1,6 @@
Feature: Advanced (manual) course policy
In order to specify course policy settings for which no custom user interface exists
- I want to be able to manually enter JSON key/value pairs
+ I want to be able to manually enter JSON key /value pairs
Scenario: A course author sees default advanced settings
Given I have opened a new course in Studio
@@ -21,8 +21,7 @@ Feature: Advanced (manual) course policy
Scenario: Test editing key value
Given I am on the Advanced Course Settings page in Studio
- When I edit the value of a policy key
- And I press the "Save" notification button
+ When I edit the value of a policy key and save
Then the policy key value is changed
And I reload the page
Then the policy key value is changed
diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py
index 7e86e94a31..6fb102faea 100644
--- a/cms/djangoapps/contentstore/features/advanced-settings.py
+++ b/cms/djangoapps/contentstore/features/advanced-settings.py
@@ -1,9 +1,10 @@
+#pylint: disable=C0111
+#pylint: disable=W0621
+
from lettuce import world, step
from common import *
import time
from terrain.steps import reload_the_page
-from selenium.common.exceptions import WebDriverException
-from selenium.webdriver.support import expected_conditions as EC
from nose.tools import assert_true, assert_false, assert_equal
@@ -18,13 +19,14 @@ DISPLAY_NAME_KEY = "display_name"
DISPLAY_NAME_VALUE = '"Robot Super Course"'
############### ACTIONS ####################
+
@step('I select the Advanced Settings$')
def i_select_advanced_settings(step):
expand_icon_css = 'li.nav-course-settings i.icon-expand'
if world.browser.is_element_present_by_css(expand_icon_css):
- css_click(expand_icon_css)
+ world.css_click(expand_icon_css)
link_css = 'li.nav-course-settings-advanced a'
- css_click(link_css)
+ world.css_click(link_css)
@step('I am on the Advanced Course Settings page in Studio$')
@@ -35,24 +37,8 @@ def i_am_on_advanced_course_settings(step):
@step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(step, name):
- def is_visible(driver):
- return EC.visibility_of_element_located((By.CSS_SELECTOR, css,))
-
- # def is_invisible(driver):
- # return EC.invisibility_of_element_located((By.CSS_SELECTOR,css,))
-
css = 'a.%s-button' % name.lower()
- wait_for(is_visible)
- time.sleep(float(1))
- css_click_at(css)
-
-# is_invisible is not returning a boolean, not working
-# try:
-# css_click_at(css)
-# wait_for(is_invisible)
-# except WebDriverException, e:
-# css_click_at(css)
-# wait_for(is_invisible)
+ world.css_click_at(css)
@step(u'I edit the value of a policy key$')
@@ -61,10 +47,15 @@ def edit_the_value_of_a_policy_key(step):
It is hard to figure out how to get into the CodeMirror
area, so cheat and do it from the policy key field :)
"""
- e = css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
+ e = world.css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
e._element.send_keys(Keys.TAB, Keys.END, Keys.ARROW_LEFT, ' ', 'X')
+@step(u'I edit the value of a policy key and save$')
+def edit_the_value_of_a_policy_key(step):
+ change_display_name_value(step, '"foo"')
+
+
@step('I create a JSON object as a value$')
def create_JSON_object(step):
change_display_name_value(step, '{"key": "value", "key_2": "value_2"}')
@@ -85,7 +76,7 @@ def i_see_default_advanced_settings(step):
@step('the settings are alphabetized$')
def they_are_alphabetized(step):
- key_elements = css_find(KEY_CSS)
+ key_elements = world.css_find(KEY_CSS)
all_keys = []
for key in key_elements:
all_keys.append(key.value)
@@ -110,7 +101,7 @@ def the_policy_key_value_is_unchanged(step):
@step(u'the policy key value is changed$')
def the_policy_key_value_is_changed(step):
- assert_equal(get_display_name_value(), '"Robot Super Course X"')
+ assert_equal(get_display_name_value(), '"foo"')
############# HELPERS ###############
@@ -118,13 +109,13 @@ def assert_policy_entries(expected_keys, expected_values):
for counter in range(len(expected_keys)):
index = get_index_of(expected_keys[counter])
assert_false(index == -1, "Could not find key: " + expected_keys[counter])
- assert_equal(expected_values[counter], css_find(VALUE_CSS)[index].value, "value is incorrect")
+ assert_equal(expected_values[counter], world.css_find(VALUE_CSS)[index].value, "value is incorrect")
def get_index_of(expected_key):
- for counter in range(len(css_find(KEY_CSS))):
+ for counter in range(len(world.css_find(KEY_CSS))):
# Sometimes get stale reference if I hold on to the array of elements
- key = css_find(KEY_CSS)[counter].value
+ key = world.css_find(KEY_CSS)[counter].value
if key == expected_key:
return counter
@@ -133,14 +124,14 @@ def get_index_of(expected_key):
def get_display_name_value():
index = get_index_of(DISPLAY_NAME_KEY)
- return css_find(VALUE_CSS)[index].value
+ return world.css_find(VALUE_CSS)[index].value
def change_display_name_value(step, new_value):
- e = css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
+ e = world.css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
display_name = get_display_name_value()
for count in range(len(display_name)):
e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE)
# Must delete "" before typing the JSON value
e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE, Keys.BACK_SPACE, new_value)
- press_the_notification_button(step, "Save")
\ No newline at end of file
+ press_the_notification_button(step, "Save")
diff --git a/cms/djangoapps/contentstore/features/checklists.feature b/cms/djangoapps/contentstore/features/checklists.feature
new file mode 100644
index 0000000000..bccb80b8d7
--- /dev/null
+++ b/cms/djangoapps/contentstore/features/checklists.feature
@@ -0,0 +1,24 @@
+Feature: Course checklists
+
+ Scenario: A course author sees checklists defined by edX
+ Given I have opened a new course in Studio
+ When I select Checklists from the Tools menu
+ Then I see the four default edX checklists
+
+ Scenario: A course author can mark tasks as complete
+ Given I have opened Checklists
+ Then I can check and uncheck tasks in a checklist
+ And They are correctly selected after I reload the page
+
+ Scenario: A task can link to a location within Studio
+ Given I have opened Checklists
+ When I select a link to the course outline
+ Then I am brought to the course outline page
+ And I press the browser back button
+ Then I am brought back to the course outline in the correct state
+
+ Scenario: A task can link to a location outside Studio
+ Given I have opened Checklists
+ When I select a link to help page
+ Then I am brought to the help page in a new window
+
diff --git a/cms/djangoapps/contentstore/features/checklists.py b/cms/djangoapps/contentstore/features/checklists.py
new file mode 100644
index 0000000000..dc399f5fac
--- /dev/null
+++ b/cms/djangoapps/contentstore/features/checklists.py
@@ -0,0 +1,123 @@
+#pylint: disable=C0111
+#pylint: disable=W0621
+
+from lettuce import world, step
+from nose.tools import assert_true, assert_equal
+from terrain.steps import reload_the_page
+from selenium.common.exceptions import StaleElementReferenceException
+
+############### ACTIONS ####################
+@step('I select Checklists from the Tools menu$')
+def i_select_checklists(step):
+ expand_icon_css = 'li.nav-course-tools i.icon-expand'
+ if world.browser.is_element_present_by_css(expand_icon_css):
+ world.css_click(expand_icon_css)
+ link_css = 'li.nav-course-tools-checklists a'
+ world.css_click(link_css)
+
+
+@step('I have opened Checklists$')
+def i_have_opened_checklists(step):
+ step.given('I have opened a new course in Studio')
+ step.given('I select Checklists from the Tools menu')
+
+
+@step('I see the four default edX checklists$')
+def i_see_default_checklists(step):
+ checklists = world.css_find('.checklist-title')
+ assert_equal(4, len(checklists))
+ assert_true(checklists[0].text.endswith('Getting Started With Studio'))
+ assert_true(checklists[1].text.endswith('Draft a Rough Course Outline'))
+ assert_true(checklists[2].text.endswith("Explore edX\'s Support Tools"))
+ assert_true(checklists[3].text.endswith('Draft Your Course About Page'))
+
+
+@step('I can check and uncheck tasks in a checklist$')
+def i_can_check_and_uncheck_tasks(step):
+ # Use the 2nd checklist as a reference
+ verifyChecklist2Status(0, 7, 0)
+ toggleTask(1, 0)
+ verifyChecklist2Status(1, 7, 14)
+ toggleTask(1, 3)
+ verifyChecklist2Status(2, 7, 29)
+ toggleTask(1, 6)
+ verifyChecklist2Status(3, 7, 43)
+ toggleTask(1, 3)
+ verifyChecklist2Status(2, 7, 29)
+
+
+@step('They are correctly selected after I reload the page$')
+def tasks_correctly_selected_after_reload(step):
+ reload_the_page(step)
+ verifyChecklist2Status(2, 7, 29)
+ # verify that task 7 is still selected by toggling its checkbox state and making sure that it deselects
+ toggleTask(1, 6)
+ verifyChecklist2Status(1, 7, 14)
+
+
+@step('I select a link to the course outline$')
+def i_select_a_link_to_the_course_outline(step):
+ clickActionLink(1, 0, 'Edit Course Outline')
+
+
+@step('I am brought to the course outline page$')
+def i_am_brought_to_course_outline(step):
+ assert_equal('Course Outline', world.css_find('.outline .title-1')[0].text)
+ assert_equal(1, len(world.browser.windows))
+
+
+@step('I am brought back to the course outline in the correct state$')
+def i_am_brought_back_to_course_outline(step):
+ step.given('I see the four default edX checklists')
+ # In a previous step, we selected (1, 0) in order to click the 'Edit Course Outline' link.
+ # Make sure the task is still showing as selected (there was a caching bug with the collection).
+ verifyChecklist2Status(1, 7, 14)
+
+
+@step('I select a link to help page$')
+def i_select_a_link_to_the_help_page(step):
+ clickActionLink(2, 0, 'Visit Studio Help')
+
+
+@step('I am brought to the help page in a new window$')
+def i_am_brought_to_help_page_in_new_window(step):
+ step.given('I see the four default edX checklists')
+ windows = world.browser.windows
+ assert_equal(2, len(windows))
+ world.browser.switch_to_window(windows[1])
+ assert_equal('http://help.edge.edx.org/', world.browser.url)
+
+
+
+
+############### HELPER METHODS ####################
+def verifyChecklist2Status(completed, total, percentage):
+ def verify_count(driver):
+ try:
+ statusCount = world.css_find('#course-checklist1 .status-count').first
+ return statusCount.text == str(completed)
+ except StaleElementReferenceException:
+ return False
+
+ world.wait_for(verify_count)
+ assert_equal(str(total), world.css_find('#course-checklist1 .status-amount').first.text)
+ # Would like to check the CSS width, but not sure how to do that.
+ assert_equal(str(percentage), world.css_find('#course-checklist1 .viz-checklist-status-value .int').first.text)
+
+
+def toggleTask(checklist, task):
+ world.css_click('#course-checklist' + str(checklist) +'-task' + str(task))
+
+
+def clickActionLink(checklist, task, actionText):
+ # toggle checklist item to make sure that the link button is showing
+ toggleTask(checklist, task)
+ action_link = world.css_find('#course-checklist' + str(checklist) + ' a')[task]
+
+ # text will be empty initially, wait for it to populate
+ def verify_action_link_text(driver):
+ return action_link.text == actionText
+
+ world.wait_for(verify_action_link_text)
+ action_link.click()
+
diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py
index 2ec0427e1d..afb38c3f9e 100644
--- a/cms/djangoapps/contentstore/features/common.py
+++ b/cms/djangoapps/contentstore/features/common.py
@@ -1,30 +1,30 @@
+#pylint: disable=C0111
+#pylint: disable=W0621
+
from lettuce import world, step
-from lettuce.django import django_url
from nose.tools import assert_true
from nose.tools import assert_equal
-from selenium.webdriver.support.ui import WebDriverWait
-from selenium.common.exceptions import WebDriverException, StaleElementReferenceException
-from selenium.webdriver.support import expected_conditions as EC
-from selenium.webdriver.common.by import By
-from terrain.factories import UserFactory, RegistrationFactory, UserProfileFactory
-from terrain.factories import CourseFactory, GroupFactory
from xmodule.modulestore.django import _MODULESTORES, modulestore
from xmodule.templates import update_templates
from auth.authz import get_user_by_email
+from selenium.webdriver.common.keys import Keys
+import time
+
from logging import getLogger
logger = getLogger(__name__)
########### STEP HELPERS ##############
+
@step('I (?:visit|access|open) the Studio homepage$')
def i_visit_the_studio_homepage(step):
# To make this go to port 8001, put
# LETTUCE_SERVER_PORT = 8001
# in your settings.py file.
- world.browser.visit(django_url('/'))
+ world.visit('/')
signin_css = 'a.action-signin'
- assert world.browser.is_element_present_by_css(signin_css, 10)
+ assert world.is_css_present(signin_css)
@step('I am logged into Studio$')
@@ -45,12 +45,12 @@ def i_press_the_category_delete_icon(step, category):
css = 'a.delete-button.delete-subsection-button span.delete-icon'
else:
assert False, 'Invalid category: %s' % category
- css_click(css)
+ world.css_click(css)
@step('I have opened a new course in Studio$')
def i_have_opened_a_new_course(step):
- clear_courses()
+ world.clear_courses()
log_into_studio()
create_a_course()
@@ -61,7 +61,7 @@ def create_studio_user(
email='robot+studio@edx.org',
password='test',
is_staff=False):
- studio_user = UserFactory.build(
+ studio_user = world.UserFactory.build(
username=uname,
email=email,
password=password,
@@ -69,87 +69,20 @@ def create_studio_user(
studio_user.set_password(password)
studio_user.save()
- registration = RegistrationFactory(user=studio_user)
+ registration = world.RegistrationFactory(user=studio_user)
registration.register(studio_user)
registration.activate()
- user_profile = 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()
+ user_profile = world.UserProfileFactory(user=studio_user)
def fill_in_course_info(
name='Robot Super Course',
org='MITx',
num='101'):
- css_fill('.new-course-name', name)
- css_fill('.new-course-org', org)
- css_fill('.new-course-number', num)
+ world.css_fill('.new-course-name', name)
+ world.css_fill('.new-course-org', org)
+ world.css_fill('.new-course-number', num)
def log_into_studio(
@@ -157,55 +90,67 @@ def log_into_studio(
email='robot+studio@edx.org',
password='test',
is_staff=False):
- create_studio_user(uname=uname, email=email, is_staff=is_staff)
- world.browser.cookies.delete()
- world.browser.visit(django_url('/'))
- signin_css = 'a.action-signin'
- world.browser.is_element_present_by_css(signin_css, 10)
- # click the signin button
- css_click(signin_css)
+ create_studio_user(uname=uname, email=email, is_staff=is_staff)
+
+ world.browser.cookies.delete()
+ world.visit('/')
+
+ signin_css = 'a.action-signin'
+ world.is_css_present(signin_css)
+ world.css_click(signin_css)
login_form = world.browser.find_by_css('form#login_form')
login_form.find_by_name('email').fill(email)
login_form.find_by_name('password').fill(password)
login_form.find_by_name('submit').click()
- assert_true(world.browser.is_element_present_by_css('.new-course-button', 5))
+ assert_true(world.is_css_present('.new-course-button'))
def create_a_course():
- c = CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
+ c = world.CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
# Add the user to the instructor group of the course
# so they will have the permissions to see it in studio
- g = GroupFactory.create(name='instructor_MITx/999/Robot_Super_Course')
+ g = world.GroupFactory.create(name='instructor_MITx/999/Robot_Super_Course')
u = get_user_by_email('robot+studio@edx.org')
u.groups.add(g)
u.save()
world.browser.reload()
course_link_css = 'span.class-name'
- css_click(course_link_css)
+ world.css_click(course_link_css)
course_title_css = 'span.course-title'
- assert_true(world.browser.is_element_present_by_css(course_title_css, 5))
+ assert_true(world.is_css_present(course_title_css))
def add_section(name='My Section'):
link_css = 'a.new-courseware-section-button'
- css_click(link_css)
+ world.css_click(link_css)
name_css = 'input.new-section-name'
save_css = 'input.new-section-name-save'
- css_fill(name_css, name)
- css_click(save_css)
+ world.css_fill(name_css, name)
+ world.css_click(save_css)
span_css = 'span.section-name-span'
- assert_true(world.browser.is_element_present_by_css(span_css, 5))
+ assert_true(world.is_css_present(span_css))
def add_subsection(name='Subsection One'):
css = 'a.new-subsection-item'
- css_click(css)
+ world.css_click(css)
name_css = 'input.new-subsection-name-input'
save_css = 'input.new-subsection-name-save'
- css_fill(name_css, name)
- css_click(save_css)
+ world.css_fill(name_css, name)
+ world.css_click(save_css)
+
+
+def set_date_and_time(date_css, desired_date, time_css, desired_time):
+ world.css_fill(date_css, desired_date)
+ # hit TAB to get to the time field
+ e = world.css_find(date_css).first
+ e._element.send_keys(Keys.TAB)
+ world.css_fill(time_css, desired_time)
+ e = world.css_find(time_css).first
+ e._element.send_keys(Keys.TAB)
+ time.sleep(float(1))
diff --git a/cms/djangoapps/contentstore/features/course-settings.feature b/cms/djangoapps/contentstore/features/course-settings.feature
new file mode 100644
index 0000000000..e869bfe47a
--- /dev/null
+++ b/cms/djangoapps/contentstore/features/course-settings.feature
@@ -0,0 +1,25 @@
+Feature: Course Settings
+ As a course author, I want to be able to configure my course settings.
+
+ Scenario: User can set course dates
+ Given I have opened a new course in Studio
+ When I select Schedule and Details
+ And I set course dates
+ Then I see the set dates on refresh
+
+ Scenario: User can clear previously set course dates (except start date)
+ Given I have set course dates
+ And I clear all the dates except start
+ Then I see cleared dates on refresh
+
+ Scenario: User cannot clear the course start date
+ Given I have set course dates
+ And I clear the course start date
+ Then I receive a warning about course start date
+ And The previously set start date is shown on refresh
+
+ Scenario: User can correct the course start date warning
+ Given I have tried to clear the course start
+ And I have entered a new course start date
+ Then The warning about course start date goes away
+ And My new course start date is shown on refresh
diff --git a/cms/djangoapps/contentstore/features/course-settings.py b/cms/djangoapps/contentstore/features/course-settings.py
new file mode 100644
index 0000000000..d69266b7de
--- /dev/null
+++ b/cms/djangoapps/contentstore/features/course-settings.py
@@ -0,0 +1,165 @@
+#pylint: disable=C0111
+#pylint: disable=W0621
+
+from lettuce import world, step
+from terrain.steps import reload_the_page
+from selenium.webdriver.common.keys import Keys
+import time
+
+from nose.tools import assert_true, assert_false, assert_equal
+
+COURSE_START_DATE_CSS = "#course-start-date"
+COURSE_END_DATE_CSS = "#course-end-date"
+ENROLLMENT_START_DATE_CSS = "#course-enrollment-start-date"
+ENROLLMENT_END_DATE_CSS = "#course-enrollment-end-date"
+
+COURSE_START_TIME_CSS = "#course-start-time"
+COURSE_END_TIME_CSS = "#course-end-time"
+ENROLLMENT_START_TIME_CSS = "#course-enrollment-start-time"
+ENROLLMENT_END_TIME_CSS = "#course-enrollment-end-time"
+
+DUMMY_TIME = "15:30"
+DEFAULT_TIME = "00:00"
+
+
+############### ACTIONS ####################
+@step('I select Schedule and Details$')
+def test_i_select_schedule_and_details(step):
+ expand_icon_css = 'li.nav-course-settings i.icon-expand'
+ if world.browser.is_element_present_by_css(expand_icon_css):
+ world.css_click(expand_icon_css)
+ link_css = 'li.nav-course-settings-schedule a'
+ world.css_click(link_css)
+
+
+@step('I have set course dates$')
+def test_i_have_set_course_dates(step):
+ step.given('I have opened a new course in Studio')
+ step.given('I select Schedule and Details')
+ step.given('And I set course dates')
+
+
+@step('And I set course dates$')
+def test_and_i_set_course_dates(step):
+ set_date_or_time(COURSE_START_DATE_CSS, '12/20/2013')
+ set_date_or_time(COURSE_END_DATE_CSS, '12/26/2013')
+ set_date_or_time(ENROLLMENT_START_DATE_CSS, '12/1/2013')
+ set_date_or_time(ENROLLMENT_END_DATE_CSS, '12/10/2013')
+
+ set_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
+ set_date_or_time(ENROLLMENT_END_TIME_CSS, DUMMY_TIME)
+
+ pause()
+
+
+@step('Then I see the set dates on refresh$')
+def test_then_i_see_the_set_dates_on_refresh(step):
+ reload_the_page(step)
+ verify_date_or_time(COURSE_START_DATE_CSS, '12/20/2013')
+ verify_date_or_time(COURSE_END_DATE_CSS, '12/26/2013')
+ verify_date_or_time(ENROLLMENT_START_DATE_CSS, '12/01/2013')
+ verify_date_or_time(ENROLLMENT_END_DATE_CSS, '12/10/2013')
+
+ verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
+ # Unset times get set to 12 AM once the corresponding date has been set.
+ verify_date_or_time(COURSE_END_TIME_CSS, DEFAULT_TIME)
+ verify_date_or_time(ENROLLMENT_START_TIME_CSS, DEFAULT_TIME)
+ verify_date_or_time(ENROLLMENT_END_TIME_CSS, DUMMY_TIME)
+
+
+@step('And I clear all the dates except start$')
+def test_and_i_clear_all_the_dates_except_start(step):
+ set_date_or_time(COURSE_END_DATE_CSS, '')
+ set_date_or_time(ENROLLMENT_START_DATE_CSS, '')
+ set_date_or_time(ENROLLMENT_END_DATE_CSS, '')
+
+ pause()
+
+
+@step('Then I see cleared dates on refresh$')
+def test_then_i_see_cleared_dates_on_refresh(step):
+ reload_the_page(step)
+ verify_date_or_time(COURSE_END_DATE_CSS, '')
+ verify_date_or_time(ENROLLMENT_START_DATE_CSS, '')
+ verify_date_or_time(ENROLLMENT_END_DATE_CSS, '')
+
+ verify_date_or_time(COURSE_END_TIME_CSS, '')
+ verify_date_or_time(ENROLLMENT_START_TIME_CSS, '')
+ verify_date_or_time(ENROLLMENT_END_TIME_CSS, '')
+
+ # Verify course start date (required) and time still there
+ verify_date_or_time(COURSE_START_DATE_CSS, '12/20/2013')
+ verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
+
+
+@step('I clear the course start date$')
+def test_i_clear_the_course_start_date(step):
+ set_date_or_time(COURSE_START_DATE_CSS, '')
+
+
+@step('I receive a warning about course start date$')
+def test_i_receive_a_warning_about_course_start_date(step):
+ assert_true(world.css_has_text('.message-error', 'The course must have an assigned start date.'))
+ assert_true('error' in world.css_find(COURSE_START_DATE_CSS).first._element.get_attribute('class'))
+ assert_true('error' in world.css_find(COURSE_START_TIME_CSS).first._element.get_attribute('class'))
+
+
+@step('The previously set start date is shown on refresh$')
+def test_the_previously_set_start_date_is_shown_on_refresh(step):
+ reload_the_page(step)
+ verify_date_or_time(COURSE_START_DATE_CSS, '12/20/2013')
+ verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
+
+
+@step('Given I have tried to clear the course start$')
+def test_i_have_tried_to_clear_the_course_start(step):
+ step.given("I have set course dates")
+ step.given("I clear the course start date")
+ step.given("I receive a warning about course start date")
+
+
+@step('I have entered a new course start date$')
+def test_i_have_entered_a_new_course_start_date(step):
+ set_date_or_time(COURSE_START_DATE_CSS, '12/22/2013')
+ pause()
+
+
+@step('The warning about course start date goes away$')
+def test_the_warning_about_course_start_date_goes_away(step):
+ assert_equal(0, len(world.css_find('.message-error')))
+ assert_false('error' in world.css_find(COURSE_START_DATE_CSS).first._element.get_attribute('class'))
+ assert_false('error' in world.css_find(COURSE_START_TIME_CSS).first._element.get_attribute('class'))
+
+
+@step('My new course start date is shown on refresh$')
+def test_my_new_course_start_date_is_shown_on_refresh(step):
+ reload_the_page(step)
+ verify_date_or_time(COURSE_START_DATE_CSS, '12/22/2013')
+ # Time should have stayed from before attempt to clear date.
+ verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
+
+
+############### HELPER METHODS ####################
+def set_date_or_time(css, date_or_time):
+ """
+ Sets date or time field.
+ """
+ world.css_fill(css, date_or_time)
+ e = world.css_find(css).first
+ # hit Enter to apply the changes
+ e._element.send_keys(Keys.ENTER)
+
+
+def verify_date_or_time(css, date_or_time):
+ """
+ Verifies date or time field.
+ """
+ assert_equal(date_or_time, world.css_find(css).first.value)
+
+
+def pause():
+ """
+ Must sleep briefly to allow last time save to finish,
+ else refresh of browser will fail.
+ """
+ time.sleep(float(1))
diff --git a/cms/djangoapps/contentstore/features/courses.feature b/cms/djangoapps/contentstore/features/courses.feature
index 39d39b50aa..455313b0e2 100644
--- a/cms/djangoapps/contentstore/features/courses.feature
+++ b/cms/djangoapps/contentstore/features/courses.feature
@@ -10,4 +10,4 @@ Feature: Create Course
And I fill in the new course information
And I press the "Save" button
Then the Courseware page has loaded in Studio
- And I see a link for adding a new section
\ No newline at end of file
+ And I see a link for adding a new section
diff --git a/cms/djangoapps/contentstore/features/courses.py b/cms/djangoapps/contentstore/features/courses.py
index e394165f08..5da7720945 100644
--- a/cms/djangoapps/contentstore/features/courses.py
+++ b/cms/djangoapps/contentstore/features/courses.py
@@ -1,3 +1,6 @@
+#pylint: disable=C0111
+#pylint: disable=W0621
+
from lettuce import world, step
from common import *
@@ -6,12 +9,12 @@ from common import *
@step('There are no courses$')
def no_courses(step):
- clear_courses()
+ world.clear_courses()
@step('I click the New Course button$')
def i_click_new_course(step):
- css_click('.new-course-button')
+ world.css_click('.new-course-button')
@step('I fill in the new course information$')
@@ -27,7 +30,7 @@ def i_create_a_course(step):
@step('I click the course link in My Courses$')
def i_click_the_course_link_in_my_courses(step):
course_css = 'span.class-name'
- css_click(course_css)
+ world.css_click(course_css)
############ ASSERTIONS ###################
@@ -35,28 +38,28 @@ def i_click_the_course_link_in_my_courses(step):
@step('the Courseware page has loaded in Studio$')
def courseware_page_has_loaded_in_studio(step):
course_title_css = 'span.course-title'
- assert world.browser.is_element_present_by_css(course_title_css)
+ assert world.is_css_present(course_title_css)
@step('I see the course listed in My Courses$')
def i_see_the_course_in_my_courses(step):
course_css = 'span.class-name'
- assert_css_with_text(course_css, 'Robot Super Course')
+ assert world.css_has_text(course_css, 'Robot Super Course')
@step('the course is loaded$')
def course_is_loaded(step):
class_css = 'a.class-name'
- assert_css_with_text(class_css, 'Robot Super Course')
+ assert world.css_has_text(course_css, 'Robot Super Cousre')
@step('I am on the "([^"]*)" tab$')
def i_am_on_tab(step, tab_name):
header_css = 'div.inner-wrapper h1'
- assert_css_with_text(header_css, tab_name)
+ assert world.css_has_text(header_css, tab_name)
@step('I see a link for adding a new section$')
def i_see_new_section_link(step):
link_css = 'a.new-courseware-section-button'
- assert_css_with_text(link_css, '+ New Section')
+ assert world.css_has_text(link_css, '+ New Section')
diff --git a/cms/djangoapps/contentstore/features/factories.py b/cms/djangoapps/contentstore/features/factories.py
deleted file mode 100644
index 087ceaaa2d..0000000000
--- a/cms/djangoapps/contentstore/features/factories.py
+++ /dev/null
@@ -1,34 +0,0 @@
-import factory
-from student.models import User, UserProfile, Registration
-from datetime import datetime
-import uuid
-
-
-class UserProfileFactory(factory.Factory):
- FACTORY_FOR = UserProfile
-
- user = None
- name = 'Robot Studio'
- courseware = 'course.xml'
-
-
-class RegistrationFactory(factory.Factory):
- FACTORY_FOR = Registration
-
- user = None
- activation_key = uuid.uuid4().hex
-
-
-class UserFactory(factory.Factory):
- FACTORY_FOR = User
-
- username = 'robot-studio'
- email = 'robot+studio@edx.org'
- password = 'test'
- first_name = 'Robot'
- last_name = 'Studio'
- is_staff = False
- is_active = True
- is_superuser = False
- last_login = datetime.now()
- date_joined = datetime.now()
diff --git a/cms/djangoapps/contentstore/features/section.py b/cms/djangoapps/contentstore/features/section.py
index b5ddb48a09..59c5a37b33 100644
--- a/cms/djangoapps/contentstore/features/section.py
+++ b/cms/djangoapps/contentstore/features/section.py
@@ -1,8 +1,9 @@
+#pylint: disable=C0111
+#pylint: disable=W0621
+
from lettuce import world, step
from common import *
from nose.tools import assert_equal
-from selenium.webdriver.common.keys import Keys
-import time
############### ACTIONS ####################
@@ -10,7 +11,7 @@ import time
@step('I click the new section link$')
def i_click_new_section_link(step):
link_css = 'a.new-courseware-section-button'
- css_click(link_css)
+ world.css_click(link_css)
@step('I enter the section name and click save$')
@@ -31,21 +32,13 @@ def i_have_added_new_section(step):
@step('I click the Edit link for the release date$')
def i_click_the_edit_link_for_the_release_date(step):
button_css = 'div.section-published-date a.edit-button'
- css_click(button_css)
+ world.css_click(button_css)
@step('I save a new section release date$')
def i_save_a_new_section_release_date(step):
- date_css = 'input.start-date.date.hasDatepicker'
- time_css = 'input.start-time.time.ui-timepicker-input'
- css_fill(date_css, '12/25/2013')
- # hit TAB to get to the time field
- e = css_find(date_css).first
- e._element.send_keys(Keys.TAB)
- css_fill(time_css, '12:00am')
- e = css_find(time_css).first
- e._element.send_keys(Keys.TAB)
- time.sleep(float(1))
+ set_date_and_time('input.start-date.date.hasDatepicker', '12/25/2013',
+ 'input.start-time.time.ui-timepicker-input', '00:00')
world.browser.click_link_by_text('Save')
@@ -64,13 +57,13 @@ def i_see_my_section_name_with_quote_on_the_courseware_page(step):
@step('I click to edit the section name$')
def i_click_to_edit_section_name(step):
- css_click('span.section-name-span')
+ world.css_click('span.section-name-span')
@step('I see the complete section name with a quote in the editor$')
def i_see_complete_section_name_with_quote_in_editor(step):
css = '.edit-section-name'
- assert world.browser.is_element_present_by_css(css, 5)
+ assert world.is_css_present(css)
assert_equal(world.browser.find_by_css(css).value, 'Section with "Quote"')
@@ -85,7 +78,7 @@ def i_see_a_release_date_for_my_section(step):
import re
css = 'span.published-status'
- assert world.browser.is_element_present_by_css(css)
+ assert world.is_css_present(css)
status_text = world.browser.find_by_css(css).text
# e.g. 11/06/2012 at 16:25
@@ -99,20 +92,20 @@ def i_see_a_release_date_for_my_section(step):
@step('I see a link to create a new subsection$')
def i_see_a_link_to_create_a_new_subsection(step):
css = 'a.new-subsection-item'
- assert world.browser.is_element_present_by_css(css)
+ assert world.is_css_present(css)
@step('the section release date picker is not visible$')
def the_section_release_date_picker_not_visible(step):
css = 'div.edit-subsection-publish-settings'
- assert False, world.browser.find_by_css(css).visible
+ assert not world.css_visible(css)
@step('the section release date is updated$')
def the_section_release_date_is_updated(step):
css = 'span.published-status'
- status_text = world.browser.find_by_css(css).text
- assert_equal(status_text,'Will Release: 12/25/2013 at 12:00am')
+ status_text = world.css_text(css)
+ assert_equal(status_text, 'Will Release: 12/25/2013 at 00:00 UTC')
############ HELPER METHODS ###################
@@ -120,10 +113,10 @@ def the_section_release_date_is_updated(step):
def save_section_name(name):
name_css = '.new-section-name'
save_css = '.new-section-name-save'
- css_fill(name_css, name)
- css_click(save_css)
+ world.css_fill(name_css, name)
+ world.css_click(save_css)
def see_my_section_on_the_courseware_page(name):
section_css = 'span.section-name-span'
- assert_css_with_text(section_css, name)
+ assert world.css_has_text(section_css, name)
diff --git a/cms/djangoapps/contentstore/features/signup.py b/cms/djangoapps/contentstore/features/signup.py
index e8d0dd8229..6ca358183b 100644
--- a/cms/djangoapps/contentstore/features/signup.py
+++ b/cms/djangoapps/contentstore/features/signup.py
@@ -1,3 +1,6 @@
+#pylint: disable=C0111
+#pylint: disable=W0621
+
from lettuce import world, step
from common import *
@@ -17,9 +20,10 @@ def i_press_the_button_on_the_registration_form(step):
submit_css = 'form#register_form button#submit'
# Workaround for click not working on ubuntu
# for some unknown reason.
- e = css_find(submit_css)
+ e = world.css_find(submit_css)
e.type(' ')
+
@step('I should see be on the studio home page$')
def i_should_see_be_on_the_studio_home_page(step):
assert world.browser.find_by_css('div.inner-wrapper')
diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature b/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature
index 52c10e41a8..762dea6838 100644
--- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature
+++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature
@@ -1,30 +1,30 @@
Feature: Overview Toggle Section
In order to quickly view the details of a course's section or to scan the inventory of sections
- As a course author
- I want to toggle the visibility of each section's subsection details in the overview listing
+ As a course author
+ I want to toggle the visibility of each section's subsection details in the overview listing
Scenario: The default layout for the overview page is to show sections in expanded view
Given I have a course with multiple sections
- When I navigate to the course overview page
- Then I see the "Collapse All Sections" link
- And all sections are expanded
+ When I navigate to the course overview page
+ Then I see the "Collapse All Sections" link
+ And all sections are expanded
- Scenario: Expand/collapse for a course with no sections
+ Scenario: Expand /collapse for a course with no sections
Given I have a course with no sections
- When I navigate to the course overview page
- Then I do not see the "Collapse All Sections" link
+ When I navigate to the course overview page
+ Then I do not see the "Collapse All Sections" link
Scenario: Collapse link appears after creating first section of a course
Given I have a course with no sections
- When I navigate to the course overview page
- And I add a section
- Then I see the "Collapse All Sections" link
- And all sections are expanded
+ When I navigate to the course overview page
+ And I add a section
+ Then I see the "Collapse All Sections" link
+ And all sections are expanded
@skip-phantom
Scenario: Collapse link is not removed after last section of a course is deleted
Given I have a course with 1 section
- And I navigate to the course overview page
+ And I navigate to the course overview page
When I press the "section" delete icon
And I confirm the alert
Then I see the "Collapse All Sections" link
diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py
index 00aa39455d..7f717b731c 100644
--- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py
+++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py
@@ -1,5 +1,7 @@
+#pylint: disable=C0111
+#pylint: disable=W0621
+
from lettuce import world, step
-from terrain.factories import *
from common import *
from nose.tools import assert_true, assert_false, assert_equal
@@ -9,16 +11,16 @@ logger = getLogger(__name__)
@step(u'I have a course with no sections$')
def have_a_course(step):
- clear_courses()
- course = CourseFactory.create()
+ world.clear_courses()
+ course = world.CourseFactory.create()
@step(u'I have a course with 1 section$')
def have_a_course_with_1_section(step):
- clear_courses()
- course = CourseFactory.create()
- section = ItemFactory.create(parent_location=course.location)
- subsection1 = ItemFactory.create(
+ world.clear_courses()
+ course = world.CourseFactory.create()
+ section = world.ItemFactory.create(parent_location=course.location)
+ subsection1 = world.ItemFactory.create(
parent_location=section.location,
template='i4x://edx/templates/sequential/Empty',
display_name='Subsection One',)
@@ -26,21 +28,21 @@ def have_a_course_with_1_section(step):
@step(u'I have a course with multiple sections$')
def have_a_course_with_two_sections(step):
- clear_courses()
- course = CourseFactory.create()
- section = ItemFactory.create(parent_location=course.location)
- subsection1 = ItemFactory.create(
+ world.clear_courses()
+ course = world.CourseFactory.create()
+ section = world.ItemFactory.create(parent_location=course.location)
+ subsection1 = world.ItemFactory.create(
parent_location=section.location,
template='i4x://edx/templates/sequential/Empty',
display_name='Subsection One',)
- section2 = ItemFactory.create(
+ section2 = world.ItemFactory.create(
parent_location=course.location,
display_name='Section Two',)
- subsection2 = ItemFactory.create(
+ subsection2 = world.ItemFactory.create(
parent_location=section2.location,
template='i4x://edx/templates/sequential/Empty',
display_name='Subsection Alpha',)
- subsection3 = ItemFactory.create(
+ subsection3 = world.ItemFactory.create(
parent_location=section2.location,
template='i4x://edx/templates/sequential/Empty',
display_name='Subsection Beta',)
@@ -50,7 +52,7 @@ def have_a_course_with_two_sections(step):
def navigate_to_the_course_overview_page(step):
log_into_studio(is_staff=True)
course_locator = '.class-name'
- css_click(course_locator)
+ world.css_click(course_locator)
@step(u'I navigate to the courseware page of a course with multiple sections')
@@ -67,44 +69,44 @@ def i_add_a_section(step):
@step(u'I click the "([^"]*)" link$')
def i_click_the_text_span(step, text):
span_locator = '.toggle-button-sections span'
- assert_true(world.browser.is_element_present_by_css(span_locator, 5))
+ assert_true(world.browser.is_element_present_by_css(span_locator))
# first make sure that the expand/collapse text is the one you expected
assert_equal(world.browser.find_by_css(span_locator).value, text)
- css_click(span_locator)
+ world.css_click(span_locator)
@step(u'I collapse the first section$')
def i_collapse_a_section(step):
collapse_locator = 'section.courseware-section a.collapse'
- css_click(collapse_locator)
+ world.css_click(collapse_locator)
@step(u'I expand the first section$')
def i_expand_a_section(step):
expand_locator = 'section.courseware-section a.expand'
- css_click(expand_locator)
+ world.css_click(expand_locator)
@step(u'I see the "([^"]*)" link$')
def i_see_the_span_with_text(step, text):
span_locator = '.toggle-button-sections span'
- assert_true(world.browser.is_element_present_by_css(span_locator, 5))
- assert_equal(world.browser.find_by_css(span_locator).value, text)
- assert_true(world.browser.find_by_css(span_locator).visible)
+ assert_true(world.is_css_present(span_locator))
+ assert_equal(world.css_find(span_locator).value, text)
+ assert_true(world.css_visible(span_locator))
@step(u'I do not see the "([^"]*)" link$')
def i_do_not_see_the_span_with_text(step, text):
# Note that the span will exist on the page but not be visible
span_locator = '.toggle-button-sections span'
- assert_true(world.browser.is_element_present_by_css(span_locator))
- assert_false(world.browser.find_by_css(span_locator).visible)
+ assert_true(world.is_css_present(span_locator))
+ assert_false(world.css_visible(span_locator))
@step(u'all sections are expanded$')
def all_sections_are_expanded(step):
subsection_locator = 'div.subsection-list'
- subsections = world.browser.find_by_css(subsection_locator)
+ subsections = world.css_find(subsection_locator)
for s in subsections:
assert_true(s.visible)
@@ -112,6 +114,6 @@ def all_sections_are_expanded(step):
@step(u'all sections are collapsed$')
def all_sections_are_expanded(step):
subsection_locator = 'div.subsection-list'
- subsections = world.browser.find_by_css(subsection_locator)
+ subsections = world.css_find(subsection_locator)
for s in subsections:
assert_false(s.visible)
diff --git a/cms/djangoapps/contentstore/features/subsection.feature b/cms/djangoapps/contentstore/features/subsection.feature
index 1be5f4aeb9..cc3b2b1cbb 100644
--- a/cms/djangoapps/contentstore/features/subsection.feature
+++ b/cms/djangoapps/contentstore/features/subsection.feature
@@ -17,6 +17,21 @@ Feature: Create Subsection
And I click to edit the subsection name
Then I see the complete subsection name with a quote in the editor
+ Scenario: Assign grading type to a subsection and verify it is still shown after refresh (bug #258)
+ Given I have opened a new course section in Studio
+ And I have added a new subsection
+ And I mark it as Homework
+ Then I see it marked as Homework
+ And I reload the page
+ Then I see it marked as Homework
+
+ Scenario: Set a due date in a different year (bug #256)
+ Given I have opened a new subsection in Studio
+ And I have set a release date and due date in different years
+ Then I see the correct dates
+ And I reload the page
+ Then I see the correct dates
+
@skip-phantom
Scenario: Delete a subsection
Given I have opened a new course section in Studio
@@ -25,3 +40,5 @@ Feature: Create Subsection
When I press the "subsection" delete icon
And I confirm the alert
Then the subsection does not exist
+
+
diff --git a/cms/djangoapps/contentstore/features/subsection.py b/cms/djangoapps/contentstore/features/subsection.py
index 88e1424898..f9e5b52bb2 100644
--- a/cms/djangoapps/contentstore/features/subsection.py
+++ b/cms/djangoapps/contentstore/features/subsection.py
@@ -1,3 +1,6 @@
+#pylint: disable=C0111
+#pylint: disable=W0621
+
from lettuce import world, step
from common import *
from nose.tools import assert_equal
@@ -7,16 +10,27 @@ from nose.tools import assert_equal
@step('I have opened a new course section in Studio$')
def i_have_opened_a_new_course_section(step):
- clear_courses()
+ world.clear_courses()
log_into_studio()
create_a_course()
add_section()
+@step('I have added a new subsection$')
+def i_have_added_a_new_subsection(step):
+ add_subsection()
+
+
+@step('I have opened a new subsection in Studio$')
+def i_have_opened_a_new_subsection(step):
+ step.given('I have opened a new course section in Studio')
+ step.given('I have added a new subsection')
+ world.css_click('span.subsection-name-value')
+
+
@step('I click the New Subsection link')
def i_click_the_new_subsection_link(step):
- css = 'a.new-subsection-item'
- css_click(css)
+ world.css_click('a.new-subsection-item')
@step('I enter the subsection name and click save$')
@@ -31,19 +45,41 @@ def i_save_subsection_name_with_quote(step):
@step('I click to edit the subsection name$')
def i_click_to_edit_subsection_name(step):
- css_click('span.subsection-name-value')
+ world.css_click('span.subsection-name-value')
@step('I see the complete subsection name with a quote in the editor$')
def i_see_complete_subsection_name_with_quote_in_editor(step):
css = '.subsection-display-name-input'
- assert world.browser.is_element_present_by_css(css, 5)
- assert_equal(world.browser.find_by_css(css).value, 'Subsection With "Quote"')
+ assert world.is_css_present(css)
+ assert_equal(world.css_find(css).value, 'Subsection With "Quote"')
-@step('I have added a new subsection$')
-def i_have_added_a_new_subsection(step):
- add_subsection()
+@step('I have set a release date and due date in different years$')
+def test_have_set_dates_in_different_years(step):
+ set_date_and_time('input#start_date', '12/25/2011', 'input#start_time', '03:00')
+ world.css_click('.set-date')
+ # Use a year in the past so that current year will always be different.
+ set_date_and_time('input#due_date', '01/02/2012', 'input#due_time', '04:00')
+
+
+@step('I see the correct dates$')
+def i_see_the_correct_dates(step):
+ assert_equal('12/25/2011', world.css_find('input#start_date').first.value)
+ assert_equal('03:00', world.css_find('input#start_time').first.value)
+ assert_equal('01/02/2012', world.css_find('input#due_date').first.value)
+ assert_equal('04:00', world.css_find('input#due_time').first.value)
+
+
+@step('I mark it as Homework$')
+def i_mark_it_as_homework(step):
+ world.css_click('a.menu-toggle')
+ world.browser.click_link_by_text('Homework')
+
+
+@step('I see it marked as Homework$')
+def i_see_it_marked__as_homework(step):
+ assert_equal(world.css_find(".status-label").value, 'Homework')
############ ASSERTIONS ###################
@@ -70,11 +106,12 @@ def the_subsection_does_not_exist(step):
def save_subsection_name(name):
name_css = 'input.new-subsection-name-input'
save_css = 'input.new-subsection-name-save'
- css_fill(name_css, name)
- css_click(save_css)
+ world.css_fill(name_css, name)
+ world.css_click(save_css)
+
def see_subsection_name(name):
css = 'span.subsection-name'
- assert world.browser.is_element_present_by_css(css)
+ assert world.is_css_present(css)
css = 'span.subsection-name-value'
- assert_css_with_text(css, name)
+ assert world.css_has_text(css, name)
diff --git a/cms/djangoapps/contentstore/management/commands/check_course.py b/cms/djangoapps/contentstore/management/commands/check_course.py
new file mode 100644
index 0000000000..57965fe793
--- /dev/null
+++ b/cms/djangoapps/contentstore/management/commands/check_course.py
@@ -0,0 +1,68 @@
+from django.core.management.base import BaseCommand, CommandError
+from xmodule.modulestore.django import modulestore
+from xmodule.modulestore.xml_importer import check_module_metadata_editability
+from xmodule.course_module import CourseDescriptor
+
+from request_cache.middleware import RequestCache
+
+
+class Command(BaseCommand):
+ help = '''Enumerates through the course and find common errors'''
+
+ def handle(self, *args, **options):
+ if len(args) != 1:
+ raise CommandError("check_course requires one argument: ")
+
+ loc_str = args[0]
+
+ loc = CourseDescriptor.id_to_location(loc_str)
+ store = modulestore()
+
+ # setup a request cache so we don't throttle the DB with all the metadata inheritance requests
+ store.request_cache = RequestCache.get_request_cache()
+
+ course = store.get_item(loc, depth=3)
+
+ err_cnt = 0
+
+ def _xlint_metadata(module):
+ err_cnt = check_module_metadata_editability(module)
+ for child in module.get_children():
+ err_cnt = err_cnt + _xlint_metadata(child)
+ return err_cnt
+
+ err_cnt = err_cnt + _xlint_metadata(course)
+
+ # we've had a bug where the xml_attributes field can we rewritten as a string rather than a dict
+ def _check_xml_attributes_field(module):
+ err_cnt = 0
+ if hasattr(module, 'xml_attributes') and isinstance(module.xml_attributes, basestring):
+ print 'module = {0} has xml_attributes as a string. It should be a dict'.format(module.location.url())
+ err_cnt = err_cnt + 1
+ for child in module.get_children():
+ err_cnt = err_cnt + _check_xml_attributes_field(child)
+ return err_cnt
+
+ err_cnt = err_cnt + _check_xml_attributes_field(course)
+
+ # check for dangling discussion items, this can cause errors in the forums
+ def _get_discussion_items(module):
+ discussion_items = []
+ if module.location.category == 'discussion':
+ discussion_items = discussion_items + [module.location.url()]
+
+ for child in module.get_children():
+ discussion_items = discussion_items + _get_discussion_items(child)
+
+ return discussion_items
+
+ discussion_items = _get_discussion_items(course)
+
+ # now query all discussion items via get_items() and compare with the tree-traversal
+ queried_discussion_items = store.get_items(['i4x', course.location.org, course.location.course,
+ 'discussion', None, None])
+
+ for item in queried_discussion_items:
+ if item.location.url() not in discussion_items:
+ print 'Found dangling discussion module = {0}'.format(item.location.url())
+
diff --git a/cms/djangoapps/contentstore/tests/test_checklists.py b/cms/djangoapps/contentstore/tests/test_checklists.py
new file mode 100644
index 0000000000..f0889b0861
--- /dev/null
+++ b/cms/djangoapps/contentstore/tests/test_checklists.py
@@ -0,0 +1,96 @@
+""" Unit tests for checklist methods in views.py. """
+from contentstore.utils import get_modulestore, get_url_reverse
+from contentstore.tests.test_course_settings import CourseTestCase
+from xmodule.modulestore.inheritance import own_metadata
+from xmodule.modulestore.tests.factories import CourseFactory
+from django.core.urlresolvers import reverse
+import json
+
+
+class ChecklistTestCase(CourseTestCase):
+ """ Test for checklist get and put methods. """
+ def setUp(self):
+ """ Creates the test course. """
+ super(ChecklistTestCase, self).setUp()
+ self.course = CourseFactory.create(org='mitX', number='333', display_name='Checklists Course')
+
+ def get_persisted_checklists(self):
+ """ Returns the checklists as persisted in the modulestore. """
+ modulestore = get_modulestore(self.course.location)
+ return modulestore.get_item(self.course.location).checklists
+
+ def test_get_checklists(self):
+ """ Tests the get checklists method. """
+ checklists_url = get_url_reverse('Checklists', self.course)
+ response = self.client.get(checklists_url)
+ self.assertContains(response, "Getting Started With Studio")
+ payload = response.content
+
+ # Now delete the checklists from the course and verify they get repopulated (for courses
+ # created before checklists were introduced).
+ self.course.checklists = None
+ modulestore = get_modulestore(self.course.location)
+ modulestore.update_metadata(self.course.location, own_metadata(self.course))
+ self.assertEquals(self.get_persisted_checklists(), None)
+ response = self.client.get(checklists_url)
+ self.assertEquals(payload, response.content)
+
+ def test_update_checklists_no_index(self):
+ """ No checklist index, should return all of them. """
+ update_url = reverse('checklists_updates', kwargs={
+ 'org': self.course.location.org,
+ 'course': self.course.location.course,
+ 'name': self.course.location.name})
+
+ returned_checklists = json.loads(self.client.get(update_url).content)
+ self.assertListEqual(self.get_persisted_checklists(), returned_checklists)
+
+ def test_update_checklists_index_ignored_on_get(self):
+ """ Checklist index ignored on get. """
+ update_url = reverse('checklists_updates', kwargs={'org': self.course.location.org,
+ 'course': self.course.location.course,
+ 'name': self.course.location.name,
+ 'checklist_index': 1})
+
+ returned_checklists = json.loads(self.client.get(update_url).content)
+ self.assertListEqual(self.get_persisted_checklists(), returned_checklists)
+
+ def test_update_checklists_post_no_index(self):
+ """ No checklist index, will error on post. """
+ update_url = reverse('checklists_updates', kwargs={'org': self.course.location.org,
+ 'course': self.course.location.course,
+ 'name': self.course.location.name})
+ response = self.client.post(update_url)
+ self.assertContains(response, 'Could not save checklist', status_code=400)
+
+ def test_update_checklists_index_out_of_range(self):
+ """ Checklist index out of range, will error on post. """
+ update_url = reverse('checklists_updates', kwargs={'org': self.course.location.org,
+ 'course': self.course.location.course,
+ 'name': self.course.location.name,
+ 'checklist_index': 100})
+ response = self.client.post(update_url)
+ self.assertContains(response, 'Could not save checklist', status_code=400)
+
+ def test_update_checklists_index(self):
+ """ Check that an update of a particular checklist works. """
+ update_url = reverse('checklists_updates', kwargs={'org': self.course.location.org,
+ 'course': self.course.location.course,
+ 'name': self.course.location.name,
+ 'checklist_index': 2})
+ payload = self.course.checklists[2]
+ self.assertFalse(payload.get('is_checked'))
+ payload['is_checked'] = True
+
+ returned_checklist = json.loads(self.client.post(update_url, json.dumps(payload), "application/json").content)
+ self.assertTrue(returned_checklist.get('is_checked'))
+ self.assertEqual(self.get_persisted_checklists()[2], returned_checklist)
+
+ def test_update_checklists_delete_unsupported(self):
+ """ Delete operation is not supported. """
+ update_url = reverse('checklists_updates', kwargs={'org': self.course.location.org,
+ 'course': self.course.location.course,
+ 'name': self.course.location.name,
+ 'checklist_index': 100})
+ response = self.client.delete(update_url)
+ self.assertContains(response, 'Unsupported request', status_code=400)
\ No newline at end of file
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index d04e1a6332..451ab96ca6 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -25,7 +25,7 @@ from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore
from xmodule.templates import update_templates
from xmodule.modulestore.xml_exporter import export_to_xml
-from xmodule.modulestore.xml_importer import import_from_xml
+from xmodule.modulestore.xml_importer import import_from_xml, perform_xlint
from xmodule.modulestore.inheritance import own_metadata
from xmodule.capa_module import CapaDescriptor
@@ -37,6 +37,14 @@ TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data')
+class MongoCollectionFindWrapper(object):
+ def __init__(self, original):
+ self.original = original
+ self.counter = 0
+
+ def find(self, query, *args, **kwargs):
+ self.counter = self.counter+1
+ return self.original(query, *args, **kwargs)
@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
class ContentStoreToyCourseTest(ModuleStoreTestCase):
@@ -77,6 +85,106 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_edit_unit_full(self):
self.check_edit_unit('full')
+ def _get_draft_counts(self, item):
+ cnt = 1 if getattr(item, 'is_draft', False) else 0
+ for child in item.get_children():
+ cnt = cnt + self._get_draft_counts(child)
+
+ return cnt
+
+ def test_draft_metadata(self):
+ '''
+ This verifies a bug we had where inherited metadata was getting written to the
+ module as 'own-metadata' when publishing. Also verifies the metadata inheritance is
+ properly computed
+ '''
+ store = modulestore()
+ draft_store = modulestore('draft')
+ import_from_xml(store, 'common/test/data/', ['simple'])
+
+ course = draft_store.get_item(Location(['i4x', 'edX', 'simple',
+ 'course', '2012_Fall', None]), depth=None)
+ html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
+
+ self.assertEqual(html_module.lms.graceperiod, course.lms.graceperiod)
+ self.assertNotIn('graceperiod', own_metadata(html_module))
+
+ draft_store.clone_item(html_module.location, html_module.location)
+
+ # refetch to check metadata
+ html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
+
+ self.assertEqual(html_module.lms.graceperiod, course.lms.graceperiod)
+ self.assertNotIn('graceperiod', own_metadata(html_module))
+
+ # publish module
+ draft_store.publish(html_module.location, 0)
+
+ # refetch to check metadata
+ html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
+
+ self.assertEqual(html_module.lms.graceperiod, course.lms.graceperiod)
+ self.assertNotIn('graceperiod', own_metadata(html_module))
+
+ # put back in draft and change metadata and see if it's now marked as 'own_metadata'
+ draft_store.clone_item(html_module.location, html_module.location)
+ html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
+
+ new_graceperiod = timedelta(**{'hours': 1})
+
+ self.assertNotIn('graceperiod', own_metadata(html_module))
+ html_module.lms.graceperiod = new_graceperiod
+ self.assertIn('graceperiod', own_metadata(html_module))
+ self.assertEqual(html_module.lms.graceperiod, new_graceperiod)
+
+ draft_store.update_metadata(html_module.location, own_metadata(html_module))
+
+ # read back to make sure it reads as 'own-metadata'
+ html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
+
+ self.assertIn('graceperiod', own_metadata(html_module))
+ self.assertEqual(html_module.lms.graceperiod, new_graceperiod)
+
+ # republish
+ draft_store.publish(html_module.location, 0)
+
+ # and re-read and verify 'own-metadata'
+ draft_store.clone_item(html_module.location, html_module.location)
+ html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
+
+ self.assertIn('graceperiod', own_metadata(html_module))
+ self.assertEqual(html_module.lms.graceperiod, new_graceperiod)
+
+ def test_get_depth_with_drafts(self):
+ import_from_xml(modulestore(), 'common/test/data/', ['simple'])
+
+ course = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple',
+ 'course', '2012_Fall', None]), depth=None)
+
+ # make sure no draft items have been returned
+ num_drafts = self._get_draft_counts(course)
+ self.assertEqual(num_drafts, 0)
+
+ problem = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple',
+ 'problem', 'ps01-simple', None]))
+
+ # put into draft
+ modulestore('draft').clone_item(problem.location, problem.location)
+
+ # make sure we can query that item and verify that it is a draft
+ draft_problem = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple',
+ 'problem', 'ps01-simple', None]))
+ self.assertTrue(getattr(draft_problem,'is_draft', False))
+
+ #now requery with depth
+ course = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple',
+ 'course', '2012_Fall', None]), depth=None)
+
+ # make sure just one draft item have been returned
+ num_drafts = self._get_draft_counts(course)
+ self.assertEqual(num_drafts, 1)
+
+
def test_static_tab_reordering(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
@@ -101,6 +209,24 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertEqual(reverse_tabs, course_tabs)
+ def test_import_polls(self):
+ import_from_xml(modulestore(), 'common/test/data/', ['full'])
+
+ module_store = modulestore('direct')
+ found = False
+
+ item = None
+ items = module_store.get_items(['i4x', 'edX', 'full', 'poll_question', None, None])
+ found = len(items) > 0
+
+ self.assertTrue(found)
+ # check that there's actually content in the 'question' field
+ self.assertGreater(len(items[0].question),0)
+
+ def test_xlint_fails(self):
+ err_cnt = perform_xlint('common/test/data', ['full'])
+ self.assertGreater(err_cnt, 0)
+
def test_delete(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
@@ -131,8 +257,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# make sure the parent no longer points to the child object which was deleted
self.assertFalse(sequential.location.url() in chapter.children)
-
-
def test_about_overrides(self):
'''
This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html
@@ -193,6 +317,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
resp = self.client.get(reverse('edit_unit', kwargs={'location': new_loc.url()}))
self.assertEqual(resp.status_code, 200)
+ def test_bad_contentstore_request(self):
+ resp = self.client.get('http://localhost:8001/c4x/CDX/123123/asset/&images_circuits_Lab7Solution2.png')
+ self.assertEqual(resp.status_code, 400)
+
def test_delete_course(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
@@ -293,6 +421,28 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# note, we know the link it should be because that's what in the 'full' course in the test data
self.assertContains(resp, '/c4x/edX/full/asset/handouts_schematic_tutorial.pdf')
+ def test_prefetch_children(self):
+ import_from_xml(modulestore(), 'common/test/data/', ['full'])
+ module_store = modulestore('direct')
+ location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
+
+ wrapper = MongoCollectionFindWrapper(module_store.collection.find)
+ module_store.collection.find = wrapper.find
+ course = module_store.get_item(location, depth=2)
+
+ # make sure we haven't done too many round trips to DB
+ # note we say 4 round trips here for 1) the course, 2 & 3) for the chapters and sequentials, and
+ # 4) because of the RT due to calculating the inherited metadata
+ self.assertEqual(wrapper.counter, 4)
+
+ # make sure we pre-fetched a known sequential which should be at depth=2
+ self.assertTrue(Location(['i4x', 'edX', 'full', 'sequential',
+ 'Administrivia_and_Circuit_Elements', None]) in course.system.module_data)
+
+ # make sure we don't have a specific vertical which should be at depth=3
+ self.assertFalse(Location(['i4x', 'edX', 'full', 'vertical', 'vertical_58',
+ None]) in course.system.module_data)
+
def test_export_course_with_unknown_metadata(self):
module_store = modulestore('direct')
content_store = contentstore()
@@ -478,6 +628,113 @@ class ContentStoreTest(ModuleStoreTestCase):
self.assertIn('markdown', context, "markdown is missing from context")
self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields")
+ def test_cms_imported_course_walkthrough(self):
+ """
+ Import and walk through some common URL endpoints. This just verifies non-500 and no other
+ correct behavior, so it is not a deep test
+ """
+ import_from_xml(modulestore(), 'common/test/data/', ['simple'])
+ loc = Location(['i4x', 'edX', 'simple', 'course', '2012_Fall', None])
+ resp = self.client.get(reverse('course_index',
+ kwargs={'org': loc.org,
+ 'course': loc.course,
+ 'name': loc.name}))
+
+ self.assertEqual(200, resp.status_code)
+ self.assertContains(resp, 'Chapter 2')
+
+ # go to various pages
+
+ # import page
+ resp = self.client.get(reverse('import_course',
+ kwargs={'org': loc.org,
+ 'course': loc.course,
+ 'name': loc.name}))
+ self.assertEqual(200, resp.status_code)
+
+ # export page
+ resp = self.client.get(reverse('export_course',
+ kwargs={'org': loc.org,
+ 'course': loc.course,
+ 'name': loc.name}))
+ self.assertEqual(200, resp.status_code)
+
+ # manage users
+ resp = self.client.get(reverse('manage_users',
+ kwargs={'location': loc.url()}))
+ self.assertEqual(200, resp.status_code)
+
+ # course info
+ resp = self.client.get(reverse('course_info',
+ kwargs={'org': loc.org,
+ 'course': loc.course,
+ 'name': loc.name}))
+ self.assertEqual(200, resp.status_code)
+
+ # settings_details
+ resp = self.client.get(reverse('settings_details',
+ kwargs={'org': loc.org,
+ 'course': loc.course,
+ 'name': loc.name}))
+ self.assertEqual(200, resp.status_code)
+
+ # settings_details
+ resp = self.client.get(reverse('settings_grading',
+ kwargs={'org': loc.org,
+ 'course': loc.course,
+ 'name': loc.name}))
+ self.assertEqual(200, resp.status_code)
+
+ # static_pages
+ resp = self.client.get(reverse('static_pages',
+ kwargs={'org': loc.org,
+ 'course': loc.course,
+ 'coursename': loc.name}))
+ self.assertEqual(200, resp.status_code)
+
+ # static_pages
+ resp = self.client.get(reverse('asset_index',
+ kwargs={'org': loc.org,
+ 'course': loc.course,
+ 'name': loc.name}))
+ self.assertEqual(200, resp.status_code)
+
+ # go look at a subsection page
+ subsection_location = loc._replace(category='sequential', name='test_sequence')
+ resp = self.client.get(reverse('edit_subsection',
+ kwargs={'location': subsection_location.url()}))
+ self.assertEqual(200, resp.status_code)
+
+ # go look at the Edit page
+ unit_location = loc._replace(category='vertical', name='test_vertical')
+ resp = self.client.get(reverse('edit_unit',
+ kwargs={'location': unit_location.url()}))
+ self.assertEqual(200, resp.status_code)
+
+ # delete a component
+ del_loc = loc._replace(category='html', name='test_html')
+ resp = self.client.post(reverse('delete_item'),
+ json.dumps({'id': del_loc.url()}), "application/json")
+ self.assertEqual(200, resp.status_code)
+
+ # delete a unit
+ del_loc = loc._replace(category='vertical', name='test_vertical')
+ resp = self.client.post(reverse('delete_item'),
+ json.dumps({'id': del_loc.url()}), "application/json")
+ self.assertEqual(200, resp.status_code)
+
+ # delete a unit
+ del_loc = loc._replace(category='sequential', name='test_sequence')
+ resp = self.client.post(reverse('delete_item'),
+ json.dumps({'id': del_loc.url()}), "application/json")
+ self.assertEqual(200, resp.status_code)
+
+ # delete a chapter
+ del_loc = loc._replace(category='chapter', name='chapter_2')
+ resp = self.client.post(reverse('delete_item'),
+ json.dumps({'id': del_loc.url()}), "application/json")
+ self.assertEqual(200, resp.status_code)
+
def test_import_metadata_with_attempts_empty_string(self):
import_from_xml(modulestore(), 'common/test/data/', ['simple'])
module_store = modulestore('direct')
@@ -514,7 +771,7 @@ class ContentStoreTest(ModuleStoreTestCase):
module_store.update_children(parent.location, parent.children + [new_component_location.url()])
# flush the cache
- module_store.get_cached_metadata_inheritance_tree(new_component_location, -1)
+ module_store.refresh_cached_metadata_inheritance_tree(new_component_location)
new_module = module_store.get_item(new_component_location)
# check for grace period definition which should be defined at the course level
@@ -529,7 +786,7 @@ class ContentStoreTest(ModuleStoreTestCase):
module_store.update_metadata(new_module.location, own_metadata(new_module))
# flush the cache and refetch
- module_store.get_cached_metadata_inheritance_tree(new_component_location, -1)
+ module_store.refresh_cached_metadata_inheritance_tree(new_component_location)
new_module = module_store.get_item(new_component_location)
self.assertEqual(timedelta(1), new_module.lms.graceperiod)
diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py
index ecdeca29e7..fe90ad18aa 100644
--- a/cms/djangoapps/contentstore/tests/test_course_settings.py
+++ b/cms/djangoapps/contentstore/tests/test_course_settings.py
@@ -1,8 +1,6 @@
import datetime
import json
import copy
-from util import converters
-from util.converters import jsdate_to_time
from django.contrib.auth.models import User
from django.test.client import Client
@@ -15,33 +13,13 @@ from models.settings.course_details import (CourseDetails,
from models.settings.course_grading import CourseGradingModel
from contentstore.utils import get_modulestore
-from django.test import TestCase
from .utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from models.settings.course_metadata import CourseMetadata
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.django import modulestore
-
-
-# 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):
- 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))
-
+from xmodule.fields import Date
class CourseTestCase(ModuleStoreTestCase):
def setUp(self):
@@ -104,7 +82,7 @@ class CourseDetailsTestCase(CourseTestCase):
self.assertIsNone(jsondetails['effort'], "effort somehow initialized")
def test_update_and_fetch(self):
- ## NOTE: I couldn't figure out how to validly test time setting w/ all the conversions
+ # # NOTE: I couldn't figure out how to validly test time setting w/ all the conversions
jsondetails = CourseDetails.fetch(self.course_location)
jsondetails.syllabus = "bar"
# encode - decode to convert date fields and other data which changes form
@@ -170,19 +148,26 @@ class CourseDetailsViewTest(CourseTestCase):
self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==")
self.assertEqual(details['effort'], encoded['effort'], context + " efforts not ==")
+ @staticmethod
+ def struct_to_datetime(struct_time):
+ return datetime.datetime(struct_time.tm_year, struct_time.tm_mon,
+ struct_time.tm_mday, struct_time.tm_hour,
+ struct_time.tm_min, struct_time.tm_sec, tzinfo=UTC())
+
def compare_date_fields(self, details, encoded, context, field):
if details[field] is not None:
+ date = Date()
if field in encoded and encoded[field] is not None:
- encoded_encoded = jsdate_to_time(encoded[field])
- dt1 = ConvertersTestCase.struct_to_datetime(encoded_encoded)
+ encoded_encoded = date.from_json(encoded[field])
+ dt1 = CourseDetailsViewTest.struct_to_datetime(encoded_encoded)
if isinstance(details[field], datetime.datetime):
dt2 = details[field]
else:
- details_encoded = jsdate_to_time(details[field])
- dt2 = ConvertersTestCase.struct_to_datetime(details_encoded)
+ details_encoded = date.from_json(details[field])
+ dt2 = CourseDetailsViewTest.struct_to_datetime(details_encoded)
- expected_delta = datetime.timedelta(0)
+ expected_delta = datetime.timedelta(0)
self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "!=" + str(dt2) + " at " + context)
else:
self.fail(field + " missing from encoded but in details at " + context)
@@ -269,7 +254,7 @@ class CourseMetadataEditingTest(CourseTestCase):
CourseTestCase.setUp(self)
# add in the full class too
import_from_xml(modulestore(), 'common/test/data/', ['full'])
- self.fullcourse_location = Location(['i4x','edX','full','course','6.002_Spring_2012', None])
+ self.fullcourse_location = Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])
def test_fetch_initial_fields(self):
diff --git a/cms/djangoapps/contentstore/tests/test_course_updates.py b/cms/djangoapps/contentstore/tests/test_course_updates.py
index 6a3a1e21f7..80d4f0bbc2 100644
--- a/cms/djangoapps/contentstore/tests/test_course_updates.py
+++ b/cms/djangoapps/contentstore/tests/test_course_updates.py
@@ -1,31 +1,145 @@
+'''unit tests for course_info views and models.'''
from contentstore.tests.test_course_settings import CourseTestCase
from django.core.urlresolvers import reverse
import json
class CourseUpdateTest(CourseTestCase):
+ '''The do all and end all of unit test cases.'''
def test_course_update(self):
+ '''Go through each interface and ensure it works.'''
# first get the update to force the creation
- url = reverse('course_info', kwargs={'org': self.course_location.org, 'course': self.course_location.course,
- 'name': self.course_location.name})
+ url = reverse('course_info',
+ kwargs={'org': self.course_location.org,
+ 'course': self.course_location.course,
+ 'name': self.course_location.name})
self.client.get(url)
- content = ''
+ init_content = ''
payload = {'content': content,
'date': 'January 8, 2013'}
- url = reverse('course_info', kwargs={'org': self.course_location.org, 'course': self.course_location.course,
- 'provided_id': ''})
+ url = reverse('course_info_json',
+ kwargs={'org': self.course_location.org,
+ 'course': self.course_location.course,
+ 'provided_id': ''})
resp = self.client.post(url, json.dumps(payload), "application/json")
payload = json.loads(resp.content)
- self.assertHTMLEqual(content, payload['content'], "single iframe")
+ self.assertHTMLEqual(payload['content'], content)
- url = reverse('course_info', kwargs={'org': self.course_location.org, 'course': self.course_location.course,
- 'provided_id': payload['id']})
- content += '