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
  • Loading branch information
Sandeep Kumar Choudhary authored and pkulkark committed Sep 4, 2023
1 parent 6e28ba3 commit 447ee97
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 7 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')
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 @@ -17,6 +17,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 @@ -63,6 +64,8 @@
update_from_source,
)
from cms.djangoapps.contentstore.xblock_storage_handlers import view_handlers 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 @@ -4218,6 +4221,150 @@ def test_self_paced_item_visibility_state(self, store_type):
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'])


@patch(
"xmodule.modulestore.split_mongo.caching_descriptor_system.CachingDescriptorSystem.applicable_aside_types",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,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.contentstore.toggles import ENABLE_COPY_PASTE_UNITS
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
from cms.lib.ai_aside_summary_config import AiAsideSummaryConfig
Expand Down Expand Up @@ -1404,6 +1405,12 @@ def create_xblock_info( # lint-amnesty, pylint: disable=too-many-statements
xblock, course=course
)

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
)

if is_xblock_unit and summary_configuration.is_enabled():
xblock_info["summary_configuration_enabled"] = summary_configuration.is_summary_enabled(xblock_info['id'])

Expand Down
20 changes: 16 additions & 4 deletions cms/static/js/spec/views/pages/course_outline_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe('CourseOutlinePage', function() {
user_partition_info: {},
highlights_enabled: true,
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 @@ -934,6 +939,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
1 change: 1 addition & 0 deletions cms/static/js/views/xblock_outline.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, XBlockStringFieldE
staffOnlyMessage: this.model.get('staff_only_message'),
course: course,
enableCopyPasteUnits: this.model.get("enable_copy_paste_units"), // ENABLE_COPY_PASTE_UNITS waffle flag
showDeleteButton: this.model.get('show_delete_button')
};
},

Expand Down
4 changes: 2 additions & 2 deletions cms/templates/js/course-outline.underscore
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ if (is_proctored_exam) {
<a class="duplicate-button" href="#" role="button"><%- gettext('Duplicate') %></a>
</li>
<% } %>
<% if (xblockInfo.isDeletable()) { %>
<% if (xblockInfo.isDeletable() && showDeleteButton) { %>
<li class="nav-item">
<a class="delete-button" href="#" role="button"><%- gettext('Delete') %></a>
</li>
Expand All @@ -224,7 +224,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
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ ignore_imports =
# -> openedx.features.enterprise_support.utils
openedx.features.enterprise_support.utils -> lms.djangoapps.branding.api
cms.djangoapps.contentstore.rest_api.v1.views.settings -> lms.djangoapps.certificates.api
cms.djangoapps.contentstore.permissions -> lms.djangoapps.courseware.rules


[importlinter:contract:2]
Expand Down

0 comments on commit 447ee97

Please sign in to comment.