feat: added topics v3 api (#31330)
Co-authored-by: adeel.tajamul <adeel.tajamul@arbisoft.com>
This commit is contained in:
committed by
GitHub
parent
883b816430
commit
7a0010ad0e
@@ -32,7 +32,12 @@ from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
|
||||
from common.djangoapps.student.models import get_retired_username_by_username, CourseEnrollment
|
||||
from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole, GlobalStaff
|
||||
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, SuperuserFactory, UserFactory
|
||||
from common.djangoapps.student.tests.factories import (
|
||||
AdminFactory,
|
||||
CourseEnrollmentFactory,
|
||||
SuperuserFactory,
|
||||
UserFactory
|
||||
)
|
||||
from common.djangoapps.util.testing import PatchMediaTypeMixin, UrlResetMixin
|
||||
from common.test.utils import disable_signal
|
||||
from lms.djangoapps.discussion.django_comment_client.tests.utils import (
|
||||
@@ -50,6 +55,9 @@ from lms.djangoapps.discussion.rest_api.tests.utils import (
|
||||
parsed_body,
|
||||
)
|
||||
from openedx.core.djangoapps.course_groups.tests.helpers import config_course_cohorts
|
||||
from openedx.core.djangoapps.discussions.config.waffle import ENABLE_NEW_STRUCTURE_DISCUSSIONS
|
||||
from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration, DiscussionTopicLink, Provider
|
||||
from openedx.core.djangoapps.discussions.tasks import update_discussions_settings_from_course_task
|
||||
from openedx.core.djangoapps.django_comment_common.models import CourseDiscussionSettings, Role
|
||||
from openedx.core.djangoapps.django_comment_common.utils import seed_permissions_roles
|
||||
from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user
|
||||
@@ -869,6 +877,97 @@ class CourseTopicsViewTest(DiscussionAPIViewTestMixin, CommentsServiceMockMixin,
|
||||
)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@mock.patch('lms.djangoapps.discussion.rest_api.api._get_course', mock.Mock())
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
||||
@override_waffle_flag(ENABLE_NEW_STRUCTURE_DISCUSSIONS, True)
|
||||
class CourseTopicsViewV3Test(DiscussionAPIViewTestMixin, CommentsServiceMockMixin, ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for CourseTopicsViewV3
|
||||
"""
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.password = "password"
|
||||
self.user = UserFactory.create(password=self.password)
|
||||
self.client.login(username=self.user.username, password=self.password)
|
||||
self.staff = AdminFactory.create()
|
||||
self.course = CourseFactory.create(
|
||||
start=datetime(2020, 1, 1),
|
||||
end=datetime(2028, 1, 1),
|
||||
enrollment_start=datetime(2020, 1, 1),
|
||||
enrollment_end=datetime(2028, 1, 1),
|
||||
discussion_topics={"Course Wide Topic": {
|
||||
"id": 'course-wide-topic',
|
||||
"usage_key": None,
|
||||
}}
|
||||
)
|
||||
self.chapter = ItemFactory.create(
|
||||
parent_location=self.course.location,
|
||||
category='chapter',
|
||||
display_name="Week 1",
|
||||
start=datetime(2015, 3, 1, tzinfo=UTC),
|
||||
)
|
||||
self.sequential = ItemFactory.create(
|
||||
parent_location=self.chapter.location,
|
||||
category='sequential',
|
||||
display_name="Lesson 1",
|
||||
start=datetime(2015, 3, 1, tzinfo=UTC),
|
||||
)
|
||||
self.verticals = [
|
||||
ItemFactory.create(
|
||||
parent_location=self.sequential.location,
|
||||
category='vertical',
|
||||
display_name='vertical',
|
||||
start=datetime(2015, 4, 1, tzinfo=UTC),
|
||||
)
|
||||
]
|
||||
course_key = self.course.id
|
||||
self.config = DiscussionsConfiguration.objects.create(context_key=course_key, provider_type=Provider.OPEN_EDX)
|
||||
topic_links = []
|
||||
update_discussions_settings_from_course_task(str(course_key))
|
||||
topic_id_query = DiscussionTopicLink.objects.filter(context_key=course_key).values_list(
|
||||
'external_id', flat=True,
|
||||
)
|
||||
topic_ids = list(topic_id_query.order_by('ordering'))
|
||||
DiscussionTopicLink.objects.bulk_create(topic_links)
|
||||
self.topic_stats = {
|
||||
**{topic_id: dict(discussion=random.randint(0, 10), question=random.randint(0, 10))
|
||||
for topic_id in set(topic_ids)},
|
||||
topic_ids[0]: dict(discussion=0, question=0),
|
||||
}
|
||||
patcher = mock.patch(
|
||||
'lms.djangoapps.discussion.rest_api.api.get_course_commentable_counts',
|
||||
mock.Mock(return_value=self.topic_stats),
|
||||
)
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
self.url = reverse("course_topics_v3", kwargs={"course_id": str(self.course.id)})
|
||||
|
||||
def test_basic(self):
|
||||
response = self.client.get(self.url)
|
||||
data = json.loads(response.content.decode())
|
||||
expected_non_courseware_keys = [
|
||||
'id', 'usage_key', 'name', 'thread_counts', 'enabled_in_context',
|
||||
'courseware'
|
||||
]
|
||||
expected_courseware_keys = [
|
||||
'id', 'block_id', 'lms_web_url', 'legacy_web_url', 'student_view_url',
|
||||
'type', 'display_name', 'children', 'courseware'
|
||||
]
|
||||
assert response.status_code == 200
|
||||
assert len(data) == 2
|
||||
non_courseware_topic_keys = list(data[0].keys())
|
||||
assert non_courseware_topic_keys == expected_non_courseware_keys
|
||||
courseware_topic_keys = list(data[1].keys())
|
||||
assert courseware_topic_keys == expected_courseware_keys
|
||||
expected_courseware_keys.remove('courseware')
|
||||
sequential_keys = list(data[1]['children'][0].keys())
|
||||
assert sequential_keys == expected_courseware_keys
|
||||
expected_non_courseware_keys.remove('courseware')
|
||||
vertical_keys = list(data[1]['children'][0]['children'][0].keys())
|
||||
assert vertical_keys == expected_non_courseware_keys
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@httpretty.activate
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
||||
|
||||
@@ -14,6 +14,7 @@ from lms.djangoapps.discussion.rest_api.views import (
|
||||
CourseDiscussionSettingsAPIView,
|
||||
CourseTopicsView,
|
||||
CourseTopicsViewV2,
|
||||
CourseTopicsViewV3,
|
||||
CourseView,
|
||||
LearnerThreadView,
|
||||
ReplaceUsernamesView,
|
||||
@@ -75,5 +76,10 @@ urlpatterns = [
|
||||
CourseTopicsViewV2.as_view(),
|
||||
name="course_topics_v2"
|
||||
),
|
||||
re_path(
|
||||
fr"^v3/course_topics/{settings.COURSE_ID_PATTERN}",
|
||||
CourseTopicsViewV3.as_view(),
|
||||
name="course_topics_v3"
|
||||
),
|
||||
path('v1/', include(ROUTER.urls)),
|
||||
]
|
||||
|
||||
@@ -159,3 +159,87 @@ def get_moderator_users_list(course_id):
|
||||
for user in role.users.all()
|
||||
]
|
||||
return moderator_user_ids
|
||||
|
||||
|
||||
def filter_topic_from_discussion_id(discussion_id, topics_list):
|
||||
"""
|
||||
Returns topic based on discussion id
|
||||
"""
|
||||
for topic in topics_list:
|
||||
if topic.get("id") == discussion_id:
|
||||
return topic
|
||||
return {}
|
||||
|
||||
|
||||
def create_discussion_children_from_ids(children_ids, blocks, topics):
|
||||
"""
|
||||
Takes ids of discussion and return discussion dictionary
|
||||
"""
|
||||
discussions = []
|
||||
for child_id in children_ids:
|
||||
topic = blocks.get(child_id)
|
||||
if topic.get('type') == 'vertical':
|
||||
discussions_id = topic.get('discussions_id')
|
||||
topic = filter_topic_from_discussion_id(discussions_id, topics)
|
||||
if topic:
|
||||
discussions.append(topic)
|
||||
return discussions
|
||||
|
||||
|
||||
def create_blocks_params(course_usage_key, user):
|
||||
"""
|
||||
Returns param dict that is needed to get blocks
|
||||
"""
|
||||
return {
|
||||
'usage_key': course_usage_key,
|
||||
'user': user,
|
||||
'depth': None,
|
||||
'nav_depth': None,
|
||||
'requested_fields': {
|
||||
'display_name',
|
||||
'student_view_data',
|
||||
'children',
|
||||
'discussions_id',
|
||||
'type',
|
||||
'block_types_filter'
|
||||
},
|
||||
'block_counts': set(),
|
||||
'student_view_data': {'discussion'},
|
||||
'return_type': 'dict',
|
||||
'block_types_filter': {
|
||||
'discussion',
|
||||
'chapter',
|
||||
'vertical',
|
||||
'sequential',
|
||||
'course'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def create_topics_v3_structure(blocks, topics):
|
||||
"""
|
||||
Create V3 topics structure from blocks and v2 topics
|
||||
"""
|
||||
non_courseware_topics = [
|
||||
dict({**topic, 'courseware': False})
|
||||
for topic in topics
|
||||
if topic.get('usage_key', '') is None
|
||||
]
|
||||
courseware_topics = []
|
||||
for key, value in blocks.items():
|
||||
if value.get("type") == "chapter":
|
||||
value['courseware'] = True
|
||||
courseware_topics.append(value)
|
||||
value['children'] = create_discussion_children_from_ids(
|
||||
value['children'],
|
||||
blocks,
|
||||
topics,
|
||||
)
|
||||
subsections = value.get('children')
|
||||
for subsection in subsections:
|
||||
subsection['children'] = create_discussion_children_from_ids(
|
||||
subsection['children'],
|
||||
blocks,
|
||||
topics,
|
||||
)
|
||||
return non_courseware_topics + courseware_topics
|
||||
|
||||
@@ -23,6 +23,7 @@ from rest_framework.viewsets import ViewSet
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
from common.djangoapps.util.file import store_uploaded_file
|
||||
from lms.djangoapps.course_api.blocks.api import get_blocks
|
||||
from lms.djangoapps.course_goals.models import UserActivity
|
||||
from lms.djangoapps.discussion.django_comment_client import settings as cc_settings
|
||||
from lms.djangoapps.discussion.django_comment_client.utils import get_group_id_for_comments_service
|
||||
@@ -73,6 +74,11 @@ from ..rest_api.serializers import (
|
||||
DiscussionTopicSerializerV2,
|
||||
TopicOrdering,
|
||||
)
|
||||
from .utils import (
|
||||
create_blocks_params,
|
||||
create_topics_v3_structure,
|
||||
)
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -292,6 +298,85 @@ class CourseTopicsViewV2(DeveloperErrorViewMixin, APIView):
|
||||
return Response(response)
|
||||
|
||||
|
||||
@view_auth_classes()
|
||||
class CourseTopicsViewV3(DeveloperErrorViewMixin, APIView):
|
||||
"""
|
||||
View for listing course topics v3.
|
||||
|
||||
** Response Example **:
|
||||
[
|
||||
{
|
||||
"id": "non-courseware-discussion-id",
|
||||
"usage_key": None,
|
||||
"name": "Non Courseware Topic",
|
||||
"thread_counts": {"discussion": 0, "question": 0},
|
||||
"enabled_in_context": true,
|
||||
"courseware": false
|
||||
},
|
||||
{
|
||||
"id": "id",
|
||||
"block_id": "block_id",
|
||||
"lms_web_url": "",
|
||||
"legacy_web_url": "",
|
||||
"student_view_url": "",
|
||||
"type": "chapter",
|
||||
"display_name": "First section",
|
||||
"children": [
|
||||
"id": "id",
|
||||
"block_id": "block_id",
|
||||
"lms_web_url": "",
|
||||
"legacy_web_url": "",
|
||||
"student_view_url": "",
|
||||
"type": "sequential",
|
||||
"display_name": "First Sub-Section",
|
||||
"children": [
|
||||
"id": "id",
|
||||
"usage_key": "",
|
||||
"name": "First Unit?",
|
||||
"thread_counts": { "discussion": 0, "question": 0 },
|
||||
"enabled_in_context": true
|
||||
]
|
||||
],
|
||||
"courseware": true,
|
||||
}
|
||||
]
|
||||
"""
|
||||
|
||||
def get(self, request, course_id):
|
||||
"""
|
||||
**Use Cases**
|
||||
|
||||
Retrieve the topic listing for a course.
|
||||
|
||||
**Example Requests**:
|
||||
|
||||
GET /api/discussion/v3/course_topics/course-v1:ExampleX+Subject101+2015
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
topics = get_course_topics_v2(
|
||||
course_key,
|
||||
request.user,
|
||||
)
|
||||
course_usage_key = modulestore().make_course_usage_key(course_key)
|
||||
blocks_params = create_blocks_params(course_usage_key, request.user)
|
||||
blocks = get_blocks(
|
||||
request,
|
||||
blocks_params['usage_key'],
|
||||
blocks_params['user'],
|
||||
blocks_params['depth'],
|
||||
blocks_params['nav_depth'],
|
||||
blocks_params['requested_fields'],
|
||||
blocks_params['block_counts'],
|
||||
blocks_params['student_view_data'],
|
||||
blocks_params['return_type'],
|
||||
blocks_params['block_types_filter'],
|
||||
hide_access_denials=False,
|
||||
)['blocks']
|
||||
|
||||
topics = create_topics_v3_structure(blocks, topics)
|
||||
return Response(topics)
|
||||
|
||||
|
||||
@view_auth_classes()
|
||||
class ThreadViewSet(DeveloperErrorViewMixin, ViewSet):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user