diff --git a/common/test/acceptance/pages/lms/instructor_dashboard.py b/common/test/acceptance/pages/lms/instructor_dashboard.py index b6000e0e3f..077c7d02f2 100644 --- a/common/test/acceptance/pages/lms/instructor_dashboard.py +++ b/common/test/acceptance/pages/lms/instructor_dashboard.py @@ -47,6 +47,15 @@ class InstructorDashboardPage(CoursePage): cohort_management_section.wait_for_page() return cohort_management_section + def select_discussion_management(self): + """ + Selects the Discussion tab and returns the DiscussionmanagementSection + """ + self.q(css='[data-section="discussions_management"').first.click() + discussion_management_section = DiscussionManagementSection(self.browser) + discussion_management_section.wait_for_page() + return discussion_management_section + def select_data_download(self): """ Selects the data download tab and returns a DataDownloadPage. @@ -666,61 +675,31 @@ class CohortManagementSection(PageObject): self.q(css=self._bounded_selector('.cohorts-state')).first.click() self.wait_for_ajax() - def toggles_showing_of_discussion_topics(self): + def cohort_management_controls_visible(self): """ - Shows the discussion topics. + Return the visibility status of cohort management controls(cohort selector section etc). """ - self.q(css=self._bounded_selector(".toggle-cohort-management-discussions")).first.click() - self.wait_for_element_visibility("#cohort-discussions-management", "Waiting for discussions to appear") + return (self.q(css=self._bounded_selector('.cohort-management-nav')).visible and + self.q(css=self._bounded_selector('.wrapper-cohort-supplemental')).visible) - def discussion_topics_visible(self): - """ - Returns the visibility status of cohort discussion controls. - """ - EmptyPromise( - lambda: self.q(css=self._bounded_selector('.cohort-discussions-nav')).results != 0, - "Waiting for discussion section to show" - ).fulfill() - return (self.q(css=self._bounded_selector('.cohort-course-wide-discussions-nav')).visible and - self.q(css=self._bounded_selector('.cohort-inline-discussions-nav')).visible) +class DiscussionManagementSection(PageObject): - def select_discussion_topic(self, key): - """ - Selects discussion topic checkbox by clicking on it. - """ - self.q(css=self._bounded_selector(".check-discussion-subcategory-%s" % key)).first.click() + url = None - def select_always_inline_discussion(self): - """ - Selects the always_cohort_inline_discussions radio button. - """ - self.q(css=self._bounded_selector(".check-all-inline-discussions")).first.click() + discussion_form_selectors = { + 'course-wide': '.cohort-course-wide-discussions-form', + 'inline': '.cohort-inline-discussions-form' + } - def always_inline_discussion_selected(self): - """ - Returns true if always_cohort_inline_discussions radio button is selected. - """ - return len(self.q(css=self._bounded_selector(".check-all-inline-discussions:checked"))) > 0 + def is_browser_on_page(self): + return self.q(css=self.discussion_form_selectors['course-wide']).present - def cohort_some_inline_discussion_selected(self): + def _bounded_selector(self, selector): """ - Returns true if some_cohort_inline_discussions radio button is selected. + Return `selector`, but limited to the divided discussion management context. """ - return len(self.q(css=self._bounded_selector(".check-cohort-inline-discussions:checked"))) > 0 - - def select_cohort_some_inline_discussion(self): - """ - Selects the cohort_some_inline_discussions radio button. - """ - self.q(css=self._bounded_selector(".check-cohort-inline-discussions")).first.click() - - def inline_discussion_topics_disabled(self): - """ - Returns the status of inline discussion topics, enabled or disabled. - """ - inline_topics = self.q(css=self._bounded_selector('.check-discussion-subcategory-inline')) - return all(topic.get_attribute('disabled') == 'true' for topic in inline_topics) + return '.discussions-management {}'.format(selector) def is_save_button_disabled(self, key): """ @@ -730,18 +709,36 @@ class CohortManagementSection(PageObject): disabled = self.q(css=self._bounded_selector(save_button_css)).attrs('disabled') return disabled[0] == 'true' - def is_category_selected(self): + def discussion_topics_visible(self): """ - Returns the status for category checkboxes. + Returns the visibility status of divide discussion controls. """ - return self.q(css=self._bounded_selector('.check-discussion-category:checked')).is_present() + return (self.q(css=self._bounded_selector('.course-wide-discussions-nav')).visible and + self.q(css=self._bounded_selector('.inline-discussions-nav')).visible) - def get_cohorted_topics_count(self, key): + def divided_discussion_heading_is_visible(self, key): """ - Returns the count for cohorted topics. + Returns the text of discussion topic headings if it exists, otherwise return False. """ - cohorted_topics = self.q(css=self._bounded_selector('.check-discussion-subcategory-%s:checked' % key)) - return len(cohorted_topics.results) + form_heading_css = '%s %s' % (self.discussion_form_selectors[key], '.subsection-title') + discussion_heading = self.q(css=self._bounded_selector(form_heading_css)) + + if len(discussion_heading) == 0: + return False + return discussion_heading.first.text[0] + + def select_always_inline_discussion(self): + """ + Selects the always_divide_inline_discussions radio button. + """ + self.q(css=self._bounded_selector(".check-all-inline-discussions")).first.click() + + def inline_discussion_topics_disabled(self): + """ + Returns the status of inline discussion topics, enabled or disabled. + """ + inline_topics = self.q(css=self._bounded_selector('.check-discussion-subcategory-inline')) + return all(topic.get_attribute('disabled') == 'true' for topic in inline_topics) def save_discussion_topics(self, key): """ @@ -750,7 +747,38 @@ class CohortManagementSection(PageObject): save_button_css = '%s %s' % (self.discussion_form_selectors[key], '.action-save') self.q(css=self._bounded_selector(save_button_css)).first.click() - def get_cohort_discussions_message(self, key, msg_type="confirmation"): + def always_inline_discussion_selected(self): + """ + Returns true if always_divide_inline_discussions radio button is selected. + """ + return len(self.q(css=self._bounded_selector(".check-all-inline-discussions:checked"))) > 0 + + def divide_some_inline_discussion_selected(self): + """ + Returns true if divide_some_inline_discussions radio button is selected. + """ + return len(self.q(css=self._bounded_selector(".check-cohort-inline-discussions:checked"))) > 0 + + def select_divide_some_inline_discussion(self): + """ + Selects the divide_some_inline_discussions radio button. + """ + self.q(css=self._bounded_selector(".check-cohort-inline-discussions")).first.click() + + def get_divided_topics_count(self, key): + """ + Returns the count for divided topics. + """ + divided_topics = self.q(css=self._bounded_selector('.check-discussion-subcategory-%s:checked' % key)) + return len(divided_topics.results) + + def select_discussion_topic(self, key): + """ + Selects discussion topic checkbox by clicking on it. + """ + self.q(css=self._bounded_selector(".check-discussion-subcategory-%s" % key)).first.click() + + def get_divide_discussions_message(self, key, msg_type="confirmation"): """ Returns the message related to modifying discussion topics. """ @@ -767,23 +795,11 @@ class CohortManagementSection(PageObject): return '' return message_title.first.text[0] - def cohort_discussion_heading_is_visible(self, key): + def is_category_selected(self): """ - Returns the visibility of discussion topic headings. + Returns the status for category checkboxes. """ - form_heading_css = '%s %s' % (self.discussion_form_selectors[key], '.subsection-title') - discussion_heading = self.q(css=self._bounded_selector(form_heading_css)) - - if len(discussion_heading) == 0: - return False - return discussion_heading.first.text[0] - - def cohort_management_controls_visible(self): - """ - Return the visibility status of cohort management controls(cohort selector section etc). - """ - return (self.q(css=self._bounded_selector('.cohort-management-nav')).visible and - self.q(css=self._bounded_selector('.wrapper-cohort-supplemental')).visible) + return self.q(css=self._bounded_selector('.check-discussion-category:checked')).is_present() class MembershipPageAutoEnrollSection(PageObject): diff --git a/common/test/acceptance/tests/discussion/helpers.py b/common/test/acceptance/tests/discussion/helpers.py index 95c4543c91..3e216dd4a4 100644 --- a/common/test/acceptance/tests/discussion/helpers.py +++ b/common/test/acceptance/tests/discussion/helpers.py @@ -76,11 +76,16 @@ class CohortTestMixin(object): def enable_cohorting(self, course_fixture): """ - enables cohorting for the current course fixture. + enables cohorts and always_divide_inline_discussions for the current course fixture. """ url = LMS_BASE_URL + "/courses/" + course_fixture._course_key + '/cohorts/settings' # pylint: disable=protected-access - data = json.dumps({'always_cohort_inline_discussions': True}) + discussions_url = LMS_BASE_URL + "/courses/" + course_fixture._course_key + '/discussions/settings' # pylint: disable=protected-access + + data = json.dumps({'is_cohorted': True}) + discussions_data = json.dumps({'always_divide_inline_discussions': True}) + response = course_fixture.session.patch(url, data=data, headers=course_fixture.headers) + course_fixture.session.patch(discussions_url, data=discussions_data, headers=course_fixture.headers) def disable_cohorting(self, course_fixture): """ diff --git a/common/test/acceptance/tests/discussion/test_cohort_management.py b/common/test/acceptance/tests/discussion/test_cohort_management.py index c3546f8a60..815971b732 100644 --- a/common/test/acceptance/tests/discussion/test_cohort_management.py +++ b/common/test/acceptance/tests/discussion/test_cohort_management.py @@ -693,267 +693,6 @@ class CohortConfigurationTest(EventsTestMixin, UniqueCourseTest, CohortTestMixin self.cohort_management_page.a11y_audit.check_for_accessibility_errors() -@attr(shard=6) -class CohortDiscussionTopicsTest(UniqueCourseTest, CohortTestMixin): - """ - Tests for cohorting the inline and course-wide discussion topics. - """ - def setUp(self): - """ - Set up a discussion topics - """ - super(CohortDiscussionTopicsTest, self).setUp() - - self.discussion_id = "test_discussion_{}".format(uuid.uuid4().hex) - self.course_fixture = CourseFixture(**self.course_info).add_children( - XBlockFixtureDesc("chapter", "Test Section").add_children( - XBlockFixtureDesc("sequential", "Test Subsection").add_children( - XBlockFixtureDesc("vertical", "Test Unit").add_children( - XBlockFixtureDesc( - "discussion", - "Test Discussion", - metadata={"discussion_id": self.discussion_id} - ) - ) - ) - ) - ).install() - - # create course with single cohort and two content groups (user_partition of type "cohort") - self.cohort_name = "OnlyCohort" - self.setup_cohort_config(self.course_fixture) - self.cohort_id = self.add_manual_cohort(self.course_fixture, self.cohort_name) - - # login as an instructor - self.instructor_name = "instructor_user" - self.instructor_id = AutoAuthPage( - self.browser, username=self.instructor_name, email="instructor_user@example.com", - course_id=self.course_id, staff=True - ).visit().get_user_id() - - # go to the membership page on the instructor dashboard - self.instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id) - self.instructor_dashboard_page.visit() - self.cohort_management_page = self.instructor_dashboard_page.select_cohort_management() - self.cohort_management_page.wait_for_page() - - self.course_wide_key = 'course-wide' - self.inline_key = 'inline' - - def cohort_discussion_topics_are_visible(self): - """ - Assert that discussion topics are visible with appropriate content. - """ - self.cohort_management_page.toggles_showing_of_discussion_topics() - self.assertTrue(self.cohort_management_page.discussion_topics_visible()) - - self.assertEqual( - "Course-Wide Discussion Topics", - self.cohort_management_page.cohort_discussion_heading_is_visible(self.course_wide_key) - ) - self.assertTrue(self.cohort_management_page.is_save_button_disabled(self.course_wide_key)) - - self.assertEqual( - "Content-Specific Discussion Topics", - self.cohort_management_page.cohort_discussion_heading_is_visible(self.inline_key) - ) - self.assertTrue(self.cohort_management_page.is_save_button_disabled(self.inline_key)) - - def save_and_verify_discussion_topics(self, key): - """ - Saves the discussion topics and the verify the changes. - """ - # click on the inline save button. - self.cohort_management_page.save_discussion_topics(key) - - # verifies that changes saved successfully. - confirmation_message = self.cohort_management_page.get_cohort_discussions_message(key=key) - self.assertEqual("Your changes have been saved.", confirmation_message) - - # save button disabled again. - self.assertTrue(self.cohort_management_page.is_save_button_disabled(key)) - - def reload_page(self): - """ - Refresh the page. - """ - self.browser.refresh() - self.cohort_management_page.wait_for_page() - - self.instructor_dashboard_page.select_cohort_management() - self.cohort_management_page.wait_for_page() - - self.cohort_discussion_topics_are_visible() - - def verify_discussion_topics_after_reload(self, key, cohorted_topics): - """ - Verifies the changed topics. - """ - self.reload_page() - self.assertEqual(self.cohort_management_page.get_cohorted_topics_count(key), cohorted_topics) - - def test_cohort_course_wide_discussion_topic(self): - """ - Scenario: cohort a course-wide discussion topic. - - Given I have a course with a cohort defined, - And a course-wide discussion with disabled Save button. - When I click on the course-wide discussion topic - Then I see the enabled save button - When I click on save button - Then I see success message - When I reload the page - Then I see the discussion topic selected - """ - self.cohort_discussion_topics_are_visible() - - cohorted_topics_before = self.cohort_management_page.get_cohorted_topics_count(self.course_wide_key) - self.cohort_management_page.select_discussion_topic(self.course_wide_key) - - self.assertFalse(self.cohort_management_page.is_save_button_disabled(self.course_wide_key)) - - self.save_and_verify_discussion_topics(key=self.course_wide_key) - cohorted_topics_after = self.cohort_management_page.get_cohorted_topics_count(self.course_wide_key) - - self.assertNotEqual(cohorted_topics_before, cohorted_topics_after) - - self.verify_discussion_topics_after_reload(self.course_wide_key, cohorted_topics_after) - - def test_always_cohort_inline_topic_enabled(self): - """ - Scenario: Select the always_cohort_inline_topics radio button - - Given I have a course with a cohort defined, - And an inline discussion topic with disabled Save button. - When I click on always_cohort_inline_topics - Then I see enabled save button - And I see disabled inline discussion topics - When I save the change - And I reload the page - Then I see the always_cohort_inline_topics option enabled - """ - self.cohort_discussion_topics_are_visible() - - # enable always inline discussion topics and save the change - self.cohort_management_page.select_always_inline_discussion() - self.assertFalse(self.cohort_management_page.is_save_button_disabled(self.inline_key)) - self.assertTrue(self.cohort_management_page.inline_discussion_topics_disabled()) - self.cohort_management_page.save_discussion_topics(key=self.inline_key) - - self.reload_page() - self.assertTrue(self.cohort_management_page.always_inline_discussion_selected()) - - def test_cohort_some_inline_topics_enabled(self): - """ - Scenario: Select the cohort_some_inline_topics radio button - - Given I have a course with a cohort defined and always_cohort_inline_topics set to True - And an inline discussion topic with disabled Save button. - When I click on cohort_some_inline_topics - Then I see enabled save button - And I see enabled inline discussion topics - When I save the change - And I reload the page - Then I see the cohort_some_inline_topics option enabled - """ - self.cohort_discussion_topics_are_visible() - # By default always inline discussion topics is False. Enable it (and reload the page). - self.assertFalse(self.cohort_management_page.always_inline_discussion_selected()) - self.cohort_management_page.select_always_inline_discussion() - self.cohort_management_page.save_discussion_topics(key=self.inline_key) - self.reload_page() - self.assertFalse(self.cohort_management_page.cohort_some_inline_discussion_selected()) - - # enable some inline discussion topic radio button. - self.cohort_management_page.select_cohort_some_inline_discussion() - # I see that save button is enabled - self.assertFalse(self.cohort_management_page.is_save_button_disabled(self.inline_key)) - # I see that inline discussion topics are enabled - self.assertFalse(self.cohort_management_page.inline_discussion_topics_disabled()) - self.cohort_management_page.save_discussion_topics(key=self.inline_key) - - self.reload_page() - self.assertTrue(self.cohort_management_page.cohort_some_inline_discussion_selected()) - - def test_cohort_inline_discussion_topic(self): - """ - Scenario: cohort inline discussion topic. - - Given I have a course with a cohort defined, - And a inline discussion topic with disabled Save button - And When I click on inline discussion topic - And I see enabled save button - And When i click save button - Then I see success message - When I reload the page - Then I see the discussion topic selected - """ - self.cohort_discussion_topics_are_visible() - - cohorted_topics_before = self.cohort_management_page.get_cohorted_topics_count(self.inline_key) - # check the discussion topic. - self.cohort_management_page.select_discussion_topic(self.inline_key) - - # Save button enabled. - self.assertFalse(self.cohort_management_page.is_save_button_disabled(self.inline_key)) - - # verifies that changes saved successfully. - self.save_and_verify_discussion_topics(key=self.inline_key) - - cohorted_topics_after = self.cohort_management_page.get_cohorted_topics_count(self.inline_key) - self.assertNotEqual(cohorted_topics_before, cohorted_topics_after) - - self.verify_discussion_topics_after_reload(self.inline_key, cohorted_topics_after) - - def test_verify_that_selecting_the_final_child_selects_category(self): - """ - Scenario: Category should be selected on selecting final child. - - Given I have a course with a cohort defined, - And a inline discussion with disabled Save button. - When I click on child topics - Then I see enabled saved button - Then I see parent category to be checked. - """ - self.cohort_discussion_topics_are_visible() - - # category should not be selected. - self.assertFalse(self.cohort_management_page.is_category_selected()) - - # check the discussion topic. - self.cohort_management_page.select_discussion_topic(self.inline_key) - - # verify that category is selected. - self.assertTrue(self.cohort_management_page.is_category_selected()) - - def test_verify_that_deselecting_the_final_child_deselects_category(self): - """ - Scenario: Category should be deselected on deselecting final child. - - Given I have a course with a cohort defined, - And a inline discussion with disabled Save button. - When I click on final child topics - Then I see enabled saved button - Then I see parent category to be deselected. - """ - self.cohort_discussion_topics_are_visible() - - # category should not be selected. - self.assertFalse(self.cohort_management_page.is_category_selected()) - - # check the discussion topic. - self.cohort_management_page.select_discussion_topic(self.inline_key) - - # verify that category is selected. - self.assertTrue(self.cohort_management_page.is_category_selected()) - - # un-check the discussion topic. - self.cohort_management_page.select_discussion_topic(self.inline_key) - - # category should not be selected. - self.assertFalse(self.cohort_management_page.is_category_selected()) - - @attr(shard=6) class CohortContentGroupAssociationTest(UniqueCourseTest, CohortTestMixin): """ diff --git a/common/test/acceptance/tests/discussion/test_discussion_management.py b/common/test/acceptance/tests/discussion/test_discussion_management.py new file mode 100644 index 0000000000..f49109bc8f --- /dev/null +++ b/common/test/acceptance/tests/discussion/test_discussion_management.py @@ -0,0 +1,273 @@ +# -*- coding: utf-8 -*- +""" +End-to-end tests related to the divided discussion management on the LMS Instructor Dashboard +""" + +from nose.plugins.attrib import attr +from common.test.acceptance.tests.discussion.helpers import CohortTestMixin +from common.test.acceptance.tests.helpers import UniqueCourseTest +from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc +from common.test.acceptance.pages.lms.auto_auth import AutoAuthPage +from common.test.acceptance.pages.lms.instructor_dashboard import InstructorDashboardPage + +import uuid + + +@attr(shard=6) +class DividedDiscussionTopicsTest(UniqueCourseTest, CohortTestMixin): + """ + Tests for dividing the inline and course-wide discussion topics. + """ + def setUp(self): + """ + Set up a discussion topic + """ + super(DividedDiscussionTopicsTest, self).setUp() + + self.discussion_id = "test_discussion_{}".format(uuid.uuid4().hex) + self.course_fixture = CourseFixture(**self.course_info).add_children( + XBlockFixtureDesc("chapter", "Test Section").add_children( + XBlockFixtureDesc("sequential", "Test Subsection").add_children( + XBlockFixtureDesc("vertical", "Test Unit").add_children( + XBlockFixtureDesc( + "discussion", + "Test Discussion", + metadata={"discussion_id": self.discussion_id} + ) + ) + ) + ) + ).install() + + # create course with single cohort and two content groups (user_partition of type "cohort") + self.cohort_name = "OnlyCohort" + self.setup_cohort_config(self.course_fixture) + self.cohort_id = self.add_manual_cohort(self.course_fixture, self.cohort_name) + + # login as an instructor + self.instructor_name = "instructor_user" + self.instructor_id = AutoAuthPage( + self.browser, username=self.instructor_name, email="instructor_user@example.com", + course_id=self.course_id, staff=True + ).visit().get_user_id() + + # go to the membership page on the instructor dashboard + self.instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id) + self.instructor_dashboard_page.visit() + self.discussion_management_page = self.instructor_dashboard_page.select_discussion_management() + self.discussion_management_page.wait_for_page() + + self.course_wide_key = 'course-wide' + self.inline_key = 'inline' + + def divided_discussion_topics_are_visible(self): + """ + Assert that discussion topics are visible with appropriate content. + """ + self.assertTrue(self.discussion_management_page.discussion_topics_visible()) + + self.assertEqual( + "Course-Wide Discussion Topics", + self.discussion_management_page.divided_discussion_heading_is_visible(self.course_wide_key) + ) + self.assertTrue(self.discussion_management_page.is_save_button_disabled(self.course_wide_key)) + + self.assertEqual( + "Content-Specific Discussion Topics", + self.discussion_management_page.divided_discussion_heading_is_visible(self.inline_key) + ) + self.assertTrue(self.discussion_management_page.is_save_button_disabled(self.inline_key)) + + def save_and_verify_discussion_topics(self, key): + """ + Saves the discussion topics and the verify the changes. + """ + # click on the inline save button. + self.discussion_management_page.save_discussion_topics(key) + + # verifies that changes saved successfully. + confirmation_message = self.discussion_management_page.get_divide_discussions_message(key=key) + self.assertEqual("Your changes have been saved.", confirmation_message) + + # save button disabled again. + self.assertTrue(self.discussion_management_page.is_save_button_disabled(key)) + + def reload_page(self): + """ + Refresh the page. + """ + self.browser.refresh() + self.discussion_management_page.wait_for_page() + + self.instructor_dashboard_page.select_discussion_management() + self.discussion_management_page.wait_for_page() + + self.divided_discussion_topics_are_visible() + + def verify_discussion_topics_after_reload(self, key, divided_topics): + """ + Verifies the changed topics. + """ + self.reload_page() + self.assertEqual(self.discussion_management_page.get_divided_topics_count(key), divided_topics) + + def test_divide_course_wide_discussion_topic(self): + """ + Scenario: divide a course-wide discussion topic. + + Given I have a course with a divide defined, + And a course-wide discussion with disabled Save button. + When I click on the course-wide discussion topic + Then I see the enabled save button + When I click on save button + Then I see success message + When I reload the page + Then I see the discussion topic selected + """ + self.divided_discussion_topics_are_visible() + + divided_topics_before = self.discussion_management_page.get_divided_topics_count(self.course_wide_key) + self.discussion_management_page.select_discussion_topic(self.course_wide_key) + + self.assertFalse(self.discussion_management_page.is_save_button_disabled(self.course_wide_key)) + + self.save_and_verify_discussion_topics(key=self.course_wide_key) + divided_topics_after = self.discussion_management_page.get_divided_topics_count(self.course_wide_key) + + self.assertNotEqual(divided_topics_before, divided_topics_after) + + self.verify_discussion_topics_after_reload(self.course_wide_key, divided_topics_after) + + def test_always_divide_inline_topic_enabled(self): + """ + Scenario: Select the always_divide_inline_topics radio button + + Given I have a course with a cohort defined, + And an inline discussion topic with disabled Save button. + When I click on always_divide_inline_topics + Then I see enabled save button + And I see disabled inline discussion topics + When I save the change + And I reload the page + Then I see the always_divide_inline_topics option enabled + """ + self.divided_discussion_topics_are_visible() + + # enable always inline discussion topics and save the change + self.discussion_management_page.select_always_inline_discussion() + self.assertFalse(self.discussion_management_page.is_save_button_disabled(self.inline_key)) + self.assertTrue(self.discussion_management_page.inline_discussion_topics_disabled()) + self.discussion_management_page.save_discussion_topics(key=self.inline_key) + + self.reload_page() + self.assertTrue(self.discussion_management_page.always_inline_discussion_selected()) + + def test_divide_some_inline_topics_enabled(self): + """ + Scenario: Select the divide_some_inline_topics radio button + + Given I have a course with a divide defined and always_divide_inline_topics set to True + And an inline discussion topic with disabled Save button. + When I click on divide_some_inline_topics + Then I see enabled save button + And I see enabled inline discussion topics + When I save the change + And I reload the page + Then I see the divide_some_inline_topics option enabled + """ + self.divided_discussion_topics_are_visible() + # By default always inline discussion topics is False. Enable it (and reload the page). + self.assertFalse(self.discussion_management_page.always_inline_discussion_selected()) + self.discussion_management_page.select_always_inline_discussion() + self.discussion_management_page.save_discussion_topics(key=self.inline_key) + self.reload_page() + self.assertFalse(self.discussion_management_page.divide_some_inline_discussion_selected()) + + # enable some inline discussion topic radio button. + self.discussion_management_page.select_divide_some_inline_discussion() + # I see that save button is enabled + self.assertFalse(self.discussion_management_page.is_save_button_disabled(self.inline_key)) + # I see that inline discussion topics are enabled + self.assertFalse(self.discussion_management_page.inline_discussion_topics_disabled()) + self.discussion_management_page.save_discussion_topics(key=self.inline_key) + + self.reload_page() + self.assertTrue(self.discussion_management_page.divide_some_inline_discussion_selected()) + + def test_divide_inline_discussion_topic(self): + """ + Scenario: divide inline discussion topic. + + Given I have a course with a divide defined, + And a inline discussion topic with disabled Save button + And When I click on inline discussion topic + And I see enabled save button + And When i click save button + Then I see success message + When I reload the page + Then I see the discussion topic selected + """ + self.divided_discussion_topics_are_visible() + + divided_topics_before = self.discussion_management_page.get_divided_topics_count(self.inline_key) + # check the discussion topic. + self.discussion_management_page.select_discussion_topic(self.inline_key) + + # Save button enabled. + self.assertFalse(self.discussion_management_page.is_save_button_disabled(self.inline_key)) + + # verifies that changes saved successfully. + self.save_and_verify_discussion_topics(key=self.inline_key) + + divided_topics_after = self.discussion_management_page.get_divided_topics_count(self.inline_key) + self.assertNotEqual(divided_topics_before, divided_topics_after) + + self.verify_discussion_topics_after_reload(self.inline_key, divided_topics_after) + + def test_verify_that_selecting_the_final_child_selects_category(self): + """ + Scenario: Category should be selected on selecting final child. + + Given I have a course with a cohort defined, + And a inline discussion with disabled Save button. + When I click on child topics + Then I see enabled saved button + Then I see parent category to be checked. + """ + self.divided_discussion_topics_are_visible() + + # category should not be selected. + self.assertFalse(self.discussion_management_page.is_category_selected()) + + # check the discussion topic. + self.discussion_management_page.select_discussion_topic(self.inline_key) + + # verify that category is selected. + self.assertTrue(self.discussion_management_page.is_category_selected()) + + def test_verify_that_deselecting_the_final_child_deselects_category(self): + """ + Scenario: Category should be deselected on deselecting final child. + + Given I have a course with a cohort defined, + And a inline discussion with disabled Save button. + When I click on final child topics + Then I see enabled saved button + Then I see parent category to be deselected. + """ + self.divided_discussion_topics_are_visible() + + # category should not be selected. + self.assertFalse(self.discussion_management_page.is_category_selected()) + + # check the discussion topic. + self.discussion_management_page.select_discussion_topic(self.inline_key) + + # verify that category is selected. + self.assertTrue(self.discussion_management_page.is_category_selected()) + + # un-check the discussion topic. + self.discussion_management_page.select_discussion_topic(self.inline_key) + + # category should not be selected. + self.assertFalse(self.discussion_management_page.is_category_selected()) diff --git a/lms/djangoapps/django_comment_client/tests/test_utils.py b/lms/djangoapps/django_comment_client/tests/test_utils.py index d1a57384de..26ce771cba 100644 --- a/lms/djangoapps/django_comment_client/tests/test_utils.py +++ b/lms/djangoapps/django_comment_client/tests/test_utils.py @@ -26,7 +26,7 @@ from courseware.tests.factories import InstructorFactory from courseware.tabs import get_course_tab_list from openedx.core.djangoapps.course_groups import cohorts from openedx.core.djangoapps.course_groups.cohorts import set_course_cohorted -from openedx.core.djangoapps.course_groups.tests.helpers import config_course_cohorts, topic_name_to_id, CohortFactory +from openedx.core.djangoapps.course_groups.tests.helpers import config_course_cohorts, config_course_discussions, topic_name_to_id, CohortFactory from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentFactory from openedx.core.djangoapps.content.course_structures.models import CourseStructure from openedx.core.djangoapps.util.testing import ContentGroupTestCase @@ -478,7 +478,7 @@ class CategoryMapTestCase(CategoryMapTestMixin, ModuleStoreTestCase): } ) - def test_inline_with_always_cohort_inline_discussion_flag(self): + def test_inline_with_always_divide_inline_discussion_flag(self): self.create_discussion("Chapter", "Discussion") set_discussion_division_settings(self.course.id, enable_cohorts=True, always_divide_inline_discussions=True) @@ -502,7 +502,7 @@ class CategoryMapTestCase(CategoryMapTestMixin, ModuleStoreTestCase): } ) - def test_inline_without_always_cohort_inline_discussion_flag(self): + def test_inline_without_always_divide_inline_discussion_flag(self): self.create_discussion("Chapter", "Discussion") set_discussion_division_settings(self.course.id, enable_cohorts=True) @@ -1301,15 +1301,16 @@ class IsCommentableDividedTestCase(ModuleStoreTestCase): ) # not cohorted - config_course_cohorts(course, is_cohorted=False, discussion_topics=["General", "Feedback"]) - + config_course_cohorts(course, is_cohorted=False) + config_course_discussions(course, discussion_topics=["General", "Feedback"]) self.assertFalse( utils.is_commentable_divided(course.id, to_id("General")), "Course isn't cohorted" ) # cohorted, but top level topics aren't - config_course_cohorts(course, is_cohorted=True, discussion_topics=["General", "Feedback"]) + config_course_cohorts(course, is_cohorted=True) + config_course_discussions(course, discussion_topics=["General", "Feedback"]) self.assertTrue(cohorts.is_course_cohorted(course.id)) self.assertFalse( @@ -1320,10 +1321,9 @@ class IsCommentableDividedTestCase(ModuleStoreTestCase): # cohorted, including "Feedback" top-level topics aren't config_course_cohorts( course, - is_cohorted=True, - discussion_topics=["General", "Feedback"], - divided_discussions=["Feedback"] + is_cohorted=True ) + config_course_discussions(course, discussion_topics=["General", "Feedback"], divided_discussions=["Feedback"]) self.assertTrue(cohorts.is_course_cohorted(course.id)) self.assertFalse( @@ -1345,42 +1345,52 @@ class IsCommentableDividedTestCase(ModuleStoreTestCase): config_course_cohorts( course, is_cohorted=True, + ) + config_course_discussions( + course, discussion_topics=["General", "Feedback"], divided_discussions=["Feedback", "random_inline"] ) + self.assertFalse( utils.is_commentable_divided(course.id, to_id("random")), "By default, Non-top-level discussions are not cohorted in a cohorted courses." ) - # if always_cohort_inline_discussions is set to False, non-top-level discussion are always - # non cohorted unless they are explicitly set in cohorted_discussions + # if always_divide_inline_discussions is set to False, non-top-level discussion are always + # not divided unless they are explicitly set in divided_discussions config_course_cohorts( course, is_cohorted=True, + ) + config_course_discussions( + course, discussion_topics=["General", "Feedback"], divided_discussions=["Feedback", "random_inline"], always_divide_inline_discussions=False ) + self.assertFalse( utils.is_commentable_divided(course.id, to_id("random")), - "Non-top-level discussion is not cohorted if always_cohort_inline_discussions is False." + "Non-top-level discussion is not cohorted if always_divide_inline_discussions is False." ) self.assertTrue( utils.is_commentable_divided(course.id, to_id("random_inline")), - "If always_cohort_inline_discussions set to False, Non-top-level discussion is " + "If always_divide_inline_discussions set to False, Non-top-level discussion is " "cohorted if explicitly set in cohorted_discussions." ) self.assertTrue( utils.is_commentable_divided(course.id, to_id("Feedback")), - "If always_cohort_inline_discussions set to False, top-level discussion are not affected." + "If always_divide_inline_discussions set to False, top-level discussion are not affected." ) def test_is_commentable_divided_team(self): course = modulestore().get_course(self.toy_course_key) self.assertFalse(cohorts.is_course_cohorted(course.id)) - config_course_cohorts(course, is_cohorted=True, always_divide_inline_discussions=True) + config_course_cohorts(course, is_cohorted=True) + config_course_discussions(course, always_divide_inline_discussions=True) + team = CourseTeamFactory(course_id=course.id) # Verify that team discussions are not cohorted, but other discussions are diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 5aecf20927..3250e335e6 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -125,6 +125,7 @@ def instructor_dashboard_2(request, course_id): _section_course_info(course, access), _section_membership(course, access, is_white_label), _section_cohort_management(course, access), + _section_discussions_management(course, access), _section_student_admin(course, access), _section_data_download(course, access), ] @@ -513,7 +514,6 @@ def _section_cohort_management(course, access): ), 'cohorts_url': reverse('cohorts', kwargs={'course_key_string': unicode(course_key)}), 'upload_cohorts_csv_url': reverse('add_users_to_cohorts', kwargs={'course_id': unicode(course_key)}), - 'discussion_topics_url': reverse('cohort_discussion_topics', kwargs={'course_key_string': unicode(course_key)}), 'verified_track_cohorting_url': reverse( 'verified_track_cohorting', kwargs={'course_key_string': unicode(course_key)} ), @@ -521,6 +521,21 @@ def _section_cohort_management(course, access): return section_data +def _section_discussions_management(course, access): + """ Provide data for the corresponding discussion management section """ + course_key = course.id + section_data = { + 'section_key': 'discussions_management', + 'section_display_name': _('Discussions'), + 'discussion_topics_url': reverse('discussion_topics', kwargs={'course_key_string': unicode(course_key)}), + 'course_discussion_settings': reverse( + 'course_discussions_settings', + kwargs={'course_key_string': unicode(course_key)} + ), + } + return section_data + + def _is_small_course(course_key): """ Compares against MAX_ENROLLMENT_INSTR_BUTTONS to determine if course enrollment is considered small. """ is_small_course = False diff --git a/lms/envs/common.py b/lms/envs/common.py index bb4656451e..13b86f4a20 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1759,6 +1759,7 @@ REQUIRE_JS_PATH_OVERRIDES = { 'js/student_profile/views/learner_profile_factory': 'js/student_profile/views/learner_profile_factory.js', 'js/courseware/courseware_factory': 'js/courseware/courseware_factory.js', 'js/groups/views/cohorts_dashboard_factory': 'js/groups/views/cohorts_dashboard_factory.js', + 'js/groups/discussions_management/discussions_dashboard_factory': 'js/discussions_management/views/discussions_dashboard_factory.js', 'draggabilly': 'js/vendor/draggabilly.js', 'hls': 'common/js/vendor/hls.js' } diff --git a/lms/static/js/groups/models/cohort_discussions.js b/lms/static/js/discussions_management/models/course_discussions_detail.js similarity index 68% rename from lms/static/js/groups/models/cohort_discussions.js rename to lms/static/js/discussions_management/models/course_discussions_detail.js index 8a309cb96b..13df59db79 100644 --- a/lms/static/js/groups/models/cohort_discussions.js +++ b/lms/static/js/discussions_management/models/course_discussions_detail.js @@ -1,12 +1,12 @@ (function(define) { 'use strict'; define(['backbone'], function(Backbone) { - var DiscussionTopicsSettingsModel = Backbone.Model.extend({ + var CourseDiscussionTopicDetailsModel = Backbone.Model.extend({ defaults: { course_wide_discussions: {}, inline_discussions: {} } }); - return DiscussionTopicsSettingsModel; + return CourseDiscussionTopicDetailsModel; }); }).call(this, define || RequireJS.define); diff --git a/lms/static/js/discussions_management/models/course_discussions_settings.js b/lms/static/js/discussions_management/models/course_discussions_settings.js new file mode 100644 index 0000000000..6eb50f0d5f --- /dev/null +++ b/lms/static/js/discussions_management/models/course_discussions_settings.js @@ -0,0 +1,14 @@ +(function(define) { + 'use strict'; + define(['backbone'], function(Backbone) { + var CourseDiscussionsSettingsModel = Backbone.Model.extend({ + idAttribute: 'id', + defaults: { + divided_inline_discussions: [], + divided_course_wide_discussions: [], + always_divide_inline_discussions: false + } + }); + return CourseDiscussionsSettingsModel; + }); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/discussions_management/views/discussions.js b/lms/static/js/discussions_management/views/discussions.js new file mode 100644 index 0000000000..a784ee74ac --- /dev/null +++ b/lms/static/js/discussions_management/views/discussions.js @@ -0,0 +1,49 @@ +(function(define) { + 'use strict'; + define(['jquery', 'underscore', 'backbone', 'gettext', + 'js/discussions_management/views/divided_discussions_inline', + 'js/discussions_management/views/divided_discussions_course_wide', + 'edx-ui-toolkit/js/utils/html-utils' + ], + + function($, _, Backbone, gettext, InlineDiscussionsView, CourseWideDiscussionsView, HtmlUtils) { + var DiscussionsView = Backbone.View.extend({ + + initialize: function(options) { + this.template = HtmlUtils.template($('#discussions-tpl').text()); + this.context = options.context; + this.discussionSettings = options.discussionSettings; + }, + + render: function() { + HtmlUtils.setHtml(this.$el, this.template({})); + this.showDiscussionTopics(); + return this; + }, + + getSectionCss: function(section) { + return ".instructor-nav .nav-item [data-section='" + section + "']"; + }, + + showDiscussionTopics: function() { + var dividedDiscussionsElement = this.$('.discussions-nav'); + if (!this.CourseWideDiscussionsView) { + this.CourseWideDiscussionsView = new CourseWideDiscussionsView({ + el: dividedDiscussionsElement, + model: this.context.courseDiscussionTopicDetailsModel, + discussionSettings: this.discussionSettings + }).render(); + } + + if (!this.InlineDiscussionsView) { + this.InlineDiscussionsView = new InlineDiscussionsView({ + el: dividedDiscussionsElement, + model: this.context.courseDiscussionTopicDetailsModel, + discussionSettings: this.discussionSettings + }).render(); + } + } + }); + return DiscussionsView; + }); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/discussions_management/views/discussions_dashboard_factory.js b/lms/static/js/discussions_management/views/discussions_dashboard_factory.js new file mode 100644 index 0000000000..c2f0cdf8d5 --- /dev/null +++ b/lms/static/js/discussions_management/views/discussions_dashboard_factory.js @@ -0,0 +1,33 @@ +(function(define) { + 'use strict'; + define('js/discussions_management/views/discussions_dashboard_factory', + ['jquery', 'js/discussions_management/views/discussions', + 'js/discussions_management/models/course_discussions_detail', + 'js/discussions_management/models/course_discussions_settings'], + function($, DiscussionsView, CourseDiscussionTopicDetailsModel, CourseDiscussionsSettingsModel) { + return function() { + var courseDiscussionSettings = new CourseDiscussionsSettingsModel(), + discussionTopicsSettings = new CourseDiscussionTopicDetailsModel(), + $discussionsManagementElement = $('.discussions-management'), + discussionsView; + + courseDiscussionSettings.url = $discussionsManagementElement.data('course-discussion-settings-url'); + discussionTopicsSettings.url = $discussionsManagementElement.data('discussion-topics-url'); + + discussionsView = new DiscussionsView({ + el: $discussionsManagementElement, + discussionSettings: courseDiscussionSettings, + context: { + courseDiscussionTopicDetailsModel: discussionTopicsSettings + } + }); + + courseDiscussionSettings.fetch().done(function() { + discussionTopicsSettings.fetch().done(function() { + discussionsView.render(); + }); + }); + }; + }); +}).call(this, define || RequireJS.define); + diff --git a/lms/static/js/groups/views/cohort_discussions.js b/lms/static/js/discussions_management/views/divided_discussions.js similarity index 74% rename from lms/static/js/groups/views/cohort_discussions.js rename to lms/static/js/discussions_management/views/divided_discussions.js index 8b13aa5056..be498d6196 100644 --- a/lms/static/js/groups/views/cohort_discussions.js +++ b/lms/static/js/discussions_management/views/divided_discussions.js @@ -1,8 +1,9 @@ (function(define) { 'use strict'; define(['jquery', 'underscore', 'backbone', 'gettext', 'js/models/notification', 'js/views/notification'], - function($, _, Backbone) { - var CohortDiscussionConfigurationView = Backbone.View.extend({ + function($, _, Backbone, gettext) { + /* global NotificationModel, NotificationView */ + var DividedDiscussionConfigurationView = Backbone.View.extend({ /** * Add/Remove the disabled attribute on given element. @@ -14,53 +15,53 @@ }, /** - * Returns the cohorted discussions list. + * Returns the divided discussions list. * @param {string} selector - To select the discussion elements whose ids to return. - * @returns {Array} - Cohorted discussions. + * @returns {Array} - Divided discussions. */ - getCohortedDiscussions: function(selector) { + getDividedDiscussions: function(selector) { var self = this, - cohortedDiscussions = []; + dividedDiscussions = []; _.each(self.$(selector), function(topic) { - cohortedDiscussions.push($(topic).data('id')); + dividedDiscussions.push($(topic).data('id')); }); - return cohortedDiscussions; + return dividedDiscussions; }, /** - * Save the cohortSettings' changed attributes to the server via PATCH method. + * Save the discussionSettings' changed attributes to the server via PATCH method. * It shows the error message(s) if any. * @param {object} $element - Messages would be shown before this element. * @param {object} fieldData - Data to update on the server. */ saveForm: function($element, fieldData) { var self = this, - cohortSettingsModel = this.cohortSettings, + discussionSettingsModel = this.discussionSettings, saveOperation = $.Deferred(), showErrorMessage; - - showErrorMessage = function(message, $element) { + showErrorMessage = function(message) { self.showMessage(message, $element, 'error'); }; this.removeNotification(); - cohortSettingsModel.save( + discussionSettingsModel.save( fieldData, {patch: true, wait: true} ).done(function() { saveOperation.resolve(); }).fail(function(result) { - var errorMessage = null; + var errorMessage = null, + jsonResponse; try { - var jsonResponse = JSON.parse(result.responseText); + jsonResponse = JSON.parse(result.responseText); errorMessage = jsonResponse.error; } catch (e) { // Ignore the exception and show the default error message instead. } if (!errorMessage) { - errorMessage = gettext("We've encountered an error. Refresh your browser and then try again."); + errorMessage = gettext("We've encountered an error. Refresh your browser and then try again."); // eslint-disable-line max-len } - showErrorMessage(errorMessage, $element); + showErrorMessage(errorMessage); saveOperation.reject(); }); return saveOperation.promise(); @@ -92,6 +93,6 @@ } }); - return CohortDiscussionConfigurationView; + return DividedDiscussionConfigurationView; }); }).call(this, define || RequireJS.define); diff --git a/lms/static/js/groups/views/cohort_discussions_course_wide.js b/lms/static/js/discussions_management/views/divided_discussions_course_wide.js similarity index 77% rename from lms/static/js/groups/views/cohort_discussions_course_wide.js rename to lms/static/js/discussions_management/views/divided_discussions_course_wide.js index 71fdcd750e..774f734077 100644 --- a/lms/static/js/groups/views/cohort_discussions_course_wide.js +++ b/lms/static/js/discussions_management/views/divided_discussions_course_wide.js @@ -1,21 +1,21 @@ (function(define) { 'use strict'; - define(['jquery', 'underscore', 'backbone', 'gettext', 'js/groups/views/cohort_discussions', + define(['jquery', 'underscore', 'backbone', 'gettext', 'js/discussions_management/views/divided_discussions', 'edx-ui-toolkit/js/utils/html-utils'], - function($, _, Backbone, gettext, CohortDiscussionConfigurationView, HtmlUtils) { - var CourseWideDiscussionsView = CohortDiscussionConfigurationView.extend({ + function($, _, Backbone, gettext, DividedDiscussionConfigurationView, HtmlUtils) { + var CourseWideDiscussionsView = DividedDiscussionConfigurationView.extend({ events: { 'change .check-discussion-subcategory-course-wide': 'discussionCategoryStateChanged', 'click .cohort-course-wide-discussions-form .action-save': 'saveCourseWideDiscussionsForm' }, initialize: function(options) { - this.template = HtmlUtils.template($('#cohort-discussions-course-wide-tpl').text()); - this.cohortSettings = options.cohortSettings; + this.template = HtmlUtils.template($('#divided-discussions-course-wide-tpl').text()); + this.discussionSettings = options.discussionSettings; }, render: function() { - HtmlUtils.setHtml(this.$('.cohort-course-wide-discussions-nav'), this.template({ + HtmlUtils.setHtml(this.$('.course-wide-discussions-nav'), this.template({ courseWideTopicsHtml: this.getCourseWideDiscussionsHtml( this.model.get('course_wide_discussions') ) @@ -56,25 +56,27 @@ }, /** - * Sends the cohorted_course_wide_discussions to the server and renders the view. + * Sends the courseWideDividedDiscussions to the server and renders the view. */ saveCourseWideDiscussionsForm: function(event) { - event.preventDefault(); - var self = this, - courseWideCohortedDiscussions = self.getCohortedDiscussions( + courseWideDividedDiscussions = self.getDividedDiscussions( '.check-discussion-subcategory-course-wide:checked' ), - fieldData = {cohorted_course_wide_discussions: courseWideCohortedDiscussions}; + fieldData = {divided_course_wide_discussions: courseWideDividedDiscussions}; + + event.preventDefault(); self.saveForm(self.$('.course-wide-discussion-topics'), fieldData) .done(function() { self.model.fetch() .done(function() { self.render(); - self.showMessage(gettext('Your changes have been saved.'), self.$('.course-wide-discussion-topics')); + self.showMessage(gettext('Your changes have been saved.'), + self.$('.course-wide-discussion-topics') + ); }).fail(function() { - var errorMessage = gettext("We've encountered an error. Refresh your browser and then try again."); + var errorMessage = gettext("We've encountered an error. Refresh your browser and then try again."); // eslint-disable-line max-len self.showMessage(errorMessage, self.$('.course-wide-discussion-topics'), 'error'); }); }); diff --git a/lms/static/js/groups/views/cohort_discussions_inline.js b/lms/static/js/discussions_management/views/divided_discussions_inline.js similarity index 75% rename from lms/static/js/groups/views/cohort_discussions_inline.js rename to lms/static/js/discussions_management/views/divided_discussions_inline.js index 817b07d58f..a965b81ec9 100644 --- a/lms/static/js/groups/views/cohort_discussions_inline.js +++ b/lms/static/js/discussions_management/views/divided_discussions_inline.js @@ -1,9 +1,9 @@ (function(define) { 'use strict'; - define(['jquery', 'underscore', 'backbone', 'gettext', 'js/groups/views/cohort_discussions', + define(['jquery', 'underscore', 'backbone', 'gettext', 'js/discussions_management/views/divided_discussions', 'edx-ui-toolkit/js/utils/html-utils', 'js/vendor/jquery.qubit'], - function($, _, Backbone, gettext, CohortDiscussionConfigurationView, HtmlUtils) { - var InlineDiscussionsView = CohortDiscussionConfigurationView.extend({ + function($, _, Backbone, gettext, DividedDiscussionConfigurationView, HtmlUtils) { + var InlineDiscussionsView = DividedDiscussionConfigurationView.extend({ events: { 'change .check-discussion-category': 'setSaveButton', 'change .check-discussion-subcategory-inline': 'setSaveButton', @@ -13,17 +13,19 @@ }, initialize: function(options) { - this.template = HtmlUtils.template($('#cohort-discussions-inline-tpl').text()); - this.cohortSettings = options.cohortSettings; + this.template = HtmlUtils.template($('#divided-discussions-inline-tpl').text()); + this.discussionSettings = options.discussionSettings; }, render: function() { - var alwaysCohortInlineDiscussions = this.cohortSettings.get('always_cohort_inline_discussions'), - inline_discussions = this.model.get('inline_discussions'); + var inlineDiscussions = this.model.get('inline_discussions'), + alwaysDivideInlineDiscussions = this.discussionSettings.get( + 'always_divide_inline_discussions' + ); - HtmlUtils.setHtml(this.$('.cohort-inline-discussions-nav'), this.template({ - inlineDiscussionTopicsHtml: this.getInlineDiscussionsHtml(inline_discussions), - alwaysCohortInlineDiscussions: alwaysCohortInlineDiscussions + HtmlUtils.setHtml(this.$('.inline-discussions-nav'), this.template({ + inlineDiscussionTopicsHtml: this.getInlineDiscussionsHtml(inlineDiscussions), + alwaysDivideInlineDiscussions: alwaysDivideInlineDiscussions })); // Provides the semantics for a nested list of tri-state checkboxes. @@ -32,7 +34,7 @@ // based on the checked values of any checkboxes in child elements of the DOM. this.$('ul.inline-topics').qubit(); - this.setElementsEnabled(alwaysCohortInlineDiscussions, true); + this.setElementsEnabled(alwaysDivideInlineDiscussions, true); }, /** @@ -99,45 +101,48 @@ * * Enable/Disable the category and sub-category checkboxes. * Enable/Disable the save button. - * @param {bool} enable_checkboxes - The flag to enable/disable the checkboxes. - * @param {bool} enable_save_button - The flag to enable/disable the save button. + * @param {bool} enableCheckboxes - The flag to enable/disable the checkboxes. + * @param {bool} enableSaveButton - The flag to enable/disable the save button. */ - setElementsEnabled: function(enable_checkboxes, enable_save_button) { - this.setDisabled(this.$('.check-discussion-category'), enable_checkboxes); - this.setDisabled(this.$('.check-discussion-subcategory-inline'), enable_checkboxes); - this.setDisabled(this.$('.cohort-inline-discussions-form .action-save'), enable_save_button); + setElementsEnabled: function(enableCheckboxes, enableSaveButton) { + this.setDisabled(this.$('.check-discussion-category'), enableCheckboxes); + this.setDisabled(this.$('.check-discussion-subcategory-inline'), enableCheckboxes); + this.setDisabled(this.$('.cohort-inline-discussions-form .action-save'), enableSaveButton); }, /** * Enables the save button for inline discussions. */ - setSaveButton: function(event) { + setSaveButton: function() { this.setDisabled(this.$('.cohort-inline-discussions-form .action-save'), false); }, /** - * Sends the cohorted_inline_discussions to the server and renders the view. + * Sends the dividedInlineDiscussions to the server and renders the view. */ saveInlineDiscussionsForm: function(event) { - event.preventDefault(); - var self = this, - cohortedInlineDiscussions = self.getCohortedDiscussions( + dividedInlineDiscussions = self.getDividedDiscussions( '.check-discussion-subcategory-inline:checked' ), fieldData = { - cohorted_inline_discussions: cohortedInlineDiscussions, - always_cohort_inline_discussions: self.$('.check-all-inline-discussions').prop('checked') + divided_inline_discussions: dividedInlineDiscussions, + always_divide_inline_discussions: self.$( + '.check-all-inline-discussions' + ).prop('checked') }; + event.preventDefault(); + self.saveForm(self.$('.inline-discussion-topics'), fieldData) .done(function() { self.model.fetch() .done(function() { self.render(); - self.showMessage(gettext('Your changes have been saved.'), self.$('.inline-discussion-topics')); + self.showMessage(gettext('Your changes have been saved.'), + self.$('.inline-discussion-topics')); }).fail(function() { - var errorMessage = gettext("We've encountered an error. Refresh your browser and then try again."); + var errorMessage = gettext("We've encountered an error. Refresh your browser and then try again."); // eslint-disable-line max-len self.showMessage(errorMessage, self.$('.inline-discussion-topics'), 'error'); }); }); diff --git a/lms/static/js/groups/models/course_cohort_settings.js b/lms/static/js/groups/models/course_cohort_settings.js index 213a14ec97..1c2a9c9d14 100644 --- a/lms/static/js/groups/models/course_cohort_settings.js +++ b/lms/static/js/groups/models/course_cohort_settings.js @@ -4,10 +4,7 @@ var CourseCohortSettingsModel = Backbone.Model.extend({ idAttribute: 'id', defaults: { - is_cohorted: false, - cohorted_inline_discussions: [], - cohorted_course_wide_discussions: [], - always_cohort_inline_discussions: false + is_cohorted: false } }); return CourseCohortSettingsModel; diff --git a/lms/static/js/groups/views/cohorts.js b/lms/static/js/groups/views/cohorts.js index 8c9614c680..44d2c0cdda 100644 --- a/lms/static/js/groups/views/cohorts.js +++ b/lms/static/js/groups/views/cohorts.js @@ -4,13 +4,11 @@ 'js/groups/models/verified_track_settings', 'js/groups/views/cohort_editor', 'js/groups/views/cohort_form', 'js/groups/views/course_cohort_settings_notification', - 'js/groups/views/cohort_discussions_inline', 'js/groups/views/cohort_discussions_course_wide', 'js/groups/views/verified_track_settings_notification', 'edx-ui-toolkit/js/utils/html-utils', 'js/views/file_uploader', 'js/models/notification', 'js/views/notification', 'string_utils'], function($, _, Backbone, gettext, CohortModel, VerifiedTrackSettingsModel, CohortEditorView, CohortFormView, - CourseCohortSettingsNotificationView, InlineDiscussionsView, CourseWideDiscussionsView, - VerifiedTrackSettingsNotificationView, HtmlUtils) { + CourseCohortSettingsNotificationView, VerifiedTrackSettingsNotificationView, HtmlUtils) { var hiddenClass = 'is-hidden', disabledClass = 'is-disabled', enableCohortsSelector = '.cohorts-state'; @@ -25,7 +23,6 @@ 'click .cohort-management-add-form .action-cancel': 'cancelAddCohortForm', 'click .link-cross-reference': 'showSection', 'click .toggle-cohort-management-secondary': 'showCsvUpload', - 'click .toggle-cohort-management-discussions': 'showDiscussionTopics' }, initialize: function(options) { @@ -306,27 +303,6 @@ }).render(); } }, - showDiscussionTopics: function(event) { - event.preventDefault(); - - $(event.currentTarget).addClass(hiddenClass); - var cohortDiscussionsElement = this.$('.cohort-discussions-nav').removeClass(hiddenClass); - - if (!this.CourseWideDiscussionsView) { - this.CourseWideDiscussionsView = new CourseWideDiscussionsView({ - el: cohortDiscussionsElement, - model: this.context.discussionTopicsSettingsModel, - cohortSettings: this.cohortSettings - }).render(); - } - if (!this.InlineDiscussionsView) { - this.InlineDiscussionsView = new InlineDiscussionsView({ - el: cohortDiscussionsElement, - model: this.context.discussionTopicsSettingsModel, - cohortSettings: this.cohortSettings - }).render(); - } - }, getSectionCss: function(section) { return ".instructor-nav .nav-item [data-section='" + section + "']"; diff --git a/lms/static/js/groups/views/cohorts_dashboard_factory.js b/lms/static/js/groups/views/cohorts_dashboard_factory.js index e85ade7bdf..02e0a73b07 100644 --- a/lms/static/js/groups/views/cohorts_dashboard_factory.js +++ b/lms/static/js/groups/views/cohorts_dashboard_factory.js @@ -1,8 +1,8 @@ (function(define, undefined) { 'use strict'; define(['jquery', 'js/groups/views/cohorts', 'js/groups/collections/cohort', 'js/groups/models/course_cohort_settings', - 'js/groups/models/cohort_discussions', 'js/groups/models/content_group'], - function($, CohortsView, CohortCollection, CourseCohortSettingsModel, DiscussionTopicsSettingsModel, ContentGroupModel) { + 'js/groups/models/content_group'], + function($, CohortsView, CohortCollection, CourseCohortSettingsModel, ContentGroupModel) { return function(contentGroups, studioGroupConfigurationsUrl) { var contentGroupModels = $.map(contentGroups, function(group) { return new ContentGroupModel({ @@ -14,33 +14,27 @@ var cohorts = new CohortCollection(), courseCohortSettings = new CourseCohortSettingsModel(), - discussionTopicsSettings = new DiscussionTopicsSettingsModel(); + $cohortManagementElement = $('.cohort-management'); - var cohortManagementElement = $('.cohort-management'); - - cohorts.url = cohortManagementElement.data('cohorts_url'); - courseCohortSettings.url = cohortManagementElement.data('course_cohort_settings_url'); - discussionTopicsSettings.url = cohortManagementElement.data('discussion-topics-url'); + cohorts.url = $cohortManagementElement.data('cohorts_url'); + courseCohortSettings.url = $cohortManagementElement.data('course_cohort_settings_url'); var cohortsView = new CohortsView({ - el: cohortManagementElement, + el: $cohortManagementElement, model: cohorts, contentGroups: contentGroupModels, cohortSettings: courseCohortSettings, context: { - discussionTopicsSettingsModel: discussionTopicsSettings, - uploadCohortsCsvUrl: cohortManagementElement.data('upload_cohorts_csv_url'), - verifiedTrackCohortingUrl: cohortManagementElement.data('verified_track_cohorting_url'), + uploadCohortsCsvUrl: $cohortManagementElement.data('upload_cohorts_csv_url'), + verifiedTrackCohortingUrl: $cohortManagementElement.data('verified_track_cohorting_url'), studioGroupConfigurationsUrl: studioGroupConfigurationsUrl, - isCcxEnabled: cohortManagementElement.data('is_ccx_enabled') + isCcxEnabled: $cohortManagementElement.data('is_ccx_enabled') } }); cohorts.fetch().done(function() { courseCohortSettings.fetch().done(function() { - discussionTopicsSettings.fetch().done(function() { - cohortsView.render(); - }); + cohortsView.render(); }); }); }; diff --git a/lms/static/js/instructor_dashboard/discussions_management.js b/lms/static/js/instructor_dashboard/discussions_management.js new file mode 100644 index 0000000000..1e883b708b --- /dev/null +++ b/lms/static/js/instructor_dashboard/discussions_management.js @@ -0,0 +1,12 @@ +(function() { + 'use strict'; + + function DiscussionsManagement($section) { + this.$section = $section; + this.$section.data('wrapper', this); + } + + DiscussionsManagement.prototype.onClickTitle = function() {}; + + window.InstructorDashboard.sections.DiscussionsManagement = DiscussionsManagement; +}).call(this); diff --git a/lms/static/js/instructor_dashboard/instructor_dashboard.js b/lms/static/js/instructor_dashboard/instructor_dashboard.js index 272e241855..d5352c7b72 100644 --- a/lms/static/js/instructor_dashboard/instructor_dashboard.js +++ b/lms/static/js/instructor_dashboard/instructor_dashboard.js @@ -187,6 +187,9 @@ such that the value can be defined later than this assignment (file load order). }, { constructor: window.InstructorDashboard.sections.CohortManagement, $element: idashContent.find('.' + CSS_IDASH_SECTION + '#cohort_management') + }, { + constructor: window.InstructorDashboard.sections.DiscussionsManagement, + $element: idashContent.find('.' + CSS_IDASH_SECTION + '#discussions_management') }, { constructor: window.InstructorDashboard.sections.Certificates, $element: idashContent.find('.' + CSS_IDASH_SECTION + '#certificates') diff --git a/lms/static/js/spec/groups/views/cohorts_spec.js b/lms/static/js/spec/groups/views/cohorts_spec.js index ba29285340..96ae5576de 100644 --- a/lms/static/js/spec/groups/views/cohorts_spec.js +++ b/lms/static/js/spec/groups/views/cohorts_spec.js @@ -1,14 +1,11 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', - 'common/js/spec_helpers/template_helpers', - 'js/groups/views/cohorts', 'js/groups/collections/cohort', 'js/groups/models/content_group', - 'js/groups/models/course_cohort_settings', 'js/utils/animation', 'js/vendor/jquery.qubit', - 'js/groups/views/course_cohort_settings_notification', 'js/groups/models/cohort_discussions', - 'js/groups/views/cohort_discussions', 'js/groups/views/cohort_discussions_course_wide', - 'js/groups/views/cohort_discussions_inline' - ], + 'common/js/spec_helpers/template_helpers', + 'js/groups/views/cohorts', 'js/groups/collections/cohort', 'js/groups/models/content_group', + 'js/groups/models/course_cohort_settings', 'js/utils/animation', 'js/vendor/jquery.qubit', + 'js/groups/views/course_cohort_settings_notification' +], function(Backbone, $, AjaxHelpers, TemplateHelpers, CohortsView, CohortCollection, ContentGroupModel, - CourseCohortSettingsModel, AnimationUtil, Qubit, CourseCohortSettingsNotificationView, DiscussionTopicsSettingsModel, - CohortDiscussionsView, CohortCourseWideDiscussionsView, CohortInlineDiscussionsView) { + CourseCohortSettingsModel, AnimationUtil, Qubit, CourseCohortSettingsNotificationView) { 'use strict'; describe('Cohorts View', function() { @@ -20,16 +17,7 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers getAddModal, selectContentGroup, clearContentGroup, saveFormAndExpectErrors, createMockCohortSettings, MOCK_COHORTED_USER_PARTITION_ID, MOCK_UPLOAD_COHORTS_CSV_URL, MOCK_STUDIO_ADVANCED_SETTINGS_URL, MOCK_STUDIO_GROUP_CONFIGURATIONS_URL, - MOCK_VERIFIED_TRACK_COHORTING_URL, MOCK_MANUAL_ASSIGNMENT, MOCK_RANDOM_ASSIGNMENT, - createMockCohortDiscussionsJson, createMockCohortDiscussions, showAndAssertDiscussionTopics; - - // Selectors - var discussionsToggle = '.toggle-cohort-management-discussions', - inlineDiscussionsFormCss = '.cohort-inline-discussions-form', - courseWideDiscussionsFormCss = '.cohort-course-wide-discussions-form', - courseWideDiscussionsSaveButtonCss = '.cohort-course-wide-discussions-form .action-save', - inlineDiscussionsSaveButtonCss = '.cohort-inline-discussions-form .action-save', - inlineDiscussionsForm, courseWideDiscussionsForm; + MOCK_VERIFIED_TRACK_COHORTING_URL, MOCK_MANUAL_ASSIGNMENT, MOCK_RANDOM_ASSIGNMENT; MOCK_MANUAL_ASSIGNMENT = 'manual'; MOCK_RANDOM_ASSIGNMENT = 'random'; @@ -70,66 +58,16 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers ]; }; - createMockCohortSettingsJson = function(isCohorted, cohortedInlineDiscussions, cohortedCourseWideDiscussions, alwaysCohortInlineDiscussions) { + createMockCohortSettingsJson = function(isCohorted) { return { id: 0, - is_cohorted: isCohorted || false, - cohorted_inline_discussions: cohortedInlineDiscussions || [], - cohorted_course_wide_discussions: cohortedCourseWideDiscussions || [], - always_cohort_inline_discussions: alwaysCohortInlineDiscussions || false + is_cohorted: isCohorted || false }; }; - createMockCohortSettings = function(isCohorted, cohortedInlineDiscussions, cohortedCourseWideDiscussions, alwaysCohortInlineDiscussions) { + createMockCohortSettings = function(isCohorted) { return new CourseCohortSettingsModel( - createMockCohortSettingsJson(isCohorted, cohortedInlineDiscussions, cohortedCourseWideDiscussions, alwaysCohortInlineDiscussions) - ); - }; - - createMockCohortDiscussionsJson = function(allCohorted) { - return { - course_wide_discussions: { - children: [['Topic_C_1', 'entry'], ['Topic_C_2', 'entry']], - entries: { - Topic_C_1: { - sort_key: null, - is_divided: true, - id: 'Topic_C_1' - }, - Topic_C_2: { - sort_key: null, - is_divided: false, - id: 'Topic_C_2' - } - } - }, - inline_discussions: { - subcategories: { - Topic_I_1: { - subcategories: {}, - children: [['Inline_Discussion_1', 'entry'], ['Inline_Discussion_2', 'entry']], - entries: { - Inline_Discussion_1: { - sort_key: null, - is_divided: true, - id: 'Inline_Discussion_1' - }, - Inline_Discussion_2: { - sort_key: null, - is_divided: allCohorted || false, - id: 'Inline_Discussion_2' - } - } - } - }, - children: [['Topic_I_1', 'subcategory']] - } - }; - }; - - createMockCohortDiscussions = function(allCohorted) { - return new DiscussionTopicsSettingsModel( - createMockCohortDiscussionsJson(allCohorted) + createMockCohortSettingsJson(isCohorted) ); }; @@ -146,7 +84,7 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers }; createCohortsView = function(test, options) { - var cohortsJson, cohorts, contentGroups, cohortSettings, cohortDiscussions; + var cohortsJson, cohorts, contentGroups, cohortSettings; options = options || {}; cohortsJson = options.cohorts ? {cohorts: options.cohorts} : createMockCohorts(); cohorts = new CohortCollection(cohortsJson, {parse: true}); @@ -155,16 +93,12 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers cohortSettings.url = '/mock_service/cohorts/settings'; cohorts.url = '/mock_service/cohorts'; - cohortDiscussions = options.cohortDiscussions || createMockCohortDiscussions(); - cohortDiscussions.url = '/mock_service/cohorts/discussion/topics'; - requests = AjaxHelpers.requests(test); cohortsView = new CohortsView({ model: cohorts, contentGroups: contentGroups, cohortSettings: cohortSettings, context: { - discussionTopicsSettingsModel: cohortDiscussions, uploadCohortsCsvUrl: MOCK_UPLOAD_COHORTS_CSV_URL, studioAdvancedSettingsUrl: MOCK_STUDIO_ADVANCED_SETTINGS_URL, studioGroupConfigurationsUrl: MOCK_STUDIO_GROUP_CONFIGURATIONS_URL, @@ -314,40 +248,6 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers verifyDetailedMessage(expectedTitle, 'error', errors); }; - showAndAssertDiscussionTopics = function(that) { - createCohortsView(that); - - // Should see the control to toggle cohort discussions. - expect(cohortsView.$(discussionsToggle)).not.toHaveClass('is-hidden'); - // But discussions form should not be visible until toggle is clicked. - expect(cohortsView.$(inlineDiscussionsFormCss).length).toBe(0); - expect(cohortsView.$(courseWideDiscussionsFormCss).length).toBe(0); - - expect(cohortsView.$(discussionsToggle).text()). - toContain('Specify whether discussion topics are divided by cohort'); - - cohortsView.$(discussionsToggle).click(); - // After toggle is clicked, it should be hidden. - expect(cohortsView.$(discussionsToggle)).toHaveClass('is-hidden'); - - // Should see the course wide discussions form and its content - courseWideDiscussionsForm = cohortsView.$(courseWideDiscussionsFormCss); - expect(courseWideDiscussionsForm.length).toBe(1); - - expect(courseWideDiscussionsForm.text()). - toContain('Course-Wide Discussion Topics'); - expect(courseWideDiscussionsForm.text()). - toContain('Select the course-wide discussion topics that you want to divide by cohort.'); - - // Should see the inline discussions form and its content - inlineDiscussionsForm = cohortsView.$(inlineDiscussionsFormCss); - expect(inlineDiscussionsForm.length).toBe(1); - expect(inlineDiscussionsForm.text()). - toContain('Content-Specific Discussion Topics'); - expect(inlineDiscussionsForm.text()). - toContain('Specify whether content-specific discussion topics are divided by cohort.'); - }; - unknownUserMessage = function(name) { return 'Unknown user: ' + name; }; @@ -364,10 +264,6 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/cohort-group-header'); TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/notification'); TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/cohort-state'); - TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/cohort-discussions-category'); - TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/cohort-discussions-subcategory'); - TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/cohort-discussions-course-wide'); - TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/cohort-discussions-inline'); TemplateHelpers.installTemplate('templates/file-upload'); }); @@ -381,8 +277,6 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers // If no cohorts have been created, can't upload a CSV file. expect(cohortsView.$('.wrapper-cohort-supplemental')).toHaveClass('is-hidden'); - // if no cohorts have been created, can't show the link to discussion topics. - expect(cohortsView.$('.cohort-discussions-nav')).toHaveClass('is-hidden'); }); it('syncs data when membership tab is clicked', function() { @@ -422,10 +316,6 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers .toBe("Your file 'upload_file.txt' has been uploaded. Allow a few minutes for processing."); }); - it('can show discussion topics if cohort exists', function() { - showAndAssertDiscussionTopics(this); - }); - describe('Cohort Selector', function() { it('has no initial selection', function() { createCohortsView(this); @@ -1172,375 +1062,5 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers }); }); }); - - describe('Discussion Topics', function() { - var createCourseWideView, createInlineView, - inlineView, courseWideView, assertCohortedTopics; - - createCourseWideView = function(that) { - createCohortsView(that); - - courseWideView = new CohortCourseWideDiscussionsView({ - el: cohortsView.$('.cohort-discussions-nav').removeClass('is-hidden'), - model: cohortsView.context.discussionTopicsSettingsModel, - cohortSettings: cohortsView.cohortSettings - }); - courseWideView.render(); - }; - - createInlineView = function(that, discussionTopicsSettingsModel) { - createCohortsView(that); - - inlineView = new CohortInlineDiscussionsView({ - el: cohortsView.$('.cohort-discussions-nav').removeClass('is-hidden'), - model: discussionTopicsSettingsModel || cohortsView.context.discussionTopicsSettingsModel, - cohortSettings: cohortsView.cohortSettings - }); - inlineView.render(); - }; - - assertCohortedTopics = function(view, type) { - expect(view.$('.check-discussion-subcategory-' + type).length).toBe(2); - expect(view.$('.check-discussion-subcategory-' + type + ':checked').length).toBe(1); - }; - - it('renders the view properly', function() { - showAndAssertDiscussionTopics(this); - }); - - describe('Course Wide', function() { - it('shows the "Save" button as disabled initially', function() { - createCourseWideView(this); - expect(courseWideView.$(courseWideDiscussionsSaveButtonCss).prop('disabled')).toBeTruthy(); - }); - - it('has one cohorted and one non-cohorted topic', function() { - createCourseWideView(this); - - assertCohortedTopics(courseWideView, 'course-wide'); - - expect(courseWideView.$('.cohorted-text').length).toBe(2); - expect(courseWideView.$('.cohorted-text.hidden').length).toBe(1); - }); - - it('enables the "Save" button after changing checkbox', function() { - createCourseWideView(this); - - // save button is disabled. - expect(courseWideView.$(courseWideDiscussionsSaveButtonCss).prop('disabled')).toBeTruthy(); - - $(courseWideView.$('.check-discussion-subcategory-course-wide')[0]).prop('checked', false).change(); - - // save button is enabled. - expect(courseWideView.$(courseWideDiscussionsSaveButtonCss).prop('disabled')).toBeFalsy(); - }); - - it('saves the topic successfully', function() { - createCourseWideView(this); - - $(courseWideView.$('.check-discussion-subcategory-course-wide')[1]).prop('checked', 'checked').change(); - expect(courseWideView.$(courseWideDiscussionsSaveButtonCss).prop('disabled')).toBeFalsy(); - - // Save the updated settings - courseWideView.$('.action-save').click(); - - // fake requests for cohort settings with PATCH method. - AjaxHelpers.expectJsonRequest( - requests, 'PATCH', '/mock_service/cohorts/settings', - {cohorted_course_wide_discussions: ['Topic_C_1', 'Topic_C_2']} - ); - AjaxHelpers.respondWithJson( - requests, - {cohorted_course_wide_discussions: ['Topic_C_1', 'Topic_C_2']} - ); - - // fake request for discussion/topics with GET method. - AjaxHelpers.expectJsonRequest( - requests, 'GET', '/mock_service/cohorts/discussion/topics' - ); - AjaxHelpers.respondWithJson( - requests, - createMockCohortDiscussions() - ); - - // verify the success message. - expect(courseWideView.$(courseWideDiscussionsSaveButtonCss).prop('disabled')).toBeTruthy(); - verifyMessage('Your changes have been saved.', 'confirmation'); - }); - - it('shows an appropriate message when subsequent "GET" returns HTTP500', function() { - createCourseWideView(this); - - $(courseWideView.$('.check-discussion-subcategory-course-wide')[1]).prop('checked', 'checked').change(); - expect(courseWideView.$(courseWideDiscussionsSaveButtonCss).prop('disabled')).toBeFalsy(); - - // Save the updated settings - courseWideView.$('.action-save').click(); - - // fake requests for cohort settings with PATCH method. - AjaxHelpers.expectJsonRequest( - requests, 'PATCH', '/mock_service/cohorts/settings', - {cohorted_course_wide_discussions: ['Topic_C_1', 'Topic_C_2']} - ); - AjaxHelpers.respondWithJson( - requests, - {cohorted_course_wide_discussions: ['Topic_C_1', 'Topic_C_2']} - ); - - // fake request for discussion/topics with GET method. - AjaxHelpers.expectJsonRequest( - requests, 'GET', '/mock_service/cohorts/discussion/topics' - ); - AjaxHelpers.respondWithError(requests, 500); - - var expectedTitle = "We've encountered an error. Refresh your browser and then try again."; - expect(courseWideView.$('.message-title').text().trim()).toBe(expectedTitle); - }); - - it('shows an appropriate error message for HTTP500', function() { - createCourseWideView(this); - - $(courseWideView.$('.check-discussion-subcategory-course-wide')[1]).prop('checked', 'checked').change(); - courseWideView.$('.action-save').click(); - - AjaxHelpers.respondWithError(requests, 500); - var expectedTitle = "We've encountered an error. Refresh your browser and then try again."; - expect(courseWideView.$('.message-title').text().trim()).toBe(expectedTitle); - }); - }); - - describe('Inline', function() { - var enableSaveButton, mockGetRequest, verifySuccess, mockPatchRequest; - - enableSaveButton = function() { - // enable the inline discussion topics. - inlineView.$('.check-cohort-inline-discussions').prop('checked', 'checked').change(); - - $(inlineView.$('.check-discussion-subcategory-inline')[0]).prop('checked', 'checked').change(); - - expect(inlineView.$(inlineDiscussionsSaveButtonCss).prop('disabled')).toBeFalsy(); - }; - - verifySuccess = function() { - // verify the success message. - expect(inlineView.$(inlineDiscussionsSaveButtonCss).prop('disabled')).toBeTruthy(); - verifyMessage('Your changes have been saved.', 'confirmation'); - }; - - mockPatchRequest = function(cohortedInlineDiscussions) { - AjaxHelpers.expectJsonRequest( - requests, 'PATCH', '/mock_service/cohorts/settings', - { - cohorted_inline_discussions: cohortedInlineDiscussions, - always_cohort_inline_discussions: false - } - ); - AjaxHelpers.respondWithJson( - requests, - { - cohorted_inline_discussions: cohortedInlineDiscussions, - always_cohort_inline_discussions: false - } - ); - }; - - mockGetRequest = function(allCohorted) { - // fake request for discussion/topics with GET method. - AjaxHelpers.expectJsonRequest( - requests, 'GET', '/mock_service/cohorts/discussion/topics' - ); - AjaxHelpers.respondWithJson( - requests, - createMockCohortDiscussions(allCohorted) - ); - }; - - it('shows the "Save" button as disabled initially', function() { - createInlineView(this); - expect(inlineView.$(inlineDiscussionsSaveButtonCss).prop('disabled')).toBeTruthy(); - }); - - it('shows always cohort radio button as selected', function() { - createInlineView(this); - inlineView.$('.check-all-inline-discussions').prop('checked', 'checked').change(); - - // verify always cohort inline discussions is being selected. - expect(inlineView.$('.check-all-inline-discussions').prop('checked')).toBeTruthy(); - - // verify that inline topics are disabled - expect(inlineView.$('.check-discussion-subcategory-inline').prop('disabled')).toBeTruthy(); - expect(inlineView.$('.check-discussion-category').prop('disabled')).toBeTruthy(); - - // verify that cohort some topics are not being selected. - expect(inlineView.$('.check-cohort-inline-discussions').prop('checked')).toBeFalsy(); - }); - - it('shows cohort some topics radio button as selected', function() { - createInlineView(this); - inlineView.$('.check-cohort-inline-discussions').prop('checked', 'checked').change(); - - // verify some cohort inline discussions radio is being selected. - expect(inlineView.$('.check-cohort-inline-discussions').prop('checked')).toBeTruthy(); - - // verify always cohort radio is not selected. - expect(inlineView.$('.check-all-inline-discussions').prop('checked')).toBeFalsy(); - - // verify that inline topics are enabled - expect(inlineView.$('.check-discussion-subcategory-inline').prop('disabled')).toBeFalsy(); - expect(inlineView.$('.check-discussion-category').prop('disabled')).toBeFalsy(); - }); - - it('has cohorted and non-cohorted topics', function() { - createInlineView(this); - enableSaveButton(); - assertCohortedTopics(inlineView, 'inline'); - }); - - it('enables "Save" button after changing from always inline option', function() { - createInlineView(this); - enableSaveButton(); - }); - - it('saves the topic', function() { - createInlineView(this); - enableSaveButton(); - - // Save the updated settings - inlineView.$('.action-save').click(); - - mockPatchRequest(['Inline_Discussion_1']); - mockGetRequest(); - - verifySuccess(); - }); - - it('selects the parent category when all children are selected', function() { - createInlineView(this); - enableSaveButton(); - - // parent category should be indeterminate. - expect(inlineView.$('.check-discussion-category:checked').length).toBe(0); - expect(inlineView.$('.check-discussion-category:indeterminate').length).toBe(1); - - inlineView.$('.check-discussion-subcategory-inline').prop('checked', 'checked').change(); - // parent should be checked as we checked all children - expect(inlineView.$('.check-discussion-category:checked').length).toBe(1); - }); - - it('selects/deselects all children when a parent category is selected/deselected', function() { - createInlineView(this); - enableSaveButton(); - - expect(inlineView.$('.check-discussion-category:checked').length).toBe(0); - - inlineView.$('.check-discussion-category').prop('checked', 'checked').change(); - - expect(inlineView.$('.check-discussion-category:checked').length).toBe(1); - expect(inlineView.$('.check-discussion-subcategory-inline:checked').length).toBe(2); - - // un-check the parent, all children should be unchecd. - inlineView.$('.check-discussion-category').prop('checked', false).change(); - expect(inlineView.$('.check-discussion-category:checked').length).toBe(0); - expect(inlineView.$('.check-discussion-subcategory-inline:checked').length).toBe(0); - }); - - it('saves correctly when a subset of topics are selected within a category', function() { - createInlineView(this); - enableSaveButton(); - - // parent category should be indeterminate. - expect(inlineView.$('.check-discussion-category:checked').length).toBe(0); - expect(inlineView.$('.check-discussion-category:indeterminate').length).toBe(1); - - // Save the updated settings - inlineView.$('.action-save').click(); - - mockPatchRequest(['Inline_Discussion_1']); - mockGetRequest(); - - verifySuccess(); - // parent category should be indeterminate. - expect(inlineView.$('.check-discussion-category:indeterminate').length).toBe(1); - }); - - it('saves correctly when all child topics are selected within a category', function() { - createInlineView(this); - enableSaveButton(); - - // parent category should be indeterminate. - expect(inlineView.$('.check-discussion-category:checked').length).toBe(0); - expect(inlineView.$('.check-discussion-category:indeterminate').length).toBe(1); - - inlineView.$('.check-discussion-subcategory-inline').prop('checked', 'checked').change(); - // Save the updated settings - inlineView.$('.action-save').click(); - - mockPatchRequest(['Inline_Discussion_1', 'Inline_Discussion_2']); - mockGetRequest(true); - - verifySuccess(); - // parent category should be checked. - expect(inlineView.$('.check-discussion-category:checked').length).toBe(1); - }); - - it('shows an appropriate message when no inline topics exist', function() { - var topicsJson, discussionTopicsSettingsModel; - - topicsJson = { - course_wide_discussions: { - children: [['Topic_C_1', 'entry']], - entries: { - Topic_C_1: { - sort_key: null, - is_divided: true, - id: 'Topic_C_1' - } - } - }, - inline_discussions: { - subcategories: {}, - children: [] - } - }; - discussionTopicsSettingsModel = new DiscussionTopicsSettingsModel(topicsJson); - - createInlineView(this, discussionTopicsSettingsModel); - - var expectedTitle = 'No content-specific discussion topics exist.'; - expect(inlineView.$('.no-topics').text().trim()).toBe(expectedTitle); - }); - - it('shows an appropriate message when subsequent "GET" returns HTTP500', function() { - createInlineView(this); - enableSaveButton(); - - // Save the updated settings - inlineView.$('.action-save').click(); - - mockPatchRequest(['Inline_Discussion_1']); - - // fake request for discussion/topics with GET method. - AjaxHelpers.expectJsonRequest( - requests, 'GET', '/mock_service/cohorts/discussion/topics' - ); - AjaxHelpers.respondWithError(requests, 500); - - var expectedTitle = "We've encountered an error. Refresh your browser and then try again."; - expect(inlineView.$('.message-title').text().trim()).toBe(expectedTitle); - }); - - it('shows an appropriate error message for HTTP500', function() { - createInlineView(this); - enableSaveButton(); - - $(inlineView.$('.check-discussion-subcategory-inline')[1]).prop('checked', 'checked').change(); - inlineView.$('.action-save').click(); - - AjaxHelpers.respondWithError(requests, 500); - var expectedTitle = "We've encountered an error. Refresh your browser and then try again."; - expect(inlineView.$('.message-title').text().trim()).toBe(expectedTitle); - }); - }); - }); }); }); diff --git a/lms/static/js/spec/groups/views/discussions_spec.js b/lms/static/js/spec/groups/views/discussions_spec.js new file mode 100644 index 0000000000..6a11910755 --- /dev/null +++ b/lms/static/js/spec/groups/views/discussions_spec.js @@ -0,0 +1,512 @@ +define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', + 'common/js/spec_helpers/template_helpers', + 'js/discussions_management/views/discussions', 'js/discussions_management/models/course_discussions_detail', + 'js/discussions_management/models/course_discussions_settings' +], + function(Backbone, $, AjaxHelpers, TemplateHelpers, DiscussionsView, CourseDiscussionTopicDetailsModel, + CourseDiscussionsSettingsModel) { + 'use strict'; + + describe('Discussions View', function() { + var createMockDiscussionsSettingsJson, createDiscussionsView, discussionsView, requests, verifyMessage, + createMockDiscussionsSettings, createMockDiscussionsJson, createMockDiscussions, + showAndAssertDiscussionTopics; + + // Selectors + var inlineDiscussionsFormCss = '.cohort-inline-discussions-form', + courseWideDiscussionsFormCss = '.cohort-course-wide-discussions-form', + courseWideDiscussionsSaveButtonCss = '.cohort-course-wide-discussions-form .action-save', + inlineDiscussionsSaveButtonCss = '.cohort-inline-discussions-form .action-save'; + + createMockDiscussionsSettingsJson = function(dividedInlineDiscussions, + dividedCourseWideDiscussions, + alwaysDivideInlineDiscussions) { + return { + id: 0, + divided_inline_discussions: dividedInlineDiscussions || [], + divided_course_wide_discussions: dividedCourseWideDiscussions || [], + always_divide_inline_discussions: alwaysDivideInlineDiscussions || false + }; + }; + + createMockDiscussionsSettings = function(dividedInlineDiscussions, + dividedCourseWideDiscussions, + alwaysDivideInlineDiscussions) { + return new CourseDiscussionsSettingsModel( + createMockDiscussionsSettingsJson(dividedInlineDiscussions, + dividedCourseWideDiscussions, + alwaysDivideInlineDiscussions) + ); + }; + + createMockDiscussionsJson = function(allDivided) { + return { + course_wide_discussions: { + children: [['Topic_C_1', 'entry'], ['Topic_C_2', 'entry']], + entries: { + Topic_C_1: { + sort_key: null, + is_divided: true, + id: 'Topic_C_1' + }, + Topic_C_2: { + sort_key: null, + is_divided: false, + id: 'Topic_C_2' + } + } + }, + inline_discussions: { + subcategories: { + Topic_I_1: { + subcategories: {}, + children: [['Inline_Discussion_1', 'entry'], ['Inline_Discussion_2', 'entry']], + entries: { + Inline_Discussion_1: { + sort_key: null, + is_divided: true, + id: 'Inline_Discussion_1' + }, + Inline_Discussion_2: { + sort_key: null, + is_divided: allDivided || false, + id: 'Inline_Discussion_2' + } + } + } + }, + children: [['Topic_I_1', 'subcategory']] + } + }; + }; + + createMockDiscussions = function(allDivided) { + return new CourseDiscussionTopicDetailsModel( + createMockDiscussionsJson(allDivided) + ); + }; + + verifyMessage = function(expectedTitle, expectedMessageType, expectedAction, hasDetails) { + expect(discussionsView.$('.message-title').text().trim()).toBe(expectedTitle); + expect(discussionsView.$('div.message')).toHaveClass('message-' + expectedMessageType); + if (expectedAction) { + expect(discussionsView.$('.message-actions .action-primary').text().trim()).toBe(expectedAction); + } else { + expect(discussionsView.$('.message-actions .action-primary').length).toBe(0); + } if (!hasDetails) { + expect(discussionsView.$('.summary-items').length).toBe(0); + } + }; + + createDiscussionsView = function(test, options) { + var discussionSettings, dividedDiscussions, discussionOptions; + discussionOptions = options || {}; + discussionSettings = discussionOptions.cohortSettings || createMockDiscussionsSettings(); + discussionSettings.url = '/mock_service/discussions/settings'; + + dividedDiscussions = discussionOptions.dividedDiscussions || createMockDiscussions(); + dividedDiscussions.url = '/mock_service/discussion/topics'; + + requests = AjaxHelpers.requests(test); + discussionsView = new DiscussionsView({ + el: $('.discussions-management'), + discussionSettings: discussionSettings, + context: { + courseDiscussionTopicDetailsModel: dividedDiscussions + } + }); + discussionsView.render(); + }; + + showAndAssertDiscussionTopics = function() { + var $courseWideDiscussionsForm, + $inlineDiscussionsForm; + + $courseWideDiscussionsForm = discussionsView.$(courseWideDiscussionsFormCss); + $inlineDiscussionsForm = discussionsView.$(inlineDiscussionsFormCss); + + // Discussions form should not be visible. + expect($inlineDiscussionsForm.length).toBe(1); + expect($courseWideDiscussionsForm.length).toBe(1); + + expect($courseWideDiscussionsForm.text()). + toContain('Course-Wide Discussion Topics'); + expect($courseWideDiscussionsForm.text()). + toContain('Select the course-wide discussion topics that you want to divide by cohort.'); + + // Should see the inline discussions form and its content + expect($inlineDiscussionsForm.length).toBe(1); + expect($inlineDiscussionsForm.text()). + toContain('Content-Specific Discussion Topics'); + expect($inlineDiscussionsForm.text()). + toContain('Specify whether content-specific discussion topics are divided by cohort.'); + }; + + beforeEach(function() { + setFixtures('
' + + '
'); + TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/discussions'); + TemplateHelpers.installTemplate( + 'templates/instructor/instructor_dashboard_2/divided-discussions-course-wide' + ); + TemplateHelpers.installTemplate( + 'templates/instructor/instructor_dashboard_2/divided-discussions-inline' + ); + TemplateHelpers.installTemplate( + 'templates/instructor/instructor_dashboard_2/cohort-discussions-category' + ); + TemplateHelpers.installTemplate( + 'templates/instructor/instructor_dashboard_2/cohort-discussions-subcategory' + ); + TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/notification'); + }); + + describe('Discussion Topics', function() { + var courseWideView, assertDividedTopics; + + assertDividedTopics = function(view, type) { + expect($('.check-discussion-subcategory-' + type).length).toBe(2); + expect($('.check-discussion-subcategory-' + type + ':checked').length).toBe(1); + }; + + it('renders the view properly', function() { + createDiscussionsView(this); + showAndAssertDiscussionTopics(this); + }); + + describe('Course Wide', function() { + it('shows the "Save" button as disabled initially', function() { + createDiscussionsView(this); + expect($(courseWideDiscussionsSaveButtonCss).prop('disabled')).toBeTruthy(); + }); + + it('has one divided and one non-divided topic', function() { + createDiscussionsView(this); + assertDividedTopics(courseWideView, 'course-wide'); + + expect($('.course-wide-discussion-topics .divided-discussion-text').length).toBe(2); + expect($('.course-wide-discussion-topics .divided-discussion-text.hidden').length).toBe(1); + }); + + it('enables the "Save" button after changing checkbox', function() { + createDiscussionsView(this); + // save button is disabled. + expect($(courseWideDiscussionsSaveButtonCss).prop('disabled')).toBeTruthy(); + + $($('.check-discussion-subcategory-course-wide')[0]).prop('checked', false).change(); + + // save button is enabled. + expect($(courseWideDiscussionsSaveButtonCss).prop('disabled')).toBeFalsy(); + }); + + it('saves the topic successfully', function() { + createDiscussionsView(this); + $($('.check-discussion-subcategory-course-wide')[1]).prop('checked', 'checked').change(); + expect($(courseWideDiscussionsSaveButtonCss).prop('disabled')).toBeFalsy(); + + // Save the updated settings + $('#cohort-course-wide-discussions-form .action-save').click(); + + // fake requests for discussions settings with PATCH method. + AjaxHelpers.expectJsonRequest( + requests, 'PATCH', '/mock_service/discussions/settings', + {divided_course_wide_discussions: ['Topic_C_1', 'Topic_C_2']} + ); + AjaxHelpers.respondWithJson( + requests, + {divided_course_wide_discussions: ['Topic_C_1', 'Topic_C_2']} + ); + + // fake request for discussion/topics with GET method. + AjaxHelpers.expectJsonRequest( + requests, 'GET', '/mock_service/discussion/topics' + ); + AjaxHelpers.respondWithJson( + requests, + createMockDiscussions() + ); + + // verify the success message. + expect($(courseWideDiscussionsSaveButtonCss).prop('disabled')).toBeTruthy(); + verifyMessage('Your changes have been saved.', 'confirmation'); + }); + + it('shows an appropriate message when subsequent "GET" returns HTTP500', function() { + var expectedTitle; + createDiscussionsView(this); + $($('.check-discussion-subcategory-course-wide')[1]).prop('checked', 'checked').change(); + expect($(courseWideDiscussionsSaveButtonCss).prop('disabled')).toBeFalsy(); + + // Save the updated settings + $('#cohort-course-wide-discussions-form .action-save').click(); + + // fake requests for discussion settings with PATCH method. + AjaxHelpers.expectJsonRequest( + requests, 'PATCH', '/mock_service/discussions/settings', + {divided_course_wide_discussions: ['Topic_C_1', 'Topic_C_2']} + ); + AjaxHelpers.respondWithJson( + requests, + {divided_course_wide_discussions: ['Topic_C_1', 'Topic_C_2']} + ); + + // fake request for discussion/topics with GET method. + AjaxHelpers.expectJsonRequest( + requests, 'GET', '/mock_service/discussion/topics' + ); + AjaxHelpers.respondWithError(requests, 500); + + expectedTitle = "We've encountered an error. Refresh your browser and then try again."; + expect($('.message-title').text().trim()).toBe(expectedTitle); + }); + + it('shows an appropriate error message for HTTP500', function() { + var expectedTitle; + createDiscussionsView(this); + $($('.check-discussion-subcategory-course-wide')[1]).prop('checked', 'checked').change(); + $('.action-save').click(); + + AjaxHelpers.respondWithError(requests, 500); + expectedTitle = "We've encountered an error. Refresh your browser and then try again."; + expect($('.message-title').text().trim()).toBe(expectedTitle); + }); + }); + + describe('Inline', function() { + var enableSaveButton, mockGetRequest, verifySuccess, mockPatchRequest; + + enableSaveButton = function() { + // enable the inline discussion topics. + $('.check-cohort-inline-discussions').prop('checked', 'checked').change(); + + $($('.check-discussion-subcategory-inline')[0]).prop('checked', 'checked').change(); + + expect($(inlineDiscussionsSaveButtonCss).prop('disabled')).toBeFalsy(); + }; + + verifySuccess = function() { + // verify the success message. + expect($(inlineDiscussionsSaveButtonCss).prop('disabled')).toBeTruthy(); + verifyMessage('Your changes have been saved.', 'confirmation'); + }; + + mockPatchRequest = function(dividededInlineDiscussions) { + AjaxHelpers.expectJsonRequest( + requests, 'PATCH', '/mock_service/discussions/settings', + { + divided_inline_discussions: dividededInlineDiscussions, + always_divide_inline_discussions: false + } + ); + AjaxHelpers.respondWithJson( + requests, + { + divided_inline_discussions: dividededInlineDiscussions, + always_divide_inline_discussions: false + } + ); + }; + + mockGetRequest = function(alldivided) { + // fake request for discussion/topics with GET method. + AjaxHelpers.expectJsonRequest( + requests, 'GET', '/mock_service/discussion/topics' + ); + AjaxHelpers.respondWithJson( + requests, + createMockDiscussions(alldivided) + ); + }; + + it('shows the "Save" button as disabled initially', function() { + createDiscussionsView(this); + expect($(inlineDiscussionsSaveButtonCss).prop('disabled')).toBeTruthy(); + }); + + it('shows always divide radio button as selected', function() { + createDiscussionsView(this); + $('.check-all-inline-discussions').prop('checked', 'checked').change(); + + // verify always divide inline discussions is being selected. + expect($('.check-all-inline-discussions').prop('checked')).toBeTruthy(); + + // verify that inline topics are disabled + expect($('.check-discussion-subcategory-inline').prop('disabled')).toBeTruthy(); + expect($('.check-discussion-category').prop('disabled')).toBeTruthy(); + + // verify that divide some topics are not being selected. + expect($('.check-cohort-inline-discussions').prop('checked')).toBeFalsy(); + }); + + it('shows divide some topics radio button as selected', function() { + createDiscussionsView(this); + $('.check-cohort-inline-discussions').prop('checked', 'checked').change(); + + // verify some divide inline discussions radio is being selected. + expect($('.check-cohort-inline-discussions').prop('checked')).toBeTruthy(); + + // verify always divide radio is not selected. + expect($('.check-all-inline-discussions').prop('checked')).toBeFalsy(); + + // verify that inline topics are enabled + expect($('.check-discussion-subcategory-inline').prop('disabled')).toBeFalsy(); + expect($('.check-discussion-category').prop('disabled')).toBeFalsy(); + }); + + it('has divided and non-divided topics', function() { + createDiscussionsView(this); + enableSaveButton(); + assertDividedTopics(this, 'inline'); + }); + + it('enables "Save" button after changing from always inline option', function() { + createDiscussionsView(this); + enableSaveButton(); + }); + + it('saves the topic', function() { + createDiscussionsView(this); + enableSaveButton(); + + // Save the updated settings + $('.action-save').click(); + + mockPatchRequest(['Inline_Discussion_1']); + mockGetRequest(); + + verifySuccess(); + }); + + it('selects the parent category when all children are selected', function() { + createDiscussionsView(this); + enableSaveButton(); + + // parent category should be indeterminate. + expect($('.check-discussion-category:checked').length).toBe(0); + expect($('.check-discussion-category:indeterminate').length).toBe(1); + + $('.check-discussion-subcategory-inline').prop('checked', 'checked').change(); + // parent should be checked as we checked all children + expect($('.check-discussion-category:checked').length).toBe(1); + }); + + it('selects/deselects all children when a parent category is selected/deselected', function() { + createDiscussionsView(this); + enableSaveButton(); + + expect($('.check-discussion-category:checked').length).toBe(0); + + $('.check-discussion-category').prop('checked', 'checked').change(); + + expect($('.check-discussion-category:checked').length).toBe(1); + expect($('.check-discussion-subcategory-inline:checked').length).toBe(2); + + // un-check the parent, all children should be unchecd. + $('.check-discussion-category').prop('checked', false).change(); + expect($('.check-discussion-category:checked').length).toBe(0); + expect($('.check-discussion-subcategory-inline:checked').length).toBe(0); + }); + + it('saves correctly when a subset of topics are selected within a category', function() { + createDiscussionsView(this); + enableSaveButton(); + + // parent category should be indeterminate. + expect($('.check-discussion-category:checked').length).toBe(0); + expect($('.check-discussion-category:indeterminate').length).toBe(1); + + // Save the updated settings + $('#cohort-inline-discussions-form .action-save').click(); + + mockPatchRequest(['Inline_Discussion_1']); + mockGetRequest(); + + verifySuccess(); + // parent category should be indeterminate. + expect($('.check-discussion-category:indeterminate').length).toBe(1); + }); + + it('saves correctly when all child topics are selected within a category', function() { + createDiscussionsView(this); + enableSaveButton(); + + // parent category should be indeterminate. + expect($('.check-discussion-category:checked').length).toBe(0); + expect($('.check-discussion-category:indeterminate').length).toBe(1); + + $('.check-discussion-subcategory-inline').prop('checked', 'checked').change(); + // Save the updated settings + $('#cohort-inline-discussions-form .action-save').click(); + + mockPatchRequest(['Inline_Discussion_1', 'Inline_Discussion_2']); + mockGetRequest(true); + + verifySuccess(); + // parent category should be checked. + expect($('.check-discussion-category:checked').length).toBe(1); + }); + + it('shows an appropriate message when no inline topics exist', function() { + var topicsJson, options, expectedTitle; + + topicsJson = { + course_wide_discussions: { + children: [['Topic_C_1', 'entry']], + entries: { + Topic_C_1: { + sort_key: null, + is_divided: true, + id: 'Topic_C_1' + } + } + }, + inline_discussions: { + subcategories: {}, + children: [] + } + }; + options = {dividedDiscussions: new CourseDiscussionTopicDetailsModel(topicsJson)}; + createDiscussionsView(this, options); + + expectedTitle = 'No content-specific discussion topics exist.'; + expect($('.no-topics').text().trim()).toBe(expectedTitle); + }); + + it('shows an appropriate message when subsequent "GET" returns HTTP500', function() { + var expectedTitle; + createDiscussionsView(this); + enableSaveButton(); + + // Save the updated settings + $('#cohort-inline-discussions-form .action-save').click(); + + mockPatchRequest(['Inline_Discussion_1']); + + // fake request for discussion/topics with GET method. + AjaxHelpers.expectJsonRequest( + requests, 'GET', '/mock_service/discussion/topics' + ); + AjaxHelpers.respondWithError(requests, 500); + + expectedTitle = "We've encountered an error. Refresh your browser and then try again."; + expect($('.message-title').text().trim()).toBe(expectedTitle); + }); + + it('shows an appropriate error message for HTTP500', function() { + var expectedTitle; + createDiscussionsView(this); + enableSaveButton(); + + $($('.check-discussion-subcategory-inline')[1]).prop('checked', 'checked').change(); + $('#cohort-inline-discussions-form .action-save').click(); + + AjaxHelpers.respondWithError(requests, 500); + expectedTitle = "We've encountered an error. Refresh your browser and then try again."; + expect($('.message-title').text().trim()).toBe(expectedTitle); + }); + }); + }); + }); + }); diff --git a/lms/static/lms/js/build.js b/lms/static/lms/js/build.js index a24009f863..999b38585c 100644 --- a/lms/static/lms/js/build.js +++ b/lms/static/lms/js/build.js @@ -28,6 +28,7 @@ 'js/edxnotes/views/page_factory', 'js/financial-assistance/financial_assistance_form_factory', 'js/groups/views/cohorts_dashboard_factory', + 'js/discussions_management/views/discussions_dashboard_factory', 'js/header_factory', 'js/learner_dashboard/program_details_factory', 'js/learner_dashboard/program_list_factory', diff --git a/lms/static/lms/js/spec/main.js b/lms/static/lms/js/spec/main.js index 7832e92fce..28e00e7267 100644 --- a/lms/static/lms/js/spec/main.js +++ b/lms/static/lms/js/spec/main.js @@ -731,6 +731,7 @@ 'js/spec/edxnotes/views/visibility_decorator_spec.js', 'js/spec/financial-assistance/financial_assistance_form_view_spec.js', 'js/spec/groups/views/cohorts_spec.js', + 'js/spec/groups/views/discussions_spec.js', 'js/spec/instructor_dashboard/certificates_bulk_exception_spec.js', 'js/spec/instructor_dashboard/certificates_exception_spec.js', 'js/spec/instructor_dashboard/certificates_invalidation_spec.js', diff --git a/lms/static/sass/course/instructor/_instructor_2.scss b/lms/static/sass/course/instructor/_instructor_2.scss index 55caeeb5f2..d01e2e7d62 100644 --- a/lms/static/sass/course/instructor/_instructor_2.scss +++ b/lms/static/sass/course/instructor/_instructor_2.scss @@ -1181,49 +1181,6 @@ } } - // cohort discussions interface. - .cohort-discussions-nav { - - .cohort-course-wide-discussions-form { - - .form-actions { - padding-top: ($baseline/2); - } - } - - .category-title, - .topic-name, - .all-inline-discussions, - .always_cohort_inline_discussions, - .cohort_inline_discussions { - padding-left: ($baseline/2); - } - - .always_cohort_inline_discussions, - .cohort_inline_discussions { - padding-top: ($baseline/2); - } - - .category-item, - .subcategory-item { - padding-top: ($baseline/2); - } - - .cohorted-text { - color: $uxpl-blue-base; - } - - .discussions-wrapper { - @extend %ui-no-list; - padding: 0 ($baseline/2); - - .subcategories { - padding: 0 ($baseline*1.5); - } - } - } - - .wrapper-tabs { // This applies to the tab-like interface that toggles between the student management and the group settings @extend %ui-no-list; @extend %ui-depth1; @@ -1280,6 +1237,77 @@ } } +// view - discussions management +// -------------------- +.instructor-dashboard-wrapper-2 section.idash-section#discussions_management { + + .form-submit { + @include idashbutton($uxpl-blue-base); + @include font-size(14); + @include line-height(14); + margin-right: ($baseline/2); + margin-bottom: 0; + text-shadow: none; + } + + .discussions-management-supplemental { + @extend %t-copy-sub1; + margin-top: $baseline; + padding: ($baseline/2) $baseline; + background: $gray-l6; + border-radius: ($baseline/10); + } + + // cohort discussions interface. + .discussions-nav { + + .cohort-course-wide-discussions-form { + + .form-actions { + padding-top: ($baseline/2); + } + } + + .category-title, + .topic-name, + .all-inline-discussions, + .always_divide_inline_discussions, + .divide_inline_discussions { + padding-left: ($baseline/2); + } + + .always_divide_inline_discussions, + .divide_inline_discussions { + padding-top: ($baseline/2); + } + + .category-item, + .subcategory-item { + padding-top: ($baseline/2); + } + + .divided-discussion-text{ + color: $uxpl-blue-base; + } + + .discussions-wrapper { + @extend %ui-no-list; + padding: 0 ($baseline/2); + + .subcategories { + padding: 0 ($baseline*1.5); + } + } + } + .wrapper-tabs { + @extend %ui-no-list; + @extend %ui-depth1; + position: relative; + top: 1px; + padding: 0 $baseline; + } +} + // view - student admin // -------------------- diff --git a/lms/templates/instructor/instructor_dashboard_2/cohort-discussions-subcategory.underscore b/lms/templates/instructor/instructor_dashboard_2/cohort-discussions-subcategory.underscore index 95ed87faf3..a93ba78877 100644 --- a/lms/templates/instructor/instructor_dashboard_2/cohort-discussions-subcategory.underscore +++ b/lms/templates/instructor/instructor_dashboard_2/cohort-discussions-subcategory.underscore @@ -3,7 +3,7 @@ diff --git a/lms/templates/instructor/instructor_dashboard_2/cohort_management.html b/lms/templates/instructor/instructor_dashboard_2/cohort_management.html index 8e7bad3439..9aa5c6c863 100644 --- a/lms/templates/instructor/instructor_dashboard_2/cohort_management.html +++ b/lms/templates/instructor/instructor_dashboard_2/cohort_management.html @@ -13,7 +13,6 @@ from openedx.core.djangoapps.course_groups.partition_scheme import get_cohorted_ data-cohorts_url="${section_data['cohorts_url']}" data-upload_cohorts_csv_url="${section_data['upload_cohorts_csv_url']}" data-course_cohort_settings_url="${section_data['course_cohort_settings_url']}" - data-discussion-topics-url="${section_data['discussion_topics_url']}" data-verified_track_cohorting_url="${section_data['verified_track_cohorting_url']}" data-is_ccx_enabled="${'true' if section_data['ccx_is_enabled'] else 'false'}" > diff --git a/lms/templates/instructor/instructor_dashboard_2/cohorts.underscore b/lms/templates/instructor/instructor_dashboard_2/cohorts.underscore index 82eb5ea17b..21e9087fd1 100644 --- a/lms/templates/instructor/instructor_dashboard_2/cohorts.underscore +++ b/lms/templates/instructor/instructor_dashboard_2/cohorts.underscore @@ -49,15 +49,5 @@ %>

- -
- - - - - <% } %> diff --git a/lms/templates/instructor/instructor_dashboard_2/discussions.underscore b/lms/templates/instructor/instructor_dashboard_2/discussions.underscore new file mode 100644 index 0000000000..444788c395 --- /dev/null +++ b/lms/templates/instructor/instructor_dashboard_2/discussions.underscore @@ -0,0 +1,5 @@ + +
+
+
+
diff --git a/lms/templates/instructor/instructor_dashboard_2/discussions_management.html b/lms/templates/instructor/instructor_dashboard_2/discussions_management.html new file mode 100644 index 0000000000..326bd8ea8c --- /dev/null +++ b/lms/templates/instructor/instructor_dashboard_2/discussions_management.html @@ -0,0 +1,22 @@ +<%page expression_filter="h" args="section_data"/> + +<%namespace name='static' file='../../static_content.html'/> +<%! +from django.utils.translation import ugettext as _ +from openedx.core.djangolib.js_utils import js_escaped_string, dump_js_escaped_json +from courseware.courses import get_studio_url +from openedx.core.djangoapps.course_groups.partition_scheme import get_cohorted_user_partition +%> + + +
+
+ +<%block name="js_extra"> +<%static:require_module module_name="js/discussions_management/views/discussions_dashboard_factory" class_name="DiscussionsFactory"> + DiscussionsFactory(); + + diff --git a/lms/templates/instructor/instructor_dashboard_2/cohort-discussions-course-wide.underscore b/lms/templates/instructor/instructor_dashboard_2/divided-discussions-course-wide.underscore similarity index 94% rename from lms/templates/instructor/instructor_dashboard_2/cohort-discussions-course-wide.underscore rename to lms/templates/instructor/instructor_dashboard_2/divided-discussions-course-wide.underscore index 6038113dbd..f359bf75b4 100644 --- a/lms/templates/instructor/instructor_dashboard_2/cohort-discussions-course-wide.underscore +++ b/lms/templates/instructor/instructor_dashboard_2/divided-discussions-course-wide.underscore @@ -1,6 +1,6 @@

<%- gettext('Specify whether discussion topics are divided by cohort') %>

-
+
diff --git a/lms/templates/instructor/instructor_dashboard_2/cohort-discussions-inline.underscore b/lms/templates/instructor/instructor_dashboard_2/divided-discussions-inline.underscore similarity index 86% rename from lms/templates/instructor/instructor_dashboard_2/cohort-discussions-inline.underscore rename to lms/templates/instructor/instructor_dashboard_2/divided-discussions-inline.underscore index 9879e7b952..b6c9e129bf 100644 --- a/lms/templates/instructor/instructor_dashboard_2/cohort-discussions-inline.underscore +++ b/lms/templates/instructor/instructor_dashboard_2/divided-discussions-inline.underscore @@ -1,21 +1,21 @@
-
+

<%- gettext('Content-Specific Discussion Topics') %>

<%- gettext('Specify whether content-specific discussion topics are divided by cohort.') %>

-
+
-
+
diff --git a/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html b/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html index bdd6cde429..cf592aab79 100644 --- a/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html +++ b/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html @@ -81,7 +81,7 @@ from openedx.core.djangolib.markup import HTML ## Include Underscore templates <%block name="header_extras"> -% for template_name in ["cohorts", "enrollment-code-lookup-links", "cohort-editor", "cohort-group-header", "cohort-selector", "cohort-form", "notification", "cohort-state", "cohort-discussions-inline", "cohort-discussions-course-wide", "cohort-discussions-category", "cohort-discussions-subcategory", "certificate-white-list", "certificate-white-list-editor", "certificate-bulk-white-list", "certificate-invalidation"]: +% for template_name in ["cohorts", "discussions", "enrollment-code-lookup-links", "cohort-editor", "cohort-group-header", "cohort-selector", "cohort-form", "notification", "cohort-state", "divided-discussions-inline", "divided-discussions-course-wide", "cohort-discussions-category", "cohort-discussions-subcategory", "certificate-white-list", "certificate-white-list-editor", "certificate-bulk-white-list", "certificate-invalidation"]: diff --git a/lms/urls.py b/lms/urls.py index ca99a2740b..5946f8af74 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -504,6 +504,15 @@ urlpatterns += ( include(COURSE_URLS) ), + # Discussions Management + url( + r'^courses/{}/discussions/settings$'.format( + settings.COURSE_KEY_PATTERN, + ), + 'openedx.core.djangoapps.course_groups.views.course_discussions_settings_handler', + name='course_discussions_settings', + ), + # Cohorts management url( r'^courses/{}/cohorts/settings$'.format( @@ -548,11 +557,11 @@ urlpatterns += ( name='debug_cohort_mgmt', ), url( - r'^courses/{}/cohorts/topics$'.format( + r'^courses/{}/discussion/topics$'.format( settings.COURSE_KEY_PATTERN, ), - 'openedx.core.djangoapps.course_groups.views.cohort_discussion_topics', - name='cohort_discussion_topics', + 'openedx.core.djangoapps.course_groups.views.discussion_topics', + name='discussion_topics', ), url( r'^courses/{}/verified_track_content/settings'.format( diff --git a/openedx/core/djangoapps/course_groups/cohorts.py b/openedx/core/djangoapps/course_groups/cohorts.py index 6e745766b9..5d919124b7 100644 --- a/openedx/core/djangoapps/course_groups/cohorts.py +++ b/openedx/core/djangoapps/course_groups/cohorts.py @@ -121,6 +121,16 @@ def is_course_cohorted(course_key): return _get_course_cohort_settings(course_key).is_cohorted +def get_course_cohort_id(course_key): + """ + Given a course key, return the int id for the cohort settings. + + Raises: + Http404 if the course doesn't exist. + """ + return _get_course_cohort_settings(course_key).id + + def set_course_cohorted(course_key, cohorted): """ Given a course course and a boolean, sets whether or not the course is cohorted. diff --git a/openedx/core/djangoapps/course_groups/tests/helpers.py b/openedx/core/djangoapps/course_groups/tests/helpers.py index 87ce36ed14..d22e5cb7bc 100644 --- a/openedx/core/djangoapps/course_groups/tests/helpers.py +++ b/openedx/core/djangoapps/course_groups/tests/helpers.py @@ -135,43 +135,72 @@ def config_course_cohorts_legacy( pass +# pylint: disable=dangerous-default-value +def config_course_discussions( + course, + discussion_topics={}, + divided_discussions=[], + always_divide_inline_discussions=False +): + """ + Set discussions and configure divided discussions for a course. + + Arguments: + course: CourseDescriptor + discussion_topics (Dict): Discussion topic names. Picks ids and + sort_keys automatically. + divided_discussions: Discussion topics to divide. Converts the + list to use the same ids as discussion topic names. + always_divide_inline_discussions (bool): Whether inline discussions + should be divided by default. + + Returns: + Nothing -- modifies course in place. + """ + + def to_id(name): + """Convert name to id.""" + return topic_name_to_id(course, name) + + set_course_discussion_settings( + course.id, + divided_discussions=[to_id(name) for name in divided_discussions], + always_divide_inline_discussions=always_divide_inline_discussions, + division_scheme=CourseDiscussionSettings.COHORT, + ) + + course.discussion_topics = dict((name, {"sort_key": "A", "id": to_id(name)}) + for name in discussion_topics) + try: + # Not implemented for XMLModulestore, which is used by test_cohorts. + modulestore().update_item(course, ModuleStoreEnum.UserID.test) + except NotImplementedError: + pass + + # pylint: disable=dangerous-default-value def config_course_cohorts( course, is_cohorted, auto_cohorts=[], manual_cohorts=[], - discussion_topics=[], - divided_discussions=[], - always_divide_inline_discussions=False ): """ - Set discussions and configure cohorts for a course. + Set and configure cohorts for a course. Arguments: course: CourseDescriptor is_cohorted (bool): Is the course cohorted? auto_cohorts (list): Names of auto cohorts to create. manual_cohorts (list): Names of manual cohorts to create. - discussion_topics (list): Discussion topic names. Picks ids and - sort_keys automatically. - divided_discussions: Discussion topics to divide. Converts the - list to use the same ids as discussion topic names. - always_divide_inline_discussions (bool): Whether inline discussions - should be divided by default. Returns: Nothing -- modifies course in place. """ - def to_id(name): - """Convert name to id.""" - return topic_name_to_id(course, name) set_course_cohorted(course.id, is_cohorted) set_course_discussion_settings( course.id, - divided_discussions=[to_id(name) for name in divided_discussions], - always_divide_inline_discussions=always_divide_inline_discussions, division_scheme=CourseDiscussionSettings.COHORT, ) @@ -183,8 +212,6 @@ def config_course_cohorts( cohort = CohortFactory(course_id=course.id, name=cohort_name) CourseCohortFactory(course_user_group=cohort, assignment_type=CourseCohort.MANUAL) - course.discussion_topics = dict((name, {"sort_key": "A", "id": to_id(name)}) - for name in discussion_topics) try: # Not implemented for XMLModulestore, which is used by test_cohorts. modulestore().update_item(course, ModuleStoreEnum.UserID.test) diff --git a/openedx/core/djangoapps/course_groups/tests/test_views.py b/openedx/core/djangoapps/course_groups/tests/test_views.py index 870ae9c945..14ef5acfee 100644 --- a/openedx/core/djangoapps/course_groups/tests/test_views.py +++ b/openedx/core/djangoapps/course_groups/tests/test_views.py @@ -25,15 +25,17 @@ from openedx.core.djangolib.testing.utils import skip_unless_lms from ..models import CourseUserGroup, CourseCohort from ..views import ( - course_cohort_settings_handler, cohort_handler, users_in_cohort, add_users_to_cohort, remove_user_from_cohort, - link_cohort_to_partition_group, cohort_discussion_topics + course_cohort_settings_handler, course_discussions_settings_handler, + cohort_handler, users_in_cohort, + add_users_to_cohort, remove_user_from_cohort, + link_cohort_to_partition_group, discussion_topics ) from ..cohorts import ( get_cohort, get_cohort_by_name, get_cohort_by_id, DEFAULT_COHORT_NAME, get_group_info_for_cohort ) from .helpers import ( - config_course_cohorts, config_course_cohorts_legacy, CohortFactory, CourseCohortFactory, topic_name_to_id + config_course_cohorts, config_course_discussions, config_course_cohorts_legacy, CohortFactory, CourseCohortFactory, topic_name_to_id ) @@ -99,13 +101,13 @@ class CohortViewsTestCase(ModuleStoreTestCase): view_args.insert(0, request) self.assertRaises(Http404, view, *view_args) - def create_cohorted_discussions(self): + def create_divided_discussions(self): """ - Set up a cohorted discussion in the system, complete with all the fixings + Set up a divided discussion in the system, complete with all the fixings """ - cohorted_inline_discussions = ['Topic A'] - cohorted_course_wide_discussions = ["Topic B"] - cohorted_discussions = cohorted_inline_discussions + cohorted_course_wide_discussions + divided_inline_discussions = ['Topic A'] + divided_course_wide_discussions = ["Topic B"] + divided_discussions = divided_inline_discussions + divided_course_wide_discussions # inline discussion ItemFactory.create( @@ -124,10 +126,14 @@ class CohortViewsTestCase(ModuleStoreTestCase): config_course_cohorts( self.course, is_cohorted=True, - discussion_topics=discussion_topics, - divided_discussions=cohorted_discussions ) - return cohorted_inline_discussions, cohorted_course_wide_discussions + + config_course_discussions( + self.course, + discussion_topics=discussion_topics, + divided_discussions=divided_discussions + ) + return divided_inline_discussions, divided_course_wide_discussions def get_handler(self, course, cohort=None, expected_response_code=200, handler=cohort_handler): """ @@ -177,6 +183,129 @@ class CohortViewsTestCase(ModuleStoreTestCase): return json.loads(response.content) +@attr(shard=2) +class CourseDiscussionsHandlerTestCase(CohortViewsTestCase): + """ + Tests the course_discussion_settings_handler + """ + def get_expected_response(self): + """ + Returns the static response dict. + """ + return { + u'always_divide_inline_discussions': False, + u'divided_inline_discussions': [], + u'divided_course_wide_discussions': [], + u'id': 1 + } + + def test_non_staff(self): + """ + Verify that we cannot access course_discussions_settings_handler if we're a non-staff user. + """ + self._verify_non_staff_cannot_access(course_discussions_settings_handler, "GET", [unicode(self.course.id)]) + self._verify_non_staff_cannot_access(course_discussions_settings_handler, "PATCH", [unicode(self.course.id)]) + + def test_update_always_divide_inline_discussion_settings(self): + """ + Verify that course_discussions_settings_handler is working for always_divide_inline_discussions via HTTP PATCH. + """ + config_course_cohorts(self.course, is_cohorted=True) + + response = self.get_handler(self.course, handler=course_discussions_settings_handler) + + expected_response = self.get_expected_response() + + self.assertEqual(response, expected_response) + + expected_response['always_divide_inline_discussions'] = True + response = self.patch_handler(self.course, data=expected_response, handler=course_discussions_settings_handler) + + self.assertEqual(response, expected_response) + + def test_update_course_wide_discussion_settings(self): + """ + Verify that course_discussions_settings_handler is working for divided_course_wide_discussions via HTTP PATCH. + """ + # course-wide discussion + discussion_topics = { + "Topic B": {"id": "Topic B"}, + } + + config_course_cohorts(self.course, is_cohorted=True) + config_course_discussions(self.course, discussion_topics=discussion_topics) + + response = self.get_handler(self.course, handler=course_discussions_settings_handler) + + expected_response = self.get_expected_response() + self.assertEqual(response, expected_response) + + expected_response['divided_course_wide_discussions'] = [topic_name_to_id(self.course, "Topic B")] + response = self.patch_handler(self.course, data=expected_response, handler=course_discussions_settings_handler) + + self.assertEqual(response, expected_response) + + def test_update_inline_discussion_settings(self): + """ + Verify that course_discussions_settings_handler is working for divided_inline_discussions via HTTP PATCH. + """ + config_course_cohorts(self.course, is_cohorted=True) + + response = self.get_handler(self.course, handler=course_discussions_settings_handler) + + expected_response = self.get_expected_response() + self.assertEqual(response, expected_response) + + now = datetime.now() + # inline discussion + ItemFactory.create( + parent_location=self.course.location, + category="discussion", + discussion_id="Topic_A", + discussion_category="Chapter", + discussion_target="Discussion", + start=now + ) + + expected_response['divided_inline_discussions'] = ["Topic_A"] + response = self.patch_handler(self.course, data=expected_response, handler=course_discussions_settings_handler) + + self.assertEqual(response, expected_response) + + def test_get_settings(self): + """ + Verify that course_discussions_settings_handler is working for HTTP GET. + """ + divided_inline_discussions, divided_course_wide_discussions = self.create_divided_discussions() + + response = self.get_handler(self.course, handler=course_discussions_settings_handler) + expected_response = self.get_expected_response() + + expected_response['divided_inline_discussions'] = [topic_name_to_id(self.course, name) + for name in divided_inline_discussions] + expected_response['divided_course_wide_discussions'] = [topic_name_to_id(self.course, name) + for name in divided_course_wide_discussions] + + self.assertEqual(response, expected_response) + + def test_update_settings_with_invalid_field_data_type(self): + """ + Verify that course_discussions_settings_handler return HTTP 400 if field data type is incorrect. + """ + config_course_cohorts(self.course, is_cohorted=True) + + response = self.patch_handler( + self.course, + data={'always_divide_inline_discussions': ''}, + expected_response_code=400, + handler=course_discussions_settings_handler + ) + self.assertEqual( + "Incorrect field type for `{}`. Type must be `{}`".format('always_divide_inline_discussions', bool.__name__), + response.get("error") + ) + + @attr(shard=2) class CourseCohortSettingsHandlerTestCase(CohortViewsTestCase): """ @@ -189,9 +318,6 @@ class CourseCohortSettingsHandlerTestCase(CohortViewsTestCase): """ return { 'is_cohorted': True, - 'always_cohort_inline_discussions': False, - 'cohorted_inline_discussions': [], - 'cohorted_course_wide_discussions': [], 'id': 1 } @@ -202,22 +328,6 @@ class CourseCohortSettingsHandlerTestCase(CohortViewsTestCase): self._verify_non_staff_cannot_access(course_cohort_settings_handler, "GET", [unicode(self.course.id)]) self._verify_non_staff_cannot_access(course_cohort_settings_handler, "PATCH", [unicode(self.course.id)]) - def test_get_settings(self): - """ - Verify that course_cohort_settings_handler is working for HTTP GET. - """ - cohorted_inline_discussions, cohorted_course_wide_discussions = self.create_cohorted_discussions() - - response = self.get_handler(self.course, handler=course_cohort_settings_handler) - expected_response = self.get_expected_response() - - expected_response['cohorted_inline_discussions'] = [topic_name_to_id(self.course, name) - for name in cohorted_inline_discussions] - expected_response['cohorted_course_wide_discussions'] = [topic_name_to_id(self.course, name) - for name in cohorted_course_wide_discussions] - - self.assertEqual(response, expected_response) - def test_update_is_cohorted_settings(self): """ Verify that course_cohort_settings_handler is working for is_cohorted via HTTP PATCH. @@ -235,71 +345,6 @@ class CourseCohortSettingsHandlerTestCase(CohortViewsTestCase): self.assertEqual(response, expected_response) - def test_update_always_cohort_inline_discussion_settings(self): - """ - Verify that course_cohort_settings_handler is working for always_cohort_inline_discussions via HTTP PATCH. - """ - config_course_cohorts(self.course, is_cohorted=True) - - response = self.get_handler(self.course, handler=course_cohort_settings_handler) - - expected_response = self.get_expected_response() - - self.assertEqual(response, expected_response) - - expected_response['always_cohort_inline_discussions'] = True - response = self.patch_handler(self.course, data=expected_response, handler=course_cohort_settings_handler) - - self.assertEqual(response, expected_response) - - def test_update_course_wide_discussion_settings(self): - """ - Verify that course_cohort_settings_handler is working for cohorted_course_wide_discussions via HTTP PATCH. - """ - # course-wide discussion - discussion_topics = { - "Topic B": {"id": "Topic B"}, - } - - config_course_cohorts(self.course, is_cohorted=True, discussion_topics=discussion_topics) - - response = self.get_handler(self.course, handler=course_cohort_settings_handler) - - expected_response = self.get_expected_response() - self.assertEqual(response, expected_response) - - expected_response['cohorted_course_wide_discussions'] = [topic_name_to_id(self.course, "Topic B")] - response = self.patch_handler(self.course, data=expected_response, handler=course_cohort_settings_handler) - - self.assertEqual(response, expected_response) - - def test_update_inline_discussion_settings(self): - """ - Verify that course_cohort_settings_handler is working for cohorted_inline_discussions via HTTP PATCH. - """ - config_course_cohorts(self.course, is_cohorted=True) - - response = self.get_handler(self.course, handler=course_cohort_settings_handler) - - expected_response = self.get_expected_response() - self.assertEqual(response, expected_response) - - now = datetime.now() - # inline discussion - ItemFactory.create( - parent_location=self.course.location, - category="discussion", - discussion_id="Topic_A", - discussion_category="Chapter", - discussion_target="Discussion", - start=now - ) - - expected_response['cohorted_inline_discussions'] = ["Topic_A"] - response = self.patch_handler(self.course, data=expected_response, handler=course_cohort_settings_handler) - - self.assertEqual(response, expected_response) - def test_update_settings_with_missing_field(self): """ Verify that course_cohort_settings_handler return HTTP 400 if required data field is missing from post data. @@ -315,17 +360,6 @@ class CourseCohortSettingsHandlerTestCase(CohortViewsTestCase): """ config_course_cohorts(self.course, is_cohorted=True) - response = self.patch_handler( - self.course, - data={'always_cohort_inline_discussions': ''}, - expected_response_code=400, - handler=course_cohort_settings_handler - ) - self.assertEqual( - "Incorrect field type for `{}`. Type must be `{}`".format('always_divide_inline_discussions', bool.__name__), - response.get("error") - ) - response = self.patch_handler( self.course, data={'is_cohorted': ''}, @@ -1240,25 +1274,25 @@ class RemoveUserFromCohortTestCase(CohortViewsTestCase): @attr(shard=2) @skip_unless_lms -class CourseCohortDiscussionTopicsTestCase(CohortViewsTestCase): +class CourseDividedDiscussionTopicsTestCase(CohortViewsTestCase): """ - Tests the `cohort_discussion_topics` view. + Tests the `divide_discussion_topics` view. """ def test_non_staff(self): """ - Verify that we cannot access cohort_discussion_topics if we're a non-staff user. + Verify that we cannot access divide_discussion_topics if we're a non-staff user. """ - self._verify_non_staff_cannot_access(cohort_discussion_topics, "GET", [unicode(self.course.id)]) + self._verify_non_staff_cannot_access(discussion_topics, "GET", [unicode(self.course.id)]) def test_get_discussion_topics(self): """ - Verify that course_cohort_settings_handler is working for HTTP GET. + Verify that divide_discussion_topics is working for HTTP GET. """ # create inline & course-wide discussion to verify the different map. - self.create_cohorted_discussions() + self.create_divided_discussions() - response = self.get_handler(self.course, handler=cohort_discussion_topics) + response = self.get_handler(self.course, handler=discussion_topics) start_date = response['inline_discussions']['subcategories']['Chapter']['start_date'] expected_response = { "course_wide_discussions": { diff --git a/openedx/core/djangoapps/course_groups/views.py b/openedx/core/djangoapps/course_groups/views.py index 0d304c3035..d955254ae0 100644 --- a/openedx/core/djangoapps/course_groups/views.py +++ b/openedx/core/djangoapps/course_groups/views.py @@ -64,20 +64,29 @@ def unlink_cohort_partition_group(cohort): # pylint: disable=invalid-name -def _get_course_cohort_settings_representation(course, is_cohorted, course_discussion_settings): +def _get_course_cohort_settings_representation(cohort_id, is_cohorted): """ Returns a JSON representation of a course cohort settings. """ + return { + 'id': cohort_id, + 'is_cohorted': is_cohorted, + } + + +def _get_course_discussion_settings_representation(course, course_discussion_settings): + """ + Returns a JSON representation of a course discussion settings. + """ divided_course_wide_discussions, divided_inline_discussions = get_divided_discussions( course, course_discussion_settings ) return { 'id': course_discussion_settings.id, - 'is_cohorted': is_cohorted, - 'cohorted_inline_discussions': divided_inline_discussions, - 'cohorted_course_wide_discussions': divided_course_wide_discussions, - 'always_cohort_inline_discussions': course_discussion_settings.always_divide_inline_discussions, + 'divided_inline_discussions': divided_inline_discussions, + 'divided_course_wide_discussions': divided_course_wide_discussions, + 'always_divide_inline_discussions': course_discussion_settings.always_divide_inline_discussions, } @@ -117,6 +126,61 @@ def get_divided_discussions(course, discussion_settings): return divided_course_wide_discussions, divided_inline_discussions +@require_http_methods(("GET", "PATCH")) +@ensure_csrf_cookie +@expect_json +@login_required +def course_discussions_settings_handler(request, course_key_string): + """ + The restful handler for divided discussion setting requests. Requires JSON. + This will raise 404 if user is not staff. + GET + Returns the JSON representation of divided discussion settings for the course. + PATCH + Updates the divided discussion settings for the course. Returns the JSON representation of updated settings. + """ + course_key = CourseKey.from_string(course_key_string) + course = get_course_with_access(request.user, 'staff', course_key) + discussion_settings = get_course_discussion_settings(course_key) + + if request.method == 'PATCH': + divided_course_wide_discussions, divided_inline_discussions = get_divided_discussions( + course, discussion_settings + ) + + settings_to_change = {} + + if 'divided_course_wide_discussions' in request.json or 'divided_inline_discussions' in request.json: + divided_course_wide_discussions = request.json.get( + 'divided_course_wide_discussions', divided_course_wide_discussions + ) + divided_inline_discussions = request.json.get( + 'divided_inline_discussions', divided_inline_discussions + ) + settings_to_change['divided_discussions'] = divided_course_wide_discussions + divided_inline_discussions + + if 'always_divide_inline_discussions' in request.json: + settings_to_change['always_divide_inline_discussions'] = request.json.get( + 'always_divide_inline_discussions' + ) + + if not settings_to_change: + return JsonResponse({"error": unicode("Bad Request")}, 400) + + try: + if settings_to_change: + discussion_settings = set_course_discussion_settings(course_key, **settings_to_change) + + except ValueError as err: + # Note: error message not translated because it is not exposed to the user (UI prevents this state). + return JsonResponse({"error": unicode(err)}, 400) + + return JsonResponse(_get_course_discussion_settings_representation( + course, + discussion_settings + )) + + @require_http_methods(("GET", "PATCH")) @ensure_csrf_cookie @expect_json @@ -131,51 +195,27 @@ def course_cohort_settings_handler(request, course_key_string): Updates the cohort settings for the course. Returns the JSON representation of updated settings. """ course_key = CourseKey.from_string(course_key_string) + # Although this course data is not used this method will return 404 is user is not staff course = get_course_with_access(request.user, 'staff', course_key) - is_cohorted = cohorts.is_course_cohorted(course_key) - discussion_settings = get_course_discussion_settings(course_key) if request.method == 'PATCH': - divided_course_wide_discussions, divided_inline_discussions = get_divided_discussions( - course, discussion_settings - ) - - settings_to_change = {} - - if 'is_cohorted' in request.json: - settings_to_change['is_cohorted'] = request.json.get('is_cohorted') - - if 'cohorted_course_wide_discussions' in request.json or 'cohorted_inline_discussions' in request.json: - divided_course_wide_discussions = request.json.get( - 'cohorted_course_wide_discussions', divided_course_wide_discussions - ) - divided_inline_discussions = request.json.get( - 'cohorted_inline_discussions', divided_inline_discussions - ) - settings_to_change['divided_discussions'] = divided_course_wide_discussions + divided_inline_discussions - - if 'always_cohort_inline_discussions' in request.json: - settings_to_change['always_divide_inline_discussions'] = request.json.get( - 'always_cohort_inline_discussions' - ) - - if not settings_to_change: + if 'is_cohorted' not in request.json: return JsonResponse({"error": unicode("Bad Request")}, 400) + is_cohorted = request.json.get('is_cohorted') try: - if 'is_cohorted' in settings_to_change: - is_cohorted = settings_to_change['is_cohorted'] - cohorts.set_course_cohorted(course_key, is_cohorted) - del settings_to_change['is_cohorted'] - settings_to_change['division_scheme'] = CourseDiscussionSettings.COHORT if is_cohorted \ - else CourseDiscussionSettings.NONE - if settings_to_change: - discussion_settings = set_course_discussion_settings(course_key, **settings_to_change) + cohorts.set_course_cohorted(course_key, is_cohorted) + scheme = CourseDiscussionSettings.COHORT if is_cohorted else CourseDiscussionSettings.NONE + scheme_settings = {'division_scheme': scheme} + set_course_discussion_settings(course_key, **scheme_settings) except ValueError as err: # Note: error message not translated because it is not exposed to the user (UI prevents this state). return JsonResponse({"error": unicode(err)}, 400) - return JsonResponse(_get_course_cohort_settings_representation(course, is_cohorted, discussion_settings)) + return JsonResponse(_get_course_cohort_settings_representation( + cohorts.get_course_cohort_id(course_key), + cohorts.is_course_cohorted(course_key) + )) @require_http_methods(("GET", "PUT", "POST", "PATCH")) @@ -428,9 +468,9 @@ def debug_cohort_mgmt(request, course_key_string): @expect_json @login_required -def cohort_discussion_topics(request, course_key_string): +def discussion_topics(request, course_key_string): """ - The handler for cohort discussion categories requests. + The handler for divided discussion categories requests. This will raise 404 if user is not staff. Returns the JSON representation of discussion topics w.r.t categories for the course.