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:
Kyle McCormick
2019-10-04 16:40:13 -04:00
committed by Simon Chen
parent 855346e9e1
commit cd3bc236eb
9 changed files with 338 additions and 210 deletions

View File

@@ -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) {

View 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

View File

@@ -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>

View File

@@ -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(

View File

@@ -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()

View 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)

View File

@@ -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);
});
});
});

View File

@@ -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: [],

View 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))