diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index 0d526bef08..3ed065eb0b 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -1309,7 +1309,9 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F # Update with gating info xblock_info.update(_get_gating_info(course, xblock)) - + if is_xblock_unit: + # if xblock is a Unit we add the discussion_enabled option + xblock_info['discussion_enabled'] = xblock.discussion_enabled if xblock.category == 'sequential': # Entrance exam subsection should be hidden. in_entrance_exam is # inherited metadata, all children will have it. diff --git a/cms/djangoapps/contentstore/views/tests/test_discussion_enabled.py b/cms/djangoapps/contentstore/views/tests/test_discussion_enabled.py new file mode 100644 index 0000000000..83f9d92349 --- /dev/null +++ b/cms/djangoapps/contentstore/views/tests/test_discussion_enabled.py @@ -0,0 +1,117 @@ +""" +Test module to test the discussion enabled flag. +""" + + +import json + +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory + +from cms.djangoapps.contentstore.tests.utils import CourseTestCase +from cms.djangoapps.contentstore.utils import reverse_usage_url + + +class TestDiscussionEnabled(CourseTestCase): + """ + Test discussion enabled flags functionality in a Unit. + """ + def setUp(self): + super().setUp() + self.course = self.get_test_course() + self.course_usage_key = self.course.id.make_usage_key("course", self.course.id.run) + self.non_staff_authed_user_client, _ = self.create_non_staff_authed_user_client() + + def get_test_course(self): + """ + Create and return a test course + """ + self.course = CourseFactory( + org="SHIELD", + number="SH101", + name="Introduction to Avengers", + run="2020_T2", + modulestore=self.store + ) + self.chapter = ItemFactory( + parent_location=self.course.location, + category="chapter", + display_name="What is SHIELD?", + modulestore=self.store + ) + self.sequential = ItemFactory( + parent_location=self.chapter.location, + category="sequential", + display_name="HQ", + modulestore=self.store + ) + self.vertical = ItemFactory( + parent_location=self.sequential.location, + category="vertical", + display_name="Triskelion", + modulestore=self.store + ) + self.vertical_1 = ItemFactory( + parent_location=self.sequential.location, + category="vertical", + display_name="Helicarrier", + modulestore=self.store + ) + self.course.save() + return self.course + + def _get_discussion_enabled_status(self, usage_key, client=None): + """ + Issue a GET request to fetch value of discussion_enabled flag of xblock represented by param:usage_key + """ + client = client if client is not None else self.client + url = reverse_usage_url("xblock_handler", usage_key) + resp = client.get(url, HTTP_ACCEPT="application/json") + return resp + + def get_discussion_enabled_status(self, xblock, client=None): + """ + Issue a GET request to fetch value of discussion_enabled flag of param:xblock's + """ + resp = self._get_discussion_enabled_status(xblock.location, client=client) + content = json.loads(resp.content.decode("utf-8")) + return content.get("discussion_enabled", None) + + def set_discussion_enabled_status(self, xblock, value, client=None): + """ + Issue a POST request to update value of discussion_enabled flag of param:xblock's + """ + client = client if client is not None else self.client + xblock_location = xblock.location + url = reverse_usage_url("xblock_handler", xblock_location) + resp = client.post( + url, + HTTP_ACCEPT="application/json", + data=json.dumps({"metadata": {"discussion_enabled": value}}), + content_type="application/json", + ) + return resp + + def test_discussion_enabled_false_initially(self): + """ + Tests discussion_enabled flag is False initially for vertical + """ + self.assertFalse(self.get_discussion_enabled_status(self.vertical)) + self.assertFalse(self.get_discussion_enabled_status(self.vertical_1)) + + def test_discussion_enabled_toggle(self): + """ + Tests discussion_enabled can be toggled. + """ + self.set_discussion_enabled_status(self.vertical, True) + self.assertTrue(self.get_discussion_enabled_status(self.vertical)) + self.assertFalse(self.get_discussion_enabled_status(self.vertical_1)) + + def test_non_course_author_cannot_get_or_set_discussion_enabled_flag(self): + """ + Test non course author cannot get/set discussion_enabled flag + """ + resp = self._get_discussion_enabled_status(self.course_usage_key, self.non_staff_authed_user_client) + self.assertEqual(resp.status_code, 403) + # Set call to the API with non authorised user should raise a 403 + resp = self.set_discussion_enabled_status(self.vertical, True, self.non_staff_authed_user_client) + self.assertEqual(resp.status_code, 403) diff --git a/common/lib/xmodule/xmodule/vertical_block.py b/common/lib/xmodule/xmodule/vertical_block.py index 926b281f44..4d1c82477b 100644 --- a/common/lib/xmodule/xmodule/vertical_block.py +++ b/common/lib/xmodule/xmodule/vertical_block.py @@ -11,28 +11,55 @@ from functools import reduce import pytz from lxml import etree from web_fragments.fragment import Fragment - -from xmodule.util.misc import is_xblock_an_assignment from xblock.core import XBlock # lint-amnesty, pylint: disable=wrong-import-order +from xblock.fields import Boolean, Scope from xmodule.mako_module import MakoTemplateBlockBase from xmodule.progress import Progress from xmodule.seq_module import SequenceFields from xmodule.studio_editable import StudioEditableBlock +from xmodule.util.misc import is_xblock_an_assignment from xmodule.util.xmodule_django import add_webpack_to_fragment from xmodule.x_module import PUBLIC_VIEW, STUDENT_VIEW, XModuleFields from xmodule.xml_module import XmlParserMixin log = logging.getLogger(__name__) +# Make '_' a no-op so we can scrape strings. Using lambda instead of +# `django.utils.translation.ugettext_noop` because Django cannot be imported in this file +_ = lambda text: text + # HACK: This shouldn't be hard-coded to two types # OBSOLETE: This obsoletes 'type' CLASS_PRIORITY = ['video', 'problem'] +class VerticalFields: + """ + A mixin to introduce fields in the Vertical Block. + """ + + discussion_enabled = Boolean( + display_name=_("Enable in-context discussions for the Unit"), + help=_( + "Add discussion for the Unit." + ), + default=False, + scope=Scope.settings, + ) + + @XBlock.needs('user', 'bookmarks') @XBlock.wants('completion') @XBlock.wants('call_to_action') -class VerticalBlock(SequenceFields, XModuleFields, StudioEditableBlock, XmlParserMixin, MakoTemplateBlockBase, XBlock): +class VerticalBlock( + SequenceFields, + VerticalFields, + XModuleFields, + StudioEditableBlock, + XmlParserMixin, + MakoTemplateBlockBase, + XBlock +): """ Layout XBlock for rendering subblocks vertically. """