* feat: [AA-922] remove deprecated Goals backend While the new Weekly Learning Goals were being rolled out, the previous goal setting feature still existed behind a waffle flag. The Weekly Learning Goals now become the one and only learning goal feature. This change does not remove the old goals feature from the legacy backend, and therefore it does not remove any of the data that was used by the old goals feature. The goals are now driven by the single pre-existing Waffle flag ENABLE_COURSE_GOALS - Removed COURSE_GOALS_NUMBER_OF_DAYS_GOALS waffle flag, replacing it where needed with the existing ENABLE_COURSE_GOALS - modified the API to remove the old goal_options, keeping the redundant weekly_learning_goal_enabled flag - updated tests - refactor tests to fit 50 line limit in lint
300 lines
12 KiB
Python
300 lines
12 KiB
Python
"""
|
|
Tests for course_info
|
|
"""
|
|
|
|
|
|
import ddt
|
|
from django.conf import settings
|
|
from django.urls import reverse
|
|
from edx_toggles.toggles.testutils import override_waffle_flag
|
|
from milestones.tests.utils import MilestonesTestCaseMixin
|
|
from mock import patch
|
|
from rest_framework.test import APIClient # pylint: disable=unused-import
|
|
|
|
from common.djangoapps.student.models import CourseEnrollment # pylint: disable=unused-import
|
|
from common.djangoapps.student.tests.factories import UserFactory # pylint: disable=unused-import
|
|
from lms.djangoapps.mobile_api.testutils import MobileAPITestCase, MobileAuthTestMixin, MobileCourseAccessTestMixin
|
|
from lms.djangoapps.mobile_api.utils import API_V1, API_V05
|
|
from openedx.features.course_experience import ENABLE_COURSE_GOALS
|
|
from xmodule.html_module import CourseInfoBlock # lint-amnesty, pylint: disable=wrong-import-order
|
|
from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order
|
|
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
|
|
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
|
|
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=unused-import, wrong-import-order
|
|
from xmodule.modulestore.xml_importer import import_course_from_xml # lint-amnesty, pylint: disable=wrong-import-order
|
|
|
|
|
|
@ddt.ddt
|
|
class TestUpdates(MobileAPITestCase, MobileAuthTestMixin, MobileCourseAccessTestMixin, MilestonesTestCaseMixin):
|
|
"""
|
|
Tests for /api/mobile/{api_version}/course_info/{course_id}/updates
|
|
"""
|
|
REVERSE_INFO = {'name': 'course-updates-list', 'params': ['course_id', 'api_version']}
|
|
|
|
def verify_success(self, response):
|
|
super().verify_success(response)
|
|
assert response.data == []
|
|
|
|
@ddt.data(
|
|
(True, API_V05),
|
|
(True, API_V1),
|
|
(False, API_V05),
|
|
(False, API_V1),
|
|
)
|
|
@ddt.unpack
|
|
def test_updates(self, new_format, api_version):
|
|
"""
|
|
Tests updates endpoint with /static in the content.
|
|
Tests both new updates format (using "items") and old format (using "data").
|
|
"""
|
|
self.login_and_enroll()
|
|
|
|
# create course Updates item in modulestore
|
|
updates_usage_key = self.course.id.make_usage_key('course_info', 'updates')
|
|
course_updates = modulestore().create_item(
|
|
self.user.id,
|
|
updates_usage_key.course_key,
|
|
updates_usage_key.block_type,
|
|
block_id=updates_usage_key.block_id
|
|
)
|
|
|
|
# store content in Updates item (either new or old format)
|
|
num_updates = 3
|
|
if new_format:
|
|
for num in range(1, num_updates + 1):
|
|
course_updates.items.append(
|
|
{
|
|
"id": num,
|
|
"date": "Date" + str(num),
|
|
"content": "<a href=\"/static/\">Update" + str(num) + "</a>",
|
|
"status": CourseInfoBlock.STATUS_VISIBLE
|
|
}
|
|
)
|
|
else:
|
|
update_data = ""
|
|
# old format stores the updates with the newest first
|
|
for num in range(num_updates, 0, -1):
|
|
update_data += "<li><h2>Date" + str(num) + "</h2><a href=\"/static/\">Update" + str(num) + "</a></li>"
|
|
course_updates.data = "<ol>" + update_data + "</ol>"
|
|
modulestore().update_item(course_updates, self.user.id)
|
|
|
|
# call API
|
|
response = self.api_response(api_version=api_version)
|
|
|
|
# verify static URLs are replaced in the content returned by the API
|
|
self.assertNotContains(response, "\"/static/")
|
|
|
|
# verify static URLs remain in the underlying content
|
|
underlying_updates = modulestore().get_item(updates_usage_key)
|
|
underlying_content = underlying_updates.items[0]['content'] if new_format else underlying_updates.data
|
|
assert '"/static/' in underlying_content
|
|
|
|
# verify content and sort order of updates (most recent first)
|
|
for num in range(1, num_updates + 1):
|
|
update_data = response.data[num_updates - num]
|
|
assert num == update_data['id']
|
|
assert 'Date' + str(num) == update_data['date']
|
|
assert 'Update' + str(num) in update_data['content']
|
|
|
|
|
|
@ddt.ddt
|
|
class TestHandouts(MobileAPITestCase, MobileAuthTestMixin, MobileCourseAccessTestMixin, MilestonesTestCaseMixin):
|
|
"""
|
|
Tests for /api/mobile/{api_version}/course_info/{course_id}/handouts
|
|
"""
|
|
REVERSE_INFO = {'name': 'course-handouts-list', 'params': ['course_id', 'api_version']}
|
|
|
|
@ddt.data(
|
|
(ModuleStoreEnum.Type.mongo, API_V05),
|
|
(ModuleStoreEnum.Type.mongo, API_V1),
|
|
(ModuleStoreEnum.Type.split, API_V05),
|
|
(ModuleStoreEnum.Type.split, API_V1),
|
|
)
|
|
@ddt.unpack
|
|
def test_handouts(self, default_ms, api_version):
|
|
with self.store.default_store(default_ms):
|
|
self.add_mobile_available_toy_course()
|
|
response = self.api_response(expected_response_code=200, api_version=api_version)
|
|
assert 'Sample' in response.data['handouts_html']
|
|
|
|
@ddt.data(
|
|
(ModuleStoreEnum.Type.mongo, API_V05),
|
|
(ModuleStoreEnum.Type.mongo, API_V1),
|
|
(ModuleStoreEnum.Type.split, API_V05),
|
|
(ModuleStoreEnum.Type.split, API_V1),
|
|
)
|
|
@ddt.unpack
|
|
def test_no_handouts(self, default_ms, api_version):
|
|
with self.store.default_store(default_ms):
|
|
self.add_mobile_available_toy_course()
|
|
|
|
# delete handouts in course
|
|
handouts_usage_key = self.course.id.make_usage_key('course_info', 'handouts')
|
|
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, self.course.id):
|
|
self.store.delete_item(handouts_usage_key, self.user.id)
|
|
|
|
response = self.api_response(expected_response_code=200, api_version=api_version)
|
|
assert response.data['handouts_html'] is None
|
|
|
|
@ddt.data(
|
|
(ModuleStoreEnum.Type.mongo, API_V05),
|
|
(ModuleStoreEnum.Type.mongo, API_V1),
|
|
(ModuleStoreEnum.Type.split, API_V05),
|
|
(ModuleStoreEnum.Type.split, API_V1),
|
|
)
|
|
@ddt.unpack
|
|
def test_empty_handouts(self, default_ms, api_version):
|
|
with self.store.default_store(default_ms):
|
|
self.add_mobile_available_toy_course()
|
|
|
|
# set handouts to empty tags
|
|
handouts_usage_key = self.course.id.make_usage_key('course_info', 'handouts')
|
|
underlying_handouts = self.store.get_item(handouts_usage_key)
|
|
underlying_handouts.data = "<ol></ol>"
|
|
self.store.update_item(underlying_handouts, self.user.id)
|
|
response = self.api_response(expected_response_code=200, api_version=api_version)
|
|
assert response.data['handouts_html'] is None
|
|
|
|
@ddt.data(
|
|
(ModuleStoreEnum.Type.mongo, API_V05),
|
|
(ModuleStoreEnum.Type.mongo, API_V1),
|
|
(ModuleStoreEnum.Type.split, API_V05),
|
|
(ModuleStoreEnum.Type.split, API_V1),
|
|
)
|
|
@ddt.unpack
|
|
def test_handouts_static_rewrites(self, default_ms, api_version):
|
|
with self.store.default_store(default_ms):
|
|
self.add_mobile_available_toy_course()
|
|
|
|
# check that we start with relative static assets
|
|
handouts_usage_key = self.course.id.make_usage_key('course_info', 'handouts')
|
|
underlying_handouts = self.store.get_item(handouts_usage_key)
|
|
assert "'/static/" in underlying_handouts.data
|
|
|
|
# but shouldn't finish with any
|
|
response = self.api_response(api_version=api_version)
|
|
assert "'/static/" not in response.data['handouts_html']
|
|
|
|
@ddt.data(
|
|
(ModuleStoreEnum.Type.mongo, API_V05),
|
|
(ModuleStoreEnum.Type.mongo, API_V1),
|
|
(ModuleStoreEnum.Type.split, API_V05),
|
|
(ModuleStoreEnum.Type.split, API_V1),
|
|
)
|
|
@ddt.unpack
|
|
def test_jump_to_id_handout_href(self, default_ms, api_version):
|
|
with self.store.default_store(default_ms):
|
|
self.add_mobile_available_toy_course()
|
|
|
|
# check that we start with relative static assets
|
|
handouts_usage_key = self.course.id.make_usage_key('course_info', 'handouts')
|
|
underlying_handouts = self.store.get_item(handouts_usage_key)
|
|
underlying_handouts.data = "<a href=\"/jump_to_id/identifier\">Intracourse Link</a>"
|
|
self.store.update_item(underlying_handouts, self.user.id)
|
|
|
|
# but shouldn't finish with any
|
|
response = self.api_response(api_version=api_version)
|
|
assert f'/courses/{self.course.id}/jump_to_id/' in response.data['handouts_html']
|
|
|
|
@ddt.data(
|
|
(ModuleStoreEnum.Type.mongo, API_V05),
|
|
(ModuleStoreEnum.Type.mongo, API_V1),
|
|
(ModuleStoreEnum.Type.split, API_V05),
|
|
(ModuleStoreEnum.Type.split, API_V1),
|
|
)
|
|
@ddt.unpack
|
|
def test_course_url_handout_href(self, default_ms, api_version):
|
|
with self.store.default_store(default_ms):
|
|
self.add_mobile_available_toy_course()
|
|
|
|
# check that we start with relative static assets
|
|
handouts_usage_key = self.course.id.make_usage_key('course_info', 'handouts')
|
|
underlying_handouts = self.store.get_item(handouts_usage_key)
|
|
underlying_handouts.data = "<a href=\"/course/identifier\">Linked Content</a>"
|
|
self.store.update_item(underlying_handouts, self.user.id)
|
|
|
|
# but shouldn't finish with any
|
|
response = self.api_response(api_version=api_version)
|
|
assert f'/courses/{self.course.id}/' in response.data['handouts_html']
|
|
|
|
def add_mobile_available_toy_course(self):
|
|
""" use toy course with handouts, and make it mobile_available """
|
|
course_items = import_course_from_xml(
|
|
self.store, self.user.id,
|
|
settings.COMMON_TEST_DATA_ROOT, ['toy'],
|
|
create_if_not_present=True
|
|
)
|
|
self.course = course_items[0]
|
|
self.course.mobile_available = True
|
|
self.store.update_item(self.course, self.user.id)
|
|
self.login_and_enroll()
|
|
|
|
|
|
@override_waffle_flag(ENABLE_COURSE_GOALS, active=True)
|
|
class TestCourseGoalsUserActivityAPI(MobileAPITestCase, SharedModuleStoreTestCase):
|
|
"""
|
|
Testing the Course Goals User Activity API.
|
|
"""
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.apiUrl = reverse('record_user_activity', args=['v1'])
|
|
self.login_and_enroll()
|
|
|
|
def test_record_activity(self):
|
|
'''
|
|
Test the happy path of recording user activity
|
|
'''
|
|
post_data = {
|
|
'course_key': self.course.id,
|
|
'user_id': self.user.id,
|
|
}
|
|
|
|
response = self.client.post(self.apiUrl, post_data)
|
|
assert response.status_code == 200
|
|
|
|
def test_invalid_parameters(self):
|
|
'''
|
|
Ensure that we check that parameters meet the requirements
|
|
and return a 400 otherwise.
|
|
'''
|
|
post_data = {
|
|
'course_key': self.course.id,
|
|
}
|
|
|
|
response = self.client.post(self.apiUrl, post_data)
|
|
assert response.status_code == 400
|
|
|
|
post_data = {
|
|
'user_id': self.user.id,
|
|
}
|
|
|
|
response = self.client.post(self.apiUrl, post_data)
|
|
assert response.status_code == 400
|
|
|
|
post_data = {
|
|
'user_id': self.user.id,
|
|
'course_key': 'invalidcoursekey',
|
|
}
|
|
|
|
response = self.client.post(self.apiUrl, post_data)
|
|
assert response.status_code == 400
|
|
|
|
@override_waffle_flag(ENABLE_COURSE_GOALS, active=False)
|
|
@patch('lms.djangoapps.mobile_api.course_info.views.log')
|
|
def test_flag_disabled(self, mock_logger):
|
|
'''
|
|
Test the API behavior when the goals flag is disabled
|
|
'''
|
|
post_data = {
|
|
'user_id': self.user.id,
|
|
'course_key': self.course.id,
|
|
}
|
|
|
|
response = self.client.post(self.apiUrl, post_data)
|
|
assert response.status_code == 200
|
|
mock_logger.warning.assert_called_with(
|
|
'For this mobile request, user activity is not enabled for this user {} and course {}'.format(
|
|
str(self.user.id), str(self.course.id))
|
|
)
|