diff --git a/.gitignore b/.gitignore
index 493df5a7fd..b13a128a63 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,4 +28,5 @@ nosetests.xml
cover_html/
.idea/
.redcar/
-chromedriver.log
\ No newline at end of file
+chromedriver.log
+ghostdriver.log
diff --git a/Gemfile b/Gemfile
index 43a9f6e2b1..7f7b146978 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,4 +1,4 @@
-source :rubygems
+source 'https://rubygems.org'
gem 'rake', '~> 10.0.3'
gem 'sass', '3.1.15'
gem 'bourbon', '~> 1.3.6'
diff --git a/cms/djangoapps/contentstore/features/advanced-settings.feature b/cms/djangoapps/contentstore/features/advanced-settings.feature
index 4708a60be1..779d44e4b2 100644
--- a/cms/djangoapps/contentstore/features/advanced-settings.feature
+++ b/cms/djangoapps/contentstore/features/advanced-settings.feature
@@ -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
diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py
index 91daf70718..1024579b45 100644
--- a/cms/djangoapps/contentstore/features/advanced-settings.py
+++ b/cms/djangoapps/contentstore/features/advanced-settings.py
@@ -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):
diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py
index 61b4fee9f6..2ec0427e1d 100644
--- a/cms/djangoapps/contentstore/features/common.py
+++ b/cms/djangoapps/contentstore/features/common.py
@@ -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):
diff --git a/cms/djangoapps/contentstore/features/section.feature b/cms/djangoapps/contentstore/features/section.feature
index 75e7a4af10..08d38367bc 100644
--- a/cms/djangoapps/contentstore/features/section.feature
+++ b/cms/djangoapps/contentstore/features/section.feature
@@ -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
\ No newline at end of file
+ Then the section does not exist
diff --git a/cms/djangoapps/contentstore/features/section.py b/cms/djangoapps/contentstore/features/section.py
index cfa4e4bb52..b5ddb48a09 100644
--- a/cms/djangoapps/contentstore/features/section.py
+++ b/cms/djangoapps/contentstore/features/section.py
@@ -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)
\ No newline at end of file
+ assert_css_with_text(section_css, name)
diff --git a/cms/djangoapps/contentstore/features/signup.py b/cms/djangoapps/contentstore/features/signup.py
index a786225ead..e8d0dd8229 100644
--- a/cms/djangoapps/contentstore/features/signup.py
+++ b/cms/djangoapps/contentstore/features/signup.py
@@ -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):
diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature b/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature
index 5276b90d12..52c10e41a8 100644
--- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature
+++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature
@@ -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
diff --git a/cms/djangoapps/contentstore/features/subsection.feature b/cms/djangoapps/contentstore/features/subsection.feature
index 4b5f5b869d..1be5f4aeb9 100644
--- a/cms/djangoapps/contentstore/features/subsection.feature
+++ b/cms/djangoapps/contentstore/features/subsection.feature
@@ -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
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index 8e4a016a0f..c0ab9ec60e 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -263,7 +263,33 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# note, we know the link it should be because that's what in the 'full' course in the test data
self.assertContains(resp, '/c4x/edX/full/asset/handouts_schematic_tutorial.pdf')
+ def test_export_course_with_unknown_metadata(self):
+ ms = modulestore('direct')
+ cs = contentstore()
+ import_from_xml(ms, '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)
+
+ # 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)
+
+ print 'Exporting to tempdir = {0}'.format(root_dir)
+
+ # export out to a tempdir
+ bExported = False
+ try:
+ export_to_xml(ms, cs, location, root_dir, 'test_export')
+ bExported = True
+ except Exception:
+ pass
+
+ self.assertTrue(bExported)
class ContentStoreTest(ModuleStoreTestCase):
"""
diff --git a/cms/envs/aws.py b/cms/envs/aws.py
index a147f84531..be7816d21f 100644
--- a/cms/envs/aws.py
+++ b/cms/envs/aws.py
@@ -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")
\ No newline at end of file
diff --git a/cms/envs/dev.py b/cms/envs/dev.py
index 3dee93a398..9164c02e3f 100644
--- a/cms/envs/dev.py
+++ b/cms/envs/dev.py
@@ -4,9 +4,6 @@ This config file runs the simplest dev environment"""
from .common import *
from logsettings import get_logger_config
-import logging
-import sys
-
DEBUG = True
TEMPLATE_DEBUG = DEBUG
LOGGING = get_logger_config(ENV_ROOT / "log",
@@ -107,3 +104,36 @@ CACHE_TIMEOUT = 0
# Dummy secret key for dev
SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
+
+################################ DEBUG TOOLBAR #################################
+INSTALLED_APPS += ('debug_toolbar', 'debug_toolbar_mongo')
+MIDDLEWARE_CLASSES += ('django_comment_client.utils.QueryCountDebugMiddleware',
+ 'debug_toolbar.middleware.DebugToolbarMiddleware',)
+INTERNAL_IPS = ('127.0.0.1',)
+
+DEBUG_TOOLBAR_PANELS = (
+ 'debug_toolbar.panels.version.VersionDebugPanel',
+ 'debug_toolbar.panels.timer.TimerDebugPanel',
+ 'debug_toolbar.panels.settings_vars.SettingsVarsDebugPanel',
+ 'debug_toolbar.panels.headers.HeaderDebugPanel',
+ 'debug_toolbar.panels.request_vars.RequestVarsDebugPanel',
+ 'debug_toolbar.panels.sql.SQLDebugPanel',
+ 'debug_toolbar.panels.signals.SignalDebugPanel',
+ 'debug_toolbar.panels.logger.LoggingPanel',
+# This is breaking Mongo updates-- Christina is investigating.
+# 'debug_toolbar_mongo.panel.MongoDebugPanel',
+
+ # Enabling the profiler has a weird bug as of django-debug-toolbar==0.9.4 and
+ # Django=1.3.1/1.4 where requests to views get duplicated (your method gets
+ # hit twice). So you can uncomment when you need to diagnose performance
+ # problems, but you shouldn't leave it on.
+ # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
+ )
+
+DEBUG_TOOLBAR_CONFIG = {
+ 'INTERCEPT_REDIRECTS': False
+}
+
+# To see stacktraces for MongoDB queries, set this to True.
+# Stacktraces slow down page loads drastically (for pages with lots of queries).
+# DEBUG_TOOLBAR_MONGO_STACKTRACES = False
diff --git a/cms/one_time_startup.py b/cms/one_time_startup.py
new file mode 100644
index 0000000000..93428a3404
--- /dev/null
+++ b/cms/one_time_startup.py
@@ -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)
diff --git a/cms/static/js/views/settings/advanced_view.js b/cms/static/js/views/settings/advanced_view.js
index d20a21f7e7..a933bbdb9b 100644
--- a/cms/static/js/views/settings/advanced_view.js
+++ b/cms/static/js/views/settings/advanced_view.js
@@ -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;
}
diff --git a/cms/static/js/views/settings/main_settings_view.js b/cms/static/js/views/settings/main_settings_view.js
index 8f998dbf7a..9bd8feab8c 100644
--- a/cms/static/js/views/settings/main_settings_view.js
+++ b/cms/static/js/views/settings/main_settings_view.js
@@ -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);
},
diff --git a/cms/static/js/views/settings/settings_grading_view.js b/cms/static/js/views/settings/settings_grading_view.js
index a7c8defb43..78972f97a7 100644
--- a/cms/static/js/views/settings/settings_grading_view.js
+++ b/cms/static/js/views/settings/settings_grading_view.js
@@ -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();
},
diff --git a/cms/static/js/views/validating_view.js b/cms/static/js/views/validating_view.js
index e4928a8ebe..041e779030 100644
--- a/cms/static/js/views/validating_view.js
+++ b/cms/static/js/views/validating_view.js
@@ -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
diff --git a/cms/urls.py b/cms/urls.py
index 7b7b5e9375..d43b9bc44c 100644
--- a/cms/urls.py
+++ b/cms/urls.py
@@ -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
diff --git a/common/djangoapps/heartbeat/views.py b/common/djangoapps/heartbeat/views.py
index 956504407b..d7c3a32192 100644
--- a/common/djangoapps/heartbeat/views.py
+++ b/common/djangoapps/heartbeat/views.py
@@ -2,8 +2,9 @@ import json
from datetime import datetime
from django.http import HttpResponse
from xmodule.modulestore.django import modulestore
+from dogapi import dog_stats_api
-
+@dog_stats_api.timed('edxapp.heartbeat')
def heartbeat(request):
"""
Simple view that a loadbalancer can check to verify that the app is up
diff --git a/common/djangoapps/static_replace/__init__.py b/common/djangoapps/static_replace/__init__.py
index fb1f48d143..b73a658c5f 100644
--- a/common/djangoapps/static_replace/__init__.py
+++ b/common/djangoapps/static_replace/__init__.py
@@ -84,12 +84,19 @@ def replace_static_urls(text, data_directory, course_namespace=None):
if rest.endswith('?raw'):
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,
- elif settings.DEBUG and finders.find(rest, True):
+ if settings.DEBUG and finders.find(rest, True):
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
else:
course_path = "/".join((data_directory, rest))
diff --git a/common/djangoapps/student/management/commands/pearson_transfer.py b/common/djangoapps/student/management/commands/pearson_transfer.py
index 5eded6484a..75716c7443 100644
--- a/common/djangoapps/student/management/commands/pearson_transfer.py
+++ b/common/djangoapps/student/management/commands/pearson_transfer.py
@@ -10,6 +10,7 @@ import paramiko
import boto
dog_http_api.api_key = settings.DATADOG_API
+dog_stats_api.start(api_key=settings.DATADOG_API, statsd=True)
class Command(BaseCommand):
diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py
index 8c2a8ba7a5..0881d86124 100644
--- a/common/djangoapps/terrain/browser.py
+++ b/common/djangoapps/terrain/browser.py
@@ -13,6 +13,7 @@ from django.core.management import call_command
def initial_setup(server):
# Launch the browser app (choose one of these below)
world.browser = Browser('chrome')
+ # world.browser = Browser('phantomjs')
# world.browser = Browser('firefox')
diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py
index 14c590a660..8b32686985 100644
--- a/common/lib/capa/capa/capa_problem.py
+++ b/common/lib/capa/capa/capa_problem.py
@@ -29,6 +29,7 @@ import sys
from lxml import etree
from xml.sax.saxutils import unescape
+from copy import deepcopy
import chem
import chem.chemcalc
@@ -497,11 +498,10 @@ class LoncapaProblem(object):
Used by get_html.
'''
-
if (problemtree.tag == 'script' and problemtree.get('type')
and 'javascript' in problemtree.get('type')):
# leave javascript intact.
- return problemtree
+ return deepcopy(problemtree)
if problemtree.tag in html_problem_semantics:
return
diff --git a/common/lib/capa/capa/correctmap.py b/common/lib/capa/capa/correctmap.py
index 9e76fc20bf..ea56863a2f 100644
--- a/common/lib/capa/capa/correctmap.py
+++ b/common/lib/capa/capa/correctmap.py
@@ -95,7 +95,7 @@ class CorrectMap(object):
def is_correct(self, answer_id):
if answer_id in self.cmap:
- return self.cmap[answer_id]['correctness'] == 'correct'
+ return self.cmap[answer_id]['correctness'] in ['correct', 'partially-correct']
return None
def is_queued(self, answer_id):
@@ -111,15 +111,14 @@ class CorrectMap(object):
return None
def get_npoints(self, answer_id):
- """ Return the number of points for an answer:
- 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')
- return npoints if npoints is not None else 1
- else:
- return 0
+ """Return the number of points for an answer, used for partial credit."""
+ npoints = self.get_property(answer_id, 'npoints')
+ if npoints is not None:
+ return npoints
+ elif self.is_correct(answer_id):
+ return 1
+ # if not correct and no points have been assigned, return 0
+ return 0
def set_property(self, answer_id, property, value):
if answer_id in self.cmap:
diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py
index 1d6c340f37..f614743e67 100644
--- a/common/lib/capa/capa/inputtypes.py
+++ b/common/lib/capa/capa/inputtypes.py
@@ -45,8 +45,10 @@ import re
import shlex # for splitting quoted strings
import sys
import os
+import pyparsing
from registry import TagRegistry
+from capa.chem import chemcalc
log = logging.getLogger('mitx.' + __name__)
@@ -752,6 +754,45 @@ class ChemicalEquationInput(InputTypeBase):
"""
return {'previewer': '/static/js/capa/chemical_equation_preview.js', }
+ def handle_ajax(self, dispatch, get):
+ '''
+ Since we only have chemcalc preview this input, check to see if it
+ matches the corresponding dispatch and send it through if it does
+ '''
+ if dispatch == 'preview_chemcalc':
+ return self.preview_chemcalc(get)
+ return {}
+
+ def preview_chemcalc(self, get):
+ """
+ Render an html preview of a chemical formula or equation. get should
+ contain a key 'formula' and value 'some formula string'.
+
+ Returns a json dictionary:
+ {
+ 'preview' : 'the-preview-html' or ''
+ 'error' : 'the-error' or ''
+ }
+ """
+
+ result = {'preview': '',
+ 'error': ''}
+ formula = get['formula']
+ if formula is None:
+ result['error'] = "No formula specified."
+ return result
+
+ try:
+ result['preview'] = chemcalc.render_to_html(formula)
+ except pyparsing.ParseException as p:
+ result['error'] = "Couldn't parse formula: {0}".format(p)
+ except Exception:
+ # this is unexpected, so log
+ log.warning("Error while previewing chemical formula", exc_info=True)
+ result['error'] = "Error while rendering preview"
+
+ return result
+
registry.register(ChemicalEquationInput)
#-----------------------------------------------------------------------------
@@ -921,33 +962,142 @@ registry.register(DesignProtein2dInput)
class EditAGeneInput(InputTypeBase):
"""
An input type for editing a gene. Integrates with the genex java applet.
-
+
Example:
-
+
@@ -37,3 +34,4 @@
The main goal of this exercise is to start practicing the art of slow reading.
+Enter your (optional) instructions for the exercise in HTML format.
+Annotations are specified by an <annotation> tag which may may have the following attributes:
title (optional). Title of the annotation. Defaults to Commentary if omitted.body (required). Text of the annotation.problem (optional). Numeric index of the problem associated with this annotation. This is a zero-based index, so the first problem on the page would have problem="0".highlight (optional). Possible values: yellow, red, orange, green, blue, or purple. Defaults to yellow if this attribute is omitted.Add your HTML with annotation spans here.
+Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Nulla facilisi.
+