From 3ce494f5c5a46f19fc20b96609aeb9d406c2f667 Mon Sep 17 00:00:00 2001
From: Muhammad Ammar
Date: Wed, 11 Feb 2015 21:35:01 +0500
Subject: [PATCH 01/11] Enable/disable cohorts from the instructor dashboard
and move cohorts management to its own tab
TNL-1268
---
.../pages/lms/instructor_dashboard.py | 57 ++-
.../discussion/test_cohort_management.py | 30 +-
.../tests/test_cohorted_courseware.py | 3 +-
.../instructor/views/instructor_dashboard.py | 22 +-
.../instructor_dashboard.coffee | 3 +
lms/static/js/factories/cohorts_factory.js | 35 ++
.../groups/models/course_cohort_settings.js | 16 +
lms/static/js/groups/views/cohorts.js | 43 +-
.../course_cohort_settings_notification.js | 35 ++
.../instructor_dashboard/cohort_management.js | 29 ++
.../js/spec/groups/views/cohorts_spec.js | 92 +++-
lms/static/js/spec/main.js | 10 +
.../sass/course/instructor/_instructor_2.scss | 482 +++++++++---------
.../cohort-form.underscore | 4 +-
.../cohort-state.underscore | 3 +
.../cohort_management.html | 45 ++
.../instructor_dashboard_2/cohorts.underscore | 89 ++--
.../instructor_dashboard_2.html | 8 +-
.../instructor_dashboard_2/membership.html | 50 --
lms/urls.py | 3 +
.../core/djangoapps/course_groups/cohorts.py | 53 +-
...ttings_cohorted_discussions__add_field_.py | 91 ++++
.../core/djangoapps/course_groups/models.py | 17 +-
.../djangoapps/course_groups/tests/helpers.py | 17 +-
.../course_groups/tests/test_cohorts.py | 41 +-
.../course_groups/tests/test_views.py | 146 ++++--
.../core/djangoapps/course_groups/views.py | 47 +-
27 files changed, 1041 insertions(+), 430 deletions(-)
create mode 100644 lms/static/js/factories/cohorts_factory.js
create mode 100644 lms/static/js/groups/models/course_cohort_settings.js
create mode 100644 lms/static/js/groups/views/course_cohort_settings_notification.js
create mode 100644 lms/static/js/instructor_dashboard/cohort_management.js
create mode 100644 lms/templates/instructor/instructor_dashboard_2/cohort-state.underscore
create mode 100644 lms/templates/instructor/instructor_dashboard_2/cohort_management.html
create mode 100644 openedx/core/djangoapps/course_groups/migrations/0004_auto__del_field_coursecohortssettings_cohorted_discussions__add_field_.py
diff --git a/common/test/acceptance/pages/lms/instructor_dashboard.py b/common/test/acceptance/pages/lms/instructor_dashboard.py
index 8a7b4dcf79..9f4b1d3527 100644
--- a/common/test/acceptance/pages/lms/instructor_dashboard.py
+++ b/common/test/acceptance/pages/lms/instructor_dashboard.py
@@ -28,6 +28,15 @@ class InstructorDashboardPage(CoursePage):
membership_section.wait_for_page()
return membership_section
+ def select_cohort_management(self):
+ """
+ Selects the cohort management tab and returns the CohortManagementSection
+ """
+ self.q(css='a[data-section=cohort_management]').first.click()
+ cohort_management_section = CohortManagementSection(self.browser)
+ cohort_management_section.wait_for_page()
+ return cohort_management_section
+
def select_data_download(self):
"""
Selects the data download tab and returns a DataDownloadPage.
@@ -84,16 +93,10 @@ class MembershipPage(PageObject):
"""
return MembershipPageAutoEnrollSection(self.browser)
- def select_cohort_management_section(self):
- """
- Returns the MembershipPageCohortManagementSection page object.
- """
- return MembershipPageCohortManagementSection(self.browser)
-
-class MembershipPageCohortManagementSection(PageObject):
+class CohortManagementSection(PageObject):
"""
- The cohort management subsection of the Membership section of the Instructor dashboard.
+ The Cohort Management section of the Instructor dashboard.
"""
url = None
csv_browse_button_selector_css = '.csv-upload #file-upload-form-file'
@@ -104,13 +107,13 @@ class MembershipPageCohortManagementSection(PageObject):
assignment_type_buttons_css = '.cohort-management-assignment-type-settings input'
def is_browser_on_page(self):
- return self.q(css='.cohort-management.membership-section').present
+ return self.q(css='.cohort-management').present
def _bounded_selector(self, selector):
"""
Return `selector`, but limited to the cohort management context.
"""
- return '.cohort-management.membership-section {}'.format(selector)
+ return '.cohort-management {}'.format(selector)
def _get_cohort_options(self):
"""
@@ -158,10 +161,10 @@ class MembershipPageCohortManagementSection(PageObject):
Return assignment settings disabled message in case of default cohort.
"""
query = self.q(css=self._bounded_selector('.copy-error'))
- if query.present:
+ if query.visible:
return query.text[0]
- else:
- return ''
+
+ return ''
@property
def cohort_name_in_header(self):
@@ -232,7 +235,11 @@ class MembershipPageCohortManagementSection(PageObject):
Adds a new manual cohort with the specified name.
If a content group should also be associated, the name of the content group should be specified.
"""
- create_buttons = self.q(css=self._bounded_selector(".action-create"))
+ add_cohort_selector = self._bounded_selector(".action-create")
+
+ # We need to wait because sometime add cohort button is not in a state to be clickable.
+ self.wait_for_element_presence(add_cohort_selector, 'Add Cohort button is present.')
+ create_buttons = self.q(css=add_cohort_selector)
# There are 2 create buttons on the page. The second one is only present when no cohort yet exists
# (in which case the first is not visible). Click on the last present create button.
create_buttons.results[len(create_buttons.results) - 1].click()
@@ -444,6 +451,28 @@ class MembershipPageCohortManagementSection(PageObject):
file_input.send_keys(path)
self.q(css=self._bounded_selector(self.csv_upload_button_selector_css)).first.click()
+ @property
+ def is_cohorted(self):
+ """
+ Returns the state of `Enable Cohorts` checkbox state.
+ """
+ return self.q(css=self._bounded_selector('.cohorts-state')).selected
+
+ @is_cohorted.setter
+ def is_cohorted(self, state):
+ """
+ Check/Uncheck the `Enable Cohorts` checkbox state.
+ """
+ if state != self.is_cohorted:
+ self.q(css=self._bounded_selector('.cohorts-state')).first.click()
+
+ 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)
+
class MembershipPageAutoEnrollSection(PageObject):
"""
diff --git a/common/test/acceptance/tests/discussion/test_cohort_management.py b/common/test/acceptance/tests/discussion/test_cohort_management.py
index 31e4db463c..7ec69497b9 100644
--- a/common/test/acceptance/tests/discussion/test_cohort_management.py
+++ b/common/test/acceptance/tests/discussion/test_cohort_management.py
@@ -63,8 +63,7 @@ class CohortConfigurationTest(EventsTestMixin, UniqueCourseTest, CohortTestMixin
# go to the membership page on the instructor dashboard
self.instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id)
self.instructor_dashboard_page.visit()
- membership_page = self.instructor_dashboard_page.select_membership()
- self.cohort_management_page = membership_page.select_cohort_management_section()
+ self.cohort_management_page = self.instructor_dashboard_page.select_cohort_management()
def verify_cohort_description(self, cohort_name, expected_description):
"""
@@ -441,9 +440,31 @@ class CohortConfigurationTest(EventsTestMixin, UniqueCourseTest, CohortTestMixin
self.assertTrue(self.cohort_management_page.is_assignment_settings_disabled)
- message = "There must be one cohort to which students can be randomly assigned."
+ message = "There must be one cohort to which students can automatically be assigned."
self.assertEqual(message, self.cohort_management_page.assignment_settings_message)
+ def test_cohort_enable_disable(self):
+ """
+ Scenario: Cohort Enable/Disable checkbox related functionality is working as intended.
+
+ Given I have a cohorted course with a user.
+ And I can see the `Enable Cohorts` checkbox is checked.
+ And cohort management controls are visible.
+ When I uncheck the `Enable Cohorts` checkbox.
+ Then I cohort management controls are not visible.
+ And When I reload the page.
+ Then I can see the `Enable Cohorts` checkbox is unchecked.
+ And cohort management controls are not visible.
+ """
+ self.assertTrue(self.cohort_management_page.is_cohorted)
+ self.assertTrue(self.cohort_management_page.cohort_management_controls_visible())
+ self.cohort_management_page.is_cohorted = False
+ self.assertFalse(self.cohort_management_page.cohort_management_controls_visible())
+ self.browser.refresh()
+ self.cohort_management_page.wait_for_page()
+ self.assertFalse(self.cohort_management_page.is_cohorted)
+ self.assertFalse(self.cohort_management_page.cohort_management_controls_visible())
+
def test_link_to_data_download(self):
"""
Scenario: a link is present from the cohort configuration in
@@ -656,8 +677,7 @@ class CohortContentGroupAssociationTest(UniqueCourseTest, CohortTestMixin):
# go to the membership page on the instructor dashboard
self.instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id)
self.instructor_dashboard_page.visit()
- membership_page = self.instructor_dashboard_page.select_membership()
- self.cohort_management_page = membership_page.select_cohort_management_section()
+ self.cohort_management_page = self.instructor_dashboard_page.select_cohort_management()
def test_no_content_group_linked(self):
"""
diff --git a/common/test/acceptance/tests/test_cohorted_courseware.py b/common/test/acceptance/tests/test_cohorted_courseware.py
index 00f42cd14e..cd1df402b1 100644
--- a/common/test/acceptance/tests/test_cohorted_courseware.py
+++ b/common/test/acceptance/tests/test_cohorted_courseware.py
@@ -154,8 +154,7 @@ class EndToEndCohortedCoursewareTest(ContainerBase):
"""
instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id)
instructor_dashboard_page.visit()
- membership_page = instructor_dashboard_page.select_membership()
- cohort_management_page = membership_page.select_cohort_management_section()
+ cohort_management_page = instructor_dashboard_page.select_cohort_management()
def add_cohort_with_student(cohort_name, content_group, student):
cohort_management_page.add_cohort(cohort_name, content_group=content_group)
diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py
index 512283db94..ed6b7aefea 100644
--- a/lms/djangoapps/instructor/views/instructor_dashboard.py
+++ b/lms/djangoapps/instructor/views/instructor_dashboard.py
@@ -65,9 +65,7 @@ def instructor_dashboard_2(request, course_id):
'finance_admin': CourseFinanceAdminRole(course_key).has_user(request.user),
'sales_admin': CourseSalesAdminRole(course_key).has_user(request.user),
'staff': has_access(request.user, 'staff', course),
- 'forum_admin': has_forum_access(
- request.user, course_key, FORUM_ROLE_ADMINISTRATOR
- ),
+ 'forum_admin': has_forum_access(request.user, course_key, FORUM_ROLE_ADMINISTRATOR),
}
if not access['staff']:
@@ -79,6 +77,7 @@ def instructor_dashboard_2(request, course_id):
_section_student_admin(course, access),
_section_data_download(course, access),
_section_analytics(course, access),
+ _section_cohort_management(course, access),
]
#check if there is corresponding entry in the CourseMode Table related to the Instructor Dashboard course
@@ -330,7 +329,22 @@ def _section_membership(course, access):
'modify_access_url': reverse('modify_access', kwargs={'course_id': unicode(course_key)}),
'list_forum_members_url': reverse('list_forum_members', kwargs={'course_id': unicode(course_key)}),
'update_forum_role_membership_url': reverse('update_forum_role_membership', kwargs={'course_id': unicode(course_key)}),
- 'cohorts_ajax_url': reverse('cohorts', kwargs={'course_key_string': unicode(course_key)}),
+ }
+ return section_data
+
+
+def _section_cohort_management(course, access):
+ """ Provide data for the corresponding cohort management section """
+ course_key = course.id
+ section_data = {
+ 'section_key': 'cohort_management',
+ 'section_display_name': _('Cohort Management'),
+ 'access': access,
+ 'course_cohort_settings_url': reverse(
+ 'course_cohort_settings',
+ kwargs={'course_key_string': unicode(course_key)}
+ ),
+ 'cohorts_url': reverse('cohorts', kwargs={'course_key_string': unicode(course_key)}),
'advanced_settings_url': get_studio_url(course, 'settings/advanced'),
'upload_cohorts_csv_url': reverse('add_users_to_cohorts', kwargs={'course_id': unicode(course_key)}),
}
diff --git a/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee b/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee
index fa7c0d353f..3334bd8fb8 100644
--- a/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee
+++ b/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee
@@ -176,6 +176,9 @@ setup_instructor_dashboard_sections = (idash_content) ->
,
constructor: window.InstructorDashboard.sections.Metrics
$element: idash_content.find ".#{CSS_IDASH_SECTION}#metrics"
+ ,
+ constructor: window.InstructorDashboard.sections.CohortManagement
+ $element: idash_content.find ".#{CSS_IDASH_SECTION}#cohort_management"
]
sections_to_initialize.map ({constructor, $element}) ->
diff --git a/lms/static/js/factories/cohorts_factory.js b/lms/static/js/factories/cohorts_factory.js
new file mode 100644
index 0000000000..3d04e1fd91
--- /dev/null
+++ b/lms/static/js/factories/cohorts_factory.js
@@ -0,0 +1,35 @@
+;(function (define, undefined) {
+ 'use strict';
+ define(['jquery', 'js/groups/views/cohorts', 'js/groups/collections/cohort', 'js/groups/models/course_cohort_settings'],
+ function($) {
+
+ return function(contentGroups, studioGroupConfigurationsUrl) {
+
+ var cohorts = new edx.groups.CohortCollection(),
+ courseCohortSettings = new edx.groups.CourseCohortSettingsModel();
+
+ var cohortManagementElement = $('.cohort-management');
+
+ cohorts.url = cohortManagementElement.data('cohorts_url');
+ courseCohortSettings.url = cohortManagementElement.data('course_cohort_settings_url');
+
+ var cohortsView = new edx.groups.CohortsView({
+ el: cohortManagementElement,
+ model: cohorts,
+ contentGroups: contentGroups,
+ cohortSettings: courseCohortSettings,
+ context: {
+ uploadCohortsCsvUrl: cohortManagementElement.data('upload_cohorts_csv_url'),
+ studioAdvancedSettingsUrl: cohortManagementElement.data('advanced-settings-url'),
+ studioGroupConfigurationsUrl: studioGroupConfigurationsUrl
+ }
+ });
+ cohorts.fetch().done(function() {
+ courseCohortSettings.fetch().done(function() {
+ cohortsView.render();
+ })
+ });
+ };
+ });
+}).call(this, define || RequireJS.define);
+
diff --git a/lms/static/js/groups/models/course_cohort_settings.js b/lms/static/js/groups/models/course_cohort_settings.js
new file mode 100644
index 0000000000..a87eeceec7
--- /dev/null
+++ b/lms/static/js/groups/models/course_cohort_settings.js
@@ -0,0 +1,16 @@
+var edx = edx || {};
+
+(function(Backbone) {
+ 'use strict';
+
+ edx.groups = edx.groups || {};
+
+ edx.groups.CourseCohortSettingsModel = Backbone.Model.extend({
+ idAttribute: 'id',
+ defaults: {
+ is_cohorted: false,
+ cohorted_discussions: [],
+ always_cohort_inline_discussions: true
+ }
+ });
+}).call(this, Backbone);
diff --git a/lms/static/js/groups/views/cohorts.js b/lms/static/js/groups/views/cohorts.js
index 4815eaa567..ba175097a9 100644
--- a/lms/static/js/groups/views/cohorts.js
+++ b/lms/static/js/groups/views/cohorts.js
@@ -1,7 +1,7 @@
var edx = edx || {};
(function($, _, Backbone, gettext, interpolate_text, CohortModel, CohortEditorView, CohortFormView,
- NotificationModel, NotificationView, FileUploaderView) {
+ CourseCohortSettingsNotificationView, NotificationModel, NotificationView, FileUploaderView) {
'use strict';
var hiddenClass = 'is-hidden',
@@ -12,6 +12,7 @@ var edx = edx || {};
edx.groups.CohortsView = Backbone.View.extend({
events : {
'change .cohort-select': 'onCohortSelected',
+ 'change .cohorts-state': 'onCohortsEnabledChanged',
'click .action-create': 'showAddCohortForm',
'click .cohort-management-add-form .action-save': 'saveAddCohortForm',
'click .cohort-management-add-form .action-cancel': 'cancelAddCohortForm',
@@ -26,19 +27,21 @@ var edx = edx || {};
this.selectorTemplate = _.template($('#cohort-selector-tpl').text());
this.context = options.context;
this.contentGroups = options.contentGroups;
+ this.cohortSettings = options.cohortSettings;
model.on('sync', this.onSync, this);
- // Update cohort counts when the user clicks back on the membership tab
+ // Update cohort counts when the user clicks back on the cohort management tab
// (for example, after uploading a csv file of cohort assignments and then
// checking results on data download tab).
- $(this.getSectionCss('membership')).click(function () {
+ $(this.getSectionCss('cohort_management')).click(function () {
model.fetch();
});
},
render: function() {
this.$el.html(this.template({
- cohorts: this.model.models
+ cohorts: this.model.models,
+ cohortsEnabled: this.cohortSettings.get('is_cohorted')
}));
this.onSync();
return this;
@@ -51,6 +54,13 @@ var edx = edx || {};
}));
},
+ renderCourseCohortSettingsNotificationView: function() {
+ var cohortStateMessageNotificationView = new CourseCohortSettingsNotificationView({
+ el: $('.cohort-state-message'),
+ cohortEnabled: this.getCohortsEnabled()});
+ cohortStateMessageNotificationView.render();
+ },
+
onSync: function(model, response, options) {
var selectedCohort = this.lastSelectedCohortId && this.model.get(this.lastSelectedCohortId),
hasCohorts = this.model.length > 0,
@@ -98,6 +108,28 @@ var edx = edx || {};
this.showCohortEditor(selectedCohort);
},
+ onCohortsEnabledChanged: function(event) {
+ event.preventDefault();
+ this.saveCohortSettings();
+ },
+
+ saveCohortSettings: function() {
+ var self = this,
+ cohortSettings,
+ fieldData = {is_cohorted: this.getCohortsEnabled()};
+ cohortSettings = this.cohortSettings;
+ cohortSettings.save(
+ fieldData, {wait: true}
+ ).done(function() {
+ self.render();
+ self.renderCourseCohortSettingsNotificationView();
+ });
+ },
+
+ getCohortsEnabled: function() {
+ return this.$('.cohorts-state').prop('checked');
+ },
+
showCohortEditor: function(cohort) {
this.removeNotification();
if (this.editor) {
@@ -242,4 +274,5 @@ var edx = edx || {};
}
});
}).call(this, $, _, Backbone, gettext, interpolate_text, edx.groups.CohortModel, edx.groups.CohortEditorView,
- edx.groups.CohortFormView, NotificationModel, NotificationView, FileUploaderView);
+ edx.groups.CohortFormView, edx.groups.CourseCohortSettingsNotificationView, NotificationModel, NotificationView,
+ FileUploaderView);
diff --git a/lms/static/js/groups/views/course_cohort_settings_notification.js b/lms/static/js/groups/views/course_cohort_settings_notification.js
new file mode 100644
index 0000000000..1708b1e532
--- /dev/null
+++ b/lms/static/js/groups/views/course_cohort_settings_notification.js
@@ -0,0 +1,35 @@
+var edx = edx || {};
+
+(function($, _, Backbone, gettext) {
+ 'use strict';
+
+ edx.groups = edx.groups || {};
+
+ edx.groups.CourseCohortSettingsNotificationView = Backbone.View.extend({
+ initialize: function(options) {
+ this.template = _.template($('#cohort-state-tpl').text());
+ this.cohortEnabled = options.cohortEnabled;
+ },
+
+ render: function() {
+ this.$el.html(this.template({}));
+ this.showCohortStateMessage();
+ return this;
+ },
+
+ showCohortStateMessage: function () {
+ var actionToggleMessage = this.$('.action-toggle-message');
+
+ // The following lines are necessary to re-trigger the CSS animation on span.action-toggle-message
+ actionToggleMessage.removeClass('is-fleeting');
+ actionToggleMessage.offset().width = actionToggleMessage.offset().width;
+ actionToggleMessage.addClass('is-fleeting');
+
+ if (this.cohortEnabled) {
+ actionToggleMessage.text(gettext('Cohorts Enabled'));
+ } else {
+ actionToggleMessage.text(gettext('Cohorts Disabled'));
+ }
+ }
+ });
+}).call(this, $, _, Backbone, gettext);
diff --git a/lms/static/js/instructor_dashboard/cohort_management.js b/lms/static/js/instructor_dashboard/cohort_management.js
new file mode 100644
index 0000000000..1d820ba2cc
--- /dev/null
+++ b/lms/static/js/instructor_dashboard/cohort_management.js
@@ -0,0 +1,29 @@
+(function() {
+ var CohortManagement;
+
+ CohortManagement = (function() {
+
+ function CohortManagement($section) {
+ this.$section = $section;
+ this.$section.data('wrapper', this);
+ }
+
+ CohortManagement.prototype.onClickTitle = function() {};
+
+ return CohortManagement;
+
+ })();
+
+ _.defaults(window, {
+ InstructorDashboard: {}
+ });
+
+ _.defaults(window.InstructorDashboard, {
+ sections: {}
+ });
+
+ _.defaults(window.InstructorDashboard.sections, {
+ CohortManagement: CohortManagement
+ });
+
+}).call(this);
diff --git a/lms/static/js/spec/groups/views/cohorts_spec.js b/lms/static/js/spec/groups/views/cohorts_spec.js
index 4aacf9caf0..3123bef275 100644
--- a/lms/static/js/spec/groups/views/cohorts_spec.js
+++ b/lms/static/js/spec/groups/views/cohorts_spec.js
@@ -1,15 +1,18 @@
define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers',
- 'js/groups/views/cohorts', 'js/groups/collections/cohort', 'js/groups/models/content_group'],
- function (Backbone, $, AjaxHelpers, TemplateHelpers, CohortsView, CohortCollection, ContentGroupModel) {
+ 'js/groups/views/cohorts', 'js/groups/collections/cohort', 'js/groups/models/content_group',
+ 'js/groups/models/course_cohort_settings', 'js/groups/views/course_cohort_settings_notification'],
+ function (Backbone, $, AjaxHelpers, TemplateHelpers, CohortsView, CohortCollection, ContentGroupModel,
+ CourseCohortSettingsModel, CourseCohortSettingsNotificationView) {
'use strict';
describe("Cohorts View", function () {
var catLoversInitialCount = 123, dogLoversInitialCount = 456, unknownUserMessage,
- createMockCohort, createMockCohorts, createMockContentGroups, createCohortsView, cohortsView,
- requests, respondToRefresh, verifyMessage, verifyNoMessage, verifyDetailedMessage, verifyHeader,
- expectCohortAddRequest, getAddModal, selectContentGroup, clearContentGroup, saveFormAndExpectErrors,
- MOCK_COHORTED_USER_PARTITION_ID, MOCK_UPLOAD_COHORTS_CSV_URL, MOCK_STUDIO_ADVANCED_SETTINGS_URL,
- MOCK_STUDIO_GROUP_CONFIGURATIONS_URL, MOCK_MANUAL_ASSIGNMENT, MOCK_RANDOM_ASSIGNMENT;
+ createMockCohort, createMockCohorts, createMockContentGroups, createCohortSettings, createCohortsView,
+ cohortsView, requests, respondToRefresh, verifyMessage, verifyNoMessage, verifyDetailedMessage,
+ verifyHeader, expectCohortAddRequest, 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_MANUAL_ASSIGNMENT, MOCK_RANDOM_ASSIGNMENT;
MOCK_MANUAL_ASSIGNMENT = 'manual';
MOCK_RANDOM_ASSIGNMENT = 'random';
@@ -49,17 +52,35 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe
];
};
+ createMockCohortSettings = function (isCohorted, cohortedDiscussions, alwaysCohortInlineDiscussions) {
+ return {
+ id: 0,
+ is_cohorted: isCohorted || false,
+ cohorted_discussions: cohortedDiscussions || [],
+ always_cohort_inline_discussions: alwaysCohortInlineDiscussions || true
+ };
+ };
+
+ createCohortSettings = function (isCohorted, cohortedDiscussions, alwaysCohortInlineDiscussions) {
+ return new CourseCohortSettingsModel(
+ createMockCohortSettings(isCohorted, cohortedDiscussions, alwaysCohortInlineDiscussions)
+ );
+ };
+
createCohortsView = function (test, options) {
- var cohortsJson, cohorts, contentGroups;
+ var cohortsJson, cohorts, contentGroups, cohortSettings;
options = options || {};
cohortsJson = options.cohorts ? {cohorts: options.cohorts} : createMockCohorts();
cohorts = new CohortCollection(cohortsJson, {parse: true});
contentGroups = options.contentGroups || createMockContentGroups();
+ cohortSettings = options.cohortSettings || createCohortSettings(true);
+ cohortSettings.url = '/mock_service/cohorts/settings';
cohorts.url = '/mock_service/cohorts';
requests = AjaxHelpers.requests(test);
cohortsView = new CohortsView({
model: cohorts,
contentGroups: contentGroups,
+ cohortSettings: cohortSettings,
context: {
uploadCohortsCsvUrl: MOCK_UPLOAD_COHORTS_CSV_URL,
studioAdvancedSettingsUrl: MOCK_STUDIO_ADVANCED_SETTINGS_URL,
@@ -177,13 +198,14 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe
};
beforeEach(function () {
- setFixtures('');
+ setFixtures('');
TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/cohorts');
TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/cohort-form');
TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/cohort-selector');
TemplateHelpers.installTemplate('templates/instructor/instructor_dashboard_2/cohort-editor');
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/file-upload');
});
@@ -202,7 +224,7 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe
it("syncs data when membership tab is clicked", function() {
createCohortsView(this, {selectCohort: 1});
verifyHeader(1, 'Cat Lovers', catLoversInitialCount);
- $(cohortsView.getSectionCss("membership")).click();
+ $(cohortsView.getSectionCss("cohort_management")).click();
AjaxHelpers.expectRequest(requests, 'GET', '/mock_service/cohorts');
respondToRefresh(1001, 2);
verifyHeader(1, 'Cat Lovers', 1001);
@@ -255,6 +277,54 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe
});
});
+ describe("Course Cohort Settings", function () {
+ it('enable/disable working correctly', function () {
+ createCohortsView(this, {cohortSettings: createCohortSettings(false)});
+
+ expect(cohortsView.$('.cohorts-state').prop('checked')).toBeFalsy();
+
+ cohortsView.$('.cohorts-state').prop('checked', true).change();
+ AjaxHelpers.expectJsonRequest(
+ requests, 'PUT', '/mock_service/cohorts/settings',
+ createMockCohortSettings(true, [], true)
+ );
+ AjaxHelpers.respondWithJson(
+ requests,
+ createMockCohortSettings(true)
+ );
+ expect(cohortsView.$('.cohorts-state').prop('checked')).toBeTruthy();
+
+ cohortsView.$('.cohorts-state').prop('checked', false).change();
+ AjaxHelpers.expectJsonRequest(
+ requests, 'PUT', '/mock_service/cohorts/settings',
+ createMockCohortSettings(false, [], true)
+ );
+ AjaxHelpers.respondWithJson(
+ requests,
+ createMockCohortSettings(false)
+ );
+ expect(cohortsView.$('.cohorts-state').prop('checked')).toBeFalsy();
+ });
+
+
+ it('Course Cohort Settings Notification View renders correctly', function () {
+ var createCourseCohortSettingsNotificationView = function (is_cohorted) {
+ var notificationView = new CourseCohortSettingsNotificationView({
+ el: $('.cohort-state-message'),
+ cohortEnabled: is_cohorted});
+ notificationView.render();
+ return notificationView;
+ };
+
+ var notificationView = createCourseCohortSettingsNotificationView(true);
+ expect(notificationView.$('.action-toggle-message').text().trim()).toBe('Cohorts Enabled');
+
+ notificationView = createCourseCohortSettingsNotificationView(false);
+ expect(notificationView.$('.action-toggle-message').text().trim()).toBe('Cohorts Disabled');
+ });
+
+ });
+
describe("Cohort Group Header", function () {
it("renders header correctly", function () {
var cohortName = 'Transformers',
@@ -867,7 +937,7 @@ define(['backbone', 'jquery', 'js/common_helpers/ajax_helpers', 'js/common_helpe
// We have a single random cohort so we should not be allowed to change it assignment type
expect(cohortsView.$('.cohort-management-assignment-type-settings')).toHaveClass('is-disabled');
- expect(cohortsView.$('.copy-error').text()).toContain("There must be one cohort to which students can be randomly assigned.");
+ expect(cohortsView.$('.copy-error').text()).toContain("There must be one cohort to which students can automatically be assigned.");
});
it("cancel settings works", function() {
diff --git a/lms/static/js/spec/main.js b/lms/static/js/spec/main.js
index caf1f418a8..0c2b9fcbca 100644
--- a/lms/static/js/spec/main.js
+++ b/lms/static/js/spec/main.js
@@ -67,6 +67,8 @@
'js/views/notification': 'js/views/notification',
'js/groups/models/cohort': 'js/groups/models/cohort',
'js/groups/models/content_group': 'js/groups/models/content_group',
+ 'js/groups/models/course_cohort_settings': 'js/groups/models/course_cohort_settings',
+ 'js/groups/views/course_cohort_settings_notification': 'js/groups/views/course_cohort_settings_notification',
'js/groups/collections/cohort': 'js/groups/collections/cohort',
'js/groups/views/cohort_editor': 'js/groups/views/cohort_editor',
'js/groups/views/cohort_form': 'js/groups/views/cohort_form',
@@ -294,6 +296,14 @@
exports: 'edx.groups.ContentGroupModel',
deps: ['backbone']
},
+ 'js/groups/models/course_cohort_settings': {
+ exports: 'edx.groups.CourseCohortSettingsModel',
+ deps: ['backbone']
+ },
+ 'js/groups/views/course_cohort_settings_notification': {
+ exports: 'edx.groups.CourseCohortSettingsNotificationView',
+ deps: ['backbone']
+ },
'js/groups/collections/cohort': {
exports: 'edx.groups.CohortCollection',
deps: ['backbone', 'js/groups/models/cohort']
diff --git a/lms/static/sass/course/instructor/_instructor_2.scss b/lms/static/sass/course/instructor/_instructor_2.scss
index 98cf31d895..5ea18fe818 100644
--- a/lms/static/sass/course/instructor/_instructor_2.scss
+++ b/lms/static/sass/course/instructor/_instructor_2.scss
@@ -435,6 +435,248 @@
}
}
+ .batch-enrollment, .batch-beta-testers {
+ textarea {
+ margin-top: 0.2em;
+ height: auto;
+ width: 90%;
+ }
+
+ input {
+ margin-right: ($baseline/4);
+ }
+
+ .request-res-section {
+ margin-top: 1.5em;
+
+ h3 {
+ color: #646464;
+ }
+
+ ul {
+ margin: 0;
+ margin-top: 0.5em;
+ padding: 0;
+ list-style-type: none;
+ line-height: 1.5em;
+ }
+ }
+ }
+ // Auto Enroll Csv Section
+ .auto_enroll_csv {
+ .results {
+
+ }
+ .enrollment_signup_button {
+ @include margin-right($baseline/4);
+ }
+ // Custom File upload
+ .customBrowseBtn {
+ margin: ($baseline/2) 0;
+ display: inline-block;
+ .file-browse {
+ position:relative;
+ overflow:hidden;
+ display: inline;
+ @include margin-left(-5px);
+ span.browse{
+ @include button(simple, $blue);
+ @include margin-right($baseline);
+ padding: 6px ($baseline/2);
+ font-size: 12px;
+ border-radius: 0 3px 3px 0;
+ }
+ input.file_field {
+ position:absolute;
+ @include right(0);
+ top:0;
+ margin:0;
+ padding:0;
+ cursor:pointer;
+ opacity:0;
+ filter:alpha(opacity=0);
+ }
+ }
+ & > span, & input[disabled]{
+ vertical-align: middle;
+ }
+ input[disabled] {
+ @include border-radius(4px 0 0 4px);
+ @include padding(6px 6px 5px);
+ border: 1px solid $lightGrey1;
+ cursor: not-allowed;
+ }
+ }
+ }
+
+ .enroll-option {
+ margin: ($baseline/2) 0;
+ position: relative;
+
+ label {
+ border-bottom: 1px dotted $base-font-color;
+ }
+
+ .hint {
+ @extend %t-copy-sub2;
+ display: none;
+ position: absolute;
+ top: 15px;
+ @include left($baseline*10);
+ padding: ($baseline/2);
+ width: 50%;
+ background-color: $light-gray;
+ box-shadow: 2px 2px 3px $shadow;
+
+ .hint-caret {
+ display: block;
+ position: absolute;
+ top: 0;
+ @include left(-15px);
+ @include border-right(8px solid $light-gray);
+ @include border-left(8px solid transparent);
+ border-top: 8px solid $light-gray;
+ border-bottom: 8px solid transparent;
+ }
+ }
+ }
+
+ label[for="auto-enroll"]:hover + .auto-enroll-hint {
+ display: block;
+ }
+
+ label[for="auto-enroll-beta"]:hover + .auto-enroll-beta-hint {
+ width: 30%;
+ display: block;
+ }
+
+ label[for="email-students"]:hover + .email-students-hint {
+ display: block;
+ }
+
+ label[for="email-students-beta"]:hover + .email-students-beta-hint {
+ width: 30%;
+ display: block;
+ }
+
+ .enroll-actions {
+ margin-top: $baseline;
+ }
+
+ .member-lists-management {
+
+ .wrapper-member-select {
+ padding: ($baseline/2);
+ background-color: $light-gray;
+ }
+
+ .member-lists-selector {
+ display: block;
+ margin: ($baseline/4) 0;
+ padding: ($baseline/4);
+ }
+
+ .auth-list-container {
+ display: none;
+ margin-bottom: ($baseline*1.5);
+
+ &.active {
+ display: block;
+ }
+
+ .member-list-widget {
+
+ .header {
+ @include box-sizing(border-box);
+ @include border-top-radius(3);
+ position: relative;
+ padding: ($baseline/2);
+ background-color: #efefef;
+ border: 1px solid $light-gray;
+ display: none; // hiding to prefer dropdown as header
+ }
+
+ .title {
+ @include font-size(16);
+ }
+
+ .label,
+ .form-label {
+ @extend %t-copy-sub1;
+ color: $lighter-base-font-color;
+ }
+
+ .info {
+ @include box-sizing(border-box);
+ padding: ($baseline/2);
+ border: 1px solid $light-gray;
+ color: $lighter-base-font-color;
+ line-height: 1.3em;
+ font-size: .85em;
+ }
+
+ .member-list {
+ @include box-sizing(border-box);
+
+ table {
+ width: 100%;
+ }
+
+ thead {
+ background-color: $light-gray;
+ }
+
+ tr {
+ border-bottom: 1px solid $light-gray;
+ }
+
+ td {
+ @extend %t-copy-sub1;
+ vertical-align: middle;
+ padding: ($baseline/2) ($baseline/4);
+ @include border-left(1px solid $light-gray);
+ @include border-right(1px solid $light-gray);
+ word-wrap: break-word;
+ }
+ }
+
+ .bottom-bar {
+ @include box-sizing(border-box);
+ @include border-bottom-radius(3);
+ position: relative;
+ padding: ($baseline/2);
+ margin-top: -1px;
+ border: 1px solid $light-gray;
+ background-color: #efefef;
+ box-shadow: inset #bbb 0px 1px 1px 0px;
+ }
+
+ // .add-field
+
+ input[type="button"].add {
+ @include idashbutton($blue);
+ position: absolute;
+ @include right($baseline);
+ }
+ }
+
+ .revoke {
+ color: $lighter-base-font-color;
+ cursor: pointer;
+
+ &:hover, &:focus {
+ color: $alert-color;
+ }
+ }
+ }
+ }
+}
+
+
+// view - cohort management
+// --------------------
+.instructor-dashboard-wrapper-2 section.idash-section#cohort_management {
+
// cohort management
%cohort-management-form {
@@ -733,245 +975,6 @@
}
}
-
-
- .batch-enrollment, .batch-beta-testers {
- textarea {
- margin-top: 0.2em;
- height: auto;
- width: 90%;
- }
-
- input {
- margin-right: ($baseline/4);
- }
-
- .request-res-section {
- margin-top: 1.5em;
-
- h3 {
- color: #646464;
- }
-
- ul {
- margin: 0;
- margin-top: 0.5em;
- padding: 0;
- list-style-type: none;
- line-height: 1.5em;
- }
- }
- }
- // Auto Enroll Csv Section
- .auto_enroll_csv {
- .results {
-
- }
- .enrollment_signup_button {
- @include margin-right($baseline/4);
- }
- // Custom File upload
- .customBrowseBtn {
- margin: ($baseline/2) 0;
- display: inline-block;
- .file-browse {
- position:relative;
- overflow:hidden;
- display: inline;
- @include margin-left(-5px);
- span.browse{
- @include button(simple, $blue);
- @include margin-right($baseline);
- padding: 6px ($baseline/2);
- font-size: 12px;
- border-radius: 0 3px 3px 0;
- }
- input.file_field {
- position:absolute;
- @include right(0);
- top:0;
- margin:0;
- padding:0;
- cursor:pointer;
- opacity:0;
- filter:alpha(opacity=0);
- }
- }
- & > span, & input[disabled]{
- vertical-align: middle;
- }
- input[disabled] {
- @include border-radius(4px 0 0 4px);
- @include padding(6px 6px 5px);
- border: 1px solid $lightGrey1;
- cursor: not-allowed;
- }
- }
- }
-
- .enroll-option {
- margin: ($baseline/2) 0;
- position: relative;
-
- label {
- border-bottom: 1px dotted $base-font-color;
- }
-
- .hint {
- @extend %t-copy-sub2;
- display: none;
- position: absolute;
- top: 15px;
- @include left($baseline*10);
- padding: ($baseline/2);
- width: 50%;
- background-color: $light-gray;
- box-shadow: 2px 2px 3px $shadow;
-
- .hint-caret {
- display: block;
- position: absolute;
- top: 0;
- @include left(-15px);
- @include border-right(8px solid $light-gray);
- @include border-left(8px solid transparent);
- border-top: 8px solid $light-gray;
- border-bottom: 8px solid transparent;
- }
- }
- }
-
- label[for="auto-enroll"]:hover + .auto-enroll-hint {
- display: block;
- }
-
- label[for="auto-enroll-beta"]:hover + .auto-enroll-beta-hint {
- width: 30%;
- display: block;
- }
-
-
- label[for="email-students"]:hover + .email-students-hint {
- display: block;
- }
-
- label[for="email-students-beta"]:hover + .email-students-beta-hint {
- width: 30%;
- display: block;
- }
-
- .enroll-actions {
- margin-top: $baseline;
- }
-
- .member-lists-management {
-
- .wrapper-member-select {
- padding: ($baseline/2);
- background-color: $light-gray;
- }
-
- .member-lists-selector {
- display: block;
- margin: ($baseline/4) 0;
- padding: ($baseline/4);
- }
-
- .auth-list-container {
- display: none;
- margin-bottom: ($baseline*1.5);
-
- &.active {
- display: block;
- }
-
- .member-list-widget {
-
- .header {
- @include box-sizing(border-box);
- @include border-top-radius(3);
- position: relative;
- padding: ($baseline/2);
- background-color: #efefef;
- border: 1px solid $light-gray;
- display: none; // hiding to prefer dropdown as header
- }
-
- .title {
- @include font-size(16);
- }
-
- .label,
- .form-label {
- @extend %t-copy-sub1;
- color: $lighter-base-font-color;
- }
-
- .info {
- @include box-sizing(border-box);
- padding: ($baseline/2);
- border: 1px solid $light-gray;
- color: $lighter-base-font-color;
- line-height: 1.3em;
- font-size: .85em;
- }
-
- .member-list {
- @include box-sizing(border-box);
-
- table {
- width: 100%;
- }
-
- thead {
- background-color: $light-gray;
- }
-
- tr {
- border-bottom: 1px solid $light-gray;
- }
-
- td {
- @extend %t-copy-sub1;
- vertical-align: middle;
- padding: ($baseline/2) ($baseline/4);
- @include border-left(1px solid $light-gray);
- @include border-right(1px solid $light-gray);
- word-wrap: break-word;
- }
- }
-
- .bottom-bar {
- @include box-sizing(border-box);
- @include border-bottom-radius(3);
- position: relative;
- padding: ($baseline/2);
- margin-top: -1px;
- border: 1px solid $light-gray;
- background-color: #efefef;
- box-shadow: inset #bbb 0px 1px 1px 0px;
- }
-
- // .add-field
-
- input[type="button"].add {
- @include idashbutton($blue);
- position: absolute;
- @include right($baseline);
- }
- }
-
- .revoke {
- color: $lighter-base-font-color;
- cursor: pointer;
-
- &:hover, &:focus {
- color: $alert-color;
- }
- }
- }
- }
-
.has-other-input-text { // Given to groups which have an 'other' input that appears when needed
display: inline-block;
@@ -1118,6 +1121,7 @@
}
}
+
// view - student admin
// --------------------
.instructor-dashboard-wrapper-2 section.idash-section#student_admin > {
diff --git a/lms/templates/instructor/instructor_dashboard_2/cohort-form.underscore b/lms/templates/instructor/instructor_dashboard_2/cohort-form.underscore
index 20049ecde5..8d7df60bf0 100644
--- a/lms/templates/instructor/instructor_dashboard_2/cohort-form.underscore
+++ b/lms/templates/instructor/instructor_dashboard_2/cohort-form.underscore
@@ -38,7 +38,7 @@
<%- gettext('Students in this cohort are:') %>
+
+
+
+
+ <%- gettext('Specify whether 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 9cac21bb1d..6148f285d3 100644
--- a/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html
+++ b/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html
@@ -48,6 +48,8 @@
+
+
<%static:js group='module-descriptor-js'/>
<%static:js group='instructor_dash'/>
<%static:js group='application'/>
@@ -63,6 +65,10 @@
+
+
+
+
@@ -71,7 +77,7 @@
## Include Underscore templates
<%block name="header_extras">
-% for template_name in ["cohorts", "cohort-editor", "cohort-group-header", "cohort-selector", "cohort-form", "notification", "cohort-state"]:
+% for template_name in ["cohorts", "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"]:
diff --git a/lms/urls.py b/lms/urls.py
index 445aecfdcd..97a9a74e4e 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -396,6 +396,9 @@ if settings.COURSEWARE_ENABLED:
url(r'^courses/{}/cohorts/debug$'.format(settings.COURSE_KEY_PATTERN),
'openedx.core.djangoapps.course_groups.views.debug_cohort_mgmt',
name="debug_cohort_mgmt"),
+ url(r'^courses/{}/cohorts/topics$'.format(settings.COURSE_KEY_PATTERN),
+ 'openedx.core.djangoapps.course_groups.views.cohort_discussion_topics',
+ name='cohort_discussion_topics'),
# Open Ended Notifications
url(r'^courses/{}/open_ended_notifications$'.format(settings.COURSE_ID_PATTERN),
diff --git a/openedx/core/djangoapps/course_groups/tests/test_views.py b/openedx/core/djangoapps/course_groups/tests/test_views.py
index 95bec032c0..900ce98889 100644
--- a/openedx/core/djangoapps/course_groups/tests/test_views.py
+++ b/openedx/core/djangoapps/course_groups/tests/test_views.py
@@ -6,20 +6,22 @@ Tests for course group views
import json
from collections import namedtuple
+from datetime import datetime
from django.contrib.auth.models import User
from django.http import Http404
from django.test.client import RequestFactory
-
+import django_test_client_utils # monkey-patch for PATCH request method.
from student.models import CourseEnrollment
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from opaque_keys.edx.locations import SlashSeparatedCourseKey
+from xmodule.modulestore.tests.factories import ItemFactory
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
+ link_cohort_to_partition_group, cohort_discussion_topics
)
from ..cohorts import (
get_cohort, get_cohort_by_name, get_cohort_by_id,
@@ -91,6 +93,33 @@ class CohortViewsTestCase(ModuleStoreTestCase):
view_args.insert(0, request)
self.assertRaises(Http404, view, *view_args)
+ def create_cohorted_discussions(self):
+ cohorted_inline_discussions = ['Topic A']
+ cohorted_course_wide_discussions = ["Topic B"]
+ cohorted_discussions = cohorted_inline_discussions + cohorted_course_wide_discussions
+
+ # inline discussion
+ ItemFactory.create(
+ parent_location=self.course.location,
+ category="discussion",
+ discussion_id=topic_name_to_id(self.course, "Topic A"),
+ discussion_category="Chapter",
+ discussion_target="Discussion",
+ start=datetime.now()
+ )
+ # course-wide discussion
+ discussion_topics = {
+ "Topic B": {"id": "Topic B"},
+ }
+
+ config_course_cohorts(
+ self.course,
+ is_cohorted=True,
+ discussion_topics=discussion_topics,
+ cohorted_discussions=cohorted_discussions
+ )
+ return cohorted_inline_discussions, cohorted_course_wide_discussions
+
def get_handler(self, course, cohort=None, expected_response_code=200, handler=cohort_handler):
"""
Call a GET on `handler` for a given `course` and return its response as a dict.
@@ -121,56 +150,143 @@ class CohortViewsTestCase(ModuleStoreTestCase):
self.assertEqual(response.status_code, expected_response_code)
return json.loads(response.content)
+ def patch_handler(self, course, cohort=None, data=None, expected_response_code=200, handler=cohort_handler):
+ """
+ Call a PATCH on `handler` for a given `course` and return its response as a dict.
+ Raise an exception if response status code is not as expected.
+ """
+ if not isinstance(data, basestring):
+ data = json.dumps(data or {})
+
+ request = RequestFactory().patch(path="dummy path", data=data, content_type="application/json")
+ request.user = self.staff_user
+ if cohort:
+ response = handler(request, unicode(course.id), cohort.id)
+ else:
+ response = handler(request, unicode(course.id))
+ self.assertEqual(response.status_code, expected_response_code)
+ return json.loads(response.content)
+
class CourseCohortSettingsHandlerTestCase(CohortViewsTestCase):
"""
Tests the `course_cohort_settings_handler` view.
"""
+
+ def get_expected_response(self):
+ """
+ Returns the static response dict.
+ """
+ return {
+ 'is_cohorted': True,
+ 'always_cohort_inline_discussions': True,
+ 'cohorted_inline_discussions': [],
+ 'cohorted_course_wide_discussions': [],
+ 'id': 1
+ }
+
def test_non_staff(self):
"""
Verify that we cannot access course_cohort_settings_handler if we're a non-staff user.
"""
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, "PUT", [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_discussions = ['Topic A', 'Topic B']
- config_course_cohorts(self.course, is_cohorted=True, cohorted_discussions=cohorted_discussions)
+ cohorted_inline_discussions, cohorted_course_wide_discussions = self.create_cohorted_discussions()
response = self.get_handler(self.course, handler=course_cohort_settings_handler)
- response['cohorted_discussions'].sort()
+ expected_response = self.get_expected_response()
- expected_response = {
- 'is_cohorted': True,
- 'always_cohort_inline_discussions': True,
- 'cohorted_discussions': [topic_name_to_id(self.course, name) for name in cohorted_discussions],
- 'id': 1
- }
- expected_response['cohorted_discussions'].sort()
+ 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_settings(self):
+ def test_update_is_cohorted_settings(self):
"""
- Verify that course_cohort_settings_handler is working for HTTP POST.
+ Verify that course_cohort_settings_handler is working for is_cohorted via HTTP PATCH.
"""
config_course_cohorts(self.course, is_cohorted=True)
response = self.get_handler(self.course, handler=course_cohort_settings_handler)
- expected_response = {
- 'is_cohorted': True,
- 'always_cohort_inline_discussions': True,
- 'cohorted_discussions': [],
- 'id': 1
- }
+ expected_response = self.get_expected_response()
+
self.assertEqual(response, expected_response)
expected_response['is_cohorted'] = False
- response = self.put_handler(self.course, data=expected_response, handler=course_cohort_settings_handler)
+ response = self.patch_handler(self.course, data=expected_response, handler=course_cohort_settings_handler)
+
+ 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'] = False
+ 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)
@@ -180,7 +296,7 @@ class CourseCohortSettingsHandlerTestCase(CohortViewsTestCase):
"""
config_course_cohorts(self.course, is_cohorted=True)
- response = self.put_handler(self.course, expected_response_code=400, handler=course_cohort_settings_handler)
+ response = self.patch_handler(self.course, expected_response_code=400, handler=course_cohort_settings_handler)
self.assertEqual("Bad Request", response.get("error"))
def test_update_settings_with_invalid_field_data_type(self):
@@ -189,7 +305,7 @@ class CourseCohortSettingsHandlerTestCase(CohortViewsTestCase):
"""
config_course_cohorts(self.course, is_cohorted=True)
- response = self.put_handler(
+ response = self.patch_handler(
self.course,
data={'is_cohorted': ''},
expected_response_code=400,
@@ -1075,3 +1191,58 @@ class RemoveUserFromCohortTestCase(CohortViewsTestCase):
cohort = CohortFactory(course_id=self.course.id, users=[user])
response_dict = self.request_remove_user_from_cohort(user.username, cohort)
self.verify_removed_user_from_cohort(user.username, response_dict, cohort)
+
+
+class CourseCohortDiscussionTopicsTestCase(CohortViewsTestCase):
+ """
+ Tests the `cohort_discussion_topics` view.
+ """
+
+ def test_non_staff(self):
+ """
+ Verify that we cannot access cohort_discussion_topics if we're a non-staff user.
+ """
+ self._verify_non_staff_cannot_access(cohort_discussion_topics, "GET", [unicode(self.course.id)])
+
+ def test_get_discussion_topics(self):
+ """
+ Verify that course_cohort_settings_handler is working for HTTP GET.
+ """
+ # create inline & course-wide discussion to verify the different map.
+ self.create_cohorted_discussions()
+
+ response = self.get_handler(self.course, handler=cohort_discussion_topics)
+ start_date = response['inline_discussions']['subcategories']['Chapter']['start_date']
+ expected_response = {
+ "course_wide_discussions": {
+ 'children': ['Topic B'],
+ 'entries': {
+ 'Topic B': {
+ 'sort_key': 'A',
+ 'is_cohorted': True,
+ 'id': topic_name_to_id(self.course, "Topic B"),
+ 'start_date': response['course_wide_discussions']['entries']['Topic B']['start_date']
+ }
+ }
+ },
+ "inline_discussions": {
+ 'subcategories': {
+ 'Chapter': {
+ 'subcategories': {},
+ 'children': ['Discussion'],
+ 'entries': {
+ 'Discussion': {
+ 'sort_key': None,
+ 'is_cohorted': True,
+ 'id': topic_name_to_id(self.course, "Topic A"),
+ 'start_date': start_date
+ }
+ },
+ 'sort_key': 'Chapter',
+ 'start_date': start_date
+ }
+ },
+ 'children': ['Chapter']
+ }
+ }
+ self.assertEqual(response, expected_response)
diff --git a/openedx/core/djangoapps/course_groups/views.py b/openedx/core/djangoapps/course_groups/views.py
index e3c89f5345..f49e278e4e 100644
--- a/openedx/core/djangoapps/course_groups/views.py
+++ b/openedx/core/djangoapps/course_groups/views.py
@@ -22,6 +22,7 @@ from courseware.courses import get_course_with_access
from edxmako.shortcuts import render_to_response
from . import cohorts
+from lms.djangoapps.django_comment_client.utils import get_discussion_category_map, get_discussion_categories_ids
from .models import CourseUserGroup, CourseUserGroupPartitionGroup
log = logging.getLogger(__name__)
@@ -61,14 +62,19 @@ def unlink_cohort_partition_group(cohort):
# pylint: disable=invalid-name
-def _get_course_cohort_settings_representation(course_cohort_settings):
+def _get_course_cohort_settings_representation(course, course_cohort_settings):
"""
Returns a JSON representation of a course cohort settings.
"""
+ cohorted_course_wide_discussions, cohorted_inline_discussions = get_cohorted_discussions(
+ course, course_cohort_settings
+ )
+
return {
'id': course_cohort_settings.id,
'is_cohorted': course_cohort_settings.is_cohorted,
- 'cohorted_discussions': course_cohort_settings.cohorted_discussions,
+ 'cohorted_inline_discussions': cohorted_inline_discussions,
+ 'cohorted_course_wide_discussions': cohorted_course_wide_discussions,
'always_cohort_inline_discussions': course_cohort_settings.always_cohort_inline_discussions,
}
@@ -89,7 +95,26 @@ def _get_cohort_representation(cohort, course):
}
-@require_http_methods(("GET", "PUT", "POST"))
+def get_cohorted_discussions(course, course_settings):
+ """
+ Returns the course-wide and inline cohorted discussion ids separately.
+ """
+ cohorted_course_wide_discussions = []
+ cohorted_inline_discussions = []
+
+ course_wide_discussions = [topic['id'] for __, topic in course.discussion_topics.items()]
+ all_discussions = get_discussion_categories_ids(course, include_all=True)
+
+ for cohorted_discussion_id in course_settings.cohorted_discussions:
+ if cohorted_discussion_id in course_wide_discussions:
+ cohorted_course_wide_discussions.append(cohorted_discussion_id)
+ elif cohorted_discussion_id in all_discussions:
+ cohorted_inline_discussions.append(cohorted_discussion_id)
+
+ return cohorted_course_wide_discussions, cohorted_inline_discussions
+
+
+@require_http_methods(("GET", "PATCH"))
@ensure_csrf_cookie
@expect_json
@login_required
@@ -99,27 +124,49 @@ def course_cohort_settings_handler(request, course_key_string):
This will raise 404 if user is not staff.
GET
Returns the JSON representation of cohort settings for the course.
- PUT or POST
+ PATCH
Updates the cohort settings for the course. Returns the JSON representation of updated settings.
"""
course_key = CourseKey.from_string(course_key_string)
- get_course_with_access(request.user, 'staff', course_key)
- if request.method == 'GET':
- cohort_settings = cohorts.get_course_cohort_settings(course_key)
- return JsonResponse(_get_course_cohort_settings_representation(cohort_settings))
- else:
- is_cohorted = request.json.get('is_cohorted')
- if is_cohorted is None:
- # Note: error message not translated because it is not exposed to the user (UI prevents this state).
- return JsonResponse({"error": "Bad Request"}, 400)
+ course = get_course_with_access(request.user, 'staff', course_key)
+ cohort_settings = cohorts.get_course_cohort_settings(course_key)
+
+ if request.method == 'PATCH':
+ cohorted_course_wide_discussions, cohorted_inline_discussions = get_cohorted_discussions(
+ course, cohort_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:
+ cohorted_course_wide_discussions = request.json.get(
+ 'cohorted_course_wide_discussions', cohorted_course_wide_discussions
+ )
+ cohorted_inline_discussions = request.json.get(
+ 'cohorted_inline_discussions', cohorted_inline_discussions
+ )
+ settings_to_change['cohorted_discussions'] = cohorted_course_wide_discussions + cohorted_inline_discussions
+
+ if 'always_cohort_inline_discussions' in request.json:
+ settings_to_change['always_cohort_inline_discussions'] = request.json.get(
+ 'always_cohort_inline_discussions'
+ )
+
+ if not settings_to_change:
+ return JsonResponse({"error": unicode("Bad Request")}, 400)
try:
- cohort_settings = cohorts.set_course_cohort_settings(course_key, is_cohorted=is_cohorted)
+ cohort_settings = cohorts.set_course_cohort_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_cohort_settings_representation(cohort_settings))
+ return JsonResponse(_get_course_cohort_settings_representation(course, cohort_settings))
@require_http_methods(("GET", "PUT", "POST", "PATCH"))
@@ -362,3 +409,79 @@ def debug_cohort_mgmt(request, course_key_string):
kwargs={'course_key': course_key.to_deprecated_string()}
)}
return render_to_response('/course_groups/debug.html', context)
+
+
+@expect_json
+@login_required
+def cohort_discussion_topics(request, course_key_string):
+ """
+ The handler for cohort 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.
+
+ Example:
+ >>> example = {
+ >>> "course_wide_discussions": {
+ >>> "entries": {
+ >>> "General": {
+ >>> "sort_key": "General",
+ >>> "is_cohorted": True,
+ >>> "id": "i4x-edx-eiorguegnru-course-foobarbaz"
+ >>> }
+ >>> }
+ >>> "children": ["General"]
+ >>> },
+ >>> "inline_discussions" : {
+ >>> "subcategories": {
+ >>> "Getting Started": {
+ >>> "subcategories": {},
+ >>> "children": [
+ >>> "Working with Videos",
+ >>> "Videos on edX"
+ >>> ],
+ >>> "entries": {
+ >>> "Working with Videos": {
+ >>> "sort_key": None,
+ >>> "is_cohorted": False,
+ >>> "id": "d9f970a42067413cbb633f81cfb12604"
+ >>> },
+ >>> "Videos on edX": {
+ >>> "sort_key": None,
+ >>> "is_cohorted": False,
+ >>> "id": "98d8feb5971041a085512ae22b398613"
+ >>> }
+ >>> }
+ >>> },
+ >>> "children": ["Getting Started"]
+ >>> },
+ >>> }
+ >>> }
+ """
+ course_key = CourseKey.from_string(course_key_string)
+ course = get_course_with_access(request.user, 'staff', course_key)
+
+ discussion_topics = {}
+ discussion_category_map = get_discussion_category_map(course, cohorted_if_in_list=True, exclude_unstarted=False)
+
+ # We extract the data for the course wide discussions from the category map.
+ course_wide_entries = discussion_category_map.pop('entries')
+
+ course_wide_children = []
+ inline_children = []
+
+ for name in discussion_category_map['children']:
+ if name in course_wide_entries:
+ course_wide_children.append(name)
+ else:
+ inline_children.append(name)
+
+ discussion_topics['course_wide_discussions'] = {
+ 'entries': course_wide_entries,
+ 'children': course_wide_children
+ }
+
+ discussion_category_map['children'] = inline_children
+ discussion_topics['inline_discussions'] = discussion_category_map
+
+ return JsonResponse(discussion_topics)
From c50f8281875cd1bdce0f2737e026f8f5ba8da607 Mon Sep 17 00:00:00 2001
From: Usman Khalid <2200617@gmail.com>
Date: Mon, 23 Mar 2015 16:36:48 +0500
Subject: [PATCH 11/11] Fixes after rebase.
---
.../django_comment_client/forum/tests.py | 25 ++++++++++---------
.../django_comment_client/tests/test_utils.py | 6 +++--
...test_remove_users_from_multiple_cohorts.py | 4 +--
.../course_groups/tests/test_views.py | 4 +++
.../core/djangoapps/course_groups/views.py | 6 +++--
5 files changed, 27 insertions(+), 18 deletions(-)
diff --git a/lms/djangoapps/django_comment_client/forum/tests.py b/lms/djangoapps/django_comment_client/forum/tests.py
index ecd30c2145..bbeb7dba09 100644
--- a/lms/djangoapps/django_comment_client/forum/tests.py
+++ b/lms/djangoapps/django_comment_client/forum/tests.py
@@ -316,11 +316,11 @@ class SingleThreadQueryCountTestCase(ModuleStoreTestCase):
@ddt.data(
# old mongo with cache: 15
- (ModuleStoreEnum.Type.mongo, 1, 21, 15, 30),
- (ModuleStoreEnum.Type.mongo, 50, 315, 15, 30),
+ (ModuleStoreEnum.Type.mongo, 1, 21, 15, 40, 27),
+ (ModuleStoreEnum.Type.mongo, 50, 315, 15, 628, 27),
# split mongo: 3 queries, regardless of thread response size.
- (ModuleStoreEnum.Type.split, 1, 3, 3, 30),
- (ModuleStoreEnum.Type.split, 50, 3, 3, 30),
+ (ModuleStoreEnum.Type.split, 1, 3, 3, 40, 27),
+ (ModuleStoreEnum.Type.split, 50, 3, 3, 628, 27),
)
@ddt.unpack
def test_number_of_mongo_queries(
@@ -329,7 +329,8 @@ class SingleThreadQueryCountTestCase(ModuleStoreTestCase):
num_thread_responses,
num_uncached_mongo_calls,
num_cached_mongo_calls,
- num_sql_queries,
+ num_uncached_sql_queries,
+ num_cached_sql_queries,
mock_request
):
with modulestore().default_store(default_store):
@@ -371,15 +372,15 @@ class SingleThreadQueryCountTestCase(ModuleStoreTestCase):
backend='django.core.cache.backends.dummy.DummyCache',
LOCATION='single_thread_local_cache'
)
- cached_calls = {
- single_thread_dummy_cache: num_uncached_mongo_calls,
- single_thread_local_cache: num_cached_mongo_calls
- }
- for single_thread_cache, expected_calls in cached_calls.items():
+ cached_calls = [
+ [single_thread_dummy_cache, num_uncached_mongo_calls, num_uncached_sql_queries],
+ [single_thread_local_cache, num_cached_mongo_calls, num_cached_sql_queries]
+ ]
+ for single_thread_cache, expected_mongo_calls, expected_sql_queries in cached_calls:
single_thread_cache.clear()
with patch("django_comment_client.permissions.CACHE", single_thread_cache):
- with self.assertNumQueries(num_sql_queries):
- with check_mongo_calls(expected_calls):
+ with self.assertNumQueries(expected_sql_queries):
+ with check_mongo_calls(expected_mongo_calls):
call_single_thread()
single_thread_cache.clear()
diff --git a/lms/djangoapps/django_comment_client/tests/test_utils.py b/lms/djangoapps/django_comment_client/tests/test_utils.py
index b67a0ad2d7..81cff7d477 100644
--- a/lms/djangoapps/django_comment_client/tests/test_utils.py
+++ b/lms/djangoapps/django_comment_client/tests/test_utils.py
@@ -17,6 +17,7 @@ from django_comment_client.tests.unicode import UnicodeTestMixin
from django_comment_client.tests.utils import ContentGroupTestCase
import django_comment_client.utils as utils
+from courseware.tests.factories import InstructorFactory
from openedx.core.djangoapps.course_groups.cohorts import set_course_cohort_settings
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
@@ -186,6 +187,7 @@ class CategoryMapTestCase(CategoryMapTestMixin, ModuleStoreTestCase):
self.course.discussion_topics = {}
self.course.save()
self.discussion_num = 0
+ self.instructor = InstructorFactory(course_key=self.course.id)
self.maxDiff = None # pylint: disable=invalid-name
def create_discussion(self, discussion_category, discussion_target, **kwargs):
@@ -199,12 +201,12 @@ class CategoryMapTestCase(CategoryMapTestMixin, ModuleStoreTestCase):
**kwargs
)
- def assert_category_map_equals(self, expected, cohorted_if_in_list=False, exclude_unstarted=True):
+ def assert_category_map_equals(self, expected, cohorted_if_in_list=False, exclude_unstarted=True): # pylint: disable=arguments-differ
"""
Asserts the expected map with the map returned by get_discussion_category_map method.
"""
self.assertEqual(
- utils.get_discussion_category_map(self.course, cohorted_if_in_list, exclude_unstarted),
+ utils.get_discussion_category_map(self.course, self.instructor, cohorted_if_in_list, exclude_unstarted),
expected
)
diff --git a/openedx/core/djangoapps/course_groups/management/commands/tests/test_remove_users_from_multiple_cohorts.py b/openedx/core/djangoapps/course_groups/management/commands/tests/test_remove_users_from_multiple_cohorts.py
index afd233b046..1546dee667 100644
--- a/openedx/core/djangoapps/course_groups/management/commands/tests/test_remove_users_from_multiple_cohorts.py
+++ b/openedx/core/djangoapps/course_groups/management/commands/tests/test_remove_users_from_multiple_cohorts.py
@@ -36,10 +36,10 @@ class TestMultipleCohortUsers(ModuleStoreTestCase):
"""
# set two auto_cohort_groups for both courses
config_course_cohorts(
- self.course1, [], cohorted=True, auto_cohort_groups=["Course1AutoGroup1", "Course1AutoGroup2"]
+ self.course1, is_cohorted=True, auto_cohorts=["Course1AutoGroup1", "Course1AutoGroup2"]
)
config_course_cohorts(
- self.course2, [], cohorted=True, auto_cohort_groups=["Course2AutoGroup1", "Course2AutoGroup2"]
+ self.course2, is_cohorted=True, auto_cohorts=["Course2AutoGroup1", "Course2AutoGroup2"]
)
# get the cohorts from the courses, which will cause auto cohorts to be created
diff --git a/openedx/core/djangoapps/course_groups/tests/test_views.py b/openedx/core/djangoapps/course_groups/tests/test_views.py
index 900ce98889..886e9eb454 100644
--- a/openedx/core/djangoapps/course_groups/tests/test_views.py
+++ b/openedx/core/djangoapps/course_groups/tests/test_views.py
@@ -7,6 +7,9 @@ import json
from collections import namedtuple
from datetime import datetime
+from unittest import skipUnless
+
+from django.conf import settings
from django.contrib.auth.models import User
from django.http import Http404
from django.test.client import RequestFactory
@@ -1193,6 +1196,7 @@ class RemoveUserFromCohortTestCase(CohortViewsTestCase):
self.verify_removed_user_from_cohort(user.username, response_dict, cohort)
+@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Tests only valid in LMS')
class CourseCohortDiscussionTopicsTestCase(CohortViewsTestCase):
"""
Tests the `cohort_discussion_topics` view.
diff --git a/openedx/core/djangoapps/course_groups/views.py b/openedx/core/djangoapps/course_groups/views.py
index f49e278e4e..699002942f 100644
--- a/openedx/core/djangoapps/course_groups/views.py
+++ b/openedx/core/djangoapps/course_groups/views.py
@@ -103,7 +103,7 @@ def get_cohorted_discussions(course, course_settings):
cohorted_inline_discussions = []
course_wide_discussions = [topic['id'] for __, topic in course.discussion_topics.items()]
- all_discussions = get_discussion_categories_ids(course, include_all=True)
+ all_discussions = get_discussion_categories_ids(course, None, include_all=True)
for cohorted_discussion_id in course_settings.cohorted_discussions:
if cohorted_discussion_id in course_wide_discussions:
@@ -462,7 +462,9 @@ def cohort_discussion_topics(request, course_key_string):
course = get_course_with_access(request.user, 'staff', course_key)
discussion_topics = {}
- discussion_category_map = get_discussion_category_map(course, cohorted_if_in_list=True, exclude_unstarted=False)
+ discussion_category_map = get_discussion_category_map(
+ course, request.user, cohorted_if_in_list=True, exclude_unstarted=False
+ )
# We extract the data for the course wide discussions from the category map.
course_wide_entries = discussion_category_map.pop('entries')