Commit bc2892f2 by Diana Huang

Merge pull request #9142 from edx/diana/context-aware-comment-client

Make context for threads more implicit.
parents 9d3e2c1a c12c2933
from django_comment_common.models import Role from django_comment_common.models import Role
class ThreadContext(object):
""" An enumeration that represents the context of a thread. Used primarily by the comments service. """
STANDALONE = 'standalone'
COURSE = 'course'
_STUDENT_ROLE_PERMISSIONS = ["vote", "update_thread", "follow_thread", "unfollow_thread", _STUDENT_ROLE_PERMISSIONS = ["vote", "update_thread", "follow_thread", "unfollow_thread",
"update_comment", "create_sub_comment", "unvote", "create_thread", "update_comment", "create_sub_comment", "unvote", "create_thread",
"follow_commentable", "unfollow_commentable", "create_comment", ] "follow_commentable", "unfollow_commentable", "create_comment", ]
......
...@@ -19,7 +19,7 @@ from django_comment_client.tests.group_id import CohortedTopicGroupIdTestMixin, ...@@ -19,7 +19,7 @@ from django_comment_client.tests.group_id import CohortedTopicGroupIdTestMixin,
from django_comment_client.tests.utils import CohortedTestCase from django_comment_client.tests.utils import CohortedTestCase
from django_comment_client.tests.unicode import UnicodeTestMixin from django_comment_client.tests.unicode import UnicodeTestMixin
from django_comment_common.models import Role from django_comment_common.models import Role
from django_comment_common.utils import seed_permissions_roles from django_comment_common.utils import seed_permissions_roles, ThreadContext
from student.tests.factories import CourseEnrollmentFactory, UserFactory, CourseAccessRoleFactory from student.tests.factories import CourseEnrollmentFactory, UserFactory, CourseAccessRoleFactory
from util.testing import UrlResetMixin from util.testing import UrlResetMixin
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
...@@ -240,7 +240,7 @@ class ViewsTestCaseMixin(object): ...@@ -240,7 +240,7 @@ class ViewsTestCaseMixin(object):
data["depth"] = 0 data["depth"] = 0
self._set_mock_request_data(mock_request, data) self._set_mock_request_data(mock_request, data)
def create_thread_helper(self, mock_request, extra_data=None): def create_thread_helper(self, mock_request, extra_request_data=None, extra_response_data=None):
""" """
Issues a request to create a thread and verifies the result. Issues a request to create a thread and verifies the result.
""" """
...@@ -283,8 +283,8 @@ class ViewsTestCaseMixin(object): ...@@ -283,8 +283,8 @@ class ViewsTestCaseMixin(object):
"anonymous": ["false"], "anonymous": ["false"],
"title": ["Hello"], "title": ["Hello"],
} }
if extra_data: if extra_request_data:
thread.update(extra_data) thread.update(extra_request_data)
url = reverse('create_thread', kwargs={'commentable_id': 'i4x-MITx-999-course-Robot_Super_Course', url = reverse('create_thread', kwargs={'commentable_id': 'i4x-MITx-999-course-Robot_Super_Course',
'course_id': self.course_id.to_deprecated_string()}) 'course_id': self.course_id.to_deprecated_string()})
response = self.client.post(url, data=thread) response = self.client.post(url, data=thread)
...@@ -292,14 +292,15 @@ class ViewsTestCaseMixin(object): ...@@ -292,14 +292,15 @@ class ViewsTestCaseMixin(object):
expected_data = { expected_data = {
'thread_type': 'discussion', 'thread_type': 'discussion',
'body': u'this is a post', 'body': u'this is a post',
'context': ThreadContext.COURSE,
'anonymous_to_peers': False, 'user_id': 1, 'anonymous_to_peers': False, 'user_id': 1,
'title': u'Hello', 'title': u'Hello',
'commentable_id': u'i4x-MITx-999-course-Robot_Super_Course', 'commentable_id': u'i4x-MITx-999-course-Robot_Super_Course',
'anonymous': False, 'anonymous': False,
'course_id': unicode(self.course_id), 'course_id': unicode(self.course_id),
} }
if extra_data: if extra_response_data:
expected_data.update(extra_data) expected_data.update(extra_response_data)
mock_request.assert_called_with( mock_request.assert_called_with(
'post', 'post',
'{prefix}/i4x-MITx-999-course-Robot_Super_Course/threads'.format(prefix=CS_PREFIX), '{prefix}/i4x-MITx-999-course-Robot_Super_Course/threads'.format(prefix=CS_PREFIX),
...@@ -387,9 +388,19 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin, V ...@@ -387,9 +388,19 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin, V
def test_create_thread(self, mock_request): def test_create_thread(self, mock_request):
self.create_thread_helper(mock_request) self.create_thread_helper(mock_request)
def test_create_thread_with_context(self, mock_request): def test_create_thread_standalone(self, mock_request):
team = CourseTeamFactory.create(
name="A Team",
course_id=self.course_id,
topic_id='topic_id',
discussion_topic_id="i4x-MITx-999-course-Robot_Super_Course"
)
# Add the student to the team so they can post to the commentable.
team.add_user(self.student)
# create_thread_helper verifies that extra data are passed through to the comments service # create_thread_helper verifies that extra data are passed through to the comments service
self.create_thread_helper(mock_request, extra_data={'context': 'standalone'}) self.create_thread_helper(mock_request, extra_response_data={'context': ThreadContext.STANDALONE})
def test_delete_comment(self, mock_request): def test_delete_comment(self, mock_request):
self._set_mock_request_data(mock_request, { self._set_mock_request_data(mock_request, {
......
...@@ -18,6 +18,7 @@ from courseware.access import has_access ...@@ -18,6 +18,7 @@ from courseware.access import has_access
from util.file import store_uploaded_file from util.file import store_uploaded_file
from courseware.courses import get_course_with_access, get_course_by_id from courseware.courses import get_course_with_access, get_course_by_id
import django_comment_client.settings as cc_settings import django_comment_client.settings as cc_settings
from django_comment_common.utils import ThreadContext
from django_comment_client.utils import ( from django_comment_client.utils import (
add_courseware_context, add_courseware_context,
get_annotated_content_info, get_annotated_content_info,
...@@ -30,7 +31,7 @@ from django_comment_client.utils import ( ...@@ -30,7 +31,7 @@ from django_comment_client.utils import (
discussion_category_id_access, discussion_category_id_access,
get_cached_discussion_id_map, get_cached_discussion_id_map,
) )
from django_comment_client.permissions import check_permissions_by_view, has_permission from django_comment_client.permissions import check_permissions_by_view, has_permission, get_team
from eventtracking import tracker from eventtracking import tracker
import lms.lib.comment_client as cc import lms.lib.comment_client as cc
...@@ -187,8 +188,11 @@ def create_thread(request, course_id, commentable_id): ...@@ -187,8 +188,11 @@ def create_thread(request, course_id, commentable_id):
'title': post["title"], 'title': post["title"],
} }
if 'context' in post: # Check for whether this commentable belongs to a team, and add the right context
params['context'] = post['context'] if get_team(commentable_id) is not None:
params['context'] = ThreadContext.STANDALONE
else:
params['context'] = ThreadContext.COURSE
thread = cc.Thread(**params) thread = cc.Thread(**params)
......
...@@ -8,7 +8,9 @@ from django.test.client import Client, RequestFactory ...@@ -8,7 +8,9 @@ from django.test.client import Client, RequestFactory
from django.test.utils import override_settings from django.test.utils import override_settings
from edxmako.tests import mako_middleware_process_request from edxmako.tests import mako_middleware_process_request
from django_comment_common.utils import ThreadContext
from django_comment_client.forum import views from django_comment_client.forum import views
from django_comment_client.permissions import get_team
from django_comment_client.tests.group_id import ( from django_comment_client.tests.group_id import (
CohortedTopicGroupIdTestMixin, CohortedTopicGroupIdTestMixin,
NonCohortedTopicGroupIdTestMixin NonCohortedTopicGroupIdTestMixin
...@@ -33,6 +35,8 @@ from mock import patch, Mock, ANY, call ...@@ -33,6 +35,8 @@ from mock import patch, Mock, ANY, call
from openedx.core.djangoapps.course_groups.models import CourseUserGroup from openedx.core.djangoapps.course_groups.models import CourseUserGroup
from teams.tests.factories import CourseTeamFactory
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# pylint: disable=missing-docstring # pylint: disable=missing-docstring
...@@ -112,21 +116,23 @@ def make_mock_thread_data( ...@@ -112,21 +116,23 @@ def make_mock_thread_data(
group_id=None, group_id=None,
group_name=None, group_name=None,
commentable_id=None, commentable_id=None,
context=None
): ):
data_commentable_id = (
commentable_id or course.discussion_topics.get('General', {}).get('id') or "dummy_commentable_id"
)
thread_data = { thread_data = {
"id": thread_id, "id": thread_id,
"type": "thread", "type": "thread",
"title": text, "title": text,
"body": text, "body": text,
"commentable_id": ( "commentable_id": data_commentable_id,
commentable_id or course.discussion_topics.get('General', {}).get('id') or "dummy_commentable_id"
),
"resp_total": 42, "resp_total": 42,
"resp_skip": 25, "resp_skip": 25,
"resp_limit": 5, "resp_limit": 5,
"group_id": group_id, "group_id": group_id,
"context": context if context else "course" "context": (
ThreadContext.COURSE if get_team(data_commentable_id) is None else ThreadContext.STANDALONE
)
} }
if group_id is not None: if group_id is not None:
thread_data['group_name'] = group_name thread_data['group_name'] = group_name
...@@ -146,7 +152,6 @@ def make_mock_request_impl( ...@@ -146,7 +152,6 @@ def make_mock_request_impl(
group_id=None, group_id=None,
commentable_id=None, commentable_id=None,
num_thread_responses=1, num_thread_responses=1,
context=None
): ):
def mock_request_impl(*args, **kwargs): def mock_request_impl(*args, **kwargs):
url = args[1] url = args[1]
...@@ -161,7 +166,6 @@ def make_mock_request_impl( ...@@ -161,7 +166,6 @@ def make_mock_request_impl(
num_children=None, num_children=None,
group_id=group_id, group_id=group_id,
commentable_id=commentable_id, commentable_id=commentable_id,
context=context
) )
] ]
} }
...@@ -172,7 +176,6 @@ def make_mock_request_impl( ...@@ -172,7 +176,6 @@ def make_mock_request_impl(
thread_id=thread_id, thread_id=thread_id,
num_children=num_thread_responses, num_children=num_thread_responses,
group_id=group_id, group_id=group_id,
context=context
) )
elif "/users/" in url: elif "/users/" in url:
data = { data = {
...@@ -672,12 +675,20 @@ class InlineDiscussionContextTestCase(ModuleStoreTestCase): ...@@ -672,12 +675,20 @@ class InlineDiscussionContextTestCase(ModuleStoreTestCase):
super(InlineDiscussionContextTestCase, self).setUp() super(InlineDiscussionContextTestCase, self).setUp()
self.course = CourseFactory.create() self.course = CourseFactory.create()
CourseEnrollmentFactory(user=self.user, course_id=self.course.id) CourseEnrollmentFactory(user=self.user, course_id=self.course.id)
self.discussion_topic_id = "dummy_topic"
self.team = CourseTeamFactory(
name="A team",
course_id=self.course.id,
topic_id='topic_id',
discussion_topic_id=self.discussion_topic_id
)
self.team.add_user(self.user) # pylint: disable=no-member
def test_context_can_be_standalone(self, mock_request): def test_context_can_be_standalone(self, mock_request):
mock_request.side_effect = make_mock_request_impl( mock_request.side_effect = make_mock_request_impl(
course=self.course, course=self.course,
text="dummy text", text="dummy text",
context="standalone" commentable_id=self.discussion_topic_id
) )
request = RequestFactory().get("dummy_url") request = RequestFactory().get("dummy_url")
...@@ -686,11 +697,11 @@ class InlineDiscussionContextTestCase(ModuleStoreTestCase): ...@@ -686,11 +697,11 @@ class InlineDiscussionContextTestCase(ModuleStoreTestCase):
response = views.inline_discussion( response = views.inline_discussion(
request, request,
unicode(self.course.id), unicode(self.course.id),
"dummy_topic", self.discussion_topic_id,
) )
json_response = json.loads(response.content) json_response = json.loads(response.content)
self.assertEqual(json_response['discussion_data'][0]['context'], 'standalone') self.assertEqual(json_response['discussion_data'][0]['context'], ThreadContext.STANDALONE)
@patch('lms.lib.comment_client.utils.requests.request') @patch('lms.lib.comment_client.utils.requests.request')
...@@ -1041,8 +1052,15 @@ class InlineDiscussionTestCase(ModuleStoreTestCase): ...@@ -1041,8 +1052,15 @@ class InlineDiscussionTestCase(ModuleStoreTestCase):
self.verify_response(self.send_request(mock_request)) self.verify_response(self.send_request(mock_request))
def test_context(self, mock_request): def test_context(self, mock_request):
response = self.send_request(mock_request, {'context': 'standalone'}) team = CourseTeamFactory(
self.assertEqual(mock_request.call_args[1]['params']['context'], 'standalone') name='Team Name',
topic_id='A topic',
course_id=self.course.id,
discussion_topic_id=self.discussion1.discussion_id
)
team.add_user(self.student) # pylint: disable=no-member
response = self.send_request(mock_request)
self.assertEqual(mock_request.call_args[1]['params']['context'], ThreadContext.STANDALONE)
self.verify_response(response) self.verify_response(response)
......
...@@ -30,7 +30,8 @@ from courseware.access import has_access ...@@ -30,7 +30,8 @@ from courseware.access import has_access
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from ccx.overrides import get_current_ccx from ccx.overrides import get_current_ccx
from django_comment_client.permissions import has_permission from django_comment_common.utils import ThreadContext
from django_comment_client.permissions import has_permission, get_team
from django_comment_client.utils import ( from django_comment_client.utils import (
merge_dict, merge_dict,
extract, extract,
...@@ -111,6 +112,7 @@ def get_threads(request, course, discussion_id=None, per_page=THREADS_PER_PAGE): ...@@ -111,6 +112,7 @@ def get_threads(request, course, discussion_id=None, per_page=THREADS_PER_PAGE):
'text': '', 'text': '',
'course_id': unicode(course.id), 'course_id': unicode(course.id),
'user_id': request.user.id, 'user_id': request.user.id,
'context': ThreadContext.COURSE,
'group_id': get_group_id_for_comments_service(request, course.id, discussion_id), # may raise ValueError 'group_id': get_group_id_for_comments_service(request, course.id, discussion_id), # may raise ValueError
} }
...@@ -118,6 +120,9 @@ def get_threads(request, course, discussion_id=None, per_page=THREADS_PER_PAGE): ...@@ -118,6 +120,9 @@ def get_threads(request, course, discussion_id=None, per_page=THREADS_PER_PAGE):
# comments_service. # comments_service.
if discussion_id is not None: if discussion_id is not None:
default_query_params['commentable_id'] = discussion_id default_query_params['commentable_id'] = discussion_id
# Use the discussion id/commentable id to determine the context we are going to pass through to the backend.
if get_team(discussion_id) is not None:
default_query_params['context'] = ThreadContext.STANDALONE
if not request.GET.get('sort_key'): if not request.GET.get('sort_key'):
# If the user did not select a sort key, use their last used sort key # If the user did not select a sort key, use their last used sort key
...@@ -149,7 +154,6 @@ def get_threads(request, course, discussion_id=None, per_page=THREADS_PER_PAGE): ...@@ -149,7 +154,6 @@ def get_threads(request, course, discussion_id=None, per_page=THREADS_PER_PAGE):
'flagged', 'flagged',
'unread', 'unread',
'unanswered', 'unanswered',
'context',
] ]
) )
) )
......
...@@ -31,20 +31,40 @@ def has_permission(user, permission, course_id=None): ...@@ -31,20 +31,40 @@ def has_permission(user, permission, course_id=None):
CONDITIONS = ['is_open', 'is_author', 'is_question_author', 'is_team_member_if_applicable'] CONDITIONS = ['is_open', 'is_author', 'is_question_author', 'is_team_member_if_applicable']
def get_team(commentable_id):
""" Returns the team that the commentable_id belongs to if it exists. Returns None otherwise. """
request_cache_dict = RequestCache.get_request_cache().data
cache_key = "django_comment_client.team_commentable.{}".format(commentable_id)
if cache_key in request_cache_dict:
return request_cache_dict[cache_key]
try:
team = CourseTeam.objects.get(discussion_topic_id=commentable_id)
except CourseTeam.DoesNotExist:
team = None
request_cache_dict[cache_key] = team
return team
def _check_condition(user, condition, content): def _check_condition(user, condition, content):
def check_open(user, content): """ Check whether or not the given condition applies for the given user and content. """
def check_open(_user, content):
""" Check whether the content is open. """
try: try:
return content and not content['closed'] return content and not content['closed']
except KeyError: except KeyError:
return False return False
def check_author(user, content): def check_author(user, content):
""" Check if the given user is the author of the content. """
try: try:
return content and content['user_id'] == str(user.id) return content and content['user_id'] == str(user.id)
except KeyError: except KeyError:
return False return False
def check_question_author(user, content): def check_question_author(user, content):
""" Check if the given user is the author of the original question for both threads and comments. """
if not content: if not content:
return False return False
try: try:
...@@ -69,17 +89,16 @@ def _check_condition(user, condition, content): ...@@ -69,17 +89,16 @@ def _check_condition(user, condition, content):
cache_key = "django_comment_client.check_team_member.{}.{}".format(user.id, commentable_id) cache_key = "django_comment_client.check_team_member.{}.{}".format(user.id, commentable_id)
if cache_key in request_cache_dict: if cache_key in request_cache_dict:
return request_cache_dict[cache_key] return request_cache_dict[cache_key]
team = CourseTeam.objects.get(discussion_topic_id=commentable_id) team = get_team(commentable_id)
passes_condition = team.users.filter(id=user.id).count() > 0 if team is None:
passes_condition = True
else:
passes_condition = team.users.filter(id=user.id).exists()
request_cache_dict[cache_key] = passes_condition request_cache_dict[cache_key] = passes_condition
except KeyError: except KeyError:
# We do not expect KeyError in production-- it usually indicates an improper test mock. # We do not expect KeyError in production-- it usually indicates an improper test mock.
logging.warning("Did not find key commentable_id in content.") logging.warning("Did not find key commentable_id in content.")
passes_condition = False passes_condition = False
except CourseTeam.DoesNotExist:
passes_condition = True
request_cache_dict[cache_key] = passes_condition
return passes_condition return passes_condition
handlers = { handlers = {
......
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