-
-
-
- -- -
Testing
-
-
${_('Course details')}
diff --git a/lms/templates/dates_banner.html b/lms/templates/dates_banner.html
deleted file mode 100644
index 594fb7991c..0000000000
--- a/lms/templates/dates_banner.html
+++ /dev/null
@@ -1,108 +0,0 @@
-## mako
-
-<%page expression_filter="h"/>
-<%!
-from django.utils.translation import ugettext as _
-
-from lms.djangoapps.courseware.date_summary import CourseAssignmentDate
-from common.djangoapps.course_modes.models import CourseMode
-%>
-
-<%
-additional_styling_class = 'on-mobile' if is_mobile_app else 'has-button'
-%>
-
-<%def name="reset_dates_banner()">
-
-
-
diff --git a/openedx/features/course_experience/static/course_experience/fixtures/enrollment-button.html b/openedx/features/course_experience/static/course_experience/fixtures/enrollment-button.html
deleted file mode 100644
index 1e55c18cb7..0000000000
--- a/openedx/features/course_experience/static/course_experience/fixtures/enrollment-button.html
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/openedx/features/course_experience/static/course_experience/fixtures/latest-update-fragment.html b/openedx/features/course_experience/static/course_experience/fixtures/latest-update-fragment.html
deleted file mode 100644
index 41fd8b44c3..0000000000
--- a/openedx/features/course_experience/static/course_experience/fixtures/latest-update-fragment.html
+++ /dev/null
@@ -1,10 +0,0 @@
-
- <%include file="/dates_banner.html" />
- % if course_sections is not None:
-
-
-
-<%static:require_module_async module_name="js/dateutil_factory" class_name="DateUtilFactory">
- DateUtilFactory.transform('.localized-datetime');
-%static:require_module_async>
-
-<%static:webpack entry="CourseOutline">
- new CourseOutline();
-%static:webpack>
diff --git a/openedx/features/course_experience/templates/course_experience/latest-update-fragment.html b/openedx/features/course_experience/templates/course_experience/latest-update-fragment.html
deleted file mode 100644
index 4f9b6082b9..0000000000
--- a/openedx/features/course_experience/templates/course_experience/latest-update-fragment.html
+++ /dev/null
@@ -1,27 +0,0 @@
-## mako
-
-<%page expression_filter="h"/>
-<%namespace name='static' file='../static_content.html'/>
-
-<%!
-from django.utils.translation import ugettext as _
-from openedx.core.djangolib.markup import HTML
-%>
-
-<%block name="content">
-
-
-%def>
-<%def name="upgrade_to_reset_banner()">
-
- % if is_mobile_app:
- ${_('It looks like you missed some important deadlines based on our suggested schedule. ')}
- ${_('To keep yourself on track, you can update this schedule and shift the past due assignments into the future by visiting ')}
- edx.org.
- ${_(" Don't worry—you won't lose any of the progress you've made when you shift your due dates.")}
- % else:
- ${_('It looks like you missed some important deadlines based on our suggested schedule.')}
- ${_("To keep yourself on track, you can update this schedule and shift the past due assignments into the future. Don't worry—you won't lose any of the progress you've made when you shift your due dates.")}
- % endif
-
- % if not is_mobile_app:
-
-
-
- % endif
-
-
-%def>
-<%def name="upgrade_to_complete_graded_banner()">
-
- % if is_mobile_app:
- ${_('You are auditing this course,')}
- ${_(' which means that you are unable to participate in graded assignments.')}
- ${_(' It looks like you missed some important deadlines based on our suggested schedule. Graded assignments and schedule adjustment are available to Verified Track learners.')}
- % else:
- ${_('You are auditing this course,')}
- ${_(' which means that you are unable to participate in graded assignments.')}
- ${_(' It looks like you missed some important deadlines based on our suggested schedule. To complete graded assignments as part of this course and shift the past due assignments into the future, you can upgrade today.')}
- % endif
-
- % if not is_mobile_app:
-
-
-
-
-
- % endif
-
-
-%def>
-
-% if not has_ended:
- % if on_dates_tab and not missed_deadlines:
- %if getattr(course, 'self_paced', False):
-
- % if is_mobile_app:
- ${_('You are auditing this course,')}
- ${_(' which means that you are unable to participate in graded assignments.')}
- ${_('Graded assignments are available to Verified Track learners.')}
- % else:
- ${_('You are auditing this course,')}
- ${_(' which means that you are unable to participate in graded assignments.')}
- ${_(' To complete graded assignments as part of this course, you can upgrade today.')}
- % endif
-
- % if not is_mobile_app:
-
-
-
-
-
- % endif
-
-
- % endif
- % if content_type_gating_enabled:
- ${upgrade_to_complete_graded_banner()}
- % endif
- % elif missed_deadlines:
- % if missed_gated_content:
- ${upgrade_to_reset_banner()}
- % else:
- ${reset_dates_banner()}
- % endif
- % endif
-% endif
diff --git a/lms/urls.py b/lms/urls.py
index 773a3e8b80..a985dec640 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -667,14 +667,6 @@ urlpatterns += [
include('openedx.features.calendar_sync.urls'),
),
- # Course search
- re_path(
- r'^courses/{}/search/'.format(
- settings.COURSE_ID_PATTERN,
- ),
- include('openedx.features.course_search.urls'),
- ),
-
# Learner profile
path(
'u/',
@@ -809,12 +801,6 @@ if configuration_helpers.get_value('ENABLE_BULK_ENROLLMENT_VIEW', settings.FEATU
path('api/bulk_enroll/v1/', include('lms.djangoapps.bulk_enroll.urls')),
]
-# Course goals
-urlpatterns += [
- path('api/course_goals/', include(('lms.djangoapps.course_goals.urls', 'lms.djangoapps.course_goals'),
- namespace='course_goals_api')),
-]
-
# Embargo
if settings.FEATURES.get('EMBARGO'):
urlpatterns += [
diff --git a/openedx/core/djangoapps/embargo/tests/test_middleware.py b/openedx/core/djangoapps/embargo/tests/test_middleware.py
index d6a274b2de..bed219bb50 100644
--- a/openedx/core/djangoapps/embargo/tests/test_middleware.py
+++ b/openedx/core/djangoapps/embargo/tests/test_middleware.py
@@ -9,14 +9,12 @@ from config_models.models import cache as config_cache
from django.conf import settings
from django.core.cache import cache as django_cache
from django.urls import reverse
-from edx_toggles.toggles.testutils import override_waffle_flag
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from openedx.core.djangolib.testing.utils import skip_unless_lms
from common.djangoapps.student.tests.factories import UserFactory
from common.djangoapps.util.testing import UrlResetMixin
-from lms.djangoapps.course_home_api.toggles import COURSE_HOME_USE_LEGACY_FRONTEND
from ..models import IPFilter, RestrictedCourse
from ..test_utils import restrict_course
@@ -24,7 +22,6 @@ from ..test_utils import restrict_course
@ddt.ddt
@skip_unless_lms
-@override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True)
class EmbargoMiddlewareAccessTests(UrlResetMixin, ModuleStoreTestCase):
"""Tests of embargo middleware country access rules.
@@ -45,10 +42,7 @@ class EmbargoMiddlewareAccessTests(UrlResetMixin, ModuleStoreTestCase):
self.course = CourseFactory.create()
self.client.login(username=self.USERNAME, password=self.PASSWORD)
- self.courseware_url = reverse(
- 'openedx.course_experience.course_home',
- kwargs={'course_id': str(self.course.id)}
- )
+ self.courseware_url = reverse('about_course', kwargs={'course_id': str(self.course.id)})
self.non_courseware_url = reverse('dashboard')
# Clear the cache to avoid interference between tests
diff --git a/openedx/core/djangoapps/schedules/resolvers.py b/openedx/core/djangoapps/schedules/resolvers.py
index caf8bda729..1b406a6d71 100644
--- a/openedx/core/djangoapps/schedules/resolvers.py
+++ b/openedx/core/djangoapps/schedules/resolvers.py
@@ -28,7 +28,7 @@ from openedx.core.djangoapps.schedules.models import Schedule, ScheduleExperienc
from openedx.core.djangoapps.schedules.utils import PrefixedDebugLoggerMixin
from openedx.core.djangoapps.site_configuration.models import SiteConfiguration
from openedx.core.djangolib.translation_utils import translate_date
-from openedx.features.course_experience import course_home_url_name
+from openedx.features.course_experience import course_home_url
LOG = logging.getLogger(__name__)
@@ -542,9 +542,8 @@ def _get_trackable_course_home_url(course_id):
Args:
course_id (CourseKey): The course to get the home page URL for.
-
+U
Returns:
- A relative path to the course home page.
+ A URL to the course home page.
"""
- course_url_name = course_home_url_name(course_id)
- return reverse(course_url_name, args=[str(course_id)])
+ return course_home_url(course_id)
diff --git a/openedx/core/djangoapps/schedules/tests/test_resolvers.py b/openedx/core/djangoapps/schedules/tests/test_resolvers.py
index 1e63a02d0a..4e177491f4 100644
--- a/openedx/core/djangoapps/schedules/tests/test_resolvers.py
+++ b/openedx/core/djangoapps/schedules/tests/test_resolvers.py
@@ -167,7 +167,7 @@ class TestCourseUpdateResolver(SchedulesResolverTestMixin, ModuleStoreTestCase):
'contact_mailing_address': '123 Sesame Street',
'course_ids': [str(self.course.id)],
'course_name': self.course.display_name,
- 'course_url': f'/courses/{self.course.id}/course/',
+ 'course_url': f'http://learning-mfe/course/{self.course.id}/home',
'dashboard_url': '/dashboard',
'homepage_url': '/',
'mobile_store_urls': {},
@@ -258,7 +258,7 @@ class TestCourseNextSectionUpdateResolver(SchedulesResolverTestMixin, ModuleStor
'contact_mailing_address': '123 Sesame Street',
'course_ids': [str(self.course.id)],
'course_name': self.course.display_name,
- 'course_url': f'/courses/{self.course.id}/course/',
+ 'course_url': f'http://learning-mfe/course/{self.course.id}/home',
'dashboard_url': '/dashboard',
'homepage_url': '/',
'mobile_store_urls': {},
diff --git a/openedx/core/djangoapps/user_authn/views/auto_auth.py b/openedx/core/djangoapps/user_authn/views/auto_auth.py
index 03b2c30b84..324a0c1959 100644
--- a/openedx/core/djangoapps/user_authn/views/auto_auth.py
+++ b/openedx/core/djangoapps/user_authn/views/auto_auth.py
@@ -19,7 +19,7 @@ from opaque_keys.edx.locator import CourseLocator
from lms.djangoapps.verify_student.models import ManualVerification
from openedx.core.djangoapps.django_comment_common.models import assign_role
from openedx.core.djangoapps.user_authn.views.registration_form import AccountCreationForm
-from openedx.features.course_experience import course_home_url_name
+from openedx.features.course_experience import course_home_url
from common.djangoapps.student.helpers import (
AccountValidationError,
authenticate_new_user,
@@ -170,9 +170,9 @@ def auto_auth(request): # pylint: disable=too-many-statements
elif course_id:
# Redirect to the course homepage (in LMS) or outline page (in Studio)
try:
- redirect_url = reverse(course_home_url_name(course_key), kwargs={'course_id': course_id})
+ redirect_url = reverse('course_handler', kwargs={'course_key_string': course_id}) # Studio
except NoReverseMatch:
- redirect_url = reverse('course_handler', kwargs={'course_key_string': course_id})
+ redirect_url = course_home_url(course_key) # LMS
else:
# Redirect to the learner dashboard (in LMS) or homepage (in Studio)
try:
diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_auto_auth.py b/openedx/core/djangoapps/user_authn/views/tests/test_auto_auth.py
index a8bbd8a42a..0346aee25a 100644
--- a/openedx/core/djangoapps/user_authn/views/tests/test_auto_auth.py
+++ b/openedx/core/djangoapps/user_authn/views/tests/test_auto_auth.py
@@ -206,13 +206,13 @@ class AutoAuthEnabledTestCase(AutoAuthTestCase, ModuleStoreTestCase):
enrollment = CourseEnrollment.objects.get(course_id=course_key)
assert enrollment.user.username == 'test'
- # Check that the redirect was to the course info/outline page
+ # Check that the redirect was to the correct outline page for either lms or studio
if settings.ROOT_URLCONF == 'lms.urls':
- url_pattern = '/course/'
+ expected_redirect_url = f'http://learning-mfe/course/{course_id}/home'
else:
- url_pattern = f'/course/{str(course_key)}'
+ expected_redirect_url = f'/course/{course_id}'
- assert response.url.endswith(url_pattern)
+ assert response.url == expected_redirect_url
def test_redirect_to_main(self):
# Create user and redirect to 'home' (cms) or 'dashboard' (lms)
diff --git a/openedx/features/calendar_sync/views/calendar_sync.py b/openedx/features/calendar_sync/views/calendar_sync.py
index 2238e1d2aa..20f182daff 100644
--- a/openedx/features/calendar_sync/views/calendar_sync.py
+++ b/openedx/features/calendar_sync/views/calendar_sync.py
@@ -8,7 +8,6 @@ import json
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from django.shortcuts import redirect
-from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.generic import View
@@ -22,6 +21,7 @@ from openedx.features.calendar_sync.api import (
subscribe_user_to_calendar,
unsubscribe_user_to_calendar
)
+from openedx.features.course_experience import course_home_url
class CalendarSyncView(View):
@@ -54,4 +54,4 @@ class CalendarSyncView(View):
else:
return HttpResponse('Toggle data was not provided or had unknown value.',
status=status.HTTP_422_UNPROCESSABLE_ENTITY)
- return redirect(reverse('openedx.course_experience.course_home', args=[course_id]))
+ return redirect(course_home_url(course_key))
diff --git a/openedx/features/course_bookmarks/views/course_bookmarks.py b/openedx/features/course_bookmarks/views/course_bookmarks.py
index 00db635d70..7cdab73fc3 100644
--- a/openedx/features/course_bookmarks/views/course_bookmarks.py
+++ b/openedx/features/course_bookmarks/views/course_bookmarks.py
@@ -19,7 +19,7 @@ from web_fragments.fragment import Fragment
from lms.djangoapps.courseware.courses import get_course_with_access
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
from openedx.core.djangoapps.user_api.models import UserPreference
-from openedx.features.course_experience import default_course_url_name
+from openedx.features.course_experience import default_course_url
from common.djangoapps.util.views import ensure_valid_course_key
@@ -41,8 +41,7 @@ class CourseBookmarksView(View):
"""
course_key = CourseKey.from_string(course_id)
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
- course_url_name = default_course_url_name(course.id)
- course_url = reverse(course_url_name, kwargs={'course_id': str(course.id)})
+ course_url = default_course_url(course.id)
# Render the bookmarks list as a fragment
bookmarks_fragment = CourseBookmarksFragmentView().render_to_fragment(request, course_id=course_id)
diff --git a/openedx/features/course_duration_limits/tests/test_course_expiration.py b/openedx/features/course_duration_limits/tests/test_course_expiration.py
index c66734e28d..8f0211513f 100644
--- a/openedx/features/course_duration_limits/tests/test_course_expiration.py
+++ b/openedx/features/course_duration_limits/tests/test_course_expiration.py
@@ -10,6 +10,9 @@ 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
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.models import CourseEnrollment, FBEEnrollmentExclusion
@@ -21,8 +24,8 @@ from common.djangoapps.student.tests.factories import InstructorFactory
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.course_home_api.toggles import COURSE_HOME_USE_LEGACY_FRONTEND
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
@@ -37,14 +40,11 @@ from openedx.features.content_type_gating.helpers import CONTENT_GATING_PARTITIO
from openedx.features.course_duration_limits.access import get_user_course_expiration_date
from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
from openedx.features.course_experience.tests.views.helpers import add_course_mode
-from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
-from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
-from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID # lint-amnesty, pylint: disable=wrong-import-order
# pylint: disable=no-member
@ddt.ddt
-@override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True)
+@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):
@@ -52,6 +52,21 @@ class CourseExpirationTestCase(ModuleStoreTestCase, MasqueradeMixin):
self.course = CourseFactory(
start=now() - timedelta(weeks=10),
)
+ self.chapter = ItemFactory.create(
+ category='chapter',
+ parent_location=self.course.location,
+ display_name='Test Chapter'
+ )
+ self.sequential = ItemFactory.create(
+ category='sequential',
+ parent_location=self.chapter.location,
+ display_name='Test Sequential'
+ )
+ ItemFactory.create(
+ category='vertical',
+ parent_location=self.sequential.location,
+ display_name='Test Vertical'
+ )
self.user = UserFactory()
self.THREE_YEARS_AGO = now() - timedelta(days=(365 * 3))
@@ -63,6 +78,18 @@ class CourseExpirationTestCase(ModuleStoreTestCase, MasqueradeMixin):
CourseEnrollment.unenroll(self.user, self.course.id)
super().tearDown() # lint-amnesty, pylint: disable=super-with-arguments
+ 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,
+ },
+ )
+ return self.client.get(courseware_url, follow=True)
+
def test_enrollment_mode(self):
"""Tests that verified enrollments do not have an expiration"""
CourseEnrollment.enroll(self.user, self.course.id, CourseMode.VERIFIED)
@@ -236,8 +263,7 @@ class CourseExpirationTestCase(ModuleStoreTestCase, MasqueradeMixin):
self.update_masquerade(**masquerade_config)
- course_home_url = reverse('openedx.course_experience.course_home', args=[str(self.course.id)])
- response = self.client.get(course_home_url, follow=True)
+ response = self.get_courseware()
assert response.status_code == 200
self.assertCountEqual(response.redirect_chain, [])
banner_text = 'You lose all access to this course, including your progress,'
@@ -273,8 +299,7 @@ class CourseExpirationTestCase(ModuleStoreTestCase, MasqueradeMixin):
self.update_masquerade(username='audit')
- course_home_url = reverse('openedx.course_experience.course_home', args=[str(self.course.id)])
- response = self.client.get(course_home_url, follow=True)
+ response = self.get_courseware()
assert response.status_code == 200
self.assertCountEqual(response.redirect_chain, [])
banner_text = 'You lose all access to this course, including your progress,'
@@ -309,8 +334,7 @@ class CourseExpirationTestCase(ModuleStoreTestCase, MasqueradeMixin):
self.update_masquerade(username='audit')
- course_home_url = reverse('openedx.course_experience.course_home', args=[str(self.course.id)])
- response = self.client.get(course_home_url, follow=True)
+ response = self.get_courseware()
assert response.status_code == 200
self.assertCountEqual(response.redirect_chain, [])
banner_text = 'This learner does not have access to this course. Their access expired on'
@@ -360,8 +384,7 @@ class CourseExpirationTestCase(ModuleStoreTestCase, MasqueradeMixin):
self.update_masquerade(username=expired_staff.username)
- course_home_url = reverse('openedx.course_experience.course_home', args=[str(self.course.id)])
- response = self.client.get(course_home_url, follow=True)
+ response = self.get_courseware()
assert response.status_code == 200
self.assertCountEqual(response.redirect_chain, [])
banner_text = 'This learner does not have access to this course. Their access expired on'
@@ -409,8 +432,7 @@ class CourseExpirationTestCase(ModuleStoreTestCase, MasqueradeMixin):
self.update_masquerade(username=expired_staff.username)
- course_home_url = reverse('openedx.course_experience.course_home', args=[str(self.course.id)])
- response = self.client.get(course_home_url, follow=True)
+ response = self.get_courseware()
assert response.status_code == 200
self.assertCountEqual(response.redirect_chain, [])
banner_text = 'This learner does not have access to this course. Their access expired on'
diff --git a/openedx/features/course_experience/__init__.py b/openedx/features/course_experience/__init__.py
index 38db72f69e..0c9eca091d 100644
--- a/openedx/features/course_experience/__init__.py
+++ b/openedx/features/course_experience/__init__.py
@@ -1,28 +1,24 @@
"""
Unified course experience settings and helper methods.
"""
-import crum
-from django.utils.translation import gettext as _
-from edx_django_utils.monitoring import set_custom_attribute
-from waffle import flag_is_active # lint-amnesty, pylint: disable=invalid-django-waffle-import
+from django.urls import reverse
+from django.utils.translation import gettext as _
from edx_toggles.toggles import LegacyWaffleFlag, LegacyWaffleFlagNamespace
-from openedx.core.djangoapps.util.user_messages import UserMessageCollection
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
+
# Namespace for course experience waffle flags.
WAFFLE_FLAG_NAMESPACE = LegacyWaffleFlagNamespace(name='course_experience')
-COURSE_EXPERIENCE_WAFFLE_FLAG_NAMESPACE = LegacyWaffleFlagNamespace(name='course_experience')
-
# Waffle flag to disable the separate course outline page and full width content.
DISABLE_COURSE_OUTLINE_PAGE_FLAG = CourseWaffleFlag( # lint-amnesty, pylint: disable=toggle-missing-annotation
- COURSE_EXPERIENCE_WAFFLE_FLAG_NAMESPACE, 'disable_course_outline_page', __name__
+ WAFFLE_FLAG_NAMESPACE, 'disable_course_outline_page', __name__
)
# Waffle flag to enable a single unified "Course" tab.
DISABLE_UNIFIED_COURSE_TAB_FLAG = CourseWaffleFlag( # lint-amnesty, pylint: disable=toggle-missing-annotation
- COURSE_EXPERIENCE_WAFFLE_FLAG_NAMESPACE, 'disable_unified_course_tab', __name__
+ WAFFLE_FLAG_NAMESPACE, 'disable_unified_course_tab', __name__
)
# Waffle flag to enable the sock on the footer of the home and courseware pages.
@@ -41,24 +37,6 @@ COURSE_PRE_START_ACCESS_FLAG = LegacyWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'pre_star
# .. toggle_warnings: This temporary feature toggle does not have a target removal date.
ENABLE_COURSE_GOALS = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'enable_course_goals', __name__) # lint-amnesty, pylint: disable=toggle-missing-annotation
-# Waffle flag to control the display of the hero
-SHOW_UPGRADE_MSG_ON_COURSE_HOME = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'show_upgrade_msg_on_course_home', __name__) # lint-amnesty, pylint: disable=toggle-missing-annotation
-
-# Waffle flag to control the display of the upgrade deadline message
-UPGRADE_DEADLINE_MESSAGE = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'upgrade_deadline_message', __name__) # lint-amnesty, pylint: disable=toggle-missing-annotation
-
-# .. toggle_name: course_experience.latest_update
-# .. toggle_implementation: CourseWaffleFlag
-# .. toggle_default: False
-# .. toggle_description: Used to switch between 'welcome message' and 'latest update' on the course home page.
-# .. toggle_use_cases: opt_out, temporary
-# .. toggle_creation_date: 2017-09-11
-# .. toggle_target_removal_date: None
-# .. toggle_warnings: This is meant to be configured using waffle_utils course override only. Either do not create the
-# actual waffle flag, or be sure to unset the flag even for Superusers. This is no longer used in the learning MFE
-# and can be removed when the outline tab is fully moved to the learning MFE.
-LATEST_UPDATE_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'latest_update', __name__) # lint-amnesty, pylint: disable=toggle-missing-annotation
-
# Waffle flag to enable anonymous access to a course
SEO_WAFFLE_FLAG_NAMESPACE = LegacyWaffleFlagNamespace(name='seo')
COURSE_ENABLE_UNENROLLED_ACCESS_FLAG = CourseWaffleFlag( # lint-amnesty, pylint: disable=toggle-missing-annotation
@@ -94,48 +72,38 @@ RELATIVE_DATES_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'relative_dates',
CALENDAR_SYNC_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'calendar_sync', __name__) # lint-amnesty, pylint: disable=toggle-missing-annotation
-def course_home_page_title(course): # pylint: disable=unused-argument
+def course_home_page_title(_course):
"""
Returns the title for the course home page.
"""
return _('Course')
-def default_course_url_name(course_id):
+def default_course_url(course_key):
"""
- Returns the default course URL name for the current user.
+ Returns the default course URL for the current user.
Arguments:
- course_id (CourseKey): The course id of the current course.
+ course_key (CourseKey): The course id of the current course.
"""
- if DISABLE_COURSE_OUTLINE_PAGE_FLAG.is_enabled(course_id):
- return 'courseware'
- return 'openedx.course_experience.course_home'
+ from .url_helpers import get_learning_mfe_home_url
+
+ if DISABLE_COURSE_OUTLINE_PAGE_FLAG.is_enabled(course_key):
+ return reverse('courseware', args=[str(course_key)])
+
+ return get_learning_mfe_home_url(course_key, url_fragment='home')
-def course_home_url_name(course_key):
+def course_home_url(course_key):
"""
- Returns the course home page's URL name for the current user.
+ Returns the course home page's URL for the current user.
Arguments:
- course_key (CourseKey): The course key for which the home url is being
- requested.
-
+ course_key (CourseKey): The course key for which the home url is being requested.
"""
+ from .url_helpers import get_learning_mfe_home_url
+
if DISABLE_UNIFIED_COURSE_TAB_FLAG.is_enabled(course_key):
- return 'info'
- return 'openedx.course_experience.course_home'
+ return reverse('info', args=[str(course_key)])
-
-class CourseHomeMessages(UserMessageCollection):
- """
- This set of messages appear above the outline on the course home page.
- """
- NAMESPACE = 'course_home_level_messages'
-
- @classmethod
- def get_namespace(cls):
- """
- Returns the namespace of the message collection.
- """
- return cls.NAMESPACE
+ return get_learning_mfe_home_url(course_key, url_fragment='home')
diff --git a/openedx/features/course_experience/static/course_experience/fixtures/course-home-fragment.html b/openedx/features/course_experience/static/course_experience/fixtures/course-home-fragment.html
deleted file mode 100644
index 4c9252e81c..0000000000
--- a/openedx/features/course_experience/static/course_experience/fixtures/course-home-fragment.html
+++ /dev/null
@@ -1,109 +0,0 @@
-
- ${_("We've built a suggested schedule to help you stay on track.")}
- ${_("But don't worry—it's flexible so you can learn at your own pace. If you happen to fall behind on our suggested dates, you'll be able to adjust them to keep yourself on track.")}
-
-
-
-
-
diff --git a/openedx/features/course_experience/static/course_experience/fixtures/course-outline-fragment.html b/openedx/features/course_experience/static/course_experience/fixtures/course-outline-fragment.html
deleted file mode 100644
index 64abaf83d6..0000000000
--- a/openedx/features/course_experience/static/course_experience/fixtures/course-outline-fragment.html
+++ /dev/null
@@ -1,131 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- Start Course
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
- - - Introduction --
-
-
- - - Demo Course Overview - - -
- -
- - - Example Week 1: Getting Started --
-
-
- - - Lesson 1 - Getting Started - - -
- - - Homework - Question Styles - - -
- -
- - - Example Week 2: Get Interactive --
-
-
- - - Lesson 2 - Let's Get Interactive! - - -
- - - Homework - Labs and Demos - - -
- - - Homework - Essays - - -
- -
- - - Example Week 3: Be Social --
-
-
- - - Lesson 3 - Be Social - - -
- - - Homework - Find Your Study Buddy - - -
- - - More Ways to Connect - - -
- -
- - - About Exams and Certificates --
-
-
- - - edX Exams - - -
- -
- - - holding section --
-
-
- - - New Subsection - - -
-
-
diff --git a/openedx/features/course_experience/static/course_experience/fixtures/welcome-message-fragment.html b/openedx/features/course_experience/static/course_experience/fixtures/welcome-message-fragment.html
deleted file mode 100644
index e83f92403d..0000000000
--- a/openedx/features/course_experience/static/course_experience/fixtures/welcome-message-fragment.html
+++ /dev/null
@@ -1,32 +0,0 @@
-Latest Update
-
-
-
- This is an update.
-
-
diff --git a/openedx/features/course_experience/static/course_experience/images/home_message_author.png b/openedx/features/course_experience/static/course_experience/images/home_message_author.png
deleted file mode 100644
index 75425438be..0000000000
Binary files a/openedx/features/course_experience/static/course_experience/images/home_message_author.png and /dev/null differ
diff --git a/openedx/features/course_experience/static/course_experience/js/CourseGoals.js b/openedx/features/course_experience/static/course_experience/js/CourseGoals.js
deleted file mode 100644
index 467997b759..0000000000
--- a/openedx/features/course_experience/static/course_experience/js/CourseGoals.js
+++ /dev/null
@@ -1,50 +0,0 @@
-/* globals gettext */
-
-import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
-
-export class CourseGoals { // eslint-disable-line import/prefer-default-export
-
- constructor(options) {
- $('.goal-option').click((e) => {
- const goalKey = $(e.target).data().choice;
- $.ajax({
- method: 'POST',
- url: options.goalApiUrl,
- headers: { 'X-CSRFToken': $.cookie('csrftoken') },
- data: {
- goal_key: goalKey,
- course_key: options.courseId,
- user: options.username,
- },
- dataType: 'json',
- success: (data) => { // LEARNER-2522 will address the success message
- $('.section-goals').slideDown();
- $('.section-goals .goal .text').text(data.goal_text);
- $('.section-goals select').val(data.goal_key);
- const successMsg = HtmlUtils.interpolateHtml(
- gettext('Thank you for setting your course goal to {goal}!'),
- { goal: data.goal_text.toLowerCase() },
- );
- if (!data.is_unsure) {
- // xss-lint: disable=javascript-jquery-html
- $('.message-content').html(`
-
-
-
-
- This is a useful welcome message that is too long!
- This is a useful welcome message that is too long!
- This is a useful welcome message that is too long!
- This is a useful welcome message that is too long!
- This is a useful welcome message that is too long!
- This is a useful welcome message that is too long!
- This is a useful welcome message that is too long!
- This is a useful welcome message that is too long!
- This is a useful welcome message that is too long!
- This is a useful welcome message that is too long!
- This is a useful welcome message that is too long!
-
-
-
-${successMsg}
`);
- } else {
- $('.message-content').parent().hide();
- }
- },
- error: () => { // LEARNER-2522 will address the error message
- const errorMsg = gettext('There was an error in setting your goal, please reload the page and try again.');
- // xss-lint: disable=javascript-jquery-html
- $('.message-content').html(` ${errorMsg}
`);
- },
- });
- });
-
- // Allow goal selection with an enter press for accessibility purposes
- $('.goal-option').keypress((e) => {
- if (e.which === 13) {
- $(e.target).click();
- }
- });
- }
-}
diff --git a/openedx/features/course_experience/static/course_experience/js/CourseHome.js b/openedx/features/course_experience/static/course_experience/js/CourseHome.js
deleted file mode 100644
index 7ee8023cc4..0000000000
--- a/openedx/features/course_experience/static/course_experience/js/CourseHome.js
+++ /dev/null
@@ -1,160 +0,0 @@
-/* globals gettext, Logger */
-
-export class CourseHome { // eslint-disable-line import/prefer-default-export
- constructor(options) {
- this.courseRunKey = options.courseRunKey;
- this.msgStateStorageKey = `course_experience.upgrade_msg.${this.courseRunKey}.collapsed`;
-
- // Logging for 'Resume Course' or 'Start Course' button click
- const $resumeCourseLink = $(options.resumeCourseLink);
- $resumeCourseLink.on('click', (event) => {
- const eventType = $resumeCourseLink.find('span').data('action-type');
- Logger.log(
- 'edx.course.home.resume_course.clicked',
- {
- event_type: eventType,
- url: event.currentTarget.href,
- },
- );
- });
-
- // Logging for course tool click events
- const $courseToolLink = $(options.courseToolLink);
- $courseToolLink.on('click', (event) => {
- const courseToolName = event.srcElement.dataset['analytics-id']; // eslint-disable-line dot-notation
- Logger.log(
- 'edx.course.tool.accessed',
- {
- tool_name: courseToolName,
- },
- );
- });
-
- // Course goal editing elements
- const $goalSection = $('.section-goals');
- const $editGoalIcon = $('.section-goals .edit-icon');
- const $currentGoalText = $('.section-goals .goal');
- const $goalSelect = $('.section-goals .edit-goal-select');
- const $responseIndicator = $('.section-goals .response-icon');
- const $responseMessageSr = $('.section-goals .sr-update-response-msg');
- const $goalUpdateTitle = $('.section-goals .title:not("label")');
- const $goalUpdateLabel = $('.section-goals label.title');
-
- // Switch to editing mode when the goal section is clicked
- $goalSection.on('click', (event) => {
- if (!$(event.target).hasClass('edit-goal-select')) {
- $goalSelect.toggle();
- $currentGoalText.toggle();
- $goalUpdateTitle.toggle();
- $goalUpdateLabel.toggle();
- $responseIndicator.removeClass().addClass('response-icon');
- $goalSelect.focus();
- }
- });
-
- // Trigger click event on enter press for accessibility purposes
- $(document.body).on('keyup', '.section-goals .edit-icon', (event) => {
- if (event.which === 13) {
- $(event.target).trigger('click');
- }
- });
-
- // Send an ajax request to update the course goal
- $goalSelect.on('blur change', (event) => {
- $currentGoalText.show();
- $goalUpdateTitle.show();
- $goalUpdateLabel.hide();
- $goalSelect.hide();
- // No need to update in the case of a blur event
- if (event.type === 'blur') return;
- const newGoalKey = $(event.target).val();
- $responseIndicator.removeClass().addClass('response-icon fa fa-spinner fa-spin');
- $.ajax({
- method: 'POST',
- url: options.goalApiUrl,
- headers: { 'X-CSRFToken': $.cookie('csrftoken') },
- data: {
- goal_key: newGoalKey,
- course_key: options.courseId,
- user: options.username,
- },
- dataType: 'json',
- success: (data) => {
- $currentGoalText.find('.text').text(data.goal_text);
- $responseMessageSr.text(gettext('You have successfully updated your goal.'));
- $responseIndicator.removeClass().addClass('response-icon fa fa-check');
- },
- error: () => {
- $responseIndicator.removeClass().addClass('response-icon fa fa-close');
- $responseMessageSr.text(gettext('There was an error updating your goal.'));
- },
- complete: () => {
- // Only show response icon indicator for 3 seconds.
- setTimeout(() => {
- $responseIndicator.removeClass().addClass('response-icon');
- }, 3000);
- $editGoalIcon.focus();
- },
- });
- });
-
- // Dismissibility for in course messages
- $(document.body).on('click', '.course-message .dismiss', (event) => {
- $(event.target).closest('.course-message').hide();
- });
-
- // Allow dismiss on enter press for accessibility purposes
- $(document.body).on('keyup', '.course-message .dismiss', (event) => {
- if (event.which === 13) {
- $(event.target).trigger('click');
- }
- });
-
- $(document).ready(() => {
- this.configureUpgradeMessage();
- this.configureUpgradeAnalytics();
- });
- }
-
- static fireSegmentEvent(event, properties) {
- /* istanbul ignore next */
- if (!window.analytics) {
- return;
- }
-
- window.analytics.track(event, properties);
- }
-
- // Promotion analytics for upgrade messages on course home.
- // eslint-disable-next-line class-methods-use-this
- configureUpgradeAnalytics() {
- $('.btn-upgrade').each(
- (index, button) => {
- const promotionEventProperties = {
- promotion_id: 'courseware_verified_certificate_upsell',
- creative: $(button).data('creative'),
- name: 'In-Course Verification Prompt',
- position: $(button).data('position'),
- };
- CourseHome.fireSegmentEvent('Promotion Viewed', promotionEventProperties);
- $(button).click(() => {
- CourseHome.fireSegmentEvent('Promotion Clicked', promotionEventProperties);
- });
- },
- );
- }
-
- configureUpgradeMessage() {
- const logEventProperties = { courseRunKey: this.courseRunKey };
-
- Logger.log('edx.bi.course.upgrade.sidebarupsell.displayed', logEventProperties);
- $('.section-upgrade .btn-upgrade').click(() => {
- Logger.log('edx.bi.course.upgrade.sidebarupsell.clicked', logEventProperties);
- Logger.log('edx.course.enrollment.upgrade.clicked', { location: 'sidebar-message' });
- });
- $('.promo-learn-more').click(() => {
- $('.action-toggle-verification-sock').click();
- $('.action-toggle-verification-sock')[0].scrollIntoView({ behavior: 'smooth', alignToTop: true });
- });
- }
-}
diff --git a/openedx/features/course_experience/static/course_experience/js/CourseOutline.js b/openedx/features/course_experience/static/course_experience/js/CourseOutline.js
deleted file mode 100644
index 587d1098b5..0000000000
--- a/openedx/features/course_experience/static/course_experience/js/CourseOutline.js
+++ /dev/null
@@ -1,112 +0,0 @@
-/* globals Logger */
-
-import { keys } from 'edx-ui-toolkit/js/utils/constants';
-
-// @TODO: Figure out how to make webpack handle default exports when libraryTarget: 'window'
-export class CourseOutline { // eslint-disable-line import/prefer-default-export
- constructor() {
- const focusable = [...document.querySelectorAll('.outline-item.focusable')];
-
- focusable.forEach(el => el.addEventListener('keydown', (event) => {
- const index = focusable.indexOf(event.target);
-
- switch (event.key) { // eslint-disable-line default-case
- case keys.down:
- event.preventDefault();
- focusable[Math.min(index + 1, focusable.length - 1)].focus();
- break;
- case keys.up: // @TODO: Get these from the UI Toolkit
- event.preventDefault();
- focusable[Math.max(index - 1, 0)].focus();
- break;
- }
- }));
-
- [...document.querySelectorAll('a:not([href^="#"])')]
- .forEach(link => link.addEventListener('click', (event) => {
- Logger.log(
- 'edx.ui.lms.link_clicked',
- {
- current_url: window.location.href,
- target_url: event.currentTarget.href,
- },
- );
- }),
- );
-
- function expandSection(sectionToggleButton) {
- const $toggleButtonChevron = $(sectionToggleButton).children('.fa-chevron-right');
- const $contentPanel = $(document.getElementById(sectionToggleButton.getAttribute('aria-controls')));
-
- $contentPanel.slideDown();
- $contentPanel.removeClass('is-hidden');
- $toggleButtonChevron.addClass('fa-rotate-90');
- sectionToggleButton.setAttribute('aria-expanded', 'true');
- }
-
- function collapseSection(sectionToggleButton) {
- const $toggleButtonChevron = $(sectionToggleButton).children('.fa-chevron-right');
- const $contentPanel = $(document.getElementById(sectionToggleButton.getAttribute('aria-controls')));
-
- $contentPanel.slideUp();
- $contentPanel.addClass('is-hidden');
- $toggleButtonChevron.removeClass('fa-rotate-90');
- sectionToggleButton.setAttribute('aria-expanded', 'false');
- }
-
- [...document.querySelectorAll(('.accordion'))]
- .forEach((accordion) => {
- const sections = Array.prototype.slice.call(accordion.querySelectorAll('.accordion-trigger'));
-
- sections.forEach(section => section.addEventListener('click', (event) => {
- const sectionToggleButton = event.currentTarget;
- if (sectionToggleButton.classList.contains('accordion-trigger')) {
- const isExpanded = sectionToggleButton.getAttribute('aria-expanded') === 'true';
- if (!isExpanded) {
- expandSection(sectionToggleButton);
- } else if (isExpanded) {
- collapseSection(sectionToggleButton);
- }
- event.stopImmediatePropagation();
- }
- }));
- });
-
- const toggleAllButton = document.querySelector('#expand-collapse-outline-all-button');
- const toggleAllSpan = document.querySelector('#expand-collapse-outline-all-span');
- const extraPaddingClass = 'expand-collapse-outline-all-extra-padding';
- toggleAllButton.addEventListener('click', (event) => {
- const toggleAllExpanded = toggleAllButton.getAttribute('aria-expanded') === 'true';
- let sectionAction;
- /* globals gettext */
- if (toggleAllExpanded) {
- toggleAllButton.setAttribute('aria-expanded', 'false');
- sectionAction = collapseSection;
- toggleAllSpan.classList.add(extraPaddingClass);
- toggleAllSpan.innerText = gettext('Expand All');
- } else {
- toggleAllButton.setAttribute('aria-expanded', 'true');
- sectionAction = expandSection;
- toggleAllSpan.classList.remove(extraPaddingClass);
- toggleAllSpan.innerText = gettext('Collapse All');
- }
- const sections = Array.prototype.slice.call(document.querySelectorAll('.accordion-trigger'));
- sections.forEach((sectionToggleButton) => {
- sectionAction(sectionToggleButton);
- });
- event.stopImmediatePropagation();
- });
-
- const urlHash = window.location.hash;
-
- if (urlHash !== '') {
- const button = document.getElementById(urlHash.substr(1, urlHash.length));
- if (button.classList.contains('subsection-text')) {
- const parentLi = button.closest('.section');
- const parentButton = parentLi.querySelector('.section-name');
- expandSection(parentButton);
- }
- expandSection(button);
- }
- }
-}
diff --git a/openedx/features/course_experience/static/course_experience/js/Enrollment.js b/openedx/features/course_experience/static/course_experience/js/Enrollment.js
deleted file mode 100644
index e28fd5e063..0000000000
--- a/openedx/features/course_experience/static/course_experience/js/Enrollment.js
+++ /dev/null
@@ -1,45 +0,0 @@
-
-/*
- * Course Enrollment on the Course Home page
- */
-export class CourseEnrollment { // eslint-disable-line import/prefer-default-export
- /**
- * Redirect to a URL. Mainly useful for mocking out in tests.
- * @param {string} url The URL to redirect to.
- */
- static redirect(url) {
- window.location.href = url;
- }
-
- static refresh() {
- window.location.reload(false);
- }
-
- static createEnrollment(courseId) {
- const data = JSON.stringify({
- course_details: { course_id: courseId },
- });
- const enrollmentAPI = '/api/enrollment/v1/enrollment';
- const trackSelection = '/course_modes/choose/';
-
- return () =>
- $.ajax(
- {
- type: 'POST',
- url: enrollmentAPI,
- data,
- contentType: 'application/json',
- }).done(() => {
- window.analytics.track('edx.bi.user.course-home.enrollment');
- CourseEnrollment.refresh();
- }).fail(() => {
- // If the simple enrollment we attempted failed, go to the track selection page,
- // which is better for handling more complex enrollment situations.
- CourseEnrollment.redirect(trackSelection + courseId);
- });
- }
-
- constructor(buttonClass, courseId) {
- $(buttonClass).click(CourseEnrollment.createEnrollment(courseId));
- }
-}
diff --git a/openedx/features/course_experience/static/course_experience/js/LatestUpdate.js b/openedx/features/course_experience/static/course_experience/js/LatestUpdate.js
deleted file mode 100644
index fc9fe52ef0..0000000000
--- a/openedx/features/course_experience/static/course_experience/js/LatestUpdate.js
+++ /dev/null
@@ -1,15 +0,0 @@
-/* globals $ */
-import 'jquery.cookie';
-
-export class LatestUpdate { // eslint-disable-line import/prefer-default-export
-
- constructor(options) {
- if ($.cookie('update-message') === 'hide') {
- $(options.messageContainer).hide();
- }
- $(options.dismissButton).click(() => {
- $.cookie('update-message', 'hide', { expires: 1 });
- $(options.messageContainer).hide();
- });
- }
-}
diff --git a/openedx/features/course_experience/static/course_experience/js/WelcomeMessage.js b/openedx/features/course_experience/static/course_experience/js/WelcomeMessage.js
deleted file mode 100644
index 90685eac52..0000000000
--- a/openedx/features/course_experience/static/course_experience/js/WelcomeMessage.js
+++ /dev/null
@@ -1,65 +0,0 @@
-/* globals $ */
-import 'jquery.cookie'; // eslint-disable-line
-import gettext from 'gettext'; // eslint-disable-line
-import { clampHtmlByWords } from 'common/js/utils/clamp-html'; // eslint-disable-line
-
-export class WelcomeMessage { // eslint-disable-line import/prefer-default-export
-
- static dismissWelcomeMessage(dismissUrl) {
- $.ajax({
- type: 'POST',
- url: dismissUrl,
- headers: {
- 'X-CSRFToken': $.cookie('csrftoken'),
- },
- success: () => {
- $('.welcome-message').hide();
- },
- });
- }
-
- constructor(options) {
- // Dismiss the welcome message if the user clicks dismiss, or auto-dismiss if
- // the user doesn't click dismiss in 7 days from when it was first viewed.
-
- // Check to see if the welcome message has been displayed at all.
- if ($('.welcome-message').length > 0) {
- // If the welcome message has been viewed.
- if ($.cookie('welcome-message-viewed') === 'True') {
- // If the timer cookie no longer exists, dismiss the welcome message permanently.
- if ($.cookie('welcome-message-timer') !== 'True') {
- WelcomeMessage.dismissWelcomeMessage(options.dismissUrl);
- }
- } else {
- // Set both the viewed cookie and the timer cookie.
- $.cookie('welcome-message-viewed', 'True', { expires: 365 });
- $.cookie('welcome-message-timer', 'True', { expires: 7 });
- }
- }
- $('.dismiss-message button').click(() => WelcomeMessage.dismissWelcomeMessage(options.dismissUrl));
-
-
- // "Show More" support for welcome messages
- const messageContent = document.querySelector('#welcome-message-content');
- const fullText = messageContent.innerHTML;
- if (clampHtmlByWords(messageContent, 100) < 0) {
- const showMoreButton = document.querySelector('#welcome-message-show-more');
- const shortText = messageContent.innerHTML;
-
- showMoreButton.removeAttribute('hidden');
-
- showMoreButton.addEventListener('click', (event) => {
- if (showMoreButton.getAttribute('data-state') === 'less') {
- showMoreButton.textContent = gettext('Show More');
- messageContent.innerHTML = shortText;
- showMoreButton.setAttribute('data-state', 'more');
- } else {
- showMoreButton.textContent = gettext('Show Less');
- messageContent.innerHTML = fullText;
- showMoreButton.setAttribute('data-state', 'less');
- }
- event.stopImmediatePropagation();
- });
- }
- }
-}
diff --git a/openedx/features/course_experience/static/course_experience/js/spec/CourseHome_spec.js b/openedx/features/course_experience/static/course_experience/js/spec/CourseHome_spec.js
deleted file mode 100644
index a7c918769b..0000000000
--- a/openedx/features/course_experience/static/course_experience/js/spec/CourseHome_spec.js
+++ /dev/null
@@ -1,70 +0,0 @@
-/* globals Logger, loadFixtures */
-
-import { CourseHome } from '../CourseHome';
-
-describe('Course Home factory', () => {
- let home;
- const runKey = 'course-v1:edX+DemoX+Demo_Course';
- window.analytics = jasmine.createSpyObj('analytics', ['page', 'track', 'trackLink']);
-
- beforeEach(() => {
- loadFixtures('course_experience/fixtures/course-home-fragment.html');
- spyOn(Logger, 'log');
- home = new CourseHome({ // eslint-disable-line no-unused-vars
- courseRunKey: runKey,
- resumeCourseLink: '.action-resume-course',
- courseToolLink: '.course-tool-link',
- });
- });
-
- describe('Ensure course tool click logging', () => {
- it('sends an event when resume or start course is clicked', () => {
- $('.action-resume-course').click();
- expect(Logger.log).toHaveBeenCalledWith(
- 'edx.course.home.resume_course.clicked',
- {
- event_type: 'start',
- url: `http://${window.location.host}/courses/course-v1:edX+DemoX+Demo_Course/courseware` +
- '/19a30717eff543078a5d94ae9d6c18a5/',
- },
- );
- });
-
- it('sends an event when an course tool is clicked', () => {
- const courseToolNames = document.querySelectorAll('.course-tool-link');
- for (let i = 0; i < courseToolNames.length; i += 1) {
- const courseToolName = courseToolNames[i].dataset['analytics-id']; // eslint-disable-line dot-notation
- const event = new CustomEvent('click');
- event.srcElement = { dataset: { 'analytics-id': courseToolName } };
- courseToolNames[i].dispatchEvent(event);
- expect(Logger.log).toHaveBeenCalledWith(
- 'edx.course.tool.accessed',
- {
- tool_name: courseToolName,
- },
- );
- }
- });
- });
-
- describe('Upgrade message events', () => {
- const segmentEventProperties = {
- promotion_id: 'courseware_verified_certificate_upsell',
- creative: 'sidebarupsell',
- name: 'In-Course Verification Prompt',
- position: 'sidebar-message',
- };
-
- it('should send events to Segment and edX on initial load', () => {
- expect(window.analytics.track).toHaveBeenCalledWith('Promotion Viewed', segmentEventProperties);
- expect(Logger.log).toHaveBeenCalledWith('edx.bi.course.upgrade.sidebarupsell.displayed', { courseRunKey: runKey });
- });
-
- it('should send events to Segment and edX after clicking the upgrade button ', () => {
- $('.section-upgrade .btn-upgrade').click();
- expect(window.analytics.track).toHaveBeenCalledWith('Promotion Viewed', segmentEventProperties);
- expect(Logger.log).toHaveBeenCalledWith('edx.bi.course.upgrade.sidebarupsell.clicked', { courseRunKey: runKey });
- expect(Logger.log).toHaveBeenCalledWith('edx.course.enrollment.upgrade.clicked', { location: 'sidebar-message' });
- });
- });
-});
diff --git a/openedx/features/course_experience/static/course_experience/js/spec/CourseOutline_spec.js b/openedx/features/course_experience/static/course_experience/js/spec/CourseOutline_spec.js
deleted file mode 100644
index 258fb3ef57..0000000000
--- a/openedx/features/course_experience/static/course_experience/js/spec/CourseOutline_spec.js
+++ /dev/null
@@ -1,113 +0,0 @@
-/* globals Logger, loadFixtures */
-
-import { keys } from 'edx-ui-toolkit/js/utils/constants';
-
-import { CourseOutline } from '../CourseOutline';
-
-describe('Course Outline factory', () => {
- let outline; // eslint-disable-line no-unused-vars
-
- // Our block IDs are invalid DOM selectors unless we first escape `:`, `+` and `@`
- const escapeIds = idObj => Object.assign({}, ...Object.keys(idObj).map(key => ({
- [key]: idObj[key]
- .replace(/@/g, '\\@')
- .replace(/:/, '\\:')
- .replace(/\+/g, '\\+'),
- })));
-
- const outlineIds = escapeIds({
- homeworkLabsAndDemos: 'a#block-v1:edX+DemoX+Demo_Course+type@sequential+block@graded_simulations',
- homeworkEssays: 'a#block-v1:edX+DemoX+Demo_Course+type@sequential+block@175e76c4951144a29d46211361266e0e',
- lesson3BeSocial: 'a#block-v1:edX+DemoX+Demo_Course+type@sequential+block@48ecb924d7fe4b66a230137626bfa93e',
- exampleWeek3BeSocial: 'li#block-v1:edX+DemoX+Demo_Course+type@chapter+block@social_integration',
- });
-
- describe('keyboard listener', () => {
- const triggerKeyListener = (current, destination, key) => {
- current.focus();
- spyOn(destination, 'focus');
-
- current.dispatchEvent(new KeyboardEvent('keydown', { key }));
- };
-
- beforeEach(() => {
- loadFixtures('course_experience/fixtures/course-outline-fragment.html');
- outline = new CourseOutline();
- });
-
- describe('when the down arrow is pressed', () => {
- it('moves focus from a subsection to the next subsection in the outline', () => {
- const current = document.querySelector(outlineIds.homeworkLabsAndDemos);
- const destination = document.querySelector(outlineIds.homeworkEssays);
-
- triggerKeyListener(current, destination, keys.down);
-
- expect(destination.focus).toHaveBeenCalled();
- });
-
- it('moves focus to the subsection list if at the top of a section', () => {
- const current = document.querySelector(outlineIds.exampleWeek3BeSocial);
- const destination = document.querySelector(`${outlineIds.exampleWeek3BeSocial} > ol`);
-
- triggerKeyListener(current, destination, keys.down);
-
- expect(destination.focus).toHaveBeenCalled();
- });
-
- it('moves focus to the next section if on the last subsection', () => {
- const current = document.querySelector(outlineIds.homeworkEssays);
- const destination = document.querySelector(outlineIds.exampleWeek3BeSocial);
-
- triggerKeyListener(current, destination, keys.down);
-
- expect(destination.focus).toHaveBeenCalled();
- });
- });
-
- describe('when the up arrow is pressed', () => {
- it('moves focus from a subsection to the previous subsection in the outline', () => {
- const current = document.querySelector(outlineIds.homeworkEssays);
- const destination = document.querySelector(outlineIds.homeworkLabsAndDemos);
-
- triggerKeyListener(current, destination, keys.up);
-
- expect(destination.focus).toHaveBeenCalled();
- });
-
- it('moves focus to the section list if at the first subsection', () => {
- const current = document.querySelector(outlineIds.lesson3BeSocial);
- const destination = document.querySelector(`${outlineIds.exampleWeek3BeSocial} > ol`);
-
- triggerKeyListener(current, destination, keys.up);
-
- expect(destination.focus).toHaveBeenCalled();
- });
-
- it('moves focus last subsection of the previous section if at a section boundary', () => {
- const current = document.querySelector(outlineIds.exampleWeek3BeSocial);
- const destination = document.querySelector(outlineIds.homeworkEssays);
-
- triggerKeyListener(current, destination, keys.up);
-
- expect(destination.focus).toHaveBeenCalled();
- });
- });
- });
-
- describe('eventing', () => {
- beforeEach(() => {
- loadFixtures('course_experience/fixtures/course-outline-fragment.html');
- outline = new CourseOutline();
- spyOn(Logger, 'log');
- });
-
- it('sends an event when an outline section is clicked', () => {
- document.querySelector(outlineIds.homeworkLabsAndDemos).dispatchEvent(new Event('click'));
-
- expect(Logger.log).toHaveBeenCalledWith('edx.ui.lms.link_clicked', {
- target_url: `${window.location.origin}/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@graded_simulations`,
- current_url: window.location.toString(),
- });
- });
- });
-});
diff --git a/openedx/features/course_experience/static/course_experience/js/spec/Currency_spec.js b/openedx/features/course_experience/static/course_experience/js/spec/Currency_spec.js
index 45e59ee254..f0233eb84a 100644
--- a/openedx/features/course_experience/static/course_experience/js/spec/Currency_spec.js
+++ b/openedx/features/course_experience/static/course_experience/js/spec/Currency_spec.js
@@ -9,6 +9,8 @@ describe('Currency factory', () => {
let usaPosition;
let japanPosition;
+ window.analytics = jasmine.createSpyObj('analytics', ['page', 'track', 'trackLink']);
+
beforeEach(() => {
loadFixtures('course_experience/fixtures/course-currency-fragment.html');
canadaPosition = {
@@ -48,5 +50,10 @@ describe('Currency factory', () => {
currency = new Currency();
expect($('[name="verified_mode"].discount').filter(':visible').text()).toEqual('Pursue a Verified Certificate($198 CAD $220 CAD)');
});
+ it('should send event on initial load', () => {
+ $.cookie('edx-price-l10n', '{"rate":1,"code":"USD","symbol":"$","countryCode":"US"}', { path: '/' });
+ currency = new Currency();
+ expect(window.analytics.track).toHaveBeenCalledWith('edx.bi.user.track_selection.local_currency_cookie_set');
+ });
});
});
diff --git a/openedx/features/course_experience/static/course_experience/js/spec/Enrollment_spec.js b/openedx/features/course_experience/static/course_experience/js/spec/Enrollment_spec.js
deleted file mode 100644
index 6220736b89..0000000000
--- a/openedx/features/course_experience/static/course_experience/js/spec/Enrollment_spec.js
+++ /dev/null
@@ -1,48 +0,0 @@
-/* globals $, loadFixtures */
-
-import {
- expectRequest,
- requests as mockRequests,
- respondWithJson,
- respondWithError,
-} from 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers';
-import { CourseEnrollment } from '../Enrollment';
-
-
-describe('CourseEnrollment tests', () => {
- describe('Ensure button behavior', () => {
- const endpointUrl = '/api/enrollment/v1/enrollment';
- const courseId = 'course-v1:edX+DemoX+Demo_Course';
- const enrollButtonClass = '.enroll-btn';
-
- window.analytics = jasmine.createSpyObj('analytics', ['page', 'track', 'trackLink']);
-
- beforeEach(() => {
- loadFixtures('course_experience/fixtures/enrollment-button.html');
- new CourseEnrollment('.enroll-btn', courseId); // eslint-disable-line no-new
- });
- it('Verify that we reload on success', () => {
- const requests = mockRequests(this);
- $(enrollButtonClass).click();
- expectRequest(
- requests,
- 'POST',
- endpointUrl,
- `{"course_details":{"course_id":"${courseId}"}}`,
- );
- spyOn(CourseEnrollment, 'refresh');
- respondWithJson(requests);
- expect(CourseEnrollment.refresh).toHaveBeenCalled();
- expect(window.analytics.track).toHaveBeenCalled();
- requests.restore();
- });
- it('Verify that we redirect to track selection on fail', () => {
- const requests = mockRequests(this);
- $(enrollButtonClass).click();
- spyOn(CourseEnrollment, 'redirect');
- respondWithError(requests, 403);
- expect(CourseEnrollment.redirect).toHaveBeenCalled();
- requests.restore();
- });
- });
-});
diff --git a/openedx/features/course_experience/static/course_experience/js/spec/LatestUpdate_spec.js b/openedx/features/course_experience/static/course_experience/js/spec/LatestUpdate_spec.js
deleted file mode 100644
index 23e3146541..0000000000
--- a/openedx/features/course_experience/static/course_experience/js/spec/LatestUpdate_spec.js
+++ /dev/null
@@ -1,38 +0,0 @@
-/* globals $, loadFixtures */
-
-import 'jquery.cookie';
-import { LatestUpdate } from '../LatestUpdate';
-
-
-describe('LatestUpdate tests', () => {
- function createLatestUpdate() {
- new LatestUpdate({ messageContainer: '.update-message', dismissButton: '.dismiss-message button' }); // eslint-disable-line no-new
- }
- describe('Test dismiss', () => {
- beforeEach(() => {
- // This causes the cookie to be deleted.
- $.cookie('update-message', '', { expires: -1 });
- loadFixtures('course_experience/fixtures/latest-update-fragment.html');
- });
-
- it('Test dismiss button', () => {
- expect($.cookie('update-message')).toBe(null);
- createLatestUpdate();
- expect($('.update-message').attr('style')).toBe(undefined);
- $('.dismiss-message button').click();
- expect($('.update-message').attr('style')).toBe('display: none;');
- expect($.cookie('update-message')).toBe('hide');
- });
-
- it('Test cookie hides update', () => {
- $.cookie('update-message', 'hide');
- createLatestUpdate();
- expect($('.update-message').attr('style')).toBe('display: none;');
-
- $.cookie('update-message', '', { expires: -1 });
- loadFixtures('course_experience/fixtures/latest-update-fragment.html');
- createLatestUpdate();
- expect($('.update-message').attr('style')).toBe(undefined);
- });
- });
-});
diff --git a/openedx/features/course_experience/static/course_experience/js/spec/WelcomeMessage_spec.js b/openedx/features/course_experience/static/course_experience/js/spec/WelcomeMessage_spec.js
deleted file mode 100644
index daea7536a3..0000000000
--- a/openedx/features/course_experience/static/course_experience/js/spec/WelcomeMessage_spec.js
+++ /dev/null
@@ -1,107 +0,0 @@
-/* globals $, loadFixtures */
-
-import {
- expectRequest,
- requests as mockRequests,
- respondWithJson,
-} from 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers';
-import { WelcomeMessage } from '../WelcomeMessage';
-
-describe('Welcome Message factory', () => {
- describe('Ensure button click', () => {
- const endpointUrl = '/course/course_id/dismiss_message/';
-
- beforeEach(() => {
- loadFixtures('course_experience/fixtures/welcome-message-fragment.html');
- new WelcomeMessage({ dismissUrl: endpointUrl }); // eslint-disable-line no-new
- });
-
- it('When button click is made, ajax call is made and message is hidden.', () => {
- const $message = $('.welcome-message');
- const requests = mockRequests(this);
- document.querySelector('.dismiss-message button').dispatchEvent(new Event('click'));
- expectRequest(
- requests,
- 'POST',
- endpointUrl,
- );
- respondWithJson(requests);
- expect($message.attr('style')).toBe('display: none;');
- requests.restore();
- });
- });
-
- describe('Ensure cookies behave as expected', () => {
- const endpointUrl = '/course/course_id/dismiss_message/';
-
- function deleteAllCookies() {
- const cookies = document.cookie.split(';');
- cookies.forEach((cookie) => {
- const eqPos = cookie.indexOf('=');
- const name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie;
- document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT`;
- });
- }
-
- beforeEach(() => {
- deleteAllCookies();
- });
-
- function createWelcomeMessage() {
- loadFixtures('course_experience/fixtures/welcome-message-fragment.html');
- new WelcomeMessage({ dismissUrl: endpointUrl }); // eslint-disable-line no-new
- }
-
- it('Cookies are created if none exist.', () => {
- createWelcomeMessage();
- expect($.cookie('welcome-message-viewed')).toBe('True');
- expect($.cookie('welcome-message-timer')).toBe('True');
- });
-
- it('Nothing is hidden or dismissed if the timer is still active', () => {
- const $message = $('.welcome-message');
- $.cookie('welcome-message-viewed', 'True');
- $.cookie('welcome-message-timer', 'True');
- createWelcomeMessage();
- expect($message.attr('style')).toBe(undefined);
- });
-
- it('Message is dismissed if the timer has expired and the message has been viewed.', () => {
- const requests = mockRequests(this);
- $.cookie('welcome-message-viewed', 'True');
- createWelcomeMessage();
-
- const $message = $('.welcome-message');
- expectRequest(
- requests,
- 'POST',
- endpointUrl,
- );
- respondWithJson(requests);
- expect($message.attr('style')).toBe('display: none;');
- requests.restore();
- });
- });
-
- describe('Shortened welcome message', () => {
- const endpointUrl = '/course/course_id/dismiss_message/';
-
- beforeEach(() => {
- loadFixtures('course_experience/fixtures/welcome-message-fragment.html');
- new WelcomeMessage({ // eslint-disable-line no-new
- dismissUrl: endpointUrl,
- });
- });
-
- it('Shortened message can be toggled', () => {
- expect($('#welcome-message-content').text()).toContain('…');
- expect($('#welcome-message-show-more').text()).toContain('Show More');
- $('#welcome-message-show-more').click();
- expect($('#welcome-message-content').text()).not.toContain('…');
- expect($('#welcome-message-show-more').text()).toContain('Show Less');
- $('#welcome-message-show-more').click();
- expect($('#welcome-message-content').text()).toContain('…');
- expect($('#welcome-message-show-more').text()).toContain('Show More');
- });
- });
-});
diff --git a/openedx/features/course_experience/templates/course_experience/course-home-fragment.html b/openedx/features/course_experience/templates/course_experience/course-home-fragment.html
deleted file mode 100644
index 1fa8df3826..0000000000
--- a/openedx/features/course_experience/templates/course_experience/course-home-fragment.html
+++ /dev/null
@@ -1,236 +0,0 @@
-## mako
-
-<%page expression_filter="h"/>
-<%namespace name='static' file='../static_content.html'/>
-
-<%!
-import json
-
-from django.conf import settings
-from django.utils.translation import ugettext as _
-from django.template.defaultfilters import escapejs
-from django.urls import reverse
-
-from lms.djangoapps.discussion.django_comment_client.permissions import has_permission
-from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
-from openedx.core.djangolib.markup import Text, HTML
-from openedx.features.course_experience import DISABLE_UNIFIED_COURSE_TAB_FLAG
-from openedx.features.course_experience.course_tools import HttpMethod
-%>
-
-<%block name="header_extras">
-
-%block>
-
-<%block name="content">
-
-
-
-
-%block>
-
-<%static:webpack entry="CourseHome">
- new CourseHome({
- courseRunKey: "${course_key | n, js_escaped_string}",
- resumeCourseLink: ".action-resume-course",
- courseToolLink: ".course-tool-link",
- goalApiUrl: "${goal_api_url | n, js_escaped_string}",
- username: "${username | n, js_escaped_string}",
- courseId: "${course.id | n, js_escaped_string}",
- });
-%static:webpack>
-
-<%static:webpack entry="Enrollment">
- new CourseEnrollment('.enroll-btn', '${course_key | n, js_escaped_string}');
-%static:webpack>
-
-<%static:require_module_async module_name="js/commerce/track_ecommerce_events" class_name="TrackECommerceEvents">
-
- var personalizedLearnerSchedulesLink = $(".personalized_learner_schedules_button");
- var fbeLink = $("#FBE_banner");
- var sockLink = $("#sock");
- var upgradeDateLink = $("#course_home_dates");
- var GreenUpgradeLink = $("#green_upgrade");
- var GreenUpgradeLink = $("#green_upgrade");
- var certificateUpsellLink = $("#certificate_upsell");
-
- TrackECommerceEvents.trackUpsellClick(personalizedLearnerSchedulesLink, 'course_home_upgrade_shift_dates', {
- pageName: "course_home",
- linkType: "button",
- linkCategory: "personalized_learner_schedules"
- });
-
- TrackECommerceEvents.trackUpsellClick(fbeLink, 'course_home_audit_access_expires', {
- pageName: "course_home",
- linkType: "link",
- linkCategory: "FBE_banner"
- });
-
- TrackECommerceEvents.trackUpsellClick(sockLink, 'course_home_sock', {
- pageName: "course_home",
- linkType: "button",
- linkCategory: "green_upgrade"
- });
-
- TrackECommerceEvents.trackUpsellClick(upgradeDateLink, 'course_home_dates', {
- pageName: "course_home",
- linkType: "link",
- linkCategory: "(none)"
- });
-
- TrackECommerceEvents.trackUpsellClick(GreenUpgradeLink, 'course_home_green', {
- pageName: "course_home",
- linkType: "button",
- linkCategory: "green_upgrade"
- });
-
- TrackECommerceEvents.trackUpsellClick(certificateUpsellLink, 'course_home_certificate', {
- pageName: "course_home",
- linkType: "link",
- linkCategory: "(none)"
- });
-
-%static:require_module_async>
diff --git a/openedx/features/course_experience/templates/course_experience/course-messages-fragment.html b/openedx/features/course_experience/templates/course_experience/course-messages-fragment.html
deleted file mode 100644
index 020e08e0ec..0000000000
--- a/openedx/features/course_experience/templates/course_experience/course-messages-fragment.html
+++ /dev/null
@@ -1,40 +0,0 @@
-## mako
-
-<%page expression_filter="h"/>
-<%namespace name='static' file='../static_content.html'/>
-
-<%!
-from django.utils.translation import get_language_bidi
-from django.utils.translation import ugettext as _
-from openedx.core.djangolib.js_utils import js_escaped_string
-from openedx.core.djangolib.markup import HTML
-from openedx.features.course_experience import CourseHomeMessages
-%>
-
-<%
-is_rtl = get_language_bidi()
-%>
-
-% if course_home_messages:
- % for message in course_home_messages:
-
-
-
-
- % if show_search:
-
-
-
-
- % endif
-
- % if resume_course_url:
-
- % if has_visited_course:
- ${_("Resume Course")}
- % else:
- ${_("Start Course")}
- % endif
-
- % endif
-
-
-
- % if course_sock_fragment:
- ${HTML(course_sock_fragment.body_html())}
- % endif
-
- % if course_expiration_fragment:
- ${HTML(course_expiration_fragment.content)}
- % endif
- % if course_home_message_fragment:
- ${HTML(course_home_message_fragment.body_html())}
- % endif
-
- % if update_message_fragment and not DISABLE_UNIFIED_COURSE_TAB_FLAG.is_enabled(course.id):
-
-
-
- ${HTML(update_message_fragment.body_html())}
-
- % endif
-
- % if outline_fragment:
- ${HTML(outline_fragment.body_html())}
- % endif
-
- % if not is_rtl:
-
- % endif
-
- % endif
-
- % endfor
-% endif
-
-<%static:webpack entry="CourseGoals">
- new CourseGoals({
- goalApiUrl: "${goal_api_url | n, js_escaped_string}",
- courseId: "${course_id | n, js_escaped_string}",
- username: "${username | n, js_escaped_string}",
- });
-%static:webpack>
diff --git a/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html b/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html
deleted file mode 100644
index 7d2ce5b158..0000000000
--- a/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html
+++ /dev/null
@@ -1,180 +0,0 @@
-## mako
-
-<%page expression_filter="h"/>
-
-<%namespace name='static' file='../static_content.html'/>
-
-<%!
-import json
-import pytz
-from datetime import date, datetime, timedelta
-
-from django.utils import timezone
-from django.utils.translation import gettext as _
-from django.utils.translation import ngettext
-
-from lms.djangoapps.courseware.access import has_access
-from openedx.core.djangolib.markup import HTML, Text
-from openedx.features.course_experience import RELATIVE_DATES_FLAG
-%>
-
-<%
-course_sections = blocks.get('children')
-self_paced = context.get('self_paced', False)
-relative_dates_flag_is_enabled = RELATIVE_DATES_FLAG.is_enabled(course_key)
-is_course_staff = bool(user and course and has_access(user, 'staff', course, course.id))
-dates_banner_displayed = False
-%>
-
- ${HTML(message.message_html)}
-
- % if is_rtl:
- -
- % for section in course_sections:
- <%
- section_is_auto_opened = section.get('resume_block') is True
- scored = 'scored' if section.get('scored', False) else ''
- %>
-
-
-
-
-
- % for subsection in section.get('children', []):
- <%
- gated_subsection = subsection['id'] in gated_content
- needs_prereqs = not gated_content[subsection['id']]['completed_prereqs'] if gated_subsection else False
- scored = 'scored' if subsection.get('scored', False) else ''
- graded = 'graded' if subsection.get('graded') else ''
- num_graded_problems = subsection.get('num_graded_problems', 0)
- %>
-
-
-
- % if graded and scored and 'special_exam_info' not in subsection:
-
- % endif
-
- ${ subsection['display_name'] } - % if num_graded_problems: - ${ngettext("({number} Question)", - "({number} Questions)", - num_graded_problems).format(number=num_graded_problems)} - % endif -
- % if subsection.get('complete'): - - ${_("Completed")} - % endif - % if needs_prereqs: -- - ${ _("Prerequisite: ") } - <% - prerequisite_id = gated_content[subsection['id']]['prerequisite'] - prerequisite_name = xblock_display_names.get(prerequisite_id) - %> - ${ prerequisite_name } -- % endif -- - ## There are behavior differences between rendering of subsections which have - ## exams (timed, graded, etc) and those that do not. - ## - ## Exam subsections expose exam status message field as well as a status icon - <% - if subsection.get('due') is None or (self_paced and not in_edx_when): - # examples: Homework, Lab, etc. - data_string = subsection.get('format') - data_datetime = "" - else: - if 'special_exam_info' in subsection: - data_string = _('due {date}') - else: - data_string = _("{subsection_format} due {{date}}").format(subsection_format=subsection.get('format')) - data_datetime = subsection.get('due') - %> - % if subsection.get('format') or 'special_exam_info' in subsection: - - % if 'special_exam_info' in subsection: - ## Display the exam status icon and status message - - - ${subsection['special_exam_info'].get('short_description', '')} - - - ## completed exam statuses should not show the due date - ## since the exam has already been submitted by the user - % if not subsection['special_exam_info'].get('in_completed_state', False): - - % endif - % else: - ## non-graded section, we just show the exam format and the due date - ## this is the standard case in edx-platform - - - % if subsection.get('graded'): - ${_("This content is graded")} - % endif - % endif - - % endif -- -
- % endfor
-
- % endfor
- -
-
- % if graded and scored and 'special_exam_info' not in subsection:
-
- % endif
-
-
-%block>
-
-<%static:webpack entry="LatestUpdate">
-new LatestUpdate( { messageContainer: '.update-message', dismissButton: '.dismiss-message button'});
-%static:webpack>
diff --git a/openedx/features/course_experience/templates/course_experience/welcome-message-fragment.html b/openedx/features/course_experience/templates/course_experience/welcome-message-fragment.html
deleted file mode 100644
index 596b5e4377..0000000000
--- a/openedx/features/course_experience/templates/course_experience/welcome-message-fragment.html
+++ /dev/null
@@ -1,41 +0,0 @@
-## mako
-
-<%page expression_filter="h"/>
-<%namespace name='static' file='../static_content.html'/>
-
-<%!
-from django.utils.translation import ugettext as _
-from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
-from openedx.core.djangolib.markup import HTML
-%>
-
-<%block name="content">
-
-
-
- ${_("Latest Update")}
- - ${HTML(update_html)} -
-
-%block>
-
-<%static:webpack entry="WelcomeMessage">
- new WelcomeMessage({
- dismissUrl: "${dismiss_url | n, js_escaped_string}",
- });
-%static:webpack>
diff --git a/openedx/features/course_experience/tests/views/test_course_dates.py b/openedx/features/course_experience/tests/views/test_course_dates.py
deleted file mode 100644
index 404ad6bc63..0000000000
--- a/openedx/features/course_experience/tests/views/test_course_dates.py
+++ /dev/null
@@ -1,48 +0,0 @@
-"""
-Tests for course dates fragment.
-"""
-
-
-from datetime import datetime, timedelta
-
-import six # lint-amnesty, pylint: disable=unused-import
-from django.urls import reverse
-
-from common.djangoapps.student.tests.factories import UserFactory
-from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order
-from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
-from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
-
-TEST_PASSWORD = 'test'
-
-
-class TestCourseDatesFragmentView(ModuleStoreTestCase):
- """Tests for the course dates fragment view."""
-
- def setUp(self):
- super().setUp()
- with self.store.default_store(ModuleStoreEnum.Type.split):
- self.course = CourseFactory.create(
- org='edX',
- number='test',
- display_name='Test Course',
- start=datetime.now() - timedelta(days=30),
- end=datetime.now() + timedelta(days=30),
- )
- self.user = UserFactory(password=TEST_PASSWORD)
- self.client.login(username=self.user.username, password=TEST_PASSWORD)
-
- self.dates_fragment_url = reverse(
- 'openedx.course_experience.mobile_dates_fragment_view',
- kwargs={
- 'course_id': str(self.course.id)
- }
- )
-
- def test_course_dates_fragment(self):
- response = self.client.get(self.dates_fragment_url)
- self.assertContains(response, 'Course ends')
-
- self.client.logout()
- response = self.client.get(self.dates_fragment_url)
- assert response.status_code == 404
diff --git a/openedx/features/course_experience/tests/views/test_course_home.py b/openedx/features/course_experience/tests/views/test_course_home.py
index ab2abc5fef..6de84ebc97 100644
--- a/openedx/features/course_experience/tests/views/test_course_home.py
+++ b/openedx/features/course_experience/tests/views/test_course_home.py
@@ -1,932 +1,16 @@
"""
-Tests for the course home page.
+Tests for the legacy course home page.
"""
-
-from datetime import datetime, timedelta
-from unittest import mock
-from urllib.parse import quote_plus
-
-import ddt
-from django.conf import settings
-from django.http import QueryDict
-from django.test.utils import override_settings
-from django.urls import reverse
-from django.utils.timezone import now
-from edx_toggles.toggles.testutils import override_waffle_flag
-from pytz import UTC
-from waffle.models import Flag
-from waffle.testutils import override_flag
-
-from common.djangoapps.course_modes.models import CourseMode
-from common.djangoapps.course_modes.tests.factories import CourseModeFactory
-from common.djangoapps.student.tests.factories import BetaTesterFactory
-from common.djangoapps.student.tests.factories import GlobalStaffFactory
-from common.djangoapps.student.tests.factories import InstructorFactory
-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.commerce.models import CommerceConfiguration
-from lms.djangoapps.commerce.utils import EcommerceService
-from lms.djangoapps.course_goals.api import add_course_goal_deprecated, get_course_goal
-from lms.djangoapps.course_home_api.toggles import COURSE_HOME_USE_LEGACY_FRONTEND
-from lms.djangoapps.courseware.tests.helpers import get_expiration_banner_text
-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.django_comment_common.models import (
- FORUM_ROLE_ADMINISTRATOR,
- FORUM_ROLE_COMMUNITY_TA,
- FORUM_ROLE_GROUP_MODERATOR,
- FORUM_ROLE_MODERATOR
-)
-from openedx.core.djangoapps.schedules.models import Schedule
-from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
-from openedx.core.djangolib.markup import HTML
-from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
-from openedx.features.course_experience import (
- COURSE_ENABLE_UNENROLLED_ACCESS_FLAG,
- COURSE_PRE_START_ACCESS_FLAG,
- DISABLE_UNIFIED_COURSE_TAB_FLAG,
- ENABLE_COURSE_GOALS,
- SHOW_UPGRADE_MSG_ON_COURSE_HOME
-)
-from openedx.features.course_experience.tests import BaseCourseUpdatesTestCase
-from openedx.features.course_experience.tests.views.helpers import add_course_mode, remove_course_mode
-from common.djangoapps.student.models import CourseEnrollment, FBEEnrollmentExclusion
-from common.djangoapps.student.tests.factories import UserFactory
-from common.djangoapps.util.date_utils import strftime_localized
-from xmodule.course_module import COURSE_VISIBILITY_PRIVATE, COURSE_VISIBILITY_PUBLIC, COURSE_VISIBILITY_PUBLIC_OUTLINE # lint-amnesty, pylint: disable=wrong-import-order
-from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order
-from xmodule.modulestore.tests.django_utils import CourseUserType, ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
-from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls # lint-amnesty, pylint: disable=wrong-import-order
-
-TEST_PASSWORD = 'test'
-TEST_CHAPTER_NAME = 'Test Chapter'
-TEST_COURSE_TOOLS = 'Course Tools'
-TEST_BANNER_CLASS = '
-
-
-
-
- ${HTML(welcome_message_html)}
-
-
-
-'
-TEST_WELCOME_MESSAGE = '
Welcome!
' -TEST_UPDATE_MESSAGE = 'Test Update!
' -TEST_COURSE_UPDATES_TOOL = '/course/updates">' -TEST_COURSE_HOME_MESSAGE = 'course-message' -TEST_COURSE_HOME_MESSAGE_ANONYMOUS = '/login' -TEST_COURSE_HOME_MESSAGE_UNENROLLED = 'Enroll now' -TEST_COURSE_HOME_MESSAGE_PRE_START = 'Course starts in' -TEST_COURSE_GOAL_OPTIONS = 'goal-options-container' -TEST_COURSE_GOAL_UPDATE_FIELD = 'section-goals' -TEST_COURSE_GOAL_UPDATE_FIELD_HIDDEN = 'section-goals hidden' -COURSE_GOAL_DISMISS_OPTION = 'unsure' -THREE_YEARS_AGO = now() - timedelta(days=(365 * 3)) - -QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES +from django.test import TestCase -def course_home_url(course): - """ - Returns the URL for the course's home page. - - Arguments: - course (CourseBlock): The course being tested. - """ - return course_home_url_from_string(str(course.id)) - - -def course_home_url_from_string(course_key_string): - """ - Returns the URL for the course's home page. - - Arguments: - course_key_string (String): The course key as string. - """ - return reverse( - 'openedx.course_experience.course_home', - kwargs={ - 'course_id': course_key_string, - } - ) - - -class CourseHomePageTestCase(BaseCourseUpdatesTestCase): - """ - Base class for testing the course home page. - """ - - @classmethod - def setUpClass(cls): - """ - Set up a course to be used for testing. - """ - # pylint: disable=super-method-not-called - with cls.setUpClassAndTestData(): - with cls.store.default_store(ModuleStoreEnum.Type.split): - cls.course = CourseFactory.create( - org='edX', - number='test', - display_name='Test Course', - start=now() - timedelta(days=30), - metadata={"invitation_only": False} - ) - cls.private_course = CourseFactory.create( - org='edX', - number='test', - display_name='Test Private Course', - start=now() - timedelta(days=30), - metadata={"invitation_only": True} - ) - with cls.store.bulk_operations(cls.course.id): - chapter = ItemFactory.create( - category='chapter', - parent_location=cls.course.location, - display_name=TEST_CHAPTER_NAME, - ) - section = ItemFactory.create(category='sequential', parent_location=chapter.location) - section2 = ItemFactory.create(category='sequential', parent_location=chapter.location) - ItemFactory.create(category='vertical', parent_location=section.location) - ItemFactory.create(category='vertical', parent_location=section2.location) - - @classmethod - def setUpTestData(cls): - """Set up and enroll our fake user in the course.""" - super().setUpTestData() - cls.staff_user = StaffFactory(course_key=cls.course.id, password=TEST_PASSWORD) - - def create_future_course(self, specific_date=None): - """ - Creates and returns a course in the future. - """ - return CourseFactory.create( - display_name='Test Future Course', - start=specific_date if specific_date else now() + timedelta(days=30), - ) - - -class TestCourseHomePage(CourseHomePageTestCase): # lint-amnesty, pylint: disable=missing-class-docstring - @override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True) - def test_welcome_message_when_unified(self): - # Create a welcome message - self.create_course_update(TEST_WELCOME_MESSAGE) - - url = course_home_url(self.course) - response = self.client.get(url) - self.assertContains(response, TEST_WELCOME_MESSAGE, status_code=200) - - @override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True) - @override_waffle_flag(DISABLE_UNIFIED_COURSE_TAB_FLAG, active=True) - def test_welcome_message_when_not_unified(self): - # Create a welcome message - self.create_course_update(TEST_WELCOME_MESSAGE) - - url = course_home_url(self.course) - response = self.client.get(url) - self.assertNotContains(response, TEST_WELCOME_MESSAGE, status_code=200) - - @override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True) - def test_updates_tool_visibility(self): - """ - Verify that the updates course tool is visible only when the course - has one or more updates. - """ - url = course_home_url(self.course) - response = self.client.get(url) - self.assertNotContains(response, TEST_COURSE_UPDATES_TOOL, status_code=200) - - self.create_course_update(TEST_UPDATE_MESSAGE) - url = course_home_url(self.course) - response = self.client.get(url) - self.assertContains(response, TEST_COURSE_UPDATES_TOOL, status_code=200) - - @override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True) - def test_queries(self): - """ - Verify that the view's query count doesn't regress. - """ - CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=UTC)) - # Pre-fetch the view to populate any caches - course_home_url(self.course) - - # Fetch the view and verify the query counts - # TODO: decrease query count as part of REVO-28 - with self.assertNumQueries(66, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST): - with check_mongo_calls(3): - url = course_home_url(self.course) - self.client.get(url) - - @mock.patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False}) - @override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True) - def test_start_date_handling(self): - """ - Verify that the course home page handles start dates correctly. - """ - # The course home page should 404 for a course starting in the future - future_course = self.create_future_course(datetime(2030, 1, 1, tzinfo=UTC)) - url = course_home_url(future_course) - response = self.client.get(url) - self.assertRedirects(response, '/dashboard?notlive=Jan+01%2C+2030') - - # With the Waffle flag enabled, the course should be visible - with override_flag(COURSE_PRE_START_ACCESS_FLAG.name, True): - url = course_home_url(future_course) - response = self.client.get(url) - assert response.status_code == 200 - +class TestCourseHomePage(TestCase): + """Tests for the legacy course home page (the legacy course outline tab)""" def test_legacy_redirect(self): """ Verify that the legacy course home page redirects to the MFE correctly. """ - url = course_home_url(self.course) + '?foo=b$r' - response = self.client.get(url) + response = self.client.get('/courses/course-v1:edX+test+Test_Course/course/?foo=b$r') assert response.status_code == 302 assert response.get('Location') == 'http://learning-mfe/course/course-v1:edX+test+Test_Course/home?foo=b%24r' - - -@ddt.ddt -@override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True) -class TestCourseHomePageAccess(CourseHomePageTestCase): - """ - Test access to the course home page. - """ - - def setUp(self): - super().setUp() - self.client.logout() # start with least access and add access back in the various test cases - - # Make this a verified course so that an upgrade message might be shown - add_course_mode(self.course, mode_slug=CourseMode.AUDIT) - add_course_mode(self.course) - - # Add a welcome message - self.create_course_update(TEST_WELCOME_MESSAGE) - - @ddt.data( - [False, COURSE_VISIBILITY_PRIVATE, CourseUserType.ANONYMOUS, True, False], - [False, COURSE_VISIBILITY_PUBLIC_OUTLINE, CourseUserType.ANONYMOUS, True, False], - [False, COURSE_VISIBILITY_PUBLIC, CourseUserType.ANONYMOUS, True, False], - [True, COURSE_VISIBILITY_PRIVATE, CourseUserType.ANONYMOUS, True, False], - [True, COURSE_VISIBILITY_PUBLIC_OUTLINE, CourseUserType.ANONYMOUS, True, True], - [True, COURSE_VISIBILITY_PUBLIC, CourseUserType.ANONYMOUS, True, True], - - [False, COURSE_VISIBILITY_PRIVATE, CourseUserType.UNENROLLED, True, False], - [False, COURSE_VISIBILITY_PUBLIC_OUTLINE, CourseUserType.UNENROLLED, True, False], - [False, COURSE_VISIBILITY_PUBLIC, CourseUserType.UNENROLLED, True, False], - [True, COURSE_VISIBILITY_PRIVATE, CourseUserType.UNENROLLED, True, False], - [True, COURSE_VISIBILITY_PUBLIC_OUTLINE, CourseUserType.UNENROLLED, True, True], - [True, COURSE_VISIBILITY_PUBLIC, CourseUserType.UNENROLLED, True, True], - - [False, COURSE_VISIBILITY_PRIVATE, CourseUserType.ENROLLED, False, True], - [True, COURSE_VISIBILITY_PRIVATE, CourseUserType.ENROLLED, False, True], - [True, COURSE_VISIBILITY_PUBLIC_OUTLINE, CourseUserType.ENROLLED, False, True], - [True, COURSE_VISIBILITY_PUBLIC, CourseUserType.ENROLLED, False, True], - - [False, COURSE_VISIBILITY_PRIVATE, CourseUserType.UNENROLLED_STAFF, True, True], - [True, COURSE_VISIBILITY_PRIVATE, CourseUserType.UNENROLLED_STAFF, True, True], - [True, COURSE_VISIBILITY_PUBLIC_OUTLINE, CourseUserType.UNENROLLED_STAFF, True, True], - [True, COURSE_VISIBILITY_PUBLIC, CourseUserType.UNENROLLED_STAFF, True, True], - - [False, COURSE_VISIBILITY_PRIVATE, CourseUserType.GLOBAL_STAFF, True, True], - [True, COURSE_VISIBILITY_PRIVATE, CourseUserType.GLOBAL_STAFF, True, True], - [True, COURSE_VISIBILITY_PUBLIC_OUTLINE, CourseUserType.GLOBAL_STAFF, True, True], - [True, COURSE_VISIBILITY_PUBLIC, CourseUserType.GLOBAL_STAFF, True, True], - ) - @ddt.unpack - def test_home_page( - self, enable_unenrolled_access, course_visibility, user_type, - expected_enroll_message, expected_course_outline, - ): - self.create_user_for_course(self.course, user_type) - - # Render the course home page - with mock.patch('xmodule.course_module.CourseBlock.course_visibility', course_visibility): - # Test access with anonymous flag and course visibility - with override_waffle_flag(COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, enable_unenrolled_access): - url = course_home_url(self.course) - response = self.client.get(url) - - private_url = course_home_url(self.private_course) - private_response = self.client.get(private_url) - - is_anonymous = user_type is CourseUserType.ANONYMOUS - is_enrolled = user_type is CourseUserType.ENROLLED - is_enrolled_or_staff = is_enrolled or user_type in ( - CourseUserType.UNENROLLED_STAFF, CourseUserType.GLOBAL_STAFF - ) - - # Verify that the course tools and dates are shown for enrolled users & staff - self.assertContains(response, TEST_COURSE_TOOLS, count=(1 if is_enrolled_or_staff else 0)) - - self.assertContains(response, 'Learn About Verified Certificate', count=(1 if is_enrolled else 0)) - - # Verify that start button, course sock, and welcome message - # are only shown to enrolled users or staff. - self.assertContains(response, 'Start Course', count=(1 if is_enrolled_or_staff else 0)) - self.assertContains(response, TEST_WELCOME_MESSAGE, count=(1 if is_enrolled_or_staff else 0)) - - # Verify the outline is shown to enrolled users, unenrolled_staff and anonymous users if allowed - self.assertContains(response, TEST_CHAPTER_NAME, count=(1 if expected_course_outline else 0)) - - # Verify the message shown to the user - if not enable_unenrolled_access or course_visibility != COURSE_VISIBILITY_PUBLIC: - self.assertContains( - response, 'To see course content', count=(1 if is_anonymous else 0) - ) - self.assertContains(response, 'Enroll now"
-
- # Verify that unenrolled users visiting a course with a Master's track
- # that is not the only track are shown an enroll call to action message
- add_course_mode(self.course, CourseMode.MASTERS, 'Master\'s Mode', upgrade_deadline_expired=False)
- remove_course_mode(self.course, CourseMode.AUDIT)
-
- self.create_user_for_course(self.course, CourseUserType.UNENROLLED)
- url = course_home_url(self.course)
- response = self.client.get(url)
-
- self.assertContains(response, TEST_COURSE_HOME_MESSAGE)
- self.assertContains(response, TEST_COURSE_HOME_MESSAGE_UNENROLLED)
- self.assertContains(response, enroll_button_html)
-
- # Verify that unenrolled users visiting a course that contains only a Master's track
- # are not shown an enroll call to action message
- remove_course_mode(self.course, CourseMode.VERIFIED)
-
- response = self.client.get(url)
-
- expected_message = ('You must be enrolled in the course to see course content. '
- 'Please contact your degree administrator or edX Support if you have questions.')
- self.assertContains(response, TEST_COURSE_HOME_MESSAGE)
- self.assertContains(response, expected_message)
- self.assertNotContains(response, enroll_button_html)
-
- @override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True)
- @override_waffle_flag(COURSE_PRE_START_ACCESS_FLAG, active=True)
- def test_course_messaging(self):
- """
- Ensure that the following four use cases work as expected
-
- 1) Anonymous users are shown a course message linking them to the login page
- 2) Unenrolled users are shown a course message allowing them to enroll
- 3) Enrolled users who show up on the course page after the course has begun
- are not shown a course message.
- 4) Enrolled users who show up on the course page after the course has begun will
- see the course expiration banner if course duration limits are on for the course.
- 5) Enrolled users who show up on the course page before the course begins
- are shown a message explaining when the course starts as well as a call to
- action button that allows them to add a calendar event.
- """
- # Verify that anonymous users are shown a login link in the course message
- url = course_home_url(self.course)
- response = self.client.get(url)
- self.assertContains(response, TEST_COURSE_HOME_MESSAGE)
- self.assertContains(response, TEST_COURSE_HOME_MESSAGE_ANONYMOUS)
-
- # Verify that unenrolled users are shown an enroll call to action message
- user = self.create_user_for_course(self.course, CourseUserType.UNENROLLED)
- url = course_home_url(self.course)
- response = self.client.get(url)
- self.assertContains(response, TEST_COURSE_HOME_MESSAGE)
- self.assertContains(response, TEST_COURSE_HOME_MESSAGE_UNENROLLED)
-
- # Verify that enrolled users are not shown any state warning message when enrolled and course has begun.
- CourseEnrollment.enroll(user, self.course.id)
- url = course_home_url(self.course)
- response = self.client.get(url)
- self.assertNotContains(response, TEST_COURSE_HOME_MESSAGE_ANONYMOUS)
- self.assertNotContains(response, TEST_COURSE_HOME_MESSAGE_UNENROLLED)
- self.assertNotContains(response, TEST_COURSE_HOME_MESSAGE_PRE_START)
-
- # Verify that enrolled users are shown the course expiration banner if content gating is enabled
-
- # We use .save() explicitly here (rather than .objects.create) in order to force the
- # cache to refresh.
- config = CourseDurationLimitConfig(
- course=CourseOverview.get_from_id(self.course.id),
- enabled=True,
- enabled_as_of=datetime(2018, 1, 1, tzinfo=UTC)
- )
- config.save()
-
- url = course_home_url(self.course)
- response = self.client.get(url)
- bannerText = get_expiration_banner_text(user, self.course)
- self.assertContains(response, bannerText, html=True)
-
- # Verify that enrolled users are not shown the course expiration banner if content gating is disabled
- config.enabled = False
- config.save()
- url = course_home_url(self.course)
- response = self.client.get(url)
- bannerText = get_expiration_banner_text(user, self.course)
- self.assertNotContains(response, bannerText, html=True)
-
- # Verify that enrolled users are shown 'days until start' message before start date
- future_course = self.create_future_course()
- CourseEnrollment.enroll(user, future_course.id)
- url = course_home_url(future_course)
- response = self.client.get(url)
- self.assertContains(response, TEST_COURSE_HOME_MESSAGE)
- self.assertContains(response, TEST_COURSE_HOME_MESSAGE_PRE_START)
-
- @override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True)
- def test_course_messaging_for_staff(self):
- """
- Staff users will not see the expiration banner when course duration limits
- are on for the course.
- """
- config = CourseDurationLimitConfig(
- course=CourseOverview.get_from_id(self.course.id),
- enabled=True,
- enabled_as_of=datetime(2018, 1, 1, tzinfo=UTC)
- )
- config.save()
- url = course_home_url(self.course)
- CourseEnrollment.enroll(self.staff_user, self.course.id)
- response = self.client.get(url)
- bannerText = get_expiration_banner_text(self.staff_user, self.course)
- self.assertNotContains(response, bannerText, html=True)
-
- @override_waffle_flag(COURSE_PRE_START_ACCESS_FLAG, active=True)
- @override_waffle_flag(ENABLE_COURSE_GOALS, active=True)
- def test_course_goals(self):
- """
- Ensure that the following five use cases work as expected.
-
- 1) Unenrolled users are not shown the set course goal message.
- 2) Enrolled users are shown the set course goal message if they have not yet set a course goal.
- 3) Enrolled users are not shown the set course goal message if they have set a course goal.
- 4) Enrolled and verified users are not shown the set course goal message.
- 5) Enrolled users are not shown the set course goal message in a course that cannot be verified.
- """
- # Create a course with a verified track.
- verifiable_course = CourseFactory.create()
- add_course_mode(verifiable_course, upgrade_deadline_expired=False)
-
- # Verify that unenrolled users are not shown the set course goal message.
- user = self.create_user_for_course(verifiable_course, CourseUserType.UNENROLLED)
- response = self.client.get(course_home_url(verifiable_course))
- self.assertNotContains(response, TEST_COURSE_GOAL_OPTIONS)
-
- # Verify that enrolled users are shown the set course goal message in a verified course.
- CourseEnrollment.enroll(user, verifiable_course.id)
- response = self.client.get(course_home_url(verifiable_course))
- self.assertContains(response, TEST_COURSE_GOAL_OPTIONS)
-
- # Verify that enrolled users that have set a course goal are not shown the set course goal message.
- add_course_goal_deprecated(user, verifiable_course.id, COURSE_GOAL_DISMISS_OPTION)
- response = self.client.get(course_home_url(verifiable_course))
- self.assertNotContains(response, TEST_COURSE_GOAL_OPTIONS)
-
- # Verify that enrolled and verified users are not shown the set course goal message.
- get_course_goal(user, verifiable_course.id).delete()
- CourseEnrollment.enroll(user, verifiable_course.id, CourseMode.VERIFIED)
- response = self.client.get(course_home_url(verifiable_course))
- self.assertNotContains(response, TEST_COURSE_GOAL_OPTIONS)
-
- # Verify that enrolled users are not shown the set course goal message in an audit only course.
- audit_only_course = CourseFactory.create()
- CourseEnrollment.enroll(user, audit_only_course.id)
- response = self.client.get(course_home_url(audit_only_course))
- self.assertNotContains(response, TEST_COURSE_GOAL_OPTIONS)
-
- @override_waffle_flag(COURSE_PRE_START_ACCESS_FLAG, active=True)
- @override_waffle_flag(ENABLE_COURSE_GOALS, active=True)
- def test_course_goal_updates(self):
- """
- Ensure that the following five use cases work as expected.
-
- 1) Unenrolled users are not shown the update goal selection field.
- 2) Enrolled users are not shown the update goal selection field if they have not yet set a course goal.
- 3) Enrolled users are shown the update goal selection field if they have set a course goal.
- 4) Enrolled users in the verified track are shown the update goal selection field.
- """
- # Create a course with a verified track.
- verifiable_course = CourseFactory.create()
- add_course_mode(verifiable_course, upgrade_deadline_expired=False)
-
- # Verify that unenrolled users are not shown the update goal selection field.
- user = self.create_user_for_course(verifiable_course, CourseUserType.UNENROLLED)
- response = self.client.get(course_home_url(verifiable_course))
- self.assertNotContains(response, TEST_COURSE_GOAL_UPDATE_FIELD)
-
- # Verify that enrolled users that have not set a course goal are shown a hidden update goal selection field.
- enrollment = CourseEnrollment.enroll(user, verifiable_course.id)
- response = self.client.get(course_home_url(verifiable_course))
- self.assertContains(response, TEST_COURSE_GOAL_UPDATE_FIELD_HIDDEN)
-
- # Verify that enrolled users that have set a course goal are shown a visible update goal selection field.
- add_course_goal_deprecated(user, verifiable_course.id, COURSE_GOAL_DISMISS_OPTION)
- response = self.client.get(course_home_url(verifiable_course))
- self.assertContains(response, TEST_COURSE_GOAL_UPDATE_FIELD)
- self.assertNotContains(response, TEST_COURSE_GOAL_UPDATE_FIELD_HIDDEN)
-
- # Verify that enrolled and verified users are shown the update goal selection
- CourseEnrollment.update_enrollment(enrollment, is_active=True, mode=CourseMode.VERIFIED)
- response = self.client.get(course_home_url(verifiable_course))
- self.assertContains(response, TEST_COURSE_GOAL_UPDATE_FIELD)
- self.assertNotContains(response, TEST_COURSE_GOAL_UPDATE_FIELD_HIDDEN)
-
-
-@ddt.ddt
-@override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True)
-class CourseHomeFragmentViewTests(ModuleStoreTestCase):
- """
- Test Messages Displayed on the Course Home
- """
- CREATE_USER = False
-
- def setUp(self):
- super().setUp()
- CommerceConfiguration.objects.create(checkout_on_ecommerce_service=True)
-
- end = now() + timedelta(days=30)
- self.course = CourseFactory(
- start=now() - timedelta(days=30),
- end=end,
- self_paced=True,
- )
- self.url = course_home_url(self.course)
-
- CourseMode.objects.create(course_id=self.course.id, mode_slug=CourseMode.AUDIT) # lint-amnesty, pylint: disable=no-member
- self.verified_mode = CourseMode.objects.create(
- course_id=self.course.id, # lint-amnesty, pylint: disable=no-member
- mode_slug=CourseMode.VERIFIED,
- min_price=100,
- expiration_datetime=end,
- sku='test'
- )
-
- self.user = UserFactory()
- self.client.login(username=self.user.username, password=TEST_PASSWORD)
-
- self.flag, __ = Flag.objects.update_or_create(
- name=SHOW_UPGRADE_MSG_ON_COURSE_HOME.name, defaults={'everyone': True}
- )
-
- def assert_upgrade_message_not_displayed(self):
- response = self.client.get(self.url)
- self.assertNotContains(response, 'section-upgrade')
-
- def assert_upgrade_message_displayed(self): # lint-amnesty, pylint: disable=missing-function-docstring
- response = self.client.get(self.url)
- self.assertContains(response, 'section-upgrade')
- url = EcommerceService().get_checkout_page_url(self.verified_mode.sku)
- self.assertContains(response, '${self.verified_mode.min_price})",
- )
-
- def test_no_upgrade_message_if_logged_out(self):
- self.client.logout()
- self.assert_upgrade_message_not_displayed()
-
- def test_no_upgrade_message_if_not_enrolled(self):
- assert len(CourseEnrollment.enrollments_for_user(self.user)) == 0
- self.assert_upgrade_message_not_displayed()
-
- def test_no_upgrade_message_if_verified_track(self):
- CourseEnrollment.enroll(self.user, self.course.id, CourseMode.VERIFIED) # lint-amnesty, pylint: disable=no-member
- self.assert_upgrade_message_not_displayed()
-
- def test_no_upgrade_message_if_upgrade_deadline_passed(self):
- self.verified_mode.expiration_datetime = now() - timedelta(days=20)
- self.verified_mode.save()
- self.assert_upgrade_message_not_displayed()
-
- def test_no_upgrade_message_if_flag_disabled(self):
- self.flag.everyone = False
- self.flag.save()
- CourseEnrollment.enroll(self.user, self.course.id, CourseMode.AUDIT) # lint-amnesty, pylint: disable=no-member
- self.assert_upgrade_message_not_displayed()
-
- def test_display_upgrade_message_if_audit_and_deadline_not_passed(self):
- CourseEnrollment.enroll(self.user, self.course.id, CourseMode.AUDIT) # lint-amnesty, pylint: disable=no-member
- self.assert_upgrade_message_displayed()
-
- @mock.patch(
- 'openedx.features.course_experience.views.course_home.format_strikeout_price',
- mock.Mock(return_value=(HTML("DISCOUNT_PRICE"), True))
- )
- def test_upgrade_message_discount(self):
- # pylint: disable=no-member
- CourseEnrollment.enroll(self.user, self.course.id, CourseMode.AUDIT)
-
- with override_waffle_flag(SHOW_UPGRADE_MSG_ON_COURSE_HOME, True):
- response = self.client.get(self.url)
-
- self.assertContains(response, "DISCOUNT_PRICE")
diff --git a/openedx/features/course_experience/tests/views/test_course_outline.py b/openedx/features/course_experience/tests/views/test_course_outline.py
deleted file mode 100644
index f952ca0b7b..0000000000
--- a/openedx/features/course_experience/tests/views/test_course_outline.py
+++ /dev/null
@@ -1,767 +0,0 @@
-"""
-Tests for the Course Outline view and supporting views.
-"""
-
-
-import datetime
-import re
-from unittest.mock import Mock, patch
-
-import ddt
-from completion.waffle import ENABLE_COMPLETION_TRACKING_SWITCH
-from completion.models import BlockCompletion
-from completion.test_utils import CompletionWaffleTestMixin
-from django.contrib.sites.models import Site
-from django.test import RequestFactory, override_settings
-from django.urls import reverse
-from django.utils import timezone
-from edx_toggles.toggles.testutils import override_waffle_flag, override_waffle_switch
-from milestones.tests.utils import MilestonesTestCaseMixin
-from opaque_keys.edx.keys import CourseKey, UsageKey
-from pyquery import PyQuery as pq
-from pytz import UTC
-from waffle.models import Switch
-from xmodule.modulestore import ModuleStoreEnum
-from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
-from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
-
-from common.djangoapps.course_modes.models import CourseMode
-from common.djangoapps.course_modes.tests.factories import CourseModeFactory
-from common.djangoapps.student.tests.factories import StaffFactory
-from lms.djangoapps.course_api.blocks.transformers.milestones import MilestonesAndSpecialExamsTransformer
-from lms.djangoapps.courseware.toggles import COURSEWARE_USE_LEGACY_FRONTEND
-from lms.djangoapps.gating import api as lms_gating_api
-from lms.djangoapps.course_home_api.toggles import COURSE_HOME_USE_LEGACY_FRONTEND
-from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin
-from lms.urls import RESET_COURSE_DEADLINES_NAME
-from openedx.core.djangoapps.course_date_signals.models import SelfPacedRelativeDatesConfig
-from openedx.core.djangoapps.schedules.models import Schedule
-from openedx.core.lib.gating import api as gating_api
-from openedx.features.content_type_gating.models import ContentTypeGatingConfig
-from openedx.features.course_experience import RELATIVE_DATES_FLAG
-from openedx.features.course_experience.views.course_outline import (
- DEFAULT_COMPLETION_TRACKING_START,
- CourseOutlineFragmentView
-)
-from common.djangoapps.student.models import CourseEnrollment
-from common.djangoapps.student.tests.factories import UserFactory
-
-from ...utils import get_course_outline_block_tree
-from .test_course_home import course_home_url
-
-TEST_PASSWORD = 'test'
-GATING_NAMESPACE_QUALIFIER = '.gating'
-
-
-@ddt.ddt
-@override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True)
-class TestCourseOutlinePage(SharedModuleStoreTestCase, MasqueradeMixin):
- """
- Test the course outline view.
- """
-
- ENABLED_SIGNALS = ['course_published']
-
- @classmethod
- def setUpClass(cls): # lint-amnesty, pylint: disable=super-method-not-called
- """
- Set up an array of various courses to be tested.
- """
- SelfPacedRelativeDatesConfig.objects.create(enabled=True)
-
- # setUpClassAndTestData() already calls setUpClass on SharedModuleStoreTestCase
- # pylint: disable=super-method-not-called
- with super().setUpClassAndTestData():
- cls.courses = []
- course = CourseFactory.create(self_paced=True, start=timezone.now() - datetime.timedelta(days=1))
- with cls.store.bulk_operations(course.id):
- chapter = ItemFactory.create(category='chapter', parent_location=course.location)
- sequential = ItemFactory.create(category='sequential', parent_location=chapter.location, graded=True,
- format="Homework")
- vertical = ItemFactory.create(category='vertical', parent_location=sequential.location)
- ItemFactory.create(category='problem', parent_location=vertical.location)
- cls.courses.append(cls.store.publish(course.location, ModuleStoreEnum.UserID.test))
-
- course = CourseFactory.create()
- with cls.store.bulk_operations(course.id):
- chapter = ItemFactory.create(category='chapter', parent_location=course.location)
- sequential = ItemFactory.create(category='sequential', parent_location=chapter.location)
- sequential2 = ItemFactory.create(category='sequential', parent_location=chapter.location)
- ItemFactory.create(
- category='vertical',
- parent_location=sequential.location,
- display_name="Vertical 1"
- )
- ItemFactory.create(
- category='vertical',
- parent_location=sequential2.location,
- display_name="Vertical 2"
- )
- cls.courses.append(cls.store.publish(course.location, ModuleStoreEnum.UserID.test))
-
- course = CourseFactory.create()
- with cls.store.bulk_operations(course.id):
- chapter = ItemFactory.create(category='chapter', parent_location=course.location)
- sequential = ItemFactory.create(
- category='sequential',
- parent_location=chapter.location,
- due=datetime.datetime.now(),
- graded=True,
- format='Homework',
- )
- ItemFactory.create(category='vertical', parent_location=sequential.location)
- cls.courses.append(cls.store.publish(course.location, ModuleStoreEnum.UserID.test))
-
- @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(password=TEST_PASSWORD)
- for course in cls.courses:
- CourseEnrollment.enroll(cls.user, course.id)
- Schedule.objects.update(start_date=timezone.now() - datetime.timedelta(days=1))
-
- def setUp(self):
- """
- Set up for the tests.
- """
- super().setUp()
- self.client.login(username=self.user.username, password=TEST_PASSWORD)
-
- @override_waffle_flag(RELATIVE_DATES_FLAG, active=True)
- def test_outline_details(self):
- for course in self.courses:
-
- url = course_home_url(course)
-
- request_factory = RequestFactory()
- request = request_factory.get(url)
- request.user = self.user
-
- course_block_tree = get_course_outline_block_tree(
- request, str(course.id), self.user
- )
-
- response = self.client.get(url)
- assert course.children
- for chapter in course_block_tree['children']:
- self.assertContains(response, chapter['display_name'])
- assert chapter['children']
- for sequential in chapter['children']:
- self.assertContains(response, sequential['display_name'])
- if sequential['graded']:
- print(sequential)
- self.assertContains(response, sequential['due'].strftime('%Y-%m-%d %H:%M:%S'))
- self.assertContains(response, sequential['format'])
- assert sequential['children']
-
- def test_num_graded_problems(self):
- course = CourseFactory.create()
- with self.store.bulk_operations(course.id):
- chapter = ItemFactory.create(category='chapter', parent_location=course.location)
- sequential = ItemFactory.create(category='sequential', parent_location=chapter.location)
- problem = ItemFactory.create(category='problem', parent_location=sequential.location)
- sequential2 = ItemFactory.create(category='sequential', parent_location=chapter.location)
- problem2 = ItemFactory.create(category='problem', graded=True, has_score=True,
- parent_location=sequential2.location)
- sequential3 = ItemFactory.create(category='sequential', parent_location=chapter.location)
- problem3_1 = ItemFactory.create(category='problem', graded=True, has_score=True,
- parent_location=sequential3.location)
- problem3_2 = ItemFactory.create(category='problem', graded=True, has_score=True,
- parent_location=sequential3.location)
- course.children = [chapter]
- chapter.children = [sequential, sequential2, sequential3]
- sequential.children = [problem]
- sequential2.children = [problem2]
- sequential3.children = [problem3_1, problem3_2]
- CourseEnrollment.enroll(self.user, course.id)
-
- url = course_home_url(course)
- response = self.client.get(url)
- content = response.content.decode('utf8')
- self.assertRegex(content, sequential.display_name + r'\s*')
- self.assertRegex(content, sequential2.display_name + r'\s*\(1 Question\)\s*')
- self.assertRegex(content, sequential3.display_name + r'\s*\(2 Questions\)\s*')
-
- @override_waffle_flag(RELATIVE_DATES_FLAG, active=True)
- @ddt.data(
- ([CourseMode.AUDIT, CourseMode.VERIFIED], CourseMode.AUDIT, False, True),
- ([CourseMode.AUDIT, CourseMode.VERIFIED], CourseMode.VERIFIED, False, True),
- ([CourseMode.MASTERS], CourseMode.MASTERS, False, True),
- ([CourseMode.PROFESSIONAL], CourseMode.PROFESSIONAL, True, True), # staff accounts should also see the banner
- )
- @ddt.unpack
- def test_reset_course_deadlines_banner_shows_for_self_paced_course(
- self,
- course_modes,
- enrollment_mode,
- is_course_staff,
- should_display
- ):
- ContentTypeGatingConfig.objects.create(
- enabled=True,
- enabled_as_of=datetime.datetime(2017, 1, 1, tzinfo=UTC),
- )
- course = self.courses[0]
- for mode in course_modes:
- CourseModeFactory.create(course_id=course.id, mode_slug=mode)
-
- enrollment = CourseEnrollment.objects.get(course_id=course.id, user=self.user)
- enrollment.mode = enrollment_mode
- enrollment.save()
- enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=30)
- enrollment.schedule.save()
- self.user.is_staff = is_course_staff
- self.user.save()
-
- url = course_home_url(course)
- response = self.client.get(url)
-
- if should_display:
- self.assertContains(response, '
= 2
- sequential = chapter.children[0]
- sequential2 = chapter.children[1]
- self.complete_sequential(course, sequential)
- self.complete_sequential(course, sequential2)
-
- # remove one of the sequentials from the chapter
- with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, course.id):
- self.store.delete_item(sequential.location, self.user.id)
-
- # check resume course buttons
- response = self.visit_course_home(course, resume_count=1)
-
- content = pq(response.content)
- assert content('.action-resume-course').attr('href').endswith('+type@sequential+block@' + sequential2.url_name)
-
- def test_resume_course_deleted_sequentials(self):
- """
- Tests resume course when the last completed sequential is deleted and
- there are no sequentials left in the vertical.
-
- """
- course = self.create_test_course()
-
- # first navigate to a sequential to make it the last accessed
- chapter = course.children[0]
- assert len(chapter.children) == 2
- sequential = chapter.children[0]
- self.complete_sequential(course, sequential)
-
- # remove all sequentials from chapter
- with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, course.id):
- for sequential in chapter.children:
- self.store.delete_item(sequential.location, self.user.id)
-
- # check resume course buttons
- self.visit_course_home(course, start_count=1, resume_count=0)
-
- def test_course_home_for_global_staff(self):
- """
- Tests that staff user can access the course home without being enrolled
- in the course.
- """
- course = self.course
- self.user.is_staff = True
- self.user.save()
-
- self.override_waffle_switch(True)
- CourseEnrollment.get_enrollment(self.user, course.id).delete()
- response = self.visit_course_home(course, start_count=1, resume_count=0)
- content = pq(response.content)
- problem = course.children[0].children[0].children[0].children[0]
- assert content('.action-resume-course').attr('href').endswith('+type@problem+block@' + problem.url_name)
-
- @override_waffle_switch(ENABLE_COMPLETION_TRACKING_SWITCH, active=True)
- def test_course_outline_auto_open(self):
- """
- Tests that the course outline auto-opens to the first subsection
- in a course if a user has no completion data, and to the
- last-accessed subsection if a user does have completion data.
- """
- def get_sequential_button(url, is_hidden):
- is_hidden_string = "is-hidden" if is_hidden else ""
-
- return ""
- # Course tree
- course = self.course
- chapter1 = course.children[0]
- chapter2 = course.children[1]
-
- response_content = self.client.get(course_home_url(course)).content
- stripped_response = str(re.sub(b"\\s+", b"", response_content), "utf-8")
-
- assert get_sequential_button(str(chapter1.location), False) in stripped_response
- assert get_sequential_button(str(chapter2.location), True) in stripped_response
-
- content = pq(response_content)
- button = content('#expand-collapse-outline-all-button')
- assert 'Expand All' == button.children()[0].text
-
- def test_user_enrolled_after_completion_collection(self):
- """
- Tests that the _completion_data_collection_start() method returns the created
- time of the waffle switch that enables completion data tracking.
- """
- view = CourseOutlineFragmentView()
- switch_name = ENABLE_COMPLETION_TRACKING_SWITCH.name
- switch, _ = Switch.objects.get_or_create(name=switch_name)
-
- # pylint: disable=protected-access
- assert switch.created == view._completion_data_collection_start()
-
- switch.delete()
-
- def test_user_enrolled_after_completion_collection_default(self):
- """
- Tests that the _completion_data_collection_start() method returns a default constant
- when no Switch object exists for completion data tracking.
- """
- view = CourseOutlineFragmentView()
-
- # pylint: disable=protected-access
- assert DEFAULT_COMPLETION_TRACKING_START == view._completion_data_collection_start()
-
-
-@override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True)
-class TestCourseOutlinePreview(SharedModuleStoreTestCase, MasqueradeMixin):
- """
- Unit tests for staff preview of the course outline.
- """
- def test_preview(self):
- """
- Verify the behavior of preview for the course outline.
- """
- course = CourseFactory.create(
- start=datetime.datetime.now() - datetime.timedelta(days=30)
- )
- staff_user = StaffFactory(course_key=course.id, password=TEST_PASSWORD)
- CourseEnrollment.enroll(staff_user, course.id)
-
- future_date = datetime.datetime.now() + datetime.timedelta(days=30)
- with self.store.bulk_operations(course.id):
- chapter = ItemFactory.create(
- category='chapter',
- parent_location=course.location,
- display_name='First Chapter',
- )
- sequential = ItemFactory.create(category='sequential', parent_location=chapter.location)
- ItemFactory.create(category='vertical', parent_location=sequential.location)
- chapter = ItemFactory.create(
- category='chapter',
- parent_location=course.location,
- display_name='Future Chapter',
- start=future_date,
- )
- sequential = ItemFactory.create(category='sequential', parent_location=chapter.location)
- ItemFactory.create(category='vertical', parent_location=sequential.location)
-
- # Verify that a staff user sees a chapter with a due date in the future
- self.client.login(username=staff_user.username, password='test')
- url = course_home_url(course)
- response = self.client.get(url)
- assert response.status_code == 200
- self.assertContains(response, 'Future Chapter')
-
- # Verify that staff masquerading as a learner see the future chapter.
- self.update_masquerade(course=course, role='student')
- response = self.client.get(url)
- assert response.status_code == 200
- self.assertContains(response, 'Future Chapter')
diff --git a/openedx/features/course_experience/tests/views/test_course_sock.py b/openedx/features/course_experience/tests/views/test_course_sock.py
index a37cc02171..6b361f8185 100644
--- a/openedx/features/course_experience/tests/views/test_course_sock.py
+++ b/openedx/features/course_experience/tests/views/test_course_sock.py
@@ -6,11 +6,12 @@ Tests for course verification sock
from unittest import mock
import ddt
+from django.urls import reverse
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.course_home_api.toggles import COURSE_HOME_USE_LEGACY_FRONTEND
+from lms.djangoapps.courseware.toggles import COURSEWARE_USE_LEGACY_FRONTEND
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
@@ -18,14 +19,13 @@ from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase #
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
from .helpers import add_course_mode
-from .test_course_home import course_home_url
TEST_PASSWORD = 'test'
TEST_VERIFICATION_SOCK_LOCATOR = '
DISCOUNT_PRICE"), True))
)
def test_upgrade_message_discount(self):
- response = self.client.get(course_home_url(self.verified_course))
+ response = self.get_courseware(self.verified_course)
self.assertContains(response, "DISCOUNT_PRICE")
def assert_verified_sock_is_visible(self, course, response): # lint-amnesty, pylint: disable=unused-argument
diff --git a/openedx/features/course_experience/tests/views/test_masquerade.py b/openedx/features/course_experience/tests/views/test_masquerade.py
index 6cb06eaf38..b366d5ef2d 100644
--- a/openedx/features/course_experience/tests/views/test_masquerade.py
+++ b/openedx/features/course_experience/tests/views/test_masquerade.py
@@ -2,10 +2,12 @@
Tests for masquerading functionality on course_experience
"""
+from django.urls import reverse
+
from edx_toggles.toggles.testutils import override_waffle_flag
-from lms.djangoapps.course_home_api.toggles import COURSE_HOME_USE_LEGACY_FRONTEND
from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin
-from openedx.features.course_experience import DISPLAY_COURSE_SOCK_FLAG, SHOW_UPGRADE_MSG_ON_COURSE_HOME
+from lms.djangoapps.courseware.toggles import COURSEWARE_USE_LEGACY_FRONTEND
+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
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
@@ -14,11 +16,9 @@ from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID # lint-
from xmodule.partitions.partitions_service import PartitionService # lint-amnesty, pylint: disable=wrong-import-order
from .helpers import add_course_mode
-from .test_course_home import course_home_url
from .test_course_sock import TEST_VERIFICATION_SOCK_LOCATOR
TEST_PASSWORD = 'test'
-UPGRADE_MESSAGE_CONTAINER = 'section-upgrade'
class MasqueradeTestBase(SharedModuleStoreTestCase, MasqueradeMixin):
@@ -67,17 +67,15 @@ class TestVerifiedUpgradesWithMasquerade(MasqueradeTestBase):
Tests for the course verification upgrade messages while the user is being masqueraded.
"""
- @override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True)
+ @override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True)
@override_waffle_flag(DISPLAY_COURSE_SOCK_FLAG, active=True)
- @override_waffle_flag(SHOW_UPGRADE_MSG_ON_COURSE_HOME, active=True)
def test_masquerade_as_student(self):
# Elevate the staff user to be student
self.update_masquerade(course=self.verified_course, user_partition_id=ENROLLMENT_TRACK_PARTITION_ID)
- response = self.client.get(course_home_url(self.verified_course))
+ response = self.client.get(reverse('courseware', kwargs={'course_id': str(self.verified_course.id)}))
self.assertContains(response, TEST_VERIFICATION_SOCK_LOCATOR, html=False)
- self.assertContains(response, UPGRADE_MESSAGE_CONTAINER, html=False)
- @override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True)
+ @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(
@@ -86,11 +84,10 @@ class TestVerifiedUpgradesWithMasquerade(MasqueradeTestBase):
)
self.update_masquerade(course=self.verified_course, group_id=user_group_id,
user_partition_id=ENROLLMENT_TRACK_PARTITION_ID)
- response = self.client.get(course_home_url(self.verified_course))
+ response = self.client.get(reverse('courseware', kwargs={'course_id': str(self.verified_course.id)}))
self.assertNotContains(response, TEST_VERIFICATION_SOCK_LOCATOR, html=False)
- self.assertNotContains(response, UPGRADE_MESSAGE_CONTAINER, html=False)
- @override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True)
+ @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(
@@ -99,6 +96,6 @@ class TestVerifiedUpgradesWithMasquerade(MasqueradeTestBase):
)
self.update_masquerade(course=self.masters_course, group_id=user_group_id,
user_partition_id=ENROLLMENT_TRACK_PARTITION_ID)
- response = self.client.get(course_home_url(self.masters_course))
+ response = self.client.get(reverse('courseware', kwargs={'course_id': str(self.masters_course.id)}))
+
self.assertNotContains(response, TEST_VERIFICATION_SOCK_LOCATOR, html=False)
- self.assertNotContains(response, UPGRADE_MESSAGE_CONTAINER, html=False)
diff --git a/openedx/features/course_experience/tests/views/test_welcome_message.py b/openedx/features/course_experience/tests/views/test_welcome_message.py
deleted file mode 100644
index e94d332230..0000000000
--- a/openedx/features/course_experience/tests/views/test_welcome_message.py
+++ /dev/null
@@ -1,80 +0,0 @@
-"""
-Tests for course welcome messages.
-"""
-
-import ddt
-from django.urls import reverse
-
-from openedx.features.course_experience.tests import BaseCourseUpdatesTestCase
-
-
-def welcome_message_url(course):
- """
- Returns the URL for the welcome message view.
- """
- return reverse(
- 'openedx.course_experience.welcome_message_fragment_view',
- kwargs={
- 'course_id': str(course.id),
- }
- )
-
-
-def latest_update_url(course):
- """
- Returns the URL for the latest update view.
- """
- return reverse(
- 'openedx.course_experience.latest_update_fragment_view',
- kwargs={
- 'course_id': str(course.id),
- }
- )
-
-
-def dismiss_message_url(course):
- """
- Returns the URL for the dismiss message endpoint.
- """
- return reverse(
- 'openedx.course_experience.dismiss_welcome_message',
- kwargs={
- 'course_id': str(course.id),
- }
- )
-
-
-@ddt.ddt
-class TestWelcomeMessageView(BaseCourseUpdatesTestCase):
- """
- Tests for the course welcome message fragment view.
-
- Also tests the LatestUpdate view because the functionality is similar.
- """
- @ddt.data(welcome_message_url, latest_update_url)
- def test_message_display(self, url_generator):
- self.create_course_update('First Update', date='January 1, 2000')
- self.create_course_update('Second Update', date='January 1, 2017')
- self.create_course_update('Retroactive Update', date='January 1, 2010')
- response = self.client.get(url_generator(self.course))
- assert response.status_code == 200
- self.assertContains(response, 'Second Update')
- self.assertContains(response, 'Dismiss')
-
- @ddt.data(welcome_message_url, latest_update_url)
- def test_empty_message(self, url_generator):
- response = self.client.get(url_generator(self.course))
- assert response.status_code == 204
-
- def test_dismiss_welcome_message(self):
- # Latest update is dimssed in JS and has no server/backend component.
- self.create_course_update('First Update')
-
- response = self.client.get(welcome_message_url(self.course))
- assert response.status_code == 200
- self.assertContains(response, 'First Update')
-
- self.client.post(dismiss_message_url(self.course))
- response = self.client.get(welcome_message_url(self.course))
- assert 'First Update' not in response
- assert response.status_code == 204
diff --git a/openedx/features/course_experience/urls.py b/openedx/features/course_experience/urls.py
index 5d81a6b819..ac560d43f9 100644
--- a/openedx/features/course_experience/urls.py
+++ b/openedx/features/course_experience/urls.py
@@ -3,42 +3,10 @@ Defines URLs for the course experience.
"""
from django.urls import path
-from .views.course_dates import CourseDatesFragmentMobileView
-from .views.course_home import CourseHomeFragmentView, CourseHomeView
-from .views.course_outline import CourseOutlineFragmentView
-from .views.course_updates import CourseUpdatesFragmentView, CourseUpdatesView
-from .views.latest_update import LatestUpdateFragmentView
-from .views.welcome_message import WelcomeMessageFragmentView, dismiss_welcome_message
-
-COURSE_HOME_VIEW_NAME = 'openedx.course_experience.course_home'
-COURSE_DATES_FRAGMENT_VIEW_NAME = 'openedx.course_experience.mobile_dates_fragment_view'
+from .views.course_home import outline_tab
+from .views.course_updates import CourseUpdatesView
urlpatterns = [
- path('', CourseHomeView.as_view(),
- name=COURSE_HOME_VIEW_NAME,
- ),
- path('updates', CourseUpdatesView.as_view(),
- name='openedx.course_experience.course_updates',
- ),
- path('home_fragment', CourseHomeFragmentView.as_view(),
- name='openedx.course_experience.course_home_fragment_view',
- ),
- path('outline_fragment', CourseOutlineFragmentView.as_view(),
- name='openedx.course_experience.course_outline_fragment_view',
- ),
- path('updates_fragment', CourseUpdatesFragmentView.as_view(),
- name='openedx.course_experience.course_updates_fragment_view',
- ),
- path('welcome_message_fragment', WelcomeMessageFragmentView.as_view(),
- name='openedx.course_experience.welcome_message_fragment_view',
- ),
- path('latest_update_fragment', LatestUpdateFragmentView.as_view(),
- name='openedx.course_experience.latest_update_fragment_view',
- ),
- path('dismiss_welcome_message', dismiss_welcome_message,
- name='openedx.course_experience.dismiss_welcome_message',
- ),
- path('mobile_dates_fragment', CourseDatesFragmentMobileView.as_view(),
- name=COURSE_DATES_FRAGMENT_VIEW_NAME,
- ),
+ path('', outline_tab), # a now-removed legacy view, redirects to MFE
+ path('updates', CourseUpdatesView.as_view(), name='openedx.course_experience.course_updates'),
]
diff --git a/openedx/features/course_experience/utils.py b/openedx/features/course_experience/utils.py
index 4c370e1f29..451d769eea 100644
--- a/openedx/features/course_experience/utils.py
+++ b/openedx/features/course_experience/utils.py
@@ -128,23 +128,6 @@ def get_course_outline_block_tree(request, course_id, user=None, allow_start_dat
return course_outline_root_block
-def get_resume_block(block):
- """
- Gets the deepest block marked as 'resume_block'.
-
- """
- if block.get('authorization_denial_reason') or not block.get('resume_block'):
- return None
- if not block.get('children'):
- return block
-
- for child in block['children']:
- resume_block = get_resume_block(child)
- if resume_block:
- return resume_block
- return block
-
-
def get_start_block(block):
"""
Gets the deepest block to use as the starting block.
diff --git a/openedx/features/course_experience/views/course_dates.py b/openedx/features/course_experience/views/course_dates.py
index 4557217d48..e9f1a8d574 100644
--- a/openedx/features/course_experience/views/course_dates.py
+++ b/openedx/features/course_experience/views/course_dates.py
@@ -3,11 +3,7 @@ Fragment for rendering the course dates sidebar.
"""
-from django.db import transaction
-from django.http import Http404
from django.template.loader import render_to_string
-from django.utils.decorators import method_decorator
-from django.utils.translation import get_language_bidi
from opaque_keys.edx.keys import CourseKey
from web_fragments.fragment import Fragment
@@ -44,35 +40,3 @@ class CourseDatesFragmentView(EdxFragmentView):
self.add_fragment_resource_urls(dates_fragment)
return dates_fragment
-
-
-@method_decorator(transaction.non_atomic_requests, name='dispatch')
-class CourseDatesFragmentMobileView(CourseDatesFragmentView):
- """
- A course dates fragment to show dates on mobile apps.
-
- Mobile apps uses WebKit mobile client to create and maintain a session with
- the server for authenticated requests, and it hasn't exposed any way to find
- out either session was created with the server or not so mobile app uses a
- mechanism to automatically create/recreate session with the server for all
- authenticated requests if the server returns 404.
- """
- template_name = 'course_experience/mobile/course-dates-fragment.html'
-
- def get(self, request, *args, **kwargs):
- if not request.user.is_authenticated:
- raise Http404
- return super().get(request, *args, **kwargs)
-
- def css_dependencies(self):
- """
- Returns list of CSS files that this view depends on.
-
- The helper function that it uses to obtain the list of CSS files
- works in conjunction with the Django pipeline to ensure that in development mode
- the files are loaded individually, but in production just the single bundle is loaded.
- """
- if get_language_bidi():
- return self.get_css_dependencies('style-mobile-rtl')
- else:
- return self.get_css_dependencies('style-mobile')
diff --git a/openedx/features/course_experience/views/course_home.py b/openedx/features/course_experience/views/course_home.py
index e488936d7b..7a3233fdb4 100644
--- a/openedx/features/course_experience/views/course_home.py
+++ b/openedx/features/course_experience/views/course_home.py
@@ -2,243 +2,13 @@
Views for the course home page.
"""
+from django.shortcuts import redirect
-from django.conf import settings
-from django.template.context_processors import csrf
-from django.template.loader import render_to_string
-from django.urls import reverse
-from django.utils.decorators import method_decorator
-from django.views.decorators.cache import cache_control
-from django.views.decorators.csrf import ensure_csrf_cookie
-from opaque_keys.edx.keys import CourseKey
-from web_fragments.fragment import Fragment
-
-from lms.djangoapps.course_home_api.toggles import course_home_legacy_is_active
-from lms.djangoapps.courseware.access import has_access
-from lms.djangoapps.courseware.courses import can_self_enroll_in_course, get_course_info_section, get_course_with_access
-from lms.djangoapps.course_goals.api import (
- get_course_goal,
- get_course_goal_options,
- get_goal_api_url,
- has_course_goal_permission
-)
-from lms.djangoapps.courseware.exceptions import CourseAccessRedirect, Redirect
-from lms.djangoapps.courseware.utils import can_show_verified_upgrade, verified_upgrade_deadline_link
-from lms.djangoapps.courseware.views.views import CourseTabView
-from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
-from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
-from openedx.core.djangoapps.util.maintenance_banner import add_maintenance_banner
-from openedx.features.course_duration_limits.access import generate_course_expired_fragment
-from openedx.features.course_experience import (
- COURSE_ENABLE_UNENROLLED_ACCESS_FLAG,
- LATEST_UPDATE_FLAG,
- SHOW_UPGRADE_MSG_ON_COURSE_HOME,
-)
-from openedx.features.course_experience.course_tools import CourseToolsPluginManager
-from openedx.features.course_experience.url_helpers import get_learning_mfe_home_url
-from openedx.features.course_experience.utils import get_course_outline_block_tree, get_resume_block, get_start_block
-from openedx.features.course_experience.views.course_dates import CourseDatesFragmentView
-from openedx.features.course_experience.views.course_home_messages import CourseHomeMessageFragmentView
-from openedx.features.course_experience.views.course_outline import CourseOutlineFragmentView
-from openedx.features.course_experience.views.course_sock import CourseSockFragmentView
-from openedx.features.course_experience.views.latest_update import LatestUpdateFragmentView
-from openedx.features.course_experience.views.welcome_message import WelcomeMessageFragmentView
-from openedx.features.discounts.utils import format_strikeout_price
-from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.util.views import ensure_valid_course_key
-from xmodule.course_module import COURSE_VISIBILITY_PUBLIC, COURSE_VISIBILITY_PUBLIC_OUTLINE # lint-amnesty, pylint: disable=wrong-import-order
-
-EMPTY_HANDOUTS_HTML = '
')
- )
-
- # Add the dismissible option for users that are unsure of their goal
- goal_choices_html += Text(
- '{initial_tag}{choice}{closing_tag}'
- ).format(
- initial_tag=HTML(
- '
')
- ),
- title=Text(_('Welcome to {course_display_name}')).format(
- course_display_name=course.display_name
- )
- )
diff --git a/openedx/features/course_experience/views/course_outline.py b/openedx/features/course_experience/views/course_outline.py
deleted file mode 100644
index bb9444d7be..0000000000
--- a/openedx/features/course_experience/views/course_outline.py
+++ /dev/null
@@ -1,165 +0,0 @@
-"""
-Views to show a course outline.
-"""
-
-
-import datetime
-import re
-
-from completion.waffle import ENABLE_COMPLETION_TRACKING_SWITCH
-from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
-from django.template.context_processors import csrf
-from django.template.loader import render_to_string
-from django.urls import reverse
-import edx_when.api as edx_when_api
-from opaque_keys.edx.keys import CourseKey
-from pytz import UTC
-from waffle.models import Switch
-from web_fragments.fragment import Fragment
-from lms.djangoapps.courseware.courses import get_course_overview_with_access
-from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link
-from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
-from openedx.features.course_experience.utils import dates_banner_should_display
-from common.djangoapps.student.models import CourseEnrollment
-from common.djangoapps.util.milestones_helpers import get_course_content_milestones
-from xmodule.course_module import COURSE_VISIBILITY_PUBLIC # lint-amnesty, pylint: disable=wrong-import-order
-from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
-
-from ..utils import get_course_outline_block_tree, get_resume_block
-
-DEFAULT_COMPLETION_TRACKING_START = datetime.datetime(2018, 1, 24, tzinfo=UTC)
-
-
-class CourseOutlineFragmentView(EdxFragmentView):
- """
- Course outline fragment to be shown in the unified course view.
- """
-
- def render_to_fragment(self, request, course_id, user_is_enrolled=True, **kwargs): # pylint: disable=arguments-differ
- """
- Renders the course outline as a fragment.
- """
- from lms.urls import RESET_COURSE_DEADLINES_NAME
-
- course_key = CourseKey.from_string(course_id)
- course_overview = get_course_overview_with_access(
- request.user, 'load', course_key, check_if_enrolled=user_is_enrolled
- )
- course = modulestore().get_course(course_key)
-
- course_block_tree = get_course_outline_block_tree(
- request, course_id, request.user if user_is_enrolled else None
- )
- if not course_block_tree:
- return None
-
- resume_block = get_resume_block(course_block_tree) if user_is_enrolled else None
-
- if not resume_block:
- self.mark_first_unit_to_resume(course_block_tree)
-
- xblock_display_names = self.create_xblock_id_and_name_dict(course_block_tree)
- gated_content = self.get_content_milestones(request, course_key)
-
- missed_deadlines, missed_gated_content = dates_banner_should_display(course_key, request.user)
-
- reset_deadlines_url = reverse(RESET_COURSE_DEADLINES_NAME)
-
- context = {
- 'csrf': csrf(request)['csrf_token'],
- 'course': course_overview,
- 'due_date_display_format': course.due_date_display_format,
- 'blocks': course_block_tree,
- 'enable_links': user_is_enrolled or course.course_visibility == COURSE_VISIBILITY_PUBLIC,
- 'course_key': course_key,
- 'gated_content': gated_content,
- 'xblock_display_names': xblock_display_names,
- 'self_paced': course.self_paced,
-
- # We're using this flag to prevent old self-paced dates from leaking out on courses not
- # managed by edx-when.
- 'in_edx_when': edx_when_api.is_enabled_for_course(course_key),
- 'reset_deadlines_url': reset_deadlines_url,
- 'verified_upgrade_link': verified_upgrade_deadline_link(request.user, course=course),
- 'on_course_outline_page': True,
- 'missed_deadlines': missed_deadlines,
- 'missed_gated_content': missed_gated_content,
- 'has_ended': course.has_ended(),
- }
-
- html = render_to_string('course_experience/course-outline-fragment.html', context)
- return Fragment(html)
-
- def create_xblock_id_and_name_dict(self, course_block_tree, xblock_display_names=None):
- """
- Creates a dictionary mapping xblock IDs to their names, using a course block tree.
- """
- if xblock_display_names is None:
- xblock_display_names = {}
-
- if not course_block_tree.get('authorization_denial_reason'):
- if course_block_tree.get('id'):
- xblock_display_names[course_block_tree['id']] = course_block_tree['display_name']
-
- if course_block_tree.get('children'):
- for child in course_block_tree['children']:
- self.create_xblock_id_and_name_dict(child, xblock_display_names)
-
- return xblock_display_names
-
- def get_content_milestones(self, request, course_key):
- """
- Returns dict of subsections with prerequisites and whether the prerequisite has been completed or not
- """
- def _get_key_of_prerequisite(namespace):
- return re.sub('.gating', '', namespace)
-
- all_course_milestones = get_course_content_milestones(course_key)
-
- uncompleted_prereqs = {
- milestone['content_id']
- for milestone in get_course_content_milestones(course_key, user_id=request.user.id)
- }
-
- gated_content = {
- milestone['content_id']: {
- 'completed_prereqs': milestone['content_id'] not in uncompleted_prereqs,
- 'prerequisite': _get_key_of_prerequisite(milestone['namespace'])
- }
- for milestone in all_course_milestones
- }
-
- return gated_content
-
- def user_enrolled_after_completion_collection(self, user, course_key):
- """
- Checks that the user has enrolled in the course after 01/24/2018, the date that
- the completion API began data collection. If the user has enrolled in the course
- before this date, they may see incomplete collection data. This is a temporary
- check until all active enrollments are created after the date.
- """
- user = User.objects.get(username=user)
- try:
- user_enrollment = CourseEnrollment.objects.get(
- user=user,
- course_id=course_key,
- is_active=True
- )
- return user_enrollment.created > self._completion_data_collection_start()
- except CourseEnrollment.DoesNotExist:
- return False
-
- def _completion_data_collection_start(self):
- """
- Returns the date that the ENABLE_COMPLETION_TRACKING waffle switch was enabled.
- """
- try:
- return Switch.objects.get(name=ENABLE_COMPLETION_TRACKING_SWITCH.name).created
- except Switch.DoesNotExist:
- return DEFAULT_COMPLETION_TRACKING_START
-
- def mark_first_unit_to_resume(self, block_node):
- children = block_node.get('children')
- if children:
- children[0]['resume_block'] = True
- self.mark_first_unit_to_resume(children[0])
diff --git a/openedx/features/course_experience/views/course_updates.py b/openedx/features/course_experience/views/course_updates.py
index f0acff801b..2e16cbc250 100644
--- a/openedx/features/course_experience/views/course_updates.py
+++ b/openedx/features/course_experience/views/course_updates.py
@@ -2,11 +2,9 @@
Views that handle course updates.
"""
-import six # lint-amnesty, pylint: disable=unused-import
from django.contrib.auth.decorators import login_required
from django.template.context_processors import csrf
from django.template.loader import render_to_string
-from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_control
from opaque_keys.edx.keys import CourseKey
@@ -15,7 +13,7 @@ from web_fragments.fragment import Fragment
from lms.djangoapps.courseware.courses import get_course_info_section_module, get_course_with_access
from lms.djangoapps.courseware.views.views import CourseTabView
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
-from openedx.features.course_experience import default_course_url_name
+from openedx.features.course_experience import default_course_url
from openedx.features.course_experience.course_updates import get_ordered_updates
@@ -48,8 +46,7 @@ class CourseUpdatesFragmentView(EdxFragmentView):
"""
course_key = CourseKey.from_string(course_id)
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
- course_url_name = default_course_url_name(course.id)
- course_url = reverse(course_url_name, kwargs={'course_id': str(course.id)})
+ course_url = default_course_url(course.id)
ordered_updates = get_ordered_updates(request, course)
plain_html_updates = ''
diff --git a/openedx/features/course_experience/views/latest_update.py b/openedx/features/course_experience/views/latest_update.py
deleted file mode 100644
index 1da34faff6..0000000000
--- a/openedx/features/course_experience/views/latest_update.py
+++ /dev/null
@@ -1,50 +0,0 @@
-"""
-View logic for handling latest course updates.
-
-Although the welcome message fragment also displays the latest update,
-this fragment dismisses the message for a limited time so new updates
-will continue to appear, where the welcome message gets permanently
-dismissed.
-"""
-
-
-from django.template.loader import render_to_string
-from opaque_keys.edx.keys import CourseKey
-from web_fragments.fragment import Fragment
-
-from lms.djangoapps.courseware.courses import get_course_with_access
-from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
-from openedx.features.course_experience.course_updates import get_current_update_for_user
-
-
-class LatestUpdateFragmentView(EdxFragmentView):
- """
- A fragment that displays the latest course update.
- """
-
- def render_to_fragment(self, request, course_id=None, **kwargs): # lint-amnesty, pylint: disable=arguments-differ
- """
- Renders the latest update message fragment for the specified course.
-
- Returns: A fragment, or None if there is no latest update message.
- """
- course_key = CourseKey.from_string(course_id)
- course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
-
- update_html = self.latest_update_html(request, course)
- if not update_html:
- return None
-
- context = {
- 'update_html': update_html,
- }
- html = render_to_string('course_experience/latest-update-fragment.html', context)
- return Fragment(html)
-
- @classmethod
- def latest_update_html(cls, request, course):
- """
- Returns the course's latest update message or None if it doesn't have one.
- """
- # Return the course update with the most recent publish date
- return get_current_update_for_user(request, course)
diff --git a/openedx/features/course_experience/views/welcome_message.py b/openedx/features/course_experience/views/welcome_message.py
deleted file mode 100644
index 410d3425a3..0000000000
--- a/openedx/features/course_experience/views/welcome_message.py
+++ /dev/null
@@ -1,67 +0,0 @@
-""" # lint-amnesty, pylint: disable=cyclic-import
-View logic for handling course welcome messages.
-"""
-
-
-import six # lint-amnesty, pylint: disable=unused-import
-from django.http import HttpResponse
-from django.template.loader import render_to_string
-from django.urls import reverse
-from django.views.decorators.csrf import ensure_csrf_cookie
-from opaque_keys.edx.keys import CourseKey
-from web_fragments.fragment import Fragment
-
-from lms.djangoapps.courseware.courses import get_course_with_access
-from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
-from openedx.features.course_experience.course_updates import (
- dismiss_current_update_for_user, get_current_update_for_user,
-)
-
-
-class WelcomeMessageFragmentView(EdxFragmentView):
- """
- A fragment that displays a course's welcome message.
- """
-
- def render_to_fragment(self, request, course_id=None, **kwargs): # lint-amnesty, pylint: disable=arguments-differ
- """
- Renders the welcome message fragment for the specified course.
-
- Returns: A fragment, or None if there is no welcome message.
- """
- course_key = CourseKey.from_string(course_id)
- course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
- welcome_message_html = self.welcome_message_html(request, course)
- if not welcome_message_html:
- return None
-
- dismiss_url = reverse(
- 'openedx.course_experience.dismiss_welcome_message', kwargs={'course_id': str(course_key)}
- )
-
- context = {
- 'dismiss_url': dismiss_url,
- 'welcome_message_html': welcome_message_html,
- }
-
- html = render_to_string('course_experience/welcome-message-fragment.html', context)
- return Fragment(html)
-
- @classmethod
- def welcome_message_html(cls, request, course):
- """
- Returns the course's welcome message or None if it doesn't have one.
- """
- # Return the course update with the most recent publish date
- return get_current_update_for_user(request, course)
-
-
-@ensure_csrf_cookie
-def dismiss_welcome_message(request, course_id):
- """
- Given the course_id in the request, disable displaying the welcome message for the user.
- """
- course_key = CourseKey.from_string(course_id)
- course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
- dismiss_current_update_for_user(request, course)
- return HttpResponse()
diff --git a/openedx/features/course_search/__init__.py b/openedx/features/course_search/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/openedx/features/course_search/templates/course_search/course-search-fragment.html b/openedx/features/course_search/templates/course_search/course-search-fragment.html
deleted file mode 100644
index 87f01b7493..0000000000
--- a/openedx/features/course_search/templates/course_search/course-search-fragment.html
+++ /dev/null
@@ -1,73 +0,0 @@
-## mako
-
-<%page expression_filter="h"/>
-<%namespace name='static' file='../static_content.html'/>
-
-<%!
-import json
-import waffle
-
-from django.conf import settings
-from django.utils.translation import ugettext as _
-from django.template.defaultfilters import escapejs
-from django.urls import reverse
-
-from lms.djangoapps.discussion.django_comment_client.permissions import has_permission
-from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
-from openedx.core.djangolib.markup import HTML
-from openedx.features.course_experience import course_home_page_title
-%>
-
-<%block name="content">
-'
- ).format(
- goal_key=GOAL_KEY_CHOICES.unsure,
- aria_label_choice=Text(_("Set goal to: {choice}")).format(
- choice=course_goal_options[GOAL_KEY_CHOICES.unsure],
- ),
- ),
- choice=Text(_('{choice}')).format(
- choice=course_goal_options[GOAL_KEY_CHOICES.unsure],
- ),
- closing_tag=HTML('
'),
- )
-
- # Add the option to set a goal to earn a certificate,
- # complete the course or explore the course
- course_goals_by_commitment_level = valid_course_goals_ordered()
- for goal in course_goals_by_commitment_level:
- goal_key, goal_text = goal
- goal_choices_html += HTML(
- '{initial_tag}{goal_text}{closing_tag}'
- ).format(
- initial_tag=HTML(
- '')
- )
-
- CourseHomeMessages.register_info_message(
- request,
- HTML('{goal_choices_html}{closing_tag}').format(
- goal_choices_html=goal_choices_html,
- closing_tag=HTML('
-
-
-
-%block>
-
-<%block name="js_extra">
-<%static:require_module module_name="course_search/js/course_search_factory" class_name="CourseSearchFactory">
- var courseId = '${course_key | n, js_escaped_string}';
- CourseSearchFactory({
- courseId: courseId,
- searchHeader: $('.page-header-search'),
- supportsActive: false,
- query: '${query | n, js_escaped_string}'
- });
-%static:require_module>
-%block>
diff --git a/openedx/features/course_search/urls.py b/openedx/features/course_search/urls.py
deleted file mode 100644
index b923c240cb..0000000000
--- a/openedx/features/course_search/urls.py
+++ /dev/null
@@ -1,15 +0,0 @@
-"""
-Defines URLs for course search.
-"""
-
-from django.urls import path
-from .views.course_search import CourseSearchFragmentView, CourseSearchView
-
-urlpatterns = [
- path('', CourseSearchView.as_view(),
- name='openedx.course_search.course_search_results',
- ),
- path('home_fragment', CourseSearchFragmentView.as_view(),
- name='openedx.course_search.course_search_results_fragment_view',
- ),
-]
diff --git a/openedx/features/course_search/views/__init__.py b/openedx/features/course_search/views/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/openedx/features/course_search/views/course_search.py b/openedx/features/course_search/views/course_search.py
deleted file mode 100644
index 369a711b0f..0000000000
--- a/openedx/features/course_search/views/course_search.py
+++ /dev/null
@@ -1,68 +0,0 @@
-"""
-Views for the course search page.
-"""
-
-
-from django.contrib.auth.decorators import login_required
-from django.template.context_processors import csrf
-from django.template.loader import render_to_string
-from django.urls import reverse
-from django.utils.decorators import method_decorator
-from django.views.decorators.cache import cache_control
-from django.views.decorators.csrf import ensure_csrf_cookie
-from opaque_keys.edx.keys import CourseKey
-from web_fragments.fragment import Fragment
-
-from lms.djangoapps.courseware.courses import get_course_overview_with_access
-from lms.djangoapps.courseware.views.views import CourseTabView
-from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
-from openedx.features.course_experience import default_course_url_name
-from common.djangoapps.util.views import ensure_valid_course_key
-
-
-class CourseSearchView(CourseTabView):
- """
- The home page for a course.
- """
- @method_decorator(login_required)
- @method_decorator(ensure_csrf_cookie)
- @method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True))
- @method_decorator(ensure_valid_course_key)
- def get(self, request, course_id, **kwargs): # lint-amnesty, pylint: disable=arguments-differ
- """
- Displays the home page for the specified course.
- """
- return super().get(request, course_id, 'courseware', **kwargs)
-
- def render_to_fragment(self, request, course=None, tab=None, **kwargs): # lint-amnesty, pylint: disable=arguments-differ, unused-argument
- course_id = str(course.id)
- home_fragment_view = CourseSearchFragmentView()
- return home_fragment_view.render_to_fragment(request, course_id=course_id, **kwargs)
-
-
-class CourseSearchFragmentView(EdxFragmentView):
- """
- A fragment to render the home page for a course.
- """
-
- def render_to_fragment(self, request, course_id=None, **kwargs): # lint-amnesty, pylint: disable=arguments-differ
- """
- Renders the course's home page as a fragment.
- """
- course_key = CourseKey.from_string(course_id)
- course = get_course_overview_with_access(request.user, 'load', course_key, check_if_enrolled=True)
- course_url_name = default_course_url_name(course.id)
- course_url = reverse(course_url_name, kwargs={'course_id': str(course.id)})
-
- # Render the course home fragment
- context = {
- 'csrf': csrf(request)['csrf_token'],
- 'course': course,
- 'course_key': course_key,
- 'course_url': course_url,
- 'query': request.GET.get('query', ''),
- 'disable_courseware_js': True,
- 'uses_bootstrap': True,
- }
- html = render_to_string('course_search/course-search-fragment.html', context)
- return Fragment(html)
diff --git a/openedx/features/enterprise_support/tests/test_utils.py b/openedx/features/enterprise_support/tests/test_utils.py
index c524b9bc21..ec271de137 100644
--- a/openedx/features/enterprise_support/tests/test_utils.py
+++ b/openedx/features/enterprise_support/tests/test_utils.py
@@ -14,13 +14,12 @@ from django.conf import settings
from django.contrib.sites.models import Site
from django.test import TestCase
from django.test.utils import override_settings
-from django.urls import NoReverseMatch, reverse
+from django.urls import NoReverseMatch
from edx_toggles.toggles.testutils import override_waffle_flag, override_waffle_switch
from opaque_keys.edx.keys import CourseKey, UsageKey
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.tests.factories import UserFactory
-from lms.djangoapps.course_home_api.toggles import COURSE_HOME_USE_LEGACY_FRONTEND
from openedx.core.djangolib.testing.utils import skip_unless_lms
from openedx.features.enterprise_support.tests import FEATURES_WITH_ENTERPRISE_ENABLED
from openedx.features.enterprise_support.tests.factories import (
@@ -616,39 +615,6 @@ class TestCourseAccessed(SharedModuleStoreTestCase, CompletionWaffleTestMixin):
completion=completion
)
- def course_home_url(self, course):
- """
- Returns the URL for the course's home page.
-
- Arguments:
- course (CourseBlock): The course being tested.
- """
- return self.course_home_url_from_string(str(course.id))
-
- def course_home_url_from_string(self, course_key_string):
- """
- Returns the URL for the course's home page.
-
- Arguments:
- course_key_string (String): The course key as string.
- """
- return reverse(
- 'openedx.course_experience.course_home',
- kwargs={
- 'course_id': course_key_string,
- }
- )
-
- @override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True)
- def test_course_accessed_for_visit_course_home(self):
- """
- Test that a visit to course home does not fall under course access
- """
- response = self.client.get(self.course_home_url(self.course))
- assert response.status_code == 200
- course_accessed = is_course_accessed(self.user, str(self.course.id))
- self.assertFalse(course_accessed)
-
@override_settings(LMS_BASE='test_url:9999')
def test_course_accessed_with_completion_api(self):
"""
diff --git a/openedx/features/enterprise_support/utils.py b/openedx/features/enterprise_support/utils.py
index bf1b7bd6e7..018ee7967f 100644
--- a/openedx/features/enterprise_support/utils.py
+++ b/openedx/features/enterprise_support/utils.py
@@ -5,12 +5,11 @@ Utility methods for Enterprise
import json
+from completion.exceptions import UnavailableCompletionData
+from completion.utilities import get_key_to_last_completed_block
from crum import get_current_request
from django.conf import settings
-from django.contrib.auth import get_backends, login
-from django.contrib.sessions.middleware import SessionMiddleware
from django.core.cache import cache
-from django.http import HttpRequest
from django.urls import NoReverseMatch, reverse
from django.utils.translation import gettext as _
from edx_django_utils.cache import TieredCache, get_cache_key
@@ -25,7 +24,6 @@ from lms.djangoapps.branding.api import get_privacy_url
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.user_authn.cookies import standard_cookie_settings
from openedx.core.djangolib.markup import HTML, Text
-from openedx.features.course_experience.utils import get_course_outline_block_tree, get_resume_block
ENTERPRISE_HEADER_LINKS = LegacyWaffleFlag('enterprise', 'enterprise_header_links', __name__) # lint-amnesty, pylint: disable=toggle-missing-annotation
@@ -471,29 +469,6 @@ def fetch_enterprise_customer_by_id(enterprise_uuid):
return EnterpriseCustomer.objects.get(uuid=enterprise_uuid)
-def _create_placeholder_request(user):
- """
- Helper method to create a placeholder request.
-
- Arguments:
- user (User): Django User object.
-
- Returns:
- request (HttpRequest): A placeholder request object.
- """
- request = HttpRequest()
- middleware = SessionMiddleware()
- middleware.process_request(request)
- request.session.save()
- backend = get_backends()[0]
- user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__)
- login(request, user)
- request.user = user
- request.META['SERVER_NAME'] = 'edx.org'
- request.META['SERVER_PORT'] = '8080'
- return request
-
-
def is_course_accessed(user, course_id):
"""
Check if the learner accessed the course.
@@ -505,7 +480,8 @@ def is_course_accessed(user, course_id):
Returns:
(bool): True if course has been accessed by the enterprise learner.
"""
- request = _create_placeholder_request(user)
- course_outline_root_block = get_course_outline_block_tree(request, course_id, user)
- resume_block = get_resume_block(course_outline_root_block) if course_outline_root_block else None
- return bool(resume_block)
+ try:
+ get_key_to_last_completed_block(user, course_id)
+ return True
+ except UnavailableCompletionData:
+ return False
diff --git a/webpack.common.config.js b/webpack.common.config.js
index 0f76269a88..427528cff1 100644
--- a/webpack.common.config.js
+++ b/webpack.common.config.js
@@ -107,14 +107,8 @@ module.exports = Merge.smart({
CompletionOnViewService: './lms/static/completion/js/CompletionOnViewService.js',
// Features
- CourseGoals: './openedx/features/course_experience/static/course_experience/js/CourseGoals.js',
- CourseHome: './openedx/features/course_experience/static/course_experience/js/CourseHome.js',
- CourseOutline: './openedx/features/course_experience/static/course_experience/js/CourseOutline.js',
CourseSock: './openedx/features/course_experience/static/course_experience/js/CourseSock.js',
Currency: './openedx/features/course_experience/static/course_experience/js/currency.js',
- Enrollment: './openedx/features/course_experience/static/course_experience/js/Enrollment.js',
- LatestUpdate: './openedx/features/course_experience/static/course_experience/js/LatestUpdate.js',
- WelcomeMessage: './openedx/features/course_experience/static/course_experience/js/WelcomeMessage.js',
AnnouncementsView: './openedx/features/announcements/static/announcements/jsx/Announcements.jsx',
CookiePolicyBanner: './common/static/js/src/CookiePolicyBanner.jsx',
-
-
-
-
-
-
-
-
-
-
-
-