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'
)
)