diff --git a/common/static/common/js/discussion/views/discussion_inline_view.js b/common/static/common/js/discussion/views/discussion_inline_view.js
index 670a8b6fee..67248861da 100644
--- a/common/static/common/js/discussion/views/discussion_inline_view.js
+++ b/common/static/common/js/discussion/views/discussion_inline_view.js
@@ -176,9 +176,15 @@
this.threadListView.$('.is-active').focus();
},
+ hideDiscussion: function() {
+ this.$('section.discussion').addClass('is-hidden');
+ this.toggleDiscussionBtn.removeClass('shown');
+ this.toggleDiscussionBtn.find('.button-text').text(gettext('Show Discussion'));
+ this.showed = false;
+ },
+
toggleDiscussion: function() {
var self = this;
-
if (this.showed) {
this.hideDiscussion();
} else {
@@ -188,25 +194,27 @@
this.$('section.discussion').removeClass('is-hidden');
this.showed = true;
} else {
- this.loadDiscussions(this.$el, function() {
- self.hideDiscussion();
- DiscussionUtil.discussionAlert(
- gettext('Error'),
- gettext('This discussion could not be loaded. Refresh the page and try again.')
- );
+ this.loadDiscussions(this.$el, function(request) {
+ if (request.status === 403 && request.responseText) {
+ DiscussionUtil.discussionAlert(
+ gettext('Warning'),
+ request.responseText
+ );
+ self.$el.text(request.responseText);
+ self.showed = true;
+ } else {
+ self.hideDiscussion();
+ DiscussionUtil.discussionAlert(
+ gettext('Error'),
+ gettext('This discussion could not be loaded. Refresh the page and try again.')
+ );
+ }
});
}
}
this.toggleDiscussionBtn.focus();
},
- hideDiscussion: function() {
- this.$('section.discussion').addClass('is-hidden');
- this.toggleDiscussionBtn.removeClass('shown');
- this.toggleDiscussionBtn.find('.button-text').text(gettext('Show Discussion'));
- this.showed = false;
- },
-
toggleNewPost: function(event) {
event.preventDefault();
if (!this.newPostForm) {
diff --git a/lms/djangoapps/discussion/exceptions.py b/lms/djangoapps/discussion/exceptions.py
new file mode 100644
index 0000000000..4407e16509
--- /dev/null
+++ b/lms/djangoapps/discussion/exceptions.py
@@ -0,0 +1,11 @@
+"""
+Custom exceptions raised by Discussion API.
+"""
+
+
+class TeamDiscussionHiddenFromUserException(BaseException):
+ """
+ This is the exception raised when a user is not
+ permitted to view the discussion thread
+ """
+ pass
diff --git a/lms/djangoapps/discussion/templates/discussion/discussion_private_fragment.html b/lms/djangoapps/discussion/templates/discussion/discussion_private_fragment.html
new file mode 100644
index 0000000000..6ba50e5dd2
--- /dev/null
+++ b/lms/djangoapps/discussion/templates/discussion/discussion_private_fragment.html
@@ -0,0 +1,20 @@
+## mako
+
+<%page expression_filter="h"/>
+
+<%!
+from django.utils.translation import ugettext as _
+%>
+
+<%block name="content">
+
${_("Private Discussion")}
+
+
+
+
+
+ ${_("This is a private discussion. You do not have permissions to view this discussion")}
+
+
+
+%block>
diff --git a/lms/djangoapps/discussion/tests/test_views.py b/lms/djangoapps/discussion/tests/test_views.py
index 6a55025ce7..f8fc0b51f3 100644
--- a/lms/djangoapps/discussion/tests/test_views.py
+++ b/lms/djangoapps/discussion/tests/test_views.py
@@ -415,6 +415,38 @@ class SingleThreadTestCase(ForumsEnableMixin, ModuleStoreTestCase):
"test_thread_id"
)
+ def test_private_team_thread_html(self, mock_request):
+ discussion_topic_id = 'dummy_discussion_id'
+ thread_id = 'test_thread_id'
+ CourseTeamFactory.create(discussion_topic_id=discussion_topic_id)
+ user_not_in_team = UserFactory.create()
+ CourseEnrollmentFactory.create(user=user_not_in_team, course_id=self.course.id)
+ self.client.login(username=user_not_in_team.username, password='test')
+
+ mock_request.side_effect = make_mock_request_impl(
+ course=self.course,
+ text="dummy",
+ thread_id=thread_id,
+ commentable_id=discussion_topic_id
+ )
+ with patch('lms.djangoapps.teams.api.is_team_discussion_private', autospec=True) as mocked:
+ mocked.return_value = True
+ response = self.client.get(
+ reverse('single_thread', kwargs={
+ 'course_id': six.text_type(self.course.id),
+ 'discussion_id': discussion_topic_id,
+ 'thread_id': thread_id,
+ })
+ )
+ self.assertEquals(response.status_code, 200)
+ self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
+ html = response.content.decode('utf-8')
+ # Verify that the access denied error message is in the HTML
+ self.assertIn(
+ 'This is a private discussion. You do not have permissions to view this discussion',
+ html
+ )
+
@ddt.ddt
@patch('requests.request', autospec=True)
@@ -432,18 +464,18 @@ class SingleThreadQueryCountTestCase(ForumsEnableMixin, ModuleStoreTestCase):
# course is outside the context manager that is verifying the number of queries,
# and with split mongo, that method ends up querying disabled_xblocks (which is then
# cached and hence not queried as part of call_single_thread).
- (ModuleStoreEnum.Type.mongo, False, 1, 5, 2, 23, 8),
- (ModuleStoreEnum.Type.mongo, False, 50, 5, 2, 23, 8),
+ (ModuleStoreEnum.Type.mongo, False, 1, 5, 2, 24, 9),
+ (ModuleStoreEnum.Type.mongo, False, 50, 5, 2, 24, 9),
# split mongo: 3 queries, regardless of thread response size.
- (ModuleStoreEnum.Type.split, False, 1, 3, 3, 23, 8),
- (ModuleStoreEnum.Type.split, False, 50, 3, 3, 23, 8),
+ (ModuleStoreEnum.Type.split, False, 1, 3, 3, 24, 9),
+ (ModuleStoreEnum.Type.split, False, 50, 3, 3, 24, 9),
# Enabling Enterprise integration should have no effect on the number of mongo queries made.
- (ModuleStoreEnum.Type.mongo, True, 1, 5, 2, 23, 8),
- (ModuleStoreEnum.Type.mongo, True, 50, 5, 2, 23, 8),
+ (ModuleStoreEnum.Type.mongo, True, 1, 5, 2, 24, 9),
+ (ModuleStoreEnum.Type.mongo, True, 50, 5, 2, 24, 9),
# split mongo: 3 queries, regardless of thread response size.
- (ModuleStoreEnum.Type.split, True, 1, 3, 3, 23, 8),
- (ModuleStoreEnum.Type.split, True, 50, 3, 3, 23, 8),
+ (ModuleStoreEnum.Type.split, True, 1, 3, 3, 24, 9),
+ (ModuleStoreEnum.Type.split, True, 50, 3, 3, 24, 9),
)
@ddt.unpack
def test_number_of_mongo_queries(
@@ -654,6 +686,22 @@ class SingleThreadAccessTestCase(CohortedTestCase):
)
self.assertEqual(resp.status_code, 200)
+ def test_private_team_thread(self, mock_request):
+ CourseTeamFactory.create(discussion_topic_id='dummy_discussion_id')
+ user_not_in_team = UserFactory.create()
+ CourseEnrollmentFactory(user=user_not_in_team, course_id=self.course.id)
+
+ with patch('lms.djangoapps.teams.api.is_team_discussion_private', autospec=True) as mocked:
+ mocked.return_value = True
+ response = self.call_view(
+ mock_request,
+ 'non_cohorted_topic',
+ user_not_in_team,
+ None
+ )
+ self.assertEqual(403, response.status_code)
+ self.assertEqual(views.TEAM_PERMISSION_MESSAGE, response.content)
+
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
class SingleThreadGroupIdTestCase(CohortedTestCase, GroupIdAssertionMixin):
@@ -912,6 +960,7 @@ class InlineDiscussionContextTestCase(ForumsEnableMixin, ModuleStoreTestCase):
)
self.team.add_user(self.user)
+ self.user_not_in_team = UserFactory.create()
def test_context_can_be_standalone(self, mock_request):
mock_request.side_effect = make_mock_request_impl(
@@ -932,6 +981,28 @@ class InlineDiscussionContextTestCase(ForumsEnableMixin, ModuleStoreTestCase):
json_response = json.loads(response.content.decode('utf-8'))
self.assertEqual(json_response['discussion_data'][0]['context'], ThreadContext.STANDALONE)
+ def test_private_team_discussion(self, mock_request):
+ # First set the team discussion to be private
+ CourseEnrollmentFactory(user=self.user_not_in_team, course_id=self.course.id)
+ request = RequestFactory().get("dummy_url")
+ request.user = self.user_not_in_team
+
+ mock_request.side_effect = make_mock_request_impl(
+ course=self.course,
+ text="dummy text",
+ commentable_id=self.discussion_topic_id
+ )
+
+ with patch('lms.djangoapps.teams.api.is_team_discussion_private', autospec=True) as mocked:
+ mocked.return_value = True
+ response = views.inline_discussion(
+ request,
+ six.text_type(self.course.id),
+ self.discussion_topic_id,
+ )
+ self.assertEqual(response.status_code, 403)
+ self.assertEqual(response.content.decode('utf-8'), views.TEAM_PERMISSION_MESSAGE)
+
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
class InlineDiscussionGroupIdTestCase(
diff --git a/lms/djangoapps/discussion/views.py b/lms/djangoapps/discussion/views.py
index 92838fcc10..5d43d4ade5 100644
--- a/lms/djangoapps/discussion/views.py
+++ b/lms/djangoapps/discussion/views.py
@@ -11,12 +11,13 @@ from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.contrib.staticfiles.storage import staticfiles_storage
-from django.http import Http404, HttpResponseServerError
+from django.http import Http404, HttpResponseForbidden, HttpResponseServerError
from django.shortcuts import render_to_response
from django.template.context_processors import csrf
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.translation import get_language_bidi
+from django.utils.translation import ugettext_lazy as _
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_GET, require_http_methods
from edx_django_utils.monitoring import function_trace
@@ -31,7 +32,7 @@ from lms.djangoapps.courseware.courses import get_course_with_access
from lms.djangoapps.courseware.views.views import CourseTabView
from lms.djangoapps.discussion.django_comment_client.base.views import track_thread_viewed_event
from lms.djangoapps.discussion.django_comment_client.constants import TYPE_ENTRY
-from lms.djangoapps.discussion.django_comment_client.permissions import get_team, has_permission
+from lms.djangoapps.discussion.django_comment_client.permissions import has_permission
from lms.djangoapps.discussion.django_comment_client.utils import (
add_courseware_context,
available_division_schemes,
@@ -43,7 +44,9 @@ from lms.djangoapps.discussion.django_comment_client.utils import (
is_commentable_divided,
strip_none
)
+from lms.djangoapps.discussion.exceptions import TeamDiscussionHiddenFromUserException
from lms.djangoapps.experiments.utils import get_experiment_user_metadata_context
+from lms.djangoapps.teams import api as team_api
from openedx.core.djangoapps.django_comment_common.models import CourseDiscussionSettings
from openedx.core.djangoapps.django_comment_common.utils import (
ThreadContext,
@@ -66,6 +69,7 @@ INLINE_THREADS_PER_PAGE = 20
PAGES_NEARBY_DELTA = 2
BOOTSTRAP_DISCUSSION_CSS_PATH = 'css/discussion/lms-discussion-bootstrap.css'
+TEAM_PERMISSION_MESSAGE = _("Access to this discussion is restricted to team members and staff.")
def make_course_settings(course, user, include_category_map=True):
@@ -122,9 +126,11 @@ def get_threads(request, course, user_info, discussion_id=None, per_page=THREADS
if discussion_id is not None:
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:
+ if team_api.get_team_by_discussion(discussion_id) is not None:
default_query_params['context'] = ThreadContext.STANDALONE
+ _check_team_discussion_access(request, course, discussion_id)
+
if not request.GET.get('sort_key'):
# If the user did not select a sort key, use their last used sort key
default_query_params['sort_key'] = user_info.get('default_sort_key') or default_query_params['sort_key']
@@ -200,7 +206,6 @@ def inline_discussion(request, course_key, discussion_id):
"""
Renders JSON for DiscussionModules
"""
-
with function_trace('get_course_and_user_info'):
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
cc_user = cc.User.from_django_user(request.user)
@@ -213,6 +218,8 @@ def inline_discussion(request, course_key, discussion_id):
)
except ValueError:
return HttpResponseServerError('Invalid group_id')
+ except TeamDiscussionHiddenFromUserException:
+ return HttpResponseForbidden(TEAM_PERMISSION_MESSAGE)
with function_trace('get_metadata_for_threads'):
annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info)
@@ -300,10 +307,17 @@ def single_thread(request, course_key, discussion_id, thread_id):
"""
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
request.user.is_community_ta = utils.is_user_community_ta(request.user, course.id)
+
if request.is_ajax():
cc_user = cc.User.from_django_user(request.user)
user_info = cc_user.to_dict()
is_staff = has_permission(request.user, 'openclose_thread', course.id)
+
+ try:
+ _check_team_discussion_access(request, course, discussion_id)
+ except TeamDiscussionHiddenFromUserException:
+ return HttpResponseForbidden(TEAM_PERMISSION_MESSAGE)
+
thread = _load_thread_for_viewing(
request,
course,
@@ -461,7 +475,7 @@ def _create_discussion_board_context(request, base_context, thread=None):
cc_user = cc.User.from_django_user(user)
user_info = context['user_info']
if thread:
-
+ _check_team_discussion_access(request, course, discussion_id)
# Since we're in page render mode, and the discussions UI will request the thread list itself,
# we need only return the thread information for this one.
threads = [thread.to_dict()]
@@ -760,6 +774,20 @@ class DiscussionBoardFragmentView(EdxFragmentView):
fragment = Fragment(html)
self.add_fragment_resource_urls(fragment)
return fragment
+ except TeamDiscussionHiddenFromUserException:
+ log.warning(
+ u'User with id={user_id} tried to view private discussion with id={discussion_id}'.format(
+ user_id=request.user.id,
+ discussion_id=discussion_id
+ )
+ )
+ html = render_to_string('discussion/discussion_private_fragment.html', {
+ 'disable_courseware_js': True,
+ 'uses_pattern_library': True,
+ })
+ fragment = Fragment(html)
+ self.add_fragment_resource_urls(fragment)
+ return fragment
def vendor_js_dependencies(self):
"""
@@ -963,3 +991,13 @@ def get_divided_discussions(course, discussion_settings):
divided_inline_discussions.append(divided_discussion_id)
return divided_course_wide_discussions, divided_inline_discussions
+
+
+def _check_team_discussion_access(request, course, discussion_id):
+ """
+ Helper function to check if the discussion is visible to the user,
+ if the user is on a team, which has the discussion set to private.
+ """
+ user_is_course_staff = has_access(request.user, "staff", course)
+ if not user_is_course_staff and not team_api.discussion_visible_by_user(discussion_id, request.user):
+ raise TeamDiscussionHiddenFromUserException()
diff --git a/lms/djangoapps/teams/api.py b/lms/djangoapps/teams/api.py
new file mode 100644
index 0000000000..d7849b3abd
--- /dev/null
+++ b/lms/djangoapps/teams/api.py
@@ -0,0 +1,52 @@
+"""
+The Python API other app should use to work with Teams feature
+"""
+from __future__ import absolute_import
+
+from lms.djangoapps.teams.models import CourseTeam
+
+
+def get_team_by_discussion(discussion_id):
+ """
+ This is a function to get team object by the discussion_id passed in.
+ If the discussion_id is not associated with any team, we return None
+ """
+ try:
+ return CourseTeam.objects.get(discussion_topic_id=discussion_id)
+ except CourseTeam.DoesNotExist:
+ # When the discussion does not belong to a team. It's visible in
+ # any team context
+ return None
+
+
+def is_team_discussion_private(team):
+ """
+ This is the function to check if the team is configured to have its discussion
+ to be private. We need a way to check the setting on the team.
+ This function also provide ways to toggle the setting of discussion visibility on the
+ individual team level.
+ To be followed up by MST-25
+ """
+ return getattr(team, 'is_discussion_private', False)
+
+
+def user_is_a_team_member(user, team):
+ """
+ Return if the user is a member of the team
+ If the team is not defined, return False
+ """
+ if team:
+ return team.users.filter(id=user.id).exists()
+ return False
+
+
+def discussion_visible_by_user(discussion_id, user):
+ """
+ This function checks whether the discussion should be visible to the user.
+ The discussion should not be visible to the user if
+ * The discussion is part of the Team AND
+ * The team is configured to hide the discussions from non-teammembers AND
+ * The user is not part of the team
+ """
+ team = get_team_by_discussion(discussion_id)
+ return not is_team_discussion_private(team) or user_is_a_team_member(user, team)
diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/team_discussion_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/team_discussion_spec.js
index 9ce818673c..86dfb74222 100644
--- a/lms/djangoapps/teams/static/teams/js/spec/views/team_discussion_spec.js
+++ b/lms/djangoapps/teams/static/teams/js/spec/views/team_discussion_spec.js
@@ -6,8 +6,8 @@ define([
'teams/js/views/team_discussion'
], function(_, AjaxHelpers, DiscussionSpecHelper, TeamSpecHelpers, TeamDiscussionView) {
'use strict';
- xdescribe('TeamDiscussionView', function() {
- var discussionView, createDiscussionView, createPost, expandReplies, postReply;
+ describe('TeamDiscussionView', function() {
+ var discussionView, createDiscussionView;
beforeEach(function() {
setFixtures('');
@@ -18,198 +18,47 @@ define([
DiscussionSpecHelper.setUnderscoreFixtures();
});
- createDiscussionView = function(requests, threads) {
+ createDiscussionView = function(requests, threads, errorStatus, errorBody) {
discussionView = new TeamDiscussionView({
el: '.discussion-module'
});
discussionView.render();
- AjaxHelpers.expectRequest(
- requests, 'GET',
- interpolate(
- '/courses/%(courseID)s/discussion/forum/%(discussionID)s/inline?page=1&ajax=1',
- {
- courseID: TeamSpecHelpers.testCourseID,
- discussionID: TeamSpecHelpers.testTeamDiscussionID
- },
- true
-
- )
- );
- AjaxHelpers.respondWithJson(requests, TeamSpecHelpers.createMockDiscussionResponse(threads));
+ if (errorStatus && errorBody) {
+ AjaxHelpers.respondWithError(
+ requests,
+ errorStatus,
+ errorBody
+ );
+ } else {
+ AjaxHelpers.respondWithJson(
+ requests,
+ TeamSpecHelpers.createMockDiscussionResponse(threads)
+ );
+ }
return discussionView;
};
- createPost = function(requests, view, title, body, threadID) {
- title = title || 'Test title';
- body = body || 'Test body';
- threadID = threadID || '999';
- view.$('.new-post-button').click();
- view.$('.js-post-title').val(title);
- view.$('.js-post-body textarea').val(body);
- view.$('.submit').click();
- AjaxHelpers.expectRequest(
- requests, 'POST',
- interpolate(
- '/courses/%(courseID)s/discussion/%(discussionID)s/threads/create?ajax=1',
- {
- courseID: TeamSpecHelpers.testCourseID,
- discussionID: TeamSpecHelpers.testTeamDiscussionID
- },
- true
- ),
- interpolate(
- 'thread_type=discussion&title=%(title)s&body=%(body)s&anonymous=false&anonymous_to_peers=false&auto_subscribe=true',
- {
- title: title.replace(/ /g, '+'),
- body: body.replace(/ /g, '+')
- },
- true
- )
- );
- AjaxHelpers.respondWithJson(requests, {
- content: TeamSpecHelpers.createMockPostResponse({
- id: threadID,
- title: title,
- body: body
- }),
- annotated_content_info: TeamSpecHelpers.createAnnotatedContentInfo()
- });
- };
-
- expandReplies = function(requests, view, threadID) {
- view.$('.forum-thread-expand').first().click();
- AjaxHelpers.expectRequest(
- requests, 'GET',
- interpolate(
- '/courses/%(courseID)s/discussion/forum/%(discussionID)s/threads/%(threadID)s?ajax=1&resp_skip=0&resp_limit=25',
- {
- courseID: TeamSpecHelpers.testCourseID,
- discussionID: TeamSpecHelpers.testTeamDiscussionID,
- threadID: threadID || '999'
- },
- true
- )
- );
- AjaxHelpers.respondWithJson(requests, {
- content: TeamSpecHelpers.createMockThreadResponse(),
- annotated_content_info: TeamSpecHelpers.createAnnotatedContentInfo()
- });
- };
-
- postReply = function(requests, view, reply, threadID) {
- var replyForm = view.$('.discussion-reply-new').first();
- replyForm.find('.reply-body textarea').val(reply);
- replyForm.find('.discussion-submit-post').click();
- AjaxHelpers.expectRequest(
- requests, 'POST',
- interpolate(
- '/courses/%(courseID)s/discussion/threads/%(threadID)s/reply?ajax=1',
- {
- courseID: TeamSpecHelpers.testCourseID,
- threadID: threadID || '999'
- },
- true
- ),
- 'body=' + reply.replace(/ /g, '+')
- );
- AjaxHelpers.respondWithJson(requests, {
- content: TeamSpecHelpers.createMockThreadResponse({
- body: reply,
- comments_count: 1
- }),
- annotated_content_info: TeamSpecHelpers.createAnnotatedContentInfo()
- });
- };
-
it('can render itself', function() {
var requests = AjaxHelpers.requests(this),
view = createDiscussionView(requests);
- expect(view.$('.discussion-thread').length).toEqual(3);
+ expect(view.$('.forum-nav-thread-list .forum-nav-thread').length).toEqual(3);
});
- it('can create a new post', function() {
+ it('cannot see discussion when user is not part of the team and discussion is set to be private', function() {
var requests = AjaxHelpers.requests(this),
- view = createDiscussionView(requests),
- testTitle = 'New Post',
- testBody = 'New post body',
- newThreadElement;
- createPost(requests, view, testTitle, testBody);
-
- // Expect the first thread to be the new post
- expect(view.$('.discussion-thread').length).toEqual(4);
- newThreadElement = view.$('.discussion-thread').first();
- expect(newThreadElement.find('.post-header-content h1').text().trim()).toEqual(testTitle);
- expect(newThreadElement.find('.post-body').text().trim()).toEqual(testBody);
- });
-
- it('can post a reply', function() {
- var requests = AjaxHelpers.requests(this),
- view = createDiscussionView(requests),
- testReply = 'Test reply',
- testThreadID = '1';
- expandReplies(requests, view, testThreadID);
- postReply(requests, view, testReply, testThreadID);
- expect(view.$('.discussion-response .response-body').text().trim()).toBe(testReply);
- });
-
- it('can post a reply to a new post', function() {
- var requests = AjaxHelpers.requests(this),
- view = createDiscussionView(requests, []),
- testReply = 'Test reply';
- createPost(requests, view);
- expandReplies(requests, view);
- postReply(requests, view, testReply);
- expect(view.$('.discussion-response .response-body').text().trim()).toBe(testReply);
- });
-
- it('cannot move an existing thread to a different topic', function() {
- var requests = AjaxHelpers.requests(this),
- view = createDiscussionView(requests),
- postTopicButton, updatedThreadElement,
- updatedTitle = 'Updated title',
- updatedBody = 'Updated body',
- testThreadID = '1';
- expandReplies(requests, view, testThreadID);
- view.$('.action-more .icon').first().click();
- view.$('.action-edit').first().click();
- postTopicButton = view.$('.post-topic');
- expect(postTopicButton.length).toBe(0);
- view.$('.js-post-post-title').val(updatedTitle);
- view.$('.js-post-body textarea').val(updatedBody);
- view.$('.submit').click();
- AjaxHelpers.expectRequest(
- requests, 'POST',
- interpolate(
- '/courses/%(courseID)s/discussion/%(discussionID)s/threads/create?ajax=1',
- {
- courseID: TeamSpecHelpers.testCourseID,
- discussionID: TeamSpecHelpers.testTeamDiscussionID
- },
- true
- ),
- 'thread_type=discussion&title=&body=Updated+body&anonymous=false&anonymous_to_peers=false&auto_subscribe=true'
- );
- AjaxHelpers.respondWithJson(requests, {
- content: TeamSpecHelpers.createMockPostResponse({
- id: '999', title: updatedTitle, body: updatedBody
- }),
- annotated_content_info: TeamSpecHelpers.createAnnotatedContentInfo()
- });
-
- // Expect the thread to have been updated
- updatedThreadElement = view.$('.discussion-thread').first();
- expect(updatedThreadElement.find('.post-header-content h1').text().trim()).toEqual(updatedTitle);
- expect(updatedThreadElement.find('.post-body').text().trim()).toEqual(updatedBody);
- });
-
- it('cannot move a new thread to a different topic', function() {
- var requests = AjaxHelpers.requests(this),
- view = createDiscussionView(requests);
- createPost(requests, view);
- expandReplies(requests, view);
- view.$('.action-more .icon').first().click();
- view.$('.action-edit').first().click();
- expect(view.$('.post-topic').length).toBe(0);
+ errorMessage = 'Access to this thread is restricted to team members and staff.',
+ view = createDiscussionView(
+ requests,
+ [],
+ 403,
+ errorMessage
+ );
+ expect(view.$el.text().trim().replace(/"/g, '')).toEqual(errorMessage);
+ expect($('.discussion-alert-wrapper p')
+ .text()
+ .trim()
+ .replace(/"/g, '')
+ ).toEqual(errorMessage);
});
});
});
diff --git a/lms/djangoapps/teams/static/teams/js/spec_helpers/team_spec_helpers.js b/lms/djangoapps/teams/static/teams/js/spec_helpers/team_spec_helpers.js
index da9a4e06cb..3b393f2266 100644
--- a/lms/djangoapps/teams/static/teams/js/spec_helpers/team_spec_helpers.js
+++ b/lms/djangoapps/teams/static/teams/js/spec_helpers/team_spec_helpers.js
@@ -143,8 +143,9 @@ define([
};
createMockDiscussionResponse = function(threads) {
+ var responseThreads = threads;
if (_.isUndefined(threads)) {
- threads = [
+ responseThreads = [
createMockPostResponse({id: '1', title: 'First Post'}),
createMockPostResponse({id: '2', title: 'Second Post'}),
createMockPostResponse({id: '3', title: 'Third Post'})
@@ -153,7 +154,7 @@ define([
return {
num_pages: 1,
page: 1,
- discussion_data: threads,
+ discussion_data: responseThreads,
user_info: {
username: testUser,
follower_ids: [],
diff --git a/lms/djangoapps/teams/tests/test_api.py b/lms/djangoapps/teams/tests/test_api.py
new file mode 100644
index 0000000000..1de7f31678
--- /dev/null
+++ b/lms/djangoapps/teams/tests/test_api.py
@@ -0,0 +1,78 @@
+# -*- coding: utf-8 -*-
+"""
+Tests for Python APIs of the Teams app
+"""
+from __future__ import absolute_import
+
+from uuid import uuid4
+import unittest
+
+from opaque_keys.edx.keys import CourseKey
+
+from lms.djangoapps.teams import api as teams_api
+from lms.djangoapps.teams.tests.factories import CourseTeamFactory
+from student.tests.factories import CourseEnrollmentFactory, UserFactory
+from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
+
+COURSE_KEY1 = CourseKey.from_string('edx/history/1')
+COURSE_KEY2 = CourseKey.from_string('edx/history/2')
+
+DISCUSSION_TOPIC_ID = uuid4().hex
+
+
+class PythonAPITests(SharedModuleStoreTestCase):
+ """
+ The set of tests for different API endpoints
+ """
+ @classmethod
+ def setUpClass(cls):
+ super(PythonAPITests, cls).setUpClass()
+ cls.user1 = UserFactory.create(username='user1')
+ cls.user2 = UserFactory.create(username='user2')
+ cls.user3 = UserFactory.create(username='user3')
+
+ for user in (cls.user1, cls.user2, cls.user3):
+ CourseEnrollmentFactory.create(user=user, course_id=COURSE_KEY1)
+
+ CourseEnrollmentFactory.create(user=cls.user3, course_id=COURSE_KEY2)
+
+ cls.team1 = CourseTeamFactory(
+ course_id=COURSE_KEY1,
+ discussion_topic_id=DISCUSSION_TOPIC_ID,
+ team_id='team1'
+ )
+ cls.team2 = CourseTeamFactory(course_id=COURSE_KEY2, team_id='team2')
+
+ cls.team1.add_user(cls.user1)
+ cls.team1.add_user(cls.user2)
+ cls.team2.add_user(cls.user3)
+
+ def test_get_team_by_discussion_non_existence(self):
+ self.assertIsNone(teams_api.get_team_by_discussion('DO_NOT_EXIST'))
+
+ def test_get_team_by_discussion_exists(self):
+ team = teams_api.get_team_by_discussion(DISCUSSION_TOPIC_ID)
+ self.assertEqual(team, self.team1)
+
+ @unittest.skip("This functionality is not yet implemented")
+ def test_is_team_discussion_private_is_private(self):
+ self.assertTrue(teams_api.is_team_discussion_private(self.team1))
+
+ def test_is_team_discussion_private_is_public(self):
+ self.assertFalse(teams_api.is_team_discussion_private(None))
+ self.assertFalse(teams_api.is_team_discussion_private(self.team2))
+
+ def test_user_is_a_team_member(self):
+ self.assertTrue(teams_api.user_is_a_team_member(self.user1, self.team1))
+ self.assertFalse(teams_api.user_is_a_team_member(self.user1, None))
+ self.assertFalse(teams_api.user_is_a_team_member(self.user1, self.team2))
+
+ def test_private_discussion_visible_by_user(self):
+ self.assertTrue(teams_api.discussion_visible_by_user(DISCUSSION_TOPIC_ID, self.user1))
+ self.assertTrue(teams_api.discussion_visible_by_user(DISCUSSION_TOPIC_ID, self.user2))
+ # self.assertFalse(teams_api.discussion_visible_by_user(DISCUSSION_TOPIC_ID, self.user3))
+
+ def test_public_discussion_visible_by_user(self):
+ self.assertTrue(teams_api.discussion_visible_by_user(self.team2.discussion_topic_id, self.user1))
+ self.assertTrue(teams_api.discussion_visible_by_user(self.team2.discussion_topic_id, self.user2))
+ self.assertTrue(teams_api.discussion_visible_by_user('DO_NOT_EXISTS', self.user3))