Commit 36c51450 by cahrens

Adds in radio button patch request and text for page

EDUCATOR-229
parent 79c5137a
...@@ -120,7 +120,7 @@ class CourseDiscussionSettingsTest(ModuleStoreTestCase): ...@@ -120,7 +120,7 @@ class CourseDiscussionSettingsTest(ModuleStoreTestCase):
def test_invalid_data_types(self): def test_invalid_data_types(self):
exception_msg_template = "Incorrect field type for `{}`. Type must be `{}`" exception_msg_template = "Incorrect field type for `{}`. Type must be `{}`"
fields = [ fields = [
{'name': 'division_scheme', 'type': str}, {'name': 'division_scheme', 'type': basestring},
{'name': 'always_divide_inline_discussions', 'type': bool}, {'name': 'always_divide_inline_discussions', 'type': bool},
{'name': 'divided_discussions', 'type': list} {'name': 'divided_discussions', 'type': list}
] ]
......
...@@ -130,12 +130,13 @@ def set_course_discussion_settings(course_key, **kwargs): ...@@ -130,12 +130,13 @@ def set_course_discussion_settings(course_key, **kwargs):
Returns: Returns:
A CourseDiscussionSettings object. A CourseDiscussionSettings object.
""" """
fields = {'division_scheme': str, 'always_divide_inline_discussions': bool, 'divided_discussions': list} fields = {'division_scheme': basestring, 'always_divide_inline_discussions': bool, 'divided_discussions': list}
course_discussion_settings = get_course_discussion_settings(course_key) course_discussion_settings = get_course_discussion_settings(course_key)
for field, field_type in fields.items(): for field, field_type in fields.items():
if field in kwargs: if field in kwargs:
if not isinstance(kwargs[field], field_type): if not isinstance(kwargs[field], field_type):
raise ValueError("Incorrect field type for `{}`. Type must be `{}`".format(field, field_type.__name__)) raise ValueError("Incorrect field type for `{}`. Type must be `{}`".format(field, field_type.__name__))
setattr(course_discussion_settings, field, kwargs[field]) setattr(course_discussion_settings, field, kwargs[field])
course_discussion_settings.save() course_discussion_settings.save()
return course_discussion_settings return course_discussion_settings
...@@ -807,19 +807,19 @@ class DiscussionManagementSection(PageObject): ...@@ -807,19 +807,19 @@ class DiscussionManagementSection(PageObject):
Returns the ID of the selected discussion division scheme Returns the ID of the selected discussion division scheme
("NOT_DIVIDED_SCHEME", "COHORT_SCHEME", or "ENROLLMENT_TRACK_SCHEME)". ("NOT_DIVIDED_SCHEME", "COHORT_SCHEME", or "ENROLLMENT_TRACK_SCHEME)".
""" """
return self.q(css=self._bounded_selector('.division-scheme:checked')).first.attrs('id')[0] return self.q(css=self._bounded_selector('.division-scheme:checked')).first.attrs('value')[0]
def select_division_scheme(self, scheme): def select_division_scheme(self, scheme):
""" """
Selects the radio button associated with the specified division scheme. Selects the radio button associated with the specified division scheme.
""" """
self.q(css=self._bounded_selector("input#%s" % scheme)).first.click() self.q(css=self._bounded_selector("input.%s" % scheme)).first.click()
def division_scheme_visible(self, scheme): def division_scheme_visible(self, scheme):
""" """
Returns whether or not the specified scheme is visible as an option. Returns whether or not the specified scheme is visible as an option.
""" """
return self.q(css=self._bounded_selector("input#%s" % scheme)).visible return self.q(css=self._bounded_selector("input.%s" % scheme)).visible
class MembershipPageAutoEnrollSection(PageObject): class MembershipPageAutoEnrollSection(PageObject):
......
...@@ -99,7 +99,7 @@ class BaseDividedDiscussionTest(UniqueCourseTest, CohortTestMixin): ...@@ -99,7 +99,7 @@ class BaseDividedDiscussionTest(UniqueCourseTest, CohortTestMixin):
Verify that the save confirmation message for the specified portion of the page is visible. Verify that the save confirmation message for the specified portion of the page is visible.
""" """
confirmation_message = self.discussion_management_page.get_divide_discussions_message(key=key) confirmation_message = self.discussion_management_page.get_divide_discussions_message(key=key)
self.assertEqual("Your changes have been saved.", confirmation_message) self.assertIn("Your changes have been saved.", confirmation_message)
@attr(shard=6) @attr(shard=6)
......
import json import json
import logging import logging
from datetime import datetime
import ddt import ddt
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import Http404 from django.http import Http404
...@@ -9,20 +10,27 @@ from django.test.utils import override_settings ...@@ -9,20 +10,27 @@ from django.test.utils import override_settings
from django.utils import translation from django.utils import translation
from lms.lib.comment_client.utils import CommentClientPaginatedResult from lms.lib.comment_client.utils import CommentClientPaginatedResult
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
from django_comment_common.utils import ThreadContext from django_comment_common.utils import ThreadContext
from django_comment_common.models import ForumsConfig from django_comment_common.models import ForumsConfig, CourseDiscussionSettings
from django_comment_client.permissions import get_team from django_comment_client.permissions import get_team
from django_comment_client.tests.utils import config_course_discussions
from django_comment_client.tests.group_id import ( from django_comment_client.tests.group_id import (
GroupIdAssertionMixin, GroupIdAssertionMixin,
CohortedTopicGroupIdTestMixin, CohortedTopicGroupIdTestMixin,
NonCohortedTopicGroupIdTestMixin, NonCohortedTopicGroupIdTestMixin,
) )
from django_comment_client.constants import TYPE_ENTRY, TYPE_SUBCATEGORY
from django_comment_client.tests.unicode import UnicodeTestMixin from django_comment_client.tests.unicode import UnicodeTestMixin
from django_comment_client.tests.utils import CohortedTestCase, ForumsEnableMixin from django_comment_client.tests.utils import CohortedTestCase, ForumsEnableMixin, topic_name_to_id
from django_comment_client.utils import strip_none from django_comment_client.utils import strip_none
from lms.djangoapps.discussion import views from lms.djangoapps.discussion import views
from lms.djangoapps.discussion.views import course_discussions_settings_handler
from student.tests.factories import UserFactory, CourseEnrollmentFactory from student.tests.factories import UserFactory, CourseEnrollmentFactory
from util.testing import UrlResetMixin from util.testing import UrlResetMixin
from openedx.core.djangoapps.course_groups.tests.helpers import config_course_cohorts
from openedx.core.djangoapps.course_groups.tests.test_views import CohortViewsTestCase
from openedx.core.djangoapps.util.testing import ContentGroupTestCase from openedx.core.djangoapps.util.testing import ContentGroupTestCase
from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
...@@ -1626,3 +1634,252 @@ class EnterpriseConsentTestCase(EnterpriseTestConsentRequired, ForumsEnableMixin ...@@ -1626,3 +1634,252 @@ class EnterpriseConsentTestCase(EnterpriseTestConsentRequired, ForumsEnableMixin
kwargs=dict(course_id=course_id, discussion_id=self.discussion_id, thread_id=thread_id)), kwargs=dict(course_id=course_id, discussion_id=self.discussion_id, thread_id=thread_id)),
): ):
self.verify_consent_required(self.client, url) self.verify_consent_required(self.client, url)
class DividedDiscussionsTestCase(CohortViewsTestCase):
def create_divided_discussions(self):
"""
Set up a divided discussion in the system, complete with all the fixings
"""
divided_inline_discussions = ['Topic A']
divided_course_wide_discussions = ["Topic B"]
divided_discussions = divided_inline_discussions + divided_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,
)
config_course_discussions(
self.course,
discussion_topics=discussion_topics,
divided_discussions=divided_discussions
)
return divided_inline_discussions, divided_course_wide_discussions
class CourseDiscussionTopicsTestCase(DividedDiscussionsTestCase):
"""
Tests the `divide_discussion_topics` view.
"""
def test_non_staff(self):
"""
Verify that we cannot access divide_discussion_topics if we're a non-staff user.
"""
self._verify_non_staff_cannot_access(views.discussion_topics, "GET", [unicode(self.course.id)])
def test_get_discussion_topics(self):
"""
Verify that discussion_topics is working for HTTP GET.
"""
# create inline & course-wide discussion to verify the different map.
self.create_divided_discussions()
response = self.get_handler(self.course, handler=views.discussion_topics)
start_date = response['inline_discussions']['subcategories']['Chapter']['start_date']
expected_response = {
"course_wide_discussions": {
'children': [['Topic B', TYPE_ENTRY]],
'entries': {
'Topic B': {
'sort_key': 'A',
'is_divided': 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', TYPE_ENTRY]],
'entries': {
'Discussion': {
'sort_key': None,
'is_divided': True,
'id': topic_name_to_id(self.course, "Topic A"),
'start_date': start_date
}
},
'sort_key': 'Chapter',
'start_date': start_date
}
},
'children': [['Chapter', TYPE_SUBCATEGORY]]
}
}
self.assertEqual(response, expected_response)
class CourseDiscussionsHandlerTestCase(DividedDiscussionsTestCase):
"""
Tests the course_discussion_settings_handler
"""
def get_expected_response(self):
"""
Returns the static response dict.
"""
return {
u'always_divide_inline_discussions': False,
u'divided_inline_discussions': [],
u'divided_course_wide_discussions': [],
u'id': 1,
u'division_scheme': u'cohort',
u'available_division_schemes': [u'cohort']
}
def test_non_staff(self):
"""
Verify that we cannot access course_discussions_settings_handler if we're a non-staff user.
"""
self._verify_non_staff_cannot_access(
course_discussions_settings_handler, "GET", [unicode(self.course.id)]
)
self._verify_non_staff_cannot_access(
course_discussions_settings_handler, "PATCH", [unicode(self.course.id)]
)
def test_update_always_divide_inline_discussion_settings(self):
"""
Verify that course_discussions_settings_handler is working for always_divide_inline_discussions via HTTP PATCH.
"""
config_course_cohorts(self.course, is_cohorted=True)
response = self.get_handler(self.course, handler=course_discussions_settings_handler)
expected_response = self.get_expected_response()
self.assertEqual(response, expected_response)
expected_response['always_divide_inline_discussions'] = True
response = self.patch_handler(
self.course, data=expected_response, handler=course_discussions_settings_handler
)
self.assertEqual(response, expected_response)
def test_update_course_wide_discussion_settings(self):
"""
Verify that course_discussions_settings_handler is working for divided_course_wide_discussions via HTTP PATCH.
"""
# course-wide discussion
discussion_topics = {
"Topic B": {"id": "Topic B"},
}
config_course_cohorts(self.course, is_cohorted=True)
config_course_discussions(self.course, discussion_topics=discussion_topics)
response = self.get_handler(self.course, handler=views.course_discussions_settings_handler)
expected_response = self.get_expected_response()
self.assertEqual(response, expected_response)
expected_response['divided_course_wide_discussions'] = [topic_name_to_id(self.course, "Topic B")]
response = self.patch_handler(
self.course, data=expected_response, handler=views.course_discussions_settings_handler
)
self.assertEqual(response, expected_response)
def test_update_inline_discussion_settings(self):
"""
Verify that course_discussions_settings_handler is working for divided_inline_discussions via HTTP PATCH.
"""
config_course_cohorts(self.course, is_cohorted=True)
response = self.get_handler(self.course, handler=views.course_discussions_settings_handler)
expected_response = self.get_expected_response()
self.assertEqual(response, expected_response)
now = datetime.now()
# inline discussion
ItemFactory.create(
parent_location=self.course.location,
category="discussion",
discussion_id="Topic_A",
discussion_category="Chapter",
discussion_target="Discussion",
start=now
)
expected_response['divided_inline_discussions'] = ["Topic_A"]
response = self.patch_handler(
self.course, data=expected_response, handler=views.course_discussions_settings_handler
)
self.assertEqual(response, expected_response)
def test_get_settings(self):
"""
Verify that course_discussions_settings_handler is working for HTTP GET.
"""
divided_inline_discussions, divided_course_wide_discussions = self.create_divided_discussions()
response = self.get_handler(self.course, handler=views.course_discussions_settings_handler)
expected_response = self.get_expected_response()
expected_response['divided_inline_discussions'] = [topic_name_to_id(self.course, name)
for name in divided_inline_discussions]
expected_response['divided_course_wide_discussions'] = [topic_name_to_id(self.course, name)
for name in divided_course_wide_discussions]
self.assertEqual(response, expected_response)
def test_update_settings_with_invalid_field_data_type(self):
"""
Verify that course_discussions_settings_handler return HTTP 400 if field data type is incorrect.
"""
config_course_cohorts(self.course, is_cohorted=True)
response = self.patch_handler(
self.course,
data={'always_divide_inline_discussions': ''},
expected_response_code=400,
handler=views.course_discussions_settings_handler
)
self.assertEqual(
"Incorrect field type for `{}`. Type must be `{}`".format('always_divide_inline_discussions', bool.__name__),
response.get("error")
)
def test_available_schemes(self):
# Cohorts disabled, single enrollment mode.
config_course_cohorts(self.course, is_cohorted=False)
response = self.get_handler(self.course, handler=views.course_discussions_settings_handler)
expected_response = self.get_expected_response()
expected_response['available_division_schemes'] = []
self.assertEqual(response, expected_response)
# Add 2 enrollment modes
CourseModeFactory.create(course_id=self.course.id, mode_slug=CourseMode.AUDIT)
CourseModeFactory.create(course_id=self.course.id, mode_slug=CourseMode.VERIFIED)
response = self.get_handler(self.course, handler=views.course_discussions_settings_handler)
expected_response['available_division_schemes'] = [CourseDiscussionSettings.ENROLLMENT_TRACK]
self.assertEqual(response, expected_response)
# Enable cohorts
config_course_cohorts(self.course, is_cohorted=True)
response = self.get_handler(self.course, handler=views.course_discussions_settings_handler)
expected_response['available_division_schemes'] = [
CourseDiscussionSettings.COHORT, CourseDiscussionSettings.ENROLLMENT_TRACK
]
self.assertEqual(response, expected_response)
...@@ -24,6 +24,9 @@ try: ...@@ -24,6 +24,9 @@ try:
except ImportError: except ImportError:
newrelic = None # pylint: disable=invalid-name newrelic = None # pylint: disable=invalid-name
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_http_methods
from rest_framework import status from rest_framework import status
from web_fragments.fragment import Fragment from web_fragments.fragment import Fragment
...@@ -36,10 +39,12 @@ from courseware.access import has_access ...@@ -36,10 +39,12 @@ from courseware.access import has_access
from student.models import CourseEnrollment from student.models import CourseEnrollment
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from django_comment_common.utils import ThreadContext, get_course_discussion_settings from django_comment_common.utils import ThreadContext, get_course_discussion_settings, set_course_discussion_settings
from django_comment_client.constants import TYPE_ENTRY
from django_comment_client.permissions import has_permission, get_team from django_comment_client.permissions import has_permission, get_team
from django_comment_client.utils import ( from django_comment_client.utils import (
available_division_schemes,
merge_dict, merge_dict,
extract, extract,
strip_none, strip_none,
...@@ -57,6 +62,8 @@ import lms.lib.comment_client as cc ...@@ -57,6 +62,8 @@ import lms.lib.comment_client as cc
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from contextlib import contextmanager from contextlib import contextmanager
from util.json_request import expect_json, JsonResponse
THREADS_PER_PAGE = 20 THREADS_PER_PAGE = 20
INLINE_THREADS_PER_PAGE = 20 INLINE_THREADS_PER_PAGE = 20
...@@ -684,3 +691,167 @@ class DiscussionBoardFragmentView(EdxFragmentView): ...@@ -684,3 +691,167 @@ class DiscussionBoardFragmentView(EdxFragmentView):
return self.get_css_dependencies('style-discussion-main-rtl') return self.get_css_dependencies('style-discussion-main-rtl')
else: else:
return self.get_css_dependencies('style-discussion-main') return self.get_css_dependencies('style-discussion-main')
@expect_json
@login_required
def discussion_topics(request, course_key_string):
"""
The handler for divided discussion categories requests.
This will raise 404 if user is not staff.
Returns the JSON representation of discussion topics w.r.t categories for the course.
Example:
>>> example = {
>>> "course_wide_discussions": {
>>> "entries": {
>>> "General": {
>>> "sort_key": "General",
>>> "is_divided": True,
>>> "id": "i4x-edx-eiorguegnru-course-foobarbaz"
>>> }
>>> }
>>> "children": ["General", "entry"]
>>> },
>>> "inline_discussions" : {
>>> "subcategories": {
>>> "Getting Started": {
>>> "subcategories": {},
>>> "children": [
>>> ["Working with Videos", "entry"],
>>> ["Videos on edX", "entry"]
>>> ],
>>> "entries": {
>>> "Working with Videos": {
>>> "sort_key": None,
>>> "is_divided": False,
>>> "id": "d9f970a42067413cbb633f81cfb12604"
>>> },
>>> "Videos on edX": {
>>> "sort_key": None,
>>> "is_divided": False,
>>> "id": "98d8feb5971041a085512ae22b398613"
>>> }
>>> }
>>> },
>>> "children": ["Getting Started", "subcategory"]
>>> },
>>> }
>>> }
"""
course_key = CourseKey.from_string(course_key_string)
course = get_course_with_access(request.user, 'staff', course_key)
discussion_topics = {}
discussion_category_map = utils.get_discussion_category_map(
course, request.user, divided_only_if_explicit=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, c_type in discussion_category_map['children']:
if name in course_wide_entries and c_type == TYPE_ENTRY:
course_wide_children.append([name, c_type])
else:
inline_children.append([name, c_type])
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)
@require_http_methods(("GET", "PATCH"))
@ensure_csrf_cookie
@expect_json
@login_required
def course_discussions_settings_handler(request, course_key_string):
"""
The restful handler for divided discussion setting requests. Requires JSON.
This will raise 404 if user is not staff.
GET
Returns the JSON representation of divided discussion settings for the course.
PATCH
Updates the divided discussion settings for the course. Returns the JSON representation of updated settings.
"""
course_key = CourseKey.from_string(course_key_string)
course = get_course_with_access(request.user, 'staff', course_key)
discussion_settings = get_course_discussion_settings(course_key)
if request.method == 'PATCH':
divided_course_wide_discussions, divided_inline_discussions = get_divided_discussions(
course, discussion_settings
)
settings_to_change = {}
if 'divided_course_wide_discussions' in request.json or 'divided_inline_discussions' in request.json:
divided_course_wide_discussions = request.json.get(
'divided_course_wide_discussions', divided_course_wide_discussions
)
divided_inline_discussions = request.json.get(
'divided_inline_discussions', divided_inline_discussions
)
settings_to_change['divided_discussions'] = divided_course_wide_discussions + divided_inline_discussions
if 'always_divide_inline_discussions' in request.json:
settings_to_change['always_divide_inline_discussions'] = request.json.get(
'always_divide_inline_discussions'
)
if 'division_scheme' in request.json:
settings_to_change['division_scheme'] = request.json.get(
'division_scheme'
)
if not settings_to_change:
return JsonResponse({"error": unicode("Bad Request")}, 400)
try:
if settings_to_change:
discussion_settings = set_course_discussion_settings(course_key, **settings_to_change)
except ValueError as err:
# Note: error message not translated because it is not exposed to the user (UI prevents this state).
return JsonResponse({"error": unicode(err)}, 400)
divided_course_wide_discussions, divided_inline_discussions = get_divided_discussions(
course, discussion_settings
)
return JsonResponse({
'id': discussion_settings.id,
'divided_inline_discussions': divided_inline_discussions,
'divided_course_wide_discussions': divided_course_wide_discussions,
'always_divide_inline_discussions': discussion_settings.always_divide_inline_discussions,
'division_scheme': discussion_settings.division_scheme,
'available_division_schemes': available_division_schemes(course_key)
})
def get_divided_discussions(course, discussion_settings):
"""
Returns the course-wide and inline divided discussion ids separately.
"""
divided_course_wide_discussions = []
divided_inline_discussions = []
course_wide_discussions = [topic['id'] for __, topic in course.discussion_topics.items()]
all_discussions = utils.get_discussion_categories_ids(course, None, include_all=True)
for divided_discussion_id in discussion_settings.divided_discussions:
if divided_discussion_id in course_wide_discussions:
divided_course_wide_discussions.append(divided_discussion_id)
elif divided_discussion_id in all_discussions:
divided_inline_discussions.append(divided_discussion_id)
return divided_course_wide_discussions, divided_inline_discussions
...@@ -14,6 +14,7 @@ from edxmako import add_lookup ...@@ -14,6 +14,7 @@ from edxmako import add_lookup
from django_comment_client.tests.factories import RoleFactory from django_comment_client.tests.factories import RoleFactory
from django_comment_client.tests.unicode import UnicodeTestMixin from django_comment_client.tests.unicode import UnicodeTestMixin
from django_comment_client.tests.utils import topic_name_to_id, config_course_discussions
from django_comment_client.constants import TYPE_ENTRY, TYPE_SUBCATEGORY from django_comment_client.constants import TYPE_ENTRY, TYPE_SUBCATEGORY
import django_comment_client.utils as utils import django_comment_client.utils as utils
from lms.lib.comment_client.utils import perform_request, CommentClientMaintenanceError from lms.lib.comment_client.utils import perform_request, CommentClientMaintenanceError
...@@ -26,7 +27,7 @@ from courseware.tests.factories import InstructorFactory ...@@ -26,7 +27,7 @@ from courseware.tests.factories import InstructorFactory
from courseware.tabs import get_course_tab_list from courseware.tabs import get_course_tab_list
from openedx.core.djangoapps.course_groups import cohorts from openedx.core.djangoapps.course_groups import cohorts
from openedx.core.djangoapps.course_groups.cohorts import set_course_cohorted from openedx.core.djangoapps.course_groups.cohorts import set_course_cohorted
from openedx.core.djangoapps.course_groups.tests.helpers import config_course_cohorts, config_course_discussions, topic_name_to_id, CohortFactory from openedx.core.djangoapps.course_groups.tests.helpers import config_course_cohorts, CohortFactory
from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentFactory from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentFactory
from openedx.core.djangoapps.content.course_structures.models import CourseStructure from openedx.core.djangoapps.content.course_structures.models import CourseStructure
from openedx.core.djangoapps.util.testing import ContentGroupTestCase from openedx.core.djangoapps.util.testing import ContentGroupTestCase
...@@ -1489,7 +1490,7 @@ class GroupIdForUserTestCase(ModuleStoreTestCase): ...@@ -1489,7 +1490,7 @@ class GroupIdForUserTestCase(ModuleStoreTestCase):
@attr(shard=1) @attr(shard=1)
class CourseDiscussionDivisionEnabledTestCase(ModuleStoreTestCase): class CourseDiscussionDivisionEnabledTestCase(ModuleStoreTestCase):
""" Test the course_discussion_division_enabled method. """ """ Test the course_discussion_division_enabled and available_division_schemes methods. """
def setUp(self): def setUp(self):
super(CourseDiscussionDivisionEnabledTestCase, self).setUp() super(CourseDiscussionDivisionEnabledTestCase, self).setUp()
...@@ -1504,6 +1505,7 @@ class CourseDiscussionDivisionEnabledTestCase(ModuleStoreTestCase): ...@@ -1504,6 +1505,7 @@ class CourseDiscussionDivisionEnabledTestCase(ModuleStoreTestCase):
def test_discussion_division_disabled(self): def test_discussion_division_disabled(self):
course_discussion_settings = get_course_discussion_settings(self.course.id) course_discussion_settings = get_course_discussion_settings(self.course.id)
self.assertFalse(utils.course_discussion_division_enabled(course_discussion_settings)) self.assertFalse(utils.course_discussion_division_enabled(course_discussion_settings))
self.assertEqual([], utils.available_division_schemes(self.course.id))
def test_discussion_division_by_cohort(self): def test_discussion_division_by_cohort(self):
set_discussion_division_settings( set_discussion_division_settings(
...@@ -1511,11 +1513,13 @@ class CourseDiscussionDivisionEnabledTestCase(ModuleStoreTestCase): ...@@ -1511,11 +1513,13 @@ class CourseDiscussionDivisionEnabledTestCase(ModuleStoreTestCase):
) )
# Because cohorts are disabled, discussion division is not enabled. # Because cohorts are disabled, discussion division is not enabled.
self.assertFalse(utils.course_discussion_division_enabled(get_course_discussion_settings(self.course.id))) self.assertFalse(utils.course_discussion_division_enabled(get_course_discussion_settings(self.course.id)))
self.assertEqual([], utils.available_division_schemes(self.course.id))
# Now enable cohorts, which will cause discussions to be divided. # Now enable cohorts, which will cause discussions to be divided.
set_discussion_division_settings( set_discussion_division_settings(
self.course.id, enable_cohorts=True, division_scheme=CourseDiscussionSettings.COHORT self.course.id, enable_cohorts=True, division_scheme=CourseDiscussionSettings.COHORT
) )
self.assertTrue(utils.course_discussion_division_enabled(get_course_discussion_settings(self.course.id))) self.assertTrue(utils.course_discussion_division_enabled(get_course_discussion_settings(self.course.id)))
self.assertEqual([CourseDiscussionSettings.COHORT], utils.available_division_schemes(self.course.id))
def test_discussion_division_by_enrollment_track(self): def test_discussion_division_by_enrollment_track(self):
set_discussion_division_settings( set_discussion_division_settings(
...@@ -1523,10 +1527,12 @@ class CourseDiscussionDivisionEnabledTestCase(ModuleStoreTestCase): ...@@ -1523,10 +1527,12 @@ class CourseDiscussionDivisionEnabledTestCase(ModuleStoreTestCase):
) )
# Only a single enrollment track exists, so discussion division is not enabled. # Only a single enrollment track exists, so discussion division is not enabled.
self.assertFalse(utils.course_discussion_division_enabled(get_course_discussion_settings(self.course.id))) self.assertFalse(utils.course_discussion_division_enabled(get_course_discussion_settings(self.course.id)))
self.assertEqual([], utils.available_division_schemes(self.course.id))
# Now create a second CourseMode, which will cause discussions to be divided. # Now create a second CourseMode, which will cause discussions to be divided.
CourseModeFactory.create(course_id=self.course.id, mode_slug=CourseMode.VERIFIED) CourseModeFactory.create(course_id=self.course.id, mode_slug=CourseMode.VERIFIED)
self.assertTrue(utils.course_discussion_division_enabled(get_course_discussion_settings(self.course.id))) self.assertTrue(utils.course_discussion_division_enabled(get_course_discussion_settings(self.course.id)))
self.assertEqual([CourseDiscussionSettings.ENROLLMENT_TRACK], utils.available_division_schemes(self.course.id))
@attr(shard=1) @attr(shard=1)
......
...@@ -5,9 +5,11 @@ from mock import patch ...@@ -5,9 +5,11 @@ from mock import patch
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
from django_comment_common.models import Role, ForumsConfig from django_comment_common.models import Role, ForumsConfig
from django_comment_common.utils import seed_permissions_roles from django_comment_common.utils import seed_permissions_roles, set_course_discussion_settings, CourseDiscussionSettings
from student.tests.factories import CourseEnrollmentFactory, UserFactory from student.tests.factories import CourseEnrollmentFactory, UserFactory
from util.testing import UrlResetMixin from util.testing import UrlResetMixin
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
...@@ -63,3 +65,58 @@ class CohortedTestCase(ForumsEnableMixin, UrlResetMixin, SharedModuleStoreTestCa ...@@ -63,3 +65,58 @@ class CohortedTestCase(ForumsEnableMixin, UrlResetMixin, SharedModuleStoreTestCa
course_id=self.course.id, course_id=self.course.id,
users=[self.moderator] users=[self.moderator]
) )
# pylint: disable=dangerous-default-value
def config_course_discussions(
course,
discussion_topics={},
divided_discussions=[],
always_divide_inline_discussions=False
):
"""
Set discussions and configure divided discussions for a course.
Arguments:
course: CourseDescriptor
discussion_topics (Dict): Discussion topic names. Picks ids and
sort_keys automatically.
divided_discussions: Discussion topics to divide. Converts the
list to use the same ids as discussion topic names.
always_divide_inline_discussions (bool): Whether inline discussions
should be divided by default.
Returns:
Nothing -- modifies course in place.
"""
def to_id(name):
"""Convert name to id."""
return topic_name_to_id(course, name)
set_course_discussion_settings(
course.id,
divided_discussions=[to_id(name) for name in divided_discussions],
always_divide_inline_discussions=always_divide_inline_discussions,
division_scheme=CourseDiscussionSettings.COHORT,
)
course.discussion_topics = dict((name, {"sort_key": "A", "id": to_id(name)})
for name in discussion_topics)
try:
# Not implemented for XMLModulestore, which is used by test_cohorts.
modulestore().update_item(course, ModuleStoreEnum.UserID.test)
except NotImplementedError:
pass
def topic_name_to_id(course, name):
"""
Given a discussion topic name, return an id for that name (includes
course and url_name).
"""
return "{course}_{run}_{name}".format(
course=course.location.course,
run=course.url_name,
name=name
)
...@@ -860,6 +860,24 @@ def course_discussion_division_enabled(course_discussion_settings): ...@@ -860,6 +860,24 @@ def course_discussion_division_enabled(course_discussion_settings):
return _get_course_division_scheme(course_discussion_settings) != CourseDiscussionSettings.NONE return _get_course_division_scheme(course_discussion_settings) != CourseDiscussionSettings.NONE
def available_division_schemes(course_key):
"""
Returns a list of possible discussion division schemes for this course.
This takes into account if cohorts are enabled and if there are multiple
enrollment tracks. If no schemes are available, returns an empty list.
Args:
course_key: CourseKey
Returns: list of possible division schemes (for example, CourseDiscussionSettings.COHORT)
"""
available_schemes = []
if is_course_cohorted(course_key):
available_schemes.append(CourseDiscussionSettings.COHORT)
if len(_get_enrollment_track_groups(course_key)) > 1:
available_schemes.append(CourseDiscussionSettings.ENROLLMENT_TRACK)
return available_schemes
def _get_course_division_scheme(course_discussion_settings): def _get_course_division_scheme(course_discussion_settings):
division_scheme = course_discussion_settings.division_scheme division_scheme = course_discussion_settings.division_scheme
if ( if (
......
...@@ -6,7 +6,8 @@ ...@@ -6,7 +6,8 @@
defaults: { defaults: {
divided_inline_discussions: [], divided_inline_discussions: [],
divided_course_wide_discussions: [], divided_course_wide_discussions: [],
always_divide_inline_discussions: false always_divide_inline_discussions: false,
division_scheme: 'none'
} }
}); });
return CourseDiscussionsSettingsModel; return CourseDiscussionsSettingsModel;
......
...@@ -3,11 +3,24 @@ ...@@ -3,11 +3,24 @@
define(['jquery', 'underscore', 'backbone', 'gettext', define(['jquery', 'underscore', 'backbone', 'gettext',
'js/discussions_management/views/divided_discussions_inline', 'js/discussions_management/views/divided_discussions_inline',
'js/discussions_management/views/divided_discussions_course_wide', 'js/discussions_management/views/divided_discussions_course_wide',
'edx-ui-toolkit/js/utils/html-utils' 'edx-ui-toolkit/js/utils/html-utils',
'edx-ui-toolkit/js/utils/string-utils',
'js/models/notification',
'js/views/notification'
], ],
function($, _, Backbone, gettext, InlineDiscussionsView, CourseWideDiscussionsView, HtmlUtils) { function($, _, Backbone, gettext, InlineDiscussionsView, CourseWideDiscussionsView, HtmlUtils, StringUtils) {
/* global NotificationModel, NotificationView */
var hiddenClass = 'is-hidden';
var cohort = 'cohort';
var none = 'none';
var enrollmentTrack = 'enrollment_track';
var DiscussionsView = Backbone.View.extend({ var DiscussionsView = Backbone.View.extend({
events: {
'click .division-scheme': 'divisionSchemeChanged'
},
initialize: function(options) { initialize: function(options) {
this.template = HtmlUtils.template($('#discussions-tpl').text()); this.template = HtmlUtils.template($('#discussions-tpl').text());
...@@ -16,16 +29,145 @@ ...@@ -16,16 +29,145 @@
}, },
render: function() { render: function() {
HtmlUtils.setHtml(this.$el, this.template({})); var numberAvailableSchemes = this.discussionSettings.attributes.available_division_schemes.length;
this.showDiscussionTopics(); HtmlUtils.setHtml(this.$el, this.template({
availableSchemes: this.getDivisionSchemeData(this.discussionSettings.attributes.division_scheme), // eslint-disable-line max-len
layoutClass: numberAvailableSchemes === 1 ? 'two-column-layout' : 'three-column-layout'
}));
this.updateTopicVisibility(this.getSelectedScheme(), this.getTopicNav());
this.renderTopics();
return this; return this;
}, },
getDivisionSchemeData: function(selectedScheme) {
return [
{
key: none,
displayName: gettext('Not divided'),
descriptiveText: gettext('Discussions are unified; all learners interact with posts from other learners, regardless of the group they are in.'), // eslint-disable-line max-len
selected: selectedScheme === none,
enabled: true // always leave none enabled
},
{
key: enrollmentTrack,
displayName: gettext('Enrollment Tracks'),
descriptiveText: gettext('Use enrollment tracks as the basis for dividing discussions. All learners, regardless of their enrollment track, see the same discussion topics, but within divided topics, only learners who are in the same enrollment track see and respond to each others’ posts.'), // eslint-disable-line max-len
selected: selectedScheme === enrollmentTrack,
enabled: this.isSchemeAvailable(enrollmentTrack) || selectedScheme === enrollmentTrack
},
{
key: cohort,
displayName: gettext('Cohorts'),
descriptiveText: gettext('Use cohorts as the basis for dividing discussions. All learners, regardless of cohort, see the same discussion topics, but within divided topics, only members of the same cohort see and respond to each others’ posts. '), // eslint-disable-line max-len
selected: selectedScheme === cohort,
enabled: this.isSchemeAvailable(cohort) || selectedScheme === cohort
}
];
},
isSchemeAvailable: function(scheme) {
return this.discussionSettings.attributes.available_division_schemes.indexOf(scheme) !== -1;
},
showMessage: function(message, type) {
var model = new NotificationModel({type: type || 'confirmation', title: message});
this.removeNotification();
this.notification = new NotificationView({
model: model
});
this.$('.division-scheme-container').prepend(this.notification.$el);
this.notification.render();
},
removeNotification: function() {
if (this.notification) {
this.notification.remove();
}
},
getSelectedScheme: function() {
return this.$('input[name="division-scheme"]:checked').val();
},
getTopicNav: function() {
return this.$('.topic-division-nav');
},
divisionSchemeChanged: function() {
var selectedScheme = this.getSelectedScheme(),
topicNav = this.getTopicNav(),
fieldData = {
division_scheme: selectedScheme
};
this.updateTopicVisibility(selectedScheme, topicNav);
this.saveDivisionScheme(topicNav, fieldData, selectedScheme);
},
saveDivisionScheme: function($element, fieldData, selectedScheme) {
var self = this,
discussionSettingsModel = this.discussionSettings,
showErrorMessage,
details = '';
this.removeNotification();
showErrorMessage = function(message) {
self.showMessage(message, 'error');
};
discussionSettingsModel.save(
fieldData, {patch: true, wait: true}
).done(function() {
switch (selectedScheme) {
case none:
details = gettext('Discussion topics in the course are not divided.');
break;
case enrollmentTrack:
details = gettext('Any divided discussion topics are divided based on enrollment track.'); // eslint-disable-line max-len
break;
case cohort:
details = gettext('Any divided discussion topics are divided based on cohort.');
break;
default:
break;
}
self.showMessage(
StringUtils.interpolate(
gettext('Your changes have been saved. {details}'),
{details: details},
true
)
);
}).fail(function(result) {
var errorMessage = null,
jsonResponse;
try {
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 have encountered an error. Refresh your browser and then try again.'); // eslint-disable-line max-len
}
showErrorMessage(errorMessage);
});
},
updateTopicVisibility: function(selectedScheme, topicNav) {
if (selectedScheme === none) {
topicNav.addClass(hiddenClass);
} else {
topicNav.removeClass(hiddenClass);
}
},
getSectionCss: function(section) { getSectionCss: function(section) {
return ".instructor-nav .nav-item [data-section='" + section + "']"; return ".instructor-nav .nav-item [data-section='" + section + "']";
}, },
showDiscussionTopics: function() { renderTopics: function() {
var dividedDiscussionsElement = this.$('.discussions-nav'); var dividedDiscussionsElement = this.$('.discussions-nav');
if (!this.CourseWideDiscussionsView) { if (!this.CourseWideDiscussionsView) {
this.CourseWideDiscussionsView = new CourseWideDiscussionsView({ this.CourseWideDiscussionsView = new CourseWideDiscussionsView({
......
...@@ -20,22 +20,26 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers ...@@ -20,22 +20,26 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers
createMockDiscussionsSettingsJson = function(dividedInlineDiscussions, createMockDiscussionsSettingsJson = function(dividedInlineDiscussions,
dividedCourseWideDiscussions, dividedCourseWideDiscussions,
alwaysDivideInlineDiscussions) { alwaysDivideInlineDiscussions,
availableDivisionSchemes) {
return { return {
id: 0, id: 0,
divided_inline_discussions: dividedInlineDiscussions || [], divided_inline_discussions: dividedInlineDiscussions || [],
divided_course_wide_discussions: dividedCourseWideDiscussions || [], divided_course_wide_discussions: dividedCourseWideDiscussions || [],
always_divide_inline_discussions: alwaysDivideInlineDiscussions || false always_divide_inline_discussions: alwaysDivideInlineDiscussions || false,
available_division_schemes: availableDivisionSchemes || ['cohort']
}; };
}; };
createMockDiscussionsSettings = function(dividedInlineDiscussions, createMockDiscussionsSettings = function(dividedInlineDiscussions,
dividedCourseWideDiscussions, dividedCourseWideDiscussions,
alwaysDivideInlineDiscussions) { alwaysDivideInlineDiscussions,
availableDivisionSchemes) {
return new CourseDiscussionsSettingsModel( return new CourseDiscussionsSettingsModel(
createMockDiscussionsSettingsJson(dividedInlineDiscussions, createMockDiscussionsSettingsJson(dividedInlineDiscussions,
dividedCourseWideDiscussions, dividedCourseWideDiscussions,
alwaysDivideInlineDiscussions) alwaysDivideInlineDiscussions,
availableDivisionSchemes)
); );
}; };
...@@ -132,14 +136,14 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers ...@@ -132,14 +136,14 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers
expect($courseWideDiscussionsForm.text()). expect($courseWideDiscussionsForm.text()).
toContain('Course-Wide Discussion Topics'); toContain('Course-Wide Discussion Topics');
expect($courseWideDiscussionsForm.text()). expect($courseWideDiscussionsForm.text()).
toContain('Select the course-wide discussion topics that you want to divide by cohort.'); toContain('Select the course-wide discussion topics that you want to divide.');
// Should see the inline discussions form and its content // Should see the inline discussions form and its content
expect($inlineDiscussionsForm.length).toBe(1); expect($inlineDiscussionsForm.length).toBe(1);
expect($inlineDiscussionsForm.text()). expect($inlineDiscussionsForm.text()).
toContain('Content-Specific Discussion Topics'); toContain('Content-Specific Discussion Topics');
expect($inlineDiscussionsForm.text()). expect($inlineDiscussionsForm.text()).
toContain('Specify whether content-specific discussion topics are divided by cohort.'); toContain('Specify whether content-specific discussion topics are divided.');
}; };
beforeEach(function() { beforeEach(function() {
......
...@@ -1241,6 +1241,36 @@ ...@@ -1241,6 +1241,36 @@
// -------------------- // --------------------
.instructor-dashboard-wrapper-2 section.idash-section#discussions_management { .instructor-dashboard-wrapper-2 section.idash-section#discussions_management {
.division-scheme-container {
// See https://css-tricks.com/snippets/css/a-guide-to-flexbox/
display: flex;
flex-direction: column;
justify-content: space-between;
.division-scheme {
font-size: 18px;
}
.division-scheme-item {
padding-left: 1%;
padding-right: 1%;
float: left;
}
.three-column-layout {
max-width: 33%;
}
.two-column-layout {
max-width: 50%;
}
.field-message {
font-size: 13px;
}
}
// cohort management
.form-submit { .form-submit {
@include idashbutton($uxpl-blue-base); @include idashbutton($uxpl-blue-base);
@include font-size(14); @include font-size(14);
......
<!-- Discussion Topics. --> <!-- Discussion Topics. -->
<div class="discussions-nav" id="discussions-management" tabindex="-1"> <div class="discussions-nav" id="discussions-management" tabindex="-1">
<div class="hd hd-3 subsection-title" id="division-scheme-title"><%- gettext('Specify whether discussion topics are divided') %></div>
<div class="division-scheme-container">
<div class="division-scheme-items" role="group" aria-labelledby="division-scheme-title">
<% for (var i = 0; i < availableSchemes.length; i++) { %>
<div class="division-scheme-item <%- layoutClass %> <% if (!availableSchemes[i].enabled) { %>is-hidden<% } %>">
<label class="division-scheme-label">
<input class="division-scheme <%- availableSchemes[i].key %>" type="radio" name="division-scheme"
value="<%- availableSchemes[i].key %>" aria-describedby="<%- availableSchemes[i].key %>-description"
<% if (availableSchemes[i].selected) { %>
checked
<% } %>
>
<%- availableSchemes[i].displayName %>
</label>
<p class='field-message' id="<%- availableSchemes[i].key %>-description"><%- availableSchemes[i].descriptiveText %></p>
</div>
<% } %>
</div>
</div>
<div class="topic-division-nav">
<div class="course-wide-discussions-nav"></div> <div class="course-wide-discussions-nav"></div>
<div class="inline-discussions-nav"></div> <div class="inline-discussions-nav"></div>
</div>
</div> </div>
<h3 class="hd hd-3 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"> <form action="" method="post" id="cohort-course-wide-discussions-form" class="cohort-course-wide-discussions-form">
<div class="wrapper discussions-management-supplemental"> <div class="wrapper discussions-management-supplemental">
<div class="form-fields"> <div class="form-fields">
<div class="form-field"> <div class="form-field">
<div class="course-wide-discussion-topics"> <div class="course-wide-discussion-topics">
<h4 class="hd hd-4 subsection-title"><%- gettext('Course-Wide Discussion Topics') %></h4> <h4 class="hd hd-4 subsection-title"><%- gettext('Course-Wide Discussion Topics') %></h4>
<p><%- gettext('Select the course-wide discussion topics that you want to divide by cohort.') %></p> <p><%- gettext('Select the course-wide discussion topics that you want to divide.') %></p>
<div class="field"> <div class="field">
<ul class="discussions-wrapper"><%= HtmlUtils.ensureHtml(courseWideTopicsHtml) %></ul> <ul class="discussions-wrapper"><%= HtmlUtils.ensureHtml(courseWideTopicsHtml) %></ul>
</div> </div>
......
...@@ -6,17 +6,17 @@ ...@@ -6,17 +6,17 @@
<div class="form-field"> <div class="form-field">
<div class="inline-discussion-topics"> <div class="inline-discussion-topics">
<h4 class="hd hd-4 subsection-title"><%- gettext('Content-Specific Discussion Topics') %></h4> <h4 class="hd hd-4 subsection-title"><%- gettext('Content-Specific Discussion Topics') %></h4>
<p><%- gettext('Specify whether content-specific discussion topics are divided by cohort.') %></p> <p><%- gettext('Specify whether content-specific discussion topics are divided.') %></p>
<div class="always_divide_inline_discussions"> <div class="always_divide_inline_discussions">
<label> <label>
<input type="radio" name="inline" class="check-all-inline-discussions" <%- alwaysDivideInlineDiscussions ? 'checked="checked"' : '' %>/> <input type="radio" name="inline" class="check-all-inline-discussions" <%- alwaysDivideInlineDiscussions ? 'checked="checked"' : '' %>/>
<span class="all-inline-discussions"><%- gettext('Always cohort content-specific discussion topics') %></span> <span class="all-inline-discussions"><%- gettext('Always divide content-specific discussion topics') %></span>
</label> </label>
</div> </div>
<div class="divide_inline_discussions"> <div class="divide_inline_discussions">
<label> <label>
<input type="radio" name="inline" class="check-cohort-inline-discussions" <%- alwaysDivideInlineDiscussions ? '' : 'checked="checked"' %>/> <input type="radio" name="inline" class="check-cohort-inline-discussions" <%- alwaysDivideInlineDiscussions ? '' : 'checked="checked"' %>/>
<span class="all-inline-discussions"><%- gettext('Cohort selected content-specific discussion topics') %></span> <span class="all-inline-discussions"><%- gettext('Divide the selected content-specific discussion topics') %></span>
</label> </label>
</div> </div>
<hr class="divider divider-lv1" /> <hr class="divider divider-lv1" />
......
...@@ -509,7 +509,7 @@ urlpatterns += ( ...@@ -509,7 +509,7 @@ urlpatterns += (
r'^courses/{}/discussions/settings$'.format( r'^courses/{}/discussions/settings$'.format(
settings.COURSE_KEY_PATTERN, settings.COURSE_KEY_PATTERN,
), ),
'openedx.core.djangoapps.course_groups.views.course_discussions_settings_handler', 'lms.djangoapps.discussion.views.course_discussions_settings_handler',
name='course_discussions_settings', name='course_discussions_settings',
), ),
...@@ -560,7 +560,7 @@ urlpatterns += ( ...@@ -560,7 +560,7 @@ urlpatterns += (
r'^courses/{}/discussion/topics$'.format( r'^courses/{}/discussion/topics$'.format(
settings.COURSE_KEY_PATTERN, settings.COURSE_KEY_PATTERN,
), ),
'openedx.core.djangoapps.course_groups.views.discussion_topics', 'lms.djangoapps.discussion.views.discussion_topics',
name='discussion_topics', name='discussion_topics',
), ),
url( url(
......
...@@ -63,25 +63,10 @@ class CourseCohortSettingsFactory(DjangoModelFactory): ...@@ -63,25 +63,10 @@ class CourseCohortSettingsFactory(DjangoModelFactory):
always_cohort_inline_discussions = False always_cohort_inline_discussions = False
def topic_name_to_id(course, name):
"""
Given a discussion topic name, return an id for that name (includes
course and url_name).
"""
return "{course}_{run}_{name}".format(
course=course.location.course,
run=course.url_name,
name=name
)
def config_course_cohorts_legacy( def config_course_cohorts_legacy(
course, course,
discussions,
cohorted, cohorted,
cohorted_discussions=None, auto_cohort_groups=None
auto_cohort_groups=None,
always_cohort_inline_discussions=None
): ):
""" """
Given a course with no discussion set up, add the discussions and set Given a course with no discussion set up, add the discussions and set
...@@ -93,39 +78,19 @@ def config_course_cohorts_legacy( ...@@ -93,39 +78,19 @@ def config_course_cohorts_legacy(
Arguments: Arguments:
course: CourseDescriptor course: CourseDescriptor
discussions: list of topic names strings. Picks ids and sort_keys
automatically.
cohorted: bool. cohorted: bool.
cohorted_discussions: optional list of topic names. If specified,
converts them to use the same ids as topic names.
auto_cohort_groups: optional list of strings auto_cohort_groups: optional list of strings
(names of groups to put students into). (names of groups to put students into).
Returns: Returns:
Nothing -- modifies course in place. Nothing -- modifies course in place.
""" """
def to_id(name): course.discussion_topics = {}
"""
Helper method to convert a discussion topic name to a database identifier
"""
return topic_name_to_id(course, name)
topics = dict((name, {"sort_key": "A",
"id": to_id(name)})
for name in discussions)
course.discussion_topics = topics
config = {"cohorted": cohorted} config = {"cohorted": cohorted}
if cohorted_discussions is not None:
config["cohorted_discussions"] = [to_id(name)
for name in cohorted_discussions]
if auto_cohort_groups is not None: if auto_cohort_groups is not None:
config["auto_cohort_groups"] = auto_cohort_groups config["auto_cohort_groups"] = auto_cohort_groups
if always_cohort_inline_discussions is not None:
config["always_cohort_inline_discussions"] = always_cohort_inline_discussions
course.cohort_config = config course.cohort_config = config
try: try:
...@@ -136,52 +101,10 @@ def config_course_cohorts_legacy( ...@@ -136,52 +101,10 @@ def config_course_cohorts_legacy(
# pylint: disable=dangerous-default-value # pylint: disable=dangerous-default-value
def config_course_discussions(
course,
discussion_topics={},
divided_discussions=[],
always_divide_inline_discussions=False
):
"""
Set discussions and configure divided discussions for a course.
Arguments:
course: CourseDescriptor
discussion_topics (Dict): Discussion topic names. Picks ids and
sort_keys automatically.
divided_discussions: Discussion topics to divide. Converts the
list to use the same ids as discussion topic names.
always_divide_inline_discussions (bool): Whether inline discussions
should be divided by default.
Returns:
Nothing -- modifies course in place.
"""
def to_id(name):
"""Convert name to id."""
return topic_name_to_id(course, name)
set_course_discussion_settings(
course.id,
divided_discussions=[to_id(name) for name in divided_discussions],
always_divide_inline_discussions=always_divide_inline_discussions,
division_scheme=CourseDiscussionSettings.COHORT,
)
course.discussion_topics = dict((name, {"sort_key": "A", "id": to_id(name)})
for name in discussion_topics)
try:
# Not implemented for XMLModulestore, which is used by test_cohorts.
modulestore().update_item(course, ModuleStoreEnum.UserID.test)
except NotImplementedError:
pass
# pylint: disable=dangerous-default-value
def config_course_cohorts( def config_course_cohorts(
course, course,
is_cohorted, is_cohorted,
discussion_division_scheme=CourseDiscussionSettings.COHORT,
auto_cohorts=[], auto_cohorts=[],
manual_cohorts=[], manual_cohorts=[],
): ):
...@@ -191,6 +114,8 @@ def config_course_cohorts( ...@@ -191,6 +114,8 @@ def config_course_cohorts(
Arguments: Arguments:
course: CourseDescriptor course: CourseDescriptor
is_cohorted (bool): Is the course cohorted? is_cohorted (bool): Is the course cohorted?
discussion_division_scheme (String): the division scheme for discussions. Default is
CourseDiscussionSettings.COHORT.
auto_cohorts (list): Names of auto cohorts to create. auto_cohorts (list): Names of auto cohorts to create.
manual_cohorts (list): Names of manual cohorts to create. manual_cohorts (list): Names of manual cohorts to create.
...@@ -201,7 +126,7 @@ def config_course_cohorts( ...@@ -201,7 +126,7 @@ def config_course_cohorts(
set_course_cohorted(course.id, is_cohorted) set_course_cohorted(course.id, is_cohorted)
set_course_discussion_settings( set_course_discussion_settings(
course.id, course.id,
division_scheme=CourseDiscussionSettings.COHORT, division_scheme=discussion_division_scheme,
) )
for cohort_name in auto_cohorts: for cohort_name in auto_cohorts:
......
...@@ -350,7 +350,6 @@ class TestCohorts(ModuleStoreTestCase): ...@@ -350,7 +350,6 @@ class TestCohorts(ModuleStoreTestCase):
# This will have no effect on lms side as we are already done with migrations # This will have no effect on lms side as we are already done with migrations
config_course_cohorts_legacy( config_course_cohorts_legacy(
course, course,
discussions=[],
cohorted=True, cohorted=True,
auto_cohort_groups=["OtherGroup"] auto_cohort_groups=["OtherGroup"]
) )
...@@ -393,7 +392,6 @@ class TestCohorts(ModuleStoreTestCase): ...@@ -393,7 +392,6 @@ class TestCohorts(ModuleStoreTestCase):
# This will have no effect on lms side as we are already done with migrations # This will have no effect on lms side as we are already done with migrations
config_course_cohorts_legacy( config_course_cohorts_legacy(
course, course,
discussions=[],
cohorted=True, cohorted=True,
auto_cohort_groups=["AutoGroup"] auto_cohort_groups=["AutoGroup"]
) )
......
...@@ -6,36 +6,34 @@ Tests for course group views ...@@ -6,36 +6,34 @@ Tests for course group views
import json import json
from collections import namedtuple from collections import namedtuple
from datetime import datetime
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.http import Http404 from django.http import Http404
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django_comment_common.models import CourseDiscussionSettings
from django_comment_common.utils import get_course_discussion_settings
from student.models import CourseEnrollment from student.models import CourseEnrollment
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from xmodule.modulestore.tests.factories import ItemFactory
from lms.djangoapps.django_comment_client.constants import TYPE_ENTRY, TYPE_SUBCATEGORY
from openedx.core.djangolib.testing.utils import skip_unless_lms
from ..models import CourseUserGroup, CourseCohort from ..models import CourseUserGroup, CourseCohort
from ..views import ( from ..views import (
course_cohort_settings_handler, course_discussions_settings_handler, course_cohort_settings_handler,
cohort_handler, users_in_cohort, cohort_handler, users_in_cohort,
add_users_to_cohort, remove_user_from_cohort, add_users_to_cohort, remove_user_from_cohort,
link_cohort_to_partition_group, discussion_topics link_cohort_to_partition_group,
) )
from ..cohorts import ( from ..cohorts import (
get_cohort, get_cohort_by_name, get_cohort_by_id, get_cohort, get_cohort_by_name, get_cohort_by_id,
DEFAULT_COHORT_NAME, get_group_info_for_cohort DEFAULT_COHORT_NAME, get_group_info_for_cohort
) )
from .helpers import ( from .helpers import (
config_course_cohorts, config_course_discussions, config_course_cohorts_legacy, CohortFactory, CourseCohortFactory, topic_name_to_id config_course_cohorts, config_course_cohorts_legacy, CohortFactory, CourseCohortFactory
) )
...@@ -101,40 +99,6 @@ class CohortViewsTestCase(ModuleStoreTestCase): ...@@ -101,40 +99,6 @@ class CohortViewsTestCase(ModuleStoreTestCase):
view_args.insert(0, request) view_args.insert(0, request)
self.assertRaises(Http404, view, *view_args) self.assertRaises(Http404, view, *view_args)
def create_divided_discussions(self):
"""
Set up a divided discussion in the system, complete with all the fixings
"""
divided_inline_discussions = ['Topic A']
divided_course_wide_discussions = ["Topic B"]
divided_discussions = divided_inline_discussions + divided_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,
)
config_course_discussions(
self.course,
discussion_topics=discussion_topics,
divided_discussions=divided_discussions
)
return divided_inline_discussions, divided_course_wide_discussions
def get_handler(self, course, cohort=None, expected_response_code=200, handler=cohort_handler): 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. Call a GET on `handler` for a given `course` and return its response as a dict.
...@@ -184,129 +148,6 @@ class CohortViewsTestCase(ModuleStoreTestCase): ...@@ -184,129 +148,6 @@ class CohortViewsTestCase(ModuleStoreTestCase):
@attr(shard=2) @attr(shard=2)
class CourseDiscussionsHandlerTestCase(CohortViewsTestCase):
"""
Tests the course_discussion_settings_handler
"""
def get_expected_response(self):
"""
Returns the static response dict.
"""
return {
u'always_divide_inline_discussions': False,
u'divided_inline_discussions': [],
u'divided_course_wide_discussions': [],
u'id': 1
}
def test_non_staff(self):
"""
Verify that we cannot access course_discussions_settings_handler if we're a non-staff user.
"""
self._verify_non_staff_cannot_access(course_discussions_settings_handler, "GET", [unicode(self.course.id)])
self._verify_non_staff_cannot_access(course_discussions_settings_handler, "PATCH", [unicode(self.course.id)])
def test_update_always_divide_inline_discussion_settings(self):
"""
Verify that course_discussions_settings_handler is working for always_divide_inline_discussions via HTTP PATCH.
"""
config_course_cohorts(self.course, is_cohorted=True)
response = self.get_handler(self.course, handler=course_discussions_settings_handler)
expected_response = self.get_expected_response()
self.assertEqual(response, expected_response)
expected_response['always_divide_inline_discussions'] = True
response = self.patch_handler(self.course, data=expected_response, handler=course_discussions_settings_handler)
self.assertEqual(response, expected_response)
def test_update_course_wide_discussion_settings(self):
"""
Verify that course_discussions_settings_handler is working for divided_course_wide_discussions via HTTP PATCH.
"""
# course-wide discussion
discussion_topics = {
"Topic B": {"id": "Topic B"},
}
config_course_cohorts(self.course, is_cohorted=True)
config_course_discussions(self.course, discussion_topics=discussion_topics)
response = self.get_handler(self.course, handler=course_discussions_settings_handler)
expected_response = self.get_expected_response()
self.assertEqual(response, expected_response)
expected_response['divided_course_wide_discussions'] = [topic_name_to_id(self.course, "Topic B")]
response = self.patch_handler(self.course, data=expected_response, handler=course_discussions_settings_handler)
self.assertEqual(response, expected_response)
def test_update_inline_discussion_settings(self):
"""
Verify that course_discussions_settings_handler is working for divided_inline_discussions via HTTP PATCH.
"""
config_course_cohorts(self.course, is_cohorted=True)
response = self.get_handler(self.course, handler=course_discussions_settings_handler)
expected_response = self.get_expected_response()
self.assertEqual(response, expected_response)
now = datetime.now()
# inline discussion
ItemFactory.create(
parent_location=self.course.location,
category="discussion",
discussion_id="Topic_A",
discussion_category="Chapter",
discussion_target="Discussion",
start=now
)
expected_response['divided_inline_discussions'] = ["Topic_A"]
response = self.patch_handler(self.course, data=expected_response, handler=course_discussions_settings_handler)
self.assertEqual(response, expected_response)
def test_get_settings(self):
"""
Verify that course_discussions_settings_handler is working for HTTP GET.
"""
divided_inline_discussions, divided_course_wide_discussions = self.create_divided_discussions()
response = self.get_handler(self.course, handler=course_discussions_settings_handler)
expected_response = self.get_expected_response()
expected_response['divided_inline_discussions'] = [topic_name_to_id(self.course, name)
for name in divided_inline_discussions]
expected_response['divided_course_wide_discussions'] = [topic_name_to_id(self.course, name)
for name in divided_course_wide_discussions]
self.assertEqual(response, expected_response)
def test_update_settings_with_invalid_field_data_type(self):
"""
Verify that course_discussions_settings_handler return HTTP 400 if field data type is incorrect.
"""
config_course_cohorts(self.course, is_cohorted=True)
response = self.patch_handler(
self.course,
data={'always_divide_inline_discussions': ''},
expected_response_code=400,
handler=course_discussions_settings_handler
)
self.assertEqual(
"Incorrect field type for `{}`. Type must be `{}`".format('always_divide_inline_discussions', bool.__name__),
response.get("error")
)
@attr(shard=2)
class CourseCohortSettingsHandlerTestCase(CohortViewsTestCase): class CourseCohortSettingsHandlerTestCase(CohortViewsTestCase):
""" """
Tests the `course_cohort_settings_handler` view. Tests the `course_cohort_settings_handler` view.
...@@ -345,6 +186,30 @@ class CourseCohortSettingsHandlerTestCase(CohortViewsTestCase): ...@@ -345,6 +186,30 @@ class CourseCohortSettingsHandlerTestCase(CohortViewsTestCase):
self.assertEqual(response, expected_response) self.assertEqual(response, expected_response)
def test_enabling_cohorts_does_not_change_division_scheme(self):
"""
Verify that enabling cohorts on a course does not automatically set the discussion division_scheme
to cohort.
"""
config_course_cohorts(self.course, is_cohorted=False, discussion_division_scheme=CourseDiscussionSettings.NONE)
response = self.get_handler(self.course, handler=course_cohort_settings_handler)
expected_response = self.get_expected_response()
expected_response['is_cohorted'] = False
self.assertEqual(response, expected_response)
self.assertEqual(
CourseDiscussionSettings.NONE, get_course_discussion_settings(self.course.id).division_scheme
)
expected_response['is_cohorted'] = True
response = self.patch_handler(self.course, data=expected_response, handler=course_cohort_settings_handler)
self.assertEqual(response, expected_response)
self.assertEqual(
CourseDiscussionSettings.NONE, get_course_discussion_settings(self.course.id).division_scheme
)
def test_update_settings_with_missing_field(self): def test_update_settings_with_missing_field(self):
""" """
Verify that course_cohort_settings_handler return HTTP 400 if required data field is missing from post data. Verify that course_cohort_settings_handler return HTTP 400 if required data field is missing from post data.
...@@ -492,7 +357,7 @@ class CohortHandlerTestCase(CohortViewsTestCase): ...@@ -492,7 +357,7 @@ class CohortHandlerTestCase(CohortViewsTestCase):
# set auto_cohort_groups # set auto_cohort_groups
# these cohort config will have not effect on lms side as we are already done with migrations # these cohort config will have not effect on lms side as we are already done with migrations
config_course_cohorts_legacy(self.course, [], cohorted=True, auto_cohort_groups=["AutoGroup"]) config_course_cohorts_legacy(self.course, cohorted=True, auto_cohort_groups=["AutoGroup"])
# We should expect the DoesNotExist exception because above cohort config have # We should expect the DoesNotExist exception because above cohort config have
# no effect on lms side so as a result there will be no AutoGroup cohort present # no effect on lms side so as a result there will be no AutoGroup cohort present
...@@ -1270,60 +1135,3 @@ class RemoveUserFromCohortTestCase(CohortViewsTestCase): ...@@ -1270,60 +1135,3 @@ class RemoveUserFromCohortTestCase(CohortViewsTestCase):
cohort = CohortFactory(course_id=self.course.id, users=[user]) cohort = CohortFactory(course_id=self.course.id, users=[user])
response_dict = self.request_remove_user_from_cohort(user.username, cohort) response_dict = self.request_remove_user_from_cohort(user.username, cohort)
self.verify_removed_user_from_cohort(user.username, response_dict, cohort) self.verify_removed_user_from_cohort(user.username, response_dict, cohort)
@attr(shard=2)
@skip_unless_lms
class CourseDividedDiscussionTopicsTestCase(CohortViewsTestCase):
"""
Tests the `divide_discussion_topics` view.
"""
def test_non_staff(self):
"""
Verify that we cannot access divide_discussion_topics if we're a non-staff user.
"""
self._verify_non_staff_cannot_access(discussion_topics, "GET", [unicode(self.course.id)])
def test_get_discussion_topics(self):
"""
Verify that divide_discussion_topics is working for HTTP GET.
"""
# create inline & course-wide discussion to verify the different map.
self.create_divided_discussions()
response = self.get_handler(self.course, handler=discussion_topics)
start_date = response['inline_discussions']['subcategories']['Chapter']['start_date']
expected_response = {
"course_wide_discussions": {
'children': [['Topic B', TYPE_ENTRY]],
'entries': {
'Topic B': {
'sort_key': 'A',
'is_divided': 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', TYPE_ENTRY]],
'entries': {
'Discussion': {
'sort_key': None,
'is_divided': True,
'id': topic_name_to_id(self.course, "Topic A"),
'start_date': start_date
}
},
'sort_key': 'Chapter',
'start_date': start_date
}
},
'children': [['Chapter', TYPE_SUBCATEGORY]]
}
}
self.assertEqual(response, expected_response)
...@@ -15,11 +15,7 @@ from django.http import Http404, HttpResponseBadRequest ...@@ -15,11 +15,7 @@ from django.http import Http404, HttpResponseBadRequest
from django.utils.translation import ugettext from django.utils.translation import ugettext
from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_http_methods, require_POST from django.views.decorators.http import require_http_methods, require_POST
from django_comment_common.models import CourseDiscussionSettings
from django_comment_common.utils import get_course_discussion_settings, set_course_discussion_settings
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
from lms.djangoapps.django_comment_client.constants import TYPE_ENTRY
from lms.djangoapps.django_comment_client.utils import get_discussion_categories_ids, get_discussion_category_map
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from util.json_request import JsonResponse, expect_json from util.json_request import JsonResponse, expect_json
...@@ -74,22 +70,6 @@ def _get_course_cohort_settings_representation(cohort_id, is_cohorted): ...@@ -74,22 +70,6 @@ def _get_course_cohort_settings_representation(cohort_id, is_cohorted):
} }
def _get_course_discussion_settings_representation(course, course_discussion_settings):
"""
Returns a JSON representation of a course discussion settings.
"""
divided_course_wide_discussions, divided_inline_discussions = get_divided_discussions(
course, course_discussion_settings
)
return {
'id': course_discussion_settings.id,
'divided_inline_discussions': divided_inline_discussions,
'divided_course_wide_discussions': divided_course_wide_discussions,
'always_divide_inline_discussions': course_discussion_settings.always_divide_inline_discussions,
}
def _get_cohort_representation(cohort, course): def _get_cohort_representation(cohort, course):
""" """
Returns a JSON representation of a cohort. Returns a JSON representation of a cohort.
...@@ -107,80 +87,6 @@ def _get_cohort_representation(cohort, course): ...@@ -107,80 +87,6 @@ def _get_cohort_representation(cohort, course):
} }
def get_divided_discussions(course, discussion_settings):
"""
Returns the course-wide and inline divided discussion ids separately.
"""
divided_course_wide_discussions = []
divided_inline_discussions = []
course_wide_discussions = [topic['id'] for __, topic in course.discussion_topics.items()]
all_discussions = get_discussion_categories_ids(course, None, include_all=True)
for divided_discussion_id in discussion_settings.divided_discussions:
if divided_discussion_id in course_wide_discussions:
divided_course_wide_discussions.append(divided_discussion_id)
elif divided_discussion_id in all_discussions:
divided_inline_discussions.append(divided_discussion_id)
return divided_course_wide_discussions, divided_inline_discussions
@require_http_methods(("GET", "PATCH"))
@ensure_csrf_cookie
@expect_json
@login_required
def course_discussions_settings_handler(request, course_key_string):
"""
The restful handler for divided discussion setting requests. Requires JSON.
This will raise 404 if user is not staff.
GET
Returns the JSON representation of divided discussion settings for the course.
PATCH
Updates the divided discussion settings for the course. Returns the JSON representation of updated settings.
"""
course_key = CourseKey.from_string(course_key_string)
course = get_course_with_access(request.user, 'staff', course_key)
discussion_settings = get_course_discussion_settings(course_key)
if request.method == 'PATCH':
divided_course_wide_discussions, divided_inline_discussions = get_divided_discussions(
course, discussion_settings
)
settings_to_change = {}
if 'divided_course_wide_discussions' in request.json or 'divided_inline_discussions' in request.json:
divided_course_wide_discussions = request.json.get(
'divided_course_wide_discussions', divided_course_wide_discussions
)
divided_inline_discussions = request.json.get(
'divided_inline_discussions', divided_inline_discussions
)
settings_to_change['divided_discussions'] = divided_course_wide_discussions + divided_inline_discussions
if 'always_divide_inline_discussions' in request.json:
settings_to_change['always_divide_inline_discussions'] = request.json.get(
'always_divide_inline_discussions'
)
if not settings_to_change:
return JsonResponse({"error": unicode("Bad Request")}, 400)
try:
if settings_to_change:
discussion_settings = set_course_discussion_settings(course_key, **settings_to_change)
except ValueError as err:
# Note: error message not translated because it is not exposed to the user (UI prevents this state).
return JsonResponse({"error": unicode(err)}, 400)
return JsonResponse(_get_course_discussion_settings_representation(
course,
discussion_settings
))
@require_http_methods(("GET", "PATCH")) @require_http_methods(("GET", "PATCH"))
@ensure_csrf_cookie @ensure_csrf_cookie
@expect_json @expect_json
...@@ -196,7 +102,7 @@ def course_cohort_settings_handler(request, course_key_string): ...@@ -196,7 +102,7 @@ def course_cohort_settings_handler(request, course_key_string):
""" """
course_key = CourseKey.from_string(course_key_string) course_key = CourseKey.from_string(course_key_string)
# Although this course data is not used this method will return 404 is user is not staff # Although this course data is not used this method will return 404 is user is not staff
course = get_course_with_access(request.user, 'staff', course_key) get_course_with_access(request.user, 'staff', course_key)
if request.method == 'PATCH': if request.method == 'PATCH':
if 'is_cohorted' not in request.json: if 'is_cohorted' not in request.json:
...@@ -205,9 +111,6 @@ def course_cohort_settings_handler(request, course_key_string): ...@@ -205,9 +111,6 @@ def course_cohort_settings_handler(request, course_key_string):
is_cohorted = request.json.get('is_cohorted') is_cohorted = request.json.get('is_cohorted')
try: try:
cohorts.set_course_cohorted(course_key, is_cohorted) cohorts.set_course_cohorted(course_key, is_cohorted)
scheme = CourseDiscussionSettings.COHORT if is_cohorted else CourseDiscussionSettings.NONE
scheme_settings = {'division_scheme': scheme}
set_course_discussion_settings(course_key, **scheme_settings)
except ValueError as err: except ValueError as err:
# Note: error message not translated because it is not exposed to the user (UI prevents this state). # 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({"error": unicode(err)}, 400)
...@@ -464,81 +367,3 @@ def debug_cohort_mgmt(request, course_key_string): ...@@ -464,81 +367,3 @@ def debug_cohort_mgmt(request, course_key_string):
kwargs={'course_key': course_key.to_deprecated_string()} kwargs={'course_key': course_key.to_deprecated_string()}
)} )}
return render_to_response('/course_groups/debug.html', context) return render_to_response('/course_groups/debug.html', context)
@expect_json
@login_required
def discussion_topics(request, course_key_string):
"""
The handler for divided discussion categories requests.
This will raise 404 if user is not staff.
Returns the JSON representation of discussion topics w.r.t categories for the course.
Example:
>>> example = {
>>> "course_wide_discussions": {
>>> "entries": {
>>> "General": {
>>> "sort_key": "General",
>>> "is_divided": True,
>>> "id": "i4x-edx-eiorguegnru-course-foobarbaz"
>>> }
>>> }
>>> "children": ["General", "entry"]
>>> },
>>> "inline_discussions" : {
>>> "subcategories": {
>>> "Getting Started": {
>>> "subcategories": {},
>>> "children": [
>>> ["Working with Videos", "entry"],
>>> ["Videos on edX", "entry"]
>>> ],
>>> "entries": {
>>> "Working with Videos": {
>>> "sort_key": None,
>>> "is_divided": False,
>>> "id": "d9f970a42067413cbb633f81cfb12604"
>>> },
>>> "Videos on edX": {
>>> "sort_key": None,
>>> "is_divided": False,
>>> "id": "98d8feb5971041a085512ae22b398613"
>>> }
>>> }
>>> },
>>> "children": ["Getting Started", "subcategory"]
>>> },
>>> }
>>> }
"""
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, request.user, divided_only_if_explicit=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, c_type in discussion_category_map['children']:
if name in course_wide_entries and c_type == TYPE_ENTRY:
course_wide_children.append([name, c_type])
else:
inline_children.append([name, c_type])
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