From 3e05e0f49b778fcddc9035da224f642eaa07da70 Mon Sep 17 00:00:00 2001 From: Kshitij Sobti Date: Thu, 7 Oct 2021 11:34:03 +0530 Subject: [PATCH] feat: add support for enabling/disabling the wiki app (#28889) Currently the wiki app can't be enabled or configured. This change allows enabling/disabling the wiki app which effectively hides/shows the wiki tab. --- .../rest_api/v0/tests/test_tabs.py | 2 +- .../contentstore/views/tests/test_tabs.py | 2 +- .../course_metadata/tests/test_views.py | 8 +- .../course_wiki/plugins/__init__.py | 67 --------------- .../course_wiki/plugins/course_app.py | 82 +++++++++++++++++++ lms/djangoapps/course_wiki/tab.py | 11 +++ lms/djangoapps/course_wiki/tests/test_tab.py | 33 +++++--- lms/djangoapps/courseware/tests/test_tabs.py | 4 +- .../course_apps/tests/test_wiki_app.py | 55 +++++++++++++ .../courseware_api/tests/test_views.py | 2 +- setup.py | 2 +- 11 files changed, 181 insertions(+), 87 deletions(-) create mode 100644 lms/djangoapps/course_wiki/plugins/course_app.py create mode 100644 openedx/core/djangoapps/course_apps/tests/test_wiki_app.py diff --git a/cms/djangoapps/contentstore/rest_api/v0/tests/test_tabs.py b/cms/djangoapps/contentstore/rest_api/v0/tests/test_tabs.py index c8d7419af2..6722ed2d84 100644 --- a/cms/djangoapps/contentstore/rest_api/v0/tests/test_tabs.py +++ b/cms/djangoapps/contentstore/rest_api/v0/tests/test_tabs.py @@ -184,8 +184,8 @@ class TabsAPITests(CourseTestCase): """ Test that toggling the visibility via the API works. """ - self.check_toggle_tab_visibility("wiki", True) self.check_toggle_tab_visibility("wiki", False) + self.check_toggle_tab_visibility("wiki", True) def test_toggle_tab_visibility_fail(self): """ diff --git a/cms/djangoapps/contentstore/views/tests/test_tabs.py b/cms/djangoapps/contentstore/views/tests/test_tabs.py index 96ae6a98cf..9d3ebee96d 100644 --- a/cms/djangoapps/contentstore/views/tests/test_tabs.py +++ b/cms/djangoapps/contentstore/views/tests/test_tabs.py @@ -159,8 +159,8 @@ class TabsPageTests(CourseTestCase): def test_toggle_tab_visibility(self): """Test toggling of tab visibility""" - self.check_toggle_tab_visiblity('wiki', True) self.check_toggle_tab_visiblity('wiki', False) + self.check_toggle_tab_visiblity('wiki', True) def test_toggle_invalid_tab_visibility(self): """Test toggling visibility of an invalid tab""" diff --git a/lms/djangoapps/course_home_api/course_metadata/tests/test_views.py b/lms/djangoapps/course_home_api/course_metadata/tests/test_views.py index ec46eef5c6..8e9760f0ca 100644 --- a/lms/djangoapps/course_home_api/course_metadata/tests/test_views.py +++ b/lms/djangoapps/course_home_api/course_metadata/tests/test_views.py @@ -41,8 +41,8 @@ class CourseHomeMetadataTests(BaseCourseHomeTests): response = self.client.get(self.url) assert response.status_code == 200 assert not response.data.get('is_staff') - # 'Course', 'Wiki', 'Progress' tabs - assert len(response.data.get('tabs', [])) == 4 + # 'Course', and 'Progress' tabs + assert len(response.data.get('tabs', [])) == 3 @ddt.data(True, False) def test_get_authenticated_not_enrolled(self, has_previously_enrolled): @@ -61,8 +61,8 @@ class CourseHomeMetadataTests(BaseCourseHomeTests): assert response.status_code == 200 assert response.data['is_staff'] # This differs for a staff user because they also receive the Instructor tab - # 'Course', 'Wiki', 'Progress', and 'Instructor' tabs - assert len(response.data.get('tabs', [])) == 5 + # 'Course', 'Progress', and 'Instructor' tabs + assert len(response.data.get('tabs', [])) == 4 def test_get_masqueraded_user(self): CourseEnrollment.enroll(self.user, self.course.id, CourseMode.VERIFIED) diff --git a/lms/djangoapps/course_wiki/plugins/__init__.py b/lms/djangoapps/course_wiki/plugins/__init__.py index f236e54c05..e69de29bb2 100644 --- a/lms/djangoapps/course_wiki/plugins/__init__.py +++ b/lms/djangoapps/course_wiki/plugins/__init__.py @@ -1,67 +0,0 @@ -"""Module with the course app configuration for the Wiki.""" -from typing import Dict, Optional, TYPE_CHECKING - -from django.conf import settings -from django.utils.translation import ugettext_noop as _ -from opaque_keys.edx.keys import CourseKey - -from openedx.core.djangoapps.course_apps.plugins import CourseApp - -# Import the User model only for type checking since importing it at runtime -# will prevent the app from starting since the model is imported before -# Django's machinery is ready. -if TYPE_CHECKING: - from django.contrib.auth import get_user_model - User = get_user_model() - -WIKI_ENABLED = settings.WIKI_ENABLED - - -class WikiCourseApp(CourseApp): - """ - Course app for the Wiki. - """ - - app_id = "wiki" - name = _("Wiki") - description = _("Enable learners to access, and collaborate on course-related information.") - documentation_links = { - "learn_more_configuration": settings.WIKI_HELP_URL, - } - - @classmethod - def is_available(cls, course_key: CourseKey) -> bool: # pylint: disable=unused-argument - """ - Returns if the app is available for the course. - - The wiki is available for all courses or none of them depending on the a Django setting. - """ - return WIKI_ENABLED - - @classmethod - def is_enabled(cls, course_key: CourseKey) -> bool: # pylint: disable=unused-argument - """ - Returns if the wiki is available for the course. - - The wiki currently cannot be enabled or disabled on a per-course basis. - """ - return WIKI_ENABLED - - @classmethod - def set_enabled(cls, course_key: CourseKey, enabled: bool, user: 'User') -> bool: - """ - The wiki cannot be enabled or disabled. - """ - # Currently, you cannot enable/disable wiki via the API - raise ValueError("Wiki cannot be enabled/disabled vis this API.") - - @classmethod - def get_allowed_operations(cls, course_key: CourseKey, user: Optional['User'] = None) -> Dict[str, bool]: - """ - Returns the operations you can perform on the wiki. - """ - return { - # The wiki cannot be enabled/disabled via the API yet. - "enable": False, - "configure": True, - } diff --git a/lms/djangoapps/course_wiki/plugins/course_app.py b/lms/djangoapps/course_wiki/plugins/course_app.py new file mode 100644 index 0000000000..822d471a05 --- /dev/null +++ b/lms/djangoapps/course_wiki/plugins/course_app.py @@ -0,0 +1,82 @@ +"""Module with the course app configuration for the Wiki.""" +from typing import Dict, Optional, TYPE_CHECKING + +from django.conf import settings +from django.utils.translation import ugettext_noop as _ +from opaque_keys.edx.keys import CourseKey + +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview, CourseOverviewTab +from openedx.core.djangoapps.course_apps.plugins import CourseApp +from openedx.core.lib.courses import get_course_by_id +from xmodule.modulestore.django import modulestore +from xmodule.tabs import CourseTab, CourseTabList + +# Import the User model only for type checking since importing it at runtime +# will prevent the app from starting since the model is imported before +# Django's machinery is ready. +if TYPE_CHECKING: + from django.contrib.auth import get_user_model + + User = get_user_model() + +WIKI_ENABLED = settings.WIKI_ENABLED + + +class WikiCourseApp(CourseApp): + """ + Course app for the Wiki. + """ + + app_id = "wiki" + name = _("Wiki") + description = _("Enable learners to access, and collaborate on course-related information.") + documentation_links = { + "learn_more_configuration": settings.WIKI_HELP_URL, + } + + @classmethod + def is_available(cls, course_key: CourseKey) -> bool: # pylint: disable=unused-argument + """ + Returns if the app is available for the course. + + The wiki is available for all courses or none of them depending on the Django setting. + """ + return WIKI_ENABLED + + @classmethod + def is_enabled(cls, course_key: CourseKey) -> bool: # pylint: disable=unused-argument + """ + Returns if the wiki is enabled for the course. + """ + try: + wiki_tab = CourseOverview.get_from_id(course_key).tab_set.get(tab_id='wiki') + return not wiki_tab.is_hidden + except CourseOverviewTab.DoesNotExist: + return False + + @classmethod + def set_enabled(cls, course_key: CourseKey, enabled: bool, user: 'User') -> bool: + """ + Enabled/disables the wiki tab in the course. + """ + course = get_course_by_id(course_key) + wiki_tab = CourseTabList.get_tab_by_id(course.tabs, 'wiki') + if wiki_tab is None: + if not enabled: + return False + # If the course doesn't already have the wiki tab, add it. + wiki_tab = CourseTab.load("wiki") + course.tabs.append(wiki_tab) + wiki_tab.is_hidden = not enabled + modulestore().update_item(course, user.id) + return enabled + + @classmethod + def get_allowed_operations(cls, course_key: CourseKey, user: Optional['User'] = None) -> Dict[str, bool]: + """ + Returns the operations you can perform on the wiki. + """ + return { + "enable": True, + "configure": True, + } diff --git a/lms/djangoapps/course_wiki/tab.py b/lms/djangoapps/course_wiki/tab.py index 17d06a680e..1f0dc7f3a8 100644 --- a/lms/djangoapps/course_wiki/tab.py +++ b/lms/djangoapps/course_wiki/tab.py @@ -21,6 +21,17 @@ class WikiTab(EnrolledTab): is_hideable = True is_default = False + def __init__(self, tab_dict): + # Default to hidden + super().__init__({"is_hidden": True, **tab_dict}) + + def to_json(self): + json_val = super().to_json() + # Persist that the tab is *not* hidden + if not self.is_hidden: + json_val.update({"is_hidden": False}) + return json_val + @classmethod def is_enabled(cls, course, user=None): """ diff --git a/lms/djangoapps/course_wiki/tests/test_tab.py b/lms/djangoapps/course_wiki/tests/test_tab.py index 5af61e5dd6..094d7a1967 100644 --- a/lms/djangoapps/course_wiki/tests/test_tab.py +++ b/lms/djangoapps/course_wiki/tests/test_tab.py @@ -4,10 +4,9 @@ Tests for wiki views. from django.conf import settings -from django.test.client import RequestFactory -from lms.djangoapps.courseware.tabs import get_course_tab_list from common.djangoapps.student.tests.factories import AdminFactory, UserFactory +from lms.djangoapps.courseware.tabs import get_course_tab_list from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory @@ -21,19 +20,28 @@ class WikiTabTestCase(ModuleStoreTestCase): self.instructor = AdminFactory.create() self.user = UserFactory() + def _enable_wiki_tab(self): + """ + Enables the wiki tab globally and unhides it for the course. + """ + settings.WIKI_ENABLED = True + # Enable the wiki tab for these tests + for tab in self.course.tabs: + if tab.type == 'wiki': + tab.is_hidden = False + self.course.save() + def get_wiki_tab(self, user, course): - """Returns true if the "Wiki" tab is shown.""" - request = RequestFactory().request() + """Returns wiki tab if it is shown.""" all_tabs = get_course_tab_list(user, course) - wiki_tabs = [tab for tab in all_tabs if tab.name == 'Wiki'] - return wiki_tabs[0] if len(wiki_tabs) == 1 else None + return next((tab for tab in all_tabs if tab.type == 'wiki'), None) def test_wiki_enabled_and_public(self): """ Test wiki tab when Enabled setting is True and the wiki is open to the public. """ - settings.WIKI_ENABLED = True + self._enable_wiki_tab() self.course.allow_public_wiki_access = True assert self.get_wiki_tab(self.user, self.course) is not None @@ -41,20 +49,19 @@ class WikiTabTestCase(ModuleStoreTestCase): """ Test wiki when it is enabled but not open to the public """ - settings.WIKI_ENABLED = True + self._enable_wiki_tab() self.course.allow_public_wiki_access = False assert self.get_wiki_tab(self.user, self.course) is None assert self.get_wiki_tab(self.instructor, self.course) is not None def test_wiki_enabled_false(self): """Test wiki tab when Enabled setting is False""" - settings.WIKI_ENABLED = False assert self.get_wiki_tab(self.user, self.course) is None assert self.get_wiki_tab(self.instructor, self.course) is None def test_wiki_visibility(self): """Test toggling of visibility of wiki tab""" - settings.WIKI_ENABLED = True + self._enable_wiki_tab() self.course.allow_public_wiki_access = True wiki_tab = self.get_wiki_tab(self.user, self.course) assert wiki_tab is not None @@ -63,3 +70,9 @@ class WikiTabTestCase(ModuleStoreTestCase): assert wiki_tab['is_hidden'] wiki_tab['is_hidden'] = False assert not wiki_tab.is_hidden + + def test_wiki_hidden_by_default(self): + """ + Test that the wiki tab is hidden by default + """ + assert self.get_wiki_tab(self.user, self.course) is None diff --git a/lms/djangoapps/courseware/tests/test_tabs.py b/lms/djangoapps/courseware/tests/test_tabs.py index 4e0c1f051e..065834a240 100644 --- a/lms/djangoapps/courseware/tests/test_tabs.py +++ b/lms/djangoapps/courseware/tests/test_tabs.py @@ -426,7 +426,7 @@ class EntranceExamsTabsTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, Mi self.client.logout() self.login(self.email, self.password) course_tab_list = get_course_tab_list(self.user, self.course) - assert len(course_tab_list) == 5 + assert len(course_tab_list) == 4 def test_course_tabs_list_for_staff_members(self): """ @@ -438,7 +438,7 @@ class EntranceExamsTabsTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, Mi staff_user = StaffFactory(course_key=self.course.id) self.client.login(username=staff_user.username, password='test') course_tab_list = get_course_tab_list(staff_user, self.course) - assert len(course_tab_list) == 5 + assert len(course_tab_list) == 4 class TextBookCourseViewsTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase): diff --git a/openedx/core/djangoapps/course_apps/tests/test_wiki_app.py b/openedx/core/djangoapps/course_apps/tests/test_wiki_app.py new file mode 100644 index 0000000000..6c7228a348 --- /dev/null +++ b/openedx/core/djangoapps/course_apps/tests/test_wiki_app.py @@ -0,0 +1,55 @@ +""" +Tests for wiki course app. +""" + +from common.djangoapps.student.tests.factories import AdminFactory, UserFactory +from lms.djangoapps.course_wiki.plugins.course_app import WikiCourseApp +from openedx.core.djangolib.testing.utils import skip_unless_cms +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory + + +@skip_unless_cms +class WikiCourseAppTestCase(ModuleStoreTestCase): + """Test cases for Wiki CourseApp.""" + + def setUp(self): + super().setUp() + self.course = CourseFactory.create() + self.instructor = AdminFactory.create() + self.user = UserFactory() + + def get_wiki_tab(self, course_key): + """ + Reload the course and fetch the wiki tab if present. + """ + course = self.store.get_course(course_key) + return next((tab for tab in course.tabs if tab.type == 'wiki'), None) + + def test_app_disabled_by_default(self): + """ + Test that the wiki tab is disabled by default. + """ + assert not WikiCourseApp.is_enabled(self.course.id) + + def test_app_enabling(self): + """ + Test that enabling and disable the app enabled/disables the tab. + """ + WikiCourseApp.set_enabled(self.course.id, True, self.instructor) + wiki_tab = self.get_wiki_tab(self.course.id) + assert not wiki_tab.is_hidden + WikiCourseApp.set_enabled(self.course.id, False, self.instructor) + wiki_tab = self.get_wiki_tab(self.course.id) + assert wiki_tab.is_hidden + + def test_app_adds_wiki(self): + """ + Test that enabling the app for a course that doesn't have the wiki tab + adds the wiki tab. + """ + self.course.tabs = [tab for tab in self.course.tabs if tab.type != 'wiki'] + self.store.update_item(self.course, self.instructor.id) + assert self.get_wiki_tab(self.course.id) is None + WikiCourseApp.set_enabled(self.course.id, True, self.instructor) + assert self.get_wiki_tab(self.course.id) is not None diff --git a/openedx/core/djangoapps/courseware_api/tests/test_views.py b/openedx/core/djangoapps/courseware_api/tests/test_views.py index c8d6412ad1..2c66336bce 100644 --- a/openedx/core/djangoapps/courseware_api/tests/test_views.py +++ b/openedx/core/djangoapps/courseware_api/tests/test_views.py @@ -155,7 +155,7 @@ class CourseApiTestViews(BaseCoursewareTests, MasqueradeMixin): enrollment = response.data['enrollment'] assert enrollment_mode == enrollment['mode'] assert enrollment['is_active'] - assert len(response.data['tabs']) == 6 + assert len(response.data['tabs']) == 5 found = False for tab in response.data['tabs']: if tab['type'] == 'external_link': diff --git a/setup.py b/setup.py index 583273d1f0..de8b17c799 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ setup( "progress = lms.djangoapps.courseware.plugins:ProgressCourseApp", "teams = lms.djangoapps.teams.plugins:TeamsCourseApp", "textbooks = lms.djangoapps.courseware.plugins:TextbooksCourseApp", - "wiki = lms.djangoapps.course_wiki.plugins:WikiCourseApp", + "wiki = lms.djangoapps.course_wiki.plugins.course_app:WikiCourseApp", "custom_pages = lms.djangoapps.courseware.plugins:CustomPagesCourseApp", ], "openedx.course_tool": [