diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index a4a5ca5fcc..38e5084196 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -298,7 +298,7 @@ class DashboardTest(ModuleStoreTestCase): self.assertIsNone(course_mode_info['days_for_upsell']) @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') - @patch('courseware.views.log.warning') + @patch('courseware.views.index.log.warning') @patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True}) def test_blocked_course_scenario(self, log_warning): @@ -349,7 +349,10 @@ class DashboardTest(ModuleStoreTestCase): # Direct link to course redirect to user dashboard self.client.get(reverse('courseware', kwargs={"course_id": self.course.id.to_deprecated_string()})) log_warning.assert_called_with( - u'User %s cannot access the course %s because payment has not yet been received', self.user, self.course.id.to_deprecated_string()) + u'User %s cannot access the course %s because payment has not yet been received', + self.user, + unicode(self.course.id), + ) # Now re-validating the invoice invoice = shoppingcart.models.Invoice.objects.get(id=sale_invoice_1.id) diff --git a/common/lib/xmodule/xmodule/lti_module.py b/common/lib/xmodule/xmodule/lti_module.py index 84b1e71640..ed2b05a7d9 100644 --- a/common/lib/xmodule/xmodule/lti_module.py +++ b/common/lib/xmodule/xmodule/lti_module.py @@ -43,7 +43,7 @@ What is supported: (http://www.imsglobal.org/lti/ltiv2p0/uml/purl.imsglobal.org/vocab/lis/v2/outcomes/Result/service.html) a.) Discovery of all such LTI http endpoints for a course. External tools GET from this discovery endpoint and receive URLs for interacting with individual grading units. - (see lms/djangoapps/courseware/views.py:get_course_lti_endpoints) + (see lms/djangoapps/courseware/views/views.py:get_course_lti_endpoints) b.) GET, PUT and DELETE in LTI Result JSON binding (http://www.imsglobal.org/lti/ltiv2p0/mediatype/application/vnd/ims/lis/v2/result+json/index.html) for a provider to synchronize grades into edx-platform. Reading, Setting, and Deleteing diff --git a/docs/en_us/internal/overview.md b/docs/en_us/internal/overview.md index 9d5d729ca3..fad76ba30b 100644 --- a/docs/en_us/internal/overview.md +++ b/docs/en_us/internal/overview.md @@ -91,7 +91,7 @@ The LMS is a django site, with root in `lms/`. It runs in many different enviro - `lms/djangoapps/courseware/models.py` - Core rendering path: - - `lms/urls.py` points to `courseware.views.index`, which gets module info from the course xml file, pulls list of `StudentModule` objects for this user (to avoid multiple db hits). + - `lms/urls.py` points to `courseware.views.views.index`, which gets module info from the course xml file, pulls list of `StudentModule` objects for this user (to avoid multiple db hits). - Calls `render_accordion` to render the "accordion"--the display of the course structure. diff --git a/docs/en_us/platform_api/source/conf.py b/docs/en_us/platform_api/source/conf.py index 5cacb96b19..af7c8e42ef 100644 --- a/docs/en_us/platform_api/source/conf.py +++ b/docs/en_us/platform_api/source/conf.py @@ -44,7 +44,7 @@ MOCK_MODULES = [ 'courseware.access', 'courseware.model_data', 'courseware.module_render', - 'courseware.views', + 'courseware.views.views', 'util.request', 'eventtracking', 'xmodule', diff --git a/lms/djangoapps/branding/tests/test_page.py b/lms/djangoapps/branding/tests/test_page.py index 16fcd87c0c..af789bf88e 100644 --- a/lms/djangoapps/branding/tests/test_page.py +++ b/lms/djangoapps/branding/tests/test_page.py @@ -196,7 +196,7 @@ class IndexPageCourseCardsSortingTests(ModuleStoreTestCase): self.factory = RequestFactory() @patch('student.views.render_to_response', RENDER_MOCK) - @patch('courseware.views.render_to_response', RENDER_MOCK) + @patch('courseware.views.views.render_to_response', RENDER_MOCK) @patch.dict('django.conf.settings.FEATURES', {'ENABLE_COURSE_DISCOVERY': False}) def test_course_discovery_off(self): """ @@ -220,7 +220,7 @@ class IndexPageCourseCardsSortingTests(ModuleStoreTestCase): self.assertIn('
module + """ + self.course_key = CourseKey.from_string(course_id) + self.request = request + 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 + + try: + self._init_new_relic() + self._verify_position() + with modulestore().bulk_operations(self.course_key): + self.course = get_course_with_access(request.user, 'load', self.course_key, depth=CONTENT_DEPTH) + self.is_staff = has_access(request.user, 'staff', self.course) + self._setup_masquerade_for_effective_user() + return self._get() + except Redirect as redirect_error: + return redirect(redirect_error.url) + except UnicodeEncodeError: + raise Http404("URL contains Unicode characters") + except Http404: + # let it propagate + raise + except Exception: # pylint: disable=broad-except + return self._handle_unexpected_error() + + 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.is_staff, + reset_masquerade_data=True + ) + # Set the user in the request to the effective user. + self.request.user = self.effective_user + + def _get(self): + """ + Render the index page. + """ + self._redirect_if_needed_to_access_course() + self._prefetch_and_bind_course() + + 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._verify_section_not_gated() + self._save_positions() + self._prefetch_and_bind_section() + + return render_to_response('courseware/courseware.html', self._create_courseware_context()) + + 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 Redirect( + reverse( + 'courseware_section', + kwargs={ + 'course_id': unicode(self.course_key), + 'chapter': self.chapter.url_name, + 'section': self.section.url_name, + }, + ) + ) + + def _init_new_relic(self): + """ + Initialize metrics for New Relic so we can slice data in New Relic Insights + """ + newrelic.agent.add_custom_parameter('course_id', unicode(self.course_key)) + newrelic.agent.add_custom_parameter('org', unicode(self.course_key.org)) + + def _verify_position(self): + """ + Verify that the given position is in fact an int. + """ + if self.position is not None: + try: + int(self.position) + except ValueError: + raise Http404(u"Position {} is not an integer!".format(self.position)) + + def _redirect_if_needed_to_access_course(self): + """ + Verifies that the user can enter the course. + """ + self._redirect_if_needed_to_pay_for_course() + self._redirect_if_needed_to_register() + self._redirect_if_needed_for_prereqs() + self._redirect_if_needed_for_course_survey() + + def _redirect_if_needed_to_pay_for_course(self): + """ + Redirect to dashboard if the course is blocked due to non-payment. + """ + self.real_user = User.objects.prefetch_related("groups").get(id=self.real_user.id) + redeemed_registration_codes = CourseRegistrationCode.objects.filter( + course_id=self.course_key, + registrationcoderedemption__redeemed_by=self.real_user + ) + if is_course_blocked(self.request, redeemed_registration_codes, self.course_key): + # registration codes may be generated via Bulk Purchase Scenario + # we have to check only for the invoice generated registration codes + # that their invoice is valid or not + log.warning( + u'User %s cannot access the course %s because payment has not yet been received', + self.real_user, + unicode(self.course_key), + ) + raise Redirect(reverse('dashboard')) + + def _redirect_if_needed_to_register(self): + """ + Verify that the user is registered in the course. + """ + if not registered_for_course(self.course, self.effective_user): + log.debug( + u'User %s tried to view course %s but is not enrolled', + self.effective_user, + unicode(self.course.id) + ) + raise Redirect(reverse('about_course', args=[unicode(self.course.id)])) + + def _redirect_if_needed_for_prereqs(self): + """ + See if all pre-requisites (as per the milestones app feature) have been + fulfilled. Note that if the pre-requisite feature flag has been turned off + (default) then this check will always pass. + """ + if not has_access(self.effective_user, 'view_courseware_with_prerequisites', self.course): + # Prerequisites have not been fulfilled. + # Therefore redirect to the Dashboard. + log.info( + u'User %d tried to view course %s ' + u'without fulfilling prerequisites', + self.effective_user.id, unicode(self.course.id)) + raise Redirect(reverse('dashboard')) + + def _redirect_if_needed_for_course_survey(self): + """ + Check to see if there is a required survey that must be taken before + the user can access the course. + """ + if must_answer_survey(self.course, self.effective_user): + raise Redirect(reverse('course_survey', args=[unicode(self.course.id)])) + + def _reset_section_to_exam_if_required(self): + """ + Check to see if an Entrance Exam is required for the user. + """ + if ( + course_has_entrance_exam(self.course) and + user_must_complete_entrance_exam(self.request, 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 _verify_section_not_gated(self): + """ + Verify whether the section is gated and accessible to the user. + """ + gated_content = gating_api.get_gated_content(self.course, self.effective_user) + if gated_content: + if unicode(self.section.location) in gated_content: + raise Http404 + + def _get_language_preference(self): + """ + Returns the preferred language for the actual user making the request. + """ + language_preference = get_user_preference(self.real_user, LANGUAGE_KEY) + if not language_preference: + language_preference = settings.LANGUAGE_CODE + 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 _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.name == 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): + """ + 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_descriptor_descendents( + self.course_key, self.effective_user, self.course, depth=CONTENT_DEPTH, + ) + + self.course = get_module_for_descriptor( + self.effective_user, + self.request, + self.course, + self.field_data_cache, + self.course_key, + course=self.course, + ) + + 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) + self.field_data_cache.add_descriptor_descendents(self.section, depth=None) + + # Bind section to user + self.section = get_module_for_descriptor( + self.effective_user, + self.request, + self.section, + self.field_data_cache, + self.course_key, + self.position, + course=self.course, + ) + + 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): + """ + Returns and creates the rendering context for the courseware. + Also returns the table of contents for the courseware. + """ + courseware_context = { + 'csrf': csrf(self.request)['csrf_token'], + 'COURSE_TITLE': self.course.display_name_with_default_escaped, + 'course': self.course, + 'init': '', + 'fragment': Fragment(), + 'staff_access': self.is_staff, + 'studio_url': get_studio_url(self.course, 'course'), + 'masquerade': self.masquerade, + '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': True, + } + 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 + if course_has_entrance_exam(self.course): + if getattr(self.chapter, 'is_entrance_exam', False): + courseware_context['entrance_exam_current_score'] = get_entrance_exam_score(self.request, self.course) + courseware_context['entrance_exam_passed'] = user_has_passed_entrance_exam(self.request, self.course) + + # staff masquerading data + now = datetime.now(UTC()) + effective_start = _adjust_start_date_for_beta_testers(self.effective_user, self.course, self.course_key) + if not in_preview_mode() and self.is_staff and now < effective_start: + # Disable student view button if user is staff and + # course is not yet visible to students. + courseware_context['disable_student_access'] = True + + 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_escaped + 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(STUDENT_VIEW, section_context) + + return courseware_context + + 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=[unicode(self.course.id), section_info['chapter_url_name'], section_info['url_name']], + ), + requested_child=requested_child, + ) + + section_context = { + 'activate_block_id': self.request.GET.get('activate_block_id'), + 'requested_child': self.request.GET.get("child"), + } + 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') + return section_context + + def _handle_unexpected_error(self): + """ + Handle unexpected exceptions raised by View. + """ + # In production, don't want to let a 500 out for any reason + if settings.DEBUG: + raise + log.exception( + u"Error in index view: user=%s, effective_user=%s, course=%s, chapter=%s section=%s position=%s", + self.real_user, + self.effective_user, + unicode(self.course_key), + self.chapter_url_name, + self.section_url_name, + self.position, + ) + try: + return render_to_response('courseware/courseware-error.html', { + 'staff_access': self.is_staff, + 'course': self.course + }) + except: + # Let the exception propagate, relying on global config to + # at least return a nice error message + log.exception("Error while rendering courseware-error page") + raise + + +def render_accordion(request, course, table_of_contents): + """ + Returns the HTML that renders the navigation for the given course. + Expects the table_of_contents to have data on each chapter and section, + including which ones are active. + """ + context = dict( + [ + ('toc', table_of_contents), + ('course_id', unicode(course.id)), + ('csrf', csrf(request)['csrf_token']), + ('due_date_display_format', course.due_date_display_format), + ] + TEMPLATE_IMPORTS.items() + ) + return render_to_string('courseware/accordion.html', context) + + +def save_child_position(seq_module, child_name): + """ + child_name: url_name of the child + """ + for position, child in enumerate(seq_module.get_display_items(), start=1): + if child.location.name == child_name: + # Only save if position changed + if position != seq_module.position: + seq_module.position = position + # Save this new position to the underlying KeyValueStore + seq_module.save() + + +def save_positions_recursively_up(user, request, field_data_cache, xmodule, course=None): + """ + Recurses up the course tree starting from a leaf + Saving the position property based on the previous node as it goes + """ + current_module = xmodule + + while current_module: + parent_location = modulestore().get_parent_location(current_module.location) + parent = None + if parent_location: + parent_descriptor = modulestore().get_item(parent_location) + parent = get_module_for_descriptor( + user, + request, + parent_descriptor, + field_data_cache, + current_module.location.course_key, + course=course + ) + + if parent and hasattr(parent, 'position'): + save_child_position(parent, current_module.location.name) + + current_module = parent diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views/views.py similarity index 72% rename from lms/djangoapps/courseware/views.py rename to lms/djangoapps/courseware/views/views.py index 6a9418e570..a8945e857b 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -9,11 +9,9 @@ from collections import OrderedDict from datetime import datetime import analytics -import newrelic.agent from django.conf import settings from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User, AnonymousUser -from django.core.context_processors import csrf from django.core.exceptions import PermissionDenied from django.core.urlresolvers import reverse from django.db import transaction @@ -32,13 +30,11 @@ from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey, UsageKey from opaque_keys.edx.locations import SlashSeparatedCourseKey from rest_framework import status -from xblock.fragment import Fragment import shoppingcart import survey.utils import survey.views from certificates import api as certs_api -from openedx.core.lib.gating import api as gating_api from commerce.utils import EcommerceService from course_modes.models import CourseMode from courseware import grades @@ -66,18 +62,15 @@ from edxmako.shortcuts import render_to_response, render_to_string, marketing_li from instructor.enrollment import uses_shib from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.coursetalk.helpers import inject_coursetalk_keys_into_context -from openedx.core.djangoapps.user_api.preferences.api import get_user_preference from openedx.core.djangoapps.credit.api import ( get_credit_requirement_status, is_user_eligible_for_credit, is_credit_course ) from openedx.core.djangoapps.theming import helpers as theming_helpers -from shoppingcart.models import CourseRegistrationCode from shoppingcart.utils import is_shopping_cart_enabled from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from student.models import UserTestGroup, CourseEnrollment -from student.views import is_course_blocked from util.cache import cache, cache_if_anonymous from util.date_utils import strftime_localized from util.db import outer_atomic @@ -89,22 +82,13 @@ from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem from xmodule.tabs import CourseTabList from xmodule.x_module import STUDENT_VIEW from lms.djangoapps.ccx.custom_exception import CCXLocatorValidationException -from .entrance_exams import ( - course_has_entrance_exam, - get_entrance_exam_content, - get_entrance_exam_score, - user_must_complete_entrance_exam, - user_has_passed_entrance_exam -) -from .module_render import toc_for_course, get_module_for_descriptor, get_module, get_module_by_usage_id +from ..entrance_exams import user_must_complete_entrance_exam +from ..module_render import get_module_for_descriptor, get_module, get_module_by_usage_id -from lang_pref import LANGUAGE_KEY log = logging.getLogger("edx.courseware") -template_imports = {'urllib': urllib} -CONTENT_DEPTH = 2 # Only display the requirements on learner dashboard for # credit and verified modes. REQUIREMENTS_DISPLAY_MODES = CourseMode.CREDIT_MODES + [CourseMode.VERIFIED] @@ -122,13 +106,13 @@ def user_groups(user): cache_expiration = 60 * 60 # one hour # Kill caching on dev machines -- we switch groups a lot - group_names = cache.get(key) + group_names = cache.get(key) # pylint: disable=no-member if settings.DEBUG: group_names = None if group_names is None: group_names = [u.name for u in UserTestGroup.objects.filter(users=user)] - cache.set(key, group_names, cache_expiration) + cache.set(key, group_names, cache_expiration) # pylint: disable=no-member return group_names @@ -158,31 +142,11 @@ def courses(request): ) -def render_accordion(request, course, toc): - """ - Draws navigation bar. Takes current position in accordion as - parameter. - - If chapter and section are '' or None, renders a default accordion. - - course, chapter, and section are the url_names. - - Returns the html string - """ - context = dict([ - ('toc', toc), - ('course_id', course.id.to_deprecated_string()), - ('csrf', csrf(request)['csrf_token']), - ('due_date_display_format', course.due_date_display_format) - ] + template_imports.items()) - return render_to_string('courseware/accordion.html', context) - - def get_current_child(xmodule, min_depth=None, requested_child=None): """ Get the xmodule.position's display item of an xmodule that has a position and children. If xmodule has no position or is out of bounds, return the first - child with children extending down to content_depth. + child with children of min_depth. For example, if chapter_one has no position set, with two child sections, section-A having no children and section-B having a discussion unit, @@ -205,414 +169,31 @@ def get_current_child(xmodule, min_depth=None, requested_child=None): def _get_default_child_module(child_modules): """Returns the first child of xmodule, subject to min_depth.""" - if not child_modules: - default_child = None - elif not min_depth > 0: - default_child = _get_child(child_modules) + if min_depth <= 0: + return _get_child(child_modules) else: - content_children = [child for child in child_modules if - child.has_children_at_depth(min_depth - 1) and child.get_display_items()] - default_child = _get_child(content_children) if content_children else None + content_children = [ + child for child in child_modules + if child.has_children_at_depth(min_depth - 1) and child.get_display_items() + ] + return _get_child(content_children) if content_children else None - return default_child + child = None + if hasattr(xmodule, 'position'): + children = xmodule.get_display_items() + if len(children) > 0: + if xmodule.position is not None and not requested_child: + pos = xmodule.position - 1 # position is 1-indexed + if 0 <= pos < len(children): + child = children[pos] + if min_depth > 0 and not child.has_children_at_depth(min_depth - 1): + child = None + if child is None: + child = _get_default_child_module(children) - if not hasattr(xmodule, 'position'): - return None - - if xmodule.position is None or requested_child: - return _get_default_child_module(xmodule.get_display_items()) - else: - # position is 1-indexed. - pos = xmodule.position - 1 - - children = xmodule.get_display_items() - if 0 <= pos < len(children): - child = children[pos] - elif len(children) > 0: - # module has a set position, but the position is out of range. - # return default child. - child = _get_default_child_module(children) - else: - child = None return child -def redirect_to_course_position(course_module, content_depth): - """ - Return a redirect to the user's current place in the course. - - If this is the user's first time, redirects to COURSE/CHAPTER/SECTION. - If this isn't the users's first time, redirects to COURSE/CHAPTER, - and the view will find the current section and display a message - about reusing the stored position. - - If there is no current position in the course or chapter, then selects - the first child. - - """ - urlargs = {'course_id': course_module.id.to_deprecated_string()} - chapter = get_current_child(course_module, min_depth=content_depth) - if chapter is None: - # oops. Something bad has happened. - raise Http404("No chapter found when loading current position in course") - - urlargs['chapter'] = chapter.url_name - if course_module.position is not None: - return redirect(reverse('courseware_chapter', kwargs=urlargs)) - - # Relying on default of returning first child - section = get_current_child(chapter, min_depth=content_depth - 1) - if section is None: - raise Http404("No section found when loading current position in course") - - urlargs['section'] = section.url_name - return redirect(reverse('courseware_section', kwargs=urlargs)) - - -def save_child_position(seq_module, child_name): - """ - child_name: url_name of the child - """ - for position, c in enumerate(seq_module.get_display_items(), start=1): - if c.location.name == child_name: - # Only save if position changed - if position != seq_module.position: - seq_module.position = position - # Save this new position to the underlying KeyValueStore - seq_module.save() - - -def save_positions_recursively_up(user, request, field_data_cache, xmodule, course=None): - """ - Recurses up the course tree starting from a leaf - Saving the position property based on the previous node as it goes - """ - current_module = xmodule - - while current_module: - parent_location = modulestore().get_parent_location(current_module.location) - parent = None - if parent_location: - parent_descriptor = modulestore().get_item(parent_location) - parent = get_module_for_descriptor( - user, - request, - parent_descriptor, - field_data_cache, - current_module.location.course_key, - course=course - ) - - if parent and hasattr(parent, 'position'): - save_child_position(parent, current_module.location.name) - - current_module = parent - - -@transaction.non_atomic_requests -@login_required -@ensure_csrf_cookie -@cache_control(no_cache=True, no_store=True, must_revalidate=True) -@ensure_valid_course_key -@outer_atomic(read_committed=True) -def index(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, redirects to user's most recent - chapter, or the first chapter if this is the user's first visit. - - Arguments: - - - request : HTTP request - - course_id : course id (str: ORG/course/URL_NAME) - - chapter : chapter url_name (str) - - section : section url_name (str) - - position : position in module, eg of module (str) - - Returns: - - - HTTPresponse - """ - - course_key = CourseKey.from_string(course_id) - - # Gather metrics for New Relic so we can slice data in New Relic Insights - newrelic.agent.add_custom_parameter('course_id', unicode(course_key)) - newrelic.agent.add_custom_parameter('org', unicode(course_key.org)) - - user = User.objects.prefetch_related("groups").get(id=request.user.id) - - redeemed_registration_codes = CourseRegistrationCode.objects.filter( - course_id=course_key, - registrationcoderedemption__redeemed_by=request.user - ) - - # Redirect to dashboard if the course is blocked due to non-payment. - if is_course_blocked(request, redeemed_registration_codes, course_key): - # registration codes may be generated via Bulk Purchase Scenario - # we have to check only for the invoice generated registration codes - # that their invoice is valid or not - log.warning( - u'User %s cannot access the course %s because payment has not yet been received', - user, - course_key.to_deprecated_string() - ) - return redirect(reverse('dashboard')) - - request.user = user # keep just one instance of User - with modulestore().bulk_operations(course_key): - return _index_bulk_op(request, course_key, chapter, section, position) - - -# pylint: disable=too-many-statements -def _index_bulk_op(request, course_key, chapter, section, position): - """ - Render the index page for the specified course. - """ - # Verify that position a string is in fact an int - if position is not None: - try: - int(position) - except ValueError: - raise Http404(u"Position {} is not an integer!".format(position)) - - course = get_course_with_access(request.user, 'load', course_key, depth=2) - staff_access = has_access(request.user, 'staff', course) - masquerade, user = setup_masquerade(request, course_key, staff_access, reset_masquerade_data=True) - - registered = registered_for_course(course, user) - if not registered: - # TODO (vshnayder): do course instructors need to be registered to see course? - log.debug(u'User %s tried to view course %s but is not enrolled', user, course.location.to_deprecated_string()) - return redirect(reverse('about_course', args=[course_key.to_deprecated_string()])) - - # see if all pre-requisites (as per the milestones app feature) have been fulfilled - # Note that if the pre-requisite feature flag has been turned off (default) then this check will - # always pass - if not has_access(user, 'view_courseware_with_prerequisites', course): - # prerequisites have not been fulfilled therefore redirect to the Dashboard - log.info( - u'User %d tried to view course %s ' - u'without fulfilling prerequisites', - user.id, unicode(course.id)) - return redirect(reverse('dashboard')) - - # Entrance Exam Check - # If the course has an entrance exam and the requested chapter is NOT the entrance exam, and - # the user hasn't yet met the criteria to bypass the entrance exam, redirect them to the exam. - if chapter and course_has_entrance_exam(course): - chapter_descriptor = course.get_child_by(lambda m: m.location.name == chapter) - if chapter_descriptor and not getattr(chapter_descriptor, 'is_entrance_exam', False) \ - and user_must_complete_entrance_exam(request, user, course): - log.info(u'User %d tried to view course %s without passing entrance exam', user.id, unicode(course.id)) - return redirect(reverse('courseware', args=[unicode(course.id)])) - - # Gated Content Check - gated_content = gating_api.get_gated_content(course, user) - if section and gated_content: - for usage_key in gated_content: - if section in usage_key: - raise Http404 - - # check to see if there is a required survey that must be taken before - # the user can access the course. - if survey.utils.must_answer_survey(course, user): - return redirect(reverse('course_survey', args=[unicode(course.id)])) - - bookmarks_api_url = reverse('bookmarks') - - try: - field_data_cache = FieldDataCache.cache_for_descriptor_descendents( - course_key, user, course, depth=2) - - studio_url = get_studio_url(course, 'course') - - language_preference = get_user_preference(request.user, LANGUAGE_KEY) - if not language_preference: - language_preference = settings.LANGUAGE_CODE - - context = { - 'csrf': csrf(request)['csrf_token'], - 'COURSE_TITLE': course.display_name_with_default_escaped, - 'course': course, - 'init': '', - 'fragment': Fragment(), - 'staff_access': staff_access, - 'studio_url': studio_url, - 'masquerade': masquerade, - 'xqa_server': settings.FEATURES.get('XQA_SERVER', "http://your_xqa_server.com"), - 'bookmarks_api_url': bookmarks_api_url, - 'language_preference': language_preference, - 'disable_optimizely': True, - } - table_of_contents, __, __ = toc_for_course(user, request, course, chapter, section, field_data_cache) - context['accordion'] = render_accordion(request, course, table_of_contents) - - now = datetime.now(UTC()) - effective_start = _adjust_start_date_for_beta_testers(user, course, course_key) - if not in_preview_mode() and staff_access and now < effective_start: - # Disable student view button if user is staff and - # course is not yet visible to students. - context['disable_student_access'] = True - - has_content = course.has_children_at_depth(CONTENT_DEPTH) - if not has_content: - # Show empty courseware for a course with no units - return render_to_response('courseware/courseware.html', context) - elif chapter is None: - # Check first to see if we should instead redirect the user to an Entrance Exam - if course_has_entrance_exam(course): - exam_chapter = get_entrance_exam_content(request, course) - if exam_chapter: - if exam_chapter.get_children(): - exam_section = exam_chapter.get_children()[0] - if exam_section: - return redirect('courseware_section', - course_id=unicode(course_key), - chapter=exam_chapter.url_name, - section=exam_section.url_name) - - # passing CONTENT_DEPTH avoids returning 404 for a course with an - # empty first section and a second section with content - return redirect_to_course_position(course, CONTENT_DEPTH) - - chapter_descriptor = course.get_child_by(lambda m: m.location.name == chapter) - if chapter_descriptor is not None: - save_child_position(course, chapter) - else: - # User may be trying to access a chapter that isn't live yet - if masquerade and masquerade.role == 'student': # if staff is masquerading as student be kinder, don't 404 - log.debug('staff masquerading as student: no chapter %s', chapter) - return redirect(reverse('courseware', args=[course.id.to_deprecated_string()])) - raise Http404('No chapter descriptor found with name {}'.format(chapter)) - - if course_has_entrance_exam(course): - # Message should not appear outside the context of entrance exam subsection. - # if section is none then we don't need to show message on welcome back screen also. - if getattr(chapter_descriptor, 'is_entrance_exam', False) and section is not None: - context['entrance_exam_current_score'] = get_entrance_exam_score(request, course) - context['entrance_exam_passed'] = user_has_passed_entrance_exam(request, course) - - if section is None: - section_descriptor = get_current_child(chapter_descriptor, requested_child=request.GET.get("child")) - if section_descriptor: - section = section_descriptor.url_name - else: - # Something went wrong -- perhaps this chapter has no sections visible to the user. - # Clearing out the last-visited state and showing "first-time" view by redirecting - # to courseware. - course.position = None - course.save() - return redirect(reverse('courseware', args=[course.id.to_deprecated_string()])) - else: - section_descriptor = chapter_descriptor.get_child_by(lambda m: m.location.name == section) - - if section_descriptor is None: - # Specifically asked-for section doesn't exist - if masquerade and masquerade.role == 'student': # don't 404 if staff is masquerading as student - log.debug('staff masquerading as student: no section %s', section) - return redirect(reverse('courseware', args=[course.id.to_deprecated_string()])) - raise Http404 - - # Allow chromeless operation - if section_descriptor.chrome: - chrome = [s.strip() for s in section_descriptor.chrome.lower().split(",")] - if 'accordion' not in chrome: - context['disable_accordion'] = True - if 'tabs' not in chrome: - context['disable_tabs'] = True - - if section_descriptor.default_tab: - context['default_tab'] = section_descriptor.default_tab - - # cdodge: this looks silly, but let's refetch the section_descriptor with depth=None - # which will prefetch the children more efficiently than doing a recursive load - section_descriptor = modulestore().get_item(section_descriptor.location, depth=None) - - # Load all descendants of the section, because we're going to display its - # html, which in general will need all of its children - field_data_cache.add_descriptor_descendents( - section_descriptor, depth=None - ) - - section_module = get_module_for_descriptor( - user, - request, - section_descriptor, - field_data_cache, - course_key, - position, - course=course - ) - - # Save where we are in the chapter. - save_child_position(chapter_descriptor, section) - - table_of_contents, prev_section_info, next_section_info = toc_for_course( - user, request, course, chapter, section, field_data_cache - ) - context['accordion'] = render_accordion(request, course, table_of_contents) - - 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=[unicode(course.id), section_info['chapter_url_name'], section_info['url_name']], - ), - requested_child=requested_child, - ) - - section_render_context = { - 'activate_block_id': request.GET.get('activate_block_id'), - 'requested_child': request.GET.get("child"), - 'prev_url': _compute_section_url(prev_section_info, 'last') if prev_section_info else None, - 'next_url': _compute_section_url(next_section_info, 'first') if next_section_info else None, - } - context['fragment'] = section_module.render(STUDENT_VIEW, section_render_context) - context['section_title'] = section_descriptor.display_name_with_default_escaped - result = render_to_response('courseware/courseware.html', context) - except Exception as e: - - # Doesn't bar Unicode characters from URL, but if Unicode characters do - # cause an error it is a graceful failure. - if isinstance(e, UnicodeEncodeError): - raise Http404("URL contains Unicode characters") - - if isinstance(e, Http404): - # let it propagate - raise - - # In production, don't want to let a 500 out for any reason - if settings.DEBUG: - raise - else: - log.exception( - u"Error in index view: user=%s, effective_user=%s, course=%s, chapter=%s section=%s position=%s", - request.user, user, course, chapter, section, position - ) - try: - result = render_to_response('courseware/courseware-error.html', { - 'staff_access': staff_access, - 'course': course - }) - except: - # Let the exception propagate, relying on global config to at - # at least return a nice error message - log.exception("Error while rendering courseware-error page") - raise - - return result - - @ensure_csrf_cookie @ensure_valid_course_key def jump_to_id(request, course_id, module_id): diff --git a/lms/djangoapps/edxnotes/helpers.py b/lms/djangoapps/edxnotes/helpers.py index 2235836f83..048b0c1b42 100644 --- a/lms/djangoapps/edxnotes/helpers.py +++ b/lms/djangoapps/edxnotes/helpers.py @@ -21,7 +21,7 @@ from django.utils.translation import ugettext as _ from edxnotes.exceptions import EdxNotesParseError, EdxNotesServiceUnavailable from edxnotes.plugins import EdxNotesTab -from courseware.views import get_current_child +from courseware.views.views import get_current_child from courseware.access import has_access from openedx.core.lib.token_utils import get_id_token from student.models import anonymous_id_for_user diff --git a/lms/djangoapps/lti_provider/views.py b/lms/djangoapps/lti_provider/views.py index 6f2e93233b..c89260ac90 100644 --- a/lms/djangoapps/lti_provider/views.py +++ b/lms/djangoapps/lti_provider/views.py @@ -142,7 +142,7 @@ def render_courseware(request, usage_key): context to render the courseware. """ # return an HttpResponse object that contains the template and necessary context to render the courseware. - from courseware.views import render_xblock + from courseware.views.views import render_xblock return render_xblock(request, unicode(usage_key), check_if_enrolled=False) diff --git a/lms/djangoapps/mobile_api/users/views.py b/lms/djangoapps/mobile_api/users/views.py index 6bf37f83de..311ff62e87 100644 --- a/lms/djangoapps/mobile_api/users/views.py +++ b/lms/djangoapps/mobile_api/users/views.py @@ -15,7 +15,8 @@ from opaque_keys import InvalidKeyError from courseware.access import is_mobile_available_for_user from courseware.model_data import FieldDataCache from courseware.module_render import get_module_for_descriptor -from courseware.views import get_current_child, save_positions_recursively_up +from courseware.views.index import save_positions_recursively_up +from courseware.views.views import get_current_child from student.models import CourseEnrollment, User from xblock.fields import Scope diff --git a/lms/urls.py b/lms/urls.py index b1b3d54853..9848cc0620 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -12,6 +12,7 @@ from microsite_configuration import microsite import auth_exchange.views from config_models.views import ConfigurationModelCurrentAPIView +from courseware.views.index import CoursewareIndex from openedx.core.djangoapps.programs.models import ProgramsApiConfig from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration @@ -270,14 +271,14 @@ urlpatterns += ( r'^courses/{}/jump_to/(?P.*)$'.format( settings.COURSE_ID_PATTERN, ), - 'courseware.views.jump_to', + 'courseware.views.views.jump_to', name='jump_to', ), url( r'^courses/{}/jump_to_id/(?P.*)$'.format( settings.COURSE_ID_PATTERN, ), - 'courseware.views.jump_to_id', + 'courseware.views.views.jump_to_id', name='jump_to_id', ), @@ -317,7 +318,7 @@ urlpatterns += ( # Note: This is not an API. Compare this with the xblock_view API above. url( r'^xblock/{usage_key_string}$'.format(usage_key_string=settings.USAGE_KEY_PATTERN), - 'courseware.views.render_xblock', + 'courseware.views.views.render_xblock', name='render_xblock', ), @@ -361,7 +362,7 @@ urlpatterns += ( r'^courses/{}/about$'.format( settings.COURSE_ID_PATTERN, ), - 'courseware.views.course_about', + 'courseware.views.views.course_about', name='about_course', ), @@ -370,14 +371,14 @@ urlpatterns += ( r'^courses/{}/$'.format( settings.COURSE_ID_PATTERN, ), - 'courseware.views.course_info', + 'courseware.views.views.course_info', name='course_root', ), url( r'^courses/{}/info$'.format( settings.COURSE_ID_PATTERN, ), - 'courseware.views.course_info', + 'courseware.views.views.course_info', name='info', ), # TODO arjun remove when custom tabs in place, see courseware/courses.py @@ -385,7 +386,7 @@ urlpatterns += ( r'^courses/{}/syllabus$'.format( settings.COURSE_ID_PATTERN, ), - 'courseware.views.syllabus', + 'courseware.views.views.syllabus', name='syllabus', ), @@ -394,7 +395,7 @@ urlpatterns += ( r'^courses/{}/survey$'.format( settings.COURSE_ID_PATTERN, ), - 'courseware.views.course_survey', + 'courseware.views.views.course_survey', name='course_survey', ), @@ -462,28 +463,28 @@ urlpatterns += ( r'^courses/{}/courseware/?$'.format( settings.COURSE_ID_PATTERN, ), - 'courseware.views.index', + CoursewareIndex.as_view(), name='courseware', ), url( r'^courses/{}/courseware/(?P[^/]*)/$'.format( settings.COURSE_ID_PATTERN, ), - 'courseware.views.index', + CoursewareIndex.as_view(), name='courseware_chapter', ), url( r'^courses/{}/courseware/(?P[^/]*)/(?P
[^/]*)/$'.format( settings.COURSE_ID_PATTERN, ), - 'courseware.views.index', + CoursewareIndex.as_view(), name='courseware_section', ), url( r'^courses/{}/courseware/(?P[^/]*)/(?P
[^/]*)/(?P[^/]*)/?$'.format( settings.COURSE_ID_PATTERN, ), - 'courseware.views.index', + CoursewareIndex.as_view(), name='courseware_position', ), @@ -491,7 +492,7 @@ urlpatterns += ( r'^courses/{}/progress$'.format( settings.COURSE_ID_PATTERN, ), - 'courseware.views.progress', + 'courseware.views.views.progress', name='progress', ), # Takes optional student_id for instructor use--shows profile as that student sees it. @@ -499,7 +500,7 @@ urlpatterns += ( r'^courses/{}/progress/(?P[^/]*)/$'.format( settings.COURSE_ID_PATTERN, ), - 'courseware.views.progress', + 'courseware.views.views.progress', name='student_progress', ), @@ -637,7 +638,7 @@ urlpatterns += ( r'^courses/{}/lti_rest_endpoints/'.format( settings.COURSE_ID_PATTERN, ), - 'courseware.views.get_course_lti_endpoints', + 'courseware.views.views.get_course_lti_endpoints', name='lti_rest_endpoints', ), @@ -702,7 +703,7 @@ urlpatterns += ( r'^courses/{}/generate_user_cert'.format( settings.COURSE_ID_PATTERN, ), - 'courseware.views.generate_user_cert', + 'courseware.views.views.generate_user_cert', name='generate_user_cert', ), ) @@ -755,7 +756,7 @@ urlpatterns += ( r'^courses/{}/(?P[^/]+)/$'.format( settings.COURSE_ID_PATTERN, ), - 'courseware.views.static_tab', + 'courseware.views.views.static_tab', name='static_tab', ), ) @@ -766,7 +767,7 @@ if settings.FEATURES.get('ENABLE_STUDENT_HISTORY_VIEW'): r'^courses/{}/submission_history/(?P[^/]*)/(?P.*?)$'.format( settings.COURSE_ID_PATTERN ), - 'courseware.views.submission_history', + 'courseware.views.views.submission_history', name='submission_history', ), ) @@ -999,17 +1000,17 @@ if settings.FEATURES.get('ENABLE_FINANCIAL_ASSISTANCE_FORM'): urlpatterns += ( url( r'^financial-assistance/$', - 'courseware.views.financial_assistance', + 'courseware.views.views.financial_assistance', name='financial_assistance' ), url( r'^financial-assistance/apply/$', - 'courseware.views.financial_assistance_form', + 'courseware.views.views.financial_assistance_form', name='financial_assistance_form' ), url( r'^financial-assistance/submit/$', - 'courseware.views.financial_assistance_request', + 'courseware.views.views.financial_assistance_request', name='submit_financial_assistance_request' ) )