diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index 05fc1705e83e..2905e04141af 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -19,6 +19,7 @@ from xblock.runtime import KvsFieldData from openedx.core.djangoapps.video_config.services import VideoConfigService +from openedx.core.djangoapps.discussions.services import DiscussionConfigService from xmodule.contentstore.django import contentstore from xmodule.exceptions import NotFoundError as XModuleNotFoundError from xmodule.modulestore.django import XBlockI18nService, modulestore @@ -217,6 +218,7 @@ def _prepare_runtime_for_preview(request, block): "cache": CacheService(cache), 'replace_urls': ReplaceURLService, 'video_config': VideoConfigService(), + 'discussion_config_service': DiscussionConfigService(), } block.runtime.get_block_for_descriptor = partial(_load_preview_block, request) diff --git a/lms/djangoapps/courseware/block_render.py b/lms/djangoapps/courseware/block_render.py index b6e4145e2ecf..767d4033a73f 100644 --- a/lms/djangoapps/courseware/block_render.py +++ b/lms/djangoapps/courseware/block_render.py @@ -43,6 +43,7 @@ from lms.djangoapps.teams.services import TeamsService from openedx.core.djangoapps.video_config.services import VideoConfigService +from openedx.core.djangoapps.discussions.services import DiscussionConfigService from openedx.core.lib.xblock_services.call_to_action import CallToActionService from xmodule.contentstore.django import contentstore from xmodule.exceptions import NotFoundError as XModuleNotFoundError @@ -637,6 +638,7 @@ def inner_get_block(block: XBlock) -> XBlock | None: 'publish': EventPublishingService(user, course_id, track_function), 'enrollments': EnrollmentsService(), 'video_config': VideoConfigService(), + 'discussion_config_service': DiscussionConfigService(), } runtime.get_block_for_descriptor = inner_get_block diff --git a/lms/djangoapps/discussion/django_comment_client/base/views.py b/lms/djangoapps/discussion/django_comment_client/base/views.py index 14ce9c4b575a..e40ee4ef58bb 100644 --- a/lms/djangoapps/discussion/django_comment_client/base/views.py +++ b/lms/djangoapps/discussion/django_comment_client/base/views.py @@ -25,6 +25,7 @@ import lms.djangoapps.discussion.django_comment_client.settings as cc_settings import openedx.core.djangoapps.django_comment_common.comment_client as cc +from openedx.core.djangoapps.django_comment_common.models import has_permission from common.djangoapps.student.roles import GlobalStaff from common.djangoapps.track import contexts from common.djangoapps.util.file import store_uploaded_file @@ -33,8 +34,7 @@ from lms.djangoapps.courseware.exceptions import CourseAccessRedirect from lms.djangoapps.discussion.django_comment_client.permissions import ( check_permissions_by_view, - get_team, - has_permission + get_team ) from lms.djangoapps.discussion.django_comment_client.utils import ( JsonError, diff --git a/lms/djangoapps/discussion/django_comment_client/permissions.py b/lms/djangoapps/discussion/django_comment_client/permissions.py index 2eeee32fe722..4801a461c608 100644 --- a/lms/djangoapps/discussion/django_comment_client/permissions.py +++ b/lms/djangoapps/discussion/django_comment_client/permissions.py @@ -12,26 +12,11 @@ from openedx.core.djangoapps.django_comment_common.comment_client import Thread from openedx.core.djangoapps.django_comment_common.models import ( CourseDiscussionSettings, - all_permissions_for_user_in_course + has_permission ) from openedx.core.lib.cache_utils import request_cached -def has_permission(user, permission, course_id=None): # lint-amnesty, pylint: disable=missing-function-docstring - assert isinstance(course_id, (type(None), CourseKey)) - request_cache_dict = DEFAULT_REQUEST_CACHE.data - cache_key = "django_comment_client.permissions.has_permission.all_permissions.{}.{}".format( - user.id, course_id - ) - if cache_key in request_cache_dict: - all_permissions = request_cache_dict[cache_key] - else: - all_permissions = all_permissions_for_user_in_course(user, course_id) - request_cache_dict[cache_key] = all_permissions - - return permission in all_permissions - - CONDITIONS = ['is_open', 'is_author', 'is_question_author', 'is_team_member_if_applicable'] diff --git a/lms/djangoapps/discussion/django_comment_client/utils.py b/lms/djangoapps/discussion/django_comment_client/utils.py index e26b748270e3..a0bb6b769183 100644 --- a/lms/djangoapps/discussion/django_comment_client/utils.py +++ b/lms/djangoapps/discussion/django_comment_client/utils.py @@ -24,10 +24,10 @@ from lms.djangoapps.discussion.django_comment_client.constants import TYPE_ENTRY, TYPE_SUBCATEGORY from lms.djangoapps.discussion.django_comment_client.permissions import ( check_permissions_by_view, - get_team, - has_permission + get_team ) from lms.djangoapps.discussion.django_comment_client.settings import MAX_COMMENT_DEPTH +from openedx.core.djangoapps.django_comment_common.models import has_permission from openedx.core.djangoapps.course_groups.cohorts import get_cohort_id from openedx.core.djangoapps.discussions.utils import ( get_accessible_discussion_xblocks, diff --git a/lms/djangoapps/discussion/templates/discussion/discussion_profile_page.html b/lms/djangoapps/discussion/templates/discussion/discussion_profile_page.html index f88c33440ce7..90f03999d539 100644 --- a/lms/djangoapps/discussion/templates/discussion/discussion_profile_page.html +++ b/lms/djangoapps/discussion/templates/discussion/discussion_profile_page.html @@ -11,7 +11,7 @@ from django.template.defaultfilters import escapejs from django.urls import reverse -from lms.djangoapps.discussion.django_comment_client.permissions import has_permission +from openedx.core.djangoapps.django_comment_common.models import has_permission from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string %> diff --git a/lms/djangoapps/discussion/views.py b/lms/djangoapps/discussion/views.py index d6f61d209433..bca6cb7768de 100644 --- a/lms/djangoapps/discussion/views.py +++ b/lms/djangoapps/discussion/views.py @@ -28,6 +28,7 @@ import lms.djangoapps.discussion.django_comment_client.utils as utils import openedx.core.djangoapps.django_comment_common.comment_client as cc +from openedx.core.djangoapps.django_comment_common.models import has_permission from common.djangoapps.student.models import CourseEnrollment from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole, GlobalStaff from common.djangoapps.util.json_request import JsonResponse, expect_json @@ -37,7 +38,6 @@ from lms.djangoapps.discussion.config.settings import is_forum_daily_digest_enabled from lms.djangoapps.discussion.django_comment_client.base.views import track_thread_viewed_event from lms.djangoapps.discussion.django_comment_client.constants import TYPE_ENTRY -from lms.djangoapps.discussion.django_comment_client.permissions import has_permission from lms.djangoapps.discussion.django_comment_client.utils import ( add_courseware_context, course_discussion_division_enabled, diff --git a/openedx/core/djangoapps/discussions/services.py b/openedx/core/djangoapps/discussions/services.py new file mode 100644 index 000000000000..12ad5692cc6e --- /dev/null +++ b/openedx/core/djangoapps/discussions/services.py @@ -0,0 +1,38 @@ +""" +Discussion Configuration Service for XBlock runtime. + +This service provides discussion-related configuration and feature flags +that are specific to the edx-platform implementation +for the extracted discussion block in xblocks-contrib repository. +""" + +from django.conf import settings +from django.contrib.auth.models import User # pylint: disable=imported-auth-user +from opaque_keys.edx.keys import CourseKey +from openedx.core.djangoapps.django_comment_common.models import has_permission +from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration, Provider + + +class DiscussionConfigService: + """ + Service for providing discussion-related configuration and feature flags. + """ + + def has_permission(self, user: User, permission: str, course_id: CourseKey = None) -> bool: + """ + Return whether the user has the given discussion permission for a given course. + """ + return has_permission(user, permission, course_id) + + def is_discussion_visible(self, course_key: CourseKey) -> bool: + """ + Discussion Xblock does not support new OPEN_EDX provider + """ + provider = DiscussionsConfiguration.get(course_key) + return provider.provider_type == Provider.LEGACY + + def is_discussion_enabled(self) -> bool: + """ + Return True if discussions are enabled; else False + """ + return settings.ENABLE_DISCUSSION_SERVICE diff --git a/openedx/core/djangoapps/django_comment_common/models.py b/openedx/core/djangoapps/django_comment_common/models.py index bd7b8fe66e67..51863c42d472 100644 --- a/openedx/core/djangoapps/django_comment_common/models.py +++ b/openedx/core/djangoapps/django_comment_common/models.py @@ -14,6 +14,8 @@ from django.utils.translation import gettext_noop from jsonfield.fields import JSONField from opaque_keys.edx.django.models import CourseKeyField +from edx_django_utils.cache import DEFAULT_REQUEST_CACHE +from opaque_keys.edx.keys import CourseKey from openedx.core.djangoapps.xmodule_django.models import NoneToEmptyManager from openedx.core.lib.cache_utils import request_cached @@ -193,6 +195,37 @@ def all_permissions_for_user_in_course(user, course_id): return permission_names +def has_permission(user, permission, course_id=None): + """ + This function resolves all discussion-related permissions for the given + user and course, caches them for the duration of the request, and verifies + whether the requested permission is present. + + Args: + user (User): Django user whose permissions are being checked. + permission (str): Discussion permission identifier + (e.g., "create_comment", "create_thread"). + course_id (CourseKey): Course context in which to evaluate + the permission + + Returns: + bool: True if the user has the specified permission in the given + course context; False otherwise. + """ + assert isinstance(course_id, (type(None), CourseKey)) + request_cache_dict = DEFAULT_REQUEST_CACHE.data + cache_key = "django_comment_client.permissions.has_permission.all_permissions.{}.{}".format( + user.id, course_id + ) + if cache_key in request_cache_dict: + all_permissions = request_cache_dict[cache_key] + else: + all_permissions = all_permissions_for_user_in_course(user, course_id) + request_cache_dict[cache_key] = all_permissions + + return permission in all_permissions + + class ForumsConfig(ConfigurationModel): """ Config for the connection to the cs_comments_service forums backend. diff --git a/openedx/core/djangoapps/xblock/runtime/runtime.py b/openedx/core/djangoapps/xblock/runtime/runtime.py index 2ae4a431bfbe..041450d8a341 100644 --- a/openedx/core/djangoapps/xblock/runtime/runtime.py +++ b/openedx/core/djangoapps/xblock/runtime/runtime.py @@ -347,6 +347,9 @@ def service(self, block: XBlock, service_name: str): # Import here to avoid circular dependency from openedx.core.djangoapps.video_config.services import VideoConfigService return VideoConfigService() + elif service_name == 'discussion_config_service': + from openedx.core.djangoapps.discussions.services import DiscussionConfigService + return DiscussionConfigService() # Otherwise, fall back to the base implementation which loads services # defined in the constructor: diff --git a/openedx/features/course_bookmarks/templates/course_bookmarks/course-bookmarks.html b/openedx/features/course_bookmarks/templates/course_bookmarks/course-bookmarks.html index a7038b3bdae6..bda15a7431af 100644 --- a/openedx/features/course_bookmarks/templates/course_bookmarks/course-bookmarks.html +++ b/openedx/features/course_bookmarks/templates/course_bookmarks/course-bookmarks.html @@ -15,7 +15,7 @@ from django.utils.translation import gettext as _ from django.template.defaultfilters import escapejs -from lms.djangoapps.discussion.django_comment_client.permissions import has_permission +from openedx.core.djangoapps.django_comment_common.models import has_permission from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string from openedx.core.djangolib.markup import HTML from openedx.features.course_experience import course_home_page_title diff --git a/xmodule/discussion_block.py b/xmodule/discussion_block.py index aaea2de7bb2a..7243516eb7d3 100644 --- a/xmodule/discussion_block.py +++ b/xmodule/discussion_block.py @@ -17,8 +17,6 @@ from xblock.utils.studio_editable import StudioEditableXBlockMixin from xblocks_contrib.discussion import DiscussionXBlock as _ExtractedDiscussionXBlock -from lms.djangoapps.discussion.django_comment_client.permissions import has_permission -from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration, Provider from openedx.core.djangolib.markup import HTML, Text from openedx.core.lib.xblock_utils import get_css_dependencies, get_js_dependencies from xmodule.xml_block import XmlMixin @@ -37,6 +35,7 @@ def _(text): @XBlock.needs('user') # pylint: disable=abstract-method @XBlock.needs('i18n') @XBlock.needs('mako') +@XBlock.needs('discussion_config_service') class _BuiltInDiscussionXBlock(XBlock, StudioEditableXBlockMixin, XmlMixin): # lint-amnesty, pylint: disable=abstract-method """ @@ -76,6 +75,13 @@ class _BuiltInDiscussionXBlock(XBlock, StudioEditableXBlockMixin, has_author_view = True # Tells Studio to use author_view + @property + def discussion_config_service(self): + """ + Returns discussion configuration service. + """ + return self.runtime.service(self, 'discussion_config_service') + @property def course_key(self): return getattr(self.scope_ids.usage_id, 'course_key', None) @@ -85,8 +91,18 @@ def is_visible(self): """ Discussion Xblock does not support new OPEN_EDX provider """ - provider = DiscussionsConfiguration.get(self.course_key) - return provider.provider_type == Provider.LEGACY + if self.discussion_config_service: + return self.discussion_config_service.is_discussion_visible(self.course_key) + return False + + @property + def is_discussion_enabled(self): + """ + Returns True if discussions are enabled; else False + """ + if self.discussion_config_service: + return self.discussion_config_service.is_discussion_enabled() + return False @property def django_user(self): @@ -159,15 +175,14 @@ def has_permission(self, permission): :param str permission: Permission :rtype: bool """ - return has_permission(self.django_user, permission, self.course_key) + if self.discussion_config_service: + return self.discussion_config_service.has_permission(self.django_user, permission, self.course_key) + return False def student_view(self, context=None): """ Renders student view for LMS. """ - # to prevent a circular import issue - import lms.djangoapps.discussion.django_comment_client.utils as utils - fragment = Fragment() if not self.is_visible: @@ -193,7 +208,7 @@ def student_view(self, context=None): url='{}?{}'.format(reverse('register_user'), qs), ), ) - if utils.is_discussion_enabled(self.course_key): + if self.is_discussion_enabled: context = { 'discussion_id': self.discussion_id, 'display_name': self.display_name if self.display_name else _("Discussion"), @@ -282,8 +297,17 @@ def _apply_metadata_and_policy(cls, block, node, runtime): setattr(block, field_name, value) -DiscussionXBlock = ( - _ExtractedDiscussionXBlock if settings.USE_EXTRACTED_DISCUSSION_BLOCK - else _BuiltInDiscussionXBlock -) +DiscussionXBlock = None + + +def reset_class(): + """Reset class as per django settings flag""" + global DiscussionXBlock + DiscussionXBlock = ( + _ExtractedDiscussionXBlock if settings.USE_EXTRACTED_DISCUSSION_BLOCK + else _BuiltInDiscussionXBlock + ) + return DiscussionXBlock + +reset_class() DiscussionXBlock.__name__ = "DiscussionXBlock"