feat!: drop legacy courseware tab access for learners

The only way to access the legacy courseware is now through the
Studio preview feature (and at some point, when the MFE supports a
preview mode, we can then remove even that).

This drops the courseware.use_legacy_frontend waffle.
This commit is contained in:
Michael Terry
2022-04-12 12:53:20 -04:00
parent 9b1db048f8
commit afd19f0513
25 changed files with 229 additions and 1155 deletions

View File

@@ -2554,7 +2554,6 @@ paths:
* `"empty"`: no start date is specified
* pacing: Course pacing. Possible values: instructor, self
* user_timezone: User's chosen timezone setting (or null for browser default)
* can_view_legacy_courseware: Indicates whether the user is able to see the legacy courseware view
* user_has_passing_grade: Whether or not the effective user's grade is equal to or above the courses minimum
passing grade
* course_exit_page_is_active: Flag for the learning mfe on whether or not the course exit page should display

View File

@@ -17,7 +17,6 @@ from django.test.utils import override_settings
from django.urls import resolve, reverse
from django.utils.translation import gettext as _
from edx_django_utils.cache import RequestCache
from edx_toggles.toggles.testutils import override_waffle_flag
from opaque_keys.edx.keys import CourseKey
from pytz import UTC
from xmodule.modulestore import ModuleStoreEnum
@@ -41,7 +40,6 @@ from lms.djangoapps.courseware.tabs import get_course_tab_list
from lms.djangoapps.courseware.tests.factories import StudentModuleFactory
from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase
from lms.djangoapps.courseware.testutils import FieldOverrideTestMixin
from lms.djangoapps.courseware.toggles import COURSEWARE_USE_LEGACY_FRONTEND
from lms.djangoapps.discussion.django_comment_client.utils import has_forum_access
from lms.djangoapps.grades.api import task_compute_all_grades_for_course
from lms.djangoapps.instructor.access import allow_access, list_with_level
@@ -1219,12 +1217,8 @@ class TestStudentViewsWithCCX(ModuleStoreTestCase):
assert response.status_code == 200
assert re.search('Test CCX', response.content.decode('utf-8'))
@override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True)
def test_load_courseware(self):
self.client.login(username=self.student.username, password=self.student_password)
response = self.client.get(reverse('courseware_section', kwargs={
'course_id': str(self.ccx_course_key),
'chapter': 'chapter_x',
'section': 'sequential_x1',
}))
sequence_key = self.ccx_course_key.make_usage_key('sequential', 'sequential_x1')
response = self.client.get(reverse('render_xblock', args=[str(sequence_key)]))
assert response.status_code == 200

View File

@@ -21,7 +21,6 @@ from lms.djangoapps.courseware.context_processor import user_timezone_locale_pre
from lms.djangoapps.courseware.courses import check_course_access
from lms.djangoapps.courseware.masquerade import setup_masquerade
from lms.djangoapps.courseware.tabs import get_course_tab_list
from lms.djangoapps.courseware.toggles import courseware_mfe_is_visible
class CourseHomeMetadataView(RetrieveAPIView):
@@ -102,12 +101,6 @@ class CourseHomeMetadataView(RetrieveAPIView):
enrollment = CourseEnrollment.get_enrollment(request.user, course_key_string)
user_is_enrolled = bool(enrollment and enrollment.is_active)
can_load_courseware = courseware_mfe_is_visible(
course_key=course_key,
is_global_staff=original_user_is_global_staff,
is_course_staff=original_user_is_staff
)
# User locale settings
user_timezone_locale = user_timezone_locale_prefs(request)
user_timezone = user_timezone_locale['user_timezone']
@@ -133,7 +126,7 @@ class CourseHomeMetadataView(RetrieveAPIView):
'is_self_paced': getattr(course, 'self_paced', False),
'is_enrolled': user_is_enrolled,
'course_access': load_access.to_json(),
'can_load_courseware': can_load_courseware,
'can_load_courseware': True, # can be removed once the MFE no longer references this field
'celebrations': celebrations,
'user_timezone': user_timezone,
'can_view_certificate': certificates_viewable_for_course(course),

View File

@@ -7,8 +7,9 @@ import ast
import json
from collections import OrderedDict
from datetime import timedelta
from unittest.mock import Mock
from unittest.mock import Mock, patch
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
from django.test import TestCase
@@ -18,6 +19,7 @@ from django.utils.timezone import now
from xblock.field_data import DictFieldData
from common.djangoapps.edxmako.shortcuts import render_to_string
from lms.djangoapps.courseware import access_utils
from lms.djangoapps.courseware.access import has_access
from lms.djangoapps.courseware.utils import verified_upgrade_deadline_link
from lms.djangoapps.courseware.masquerade import MasqueradeView
@@ -451,3 +453,11 @@ def get_context_dict_from_string(data):
sorted(json.loads(cleaned_data['metadata']).items(), key=lambda t: t[0])
)
return cleaned_data
def set_preview_mode(preview_mode: bool):
"""
A decorator to force the preview mode on or off.
"""
hostname = settings.FEATURES.get('PREVIEW_LMS_BASE') if preview_mode else None
return patch.object(access_utils, 'get_current_request_hostname', new=lambda: hostname)

View File

@@ -3,10 +3,9 @@ Tests use cases related to LMS Entrance Exam behavior, such as gated content acc
"""
from unittest.mock import Mock, patch
from unittest.mock import patch
from crum import set_current_request
from django.urls import reverse
from edx_toggles.toggles.testutils import override_waffle_flag
from milestones.tests.utils import MilestonesTestCaseMixin
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
@@ -21,11 +20,9 @@ from lms.djangoapps.courseware.entrance_exams import (
from lms.djangoapps.courseware.model_data import FieldDataCache
from lms.djangoapps.courseware.module_render import get_module, handle_xblock_callback, toc_for_course
from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase
from lms.djangoapps.courseware.toggles import COURSEWARE_USE_LEGACY_FRONTEND
from openedx.core.djangolib.testing.utils import get_mock_request
from openedx.features.course_experience import DISABLE_COURSE_OUTLINE_PAGE_FLAG, DISABLE_UNIFIED_COURSE_TAB_FLAG
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.tests.factories import AnonymousUserFactory, CourseEnrollmentFactory
from common.djangoapps.student.tests.factories import AnonymousUserFactory
from common.djangoapps.student.tests.factories import InstructorFactory
from common.djangoapps.student.tests.factories import RequestFactoryNoCsrf
from common.djangoapps.student.tests.factories import StaffFactory
@@ -40,7 +37,6 @@ from common.djangoapps.util.milestones_helpers import (
)
@override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True)
@patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': True})
class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTestCaseMixin):
"""
@@ -216,54 +212,6 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
]
)
def test_view_redirect_if_entrance_exam_required(self):
"""
Unit Test: if entrance exam is required. Should return a redirect.
"""
url = reverse('courseware', kwargs={'course_id': str(self.course.id)})
expected_url = reverse('courseware_section',
kwargs={
'course_id': str(self.course.id),
'chapter': self.entrance_exam.location.block_id,
'section': self.exam_1.location.block_id
})
resp = self.client.get(url)
self.assertRedirects(resp, expected_url, status_code=302, target_status_code=200)
@patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': False})
def test_entrance_exam_content_absence(self):
"""
Unit Test: If entrance exam is not enabled then page should be redirected with chapter contents.
"""
url = reverse('courseware', kwargs={'course_id': str(self.course.id)})
expected_url = reverse('courseware_section',
kwargs={
'course_id': str(self.course.id),
'chapter': self.chapter.location.block_id,
'section': self.welcome.location.block_id
})
resp = self.client.get(url)
self.assertRedirects(resp, expected_url, status_code=302, target_status_code=200)
resp = self.client.get(expected_url)
self.assertNotContains(resp, 'Exam Vertical - Unit 1')
def test_entrance_exam_content_presence(self):
"""
Unit Test: If entrance exam is enabled then its content e.g. problems should be loaded and redirection will
occur with entrance exam contents.
"""
url = reverse('courseware', kwargs={'course_id': str(self.course.id)})
expected_url = reverse('courseware_section',
kwargs={
'course_id': str(self.course.id),
'chapter': self.entrance_exam.location.block_id,
'section': self.exam_1.location.block_id
})
resp = self.client.get(url)
self.assertRedirects(resp, expected_url, status_code=302, target_status_code=200)
resp = self.client.get(expected_url)
self.assertContains(resp, 'Exam Vertical - Unit 1')
def test_get_entrance_exam_content(self):
"""
test get entrance exam content method
@@ -279,95 +227,6 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
assert exam_chapter is None
assert user_has_passed_entrance_exam(self.request.user, self.course)
def test_entrance_exam_requirement_message(self):
"""
Unit Test: entrance exam requirement message should be present in response
"""
url = reverse(
'courseware_section',
kwargs={
'course_id': str(self.course.id),
'chapter': self.entrance_exam.location.block_id,
'section': self.exam_1.location.block_id,
}
)
resp = self.client.get(url)
self.assertContains(resp, 'To access course materials, you must score')
def test_entrance_exam_requirement_message_with_correct_percentage(self):
"""
Unit Test: entrance exam requirement message should be present in response
and percentage of required score should be rounded as expected
"""
minimum_score_pct = 29
self.course.entrance_exam_minimum_score_pct = float(minimum_score_pct) / 100
self.update_course(self.course, self.request.user.id)
# answer the problem so it results in only 20% correct.
answer_entrance_exam_problem(self.course, self.request, self.problem_1, value=1, max_value=5)
url = reverse(
'courseware_section',
kwargs={
'course_id': str(self.course.id),
'chapter': self.entrance_exam.location.block_id,
'section': self.exam_1.location.block_id
}
)
resp = self.client.get(url)
self.assertContains(
resp,
f'To access course materials, you must score {minimum_score_pct}% or higher',
)
assert 'Your current score is 20%.' in resp.content.decode(resp.charset)
def test_entrance_exam_requirement_message_hidden(self):
"""
Unit Test: entrance exam message should not be present outside the context of entrance exam subsection.
"""
# Login as staff to avoid redirect to entrance exam
self.client.logout()
staff_user = StaffFactory(course_key=self.course.id)
self.client.login(username=staff_user.username, password='test')
CourseEnrollment.enroll(staff_user, self.course.id)
url = reverse(
'courseware_section',
kwargs={
'course_id': str(self.course.id),
'chapter': self.chapter.location.block_id,
'section': self.chapter_subsection.location.block_id
}
)
resp = self.client.get(url)
assert resp.status_code == 200
self.assertNotContains(resp, 'To access course materials, you must score')
self.assertNotContains(resp, 'You have passed the entrance exam.')
# TODO: LEARNER-71: Do we need to adjust or remove this test?
@override_waffle_flag(DISABLE_COURSE_OUTLINE_PAGE_FLAG, active=True)
def test_entrance_exam_passed_message_and_course_content(self):
"""
Unit Test: exam passing message and rest of the course section should be present
when user achieves the entrance exam milestone/pass the exam.
"""
url = reverse(
'courseware_section',
kwargs={
'course_id': str(self.course.id),
'chapter': self.entrance_exam.location.block_id,
'section': self.exam_1.location.block_id
}
)
answer_entrance_exam_problem(self.course, self.request, self.problem_1)
answer_entrance_exam_problem(self.course, self.request, self.problem_2)
resp = self.client.get(url)
self.assertNotContains(resp, 'To access course materials, you must score')
self.assertContains(resp, 'Your score is 100%. You have passed the entrance exam.')
self.assertContains(resp, 'Lesson 1')
def test_entrance_exam_gating(self):
"""
Unit Test: test_entrance_exam_gating
@@ -427,71 +286,6 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
for toc_section in self.expected_unlocked_toc:
assert toc_section in unlocked_toc
def test_courseware_page_access_without_passing_entrance_exam(self):
"""
Test courseware access page without passing entrance exam
"""
url = reverse(
'courseware_chapter',
kwargs={'course_id': str(self.course.id), 'chapter': self.chapter.url_name}
)
response = self.client.get(url)
expected_url = reverse('courseware_section',
kwargs={
'course_id': str(self.course.id),
'chapter': self.entrance_exam.location.block_id,
'section': self.exam_1.location.block_id
})
self.assertRedirects(response, expected_url, status_code=302, target_status_code=200)
@override_waffle_flag(DISABLE_UNIFIED_COURSE_TAB_FLAG, active=True)
def test_courseinfo_page_access_without_passing_entrance_exam(self):
"""
Test courseware access page without passing entrance exam
"""
url = reverse('info', args=[str(self.course.id)])
response = self.client.get(url)
redirect_url = reverse('courseware', args=[str(self.course.id)])
self.assertRedirects(response, redirect_url, status_code=302, target_status_code=302)
response = self.client.get(redirect_url)
exam_url = response.get('Location')
self.assertRedirects(response, exam_url)
@patch('lms.djangoapps.courseware.entrance_exams.get_entrance_exam_content', Mock(return_value=None))
def test_courseware_page_access_after_passing_entrance_exam(self):
"""
Test courseware access page after passing entrance exam
"""
self._assert_chapter_loaded(self.course, self.chapter)
@patch('common.djangoapps.util.milestones_helpers.get_required_content', Mock(return_value=['a value']))
def test_courseware_page_access_with_staff_user_without_passing_entrance_exam(self):
"""
Test courseware access page without passing entrance exam but with staff user
"""
self.logout()
staff_user = StaffFactory.create(course_key=self.course.id)
self.login(staff_user.email, 'test')
CourseEnrollmentFactory(user=staff_user, course_id=self.course.id)
self._assert_chapter_loaded(self.course, self.chapter)
def test_courseware_page_access_with_staff_user_after_passing_entrance_exam(self):
"""
Test courseware access page after passing entrance exam but with staff user
"""
self.logout()
staff_user = StaffFactory.create(course_key=self.course.id)
self.login(staff_user.email, 'test')
CourseEnrollmentFactory(user=staff_user, course_id=self.course.id)
self._assert_chapter_loaded(self.course, self.chapter)
@patch.dict("django.conf.settings.FEATURES", {'ENTRANCE_EXAMS': False})
def test_courseware_page_access_when_entrance_exams_disabled(self):
"""
Test courseware page access when ENTRANCE_EXAMS feature is disabled
"""
self._assert_chapter_loaded(self.course, self.chapter)
def test_can_skip_entrance_exam_with_anonymous_user(self):
"""
Test can_skip_entrance_exam method with anonymous user
@@ -540,17 +334,6 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
assert response.status_code == 200
self.assertContains(response, 'entrance_exam_passed')
def _assert_chapter_loaded(self, course, chapter):
"""
Asserts courseware chapter load successfully.
"""
url = reverse(
'courseware_chapter',
kwargs={'course_id': str(course.id), 'chapter': chapter.url_name}
)
response = self.client.get(url)
assert response.status_code == 200
def _return_table_of_contents(self):
"""
Returns table of content for the entrance exam specific to this test

View File

@@ -26,9 +26,10 @@ from lms.djangoapps.courseware.masquerade import (
setup_masquerade,
)
from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase, MasqueradeMixin, masquerade_as_group_member
from lms.djangoapps.courseware.tests.helpers import (
LoginEnrollmentTestCase, MasqueradeMixin, masquerade_as_group_member, set_preview_mode,
)
from lms.djangoapps.courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin
from lms.djangoapps.courseware.toggles import COURSEWARE_USE_LEGACY_FRONTEND
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference, set_user_preference
@@ -42,7 +43,6 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory # li
from xmodule.partitions.partitions import Group, UserPartition # lint-amnesty, pylint: disable=wrong-import-order
@override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True)
class MasqueradeTestCase(SharedModuleStoreTestCase, LoginEnrollmentTestCase, MasqueradeMixin):
"""
Base class for masquerade tests that sets up a test course and enrolls a user in the course.
@@ -189,32 +189,6 @@ class MasqueradeTestCase(SharedModuleStoreTestCase, LoginEnrollmentTestCase, Mas
assert 200 == masquerade_as_group_member(self.test_user, self.course, partition_id, group_id)
class NormalStudentVisibilityTest(MasqueradeTestCase):
"""
Verify the course displays as expected for a "normal" student (to ensure test setup is correct).
"""
def create_user(self):
"""
Creates a normal student user.
"""
return UserFactory()
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
def test_staff_debug_not_visible(self):
"""
Tests that staff debug control is not present for a student.
"""
self.verify_staff_debug_present(False)
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
def test_show_answer_not_visible(self):
"""
Tests that "Show Answer" is not visible for a student.
"""
self.verify_show_answer_present(False)
class StaffMasqueradeTestCase(MasqueradeTestCase):
"""
Base class for tests of the masquerade behavior for a staff member.
@@ -285,6 +259,7 @@ class TestMasqueradeOptionsNoContentGroups(StaffMasqueradeTestCase):
assert is_target_available == expected
@set_preview_mode(True)
class TestStaffMasqueradeAsStudent(StaffMasqueradeTestCase):
"""
Check for staff being able to masquerade as student.

View File

@@ -15,19 +15,15 @@ from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from common.djangoapps.student.tests.factories import GlobalStaffFactory
from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase
from lms.djangoapps.courseware.toggles import COURSEWARE_USE_LEGACY_FRONTEND
from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase, set_preview_mode
from openedx.features.course_experience import DISABLE_COURSE_OUTLINE_PAGE_FLAG
from common.djangoapps.student.tests.factories import UserFactory
@override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True)
@set_preview_mode(True)
class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
"""
Check that navigation state is saved properly.
"""
STUDENT_INFO = [('view@test.com', 'foo'), ('view2@test.com', 'foo')]
@classmethod
def setUpClass(cls):
# pylint: disable=super-method-not-called
@@ -71,18 +67,11 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
display_name='pdf_textbooks_tab',
default_tab='progress')
cls.staff_user = GlobalStaffFactory()
cls.user = UserFactory()
cls.user = GlobalStaffFactory(password='test')
def setUp(self):
super().setUp()
# Create student accounts and activate them.
for i in range(len(self.STUDENT_INFO)):
email, password = self.STUDENT_INFO[i]
username = f'u{i}'
self.create_account(username, email, password)
self.activate_user(email)
self.login(self.user.email, 'test')
def assertTabActive(self, tabname, response):
''' Check if the progress tab is active in the tab set '''
@@ -106,10 +95,6 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
- Accordion enabled, or disabled
- Navigation tabs enabled, disabled, or redirected
'''
email, password = self.STUDENT_INFO[0]
self.login(email, password)
self.enroll(self.course, True)
test_data = (
('tabs', False, True),
('none', False, False),
@@ -143,9 +128,6 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
Verify that an inactive session times out and redirects to the
login page
"""
email, password = self.STUDENT_INFO[0]
self.login(email, password)
# make sure we can access courseware immediately
resp = self.client.get(reverse('dashboard'))
assert resp.status_code == 200
@@ -163,11 +145,6 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
Verify that the first time we click on the courseware tab we are
redirected to the 'Welcome' section.
"""
email, password = self.STUDENT_INFO[0]
self.login(email, password)
self.enroll(self.course, True)
self.enroll(self.test_course, True)
resp = self.client.get(reverse('courseware',
kwargs={'course_id': str(self.course.id)}))
self.assertRedirects(resp, reverse(
@@ -180,11 +157,6 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
Verify the accordion remembers we've already visited the Welcome section
and redirects correspondingly.
"""
email, password = self.STUDENT_INFO[0]
self.login(email, password)
self.enroll(self.course, True)
self.enroll(self.test_course, True)
section_url = reverse(
'courseware_section',
kwargs={
@@ -203,11 +175,6 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
"""
Verify the accordion remembers which chapter you were last viewing.
"""
email, password = self.STUDENT_INFO[0]
self.login(email, password)
self.enroll(self.course, True)
self.enroll(self.test_course, True)
# Now we directly navigate to a section in a chapter other than 'Overview'.
section_url = reverse(
'courseware_section',
@@ -230,11 +197,6 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
# TODO: LEARNER-71: Do we need to adjust or remove this test?
@override_waffle_flag(DISABLE_COURSE_OUTLINE_PAGE_FLAG, active=True)
def test_incomplete_course(self):
email = self.staff_user.email
password = "test"
self.login(email, password)
self.enroll(self.test_course, True)
test_course_id = str(self.test_course.id)
url = reverse(
@@ -284,11 +246,6 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
courseware pages if either the FEATURE flag is turned off
or the course is not proctored enabled
"""
email, password = self.STUDENT_INFO[0]
self.login(email, password)
self.enroll(self.test_course_proctored, True)
test_course_id = str(self.test_course_proctored.id)
with patch.dict(settings.FEATURES, {'ENABLE_SPECIAL_EXAMS': False}):

View File

@@ -5,19 +5,16 @@ Test for split test XModule
from unittest.mock import MagicMock
from django.urls import reverse
from edx_toggles.toggles.testutils import override_waffle_flag
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.partitions.partitions import Group, UserPartition
from lms.djangoapps.courseware.model_data import FieldDataCache
from lms.djangoapps.courseware.module_render import get_module_for_descriptor
from lms.djangoapps.courseware.toggles import COURSEWARE_USE_LEGACY_FRONTEND
from openedx.core.djangoapps.user_api.tests.factories import UserCourseTagFactory
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
@override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True)
class SplitTestBase(ModuleStoreTestCase):
"""
Sets up a basic course and user for split test testing.
@@ -118,12 +115,7 @@ class SplitTestBase(ModuleStoreTestCase):
value=str(user_tag)
)
resp = self.client.get(reverse(
'courseware_section',
kwargs={'course_id': str(self.course.id),
'chapter': self.chapter.url_name,
'section': self.sequential.url_name}
))
resp = self.client.get(reverse('render_xblock', args=[str(self.sequential.location)]))
unicode_content = resp.content.decode(resp.charset)
# Assert we see the proper icon in the top display

View File

@@ -2,14 +2,13 @@
Tests courseware views.py
"""
import html
import itertools
import json
import re
from datetime import datetime, timedelta
from unittest.mock import MagicMock, PropertyMock, create_autospec, patch
from urllib.parse import urlencode
from urllib.parse import quote, urlencode
from uuid import uuid4
import ddt
@@ -26,15 +25,12 @@ from django.test.utils import override_settings
from django.urls import reverse, reverse_lazy
from edx_toggles.toggles.testutils import override_waffle_flag, override_waffle_switch
from freezegun import freeze_time
from markupsafe import escape
from milestones.tests.utils import MilestonesTestCaseMixin
from opaque_keys.edx.keys import CourseKey, UsageKey
from pytz import UTC
from rest_framework import status
from web_fragments.fragment import Fragment
from xblock.core import XBlock
from xblock.fields import Scope, String
from xmodule.course_module import COURSE_VISIBILITY_PRIVATE, COURSE_VISIBILITY_PUBLIC, COURSE_VISIBILITY_PUBLIC_OUTLINE
from xmodule.data import CertificatesDisplayBehaviors
from xmodule.graders import ShowCorrectness
from xmodule.modulestore import ModuleStoreEnum
@@ -71,19 +67,13 @@ from lms.djangoapps.certificates.tests.factories import (
)
from lms.djangoapps.commerce.models import CommerceConfiguration
from lms.djangoapps.commerce.utils import EcommerceService
from lms.djangoapps.courseware import access_utils
from lms.djangoapps.courseware.access_utils import check_course_open_for_learner
from lms.djangoapps.courseware.model_data import FieldDataCache, set_score
from lms.djangoapps.courseware.module_render import get_module, handle_xblock_callback
from lms.djangoapps.courseware.tests.factories import StudentModuleFactory
from lms.djangoapps.courseware.tests.helpers import get_expiration_banner_text
from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin, get_expiration_banner_text, set_preview_mode
from lms.djangoapps.courseware.testutils import RenderXBlockTestMixin
from lms.djangoapps.courseware.toggles import (
COURSEWARE_MICROFRONTEND_COURSE_TEAM_PREVIEW,
COURSEWARE_OPTIMIZED_RENDER_XBLOCK,
COURSEWARE_USE_LEGACY_FRONTEND,
courseware_mfe_is_advertised
)
from lms.djangoapps.courseware.toggles import COURSEWARE_OPTIMIZED_RENDER_XBLOCK
from lms.djangoapps.courseware.user_state_client import DjangoXBlockUserStateClient
from lms.djangoapps.grades.config.waffle import ASSUME_ZERO_GRADE_IF_ABSENT
from lms.djangoapps.grades.config.waffle import waffle_switch as grades_waffle_switch
@@ -97,18 +87,15 @@ from openedx.core.djangoapps.credit.api import set_credit_requirements
from openedx.core.djangoapps.credit.models import CreditCourse, CreditProvider
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from openedx.core.djangolib.testing.utils import get_mock_request
from openedx.core.lib.gating import api as gating_api
from openedx.core.lib.url_utils import quote_slashes
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
from openedx.features.course_experience import (
COURSE_ENABLE_UNENROLLED_ACCESS_FLAG,
DISABLE_COURSE_OUTLINE_PAGE_FLAG,
DISABLE_UNIFIED_COURSE_TAB_FLAG,
)
from openedx.features.course_experience.tests.views.helpers import add_course_mode
from openedx.features.course_experience.url_helpers import (
ExperienceOption,
get_courseware_url,
get_learning_mfe_home_url,
make_learning_mfe_courseware_url
@@ -121,61 +108,38 @@ FEATURES_WITH_DISABLE_HONOR_CERTIFICATE = settings.FEATURES.copy()
FEATURES_WITH_DISABLE_HONOR_CERTIFICATE['DISABLE_HONOR_CERTIFICATES'] = True
def _set_mfe_flag(activate_mfe: bool):
"""
A decorator/contextmanager to force the base courseware MFE flag on or off.
"""
return override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=(not activate_mfe))
def _set_preview_mfe_flag(active: bool):
"""
A decorator/contextmanager to force the courseware MFE educator preview flag on or off.
"""
return override_waffle_flag(COURSEWARE_MICROFRONTEND_COURSE_TEAM_PREVIEW, active=active)
@ddt.ddt
class TestJumpTo(ModuleStoreTestCase):
"""
Check the jumpto link for a course.
"""
@ddt.data(
(False, None, False), # not provided -> Active experience
(False, "blarfingar", False), # nonsense -> Active experience
(False, "legacy", False), # "legacy" -> Legacy experience
(False, "new", True), # "new" -> MFE experience
(True, None, True), # not provided -> Active experience
(True, "blarfingar", True), # nonsense -> Active experience
(True, "legacy", False), # "legacy" -> Legacy experience
(True, "new", True), # "new" -> MFE experience
(True, False), # preview -> Legacy experience
(False, True), # no preview -> MFE experience
)
@ddt.unpack
def test_jump_to_legacy_vs_mfe(self, activate_mfe, experience_param, expect_mfe):
def test_jump_to_legacy_vs_mfe(self, preview_mode, expect_mfe):
"""
Test that jump_to and jump_to_id correctly choose which courseware
frontend to redirect to, taking into account the '?experience=' query
param.
Test that jump_to and jump_to_id correctly choose which courseware frontend to redirect to.
Will be removed along with DEPR-109.
Can be removed when the MFE supports a preview mode.
"""
course = CourseFactory.create()
chapter = ItemFactory.create(category='chapter', parent_location=course.location)
querystring = f"experience={experience_param}" if experience_param else ""
if expect_mfe:
expected_url = f'http://learning-mfe/course/{course.id}/{chapter.location}'
else:
expected_url = f'/courses/{course.id}/courseware/{chapter.url_name}/'
jumpto_url = f'/courses/{course.id}/jump_to/{chapter.location}?{querystring}'
with _set_mfe_flag(activate_mfe):
jumpto_url = f'/courses/{course.id}/jump_to/{chapter.location}'
with set_preview_mode(preview_mode):
response = self.client.get(jumpto_url)
assert response.status_code == 302
# Check the response URL, but chop off the querystring; we don't care here.
assert response.url.split('?')[0] == expected_url
jumpto_id_url = f'/courses/{course.id}/jump_to_id/{chapter.url_name}?{querystring}'
with _set_mfe_flag(activate_mfe):
jumpto_id_url = f'/courses/{course.id}/jump_to_id/{chapter.url_name}'
with set_preview_mode(preview_mode):
response = self.client.get(jumpto_id_url)
assert response.status_code == 302
# Check the response URL, but chop off the querystring; we don't care here.
@@ -187,25 +151,25 @@ class TestJumpTo(ModuleStoreTestCase):
(True, ModuleStoreEnum.Type.split),
)
@ddt.unpack
def test_jump_to_invalid_location(self, activate_mfe, store_type):
def test_jump_to_invalid_location(self, preview_mode, store_type):
"""Confirm that invalid locations redirect back to a general course URL"""
with self.store.default_store(store_type):
course = CourseFactory.create()
location = course.id.make_usage_key(None, 'NoSuchPlace')
expected_redirect_url = (
f'http://learning-mfe/course/{course.id}'
) if activate_mfe else (
f'/courses/{course.id}/courseware?' + urlencode({'activate_block_id': str(course.location)})
) if preview_mode else (
f'http://learning-mfe/course/{course.id}'
)
# This is fragile, but unfortunately the problem is that within the LMS we
# can't use the reverse calls from the CMS
jumpto_url = f'/courses/{course.id}/jump_to/{location}'
with _set_mfe_flag(activate_mfe):
with set_preview_mode(preview_mode):
response = self.client.get(jumpto_url)
assert response.status_code == 302
assert response.url == expected_redirect_url
@_set_mfe_flag(activate_mfe=False)
@set_preview_mode(True)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_jump_to_legacy_from_sequence(self, store_type):
with self.store.default_store(store_type):
@@ -220,7 +184,7 @@ class TestJumpTo(ModuleStoreTestCase):
response = self.client.get(jumpto_url)
self.assertRedirects(response, expected_redirect_url, status_code=302, target_status_code=302)
@_set_mfe_flag(activate_mfe=True)
@set_preview_mode(False)
def test_jump_to_mfe_from_sequence(self):
course = CourseFactory.create()
chapter = ItemFactory.create(category='chapter', parent_location=course.location)
@@ -233,7 +197,7 @@ class TestJumpTo(ModuleStoreTestCase):
assert response.status_code == 302
assert response.url == expected_redirect_url
@_set_mfe_flag(activate_mfe=False)
@set_preview_mode(True)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_jump_to_legacy_from_module(self, store_type):
with self.store.default_store(store_type):
@@ -261,7 +225,7 @@ class TestJumpTo(ModuleStoreTestCase):
response = self.client.get(jumpto_url)
self.assertRedirects(response, expected_redirect_url, status_code=302, target_status_code=302)
@_set_mfe_flag(activate_mfe=True)
@set_preview_mode(False)
def test_jump_to_mfe_from_module(self):
course = CourseFactory.create()
chapter = ItemFactory.create(category='chapter', parent_location=course.location)
@@ -289,7 +253,7 @@ class TestJumpTo(ModuleStoreTestCase):
# The new courseware experience does not support this sort of course structure;
# it assumes a simple course->chapter->sequence->unit->component tree.
@_set_mfe_flag(activate_mfe=False)
@set_preview_mode(True)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_jump_to_legacy_from_nested_module(self, store_type):
with self.store.default_store(store_type):
@@ -319,15 +283,15 @@ class TestJumpTo(ModuleStoreTestCase):
(True, ModuleStoreEnum.Type.split),
)
@ddt.unpack
def test_jump_to_id_invalid_location(self, activate_mfe, store_type):
def test_jump_to_id_invalid_location(self, preview_mode, store_type):
with self.store.default_store(store_type):
course = CourseFactory.create()
jumpto_url = f'/courses/{course.id}/jump_to/NoSuchPlace'
with _set_mfe_flag(activate_mfe):
with set_preview_mode(preview_mode):
response = self.client.get(jumpto_url)
assert response.status_code == 404
@_set_mfe_flag(activate_mfe=False)
@set_preview_mode(True)
@ddt.data(
(ModuleStoreEnum.Type.mongo, False, '1'),
(ModuleStoreEnum.Type.mongo, True, '2'),
@@ -366,16 +330,14 @@ class TestJumpTo(ModuleStoreTestCase):
}
)
expected_url += "?{}".format(urlencode({'activate_block_id': str(staff_only_vertical.location)}))
assert expected_url == get_courseware_url(usage_key, request, ExperienceOption.LEGACY)
assert expected_url == get_courseware_url(usage_key, request)
@ddt.ddt
@_set_mfe_flag(activate_mfe=False)
@set_preview_mode(True)
class IndexQueryTestCase(ModuleStoreTestCase):
"""
Tests for query count.
"""
CREATE_USER = False
NUM_PROBLEMS = 20
def test_index_query_counts(self):
@@ -390,11 +352,10 @@ class IndexQueryTestCase(ModuleStoreTestCase):
for _ in range(self.NUM_PROBLEMS):
ItemFactory.create(category='problem', parent_location=vertical.location)
self.user = UserFactory()
self.client.login(username=self.user.username, password=TEST_PASSWORD)
self.client.login(username=self.user.username, password=self.user_password)
CourseEnrollment.enroll(self.user, course.id)
with self.assertNumQueries(206, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
with self.assertNumQueries(203, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
with check_mongo_calls(3):
url = reverse(
'courseware_section',
@@ -408,7 +369,10 @@ class IndexQueryTestCase(ModuleStoreTestCase):
assert response.status_code == 200
class BaseViewsTestCase(ModuleStoreTestCase): # lint-amnesty, pylint: disable=missing-class-docstring
class BaseViewsTestCase(ModuleStoreTestCase, MasqueradeMixin):
"""Base class for courseware tests"""
CREATE_USER = False
def setUp(self):
super().setUp()
self.course = CourseFactory.create(display_name='teᴛ course', run="Testing_course")
@@ -497,16 +461,14 @@ class BaseViewsTestCase(ModuleStoreTestCase): # lint-amnesty, pylint: disable=m
@ddt.ddt
@_set_mfe_flag(activate_mfe=False)
class ViewsTestCase(BaseViewsTestCase):
@set_preview_mode(True)
class CoursewareIndexTestCase(BaseViewsTestCase):
"""
Tests for views.py methods.
Tests for the courseware index view, used for instructor previews.
"""
YESTERDAY = 'yesterday'
DATES = {
YESTERDAY: datetime.now(UTC) - timedelta(days=1),
None: None,
}
def setUp(self):
super().setUp()
self._create_global_staff_user() # this view needs staff permission
def test_index_success(self):
response = self._verify_index_response()
@@ -520,23 +482,20 @@ class ViewsTestCase(BaseViewsTestCase):
self.assertNotContains(response, self.problem.location.replace(branch=None, version_guid=None))
self.assertContains(response, self.problem2.location.replace(branch=None, version_guid=None))
@set_preview_mode(True)
def test_index_nonexistent_chapter(self):
self._verify_index_response(expected_response_code=404, chapter_name='non-existent')
def test_index_nonexistent_chapter_masquerade(self):
with patch('lms.djangoapps.courseware.views.index.setup_masquerade') as patch_masquerade:
masquerade = MagicMock(role='student')
patch_masquerade.return_value = (masquerade, self.user)
self._verify_index_response(expected_response_code=302, chapter_name='non-existent')
self.update_masquerade(username=self.user.username)
self._verify_index_response(expected_response_code=302, chapter_name='non-existent')
def test_index_nonexistent_section(self):
self._verify_index_response(expected_response_code=404, section_name='non-existent')
def test_index_nonexistent_section_masquerade(self):
with patch('lms.djangoapps.courseware.views.index.setup_masquerade') as patch_masquerade:
masquerade = MagicMock(role='student')
patch_masquerade.return_value = (masquerade, self.user)
self._verify_index_response(expected_response_code=302, section_name='non-existent')
self.update_masquerade(username=self.user.username)
self._verify_index_response(expected_response_code=302, section_name='non-existent')
def _verify_index_response(self, expected_response_code=200, chapter_name=None, section_name=None):
"""
@@ -555,25 +514,76 @@ class ViewsTestCase(BaseViewsTestCase):
assert response.status_code == expected_response_code
return response
def test_index_no_visible_section_in_chapter(self):
def test_get_redirect_url(self):
# test the course location
assert '/courses/{course_key}/courseware?{activate_block_id}'.format(
course_key=str(self.course_key),
activate_block_id=urlencode({'activate_block_id': str(self.course.location)})
) == get_courseware_url(self.course.location)
# test a section location
assert '/courses/{course_key}/courseware/Chapter_1/Sequential_1/?{activate_block_id}'.format(
course_key=str(self.course_key),
activate_block_id=urlencode({'activate_block_id': str(self.section.location)})
) == get_courseware_url(self.section.location)
# reload the chapter from the store so its children information is updated
self.chapter = self.store.get_item(self.chapter.location)
def test_index_invalid_position(self):
request_url = '/'.join([
'/courses',
str(self.course.id),
'courseware',
self.chapter.location.block_id,
self.section.location.block_id,
'f'
])
response = self.client.get(request_url)
assert response.status_code == 404
# disable the visibility of the sections in the chapter
for section in self.chapter.get_children():
section.visible_to_staff_only = True
self.store.update_item(section, ModuleStoreEnum.UserID.test)
def test_unicode_handling_in_url(self):
url_parts = [
'/courses',
str(self.course.id),
'courseware',
self.chapter.location.block_id,
self.section.location.block_id,
'1'
]
for idx, val in enumerate(url_parts):
url_parts_copy = url_parts[:]
url_parts_copy[idx] = val + 'χ'
request_url = '/'.join(url_parts_copy)
response = self.client.get(request_url)
assert response.status_code == 404
url = reverse(
'courseware_chapter',
kwargs={'course_id': str(self.course.id),
'chapter': str(self.chapter.location.block_id)},
# TODO: TNL-6387: Remove test
@override_waffle_flag(DISABLE_COURSE_OUTLINE_PAGE_FLAG, active=True)
def test_accordion(self):
"""
This needs a response_context, which is not included in the render_accordion's main method
returning a render_to_string, so we will render via the courseware URL in order to include
the needed context
"""
response = self.client.get(
reverse('courseware', args=[str(self.course.id)]),
follow=True
)
response = self.client.get(url)
assert response.status_code == 200
self.assertNotContains(response, 'Problem 1')
self.assertNotContains(response, 'Problem 2')
test_responses = [
'<p class="accordion-display-name">Sequential 1 <span class="sr">current section</span></p>',
'<p class="accordion-display-name">Sequential 2 </p>'
]
for test in test_responses:
self.assertContains(response, test)
@ddt.ddt
class ViewsTestCase(BaseViewsTestCase):
"""
Tests for views.py methods.
"""
YESTERDAY = 'yesterday'
DATES = {
YESTERDAY: datetime.now(UTC) - timedelta(days=1),
None: None,
}
def test_mfe_link_from_about_page(self):
"""
@@ -582,7 +592,6 @@ class ViewsTestCase(BaseViewsTestCase):
with self.store.default_store(ModuleStoreEnum.Type.split):
course = CourseFactory.create()
CourseEnrollment.enroll(self.user, course.id)
assert self.client.login(username=self.user.username, password=TEST_PASSWORD)
response = self.client.get(reverse('about_course', args=[str(course.id)]))
self.assertContains(response, get_learning_mfe_home_url(course_key=course.id, url_fragment='home'))
@@ -592,14 +601,8 @@ class ViewsTestCase(BaseViewsTestCase):
creates the courseware url and enroll staff url
"""
# create the _next parameter
courseware_url = reverse(
'courseware_section',
kwargs={
'course_id': str(self.course_key),
'chapter': str(self.chapter.location.block_id),
'section': str(self.section.location.block_id),
}
)
courseware_url = make_learning_mfe_courseware_url(self.course.id, self.chapter.location, self.section.location)
courseware_url = quote(courseware_url, safe=':/')
# create the url for enroll_staff view
enroll_url = "{enroll_url}?next={courseware_url}".format(
enroll_url=reverse('enroll_staff', kwargs={'course_id': str(self.course.id)}),
@@ -617,14 +620,11 @@ class ViewsTestCase(BaseViewsTestCase):
"""
self._create_global_staff_user()
courseware_url, enroll_url = self._create_url_for_enroll_staff()
response = self.client.post(enroll_url, data=data, follow=True)
assert response.status_code == 200
response = self.client.post(enroll_url, data=data)
# we were redirected to our current location
assert 302 in response.redirect_chain[0]
assert len(response.redirect_chain) == 1
if enrollment:
self.assertRedirects(response, courseware_url)
self.assertRedirects(response, courseware_url, fetch_redirect_response=False)
else:
self.assertRedirects(response, f'/courses/{str(self.course_key)}/about')
@@ -681,22 +681,6 @@ class ViewsTestCase(BaseViewsTestCase):
type(mock_user).is_authenticated = PropertyMock(return_value=False)
assert views.user_groups(mock_user) == []
def test_get_redirect_url(self):
# test the course location
assert '/courses/{course_key}/courseware?{activate_block_id}'.format(
course_key=str(self.course_key),
activate_block_id=urlencode({'activate_block_id': str(self.course.location)})
) == get_courseware_url(
self.course.location, experience=ExperienceOption.LEGACY
)
# test a section location
assert '/courses/{course_key}/courseware/Chapter_1/Sequential_1/?{activate_block_id}'.format(
course_key=str(self.course_key),
activate_block_id=urlencode({'activate_block_id': str(self.section.location)})
) == get_courseware_url(
self.section.location, experience=ExperienceOption.LEGACY
)
def test_invalid_course_id(self):
response = self.client.get('/courses/MITx/3.091X/')
assert response.status_code == 404
@@ -705,36 +689,6 @@ class ViewsTestCase(BaseViewsTestCase):
response = self.client.get('/courses/MITx/')
assert response.status_code == 404
def test_index_invalid_position(self):
request_url = '/'.join([
'/courses',
str(self.course.id),
'courseware',
self.chapter.location.block_id,
self.section.location.block_id,
'f'
])
assert self.client.login(username=self.user.username, password=TEST_PASSWORD)
response = self.client.get(request_url)
assert response.status_code == 404
def test_unicode_handling_in_url(self):
url_parts = [
'/courses',
str(self.course.id),
'courseware',
self.chapter.location.block_id,
self.section.location.block_id,
'1'
]
assert self.client.login(username=self.user.username, password=TEST_PASSWORD)
for idx, val in enumerate(url_parts):
url_parts_copy = url_parts[:]
url_parts_copy[idx] = val + 'χ'
request_url = '/'.join(url_parts_copy)
response = self.client.get(request_url)
assert response.status_code == 404
def test_jump_to_invalid(self):
# TODO add a test for invalid location
# TODO add a test for no data *
@@ -1063,25 +1017,6 @@ class ViewsTestCase(BaseViewsTestCase):
response = self.client.get(reverse('info', args=[course_id]), HTTP_REFERER='foo')
assert response.status_code == 200
# TODO: TNL-6387: Remove test
@override_waffle_flag(DISABLE_COURSE_OUTLINE_PAGE_FLAG, active=True)
def test_accordion(self):
"""
This needs a response_context, which is not included in the render_accordion's main method
returning a render_to_string, so we will render via the courseware URL in order to include
the needed context
"""
response = self.client.get(
reverse('courseware', args=[str(self.course.id)]),
follow=True
)
test_responses = [
'<p class="accordion-display-name">Sequential 1 <span class="sr">current section</span></p>',
'<p class="accordion-display-name">Sequential 2 </p>'
]
for test in test_responses:
self.assertContains(response, test)
# Patching 'lms.djangoapps.courseware.views.views.get_programs' would be ideal,
# but for some unknown reason that patch doesn't seem to be applied.
@@ -1126,7 +1061,6 @@ class TestProgramMarketingView(SharedModuleStoreTestCase):
# setting TIME_ZONE_DISPLAYED_FOR_DEADLINES explicitly
@override_settings(TIME_ZONE_DISPLAYED_FOR_DEADLINES="UTC")
@_set_mfe_flag(activate_mfe=False)
class BaseDueDateTests(ModuleStoreTestCase):
"""
Base class that verifies that due dates are rendered correctly on a page
@@ -1162,8 +1096,7 @@ class BaseDueDateTests(ModuleStoreTestCase):
def setUp(self):
super().setUp()
self.user = UserFactory.create()
assert self.client.login(username=self.user.username, password='test')
assert self.client.login(username=self.user.username, password=self.user_password)
self.time_with_tz = "2013-09-18 11:30:00+00:00"
@@ -1216,6 +1149,7 @@ class TestProgressDueDate(BaseDueDateTests):
# TODO: LEARNER-71: Delete entire TestAccordionDueDate class
@set_preview_mode(True)
class TestAccordionDueDate(BaseDueDateTests):
"""
Test that the accordion page displays due dates correctly
@@ -2504,7 +2438,7 @@ class ViewCheckerBlock(XBlock):
@ddt.ddt
@_set_mfe_flag(activate_mfe=False)
@set_preview_mode(True)
class TestIndexView(ModuleStoreTestCase):
"""
Tests of the courseware.views.index view.
@@ -2514,8 +2448,6 @@ class TestIndexView(ModuleStoreTestCase):
"""
Verify that saved student state is loaded for xblocks rendered in the index view.
"""
user = UserFactory()
with modulestore().default_store(ModuleStoreEnum.Type.split):
course = CourseFactory.create()
chapter = ItemFactory.create(parent_location=course.location, category='chapter')
@@ -2528,16 +2460,16 @@ class TestIndexView(ModuleStoreTestCase):
for item in (section, vertical, block):
StudentModuleFactory.create(
student=user,
student=self.user,
course_id=course.id,
module_state_key=item.scope_ids.usage_id,
state=json.dumps({'state': str(item.scope_ids.usage_id)})
)
CourseOverview.load_from_module_store(course.id)
CourseEnrollmentFactory(user=user, course_id=course.id)
CourseEnrollmentFactory(user=self.user, course_id=course.id)
assert self.client.login(username=user.username, password='test')
assert self.client.login(username=self.user.username, password=self.user_password)
response = self.client.get(
reverse(
'courseware_section',
@@ -2553,8 +2485,6 @@ class TestIndexView(ModuleStoreTestCase):
@XBlock.register_temp_plugin(ActivateIDCheckerBlock, 'id_checker')
def test_activate_block_id(self):
user = UserFactory()
course = CourseFactory.create()
with self.store.bulk_operations(course.id):
chapter = ItemFactory.create(parent=course, category='chapter')
@@ -2563,9 +2493,9 @@ class TestIndexView(ModuleStoreTestCase):
ItemFactory.create(parent=vertical, category='id_checker', display_name="ID Checker")
CourseOverview.load_from_module_store(course.id)
CourseEnrollmentFactory(user=user, course_id=course.id)
CourseEnrollmentFactory(user=self.user, course_id=course.id)
assert self.client.login(username=user.username, password='test')
assert self.client.login(username=self.user.username, password=self.user_password)
response = self.client.get(
reverse(
'courseware_section',
@@ -2578,83 +2508,6 @@ class TestIndexView(ModuleStoreTestCase):
)
self.assertContains(response, "Activate Block ID: test_block_id")
@ddt.data(
[False, COURSE_VISIBILITY_PRIVATE, CourseUserType.ANONYMOUS, False],
[False, COURSE_VISIBILITY_PUBLIC_OUTLINE, CourseUserType.ANONYMOUS, False],
[False, COURSE_VISIBILITY_PUBLIC, CourseUserType.ANONYMOUS, False],
[True, COURSE_VISIBILITY_PRIVATE, CourseUserType.ANONYMOUS, False],
[True, COURSE_VISIBILITY_PUBLIC_OUTLINE, CourseUserType.ANONYMOUS, False],
[True, COURSE_VISIBILITY_PUBLIC, CourseUserType.ANONYMOUS, True],
[False, COURSE_VISIBILITY_PRIVATE, CourseUserType.UNENROLLED, False],
[False, COURSE_VISIBILITY_PUBLIC_OUTLINE, CourseUserType.UNENROLLED, False],
[False, COURSE_VISIBILITY_PUBLIC, CourseUserType.UNENROLLED, False],
[True, COURSE_VISIBILITY_PRIVATE, CourseUserType.UNENROLLED, False],
[True, COURSE_VISIBILITY_PUBLIC_OUTLINE, CourseUserType.UNENROLLED, False],
[True, COURSE_VISIBILITY_PUBLIC, CourseUserType.UNENROLLED, True],
[False, COURSE_VISIBILITY_PRIVATE, CourseUserType.ENROLLED, True],
[True, COURSE_VISIBILITY_PRIVATE, CourseUserType.ENROLLED, True],
[True, COURSE_VISIBILITY_PUBLIC_OUTLINE, CourseUserType.ENROLLED, True],
[True, COURSE_VISIBILITY_PUBLIC, CourseUserType.ENROLLED, True],
[False, COURSE_VISIBILITY_PRIVATE, CourseUserType.UNENROLLED_STAFF, True],
[True, COURSE_VISIBILITY_PRIVATE, CourseUserType.UNENROLLED_STAFF, True],
[True, COURSE_VISIBILITY_PUBLIC_OUTLINE, CourseUserType.UNENROLLED_STAFF, True],
[True, COURSE_VISIBILITY_PUBLIC, CourseUserType.UNENROLLED_STAFF, True],
[False, COURSE_VISIBILITY_PRIVATE, CourseUserType.GLOBAL_STAFF, True],
[True, COURSE_VISIBILITY_PRIVATE, CourseUserType.GLOBAL_STAFF, True],
[True, COURSE_VISIBILITY_PUBLIC_OUTLINE, CourseUserType.GLOBAL_STAFF, True],
[True, COURSE_VISIBILITY_PUBLIC, CourseUserType.GLOBAL_STAFF, True],
)
@ddt.unpack
def test_courseware_access(self, waffle_override, course_visibility, user_type, expected_course_content):
course = CourseFactory(course_visibility=course_visibility)
with self.store.bulk_operations(course.id):
chapter = ItemFactory(parent=course, category='chapter')
section = ItemFactory(parent=chapter, category='sequential')
vertical = ItemFactory.create(parent=section, category='vertical', display_name="Vertical")
ItemFactory.create(parent=vertical, category='html', display_name='HTML block')
ItemFactory.create(parent=vertical, category='video', display_name='Video')
self.create_user_for_course(course, user_type)
url = reverse(
'courseware_section',
kwargs={
'course_id': str(course.id),
'chapter': chapter.url_name, # lint-amnesty, pylint: disable=no-member
'section': section.url_name, # lint-amnesty, pylint: disable=no-member
}
)
with override_waffle_flag(COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, active=waffle_override):
response = self.client.get(url, follow=False)
assert response.status_code == (200 if expected_course_content else 302)
unicode_content = response.content.decode('utf-8')
if expected_course_content:
if user_type in (CourseUserType.ANONYMOUS, CourseUserType.UNENROLLED):
assert 'data-save-position="false"' in unicode_content
assert 'data-show-completion="false"' in unicode_content
assert 'xblock-public_view-sequential' in unicode_content
assert 'xblock-public_view-vertical' in unicode_content
assert 'xblock-public_view-html' in unicode_content
assert 'xblock-public_view-video' in unicode_content
if user_type == CourseUserType.ANONYMOUS and course_visibility == COURSE_VISIBILITY_PRIVATE:
assert 'To see course content' in unicode_content
if user_type == CourseUserType.UNENROLLED and course_visibility == COURSE_VISIBILITY_PRIVATE:
assert 'You must be enrolled' in unicode_content
else:
assert 'data-save-position="true"' in unicode_content
assert 'data-show-completion="true"' in unicode_content
assert 'xblock-student_view-sequential' in unicode_content
assert 'xblock-student_view-vertical' in unicode_content
assert 'xblock-student_view-html' in unicode_content
assert 'xblock-student_view-video' in unicode_content
@patch('lms.djangoapps.courseware.views.views.CourseTabView.course_open_for_learner_enrollment')
@patch('openedx.core.djangoapps.util.user_messages.PageLevelMessages.register_warning_message')
def test_courseware_messages_differentiate_for_anonymous_users(
@@ -2739,7 +2592,7 @@ class TestIndexView(ModuleStoreTestCase):
@ddt.ddt
@_set_mfe_flag(activate_mfe=False)
@set_preview_mode(True)
class TestIndexViewCompleteOnView(ModuleStoreTestCase, CompletionWaffleTestMixin):
"""
Tests CompleteOnView is set up correctly in CoursewareIndex.
@@ -2752,7 +2605,6 @@ class TestIndexViewCompleteOnView(ModuleStoreTestCase, CompletionWaffleTestMixin
# pylint:disable=attribute-defined-outside-init
self.request_factory = RequestFactoryNoCsrf()
self.user = UserFactory()
with modulestore().default_store(default_store):
self.course = CourseFactory.create()
@@ -2811,11 +2663,11 @@ class TestIndexViewCompleteOnView(ModuleStoreTestCase, CompletionWaffleTestMixin
CourseOverview.load_from_module_store(self.course.id)
CourseEnrollmentFactory(user=self.user, course_id=self.course.id)
assert self.client.login(username=self.user.username, password=self.user_password)
def test_completion_service_disabled(self):
self.setup_course(ModuleStoreEnum.Type.split)
assert self.client.login(username=self.user.username, password='test')
response = self.client.get(self.section_1_url)
self.assertNotContains(response, 'data-mark-completed-on-view-after-delay')
@@ -2828,7 +2680,6 @@ class TestIndexViewCompleteOnView(ModuleStoreTestCase, CompletionWaffleTestMixin
self.override_waffle_switch(True)
self.setup_course(ModuleStoreEnum.Type.split)
assert self.client.login(username=self.user.username, password='test')
response = self.client.get(self.section_1_url)
self.assertContains(response, 'data-mark-completed-on-view-after-delay')
@@ -2840,6 +2691,7 @@ class TestIndexViewCompleteOnView(ModuleStoreTestCase, CompletionWaffleTestMixin
content_type='application/json',
)
request.user = self.user
request.session = {}
response = handle_xblock_callback(
request,
str(self.course.id),
@@ -2858,6 +2710,7 @@ class TestIndexViewCompleteOnView(ModuleStoreTestCase, CompletionWaffleTestMixin
content_type='application/json',
)
request.user = self.user
request.session = {}
response = handle_xblock_callback(
request,
str(self.course.id),
@@ -2874,7 +2727,7 @@ class TestIndexViewCompleteOnView(ModuleStoreTestCase, CompletionWaffleTestMixin
@ddt.ddt
@_set_mfe_flag(activate_mfe=False)
@set_preview_mode(True)
class TestIndexViewWithVerticalPositions(ModuleStoreTestCase):
"""
Test the index view to handle vertical positions. Confirms that first position is loaded
@@ -2887,8 +2740,6 @@ class TestIndexViewWithVerticalPositions(ModuleStoreTestCase):
"""
super().setUp()
self.user = UserFactory()
# create course with 3 positions
self.course = CourseFactory.create()
with self.store.bulk_operations(self.course.id):
@@ -2901,7 +2752,7 @@ class TestIndexViewWithVerticalPositions(ModuleStoreTestCase):
CourseOverview.load_from_module_store(self.course.id)
self.client.login(username=self.user, password='test')
self.client.login(username=self.user, password=self.user_password)
CourseEnrollmentFactory(user=self.user, course_id=self.course.id)
def _get_course_vertical_by_position(self, input_position):
@@ -2940,130 +2791,6 @@ class TestIndexViewWithVerticalPositions(ModuleStoreTestCase):
self._assert_correct_position(resp, expected_position)
@_set_mfe_flag(activate_mfe=False)
class TestIndexViewWithGating(ModuleStoreTestCase, MilestonesTestCaseMixin):
"""
Test the index view for a course with gated content
"""
def setUp(self):
"""
Set up the initial test data
"""
super().setUp()
self.user = UserFactory()
self.course = CourseFactory.create()
with self.store.bulk_operations(self.course.id):
self.course.enable_subsection_gating = True
self.course.save()
self.course = self.update_course(self.course, 0)
self.chapter = ItemFactory.create(
parent_location=self.course.location, category="chapter", display_name="Chapter",
)
self.open_seq = ItemFactory.create(
parent_location=self.chapter.location, category='sequential', display_name="Open Sequential"
)
ItemFactory.create(parent_location=self.open_seq.location, category='problem', display_name="Problem 1")
self.gated_seq = ItemFactory.create(
parent_location=self.chapter.location, category='sequential', display_name="Gated Sequential"
)
ItemFactory.create(parent_location=self.gated_seq.location, category='problem', display_name="Problem 2")
gating_api.add_prerequisite(self.course.id, self.open_seq.location)
gating_api.set_required_content(self.course.id, self.gated_seq.location, self.open_seq.location, 100)
CourseEnrollmentFactory(user=self.user, course_id=self.course.id)
def test_index_with_gated_sequential(self):
"""
Test index view with a gated sequential raises Http404
"""
assert self.client.login(username=self.user.username, password='test')
response = self.client.get(
reverse(
'courseware_section',
kwargs={
'course_id': str(self.course.id),
'chapter': self.chapter.url_name,
'section': self.gated_seq.url_name,
}
)
)
assert response.status_code == 200
self.assertContains(response, "Content Locked")
@_set_mfe_flag(activate_mfe=False)
class TestIndexViewWithCourseDurationLimits(ModuleStoreTestCase):
"""
Test the index view for a course with course duration limits enabled.
"""
def setUp(self):
"""
Set up the initial test data.
"""
super().setUp()
self.user = UserFactory()
self.course = CourseFactory.create(start=datetime.now() - timedelta(weeks=1))
with self.store.bulk_operations(self.course.id):
self.chapter = ItemFactory.create(parent_location=self.course.location, category="chapter")
self.sequential = ItemFactory.create(parent_location=self.chapter.location, category='sequential')
self.vertical = ItemFactory.create(parent_location=self.sequential.location, category="vertical")
CourseEnrollmentFactory(user=self.user, course_id=self.course.id)
def test_index_with_course_duration_limits(self):
"""
Test that the courseware contains the course expiration banner
when course_duration_limits are enabled.
"""
CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
assert self.client.login(username=self.user.username, password='test')
add_course_mode(self.course, mode_slug=CourseMode.AUDIT)
add_course_mode(self.course)
response = self.client.get(
reverse(
'courseware_section',
kwargs={
'course_id': str(self.course.id),
'chapter': self.chapter.url_name,
'section': self.sequential.url_name,
}
)
)
bannerText = get_expiration_banner_text(self.user, self.course)
# Banner is XBlock wrapper, so it is escaped in raw response. Since
# it's escaped, ignoring the whitespace with assertContains doesn't
# work. Instead we remove all whitespace to verify content is correct.
bannerText_no_spaces = escape(bannerText).replace(' ', '')
response_no_spaces = response.content.decode('utf-8').replace(' ', '')
assert bannerText_no_spaces in response_no_spaces
def test_index_without_course_duration_limits(self):
"""
Test that the courseware does not contain the course expiration banner
when course_duration_limits are disabled.
"""
CourseDurationLimitConfig.objects.create(enabled=False)
assert self.client.login(username=self.user.username, password='test')
add_course_mode(self.course, upgrade_deadline_expired=False)
response = self.client.get(
reverse(
'courseware_section',
kwargs={
'course_id': str(self.course.id),
'chapter': self.chapter.url_name,
'section': self.sequential.url_name,
}
)
)
bannerText = get_expiration_banner_text(self.user, self.course)
self.assertNotContains(response, bannerText, html=True)
class TestRenderXBlock(RenderXBlockTestMixin, ModuleStoreTestCase, CompletionWaffleTestMixin):
"""
Tests for the courseware.render_xblock endpoint.
@@ -3354,7 +3081,7 @@ class TestRenderXBlockSelfPaced(TestRenderXBlock): # lint-amnesty, pylint: disa
return options
@_set_mfe_flag(activate_mfe=False)
@set_preview_mode(True)
class TestIndexViewCrawlerStudentStateWrites(SharedModuleStoreTestCase):
"""
Ensure that courseware index requests do not trigger student state writes.
@@ -3377,7 +3104,7 @@ class TestIndexViewCrawlerStudentStateWrites(SharedModuleStoreTestCase):
@classmethod
def setUpTestData(cls): # lint-amnesty, pylint: disable=super-method-not-called
"""Set up and enroll our fake user in the course."""
cls.user = UserFactory()
cls.user = UserFactory(is_staff=True)
CourseEnrollment.enroll(cls.user, cls.course.id)
def setUp(self):
@@ -3494,91 +3221,10 @@ class DatesTabTestCase(TestCase):
assert response.get('Location') == 'http://learning-mfe/course/course-v1:Org+Course+Run/dates?foo=b%24r'
class TestShowCoursewareMFE(TestCase):
class MFEUrlTests(TestCase):
"""
Make sure we're showing the Courseware MFE link when appropriate.
There are an unfortunate number of state permutations here since we have
the product of the following binary states:
* user is global staff member
* user is member of the course team
* whether the course_key is an old Mongo style of key
* the COURSEWARE_MICROFRONTEND_COURSE_TEAM_PREVIEW CourseWaffleFlag
* the COURSEWARE_USE_LEGACY_FRONTEND opt-out CourseWaffleFlag
Giving us theoretically 2^5 = 32 states. >_<
Test url utility method
"""
def test_permutations(self):
"""Test every permutation"""
old_course_key = CourseKey.from_string("OpenEdX/Old/2020")
new_course_key = CourseKey.from_string("course-v1:OpenEdX+New+2020")
# Old style course keys are never supported and should always return false...
old_mongo_combos = itertools.product(
[True, False], # is_global_staff
[True, False], # is_course_staff
[True, False], # preview_active (COURSEWARE_MICROFRONTEND_COURSE_TEAM_PREVIEW)
[True, False], # redirect_active (not COURSEWARE_USE_LEGACY_FRONTEND)
)
for is_global_staff, is_course_staff, preview_active, redirect_active in old_mongo_combos:
with _set_preview_mfe_flag(preview_active):
with _set_mfe_flag(redirect_active):
assert not courseware_mfe_is_advertised(
is_global_staff=is_global_staff,
is_course_staff=is_course_staff,
course_key=old_course_key,
)
# We've checked all old-style course keys now, so we can test only the
# new ones going forward. Now we check combinations of waffle flags and
# user permissions...
with _set_preview_mfe_flag(True):
with _set_mfe_flag(activate_mfe=True):
# (preview=on, redirect=on)
# Global and Course Staff can see the link.
assert courseware_mfe_is_advertised(new_course_key, True, True)
assert courseware_mfe_is_advertised(new_course_key, True, False)
assert courseware_mfe_is_advertised(new_course_key, False, True)
# (Regular users would see the link, but they can't see the Legacy
# experience, so it doesn't matter.)
with _set_mfe_flag(activate_mfe=False):
# (preview=on, redirect=off)
# Global and Course Staff can see the link.
assert courseware_mfe_is_advertised(new_course_key, True, True)
assert courseware_mfe_is_advertised(new_course_key, True, False)
assert courseware_mfe_is_advertised(new_course_key, False, True)
# Regular users don't see the link.
assert not courseware_mfe_is_advertised(new_course_key, False, False)
with _set_preview_mfe_flag(False):
with _set_mfe_flag(activate_mfe=True):
# (preview=off, redirect=on)
# Global staff see the link anyway
assert courseware_mfe_is_advertised(new_course_key, True, True)
assert courseware_mfe_is_advertised(new_course_key, True, False)
# If redirect is active for their students, course staff see the link even
# if preview=off.
assert courseware_mfe_is_advertised(new_course_key, False, True)
# (Regular users would see the link, but they can't see the Legacy
# experience, so it doesn't matter.)
with _set_mfe_flag(activate_mfe=False):
# (preview=off, redirect=off)
# Global staff and course teams can NOT see the link
# because both rollout waffle flags are false.
assert not courseware_mfe_is_advertised(new_course_key, True, True)
assert not courseware_mfe_is_advertised(new_course_key, True, False)
assert not courseware_mfe_is_advertised(new_course_key, False, True)
# Regular users don't see the link.
assert not courseware_mfe_is_advertised(new_course_key, False, False)
@override_settings(LEARNING_MICROFRONTEND_URL='https://learningmfe.openedx.org')
def test_url_generation(self):
course_key = CourseKey.from_string("course-v1:OpenEdX+MFE+2020")
@@ -3606,82 +3252,24 @@ class TestShowCoursewareMFE(TestCase):
)
@ddt.ddt
class MFERedirectTests(BaseViewsTestCase): # lint-amnesty, pylint: disable=missing-class-docstring
class PreviewTests(BaseViewsTestCase):
"""
Make sure we allow the Legacy view for course previews.
"""
def test_learner_redirect(self):
# learners will be redirected when the waffle flag is set
# learners will be redirected by default
lms_url, mfe_url, __ = self._get_urls()
assert self.client.get(lms_url).url == mfe_url
def test_staff_no_redirect(self):
lms_url, __, __ = self._get_urls()
# course staff will redirect in an MFE-enabled course - and not redirect otherwise.
course_staff = UserFactory.create(is_staff=False)
CourseStaffRole(self.course_key).add_users(course_staff)
self.client.login(username=course_staff.username, password='test')
with _set_mfe_flag(activate_mfe=False):
assert self.client.get(lms_url).status_code == 200
assert self.client.get(lms_url).status_code == 302
# global staff will never be redirected
self._create_global_staff_user()
with _set_mfe_flag(activate_mfe=False):
assert self.client.get(lms_url).status_code == 200
assert self.client.get(lms_url).status_code == 200
def test_exam_no_redirect(self):
# exams will not redirect to the mfe, for the time being
self.section2.is_time_limited = True
self.store.update_item(self.section2, self.user.id)
lms_url, __, __ = self._get_urls()
assert self.client.get(lms_url).status_code == 200
class PreviewRedirectTests(BaseViewsTestCase):
"""
Make sure we're redirecting to the Legacy view for course previews.
The user should always be redirected to the Legacy view as long as they are
part of the two following groups:
* user is global staff member
* user is member of the course team
"""
def test_staff_no_redirect(self):
def test_preview_no_redirect(self):
__, __, preview_url = self._get_urls()
with patch.object(access_utils, 'get_current_request_hostname',
return_value=settings.FEATURES.get('PREVIEW_LMS_BASE', None)):
# Previews will not redirect to the mfe,, for the time being.
with set_preview_mode(True):
# Previews will not redirect to the mfe
course_staff = UserFactory.create(is_staff=False)
CourseStaffRole(self.course_key).add_users(course_staff)
self.client.login(username=course_staff.username, password='test')
with _set_mfe_flag(activate_mfe=False):
assert self.client.get(preview_url).status_code == 200
assert self.client.get(preview_url).status_code == 200
# global staff will never be redirected
self._create_global_staff_user()
with _set_mfe_flag(activate_mfe=False):
assert self.client.get(preview_url).status_code == 200
assert self.client.get(preview_url).status_code == 200
def test_exam_no_redirect(self):
# exams will not redirect to the mfe, for the time being
self.section2.is_time_limited = True
self.store.update_item(self.section2, self.user.id)
__, __, preview_url = self._get_urls()
assert self.client.get(preview_url).status_code == 200
class ContentOptimizationTestCase(ModuleStoreTestCase):
"""

View File

@@ -11,8 +11,9 @@ from urllib.parse import urlencode
import ddt
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from lms.djangoapps.courseware.tests.helpers import set_preview_mode
from lms.djangoapps.courseware.utils import is_mode_upsellable
from openedx.features.course_experience.url_helpers import get_courseware_url, ExperienceOption
from openedx.features.course_experience.url_helpers import get_courseware_url
from common.djangoapps.student.tests.factories import AdminFactory, CourseEnrollmentFactory, UserFactory
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
@@ -170,6 +171,7 @@ class RenderXBlockTestMixin(MasqueradeMixin, metaclass=ABCMeta):
('html_block', 4),
)
@ddt.unpack
@set_preview_mode(True)
def test_courseware_html(self, block_name, mongo_calls):
"""
To verify that the removal of courseware chrome elements is working,
@@ -184,10 +186,7 @@ class RenderXBlockTestMixin(MasqueradeMixin, metaclass=ABCMeta):
self.setup_user(admin=True, enroll=True, login=True)
with check_mongo_calls(mongo_calls):
url = get_courseware_url(
self.block_to_be_tested.location,
experience=ExperienceOption.LEGACY,
)
url = get_courseware_url(self.block_to_be_tested.location)
response = self.client.get(url)
expected_elements = self.block_specific_chrome_html_elements + self.COURSEWARE_CHROME_HTML_ELEMENTS
for chrome_element in expected_elements:

View File

@@ -3,7 +3,6 @@ Toggles for courseware in-course experience.
"""
from edx_toggles.toggles import LegacyWaffleFlagNamespace, SettingToggle
from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
@@ -11,35 +10,6 @@ from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
WAFFLE_FLAG_NAMESPACE = LegacyWaffleFlagNamespace(name='courseware')
# .. toggle_name: courseware.use_legacy_frontend
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: Waffle flag to direct learners to the legacy courseware experience - the default behavior
# directs to the new MFE-based courseware in frontend-app-learning. Supports the ability to globally flip back to
# the legacy courseware experience.
# .. toggle_use_cases: temporary, open_edx
# .. toggle_creation_date: 2021-06-03
# .. toggle_target_removal_date: 2021-10-09
# .. toggle_tickets: DEPR-109
COURSEWARE_USE_LEGACY_FRONTEND = CourseWaffleFlag(
WAFFLE_FLAG_NAMESPACE, 'use_legacy_frontend', __name__
)
# .. toggle_name: courseware.microfrontend_course_team_preview
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: Waffle flag to display a link for the new learner experience to course teams without
# redirecting students. Supports staged rollout to course teams of a new micro-frontend-based implementation of the
# courseware page.
# .. toggle_use_cases: temporary, open_edx
# .. toggle_creation_date: 2020-03-09
# .. toggle_target_removal_date: 2020-12-31
# .. toggle_warnings: Also set settings.LEARNING_MICROFRONTEND_URL.
# .. toggle_tickets: DEPR-109
COURSEWARE_MICROFRONTEND_COURSE_TEAM_PREVIEW = CourseWaffleFlag(
WAFFLE_FLAG_NAMESPACE, 'microfrontend_course_team_preview', __name__
)
# Waffle flag to enable the course exit page in the learning MFE.
#
# .. toggle_name: courseware.microfrontend_course_exit_page
@@ -128,118 +98,25 @@ COURSEWARE_OPTIMIZED_RENDER_XBLOCK = CourseWaffleFlag(
COURSES_INVITE_ONLY = SettingToggle('COURSES_INVITE_ONLY', default=False)
def courseware_mfe_is_active(course_key: CourseKey) -> bool:
def courseware_mfe_is_active() -> bool:
"""
Should we serve the Learning MFE as the canonical courseware experience?
"""
#Avoid circular imports.
from lms.djangoapps.courseware.access_utils import in_preview_mode
# NO: Old Mongo courses are always served in the Legacy frontend,
# regardless of configuration.
if course_key.deprecated:
return False
# NO: MFE courseware can be disabled for users/courses/globally via this
# Waffle flag.
if COURSEWARE_USE_LEGACY_FRONTEND.is_enabled(course_key):
return False
# NO: Course preview doesn't work in the MFE
if in_preview_mode():
return False
# OTHERWISE: MFE courseware experience is active by default.
return True
def courseware_mfe_is_visible(
course_key: CourseKey,
is_global_staff=False,
is_course_staff=False,
) -> bool:
"""
Can we see a course run's content in the Learning MFE?
"""
#Avoid circular imports.
from lms.djangoapps.courseware.access_utils import in_preview_mode
# DENY: Old Mongo courses don't work in the MFE.
if course_key.deprecated:
return False
# DENY: Course preview doesn't work in the MFE
if in_preview_mode():
return False
# ALLOW: Where techincally possible, global staff may always see the MFE.
if is_global_staff:
return True
# ALLOW: If course team preview is enabled, then course staff may see their
# course in the MFE.
if is_course_staff and COURSEWARE_MICROFRONTEND_COURSE_TEAM_PREVIEW.is_enabled(course_key):
return True
# OTHERWISE: The MFE is only visible if it's the active (ie canonical) experience.
return courseware_mfe_is_active(course_key)
def courseware_mfe_is_advertised(
course_key: CourseKey,
is_global_staff=False,
is_course_staff=False,
) -> bool:
"""
Should we invite the user to view a course run's content in the Learning MFE?
This check is slightly different than `courseware_mfe_is_visible`, in that
we always *permit* global staff to view MFE content (assuming it's deployed),
but we do not shove the New Experience in their face if the preview isn't
enabled.
"""
#Avoid circular imports.
from lms.djangoapps.courseware.access_utils import in_preview_mode
# DENY: Old Mongo courses don't work in the MFE.
if course_key.deprecated:
return False
# DENY: Course preview doesn't work in the MFE
if in_preview_mode():
return False
# ALLOW: Both global and course staff can see the MFE link if the course team
# preview is enabled.
is_staff = is_global_staff or is_course_staff
if is_staff and COURSEWARE_MICROFRONTEND_COURSE_TEAM_PREVIEW.is_enabled(course_key):
return True
# OTHERWISE: The MFE is only advertised if it's the active (ie canonical) experience.
return courseware_mfe_is_active(course_key)
def courseware_legacy_is_visible(
course_key: CourseKey,
is_global_staff=False,
) -> bool:
"""
Can we see a course run's content in the Legacy frontend?
Note: This function will always return True for Old Mongo courses,
since `courseware_mfe_is_active` will always return False for them.
"""
#Avoid circular imports.
from lms.djangoapps.courseware.access_utils import in_preview_mode
# ALLOW: Global staff may always see the Legacy experience.
if is_global_staff:
return True
# ALLOW: All course previews will be shown in Legacy experience
if in_preview_mode():
return True
# OTHERWISE: Legacy is only visible if it's the active (ie canonical) experience.
# Note that Old Mongo courses are never the active experience,
# so we effectively always ALLOW them to be viewed in Legacy.
return not courseware_mfe_is_active(course_key)
from lms.djangoapps.courseware.access_utils import in_preview_mode # avoid a circular import
# We only use legacy views for the Studio "preview mode" feature these days, while everyone else gets the MFE
return not in_preview_mode()
def course_exit_page_is_active(course_key):
return (
courseware_mfe_is_active(course_key) and
courseware_mfe_is_active() and
COURSEWARE_MICROFRONTEND_COURSE_EXIT_PAGE.is_enabled(course_key)
)
def courseware_mfe_progress_milestones_are_active(course_key):
return (
courseware_mfe_is_active(course_key) and
courseware_mfe_is_active() and
COURSEWARE_MICROFRONTEND_PROGRESS_MILESTONES.is_enabled(course_key)
)

View File

@@ -64,10 +64,7 @@ from ..masquerade import check_content_start_date_for_masquerade_user, setup_mas
from ..model_data import FieldDataCache
from ..module_render import get_module_for_descriptor, toc_for_course
from ..permissions import MASQUERADE_AS_STUDENT
from ..toggles import (
courseware_legacy_is_visible,
courseware_mfe_is_advertised
)
from ..toggles import courseware_mfe_is_active
from .views import CourseTabView
log = logging.getLogger("edx.courseware.views.index")
@@ -172,23 +169,11 @@ class CoursewareIndex(View):
def _redirect_to_learning_mfe(self):
"""
Can the user access this sequence in Legacy courseware? If not, redirect to MFE.
We specifically allow users to stay in the Legacy frontend for special
(ie timed/proctored) exams since they're not yet supported by the MFE.
Can the user access this sequence in the courseware MFE? If so, redirect to MFE.
"""
# STAY: if the course run as a whole is visible in the Legacy experience.
if courseware_legacy_is_visible(
course_key=self.course_key,
is_global_staff=self.request.user.is_staff,
):
return
# STAY: if we are in a special (ie proctored/timed) exam, which isn't yet
# supported on the MFE.
if getattr(self.section, 'is_time_limited', False):
return
# REDIRECT otherwise.
raise Redirect(self.microfrontend_url)
# If the MFE is active, prefer that
if courseware_mfe_is_active():
raise Redirect(self.microfrontend_url)
@property
def microfrontend_url(self):
@@ -497,16 +482,6 @@ class CoursewareIndex(View):
if self.section.position and self.section.has_children:
self._add_sequence_title_to_context(courseware_context)
# Courseware MFE link
if courseware_mfe_is_advertised(
is_global_staff=request.user.is_staff,
is_course_staff=staff_access,
course_key=self.course.id,
):
courseware_context['microfrontend_link'] = self.microfrontend_url
else:
courseware_context['microfrontend_link'] = None
return courseware_context
def _add_sequence_title_to_context(self, courseware_context):

View File

@@ -119,7 +119,6 @@ from openedx.features.course_duration_limits.access import generate_course_expir
from openedx.features.course_experience import DISABLE_UNIFIED_COURSE_TAB_FLAG, course_home_url
from openedx.features.course_experience.course_tools import CourseToolsPluginManager
from openedx.features.course_experience.url_helpers import (
ExperienceOption,
get_courseware_url,
get_learning_mfe_home_url,
is_request_from_learning_mfe
@@ -410,19 +409,10 @@ def jump_to(request, course_id, location):
except InvalidKeyError as exc:
raise Http404("Invalid course_key or usage_key") from exc
experience_param = request.GET.get("experience", "").lower()
if experience_param == "new":
experience = ExperienceOption.NEW
elif experience_param == "legacy":
experience = ExperienceOption.LEGACY
else:
experience = ExperienceOption.ACTIVE
try:
redirect_url = get_courseware_url(
usage_key=usage_key,
request=request,
experience=experience,
)
except (ItemNotFoundError, NoPathToItem):
# We used to 404 here, but that's ultimately a bad experience. There are real world use cases where a user
@@ -432,7 +422,6 @@ def jump_to(request, course_id, location):
redirect_url = get_courseware_url(
usage_key=course_location_from_key(course_key),
request=request,
experience=experience,
)
return redirect(redirect_url)

View File

@@ -91,11 +91,6 @@ show_preview_menu = course and can_masquerade and supports_preview_menu
</div>
% endif
<div style="flex-shrink: 1; text-align: center;">
% if microfrontend_link:
<a class="btn btn-primary" style="border: solid 1px white;" href="${microfrontend_link}">
${_("View in the new experience")}
</a>
% endif
% if studio_url:
<a
class="btn btn-primary view-in-studio"

View File

@@ -102,7 +102,6 @@ class CourseInfoSerializer(serializers.Serializer): # pylint: disable=abstract-
pacing = serializers.CharField()
user_timezone = serializers.CharField()
show_calculator = serializers.BooleanField()
can_view_legacy_courseware = serializers.BooleanField()
can_access_proctored_exams = serializers.BooleanField()
notes = serializers.DictField()
marketing_url = serializers.CharField()

View File

@@ -70,7 +70,6 @@
},
"show_calculator": false,
"original_user_is_staff": true,
"can_view_legacy_courseware": true,
"is_staff": true,
"course_access": {
"has_access": true,
@@ -200,9 +199,6 @@
"$.body.original_user_is_staff": {
"match": "type"
},
"$.body.can_view_legacy_courseware": {
"match": "type"
},
"$.body.is_staff": {
"match": "type"
},

View File

@@ -41,10 +41,7 @@ from lms.djangoapps.courseware.masquerade import (
)
from lms.djangoapps.courseware.models import LastSeenCoursewareTimezone
from lms.djangoapps.courseware.module_render import get_module_by_usage_id
from lms.djangoapps.courseware.toggles import (
courseware_legacy_is_visible,
course_exit_page_is_active,
)
from lms.djangoapps.courseware.toggles import course_exit_page_is_active
from lms.djangoapps.courseware.views.views import get_cert_data
from lms.djangoapps.gating.api import get_entrance_exam_score, get_entrance_exam_usage_key
from lms.djangoapps.grades.api import CourseGradeFactory
@@ -94,10 +91,6 @@ class CoursewareMeta:
self.request.user = self.effective_user
self.enrollment_object = CourseEnrollment.get_enrollment(self.effective_user, self.course_key,
select_related=['celebration', 'user__celebration'])
self.can_view_legacy_courseware = courseware_legacy_is_visible(
course_key=course_key,
is_global_staff=self.original_user_is_global_staff,
)
def __getattr__(self, name):
return getattr(self.overview, name)
@@ -448,7 +441,6 @@ class CoursewareInformation(RetrieveAPIView):
* pacing: Course pacing. Possible values: instructor, self
* user_timezone: User's chosen timezone setting (or null for browser default)
* can_load_course: Whether the user can view the course (AccessResponse object)
* can_view_legacy_courseware: Indicates whether the user is able to see the legacy courseware view
* user_has_passing_grade: Whether or not the effective user's grade is equal to or above the courses minimum
passing grade
* course_exit_page_is_active: Flag for the learning mfe on whether or not the course exit page should display

View File

@@ -12,7 +12,6 @@ from django.test.utils import override_settings
from django.urls import reverse
from django.utils import timezone
from django.contrib.auth import get_user_model
from edx_toggles.toggles.testutils import override_waffle_flag
from pyquery import PyQuery as pq
from xmodule.modulestore.tests.django_utils import (
TEST_DATA_MONGO_AMNESTY_MODULESTORE, ModuleStoreTestCase, SharedModuleStoreTestCase,
@@ -30,7 +29,6 @@ from common.djangoapps.student.tests.factories import OrgStaffFactory
from common.djangoapps.student.tests.factories import StaffFactory
from lms.djangoapps.courseware.module_render import load_single_xblock
from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin
from lms.djangoapps.courseware.toggles import COURSEWARE_USE_LEGACY_FRONTEND
from lms.djangoapps.discussion.django_comment_client.tests.factories import RoleFactory
from openedx.core.djangoapps.django_comment_common.models import (
FORUM_ROLE_ADMINISTRATOR,
@@ -59,7 +57,7 @@ User = get_user_model()
@patch("crum.get_current_request")
def _get_content_from_fragment(block, user_id, course, request_factory, mock_get_current_request):
def _get_content_from_fragment(_store, block, user_id, course, request_factory, mock_get_current_request):
"""
Returns the content from the rendered fragment of a block
Arguments:
@@ -84,29 +82,30 @@ def _get_content_from_fragment(block, user_id, course, request_factory, mock_get
return frag.content
def _get_content_from_lms_index(block, user_id, course, request_factory):
def _get_content_from_lms_index(store, block, user_id, _course, _request_factory):
"""
Returns the content from the lms index view of the block
Arguments:
block: some sort of xblock descriptor, must implement .scope_ids.usage_id
user_id (int): id of user
course_id (CourseLocator): id of course
"""
client = Client()
client.login(username=User.objects.get(id=user_id).username, password=TEST_PASSWORD)
# Re-load the block from the store to ensure that its `parent` field is filled out correctly
block = store.get_item(block.location)
page_content = client.get(
reverse('courseware', kwargs={'course_id': block.scope_ids.usage_id.course_key}),
reverse('render_xblock', args=[str(block.parent)]) + '?recheck_access=1',
follow=True,
)
page = pq(page_content.content)
seq_contents = page('#seq_contents_0').html()
seq = pq(seq_contents)
block_contents = seq(f'[data-id="{block.scope_ids.usage_id}"]')
page = pq(page_content.content)
block_contents = page(f'[data-id="{block.scope_ids.usage_id}"]')
return block_contents.html()
def _assert_block_is_gated(block, is_gated, user, course, request_factory, has_upgrade_link=True):
def _assert_block_is_gated(store, block, is_gated, user, course, request_factory, has_upgrade_link=True):
"""
Asserts that a block in a specific course is gated for a specific user
Arguments:
@@ -118,7 +117,7 @@ def _assert_block_is_gated(block, is_gated, user, course, request_factory, has_u
checkout_link = '#' if has_upgrade_link else None
for content_getter in (_get_content_from_fragment, _get_content_from_lms_index):
with patch.object(ContentTypeGatingPartition, '_get_checkout_link', return_value=checkout_link):
content = content_getter(block, user.id, course, request_factory) # pylint: disable=no-value-for-parameter
content = content_getter(store, block, user.id, course, request_factory) # pylint: disable=no-value-for-parameter
if is_gated:
assert 'content-paywall' in content
if has_upgrade_link:
@@ -148,7 +147,7 @@ def _assert_block_is_gated(block, is_gated, user, course, request_factory, has_u
assert 'student_view_data' in course_api_block
def _assert_block_is_empty(block, user_id, course, request_factory):
def _assert_block_is_empty(store, block, user_id, course, request_factory):
"""
Asserts that a block in a specific course is empty for a specific user
Arguments:
@@ -157,7 +156,7 @@ def _assert_block_is_empty(block, user_id, course, request_factory):
user_id (int): id of user
course_id (CourseLocator): id of course
"""
content = _get_content_from_lms_index(block, user_id, course, request_factory)
content = _get_content_from_lms_index(store, block, user_id, course, request_factory)
assert content is None
@@ -165,7 +164,6 @@ def _assert_block_is_empty(block, user_id, course, request_factory):
@override_settings(FIELD_OVERRIDE_PROVIDERS=(
'openedx.features.content_type_gating.field_override.ContentTypeGatingFieldOverride',
))
@override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True)
class TestProblemTypeAccess(SharedModuleStoreTestCase, MasqueradeMixin): # pylint: disable=missing-class-docstring
PROBLEM_TYPES = ['problem', 'openassessment', 'drag-and-drop-v2', 'done', 'edx_sga']
@@ -465,6 +463,7 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase, MasqueradeMixin): # pyli
@ddt.unpack
def test_access_to_problems(self, prob_type, is_gated):
_assert_block_is_gated(
self.store,
block=self.blocks_dict[prob_type],
user=self.users['audit'],
course=self.course,
@@ -472,6 +471,7 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase, MasqueradeMixin): # pyli
request_factory=self.factory,
)
_assert_block_is_gated(
self.store,
block=self.blocks_dict[prob_type],
user=self.users['verified'],
course=self.course,
@@ -487,6 +487,7 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase, MasqueradeMixin): # pyli
# Verify that graded, has_score and weight must all be true for a component to be gated
block = self.graded_score_weight_blocks[(graded, has_score, weight)]
_assert_block_is_gated(
self.store,
block=block,
user=self.audit_user,
course=self.course,
@@ -522,6 +523,7 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase, MasqueradeMixin): # pyli
All users should have access to non-problem component types, the 'html' components test that.
"""
_assert_block_is_gated(
self.store,
block=self.courses[course]['blocks'][component_type],
user=self.users[user_track],
course=self.courses[course]['course'],
@@ -535,6 +537,7 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase, MasqueradeMixin): # pyli
the user will continue to see gated content, but the upgrade messaging will be removed.
"""
_assert_block_is_gated(
self.store,
block=self.courses['expired_upgrade_deadline']['blocks']['problem'],
user=self.users['audit'],
course=self.courses['expired_upgrade_deadline']['course'],
@@ -599,6 +602,7 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase, MasqueradeMixin): # pyli
# assert that course team members have access to graded content
_assert_block_is_gated(
self.store,
block=self.blocks_dict['problem'],
user=user,
course=self.course,
@@ -626,6 +630,7 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase, MasqueradeMixin): # pyli
mode='audit',
)
_assert_block_is_gated(
self.store,
block=self.blocks_dict['problem'],
user=user,
course=self.course,
@@ -650,6 +655,7 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase, MasqueradeMixin): # pyli
graded, has_score, weight = True, True, 1
block = self.graded_score_weight_blocks[(graded, has_score, weight)]
_assert_block_is_gated(
self.store,
block=block,
user=user,
course=self.course,
@@ -758,6 +764,7 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase, MasqueradeMixin): # pyli
self.update_masquerade(username=user.username)
_assert_block_is_gated(
self.store,
block=self.blocks_dict['problem'],
user=user,
course=self.course,
@@ -772,12 +779,8 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase, MasqueradeMixin): # pyli
def test_discount_display(self):
with patch.object(ContentTypeGatingPartition, '_get_checkout_link', return_value='#'):
block_content = _get_content_from_lms_index(
block=self.blocks_dict['problem'],
user_id=self.audit_user.id,
course=self.course,
request_factory=self.factory,
)
block_content = _get_content_from_lms_index(self.store, self.blocks_dict['problem'], self.audit_user.id,
self.course, self.factory)
assert '<span>DISCOUNT_PRICE</span>' in block_content
@@ -785,7 +788,6 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase, MasqueradeMixin): # pyli
@override_settings(FIELD_OVERRIDE_PROVIDERS=(
'openedx.features.content_type_gating.field_override.ContentTypeGatingFieldOverride',
))
@override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True)
class TestConditionalContentAccess(TestConditionalContent):
"""
Conditional Content allows course authors to run a/b tests on course content. We want to make sure that
@@ -852,6 +854,7 @@ class TestConditionalContentAccess(TestConditionalContent):
# Make sure that all audit enrollments are gated regardless of if they see vertical a or vertical b
_assert_block_is_gated(
self.store,
block=self.block_a,
user=self.student_audit_a,
course=self.course,
@@ -859,6 +862,7 @@ class TestConditionalContentAccess(TestConditionalContent):
request_factory=self.factory,
)
_assert_block_is_gated(
self.store,
block=self.block_b,
user=self.student_audit_b,
course=self.course,
@@ -868,6 +872,7 @@ class TestConditionalContentAccess(TestConditionalContent):
# Make sure that all verified enrollments are not gated regardless of if they see vertical a or vertical b
_assert_block_is_gated(
self.store,
block=self.block_a,
user=self.student_verified_a,
course=self.course,
@@ -875,6 +880,7 @@ class TestConditionalContentAccess(TestConditionalContent):
request_factory=self.factory,
)
_assert_block_is_gated(
self.store,
block=self.block_b,
user=self.student_verified_b,
course=self.course,
@@ -886,7 +892,6 @@ class TestConditionalContentAccess(TestConditionalContent):
@override_settings(FIELD_OVERRIDE_PROVIDERS=(
'openedx.features.content_type_gating.field_override.ContentTypeGatingFieldOverride',
))
@override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True)
class TestMessageDeduplication(ModuleStoreTestCase):
"""
Tests to verify that access denied messages isn't shown if multiple items in a row are denied.
@@ -943,12 +948,13 @@ class TestMessageDeduplication(ModuleStoreTestCase):
mode='audit'
)
blocks_dict['graded_1'] = ItemFactory.create(
parent=blocks_dict['vertical'],
parent_location=blocks_dict['vertical'].location,
category='problem',
graded=True,
metadata=METADATA,
)
_assert_block_is_gated(
self.store,
block=blocks_dict['graded_1'],
user=self.user,
course=course['course'],
@@ -978,6 +984,7 @@ class TestMessageDeduplication(ModuleStoreTestCase):
mode='audit'
)
_assert_block_is_gated(
self.store,
block=blocks_dict['graded_1'],
user=self.user,
course=course['course'],
@@ -985,6 +992,7 @@ class TestMessageDeduplication(ModuleStoreTestCase):
request_factory=self.request_factory,
)
_assert_block_is_empty(
self.store,
block=blocks_dict['graded_2'],
user_id=self.user.id,
course=course['course'],
@@ -1025,6 +1033,7 @@ class TestMessageDeduplication(ModuleStoreTestCase):
mode='audit'
)
_assert_block_is_gated(
self.store,
block=blocks_dict['graded_1'],
user=self.user,
course=course['course'],
@@ -1032,18 +1041,21 @@ class TestMessageDeduplication(ModuleStoreTestCase):
request_factory=self.request_factory,
)
_assert_block_is_empty(
self.store,
block=blocks_dict['graded_2'],
user_id=self.user.id,
course=course['course'],
request_factory=self.request_factory,
)
_assert_block_is_empty(
self.store,
block=blocks_dict['graded_3'],
user_id=self.user.id,
course=course['course'],
request_factory=self.request_factory,
)
_assert_block_is_empty(
self.store,
block=blocks_dict['graded_4'],
user_id=self.user.id,
course=course['course'],
@@ -1077,6 +1089,7 @@ class TestMessageDeduplication(ModuleStoreTestCase):
mode='audit'
)
_assert_block_is_gated(
self.store,
block=blocks_dict['graded_1'],
user=self.user,
course=course['course'],
@@ -1084,6 +1097,7 @@ class TestMessageDeduplication(ModuleStoreTestCase):
request_factory=self.request_factory,
)
_assert_block_is_gated(
self.store,
block=blocks_dict['ungraded_2'],
user=self.user,
course=course['course'],
@@ -1091,6 +1105,7 @@ class TestMessageDeduplication(ModuleStoreTestCase):
request_factory=self.request_factory,
)
_assert_block_is_gated(
self.store,
block=blocks_dict['graded_3'],
user=self.user,
course=course['course'],

View File

@@ -9,7 +9,6 @@ import ddt
from django.conf import settings
from django.urls import reverse
from django.utils.timezone import now
from edx_toggles.toggles.testutils import override_waffle_flag
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID
@@ -25,7 +24,6 @@ from common.djangoapps.student.tests.factories import OrgInstructorFactory
from common.djangoapps.student.tests.factories import OrgStaffFactory
from common.djangoapps.student.tests.factories import StaffFactory
from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin
from lms.djangoapps.courseware.toggles import COURSEWARE_USE_LEGACY_FRONTEND
from lms.djangoapps.discussion.django_comment_client.tests.factories import RoleFactory
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.course_date_signals.utils import MAX_DURATION, MIN_DURATION
@@ -44,7 +42,6 @@ from openedx.features.course_experience.tests.views.helpers import add_course_mo
# pylint: disable=no-member
@ddt.ddt
@override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True)
class CourseExpirationTestCase(ModuleStoreTestCase, MasqueradeMixin):
"""Tests to verify the get_user_course_expiration_date function is working correctly"""
def setUp(self):
@@ -80,14 +77,7 @@ class CourseExpirationTestCase(ModuleStoreTestCase, MasqueradeMixin):
def get_courseware(self):
"""Returns a response from a GET on a courseware section"""
courseware_url = reverse(
'courseware_section',
kwargs={
'course_id': str(self.course.id),
'chapter': self.chapter.location.block_id,
'section': self.sequential.location.block_id,
},
)
courseware_url = reverse('render_xblock', args=[str(self.sequential.location)])
return self.client.get(courseware_url, follow=True)
def test_enrollment_mode(self):

View File

@@ -284,4 +284,4 @@ class GetCoursewareUrlTests(SharedModuleStoreTestCase):
path = url.split('?')[0]
assert path == expected_path
course_run = self.items[store_type]['course_run']
mock_mfe_is_active.assert_called_once_with(course_run.id)
mock_mfe_is_active.assert_called_once()

View File

@@ -11,7 +11,7 @@ from edx_toggles.toggles.testutils import override_waffle_flag
from common.djangoapps.course_modes.models import CourseMode
from lms.djangoapps.commerce.models import CommerceConfiguration
from lms.djangoapps.courseware.toggles import COURSEWARE_USE_LEGACY_FRONTEND
from lms.djangoapps.courseware.tests.helpers import set_preview_mode
from openedx.core.djangolib.markup import HTML
from openedx.features.course_experience import DISPLAY_COURSE_SOCK_FLAG
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
@@ -25,7 +25,7 @@ TEST_VERIFICATION_SOCK_LOCATOR = '<div class="verification-sock"'
@ddt.ddt
@override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True)
@set_preview_mode(True)
class TestCourseSockView(SharedModuleStoreTestCase):
"""
Tests for the course verification sock fragment view.
@@ -47,7 +47,7 @@ class TestCourseSockView(SharedModuleStoreTestCase):
def setUp(self):
super().setUp()
self.user = UserFactory.create()
self.user = UserFactory.create(is_staff=True)
# Enroll the user in the four courses
CourseEnrollmentFactory.create(user=self.user, course_id=self.standard_course.id)

View File

@@ -5,8 +5,7 @@ Tests for masquerading functionality on course_experience
from django.urls import reverse
from edx_toggles.toggles.testutils import override_waffle_flag
from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin
from lms.djangoapps.courseware.toggles import COURSEWARE_USE_LEGACY_FRONTEND
from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin, set_preview_mode
from openedx.features.course_experience import DISPLAY_COURSE_SOCK_FLAG
from common.djangoapps.student.roles import CourseStaffRole
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
@@ -62,12 +61,12 @@ class MasqueradeTestBase(SharedModuleStoreTestCase, MasqueradeMixin):
return None
@set_preview_mode(True)
class TestVerifiedUpgradesWithMasquerade(MasqueradeTestBase):
"""
Tests for the course verification upgrade messages while the user is being masqueraded.
"""
@override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True)
@override_waffle_flag(DISPLAY_COURSE_SOCK_FLAG, active=True)
def test_masquerade_as_student(self):
# Elevate the staff user to be student
@@ -75,7 +74,6 @@ class TestVerifiedUpgradesWithMasquerade(MasqueradeTestBase):
response = self.client.get(reverse('courseware', kwargs={'course_id': str(self.verified_course.id)}))
self.assertContains(response, TEST_VERIFICATION_SOCK_LOCATOR, html=False)
@override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True)
@override_waffle_flag(DISPLAY_COURSE_SOCK_FLAG, active=True)
def test_masquerade_as_verified_student(self):
user_group_id = self.get_group_id_by_course_mode_name(
@@ -87,7 +85,6 @@ class TestVerifiedUpgradesWithMasquerade(MasqueradeTestBase):
response = self.client.get(reverse('courseware', kwargs={'course_id': str(self.verified_course.id)}))
self.assertNotContains(response, TEST_VERIFICATION_SOCK_LOCATOR, html=False)
@override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True)
@override_waffle_flag(DISPLAY_COURSE_SOCK_FLAG, active=True)
def test_masquerade_as_masters_student(self):
user_group_id = self.get_group_id_by_course_mode_name(

View File

@@ -4,7 +4,6 @@ Helper functions for logic related to learning (courseare & course home) URLs.
Centralized in openedx/features/course_experience instead of lms/djangoapps/courseware
because the Studio course outline may need these utilities.
"""
from enum import Enum
from typing import Optional
from django.conf import settings
@@ -22,24 +21,9 @@ from xmodule.modulestore.search import navigation_index, path_to_location # lin
User = get_user_model()
class ExperienceOption(Enum):
"""
Versions of the courseware experience that can be requested.
`ACTIVE` indicates that the default experience (in the context of the
course run) should be used.
To be removed as part of DEPR-109.
"""
ACTIVE = 'courseware-experience-active'
NEW = 'courseware-experience-new'
LEGACY = 'courseware-experience-legacy'
def get_courseware_url(
usage_key: UsageKey,
request: Optional[HttpRequest] = None,
experience: ExperienceOption = ExperienceOption.ACTIVE,
) -> str:
"""
Return the URL to the canonical learning experience for a given block.
@@ -56,12 +40,7 @@ def get_courseware_url(
* ItemNotFoundError if no data at the `usage_key`.
* NoPathToItem if we cannot build a path to the `usage_key`.
"""
course_key = usage_key.course_key.replace(version_guid=None, branch=None)
if experience == ExperienceOption.NEW:
get_url_fn = _get_new_courseware_url
elif experience == ExperienceOption.LEGACY:
get_url_fn = _get_legacy_courseware_url
elif courseware_mfe_is_active(course_key):
if courseware_mfe_is_active():
get_url_fn = _get_new_courseware_url
else:
get_url_fn = _get_legacy_courseware_url

View File

@@ -14,17 +14,14 @@ import simplejson as json
from ddt import data, ddt
from django.conf import settings
from django.urls import reverse
from edx_toggles.toggles.testutils import override_waffle_flag
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from common.djangoapps.student.tests.factories import GlobalStaffFactory
from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase
from lms.djangoapps.courseware.toggles import COURSEWARE_USE_LEGACY_FRONTEND
from openedx.core.lib.url_utils import quote_slashes
@override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True)
class TestRecommender(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
"""
Check that Recommender state is saved properly
@@ -68,14 +65,7 @@ class TestRecommender(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
display_name='recommender_second'
)
cls.course_url = reverse(
'courseware_section',
kwargs={
'course_id': str(cls.course.id),
'chapter': 'Overview',
'section': 'Welcome',
}
)
cls.course_url = reverse('render_xblock', args=[str(cls.section.location)])
cls.resource_urls = [
(

View File

@@ -48,14 +48,12 @@ import pytz
from bs4 import BeautifulSoup
from django.conf import settings
from django.urls import reverse
from edx_toggles.toggles.testutils import override_waffle_flag
from xblock.plugin import Plugin
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
import lms.djangoapps.lms_xblock.runtime
from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase
from lms.djangoapps.courseware.toggles import COURSEWARE_USE_LEGACY_FRONTEND
class XBlockEventTestMixin:
@@ -273,14 +271,7 @@ class XBlockScenarioTestCaseMixin:
)
cls.xblocks[xblock_config['urlname']] = xblock
scenario_url = str(reverse(
'courseware_section',
kwargs={
'course_id': str(cls.course.id),
'chapter': "ch_" + chapter_config['urlname'],
'section': "sec_" + chapter_config['urlname']
}
))
scenario_url = reverse('render_xblock', args=[str(section.location)])
cls.scenario_urls[chapter_config['urlname']] = scenario_url
@@ -337,7 +328,6 @@ class XBlockStudentTestCaseMixin:
self.login(email, password)
@override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True)
class XBlockTestCase(XBlockStudentTestCaseMixin,
XBlockScenarioTestCaseMixin,
XBlockEventTestMixin,