feat: add Studio API for bulk enable/disable discussions for a course

Implemented Studio API for bulk enable/disable discussions for a course.
This commit is contained in:
Pandi Ganesh
2025-06-17 08:50:00 +00:00
committed by Muhammad Faraz Maqsood
parent 854d04dd33
commit 0c493b6ec2
3 changed files with 177 additions and 1 deletions

View File

@@ -0,0 +1,117 @@
"""
Test the enable/disable discussions for all units API endpoint.
"""
import json
from django.urls import reverse
from opaque_keys.edx.keys import CourseKey
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory
from cms.djangoapps.contentstore.tests.utils import AjaxEnabledTestClient
from common.djangoapps.student.tests.factories import UserFactory
class BulkEnableDisableDiscussionsTestCase(ModuleStoreTestCase):
"""
Test the enable/disable discussions for all units API endpoint.
"""
def setUp(self):
super().setUp()
self.user = UserFactory(is_staff=True, is_superuser=True)
self.user.set_password(self.user_password)
self.user.save()
self.course_key = CourseKey.from_string("course-v1:edx+TestX+2025")
self.url = reverse('bulk_enable_disable_discussions', args=[str(self.course_key)])
self.client = AjaxEnabledTestClient()
self.client.login(username=self.user.username, password=self.user_password)
# Create a test course
self.course = CourseFactory.create(
org=self.course_key.org,
course=self.course_key.course,
run=self.course_key.run,
default_store=ModuleStoreEnum.Type.split,
display_name="EnableDisableDiscussionsTestCase Course",
)
with self.store.bulk_operations(self.course_key):
section = BlockFactory.create(
parent=self.course,
category='chapter',
display_name="Generated Section",
)
sequence = BlockFactory.create(
parent=section,
category='sequential',
display_name="Generated Sequence",
)
unit1 = BlockFactory.create(
parent=sequence,
category='vertical',
display_name="Unit in Section1",
discussion_enabled=True,
)
unit2 = BlockFactory.create(
parent=sequence,
category='vertical',
display_name="Unit in Section2",
discussion_enabled=True,
)
def test_disable_discussions_for_all_units(self):
"""
Test that the API successfully disables discussions for all units.
"""
self.enable_disable_discussions_for_all_units(False)
def test_enable_discussions_for_all_units(self):
"""
Test that the API successfully enables discussions for all units.
"""
self.enable_disable_discussions_for_all_units(True)
def enable_disable_discussions_for_all_units(self, is_enabled):
"""
Test that the API successfully enables/disables discussions for all units.
"""
data = {
"discussion_enabled": is_enabled
}
response = self.client.put(self.url, data=json.dumps(data), content_type='application/json')
self.assertEqual(response.status_code, 200)
response_data = response.json()
print(response_data)
self.assertEqual(response_data['updated_and_republished'], 0 if is_enabled else 2)
# Check that all verticals now have discussion_enabled set to the expected value
with self.store.bulk_operations(self.course_key):
verticals = self.store.get_items(self.course_key, qualifiers={'block_type': 'vertical'})
for vertical in verticals:
self.assertEqual(vertical.discussion_enabled, is_enabled)
def test_permission_denied_for_non_staff(self):
"""
Test that non-staff users are denied access to the API.
"""
# Create a non-staff user
non_staff_user = UserFactory(is_staff=False, is_superuser=False)
non_staff_user.set_password(self.user_password)
non_staff_user.save()
# Create a new client for the non-staff user
non_staff_client = AjaxEnabledTestClient()
non_staff_client.login(username=non_staff_user.username, password=self.user_password)
response = non_staff_client.put(self.url, content_type='application/json')
self.assertEqual(response.status_code, 403)
def test_badrequest_for_empty_request_body(self):
"""
Test that the API returns a 400 for an empty request body.
"""
response = self.client.put(self.url, data=json.dumps({}), content_type='application/json')
self.assertEqual(response.status_code, 400)

View File

@@ -135,7 +135,7 @@ __all__ = ['course_info_handler', 'course_handler', 'course_listing',
'course_notifications_handler',
'textbooks_list_handler', 'textbooks_detail_handler',
'group_configurations_list_handler', 'group_configurations_detail_handler',
'get_course_and_check_access']
'get_course_and_check_access', 'bulk_enable_disable_discussions']
class AccessListFallback(Exception):
@@ -1710,6 +1710,62 @@ def group_configurations_detail_handler(request, course_key_string, group_config
)
@login_required
@expect_json
@ensure_csrf_cookie
@require_http_methods(["PUT"])
def bulk_enable_disable_discussions(request, course_key_string):
"""
API endpoint to enable/disable discussions for all verticals in the course and republish them.
PUT
json: enable/disable discussions for all units and republish
"""
try:
# Validate the course key
course_key = CourseKey.from_string(course_key_string)
except InvalidKeyError:
return JsonResponseBadRequest({"error": "Invalid course key format"})
user = request.user
# check that logged in user has permissions to update this course
if not has_studio_write_access(user, course_key):
raise PermissionDenied()
if 'application/json' not in request.META.get('HTTP_ACCEPT', 'application/json'):
return JsonResponseBadRequest({"error": "Only supports json requests"})
if 'discussion_enabled' not in request.json:
return JsonResponseBadRequest({"error": "Missing 'discussion_enabled' field in request body"})
discussion_enabled = request.json['discussion_enabled']
log.info(
"User %s is attempting to %s discussions for all verticals in course %s",
user.username,
"enable" if discussion_enabled else "disable",
course_key
)
if request.method == 'PUT':
try:
store = modulestore()
changed = 0
with store.bulk_operations(course_key):
verticals = store.get_items(course_key, qualifiers={'block_type': 'vertical'})
for vertical in verticals:
if vertical.discussion_enabled != discussion_enabled:
vertical.discussion_enabled = discussion_enabled
store.update_item(vertical, user.id)
if store.has_published_version(vertical):
store.publish(vertical.location, user.id)
changed += 1
return JsonResponse({"updated_and_republished": changed})
except Exception as e: # lint-amnesty, pylint: disable=broad-except
log.exception("Exception occurred while enabling/disabling discussion: %s", str(e))
return JsonResponseBadRequest({"error": str(e)})
def are_content_experiments_enabled(course):
"""
Returns True if content experiments have been enabled for the course.

View File

@@ -201,6 +201,9 @@ urlpatterns = oauth2_urlpatterns + [
path('accessibility', contentstore_views.accessibility, name='accessibility'),
re_path(fr'api/youtube/courses/{COURSELIKE_KEY_PATTERN}/edx-video-ids$',
contentstore_views.get_course_youtube_edx_videos_ids, name='youtube_edx_video_ids'),
re_path(fr'^api/courses/{settings.COURSE_KEY_PATTERN}/bulk_enable_disable_discussions$',
contentstore_views.bulk_enable_disable_discussions,
name='bulk_enable_disable_discussions'),
]
if not settings.DISABLE_DEPRECATED_SIGNIN_URL: