Make the team discussion thread private
Develop the capability to allow instructors to designate teams to have private discussions. This way, so non-teammembers cannot view discussion. And the intend is for course teams to manage the team membership as well. MST-10
This commit is contained in:
committed by
Simon Chen
parent
855346e9e1
commit
cd3bc236eb
@@ -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) {
|
||||
|
||||
11
lms/djangoapps/discussion/exceptions.py
Normal file
11
lms/djangoapps/discussion/exceptions.py
Normal file
@@ -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
|
||||
@@ -0,0 +1,20 @@
|
||||
## mako
|
||||
|
||||
<%page expression_filter="h"/>
|
||||
|
||||
<%!
|
||||
from django.utils.translation import ugettext as _
|
||||
%>
|
||||
|
||||
<%block name="content">
|
||||
<h2>${_("Private Discussion")}</h2>
|
||||
<div class="alert alert-error" role="alert" aria-labelledby="alert-title-error" tabindex="-1">
|
||||
<span class="icon alert-icon fa fa-exclamation-triangle" aria-hidden="true"></span>
|
||||
|
||||
<div class="alert-message-with-action">
|
||||
<p class="alert-copy">
|
||||
${_("This is a private discussion. You do not have permissions to view this discussion")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</%block>
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
52
lms/djangoapps/teams/api.py
Normal file
52
lms/djangoapps/teams/api.py
Normal file
@@ -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)
|
||||
@@ -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('<div class="discussion-module""></div>');
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
78
lms/djangoapps/teams/tests/test_api.py
Normal file
78
lms/djangoapps/teams/tests/test_api.py
Normal file
@@ -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))
|
||||
Reference in New Issue
Block a user