Commit 946af8e1 by Arthur Barrett

Merge branch 'master' into feature/abarrett/annotation-styling-fixes

parents 094458dd 894f9171
...@@ -29,3 +29,4 @@ cover_html/ ...@@ -29,3 +29,4 @@ cover_html/
.idea/ .idea/
.redcar/ .redcar/
chromedriver.log chromedriver.log
ghostdriver.log
1.8.7-p371 1.9.3-p374
\ No newline at end of file
source :rubygems source 'https://rubygems.org'
gem 'rake', '~> 10.0.3' gem 'rake', '~> 10.0.3'
gem 'sass', '3.1.15' gem 'sass', '3.1.15'
gem 'bourbon', '~> 1.3.6' gem 'bourbon', '~> 1.3.6'
......
...@@ -7,6 +7,7 @@ Feature: Advanced (manual) course policy ...@@ -7,6 +7,7 @@ Feature: Advanced (manual) course policy
When I select the Advanced Settings When I select the Advanced Settings
Then I see only the display name Then I see only the display name
@skip-phantom
Scenario: Test if there are no policy settings without existing UI controls Scenario: Test if there are no policy settings without existing UI controls
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I delete the display name When I delete the display name
...@@ -14,6 +15,7 @@ Feature: Advanced (manual) course policy ...@@ -14,6 +15,7 @@ Feature: Advanced (manual) course policy
And I reload the page And I reload the page
Then there are no advanced policy settings Then there are no advanced policy settings
@skip-phantom
Scenario: Test cancel editing key name Scenario: Test cancel editing key name
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I edit the name of a policy key When I edit the name of a policy key
...@@ -32,6 +34,7 @@ Feature: Advanced (manual) course policy ...@@ -32,6 +34,7 @@ Feature: Advanced (manual) course policy
And I press the "Cancel" notification button And I press the "Cancel" notification button
Then the policy key value is unchanged Then the policy key value is unchanged
@skip-phantom
Scenario: Test editing key value Scenario: Test editing key value
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key When I edit the value of a policy key
......
from lettuce import world, step from lettuce import world, step
from common import * from common import *
import time import time
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.support import expected_conditions as EC
from nose.tools import assert_equal from nose.tools import assert_true, assert_false, assert_equal
from nose.tools import assert_true
""" """
http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdriver.common.keys.html http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdriver.common.keys.html
...@@ -19,6 +20,7 @@ def i_select_advanced_settings(step): ...@@ -19,6 +20,7 @@ def i_select_advanced_settings(step):
css_click(expand_icon_css) css_click(expand_icon_css)
link_css = 'li.nav-course-settings-advanced a' link_css = 'li.nav-course-settings-advanced a'
css_click(link_css) css_click(link_css)
# world.browser.click_link_by_text('Advanced Settings')
@step('I am on the Advanced Course Settings page in Studio$') @step('I am on the Advanced Course Settings page in Studio$')
...@@ -37,13 +39,25 @@ def reload_the_page(step): ...@@ -37,13 +39,25 @@ def reload_the_page(step):
def edit_the_name_of_a_policy_key(step): def edit_the_name_of_a_policy_key(step):
policy_key_css = 'input.policy-key' policy_key_css = 'input.policy-key'
e = css_find(policy_key_css).first e = css_find(policy_key_css).first
e.fill('new') e.type('_new')
@step(u'I press the "([^"]*)" notification button$') @step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(step, name): def press_the_notification_button(step, name):
world.browser.click_link_by_text(name) def is_visible(driver):
return EC.visibility_of_element_located((By.CSS_SELECTOR,css,))
def is_invisible(driver):
return EC.invisibility_of_element_located((By.CSS_SELECTOR,css,))
css = 'a.%s-button' % name.lower()
wait_for(is_visible)
try:
css_click_at(css)
wait_for(is_invisible)
except WebDriverException, e:
css_click_at(css)
wait_for(is_invisible)
@step(u'I edit the value of a policy key$') @step(u'I edit the value of a policy key$')
def edit_the_value_of_a_policy_key(step): def edit_the_value_of_a_policy_key(step):
...@@ -83,7 +97,12 @@ def i_see_only_display_name(step): ...@@ -83,7 +97,12 @@ def i_see_only_display_name(step):
@step('there are no advanced policy settings$') @step('there are no advanced policy settings$')
def no_policy_settings(step): 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$') @step('they are alphabetized$')
...@@ -99,29 +118,29 @@ def it_is_formatted(step): ...@@ -99,29 +118,29 @@ def it_is_formatted(step):
@step(u'the policy key name is unchanged$') @step(u'the policy key name is unchanged$')
def the_policy_key_name_is_unchanged(step): def the_policy_key_name_is_unchanged(step):
policy_key_css = 'input.policy-key' policy_key_css = 'input.policy-key'
e = css_find(policy_key_css).first val = css_find(policy_key_css).first.value
assert_equal(e.value, 'display_name') assert_equal(val, 'display_name')
@step(u'the policy key name is changed$') @step(u'the policy key name is changed$')
def the_policy_key_name_is_changed(step): def the_policy_key_name_is_changed(step):
policy_key_css = 'input.policy-key' policy_key_css = 'input.policy-key'
e = css_find(policy_key_css).first val = css_find(policy_key_css).first.value
assert_equal(e.value, 'new') assert_equal(val, 'display_name_new')
@step(u'the policy key value is unchanged$') @step(u'the policy key value is unchanged$')
def the_policy_key_value_is_unchanged(step): def the_policy_key_value_is_unchanged(step):
policy_value_css = 'li.course-advanced-policy-list-item div.value textarea' policy_value_css = 'li.course-advanced-policy-list-item div.value textarea'
e = css_find(policy_value_css).first val = css_find(policy_value_css).first.value
assert_equal(e.value, '"Robot Super Course"') assert_equal(val, '"Robot Super Course"')
@step(u'the policy key value is changed$') @step(u'the policy key value is changed$')
def the_policy_key_value_is_unchanged(step): def the_policy_key_value_is_unchanged(step):
policy_value_css = 'li.course-advanced-policy-list-item div.value textarea' policy_value_css = 'li.course-advanced-policy-list-item div.value textarea'
e = css_find(policy_value_css).first val = css_find(policy_value_css).first.value
assert_equal(e.value, '"Robot Super Course X"') assert_equal(val, '"Robot Super Course X"')
############# HELPERS ############### ############# HELPERS ###############
...@@ -132,19 +151,23 @@ def create_entry(key, value): ...@@ -132,19 +151,23 @@ def create_entry(key, value):
new_key_css = 'div#__new_advanced_key__ input' new_key_css = 'div#__new_advanced_key__ input'
new_key_element = css_find(new_key_css).first new_key_element = css_find(new_key_css).first
new_key_element.fill(key) new_key_element.fill(key)
# For some reason have to get the instance for each command (get error that it is no longer attached to the DOM) # For some reason have to get the instance for each command
# Have to do all this because Selenium has a bug that fill does not remove existing text # (get error that it is no longer attached to the DOM)
# Have to do all this because Selenium fill does not remove existing text
new_value_css = 'div.CodeMirror textarea' new_value_css = 'div.CodeMirror textarea'
css_find(new_value_css).last.fill("") css_find(new_value_css).last.fill("")
css_find(new_value_css).last._element.send_keys(Keys.DELETE, Keys.DELETE) css_find(new_value_css).last._element.send_keys(Keys.DELETE, Keys.DELETE)
css_find(new_value_css).last.fill(value) css_find(new_value_css).last.fill(value)
# Add in a TAB key press because intermittently on ubuntu the
# last character of "value" above was not getting typed in
css_find(new_value_css).last._element.send_keys(Keys.TAB)
def delete_entry(index): def delete_entry(index):
""" """
Delete the nth entry where index is 0-based Delete the nth entry where index is 0-based
""" """
css = '.delete-button' css = 'a.delete-button'
assert_true(world.browser.is_element_present_by_css(css, 5)) assert_true(world.browser.is_element_present_by_css(css, 5))
delete_buttons = css_find(css) delete_buttons = css_find(css)
assert_true(len(delete_buttons) > index, "no delete button exists for entry " + str(index)) assert_true(len(delete_buttons) > index, "no delete button exists for entry " + str(index))
...@@ -152,8 +175,8 @@ def delete_entry(index): ...@@ -152,8 +175,8 @@ def delete_entry(index):
def assert_policy_entries(expected_keys, expected_values): def assert_policy_entries(expected_keys, expected_values):
assert_entries('.key input', expected_keys) assert_entries('.key input.policy-key', expected_keys)
assert_entries('.json', expected_values) assert_entries('textarea.json', expected_values)
def assert_entries(css, expected_values): def assert_entries(css, expected_values):
...@@ -165,16 +188,8 @@ def assert_entries(css, expected_values): ...@@ -165,16 +188,8 @@ def assert_entries(css, expected_values):
def click_save(): def click_save():
css = ".save-button" css = "a.save-button"
css_click_at(css)
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)
def fill_last_field(value): def fill_last_field(value):
......
...@@ -3,18 +3,20 @@ from lettuce.django import django_url ...@@ -3,18 +3,20 @@ from lettuce.django import django_url
from nose.tools import assert_true from nose.tools import assert_true
from nose.tools import assert_equal from nose.tools import assert_equal
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import WebDriverException, StaleElementReferenceException
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from terrain.factories import UserFactory, RegistrationFactory, UserProfileFactory from terrain.factories import UserFactory, RegistrationFactory, UserProfileFactory
from terrain.factories import CourseFactory, GroupFactory from terrain.factories import CourseFactory, GroupFactory
import xmodule.modulestore.django from xmodule.modulestore.django import _MODULESTORES, modulestore
from xmodule.templates import update_templates
from auth.authz import get_user_by_email from auth.authz import get_user_by_email
from logging import getLogger from logging import getLogger
logger = getLogger(__name__) logger = getLogger(__name__)
########### STEP HELPERS ############## ########### STEP HELPERS ##############
@step('I (?:visit|access|open) the Studio homepage$') @step('I (?:visit|access|open) the Studio homepage$')
def i_visit_the_studio_homepage(step): def i_visit_the_studio_homepage(step):
# To make this go to port 8001, put # To make this go to port 8001, put
...@@ -52,9 +54,8 @@ def i_have_opened_a_new_course(step): ...@@ -52,9 +54,8 @@ def i_have_opened_a_new_course(step):
log_into_studio() log_into_studio()
create_a_course() create_a_course()
####### HELPER FUNCTIONS ##############
####### HELPER FUNCTIONS ##############
def create_studio_user( def create_studio_user(
uname='robot', uname='robot',
email='robot+studio@edx.org', email='robot+studio@edx.org',
...@@ -83,9 +84,9 @@ def flush_xmodule_store(): ...@@ -83,9 +84,9 @@ def flush_xmodule_store():
# (though it shouldn't), do this manually # (though it shouldn't), do this manually
# from the bash shell to drop it: # from the bash shell to drop it:
# $ mongo test_xmodule --eval "db.dropDatabase()" # $ mongo test_xmodule --eval "db.dropDatabase()"
xmodule.modulestore.django._MODULESTORES = {} _MODULESTORES = {}
xmodule.modulestore.django.modulestore().collection.drop() modulestore().collection.drop()
xmodule.templates.update_templates() update_templates()
def assert_css_with_text(css, text): def assert_css_with_text(css, text):
...@@ -94,8 +95,16 @@ def assert_css_with_text(css, text): ...@@ -94,8 +95,16 @@ def assert_css_with_text(css, text):
def css_click(css): def css_click(css):
assert_true(world.browser.is_element_present_by_css(css, 5)) '''
world.browser.find_by_css(css).first.click() First try to use the regular click method,
but if clicking in the middle of an element
doesn't work it might be that it thinks some other
element is on top of it there so click in the upper left
'''
try:
css_find(css).first.click()
except WebDriverException, e:
css_click_at(css)
def css_click_at(css, x=10, y=10): def css_click_at(css, x=10, y=10):
...@@ -103,8 +112,7 @@ def css_click_at(css, x=10, y=10): ...@@ -103,8 +112,7 @@ def css_click_at(css, x=10, y=10):
A method to click at x,y coordinates of the element A method to click at x,y coordinates of the element
rather than in the center of the element rather than in the center of the element
''' '''
assert_true(world.browser.is_element_present_by_css(css, 5)) e = css_find(css).first
e = world.browser.find_by_css(css).first
e.action_chains.move_to_element_with_offset(e._element, x, y) e.action_chains.move_to_element_with_offset(e._element, x, y)
e.action_chains.click() e.action_chains.click()
e.action_chains.perform() e.action_chains.perform()
...@@ -115,11 +123,16 @@ def css_fill(css, value): ...@@ -115,11 +123,16 @@ def css_fill(css, value):
def css_find(css): def css_find(css):
def is_visible(driver):
return EC.visibility_of_element_located((By.CSS_SELECTOR,css,))
world.browser.is_element_present_by_css(css, 5)
wait_for(is_visible)
return world.browser.find_by_css(css) return world.browser.find_by_css(css)
def wait_for(func): def wait_for(func):
WebDriverWait(world.browser.driver, 10).until(func) WebDriverWait(world.browser.driver, 5).until(func)
def id_find(id): def id_find(id):
......
...@@ -26,6 +26,7 @@ Feature: Create Section ...@@ -26,6 +26,7 @@ Feature: Create Section
And I save a new section release date And I save a new section release date
Then the section release date is updated Then the section release date is updated
@skip-phantom
Scenario: Delete section Scenario: Delete section
Given I have opened a new course in Studio Given I have opened a new course in Studio
And I have added a new section And I have added a new section
......
from lettuce import world, step from lettuce import world, step
from common import * from common import *
from nose.tools import assert_equal from nose.tools import assert_equal
from selenium.webdriver.common.keys import Keys
import time
############### ACTIONS #################### ############### ACTIONS ####################
...@@ -37,10 +39,14 @@ def i_save_a_new_section_release_date(step): ...@@ -37,10 +39,14 @@ def i_save_a_new_section_release_date(step):
date_css = 'input.start-date.date.hasDatepicker' date_css = 'input.start-date.date.hasDatepicker'
time_css = 'input.start-time.time.ui-timepicker-input' time_css = 'input.start-time.time.ui-timepicker-input'
css_fill(date_css, '12/25/2013') css_fill(date_css, '12/25/2013')
# click here to make the calendar go away # hit TAB to get to the time field
css_click(time_css) e = css_find(date_css).first
e._element.send_keys(Keys.TAB)
css_fill(time_css, '12:00am') css_fill(time_css, '12:00am')
css_click('a.save-button') e = css_find(time_css).first
e._element.send_keys(Keys.TAB)
time.sleep(float(1))
world.browser.click_link_by_text('Save')
############ ASSERTIONS ################### ############ ASSERTIONS ###################
...@@ -106,7 +112,7 @@ def the_section_release_date_picker_not_visible(step): ...@@ -106,7 +112,7 @@ def the_section_release_date_picker_not_visible(step):
def the_section_release_date_is_updated(step): def the_section_release_date_is_updated(step):
css = 'span.published-status' css = 'span.published-status'
status_text = world.browser.find_by_css(css).text status_text = world.browser.find_by_css(css).text
assert status_text == 'Will Release: 12/25/2013 at 12:00am' assert_equal(status_text,'Will Release: 12/25/2013 at 12:00am')
############ HELPER METHODS ################### ############ HELPER METHODS ###################
......
from lettuce import world, step from lettuce import world, step
from common import *
@step('I fill in the registration form$') @step('I fill in the registration form$')
...@@ -13,10 +14,11 @@ def i_fill_in_the_registration_form(step): ...@@ -13,10 +14,11 @@ def i_fill_in_the_registration_form(step):
@step('I press the Create My Account button on the registration form$') @step('I press the Create My Account button on the registration form$')
def i_press_the_button_on_the_registration_form(step): def i_press_the_button_on_the_registration_form(step):
register_form = world.browser.find_by_css('form#register_form') submit_css = 'form#register_form button#submit'
submit_css = 'button#submit' # Workaround for click not working on ubuntu
register_form.find_by_css(submit_css).click() # for some unknown reason.
e = css_find(submit_css)
e.type(' ')
@step('I should see be on the studio home page$') @step('I should see be on the studio home page$')
def i_should_see_be_on_the_studio_home_page(step): def i_should_see_be_on_the_studio_home_page(step):
......
...@@ -21,6 +21,7 @@ Feature: Overview Toggle Section ...@@ -21,6 +21,7 @@ Feature: Overview Toggle Section
Then I see the "Collapse All Sections" link Then I see the "Collapse All Sections" link
And all sections are expanded And all sections are expanded
@skip-phantom
Scenario: Collapse link is not removed after last section of a course is deleted Scenario: Collapse link is not removed after last section of a course is deleted
Given I have a course with 1 section Given I have a course with 1 section
And I navigate to the course overview page And I navigate to the course overview page
......
...@@ -17,6 +17,7 @@ Feature: Create Subsection ...@@ -17,6 +17,7 @@ Feature: Create Subsection
And I click to edit the subsection name And I click to edit the subsection name
Then I see the complete subsection name with a quote in the editor Then I see the complete subsection name with a quote in the editor
@skip-phantom
Scenario: Delete a subsection Scenario: Delete a subsection
Given I have opened a new course section in Studio Given I have opened a new course section in Studio
And I have added a new subsection And I have added a new subsection
......
...@@ -63,7 +63,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -63,7 +63,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.client = Client() self.client = Client()
self.client.login(username=uname, password=password) self.client.login(username=uname, password=password)
def check_edit_unit(self, test_course_name): def check_edit_unit(self, test_course_name):
import_from_xml(modulestore(), 'common/test/data/', [test_course_name]) import_from_xml(modulestore(), 'common/test/data/', [test_course_name])
...@@ -103,6 +102,37 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -103,6 +102,37 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertEqual(reverse_tabs, course_tabs) self.assertEqual(reverse_tabs, course_tabs)
def test_delete(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]))
sequential = ms.get_item(Location(['i4x', 'edX', 'full', 'sequential','Administrivia_and_Circuit_Elements', None]))
chapter = ms.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'])
resp = self.client.post(reverse('delete_item'), json.dumps({'id': sequential.location.url(), 'delete_children':'true'}), "application/json")
bFound = False
try:
sequential = ms.get_item(Location(['i4x', 'edX', 'full', 'sequential','Administrivia_and_Circuit_Elements', None]))
bFound = True
except ItemNotFoundError:
pass
self.assertFalse(bFound)
chapter = ms.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): 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 This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html
......
...@@ -86,12 +86,14 @@ def signup(request): ...@@ -86,12 +86,14 @@ def signup(request):
csrf_token = csrf(request)['csrf_token'] csrf_token = csrf(request)['csrf_token']
return render_to_response('signup.html', {'csrf': csrf_token}) return render_to_response('signup.html', {'csrf': csrf_token})
def old_login_redirect(request): def old_login_redirect(request):
''' '''
Redirect to the active login url. Redirect to the active login url.
''' '''
return redirect('login', permanent=True) return redirect('login', permanent=True)
@ssl_login_shortcut @ssl_login_shortcut
@ensure_csrf_cookie @ensure_csrf_cookie
def login_page(request): def login_page(request):
...@@ -104,6 +106,7 @@ def login_page(request): ...@@ -104,6 +106,7 @@ def login_page(request):
'forgot_password_link': "//{base}/#forgot-password-modal".format(base=settings.LMS_BASE), 'forgot_password_link': "//{base}/#forgot-password-modal".format(base=settings.LMS_BASE),
}) })
def howitworks(request): def howitworks(request):
if request.user.is_authenticated(): if request.user.is_authenticated():
return index(request) return index(request)
...@@ -112,6 +115,7 @@ def howitworks(request): ...@@ -112,6 +115,7 @@ def howitworks(request):
# ==== Views for any logged-in user ================================== # ==== Views for any logged-in user ==================================
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def index(request): def index(request):
...@@ -145,6 +149,7 @@ def index(request): ...@@ -145,6 +149,7 @@ def index(request):
# ==== Views with per-item permissions================================ # ==== Views with per-item permissions================================
def has_access(user, location, role=STAFF_ROLE_NAME): def has_access(user, location, role=STAFF_ROLE_NAME):
''' '''
Return True if user allowed to access this piece of data Return True if user allowed to access this piece of data
...@@ -393,6 +398,7 @@ def preview_component(request, location): ...@@ -393,6 +398,7 @@ def preview_component(request, location):
'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(), 'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(),
}) })
@expect_json @expect_json
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
...@@ -636,6 +642,17 @@ def delete_item(request): ...@@ -636,6 +642,17 @@ def delete_item(request):
if item.location.revision is None and item.location.category == 'vertical' and delete_all_versions: if item.location.revision is None and item.location.category == 'vertical' and delete_all_versions:
modulestore('direct').delete_item(item.location) 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() return HttpResponse()
...@@ -709,6 +726,7 @@ def create_draft(request): ...@@ -709,6 +726,7 @@ def create_draft(request):
return HttpResponse() return HttpResponse()
@login_required @login_required
@expect_json @expect_json
def publish_draft(request): def publish_draft(request):
...@@ -738,6 +756,7 @@ def unpublish_unit(request): ...@@ -738,6 +756,7 @@ def unpublish_unit(request):
return HttpResponse() return HttpResponse()
@login_required @login_required
@expect_json @expect_json
def clone_item(request): def clone_item(request):
...@@ -768,8 +787,7 @@ def clone_item(request): ...@@ -768,8 +787,7 @@ def clone_item(request):
return HttpResponse(json.dumps({'id': dest_location.url()})) return HttpResponse(json.dumps({'id': dest_location.url()}))
#@login_required
#@ensure_csrf_cookie
def upload_asset(request, org, course, coursename): def upload_asset(request, org, course, coursename):
''' '''
cdodge: this method allows for POST uploading of files into the course asset library, which will cdodge: this method allows for POST uploading of files into the course asset library, which will
...@@ -831,6 +849,7 @@ def upload_asset(request, org, course, coursename): ...@@ -831,6 +849,7 @@ def upload_asset(request, org, course, coursename):
response['asset_url'] = StaticContent.get_url_path_from_location(content.location) response['asset_url'] = StaticContent.get_url_path_from_location(content.location)
return response return response
''' '''
This view will return all CMS users who are editors for the specified course This view will return all CMS users who are editors for the specified course
''' '''
...@@ -863,6 +882,7 @@ def create_json_response(errmsg = None): ...@@ -863,6 +882,7 @@ def create_json_response(errmsg = None):
return resp return resp
''' '''
This POST-back view will add a user - specified by email - to the list of editors for This POST-back view will add a user - specified by email - to the list of editors for
the specified course the specified course
...@@ -895,6 +915,7 @@ def add_user(request, location): ...@@ -895,6 +915,7 @@ def add_user(request, location):
return create_json_response() return create_json_response()
''' '''
This POST-back view will remove a user - specified by email - from the list of editors for This POST-back view will remove a user - specified by email - from the list of editors for
the specified course the specified course
...@@ -926,6 +947,7 @@ def remove_user(request, location): ...@@ -926,6 +947,7 @@ def remove_user(request, location):
def landing(request, org, course, coursename): def landing(request, org, course, coursename):
return render_to_response('temp-course-landing.html', {}) return render_to_response('temp-course-landing.html', {})
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def static_pages(request, org, course, coursename): def static_pages(request, org, course, coursename):
...@@ -1029,6 +1051,7 @@ def edit_tabs(request, org, course, coursename): ...@@ -1029,6 +1051,7 @@ def edit_tabs(request, org, course, coursename):
'components': components 'components': components
}) })
def not_found(request): def not_found(request):
return render_to_response('error.html', {'error': '404'}) return render_to_response('error.html', {'error': '404'})
...@@ -1064,6 +1087,7 @@ def course_info(request, org, course, name, provided_id=None): ...@@ -1064,6 +1087,7 @@ def course_info(request, org, course, name, provided_id=None):
'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url() 'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url()
}) })
@expect_json @expect_json
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
...@@ -1161,6 +1185,7 @@ def get_course_settings(request, org, course, name): ...@@ -1161,6 +1185,7 @@ def get_course_settings(request, org, course, name):
"section": "details"}) "section": "details"})
}) })
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def course_config_graders_page(request, org, course, name): def course_config_graders_page(request, org, course, name):
...@@ -1184,6 +1209,7 @@ def course_config_graders_page(request, org, course, name): ...@@ -1184,6 +1209,7 @@ def course_config_graders_page(request, org, course, name):
'course_details': json.dumps(course_details, cls=CourseSettingsEncoder) 'course_details': json.dumps(course_details, cls=CourseSettingsEncoder)
}) })
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def course_config_advanced_page(request, org, course, name): def course_config_advanced_page(request, org, course, name):
...@@ -1207,6 +1233,7 @@ def course_config_advanced_page(request, org, course, name): ...@@ -1207,6 +1233,7 @@ def course_config_advanced_page(request, org, course, name):
'advanced_dict' : json.dumps(CourseMetadata.fetch(location)), 'advanced_dict' : json.dumps(CourseMetadata.fetch(location)),
}) })
@expect_json @expect_json
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
...@@ -1238,6 +1265,7 @@ def course_settings_updates(request, org, course, name, section): ...@@ -1238,6 +1265,7 @@ def course_settings_updates(request, org, course, name, section):
return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder), return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
mimetype="application/json") mimetype="application/json")
@expect_json @expect_json
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
...@@ -1363,6 +1391,7 @@ def asset_index(request, org, course, name): ...@@ -1363,6 +1391,7 @@ def asset_index(request, org, course, name):
def edge(request): def edge(request):
return render_to_response('university_profiles/edge.html', {}) return render_to_response('university_profiles/edge.html', {})
@login_required @login_required
@expect_json @expect_json
def create_new_course(request): def create_new_course(request):
...@@ -1418,6 +1447,7 @@ def create_new_course(request): ...@@ -1418,6 +1447,7 @@ def create_new_course(request):
return HttpResponse(json.dumps({'id': new_course.location.url()})) return HttpResponse(json.dumps({'id': new_course.location.url()}))
def initialize_course_tabs(course): def initialize_course_tabs(course):
# set up the default tabs # 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 # I've added this because when we add static tabs, the LMS either expects a None for the tabs list or
...@@ -1435,6 +1465,7 @@ def initialize_course_tabs(course): ...@@ -1435,6 +1465,7 @@ def initialize_course_tabs(course):
modulestore('direct').update_metadata(course.location.url(), course.own_metadata) modulestore('direct').update_metadata(course.location.url(), course.own_metadata)
@ensure_csrf_cookie @ensure_csrf_cookie
@login_required @login_required
def import_course(request, org, course, name): def import_course(request, org, course, name):
...@@ -1512,6 +1543,7 @@ def import_course(request, org, course, name): ...@@ -1512,6 +1543,7 @@ def import_course(request, org, course, name):
course_module.location.name]) course_module.location.name])
}) })
@ensure_csrf_cookie @ensure_csrf_cookie
@login_required @login_required
def generate_export_course(request, org, course, name): def generate_export_course(request, org, course, name):
...@@ -1563,6 +1595,7 @@ def export_course(request, org, course, name): ...@@ -1563,6 +1595,7 @@ def export_course(request, org, course, name):
'successful_import_redirect_url': '' 'successful_import_redirect_url': ''
}) })
def event(request): def event(request):
''' '''
A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at
......
...@@ -62,3 +62,6 @@ AWS_SECRET_ACCESS_KEY = AUTH_TOKENS["AWS_SECRET_ACCESS_KEY"] ...@@ -62,3 +62,6 @@ AWS_SECRET_ACCESS_KEY = AUTH_TOKENS["AWS_SECRET_ACCESS_KEY"]
DATABASES = AUTH_TOKENS['DATABASES'] DATABASES = AUTH_TOKENS['DATABASES']
MODULESTORE = AUTH_TOKENS['MODULESTORE'] MODULESTORE = AUTH_TOKENS['MODULESTORE']
CONTENTSTORE = AUTH_TOKENS['CONTENTSTORE'] CONTENTSTORE = AUTH_TOKENS['CONTENTSTORE']
# Datadog for events!
DATADOG_API = AUTH_TOKENS.get("DATADOG_API")
\ No newline at end of file
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)
...@@ -31,7 +31,8 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -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 // because these are outside of this.$el, they can't be in the event hash
$('.save-button').on('click', this, this.saveView); $('.save-button').on('click', this, this.saveView);
$('.cancel-button').on('click', this, this.revertView); $('.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() { render: function() {
// catch potential outside call before template loaded // catch potential outside call before template loaded
...@@ -228,7 +229,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -228,7 +229,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
var error = {}; var error = {};
error[oldKey] = 'You have already defined "' + newKey + '" in the manual policy definitions.'; error[oldKey] = 'You have already defined "' + newKey + '" in the manual policy definitions.';
error[newKey] = "You tried to enter a duplicate of this key."; 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; return false;
} }
...@@ -244,7 +245,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -244,7 +245,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
// swap to the key which the map knows about // swap to the key which the map knows about
validation[oldKey] = validation[newKey]; validation[oldKey] = validation[newKey];
} }
this.model.trigger("error", this.model, validation); this.model.trigger("invalid", this.model, validation);
// abandon update // abandon update
return; return;
} }
......
...@@ -26,7 +26,8 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ ...@@ -26,7 +26,8 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
var dateIntrospect = new Date(); var dateIntrospect = new Date();
this.$el.find('#timezone').html("(" + dateIntrospect.getTimezone() + ")"); 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); this.selectorToField = _.invert(this.fieldToSelectorMap);
}, },
......
...@@ -44,7 +44,8 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ ...@@ -44,7 +44,8 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
self.render(); 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('remove', this.render, this);
this.model.get('graders').on('reset', this.render, this); this.model.get('graders').on('reset', this.render, this);
this.model.get('graders').on('add', this.render, this); this.model.get('graders').on('add', this.render, this);
...@@ -316,7 +317,8 @@ CMS.Views.Settings.GraderView = CMS.Views.ValidatingView.extend({ ...@@ -316,7 +317,8 @@ CMS.Views.Settings.GraderView = CMS.Views.ValidatingView.extend({
'blur :input' : "inputUnfocus" 'blur :input' : "inputUnfocus"
}, },
initialize : function() { 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.selectorToField = _.invert(this.fieldToSelectorMap);
this.render(); this.render();
}, },
......
...@@ -3,7 +3,8 @@ CMS.Views.ValidatingView = Backbone.View.extend({ ...@@ -3,7 +3,8 @@ CMS.Views.ValidatingView = Backbone.View.extend({
// decorates the fields. Needs wiring per class, but this initialization shows how // decorates the fields. Needs wiring per class, but this initialization shows how
// either have your init call this one or copy the contents // either have your init call this one or copy the contents
initialize : function() { 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.selectorToField = _.invert(this.fieldToSelectorMap);
}, },
...@@ -18,20 +19,11 @@ CMS.Views.ValidatingView = Backbone.View.extend({ ...@@ -18,20 +19,11 @@ CMS.Views.ValidatingView = Backbone.View.extend({
// which may be the subjects of validation errors // which may be the subjects of validation errors
}, },
_cacheValidationErrors : [], _cacheValidationErrors : [],
handleValidationError : function(model, error) { handleValidationError : function(model, error) {
// error triggered either by validation or server error
// error is object w/ fields and error strings // error is object w/ fields and error strings
for (var field in error) { for (var field in error) {
var ele = this.$el.find('#' + this.fieldToSelectorMap[field]); 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); this._cacheValidationErrors.push(ele);
if ($(ele).is('div')) { if ($(ele).is('div')) {
// put error on the contained inputs // put error on the contained inputs
......
from django.conf import settings from django.conf import settings
from django.conf.urls import patterns, include, url from django.conf.urls import patterns, include, url
from . import one_time_startup
# Uncomment the next two lines to enable the admin: # Uncomment the next two lines to enable the admin:
# from django.contrib import admin # from django.contrib import admin
......
...@@ -2,8 +2,9 @@ import json ...@@ -2,8 +2,9 @@ import json
from datetime import datetime from datetime import datetime
from django.http import HttpResponse from django.http import HttpResponse
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from dogapi import dog_stats_api
@dog_stats_api.timed('edxapp.heartbeat')
def heartbeat(request): def heartbeat(request):
""" """
Simple view that a loadbalancer can check to verify that the app is up Simple view that a loadbalancer can check to verify that the app is up
......
...@@ -84,12 +84,19 @@ def replace_static_urls(text, data_directory, course_namespace=None): ...@@ -84,12 +84,19 @@ def replace_static_urls(text, data_directory, course_namespace=None):
if rest.endswith('?raw'): if rest.endswith('?raw'):
return original return original
# course_namespace is not None, then use studio style urls
if course_namespace is not None and not isinstance(modulestore(), XMLModuleStore):
url = StaticContent.convert_legacy_static_url(rest, course_namespace)
# In debug mode, if we can find the url as is, # In debug mode, if we can find the url as is,
elif settings.DEBUG and finders.find(rest, True): if settings.DEBUG and finders.find(rest, True):
return original return original
# if we're running with a MongoBacked store course_namespace is not None, then use studio style urls
elif course_namespace is not None and not isinstance(modulestore(), XMLModuleStore):
# first look in the static file pipeline and see if we are trying to reference
# a piece of static content which is in the mitx repo (e.g. JS associated with an xmodule)
if staticfiles_storage.exists(rest):
url = staticfiles_storage.url(rest)
else:
# if not, then assume it's courseware specific content and then look in the
# Mongo-backed database
url = StaticContent.convert_legacy_static_url(rest, course_namespace)
# Otherwise, look the file up in staticfiles_storage, and append the data directory if needed # Otherwise, look the file up in staticfiles_storage, and append the data directory if needed
else: else:
course_path = "/".join((data_directory, rest)) course_path = "/".join((data_directory, rest))
......
...@@ -10,6 +10,7 @@ import paramiko ...@@ -10,6 +10,7 @@ import paramiko
import boto import boto
dog_http_api.api_key = settings.DATADOG_API dog_http_api.api_key = settings.DATADOG_API
dog_stats_api.start(api_key=settings.DATADOG_API, statsd=True)
class Command(BaseCommand): class Command(BaseCommand):
......
...@@ -13,6 +13,7 @@ from django.core.management import call_command ...@@ -13,6 +13,7 @@ from django.core.management import call_command
def initial_setup(server): def initial_setup(server):
# Launch the browser app (choose one of these below) # Launch the browser app (choose one of these below)
world.browser = Browser('chrome') world.browser = Browser('chrome')
# world.browser = Browser('phantomjs')
# world.browser = Browser('firefox') # world.browser = Browser('firefox')
......
...@@ -183,7 +183,7 @@ def evaluator(variables, functions, string, cs=False): ...@@ -183,7 +183,7 @@ def evaluator(variables, functions, string, cs=False):
# 0.33k or -17 # 0.33k or -17
number = (Optional(minus | plus) + inner_number number = (Optional(minus | plus) + inner_number
+ Optional(CaselessLiteral("E") + Optional("-") + number_part) + Optional(CaselessLiteral("E") + Optional((plus | minus)) + number_part)
+ Optional(number_suffix)) + Optional(number_suffix))
number = number.setParseAction(number_parse_action) # Convert to number number = number.setParseAction(number_parse_action) # Convert to number
......
...@@ -29,6 +29,7 @@ import sys ...@@ -29,6 +29,7 @@ import sys
from lxml import etree from lxml import etree
from xml.sax.saxutils import unescape from xml.sax.saxutils import unescape
from copy import deepcopy
import chem import chem
import chem.chemcalc import chem.chemcalc
...@@ -497,11 +498,10 @@ class LoncapaProblem(object): ...@@ -497,11 +498,10 @@ class LoncapaProblem(object):
Used by get_html. Used by get_html.
''' '''
if (problemtree.tag == 'script' and problemtree.get('type') if (problemtree.tag == 'script' and problemtree.get('type')
and 'javascript' in problemtree.get('type')): and 'javascript' in problemtree.get('type')):
# leave javascript intact. # leave javascript intact.
return problemtree return deepcopy(problemtree)
if problemtree.tag in html_problem_semantics: if problemtree.tag in html_problem_semantics:
return return
......
...@@ -111,14 +111,13 @@ class CorrectMap(object): ...@@ -111,14 +111,13 @@ class CorrectMap(object):
return None return None
def get_npoints(self, answer_id): def get_npoints(self, answer_id):
""" Return the number of points for an answer: """Return the number of points for an answer, used for partial credit."""
If the answer is correct, return the assigned
number of points (default: 1 point)
Otherwise, return 0 points """
if self.is_correct(answer_id):
npoints = self.get_property(answer_id, 'npoints') npoints = self.get_property(answer_id, 'npoints')
return npoints if npoints is not None else 1 if npoints is not None:
else: return npoints
elif self.is_correct(answer_id):
return 1
# if not correct and no points have been assigned, return 0
return 0 return 0
def set_property(self, answer_id, property, value): def set_property(self, answer_id, property, value):
......
...@@ -366,6 +366,12 @@ class ChoiceGroup(InputTypeBase): ...@@ -366,6 +366,12 @@ class ChoiceGroup(InputTypeBase):
self.choices = self.extract_choices(self.xml) self.choices = self.extract_choices(self.xml)
@classmethod
def get_attributes(cls):
return [Attribute("show_correctness", "always"),
Attribute("submitted_message", "Answer received.")]
def _extra_context(self): def _extra_context(self):
return {'input_type': self.html_input_type, return {'input_type': self.html_input_type,
'choices': self.choices, 'choices': self.choices,
......
<form class="choicegroup capa_inputtype" id="inputtype_${id}"> <form class="choicegroup capa_inputtype" id="inputtype_${id}">
<div class="indicator_container">
<fieldset> % if input_type == 'checkbox' or not value:
% for choice_id, choice_description in choices: % if status == 'unsubmitted' or show_correctness == 'never':
<label for="input_${id}_${choice_id}">
% if choice_id in value:
<span class="indicator_container">
% if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span> <span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif status == 'correct': % elif status == 'correct':
<span class="correct" id="status_${id}"></span> <span class="correct" id="status_${id}"></span>
...@@ -15,23 +10,38 @@ ...@@ -15,23 +10,38 @@
% elif status == 'incomplete': % elif status == 'incomplete':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}"></span>
% endif % endif
</span>
% else:
<span class="indicator_container">&#160;</span>
% endif % endif
</div>
<fieldset>
% for choice_id, choice_description in choices:
<label for="input_${id}_${choice_id}"
% if input_type == 'radio' and choice_id in value:
<%
if status == 'correct':
correctness = 'correct'
elif status == 'incorrect':
correctness = 'incorrect'
else:
correctness = None
%>
% if correctness and not show_correctness=='never':
class="choicegroup_${correctness}"
% endif
% endif
>
<input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" value="${choice_id}" <input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" value="${choice_id}"
% if choice_id in value: % if choice_id in value:
checked="true" checked="true"
% endif % endif
/>
${choice_description}
</label>
/> ${choice_description} </label>
% endfor % endfor
<span id="answer_${id}"></span> <span id="answer_${id}"></span>
</fieldset> </fieldset>
% if show_correctness == "never" and (value or status not in ['unsubmitted']):
<div class="capa_alert">${submitted_message}</div>
%endif
</form> </form>
...@@ -91,12 +91,12 @@ class CorrectMapTest(unittest.TestCase): ...@@ -91,12 +91,12 @@ class CorrectMapTest(unittest.TestCase):
npoints=0) npoints=0)
# Assert that we get the expected points # Assert that we get the expected points
# If points assigned and correct --> npoints # If points assigned --> npoints
# If no points assigned and correct --> 1 point # If no points assigned and correct --> 1 point
# Otherwise --> 0 points # If no points assigned and incorrect --> 0 points
self.assertEqual(self.cmap.get_npoints('1_2_1'), 5) self.assertEqual(self.cmap.get_npoints('1_2_1'), 5)
self.assertEqual(self.cmap.get_npoints('2_2_1'), 1) self.assertEqual(self.cmap.get_npoints('2_2_1'), 1)
self.assertEqual(self.cmap.get_npoints('3_2_1'), 0) self.assertEqual(self.cmap.get_npoints('3_2_1'), 5)
self.assertEqual(self.cmap.get_npoints('4_2_1'), 0) self.assertEqual(self.cmap.get_npoints('4_2_1'), 0)
self.assertEqual(self.cmap.get_npoints('5_2_1'), 0) self.assertEqual(self.cmap.get_npoints('5_2_1'), 0)
......
...@@ -3,6 +3,7 @@ from lxml import etree ...@@ -3,6 +3,7 @@ from lxml import etree
import os import os
import textwrap import textwrap
import json import json
import mock import mock
from capa.capa_problem import LoncapaProblem from capa.capa_problem import LoncapaProblem
...@@ -49,6 +50,8 @@ class CapaHtmlRenderTest(unittest.TestCase): ...@@ -49,6 +50,8 @@ class CapaHtmlRenderTest(unittest.TestCase):
self.assertEqual(test_element.text, "Test include") self.assertEqual(test_element.text, "Test include")
def test_process_outtext(self): def test_process_outtext(self):
# Generate some XML with <startouttext /> and <endouttext /> # Generate some XML with <startouttext /> and <endouttext />
xml_str = textwrap.dedent(""" xml_str = textwrap.dedent("""
...@@ -86,6 +89,25 @@ class CapaHtmlRenderTest(unittest.TestCase): ...@@ -86,6 +89,25 @@ class CapaHtmlRenderTest(unittest.TestCase):
script_element = rendered_html.find('script') script_element = rendered_html.find('script')
self.assertEqual(None, script_element) self.assertEqual(None, script_element)
def test_render_javascript(self):
# Generate some XML with a <script> tag
xml_str = textwrap.dedent("""
<problem>
<script type="text/javascript">function(){}</script>
</problem>
""")
# Create the problem
problem = LoncapaProblem(xml_str, '1', system=test_system)
# Render the HTML
rendered_html = etree.XML(problem.get_html())
# expect the javascript is still present in the rendered html
self.assertTrue("<script type=\"text/javascript\">function(){}</script>" in etree.tostring(rendered_html))
def test_render_response_xml(self): def test_render_response_xml(self):
# Generate some XML for a string response # Generate some XML for a string response
kwargs = {'question_text': "Test question", kwargs = {'question_text': "Test question",
......
...@@ -102,6 +102,8 @@ class ChoiceGroupTest(unittest.TestCase): ...@@ -102,6 +102,8 @@ class ChoiceGroupTest(unittest.TestCase):
'choices': [('foil1', '<text>This is foil One.</text>'), 'choices': [('foil1', '<text>This is foil One.</text>'),
('foil2', '<text>This is foil Two.</text>'), ('foil2', '<text>This is foil Two.</text>'),
('foil3', 'This is foil Three.'), ], ('foil3', 'This is foil Three.'), ],
'show_correctness': 'always',
'submitted_message': 'Answer received.',
'name_array_suffix': expected_suffix, # what is this for?? 'name_array_suffix': expected_suffix, # what is this for??
} }
......
...@@ -642,6 +642,15 @@ class NumericalResponseTest(ResponseTest): ...@@ -642,6 +642,15 @@ class NumericalResponseTest(ResponseTest):
incorrect_responses = ["", "2.11", "1.89", "0"] incorrect_responses = ["", "2.11", "1.89", "0"]
self.assert_multiple_grade(problem, correct_responses, incorrect_responses) self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
def test_exponential_answer(self):
problem = self.build_problem(question_text="What 5 * 10?",
explanation="The answer is 50",
answer="5e+1")
correct_responses = ["50", "50.0", "5e1", "5e+1", "50e0", "500e-1"]
incorrect_responses = ["", "3.9", "4.1", "0", "5.01e1"]
self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
class CustomResponseTest(ResponseTest): class CustomResponseTest(ResponseTest):
from response_xml_factory import CustomResponseXMLFactory from response_xml_factory import CustomResponseXMLFactory
......
...@@ -582,7 +582,7 @@ class CapaModule(XModule): ...@@ -582,7 +582,7 @@ class CapaModule(XModule):
@staticmethod @staticmethod
def make_dict_of_responses(get): def make_dict_of_responses(get):
'''Make dictionary of student responses (aka "answers") '''Make dictionary of student responses (aka "answers")
get is POST dictionary. get is POST dictionary (Djano QueryDict).
The *get* dict has keys of the form 'x_y', which are mapped The *get* dict has keys of the form 'x_y', which are mapped
to key 'y' in the returned dict. For example, to key 'y' in the returned dict. For example,
...@@ -606,6 +606,7 @@ class CapaModule(XModule): ...@@ -606,6 +606,7 @@ class CapaModule(XModule):
to 'input_1' in the returned dict) to 'input_1' in the returned dict)
''' '''
answers = dict() answers = dict()
for key in get: for key in get:
# e.g. input_resistor_1 ==> resistor_1 # e.g. input_resistor_1 ==> resistor_1
_, _, name = key.partition('_') _, _, name = key.partition('_')
...@@ -613,7 +614,7 @@ class CapaModule(XModule): ...@@ -613,7 +614,7 @@ class CapaModule(XModule):
# If key has no underscores, then partition # If key has no underscores, then partition
# will return (key, '', '') # will return (key, '', '')
# We detect this and raise an error # We detect this and raise an error
if name is '': if not name:
raise ValueError("%s must contain at least one underscore" % str(key)) raise ValueError("%s must contain at least one underscore" % str(key))
else: else:
...@@ -625,10 +626,7 @@ class CapaModule(XModule): ...@@ -625,10 +626,7 @@ class CapaModule(XModule):
name = name[:-2] if is_list_key else name name = name[:-2] if is_list_key else name
if is_list_key: if is_list_key:
if type(get[key]) is list: val = get.getlist(key)
val = get[key]
else:
val = [get[key]]
else: else:
val = get[key] val = get[key]
......
...@@ -246,7 +246,7 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -246,7 +246,7 @@ class CourseDescriptor(SequenceDescriptor):
policy = json.loads(cls.read_grading_policy(paths, system)) policy = json.loads(cls.read_grading_policy(paths, system))
except ValueError: except ValueError:
system.error_tracker("Unable to decode grading policy as json") system.error_tracker("Unable to decode grading policy as json")
policy = None policy = {}
# cdodge: import the grading policy information that is on disk and put into the # cdodge: import the grading policy information that is on disk and put into the
# descriptor 'definition' bucket as a dictionary so that it is persisted in the DB # descriptor 'definition' bucket as a dictionary so that it is persisted in the DB
...@@ -356,7 +356,14 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -356,7 +356,14 @@ class CourseDescriptor(SequenceDescriptor):
""" """
Return the pdf_textbooks config, as a python object, or None if not specified. Return the pdf_textbooks config, as a python object, or None if not specified.
""" """
return self.metadata.get('pdf_textbooks') return self.metadata.get('pdf_textbooks', [])
@property
def html_textbooks(self):
"""
Return the html_textbooks config, as a python object, or None if not specified.
"""
return self.metadata.get('html_textbooks', [])
@tabs.setter @tabs.setter
def tabs(self, value): def tabs(self, value):
......
...@@ -42,6 +42,14 @@ section.problem { ...@@ -42,6 +42,14 @@ section.problem {
label.choicegroup_correct{ label.choicegroup_correct{
&:after{ &:after{
content: url('../images/correct-icon.png'); content: url('../images/correct-icon.png');
margin-left:15px
}
}
label.choicegroup_incorrect{
&:after{
content: url('../images/incorrect-icon.png');
margin-left:15px;
} }
} }
...@@ -52,6 +60,7 @@ section.problem { ...@@ -52,6 +60,7 @@ section.problem {
.indicator_container { .indicator_container {
float: left; float: left;
width: 25px; width: 25px;
height: 1px;
margin-right: 15px; margin-right: 15px;
} }
...@@ -69,7 +78,7 @@ section.problem { ...@@ -69,7 +78,7 @@ section.problem {
} }
text { text {
display: block; display: inline;
margin-left: 25px; margin-left: 25px;
} }
} }
......
...@@ -11,6 +11,8 @@ from xmodule.capa_module import CapaModule ...@@ -11,6 +11,8 @@ from xmodule.capa_module import CapaModule
from xmodule.modulestore import Location from xmodule.modulestore import Location
from lxml import etree from lxml import etree
from django.http import QueryDict
from . import test_system from . import test_system
...@@ -326,14 +328,18 @@ class CapaModuleTest(unittest.TestCase): ...@@ -326,14 +328,18 @@ class CapaModuleTest(unittest.TestCase):
def test_parse_get_params(self): def test_parse_get_params(self):
# We have to set up Django settings in order to use QueryDict
from django.conf import settings
settings.configure()
# Valid GET param dict # Valid GET param dict
valid_get_dict = {'input_1': 'test', valid_get_dict = self._querydict_from_dict({'input_1': 'test',
'input_1_2': 'test', 'input_1_2': 'test',
'input_1_2_3': 'test', 'input_1_2_3': 'test',
'input_[]_3': 'test', 'input_[]_3': 'test',
'input_4': None, 'input_4': None,
'input_5': [], 'input_5': [],
'input_6': 5} 'input_6': 5})
result = CapaModule.make_dict_of_responses(valid_get_dict) result = CapaModule.make_dict_of_responses(valid_get_dict)
...@@ -347,20 +353,19 @@ class CapaModuleTest(unittest.TestCase): ...@@ -347,20 +353,19 @@ class CapaModuleTest(unittest.TestCase):
# Valid GET param dict with list keys # Valid GET param dict with list keys
valid_get_dict = {'input_2[]': ['test1', 'test2']} valid_get_dict = self._querydict_from_dict({'input_2[]': ['test1', 'test2']})
result = CapaModule.make_dict_of_responses(valid_get_dict) result = CapaModule.make_dict_of_responses(valid_get_dict)
self.assertTrue('2' in result) self.assertTrue('2' in result)
self.assertEqual(valid_get_dict['input_2[]'], result['2']) self.assertEqual(['test1','test2'], result['2'])
# If we use [] at the end of a key name, we should always # If we use [] at the end of a key name, we should always
# get a list, even if there's just one value # get a list, even if there's just one value
valid_get_dict = {'input_1[]': 'test'} valid_get_dict = self._querydict_from_dict({'input_1[]': 'test'})
result = CapaModule.make_dict_of_responses(valid_get_dict) result = CapaModule.make_dict_of_responses(valid_get_dict)
self.assertEqual(result['1'], ['test']) self.assertEqual(result['1'], ['test'])
# If we have no underscores in the name, then the key is invalid # If we have no underscores in the name, then the key is invalid
invalid_get_dict = {'input': 'test'} invalid_get_dict = self._querydict_from_dict({'input': 'test'})
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
result = CapaModule.make_dict_of_responses(invalid_get_dict) result = CapaModule.make_dict_of_responses(invalid_get_dict)
...@@ -368,11 +373,32 @@ class CapaModuleTest(unittest.TestCase): ...@@ -368,11 +373,32 @@ class CapaModuleTest(unittest.TestCase):
# Two equivalent names (one list, one non-list) # Two equivalent names (one list, one non-list)
# One of the values would overwrite the other, so detect this # One of the values would overwrite the other, so detect this
# and raise an exception # and raise an exception
invalid_get_dict = {'input_1[]': 'test 1', invalid_get_dict = self._querydict_from_dict({'input_1[]': 'test 1',
'input_1': 'test 2' } 'input_1': 'test 2' })
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
result = CapaModule.make_dict_of_responses(invalid_get_dict) result = CapaModule.make_dict_of_responses(invalid_get_dict)
def _querydict_from_dict(self, param_dict):
""" Create a Django QueryDict from a Python dictionary """
# QueryDict objects are immutable by default, so we make
# a copy that we can update.
querydict = QueryDict('')
copyDict = querydict.copy()
for (key, val) in param_dict.items():
# QueryDicts handle lists differently from ordinary values,
# so we have to specifically tell the QueryDict that
# this is a list
if type(val) is list:
copyDict.setlist(key, val)
else:
copyDict[key] = val
return copyDict
def test_check_problem_correct(self): def test_check_problem_correct(self):
module = CapaFactory.create(attempts=1) module = CapaFactory.create(attempts=1)
......
...@@ -3,4 +3,3 @@ ...@@ -3,4 +3,3 @@
-e git://github.com/MITx/django-pipeline.git#egg=django-pipeline -e git://github.com/MITx/django-pipeline.git#egg=django-pipeline
-e git://github.com/MITx/django-wiki.git@e2e84558#egg=django-wiki -e git://github.com/MITx/django-wiki.git@e2e84558#egg=django-wiki
-e git://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev -e git://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
-e git://github.com/MITx/dogapi.git@003a4fc9#egg=dogapi
#! /bin/bash
set -e
set -x
git remote prune origin
# Reset the submodule, in case it changed
git submodule foreach 'git reset --hard HEAD'
# Set the IO encoding to UTF-8 so that askbot will start
export PYTHONIOENCODING=UTF-8
rake clobber
rake pep8 || echo "pep8 failed, continuing"
rake pylint || echo "pylint failed, continuing"
...@@ -38,6 +38,9 @@ pip install -q -r test-requirements.txt ...@@ -38,6 +38,9 @@ pip install -q -r test-requirements.txt
yes w | pip install -q -r requirements.txt yes w | pip install -q -r requirements.txt
rake clobber rake clobber
rake pep8
rake pylint
TESTS_FAILED=0 TESTS_FAILED=0
rake test_cms[false] || TESTS_FAILED=1 rake test_cms[false] || TESTS_FAILED=1
rake test_lms[false] || TESTS_FAILED=1 rake test_lms[false] || TESTS_FAILED=1
......
...@@ -500,7 +500,7 @@ def modx_dispatch(request, dispatch, location, course_id): ...@@ -500,7 +500,7 @@ def modx_dispatch(request, dispatch, location, course_id):
if instance is None: if instance is None:
# Either permissions just changed, or someone is trying to be clever # Either permissions just changed, or someone is trying to be clever
# and load something they shouldn't have access to. # and load something they shouldn't have access to.
log.debug("No module {0} for user {1}--access denied?".format(location, user)) log.debug("No module {0} for user {1}--access denied?".format(location, request.user))
raise Http404 raise Http404
instance_module = get_instance_module(course_id, request.user, instance, student_module_cache) instance_module = get_instance_module(course_id, request.user, instance, student_module_cache)
......
...@@ -130,6 +130,17 @@ def _pdf_textbooks(tab, user, course, active_page): ...@@ -130,6 +130,17 @@ def _pdf_textbooks(tab, user, course, active_page):
for index, textbook in enumerate(course.pdf_textbooks)] for index, textbook in enumerate(course.pdf_textbooks)]
return [] return []
def _html_textbooks(tab, user, course, active_page):
"""
Generates one tab per textbook. Only displays if user is authenticated.
"""
if user.is_authenticated():
# since there can be more than one textbook, active_page is e.g. "book/0".
return [CourseTab(textbook['tab_title'], reverse('html_book', args=[course.id, index]),
active_page == "htmltextbook/{0}".format(index))
for index, textbook in enumerate(course.html_textbooks)]
return []
def _staff_grading(tab, user, course, active_page): def _staff_grading(tab, user, course, active_page):
if has_access(user, course, 'staff'): if has_access(user, course, 'staff'):
link = reverse('staff_grading', args=[course.id]) link = reverse('staff_grading', args=[course.id])
...@@ -209,6 +220,7 @@ VALID_TAB_TYPES = { ...@@ -209,6 +220,7 @@ VALID_TAB_TYPES = {
'external_link': TabImpl(key_checker(['name', 'link']), _external_link), 'external_link': TabImpl(key_checker(['name', 'link']), _external_link),
'textbooks': TabImpl(null_validator, _textbooks), 'textbooks': TabImpl(null_validator, _textbooks),
'pdf_textbooks': TabImpl(null_validator, _pdf_textbooks), 'pdf_textbooks': TabImpl(null_validator, _pdf_textbooks),
'html_textbooks': TabImpl(null_validator, _html_textbooks),
'progress': TabImpl(need_name, _progress), 'progress': TabImpl(need_name, _progress),
'static_tab': TabImpl(key_checker(['name', 'url_slug']), _static_tab), 'static_tab': TabImpl(key_checker(['name', 'url_slug']), _static_tab),
'peer_grading': TabImpl(null_validator, _peer_grading), 'peer_grading': TabImpl(null_validator, _peer_grading),
......
...@@ -18,7 +18,6 @@ import pystache_custom as pystache ...@@ -18,7 +18,6 @@ import pystache_custom as pystache
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.search import path_to_location
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -166,7 +165,6 @@ def initialize_discussion_info(course): ...@@ -166,7 +165,6 @@ def initialize_discussion_info(course):
# get all discussion models within this course_id # get all discussion models within this course_id
all_modules = modulestore().get_items(['i4x', course.location.org, course.location.course, 'discussion', None], course_id=course_id) all_modules = modulestore().get_items(['i4x', course.location.org, course.location.course, 'discussion', None], course_id=course_id)
path_to_locations = {}
for module in all_modules: for module in all_modules:
skip_module = False skip_module = False
for key in ('id', 'discussion_category', 'for'): for key in ('id', 'discussion_category', 'for'):
...@@ -174,14 +172,6 @@ def initialize_discussion_info(course): ...@@ -174,14 +172,6 @@ def initialize_discussion_info(course):
log.warning("Required key '%s' not in discussion %s, leaving out of category map" % (key, module.location)) log.warning("Required key '%s' not in discussion %s, leaving out of category map" % (key, module.location))
skip_module = True skip_module = True
# cdodge: pre-compute the path_to_location. Note this can throw an exception for any
# dangling discussion modules
try:
path_to_locations[module.location] = path_to_location(modulestore(), course.id, module.location)
except NoPathToItem:
log.warning("Could not compute path_to_location for {0}. Perhaps this is an orphaned discussion module?!? Skipping...".format(module.location))
skip_module = True
if skip_module: if skip_module:
continue continue
...@@ -246,7 +236,6 @@ def initialize_discussion_info(course): ...@@ -246,7 +236,6 @@ def initialize_discussion_info(course):
_DISCUSSIONINFO[course.id]['id_map'] = discussion_id_map _DISCUSSIONINFO[course.id]['id_map'] = discussion_id_map
_DISCUSSIONINFO[course.id]['category_map'] = category_map _DISCUSSIONINFO[course.id]['category_map'] = category_map
_DISCUSSIONINFO[course.id]['timestamp'] = datetime.now() _DISCUSSIONINFO[course.id]['timestamp'] = datetime.now()
_DISCUSSIONINFO[course.id]['path_to_location'] = path_to_locations
class JsonResponse(HttpResponse): class JsonResponse(HttpResponse):
...@@ -403,21 +392,8 @@ def get_courseware_context(content, course): ...@@ -403,21 +392,8 @@ def get_courseware_context(content, course):
location = id_map[id]["location"].url() location = id_map[id]["location"].url()
title = id_map[id]["title"] title = id_map[id]["title"]
# cdodge: did we pre-compute, if so, then let's use that rather than recomputing url = reverse('jump_to', kwargs={"course_id":course.location.course_id,
if 'path_to_location' in _DISCUSSIONINFO[course.id] and location in _DISCUSSIONINFO[course.id]['path_to_location']: "location": location})
(course_id, chapter, section, position) = _DISCUSSIONINFO[course.id]['path_to_location'][location]
else:
try:
(course_id, chapter, section, position) = path_to_location(modulestore(), course.id, location)
except NoPathToItem:
# Object is not in the graph any longer, let's just get path to the base of the course
# so that we can at least return something to the caller
(course_id, chapter, section, position) = path_to_location(modulestore(), course.id, course.location)
url = reverse('courseware_position', kwargs={"course_id":course_id,
"chapter":chapter,
"section":section,
"position":position})
content_info = {"courseware_url": url, "courseware_title": title} content_info = {"courseware_url": url, "courseware_title": title}
return content_info return content_info
......
from lxml import etree from lxml import etree
# from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.http import Http404
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
from courseware.access import has_access from courseware.access import has_access
...@@ -15,6 +15,8 @@ def index(request, course_id, book_index, page=None): ...@@ -15,6 +15,8 @@ def index(request, course_id, book_index, page=None):
staff_access = has_access(request.user, course, 'staff') staff_access = has_access(request.user, course, 'staff')
book_index = int(book_index) book_index = int(book_index)
if book_index < 0 or book_index >= len(course.textbooks):
raise Http404("Invalid book index value: {0}".format(book_index))
textbook = course.textbooks[book_index] textbook = course.textbooks[book_index]
table_of_contents = textbook.table_of_contents table_of_contents = textbook.table_of_contents
...@@ -40,6 +42,8 @@ def pdf_index(request, course_id, book_index, chapter=None, page=None): ...@@ -40,6 +42,8 @@ def pdf_index(request, course_id, book_index, chapter=None, page=None):
staff_access = has_access(request.user, course, 'staff') staff_access = has_access(request.user, course, 'staff')
book_index = int(book_index) book_index = int(book_index)
if book_index < 0 or book_index >= len(course.pdf_textbooks):
raise Http404("Invalid book index value: {0}".format(book_index))
textbook = course.pdf_textbooks[book_index] textbook = course.pdf_textbooks[book_index]
def remap_static_url(original_url, course): def remap_static_url(original_url, course):
...@@ -67,3 +71,39 @@ def pdf_index(request, course_id, book_index, chapter=None, page=None): ...@@ -67,3 +71,39 @@ def pdf_index(request, course_id, book_index, chapter=None, page=None):
'chapter': chapter, 'chapter': chapter,
'page': page, 'page': page,
'staff_access': staff_access}) 'staff_access': staff_access})
@login_required
def html_index(request, course_id, book_index, chapter=None, anchor_id=None):
course = get_course_with_access(request.user, course_id, 'load')
staff_access = has_access(request.user, course, 'staff')
book_index = int(book_index)
if book_index < 0 or book_index >= len(course.html_textbooks):
raise Http404("Invalid book index value: {0}".format(book_index))
textbook = course.html_textbooks[book_index]
def remap_static_url(original_url, course):
input_url = "'" + original_url + "'"
output_url = replace_static_urls(
input_url,
course.metadata['data_dir'],
course_namespace=course.location
)
# strip off the quotes again...
return output_url[1:-1]
if 'url' in textbook:
textbook['url'] = remap_static_url(textbook['url'], course)
# then remap all the chapter URLs as well, if they are provided.
if 'chapters' in textbook:
for entry in textbook['chapters']:
entry['url'] = remap_static_url(entry['url'], course)
return render_to_response('static_htmlbook.html',
{'book_index': book_index,
'course': course,
'textbook': textbook,
'chapter': chapter,
'anchor_id': anchor_id,
'staff_access': staff_access})
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)
...@@ -158,6 +158,19 @@ div.book-wrapper { ...@@ -158,6 +158,19 @@ div.book-wrapper {
img { img {
max-width: 100%; max-width: 100%;
} }
div {
text-align: left;
line-height: 1.6em;
margin-left: 5px;
margin-right: 5px;
margin-top: 5px;
margin-bottom: 5px;
.Paragraph, h2 {
margin-top: 10px;
}
}
} }
} }
......
<%inherit file="main.html" />
<%namespace name='static' file='static_content.html'/>
<%block name="title"><title>${course.number} Textbook</title>
</%block>
<%block name="headextra">
<%static:css group='course'/>
<%static:js group='courseware'/>
</%block>
<%block name="js_extra">
<script type="text/javascript">
(function($) {
$.fn.myHTMLViewer = function(options) {
var urlToLoad = null;
if (options.url) {
urlToLoad = options.url;
}
var chapterUrls = null;
if (options.chapters) {
chapterUrls = options.chapters;
}
var chapterToLoad = 1;
if (options.chapterNum) {
// TODO: this should only be specified if there are
// chapters, and it should be in-bounds.
chapterToLoad = options.chapterNum;
}
var anchorToLoad = null;
if (options.chapters) {
anchorToLoad = options.anchor_id;
}
loadUrl = function htmlViewLoadUrl(url, anchorId) {
// clear out previous load, if any:
parentElement = document.getElementById('bookpage');
while (parentElement.hasChildNodes())
parentElement.removeChild(parentElement.lastChild);
// load new URL in:
$('#bookpage').load(url);
// if there is an anchor set, then go to that location:
if (anchorId != null) {
// TODO: add implementation....
}
};
loadChapterUrl = function htmlViewLoadChapterUrl(chapterNum, anchorId) {
if (chapterNum < 1 || chapterNum > chapterUrls.length) {
return;
}
var chapterUrl = chapterUrls[chapterNum-1];
loadUrl(chapterUrl, anchorId);
};
// define navigation links for chapters:
if (chapterUrls != null) {
var loadChapterUrlHelper = function(i) {
return function(event) {
// when opening a new chapter, always open to the top:
loadChapterUrl(i, null);
};
};
for (var index = 1; index <= chapterUrls.length; index += 1) {
$("#htmlchapter-" + index).click(loadChapterUrlHelper(index));
}
}
// finally, load the appropriate url/page
if (urlToLoad != null) {
loadUrl(urlToLoad, anchorToLoad);
} else {
loadChapterUrl(chapterToLoad, anchorToLoad);
}
}
})(jQuery);
$(document).ready(function() {
var options = {};
%if 'url' in textbook:
options.url = "${textbook['url']}";
%endif
%if 'chapters' in textbook:
var chptrs = [];
%for chap in textbook['chapters']:
chptrs.push("${chap['url']}");
%endfor
options.chapters = chptrs;
%endif
%if chapter is not None:
options.chapterNum = ${chapter};
%endif
%if anchor_id is not None:
options.anchor_id = ${anchor_id};
%endif
$('#outerContainer').myHTMLViewer(options);
});
</script>
</%block>
<%include file="/courseware/course_navigation.html" args="active_page='htmltextbook/{0}'.format(book_index)" />
<div id="outerContainer">
<div id="mainContainer" class="book-wrapper">
%if 'chapters' in textbook:
<section aria-label="Textbook Navigation" class="book-sidebar">
<ul id="booknav" class="treeview-booknav">
<%def name="print_entry(entry, index_value)">
<li id="htmlchapter-${index_value}">
<a class="chapter">
${entry.get('title')}
</a>
</li>
</%def>
%for (index, entry) in enumerate(textbook['chapters']):
${print_entry(entry, index+1)}
% endfor
</ul>
</section>
%endif
<section id="viewerContainer" class="book">
<section class="page">
<div id="bookpage" />
</section>
</section>
</div>
</div>
...@@ -3,6 +3,9 @@ from django.conf.urls import patterns, include, url ...@@ -3,6 +3,9 @@ from django.conf.urls import patterns, include, url
from django.contrib import admin from django.contrib import admin
from django.conf.urls.static import static from django.conf.urls.static import static
from django.views.generic import RedirectView from django.views.generic import RedirectView
from . import one_time_startup
import django.contrib.auth.views import django.contrib.auth.views
# Uncomment the next two lines to enable the admin: # Uncomment the next two lines to enable the admin:
...@@ -277,6 +280,15 @@ if settings.COURSEWARE_ENABLED: ...@@ -277,6 +280,15 @@ if settings.COURSEWARE_ENABLED:
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/pdfbook/(?P<book_index>[^/]*)/chapter/(?P<chapter>[^/]*)/(?P<page>[^/]*)$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/pdfbook/(?P<book_index>[^/]*)/chapter/(?P<chapter>[^/]*)/(?P<page>[^/]*)$',
'staticbook.views.pdf_index'), 'staticbook.views.pdf_index'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/htmlbook/(?P<book_index>[^/]*)/$',
'staticbook.views.html_index', name="html_book"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/htmlbook/(?P<book_index>[^/]*)/chapter/(?P<chapter>[^/]*)/$',
'staticbook.views.html_index'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/htmlbook/(?P<book_index>[^/]*)/chapter/(?P<chapter>[^/]*)/(?P<anchor_id>[^/]*)/$',
'staticbook.views.html_index'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/htmlbook/(?P<book_index>[^/]*)/(?P<anchor_id>[^/]*)/$',
'staticbook.views.html_index'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/?$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/?$',
'courseware.views.index', name="courseware"), 'courseware.views.index', name="courseware"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/(?P<chapter>[^/]*)/$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/(?P<chapter>[^/]*)/$',
......
...@@ -58,3 +58,4 @@ ipython==0.13.1 ...@@ -58,3 +58,4 @@ ipython==0.13.1
xmltodict==0.4.1 xmltodict==0.4.1
paramiko==1.9.0 paramiko==1.9.0
Pillow==1.7.8 Pillow==1.7.8
dogapi==1.2.1
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment