Skip to content

Commit 653d353

Browse files
Sandeep Kumar Choudharyxitij2000
authored andcommitted
feat: Allow delete course content in Studio only for admin users
(cherry picked from commit c812a6c1d5c0961900507a6e7abe3d0f3b8a7570) (cherry picked from commit 004f2fe)
1 parent 43c635b commit 653d353

File tree

7 files changed

+195
-9
lines changed

7 files changed

+195
-9
lines changed

cms/djangoapps/contentstore/config/waffle.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,13 @@
5353
# .. toggle_warning: Flag course_experience.relative_dates should also be active for relative dates functionalities to work.
5454
# .. toggle_tickets: https://openedx.atlassian.net/browse/AA-844
5555
CUSTOM_RELATIVE_DATES = CourseWaffleFlag(f'{WAFFLE_NAMESPACE}.custom_relative_dates', __name__)
56+
57+
# .. toggle_name: studio.prevent_staff_structure_deletion
58+
# .. toggle_implementation: WaffleFlag
59+
# .. toggle_default: False
60+
# .. toggle_description: Prevents staff from deleting course structures
61+
# .. toggle_use_cases: opt_in
62+
# .. toggle_creation_date: 2021-06-25
63+
PREVENT_STAFF_STRUCTURE_DELETION = WaffleFlag(
64+
f'{WAFFLE_NAMESPACE}.prevent_staff_structure_deletion', __name__, LOG_PREFIX
65+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""
2+
Permission definitions for the contentstore djangoapp
3+
"""
4+
5+
from bridgekeeper import perms
6+
7+
from lms.djangoapps.courseware.rules import HasRolesRule
8+
9+
DELETE_COURSE_CONTENT = 'contentstore.delete_course_content'
10+
perms[DELETE_COURSE_CONTENT] = HasRolesRule('instructor')

cms/djangoapps/contentstore/views/block.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
from xblock.core import XBlock
3333
from xblock.fields import Scope
3434

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

1376+
xblock_info['show_delete_button'] = True
1377+
if PREVENT_STAFF_STRUCTURE_DELETION.is_enabled():
1378+
xblock_info['show_delete_button'] = (
1379+
user.has_perm(DELETE_COURSE_CONTENT, xblock) if user is not None else False
1380+
)
1381+
13751382
xblock_info['has_partition_group_components'] = has_children_visible_to_specific_partition_groups(
13761383
xblock
13771384
)

cms/djangoapps/contentstore/views/tests/test_block.py

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from openedx_events.content_authoring.signals import XBLOCK_DUPLICATED
1717
from openedx_events.tests.utils import OpenEdxEventsTestMixin
1818
from edx_proctoring.exceptions import ProctoredExamNotFoundException
19+
from edx_toggles.toggles.testutils import override_waffle_flag
1920
from opaque_keys import InvalidKeyError
2021
from opaque_keys.edx.asides import AsideUsageKeyV2
2122
from opaque_keys.edx.keys import CourseKey, UsageKey
@@ -49,6 +50,8 @@
4950
from cms.djangoapps.contentstore.tests.utils import CourseTestCase
5051
from cms.djangoapps.contentstore.utils import reverse_course_url, reverse_usage_url
5152
from cms.djangoapps.contentstore.views import block as item_module
53+
from cms.djangoapps.contentstore.config.waffle import PREVENT_STAFF_STRUCTURE_DELETION
54+
from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole, CourseCreatorRole
5255
from common.djangoapps.student.tests.factories import StaffFactory, UserFactory
5356
from common.djangoapps.xblock_django.models import (
5457
XBlockConfiguration,
@@ -3473,3 +3476,147 @@ def test_self_paced_item_visibility_state(self, store_type):
34733476
# Check that in self paced course content has live state now
34743477
xblock_info = self._get_xblock_info(chapter.location)
34753478
self._verify_visibility_state(xblock_info, VisibilityState.live)
3479+
3480+
def test_staff_show_delete_button(self):
3481+
"""
3482+
Test delete button is *not visible* to user with CourseStaffRole
3483+
"""
3484+
# Add user as course staff
3485+
CourseStaffRole(self.course_key).add_users(self.user)
3486+
3487+
# Get xblock outline
3488+
xblock_info = create_xblock_info(
3489+
self.course,
3490+
include_child_info=True,
3491+
course_outline=True,
3492+
include_children_predicate=lambda xblock: not xblock.category == 'vertical',
3493+
user=self.user
3494+
)
3495+
self.assertTrue(xblock_info['show_delete_button'])
3496+
3497+
def test_staff_show_delete_button_with_waffle(self):
3498+
"""
3499+
Test delete button is *not visible* to user with CourseStaffRole and
3500+
PREVENT_STAFF_STRUCTURE_DELETION waffle set
3501+
"""
3502+
# Add user as course staff
3503+
CourseStaffRole(self.course_key).add_users(self.user)
3504+
3505+
with override_waffle_flag(PREVENT_STAFF_STRUCTURE_DELETION, active=True):
3506+
# Get xblock outline
3507+
xblock_info = create_xblock_info(
3508+
self.course,
3509+
include_child_info=True,
3510+
course_outline=True,
3511+
include_children_predicate=lambda xblock: not xblock.category == 'vertical',
3512+
user=self.user
3513+
)
3514+
3515+
self.assertFalse(xblock_info['show_delete_button'])
3516+
3517+
def test_no_user_show_delete_button(self):
3518+
"""
3519+
Test delete button is *visible* when user attribute is not set on
3520+
xblock. This happens with ajax requests.
3521+
"""
3522+
# Get xblock outline
3523+
xblock_info = create_xblock_info(
3524+
self.course,
3525+
include_child_info=True,
3526+
course_outline=True,
3527+
include_children_predicate=lambda xblock: not xblock.category == 'vertical',
3528+
user=None
3529+
)
3530+
self.assertTrue(xblock_info['show_delete_button'])
3531+
3532+
def test_no_user_show_delete_button_with_waffle(self):
3533+
"""
3534+
Test delete button is *visible* when user attribute is not set on
3535+
xblock (this happens with ajax requests) and PREVENT_STAFF_STRUCTURE_DELETION waffle set.
3536+
"""
3537+
3538+
with override_waffle_flag(PREVENT_STAFF_STRUCTURE_DELETION, active=True):
3539+
# Get xblock outline
3540+
xblock_info = create_xblock_info(
3541+
self.course,
3542+
include_child_info=True,
3543+
course_outline=True,
3544+
include_children_predicate=lambda xblock: not xblock.category == 'vertical',
3545+
user=None
3546+
)
3547+
3548+
self.assertFalse(xblock_info['show_delete_button'])
3549+
3550+
def test_instructor_show_delete_button(self):
3551+
"""
3552+
Test delete button is *visible* to user with CourseInstructorRole only
3553+
"""
3554+
# Add user as course instructor
3555+
CourseInstructorRole(self.course_key).add_users(self.user)
3556+
3557+
# Get xblock outline
3558+
xblock_info = create_xblock_info(
3559+
self.course,
3560+
include_child_info=True,
3561+
course_outline=True,
3562+
include_children_predicate=lambda xblock: not xblock.category == 'vertical',
3563+
user=self.user
3564+
)
3565+
self.assertTrue(xblock_info['show_delete_button'])
3566+
3567+
def test_instructor_show_delete_button_with_waffle(self):
3568+
"""
3569+
Test delete button is *visible* to user with CourseInstructorRole only
3570+
and PREVENT_STAFF_STRUCTURE_DELETION waffle set
3571+
"""
3572+
# Add user as course instructor
3573+
CourseInstructorRole(self.course_key).add_users(self.user)
3574+
3575+
with override_waffle_flag(PREVENT_STAFF_STRUCTURE_DELETION, active=True):
3576+
# Get xblock outline
3577+
xblock_info = create_xblock_info(
3578+
self.course,
3579+
include_child_info=True,
3580+
course_outline=True,
3581+
include_children_predicate=lambda xblock: not xblock.category == 'vertical',
3582+
user=self.user
3583+
)
3584+
3585+
self.assertTrue(xblock_info['show_delete_button'])
3586+
3587+
def test_creator_show_delete_button(self):
3588+
"""
3589+
Test delete button is *visible* to user with CourseInstructorRole only
3590+
"""
3591+
# Add user as course creator
3592+
CourseCreatorRole(self.course_key).add_users(self.user)
3593+
3594+
# Get xblock outline
3595+
xblock_info = create_xblock_info(
3596+
self.course,
3597+
include_child_info=True,
3598+
course_outline=True,
3599+
include_children_predicate=lambda xblock: not xblock.category == 'vertical',
3600+
user=self.user
3601+
)
3602+
self.assertTrue(xblock_info['show_delete_button'])
3603+
3604+
def test_creator_show_delete_button_with_waffle(self):
3605+
"""
3606+
Test delete button is *visible* to user with CourseInstructorRole only
3607+
and PREVENT_STAFF_STRUCTURE_DELETION waffle set
3608+
"""
3609+
# Add user as course creator
3610+
CourseCreatorRole(self.course_key).add_users(self.user)
3611+
3612+
with override_waffle_flag(PREVENT_STAFF_STRUCTURE_DELETION, active=True):
3613+
# Get xblock outline
3614+
xblock_info = create_xblock_info(
3615+
self.course,
3616+
include_child_info=True,
3617+
course_outline=True,
3618+
include_children_predicate=lambda xblock: not xblock.category == 'vertical',
3619+
user=self.user
3620+
)
3621+
3622+
self.assertFalse(xblock_info['show_delete_button'])

cms/static/js/spec/views/pages/course_outline_spec.js

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ describe('CourseOutlinePage', function() {
4141
user_partitions: [],
4242
user_partition_info: {},
4343
highlights_enabled: true,
44-
highlights_enabled_for_messaging: false
44+
highlights_enabled_for_messaging: false,
45+
show_delete_button: true
4546
}, options, {child_info: {children: children}});
4647
};
4748

@@ -68,7 +69,8 @@ describe('CourseOutlinePage', function() {
6869
show_review_rules: true,
6970
user_partition_info: {},
7071
highlights_enabled: true,
71-
highlights_enabled_for_messaging: false
72+
highlights_enabled_for_messaging: false,
73+
show_delete_button: true
7274
}, options, {child_info: {children: children}});
7375
};
7476

@@ -93,7 +95,8 @@ describe('CourseOutlinePage', function() {
9395
group_access: {},
9496
user_partition_info: {},
9597
highlights: [],
96-
highlights_enabled: true
98+
highlights_enabled: true,
99+
show_delete_button: true
97100
}, options, {child_info: {children: children}});
98101
};
99102

@@ -123,7 +126,8 @@ describe('CourseOutlinePage', function() {
123126
},
124127
user_partitions: [],
125128
group_access: {},
126-
user_partition_info: {}
129+
user_partition_info: {},
130+
show_delete_button: true
127131
}, options, {child_info: {children: children}});
128132
};
129133

@@ -141,7 +145,8 @@ describe('CourseOutlinePage', function() {
141145
edited_by: 'MockUser',
142146
user_partitions: [],
143147
group_access: {},
144-
user_partition_info: {}
148+
user_partition_info: {},
149+
show_delete_button: true
145150
}, options);
146151
};
147152

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

870+
it('remains un-visible if show_delete_button is false ', function() {
871+
createCourseOutlinePage(this, createMockCourseJSON({show_delete_button: false}, [
872+
createMockSectionJSON({show_delete_button: false})
873+
]));
874+
expect(getItemHeaders('section').find('.delete-button').first()).not.toExist();
875+
});
876+
865877
it('can be deleted if it is the only section', function() {
866878
var promptSpy = EditHelpers.createPromptSpy();
867879
createCourseOutlinePage(this, mockSingleSectionCourseJSON);

cms/static/js/views/xblock_outline.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,8 @@ function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, XBlockStringFieldE
109109
includesChildren: this.shouldRenderChildren(),
110110
hasExplicitStaffLock: this.model.get('has_explicit_staff_lock'),
111111
staffOnlyMessage: this.model.get('staff_only_message'),
112-
course: course
113-
};
112+
course: course,
113+
showDeleteButton: this.model.get('show_delete_button')};
114114
},
115115

116116
renderChildren: function() {

cms/templates/js/course-outline.underscore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ if (is_proctored_exam) {
179179
</a>
180180
</li>
181181
<% } %>
182-
<% if (xblockInfo.isDeletable()) { %>
182+
<% if (xblockInfo.isDeletable() && showDeleteButton) { %>
183183
<li class="action-item action-delete">
184184
<a href="#" data-tooltip="<%- gettext('Delete') %>" class="delete-button action-button">
185185
<span class="icon fa fa-trash-o" aria-hidden="true"></span>

0 commit comments

Comments
 (0)