Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: optional xblocks #638

Open
wants to merge 10 commits into
base: opencraft-release/palm.1
Choose a base branch
from
48 changes: 48 additions & 0 deletions cms/djangoapps/contentstore/tests/test_utils.py
Expand Up @@ -336,6 +336,54 @@ def test_no_inheritance_for_orphan(self):
self.assertFalse(utils.ancestor_has_staff_lock(self.orphan))


class InheritedOptionalCompletionTest(CourseTestCase):
"""Tests for determining if an xblock inherits optional completion."""

def setUp(self):
super().setUp()
chapter = BlockFactory.create(category='chapter', parent=self.course)
sequential = BlockFactory.create(category='sequential', parent=chapter)
vertical = BlockFactory.create(category='vertical', parent=sequential)
html = BlockFactory.create(category='html', parent=vertical)
problem = BlockFactory.create(
category='problem', parent=vertical, data="<problem></problem>"
)
self.chapter = self.store.get_item(chapter.location)
self.sequential = self.store.get_item(sequential.location)
self.vertical = self.store.get_item(vertical.location)
self.html = self.store.get_item(html.location)
self.problem = self.store.get_item(problem.location)
self.orphan = BlockFactory.create(category='vertical', parent_location=self.sequential.location)

def set_optional_completion(self, xblock, value):
""" Sets optional_completion to specified value and calls update_item to persist the change. """
xblock.optional_completion = value
self.store.update_item(xblock, self.user.id)

def update_optional_completions(self, chapter, sequential, vertical):
self.set_optional_completion(self.chapter, chapter)
self.set_optional_completion(self.sequential, sequential)
self.set_optional_completion(self.vertical, vertical)

def test_no_inheritance(self):
"""Tests that vertical with no optional ancestors does not have an inherited optional completion"""
self.update_optional_completions(False, False, False)
self.assertFalse(utils.ancestor_has_optional_completion(self.vertical))
self.update_optional_completions(False, False, True)
self.assertFalse(utils.ancestor_has_optional_completion(self.vertical))

def test_inheritance_in_optional_subsection(self):
"""Tests that a vertical in an optional subsection has an inherited optional completion"""
self.update_optional_completions(False, True, False)
self.assertTrue(utils.ancestor_has_optional_completion(self.vertical))
self.update_optional_completions(False, True, True)
self.assertTrue(utils.ancestor_has_optional_completion(self.vertical))

def test_no_inheritance_for_orphan(self):
"""Tests that an orphaned xblock does not inherit optional completion"""
self.assertFalse(utils.ancestor_has_optional_completion(self.orphan))


class GroupVisibilityTest(CourseTestCase):
"""
Test content group access rules.
Expand Down
16 changes: 15 additions & 1 deletion cms/djangoapps/contentstore/utils.py
Expand Up @@ -342,7 +342,7 @@ def find_staff_lock_source(xblock):

def ancestor_has_staff_lock(xblock, parent_xblock=None):
"""
Returns True iff one of xblock's ancestors has staff lock.
Returns True if one of xblock's ancestors has staff lock.
Can avoid mongo query by passing in parent_xblock.
"""
if parent_xblock is None:
Expand All @@ -354,6 +354,20 @@ def ancestor_has_staff_lock(xblock, parent_xblock=None):
return parent_xblock.visible_to_staff_only


def ancestor_has_optional_completion(xblock, parent_xblock=None):
"""
Returns True if one of xblock's ancestors has optional completion.
Agrendalath marked this conversation as resolved.
Show resolved Hide resolved
Can avoid mongo query by passing in parent_xblock.
"""
if parent_xblock is None:
parent_location = modulestore().get_parent_location(xblock.location,
revision=ModuleStoreEnum.RevisionOption.draft_preferred)
if not parent_location:
return False
parent_xblock = modulestore().get_item(parent_location)
return parent_xblock.optional_completion


def reverse_url(handler_name, key_name=None, key_value=None, kwargs=None):
"""
Creates the URL for the given handler.
Expand Down
4 changes: 4 additions & 0 deletions cms/djangoapps/contentstore/views/block.py
Expand Up @@ -60,6 +60,7 @@
from xmodule.x_module import AUTHOR_VIEW, PREVIEW_VIEWS, STUDENT_VIEW, STUDIO_VIEW # lint-amnesty, pylint: disable=wrong-import-order

from ..utils import (
ancestor_has_optional_completion,
ancestor_has_staff_lock,
find_release_date_source,
find_staff_lock_source,
Expand Down Expand Up @@ -1321,6 +1322,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
'group_access': xblock.group_access,
'user_partitions': user_partitions,
'show_correctness': xblock.show_correctness,
'optional_completion': xblock.optional_completion,
})

if xblock.category == 'sequential':
Expand Down Expand Up @@ -1405,6 +1407,8 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
xblock_info['ancestor_has_staff_lock'] = False

if course_outline:
xblock_info['ancestor_has_optional_completion'] = ancestor_has_optional_completion(xblock, parent_xblock)

if xblock_info['has_explicit_staff_lock']:
xblock_info['staff_only_message'] = True
elif child_info and child_info['children']:
Expand Down
2 changes: 1 addition & 1 deletion cms/djangoapps/contentstore/views/tests/test_block.py
Expand Up @@ -2630,7 +2630,7 @@ def test_json_responses(self):

@ddt.data(
(ModuleStoreEnum.Type.split, 3, 3),
(ModuleStoreEnum.Type.mongo, 8, 12),
(ModuleStoreEnum.Type.mongo, 10, 14),
)
@ddt.unpack
def test_xblock_outline_handler_mongo_calls(self, store_type, chapter_queries, chapter_queries_1):
Expand Down
1 change: 1 addition & 0 deletions cms/djangoapps/models/settings/course_metadata.py
Expand Up @@ -78,6 +78,7 @@ class CourseMetadata:
'highlights_enabled_for_messaging',
'is_onboarding_exam',
'discussions_settings',
'optional_completion',
]

@classmethod
Expand Down
4 changes: 4 additions & 0 deletions cms/static/js/models/xblock_info.js
Expand Up @@ -120,6 +120,10 @@ define(
*/
ancestor_has_staff_lock: null,
/**
* True if this any of this xblock's ancestors are optional for completion.
*/
ancestor_has_optional_completion: null,
/**
* The xblock which is determining the staff lock value. For instance, for a unit,
* this will either be the parent subsection or the grandparent section.
* This can be null if the xblock has no inherited staff lock. Will only be present if
Expand Down
176 changes: 175 additions & 1 deletion cms/static/js/spec/views/pages/course_outline_spec.js
Expand Up @@ -313,7 +313,7 @@ describe('CourseOutlinePage', function() {
'staff-lock-editor', 'unit-access-editor', 'discussion-editor', 'content-visibility-editor',
'settings-modal-tabs', 'timed-examination-preference-editor', 'access-editor',
'show-correctness-editor', 'highlights-editor', 'highlights-enable-editor',
'course-highlights-enable'
'course-highlights-enable', 'optional-completion-editor'
]);
appendSetFixtures(mockOutlinePage);
mockCourseJSON = createMockCourseJSON({}, [
Expand Down Expand Up @@ -1021,6 +1021,62 @@ describe('CourseOutlinePage', function() {
);
expect($modalWindow.find('.outline-subsection').length).toBe(2);
});

it('hides optional completion checkbox by default', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.section-header-actions .configure-button').click();
expect($('.edit-optional-completion')).not.toExist();
});

describe('supports optional completion and', function () {
beforeEach(function() {
window.course.attributes.completion_tracking_enabled = true;
});

it('shows optional completion checkbox unchecked by default', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.section-header-actions .configure-button').click();
expect($('.edit-optional-completion')).toExist();
expect($('#optional_completion').is(':checked')).toBe(false);
});

it('shows optional completion checkbox checked', function() {
var mockCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON({optional_completion: true})
]);
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.section-header-actions .configure-button').click();
expect($('#optional_completion').is(':disabled')).toBe(false);
expect($('#optional_completion').is(':checked')).toBe(true);
});

it('disables optional completion checkbox when the parent uses optional completion', function() {
var mockCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON({ancestor_has_optional_completion: true})
]);
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.section-header-actions .configure-button').click();
expect($('#optional_completion').is(':disabled')).toBe(true);
});

it('sets optional completion to null instead of false', function() {
var mockCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON({optional_completion: true})
]);
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.section-header-actions .configure-button').click();
expect($('#optional_completion').is(':checked')).toBe(true);
$('#optional_completion').click()
expect($('#optional_completion').is(':checked')).toBe(false);
$('.wrapper-modal-window .action-save').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-section', {
publish: 'republish',
metadata: {
optional_completion: null
}
});
});
});
});

describe('Subsection', function() {
Expand Down Expand Up @@ -2321,6 +2377,76 @@ describe('CourseOutlinePage', function() {
);
});
})

it('hides optional completion checkbox by default', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
expect($('.edit-optional-completion')).not.toExist();
});

describe('supports optional completion and', function () {
beforeEach(function() {
window.course.attributes.completion_tracking_enabled = true;
});

it('shows optional completion checkbox unchecked by default', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
expect($('.edit-optional-completion')).toExist();
expect($('#optional_completion').is(':checked')).toBe(false);
});

it('shows optional completion checkbox checked', function() {
var mockCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({optional_completion: true}, [])
])
]);
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
expect($('#optional_completion').is(':disabled')).toBe(false);
expect($('#optional_completion').is(':checked')).toBe(true);
});

it('disables optional completion checkbox when the parent uses optional completion', function() {
var mockCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({ancestor_has_optional_completion: true}, [])
])
]);
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
expect($('#optional_completion').is(':disabled')).toBe(true);
});

it('sets optional completion to null instead of false', function() {
var mockCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({optional_completion: true}, [])
])
]);
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
expect($('#optional_completion').is(':checked')).toBe(true);
$('#optional_completion').click()
expect($('#optional_completion').is(':checked')).toBe(false);
$('.wrapper-modal-window .action-save').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection', {
publish: 'republish',
graderType: 'notgraded',
isPrereq: false,
metadata: {
optional_completion: null,
due: null,
is_practice_exam: false,
is_time_limited: false,
is_proctored_enabled: false,
default_time_limit_minutes: null,
is_onboarding_exam: false,
}
});
});
});
});

// Note: most tests for units can be found in Bok Choy
Expand Down Expand Up @@ -2437,6 +2563,54 @@ describe('CourseOutlinePage', function() {
])
]);
});

it('hides optional completion checkbox by default', function() {
getUnitStatus({}, {});
outlinePage.$('.outline-unit .configure-button').click();
expect($('.edit-optional-completion')).not.toExist();
});

describe('supports optional completion and', function () {
beforeEach(function() {
window.course.attributes.completion_tracking_enabled = true;
});

it('shows optional completion checkbox unchecked by default', function() {
getUnitStatus({}, {});
outlinePage.$('.outline-unit .configure-button').click();
expect($('.edit-optional-completion')).toExist();
expect($('#optional_completion').is(':checked')).toBe(false);
});

it('shows optional completion checkbox checked', function() {
getUnitStatus({optional_completion: true}, {});
outlinePage.$('.outline-unit .configure-button').click();
expect($('#optional_completion').is(':disabled')).toBe(false);
expect($('#optional_completion').is(':checked')).toBe(true);
});

it('disables optional completion checkbox when the parent uses optional completion', function() {
getUnitStatus({ancestor_has_optional_completion: true}, {});
outlinePage.$('.outline-unit .configure-button').click();
expect($('#optional_completion').is(':disabled')).toBe(true);
});

it('sets optional completion to null instead of false', function() {
getUnitStatus({optional_completion: true}, {});
outlinePage.$('.outline-unit .configure-button').click();
expect($('#optional_completion').is(':checked')).toBe(true);
$('#optional_completion').click()
expect($('#optional_completion').is(':checked')).toBe(false);
$('.wrapper-modal-window .action-save').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-unit', {
publish: 'republish',
metadata: {
visible_to_staff_only: null,
optional_completion: null
}
});
});
});
});

describe('Date and Time picker', function() {
Expand Down