Adds in radio button patch request and text for page

EDUCATOR-229
This commit is contained in:
cahrens
2017-05-13 21:46:49 -04:00
parent 79c5137a68
commit 36c51450c2
21 changed files with 778 additions and 515 deletions

View File

@@ -63,25 +63,10 @@ class CourseCohortSettingsFactory(DjangoModelFactory):
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(
course,
discussions,
cohorted,
cohorted_discussions=None,
auto_cohort_groups=None,
always_cohort_inline_discussions=None
auto_cohort_groups=None
):
"""
Given a course with no discussion set up, add the discussions and set
@@ -93,39 +78,19 @@ def config_course_cohorts_legacy(
Arguments:
course: CourseDescriptor
discussions: list of topic names strings. Picks ids and sort_keys
automatically.
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
(names of groups to put students into).
Returns:
Nothing -- modifies course in place.
"""
def to_id(name):
"""
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
course.discussion_topics = {}
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:
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
try:
@@ -135,53 +100,11 @@ def config_course_cohorts_legacy(
pass
# pylint: disable=dangerous-default-value
def config_course_discussions(
course,
discussion_topics={},
divided_discussions=[],
always_divide_inline_discussions=False
):
"""
Set discussions and configure divided discussions for a course.
Arguments:
course: CourseDescriptor
discussion_topics (Dict): Discussion topic names. Picks ids and
sort_keys automatically.
divided_discussions: Discussion topics to divide. Converts the
list to use the same ids as discussion topic names.
always_divide_inline_discussions (bool): Whether inline discussions
should be divided by default.
Returns:
Nothing -- modifies course in place.
"""
def to_id(name):
"""Convert name to id."""
return topic_name_to_id(course, name)
set_course_discussion_settings(
course.id,
divided_discussions=[to_id(name) for name in divided_discussions],
always_divide_inline_discussions=always_divide_inline_discussions,
division_scheme=CourseDiscussionSettings.COHORT,
)
course.discussion_topics = dict((name, {"sort_key": "A", "id": to_id(name)})
for name in discussion_topics)
try:
# Not implemented for XMLModulestore, which is used by test_cohorts.
modulestore().update_item(course, ModuleStoreEnum.UserID.test)
except NotImplementedError:
pass
# pylint: disable=dangerous-default-value
def config_course_cohorts(
course,
is_cohorted,
discussion_division_scheme=CourseDiscussionSettings.COHORT,
auto_cohorts=[],
manual_cohorts=[],
):
@@ -191,6 +114,8 @@ def config_course_cohorts(
Arguments:
course: CourseDescriptor
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.
manual_cohorts (list): Names of manual cohorts to create.
@@ -201,7 +126,7 @@ def config_course_cohorts(
set_course_cohorted(course.id, is_cohorted)
set_course_discussion_settings(
course.id,
division_scheme=CourseDiscussionSettings.COHORT,
division_scheme=discussion_division_scheme,
)
for cohort_name in auto_cohorts:

View File

@@ -350,7 +350,6 @@ class TestCohorts(ModuleStoreTestCase):
# This will have no effect on lms side as we are already done with migrations
config_course_cohorts_legacy(
course,
discussions=[],
cohorted=True,
auto_cohort_groups=["OtherGroup"]
)
@@ -393,7 +392,6 @@ class TestCohorts(ModuleStoreTestCase):
# This will have no effect on lms side as we are already done with migrations
config_course_cohorts_legacy(
course,
discussions=[],
cohorted=True,
auto_cohort_groups=["AutoGroup"]
)

View File

@@ -6,36 +6,34 @@ Tests for course group views
import json
from collections import namedtuple
from datetime import datetime
from nose.plugins.attrib import attr
from django.contrib.auth.models import User
from django.http import Http404
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.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from xmodule.modulestore.tests.factories import ItemFactory
from 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 ..views import (
course_cohort_settings_handler, course_discussions_settings_handler,
course_cohort_settings_handler,
cohort_handler, users_in_cohort,
add_users_to_cohort, remove_user_from_cohort,
link_cohort_to_partition_group, discussion_topics
link_cohort_to_partition_group,
)
from ..cohorts import (
get_cohort, get_cohort_by_name, get_cohort_by_id,
DEFAULT_COHORT_NAME, get_group_info_for_cohort
)
from .helpers import (
config_course_cohorts, config_course_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):
view_args.insert(0, request)
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):
"""
Call a GET on `handler` for a given `course` and return its response as a dict.
@@ -183,129 +147,6 @@ class CohortViewsTestCase(ModuleStoreTestCase):
return json.loads(response.content)
@attr(shard=2)
class CourseDiscussionsHandlerTestCase(CohortViewsTestCase):
"""
Tests the course_discussion_settings_handler
"""
def get_expected_response(self):
"""
Returns the static response dict.
"""
return {
u'always_divide_inline_discussions': False,
u'divided_inline_discussions': [],
u'divided_course_wide_discussions': [],
u'id': 1
}
def test_non_staff(self):
"""
Verify that we cannot access course_discussions_settings_handler if we're a non-staff user.
"""
self._verify_non_staff_cannot_access(course_discussions_settings_handler, "GET", [unicode(self.course.id)])
self._verify_non_staff_cannot_access(course_discussions_settings_handler, "PATCH", [unicode(self.course.id)])
def test_update_always_divide_inline_discussion_settings(self):
"""
Verify that course_discussions_settings_handler is working for always_divide_inline_discussions via HTTP PATCH.
"""
config_course_cohorts(self.course, is_cohorted=True)
response = self.get_handler(self.course, handler=course_discussions_settings_handler)
expected_response = self.get_expected_response()
self.assertEqual(response, expected_response)
expected_response['always_divide_inline_discussions'] = True
response = self.patch_handler(self.course, data=expected_response, handler=course_discussions_settings_handler)
self.assertEqual(response, expected_response)
def test_update_course_wide_discussion_settings(self):
"""
Verify that course_discussions_settings_handler is working for divided_course_wide_discussions via HTTP PATCH.
"""
# course-wide discussion
discussion_topics = {
"Topic B": {"id": "Topic B"},
}
config_course_cohorts(self.course, is_cohorted=True)
config_course_discussions(self.course, discussion_topics=discussion_topics)
response = self.get_handler(self.course, handler=course_discussions_settings_handler)
expected_response = self.get_expected_response()
self.assertEqual(response, expected_response)
expected_response['divided_course_wide_discussions'] = [topic_name_to_id(self.course, "Topic B")]
response = self.patch_handler(self.course, data=expected_response, handler=course_discussions_settings_handler)
self.assertEqual(response, expected_response)
def test_update_inline_discussion_settings(self):
"""
Verify that course_discussions_settings_handler is working for divided_inline_discussions via HTTP PATCH.
"""
config_course_cohorts(self.course, is_cohorted=True)
response = self.get_handler(self.course, handler=course_discussions_settings_handler)
expected_response = self.get_expected_response()
self.assertEqual(response, expected_response)
now = datetime.now()
# inline discussion
ItemFactory.create(
parent_location=self.course.location,
category="discussion",
discussion_id="Topic_A",
discussion_category="Chapter",
discussion_target="Discussion",
start=now
)
expected_response['divided_inline_discussions'] = ["Topic_A"]
response = self.patch_handler(self.course, data=expected_response, handler=course_discussions_settings_handler)
self.assertEqual(response, expected_response)
def test_get_settings(self):
"""
Verify that course_discussions_settings_handler is working for HTTP GET.
"""
divided_inline_discussions, divided_course_wide_discussions = self.create_divided_discussions()
response = self.get_handler(self.course, handler=course_discussions_settings_handler)
expected_response = self.get_expected_response()
expected_response['divided_inline_discussions'] = [topic_name_to_id(self.course, name)
for name in divided_inline_discussions]
expected_response['divided_course_wide_discussions'] = [topic_name_to_id(self.course, name)
for name in divided_course_wide_discussions]
self.assertEqual(response, expected_response)
def test_update_settings_with_invalid_field_data_type(self):
"""
Verify that course_discussions_settings_handler return HTTP 400 if field data type is incorrect.
"""
config_course_cohorts(self.course, is_cohorted=True)
response = self.patch_handler(
self.course,
data={'always_divide_inline_discussions': ''},
expected_response_code=400,
handler=course_discussions_settings_handler
)
self.assertEqual(
"Incorrect field type for `{}`. Type must be `{}`".format('always_divide_inline_discussions', bool.__name__),
response.get("error")
)
@attr(shard=2)
class CourseCohortSettingsHandlerTestCase(CohortViewsTestCase):
"""
@@ -345,6 +186,30 @@ class CourseCohortSettingsHandlerTestCase(CohortViewsTestCase):
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):
"""
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):
# set auto_cohort_groups
# 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
# no effect on lms side so as a result there will be no AutoGroup cohort present
@@ -1270,60 +1135,3 @@ class RemoveUserFromCohortTestCase(CohortViewsTestCase):
cohort = CohortFactory(course_id=self.course.id, users=[user])
response_dict = self.request_remove_user_from_cohort(user.username, cohort)
self.verify_removed_user_from_cohort(user.username, response_dict, cohort)
@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)

View File

@@ -15,11 +15,7 @@ from django.http import Http404, HttpResponseBadRequest
from django.utils.translation import ugettext
from django.views.decorators.csrf import ensure_csrf_cookie
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 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.locations import SlashSeparatedCourseKey
from util.json_request import JsonResponse, expect_json
@@ -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):
"""
Returns a JSON representation of a cohort.
@@ -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"))
@ensure_csrf_cookie
@expect_json
@@ -196,7 +102,7 @@ def course_cohort_settings_handler(request, 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
course = get_course_with_access(request.user, 'staff', course_key)
get_course_with_access(request.user, 'staff', course_key)
if request.method == 'PATCH':
if 'is_cohorted' not in request.json:
@@ -205,9 +111,6 @@ def course_cohort_settings_handler(request, course_key_string):
is_cohorted = request.json.get('is_cohorted')
try:
cohorts.set_course_cohorted(course_key, is_cohorted)
scheme = CourseDiscussionSettings.COHORT if is_cohorted else CourseDiscussionSettings.NONE
scheme_settings = {'division_scheme': scheme}
set_course_discussion_settings(course_key, **scheme_settings)
except ValueError as err:
# Note: error message not translated because it is not exposed to the user (UI prevents this state).
return JsonResponse({"error": unicode(err)}, 400)
@@ -464,81 +367,3 @@ 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 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)