feat: added topics v3 api (#31330)

Co-authored-by: adeel.tajamul <adeel.tajamul@arbisoft.com>
This commit is contained in:
Muhammad Adeel Tajamul
2022-12-05 15:07:21 +05:00
committed by GitHub
parent 883b816430
commit 7a0010ad0e
4 changed files with 275 additions and 1 deletions

View File

@@ -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})

View File

@@ -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)),
]

View File

@@ -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

View File

@@ -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):
"""