Skip to content

Commit 0c493b6

Browse files
pganesh-apphelixFaraz32123
authored andcommitted
feat: add Studio API for bulk enable/disable discussions for a course
Implemented Studio API for bulk enable/disable discussions for a course.
1 parent 854d04d commit 0c493b6

3 files changed

Lines changed: 177 additions & 1 deletion

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"""
2+
Test the enable/disable discussions for all units API endpoint.
3+
"""
4+
import json
5+
6+
from django.urls import reverse
7+
from opaque_keys.edx.keys import CourseKey
8+
from xmodule.modulestore import ModuleStoreEnum
9+
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
10+
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory
11+
12+
from cms.djangoapps.contentstore.tests.utils import AjaxEnabledTestClient
13+
from common.djangoapps.student.tests.factories import UserFactory
14+
15+
16+
class BulkEnableDisableDiscussionsTestCase(ModuleStoreTestCase):
17+
"""
18+
Test the enable/disable discussions for all units API endpoint.
19+
"""
20+
21+
def setUp(self):
22+
super().setUp()
23+
self.user = UserFactory(is_staff=True, is_superuser=True)
24+
self.user.set_password(self.user_password)
25+
self.user.save()
26+
27+
self.course_key = CourseKey.from_string("course-v1:edx+TestX+2025")
28+
29+
self.url = reverse('bulk_enable_disable_discussions', args=[str(self.course_key)])
30+
self.client = AjaxEnabledTestClient()
31+
self.client.login(username=self.user.username, password=self.user_password)
32+
33+
# Create a test course
34+
self.course = CourseFactory.create(
35+
org=self.course_key.org,
36+
course=self.course_key.course,
37+
run=self.course_key.run,
38+
default_store=ModuleStoreEnum.Type.split,
39+
display_name="EnableDisableDiscussionsTestCase Course",
40+
)
41+
with self.store.bulk_operations(self.course_key):
42+
section = BlockFactory.create(
43+
parent=self.course,
44+
category='chapter',
45+
display_name="Generated Section",
46+
)
47+
sequence = BlockFactory.create(
48+
parent=section,
49+
category='sequential',
50+
display_name="Generated Sequence",
51+
)
52+
unit1 = BlockFactory.create(
53+
parent=sequence,
54+
category='vertical',
55+
display_name="Unit in Section1",
56+
discussion_enabled=True,
57+
)
58+
unit2 = BlockFactory.create(
59+
parent=sequence,
60+
category='vertical',
61+
display_name="Unit in Section2",
62+
discussion_enabled=True,
63+
)
64+
65+
def test_disable_discussions_for_all_units(self):
66+
"""
67+
Test that the API successfully disables discussions for all units.
68+
"""
69+
self.enable_disable_discussions_for_all_units(False)
70+
71+
def test_enable_discussions_for_all_units(self):
72+
"""
73+
Test that the API successfully enables discussions for all units.
74+
"""
75+
self.enable_disable_discussions_for_all_units(True)
76+
77+
def enable_disable_discussions_for_all_units(self, is_enabled):
78+
"""
79+
Test that the API successfully enables/disables discussions for all units.
80+
"""
81+
data = {
82+
"discussion_enabled": is_enabled
83+
}
84+
response = self.client.put(self.url, data=json.dumps(data), content_type='application/json')
85+
self.assertEqual(response.status_code, 200)
86+
response_data = response.json()
87+
print(response_data)
88+
self.assertEqual(response_data['updated_and_republished'], 0 if is_enabled else 2)
89+
90+
# Check that all verticals now have discussion_enabled set to the expected value
91+
with self.store.bulk_operations(self.course_key):
92+
verticals = self.store.get_items(self.course_key, qualifiers={'block_type': 'vertical'})
93+
for vertical in verticals:
94+
self.assertEqual(vertical.discussion_enabled, is_enabled)
95+
96+
def test_permission_denied_for_non_staff(self):
97+
"""
98+
Test that non-staff users are denied access to the API.
99+
"""
100+
# Create a non-staff user
101+
non_staff_user = UserFactory(is_staff=False, is_superuser=False)
102+
non_staff_user.set_password(self.user_password)
103+
non_staff_user.save()
104+
105+
# Create a new client for the non-staff user
106+
non_staff_client = AjaxEnabledTestClient()
107+
non_staff_client.login(username=non_staff_user.username, password=self.user_password)
108+
109+
response = non_staff_client.put(self.url, content_type='application/json')
110+
self.assertEqual(response.status_code, 403)
111+
112+
def test_badrequest_for_empty_request_body(self):
113+
"""
114+
Test that the API returns a 400 for an empty request body.
115+
"""
116+
response = self.client.put(self.url, data=json.dumps({}), content_type='application/json')
117+
self.assertEqual(response.status_code, 400)

cms/djangoapps/contentstore/views/course.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@
135135
'course_notifications_handler',
136136
'textbooks_list_handler', 'textbooks_detail_handler',
137137
'group_configurations_list_handler', 'group_configurations_detail_handler',
138-
'get_course_and_check_access']
138+
'get_course_and_check_access', 'bulk_enable_disable_discussions']
139139

140140

141141
class AccessListFallback(Exception):
@@ -1710,6 +1710,62 @@ def group_configurations_detail_handler(request, course_key_string, group_config
17101710
)
17111711

17121712

1713+
@login_required
1714+
@expect_json
1715+
@ensure_csrf_cookie
1716+
@require_http_methods(["PUT"])
1717+
def bulk_enable_disable_discussions(request, course_key_string):
1718+
"""
1719+
API endpoint to enable/disable discussions for all verticals in the course and republish them.
1720+
1721+
PUT
1722+
json: enable/disable discussions for all units and republish
1723+
"""
1724+
try:
1725+
# Validate the course key
1726+
course_key = CourseKey.from_string(course_key_string)
1727+
except InvalidKeyError:
1728+
return JsonResponseBadRequest({"error": "Invalid course key format"})
1729+
1730+
user = request.user
1731+
1732+
# check that logged in user has permissions to update this course
1733+
if not has_studio_write_access(user, course_key):
1734+
raise PermissionDenied()
1735+
1736+
if 'application/json' not in request.META.get('HTTP_ACCEPT', 'application/json'):
1737+
return JsonResponseBadRequest({"error": "Only supports json requests"})
1738+
1739+
if 'discussion_enabled' not in request.json:
1740+
return JsonResponseBadRequest({"error": "Missing 'discussion_enabled' field in request body"})
1741+
discussion_enabled = request.json['discussion_enabled']
1742+
log.info(
1743+
"User %s is attempting to %s discussions for all verticals in course %s",
1744+
user.username,
1745+
"enable" if discussion_enabled else "disable",
1746+
course_key
1747+
)
1748+
1749+
if request.method == 'PUT':
1750+
try:
1751+
store = modulestore()
1752+
changed = 0
1753+
with store.bulk_operations(course_key):
1754+
verticals = store.get_items(course_key, qualifiers={'block_type': 'vertical'})
1755+
for vertical in verticals:
1756+
if vertical.discussion_enabled != discussion_enabled:
1757+
vertical.discussion_enabled = discussion_enabled
1758+
store.update_item(vertical, user.id)
1759+
1760+
if store.has_published_version(vertical):
1761+
store.publish(vertical.location, user.id)
1762+
changed += 1
1763+
return JsonResponse({"updated_and_republished": changed})
1764+
except Exception as e: # lint-amnesty, pylint: disable=broad-except
1765+
log.exception("Exception occurred while enabling/disabling discussion: %s", str(e))
1766+
return JsonResponseBadRequest({"error": str(e)})
1767+
1768+
17131769
def are_content_experiments_enabled(course):
17141770
"""
17151771
Returns True if content experiments have been enabled for the course.

cms/urls.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,9 @@
201201
path('accessibility', contentstore_views.accessibility, name='accessibility'),
202202
re_path(fr'api/youtube/courses/{COURSELIKE_KEY_PATTERN}/edx-video-ids$',
203203
contentstore_views.get_course_youtube_edx_videos_ids, name='youtube_edx_video_ids'),
204+
re_path(fr'^api/courses/{settings.COURSE_KEY_PATTERN}/bulk_enable_disable_discussions$',
205+
contentstore_views.bulk_enable_disable_discussions,
206+
name='bulk_enable_disable_discussions'),
204207
]
205208

206209
if not settings.DISABLE_DEPRECATED_SIGNIN_URL:

0 commit comments

Comments
 (0)