diff --git a/cms/djangoapps/contentstore/features/course-settings.feature b/cms/djangoapps/contentstore/features/course-settings.feature
new file mode 100644
index 0000000000..e869bfe47a
--- /dev/null
+++ b/cms/djangoapps/contentstore/features/course-settings.feature
@@ -0,0 +1,25 @@
+Feature: Course Settings
+ As a course author, I want to be able to configure my course settings.
+
+ Scenario: User can set course dates
+ Given I have opened a new course in Studio
+ When I select Schedule and Details
+ And I set course dates
+ Then I see the set dates on refresh
+
+ Scenario: User can clear previously set course dates (except start date)
+ Given I have set course dates
+ And I clear all the dates except start
+ Then I see cleared dates on refresh
+
+ Scenario: User cannot clear the course start date
+ Given I have set course dates
+ And I clear the course start date
+ Then I receive a warning about course start date
+ And The previously set start date is shown on refresh
+
+ Scenario: User can correct the course start date warning
+ Given I have tried to clear the course start
+ And I have entered a new course start date
+ Then The warning about course start date goes away
+ And My new course start date is shown on refresh
diff --git a/cms/djangoapps/contentstore/features/course-settings.py b/cms/djangoapps/contentstore/features/course-settings.py
new file mode 100644
index 0000000000..a0c25045f2
--- /dev/null
+++ b/cms/djangoapps/contentstore/features/course-settings.py
@@ -0,0 +1,163 @@
+from lettuce import world, step
+from common import *
+from terrain.steps import reload_the_page
+from selenium.webdriver.common.keys import Keys
+import time
+
+from nose.tools import assert_true, assert_false, assert_equal
+
+COURSE_START_DATE_CSS = "#course-start-date"
+COURSE_END_DATE_CSS = "#course-end-date"
+ENROLLMENT_START_DATE_CSS = "#course-enrollment-start-date"
+ENROLLMENT_END_DATE_CSS = "#course-enrollment-end-date"
+
+COURSE_START_TIME_CSS = "#course-start-time"
+COURSE_END_TIME_CSS = "#course-end-time"
+ENROLLMENT_START_TIME_CSS = "#course-enrollment-start-time"
+ENROLLMENT_END_TIME_CSS = "#course-enrollment-end-time"
+
+DUMMY_TIME = "3:30pm"
+DEFAULT_TIME = "12:00am"
+
+
+############### ACTIONS ####################
+@step('I select Schedule and Details$')
+def test_i_select_schedule_and_details(step):
+ expand_icon_css = 'li.nav-course-settings i.icon-expand'
+ if world.browser.is_element_present_by_css(expand_icon_css):
+ css_click(expand_icon_css)
+ link_css = 'li.nav-course-settings-schedule a'
+ css_click(link_css)
+
+
+@step('I have set course dates$')
+def test_i_have_set_course_dates(step):
+ step.given('I have opened a new course in Studio')
+ step.given('I select Schedule and Details')
+ step.given('And I set course dates')
+
+
+@step('And I set course dates$')
+def test_and_i_set_course_dates(step):
+ set_date_or_time(COURSE_START_DATE_CSS, '12/20/2013')
+ set_date_or_time(COURSE_END_DATE_CSS, '12/26/2013')
+ set_date_or_time(ENROLLMENT_START_DATE_CSS, '12/1/2013')
+ set_date_or_time(ENROLLMENT_END_DATE_CSS, '12/10/2013')
+
+ set_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
+ set_date_or_time(ENROLLMENT_END_TIME_CSS, DUMMY_TIME)
+
+ pause()
+
+
+@step('Then I see the set dates on refresh$')
+def test_then_i_see_the_set_dates_on_refresh(step):
+ reload_the_page(step)
+ verify_date_or_time(COURSE_START_DATE_CSS, '12/20/2013')
+ verify_date_or_time(COURSE_END_DATE_CSS, '12/26/2013')
+ verify_date_or_time(ENROLLMENT_START_DATE_CSS, '12/01/2013')
+ verify_date_or_time(ENROLLMENT_END_DATE_CSS, '12/10/2013')
+
+ verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
+ # Unset times get set to 12 AM once the corresponding date has been set.
+ verify_date_or_time(COURSE_END_TIME_CSS, DEFAULT_TIME)
+ verify_date_or_time(ENROLLMENT_START_TIME_CSS, DEFAULT_TIME)
+ verify_date_or_time(ENROLLMENT_END_TIME_CSS, DUMMY_TIME)
+
+
+@step('And I clear all the dates except start$')
+def test_and_i_clear_all_the_dates_except_start(step):
+ set_date_or_time(COURSE_END_DATE_CSS, '')
+ set_date_or_time(ENROLLMENT_START_DATE_CSS, '')
+ set_date_or_time(ENROLLMENT_END_DATE_CSS, '')
+
+ pause()
+
+
+@step('Then I see cleared dates on refresh$')
+def test_then_i_see_cleared_dates_on_refresh(step):
+ reload_the_page(step)
+ verify_date_or_time(COURSE_END_DATE_CSS, '')
+ verify_date_or_time(ENROLLMENT_START_DATE_CSS, '')
+ verify_date_or_time(ENROLLMENT_END_DATE_CSS, '')
+
+ verify_date_or_time(COURSE_END_TIME_CSS, '')
+ verify_date_or_time(ENROLLMENT_START_TIME_CSS, '')
+ verify_date_or_time(ENROLLMENT_END_TIME_CSS, '')
+
+ # Verify course start date (required) and time still there
+ verify_date_or_time(COURSE_START_DATE_CSS, '12/20/2013')
+ verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
+
+
+@step('I clear the course start date$')
+def test_i_clear_the_course_start_date(step):
+ set_date_or_time(COURSE_START_DATE_CSS, '')
+
+
+@step('I receive a warning about course start date$')
+def test_i_receive_a_warning_about_course_start_date(step):
+ assert_css_with_text('.message-error', 'The course must have an assigned start date.')
+ assert_true('error' in css_find(COURSE_START_DATE_CSS).first._element.get_attribute('class'))
+ assert_true('error' in css_find(COURSE_START_TIME_CSS).first._element.get_attribute('class'))
+
+
+@step('The previously set start date is shown on refresh$')
+def test_the_previously_set_start_date_is_shown_on_refresh(step):
+ reload_the_page(step)
+ verify_date_or_time(COURSE_START_DATE_CSS, '12/20/2013')
+ verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
+
+
+@step('Given I have tried to clear the course start$')
+def test_i_have_tried_to_clear_the_course_start(step):
+ step.given("I have set course dates")
+ step.given("I clear the course start date")
+ step.given("I receive a warning about course start date")
+
+
+@step('I have entered a new course start date$')
+def test_i_have_entered_a_new_course_start_date(step):
+ set_date_or_time(COURSE_START_DATE_CSS, '12/22/2013')
+ pause()
+
+
+@step('The warning about course start date goes away$')
+def test_the_warning_about_course_start_date_goes_away(step):
+ assert_equal(0, len(css_find('.message-error')))
+ assert_false('error' in css_find(COURSE_START_DATE_CSS).first._element.get_attribute('class'))
+ assert_false('error' in css_find(COURSE_START_TIME_CSS).first._element.get_attribute('class'))
+
+
+@step('My new course start date is shown on refresh$')
+def test_my_new_course_start_date_is_shown_on_refresh(step):
+ reload_the_page(step)
+ verify_date_or_time(COURSE_START_DATE_CSS, '12/22/2013')
+ # Time should have stayed from before attempt to clear date.
+ verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
+
+
+############### HELPER METHODS ####################
+def set_date_or_time(css, date_or_time):
+ """
+ Sets date or time field.
+ """
+ css_fill(css, date_or_time)
+ e = css_find(css).first
+ # hit Enter to apply the changes
+ e._element.send_keys(Keys.ENTER)
+
+
+def verify_date_or_time(css, date_or_time):
+ """
+ Verifies date or time field.
+ """
+ assert_equal(date_or_time, css_find(css).first.value)
+
+
+def pause():
+ """
+ Must sleep briefly to allow last time save to finish,
+ else refresh of browser will fail.
+ """
+ time.sleep(float(1))
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index 615ffb6ed0..edb20561bc 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -37,6 +37,14 @@ TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data')
+class MongoCollectionFindWrapper(object):
+ def __init__(self, original):
+ self.original = original
+ self.counter = 0
+
+ def find(self, query, *args, **kwargs):
+ self.counter = self.counter+1
+ return self.original(query, *args, **kwargs)
@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
class ContentStoreToyCourseTest(ModuleStoreTestCase):
@@ -145,8 +153,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# make sure the parent no longer points to the child object which was deleted
self.assertFalse(sequential.location.url() in chapter.children)
-
-
def test_about_overrides(self):
'''
This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html
@@ -205,7 +211,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
new_loc = descriptor.location._replace(org='MITx', course='999')
print "Checking {0} should now also be at {1}".format(descriptor.location.url(), new_loc.url())
resp = self.client.get(reverse('edit_unit', kwargs={'location': new_loc.url()}))
- self.assertEqual(resp.status_code, 200)
+ self.assertEqual(resp.status_code, 200)
def test_delete_course(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
@@ -307,6 +313,28 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# note, we know the link it should be because that's what in the 'full' course in the test data
self.assertContains(resp, '/c4x/edX/full/asset/handouts_schematic_tutorial.pdf')
+ def test_prefetch_children(self):
+ import_from_xml(modulestore(), 'common/test/data/', ['full'])
+ module_store = modulestore('direct')
+ location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
+
+ wrapper = MongoCollectionFindWrapper(module_store.collection.find)
+ module_store.collection.find = wrapper.find
+ course = module_store.get_item(location, depth=2)
+
+ # make sure we haven't done too many round trips to DB
+ # note we say 4 round trips here for 1) the course, 2 & 3) for the chapters and sequentials, and
+ # 4) because of the RT due to calculating the inherited metadata
+ self.assertEqual(wrapper.counter, 4)
+
+ # make sure we pre-fetched a known sequential which should be at depth=2
+ self.assertTrue(Location(['i4x', 'edX', 'full', 'sequential',
+ 'Administrivia_and_Circuit_Elements', None]) in course.system.module_data)
+
+ # make sure we don't have a specific vertical which should be at depth=3
+ self.assertFalse(Location(['i4x', 'edX', 'full', 'vertical', 'vertical_58',
+ None]) in course.system.module_data)
+
def test_export_course_with_unknown_metadata(self):
module_store = modulestore('direct')
content_store = contentstore()
diff --git a/cms/static/js/base.js b/cms/static/js/base.js
index 05678583ab..eb9997622b 100644
--- a/cms/static/js/base.js
+++ b/cms/static/js/base.js
@@ -251,7 +251,7 @@ function getEdxTimeFromDateTimeVals(date_val, time_val, format) {
time_val = '00:00';
// Note, we are using date.js utility which has better parsing abilities than the built in JS date parsing
- date = Date.parse(date_val + " " + time_val);
+ var date = Date.parse(date_val + " " + time_val);
if (format == null)
format = 'yyyy-MM-ddTHH:mm';
@@ -269,6 +269,7 @@ function getEdxTimeFromDateTimeInputs(date_id, time_id, format) {
}
function autosaveInput(e) {
+ var self = this;
if (this.saveTimer) {
clearTimeout(this.saveTimer);
}
@@ -276,7 +277,7 @@ function autosaveInput(e) {
this.saveTimer = setTimeout(function () {
$changedInput = $(e.target);
saveSubsection();
- this.saveTimer = null;
+ self.saveTimer = null;
}, 500);
}
@@ -318,6 +319,7 @@ function saveSubsection() {
data: JSON.stringify({ 'id': id, 'metadata': metadata}),
success: function () {
$spinner.delay(500).fadeOut(150);
+ $changedInput = null;
},
error: function () {
showToastMessage('There has been an error while saving your changes.');
diff --git a/cms/static/js/models/settings/course_details.js b/cms/static/js/models/settings/course_details.js
index 148df7a325..d41545cca9 100644
--- a/cms/static/js/models/settings/course_details.js
+++ b/cms/static/js/models/settings/course_details.js
@@ -37,6 +37,9 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
// Returns either nothing (no return call) so that validate works or an object of {field: errorstring} pairs
// A bit funny in that the video key validation is asynchronous; so, it won't stop the validation.
var errors = {};
+ if (newattrs.start_date === null) {
+ errors.start_date = "The course must have an assigned start date.";
+ }
if (newattrs.start_date && newattrs.end_date && newattrs.start_date >= newattrs.end_date) {
errors.end_date = "The course end date cannot be before the course start date.";
}
diff --git a/cms/static/js/views/settings/main_settings_view.js b/cms/static/js/views/settings/main_settings_view.js
index 9bd8feab8c..3e1690f0b6 100644
--- a/cms/static/js/views/settings/main_settings_view.js
+++ b/cms/static/js/views/settings/main_settings_view.js
@@ -101,6 +101,12 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
cacheModel.save(fieldName, newVal);
}
}
+ else {
+ // Clear date (note that this clears the time as well, as date and time are linked).
+ // Note also that the validation logic prevents us from clearing the start date
+ // (start date is required by the back end).
+ cacheModel.save(fieldName, null);
+ }
};
// instrument as date and time pickers
diff --git a/cms/static/js/views/validating_view.js b/cms/static/js/views/validating_view.js
index c3ea57fd20..3376e5fe9b 100644
--- a/cms/static/js/views/validating_view.js
+++ b/cms/static/js/views/validating_view.js
@@ -25,14 +25,7 @@ CMS.Views.ValidatingView = Backbone.View.extend({
for (var field in error) {
var ele = this.$el.find('#' + this.fieldToSelectorMap[field]);
this._cacheValidationErrors.push(ele);
- var inputElements = 'input, textarea';
- if ($(ele).is(inputElements)) {
- $(ele).addClass('error');
- }
- else {
- // put error on the contained inputs
- $(ele).find(inputElements).addClass('error');
- }
+ this.getInputElements(ele).addClass('error');
$(ele).parent().append(this.errorTemplate({message : error[field]}));
}
},
@@ -40,12 +33,8 @@ CMS.Views.ValidatingView = Backbone.View.extend({
clearValidationErrors : function() {
// error is object w/ fields and error strings
while (this._cacheValidationErrors.length > 0) {
- var ele = this._cacheValidationErrors.pop();
- if ($(ele).is('div')) {
- // put error on the contained inputs
- $(ele).find('input, textarea').removeClass('error');
- }
- else $(ele).removeClass('error');
+ var ele = this._cacheValidationErrors.pop();
+ this.getInputElements(ele).removeClass('error');
$(ele).nextAll('.message-error').remove();
}
},
@@ -68,5 +57,16 @@ CMS.Views.ValidatingView = Backbone.View.extend({
},
inputUnfocus : function(event) {
$("label[for='" + event.currentTarget.id + "']").removeClass("is-focused");
+ },
+
+ getInputElements: function(ele) {
+ var inputElements = 'input, textarea';
+ if ($(ele).is(inputElements)) {
+ return $(ele);
+ }
+ else {
+ // put error on the contained inputs
+ return $(ele).find(inputElements);
+ }
}
});
diff --git a/common/lib/capa/capa/templates/choicegroup.html b/common/lib/capa/capa/templates/choicegroup.html
index e1ff40b6a1..758e2ffba1 100644
--- a/common/lib/capa/capa/templates/choicegroup.html
+++ b/common/lib/capa/capa/templates/choicegroup.html
@@ -17,7 +17,7 @@
% for choice_id, choice_description in choices:
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index b1e5fa02c8..7999f8d6da 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -7,6 +7,8 @@ import requests
import time
from datetime import datetime
+import dateutil.parser
+
from xmodule.modulestore import Location
from xmodule.seq_module import SequenceDescriptor, SequenceModule
from xmodule.timeparse import parse_time
@@ -150,7 +152,7 @@ class CourseFields(object):
enrollment_end = Date(help="Date that enrollment for this class is closed", scope=Scope.settings)
start = Date(help="Start time when this module is visible", scope=Scope.settings)
end = Date(help="Date that this class ends", scope=Scope.settings)
- advertised_start = StringOrDate(help="Date that this course is advertised to start", scope=Scope.settings)
+ advertised_start = String(help="Date that this course is advertised to start", scope=Scope.settings)
grading_policy = Object(help="Grading policy definition for this class", scope=Scope.content)
show_calculator = Boolean(help="Whether to show the calculator in this course", default=False, scope=Scope.settings)
display_name = String(help="Display name for this module", scope=Scope.settings)
@@ -537,10 +539,12 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
announcement = self.announcement
if announcement is not None:
announcement = to_datetime(announcement)
- if self.advertised_start is None or isinstance(self.advertised_start, basestring):
+
+ try:
+ start = dateutil.parser.parse(self.advertised_start)
+ except (ValueError, AttributeError):
start = to_datetime(self.start)
- else:
- start = to_datetime(self.advertised_start)
+
now = to_datetime(time.gmtime())
return announcement, start, now
diff --git a/common/lib/xmodule/xmodule/fields.py b/common/lib/xmodule/xmodule/fields.py
index 99ead854ad..0abe850d68 100644
--- a/common/lib/xmodule/xmodule/fields.py
+++ b/common/lib/xmodule/xmodule/fields.py
@@ -23,6 +23,8 @@ class Date(ModelType):
"""
if field is None:
return field
+ elif field is "":
+ return None
elif isinstance(field, basestring):
d = dateutil.parser.parse(field)
return d.utctimetuple()
diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py
index f6fa98fc28..b76251bb99 100644
--- a/common/lib/xmodule/xmodule/modulestore/mongo.py
+++ b/common/lib/xmodule/xmodule/modulestore/mongo.py
@@ -366,6 +366,9 @@ class MongoModuleStore(ModuleStoreBase):
children.extend(item.get('definition', {}).get('children', []))
data[Location(item['location'])] = item
+ if depth == 0:
+ break;
+
# Load all children by id. See
# http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%24or
# for or-query syntax
diff --git a/common/lib/xmodule/xmodule/tests/test_course_module.py b/common/lib/xmodule/xmodule/tests/test_course_module.py
index 28095979ec..e1de8a1ed0 100644
--- a/common/lib/xmodule/xmodule/tests/test_course_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_course_module.py
@@ -89,18 +89,19 @@ class IsNewCourseTestCase(unittest.TestCase):
((day2, None, None), (day1, None, None), self.assertLess),
((day1, None, None), (day1, None, None), self.assertEqual),
- # Non-parseable advertised starts are ignored in preference
- # to actual starts
- ((day2, None, "Spring 2013"), (day1, None, "Fall 2012"), self.assertLess),
- ((day1, None, "Spring 2013"), (day1, None, "Fall 2012"), self.assertEqual),
+ # Non-parseable advertised starts are ignored in preference to actual starts
+ ((day2, None, "Spring"), (day1, None, "Fall"), self.assertLess),
+ ((day1, None, "Spring"), (day1, None, "Fall"), self.assertEqual),
+
+ # Partially parsable advertised starts should take priority over start dates
+ ((day2, None, "October 2013"), (day2, None, "October 2012"), self.assertLess),
+ ((day2, None, "October 2013"), (day1, None, "October 2013"), self.assertEqual),
# Parseable advertised starts take priority over start dates
((day1, None, day2), (day1, None, day1), self.assertLess),
((day2, None, day2), (day1, None, day2), self.assertEqual),
-
]
- data = []
for a, b, assertion in dates:
a_score = self.get_dummy_course(start=a[0], announcement=a[1], advertised_start=a[2]).sorting_score
b_score = self.get_dummy_course(start=b[0], announcement=b[1], advertised_start=b[2]).sorting_score
diff --git a/lms/djangoapps/courseware/features/problems.feature b/lms/djangoapps/courseware/features/problems.feature
index efeb338c45..dc8495af60 100644
--- a/lms/djangoapps/courseware/features/problems.feature
+++ b/lms/djangoapps/courseware/features/problems.feature
@@ -8,6 +8,7 @@ Feature: Answer problems
And I am viewing a "" problem
When I answer a "" problem "correctly"
Then My "" answer is marked "correct"
+ And The "" problem displays a "correct" answer
Examples:
| ProblemType |
@@ -25,6 +26,7 @@ Feature: Answer problems
And I am viewing a "" problem
When I answer a "" problem "incorrectly"
Then My "" answer is marked "incorrect"
+ And The "" problem displays a "incorrect" answer
Examples:
| ProblemType |
@@ -41,6 +43,7 @@ Feature: Answer problems
Given I am viewing a "" problem
When I check a problem
Then My "" answer is marked "incorrect"
+ And The "" problem displays a "blank" answer
Examples:
| ProblemType |
@@ -58,6 +61,7 @@ Feature: Answer problems
And I answer a "" problem "ly"
When I reset the problem
Then My "" answer is marked "unanswered"
+ And The "" problem displays a "blank" answer
Examples:
| ProblemType | Correctness |
diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py
index 6b2239c38b..d2d379a212 100644
--- a/lms/djangoapps/courseware/features/problems.py
+++ b/lms/djangoapps/courseware/features/problems.py
@@ -1,9 +1,14 @@
+'''
+Steps for problem.feature lettuce tests
+'''
+
+
from lettuce import world, step
from lettuce.django import django_url
import random
import textwrap
-import time
-from common import i_am_registered_for_the_course, TEST_SECTION_NAME, section_location
+from common import i_am_registered_for_the_course, \
+ TEST_SECTION_NAME, section_location
from capa.tests.response_xml_factory import OptionResponseXMLFactory, \
ChoiceResponseXMLFactory, MultipleChoiceResponseXMLFactory, \
StringResponseXMLFactory, NumericalResponseXMLFactory, \
@@ -26,7 +31,7 @@ PROBLEM_FACTORY_DICT = {
'kwargs': {
'question_text': 'The correct answer is Choice 3',
'choices': [False, False, True, False],
- 'choice_names': ['choice_1', 'choice_2', 'choice_3', 'choice_4']}},
+ 'choice_names': ['choice_0', 'choice_1', 'choice_2', 'choice_3']}},
'checkbox': {
'factory': ChoiceResponseXMLFactory(),
@@ -88,6 +93,9 @@ PROBLEM_FACTORY_DICT = {
def add_problem_to_course(course, problem_type):
+ '''
+ Add a problem to the course we have created using factories.
+ '''
assert(problem_type in PROBLEM_FACTORY_DICT)
@@ -98,11 +106,12 @@ def add_problem_to_course(course, problem_type):
# Create a problem item using our generated XML
# We set rerandomize=always in the metadata so that the "Reset" button
# will appear.
- problem_item = world.ItemFactory.create(parent_location=section_location(course),
- template="i4x://edx/templates/problem/Blank_Common_Problem",
- display_name=str(problem_type),
- data=problem_xml,
- metadata={'rerandomize': 'always'})
+ template_name = "i4x://edx/templates/problem/Blank_Common_Problem"
+ world.ItemFactory.create(parent_location=section_location(course),
+ template=template_name,
+ display_name=str(problem_type),
+ data=problem_xml,
+ metadata={'rerandomize': 'always'})
@step(u'I am viewing a "([^"]*)" problem')
@@ -152,9 +161,9 @@ def answer_problem(step, problem_type, correctness):
elif problem_type == "multiple choice":
if correctness == 'correct':
- inputfield('multiple choice', choice='choice_3').check()
- else:
inputfield('multiple choice', choice='choice_2').check()
+ else:
+ inputfield('multiple choice', choice='choice_1').check()
elif problem_type == "checkbox":
if correctness == 'correct':
@@ -164,11 +173,13 @@ def answer_problem(step, problem_type, correctness):
inputfield('checkbox', choice='choice_3').check()
elif problem_type == 'string':
- textvalue = 'correct string' if correctness == 'correct' else 'incorrect'
+ textvalue = 'correct string' if correctness == 'correct' \
+ else 'incorrect'
inputfield('string').fill(textvalue)
elif problem_type == 'numerical':
- textvalue = "pi + 1" if correctness == 'correct' else str(random.randint(-2, 2))
+ textvalue = "pi + 1" if correctness == 'correct' \
+ else str(random.randint(-2, 2))
inputfield('numerical').fill(textvalue)
elif problem_type == 'formula':
@@ -203,6 +214,67 @@ def answer_problem(step, problem_type, correctness):
check_problem(step)
+@step(u'The "([^"]*)" problem displays a "([^"]*)" answer')
+def assert_problem_has_answer(step, problem_type, answer_class):
+ '''
+ Assert that the problem is displaying a particular answer.
+ These correspond to the same correct/incorrect
+ answers we set in answer_problem()
+
+ We can also check that a problem has been left blank
+ by setting answer_class='blank'
+ '''
+ assert answer_class in ['correct', 'incorrect', 'blank']
+
+ if problem_type == "drop down":
+ if answer_class == 'blank':
+ assert world.browser.is_element_not_present_by_css('option[selected="true"]')
+ else:
+ actual = world.browser.find_by_css('option[selected="true"]').value
+ expected = 'Option 2' if answer_class == 'correct' else 'Option 3'
+ assert actual == expected
+
+ elif problem_type == "multiple choice":
+ if answer_class == 'correct':
+ assert_checked('multiple choice', ['choice_2'])
+ elif answer_class == 'incorrect':
+ assert_checked('multiple choice', ['choice_1'])
+ else:
+ assert_checked('multiple choice', [])
+
+ elif problem_type == "checkbox":
+ if answer_class == 'correct':
+ assert_checked('checkbox', ['choice_0', 'choice_2'])
+ elif answer_class == 'incorrect':
+ assert_checked('checkbox', ['choice_3'])
+ else:
+ assert_checked('checkbox', [])
+
+ elif problem_type == 'string':
+ if answer_class == 'blank':
+ expected = ''
+ else:
+ expected = 'correct string' if answer_class == 'correct' \
+ else 'incorrect'
+
+ assert_textfield('string', expected)
+
+ elif problem_type == 'formula':
+ if answer_class == 'blank':
+ expected = ''
+ else:
+ expected = "x^2+2*x+y" if answer_class == 'correct' else 'x^2'
+
+ assert_textfield('formula', expected)
+
+ else:
+ # The other response types use random data,
+ # which would be difficult to check
+ # We trade input value coverage in the other tests for
+ # input type coverage in this test.
+ pass
+
+
@step(u'I check a problem')
def check_problem(step):
world.css_click("input.check")
@@ -227,7 +299,7 @@ CORRECTNESS_SELECTORS = {
'string': ['div.correct'],
'numerical': ['div.correct'],
'formula': ['div.correct'],
- 'script': ['div.correct'],
+ 'script': ['div.correct'],
'code': ['span.correct']},
'incorrect': {'drop down': ['span.incorrect'],
@@ -247,12 +319,14 @@ CORRECTNESS_SELECTORS = {
'numerical': ['div.unanswered'],
'formula': ['div.unanswered'],
'script': ['div.unanswered'],
- 'code': ['span.unanswered'] }}
+ 'code': ['span.unanswered']}}
@step(u'My "([^"]*)" answer is marked "([^"]*)"')
def assert_answer_mark(step, problem_type, correctness):
- """ Assert that the expected answer mark is visible for a given problem type.
+ """
+ Assert that the expected answer mark is visible
+ for a given problem type.
*problem_type* is a string identifying the type of problem (e.g. 'drop down')
*correctness* is in ['correct', 'incorrect', 'unanswered']
@@ -274,6 +348,7 @@ def assert_answer_mark(step, problem_type, correctness):
# Expect that we found the expected selector
assert(has_expected)
+
def inputfield(problem_type, choice=None, input_num=1):
""" Return the element for *problem_type*.
For example, if problem_type is 'string', return
@@ -289,8 +364,32 @@ def inputfield(problem_type, choice=None, input_num=1):
base = "_choice_" if problem_type == "multiple choice" else "_"
sel = sel + base + str(choice)
+
# If the input element doesn't exist, fail immediately
assert(world.browser.is_element_present_by_css(sel, wait_time=4))
# Retrieve the input element
return world.browser.find_by_css(sel)
+
+
+def assert_checked(problem_type, choices):
+ '''
+ Assert that choice names given in *choices* are the only
+ ones checked.
+
+ Works for both radio and checkbox problems
+ '''
+
+ all_choices = ['choice_0', 'choice_1', 'choice_2', 'choice_3']
+ for this_choice in all_choices:
+ element = inputfield(problem_type, choice=this_choice)
+
+ if this_choice in choices:
+ assert element.checked
+ else:
+ assert not element.checked
+
+
+def assert_textfield(problem_type, expected_text, input_num=1):
+ element = inputfield(problem_type, input_num=input_num)
+ assert element.value == expected_text
diff --git a/lms/templates/accounts_login.html b/lms/templates/accounts_login.html
index 011ca643c6..db9cca2b22 100644
--- a/lms/templates/accounts_login.html
+++ b/lms/templates/accounts_login.html
@@ -10,7 +10,7 @@
left: 0;
margin: 100px auto;
top: 0;
- z-index: 200;
+ height:500px;
}
.login-box input[type=submit] {
@@ -18,75 +18,18 @@
height: auto !important;
}
-#lean_overlay {
- display: block;
- position: fixed;
- left: 0px;
- top: 0px;
- z-index: 100;
- width:100%;
- height:100%;
-}
%block>
-
-