Merge branch 'master' into feature/btalbot/studio-tenderwidget
This commit is contained in:
25
cms/djangoapps/contentstore/features/course-settings.feature
Normal file
25
cms/djangoapps/contentstore/features/course-settings.feature
Normal file
@@ -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
|
||||
163
cms/djangoapps/contentstore/features/course-settings.py
Normal file
163
cms/djangoapps/contentstore/features/course-settings.py
Normal file
@@ -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))
|
||||
@@ -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()
|
||||
|
||||
@@ -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.');
|
||||
|
||||
@@ -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.";
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
% for choice_id, choice_description in choices:
|
||||
<label for="input_${id}_${choice_id}"
|
||||
% if input_type == 'radio' and choice_id in value:
|
||||
% if input_type == 'radio' and choice_id == value:
|
||||
<%
|
||||
if status == 'correct':
|
||||
correctness = 'correct'
|
||||
@@ -32,8 +32,10 @@
|
||||
% endif
|
||||
>
|
||||
<input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" value="${choice_id}"
|
||||
% if choice_id in value:
|
||||
% if input_type == 'radio' and choice_id == value:
|
||||
checked="true"
|
||||
% elif input_type != 'radio' and choice_id in value:
|
||||
checked="true"
|
||||
% endif
|
||||
|
||||
/> ${choice_description} </label>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,6 +8,7 @@ Feature: Answer problems
|
||||
And I am viewing a "<ProblemType>" problem
|
||||
When I answer a "<ProblemType>" problem "correctly"
|
||||
Then My "<ProblemType>" answer is marked "correct"
|
||||
And The "<ProblemType>" problem displays a "correct" answer
|
||||
|
||||
Examples:
|
||||
| ProblemType |
|
||||
@@ -25,6 +26,7 @@ Feature: Answer problems
|
||||
And I am viewing a "<ProblemType>" problem
|
||||
When I answer a "<ProblemType>" problem "incorrectly"
|
||||
Then My "<ProblemType>" answer is marked "incorrect"
|
||||
And The "<ProblemType>" problem displays a "incorrect" answer
|
||||
|
||||
Examples:
|
||||
| ProblemType |
|
||||
@@ -41,6 +43,7 @@ Feature: Answer problems
|
||||
Given I am viewing a "<ProblemType>" problem
|
||||
When I check a problem
|
||||
Then My "<ProblemType>" answer is marked "incorrect"
|
||||
And The "<ProblemType>" problem displays a "blank" answer
|
||||
|
||||
Examples:
|
||||
| ProblemType |
|
||||
@@ -58,6 +61,7 @@ Feature: Answer problems
|
||||
And I answer a "<ProblemType>" problem "<Correctness>ly"
|
||||
When I reset the problem
|
||||
Then My "<ProblemType>" answer is marked "unanswered"
|
||||
And The "<ProblemType>" problem displays a "blank" answer
|
||||
|
||||
Examples:
|
||||
| ProblemType | Correctness |
|
||||
|
||||
@@ -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 <input> 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
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
</style>
|
||||
</%block>
|
||||
|
||||
<section id="login-modal" class="modal login-modal login-box">
|
||||
<div class="inner-wrapper">
|
||||
<header>
|
||||
<h2>Log In</h2>
|
||||
<hr>
|
||||
</header>
|
||||
|
||||
<form id="login_form" class="login_form" method="post" data-remote="true" action="/login">
|
||||
<label>E-mail</label>
|
||||
<input name="email" type="email">
|
||||
<label>Password</label>
|
||||
<input name="password" type="password">
|
||||
<label class="remember-me">
|
||||
<input name="remember" type="checkbox" value="true">
|
||||
Remember me
|
||||
</label>
|
||||
<div class="submit">
|
||||
<input name="submit" type="submit" value="Access My Courses">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<section class="login-extra">
|
||||
<p>
|
||||
<span>Not enrolled? <a href="#signup-modal" class="close-login" rel="leanModal">Sign up.</a></span>
|
||||
<a href="#forgot-password-modal" rel="leanModal" class="pwd-reset">Forgot password?</a>
|
||||
</p>
|
||||
% if settings.MITX_FEATURES.get('AUTH_USE_OPENID'):
|
||||
<p>
|
||||
<a href="${MITX_ROOT_URL}/openid/login/">login via openid</a>
|
||||
</p>
|
||||
% endif
|
||||
</section>
|
||||
|
||||
<div class="close-modal">
|
||||
<div class="inner">
|
||||
<p>✕</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class='login-box'></section>
|
||||
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
$(document).delegate('#login_form', 'ajax:success', function(data, json, xhr) {
|
||||
if(json.success) {
|
||||
next = getParameterByName('next');
|
||||
if(next) {
|
||||
location.href = next;
|
||||
} else {
|
||||
location.href = "${reverse('dashboard')}";
|
||||
}
|
||||
} else {
|
||||
if($('#login_error').length == 0) {
|
||||
$('#login_form').prepend('<div id="login_error" class="modal-form-error"></div>');
|
||||
}
|
||||
$('#login_error').html(json.value).stop().css("display", "block");
|
||||
$(document).ready(
|
||||
function() {
|
||||
// show dialog
|
||||
$('#login').click();
|
||||
}
|
||||
});
|
||||
);
|
||||
})(this)
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user