Commit 4f7e09e9 by Brian Talbot

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

parents 7e56e98c f70511eb
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
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) ...@@ -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['default']['OPTIONS']['fs_root'] = path('common/test/data')
TEST_DATA_MODULESTORE['direct']['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) @override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
class ContentStoreToyCourseTest(ModuleStoreTestCase): class ContentStoreToyCourseTest(ModuleStoreTestCase):
...@@ -145,8 +153,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -145,8 +153,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# make sure the parent no longer points to the child object which was deleted # make sure the parent no longer points to the child object which was deleted
self.assertFalse(sequential.location.url() in chapter.children) self.assertFalse(sequential.location.url() in chapter.children)
def test_about_overrides(self): def test_about_overrides(self):
''' '''
This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html
...@@ -307,6 +313,28 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -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 # 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') 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): def test_export_course_with_unknown_metadata(self):
module_store = modulestore('direct') module_store = modulestore('direct')
content_store = contentstore() content_store = contentstore()
......
...@@ -251,7 +251,7 @@ function getEdxTimeFromDateTimeVals(date_val, time_val, format) { ...@@ -251,7 +251,7 @@ function getEdxTimeFromDateTimeVals(date_val, time_val, format) {
time_val = '00:00'; time_val = '00:00';
// Note, we are using date.js utility which has better parsing abilities than the built in JS date parsing // 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) if (format == null)
format = 'yyyy-MM-ddTHH:mm'; format = 'yyyy-MM-ddTHH:mm';
...@@ -269,6 +269,7 @@ function getEdxTimeFromDateTimeInputs(date_id, time_id, format) { ...@@ -269,6 +269,7 @@ function getEdxTimeFromDateTimeInputs(date_id, time_id, format) {
} }
function autosaveInput(e) { function autosaveInput(e) {
var self = this;
if (this.saveTimer) { if (this.saveTimer) {
clearTimeout(this.saveTimer); clearTimeout(this.saveTimer);
} }
...@@ -276,7 +277,7 @@ function autosaveInput(e) { ...@@ -276,7 +277,7 @@ function autosaveInput(e) {
this.saveTimer = setTimeout(function () { this.saveTimer = setTimeout(function () {
$changedInput = $(e.target); $changedInput = $(e.target);
saveSubsection(); saveSubsection();
this.saveTimer = null; self.saveTimer = null;
}, 500); }, 500);
} }
...@@ -318,6 +319,7 @@ function saveSubsection() { ...@@ -318,6 +319,7 @@ function saveSubsection() {
data: JSON.stringify({ 'id': id, 'metadata': metadata}), data: JSON.stringify({ 'id': id, 'metadata': metadata}),
success: function () { success: function () {
$spinner.delay(500).fadeOut(150); $spinner.delay(500).fadeOut(150);
$changedInput = null;
}, },
error: function () { error: function () {
showToastMessage('There has been an error while saving your changes.'); showToastMessage('There has been an error while saving your changes.');
......
...@@ -37,6 +37,9 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({ ...@@ -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 // 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. // A bit funny in that the video key validation is asynchronous; so, it won't stop the validation.
var errors = {}; 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) { 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."; 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({ ...@@ -101,6 +101,12 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
cacheModel.save(fieldName, newVal); 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 // instrument as date and time pickers
......
...@@ -25,14 +25,7 @@ CMS.Views.ValidatingView = Backbone.View.extend({ ...@@ -25,14 +25,7 @@ CMS.Views.ValidatingView = Backbone.View.extend({
for (var field in error) { for (var field in error) {
var ele = this.$el.find('#' + this.fieldToSelectorMap[field]); var ele = this.$el.find('#' + this.fieldToSelectorMap[field]);
this._cacheValidationErrors.push(ele); this._cacheValidationErrors.push(ele);
var inputElements = 'input, textarea'; this.getInputElements(ele).addClass('error');
if ($(ele).is(inputElements)) {
$(ele).addClass('error');
}
else {
// put error on the contained inputs
$(ele).find(inputElements).addClass('error');
}
$(ele).parent().append(this.errorTemplate({message : error[field]})); $(ele).parent().append(this.errorTemplate({message : error[field]}));
} }
}, },
...@@ -41,11 +34,7 @@ CMS.Views.ValidatingView = Backbone.View.extend({ ...@@ -41,11 +34,7 @@ CMS.Views.ValidatingView = Backbone.View.extend({
// error is object w/ fields and error strings // error is object w/ fields and error strings
while (this._cacheValidationErrors.length > 0) { while (this._cacheValidationErrors.length > 0) {
var ele = this._cacheValidationErrors.pop(); var ele = this._cacheValidationErrors.pop();
if ($(ele).is('div')) { this.getInputElements(ele).removeClass('error');
// put error on the contained inputs
$(ele).find('input, textarea').removeClass('error');
}
else $(ele).removeClass('error');
$(ele).nextAll('.message-error').remove(); $(ele).nextAll('.message-error').remove();
} }
}, },
...@@ -68,5 +57,16 @@ CMS.Views.ValidatingView = Backbone.View.extend({ ...@@ -68,5 +57,16 @@ CMS.Views.ValidatingView = Backbone.View.extend({
}, },
inputUnfocus : function(event) { inputUnfocus : function(event) {
$("label[for='" + event.currentTarget.id + "']").removeClass("is-focused"); $("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 @@ ...@@ -17,7 +17,7 @@
% for choice_id, choice_description in choices: % for choice_id, choice_description in choices:
<label for="input_${id}_${choice_id}" <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': if status == 'correct':
correctness = 'correct' correctness = 'correct'
...@@ -32,7 +32,9 @@ ...@@ -32,7 +32,9 @@
% endif % endif
> >
<input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" value="${choice_id}" <input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" value="${choice_id}"
% if choice_id in value: % if input_type == 'radio' and choice_id == value:
checked="true"
% elif input_type != 'radio' and choice_id in value:
checked="true" checked="true"
% endif % endif
......
...@@ -7,6 +7,8 @@ import requests ...@@ -7,6 +7,8 @@ import requests
import time import time
from datetime import datetime from datetime import datetime
import dateutil.parser
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.seq_module import SequenceDescriptor, SequenceModule from xmodule.seq_module import SequenceDescriptor, SequenceModule
from xmodule.timeparse import parse_time from xmodule.timeparse import parse_time
...@@ -150,7 +152,7 @@ class CourseFields(object): ...@@ -150,7 +152,7 @@ class CourseFields(object):
enrollment_end = Date(help="Date that enrollment for this class is closed", scope=Scope.settings) 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) start = Date(help="Start time when this module is visible", scope=Scope.settings)
end = Date(help="Date that this class ends", 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) 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) 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) display_name = String(help="Display name for this module", scope=Scope.settings)
...@@ -537,10 +539,12 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -537,10 +539,12 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
announcement = self.announcement announcement = self.announcement
if announcement is not None: if announcement is not None:
announcement = to_datetime(announcement) 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) start = to_datetime(self.start)
else:
start = to_datetime(self.advertised_start)
now = to_datetime(time.gmtime()) now = to_datetime(time.gmtime())
return announcement, start, now return announcement, start, now
......
...@@ -23,6 +23,8 @@ class Date(ModelType): ...@@ -23,6 +23,8 @@ class Date(ModelType):
""" """
if field is None: if field is None:
return field return field
elif field is "":
return None
elif isinstance(field, basestring): elif isinstance(field, basestring):
d = dateutil.parser.parse(field) d = dateutil.parser.parse(field)
return d.utctimetuple() return d.utctimetuple()
......
...@@ -366,6 +366,9 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -366,6 +366,9 @@ class MongoModuleStore(ModuleStoreBase):
children.extend(item.get('definition', {}).get('children', [])) children.extend(item.get('definition', {}).get('children', []))
data[Location(item['location'])] = item data[Location(item['location'])] = item
if depth == 0:
break;
# Load all children by id. See # Load all children by id. See
# http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%24or # http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%24or
# for or-query syntax # for or-query syntax
......
...@@ -89,18 +89,19 @@ class IsNewCourseTestCase(unittest.TestCase): ...@@ -89,18 +89,19 @@ class IsNewCourseTestCase(unittest.TestCase):
((day2, None, None), (day1, None, None), self.assertLess), ((day2, None, None), (day1, None, None), self.assertLess),
((day1, None, None), (day1, None, None), self.assertEqual), ((day1, None, None), (day1, None, None), self.assertEqual),
# Non-parseable advertised starts are ignored in preference # Non-parseable advertised starts are ignored in preference to actual starts
# to actual starts ((day2, None, "Spring"), (day1, None, "Fall"), self.assertLess),
((day2, None, "Spring 2013"), (day1, None, "Fall 2012"), self.assertLess), ((day1, None, "Spring"), (day1, None, "Fall"), self.assertEqual),
((day1, None, "Spring 2013"), (day1, None, "Fall 2012"), 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 # Parseable advertised starts take priority over start dates
((day1, None, day2), (day1, None, day1), self.assertLess), ((day1, None, day2), (day1, None, day1), self.assertLess),
((day2, None, day2), (day1, None, day2), self.assertEqual), ((day2, None, day2), (day1, None, day2), self.assertEqual),
] ]
data = []
for a, b, assertion in dates: for a, b, assertion in dates:
a_score = self.get_dummy_course(start=a[0], announcement=a[1], advertised_start=a[2]).sorting_score 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 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 ...@@ -8,6 +8,7 @@ Feature: Answer problems
And I am viewing a "<ProblemType>" problem And I am viewing a "<ProblemType>" problem
When I answer a "<ProblemType>" problem "correctly" When I answer a "<ProblemType>" problem "correctly"
Then My "<ProblemType>" answer is marked "correct" Then My "<ProblemType>" answer is marked "correct"
And The "<ProblemType>" problem displays a "correct" answer
Examples: Examples:
| ProblemType | | ProblemType |
...@@ -25,6 +26,7 @@ Feature: Answer problems ...@@ -25,6 +26,7 @@ Feature: Answer problems
And I am viewing a "<ProblemType>" problem And I am viewing a "<ProblemType>" problem
When I answer a "<ProblemType>" problem "incorrectly" When I answer a "<ProblemType>" problem "incorrectly"
Then My "<ProblemType>" answer is marked "incorrect" Then My "<ProblemType>" answer is marked "incorrect"
And The "<ProblemType>" problem displays a "incorrect" answer
Examples: Examples:
| ProblemType | | ProblemType |
...@@ -41,6 +43,7 @@ Feature: Answer problems ...@@ -41,6 +43,7 @@ Feature: Answer problems
Given I am viewing a "<ProblemType>" problem Given I am viewing a "<ProblemType>" problem
When I check a problem When I check a problem
Then My "<ProblemType>" answer is marked "incorrect" Then My "<ProblemType>" answer is marked "incorrect"
And The "<ProblemType>" problem displays a "blank" answer
Examples: Examples:
| ProblemType | | ProblemType |
...@@ -58,6 +61,7 @@ Feature: Answer problems ...@@ -58,6 +61,7 @@ Feature: Answer problems
And I answer a "<ProblemType>" problem "<Correctness>ly" And I answer a "<ProblemType>" problem "<Correctness>ly"
When I reset the problem When I reset the problem
Then My "<ProblemType>" answer is marked "unanswered" Then My "<ProblemType>" answer is marked "unanswered"
And The "<ProblemType>" problem displays a "blank" answer
Examples: Examples:
| ProblemType | Correctness | | ProblemType | Correctness |
......
'''
Steps for problem.feature lettuce tests
'''
from lettuce import world, step from lettuce import world, step
from lettuce.django import django_url from lettuce.django import django_url
import random import random
import textwrap import textwrap
import time from common import i_am_registered_for_the_course, \
from common import i_am_registered_for_the_course, TEST_SECTION_NAME, section_location TEST_SECTION_NAME, section_location
from capa.tests.response_xml_factory import OptionResponseXMLFactory, \ from capa.tests.response_xml_factory import OptionResponseXMLFactory, \
ChoiceResponseXMLFactory, MultipleChoiceResponseXMLFactory, \ ChoiceResponseXMLFactory, MultipleChoiceResponseXMLFactory, \
StringResponseXMLFactory, NumericalResponseXMLFactory, \ StringResponseXMLFactory, NumericalResponseXMLFactory, \
...@@ -26,7 +31,7 @@ PROBLEM_FACTORY_DICT = { ...@@ -26,7 +31,7 @@ PROBLEM_FACTORY_DICT = {
'kwargs': { 'kwargs': {
'question_text': 'The correct answer is Choice 3', 'question_text': 'The correct answer is Choice 3',
'choices': [False, False, True, False], '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': { 'checkbox': {
'factory': ChoiceResponseXMLFactory(), 'factory': ChoiceResponseXMLFactory(),
...@@ -88,6 +93,9 @@ PROBLEM_FACTORY_DICT = { ...@@ -88,6 +93,9 @@ PROBLEM_FACTORY_DICT = {
def add_problem_to_course(course, problem_type): 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) assert(problem_type in PROBLEM_FACTORY_DICT)
...@@ -98,8 +106,9 @@ def add_problem_to_course(course, problem_type): ...@@ -98,8 +106,9 @@ def add_problem_to_course(course, problem_type):
# Create a problem item using our generated XML # Create a problem item using our generated XML
# We set rerandomize=always in the metadata so that the "Reset" button # We set rerandomize=always in the metadata so that the "Reset" button
# will appear. # will appear.
problem_item = world.ItemFactory.create(parent_location=section_location(course), template_name = "i4x://edx/templates/problem/Blank_Common_Problem"
template="i4x://edx/templates/problem/Blank_Common_Problem", world.ItemFactory.create(parent_location=section_location(course),
template=template_name,
display_name=str(problem_type), display_name=str(problem_type),
data=problem_xml, data=problem_xml,
metadata={'rerandomize': 'always'}) metadata={'rerandomize': 'always'})
...@@ -152,9 +161,9 @@ def answer_problem(step, problem_type, correctness): ...@@ -152,9 +161,9 @@ def answer_problem(step, problem_type, correctness):
elif problem_type == "multiple choice": elif problem_type == "multiple choice":
if correctness == 'correct': if correctness == 'correct':
inputfield('multiple choice', choice='choice_3').check()
else:
inputfield('multiple choice', choice='choice_2').check() inputfield('multiple choice', choice='choice_2').check()
else:
inputfield('multiple choice', choice='choice_1').check()
elif problem_type == "checkbox": elif problem_type == "checkbox":
if correctness == 'correct': if correctness == 'correct':
...@@ -164,11 +173,13 @@ def answer_problem(step, problem_type, correctness): ...@@ -164,11 +173,13 @@ def answer_problem(step, problem_type, correctness):
inputfield('checkbox', choice='choice_3').check() inputfield('checkbox', choice='choice_3').check()
elif problem_type == 'string': elif problem_type == 'string':
textvalue = 'correct string' if correctness == 'correct' else 'incorrect' textvalue = 'correct string' if correctness == 'correct' \
else 'incorrect'
inputfield('string').fill(textvalue) inputfield('string').fill(textvalue)
elif problem_type == 'numerical': 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) inputfield('numerical').fill(textvalue)
elif problem_type == 'formula': elif problem_type == 'formula':
...@@ -203,6 +214,67 @@ def answer_problem(step, problem_type, correctness): ...@@ -203,6 +214,67 @@ def answer_problem(step, problem_type, correctness):
check_problem(step) 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') @step(u'I check a problem')
def check_problem(step): def check_problem(step):
world.css_click("input.check") world.css_click("input.check")
...@@ -247,12 +319,14 @@ CORRECTNESS_SELECTORS = { ...@@ -247,12 +319,14 @@ CORRECTNESS_SELECTORS = {
'numerical': ['div.unanswered'], 'numerical': ['div.unanswered'],
'formula': ['div.unanswered'], 'formula': ['div.unanswered'],
'script': ['div.unanswered'], 'script': ['div.unanswered'],
'code': ['span.unanswered'] }} 'code': ['span.unanswered']}}
@step(u'My "([^"]*)" answer is marked "([^"]*)"') @step(u'My "([^"]*)" answer is marked "([^"]*)"')
def assert_answer_mark(step, problem_type, correctness): 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') *problem_type* is a string identifying the type of problem (e.g. 'drop down')
*correctness* is in ['correct', 'incorrect', 'unanswered'] *correctness* is in ['correct', 'incorrect', 'unanswered']
...@@ -274,6 +348,7 @@ def assert_answer_mark(step, problem_type, correctness): ...@@ -274,6 +348,7 @@ def assert_answer_mark(step, problem_type, correctness):
# Expect that we found the expected selector # Expect that we found the expected selector
assert(has_expected) assert(has_expected)
def inputfield(problem_type, choice=None, input_num=1): def inputfield(problem_type, choice=None, input_num=1):
""" Return the <input> element for *problem_type*. """ Return the <input> element for *problem_type*.
For example, if problem_type is 'string', return For example, if problem_type is 'string', return
...@@ -289,8 +364,32 @@ def inputfield(problem_type, choice=None, input_num=1): ...@@ -289,8 +364,32 @@ def inputfield(problem_type, choice=None, input_num=1):
base = "_choice_" if problem_type == "multiple choice" else "_" base = "_choice_" if problem_type == "multiple choice" else "_"
sel = sel + base + str(choice) sel = sel + base + str(choice)
# If the input element doesn't exist, fail immediately # If the input element doesn't exist, fail immediately
assert(world.browser.is_element_present_by_css(sel, wait_time=4)) assert(world.browser.is_element_present_by_css(sel, wait_time=4))
# Retrieve the input element # Retrieve the input element
return world.browser.find_by_css(sel) 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 @@ ...@@ -10,7 +10,7 @@
left: 0; left: 0;
margin: 100px auto; margin: 100px auto;
top: 0; top: 0;
z-index: 200; height:500px;
} }
.login-box input[type=submit] { .login-box input[type=submit] {
...@@ -18,75 +18,18 @@ ...@@ -18,75 +18,18 @@
height: auto !important; height: auto !important;
} }
#lean_overlay {
display: block;
position: fixed;
left: 0px;
top: 0px;
z-index: 100;
width:100%;
height:100%;
}
</style> </style>
</%block> </%block>
<section id="login-modal" class="modal login-modal login-box"> <section class='login-box'></section>
<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>&#10005;</p>
</div>
</div>
</div>
</section>
<script type="text/javascript"> <script type="text/javascript">
(function() { (function() {
$(document).delegate('#login_form', 'ajax:success', function(data, json, xhr) { $(document).ready(
if(json.success) { function() {
next = getParameterByName('next'); // show dialog
if(next) { $('#login').click();
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");
} }
}); );
})(this) })(this)
</script> </script>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment