- This will force the use of the new v2 forum's APIs for Threads & Comment. - Update params for get_user_subscription function. It uses the same structure as we have in the get_user_threads.
822 lines
31 KiB
Python
822 lines
31 KiB
Python
"""
|
|
Tests the forum notification views.
|
|
"""
|
|
import json
|
|
import logging
|
|
from datetime import datetime
|
|
from unittest import mock
|
|
from unittest.mock import ANY, Mock, call, patch
|
|
|
|
import ddt
|
|
import pytest
|
|
from django.conf import settings
|
|
from django.test.client import Client, RequestFactory
|
|
from django.test.utils import override_settings
|
|
from django.urls import reverse
|
|
from django.utils import translation
|
|
from edx_django_utils.cache import RequestCache
|
|
from edx_toggles.toggles.testutils import override_waffle_flag
|
|
from xmodule.modulestore.tests.django_utils import (
|
|
TEST_DATA_SPLIT_MODULESTORE,
|
|
ModuleStoreTestCase,
|
|
SharedModuleStoreTestCase
|
|
)
|
|
from xmodule.modulestore.tests.factories import (
|
|
CourseFactory,
|
|
BlockFactory,
|
|
)
|
|
|
|
from common.djangoapps.course_modes.models import CourseMode
|
|
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
|
|
from common.djangoapps.student.tests.factories import AdminFactory, CourseEnrollmentFactory, UserFactory
|
|
from common.djangoapps.util.testing import UrlResetMixin
|
|
from lms.djangoapps.courseware.exceptions import CourseAccessRedirect
|
|
from lms.djangoapps.discussion import views
|
|
from lms.djangoapps.discussion.django_comment_client.constants import TYPE_ENTRY, TYPE_SUBCATEGORY
|
|
from lms.djangoapps.discussion.django_comment_client.permissions import get_team
|
|
from lms.djangoapps.discussion.django_comment_client.tests.unicode import UnicodeTestMixin
|
|
from lms.djangoapps.discussion.django_comment_client.tests.utils import (
|
|
ForumsEnableMixin,
|
|
config_course_discussions,
|
|
topic_name_to_id
|
|
)
|
|
from lms.djangoapps.discussion.toggles import ENABLE_DISCUSSIONS_MFE
|
|
from lms.djangoapps.discussion.views import _get_discussion_default_topic_id, course_discussions_settings_handler
|
|
from openedx.core.djangoapps.course_groups.tests.helpers import config_course_cohorts
|
|
from openedx.core.djangoapps.course_groups.tests.test_views import CohortViewsTestCase
|
|
from openedx.core.djangoapps.django_comment_common.comment_client.utils import CommentClientPaginatedResult
|
|
from openedx.core.djangoapps.django_comment_common.models import (
|
|
CourseDiscussionSettings,
|
|
ForumsConfig
|
|
)
|
|
from openedx.core.djangoapps.django_comment_common.utils import ThreadContext
|
|
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES
|
|
|
|
|
|
class ViewsExceptionTestCase(UrlResetMixin, ModuleStoreTestCase): # lint-amnesty, pylint: disable=missing-class-docstring
|
|
|
|
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
|
def setUp(self):
|
|
|
|
# Patching the ENABLE_DISCUSSION_SERVICE value affects the contents of urls.py,
|
|
# so we need to call super.setUp() which reloads urls.py (because
|
|
# of the UrlResetMixin)
|
|
super().setUp()
|
|
|
|
# create a course
|
|
self.course = CourseFactory.create(org='MITx', course='999',
|
|
display_name='Robot Super Course')
|
|
|
|
# Patch the comment client user save method so it does not try
|
|
# to create a new cc user when creating a django user
|
|
with patch('common.djangoapps.student.models.user.cc.User.save'):
|
|
uname = 'student'
|
|
email = 'student@edx.org'
|
|
password = 'test'
|
|
|
|
# Create the student
|
|
self.student = UserFactory(username=uname, password=password, email=email)
|
|
|
|
# Enroll the student in the course
|
|
CourseEnrollmentFactory(user=self.student, course_id=self.course.id)
|
|
|
|
# Log the student in
|
|
self.client = Client()
|
|
assert self.client.login(username=uname, password=password)
|
|
|
|
config = ForumsConfig.current()
|
|
config.enabled = True
|
|
config.save()
|
|
patcher = mock.patch(
|
|
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
|
|
return_value=False
|
|
)
|
|
patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
patcher = mock.patch(
|
|
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
|
|
)
|
|
self.mock_get_course_id_by_thread = patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
|
|
@patch('common.djangoapps.student.models.user.cc.User.from_django_user')
|
|
@patch('openedx.core.djangoapps.django_comment_common.comment_client.user.User.active_threads')
|
|
def test_user_profile_exception(self, mock_threads, mock_from_django_user):
|
|
|
|
# Mock the code that makes the HTTP requests to the cs_comment_service app
|
|
# for the profiled user's active threads
|
|
mock_threads.return_value = [], 1, 1
|
|
|
|
# Mock the code that makes the HTTP request to the cs_comment_service app
|
|
# that gets the current user's info
|
|
mock_from_django_user.return_value = Mock()
|
|
|
|
url = reverse('user_profile',
|
|
kwargs={'course_id': str(self.course.id), 'user_id': '12345'}) # There is no user 12345
|
|
response = self.client.get(url)
|
|
assert response.status_code == 404
|
|
|
|
@patch('common.djangoapps.student.models.user.cc.User.from_django_user')
|
|
@patch('common.djangoapps.student.models.user.cc.User.subscribed_threads')
|
|
def test_user_followed_threads_exception(self, mock_threads, mock_from_django_user):
|
|
|
|
# Mock the code that makes the HTTP requests to the cs_comment_service app
|
|
# for the profiled user's active threads
|
|
mock_threads.return_value = CommentClientPaginatedResult(collection=[], page=1, num_pages=1)
|
|
|
|
# Mock the code that makes the HTTP request to the cs_comment_service app
|
|
# that gets the current user's info
|
|
mock_from_django_user.return_value = Mock()
|
|
|
|
url = reverse('followed_threads',
|
|
kwargs={'course_id': str(self.course.id), 'user_id': '12345'}) # There is no user 12345
|
|
response = self.client.get(url)
|
|
assert response.status_code == 404
|
|
|
|
|
|
def make_mock_thread_data( # lint-amnesty, pylint: disable=missing-function-docstring
|
|
course,
|
|
text,
|
|
thread_id,
|
|
num_children,
|
|
group_id=None,
|
|
group_name=None,
|
|
commentable_id=None,
|
|
is_commentable_divided=None,
|
|
anonymous=False,
|
|
anonymous_to_peers=False,
|
|
):
|
|
data_commentable_id = (
|
|
commentable_id or course.discussion_topics.get('General', {}).get('id') or "dummy_commentable_id"
|
|
)
|
|
thread_data = {
|
|
"id": thread_id,
|
|
"type": "thread",
|
|
"title": text,
|
|
"body": text,
|
|
"commentable_id": data_commentable_id,
|
|
"resp_total": 42,
|
|
"resp_skip": 25,
|
|
"resp_limit": 5,
|
|
"group_id": group_id,
|
|
"anonymous": anonymous,
|
|
"anonymous_to_peers": anonymous_to_peers,
|
|
"context": (
|
|
ThreadContext.COURSE if get_team(data_commentable_id) is None else ThreadContext.STANDALONE
|
|
)
|
|
}
|
|
if group_id is not None:
|
|
thread_data['group_name'] = group_name
|
|
if is_commentable_divided is not None:
|
|
thread_data['is_commentable_divided'] = is_commentable_divided
|
|
if num_children is not None:
|
|
thread_data["children"] = [{
|
|
"id": f"dummy_comment_id_{i}",
|
|
"type": "comment",
|
|
"body": text,
|
|
} for i in range(num_children)]
|
|
return thread_data
|
|
|
|
|
|
def make_mock_collection_data( # lint-amnesty, pylint: disable=missing-function-docstring
|
|
course,
|
|
text,
|
|
thread_id,
|
|
num_children=None,
|
|
group_id=None,
|
|
commentable_id=None,
|
|
thread_list=None
|
|
):
|
|
if thread_list:
|
|
return [
|
|
make_mock_thread_data(course=course, text=text, num_children=num_children, **thread)
|
|
for thread in thread_list
|
|
]
|
|
else:
|
|
return [
|
|
make_mock_thread_data(
|
|
course=course,
|
|
text=text,
|
|
thread_id=thread_id,
|
|
num_children=num_children,
|
|
group_id=group_id,
|
|
commentable_id=commentable_id,
|
|
)
|
|
]
|
|
|
|
|
|
def make_mock_perform_request_impl( # lint-amnesty, pylint: disable=missing-function-docstring
|
|
course,
|
|
text,
|
|
thread_id="dummy_thread_id",
|
|
group_id=None,
|
|
commentable_id=None,
|
|
num_thread_responses=1,
|
|
thread_list=None,
|
|
anonymous=False,
|
|
anonymous_to_peers=False,
|
|
):
|
|
def mock_perform_request_impl(*args, **kwargs):
|
|
url = args[1]
|
|
if url.endswith("threads") or url.endswith("user_profile"):
|
|
return {
|
|
"collection": make_mock_collection_data(
|
|
course, text, thread_id, None, group_id, commentable_id, thread_list
|
|
)
|
|
}
|
|
elif thread_id and url.endswith(thread_id):
|
|
return make_mock_thread_data(
|
|
course=course,
|
|
text=text,
|
|
thread_id=thread_id,
|
|
num_children=num_thread_responses,
|
|
group_id=group_id,
|
|
commentable_id=commentable_id,
|
|
anonymous=anonymous,
|
|
anonymous_to_peers=anonymous_to_peers,
|
|
)
|
|
elif "/users/" in url:
|
|
res = {
|
|
"default_sort_key": "date",
|
|
"upvoted_ids": [],
|
|
"downvoted_ids": [],
|
|
"subscribed_thread_ids": [],
|
|
}
|
|
# comments service adds these attributes when course_id param is present
|
|
if kwargs.get('params', {}).get('course_id'):
|
|
res.update({
|
|
"threads_count": 1,
|
|
"comments_count": 2
|
|
})
|
|
return res
|
|
else:
|
|
return None
|
|
return mock_perform_request_impl
|
|
|
|
|
|
def make_mock_request_impl( # lint-amnesty, pylint: disable=missing-function-docstring
|
|
course,
|
|
text,
|
|
thread_id="dummy_thread_id",
|
|
group_id=None,
|
|
commentable_id=None,
|
|
num_thread_responses=1,
|
|
thread_list=None,
|
|
anonymous=False,
|
|
anonymous_to_peers=False,
|
|
):
|
|
impl = make_mock_perform_request_impl(
|
|
course,
|
|
text,
|
|
thread_id=thread_id,
|
|
group_id=group_id,
|
|
commentable_id=commentable_id,
|
|
num_thread_responses=num_thread_responses,
|
|
thread_list=thread_list,
|
|
anonymous=anonymous,
|
|
anonymous_to_peers=anonymous_to_peers,
|
|
)
|
|
|
|
def mock_request_impl(*args, **kwargs):
|
|
data = impl(*args, **kwargs)
|
|
if data:
|
|
return Mock(status_code=200, text=json.dumps(data), json=Mock(return_value=data))
|
|
else:
|
|
return Mock(status_code=404)
|
|
return mock_request_impl
|
|
|
|
|
|
class StringEndsWithMatcher: # lint-amnesty, pylint: disable=missing-class-docstring
|
|
def __init__(self, suffix):
|
|
self.suffix = suffix
|
|
|
|
def __eq__(self, other):
|
|
return other.endswith(self.suffix)
|
|
|
|
|
|
class PartialDictMatcher: # lint-amnesty, pylint: disable=missing-class-docstring
|
|
def __init__(self, expected_values):
|
|
self.expected_values = expected_values
|
|
|
|
def __eq__(self, other):
|
|
return all(
|
|
key in other and other[key] == value
|
|
for key, value in self.expected_values.items()
|
|
)
|
|
|
|
|
|
class AllowPlusOrMinusOneInt(int):
|
|
"""
|
|
A workaround for the fact that assertNumQueries doesn't let you
|
|
specify a range or any tolerance. An 'int' that is 'equal to' its value,
|
|
but also its value +/- 1
|
|
"""
|
|
|
|
def __init__(self, value):
|
|
super().__init__()
|
|
self.value = value
|
|
self.values = (value, value - 1, value + 1)
|
|
|
|
def __eq__(self, other):
|
|
return other in self.values
|
|
|
|
def __repr__(self):
|
|
return f"({self.value} +/- 1)"
|
|
|
|
|
|
@patch('requests.request', autospec=True)
|
|
class CommentsServiceRequestHeadersTestCase(ForumsEnableMixin, UrlResetMixin, ModuleStoreTestCase): # lint-amnesty, pylint: disable=missing-class-docstring
|
|
|
|
CREATE_USER = False
|
|
|
|
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
|
def setUp(self):
|
|
super().setUp()
|
|
patcher = mock.patch(
|
|
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
|
|
return_value=False
|
|
)
|
|
patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
patcher = mock.patch(
|
|
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment"
|
|
)
|
|
self.mock_get_course_id_by_comment = patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
patcher = mock.patch(
|
|
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
|
|
)
|
|
self.mock_get_course_id_by_thread = patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
|
|
username = "foo"
|
|
password = "bar"
|
|
|
|
# Invoke UrlResetMixin
|
|
super().setUp()
|
|
self.course = CourseFactory.create(discussion_topics={'dummy discussion': {'id': 'dummy_discussion_id'}})
|
|
self.student = UserFactory.create(username=username, password=password)
|
|
CourseEnrollmentFactory.create(user=self.student, course_id=self.course.id)
|
|
assert self.client.login(username=username, password=password)
|
|
|
|
self.addCleanup(translation.deactivate)
|
|
|
|
def assert_all_calls_have_header(self, mock_request, key, value): # lint-amnesty, pylint: disable=missing-function-docstring
|
|
expected = call(
|
|
ANY, # method
|
|
ANY, # url
|
|
data=ANY,
|
|
params=ANY,
|
|
headers=PartialDictMatcher({key: value}),
|
|
timeout=ANY
|
|
)
|
|
for actual in mock_request.call_args_list:
|
|
assert expected == actual
|
|
|
|
def test_accept_language(self, mock_request):
|
|
lang = "eo"
|
|
text = "dummy content"
|
|
thread_id = "test_thread_id"
|
|
mock_request.side_effect = make_mock_request_impl(course=self.course, text=text, thread_id=thread_id)
|
|
|
|
self.client.get(
|
|
reverse(
|
|
"single_thread",
|
|
kwargs={
|
|
"course_id": str(self.course.id),
|
|
"discussion_id": "dummy_discussion_id",
|
|
"thread_id": thread_id,
|
|
}
|
|
),
|
|
HTTP_ACCEPT_LANGUAGE=lang,
|
|
)
|
|
self.assert_all_calls_have_header(mock_request, "Accept-Language", lang)
|
|
|
|
@override_settings(COMMENTS_SERVICE_KEY="test_api_key")
|
|
def test_api_key(self, mock_request):
|
|
mock_request.side_effect = make_mock_request_impl(course=self.course, text="dummy", thread_id="dummy")
|
|
|
|
self.client.get(
|
|
reverse(
|
|
"forum_form_discussion",
|
|
kwargs={"course_id": str(self.course.id)}
|
|
),
|
|
)
|
|
self.assert_all_calls_have_header(mock_request, "X-Edx-Api-Key", "test_api_key")
|
|
|
|
|
|
class UserProfileUnicodeTestCase(ForumsEnableMixin, SharedModuleStoreTestCase, UnicodeTestMixin): # lint-amnesty, pylint: disable=missing-class-docstring
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
# pylint: disable=super-method-not-called
|
|
with super().setUpClassAndTestData():
|
|
cls.course = CourseFactory.create()
|
|
|
|
@classmethod
|
|
def setUpTestData(cls):
|
|
super().setUpTestData()
|
|
|
|
cls.student = UserFactory.create()
|
|
CourseEnrollmentFactory(user=cls.student, course_id=cls.course.id)
|
|
|
|
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
|
|
def _test_unicode_data(self, text, mock_request): # lint-amnesty, pylint: disable=missing-function-docstring
|
|
mock_request.side_effect = make_mock_request_impl(course=self.course, text=text)
|
|
request = RequestFactory().get("dummy_url")
|
|
request.user = self.student
|
|
# so (request.headers.get('x-requested-with') == 'XMLHttpRequest') == True
|
|
request.META["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest"
|
|
|
|
response = views.user_profile(request, str(self.course.id), str(self.student.id))
|
|
assert response.status_code == 200
|
|
response_data = json.loads(response.content.decode('utf-8'))
|
|
assert response_data['discussion_data'][0]['title'] == text
|
|
assert response_data['discussion_data'][0]['body'] == text
|
|
|
|
|
|
class EnrollmentTestCase(ForumsEnableMixin, ModuleStoreTestCase):
|
|
"""
|
|
Tests for the behavior of views depending on if the student is enrolled
|
|
in the course
|
|
"""
|
|
|
|
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.course = CourseFactory.create()
|
|
self.student = UserFactory.create()
|
|
|
|
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
|
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
|
|
def test_unenrolled(self, mock_request):
|
|
mock_request.side_effect = make_mock_request_impl(course=self.course, text='dummy')
|
|
request = RequestFactory().get('dummy_url')
|
|
request.user = self.student
|
|
with pytest.raises(CourseAccessRedirect):
|
|
views.forum_form_discussion(request, course_id=str(self.course.id)) # pylint: disable=no-value-for-parameter, unexpected-keyword-arg
|
|
|
|
|
|
class DividedDiscussionsTestCase(CohortViewsTestCase): # lint-amnesty, pylint: disable=missing-class-docstring
|
|
|
|
def create_divided_discussions(self):
|
|
"""
|
|
Set up a divided discussion in the system, complete with all the fixings
|
|
"""
|
|
divided_inline_discussions = ['Topic A']
|
|
divided_course_wide_discussions = ["Topic B"]
|
|
divided_discussions = divided_inline_discussions + divided_course_wide_discussions
|
|
|
|
# inline discussion
|
|
BlockFactory.create(
|
|
parent_location=self.course.location,
|
|
category="discussion",
|
|
discussion_id=topic_name_to_id(self.course, "Topic A"),
|
|
discussion_category="Chapter",
|
|
discussion_target="Discussion",
|
|
start=datetime.now()
|
|
)
|
|
# get updated course
|
|
self.course = self.store.get_item(self.course.location)
|
|
# course-wide discussion
|
|
discussion_topics = {
|
|
"Topic B": {"id": "Topic B"},
|
|
}
|
|
|
|
config_course_cohorts(
|
|
self.course,
|
|
is_cohorted=True,
|
|
)
|
|
|
|
config_course_discussions(
|
|
self.course,
|
|
discussion_topics=discussion_topics,
|
|
divided_discussions=divided_discussions
|
|
)
|
|
return divided_inline_discussions, divided_course_wide_discussions
|
|
|
|
|
|
class CourseDiscussionTopicsTestCase(DividedDiscussionsTestCase):
|
|
"""
|
|
Tests the `divide_discussion_topics` view.
|
|
"""
|
|
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
|
|
|
|
def test_non_staff(self):
|
|
"""
|
|
Verify that we cannot access divide_discussion_topics if we're a non-staff user.
|
|
"""
|
|
self._verify_non_staff_cannot_access(views.discussion_topics, "GET", [str(self.course.id)])
|
|
|
|
def test_get_discussion_topics(self):
|
|
"""
|
|
Verify that discussion_topics is working for HTTP GET.
|
|
"""
|
|
# create inline & course-wide discussion to verify the different map.
|
|
self.create_divided_discussions()
|
|
|
|
response = self.get_handler(self.course, handler=views.discussion_topics)
|
|
start_date = response['inline_discussions']['subcategories']['Chapter']['start_date']
|
|
expected_response = {
|
|
"course_wide_discussions": {
|
|
'children': [['Topic B', TYPE_ENTRY]],
|
|
'entries': {
|
|
'Topic B': {
|
|
'sort_key': 'A',
|
|
'is_divided': True,
|
|
'id': topic_name_to_id(self.course, "Topic B"),
|
|
'start_date': response['course_wide_discussions']['entries']['Topic B']['start_date']
|
|
}
|
|
}
|
|
},
|
|
"inline_discussions": {
|
|
'subcategories': {
|
|
'Chapter': {
|
|
'subcategories': {},
|
|
'children': [['Discussion', TYPE_ENTRY]],
|
|
'entries': {
|
|
'Discussion': {
|
|
'sort_key': None,
|
|
'is_divided': True,
|
|
'id': topic_name_to_id(self.course, "Topic A"),
|
|
'start_date': start_date
|
|
}
|
|
},
|
|
'sort_key': 'Chapter',
|
|
'start_date': start_date
|
|
}
|
|
},
|
|
'children': [['Chapter', TYPE_SUBCATEGORY]]
|
|
}
|
|
}
|
|
assert response == expected_response
|
|
|
|
|
|
class CourseDiscussionsHandlerTestCase(DividedDiscussionsTestCase):
|
|
"""
|
|
Tests the course_discussion_settings_handler
|
|
"""
|
|
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
|
|
|
|
def get_expected_response(self):
|
|
"""
|
|
Returns the static response dict.
|
|
"""
|
|
return {
|
|
'always_divide_inline_discussions': False,
|
|
'divided_inline_discussions': [],
|
|
'divided_course_wide_discussions': [],
|
|
'id': 1,
|
|
'division_scheme': 'cohort',
|
|
'available_division_schemes': ['cohort']
|
|
}
|
|
|
|
def test_non_staff(self):
|
|
"""
|
|
Verify that we cannot access course_discussions_settings_handler if we're a non-staff user.
|
|
"""
|
|
self._verify_non_staff_cannot_access(
|
|
course_discussions_settings_handler, "GET", [str(self.course.id)]
|
|
)
|
|
self._verify_non_staff_cannot_access(
|
|
course_discussions_settings_handler, "PATCH", [str(self.course.id)]
|
|
)
|
|
|
|
def test_update_always_divide_inline_discussion_settings(self):
|
|
"""
|
|
Verify that course_discussions_settings_handler is working for always_divide_inline_discussions via HTTP PATCH.
|
|
"""
|
|
config_course_cohorts(self.course, is_cohorted=True)
|
|
|
|
response = self.get_handler(self.course, handler=course_discussions_settings_handler)
|
|
|
|
expected_response = self.get_expected_response()
|
|
|
|
assert response == expected_response
|
|
|
|
expected_response['always_divide_inline_discussions'] = True
|
|
response = self.patch_handler(
|
|
self.course, data=expected_response, handler=course_discussions_settings_handler
|
|
)
|
|
|
|
assert response == expected_response
|
|
|
|
def test_update_course_wide_discussion_settings(self):
|
|
"""
|
|
Verify that course_discussions_settings_handler is working for divided_course_wide_discussions via HTTP PATCH.
|
|
"""
|
|
# course-wide discussion
|
|
discussion_topics = {
|
|
"Topic B": {"id": "Topic B"},
|
|
}
|
|
|
|
config_course_cohorts(self.course, is_cohorted=True)
|
|
config_course_discussions(self.course, discussion_topics=discussion_topics)
|
|
|
|
response = self.get_handler(self.course, handler=views.course_discussions_settings_handler)
|
|
|
|
expected_response = self.get_expected_response()
|
|
assert response == expected_response
|
|
|
|
expected_response['divided_course_wide_discussions'] = [topic_name_to_id(self.course, "Topic B")]
|
|
response = self.patch_handler(
|
|
self.course, data=expected_response, handler=views.course_discussions_settings_handler
|
|
)
|
|
|
|
assert response == expected_response
|
|
|
|
def test_update_inline_discussion_settings(self):
|
|
"""
|
|
Verify that course_discussions_settings_handler is working for divided_inline_discussions via HTTP PATCH.
|
|
"""
|
|
config_course_cohorts(self.course, is_cohorted=True)
|
|
|
|
response = self.get_handler(self.course, handler=views.course_discussions_settings_handler)
|
|
|
|
expected_response = self.get_expected_response()
|
|
assert response == expected_response
|
|
|
|
RequestCache.clear_all_namespaces()
|
|
now = datetime.now()
|
|
# inline discussion
|
|
BlockFactory.create(
|
|
parent_location=self.course.location,
|
|
category="discussion",
|
|
discussion_id="Topic_A",
|
|
discussion_category="Chapter",
|
|
discussion_target="Discussion",
|
|
start=now
|
|
)
|
|
|
|
expected_response['divided_inline_discussions'] = ["Topic_A"]
|
|
response = self.patch_handler(
|
|
self.course, data=expected_response, handler=views.course_discussions_settings_handler
|
|
)
|
|
|
|
assert response == expected_response
|
|
|
|
def test_get_settings(self):
|
|
"""
|
|
Verify that course_discussions_settings_handler is working for HTTP GET.
|
|
"""
|
|
divided_inline_discussions, divided_course_wide_discussions = self.create_divided_discussions()
|
|
|
|
response = self.get_handler(self.course, handler=views.course_discussions_settings_handler)
|
|
expected_response = self.get_expected_response()
|
|
|
|
expected_response['divided_inline_discussions'] = [topic_name_to_id(self.course, name)
|
|
for name in divided_inline_discussions]
|
|
expected_response['divided_course_wide_discussions'] = [topic_name_to_id(self.course, name)
|
|
for name in divided_course_wide_discussions]
|
|
|
|
assert response == expected_response
|
|
|
|
def test_update_settings_with_invalid_field_data_type(self):
|
|
"""
|
|
Verify that course_discussions_settings_handler return HTTP 400 if field data type is incorrect.
|
|
"""
|
|
config_course_cohorts(self.course, is_cohorted=True)
|
|
|
|
response = self.patch_handler(
|
|
self.course,
|
|
data={'always_divide_inline_discussions': ''},
|
|
expected_response_code=400,
|
|
handler=views.course_discussions_settings_handler
|
|
)
|
|
assert 'Incorrect field type for `{}`. Type must be `{}`'.format(
|
|
'always_divide_inline_discussions',
|
|
bool.__name__
|
|
) == response.get('error')
|
|
|
|
def test_available_schemes(self):
|
|
# Cohorts disabled, single enrollment mode.
|
|
config_course_cohorts(self.course, is_cohorted=False)
|
|
response = self.get_handler(self.course, handler=views.course_discussions_settings_handler)
|
|
expected_response = self.get_expected_response()
|
|
expected_response['available_division_schemes'] = []
|
|
assert response == expected_response
|
|
|
|
# Add 2 enrollment modes
|
|
CourseModeFactory.create(course_id=self.course.id, mode_slug=CourseMode.AUDIT)
|
|
CourseModeFactory.create(course_id=self.course.id, mode_slug=CourseMode.VERIFIED)
|
|
response = self.get_handler(self.course, handler=views.course_discussions_settings_handler)
|
|
expected_response['available_division_schemes'] = [CourseDiscussionSettings.ENROLLMENT_TRACK]
|
|
assert response == expected_response
|
|
|
|
# Enable cohorts
|
|
config_course_cohorts(self.course, is_cohorted=True)
|
|
response = self.get_handler(self.course, handler=views.course_discussions_settings_handler)
|
|
expected_response['available_division_schemes'] = [
|
|
CourseDiscussionSettings.COHORT, CourseDiscussionSettings.ENROLLMENT_TRACK
|
|
]
|
|
assert response == expected_response
|
|
|
|
|
|
class DefaultTopicIdGetterTestCase(ModuleStoreTestCase):
|
|
"""
|
|
Tests the `_get_discussion_default_topic_id` helper.
|
|
"""
|
|
|
|
def test_no_default_topic(self):
|
|
discussion_topics = {
|
|
'dummy discussion': {
|
|
'id': 'dummy_discussion_id',
|
|
},
|
|
}
|
|
course = CourseFactory.create(discussion_topics=discussion_topics)
|
|
expected_id = None
|
|
result = _get_discussion_default_topic_id(course)
|
|
assert expected_id == result
|
|
|
|
def test_default_topic_id(self):
|
|
discussion_topics = {
|
|
'dummy discussion': {
|
|
'id': 'dummy_discussion_id',
|
|
},
|
|
'another discussion': {
|
|
'id': 'another_discussion_id',
|
|
'default': True,
|
|
},
|
|
}
|
|
course = CourseFactory.create(discussion_topics=discussion_topics)
|
|
expected_id = 'another_discussion_id'
|
|
result = _get_discussion_default_topic_id(course)
|
|
assert expected_id == result
|
|
|
|
|
|
@ddt.ddt
|
|
@patch(
|
|
'openedx.core.djangoapps.django_comment_common.comment_client.utils.perform_request',
|
|
Mock(
|
|
return_value={
|
|
"id": "test_thread",
|
|
"title": "Title",
|
|
"body": "<p></p>",
|
|
"default_sort_key": "date",
|
|
"upvoted_ids": [],
|
|
"downvoted_ids": [],
|
|
"subscribed_thread_ids": [],
|
|
}
|
|
)
|
|
)
|
|
class ForumMFETestCase(ForumsEnableMixin, SharedModuleStoreTestCase):
|
|
"""
|
|
Tests that the MFE upgrade banner and MFE is shown in the correct situation with the correct UI
|
|
"""
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.course = CourseFactory.create()
|
|
self.user = UserFactory.create()
|
|
self.staff_user = AdminFactory.create()
|
|
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id)
|
|
|
|
@override_settings(DISCUSSIONS_MICROFRONTEND_URL="http://test.url")
|
|
def test_redirect_from_legacy_base_url_to_new_experience(self):
|
|
"""
|
|
Verify that the legacy url is redirected to MFE homepage when
|
|
ENABLE_DISCUSSIONS_MFE flag is enabled.
|
|
"""
|
|
|
|
with override_waffle_flag(ENABLE_DISCUSSIONS_MFE, True):
|
|
self.client.login(username=self.user.username, password=self.TEST_PASSWORD)
|
|
url = reverse("forum_form_discussion", args=[self.course.id])
|
|
response = self.client.get(url)
|
|
assert response.status_code == 302
|
|
expected_url = f"{settings.DISCUSSIONS_MICROFRONTEND_URL}/{str(self.course.id)}"
|
|
assert response.url == expected_url
|
|
|
|
@override_settings(DISCUSSIONS_MICROFRONTEND_URL="http://test.url")
|
|
def test_redirect_from_legacy_profile_url_to_new_experience(self):
|
|
"""
|
|
Verify that the requested user profile is redirected to MFE learners tab when
|
|
ENABLE_DISCUSSIONS_MFE flag is enabled
|
|
"""
|
|
|
|
with override_waffle_flag(ENABLE_DISCUSSIONS_MFE, True):
|
|
self.client.login(username=self.user.username, password=self.TEST_PASSWORD)
|
|
url = reverse("user_profile", args=[self.course.id, self.user.id])
|
|
response = self.client.get(url)
|
|
assert response.status_code == 302
|
|
expected_url = f"{settings.DISCUSSIONS_MICROFRONTEND_URL}/{str(self.course.id)}/learners"
|
|
assert response.url == expected_url
|
|
|
|
@override_settings(DISCUSSIONS_MICROFRONTEND_URL="http://test.url")
|
|
def test_redirect_from_legacy_single_thread_to_new_experience(self):
|
|
"""
|
|
Verify that a legacy single url is redirected to corresponding MFE thread url when the ENABLE_DISCUSSIONS_MFE
|
|
flag is enabled
|
|
"""
|
|
|
|
with override_waffle_flag(ENABLE_DISCUSSIONS_MFE, True):
|
|
self.client.login(username=self.user.username, password=self.TEST_PASSWORD)
|
|
url = reverse("single_thread", args=[self.course.id, "test_discussion", "test_thread"])
|
|
response = self.client.get(url)
|
|
assert response.status_code == 302
|
|
expected_url = f"{settings.DISCUSSIONS_MICROFRONTEND_URL}/{str(self.course.id)}/posts/test_thread"
|
|
assert response.url == expected_url
|