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%; -} - +