diff --git a/lms/djangoapps/courseware/tests/test_course_survey.py b/lms/djangoapps/courseware/tests/test_course_survey.py index 98b5842cd2..25dc37724d 100644 --- a/lms/djangoapps/courseware/tests/test_course_survey.py +++ b/lms/djangoapps/courseware/tests/test_course_survey.py @@ -16,6 +16,7 @@ from common.test.utils import XssTestMixin from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase from lms.djangoapps.survey.models import SurveyAnswer, SurveyForm from openedx.features.course_experience import course_home_url +from openedx.features.course_experience.url_helpers import make_learning_mfe_courseware_url class SurveyViewsTests(LoginEnrollmentTestCase, SharedModuleStoreTestCase, XssTestMixin): @@ -89,26 +90,44 @@ class SurveyViewsTests(LoginEnrollmentTestCase, SharedModuleStoreTestCase, XssTe reverse('course_survey', kwargs={'course_id': str(course.id)}) ) - def _assert_no_redirect(self, course): + def _assert_no_survey_redirect(self, course): """ - Helper method to asswer that all known conditionally redirect points do - not redirect as expected + Helper method to assert that all known conditionally redirecting endpoints do + not redirect to the survey as expected """ - for view_name in ['courseware', 'progress']: - resp = self.client.get( - reverse( - view_name, - kwargs={'course_id': str(course.id)} - ) + + # Make sure we get to the progress page. + resp = self.client.get( + reverse( + 'progress', + kwargs={'course_id': str(course.id)} ) - assert resp.status_code == 200 + ) + assert resp.status_code == 200 + + # Make sure we are redirected to the MFE for courseware + resp = self.client.get( + reverse( + 'courseware', + kwargs={'course_id': str(course.id)} + ) + ) + assert resp.status_code == 302 + expected_redirect_url = make_learning_mfe_courseware_url( + course.id, + None, + None, + params=None, + preview=False + ) + assert resp.url == expected_redirect_url def test_visiting_course_without_survey(self): """ Verifies that going to the courseware which does not have a survey does not redirect to a survey """ - self._assert_no_redirect(self.course_without_survey) + self._assert_no_survey_redirect(self.course_without_survey) def test_visiting_course_with_survey_redirects(self): """ @@ -143,7 +162,7 @@ class SurveyViewsTests(LoginEnrollmentTestCase, SharedModuleStoreTestCase, XssTe ) assert resp.status_code == 200 - self._assert_no_redirect(self.course) + self._assert_no_survey_redirect(self.course) def test_course_id_field(self): """ @@ -180,7 +199,7 @@ class SurveyViewsTests(LoginEnrollmentTestCase, SharedModuleStoreTestCase, XssTe ) assert resp.status_code == 200 - self._assert_no_redirect(self.course) + self._assert_no_survey_redirect(self.course) # however we want to make sure we persist the course_id answer_objs = SurveyAnswer.objects.filter( @@ -195,7 +214,7 @@ class SurveyViewsTests(LoginEnrollmentTestCase, SharedModuleStoreTestCase, XssTe """ Verifies that going to the courseware with a required, but non-existing survey, does not redirect """ - self._assert_no_redirect(self.course_with_bogus_survey) + self._assert_no_survey_redirect(self.course_with_bogus_survey) def test_visiting_survey_with_bogus_survey_name(self): """ diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index da5b178136..4b59bfac24 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -316,6 +316,58 @@ class BaseViewsTestCase(ModuleStoreTestCase, MasqueradeMixin): assert self.client.login(username=self.global_staff.username, password=TEST_PASSWORD) +@ddt.ddt +class CoursewareIndexTestCase(BaseViewsTestCase): + """ + Tests for the courseware index view, used for instructor previews. + """ + def setUp(self): + super().setUp() + self._create_global_staff_user() # this view needs staff permission + + def test_course_redirect(self): + lms_url = reverse( + 'courseware', + kwargs={ + 'course_id': str(self.course_key), + } + ) + + mfe_url = make_learning_mfe_courseware_url(self.course.id) + + response = self.client.get(lms_url) + assert response.url == mfe_url + + def test_section_redirect(self): + lms_url = reverse( + 'courseware_section', + kwargs={ + 'course_id': str(self.course_key), + 'section': str(self.chapter.location.block_id), + } + ) + + mfe_url = make_learning_mfe_courseware_url(self.course.id) + + response = self.client.get(lms_url) + assert response.url == mfe_url + + def test_subsection_redirect(self): + lms_url = reverse( + 'courseware_subsection', + kwargs={ + 'course_id': str(self.course_key), + 'section': str(self.chapter.location.block_id), + 'subsection': str(self.section2.location.block_id), + } + ) + + mfe_url = make_learning_mfe_courseware_url(self.course.id, self.section2.location) + + response = self.client.get(lms_url) + assert response.url == mfe_url + + @ddt.ddt class ViewsTestCase(BaseViewsTestCase): """ diff --git a/lms/djangoapps/courseware/views/index.py b/lms/djangoapps/courseware/views/index.py index 02f0ef1f7f..e36d43b75b 100644 --- a/lms/djangoapps/courseware/views/index.py +++ b/lms/djangoapps/courseware/views/index.py @@ -6,72 +6,32 @@ View for Courseware Index import logging -import urllib -from django.conf import settings from django.contrib.auth.views import redirect_to_login -from django.db import transaction -from django.http import Http404 -from django.template.context_processors import csrf -from django.urls import reverse from django.utils.decorators import method_decorator -from django.utils.functional import cached_property -from django.utils.translation import gettext as _ from django.views.decorators.cache import cache_control from django.views.decorators.csrf import ensure_csrf_cookie +from django.utils.functional import cached_property from django.views.generic import View -from edx_django_utils.monitoring import set_custom_attributes_for_course_key + from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey, UsageKey -from web_fragments.fragment import Fragment -from xmodule.course_block import COURSE_VISIBILITY_PUBLIC from xmodule.modulestore.django import modulestore -from xmodule.x_module import PUBLIC_VIEW, STUDENT_VIEW -from common.djangoapps.edxmako.shortcuts import render_to_response, render_to_string -from common.djangoapps.student.models import CourseEnrollment from common.djangoapps.util.views import ensure_valid_course_key -from lms.djangoapps.courseware.exceptions import CourseAccessRedirect, Redirect -from lms.djangoapps.experiments.utils import get_experiment_user_metadata_context -from lms.djangoapps.gating.api import get_entrance_exam_score, get_entrance_exam_usage_key -from lms.djangoapps.grades.api import CourseGradeFactory -from openedx.core.djangoapps.content.course_overviews.models import CourseOverview -from openedx.core.djangoapps.crawlers.models import CrawlersConfig -from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY -from openedx.core.djangoapps.user_api.preferences.api import get_user_preference -from openedx.core.djangoapps.util.user_messages import PageLevelMessages -from openedx.core.djangolib.markup import HTML, Text -from openedx.features.course_experience import ( - COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, - DISABLE_COURSE_OUTLINE_PAGE_FLAG, - default_course_url -) +from lms.djangoapps.courseware.exceptions import Redirect +from lms.djangoapps.courseware.masquerade import setup_masquerade from openedx.features.course_experience.url_helpers import make_learning_mfe_courseware_url +from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG from openedx.features.enterprise_support.api import data_sharing_consent_required -from ..access import has_access -from ..access_utils import check_public_access -from ..courses import get_course_with_access, get_current_child, get_studio_url -from ..entrance_exams import ( - course_has_entrance_exam, - get_entrance_exam_content, - user_can_skip_entrance_exam, - user_has_passed_entrance_exam -) -from ..masquerade import check_content_start_date_for_masquerade_user, setup_masquerade -from ..model_data import FieldDataCache -from ..block_render import get_block_for_descriptor, toc_for_course +from ..block_render import get_block_for_descriptor +from ..courses import get_course_with_access from ..permissions import MASQUERADE_AS_STUDENT -from ..toggles import ENABLE_OPTIMIZELY_IN_COURSEWARE -from .views import CourseTabView log = logging.getLogger("edx.courseware.views.index") -TEMPLATE_IMPORTS = {'urllib': urllib} -CONTENT_DEPTH = 2 - -@method_decorator(transaction.non_atomic_requests, name='dispatch') class CoursewareIndex(View): """ View class for the Courseware page. @@ -87,15 +47,9 @@ class CoursewareIndex(View): @method_decorator(data_sharing_consent_required) def get(self, request, course_id, chapter=None, section=None, position=None): """ - Displays courseware accordion and associated content. If course, chapter, - and section are all specified, renders the page, or returns an error if they - are invalid. - - If section is not specified, displays the accordion opened to the right - chapter. - - If neither chapter or section are specified, displays the user's most - recent chapter, or the first chapter if this is the user's first visit. + Instead of loading the legacy courseware sequences pages, load the equivalent URL + in the learning MFE. This view does not do any auth checks since they are done by + the MFE when attempting to load content. Arguments: request: HTTP request @@ -103,442 +57,63 @@ class CoursewareIndex(View): chapter (unicode): chapter url_name section (unicode): section url_name position (unicode): position in block, eg of block + """ + self.course_key = CourseKey.from_string(course_id) if not (request.user.is_authenticated or self.enable_unenrolled_access): return redirect_to_login(request.get_full_path()) - self.original_chapter_url_name = chapter - self.original_section_url_name = section - self.chapter_url_name = chapter - self.section_url_name = section - self.position = position - self.chapter, self.section = None, None - self.course = None - self.url = request.path + # Course load to resolve chapters/sections + with modulestore().bulk_operations(self.course_key): + course = get_course_with_access( + request.user, + "load", + self.course_key, + depth=2, + check_if_enrolled=True, + check_if_authenticated=True, + ) + + # Get the chapter, section and unit blocks so that we can redirect to the right content + # location in the MFE + section_location = None + if chapter and section: + chapter_block = course.get_child_by(lambda m: m.location.block_id == chapter) + if chapter_block: + section_block = chapter_block.get_child_by(lambda m: m.location.block_id == section) + if section_block: + section_location = section_block.location try: - set_custom_attributes_for_course_key(self.course_key) - self._clean_position() - with modulestore().bulk_operations(self.course_key): - - self.view = STUDENT_VIEW - - self.course = get_course_with_access( - request.user, 'load', self.course_key, - depth=CONTENT_DEPTH, - check_if_enrolled=True, - check_if_authenticated=True - ) - self.course_overview = CourseOverview.get_from_id(self.course.id) - self.is_staff = has_access(request.user, 'staff', self.course) - - # There's only one situation where we want to show the public view - if ( - not self.is_staff and - self.enable_unenrolled_access and - self.course.course_visibility == COURSE_VISIBILITY_PUBLIC and - not CourseEnrollment.is_enrolled(request.user, self.course_key) - ): - self.view = PUBLIC_VIEW - - self.can_masquerade = request.user.has_perm(MASQUERADE_AS_STUDENT, self.course) - self._setup_masquerade_for_effective_user() - - return self.render(request) - except Exception as exception: # pylint: disable=broad-except - return CourseTabView.handle_exceptions(request, self.course_key, self.course, exception) - - def _setup_masquerade_for_effective_user(self): - """ - Setup the masquerade information to allow the request to - be processed for the requested effective user. - """ - self.real_user = self.request.user - self.masquerade, self.effective_user = setup_masquerade( - self.request, - self.course_key, - self.can_masquerade, - reset_masquerade_data=True - ) - # Set the user in the request to the effective user. - self.request.user = self.effective_user - - def _redirect_to_learning_mfe(self): - """ - Can the user access this sequence in the courseware MFE? If so, redirect to MFE. - """ - # If the MFE is active, prefer that - raise Redirect(self.microfrontend_url) - - @property - def microfrontend_url(self): - """ - Return absolute URL to this section in the courseware micro-frontend. - """ - try: - unit_key = UsageKey.from_string(self.request.GET.get('activate_block_id', '')) - # `activate_block_id` is typically a Unit (a.k.a. Vertical), - # but it can technically be any block type. Do a check to - # make sure it's really a Unit before we use it for the MFE. + unit_key = UsageKey.from_string(request.GET.get('activate_block_id', '')) if unit_key.block_type != 'vertical': unit_key = None except InvalidKeyError: unit_key = None - is_preview = False - url = make_learning_mfe_courseware_url( + + # Setup masquerading if needed for this user. + # This in needed even though this view just does a redirect because + # the relevant cookies and session data is set for future requests + # when this function is called. + self.masquerade, self.effective_user = setup_masquerade( + self.request, self.course_key, - self.section.location if self.section else None, + request.user.has_perm(MASQUERADE_AS_STUDENT, course), + reset_masquerade_data=True + ) + # Set the user in the request to the effective user. + self.request.user = self.effective_user + mfe_url = make_learning_mfe_courseware_url( + self.course_key, + section_location, unit_key, - params=self.request.GET, - preview=is_preview, + params=request.GET, + preview=False ) - return url + raise Redirect(mfe_url) - def render(self, request): - """ - Render the index page. - """ - self._prefetch_and_bind_course(request) - - if self.course.has_children_at_depth(CONTENT_DEPTH): - self._reset_section_to_exam_if_required() - self.chapter = self._find_chapter() - self.section = self._find_section() - - if self.chapter and self.section: - self._redirect_if_not_requested_section() - self._save_positions() - self._prefetch_and_bind_section() - self._redirect_to_learning_mfe() - - check_content_start_date_for_masquerade_user(self.course_key, self.effective_user, request, - self.course.start, self.chapter.start, self.section.start) - - if not request.user.is_authenticated: - qs = urllib.parse.urlencode({ - 'course_id': self.course_key, - 'enrollment_action': 'enroll', - 'email_opt_in': False, - }) - - allow_anonymous = check_public_access(self.course, [COURSE_VISIBILITY_PUBLIC]) - - if not allow_anonymous: - PageLevelMessages.register_warning_message( - request, - Text(_("You are not signed in. To see additional course content, {sign_in_link} or " - "{register_link}, and enroll in this course.")).format( - sign_in_link=HTML('{sign_in_label}').format( - sign_in_label=_('sign in'), - url='{}?{}'.format(reverse('signin_user'), qs), - ), - register_link=HTML('{register_label}').format( - register_label=_('register'), - url='{}?{}'.format(reverse('register_user'), qs), - ), - ) - ) - - return render_to_response('courseware/courseware.html', self._create_courseware_context(request)) - - def _redirect_if_not_requested_section(self): - """ - If the resulting section and chapter are different from what was initially - requested, redirect back to the index page, but with an updated URL that includes - the correct section and chapter values. We do this so that our analytics events - and error logs have the appropriate URLs. - """ - if ( - self.chapter.url_name != self.original_chapter_url_name or - (self.original_section_url_name and self.section.url_name != self.original_section_url_name) - ): - raise CourseAccessRedirect( - reverse( - 'courseware_section', - kwargs={ - 'course_id': str(self.course_key), - 'chapter': self.chapter.url_name, - 'section': self.section.url_name, - }, - ) - ) - - def _clean_position(self): - """ - Verify that the given position is an integer. If it is not positive, set it to 1. - """ - if self.position is not None: - try: - self.position = max(int(self.position), 1) - except ValueError: - raise Http404(f"Position {self.position} is not an integer!") # lint-amnesty, pylint: disable=raise-missing-from - - def _reset_section_to_exam_if_required(self): - """ - Check to see if an Entrance Exam is required for the user. - """ - if not user_can_skip_entrance_exam(self.effective_user, self.course): - exam_chapter = get_entrance_exam_content(self.effective_user, self.course) - if exam_chapter and exam_chapter.get_children(): - exam_section = exam_chapter.get_children()[0] - if exam_section: - self.chapter_url_name = exam_chapter.url_name - self.section_url_name = exam_section.url_name - - def _get_language_preference(self): - """ - Returns the preferred language for the actual user making the request. - """ - language_preference = settings.LANGUAGE_CODE - - if self.request.user.is_authenticated: - language_preference = get_user_preference(self.real_user, LANGUAGE_KEY) - - return language_preference - - def _is_masquerading_as_student(self): - """ - Returns whether the current request is masquerading as a student. - """ - return self.masquerade and self.masquerade.role == 'student' - - def _is_masquerading_as_specific_student(self): - """ - Returns whether the current request is masqueurading as a specific student. - """ - return self._is_masquerading_as_student() and self.masquerade.user_name - - def _find_block(self, parent, url_name, block_type, min_depth=None): - """ - Finds the block in the parent with the specified url_name. - If not found, calls get_current_child on the parent. - """ - child = None - if url_name: - child = parent.get_child_by(lambda m: m.location.block_id == url_name) - if not child: - # User may be trying to access a child that isn't live yet - if not self._is_masquerading_as_student(): - raise Http404('No {block_type} found with name {url_name}'.format( - block_type=block_type, - url_name=url_name, - )) - elif min_depth and not child.has_children_at_depth(min_depth - 1): - child = None - if not child: - child = get_current_child(parent, min_depth=min_depth, requested_child=self.request.GET.get("child")) - return child - - def _find_chapter(self): - """ - Finds the requested chapter. - """ - return self._find_block(self.course, self.chapter_url_name, 'chapter', CONTENT_DEPTH - 1) - - def _find_section(self): - """ - Finds the requested section. - """ - if self.chapter: - return self._find_block(self.chapter, self.section_url_name, 'section') - - def _prefetch_and_bind_course(self, request): - """ - Prefetches all descendant data for the requested section and - sets up the runtime, which binds the request user to the section. - """ - self.field_data_cache = FieldDataCache.cache_for_block_descendents( - self.course_key, - self.effective_user, - self.course, - depth=CONTENT_DEPTH, - read_only=CrawlersConfig.is_crawler(request), - ) - - self.course = get_block_for_descriptor( - self.effective_user, - self.request, - self.course, - self.field_data_cache, - self.course_key, - course=self.course, - will_recheck_access=True, - ) - - def _prefetch_and_bind_section(self): - """ - Prefetches all descendant data for the requested section and - sets up the runtime, which binds the request user to the section. - """ - # Pre-fetch all descendant data - self.section = modulestore().get_item(self.section.location, depth=None, lazy=False) - self.field_data_cache.add_block_descendents(self.section, depth=None) - - # Bind section to user - self.section = get_block_for_descriptor( - self.effective_user, - self.request, - self.section, - self.field_data_cache, - self.course_key, - self.position, - course=self.course, - will_recheck_access=True, - ) - - def _save_positions(self): - """ - Save where we are in the course and chapter. - """ - save_child_position(self.course, self.chapter_url_name) - save_child_position(self.chapter, self.section_url_name) - - def _create_courseware_context(self, request): - """ - Returns and creates the rendering context for the courseware. - Also returns the table of contents for the courseware. - """ - - course_url = default_course_url(self.course.id) - show_search = ( - settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH') or - (settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH_FOR_COURSE_STAFF') and self.is_staff) - ) - staff_access = self.is_staff - - courseware_context = { - 'csrf': csrf(self.request)['csrf_token'], - 'course': self.course, - 'course_url': course_url, - 'chapter': self.chapter, - 'section': self.section, - 'init': '', - 'fragment': Fragment(), - 'staff_access': staff_access, - 'can_masquerade': self.can_masquerade, - 'masquerade': self.masquerade, - 'supports_preview_menu': True, - 'studio_url': get_studio_url(self.course, 'course'), - 'xqa_server': settings.FEATURES.get('XQA_SERVER', "http://your_xqa_server.com"), - 'bookmarks_api_url': reverse('bookmarks'), - 'language_preference': self._get_language_preference(), - 'disable_optimizely': not ENABLE_OPTIMIZELY_IN_COURSEWARE.is_enabled(), - 'section_title': None, - 'sequence_title': None, - 'disable_accordion': not DISABLE_COURSE_OUTLINE_PAGE_FLAG.is_enabled(self.course.id), - 'show_search': show_search, - 'render_course_wide_assets': True, - } - courseware_context.update( - get_experiment_user_metadata_context( - self.course, - self.effective_user, - ) - ) - table_of_contents = toc_for_course( - self.effective_user, - self.request, - self.course, - self.chapter_url_name, - self.section_url_name, - self.field_data_cache, - ) - courseware_context['accordion'] = render_accordion( - self.request, - self.course, - table_of_contents['chapters'], - ) - - # entrance exam data - self._add_entrance_exam_to_context(courseware_context) - - if self.section: - # chromeless data - if self.section.chrome: - chrome = [s.strip() for s in self.section.chrome.lower().split(",")] - if 'accordion' not in chrome: - courseware_context['disable_accordion'] = True - if 'tabs' not in chrome: - courseware_context['disable_tabs'] = True - - # default tab - if self.section.default_tab: - courseware_context['default_tab'] = self.section.default_tab - - # section data - courseware_context['section_title'] = self.section.display_name_with_default - section_context = self._create_section_context( - table_of_contents['previous_of_active_section'], - table_of_contents['next_of_active_section'], - ) - courseware_context['fragment'] = self.section.render(self.view, section_context) - - if self.section.position and self.section.has_children: - self._add_sequence_title_to_context(courseware_context) - - return courseware_context - - def _add_sequence_title_to_context(self, courseware_context): - """ - Adds sequence title to the given context. - - If we're rendering a section with some display items, but position - exceeds the length of the displayable items, default the position - to the first element. - """ - display_items = self.section.get_children() - if not display_items: - return - if self.section.position > len(display_items): - self.section.position = 1 - courseware_context['sequence_title'] = display_items[self.section.position - 1].display_name_with_default - - def _add_entrance_exam_to_context(self, courseware_context): - """ - Adds entrance exam related information to the given context. - """ - if course_has_entrance_exam(self.course) and getattr(self.chapter, 'is_entrance_exam', False): - courseware_context['entrance_exam_passed'] = user_has_passed_entrance_exam(self.effective_user, self.course) - courseware_context['entrance_exam_current_score'] = get_entrance_exam_score( - CourseGradeFactory().read(self.effective_user, self.course), - get_entrance_exam_usage_key(self.course), - ) - - def _create_section_context(self, previous_of_active_section, next_of_active_section): - """ - Returns and creates the rendering context for the section. - """ - def _compute_section_url(section_info, requested_child): - """ - Returns the section URL for the given section_info with the given child parameter. - """ - return "{url}?child={requested_child}".format( - url=reverse( - 'courseware_section', - args=[str(self.course_key), section_info['chapter_url_name'], section_info['url_name']], - ), - requested_child=requested_child, - ) - - # NOTE (CCB): Pull the position from the URL for un-authenticated users. Otherwise, pull the saved - # state from the data store. - position = None if self.request.user.is_authenticated else self.position - section_context = { - 'activate_block_id': self.request.GET.get('activate_block_id'), - 'requested_child': self.request.GET.get("child"), - 'progress_url': reverse('progress', kwargs={'course_id': str(self.course_key)}), - 'user_authenticated': self.request.user.is_authenticated, - 'position': position, - } - if previous_of_active_section: - section_context['prev_url'] = _compute_section_url(previous_of_active_section, 'last') - if next_of_active_section: - section_context['next_url'] = _compute_section_url(next_of_active_section, 'first') - # sections can hide data that masquerading staff should see when debugging issues with specific students - section_context['specific_masquerade'] = self._is_masquerading_as_specific_student() - return section_context def render_accordion(request, course, table_of_contents): diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html deleted file mode 100644 index ce9dd19b29..0000000000 --- a/lms/templates/courseware/courseware.html +++ /dev/null @@ -1,318 +0,0 @@ -<%page expression_filter="h"/> -<%inherit file="/main.html" /> -<%namespace name='static' file='/static_content.html'/> -<%def name="online_help_token()"><% return "courseware" %> -<%! -import waffle - -from django.conf import settings -from django.urls import reverse -from django.utils.translation import gettext as _ - -from lms.djangoapps.edxnotes.helpers import is_feature_enabled as is_edxnotes_enabled -from openedx.core.djangolib.js_utils import js_escaped_string -from openedx.core.djangolib.markup import HTML -from openedx.features.course_experience import course_home_page_title, DISABLE_COURSE_OUTLINE_PAGE_FLAG -%> -<% - include_special_exams = ( - request.user.is_authenticated and - settings.FEATURES.get('ENABLE_SPECIAL_EXAMS', False) and - (course.enable_proctored_exams or course.enable_timed_exams) - ) - - completion_aggregator_url = getattr(settings, "COMPLETION_AGGREGATOR_URL", "") -%> - -<%def name="course_name()"> - <% return _("{course_number} Courseware").format(course_number=course.display_number_with_default) %> - - -<%block name="bodyclass">view-in-course view-courseware courseware ${course.css_class or ''} - -<%block name="title"> - - ${static.get_page_title_breadcrumbs(sequence_title, section_title, course_name())} - - - -<%block name="header_extras"> - -% for template_name in ["image-modal"]: - -% endfor - -% if include_special_exams is not UNDEFINED and include_special_exams: - % for template_name in ["proctored-exam-status"]: - - % endfor -% endif - - - -<%block name="headextra"> -<%static:css group='style-course-vendor'/> -<%static:css group='style-course'/> -## Utility: Notes -% if is_edxnotes_enabled(course, request.user): -<%static:css group='style-student-notes'/> -% endif - - - - - - ${HTML(fragment.head_html())} - - -<%block name="js_extra"> - - - - <%static:js group='courseware'/> - <%include file="/mathjax_include.html" args="disable_fast_preview=True"/> - - % if show_search: - <%static:require_module module_name="course_search/js/course_search_factory" class_name="CourseSearchFactory"> - var courseId = $('.courseware-results').data('courseId'); - CourseSearchFactory({ - courseId: courseId, - searchHeader: $('.search-bar') - }); - - % endif - - <%static:require_module module_name="js/courseware/courseware_factory" class_name="CoursewareFactory"> - CoursewareFactory(); - - - % if staff_access: - <%include file="xqa_interface.html"/> - % endif - - - - % if not request.user.is_authenticated: - - % endif - - - -${HTML(fragment.foot_html())} - - - -
- -% if default_tab: - <%include file="/courseware/course_navigation.html" /> -% else: - <%include file="/courseware/course_navigation.html" args="active_page='courseware'" /> -% endif - -
- -
- -% if course.show_calculator or is_edxnotes_enabled(course, request.user): - -% endif