Adds API to fetch all legacy library content blocks that are ready to be updated to use library v2 and convert to item banks. Also adds API to update all the references via a user celery task and to fetch its status.
275 lines
9.5 KiB
Python
275 lines
9.5 KiB
Python
"""
|
|
Tests for the course import API views
|
|
"""
|
|
from datetime import datetime
|
|
from unittest.mock import MagicMock, patch
|
|
from uuid import uuid4
|
|
|
|
import ddt
|
|
import factory
|
|
from django.conf import settings
|
|
from django.contrib.auth import get_user_model
|
|
from django.test.utils import override_settings
|
|
from django.urls import reverse
|
|
from rest_framework import status
|
|
from rest_framework.test import APITestCase
|
|
|
|
from common.djangoapps.course_modes.models import CourseMode
|
|
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
|
|
from common.djangoapps.student.tests.factories import StaffFactory, UserFactory
|
|
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
|
from xmodule.modulestore.tests.factories import BlockFactory, CourseFactory
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
@ddt.ddt
|
|
@override_settings(
|
|
PROCTORING_BACKENDS={
|
|
"DEFAULT": "test_proctoring_provider",
|
|
"test_proctoring_provider": {"requires_escalation_email": True},
|
|
}
|
|
)
|
|
class CourseValidationViewTest(SharedModuleStoreTestCase, APITestCase):
|
|
"""
|
|
Test course validation view via a RESTful API
|
|
"""
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
|
|
cls.course = CourseFactory.create(
|
|
display_name='test course',
|
|
run="Testing_course",
|
|
proctoring_provider='test_proctoring_provider',
|
|
proctoring_escalation_email='test@example.com',
|
|
)
|
|
cls.course_key = cls.course.id
|
|
|
|
cls.password = 'test'
|
|
cls.student = UserFactory(username='dummy', password=cls.password)
|
|
cls.staff = StaffFactory(course_key=cls.course.id, password=cls.password)
|
|
|
|
cls.initialize_course(cls.course)
|
|
|
|
@classmethod
|
|
def initialize_course(cls, course):
|
|
"""
|
|
Sets up test course structure.
|
|
"""
|
|
course.start = datetime.now()
|
|
course.self_paced = True
|
|
cls.store.update_item(course, cls.staff.id)
|
|
|
|
update_key = course.id.make_usage_key('course_info', 'updates')
|
|
cls.store.create_item(
|
|
cls.staff.id,
|
|
update_key.course_key,
|
|
update_key.block_type,
|
|
block_id=update_key.block_id,
|
|
fields=dict(data="<ol><li><h2>Date</h2>Hello world!</li></ol>"),
|
|
)
|
|
|
|
section = BlockFactory.create(
|
|
parent_location=course.location,
|
|
category="chapter",
|
|
)
|
|
BlockFactory.create(
|
|
parent_location=section.location,
|
|
category="sequential",
|
|
)
|
|
|
|
def get_url(self, course_id):
|
|
"""
|
|
Helper function to create the url
|
|
"""
|
|
return reverse(
|
|
'courses_api:course_validation',
|
|
kwargs={
|
|
'course_id': course_id,
|
|
}
|
|
)
|
|
|
|
def test_student_fails(self):
|
|
self.client.login(username=self.student.username, password=self.password)
|
|
resp = self.client.get(self.get_url(self.course_key))
|
|
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
|
|
|
|
@ddt.data(
|
|
(False, False),
|
|
(True, False),
|
|
(False, True),
|
|
(True, True),
|
|
)
|
|
@ddt.unpack
|
|
def test_staff_succeeds(self, certs_html_view, with_modes):
|
|
features = dict(settings.FEATURES, CERTIFICATES_HTML_VIEW=certs_html_view)
|
|
with override_settings(FEATURES=features):
|
|
if with_modes:
|
|
CourseModeFactory.create_batch(
|
|
2,
|
|
course_id=self.course.id,
|
|
mode_slug=factory.Iterator([CourseMode.AUDIT, CourseMode.VERIFIED]),
|
|
)
|
|
self.client.login(username=self.staff.username, password=self.password)
|
|
resp = self.client.get(self.get_url(self.course_key), {'all': 'true'})
|
|
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
|
expected_data = {
|
|
'assignments': {
|
|
'total_number': 1,
|
|
'total_visible': 1,
|
|
'assignments_with_dates_before_start': [],
|
|
'assignments_with_dates_after_end': [],
|
|
'assignments_with_ora_dates_after_end': [],
|
|
'assignments_with_ora_dates_before_start': [],
|
|
},
|
|
'dates': {
|
|
'has_start_date': True,
|
|
'has_end_date': False,
|
|
},
|
|
'updates': {
|
|
'has_update': True,
|
|
},
|
|
'certificates': {
|
|
'is_enabled': with_modes,
|
|
'is_activated': False,
|
|
'has_certificate': False,
|
|
},
|
|
'grades': {
|
|
'has_grading_policy': False,
|
|
'sum_of_weights': 1.0,
|
|
},
|
|
'proctoring': {
|
|
'needs_proctoring_escalation_email': True,
|
|
'has_proctoring_escalation_email': True,
|
|
},
|
|
'is_self_paced': True,
|
|
}
|
|
self.assertDictEqual(resp.data, expected_data)
|
|
|
|
|
|
class TestMigrationViewSetCreate(SharedModuleStoreTestCase, APITestCase):
|
|
"""
|
|
Test the MigrationViewSet.create() endpoint.
|
|
|
|
Focus: validation, return codes, serialization/deserialization.
|
|
"""
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
|
|
cls.course = CourseFactory.create(
|
|
display_name='test course',
|
|
run="Testing_course",
|
|
proctoring_provider='test_proctoring_provider',
|
|
proctoring_escalation_email='test@example.com',
|
|
)
|
|
cls.course_key = cls.course.id
|
|
|
|
cls.password = 'test'
|
|
cls.student = UserFactory(username='dummy', password=cls.password)
|
|
cls.staff = StaffFactory(course_key=cls.course.id, password=cls.password)
|
|
|
|
cls.initialize_course(cls.course)
|
|
|
|
@classmethod
|
|
def initialize_course(cls, course):
|
|
"""
|
|
Sets up test course structure.
|
|
"""
|
|
section = BlockFactory.create(
|
|
parent_location=course.location,
|
|
category="chapter",
|
|
)
|
|
subsection = BlockFactory.create(
|
|
parent_location=section.location,
|
|
category="sequential",
|
|
)
|
|
unit = BlockFactory.create(
|
|
parent_location=subsection.location,
|
|
category="vertical",
|
|
)
|
|
cls.block1 = BlockFactory.create(
|
|
parent_location=unit.location,
|
|
category="library_content",
|
|
)
|
|
cls.block2 = BlockFactory.create(
|
|
parent_location=unit.location,
|
|
category="library_content",
|
|
)
|
|
|
|
@patch('cms.djangoapps.contentstore.api.views.utils.has_course_author_access')
|
|
@patch('cms.djangoapps.contentstore.api.views.course_validation.UserTaskStatus')
|
|
@patch('xmodule.library_content_block.LegacyLibraryContentBlock.is_ready_to_migrate_to_v2')
|
|
def test_create_update_reference_success(self, mock_block, mock_user_task_status, mock_auth):
|
|
"""
|
|
Test successful migration creation with minimal required fields.
|
|
|
|
Validates:
|
|
- 201 status code is returned
|
|
- Response contains expected serialized fields
|
|
- Request data is properly deserialized
|
|
- Permission checks are performed for both source and target
|
|
"""
|
|
mock_auth.return_value = True
|
|
|
|
mock_task_status = MagicMock(autospec=True)
|
|
mock_task_status.uuid = uuid4()
|
|
mock_task_status.state = 'Pending'
|
|
mock_task_status.state_text = 'Pending'
|
|
mock_task_status.completed_steps = 0
|
|
mock_task_status.total_steps = 10
|
|
mock_task_status.attempts = 1
|
|
mock_task_status.created = '2025-01-01T00:00:00Z'
|
|
mock_task_status.modified = '2025-01-01T00:00:00Z'
|
|
mock_task_status.artifacts = []
|
|
mock_task_status.migrations.all.return_value = []
|
|
|
|
mock_user_task_status.objects.get.return_value = mock_task_status
|
|
|
|
mock_block.return_value = True
|
|
|
|
self.client.login(username=self.staff.username, password=self.password)
|
|
response = self.client.post(
|
|
f'/api/courses/v1/migrate_legacy_content_blocks/{self.course_key}/',
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_201_CREATED
|
|
|
|
assert 'uuid' in response.data
|
|
assert 'state' in response.data
|
|
assert 'state_text' in response.data
|
|
assert 'completed_steps' in response.data
|
|
assert 'total_steps' in response.data
|
|
|
|
mock_auth.assert_called_once()
|
|
|
|
@patch('cms.djangoapps.contentstore.api.views.utils.has_course_author_access')
|
|
@patch('xmodule.library_content_block.LegacyLibraryContentBlock.is_ready_to_migrate_to_v2')
|
|
def test_list_ready_to_update_reference_success(self, mock_block, mock_auth):
|
|
"""
|
|
Test successful migration creation with minimal required fields.
|
|
|
|
Validates:
|
|
- 201 status code is returned
|
|
- Response contains expected serialized fields
|
|
- Request data is properly deserialized
|
|
- Permission checks are performed for both source and target
|
|
"""
|
|
mock_auth.return_value = True
|
|
mock_block.return_value = True
|
|
|
|
self.client.login(username=self.staff.username, password=self.password)
|
|
response = self.client.get(
|
|
f'/api/courses/v1/migrate_legacy_content_blocks/{self.course_key}/',
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
|
|
data = response.json()
|
|
self.assertListEqual(data, [
|
|
{'usage_key': str(self.block1.location)},
|
|
{'usage_key': str(self.block2.location)},
|
|
])
|
|
mock_auth.assert_called_once()
|