Skip to content

Commit

Permalink
feat: Allow delete course content in Studio only for admin users
Browse files Browse the repository at this point in the history
(cherry picked from commit c812a6c1d5c0961900507a6e7abe3d0f3b8a7570)
(cherry picked from commit 004f2fe)
  • Loading branch information
Sandeep Kumar Choudhary authored and xitij2000 committed Jun 23, 2023
1 parent 43c635b commit 653d353
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 9 deletions.
10 changes: 10 additions & 0 deletions cms/djangoapps/contentstore/config/waffle.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,13 @@
# .. toggle_warning: Flag course_experience.relative_dates should also be active for relative dates functionalities to work.
# .. toggle_tickets: https://openedx.atlassian.net/browse/AA-844
CUSTOM_RELATIVE_DATES = CourseWaffleFlag(f'{WAFFLE_NAMESPACE}.custom_relative_dates', __name__)

# .. toggle_name: studio.prevent_staff_structure_deletion
# .. toggle_implementation: WaffleFlag
# .. toggle_default: False
# .. toggle_description: Prevents staff from deleting course structures
# .. toggle_use_cases: opt_in
# .. toggle_creation_date: 2021-06-25
PREVENT_STAFF_STRUCTURE_DELETION = WaffleFlag(
f'{WAFFLE_NAMESPACE}.prevent_staff_structure_deletion', __name__, LOG_PREFIX
)
10 changes: 10 additions & 0 deletions cms/djangoapps/contentstore/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
Permission definitions for the contentstore djangoapp
"""

from bridgekeeper import perms

from lms.djangoapps.courseware.rules import HasRolesRule

DELETE_COURSE_CONTENT = 'contentstore.delete_course_content'
perms[DELETE_COURSE_CONTENT] = HasRolesRule('instructor')
9 changes: 8 additions & 1 deletion cms/djangoapps/contentstore/views/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
from xblock.core import XBlock
from xblock.fields import Scope

from cms.djangoapps.contentstore.config.waffle import SHOW_REVIEW_RULES_FLAG
from cms.djangoapps.contentstore.config.waffle import PREVENT_STAFF_STRUCTURE_DELETION, SHOW_REVIEW_RULES_FLAG
from cms.djangoapps.contentstore.permissions import DELETE_COURSE_CONTENT
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
from cms.lib.xblock.authoring_mixin import VISIBILITY_VIEW
from common.djangoapps.edxmako.services import MakoService
Expand Down Expand Up @@ -1372,6 +1373,12 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
else:
xblock_info['staff_only_message'] = False

xblock_info['show_delete_button'] = True
if PREVENT_STAFF_STRUCTURE_DELETION.is_enabled():
xblock_info['show_delete_button'] = (
user.has_perm(DELETE_COURSE_CONTENT, xblock) if user is not None else False
)

xblock_info['has_partition_group_components'] = has_children_visible_to_specific_partition_groups(
xblock
)
Expand Down
147 changes: 147 additions & 0 deletions cms/djangoapps/contentstore/views/tests/test_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from openedx_events.content_authoring.signals import XBLOCK_DUPLICATED
from openedx_events.tests.utils import OpenEdxEventsTestMixin
from edx_proctoring.exceptions import ProctoredExamNotFoundException
from edx_toggles.toggles.testutils import override_waffle_flag
from opaque_keys import InvalidKeyError
from opaque_keys.edx.asides import AsideUsageKeyV2
from opaque_keys.edx.keys import CourseKey, UsageKey
Expand Down Expand Up @@ -49,6 +50,8 @@
from cms.djangoapps.contentstore.tests.utils import CourseTestCase
from cms.djangoapps.contentstore.utils import reverse_course_url, reverse_usage_url
from cms.djangoapps.contentstore.views import block as item_module
from cms.djangoapps.contentstore.config.waffle import PREVENT_STAFF_STRUCTURE_DELETION
from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole, CourseCreatorRole
from common.djangoapps.student.tests.factories import StaffFactory, UserFactory
from common.djangoapps.xblock_django.models import (
XBlockConfiguration,
Expand Down Expand Up @@ -3473,3 +3476,147 @@ def test_self_paced_item_visibility_state(self, store_type):
# Check that in self paced course content has live state now
xblock_info = self._get_xblock_info(chapter.location)
self._verify_visibility_state(xblock_info, VisibilityState.live)

def test_staff_show_delete_button(self):
"""
Test delete button is *not visible* to user with CourseStaffRole
"""
# Add user as course staff
CourseStaffRole(self.course_key).add_users(self.user)

# Get xblock outline
xblock_info = create_xblock_info(
self.course,
include_child_info=True,
course_outline=True,
include_children_predicate=lambda xblock: not xblock.category == 'vertical',
user=self.user
)
self.assertTrue(xblock_info['show_delete_button'])

def test_staff_show_delete_button_with_waffle(self):
"""
Test delete button is *not visible* to user with CourseStaffRole and
PREVENT_STAFF_STRUCTURE_DELETION waffle set
"""
# Add user as course staff
CourseStaffRole(self.course_key).add_users(self.user)

with override_waffle_flag(PREVENT_STAFF_STRUCTURE_DELETION, active=True):
# Get xblock outline
xblock_info = create_xblock_info(
self.course,
include_child_info=True,
course_outline=True,
include_children_predicate=lambda xblock: not xblock.category == 'vertical',
user=self.user
)

self.assertFalse(xblock_info['show_delete_button'])

def test_no_user_show_delete_button(self):
"""
Test delete button is *visible* when user attribute is not set on
xblock. This happens with ajax requests.
"""
# Get xblock outline
xblock_info = create_xblock_info(
self.course,
include_child_info=True,
course_outline=True,
include_children_predicate=lambda xblock: not xblock.category == 'vertical',
user=None
)
self.assertTrue(xblock_info['show_delete_button'])

def test_no_user_show_delete_button_with_waffle(self):
"""
Test delete button is *visible* when user attribute is not set on
xblock (this happens with ajax requests) and PREVENT_STAFF_STRUCTURE_DELETION waffle set.
"""

with override_waffle_flag(PREVENT_STAFF_STRUCTURE_DELETION, active=True):
# Get xblock outline
xblock_info = create_xblock_info(
self.course,
include_child_info=True,
course_outline=True,
include_children_predicate=lambda xblock: not xblock.category == 'vertical',
user=None
)

self.assertFalse(xblock_info['show_delete_button'])

def test_instructor_show_delete_button(self):
"""
Test delete button is *visible* to user with CourseInstructorRole only
"""
# Add user as course instructor
CourseInstructorRole(self.course_key).add_users(self.user)

# Get xblock outline
xblock_info = create_xblock_info(
self.course,
include_child_info=True,
course_outline=True,
include_children_predicate=lambda xblock: not xblock.category == 'vertical',
user=self.user
)
self.assertTrue(xblock_info['show_delete_button'])

def test_instructor_show_delete_button_with_waffle(self):
"""
Test delete button is *visible* to user with CourseInstructorRole only
and PREVENT_STAFF_STRUCTURE_DELETION waffle set
"""
# Add user as course instructor
CourseInstructorRole(self.course_key).add_users(self.user)

with override_waffle_flag(PREVENT_STAFF_STRUCTURE_DELETION, active=True):
# Get xblock outline
xblock_info = create_xblock_info(
self.course,
include_child_info=True,
course_outline=True,
include_children_predicate=lambda xblock: not xblock.category == 'vertical',
user=self.user
)

self.assertTrue(xblock_info['show_delete_button'])

def test_creator_show_delete_button(self):
"""
Test delete button is *visible* to user with CourseInstructorRole only
"""
# Add user as course creator
CourseCreatorRole(self.course_key).add_users(self.user)

# Get xblock outline
xblock_info = create_xblock_info(
self.course,
include_child_info=True,
course_outline=True,
include_children_predicate=lambda xblock: not xblock.category == 'vertical',
user=self.user
)
self.assertTrue(xblock_info['show_delete_button'])

def test_creator_show_delete_button_with_waffle(self):
"""
Test delete button is *visible* to user with CourseInstructorRole only
and PREVENT_STAFF_STRUCTURE_DELETION waffle set
"""
# Add user as course creator
CourseCreatorRole(self.course_key).add_users(self.user)

with override_waffle_flag(PREVENT_STAFF_STRUCTURE_DELETION, active=True):
# Get xblock outline
xblock_info = create_xblock_info(
self.course,
include_child_info=True,
course_outline=True,
include_children_predicate=lambda xblock: not xblock.category == 'vertical',
user=self.user
)

self.assertFalse(xblock_info['show_delete_button'])
22 changes: 17 additions & 5 deletions cms/static/js/spec/views/pages/course_outline_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ describe('CourseOutlinePage', function() {
user_partitions: [],
user_partition_info: {},
highlights_enabled: true,
highlights_enabled_for_messaging: false
highlights_enabled_for_messaging: false,
show_delete_button: true
}, options, {child_info: {children: children}});
};

Expand All @@ -68,7 +69,8 @@ describe('CourseOutlinePage', function() {
show_review_rules: true,
user_partition_info: {},
highlights_enabled: true,
highlights_enabled_for_messaging: false
highlights_enabled_for_messaging: false,
show_delete_button: true
}, options, {child_info: {children: children}});
};

Expand All @@ -93,7 +95,8 @@ describe('CourseOutlinePage', function() {
group_access: {},
user_partition_info: {},
highlights: [],
highlights_enabled: true
highlights_enabled: true,
show_delete_button: true
}, options, {child_info: {children: children}});
};

Expand Down Expand Up @@ -123,7 +126,8 @@ describe('CourseOutlinePage', function() {
},
user_partitions: [],
group_access: {},
user_partition_info: {}
user_partition_info: {},
show_delete_button: true
}, options, {child_info: {children: children}});
};

Expand All @@ -141,7 +145,8 @@ describe('CourseOutlinePage', function() {
edited_by: 'MockUser',
user_partitions: [],
group_access: {},
user_partition_info: {}
user_partition_info: {},
show_delete_button: true
}, options);
};

Expand Down Expand Up @@ -862,6 +867,13 @@ describe('CourseOutlinePage', function() {
expect(outlinePage.$('[data-locator="mock-section-2"]')).toExist();
});

it('remains un-visible if show_delete_button is false ', function() {
createCourseOutlinePage(this, createMockCourseJSON({show_delete_button: false}, [
createMockSectionJSON({show_delete_button: false})
]));
expect(getItemHeaders('section').find('.delete-button').first()).not.toExist();
});

it('can be deleted if it is the only section', function() {
var promptSpy = EditHelpers.createPromptSpy();
createCourseOutlinePage(this, mockSingleSectionCourseJSON);
Expand Down
4 changes: 2 additions & 2 deletions cms/static/js/views/xblock_outline.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, XBlockStringFieldE
includesChildren: this.shouldRenderChildren(),
hasExplicitStaffLock: this.model.get('has_explicit_staff_lock'),
staffOnlyMessage: this.model.get('staff_only_message'),
course: course
};
course: course,
showDeleteButton: this.model.get('show_delete_button')};
},

renderChildren: function() {
Expand Down
2 changes: 1 addition & 1 deletion cms/templates/js/course-outline.underscore
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ if (is_proctored_exam) {
</a>
</li>
<% } %>
<% if (xblockInfo.isDeletable()) { %>
<% if (xblockInfo.isDeletable() && showDeleteButton) { %>
<li class="action-item action-delete">
<a href="#" data-tooltip="<%- gettext('Delete') %>" class="delete-button action-button">
<span class="icon fa fa-trash-o" aria-hidden="true"></span>
Expand Down

0 comments on commit 653d353

Please sign in to comment.