From e04d53a9a1a23df6fb399f224a342db131ab4cae Mon Sep 17 00:00:00 2001 From: Muhammad Adeel Tajamul <77053848+muhammadadeeltajamul@users.noreply.github.com> Date: Wed, 14 Dec 2022 13:21:31 +0500 Subject: [PATCH] feat: added support for new topics for mobile (#31441) Co-authored-by: adeel.tajamul --- lms/djangoapps/discussion/rest_api/api.py | 106 ++++++++++++++++++ .../discussion/rest_api/tests/test_views.py | 41 +++++++ lms/djangoapps/discussion/rest_api/utils.py | 9 ++ lms/djangoapps/discussion/rest_api/views.py | 24 +++- 4 files changed, 175 insertions(+), 5 deletions(-) diff --git a/lms/djangoapps/discussion/rest_api/api.py b/lms/djangoapps/discussion/rest_api/api.py index c6f8d2535a..7c85c93bdd 100644 --- a/lms/djangoapps/discussion/rest_api/api.py +++ b/lms/djangoapps/discussion/rest_api/api.py @@ -33,6 +33,7 @@ from common.djangoapps.student.roles import ( CourseStaffRole, ) +from lms.djangoapps.course_api.blocks.api import get_blocks from lms.djangoapps.course_blocks.api import get_course_blocks from lms.djangoapps.courseware.courses import get_course_with_access from lms.djangoapps.courseware.exceptions import CourseAccessRedirect @@ -116,13 +117,16 @@ from .serializers import ( get_context ) from .utils import ( + AttributeDict, add_stats_for_users_with_no_discussion_content, + create_blocks_params, discussion_open_for_user, get_usernames_for_course, get_usernames_from_search_string, set_attribute ) + User = get_user_model() ThreadType = Literal["discussion", "question"] @@ -533,6 +537,108 @@ def get_course_topics(request: Request, course_key: CourseKey, topic_ids: Option } +def get_v2_non_courseware_topics_as_v1(request, course_key, topics): + """ + Takes v2 topics list and returns v1 list of non courseware topics + """ + non_courseware_topics = [] + for topic in topics: + if topic.get('usage_key', '') is None: + for key in ['usage_key', 'enabled_in_context']: + topic.pop(key) + topic.update({ + 'children': [], + 'thread_list_url': get_thread_list_url( + request, + course_key, + topic.get('id'), + ) + }) + non_courseware_topics.append(topic) + return non_courseware_topics + + +def get_v2_courseware_topics_as_v1(request, course_key, sequentials, topics): + """ + Returns v2 courseware topics list as v1 structure + """ + courseware_topics = [] + for sequential in sequentials: + children = [] + for child in sequential.get('children', []): + for topic in topics: + if child == topic.get('usage_key'): + topic.update({ + 'children': [], + 'thread_list_url': get_thread_list_url( + request, + course_key, + [topic.get('id')], + ) + }) + topic.pop('enabled_in_context') + children.append(AttributeDict(topic)) + + discussion_topic = DiscussionTopic( + None, + sequential.get('display_name'), + get_thread_list_url( + request, + course_key, + [child.id for child in children], + ), + children, + None, + ) + courseware_topics.append(DiscussionTopicSerializer(discussion_topic).data) + return courseware_topics + + +def get_v2_course_topics_as_v1( + request: Request, + course_key: CourseKey, + topic_ids: Optional[Iterable[str]] = None, +): + """ + Returns v2 topics in v1 structure + """ + 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'] + + sequentials = [value for _, value in blocks.items() + if value.get('type') == "sequential"] + + topics = get_course_topics_v2(course_key, request.user, topic_ids) + non_courseware_topics = get_v2_non_courseware_topics_as_v1( + request, + course_key, + topics, + ) + courseware_topics = get_v2_courseware_topics_as_v1( + request, + course_key, + sequentials, + topics, + ) + return { + "courseware_topics": courseware_topics, + "non_courseware_topics": non_courseware_topics, + } + + def get_course_topics_v2( course_key: CourseKey, user: User, diff --git a/lms/djangoapps/discussion/rest_api/tests/test_views.py b/lms/djangoapps/discussion/rest_api/tests/test_views.py index f82abecbdf..2455d73e0e 100644 --- a/lms/djangoapps/discussion/rest_api/tests/test_views.py +++ b/lms/djangoapps/discussion/rest_api/tests/test_views.py @@ -876,6 +876,47 @@ class CourseTopicsViewTest(DiscussionAPIViewTestMixin, CommentsServiceMockMixin, } ) + @override_waffle_flag(ENABLE_NEW_STRUCTURE_DISCUSSIONS, True) + def test_new_course_structure_response(self): + """ + Tests whether the new structure is available on old topics API + (For mobile compatibility) + """ + chapter = ItemFactory.create( + parent_location=self.course.location, + category='chapter', + display_name="Week 1", + start=datetime(2015, 3, 1, tzinfo=UTC), + ) + sequential = ItemFactory.create( + parent_location=chapter.location, + category='sequential', + display_name="Lesson 1", + start=datetime(2015, 3, 1, tzinfo=UTC), + ) + ItemFactory.create( + parent_location=sequential.location, + category='vertical', + display_name='vertical', + start=datetime(2015, 4, 1, tzinfo=UTC), + ) + DiscussionsConfiguration.objects.create( + context_key=self.course.id, + provider_type=Provider.OPEN_EDX + ) + update_discussions_settings_from_course_task(str(self.course.id)) + response = json.loads(self.client.get(self.url).content.decode()) + keys = ['children', 'id', 'name', 'thread_counts', 'thread_list_url'] + assert list(response.keys()) == ['courseware_topics', 'non_courseware_topics'] + assert len(response['courseware_topics']) == 1 + courseware_keys = list(response['courseware_topics'][0].keys()) + courseware_keys.sort() + assert courseware_keys == keys + assert len(response['non_courseware_topics']) == 1 + non_courseware_keys = list(response['non_courseware_topics'][0].keys()) + non_courseware_keys.sort() + assert non_courseware_keys == keys + @ddt.ddt @mock.patch('lms.djangoapps.discussion.rest_api.api._get_course', mock.Mock()) diff --git a/lms/djangoapps/discussion/rest_api/utils.py b/lms/djangoapps/discussion/rest_api/utils.py index 905f5fbcb1..a0d2e35943 100644 --- a/lms/djangoapps/discussion/rest_api/utils.py +++ b/lms/djangoapps/discussion/rest_api/utils.py @@ -17,6 +17,15 @@ from openedx.core.djangoapps.django_comment_common.models import ( ) +class AttributeDict(dict): + """ + Converts Dict Keys into Attributes + """ + __getattr__ = dict.__getitem__ + __setattr__ = dict.__setitem__ + __delattr__ = dict.__delitem__ + + def discussion_open_for_user(course, user): """ Check if course discussion are open or not for user. diff --git a/lms/djangoapps/discussion/rest_api/views.py b/lms/djangoapps/discussion/rest_api/views.py index 6bf8a8dca9..bcfc9c902d 100644 --- a/lms/djangoapps/discussion/rest_api/views.py +++ b/lms/djangoapps/discussion/rest_api/views.py @@ -28,6 +28,8 @@ 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 from lms.djangoapps.instructor.access import update_forum_role +from openedx.core.djangoapps.discussions.config.waffle import ENABLE_NEW_STRUCTURE_DISCUSSIONS +from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration, Provider from openedx.core.djangoapps.discussions.serializers import DiscussionSettingsSerializer from openedx.core.djangoapps.django_comment_common import comment_client from openedx.core.djangoapps.django_comment_common.models import CourseDiscussionSettings, Role @@ -52,6 +54,7 @@ from ..rest_api.api import ( get_thread_list, get_learner_active_thread_list, get_user_comments, + get_v2_course_topics_as_v1, update_comment, update_thread, ) @@ -227,11 +230,22 @@ class CourseTopicsView(DeveloperErrorViewMixin, APIView): topic_ids = self.request.GET.get('topic_id') topic_ids = set(topic_ids.strip(',').split(',')) if topic_ids else None with modulestore().bulk_operations(course_key): - response = get_course_topics( - request, - course_key, - topic_ids, - ) + configuration = DiscussionsConfiguration.get(context_key=course_key) + provider = configuration.provider_type + # This will be removed when mobile app will support new topic structure + new_structure_enabled = ENABLE_NEW_STRUCTURE_DISCUSSIONS.is_enabled(course_key) + if provider == Provider.OPEN_EDX and new_structure_enabled: + response = get_v2_course_topics_as_v1( + request, + course_key, + topic_ids + ) + else: + response = get_course_topics( + request, + course_key, + topic_ids, + ) # Record user activity for tracking progress towards a user's course goals (for mobile app) UserActivity.record_user_activity(request.user, course_key, request=request, only_if_mobile_app=True) return Response(response)