From 7862e375f187c0ee40edad9690caeea89163dcb2 Mon Sep 17 00:00:00 2001 From: Andy Armstrong Date: Mon, 30 Jan 2017 22:52:03 -0500 Subject: [PATCH 01/17] Implement a stub unified course tab --- lms/djangoapps/courseware/tabs.py | 14 +++- lms/djangoapps/courseware/views/views.py | 53 ++++++++++++++ lms/templates/courseware/course-outline.html | 12 ++++ .../courseware/unified-course-view.html | 72 +++++++++++++++++++ lms/urls.py | 15 ++++ 5 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 lms/templates/courseware/course-outline.html create mode 100644 lms/templates/courseware/unified-course-view.html diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py index cae742e0f4..b6b57b8760 100644 --- a/lms/djangoapps/courseware/tabs.py +++ b/lms/djangoapps/courseware/tabs.py @@ -2,6 +2,8 @@ This module is essentially a broker to xmodule/tabs.py -- it was originally introduced to perform some LMS-specific tab display gymnastics for the Entrance Exams feature """ +import waffle + from django.conf import settings from django.utils.translation import ugettext as _, ugettext_noop @@ -9,7 +11,7 @@ from courseware.access import has_access from courseware.entrance_exams import user_must_complete_entrance_exam from openedx.core.lib.course_tabs import CourseTabPluginManager from student.models import CourseEnrollment -from xmodule.tabs import CourseTab, CourseTabList, key_checker +from xmodule.tabs import CourseTab, CourseTabList, key_checker, link_reverse_func class EnrolledTab(CourseTab): @@ -34,6 +36,16 @@ class CoursewareTab(EnrolledTab): is_movable = False is_default = False + @property + def link_func(self): + """ + Returns a function that computes the URL for this tab. + """ + if waffle.switch_is_active('unified_course_view'): + return link_reverse_func('unified_course_view') + else: + return link_reverse_func('courseware') + class CourseInfoTab(CourseTab): """ diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 6a01baf548..24d06ed72c 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -25,6 +25,7 @@ from django.http import ( QueryDict, ) from django.shortcuts import redirect +from django.template.loader import render_to_string from django.utils.decorators import method_decorator from django.utils.timezone import UTC from django.utils.translation import ugettext as _ @@ -1623,3 +1624,55 @@ def financial_assistance_form(request): } ], }) + + +class UnifiedCourseView(View): + """ + Unified view for a course. + """ + @method_decorator(login_required) + @method_decorator(ensure_csrf_cookie) + @method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True)) + @method_decorator(ensure_valid_course_key) + def get(self, request, course_id): + """ + Displays the main view for the specified course. + + Arguments: + request: HTTP request + course_id (unicode): course id + """ + course_key = CourseKey.from_string(course_id) + course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) + + # Render the outline as a fragment + outline_fragment = CourseOutlineFragmentView().render_fragment(request, course_id=course_id) + + # Render the entire unified course view + context = { + 'csrf': csrf(request)['csrf_token'], + 'course': course, + 'outline_fragment': outline_fragment, + 'disable_courseware_js': True, + 'uses_pattern_library': True, + } + return render_to_response('courseware/unified-course-view.html', context) + + +class CourseOutlineFragmentView(FragmentView): + """ + Course outline fragment to be shown in the unified course view. + """ + + def render_fragment(self, request, course_id=None): + """ + Renders the course outline as a fragment. + """ + course_key = CourseKey.from_string(course_id) + course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) + context = { + 'csrf': csrf(request)['csrf_token'], + 'course': course, + } + html = render_to_string('courseware/course-outline.html', context) + return Fragment(html) diff --git a/lms/templates/courseware/course-outline.html b/lms/templates/courseware/course-outline.html new file mode 100644 index 0000000000..09ff7acfbf --- /dev/null +++ b/lms/templates/courseware/course-outline.html @@ -0,0 +1,12 @@ +## mako + +<%namespace name='static' file='../static_content.html'/> + +<%! +import json +from django.utils.translation import ugettext as _ +%> + +
+

Hello, world!

+
diff --git a/lms/templates/courseware/unified-course-view.html b/lms/templates/courseware/unified-course-view.html new file mode 100644 index 0000000000..27d1b07241 --- /dev/null +++ b/lms/templates/courseware/unified-course-view.html @@ -0,0 +1,72 @@ +## mako + +<%! main_css = "style-main-v2" %> + +<%page expression_filter="h"/> +<%inherit file="../main.html" /> +<%namespace name='static' file='../static_content.html'/> +<%def name="online_help_token()"><% return "courseware" %> +<%def name="course_name()"> +<% return _("{course_number} Courseware").format(course_number=course.display_number_with_default) %> + + +<%! +import json +from django.utils.translation import ugettext as _ +from django.template.defaultfilters import escapejs +from django.core.urlresolvers import reverse + +from django_comment_client.permissions import has_permission +from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string +from openedx.core.djangolib.markup import HTML +%> + +<%block name="bodyclass">course + +<%block name="pagetitle">${course_name()} + +<%include file="../courseware/course_navigation.html" args="active_page='courseware'" /> + +<%block name="headextra"> +${HTML(outline_fragment.head_html())} + + +<%block name="js_extra"> +${HTML(outline_fragment.foot_html())} + + +<%block name="content"> +
+ +
+ ${HTML(outline_fragment.body_html())} +
+
+ diff --git a/lms/urls.py b/lms/urls.py index e5b6736bdd..2583fc9a77 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -11,6 +11,7 @@ from django.conf.urls.static import static from courseware.views.views import CourseTabView, EnrollStaffView, StaticCourseTabView from config_models.views import ConfigurationModelCurrentAPIView from courseware.views.index import CoursewareIndex +from courseware.views.views import UnifiedCourseView, CourseOutlineFragmentView from openedx.core.djangoapps.auth_exchange.views import LoginWithAccessTokenView from openedx.core.djangoapps.catalog.models import CatalogIntegration from openedx.core.djangoapps.programs.models import ProgramsApiConfig @@ -379,6 +380,20 @@ urlpatterns += ( name='html_book', ), + url( + r'^courses/{}/course/?$'.format( + settings.COURSE_ID_PATTERN, + ), + UnifiedCourseView.as_view(), + name='unified_course_view', + ), + url( + r'^courses/{}/course/outline?$'.format( + settings.COURSE_ID_PATTERN, + ), + CourseOutlineFragmentView.as_view(), + name='course_outline_fragment_view', + ), url( r'^courses/{}/courseware/?$'.format( settings.COURSE_ID_PATTERN, From 74b789b6768cd9d0f9fe29abc1427bd7da2fb48c Mon Sep 17 00:00:00 2001 From: Brian Jacobel Date: Mon, 6 Feb 2017 16:27:33 -0500 Subject: [PATCH 02/17] Get the block tree and display it on the outline page --- lms/djangoapps/courseware/views/views.py | 30 ++++++++++++++++++++ lms/templates/courseware/course-outline.html | 17 ++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 24d06ed72c..13ac3f88ef 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -45,6 +45,7 @@ from lms.djangoapps.grades.new.course_grade import CourseGradeFactory from lms.djangoapps.instructor.enrollment import uses_shib from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification from lms.djangoapps.ccx.custom_exception import CCXLocatorValidationException +from lms.djangoapps.course_api.blocks.api import get_blocks import shoppingcart import survey.utils @@ -1664,15 +1665,44 @@ class CourseOutlineFragmentView(FragmentView): Course outline fragment to be shown in the unified course view. """ + def populate_children(self, block, all_blocks): + """ + For a passed block, replace each id in its children array with the full representation of that child, + which will be looked up by id in the passed all_blocks dict. + Recursively do the same replacement for children of those children. + """ + children = block.get('children') or [] + + for i in range(len(children)): + child_id = block['children'][i] + child_detail = self.populate_children(all_blocks[child_id], all_blocks) + block['children'][i] = child_detail + + return block + def render_fragment(self, request, course_id=None): """ Renders the course outline as a fragment. """ course_key = CourseKey.from_string(course_id) course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) + course_usage_key = modulestore().make_course_usage_key(course_key) + all_blocks = get_blocks( + request, + course_usage_key, + user=request.user, + nav_depth=3, + requested_fields=['children', 'display_name', 'type'], + block_types_filter=['course', 'chapter', 'vertical', 'sequential'] + ) + + course_block_tree = all_blocks['blocks'][all_blocks['root']] # Get the root of the block tree + context = { 'csrf': csrf(request)['csrf_token'], 'course': course, + # Recurse through the block tree, fleshing out each child object + 'blocks': self.populate_children(course_block_tree, all_blocks['blocks']) } html = render_to_string('courseware/course-outline.html', context) return Fragment(html) diff --git a/lms/templates/courseware/course-outline.html b/lms/templates/courseware/course-outline.html index 09ff7acfbf..66559d7052 100644 --- a/lms/templates/courseware/course-outline.html +++ b/lms/templates/courseware/course-outline.html @@ -4,9 +4,24 @@ <%! import json +import pprint from django.utils.translation import ugettext as _ %>
-

Hello, world!

+
    + % for unit in blocks.get('children') or []: +
  • ${ unit['display_name'] }
  • +
      + % for section in unit.get('children') or []: +
    • ${ section['display_name'] }
    • +
        + % for subsection in section.get('children') or []: +
      • ${ subsection['display_name'] }
      • + % endfor +
      + % endfor +
    + % endfor +
From eca83ba70a0da206e53de9449fd1a01174673885 Mon Sep 17 00:00:00 2001 From: Brian Jacobel Date: Tue, 7 Feb 2017 11:27:40 -0500 Subject: [PATCH 03/17] Outline will only show section and subsection, not unit --- lms/envs/common.py | 1 + .../js/courseware/course_outline_factory.js | 29 +++++++++++++++++++ lms/templates/courseware/course-outline.html | 25 +++++++--------- requirements/edx/base.txt | 2 ++ 4 files changed, 43 insertions(+), 14 deletions(-) create mode 100644 lms/static/js/courseware/course_outline_factory.js diff --git a/lms/envs/common.py b/lms/envs/common.py index eb8c3020d5..519dfd4063 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1736,6 +1736,7 @@ REQUIRE_JS_PATH_OVERRIDES = { 'js/student_account/logistration_factory': 'js/student_account/logistration_factory.js', 'js/student_profile/views/learner_profile_factory': 'js/student_profile/views/learner_profile_factory.js', 'js/courseware/courseware_factory': 'js/courseware/courseware_factory.js', + 'js/courseware/course_outline_factory': 'js/courseware/course_outline_factory.js', 'js/groups/views/cohorts_dashboard_factory': 'js/groups/views/cohorts_dashboard_factory.js', 'draggabilly': 'js/vendor/draggabilly.js' } diff --git a/lms/static/js/courseware/course_outline_factory.js b/lms/static/js/courseware/course_outline_factory.js new file mode 100644 index 0000000000..3f2d3643c5 --- /dev/null +++ b/lms/static/js/courseware/course_outline_factory.js @@ -0,0 +1,29 @@ +(function(define) { + 'use strict'; + + define([ + 'jquery', + 'edx-ui-toolkit/js/utils/constants' + ], + function($, constants) { + return function(root) { + // In the future this factory could instantiate a Backbone view or React component that handles events + $(root).keydown(function(event) { + var $focusable = $('.outline-item.focusable'), + currentFocusIndex = $.inArray(event.target, $focusable); + + switch (event.keyCode) { // eslint-disable-line default-case + case constants.keyCodes.down: + event.preventDefault(); + $focusable.eq(Math.min(currentFocusIndex + 1, $focusable.length - 1)).focus(); + break; + case constants.keyCodes.up: + event.preventDefault(); + $focusable.eq(Math.max(currentFocusIndex - 1, 0)).focus(); + break; + } + }); + }; + } + ); +}).call(this, define || RequireJS.define); diff --git a/lms/templates/courseware/course-outline.html b/lms/templates/courseware/course-outline.html index 66559d7052..2e1d9f2c53 100644 --- a/lms/templates/courseware/course-outline.html +++ b/lms/templates/courseware/course-outline.html @@ -3,23 +3,20 @@ <%namespace name='static' file='../static_content.html'/> <%! -import json -import pprint from django.utils.translation import ugettext as _ %> -
-