Commit e03b3914 by Eugeny Kolpakov Committed by GitHub

Merge pull request #14737 from open-craft/mit-capa-hide-correct

MIT CAPA improvements: Add show_correctness field
parents 529df7c6 f18a2be8
...@@ -1163,6 +1163,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F ...@@ -1163,6 +1163,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
'explanatory_message': explanatory_message, 'explanatory_message': explanatory_message,
'group_access': xblock.group_access, 'group_access': xblock.group_access,
'user_partitions': user_partitions, 'user_partitions': user_partitions,
'show_correctness': xblock.show_correctness,
}) })
if xblock.category == 'sequential': if xblock.category == 'sequential':
......
...@@ -54,6 +54,7 @@ class CourseMetadata(object): ...@@ -54,6 +54,7 @@ class CourseMetadata(object):
'exam_review_rules', 'exam_review_rules',
'hide_after_due', 'hide_after_due',
'self_paced', 'self_paced',
'show_correctness',
'chrome', 'chrome',
'default_tab', 'default_tab',
] ]
......
...@@ -4,12 +4,12 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j ...@@ -4,12 +4,12 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
function($, AjaxHelpers, ViewUtils, CourseOutlinePage, XBlockOutlineInfo, DateUtils, function($, AjaxHelpers, ViewUtils, CourseOutlinePage, XBlockOutlineInfo, DateUtils,
EditHelpers, TemplateHelpers, Course) { EditHelpers, TemplateHelpers, Course) {
describe('CourseOutlinePage', function() { describe('CourseOutlinePage', function() {
var createCourseOutlinePage, displayNameInput, model, outlinePage, requests, var createCourseOutlinePage, displayNameInput, model, outlinePage, requests, getItemsOfType, getItemHeaders,
getItemsOfType, getItemHeaders, verifyItemsExpanded, expandItemsAndVerifyState, verifyItemsExpanded, expandItemsAndVerifyState, collapseItemsAndVerifyState, selectBasicSettings,
collapseItemsAndVerifyState, selectBasicSettings, selectAdvancedSettings, createMockCourseJSON, selectVisibilitySettings, selectAdvancedSettings, createMockCourseJSON, createMockSectionJSON,
createMockSectionJSON, createMockSubsectionJSON, verifyTypePublishable, mockCourseJSON, createMockSubsectionJSON, verifyTypePublishable, mockCourseJSON, mockEmptyCourseJSON,
mockEmptyCourseJSON, mockSingleSectionCourseJSON, createMockVerticalJSON, createMockIndexJSON, mockSingleSectionCourseJSON, createMockVerticalJSON, createMockIndexJSON, mockCourseEntranceExamJSON,
mockCourseEntranceExamJSON, mockOutlinePage = readFixtures('mock/mock-course-outline-page.underscore'), mockOutlinePage = readFixtures('mock/mock-course-outline-page.underscore'),
mockRerunNotification = readFixtures('mock/mock-course-rerun-notification.underscore'); mockRerunNotification = readFixtures('mock/mock-course-rerun-notification.underscore');
createMockCourseJSON = function(options, children) { createMockCourseJSON = function(options, children) {
...@@ -71,6 +71,7 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j ...@@ -71,6 +71,7 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
prereqs: [], prereqs: [],
prereq: '', prereq: '',
prereq_min_score: '', prereq_min_score: '',
show_correctness: 'always',
child_info: { child_info: {
category: 'vertical', category: 'vertical',
display_name: 'Unit', display_name: 'Unit',
...@@ -140,6 +141,10 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j ...@@ -140,6 +141,10 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
this.$(".modal-section .settings-tab-button[data-tab='basic']").click(); this.$(".modal-section .settings-tab-button[data-tab='basic']").click();
}; };
selectVisibilitySettings = function() {
this.$(".modal-section .settings-tab-button[data-tab='visibility']").click();
};
selectAdvancedSettings = function() { selectAdvancedSettings = function() {
this.$(".modal-section .settings-tab-button[data-tab='advanced']").click(); this.$(".modal-section .settings-tab-button[data-tab='advanced']").click();
}; };
...@@ -238,7 +243,7 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j ...@@ -238,7 +243,7 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
'basic-modal', 'course-outline-modal', 'release-date-editor', 'basic-modal', 'course-outline-modal', 'release-date-editor',
'due-date-editor', 'grading-editor', 'publish-editor', 'due-date-editor', 'grading-editor', 'publish-editor',
'staff-lock-editor', 'content-visibility-editor', 'settings-modal-tabs', 'staff-lock-editor', 'content-visibility-editor', 'settings-modal-tabs',
'timed-examination-preference-editor', 'access-editor' 'timed-examination-preference-editor', 'access-editor', 'show-correctness-editor'
]); ]);
appendSetFixtures(mockOutlinePage); appendSetFixtures(mockOutlinePage);
mockCourseJSON = createMockCourseJSON({}, [ mockCourseJSON = createMockCourseJSON({}, [
...@@ -604,8 +609,8 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j ...@@ -604,8 +609,8 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
expect($('due_date')).not.toExist(); expect($('due_date')).not.toExist();
expect($('grading_format')).not.toExist(); expect($('grading_format')).not.toExist();
// Staff lock controls are always visible on the advanced tab // Staff lock controls are always visible on the visibility tab
selectAdvancedSettings(); selectVisibilitySettings();
expect($('#staff_lock')).toExist(); expect($('#staff_lock')).toExist();
selectBasicSettings(); selectBasicSettings();
$('.wrapper-modal-window .action-save').click(); $('.wrapper-modal-window .action-save').click();
...@@ -678,7 +683,8 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j ...@@ -678,7 +683,8 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
describe('Subsection', function() { describe('Subsection', function() {
var getDisplayNameWrapper, setEditModalValues, setContentVisibility, mockServerValuesJson, var getDisplayNameWrapper, setEditModalValues, setContentVisibility, mockServerValuesJson,
selectDisableSpecialExams, selectTimedExam, selectProctoredExam, selectPracticeExam, selectDisableSpecialExams, selectTimedExam, selectProctoredExam, selectPracticeExam,
selectPrerequisite, selectLastPrerequisiteSubsection, checkOptionFieldVisibility; selectPrerequisite, selectLastPrerequisiteSubsection, checkOptionFieldVisibility,
defaultModalSettings, getMockNoPrereqOrExamsCourseJSON, expectShowCorrectness;
getDisplayNameWrapper = function() { getDisplayNameWrapper = function() {
return getItemHeaders('subsection').find('.wrapper-xblock-field'); return getItemHeaders('subsection').find('.wrapper-xblock-field');
...@@ -732,6 +738,38 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j ...@@ -732,6 +738,38 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
expect($('.field-exam-review-rules').is(':visible')).toBe(review_rules); expect($('.field-exam-review-rules').is(':visible')).toBe(review_rules);
}; };
expectShowCorrectness = function(showCorrectness) {
expect($('input[name=show-correctness][value=' + showCorrectness + ']').is(':checked')).toBe(true);
};
getMockNoPrereqOrExamsCourseJSON = function() {
var mockVerticalJSON = createMockVerticalJSON({}, []);
var mockSubsectionJSON = createMockSubsectionJSON({}, [mockVerticalJSON]);
delete mockSubsectionJSON.is_prereq;
delete mockSubsectionJSON.prereqs;
delete mockSubsectionJSON.prereq;
delete mockSubsectionJSON.prereq_min_score;
return createMockCourseJSON({
enable_proctored_exams: false,
enable_timed_exams: false
}, [
createMockSectionJSON({}, [mockSubsectionJSON])
]);
};
defaultModalSettings = {
graderType: 'notgraded',
isPrereq: false,
metadata: {
due: null,
is_practice_exam: false,
is_time_limited: false,
exam_review_rules: '',
is_proctored_enabled: false,
default_time_limit_minutes: null
}
};
// Contains hard-coded dates because dates are presented in different formats. // Contains hard-coded dates because dates are presented in different formats.
mockServerValuesJson = createMockSectionJSON({ mockServerValuesJson = createMockSectionJSON({
release_date: 'Jan 01, 2970 at 05:00 UTC' release_date: 'Jan 01, 2970 at 05:00 UTC'
...@@ -746,6 +784,7 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j ...@@ -746,6 +784,7 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
has_explicit_staff_lock: true, has_explicit_staff_lock: true,
staff_only_message: true, staff_only_message: true,
is_prereq: false, is_prereq: false,
show_correctness: 'never',
'is_time_limited': true, 'is_time_limited': true,
'is_practice_exam': false, 'is_practice_exam': false,
'is_proctored_exam': false, 'is_proctored_exam': false,
...@@ -821,36 +860,45 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j ...@@ -821,36 +860,45 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
expandItemsAndVerifyState('subsection'); expandItemsAndVerifyState('subsection');
}); });
it('can show basic settings', function() { it('subsection can show basic settings', function() {
createCourseOutlinePage(this, mockCourseJSON, false); createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click(); outlinePage.$('.outline-subsection .configure-button').click();
selectBasicSettings(); selectBasicSettings();
expect($('.modal-section .settings-tab-button[data-tab="basic"]')).toHaveClass('active'); expect($('.modal-section .settings-tab-button[data-tab="basic"]')).toHaveClass('active');
expect($('.modal-section .settings-tab-button[data-tab="visibility"]')).not.toHaveClass('active');
expect($('.modal-section .settings-tab-button[data-tab="advanced"]')).not.toHaveClass('active'); expect($('.modal-section .settings-tab-button[data-tab="advanced"]')).not.toHaveClass('active');
}); });
it('can show advanced settings', function() { it('subsection can show visibility settings', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectVisibilitySettings();
expect($('.modal-section .settings-tab-button[data-tab="basic"]')).not.toHaveClass('active');
expect($('.modal-section .settings-tab-button[data-tab="visibility"]')).toHaveClass('active');
expect($('.modal-section .settings-tab-button[data-tab="advanced"]')).not.toHaveClass('active');
});
it('subsection can show advanced settings', function() {
createCourseOutlinePage(this, mockCourseJSON, false); createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click(); outlinePage.$('.outline-subsection .configure-button').click();
selectAdvancedSettings(); selectAdvancedSettings();
expect($('.modal-section .settings-tab-button[data-tab="basic"]')).not.toHaveClass('active'); expect($('.modal-section .settings-tab-button[data-tab="basic"]')).not.toHaveClass('active');
expect($('.modal-section .settings-tab-button[data-tab="visibility"]')).not.toHaveClass('active');
expect($('.modal-section .settings-tab-button[data-tab="advanced"]')).toHaveClass('active'); expect($('.modal-section .settings-tab-button[data-tab="advanced"]')).toHaveClass('active');
}); });
it('does not show settings tab headers if there is only one tab to show', function() { it('subsection does not show advanced settings tab if no special exams or prerequisites', function() {
var mockVerticalJSON = createMockVerticalJSON({}, []); var mockNoPrereqCourseJSON = getMockNoPrereqOrExamsCourseJSON();
var mockSubsectionJSON = createMockSubsectionJSON({}, [mockVerticalJSON]); createCourseOutlinePage(this, mockNoPrereqCourseJSON, false);
delete mockSubsectionJSON.is_prereq; outlinePage.$('.outline-subsection .configure-button').click();
delete mockSubsectionJSON.prereqs; expect($('.modal-section .settings-tab-button[data-tab="basic"]')).toExist();
delete mockSubsectionJSON.prereq; expect($('.modal-section .settings-tab-button[data-tab="visibility"]')).toExist();
delete mockSubsectionJSON.prereq_min_score; expect($('.modal-section .settings-tab-button[data-tab="advanced"]')).not.toExist();
var mockCourseJSON = createMockCourseJSON({ });
enable_proctored_exams: false,
enable_timed_exams: false it('unit does not show settings tab headers if there is only one tab to show', function() {
}, [ var mockNoPrereqCourseJSON = getMockNoPrereqOrExamsCourseJSON();
createMockSectionJSON({}, [mockSubsectionJSON]) createCourseOutlinePage(this, mockNoPrereqCourseJSON, false);
]);
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-unit .configure-button').click(); outlinePage.$('.outline-unit .configure-button').click();
expect($('.settings-tabs-header').length).toBe(0); expect($('.settings-tabs-header').length).toBe(0);
}); });
...@@ -869,6 +917,7 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j ...@@ -869,6 +917,7 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
expect($('.grading-due-date').length).toBe(0); expect($('.grading-due-date').length).toBe(0);
expect($('.edit-settings-grading').length).toBe(1); expect($('.edit-settings-grading').length).toBe(1);
expect($('.edit-content-visibility').length).toBe(1); expect($('.edit-content-visibility').length).toBe(1);
expect($('.edit-show-correctness').length).toBe(1);
}); });
it('can select valid time', function() { it('can select valid time', function() {
...@@ -894,6 +943,14 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j ...@@ -894,6 +943,14 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
} }
}); });
it('can be saved', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
$('.wrapper-modal-window .action-save').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection', defaultModalSettings);
expect(requests[0].requestHeaders['X-HTTP-Method-Override']).toBe('PATCH');
});
it('can be edited', function() { it('can be edited', function() {
createCourseOutlinePage(this, mockCourseJSON, false); createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click(); outlinePage.$('.outline-subsection .configure-button').click();
...@@ -948,15 +1005,17 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j ...@@ -948,15 +1005,17 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
expect($('input.no_special_exam').is(':checked')).toBe(false); expect($('input.no_special_exam').is(':checked')).toBe(false);
expect($('input.practice_exam').is(':checked')).toBe(false); expect($('input.practice_exam').is(':checked')).toBe(false);
expect($('.field-time-limit input').val()).toBe('02:30'); expect($('.field-time-limit input').val()).toBe('02:30');
expectShowCorrectness('never');
}); });
it('can hide time limit and hide after due fields when the None radio box is selected', function() { it('can hide time limit and hide after due fields when the None radio box is selected', function() {
createCourseOutlinePage(this, mockCourseJSON, false); createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click(); outlinePage.$('.outline-subsection .configure-button').click();
setEditModalValues('7/9/2014', '7/10/2014', 'Lab'); setEditModalValues('7/9/2014', '7/10/2014', 'Lab');
selectVisibilitySettings();
setContentVisibility('staff_only');
selectAdvancedSettings(); selectAdvancedSettings();
selectDisableSpecialExams(); selectDisableSpecialExams();
setContentVisibility('staff_only');
// all additional options should be hidden // all additional options should be hidden
expect($('.exam-options').is(':hidden')).toBe(true); expect($('.exam-options').is(':hidden')).toBe(true);
...@@ -966,9 +1025,10 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j ...@@ -966,9 +1025,10 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
createCourseOutlinePage(this, mockCourseJSON, false); createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click(); outlinePage.$('.outline-subsection .configure-button').click();
setEditModalValues('7/9/2014', '7/10/2014', 'Lab'); setEditModalValues('7/9/2014', '7/10/2014', 'Lab');
selectVisibilitySettings();
setContentVisibility('staff_only');
selectAdvancedSettings(); selectAdvancedSettings();
selectPracticeExam('00:30'); selectPracticeExam('00:30');
setContentVisibility('staff_only');
// time limit should be visible, review rules should be hidden // time limit should be visible, review rules should be hidden
checkOptionFieldVisibility(true, false); checkOptionFieldVisibility(true, false);
...@@ -993,9 +1053,10 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j ...@@ -993,9 +1053,10 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
createCourseOutlinePage(this, mockCourseJSON, false); createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click(); outlinePage.$('.outline-subsection .configure-button').click();
setEditModalValues('7/9/2014', '7/10/2014', 'Lab'); setEditModalValues('7/9/2014', '7/10/2014', 'Lab');
selectVisibilitySettings();
setContentVisibility('staff_only');
selectAdvancedSettings(); selectAdvancedSettings();
selectProctoredExam('00:30'); selectProctoredExam('00:30');
setContentVisibility('staff_only');
// time limit and review rules should be visible // time limit and review rules should be visible
checkOptionFieldVisibility(true, true); checkOptionFieldVisibility(true, true);
...@@ -1007,9 +1068,10 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j ...@@ -1007,9 +1068,10 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
createCourseOutlinePage(this, mockCourseJSON, false); createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click(); outlinePage.$('.outline-subsection .configure-button').click();
setEditModalValues('7/9/2014', '7/10/2014', 'Lab'); setEditModalValues('7/9/2014', '7/10/2014', 'Lab');
selectVisibilitySettings();
setContentVisibility('staff_only');
selectAdvancedSettings(); selectAdvancedSettings();
selectProctoredExam('abcd'); selectProctoredExam('abcd');
setContentVisibility('staff_only');
// time limit field should be visible and have the correct value // time limit field should be visible and have the correct value
expect($('.field-time-limit').is(':visible')).toBe(true); expect($('.field-time-limit').is(':visible')).toBe(true);
...@@ -1414,6 +1476,57 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j ...@@ -1414,6 +1476,57 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
); );
}); });
describe('Show correctness setting set as expected.', function() {
var setShowCorrectness;
setShowCorrectness = function(showCorrectness) {
$('input[name=show-correctness][value=' + showCorrectness + ']').click();
};
describe('Show correctness set by subsection metadata.', function() {
$.each(['always', 'never', 'past_due'], function(index, showCorrectness) {
it('show_correctness="' + showCorrectness + '"', function() {
var mockCourseJSONCorrectness = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({show_correctness: showCorrectness}, [])
])
]);
createCourseOutlinePage(this, mockCourseJSONCorrectness, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectVisibilitySettings();
expectShowCorrectness(showCorrectness);
});
});
});
describe('Show correctness editor works as expected.', function() {
beforeEach(function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectVisibilitySettings();
});
it('show_correctness="always" (default, unchanged metadata)', function() {
setShowCorrectness('always');
$('.wrapper-modal-window .action-save').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection',
defaultModalSettings);
});
$.each(['never', 'past_due'], function(index, showCorrectness) {
it('show_correctness="' + showCorrectness + '" updates settings, republishes', function() {
var expectedSettings = $.extend(true, {}, defaultModalSettings, {publish: 'republish'});
expectedSettings.metadata.show_correctness = showCorrectness;
setShowCorrectness(showCorrectness);
$('.wrapper-modal-window .action-save').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection',
expectedSettings);
});
});
});
});
verifyTypePublishable('subsection', function(options) { verifyTypePublishable('subsection', function(options) {
return createMockCourseJSON({}, [ return createMockCourseJSON({}, [
createMockSectionJSON({}, [ createMockSectionJSON({}, [
......
...@@ -15,7 +15,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview', ...@@ -15,7 +15,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
'use strict'; 'use strict';
var CourseOutlineXBlockModal, SettingsXBlockModal, PublishXBlockModal, AbstractEditor, BaseDateEditor, var CourseOutlineXBlockModal, SettingsXBlockModal, PublishXBlockModal, AbstractEditor, BaseDateEditor,
ReleaseDateEditor, DueDateEditor, GradingEditor, PublishEditor, AbstractVisibilityEditor, StaffLockEditor, ReleaseDateEditor, DueDateEditor, GradingEditor, PublishEditor, AbstractVisibilityEditor, StaffLockEditor,
ContentVisibilityEditor, TimedExaminationPreferenceEditor, AccessEditor; ContentVisibilityEditor, TimedExaminationPreferenceEditor, AccessEditor, ShowCorrectnessEditor;
CourseOutlineXBlockModal = BaseModal.extend({ CourseOutlineXBlockModal = BaseModal.extend({
events: _.extend({}, BaseModal.prototype.events, { events: _.extend({}, BaseModal.prototype.events, {
...@@ -714,7 +714,51 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview', ...@@ -714,7 +714,51 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
AbstractVisibilityEditor.prototype.getContext.call(this), AbstractVisibilityEditor.prototype.getContext.call(this),
{ {
hide_after_due: this.modelVisibility() === 'hide_after_due', hide_after_due: this.modelVisibility() === 'hide_after_due',
self_paced: this.model.get('self_paced') === true self_paced: course.get('self_paced') === true
}
);
}
});
ShowCorrectnessEditor = AbstractEditor.extend({
templateName: 'show-correctness-editor',
className: 'edit-show-correctness',
afterRender: function() {
AbstractEditor.prototype.afterRender.call(this);
this.setValue(this.model.get('show_correctness') || 'always');
},
setValue: function(value) {
this.$('input[name=show-correctness][value=' + value + ']').prop('checked', true);
},
currentValue: function() {
return this.$('input[name=show-correctness]:checked').val();
},
hasChanges: function() {
return this.model.get('show_correctness') !== this.currentValue();
},
getRequestData: function() {
if (this.hasChanges()) {
return {
publish: 'republish',
metadata: {
show_correctness: this.currentValue()
}
};
} else {
return {};
}
},
getContext: function() {
return $.extend(
{},
AbstractEditor.prototype.getContext.call(this),
{
self_paced: course.get('self_paced') === true
} }
); );
} }
...@@ -732,6 +776,11 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview', ...@@ -732,6 +776,11 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
getEditModal: function(xblockInfo, options) { getEditModal: function(xblockInfo, options) {
var tabs = []; var tabs = [];
var editors = []; var editors = [];
var advancedTab = {
name: 'advanced',
displayName: gettext('Advanced'),
editors: []
};
if (xblockInfo.isVertical()) { if (xblockInfo.isVertical()) {
editors = [StaffLockEditor]; editors = [StaffLockEditor];
} else { } else {
...@@ -742,8 +791,8 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview', ...@@ -742,8 +791,8 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
editors: [] editors: []
}, },
{ {
name: 'advanced', name: 'visibility',
displayName: gettext('Advanced'), displayName: gettext('Visibility'),
editors: [] editors: []
} }
]; ];
...@@ -752,14 +801,19 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview', ...@@ -752,14 +801,19 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
tabs[1].editors = [StaffLockEditor]; tabs[1].editors = [StaffLockEditor];
} else if (xblockInfo.isSequential()) { } else if (xblockInfo.isSequential()) {
tabs[0].editors = [ReleaseDateEditor, GradingEditor, DueDateEditor]; tabs[0].editors = [ReleaseDateEditor, GradingEditor, DueDateEditor];
tabs[1].editors = [ContentVisibilityEditor]; tabs[1].editors = [ContentVisibilityEditor, ShowCorrectnessEditor];
if (options.enable_proctored_exams || options.enable_timed_exams) { if (options.enable_proctored_exams || options.enable_timed_exams) {
tabs[1].editors.push(TimedExaminationPreferenceEditor); advancedTab.editors.push(TimedExaminationPreferenceEditor);
} }
if (typeof(xblockInfo.get('is_prereq')) !== 'undefined') { if (typeof(xblockInfo.get('is_prereq')) !== 'undefined') {
tabs[1].editors.push(AccessEditor); advancedTab.editors.push(AccessEditor);
}
// Show the Advanced tab iff it has editors to display
if (advancedTab.editors.length > 0) {
tabs.push(advancedTab);
} }
} }
} }
......
...@@ -585,8 +585,11 @@ ...@@ -585,8 +585,11 @@
} }
.list-fields { .list-fields {
.field-message { .field-message {
@extend %t-copy-sub2;
color: $gray-d1; color: $gray-d1;
font-size: ($baseline/2); }
label {
@extend %t-title7;
} }
.field { .field {
display: inline-block; display: inline-block;
...@@ -595,7 +598,6 @@ ...@@ -595,7 +598,6 @@
margin-bottom: ($baseline/4); margin-bottom: ($baseline/4);
label { label {
@extend %t-copy-sub1;
@extend %t-strong; @extend %t-strong;
@include transition(color $tmg-f3 ease-in-out 0s); @include transition(color $tmg-f3 ease-in-out 0s);
margin: 0 0 ($baseline/4) 0; margin: 0 0 ($baseline/4) 0;
......
...@@ -26,7 +26,7 @@ from openedx.core.djangolib.markup import HTML, Text ...@@ -26,7 +26,7 @@ from openedx.core.djangolib.markup import HTML, Text
<%block name="header_extras"> <%block name="header_extras">
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" /> <link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
% for template_name in ['course-outline', 'xblock-string-field-editor', 'basic-modal', 'modal-button', 'course-outline-modal', 'due-date-editor', 'release-date-editor', 'grading-editor', 'publish-editor', 'staff-lock-editor', 'content-visibility-editor', 'verification-access-editor', 'timed-examination-preference-editor', 'access-editor', 'settings-modal-tabs']: % for template_name in ['course-outline', 'xblock-string-field-editor', 'basic-modal', 'modal-button', 'course-outline-modal', 'due-date-editor', 'release-date-editor', 'grading-editor', 'publish-editor', 'staff-lock-editor', 'content-visibility-editor', 'verification-access-editor', 'timed-examination-preference-editor', 'access-editor', 'settings-modal-tabs', 'show-correctness-editor']:
<script type="text/template" id="${template_name}-tpl"> <script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" /> <%static:include path="js/${template_name}.underscore" />
</script> </script>
......
<form>
<h3 class="modal-section-title" id="show_correctness_label"><%- gettext('Assessment Results Visibility') %></h3>
<div class="modal-section-content show-correctness">
<div role="group" class="list-fields" aria-labelledby="show_correctness_label">
<label class="label">
<input class="input input-radio" name="show-correctness" type="radio" value="always" aria-describedby="always_show_correctness_description" />
<%- gettext('Always show assessment results') %>
</label>
<p class='field-message' id='always_show_correctness_description'>
<%- gettext('When learners submit an answer to an assessment, they immediately see whether the answer is correct or incorrect, and the score received.') %>
</p>
<label class="label">
<input class="input input-radio" name="show-correctness" type="radio" value="never" aria-describedby="never_show_correctness_description" />
<%- gettext('Never show assessment results') %>
</label>
<p class='field-message' id='never_show_correctness_description'>
<%- gettext('Learners never see whether their answers to assessments are correct or incorrect, nor the score received.') %>
</p>
<label class="label">
<input class="input input-radio" name="show-correctness" type="radio" value="past_due" aria-describedby="show_correctness_past_due_description" />
<%- gettext('Show assessment results when subsection is past due') %>
</label>
<p class='field-message' id='show_correctness_past_due_description'>
<% if (self_paced) { %>
<%- gettext('Learners do not see whether their answers to assessments were correct or incorrect, nor the score received, until after the course end date has passed.') %>
<%- gettext('If the course does not have an end date, learners always see their scores when they submit answers to assessments.') %>
<% } else { %>
<%- gettext('Learners do not see whether their answers to assessments were correct or incorrect, nor the score received, until after the due date for the subsection has passed.') %>
<%- gettext('If the subsection does not have a due date, learners always see their scores when they submit answers to assessments.') %>
<% } %>
</p>
</div>
</div>
</form>
...@@ -798,6 +798,11 @@ class LoncapaProblem(object): ...@@ -798,6 +798,11 @@ class LoncapaProblem(object):
if problemid in self.correct_map: if problemid in self.correct_map:
pid = input_id pid = input_id
# If we're withholding correctness, don't show adaptive hints either.
# Note that regular, "demand" hints will be shown, if the course author has added them to the problem.
if not self.capa_module.correctness_available():
status = 'submitted'
else:
# If the the problem has not been saved since the last submit set the status to the # If the the problem has not been saved since the last submit set the status to the
# current correctness value and set the message as expected. Otherwise we do not want to # current correctness value and set the message as expected. Otherwise we do not want to
# display correctness because the answer may have changed since the problem was graded. # display correctness because the answer may have changed since the problem was graded.
......
...@@ -90,6 +90,7 @@ class Status(object): ...@@ -90,6 +90,7 @@ class Status(object):
'incomplete': _('incomplete'), 'incomplete': _('incomplete'),
'unanswered': _('unanswered'), 'unanswered': _('unanswered'),
'unsubmitted': _('unanswered'), 'unsubmitted': _('unanswered'),
'submitted': _('submitted'),
'queued': _('processing'), 'queued': _('processing'),
} }
tooltips = { tooltips = {
...@@ -197,7 +198,7 @@ class InputTypeBase(object): ...@@ -197,7 +198,7 @@ class InputTypeBase(object):
(what the student entered last time) (what the student entered last time)
* 'id' -- the id of this input, typically * 'id' -- the id of this input, typically
"{problem-location}_{response-num}_{input-num}" "{problem-location}_{response-num}_{input-num}"
* 'status' (answered, unanswered, unsubmitted) * 'status' (submitted, unanswered, unsubmitted)
* 'input_state' -- dictionary containing any inputtype-specific state * 'input_state' -- dictionary containing any inputtype-specific state
that has been preserved that has been preserved
* 'feedback' (dictionary containing keys for hints, errors, or other * 'feedback' (dictionary containing keys for hints, errors, or other
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
<div class="script_placeholder" data-src="${jschannel_loader}"/> <div class="script_placeholder" data-src="${jschannel_loader}"/>
<div class="script_placeholder" data-src="${jsinput_loader}"/> <div class="script_placeholder" data-src="${jsinput_loader}"/>
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']: % if status in ['unsubmitted', 'submitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']:
<div class="${status.classname}"> <div class="${status.classname}">
% endif % endif
...@@ -49,7 +49,7 @@ ...@@ -49,7 +49,7 @@
<div class="error_message" style="padding: 5px 5px 5px 5px; background-color:#FA6666; height:60px;width:400px; display: none"></div> <div class="error_message" style="padding: 5px 5px 5px 5px; background-color:#FA6666; height:60px;width:400px; display: none"></div>
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']: % if status in ['unsubmitted', 'submitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']:
</div> </div>
% endif % endif
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<div class="script_placeholder" data-src="${preprocessor['script_src']}"/> <div class="script_placeholder" data-src="${preprocessor['script_src']}"/>
% endif % endif
% if status in ('unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete'): % if status in ('unsubmitted', 'submitted', 'correct', 'incorrect', 'partially-correct', 'incomplete'):
<div class="${status.classname} ${doinline}"> <div class="${status.classname} ${doinline}">
% endif % endif
...@@ -45,7 +45,7 @@ ...@@ -45,7 +45,7 @@
<textarea style="display:none" id="input_${id}_dynamath" name="input_${id}_dynamath"></textarea> <textarea style="display:none" id="input_${id}_dynamath" name="input_${id}_dynamath"></textarea>
% endif % endif
% if status in ('unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete'): % if status in ('unsubmitted', 'submitted', 'correct', 'incorrect', 'partially-correct', 'incomplete'):
</div> </div>
% endif % endif
......
...@@ -173,6 +173,7 @@ class TemplateTestCase(unittest.TestCase): ...@@ -173,6 +173,7 @@ class TemplateTestCase(unittest.TestCase):
cases = [ cases = [
('correct', 'correct'), ('correct', 'correct'),
('unsubmitted', 'unanswered'), ('unsubmitted', 'unanswered'),
('submitted', 'submitted'),
('incorrect', 'incorrect'), ('incorrect', 'incorrect'),
('incomplete', 'incorrect') ('incomplete', 'incorrect')
] ]
......
...@@ -24,7 +24,7 @@ from capa.inputtypes import Status ...@@ -24,7 +24,7 @@ from capa.inputtypes import Status
from capa.responsetypes import StudentInputError, ResponseError, LoncapaProblemError from capa.responsetypes import StudentInputError, ResponseError, LoncapaProblemError
from capa.util import convert_files_to_filenames, get_inner_html_from_xpath from capa.util import convert_files_to_filenames, get_inner_html_from_xpath
from xblock.fields import Boolean, Dict, Float, Integer, Scope, String, XMLString from xblock.fields import Boolean, Dict, Float, Integer, Scope, String, XMLString
from xmodule.capa_base_constants import RANDOMIZATION, SHOWANSWER from xmodule.capa_base_constants import RANDOMIZATION, SHOWANSWER, SHOW_CORRECTNESS
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError
from .fields import Date, Timedelta from .fields import Date, Timedelta
from .progress import Progress from .progress import Progress
...@@ -114,6 +114,18 @@ class CapaFields(object): ...@@ -114,6 +114,18 @@ class CapaFields(object):
help=_("Amount of time after the due date that submissions will be accepted"), help=_("Amount of time after the due date that submissions will be accepted"),
scope=Scope.settings scope=Scope.settings
) )
show_correctness = String(
display_name=_("Show Results"),
help=_("Defines when to show whether a learner's answer to the problem is correct. "
"Configured on the subsection."),
scope=Scope.settings,
default=SHOW_CORRECTNESS.ALWAYS,
values=[
{"display_name": _("Always"), "value": SHOW_CORRECTNESS.ALWAYS},
{"display_name": _("Never"), "value": SHOW_CORRECTNESS.NEVER},
{"display_name": _("Past Due"), "value": SHOW_CORRECTNESS.PAST_DUE},
],
)
showanswer = String( showanswer = String(
display_name=_("Show Answer"), display_name=_("Show Answer"),
help=_("Defines when to show the answer to the problem. " help=_("Defines when to show the answer to the problem. "
...@@ -391,12 +403,25 @@ class CapaMixin(CapaFields): ...@@ -391,12 +403,25 @@ class CapaMixin(CapaFields):
return None return None
return None return None
def get_display_progress(self):
"""
Return (score, total) to be displayed to the learner.
"""
progress = self.get_progress()
score, total = (progress.frac() if progress else (0, 0))
# Withhold the score if hiding correctness
if not self.correctness_available():
score = None
return score, total
def get_html(self): def get_html(self):
""" """
Return some html with data about the module Return some html with data about the module
""" """
progress = self.get_progress() curr_score, total_possible = self.get_display_progress()
curr_score, total_possible = (progress.frac() if progress else (0, 0))
return self.runtime.render_template('problem_ajax.html', { return self.runtime.render_template('problem_ajax.html', {
'element_id': self.location.html_id(), 'element_id': self.location.html_id(),
'id': self.location.to_deprecated_string(), 'id': self.location.to_deprecated_string(),
...@@ -739,7 +764,11 @@ class CapaMixin(CapaFields): ...@@ -739,7 +764,11 @@ class CapaMixin(CapaFields):
if render_notifications: if render_notifications:
progress = self.get_progress() progress = self.get_progress()
id_list = self.lcp.correct_map.keys() id_list = self.lcp.correct_map.keys()
if len(id_list) == 1:
# Show only a generic message if hiding correctness
if not self.correctness_available():
answer_notification_type = 'submitted'
elif len(id_list) == 1:
# Only one answer available # Only one answer available
answer_notification_type = self.lcp.correct_map.get_correctness(id_list[0]) answer_notification_type = self.lcp.correct_map.get_correctness(id_list[0])
elif len(id_list) > 1: elif len(id_list) > 1:
...@@ -782,6 +811,8 @@ class CapaMixin(CapaFields): ...@@ -782,6 +811,8 @@ class CapaMixin(CapaFields):
).format(progress=str(progress)) ).format(progress=str(progress))
else: else:
answer_notification_message = _('Partially Correct') answer_notification_message = _('Partially Correct')
elif answer_notification_type == 'submitted':
answer_notification_message = _("Answer submitted.")
return answer_notification_type, answer_notification_message return answer_notification_type, answer_notification_message
...@@ -855,7 +886,10 @@ class CapaMixin(CapaFields): ...@@ -855,7 +886,10 @@ class CapaMixin(CapaFields):
""" """
Is the user allowed to see an answer? Is the user allowed to see an answer?
""" """
if self.showanswer == '': if not self.correctness_available():
# If correctness is being withheld, then don't show answers either.
return False
elif self.showanswer == '':
return False return False
elif self.showanswer == SHOWANSWER.NEVER: elif self.showanswer == SHOWANSWER.NEVER:
return False return False
...@@ -883,6 +917,24 @@ class CapaMixin(CapaFields): ...@@ -883,6 +917,24 @@ class CapaMixin(CapaFields):
return False return False
def correctness_available(self):
"""
Is the user allowed to see whether she's answered correctly?
Limits access to the correct/incorrect flags, messages, and problem score.
"""
if self.show_correctness == SHOW_CORRECTNESS.NEVER:
return False
elif self.runtime.user_is_staff:
# This is after the 'never' check because admins can see correctness
# unless the problem explicitly prevents it
return True
elif self.show_correctness == SHOW_CORRECTNESS.PAST_DUE:
return self.is_past_due()
# else: self.show_correctness == SHOW_CORRECTNESS.ALWAYS
return True
def update_score(self, data): def update_score(self, data):
""" """
Delivers grading response (e.g. from asynchronous code checking) to Delivers grading response (e.g. from asynchronous code checking) to
...@@ -1233,6 +1285,10 @@ class CapaMixin(CapaFields): ...@@ -1233,6 +1285,10 @@ class CapaMixin(CapaFields):
# render problem into HTML # render problem into HTML
html = self.get_problem_html(encapsulate=False, submit_notification=True) html = self.get_problem_html(encapsulate=False, submit_notification=True)
# Withhold success indicator if hiding correctness
if not self.correctness_available():
success = 'submitted'
return { return {
'success': success, 'success': success,
'contents': html 'contents': html
......
...@@ -4,6 +4,15 @@ Constants for capa_base problems ...@@ -4,6 +4,15 @@ Constants for capa_base problems
""" """
class SHOW_CORRECTNESS(object): # pylint: disable=invalid-name
"""
Constants for when to show correctness
"""
ALWAYS = "always"
PAST_DUE = "past_due"
NEVER = "never"
class SHOWANSWER(object): class SHOWANSWER(object):
""" """
Constants for when to show answer Constants for when to show answer
......
...@@ -120,7 +120,8 @@ class CapaModule(CapaMixin, XModule): ...@@ -120,7 +120,8 @@ class CapaModule(CapaMixin, XModule):
after = self.get_progress() after = self.get_progress()
after_attempts = self.attempts after_attempts = self.attempts
progress_changed = (after != before) or (after_attempts != before_attempts) progress_changed = (after != before) or (after_attempts != before_attempts)
curr_score, total_possible = (after.frac() if after else (0, 0)) curr_score, total_possible = self.get_display_progress()
result.update({ result.update({
'progress_changed': progress_changed, 'progress_changed': progress_changed,
'current_score': curr_score, 'current_score': curr_score,
...@@ -215,6 +216,7 @@ class CapaDescriptor(CapaFields, RawDescriptor): ...@@ -215,6 +216,7 @@ class CapaDescriptor(CapaFields, RawDescriptor):
CapaDescriptor.force_save_button, CapaDescriptor.force_save_button,
CapaDescriptor.markdown, CapaDescriptor.markdown,
CapaDescriptor.use_latex_compiler, CapaDescriptor.use_latex_compiler,
CapaDescriptor.show_correctness,
]) ])
return non_editable_fields return non_editable_fields
......
...@@ -250,6 +250,15 @@ div.problem { ...@@ -250,6 +250,15 @@ div.problem {
border-color: $incorrect; border-color: $incorrect;
} }
} }
&.choicegroup_submitted {
border: 2px solid $submitted;
// keep blue for submitted answers on hover.
&:hover {
border-color: $submitted;
}
}
} }
.indicator-container { .indicator-container {
...@@ -325,6 +334,7 @@ div.problem { ...@@ -325,6 +334,7 @@ div.problem {
@include status-icon($incorrect, $cross-icon); @include status-icon($incorrect, $cross-icon);
} }
&.submitted,
&.unsubmitted, &.unsubmitted,
&.unanswered { &.unanswered {
.status-icon { .status-icon {
...@@ -419,6 +429,12 @@ div.problem { ...@@ -419,6 +429,12 @@ div.problem {
} }
} }
&.submitted, &.ui-icon-check {
input {
border-color: $submitted;
}
}
p.answer { p.answer {
display: inline-block; display: inline-block;
margin-top: ($baseline / 2); margin-top: ($baseline / 2);
...@@ -790,6 +806,18 @@ div.problem { ...@@ -790,6 +806,18 @@ div.problem {
} }
} }
// CASE: submitted, correctness withheld
> .submitted {
input {
border: 2px solid $submitted;
}
.status {
content: '';
}
}
// CASE: unanswered and unsubmitted // CASE: unanswered and unsubmitted
> .unanswered, > .unsubmitted { > .unanswered, > .unsubmitted {
...@@ -824,7 +852,11 @@ div.problem { ...@@ -824,7 +852,11 @@ div.problem {
.indicator-container { .indicator-container {
display: inline-block; display: inline-block;
.status.correct:after, .status.partially-correct:after, .status.incorrect:after, .status.unanswered:after { .status.correct:after,
.status.partially-correct:after,
.status.incorrect:after,
.status.submitted:after,
.status.unanswered:after {
@include margin-left(0); @include margin-left(0);
} }
} }
...@@ -1531,6 +1563,10 @@ div.problem { ...@@ -1531,6 +1563,10 @@ div.problem {
@extend label.choicegroup_incorrect; @extend label.choicegroup_incorrect;
} }
label.choicetextgroup_submitted, section.choicetextgroup_submitted {
@extend label.choicegroup_submitted;
}
label.choicetextgroup_show_correct, section.choicetextgroup_show_correct { label.choicetextgroup_show_correct, section.choicetextgroup_show_correct {
&:after { &:after {
@include margin-left($baseline*.75); @include margin-left($baseline*.75);
...@@ -1569,6 +1605,10 @@ div.problem .imageinput.capa_inputtype { ...@@ -1569,6 +1605,10 @@ div.problem .imageinput.capa_inputtype {
.partially-correct { .partially-correct {
@include status-icon($partially-correct, $asterisk-icon); @include status-icon($partially-correct, $asterisk-icon);
} }
.submitted {
content: '';
}
} }
// +Problem - Annotation Problem Overrides // +Problem - Annotation Problem Overrides
...@@ -1596,4 +1636,8 @@ div.problem .annotation-input { ...@@ -1596,4 +1636,8 @@ div.problem .annotation-input {
.partially-correct { .partially-correct {
@include status-icon($partially-correct, $asterisk-icon); @include status-icon($partially-correct, $asterisk-icon);
} }
.submitted {
content: '';
}
} }
...@@ -138,6 +138,28 @@ describe 'Problem', -> ...@@ -138,6 +138,28 @@ describe 'Problem', ->
it 'shows 0 points possible for the detail', -> it 'shows 0 points possible for the detail', ->
testProgessData(@problem, 0, 0, 1, "False", "0 points possible (ungraded)") testProgessData(@problem, 0, 0, 1, "False", "0 points possible (ungraded)")
describe 'with a score of null (show_correctness == false)', ->
it 'reports the number of points possible and graded, results hidden', ->
testProgessData(@problem, null, 1, 0, "True", "1 point possible (graded, results hidden)")
it 'reports the number of points possible (plural) and graded, results hidden', ->
testProgessData(@problem, null, 2, 0, "True", "2 points possible (graded, results hidden)")
it 'reports the number of points possible and ungraded, results hidden', ->
testProgessData(@problem, null, 1, 0, "False", "1 point possible (ungraded, results hidden)")
it 'displays ungraded if number of points possible is 0, results hidden', ->
testProgessData(@problem, null, 0, 0, "False", "0 points possible (ungraded, results hidden)")
it 'displays ungraded if number of points possible is 0, even if graded value is True, results hidden', ->
testProgessData(@problem, null, 0, 0, "True", "0 points possible (ungraded, results hidden)")
it 'reports the correct score with status none and >0 attempts, results hidden', ->
testProgessData(@problem, null, 1, 1, "True", "1 point possible (graded, results hidden)")
it 'reports the correct score with >1 weight, status none, and >0 attempts, results hidden', ->
testProgessData(@problem, null, 2, 2, "True", "2 points possible (graded, results hidden)")
describe 'render', -> describe 'render', ->
beforeEach -> beforeEach ->
@problem = new Problem($('.xblock-student_view')) @problem = new Problem($('.xblock-student_view'))
......
...@@ -214,11 +214,36 @@ ...@@ -214,11 +214,36 @@
attemptsUsed = this.el.data('attempts-used'); attemptsUsed = this.el.data('attempts-used');
graded = this.el.data('graded'); graded = this.el.data('graded');
// The problem is ungraded if it's explicitly marked as such, or if the total possible score is 0
if (graded === 'True' && totalScore !== 0) {
graded = true;
} else {
graded = false;
}
if (curScore === undefined || totalScore === undefined) { if (curScore === undefined || totalScore === undefined) {
progress = ''; // Render an empty string.
progressTemplate = '';
} else if (curScore === null || curScore === 'None') {
// Render 'x point(s) possible (un/graded, results hidden)' if no current score provided.
if (graded) {
progressTemplate = ngettext(
// Translators: %(num_points)s is the number of points possible (examples: 1, 3, 10).;
'%(num_points)s point possible (graded, results hidden)',
'%(num_points)s points possible (graded, results hidden)',
totalScore
);
} else {
progressTemplate = ngettext(
// Translators: %(num_points)s is the number of points possible (examples: 1, 3, 10).;
'%(num_points)s point possible (ungraded, results hidden)',
'%(num_points)s points possible (ungraded, results hidden)',
totalScore
);
}
} else if (attemptsUsed === 0 || totalScore === 0) { } else if (attemptsUsed === 0 || totalScore === 0) {
// Render 'x point(s) possible' if student has not yet attempted question // Render 'x point(s) possible' if student has not yet attempted question
if (graded === 'True' && totalScore !== 0) { if (graded) {
progressTemplate = ngettext( progressTemplate = ngettext(
// Translators: %(num_points)s is the number of points possible (examples: 1, 3, 10).; // Translators: %(num_points)s is the number of points possible (examples: 1, 3, 10).;
'%(num_points)s point possible (graded)', '%(num_points)s points possible (graded)', '%(num_points)s point possible (graded)', '%(num_points)s points possible (graded)',
...@@ -231,10 +256,9 @@ ...@@ -231,10 +256,9 @@
totalScore totalScore
); );
} }
progress = interpolate(progressTemplate, {num_points: totalScore}, true);
} else { } else {
// Render 'x/y point(s)' if student has attempted question // Render 'x/y point(s)' if student has attempted question
if (graded === 'True' && totalScore !== 0) { if (graded) {
progressTemplate = ngettext( progressTemplate = ngettext(
// This comment needs to be on one line to be properly scraped for the translators. // This comment needs to be on one line to be properly scraped for the translators.
// Translators: %(earned)s is the number of points earned. %(possible)s is the total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of points will always be at least 1. We pluralize based on the total number of points (example: 0/1 point; 1/2 points); // Translators: %(earned)s is the number of points earned. %(possible)s is the total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of points will always be at least 1. We pluralize based on the total number of points (example: 0/1 point; 1/2 points);
...@@ -249,13 +273,14 @@ ...@@ -249,13 +273,14 @@
totalScore totalScore
); );
} }
}
progress = interpolate( progress = interpolate(
progressTemplate, { progressTemplate, {
earned: curScore, earned: curScore,
num_points: totalScore,
possible: totalScore possible: totalScore
}, true }, true
); );
}
return this.$('.problem-progress').text(progress); return this.$('.problem-progress').text(progress);
}; };
...@@ -573,6 +598,7 @@ ...@@ -573,6 +598,7 @@
complete: this.enableSubmitButtonAfterResponse, complete: this.enableSubmitButtonAfterResponse,
success: function(response) { success: function(response) {
switch (response.success) { switch (response.success) {
case 'submitted':
case 'incorrect': case 'incorrect':
case 'correct': case 'correct':
that.render(response.contents); that.render(response.contents);
...@@ -599,6 +625,7 @@ ...@@ -599,6 +625,7 @@
Logger.log('problem_check', this.answers); Logger.log('problem_check', this.answers);
return $.postWithPrefix('' + this.url + '/problem_check', this.answers, function(response) { return $.postWithPrefix('' + this.url + '/problem_check', this.answers, function(response) {
switch (response.success) { switch (response.success) {
case 'submitted':
case 'incorrect': case 'incorrect':
case 'correct': case 'correct':
window.SR.readTexts(that.get_sr_status(response.contents)); window.SR.readTexts(that.get_sr_status(response.contents));
......
...@@ -100,6 +100,19 @@ class InheritanceMixin(XBlockMixin): ...@@ -100,6 +100,19 @@ class InheritanceMixin(XBlockMixin):
scope=Scope.settings, scope=Scope.settings,
default="finished", default="finished",
) )
show_correctness = String(
display_name=_("Show Results"),
help=_(
# Translators: DO NOT translate the words in quotes here, they are
# specific words for the acceptable values.
'Specify when to show answer correctness and score to learners. '
'Valid values are "always", "never", and "past_due".'
),
scope=Scope.settings,
default="always",
)
rerandomize = String( rerandomize = String(
display_name=_("Randomization"), display_name=_("Randomization"),
help=_( help=_(
......
...@@ -265,6 +265,46 @@ class CapaModuleTest(unittest.TestCase): ...@@ -265,6 +265,46 @@ class CapaModuleTest(unittest.TestCase):
problem.attempts = 1 problem.attempts = 1
self.assertTrue(problem.answer_available()) self.assertTrue(problem.answer_available())
@ddt.data(
# If show_correctness=always, Answer is visible after attempted
({
'showanswer': 'attempted',
'max_attempts': '1',
'show_correctness': 'always',
}, True),
# If show_correctness=never, Answer is never visible
({
'showanswer': 'attempted',
'max_attempts': '1',
'show_correctness': 'never',
}, False),
# If show_correctness=past_due, answer is not visible before due date
({
'showanswer': 'attempted',
'show_correctness': 'past_due',
'max_attempts': '1',
'due': 'tomorrow_str',
}, False),
# If show_correctness=past_due, answer is visible after due date
({
'showanswer': 'attempted',
'show_correctness': 'past_due',
'max_attempts': '1',
'due': 'yesterday_str',
}, True),
)
@ddt.unpack
def test_showanswer_hide_correctness(self, problem_data, answer_available):
"""
Ensure that the answer will not be shown when correctness is being hidden.
"""
if 'due' in problem_data:
problem_data['due'] = getattr(self, problem_data['due'])
problem = CapaFactory.create(**problem_data)
self.assertFalse(problem.answer_available())
problem.attempts = 1
self.assertEqual(problem.answer_available(), answer_available)
def test_showanswer_closed(self): def test_showanswer_closed(self):
# can see after attempts used up, even with due date in the future # can see after attempts used up, even with due date in the future
...@@ -414,6 +454,73 @@ class CapaModuleTest(unittest.TestCase): ...@@ -414,6 +454,73 @@ class CapaModuleTest(unittest.TestCase):
graceperiod=self.two_day_delta_str) graceperiod=self.two_day_delta_str)
self.assertTrue(still_in_grace.answer_available()) self.assertTrue(still_in_grace.answer_available())
@ddt.data('', 'other-value')
def test_show_correctness_other(self, show_correctness):
"""
Test that correctness is visible if show_correctness is not set to one of the values
from SHOW_CORRECTNESS constant.
"""
problem = CapaFactory.create(show_correctness=show_correctness)
self.assertTrue(problem.correctness_available())
def test_show_correctness_default(self):
"""
Test that correctness is visible by default.
"""
problem = CapaFactory.create()
self.assertTrue(problem.correctness_available())
def test_show_correctness_never(self):
"""
Test that correctness is hidden when show_correctness turned off.
"""
problem = CapaFactory.create(show_correctness='never')
self.assertFalse(problem.correctness_available())
@ddt.data(
# Correctness not visible if due date in the future, even after using up all attempts
({
'show_correctness': 'past_due',
'max_attempts': '1',
'attempts': '1',
'due': 'tomorrow_str',
}, False),
# Correctness visible if due date in the past
({
'show_correctness': 'past_due',
'max_attempts': '1',
'attempts': '0',
'due': 'yesterday_str',
}, True),
# Correctness not visible if due date in the future
({
'show_correctness': 'past_due',
'max_attempts': '1',
'attempts': '0',
'due': 'tomorrow_str',
}, False),
# Correctness not visible because grace period hasn't expired,
# even after using up all attempts
({
'show_correctness': 'past_due',
'max_attempts': '1',
'attempts': '1',
'due': 'yesterday_str',
'graceperiod': 'two_day_delta_str',
}, False),
)
@ddt.unpack
def test_show_correctness_past_due(self, problem_data, expected_result):
"""
Test that with show_correctness="past_due", correctness will only be visible
after the problem is closed for everyone--e.g. after due date + grace period.
"""
problem_data['due'] = getattr(self, problem_data['due'])
if 'graceperiod' in problem_data:
problem_data['graceperiod'] = getattr(self, problem_data['graceperiod'])
problem = CapaFactory.create(**problem_data)
self.assertEqual(problem.correctness_available(), expected_result)
def test_closed(self): def test_closed(self):
# Attempts < Max attempts --> NOT closed # Attempts < Max attempts --> NOT closed
...@@ -814,6 +921,36 @@ class CapaModuleTest(unittest.TestCase): ...@@ -814,6 +921,36 @@ class CapaModuleTest(unittest.TestCase):
# Expect that the number of attempts is NOT incremented # Expect that the number of attempts is NOT incremented
self.assertEqual(module.attempts, 1) self.assertEqual(module.attempts, 1)
@ddt.data(
("never", True, None, 'submitted'),
("never", False, None, 'submitted'),
("past_due", True, None, 'submitted'),
("past_due", False, None, 'submitted'),
("always", True, 1, 'correct'),
("always", False, 0, 'incorrect'),
)
@ddt.unpack
def test_handle_ajax_show_correctness(self, show_correctness, is_correct, expected_score, expected_success):
module = CapaFactory.create(show_correctness=show_correctness,
due=self.tomorrow_str,
correct=is_correct)
# Simulate marking the input correct/incorrect
with patch('capa.correctmap.CorrectMap.is_correct') as mock_is_correct:
mock_is_correct.return_value = is_correct
# Check the problem
get_request_dict = {CapaFactory.input_key(): '0'}
json_result = module.handle_ajax('problem_check', get_request_dict)
result = json.loads(json_result)
# Expect that the AJAX result withholds correctness and score
self.assertEqual(result['current_score'], expected_score)
self.assertEqual(result['success'], expected_success)
# Expect that the number of attempts is incremented by 1
self.assertEqual(module.attempts, 1)
def test_reset_problem(self): def test_reset_problem(self):
module = CapaFactory.create(done=True) module = CapaFactory.create(done=True)
module.new_lcp = Mock(wraps=module.new_lcp) module.new_lcp = Mock(wraps=module.new_lcp)
...@@ -1584,6 +1721,27 @@ class CapaModuleTest(unittest.TestCase): ...@@ -1584,6 +1721,27 @@ class CapaModuleTest(unittest.TestCase):
other_module.get_progress() other_module.get_progress()
mock_progress.assert_called_with(1, 1) mock_progress.assert_called_with(1, 1)
@ddt.data(
("never", True, None),
("never", False, None),
("past_due", True, None),
("past_due", False, None),
("always", True, 1),
("always", False, 0),
)
@ddt.unpack
def test_get_display_progress_show_correctness(self, show_correctness, is_correct, expected_score):
"""
Check that score and total are calculated correctly for the progress fraction.
"""
module = CapaFactory.create(correct=is_correct,
show_correctness=show_correctness,
due=self.tomorrow_str)
module.weight = 1
score, total = module.get_display_progress()
self.assertEqual(score, expected_score)
self.assertEqual(total, 1)
def test_get_html(self): def test_get_html(self):
""" """
Check that get_html() calls get_progress() with no arguments. Check that get_html() calls get_progress() with no arguments.
......
...@@ -151,6 +151,7 @@ $general-color-accent: $uxpl-blue-base !default ...@@ -151,6 +151,7 @@ $general-color-accent: $uxpl-blue-base !default
$correct: $success-color !default; $correct: $success-color !default;
$partially-correct: $success-color !default; $partially-correct: $success-color !default;
$incorrect: $error-color !default; $incorrect: $error-color !default;
$submitted: $general-color !default;
// BUTTONS // BUTTONS
......
...@@ -317,6 +317,14 @@ class ProblemPage(PageObject): ...@@ -317,6 +317,14 @@ class ProblemPage(PageObject):
self.wait_for_element_visibility('.fa-asterisk', "Waiting for asterisk notification icon") self.wait_for_element_visibility('.fa-asterisk', "Waiting for asterisk notification icon")
self.wait_for_focus_on_submit_notification() self.wait_for_focus_on_submit_notification()
def wait_submitted_notification(self):
"""
Check for visibility of the "answer received" general notification and icon.
"""
msg = "Wait for submitted notification to be visible"
self.wait_for_element_visibility('.notification.general.notification-submit', msg)
self.wait_for_focus_on_submit_notification()
def click_hint(self): def click_hint(self):
""" """
Click the Hint button. Click the Hint button.
...@@ -418,14 +426,16 @@ class ProblemPage(PageObject): ...@@ -418,14 +426,16 @@ class ProblemPage(PageObject):
solution_selector = '.solution-span div.detailed-solution' solution_selector = '.solution-span div.detailed-solution'
return self.q(css=solution_selector).is_present() return self.q(css=solution_selector).is_present()
def is_correct_choice_highlighted(self, correct_choices): def is_choice_highlighted(self, choice, choices_list):
""" """
Check if correct answer/choice highlighted for choice group. Check if the given answer/choice is highlighted for choice group.
""" """
correct_status_xpath = '//fieldset/div[contains(@class, "field")][{0}]/label[contains(@class, "choicegroup_correct")]/span[contains(@class, "status correct")]' # pylint: disable=line-too-long choice_status_xpath = ('//fieldset/div[contains(@class, "field")][{{0}}]'
'/label[contains(@class, "choicegroup_{choice}")]'
'/span[contains(@class, "status {choice}")]'.format(choice=choice))
any_status_xpath = '//fieldset/div[contains(@class, "field")][{0}]/label/span' any_status_xpath = '//fieldset/div[contains(@class, "field")][{0}]/label/span'
for choice in correct_choices: for choice in choices_list:
if not self.q(xpath=correct_status_xpath.format(choice)).is_present(): if not self.q(xpath=choice_status_xpath.format(choice)).is_present():
return False return False
# Check that there is only a single status span, as there were some bugs with multiple # Check that there is only a single status span, as there were some bugs with multiple
...@@ -435,6 +445,18 @@ class ProblemPage(PageObject): ...@@ -435,6 +445,18 @@ class ProblemPage(PageObject):
return True return True
def is_correct_choice_highlighted(self, correct_choices):
"""
Check if correct answer/choice highlighted for choice group.
"""
return self.is_choice_highlighted('correct', correct_choices)
def is_submitted_choice_highlighted(self, correct_choices):
"""
Check if submitted answer/choice highlighted for choice group.
"""
return self.is_choice_highlighted('submitted', correct_choices)
@property @property
def problem_question(self): def problem_question(self):
""" """
......
...@@ -575,6 +575,13 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer): ...@@ -575,6 +575,13 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
self.q(css=".action-save").first.click() self.q(css=".action-save").first.click()
self.wait_for_ajax() self.wait_for_ajax()
def select_visibility_tab(self):
"""
Select the advanced settings tab
"""
self.q(css=".settings-tab-button[data-tab='visibility']").first.click()
self.wait_for_element_presence('input[value=hide_after_due]', 'Visibility fields not present.')
def select_advanced_tab(self, desired_item='special_exam'): def select_advanced_tab(self, desired_item='special_exam'):
""" """
Select the advanced settings tab Select the advanced settings tab
...@@ -584,8 +591,6 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer): ...@@ -584,8 +591,6 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
self.wait_for_element_presence('input.no_special_exam', 'Special exam settings fields not present.') self.wait_for_element_presence('input.no_special_exam', 'Special exam settings fields not present.')
if desired_item == 'gated_content': if desired_item == 'gated_content':
self.wait_for_element_visibility('#is_prereq', 'Gating settings fields are present.') self.wait_for_element_visibility('#is_prereq', 'Gating settings fields are present.')
if desired_item == 'hide_after_due_date':
self.wait_for_element_presence('input[value=hide_after_due]', 'Visibility fields not present.')
def make_exam_proctored(self): def make_exam_proctored(self):
""" """
...@@ -601,6 +606,7 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer): ...@@ -601,6 +606,7 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
""" """
self.q(css="input.timed_exam").first.click() self.q(css="input.timed_exam").first.click()
if hide_after_due: if hide_after_due:
self.select_visibility_tab()
self.q(css='input[name=content-visibility][value=hide_after_due]').first.click() self.q(css='input[name=content-visibility][value=hide_after_due]').first.click()
self.q(css=".action-save").first.click() self.q(css=".action-save").first.click()
self.wait_for_ajax() self.wait_for_ajax()
...@@ -1057,7 +1063,7 @@ class CourseOutlineModal(object): ...@@ -1057,7 +1063,7 @@ class CourseOutlineModal(object):
if needed. if needed.
""" """
if not self.is_staff_lock_visible: if not self.is_staff_lock_visible:
self.find_css(".settings-tab-button[data-tab=advanced]").click() self.find_css(".settings-tab-button[data-tab=visibility]").click()
EmptyPromise( EmptyPromise(
lambda: self.is_staff_lock_visible, lambda: self.is_staff_lock_visible,
"Staff lock option is visible", "Staff lock option is visible",
......
...@@ -900,7 +900,7 @@ class SubsectionHiddenAfterDueDateTest(UniqueCourseTest): ...@@ -900,7 +900,7 @@ class SubsectionHiddenAfterDueDateTest(UniqueCourseTest):
self.studio_course_outline.visit() self.studio_course_outline.visit()
self.studio_course_outline.open_subsection_settings_dialog() self.studio_course_outline.open_subsection_settings_dialog()
self.studio_course_outline.select_advanced_tab('hide_after_due_date') self.studio_course_outline.select_visibility_tab()
self.studio_course_outline.make_subsection_hidden_after_due_date() self.studio_course_outline.make_subsection_hidden_after_due_date()
self.logout_page.visit() self.logout_page.visit()
......
...@@ -3,6 +3,7 @@ Bok choy acceptance and a11y tests for problem types in the LMS ...@@ -3,6 +3,7 @@ Bok choy acceptance and a11y tests for problem types in the LMS
See also lettuce tests in lms/djangoapps/courseware/features/problems.feature See also lettuce tests in lms/djangoapps/courseware/features/problems.feature
""" """
import ddt
import random import random
import textwrap import textwrap
...@@ -84,12 +85,14 @@ class ProblemTypeTestBase(ProblemsTest, EventsTestMixin): ...@@ -84,12 +85,14 @@ class ProblemTypeTestBase(ProblemsTest, EventsTestMixin):
problem_name = None problem_name = None
problem_type = None problem_type = None
problem_points = 1
factory = None factory = None
factory_kwargs = {} factory_kwargs = {}
status_indicators = { status_indicators = {
'correct': ['span.correct'], 'correct': ['span.correct'],
'incorrect': ['span.incorrect'], 'incorrect': ['span.incorrect'],
'unanswered': ['span.unanswered'], 'unanswered': ['span.unanswered'],
'submitted': ['span.submitted'],
} }
def setUp(self): def setUp(self):
...@@ -100,6 +103,10 @@ class ProblemTypeTestBase(ProblemsTest, EventsTestMixin): ...@@ -100,6 +103,10 @@ class ProblemTypeTestBase(ProblemsTest, EventsTestMixin):
self.courseware_page.visit() self.courseware_page.visit()
self.problem_page = ProblemPage(self.browser) self.problem_page = ProblemPage(self.browser)
def get_sequential(self):
""" Allow any class in the inheritance chain to customize subsection metadata."""
return XBlockFixtureDesc('sequential', 'Test Subsection', metadata=getattr(self, 'sequential_metadata', {}))
def get_problem(self): def get_problem(self):
""" """
Creates a {problem_type} problem Creates a {problem_type} problem
...@@ -117,7 +124,7 @@ class ProblemTypeTestBase(ProblemsTest, EventsTestMixin): ...@@ -117,7 +124,7 @@ class ProblemTypeTestBase(ProblemsTest, EventsTestMixin):
Waits for the expected status indicator. Waits for the expected status indicator.
Args: Args:
status: one of ("correct", "incorrect", "unanswered) status: one of ("correct", "incorrect", "unanswered", "submitted")
""" """
msg = "Wait for status to be {}".format(status) msg = "Wait for status to be {}".format(status)
selector = ', '.join(self.status_indicators[status]) selector = ', '.join(self.status_indicators[status])
...@@ -381,12 +388,83 @@ class ProblemTypeTestMixin(ProblemTypeA11yTestMixin): ...@@ -381,12 +388,83 @@ class ProblemTypeTestMixin(ProblemTypeA11yTestMixin):
self.problem_page.wait_partial_notification() self.problem_page.wait_partial_notification()
class AnnotationProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): @ddt.ddt
class ProblemNeverShowCorrectnessMixin(object):
"""
Tests the effect of adding `show_correctness: never` to the sequence metadata
for subclasses of ProblemTypeTestMixin.
"""
sequential_metadata = {'show_correctness': 'never'}
@attr(shard=7)
@ddt.data('correct', 'incorrect', 'partially-correct')
def test_answer_says_submitted(self, correctness):
"""
Scenario: I can answer a problem <Correctness>ly
Given External graders respond "<Correctness>"
And I am viewing a "<ProblemType>" problem
in a subsection with show_correctness set to "never"
Then I should see a score of "N point(s) possible (ungraded, results hidden)"
When I answer a "<ProblemType>" problem "<Correctness>ly"
And the "<ProblemType>" problem displays only a "submitted" notification.
And I should see a score of "N point(s) possible (ungraded, results hidden)"
And a "problem_check" server event is emitted
And a "problem_check" browser event is emitted
"""
# Not all problems have partially correct solutions configured
if correctness == 'partially-correct' and not self.partially_correct:
raise SkipTest("Test incompatible with the current problem type")
# Problem progress text depends on points possible
possible = 'possible (ungraded, results hidden)'
if self.problem_points == 1:
problem_progress = '1 point {}'.format(possible)
else:
problem_progress = '{} points {}'.format(self.problem_points, possible)
# Make sure we're looking at the right problem
self.problem_page.wait_for(
lambda: self.problem_page.problem_name == self.problem_name,
"Make sure the correct problem is on the page"
)
# Learner can see that score will be hidden prior to submitting answer
self.assertEqual(self.problem_page.problem_progress_graded_value, problem_progress)
# Answer the problem correctly
self.answer_problem(correctness=correctness)
self.problem_page.click_submit()
self.wait_for_status('submitted')
self.problem_page.wait_submitted_notification()
# Score is still hidden after submitting answer
self.assertEqual(self.problem_page.problem_progress_graded_value, problem_progress)
# Check for corresponding tracking event
expected_events = [
{
'event_source': 'server',
'event_type': 'problem_check',
'username': self.username,
}, {
'event_source': 'browser',
'event_type': 'problem_check',
'username': self.username,
},
]
for event in expected_events:
self.wait_for_events(event_filter=event, number_of_matches=1)
class AnnotationProblemTypeBase(ProblemTypeTestBase):
""" """
TestCase Class for Annotation Problem Type ProblemTypeTestBase specialization for Annotation Problem Type
""" """
problem_name = 'ANNOTATION TEST PROBLEM' problem_name = 'ANNOTATION TEST PROBLEM'
problem_type = 'annotationresponse' problem_type = 'annotationresponse'
problem_points = 2
factory = AnnotationResponseXMLFactory() factory = AnnotationResponseXMLFactory()
partially_correct = True partially_correct = True
...@@ -411,13 +489,14 @@ class AnnotationProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -411,13 +489,14 @@ class AnnotationProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
'incorrect': ['span.incorrect'], 'incorrect': ['span.incorrect'],
'partially-correct': ['span.partially-correct'], 'partially-correct': ['span.partially-correct'],
'unanswered': ['span.unanswered'], 'unanswered': ['span.unanswered'],
'submitted': ['span.submitted'],
} }
def setUp(self, *args, **kwargs): def setUp(self, *args, **kwargs):
""" """
Additional setup for AnnotationProblemTypeTest Additional setup for AnnotationProblemTypeBase
""" """
super(AnnotationProblemTypeTest, self).setUp(*args, **kwargs) super(AnnotationProblemTypeBase, self).setUp(*args, **kwargs)
self.problem_page.a11y_audit.config.set_rules({ self.problem_page.a11y_audit.config.set_rules({
"ignore": [ "ignore": [
...@@ -443,9 +522,23 @@ class AnnotationProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -443,9 +522,23 @@ class AnnotationProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
).nth(choice).click() ).nth(choice).click()
class CheckboxProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): class AnnotationProblemTypeTest(AnnotationProblemTypeBase, ProblemTypeTestMixin):
"""
Standard tests for the Annotation Problem Type
"""
pass
class AnnotationProblemTypeNeverShowCorrectnessTest(AnnotationProblemTypeBase, ProblemNeverShowCorrectnessMixin):
"""
Ensure that correctness can be withheld for Annotation Problem Type problems.
"""
pass
class CheckboxProblemTypeBase(ProblemTypeTestBase):
""" """
TestCase Class for Checkbox Problem Type ProblemTypeTestBase specialization Checkbox Problem Type
""" """
problem_name = 'CHECKBOX TEST PROBLEM' problem_name = 'CHECKBOX TEST PROBLEM'
problem_type = 'checkbox' problem_type = 'checkbox'
...@@ -462,12 +555,6 @@ class CheckboxProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -462,12 +555,6 @@ class CheckboxProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
'explanation_text': 'This is explanation text' 'explanation_text': 'This is explanation text'
} }
def setUp(self, *args, **kwargs):
"""
Additional setup for CheckboxProblemTypeTest
"""
super(CheckboxProblemTypeTest, self).setUp(*args, **kwargs)
def answer_problem(self, correctness): def answer_problem(self, correctness):
""" """
Answer checkbox problem. Answer checkbox problem.
...@@ -481,6 +568,11 @@ class CheckboxProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -481,6 +568,11 @@ class CheckboxProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
self.problem_page.click_choice("choice_1") self.problem_page.click_choice("choice_1")
self.problem_page.click_choice("choice_3") self.problem_page.click_choice("choice_3")
class CheckboxProblemTypeTest(CheckboxProblemTypeBase, ProblemTypeTestMixin):
"""
Standard tests for the Checkbox Problem Type
"""
@attr(shard=7) @attr(shard=7)
def test_can_show_answer(self): def test_can_show_answer(self):
""" """
...@@ -498,9 +590,16 @@ class CheckboxProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -498,9 +590,16 @@ class CheckboxProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
self.problem_page.wait_for_show_answer_notification() self.problem_page.wait_for_show_answer_notification()
class MultipleChoiceProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): class CheckboxProblemTypeNeverShowCorrectnessTest(CheckboxProblemTypeBase, ProblemNeverShowCorrectnessMixin):
"""
Ensure that correctness can be withheld for Checkbox Problem Type problems.
"""
pass
class MultipleChoiceProblemTypeBase(ProblemTypeTestBase):
""" """
TestCase Class for Multiple Choice Problem Type ProblemTypeTestBase specialization Multiple Choice Problem Type
""" """
problem_name = 'MULTIPLE CHOICE TEST PROBLEM' problem_name = 'MULTIPLE CHOICE TEST PROBLEM'
problem_type = 'multiple choice' problem_type = 'multiple choice'
...@@ -518,14 +617,9 @@ class MultipleChoiceProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -518,14 +617,9 @@ class MultipleChoiceProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
'correct': ['label.choicegroup_correct'], 'correct': ['label.choicegroup_correct'],
'incorrect': ['label.choicegroup_incorrect', 'span.incorrect'], 'incorrect': ['label.choicegroup_incorrect', 'span.incorrect'],
'unanswered': ['span.unanswered'], 'unanswered': ['span.unanswered'],
'submitted': ['label.choicegroup_submitted', 'span.submitted'],
} }
def setUp(self, *args, **kwargs):
"""
Additional setup for MultipleChoiceProblemTypeTest
"""
super(MultipleChoiceProblemTypeTest, self).setUp(*args, **kwargs)
def answer_problem(self, correctness): def answer_problem(self, correctness):
""" """
Answer multiple choice problem. Answer multiple choice problem.
...@@ -535,6 +629,11 @@ class MultipleChoiceProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -535,6 +629,11 @@ class MultipleChoiceProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
else: else:
self.problem_page.click_choice("choice_choice_2") self.problem_page.click_choice("choice_choice_2")
class MultipleChoiceProblemTypeTest(MultipleChoiceProblemTypeBase, ProblemTypeTestMixin):
"""
Standard tests for the Multiple Choice Problem Type
"""
@attr(shard=7) @attr(shard=7)
def test_can_show_answer(self): def test_can_show_answer(self):
""" """
...@@ -565,9 +664,17 @@ class MultipleChoiceProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -565,9 +664,17 @@ class MultipleChoiceProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
self.problem_page.wait_for_show_answer_notification() self.problem_page.wait_for_show_answer_notification()
class RadioProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): class MultipleChoiceProblemTypeNeverShowCorrectnessTest(MultipleChoiceProblemTypeBase,
ProblemNeverShowCorrectnessMixin):
"""
Ensure that correctness can be withheld for Multiple Choice Problem Type problems.
"""
pass
class RadioProblemTypeBase(ProblemTypeTestBase):
""" """
TestCase Class for Radio Problem Type ProblemTypeTestBase specialization for Radio Problem Type
""" """
problem_name = 'RADIO TEST PROBLEM' problem_name = 'RADIO TEST PROBLEM'
problem_type = 'radio' problem_type = 'radio'
...@@ -586,14 +693,9 @@ class RadioProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -586,14 +693,9 @@ class RadioProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
'correct': ['label.choicegroup_correct'], 'correct': ['label.choicegroup_correct'],
'incorrect': ['label.choicegroup_incorrect', 'span.incorrect'], 'incorrect': ['label.choicegroup_incorrect', 'span.incorrect'],
'unanswered': ['span.unanswered'], 'unanswered': ['span.unanswered'],
'submitted': ['label.choicegroup_submitted', 'span.submitted'],
} }
def setUp(self, *args, **kwargs):
"""
Additional setup for RadioProblemTypeTest
"""
super(RadioProblemTypeTest, self).setUp(*args, **kwargs)
def answer_problem(self, correctness): def answer_problem(self, correctness):
""" """
Answer radio problem. Answer radio problem.
...@@ -604,9 +706,23 @@ class RadioProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -604,9 +706,23 @@ class RadioProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
self.problem_page.click_choice("choice_1") self.problem_page.click_choice("choice_1")
class DropDownProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): class RadioProblemTypeTest(RadioProblemTypeBase, ProblemTypeTestMixin):
"""
Standard tests for the Multiple Radio Problem Type
"""
pass
class RadioProblemTypeNeverShowCorrectnessTest(RadioProblemTypeBase, ProblemNeverShowCorrectnessMixin):
"""
Ensure that correctness can be withheld for Radio Problem Type problems.
"""
pass
class DropDownProblemTypeBase(ProblemTypeTestBase):
""" """
TestCase Class for Drop Down Problem Type ProblemTypeTestBase specialization for Drop Down Problem Type
""" """
problem_name = 'DROP DOWN TEST PROBLEM' problem_name = 'DROP DOWN TEST PROBLEM'
problem_type = 'drop down' problem_type = 'drop down'
...@@ -621,12 +737,6 @@ class DropDownProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -621,12 +737,6 @@ class DropDownProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
'correct_option': 'Option 2' 'correct_option': 'Option 2'
} }
def setUp(self, *args, **kwargs):
"""
Additional setup for DropDownProblemTypeTest
"""
super(DropDownProblemTypeTest, self).setUp(*args, **kwargs)
def answer_problem(self, correctness): def answer_problem(self, correctness):
""" """
Answer drop down problem. Answer drop down problem.
...@@ -637,9 +747,23 @@ class DropDownProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -637,9 +747,23 @@ class DropDownProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
select_option_by_text(selector_element, answer) select_option_by_text(selector_element, answer)
class StringProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): class DropDownProblemTypeTest(DropDownProblemTypeBase, ProblemTypeTestMixin):
"""
Standard tests for the Multiple Radio Problem Type
"""
pass
class DropDownProblemTypeNeverShowCorrectnessTest(DropDownProblemTypeBase, ProblemNeverShowCorrectnessMixin):
"""
Ensure that correctness can be withheld for Drop Down Problem Type problems.
"""
pass
class StringProblemTypeBase(ProblemTypeTestBase):
""" """
TestCase Class for String Problem Type ProblemTypeTestBase specialization for String Problem Type
""" """
problem_name = 'STRING TEST PROBLEM' problem_name = 'STRING TEST PROBLEM'
problem_type = 'string' problem_type = 'string'
...@@ -658,14 +782,9 @@ class StringProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -658,14 +782,9 @@ class StringProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
'correct': ['div.correct'], 'correct': ['div.correct'],
'incorrect': ['div.incorrect'], 'incorrect': ['div.incorrect'],
'unanswered': ['div.unanswered', 'div.unsubmitted'], 'unanswered': ['div.unanswered', 'div.unsubmitted'],
'submitted': ['span.submitted'],
} }
def setUp(self, *args, **kwargs):
"""
Additional setup for StringProblemTypeTest
"""
super(StringProblemTypeTest, self).setUp(*args, **kwargs)
def answer_problem(self, correctness): def answer_problem(self, correctness):
""" """
Answer string problem. Answer string problem.
...@@ -674,9 +793,23 @@ class StringProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -674,9 +793,23 @@ class StringProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
self.problem_page.fill_answer(textvalue) self.problem_page.fill_answer(textvalue)
class NumericalProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): class StringProblemTypeTest(StringProblemTypeBase, ProblemTypeTestMixin):
"""
Standard tests for the String Problem Type
"""
pass
class StringProblemTypeNeverShowCorrectnessTest(StringProblemTypeBase, ProblemNeverShowCorrectnessMixin):
"""
Ensure that correctness can be withheld for String Problem Type problems.
"""
pass
class NumericalProblemTypeBase(ProblemTypeTestBase):
""" """
TestCase Class for Numerical Problem Type ProblemTypeTestBase specialization for Numerical Problem Type
""" """
problem_name = 'NUMERICAL TEST PROBLEM' problem_name = 'NUMERICAL TEST PROBLEM'
problem_type = 'numerical' problem_type = 'numerical'
...@@ -695,14 +828,9 @@ class NumericalProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -695,14 +828,9 @@ class NumericalProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
'correct': ['div.correct'], 'correct': ['div.correct'],
'incorrect': ['div.incorrect'], 'incorrect': ['div.incorrect'],
'unanswered': ['div.unanswered', 'div.unsubmitted'], 'unanswered': ['div.unanswered', 'div.unsubmitted'],
'submitted': ['div.submitted'],
} }
def setUp(self, *args, **kwargs):
"""
Additional setup for NumericalProblemTypeTest
"""
super(NumericalProblemTypeTest, self).setUp(*args, **kwargs)
def answer_problem(self, correctness): def answer_problem(self, correctness):
""" """
Answer numerical problem. Answer numerical problem.
...@@ -716,6 +844,11 @@ class NumericalProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -716,6 +844,11 @@ class NumericalProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
textvalue = str(random.randint(-2, 2)) textvalue = str(random.randint(-2, 2))
self.problem_page.fill_answer(textvalue) self.problem_page.fill_answer(textvalue)
class NumericalProblemTypeTest(NumericalProblemTypeBase, ProblemTypeTestMixin):
"""
Standard tests for the Numerical Problem Type
"""
def test_error_input_gentle_alert(self): def test_error_input_gentle_alert(self):
""" """
Scenario: I can answer a problem with erroneous input and will see a gentle alert Scenario: I can answer a problem with erroneous input and will see a gentle alert
...@@ -741,9 +874,16 @@ class NumericalProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -741,9 +874,16 @@ class NumericalProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
self.problem_page.wait_for_focus_on_problem_meta() self.problem_page.wait_for_focus_on_problem_meta()
class FormulaProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): class NumericalProblemTypeNeverShowCorrectnessTest(NumericalProblemTypeBase, ProblemNeverShowCorrectnessMixin):
"""
Ensure that correctness can be withheld for Numerical Problem Type problems.
"""
pass
class FormulaProblemTypeBase(ProblemTypeTestBase):
""" """
TestCase Class for Formula Problem Type ProblemTypeTestBase specialization for Formula Problem Type
""" """
problem_name = 'FORMULA TEST PROBLEM' problem_name = 'FORMULA TEST PROBLEM'
problem_type = 'formula' problem_type = 'formula'
...@@ -764,14 +904,9 @@ class FormulaProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -764,14 +904,9 @@ class FormulaProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
'correct': ['div.correct'], 'correct': ['div.correct'],
'incorrect': ['div.incorrect'], 'incorrect': ['div.incorrect'],
'unanswered': ['div.unanswered', 'div.unsubmitted'], 'unanswered': ['div.unanswered', 'div.unsubmitted'],
'submitted': ['div.submitted'],
} }
def setUp(self, *args, **kwargs):
"""
Additional setup for FormulaProblemTypeTest
"""
super(FormulaProblemTypeTest, self).setUp(*args, **kwargs)
def answer_problem(self, correctness): def answer_problem(self, correctness):
""" """
Answer formula problem. Answer formula problem.
...@@ -780,12 +915,27 @@ class FormulaProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -780,12 +915,27 @@ class FormulaProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
self.problem_page.fill_answer(textvalue) self.problem_page.fill_answer(textvalue)
class ScriptProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): class FormulaProblemTypeTest(FormulaProblemTypeBase, ProblemTypeTestMixin):
"""
Standard tests for the Formula Problem Type
"""
pass
class FormulaProblemTypeNeverShowCorrectnessTest(FormulaProblemTypeBase, ProblemNeverShowCorrectnessMixin):
"""
Ensure that correctness can be withheld for Formula Problem Type problems.
"""
pass
class ScriptProblemTypeBase(ProblemTypeTestBase):
""" """
TestCase Class for Script Problem Type ProblemTypeTestBase specialization for Script Problem Type
""" """
problem_name = 'SCRIPT TEST PROBLEM' problem_name = 'SCRIPT TEST PROBLEM'
problem_type = 'script' problem_type = 'script'
problem_points = 2
partially_correct = False partially_correct = False
factory = CustomResponseXMLFactory() factory = CustomResponseXMLFactory()
...@@ -811,14 +961,9 @@ class ScriptProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -811,14 +961,9 @@ class ScriptProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
'correct': ['div.correct'], 'correct': ['div.correct'],
'incorrect': ['div.incorrect'], 'incorrect': ['div.incorrect'],
'unanswered': ['div.unanswered', 'div.unsubmitted'], 'unanswered': ['div.unanswered', 'div.unsubmitted'],
'submitted': ['div.submitted'],
} }
def setUp(self, *args, **kwargs):
"""
Additional setup for ScriptProblemTypeTest
"""
super(ScriptProblemTypeTest, self).setUp(*args, **kwargs)
def answer_problem(self, correctness): def answer_problem(self, correctness):
""" """
Answer script problem. Answer script problem.
...@@ -836,6 +981,20 @@ class ScriptProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -836,6 +981,20 @@ class ScriptProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
self.problem_page.fill_answer(second_addend, input_num=1) self.problem_page.fill_answer(second_addend, input_num=1)
class ScriptProblemTypeTest(ScriptProblemTypeBase, ProblemTypeTestMixin):
"""
Standard tests for the Script Problem Type
"""
pass
class ScriptProblemTypeNeverShowCorrectnessTest(ScriptProblemTypeBase, ProblemNeverShowCorrectnessMixin):
"""
Ensure that correctness can be withheld for Script Problem Type problems.
"""
pass
class JSInputTypeTest(ProblemTypeTestBase, ProblemTypeA11yTestMixin): class JSInputTypeTest(ProblemTypeTestBase, ProblemTypeA11yTestMixin):
""" """
TestCase Class for jsinput (custom JavaScript) problem type. TestCase Class for jsinput (custom JavaScript) problem type.
...@@ -859,9 +1018,9 @@ class JSInputTypeTest(ProblemTypeTestBase, ProblemTypeA11yTestMixin): ...@@ -859,9 +1018,9 @@ class JSInputTypeTest(ProblemTypeTestBase, ProblemTypeA11yTestMixin):
raise NotImplementedError() raise NotImplementedError()
class CodeProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): class CodeProblemTypeBase(ProblemTypeTestBase):
""" """
TestCase Class for Code Problem Type ProblemTypeTestBase specialization for Code Problem Type
""" """
problem_name = 'CODE TEST PROBLEM' problem_name = 'CODE TEST PROBLEM'
problem_type = 'code' problem_type = 'code'
...@@ -879,6 +1038,7 @@ class CodeProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -879,6 +1038,7 @@ class CodeProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
'correct': ['.grader-status .correct ~ .debug'], 'correct': ['.grader-status .correct ~ .debug'],
'incorrect': ['.grader-status .incorrect ~ .debug'], 'incorrect': ['.grader-status .incorrect ~ .debug'],
'unanswered': ['.grader-status .unanswered ~ .debug'], 'unanswered': ['.grader-status .unanswered ~ .debug'],
'submitted': ['.grader-status .submitted ~ .debug'],
} }
def answer_problem(self, correctness): def answer_problem(self, correctness):
...@@ -895,6 +1055,11 @@ class CodeProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -895,6 +1055,11 @@ class CodeProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
# (configured in the problem XML above) # (configured in the problem XML above)
pass pass
class CodeProblemTypeTest(CodeProblemTypeBase, ProblemTypeTestMixin):
"""
Standard tests for the Code Problem Type
"""
def test_answer_incorrectly(self): def test_answer_incorrectly(self):
""" """
Overridden for script test because the testing grader always responds Overridden for script test because the testing grader always responds
...@@ -924,7 +1089,14 @@ class CodeProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -924,7 +1089,14 @@ class CodeProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
pass pass
class ChoiceTextProbelmTypeTestBase(ProblemTypeTestBase): class CodeProblemTypeNeverShowCorrectnessTest(CodeProblemTypeBase, ProblemNeverShowCorrectnessMixin):
"""
Ensure that correctness can be withheld for Code Problem Type problems.
"""
pass
class ChoiceTextProblemTypeTestBase(ProblemTypeTestBase):
""" """
Base class for "Choice + Text" Problem Types. Base class for "Choice + Text" Problem Types.
(e.g. RadioText, CheckboxText) (e.g. RadioText, CheckboxText)
...@@ -961,9 +1133,9 @@ class ChoiceTextProbelmTypeTestBase(ProblemTypeTestBase): ...@@ -961,9 +1133,9 @@ class ChoiceTextProbelmTypeTestBase(ProblemTypeTestBase):
self._fill_input_text(input_value, choice) self._fill_input_text(input_value, choice)
class RadioTextProblemTypeTest(ChoiceTextProbelmTypeTestBase, ProblemTypeTestMixin): class RadioTextProblemTypeBase(ChoiceTextProblemTypeTestBase):
""" """
TestCase Class for Radio Text Problem Type ProblemTypeTestBase specialization for Radio Text Problem Type
""" """
problem_name = 'RADIO TEXT TEST PROBLEM' problem_name = 'RADIO TEXT TEST PROBLEM'
problem_type = 'radio_text' problem_type = 'radio_text'
...@@ -986,13 +1158,14 @@ class RadioTextProblemTypeTest(ChoiceTextProbelmTypeTestBase, ProblemTypeTestMix ...@@ -986,13 +1158,14 @@ class RadioTextProblemTypeTest(ChoiceTextProbelmTypeTestBase, ProblemTypeTestMix
'correct': ['section.choicetextgroup_correct'], 'correct': ['section.choicetextgroup_correct'],
'incorrect': ['section.choicetextgroup_incorrect', 'span.incorrect'], 'incorrect': ['section.choicetextgroup_incorrect', 'span.incorrect'],
'unanswered': ['span.unanswered'], 'unanswered': ['span.unanswered'],
'submitted': ['section.choicetextgroup_submitted', 'span.submitted'],
} }
def setUp(self, *args, **kwargs): def setUp(self, *args, **kwargs):
""" """
Additional setup for RadioTextProblemTypeTest Additional setup for RadioTextProblemTypeBase
""" """
super(RadioTextProblemTypeTest, self).setUp(*args, **kwargs) super(RadioTextProblemTypeBase, self).setUp(*args, **kwargs)
self.problem_page.a11y_audit.config.set_rules({ self.problem_page.a11y_audit.config.set_rules({
"ignore": [ "ignore": [
...@@ -1003,9 +1176,23 @@ class RadioTextProblemTypeTest(ChoiceTextProbelmTypeTestBase, ProblemTypeTestMix ...@@ -1003,9 +1176,23 @@ class RadioTextProblemTypeTest(ChoiceTextProbelmTypeTestBase, ProblemTypeTestMix
}) })
class CheckboxTextProblemTypeTest(ChoiceTextProbelmTypeTestBase, ProblemTypeTestMixin): class RadioTextProblemTypeTest(RadioTextProblemTypeBase, ProblemTypeTestMixin):
"""
Standard tests for the Radio Text Problem Type
"""
pass
class RadioTextProblemTypeNeverShowCorrectnessTest(RadioTextProblemTypeBase, ProblemNeverShowCorrectnessMixin):
""" """
TestCase Class for Checkbox Text Problem Type Ensure that correctness can be withheld for Radio + Text Problem Type problems.
"""
pass
class CheckboxTextProblemTypeBase(ChoiceTextProblemTypeTestBase):
"""
ProblemTypeTestBase specialization for Checkbox Text Problem Type
""" """
problem_name = 'CHECKBOX TEXT TEST PROBLEM' problem_name = 'CHECKBOX TEXT TEST PROBLEM'
problem_type = 'checkbox_text' problem_type = 'checkbox_text'
...@@ -1025,9 +1212,9 @@ class CheckboxTextProblemTypeTest(ChoiceTextProbelmTypeTestBase, ProblemTypeTest ...@@ -1025,9 +1212,9 @@ class CheckboxTextProblemTypeTest(ChoiceTextProbelmTypeTestBase, ProblemTypeTest
def setUp(self, *args, **kwargs): def setUp(self, *args, **kwargs):
""" """
Additional setup for CheckboxTextProblemTypeTest Additional setup for CheckboxTextProblemTypeBase
""" """
super(CheckboxTextProblemTypeTest, self).setUp(*args, **kwargs) super(CheckboxTextProblemTypeBase, self).setUp(*args, **kwargs)
self.problem_page.a11y_audit.config.set_rules({ self.problem_page.a11y_audit.config.set_rules({
"ignore": [ "ignore": [
...@@ -1038,9 +1225,23 @@ class CheckboxTextProblemTypeTest(ChoiceTextProbelmTypeTestBase, ProblemTypeTest ...@@ -1038,9 +1225,23 @@ class CheckboxTextProblemTypeTest(ChoiceTextProbelmTypeTestBase, ProblemTypeTest
}) })
class ImageProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): class CheckboxTextProblemTypeTest(CheckboxTextProblemTypeBase, ProblemTypeTestMixin):
"""
Standard tests for the Checkbox Text Problem Type
"""
pass
class CheckboxTextProblemTypeNeverShowCorrectnessTest(CheckboxTextProblemTypeBase, ProblemNeverShowCorrectnessMixin):
""" """
TestCase Class for Image Problem Type Ensure that correctness can be withheld for Checkbox + Text Problem Type problems.
"""
pass
class ImageProblemTypeBase(ProblemTypeTestBase):
"""
ProblemTypeTestBase specialization for Image Problem Type
""" """
problem_name = 'IMAGE TEST PROBLEM' problem_name = 'IMAGE TEST PROBLEM'
problem_type = 'image' problem_type = 'image'
...@@ -1071,9 +1272,23 @@ class ImageProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -1071,9 +1272,23 @@ class ImageProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
chain.perform() chain.perform()
class SymbolicProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): class ImageProblemTypeTest(ImageProblemTypeBase, ProblemTypeTestMixin):
"""
Standard tests for the Image Problem Type
"""
pass
class ImageProblemTypeNeverShowCorrectnessTest(ImageProblemTypeBase, ProblemNeverShowCorrectnessMixin):
"""
Ensure that correctness can be withheld for Image Problem Type problems.
"""
pass
class SymbolicProblemTypeBase(ProblemTypeTestBase):
""" """
TestCase Class for Symbolic Problem Type ProblemTypeTestBase specialization for Symbolic Problem Type
""" """
problem_name = 'SYMBOLIC TEST PROBLEM' problem_name = 'SYMBOLIC TEST PROBLEM'
problem_type = 'symbolicresponse' problem_type = 'symbolicresponse'
...@@ -1090,6 +1305,7 @@ class SymbolicProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -1090,6 +1305,7 @@ class SymbolicProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
'correct': ['div.capa_inputtype div.correct'], 'correct': ['div.capa_inputtype div.correct'],
'incorrect': ['div.capa_inputtype div.incorrect'], 'incorrect': ['div.capa_inputtype div.incorrect'],
'unanswered': ['div.capa_inputtype div.unanswered'], 'unanswered': ['div.capa_inputtype div.unanswered'],
'submitted': ['div.capa_inputtype div.submitted'],
} }
def answer_problem(self, correctness): def answer_problem(self, correctness):
...@@ -1098,3 +1314,17 @@ class SymbolicProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -1098,3 +1314,17 @@ class SymbolicProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
""" """
choice = "2*x+3*y" if correctness == 'correct' else "3*a+4*b" choice = "2*x+3*y" if correctness == 'correct' else "3*a+4*b"
self.problem_page.fill_answer(choice) self.problem_page.fill_answer(choice)
class SymbolicProblemTypeTest(SymbolicProblemTypeBase, ProblemTypeTestMixin):
"""
Standard tests for the Symbolic Problem Type
"""
pass
class SymbolicProblemTypeNeverShowCorrectnessTest(SymbolicProblemTypeBase, ProblemNeverShowCorrectnessMixin):
"""
Ensure that correctness can be withheld for Symbolic Problem Type problems.
"""
pass
...@@ -98,6 +98,15 @@ from openedx.core.djangolib.markup import HTML ...@@ -98,6 +98,15 @@ from openedx.core.djangolib.markup import HTML
notification_message=answer_notification_message" notification_message=answer_notification_message"
/> />
% endif % endif
% if 'submitted' == answer_notification_type:
<%include file="problem_notifications.html" args="
notification_type='general',
notification_icon='fa-info-circle',
notification_name='submit',
is_hidden=False,
notification_message=answer_notification_message"
/>
% endif
% endif % endif
<%include file="problem_notifications.html" args=" <%include file="problem_notifications.html" args="
notification_type='warning', notification_type='warning',
......
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