Merge branch 'master' into feature/btalbot/studio-alerts

This commit is contained in:
Brian Talbot
2013-03-12 16:05:54 -04:00
79 changed files with 1316 additions and 808 deletions

View File

@@ -7,6 +7,7 @@ Feature: Advanced (manual) course policy
When I select the Advanced Settings
Then I see only the display name
@skip-phantom
Scenario: Test if there are no policy settings without existing UI controls
Given I am on the Advanced Course Settings page in Studio
When I delete the display name
@@ -14,6 +15,7 @@ Feature: Advanced (manual) course policy
And I reload the page
Then there are no advanced policy settings
@skip-phantom
Scenario: Test cancel editing key name
Given I am on the Advanced Course Settings page in Studio
When I edit the name of a policy key
@@ -32,6 +34,7 @@ Feature: Advanced (manual) course policy
And I press the "Cancel" notification button
Then the policy key value is unchanged
@skip-phantom
Scenario: Test editing key value
Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key

View File

@@ -1,9 +1,10 @@
from lettuce import world, step
from common import *
import time
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.support import expected_conditions as EC
from nose.tools import assert_equal
from nose.tools import assert_true
from nose.tools import assert_true, assert_false, assert_equal
"""
http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdriver.common.keys.html
@@ -19,6 +20,7 @@ def i_select_advanced_settings(step):
css_click(expand_icon_css)
link_css = 'li.nav-course-settings-advanced a'
css_click(link_css)
# world.browser.click_link_by_text('Advanced Settings')
@step('I am on the Advanced Course Settings page in Studio$')
@@ -37,13 +39,25 @@ def reload_the_page(step):
def edit_the_name_of_a_policy_key(step):
policy_key_css = 'input.policy-key'
e = css_find(policy_key_css).first
e.fill('new')
e.type('_new')
@step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(step, name):
world.browser.click_link_by_text(name)
def is_visible(driver):
return EC.visibility_of_element_located((By.CSS_SELECTOR,css,))
def is_invisible(driver):
return EC.invisibility_of_element_located((By.CSS_SELECTOR,css,))
css = 'a.%s-button' % name.lower()
wait_for(is_visible)
try:
css_click_at(css)
wait_for(is_invisible)
except WebDriverException, e:
css_click_at(css)
wait_for(is_invisible)
@step(u'I edit the value of a policy key$')
def edit_the_value_of_a_policy_key(step):
@@ -83,7 +97,12 @@ def i_see_only_display_name(step):
@step('there are no advanced policy settings$')
def no_policy_settings(step):
assert_policy_entries([], [])
keys_css = 'input.policy-key'
val_css = 'textarea.json'
k = world.browser.is_element_not_present_by_css(keys_css, 5)
v = world.browser.is_element_not_present_by_css(val_css, 5)
assert_true(k)
assert_true(v)
@step('they are alphabetized$')
@@ -99,29 +118,29 @@ def it_is_formatted(step):
@step(u'the policy key name is unchanged$')
def the_policy_key_name_is_unchanged(step):
policy_key_css = 'input.policy-key'
e = css_find(policy_key_css).first
assert_equal(e.value, 'display_name')
val = css_find(policy_key_css).first.value
assert_equal(val, 'display_name')
@step(u'the policy key name is changed$')
def the_policy_key_name_is_changed(step):
policy_key_css = 'input.policy-key'
e = css_find(policy_key_css).first
assert_equal(e.value, 'new')
val = css_find(policy_key_css).first.value
assert_equal(val, 'display_name_new')
@step(u'the policy key value is unchanged$')
def the_policy_key_value_is_unchanged(step):
policy_value_css = 'li.course-advanced-policy-list-item div.value textarea'
e = css_find(policy_value_css).first
assert_equal(e.value, '"Robot Super Course"')
val = css_find(policy_value_css).first.value
assert_equal(val, '"Robot Super Course"')
@step(u'the policy key value is changed$')
def the_policy_key_value_is_unchanged(step):
policy_value_css = 'li.course-advanced-policy-list-item div.value textarea'
e = css_find(policy_value_css).first
assert_equal(e.value, '"Robot Super Course X"')
val = css_find(policy_value_css).first.value
assert_equal(val, '"Robot Super Course X"')
############# HELPERS ###############
@@ -132,19 +151,23 @@ def create_entry(key, value):
new_key_css = 'div#__new_advanced_key__ input'
new_key_element = css_find(new_key_css).first
new_key_element.fill(key)
# For some reason have to get the instance for each command (get error that it is no longer attached to the DOM)
# Have to do all this because Selenium has a bug that fill does not remove existing text
# For some reason have to get the instance for each command
# (get error that it is no longer attached to the DOM)
# Have to do all this because Selenium fill does not remove existing text
new_value_css = 'div.CodeMirror textarea'
css_find(new_value_css).last.fill("")
css_find(new_value_css).last._element.send_keys(Keys.DELETE, Keys.DELETE)
css_find(new_value_css).last.fill(value)
# Add in a TAB key press because intermittently on ubuntu the
# last character of "value" above was not getting typed in
css_find(new_value_css).last._element.send_keys(Keys.TAB)
def delete_entry(index):
"""
Delete the nth entry where index is 0-based
"""
css = '.delete-button'
css = 'a.delete-button'
assert_true(world.browser.is_element_present_by_css(css, 5))
delete_buttons = css_find(css)
assert_true(len(delete_buttons) > index, "no delete button exists for entry " + str(index))
@@ -152,8 +175,8 @@ def delete_entry(index):
def assert_policy_entries(expected_keys, expected_values):
assert_entries('.key input', expected_keys)
assert_entries('.json', expected_values)
assert_entries('.key input.policy-key', expected_keys)
assert_entries('textarea.json', expected_values)
def assert_entries(css, expected_values):
@@ -165,16 +188,8 @@ def assert_entries(css, expected_values):
def click_save():
css = ".save-button"
def is_shown(driver):
visible = css_find(css).first.visible
if visible:
# Even when waiting for visible, this fails sporadically. Adding in a small wait.
time.sleep(float(1))
return visible
wait_for(is_shown)
css_click(css)
css = "a.save-button"
css_click_at(css)
def fill_last_field(value):

View File

@@ -3,18 +3,20 @@ from lettuce.django import django_url
from nose.tools import assert_true
from nose.tools import assert_equal
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import WebDriverException, StaleElementReferenceException
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from terrain.factories import UserFactory, RegistrationFactory, UserProfileFactory
from terrain.factories import CourseFactory, GroupFactory
import xmodule.modulestore.django
from xmodule.modulestore.django import _MODULESTORES, modulestore
from xmodule.templates import update_templates
from auth.authz import get_user_by_email
from logging import getLogger
logger = getLogger(__name__)
########### STEP HELPERS ##############
@step('I (?:visit|access|open) the Studio homepage$')
def i_visit_the_studio_homepage(step):
# To make this go to port 8001, put
@@ -52,9 +54,8 @@ def i_have_opened_a_new_course(step):
log_into_studio()
create_a_course()
####### HELPER FUNCTIONS ##############
def create_studio_user(
uname='robot',
email='robot+studio@edx.org',
@@ -83,9 +84,9 @@ def flush_xmodule_store():
# (though it shouldn't), do this manually
# from the bash shell to drop it:
# $ mongo test_xmodule --eval "db.dropDatabase()"
xmodule.modulestore.django._MODULESTORES = {}
xmodule.modulestore.django.modulestore().collection.drop()
xmodule.templates.update_templates()
_MODULESTORES = {}
modulestore().collection.drop()
update_templates()
def assert_css_with_text(css, text):
@@ -94,8 +95,16 @@ def assert_css_with_text(css, text):
def css_click(css):
assert_true(world.browser.is_element_present_by_css(css, 5))
world.browser.find_by_css(css).first.click()
'''
First try to use the regular click method,
but if clicking in the middle of an element
doesn't work it might be that it thinks some other
element is on top of it there so click in the upper left
'''
try:
css_find(css).first.click()
except WebDriverException, e:
css_click_at(css)
def css_click_at(css, x=10, y=10):
@@ -103,8 +112,7 @@ def css_click_at(css, x=10, y=10):
A method to click at x,y coordinates of the element
rather than in the center of the element
'''
assert_true(world.browser.is_element_present_by_css(css, 5))
e = world.browser.find_by_css(css).first
e = css_find(css).first
e.action_chains.move_to_element_with_offset(e._element, x, y)
e.action_chains.click()
e.action_chains.perform()
@@ -115,11 +123,16 @@ def css_fill(css, value):
def css_find(css):
def is_visible(driver):
return EC.visibility_of_element_located((By.CSS_SELECTOR,css,))
world.browser.is_element_present_by_css(css, 5)
wait_for(is_visible)
return world.browser.find_by_css(css)
def wait_for(func):
WebDriverWait(world.browser.driver, 10).until(func)
WebDriverWait(world.browser.driver, 5).until(func)
def id_find(id):

View File

@@ -26,9 +26,10 @@ Feature: Create Section
And I save a new section release date
Then the section release date is updated
@skip-phantom
Scenario: Delete section
Given I have opened a new course in Studio
And I have added a new section
When I press the "section" delete icon
And I confirm the alert
Then the section does not exist
Then the section does not exist

View File

@@ -1,6 +1,8 @@
from lettuce import world, step
from common import *
from nose.tools import assert_equal
from selenium.webdriver.common.keys import Keys
import time
############### ACTIONS ####################
@@ -37,10 +39,14 @@ def i_save_a_new_section_release_date(step):
date_css = 'input.start-date.date.hasDatepicker'
time_css = 'input.start-time.time.ui-timepicker-input'
css_fill(date_css, '12/25/2013')
# click here to make the calendar go away
css_click(time_css)
# hit TAB to get to the time field
e = css_find(date_css).first
e._element.send_keys(Keys.TAB)
css_fill(time_css, '12:00am')
css_click('a.save-button')
e = css_find(time_css).first
e._element.send_keys(Keys.TAB)
time.sleep(float(1))
world.browser.click_link_by_text('Save')
############ ASSERTIONS ###################
@@ -106,7 +112,7 @@ def the_section_release_date_picker_not_visible(step):
def the_section_release_date_is_updated(step):
css = 'span.published-status'
status_text = world.browser.find_by_css(css).text
assert status_text == 'Will Release: 12/25/2013 at 12:00am'
assert_equal(status_text,'Will Release: 12/25/2013 at 12:00am')
############ HELPER METHODS ###################
@@ -120,4 +126,4 @@ def save_section_name(name):
def see_my_section_on_the_courseware_page(name):
section_css = 'span.section-name-span'
assert_css_with_text(section_css, name)
assert_css_with_text(section_css, name)

View File

@@ -1,4 +1,5 @@
from lettuce import world, step
from common import *
@step('I fill in the registration form$')
@@ -13,10 +14,11 @@ def i_fill_in_the_registration_form(step):
@step('I press the Create My Account button on the registration form$')
def i_press_the_button_on_the_registration_form(step):
register_form = world.browser.find_by_css('form#register_form')
submit_css = 'button#submit'
register_form.find_by_css(submit_css).click()
submit_css = 'form#register_form button#submit'
# Workaround for click not working on ubuntu
# for some unknown reason.
e = css_find(submit_css)
e.type(' ')
@step('I should see be on the studio home page$')
def i_should_see_be_on_the_studio_home_page(step):

View File

@@ -21,6 +21,7 @@ Feature: Overview Toggle Section
Then I see the "Collapse All Sections" link
And all sections are expanded
@skip-phantom
Scenario: Collapse link is not removed after last section of a course is deleted
Given I have a course with 1 section
And I navigate to the course overview page

View File

@@ -17,6 +17,7 @@ Feature: Create Subsection
And I click to edit the subsection name
Then I see the complete subsection name with a quote in the editor
@skip-phantom
Scenario: Delete a subsection
Given I have opened a new course section in Studio
And I have added a new subsection

View File

@@ -9,10 +9,8 @@ from tempdir import mkdtemp_clean
import json
from fs.osfs import OSFS
import copy
from mock import Mock
from json import dumps, loads
from json import loads
from student.models import Registration
from django.contrib.auth.models import User
from cms.djangoapps.contentstore.utils import get_modulestore
@@ -22,12 +20,11 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore import Location
from xmodule.modulestore.store_utilities import clone_course
from xmodule.modulestore.store_utilities import delete_course
from xmodule.modulestore.django import modulestore, _MODULESTORES
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.templates import update_templates
from xmodule.capa_module import CapaDescriptor
from xmodule.course_module import CourseDescriptor
@@ -63,7 +60,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.client = Client()
self.client.login(username=uname, password=password)
def check_edit_unit(self, test_course_name):
import_from_xml(modulestore(), 'common/test/data/', [test_course_name])
@@ -82,8 +78,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_static_tab_reordering(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
ms = modulestore('direct')
course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
module_store = modulestore('direct')
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
# reverse the ordering
reverse_tabs = []
@@ -91,9 +87,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
if tab['type'] == 'static_tab':
reverse_tabs.insert(0, 'i4x://edX/full/static_tab/{0}'.format(tab['url_slug']))
resp = self.client.post(reverse('reorder_static_tabs'), json.dumps({'tabs': reverse_tabs}), "application/json")
self.client.post(reverse('reorder_static_tabs'), json.dumps({'tabs': reverse_tabs}), "application/json")
course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
# compare to make sure that the tabs information is in the expected order after the server call
course_tabs = []
@@ -103,28 +99,60 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertEqual(reverse_tabs, course_tabs)
def test_delete(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
sequential = module_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None]))
# make sure the parent no longer points to the child object which was deleted
self.assertTrue(sequential.location.url() in chapter.definition['children'])
self.client.post(reverse('delete_item'),
json.dumps({'id': sequential.location.url(), 'delete_children':'true'}),
"application/json")
found = False
try:
module_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
found = True
except ItemNotFoundError:
pass
self.assertFalse(found)
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None]))
# make sure the parent no longer points to the child object which was deleted
self.assertFalse(sequential.location.url() in chapter.definition['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
while there is a base definition in /about/effort.html
'''
import_from_xml(modulestore(), 'common/test/data/', ['full'])
ms = modulestore('direct')
effort = ms.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None]))
module_store = modulestore('direct')
effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None]))
self.assertEqual(effort.definition['data'], '6 hours')
# this one should be in a non-override folder
effort = ms.get_item(Location(['i4x', 'edX', 'full', 'about', 'end_date', None]))
effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'end_date', None]))
self.assertEqual(effort.definition['data'], 'TBD')
def test_remove_hide_progress_tab(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
ms = modulestore('direct')
cs = contentstore()
module_store = modulestore('direct')
content_store = contentstore()
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
course = ms.get_item(source_location)
course = module_store.get_item(source_location)
self.assertNotIn('hide_progress_tab', course.metadata)
def test_clone_course(self):
@@ -143,19 +171,19 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
data = parse_json(resp)
self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course')
ms = modulestore('direct')
cs = contentstore()
module_store = modulestore('direct')
content_store = contentstore()
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
dest_location = CourseDescriptor.id_to_location('MITx/999/Robot_Super_Course')
clone_course(ms, cs, source_location, dest_location)
clone_course(module_store, content_store, source_location, dest_location)
# now loop through all the units in the course and verify that the clone can render them, which
# means the objects are at least present
items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
self.assertGreater(len(items), 0)
clone_items = ms.get_items(Location(['i4x', 'MITx', '999', 'vertical', None]))
clone_items = module_store.get_items(Location(['i4x', 'MITx', '999', 'vertical', None]))
self.assertGreater(len(clone_items), 0)
for descriptor in items:
new_loc = descriptor.location._replace(org='MITx', course='999')
@@ -166,14 +194,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_delete_course(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
ms = modulestore('direct')
cs = contentstore()
module_store = modulestore('direct')
content_store = contentstore()
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
delete_course(ms, cs, location, commit=True)
delete_course(module_store, content_store, location, commit=True)
items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
self.assertEqual(len(items), 0)
def verify_content_existence(self, modulestore, root_dir, location, dirname, category_name, filename_suffix=''):
@@ -188,10 +216,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertTrue(fs.exists(item.location.name + filename_suffix))
def test_export_course(self):
ms = modulestore('direct')
cs = contentstore()
module_store = modulestore('direct')
content_store = contentstore()
import_from_xml(ms, 'common/test/data/', ['full'])
import_from_xml(module_store, 'common/test/data/', ['full'])
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
root_dir = path(mkdtemp_clean())
@@ -199,43 +227,43 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
print 'Exporting to tempdir = {0}'.format(root_dir)
# export out to a tempdir
export_to_xml(ms, cs, location, root_dir, 'test_export')
export_to_xml(module_store, content_store, location, root_dir, 'test_export')
# check for static tabs
self.verify_content_existence(ms, root_dir, location, 'tabs', 'static_tab', '.html')
self.verify_content_existence(module_store, root_dir, location, 'tabs', 'static_tab', '.html')
# check for custom_tags
self.verify_content_existence(ms, root_dir, location, 'info', 'course_info', '.html')
self.verify_content_existence(module_store, root_dir, location, 'info', 'course_info', '.html')
# check for custom_tags
self.verify_content_existence(ms, root_dir, location, 'custom_tags', 'custom_tag_template')
self.verify_content_existence(module_store, root_dir, location, 'custom_tags', 'custom_tag_template')
# check for graiding_policy.json
fs = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012')
self.assertTrue(fs.exists('grading_policy.json'))
course = ms.get_item(location)
course = module_store.get_item(location)
# compare what's on disk compared to what we have in our course
with fs.open('grading_policy.json','r') as grading_policy:
on_disk = loads(grading_policy.read())
with fs.open('grading_policy.json', 'r') as grading_policy:
on_disk = loads(grading_policy.read())
self.assertEqual(on_disk, course.definition['data']['grading_policy'])
#check for policy.json
self.assertTrue(fs.exists('policy.json'))
# compare what's on disk to what we have in the course module
with fs.open('policy.json','r') as course_policy:
with fs.open('policy.json', 'r') as course_policy:
on_disk = loads(course_policy.read())
self.assertIn('course/6.002_Spring_2012', on_disk)
self.assertEqual(on_disk['course/6.002_Spring_2012'], course.metadata)
# remove old course
delete_course(ms, cs, location)
delete_course(module_store, content_store, location)
# reimport
import_from_xml(ms, root_dir, ['test_export'])
import_from_xml(module_store, root_dir, ['test_export'])
items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
self.assertGreater(len(items), 0)
for descriptor in items:
print "Checking {0}....".format(descriptor.location.url())
@@ -245,11 +273,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
shutil.rmtree(root_dir)
def test_course_handouts_rewrites(self):
ms = modulestore('direct')
cs = contentstore()
module_store = modulestore('direct')
content_store = contentstore()
# import a test course
import_from_xml(ms, 'common/test/data/', ['full'])
import_from_xml(module_store, 'common/test/data/', ['full'])
handout_location = Location(['i4x', 'edX', 'full', 'course_info', 'handouts'])
@@ -264,32 +292,32 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertContains(resp, '/c4x/edX/full/asset/handouts_schematic_tutorial.pdf')
def test_export_course_with_unknown_metadata(self):
ms = modulestore('direct')
cs = contentstore()
module_store = modulestore('direct')
content_store = contentstore()
import_from_xml(ms, 'common/test/data/', ['full'])
import_from_xml(module_store, 'common/test/data/', ['full'])
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
root_dir = path(mkdtemp_clean())
course = ms.get_item(location)
course = module_store.get_item(location)
# add a bool piece of unknown metadata so we can verify we don't throw an exception
course.metadata['new_metadata'] = True
ms.update_metadata(location, course.metadata)
module_store.update_metadata(location, course.metadata)
print 'Exporting to tempdir = {0}'.format(root_dir)
# export out to a tempdir
bExported = False
exported = False
try:
export_to_xml(ms, cs, location, root_dir, 'test_export')
bExported = True
export_to_xml(module_store, content_store, location, root_dir, 'test_export')
exported = True
except Exception:
pass
self.assertTrue(bExported)
self.assertTrue(exported)
class ContentStoreTest(ModuleStoreTestCase):
"""
@@ -428,7 +456,7 @@ class ContentStoreTest(ModuleStoreTestCase):
def test_capa_module(self):
"""Test that a problem treats markdown specially."""
course = CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
problem_data = {
'parent_location': 'i4x://MITx/999/course/Robot_Super_Course',
@@ -450,10 +478,10 @@ class ContentStoreTest(ModuleStoreTestCase):
def test_import_metadata_with_attempts_empty_string(self):
import_from_xml(modulestore(), 'common/test/data/', ['simple'])
ms = modulestore('direct')
module_store = modulestore('direct')
did_load_item = False
try:
ms.get_item(Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None]))
module_store.get_item(Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None]))
did_load_item = True
except ItemNotFoundError:
pass
@@ -464,10 +492,10 @@ class ContentStoreTest(ModuleStoreTestCase):
def test_metadata_inheritance(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
ms = modulestore('direct')
course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
module_store = modulestore('direct')
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
verticals = ms.get_items(['i4x', 'edX', 'full', 'vertical', None, None])
verticals = module_store.get_items(['i4x', 'edX', 'full', 'vertical', None, None])
# let's assert on the metadata_inheritance on an existing vertical
for vertical in verticals:
@@ -478,15 +506,15 @@ class ContentStoreTest(ModuleStoreTestCase):
new_component_location = Location('i4x', 'edX', 'full', 'html', 'new_component')
source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page')
# crate a new module and add it as a child to a vertical
ms.clone_item(source_template_location, new_component_location)
module_store.clone_item(source_template_location, new_component_location)
parent = verticals[0]
ms.update_children(parent.location, parent.definition.get('children', []) + [new_component_location.url()])
module_store.update_children(parent.location, parent.definition.get('children', []) + [new_component_location.url()])
# flush the cache
ms.get_cached_metadata_inheritance_tree(new_component_location, -1)
new_module = ms.get_item(new_component_location)
module_store.get_cached_metadata_inheritance_tree(new_component_location, -1)
new_module = module_store.get_item(new_component_location)
# check for grace period definition which should be defined at the course level
self.assertIn('graceperiod', new_module.metadata)
@@ -499,11 +527,11 @@ class ContentStoreTest(ModuleStoreTestCase):
# now let's define an override at the leaf node level
#
new_module.metadata['graceperiod'] = '1 day'
ms.update_metadata(new_module.location, new_module.metadata)
module_store.update_metadata(new_module.location, new_module.metadata)
# flush the cache and refetch
ms.get_cached_metadata_inheritance_tree(new_component_location, -1)
new_module = ms.get_item(new_component_location)
module_store.get_cached_metadata_inheritance_tree(new_component_location, -1)
new_module = module_store.get_item(new_component_location)
self.assertIn('graceperiod', new_module.metadata)
self.assertEqual('1 day', new_module.metadata['graceperiod'])
@@ -512,15 +540,15 @@ class ContentStoreTest(ModuleStoreTestCase):
class TemplateTestCase(ModuleStoreTestCase):
def test_template_cleanup(self):
ms = modulestore('direct')
module_store = modulestore('direct')
# insert a bogus template in the store
bogus_template_location = Location('i4x', 'edx', 'templates', 'html', 'bogus')
source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page')
ms.clone_item(source_template_location, bogus_template_location)
verify_create = ms.get_item(bogus_template_location)
module_store.clone_item(source_template_location, bogus_template_location)
verify_create = module_store.get_item(bogus_template_location)
self.assertIsNotNone(verify_create)
# now run cleanup
@@ -529,10 +557,9 @@ class TemplateTestCase(ModuleStoreTestCase):
# now try to find dangling template, it should not be in DB any longer
asserted = False
try:
verify_create = ms.get_item(bogus_template_location)
verify_create = module_store.get_item(bogus_template_location)
except ItemNotFoundError:
asserted = True
self.assertTrue(asserted)
self.assertTrue(asserted)

View File

@@ -86,12 +86,14 @@ def signup(request):
csrf_token = csrf(request)['csrf_token']
return render_to_response('signup.html', {'csrf': csrf_token})
def old_login_redirect(request):
'''
Redirect to the active login url.
'''
return redirect('login', permanent=True)
@ssl_login_shortcut
@ensure_csrf_cookie
def login_page(request):
@@ -104,6 +106,7 @@ def login_page(request):
'forgot_password_link': "//{base}/#forgot-password-modal".format(base=settings.LMS_BASE),
})
def howitworks(request):
if request.user.is_authenticated():
return index(request)
@@ -115,6 +118,7 @@ def ux_alerts(request):
# ==== Views for any logged-in user ==================================
@login_required
@ensure_csrf_cookie
def index(request):
@@ -148,6 +152,7 @@ def index(request):
# ==== Views with per-item permissions================================
def has_access(user, location, role=STAFF_ROLE_NAME):
'''
Return True if user allowed to access this piece of data
@@ -396,6 +401,7 @@ def preview_component(request, location):
'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(),
})
@expect_json
@login_required
@ensure_csrf_cookie
@@ -639,6 +645,17 @@ def delete_item(request):
if item.location.revision is None and item.location.category == 'vertical' and delete_all_versions:
modulestore('direct').delete_item(item.location)
# cdodge: we need to remove our parent's pointer to us so that it is no longer dangling
parent_locs = modulestore('direct').get_parent_locations(item_loc, None)
for parent_loc in parent_locs:
parent = modulestore('direct').get_item(parent_loc)
item_url = item_loc.url()
if item_url in parent.definition["children"]:
parent.definition["children"].remove(item_url)
modulestore('direct').update_children(parent.location, parent.definition["children"])
return HttpResponse()
@@ -712,6 +729,7 @@ def create_draft(request):
return HttpResponse()
@login_required
@expect_json
def publish_draft(request):
@@ -741,6 +759,7 @@ def unpublish_unit(request):
return HttpResponse()
@login_required
@expect_json
def clone_item(request):
@@ -771,8 +790,7 @@ def clone_item(request):
return HttpResponse(json.dumps({'id': dest_location.url()}))
#@login_required
#@ensure_csrf_cookie
def upload_asset(request, org, course, coursename):
'''
cdodge: this method allows for POST uploading of files into the course asset library, which will
@@ -834,6 +852,7 @@ def upload_asset(request, org, course, coursename):
response['asset_url'] = StaticContent.get_url_path_from_location(content.location)
return response
'''
This view will return all CMS users who are editors for the specified course
'''
@@ -866,6 +885,7 @@ def create_json_response(errmsg = None):
return resp
'''
This POST-back view will add a user - specified by email - to the list of editors for
the specified course
@@ -898,6 +918,7 @@ def add_user(request, location):
return create_json_response()
'''
This POST-back view will remove a user - specified by email - from the list of editors for
the specified course
@@ -929,6 +950,7 @@ def remove_user(request, location):
def landing(request, org, course, coursename):
return render_to_response('temp-course-landing.html', {})
@login_required
@ensure_csrf_cookie
def static_pages(request, org, course, coursename):
@@ -1032,6 +1054,7 @@ def edit_tabs(request, org, course, coursename):
'components': components
})
def not_found(request):
return render_to_response('error.html', {'error': '404'})
@@ -1067,6 +1090,7 @@ def course_info(request, org, course, name, provided_id=None):
'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url()
})
@expect_json
@login_required
@ensure_csrf_cookie
@@ -1164,6 +1188,7 @@ def get_course_settings(request, org, course, name):
"section": "details"})
})
@login_required
@ensure_csrf_cookie
def course_config_graders_page(request, org, course, name):
@@ -1187,6 +1212,7 @@ def course_config_graders_page(request, org, course, name):
'course_details': json.dumps(course_details, cls=CourseSettingsEncoder)
})
@login_required
@ensure_csrf_cookie
def course_config_advanced_page(request, org, course, name):
@@ -1210,6 +1236,7 @@ def course_config_advanced_page(request, org, course, name):
'advanced_dict' : json.dumps(CourseMetadata.fetch(location)),
})
@expect_json
@login_required
@ensure_csrf_cookie
@@ -1241,6 +1268,7 @@ def course_settings_updates(request, org, course, name, section):
return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
mimetype="application/json")
@expect_json
@login_required
@ensure_csrf_cookie
@@ -1275,7 +1303,7 @@ def course_grader_updates(request, org, course, name, grader_index=None):
return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(['i4x', org, course, 'course', name]), request.POST)),
mimetype="application/json")
## NB: expect_json failed on ["key", "key2"] and json payload
@login_required
@ensure_csrf_cookie
@@ -1366,6 +1394,7 @@ def asset_index(request, org, course, name):
def edge(request):
return render_to_response('university_profiles/edge.html', {})
@login_required
@expect_json
def create_new_course(request):
@@ -1421,6 +1450,7 @@ def create_new_course(request):
return HttpResponse(json.dumps({'id': new_course.location.url()}))
def initialize_course_tabs(course):
# set up the default tabs
# I've added this because when we add static tabs, the LMS either expects a None for the tabs list or
@@ -1438,6 +1468,7 @@ def initialize_course_tabs(course):
modulestore('direct').update_metadata(course.location.url(), course.own_metadata)
@ensure_csrf_cookie
@login_required
def import_course(request, org, course, name):
@@ -1515,6 +1546,7 @@ def import_course(request, org, course, name):
course_module.location.name])
})
@ensure_csrf_cookie
@login_required
def generate_export_course(request, org, course, name):
@@ -1566,6 +1598,7 @@ def export_course(request, org, course, name):
'successful_import_redirect_url': ''
})
def event(request):
'''
A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at

View File

@@ -10,7 +10,7 @@ class CourseMetadata(object):
'''
# __new_advanced_key__ is used by client not server; so, could argue against it being here
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end', 'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod', '__new_advanced_key__']
@classmethod
def fetch(cls, course_location):
"""
@@ -18,17 +18,17 @@ class CourseMetadata(object):
"""
if not isinstance(course_location, Location):
course_location = Location(course_location)
course = {}
descriptor = get_modulestore(course_location).get_item(course_location)
for k, v in descriptor.metadata.iteritems():
if k not in cls.FILTERED_LIST:
course[k] = v
return course
@classmethod
def update_from_json(cls, course_location, jsondict):
"""
@@ -37,7 +37,7 @@ class CourseMetadata(object):
Ensures none of the fields are in the blacklist.
"""
descriptor = get_modulestore(course_location).get_item(course_location)
dirty = False
for k, v in jsondict.iteritems():
@@ -45,26 +45,26 @@ class CourseMetadata(object):
if k not in cls.FILTERED_LIST and (k not in descriptor.metadata or descriptor.metadata[k] != v):
dirty = True
descriptor.metadata[k] = v
if dirty:
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
# Could just generate and return a course obj w/o doing any db reads, but I put the reads in as a means to confirm
# it persisted correctly
return cls.fetch(course_location)
@classmethod
def delete_key(cls, course_location, payload):
'''
Remove the given metadata key(s) from the course. payload can be a single key or [key..]
'''
descriptor = get_modulestore(course_location).get_item(course_location)
for key in payload['deleteKeys']:
if key in descriptor.metadata:
del descriptor.metadata[key]
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
return cls.fetch(course_location)

View File

@@ -62,3 +62,6 @@ AWS_SECRET_ACCESS_KEY = AUTH_TOKENS["AWS_SECRET_ACCESS_KEY"]
DATABASES = AUTH_TOKENS['DATABASES']
MODULESTORE = AUTH_TOKENS['MODULESTORE']
CONTENTSTORE = AUTH_TOKENS['CONTENTSTORE']
# Datadog for events!
DATADOG_API = AUTH_TOKENS.get("DATADOG_API")

6
cms/one_time_startup.py Normal file
View File

@@ -0,0 +1,6 @@
from dogapi import dog_http_api, dog_stats_api
from django.conf import settings
if hasattr(settings, 'DATADOG_API'):
dog_http_api.api_key = settings.DATADOG_API
dog_stats_api.start(api_key=settings.DATADOG_API, statsd=True)

View File

@@ -31,7 +31,8 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
// because these are outside of this.$el, they can't be in the event hash
$('.save-button').on('click', this, this.saveView);
$('.cancel-button').on('click', this, this.revertView);
this.model.on('error', this.handleValidationError, this);
this.listenTo(this.model, 'error', CMS.ServerError);
this.listenTo(this.model, 'invalid', this.handleValidationError);
},
render: function() {
// catch potential outside call before template loaded
@@ -228,7 +229,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
var error = {};
error[oldKey] = 'You have already defined "' + newKey + '" in the manual policy definitions.';
error[newKey] = "You tried to enter a duplicate of this key.";
this.model.trigger("error", this.model, error);
this.model.trigger("invalid", this.model, error);
return false;
}
@@ -244,7 +245,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
// swap to the key which the map knows about
validation[oldKey] = validation[newKey];
}
this.model.trigger("error", this.model, validation);
this.model.trigger("invalid", this.model, validation);
// abandon update
return;
}

View File

@@ -26,7 +26,8 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
var dateIntrospect = new Date();
this.$el.find('#timezone').html("(" + dateIntrospect.getTimezone() + ")");
this.model.on('error', this.handleValidationError, this);
this.listenTo(this.model, 'error', CMS.ServerError);
this.listenTo(this.model, 'invalid', this.handleValidationError);
this.selectorToField = _.invert(this.fieldToSelectorMap);
},

View File

@@ -44,7 +44,8 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
self.render();
}
);
this.model.on('error', this.handleValidationError, this);
this.listenTo(this.model, 'error', CMS.ServerError);
this.listenTo(this.model, 'invalid', this.handleValidationError);
this.model.get('graders').on('remove', this.render, this);
this.model.get('graders').on('reset', this.render, this);
this.model.get('graders').on('add', this.render, this);
@@ -316,7 +317,8 @@ CMS.Views.Settings.GraderView = CMS.Views.ValidatingView.extend({
'blur :input' : "inputUnfocus"
},
initialize : function() {
this.model.on('error', this.handleValidationError, this);
this.listenTo(this.model, 'error', CMS.ServerError);
this.listenTo(this.model, 'invalid', this.handleValidationError);
this.selectorToField = _.invert(this.fieldToSelectorMap);
this.render();
},

View File

@@ -3,7 +3,8 @@ CMS.Views.ValidatingView = Backbone.View.extend({
// decorates the fields. Needs wiring per class, but this initialization shows how
// either have your init call this one or copy the contents
initialize : function() {
this.model.on('error', this.handleValidationError, this);
this.listenTo(this.model, 'error', CMS.ServerError);
this.listenTo(this.model, 'invalid', this.handleValidationError);
this.selectorToField = _.invert(this.fieldToSelectorMap);
},
@@ -18,20 +19,11 @@ CMS.Views.ValidatingView = Backbone.View.extend({
// which may be the subjects of validation errors
},
_cacheValidationErrors : [],
handleValidationError : function(model, error) {
// error triggered either by validation or server error
// error is object w/ fields and error strings
for (var field in error) {
var ele = this.$el.find('#' + this.fieldToSelectorMap[field]);
if (ele.length === 0) {
// check if it might a server error: note a typo in the field name
// or failure to put in a map may cause this to muffle validation errors
if (_.has(error, 'error') && _.has(error, 'responseText')) {
CMS.ServerError(model, error);
return;
}
else continue;
}
this._cacheValidationErrors.push(ele);
if ($(ele).is('div')) {
// put error on the contained inputs

View File

@@ -1,5 +1,6 @@
from django.conf import settings
from django.conf.urls import patterns, include, url
from . import one_time_startup
# Uncomment the next two lines to enable the admin:
# from django.contrib import admin