Commit e6c75529 by muzaffaryousaf Committed by Usman Khalid

Cohort discussion topics via UI in instructor dashboard.

TNL-1256
parent e07f45b5
"""
This file includes the monkey-patch for requests' PATCH method, as we are using
older version of django that does not contains the PATCH method in its test client.
"""
from __future__ import unicode_literals
from urlparse import urlparse
from django.test.client import RequestFactory, Client, FakePayload
BOUNDARY = 'BoUnDaRyStRiNg'
MULTIPART_CONTENT = 'multipart/form-data; boundary=%s' % BOUNDARY
def request_factory_patch(self, path, data={}, content_type=MULTIPART_CONTENT, **extra):
"""
Construct a PATCH request.
"""
patch_data = self._encode_data(data, content_type)
parsed = urlparse(path)
r = {
'CONTENT_LENGTH': len(patch_data),
'CONTENT_TYPE': content_type,
'PATH_INFO': self._get_path(parsed),
'QUERY_STRING': parsed[4],
'REQUEST_METHOD': 'PATCH',
'wsgi.input': FakePayload(patch_data),
}
r.update(extra)
return self.request(**r)
def client_patch(self, path, data={}, content_type=MULTIPART_CONTENT, follow=False, **extra):
"""
Send a resource to the server using PATCH.
"""
response = super(Client, self).patch(path, data=data, content_type=content_type, **extra)
if follow:
response = self._handle_redirects(response, **extra)
return response
if not hasattr(RequestFactory, 'patch'):
setattr(RequestFactory, 'patch', request_factory_patch)
if not hasattr(Client, 'patch'):
setattr(Client, 'patch', client_patch)
......@@ -105,6 +105,10 @@ class CohortManagementSection(PageObject):
no_content_group_button_css = '.cohort-management-details-association-course input.radio-no'
select_content_group_button_css = '.cohort-management-details-association-course input.radio-yes'
assignment_type_buttons_css = '.cohort-management-assignment-type-settings input'
discussion_form_selectors = {
'course-wide': '.cohort-course-wide-discussions-form',
'inline': '.cohort-inline-discussions-form'
}
def is_browser_on_page(self):
return self.q(css='.cohort-management').present
......@@ -466,6 +470,123 @@ class CohortManagementSection(PageObject):
if state != self.is_cohorted:
self.q(css=self._bounded_selector('.cohorts-state')).first.click()
def toggles_showing_of_discussion_topics(self):
"""
Shows the discussion topics.
"""
EmptyPromise(
lambda: self.q(css=self._bounded_selector('.toggle-cohort-management-discussions')).results != 0,
"Waiting for discussion section to show"
).fulfill()
# If the discussion topic section has not yet been toggled on, click on the toggle link.
self.q(css=self._bounded_selector(".toggle-cohort-management-discussions")).click()
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)
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 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()
def always_inline_discussion_selected(self):
"""
Returns the checked always_cohort_inline_discussions radio button.
"""
return self.q(css=self._bounded_selector(".check-all-inline-discussions:checked"))
def cohort_some_inline_discussion_selected(self):
"""
Returns the checked some_cohort_inline_discussions radio button.
"""
return self.q(css=self._bounded_selector(".check-cohort-inline-discussions:checked"))
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)
def is_save_button_disabled(self, key):
"""
Returns the status for form's save button, enabled or disabled.
"""
save_button_css = '%s %s' % (self.discussion_form_selectors[key], '.action-save')
disabled = self.q(css=self._bounded_selector(save_button_css)).attrs('disabled')
return disabled[0] == 'true'
def is_category_selected(self):
"""
Returns the status for category checkboxes.
"""
return self.q(css=self._bounded_selector('.check-discussion-category:checked')).is_present()
def get_cohorted_topics_count(self, key):
"""
Returns the count for cohorted topics.
"""
cohorted_topics = self.q(css=self._bounded_selector('.check-discussion-subcategory-%s:checked' % key))
return len(cohorted_topics.results)
def save_discussion_topics(self, key):
"""
Saves the discussion topics.
"""
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"):
"""
Returns the message related to modifying discussion topics.
"""
title_css = "%s .message-%s .message-title" % (self.discussion_form_selectors[key], msg_type)
EmptyPromise(
lambda: self.q(css=self._bounded_selector(title_css)),
"Waiting for message to appear"
).fulfill()
message_title = self.q(css=self._bounded_selector(title_css))
if len(message_title.results) == 0:
return ''
return message_title.first.text[0]
def cohort_discussion_heading_is_visible(self, key):
"""
Returns the visibility of discussion topic headings.
"""
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).
......
......@@ -62,10 +62,9 @@ class CohortTestMixin(object):
"""
url = LMS_BASE_URL + "/courses/" + course_fixture._course_key + '/cohorts/settings' # pylint: disable=protected-access
data = json.dumps({'is_cohorted': False})
response = course_fixture.session.post(url, data=data, headers=course_fixture.headers)
response = course_fixture.session.patch(url, data=data, headers=course_fixture.headers)
self.assertTrue(response.ok, "Failed to disable cohorts")
def add_manual_cohort(self, course_fixture, cohort_name):
"""
Adds a cohort by name, returning its ID.
......
......@@ -2,18 +2,17 @@
End-to-end test for cohorted courseware. This uses both Studio and LMS.
"""
from nose.plugins.attrib import attr
import json
from nose.plugins.attrib import attr
from studio.base_studio_test import ContainerBase
from ..pages.studio.settings_group_configurations import GroupConfigurationsPage
from ..pages.studio.settings_advanced import AdvancedSettingsPage
from ..pages.studio.auto_auth import AutoAuthPage as StudioAutoAuthPage
from ..fixtures.course import XBlockFixtureDesc
from ..fixtures import LMS_BASE_URL
from ..pages.studio.component_editor import ComponentVisibilityEditorView
from ..pages.lms.instructor_dashboard import InstructorDashboardPage
from ..pages.lms.course_nav import CourseNavPage
from ..pages.lms.courseware import CoursewarePage
from ..pages.lms.auto_auth import AutoAuthPage as LmsAutoAuthPage
from ..tests.lms.test_lms_user_preview import verify_expected_problem_visibility
......@@ -80,28 +79,14 @@ class EndToEndCohortedCoursewareTest(ContainerBase):
)
)
def enable_cohorts_in_course(self):
def enable_cohorting(self, course_fixture):
"""
This turns on cohorts for the course. Currently this is still done through Advanced
Settings. Eventually it will be done in the LMS Instructor Dashboard.
Enables cohorting for the current course.
"""
advanced_settings = AdvancedSettingsPage(
self.browser,
self.course_info['org'],
self.course_info['number'],
self.course_info['run']
)
advanced_settings.visit()
cohort_config = '{"cohorted": true}'
advanced_settings.set('Cohort Configuration', cohort_config)
advanced_settings.refresh_and_wait_for_load()
self.assertEquals(
json.loads(cohort_config),
json.loads(advanced_settings.get('Cohort Configuration')),
'Wrong input for Cohort Configuration'
)
url = LMS_BASE_URL + "/courses/" + course_fixture._course_key + '/cohorts/settings' # pylint: disable=protected-access
data = json.dumps({'is_cohorted': True})
response = course_fixture.session.patch(url, data=data, headers=course_fixture.headers)
self.assertTrue(response.ok, "Failed to enable cohorts")
def create_content_groups(self):
"""
......@@ -219,7 +204,7 @@ class EndToEndCohortedCoursewareTest(ContainerBase):
And the student in Cohort B can see all the problems except the one linked to Content Group A
And the student in the default cohort can ony see the problem that is unlinked to any Content Group
"""
self.enable_cohorts_in_course()
self.enable_cohorting(self.course_fixture)
self.create_content_groups()
self.link_problems_to_content_groups_and_publish()
self.create_cohorts_and_assign_students()
......
# -*- coding: utf-8 -*-
from datetime import datetime
import datetime
import json
import mock
from pytz import UTC
from django.utils.timezone import UTC as django_utc
from django_comment_client.tests.factories import RoleFactory
from django_comment_client.tests.unicode import UnicodeTestMixin
......@@ -179,7 +180,7 @@ class CategoryMapTestCase(CategoryMapTestMixin, ModuleStoreTestCase):
# This test needs to use a course that has already started --
# discussion topics only show up if the course has already started,
# and the default start date for courses is Jan 1, 2030.
start=datetime(2012, 2, 3, tzinfo=UTC)
start=datetime.datetime(2012, 2, 3, tzinfo=UTC)
)
# Courses get a default discussion topic on creation, so remove it
self.course.discussion_topics = {}
......@@ -198,6 +199,15 @@ class CategoryMapTestCase(CategoryMapTestMixin, ModuleStoreTestCase):
**kwargs
)
def assert_category_map_equals(self, expected, cohorted_if_in_list=False, exclude_unstarted=True):
"""
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),
expected
)
def test_empty(self):
self.assert_category_map_equals({"entries": {}, "subcategories": {}, "children": []})
......@@ -276,6 +286,85 @@ class CategoryMapTestCase(CategoryMapTestMixin, ModuleStoreTestCase):
}
)
def test_inline_with_always_cohort_inline_discussion_flag(self):
self.create_discussion("Chapter", "Discussion")
set_course_cohort_settings(course_key=self.course.id, is_cohorted=True)
self.assert_category_map_equals(
{
"entries": {},
"subcategories": {
"Chapter": {
"entries": {
"Discussion": {
"id": "discussion1",
"sort_key": None,
"is_cohorted": True,
}
},
"subcategories": {},
"children": ["Discussion"]
}
},
"children": ["Chapter"]
}
)
def test_inline_without_always_cohort_inline_discussion_flag(self):
self.create_discussion("Chapter", "Discussion")
set_course_cohort_settings(course_key=self.course.id, is_cohorted=True, always_cohort_inline_discussions=False)
self.assert_category_map_equals(
{
"entries": {},
"subcategories": {
"Chapter": {
"entries": {
"Discussion": {
"id": "discussion1",
"sort_key": None,
"is_cohorted": False,
}
},
"subcategories": {},
"children": ["Discussion"]
}
},
"children": ["Chapter"]
},
cohorted_if_in_list=True
)
def test_get_unstarted_discussion_modules(self):
later = datetime.datetime(datetime.MAXYEAR, 1, 1, tzinfo=django_utc())
self.create_discussion("Chapter 1", "Discussion 1", start=later)
self.assert_category_map_equals(
{
"entries": {},
"subcategories": {
"Chapter 1": {
"entries": {
"Discussion 1": {
"id": "discussion1",
"sort_key": None,
"is_cohorted": False,
"start_date": later
}
},
"subcategories": {},
"children": ["Discussion 1"],
"start_date": later,
"sort_key": "Chapter 1"
}
},
"children": ["Chapter 1"]
},
cohorted_if_in_list=True,
exclude_unstarted=False
)
def test_tree(self):
self.create_discussion("Chapter 1", "Discussion 1")
self.create_discussion("Chapter 1", "Discussion 2")
......@@ -401,8 +490,8 @@ class CategoryMapTestCase(CategoryMapTestMixin, ModuleStoreTestCase):
self.assertEqual(set(subsection1["entries"].keys()), subsection1_discussions)
def test_start_date_filter(self):
now = datetime.now()
later = datetime.max
now = datetime.datetime.now()
later = datetime.datetime.max
self.create_discussion("Chapter 1", "Discussion 1", start=now)
self.create_discussion("Chapter 1", "Discussion 2 обсуждение", start=later)
self.create_discussion("Chapter 2", "Discussion", start=now)
......@@ -440,6 +529,7 @@ class CategoryMapTestCase(CategoryMapTestMixin, ModuleStoreTestCase):
"children": ["Chapter 1", "Chapter 2"]
}
)
self.maxDiff = None
def test_sort_inline_explicit(self):
self.create_discussion("Chapter", "Discussion 1", sort_key="D")
......
......@@ -61,8 +61,7 @@ def has_forum_access(uname, course_id, rolename):
return role.users.filter(username=uname).exists()
# pylint: disable=invalid-name
def get_accessible_discussion_modules(course, user):
def get_accessible_discussion_modules(course, user, include_all=False): # pylint: disable=invalid-name
"""
Return a list of all valid discussion modules in this course that
are accessible to the given user.
......@@ -71,14 +70,14 @@ def get_accessible_discussion_modules(course, user):
def has_required_keys(module):
for key in ('discussion_id', 'discussion_category', 'discussion_target'):
if getattr(module, key) is None:
if getattr(module, key, None) is None:
log.warning("Required key '%s' not in discussion %s, leaving out of category map" % (key, module.location))
return False
return True
return [
module for module in all_modules
if has_required_keys(module) and has_access(user, 'load', module, course.id)
if has_required_keys(module) and (include_all or has_access(user, 'load', module, course.id))
]
......@@ -146,10 +145,50 @@ def _sort_map_entries(category_map, sort_alpha):
category_map["children"] = [x[0] for x in sorted(things, key=lambda x: x[1]["sort_key"])]
def get_discussion_category_map(course, user):
def get_discussion_category_map(course, user, cohorted_if_in_list=False, exclude_unstarted=True):
"""
Transform the list of this course's discussion modules into a recursive dictionary structure. This is used
to render the discussion category map in the discussion tab sidebar for a given user.
Args:
course: Course for which to get the ids.
user: User to check for access.
cohorted_if_in_list (bool): If True, inline topics are marked is_cohorted only if they are
in course_cohort_settings.discussion_topics.
Example:
>>> example = {
>>> "entries": {
>>> "General": {
>>> "sort_key": "General",
>>> "is_cohorted": True,
>>> "id": "i4x-edx-eiorguegnru-course-foobarbaz"
>>> }
>>> },
>>> "children": ["General", "Getting Started"],
>>> "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"
>>> }
>>> }
>>> }
>>> }
>>> }
"""
unexpanded_category_map = defaultdict(list)
......@@ -162,7 +201,7 @@ def get_discussion_category_map(course, user):
title = module.discussion_target
sort_key = module.sort_key
category = " / ".join([x.strip() for x in module.discussion_category.split("/")])
#Handle case where module.start is None
# Handle case where module.start is None
entry_start_date = module.start if module.start else datetime.max.replace(tzinfo=pytz.UTC)
unexpanded_category_map[category].append({"title": title, "id": id, "sort_key": sort_key, "start_date": entry_start_date})
......@@ -198,8 +237,17 @@ def get_discussion_category_map(course, user):
if node[level]["start_date"] > category_start_date:
node[level]["start_date"] = category_start_date
always_cohort_inline_discussions = ( # pylint: disable=invalid-name
not cohorted_if_in_list and course_cohort_settings.always_cohort_inline_discussions
)
dupe_counters = defaultdict(lambda: 0) # counts the number of times we see each title
for entry in entries:
is_entry_cohorted = (
course_cohort_settings.is_cohorted and (
always_cohort_inline_discussions or entry["id"] in course_cohort_settings.cohorted_discussions
)
)
title = entry["title"]
if node[level]["entries"][title]:
# If we've already seen this title, append an incrementing number to disambiguate
......@@ -209,7 +257,7 @@ def get_discussion_category_map(course, user):
node[level]["entries"][title] = {"id": entry["id"],
"sort_key": entry["sort_key"],
"start_date": entry["start_date"],
"is_cohorted": course_cohort_settings.is_cohorted}
"is_cohorted": is_entry_cohorted}
# TODO. BUG! : course location is not unique across multiple course runs!
# (I think Kevin already noticed this) Need to send course_id with requests, store it
......@@ -225,16 +273,22 @@ def get_discussion_category_map(course, user):
_sort_map_entries(category_map, course.discussion_sort_alpha)
return _filter_unstarted_categories(category_map)
return _filter_unstarted_categories(category_map) if exclude_unstarted else category_map
def get_discussion_categories_ids(course, user):
def get_discussion_categories_ids(course, user, include_all=False):
"""
Returns a list of available ids of categories for the course that
are accessible to the given user.
Args:
course: Course for which to get the ids.
user: User to check for access.
include_all (bool): If True, return all ids. Used by configuration views.
"""
accessible_discussion_ids = [
module.discussion_id for module in get_accessible_discussion_modules(course, user)
module.discussion_id for module in get_accessible_discussion_modules(course, user, include_all=include_all)
]
return course.top_level_discussion_topic_ids + accessible_discussion_ids
......
......@@ -345,8 +345,8 @@ def _section_cohort_management(course, access):
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)}),
'discussion_topics_url': reverse('cohort_discussion_topics', kwargs={'course_key_string': unicode(course_key)}),
}
return section_data
......
var edx = edx || {};
(function(Backbone) {
'use strict';
edx.groups = edx.groups || {};
edx.groups.DiscussionTopicsSettingsModel = Backbone.Model.extend({
defaults: {
course_wide_discussions: {},
inline_discussions: {}
}
});
}).call(this, Backbone);
......@@ -9,7 +9,8 @@ var edx = edx || {};
idAttribute: 'id',
defaults: {
is_cohorted: false,
cohorted_discussions: [],
cohorted_inline_discussions: [],
cohorted_course_wide_discussions:[],
always_cohort_inline_discussions: true
}
});
......
var edx = edx || {};
(function ($, _, Backbone, gettext, interpolate_text, NotificationModel, NotificationView) {
'use strict';
edx.groups = edx.groups || {};
edx.groups.CohortDiscussionConfigurationView = Backbone.View.extend({
/**
* Add/Remove the disabled attribute on given element.
* @param {object} $element - The element to disable/enable.
* @param {bool} disable - The flag to add/remove 'disabled' attribute.
*/
setDisabled: function($element, disable) {
$element.prop('disabled', disable ? 'disabled' : false);
},
/**
* Returns the cohorted discussions list.
* @param {string} selector - To select the discussion elements whose ids to return.
* @returns {Array} - Cohorted discussions.
*/
getCohortedDiscussions: function(selector) {
var self=this,
cohortedDiscussions = [];
_.each(self.$(selector), function (topic) {
cohortedDiscussions.push($(topic).data('id'))
});
return cohortedDiscussions;
},
/**
* Save the cohortSettings' 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,
saveOperation = $.Deferred(),
showErrorMessage;
showErrorMessage = function (message, $element) {
self.showMessage(message, $element, 'error');
};
this.removeNotification();
cohortSettingsModel.save(
fieldData, {patch: true, wait: true}
).done(function () {
saveOperation.resolve();
}).fail(function (result) {
var errorMessage = null;
try {
var 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.");
}
showErrorMessage(errorMessage, $element);
saveOperation.reject();
});
return saveOperation.promise();
},
/**
* Shows the notification messages before given element using the NotificationModel.
* @param {string} message - Text message to show.
* @param {object} $element - Message would be shown before this element.
* @param {string} type - Type of message to show e.g. confirmation or error.
*/
showMessage: function (message, $element, type) {
var model = new NotificationModel({type: type || 'confirmation', title: message});
this.removeNotification();
this.notification = new NotificationView({
model: model
});
$element.before(this.notification.$el);
this.notification.render();
},
/**
*Removes the notification messages.
*/
removeNotification: function () {
if (this.notification) {
this.notification.remove();
}
}
});
}).call(this, $, _, Backbone, gettext, interpolate_text, NotificationModel, NotificationView
);
var edx = edx || {};
(function ($, _, Backbone, gettext, interpolate_text, CohortDiscussionConfigurationView) {
'use strict';
edx.groups = edx.groups || {};
edx.groups.CourseWideDiscussionsView = CohortDiscussionConfigurationView.extend({
events: {
'change .check-discussion-subcategory-course-wide': 'discussionCategoryStateChanged',
'click .cohort-course-wide-discussions-form .action-save': 'saveCourseWideDiscussionsForm'
},
initialize: function (options) {
this.template = _.template($('#cohort-discussions-course-wide-tpl').text());
this.cohortSettings = options.cohortSettings;
},
render: function () {
this.$('.cohort-course-wide-discussions-nav').html(this.template({
courseWideTopics: this.getCourseWideDiscussionsHtml(
this.model.get('course_wide_discussions')
)
}));
this.setDisabled(this.$('.cohort-course-wide-discussions-form .action-save'), true);
},
/**
* Returns the html list for course-wide discussion topics.
* @param {object} courseWideDiscussions - course-wide discussions object from server.
* @returns {Array} - HTML list for course-wide discussion topics.
*/
getCourseWideDiscussionsHtml: function (courseWideDiscussions) {
var subCategoryTemplate = _.template($('#cohort-discussions-subcategory-tpl').html()),
entries = courseWideDiscussions.entries,
children = courseWideDiscussions.children;
return _.map(children, function (name) {
var entry = entries[name];
return subCategoryTemplate({
name: name,
id: entry.id,
is_cohorted: entry.is_cohorted,
type: 'course-wide'
});
}).join('');
},
/**
* Enables the save button for course-wide discussions.
*/
discussionCategoryStateChanged: function(event) {
event.preventDefault();
this.setDisabled(this.$('.cohort-course-wide-discussions-form .action-save'), false);
},
/**
* Sends the cohorted_course_wide_discussions to the server and renders the view.
*/
saveCourseWideDiscussionsForm: function (event) {
event.preventDefault();
var self = this,
courseWideCohortedDiscussions = self.getCohortedDiscussions(
'.check-discussion-subcategory-course-wide:checked'
),
fieldData = { cohorted_course_wide_discussions: courseWideCohortedDiscussions };
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'));
}).fail(function() {
var errorMessage = gettext("We've encountered an error. Refresh your browser and then try again.");
self.showMessage(errorMessage, self.$('.course-wide-discussion-topics'), 'error')
});
});
}
});
}).call(this, $, _, Backbone, gettext, interpolate_text, edx.groups.CohortDiscussionConfigurationView);
var edx = edx || {};
(function ($, _, Backbone, gettext, interpolate_text, CohortDiscussionConfigurationView) {
'use strict';
edx.groups = edx.groups || {};
edx.groups.InlineDiscussionsView = CohortDiscussionConfigurationView.extend({
events: {
'change .check-discussion-category': 'setSaveButton',
'change .check-discussion-subcategory-inline': 'setSaveButton',
'click .cohort-inline-discussions-form .action-save': 'saveInlineDiscussionsForm',
'change .check-all-inline-discussions': 'setAllInlineDiscussions',
'change .check-cohort-inline-discussions': 'setSomeInlineDiscussions'
},
initialize: function (options) {
this.template = _.template($('#cohort-discussions-inline-tpl').text());
this.cohortSettings = options.cohortSettings;
},
render: function () {
var alwaysCohortInlineDiscussions = this.cohortSettings.get('always_cohort_inline_discussions');
this.$('.cohort-inline-discussions-nav').html(this.template({
inlineDiscussionTopics: this.getInlineDiscussionsHtml(this.model.get('inline_discussions')),
alwaysCohortInlineDiscussions:alwaysCohortInlineDiscussions
}));
// Provides the semantics for a nested list of tri-state checkboxes.
// When attached to a jQuery element it listens for change events to
// input[type=checkbox] elements, and updates the checked and indeterminate
// based on the checked values of any checkboxes in child elements of the DOM.
this.$('ul.inline-topics').qubit();
this.setElementsEnabled(alwaysCohortInlineDiscussions, true);
},
/**
* Generate html list for inline discussion topics.
* @params {object} inlineDiscussions - inline discussions object from server.
* @returns {Array} - HTML for inline discussion topics.
*/
getInlineDiscussionsHtml: function (inlineDiscussions) {
var categoryTemplate = _.template($('#cohort-discussions-category-tpl').html()),
entryTemplate = _.template($('#cohort-discussions-subcategory-tpl').html()),
isCategoryCohorted = false,
children = inlineDiscussions.children,
entries = inlineDiscussions.entries,
subcategories = inlineDiscussions.subcategories;
return _.map(children, function (name) {
var html = '', entry;
if (entries && _.has(entries, name)) {
entry = entries[name];
html = entryTemplate({
name: name,
id: entry.id,
is_cohorted: entry.is_cohorted,
type: 'inline'
});
} else { // subcategory
html = categoryTemplate({
name: name,
entries: this.getInlineDiscussionsHtml(subcategories[name]),
isCategoryCohorted: isCategoryCohorted
});
}
return html;
}, this).join('');
},
/**
* Enable/Disable the inline discussion elements.
*
* Disables the category and sub-category checkboxes.
* Enables the save button.
*/
setAllInlineDiscussions: function(event) {
event.preventDefault();
this.setElementsEnabled(($(event.currentTarget).prop('checked')), false);
},
/**
* Enables the inline discussion elements.
*
* Enables the category and sub-category checkboxes.
* Enables the save button.
*/
setSomeInlineDiscussions: function(event) {
event.preventDefault();
this.setElementsEnabled(!($(event.currentTarget).prop('checked')), false);
},
/**
* Enable/Disable the inline discussion elements.
*
* 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.
*/
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);
},
/**
* Enables the save button for inline discussions.
*/
setSaveButton: function(event) {
this.setDisabled(this.$('.cohort-inline-discussions-form .action-save'), false);
},
/**
* Sends the cohorted_inline_discussions to the server and renders the view.
*/
saveInlineDiscussionsForm: function (event) {
event.preventDefault();
var self = this,
cohortedInlineDiscussions = self.getCohortedDiscussions(
'.check-discussion-subcategory-inline:checked'
),
fieldData= {
cohorted_inline_discussions: cohortedInlineDiscussions,
always_cohort_inline_discussions: self.$('.check-all-inline-discussions').prop('checked')
};
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'));
}).fail(function() {
var errorMessage = gettext("We've encountered an error. Refresh your browser and then try again.");
self.showMessage(errorMessage, self.$('.inline-discussion-topics'), 'error')
});
});
}
});
}).call(this, $, _, Backbone, gettext, interpolate_text, edx.groups.CohortDiscussionConfigurationView);
......@@ -44,8 +44,7 @@ var edx = edx || {};
renderGroupHeader: function() {
this.$('.cohort-management-group-header').html(this.groupHeaderTemplate({
cohort: this.model,
studioAdvancedSettingsUrl: this.context.studioAdvancedSettingsUrl
cohort: this.model
}));
},
......
var edx = edx || {};
(function($, _, Backbone, gettext, interpolate_text, CohortModel, CohortEditorView, CohortFormView,
CourseCohortSettingsNotificationView, NotificationModel, NotificationView, FileUploaderView) {
CourseCohortSettingsNotificationView, NotificationModel, NotificationView, FileUploaderView,
InlineDiscussionsView, CourseWideDiscussionsView) {
'use strict';
var hiddenClass = 'is-hidden',
......@@ -17,7 +18,8 @@ var edx = edx || {};
'click .cohort-management-add-form .action-save': 'saveAddCohortForm',
'click .cohort-management-add-form .action-cancel': 'cancelAddCohortForm',
'click .link-cross-reference': 'showSection',
'click .toggle-cohort-management-secondary': 'showCsvUpload'
'click .toggle-cohort-management-secondary': 'showCsvUpload',
'click .toggle-cohort-management-discussions': 'showDiscussionTopics'
},
initialize: function(options) {
......@@ -120,7 +122,7 @@ var edx = edx || {};
fieldData = {is_cohorted: this.getCohortsEnabled()};
cohortSettings = this.cohortSettings;
cohortSettings.save(
fieldData, {wait: true}
fieldData, {patch: true, wait: true}
).done(function() {
self.render();
self.renderCourseCohortSettingsNotificationView();
......@@ -277,6 +279,27 @@ var edx = edx || {};
this.$('#file-upload-form-file').focus();
}
},
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 a[data-section='" + section + "']";
......@@ -284,4 +307,4 @@ var edx = edx || {};
});
}).call(this, $, _, Backbone, gettext, interpolate_text, edx.groups.CohortModel, edx.groups.CohortEditorView,
edx.groups.CohortFormView, edx.groups.CourseCohortSettingsNotificationView, NotificationModel, NotificationView,
FileUploaderView);
FileUploaderView, edx.groups.InlineDiscussionsView, edx.groups.CourseWideDiscussionsView);
;(function (define, undefined) {
'use strict';
define(['jquery', 'js/groups/views/cohorts', 'js/groups/collections/cohort', 'js/groups/models/course_cohort_settings'],
define(['jquery', 'js/groups/views/cohorts', 'js/groups/collections/cohort', 'js/groups/models/course_cohort_settings',
'js/groups/models/cohort_discussions'],
function($) {
return function(contentGroups, studioGroupConfigurationsUrl) {
var cohorts = new edx.groups.CohortCollection(),
courseCohortSettings = new edx.groups.CourseCohortSettingsModel();
courseCohortSettings = new edx.groups.CourseCohortSettingsModel(),
discussionTopicsSettings = new edx.groups.DiscussionTopicsSettingsModel();
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');
var cohortsView = new edx.groups.CohortsView({
el: cohortManagementElement,
model: cohorts,
contentGroups: contentGroups,
cohortSettings: courseCohortSettings,
context: {
discussionTopicsSettingsModel: discussionTopicsSettings,
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();
})
discussionTopicsSettings.fetch().done(function() {
cohortsView.render();
});
});
});
};
});
......
......@@ -60,6 +60,7 @@
'history': 'js/vendor/history',
'js/verify_student/photocapture': 'js/verify_student/photocapture',
'js/staff_debug_actions': 'js/staff_debug_actions',
'js/vendor/jquery.qubit': 'js/vendor/jquery.qubit',
// Backbone classes loaded explicitly until they are converted to use RequireJS
'js/models/notification': 'js/models/notification',
......@@ -68,6 +69,10 @@
'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/models/cohort_discussions': 'js/groups/models/cohort_discussions',
'js/groups/views/cohort_discussions': 'js/groups/views/cohort_discussions',
'js/groups/views/cohort_discussions_course_wide': 'js/groups/views/cohort_discussions_course_wide',
'js/groups/views/cohort_discussions_inline': 'js/groups/views/cohort_discussions_inline',
'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',
......@@ -300,6 +305,22 @@
exports: 'edx.groups.CourseCohortSettingsModel',
deps: ['backbone']
},
'js/groups/models/cohort_discussions': {
exports: 'edx.groups.DiscussionTopicsSettingsModel',
deps: ['backbone']
},
'js/groups/views/cohort_discussions': {
exports: 'edx.groups.CohortDiscussionConfigurationView',
deps: ['backbone']
},
'js/groups/views/cohort_discussions_course_wide': {
exports: 'edx.groups.CourseWideDiscussionsView',
deps: ['backbone', 'js/groups/views/cohort_discussions']
},
'js/groups/views/cohort_discussions_inline': {
exports: 'edx.groups.InlineDiscussionsView',
deps: ['backbone', 'js/groups/views/cohort_discussions', 'js/vendor/jquery.qubit']
},
'js/groups/views/course_cohort_settings_notification': {
exports: 'edx.groups.CourseCohortSettingsNotificationView',
deps: ['backbone']
......
/*
** Checkboxes TreeView- jQuery
** https://github.com/aexmachina/jquery-qubit
**
** Copyright (c) 2014 Simon Wade
** The MIT License (MIT)
** https://github.com/aexmachina/jquery-qubit/blob/master/LICENSE.txt
**
*/
(function($) {
$.fn.qubit = function(options) {
return this.each(function() {
var qubit = new Qubit(this, options);
});
};
var Qubit = function(el) {
var self = this;
this.scope = $(el);
this.scope.on('change', 'input[type=checkbox]', function(e) {
if (!self.suspendListeners) {
self.process(e.target);
}
});
this.scope.find('input[type=checkbox]:checked').each(function() {
self.process(this);
});
};
Qubit.prototype = {
itemSelector: 'li',
process: function(checkbox) {
var checkbox = $(checkbox),
parentItems = checkbox.parentsUntil(this.scope, this.itemSelector);
try {
this.suspendListeners = true;
// all children inherit my state
parentItems.eq(0).find('input[type=checkbox]')
.filter(checkbox.prop('checked') ? ':not(:checked)' : ':checked')
.each(function() {
if (!$(this).parent().hasClass('hidden')) {
$(this).prop('checked', checkbox.prop('checked'));
}
})
.trigger('change');
this.processParents(checkbox);
} finally {
this.suspendListeners = false;
}
},
processParents: function() {
var self = this, changed = false;
this.scope.find('input[type=checkbox]').each(function() {
var $this = $(this),
parent = $this.closest(self.itemSelector),
children = parent.find('input[type=checkbox]').not($this),
numChecked = children.filter(function() {
return $(this).prop('checked') || $(this).prop('indeterminate');
}).length;
if (children.length) {
if (numChecked == 0) {
if (self.setChecked($this, false)) changed = true;
} else if (numChecked == children.length) {
if (self.setChecked($this, true)) changed = true;
} else {
if (self.setIndeterminate($this, true)) changed = true;
}
}
else {
if (self.setIndeterminate($this, false)) changed = true;
}
});
if (changed) this.processParents();
},
setChecked: function(checkbox, value, event) {
var changed = false;
if (checkbox.prop('indeterminate')) {
checkbox.prop('indeterminate', false);
changed = true;
}
if (checkbox.prop('checked') != value) {
checkbox.prop('checked', value).trigger('change');
changed = true;
}
return changed;
},
setIndeterminate: function(checkbox, value) {
if (value) {
checkbox.prop('checked', false);
}
if (checkbox.prop('indeterminate') != value) {
checkbox.prop('indeterminate', value);
return true;
}
}
};
}(jQuery));
......@@ -725,19 +725,18 @@
vertical-align: middle;
}
.form-submit {
@include idashbutton($blue);
@include font-size(14);
@include line-height(14);
margin-right: ($baseline/2);
margin-bottom: 0;
text-shadow: none;
}
.form-cancel {
@extend %t-copy-sub1;
}
}
.form-submit {
@include idashbutton($blue);
@include font-size(14);
@include line-height(14);
margin-right: ($baseline/2);
margin-bottom: 0;
text-shadow: none;
}
.cohort-management-nav {
@include clearfix();
......@@ -924,8 +923,9 @@
}
}
// CSV-based file upload for auto cohort assigning
.toggle-cohort-management-secondary {
// CSV-based file upload for auto cohort assigning and
// cohort the discussion topics.
.toggle-cohort-management-secondary, .toggle-cohort-management-discussions {
@extend %t-copy-sub1;
}
......@@ -1071,6 +1071,49 @@
}
}
// 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: $link-color;
}
.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;
......
<li class="discussion-category-item">
<div class="category-item">
<label>
<input type="checkbox" class="check-discussion-category" <%- isCategoryCohorted ? 'checked="checked"' : '' %>/>
<span class="category-title"><%- name %></span>
</label>
</div>
<ul class="wrapper-tabs subcategories"><%= entries %></ul>
</li>
<h3 class="subsection-title"><%- gettext('Specify whether discussion topics are divided by cohort') %></h3>
<form action="" method="post" id="cohort-course-wide-discussions-form" class="cohort-course-wide-discussions-form">
<div class="wrapper cohort-management-supplemental">
<div class="form-fields">
<div class="form-field">
<div class="course-wide-discussion-topics">
<h3 class="subsection-title"><%- gettext('Course-Wide Discussion Topics') %></h3>
<p><%- gettext('Select the course-wide discussion topics that you want to divide by cohort.') %></p>
<div class="field">
<ul class="discussions-wrapper"><%= courseWideTopics %></ul>
</div>
</div>
</div>
</div>
<div class="form-actions">
<button class="form-submit button action-primary action-save"><%- gettext('Save') %></button>
</div>
</div>
</form>
<hr class="divider divider-lv1" />
<form action="" method="post" id="cohort-inline-discussions-form" class="cohort-inline-discussions-form">
<div class="wrapper cohort-management-supplemental">
<div class="form-fields">
<div class="form-field">
<div class="inline-discussion-topics">
<h3 class="subsection-title"><%- gettext('Content-Specific Discussion Topics') %></h3>
<p><%- gettext('Specify whether content-specific discussion topics are divided by cohort.') %></p>
<div class="always_cohort_inline_discussions">
<label>
<input type="radio" name="inline" class="check-all-inline-discussions" <%- alwaysCohortInlineDiscussions ? 'checked="checked"' : '' %>/>
<span class="all-inline-discussions"><%- gettext('Always cohort content-specific discussion topics') %></span>
</label>
</div>
<div class="cohort_inline_discussions">
<label>
<input type="radio" name="inline" class="check-cohort-inline-discussions" <%- alwaysCohortInlineDiscussions ? '' : 'checked="checked"' %>/>
<span class="all-inline-discussions"><%- gettext('Cohort selected content-specific discussion topics') %></span>
</label>
</div>
<hr class="divider divider-lv1" />
<div class="field">
<% if ( inlineDiscussionTopics ) { %>
<ul class="inline-topics discussions-wrapper"><%= inlineDiscussionTopics %></ul>
<% } else { %>
<span class="no-topics"><%- gettext('No content-specific discussion topics exist.') %></span>
<% } %>
</div>
</div>
</div>
</div>
<hr class="divider divider-lv1" />
<div class="form-actions">
<button class="form-submit button action-primary action-save"><%- gettext('Save') %></button>
</div>
</div>
</form>
<li class="discussion-subcategory-item">
<div class="subcategory-item">
<label>
<input data-id="<%- id %>" class="check-discussion-subcategory-<%- type %>" type="checkbox" <%- is_cohorted ? 'checked="checked"' : '' %> />
<span class="topic-name"><%- name %></span>
<span class="cohorted-text <%- is_cohorted ? '' : 'hidden'%>">- <%- gettext('Cohorted') %></span>
</label>
</div>
</li>
......@@ -18,9 +18,4 @@
<a href="http://edx.readthedocs.org/projects/edx-partner-course-staff/en/latest/cohorts/cohorts_overview.html#all-automated-assignment" class="incontext-help action-secondary action-help"><%- gettext("What does this mean?") %></a>
<% } %>
</div>
<div class="setup-actions">
<% if (studioAdvancedSettingsUrl !== "None") { %>
<a href="<%= studioAdvancedSettingsUrl %>" class="action-secondary action-edit"><%- gettext("Edit settings in Studio") %></a>
<% } %>
</div>
</div>
\ No newline at end of file
</div>
......@@ -7,9 +7,9 @@
<div class="cohort-management"
data-cohorts_url="${section_data['cohorts_url']}"
data-advanced-settings-url="${section_data['advanced_settings_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']}"
>
</div>
......@@ -34,7 +34,7 @@
];
(function (require) {
require(['js/factories/cohorts_factory'], function (CohortsFactory) {
require(['js/groups/views/cohorts_dashboard_factory'], function (CohortsFactory) {
CohortsFactory(contentGroups, '${get_studio_url(course, 'group_configurations') | h}');
});
}).call(this, require || RequireJS.require);
......
......@@ -47,5 +47,15 @@
) %>
</p>
</div>
<hr class="divider divider-lv1" />
<!-- Discussion Topics. -->
<a class="toggle-cohort-management-discussions" href="#cohort-discussions-management"><%- gettext('Specify whether discussion topics are divided by cohort') %></a>
<div class="cohort-discussions-nav is-hidden" id="cohort-management-discussion-topics">
<div class="cohort-course-wide-discussions-nav"></div>
<div class="cohort-inline-discussions-nav"></div>
</div>
</div>
<% } %>
......@@ -48,6 +48,8 @@
<script type="text/javascript" src="${static.url('js/vendor/tinymce/js/tinymce/tinymce.full.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/tinymce/js/tinymce/jquery.tinymce.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/jQuery-File-Upload/js/jquery.fileupload.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/jquery.qubit.js')}"></script>
<%static:js group='module-descriptor-js'/>
<%static:js group='instructor_dash'/>
<%static:js group='application'/>
......@@ -63,6 +65,10 @@
<script type="text/javascript" src="${static.url('js/groups/models/course_cohort_settings.js')}"></script>
<script type="text/javascript" src="${static.url('js/groups/collections/cohort.js')}"></script>
<script type="text/javascript" src="${static.url('js/groups/views/course_cohort_settings_notification.js')}"></script>
<script type="text/javascript" src="${static.url('js/groups/models/cohort_discussions.js')}"></script>
<script type="text/javascript" src="${static.url('js/groups/views/cohort_discussions.js')}"></script>
<script type="text/javascript" src="${static.url('js/groups/views/cohort_discussions_course_wide.js')}"></script>
<script type="text/javascript" src="${static.url('js/groups/views/cohort_discussions_inline.js')}"></script>
<script type="text/javascript" src="${static.url('js/groups/views/cohort_form.js')}"></script>
<script type="text/javascript" src="${static.url('js/groups/views/cohort_editor.js')}"></script>
<script type="text/javascript" src="${static.url('js/groups/views/cohorts.js')}"></script>
......@@ -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"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="instructor/instructor_dashboard_2/${template_name}.underscore" />
</script>
......
......@@ -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),
......
......@@ -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)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment