diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py index 22ebe9f357..0720dc5ff6 100644 --- a/lms/djangoapps/courseware/tabs.py +++ b/lms/djangoapps/courseware/tabs.py @@ -44,7 +44,7 @@ class CoursewareTab(EnrolledTab): """ request = RequestCache.get_current_request() if waffle.flag_is_active(request, 'unified_course_view'): - return link_reverse_func('unified_course_view') + return link_reverse_func('edx.course_experience.course_home') else: return link_reverse_func('courseware') diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 07ee0ccf9a..5575c5fd3c 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -45,7 +45,6 @@ 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 @@ -1625,84 +1624,3 @@ 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_to_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 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_to_fragment(self, request, course_id=None, **kwargs): - """ - 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/envs/common.py b/lms/envs/common.py index 519dfd4063..d607abfbff 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1736,7 +1736,6 @@ 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' } @@ -2174,6 +2173,9 @@ INSTALLED_APPS = ( # Unusual migrations 'database_fixups', + + # Features + 'openedx.features.course_experience', ) ######################### CSRF ######################################### diff --git a/lms/static/course_experience b/lms/static/course_experience new file mode 120000 index 0000000000..7daa76d5a5 --- /dev/null +++ b/lms/static/course_experience @@ -0,0 +1 @@ +../../openedx/features/course_experience/static/course_experience \ No newline at end of file diff --git a/lms/static/karma_lms.conf.js b/lms/static/karma_lms.conf.js index 04deb9c5f0..d520316d71 100644 --- a/lms/static/karma_lms.conf.js +++ b/lms/static/karma_lms.conf.js @@ -27,6 +27,7 @@ var options = { // Otherwise Istanbul which is used for coverage tracking will cause tests to not run. sourceFiles: [ {pattern: 'coffee/src/**/!(*spec).js'}, + {pattern: 'course_experience/js/**/!(*spec).js'}, {pattern: 'discussion/js/**/!(*spec).js'}, {pattern: 'js/**/!(*spec|djangojs).js'}, {pattern: 'lms/js/**/!(*spec).js'}, diff --git a/lms/static/lms/js/build.js b/lms/static/lms/js/build.js index 2a1b44b360..1a2a536abf 100644 --- a/lms/static/lms/js/build.js +++ b/lms/static/lms/js/build.js @@ -18,6 +18,7 @@ * done. */ modules: getModulesList([ + 'course_experience/js/course_outline_factory', 'discussion/js/discussion_board_factory', 'discussion/js/discussion_profile_page_factory', 'js/api_admin/catalog_preview_factory', diff --git a/lms/static/lms/js/spec/main.js b/lms/static/lms/js/spec/main.js index 11ecbffcba..30449257f6 100644 --- a/lms/static/lms/js/spec/main.js +++ b/lms/static/lms/js/spec/main.js @@ -679,6 +679,7 @@ }); testFiles = [ + 'course_experience/js/spec/course_outline_factory_spec.js', 'discussion/js/spec/discussion_board_factory_spec.js', 'discussion/js/spec/discussion_profile_page_factory_spec.js', 'discussion/js/spec/discussion_board_view_spec.js', @@ -694,7 +695,6 @@ 'js/spec/courseware/course_home_events_spec.js', 'js/spec/courseware/link_clicked_events_spec.js', 'js/spec/courseware/updates_visibility_spec.js', - 'js/spec/courseware/course_outline_factory_spec.js', 'js/spec/dashboard/donation.js', 'js/spec/dashboard/dropdown_spec.js', 'js/spec/dashboard/track_events_spec.js', diff --git a/lms/urls.py b/lms/urls.py index 2583fc9a77..cdcb0b8b34 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -11,7 +11,6 @@ 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 @@ -380,20 +379,6 @@ 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, @@ -616,10 +601,19 @@ urlpatterns += ( name='edxnotes_endpoints', ), + # Branding API url( r'^api/branding/v1/', include('branding.api_urls') ), + + # Course experience + url( + r'^courses/{}/course/'.format( + settings.COURSE_ID_PATTERN, + ), + include('openedx.features.course_experience.urls'), + ), ) if settings.FEATURES["ENABLE_TEAMS"]: diff --git a/openedx/features/README.rst b/openedx/features/README.rst new file mode 100644 index 0000000000..ce592b57f0 --- /dev/null +++ b/openedx/features/README.rst @@ -0,0 +1,6 @@ +Open EdX Features +----------------- + +This is the root package for Open edX features that extend the edX platform. +The intention is that these features would ideally live in an external +repository, but for now they live in edx-platform but are cleanly modularized. diff --git a/openedx/features/__init__.py b/openedx/features/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openedx/features/course_experience/__init__.py b/openedx/features/course_experience/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/static/js/fixtures/courseware/course_outline.html b/openedx/features/course_experience/static/course_experience/fixtures/course-outline-fragment.html similarity index 100% rename from lms/static/js/fixtures/courseware/course_outline.html rename to openedx/features/course_experience/static/course_experience/fixtures/course-outline-fragment.html diff --git a/lms/static/js/courseware/course_outline_factory.js b/openedx/features/course_experience/static/course_experience/js/course_outline_factory.js similarity index 100% rename from lms/static/js/courseware/course_outline_factory.js rename to openedx/features/course_experience/static/course_experience/js/course_outline_factory.js diff --git a/lms/static/js/spec/courseware/course_outline_factory_spec.js b/openedx/features/course_experience/static/course_experience/js/spec/course_outline_factory_spec.js similarity index 96% rename from lms/static/js/spec/courseware/course_outline_factory_spec.js rename to openedx/features/course_experience/static/course_experience/js/spec/course_outline_factory_spec.js index 3fde909c3e..ccea16b8f2 100644 --- a/lms/static/js/spec/courseware/course_outline_factory_spec.js +++ b/openedx/features/course_experience/static/course_experience/js/spec/course_outline_factory_spec.js @@ -1,7 +1,7 @@ define([ 'jquery', 'edx-ui-toolkit/js/utils/constants', - 'js/courseware/course_outline_factory' + 'course_experience/js/course_outline_factory' ], function($, constants, CourseOutlineFactory) { 'use strict'; @@ -19,7 +19,7 @@ define([ }; beforeEach(function() { - loadFixtures('js/fixtures/courseware/course_outline.html'); + loadFixtures('course_experience/fixtures/course-outline-fragment.html'); CourseOutlineFactory('.block-tree'); }); diff --git a/lms/templates/courseware/unified-course-view.html b/openedx/features/course_experience/templates/course_experience/course-home.html similarity index 100% rename from lms/templates/courseware/unified-course-view.html rename to openedx/features/course_experience/templates/course_experience/course-home.html diff --git a/lms/templates/courseware/course_outline.html b/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html similarity index 100% rename from lms/templates/courseware/course_outline.html rename to openedx/features/course_experience/templates/course_experience/course-outline-fragment.html diff --git a/openedx/features/course_experience/tests/__init__.py b/openedx/features/course_experience/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openedx/features/course_experience/tests/views/__init__.py b/openedx/features/course_experience/tests/views/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/courseware/tests/test_course_outline_views.py b/openedx/features/course_experience/tests/views/test_course_outline.py similarity index 98% rename from lms/djangoapps/courseware/tests/test_course_outline_views.py rename to openedx/features/course_experience/tests/views/test_course_outline.py index c57c0a683d..76a6cfa456 100644 --- a/lms/djangoapps/courseware/tests/test_course_outline_views.py +++ b/openedx/features/course_experience/tests/views/test_course_outline.py @@ -55,7 +55,7 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase): def test_render(self): for course in self.courses: url = reverse( - 'unified_course_view', + 'edx.course_experience.course_home', kwargs={ 'course_id': unicode(course.id), } diff --git a/openedx/features/course_experience/urls.py b/openedx/features/course_experience/urls.py new file mode 100644 index 0000000000..3e25fd59ba --- /dev/null +++ b/openedx/features/course_experience/urls.py @@ -0,0 +1,21 @@ +""" +Defines URLs for the course experience. +""" + +from django.conf.urls import url + +from views.course_home import CourseHomeView +from views.course_outline import CourseOutlineFragmentView + +urlpatterns = [ + url( + r'^$', + CourseHomeView.as_view(), + name='edx.course_experience.course_home', + ), + url( + r'^outline_fragment$', + CourseOutlineFragmentView.as_view(), + name='edx.course_experience.course_outline_fragment_view', + ), +] diff --git a/openedx/features/course_experience/views/__init__.py b/openedx/features/course_experience/views/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openedx/features/course_experience/views/course_home.py b/openedx/features/course_experience/views/course_home.py new file mode 100644 index 0000000000..343f245d26 --- /dev/null +++ b/openedx/features/course_experience/views/course_home.py @@ -0,0 +1,50 @@ +""" +Views for the course home page. +""" + +from django.contrib.auth.decorators import login_required +from django.core.context_processors import csrf +from django.shortcuts import render_to_response +from django.utils.decorators import method_decorator +from django.views.decorators.cache import cache_control +from django.views.decorators.csrf import ensure_csrf_cookie +from django.views.generic import View + +from courseware.courses import get_course_with_access +from opaque_keys.edx.keys import CourseKey +from util.views import ensure_valid_course_key + +from course_outline import CourseOutlineFragmentView + + +class CourseHomeView(View): + """ + The home page for a course. + """ + @method_decorator(login_required) + @method_decorator(ensure_csrf_cookie) + @method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True)) + @method_decorator(ensure_valid_course_key) + def get(self, request, course_id): + """ + Displays the home page 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_to_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('course_experience/course-home.html', context) diff --git a/openedx/features/course_experience/views/course_outline.py b/openedx/features/course_experience/views/course_outline.py new file mode 100644 index 0000000000..c0d42f2fc9 --- /dev/null +++ b/openedx/features/course_experience/views/course_outline.py @@ -0,0 +1,61 @@ +""" +Views to show a course outline. +""" + +from django.core.context_processors import csrf +from django.template.loader import render_to_string + +from courseware.courses import get_course_with_access +from lms.djangoapps.course_api.blocks.api import get_blocks +from opaque_keys.edx.keys import CourseKey +from web_fragments.fragment import Fragment +from web_fragments.views import FragmentView +from xmodule.modulestore.django import modulestore + + +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_to_fragment(self, request, course_id=None, **kwargs): + """ + 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('course_experience/course-outline-fragment.html', context) + return Fragment(html) diff --git a/pavelib/quality.py b/pavelib/quality.py index 7f1c12ceb4..8228ac8fc7 100644 --- a/pavelib/quality.py +++ b/pavelib/quality.py @@ -5,13 +5,20 @@ from paver.easy import sh, task, cmdopts, needs, BuildFailure import json import os import re +from string import join from openedx.core.djangolib.markup import HTML from .utils.envs import Env from .utils.timer import timed -ALL_SYSTEMS = 'lms,cms,common,openedx,pavelib' +ALL_SYSTEMS = [ + 'cms', + 'common', + 'lms', + 'openedx', + 'pavelib', +] def top_python_dirs(dirname): @@ -45,7 +52,7 @@ def find_fixme(options): Run pylint on system code, only looking for fixme items. """ num_fixme = 0 - systems = getattr(options, 'system', ALL_SYSTEMS).split(',') + systems = getattr(options, 'system', '').split(',') or ALL_SYSTEMS for system in systems: # Directory to put the pylint report in. @@ -93,7 +100,7 @@ def run_pylint(options): num_violations = 0 violations_limit = int(getattr(options, 'limit', -1)) errors = getattr(options, 'errors', False) - systems = getattr(options, 'system', ALL_SYSTEMS).split(',') + systems = getattr(options, 'system', '').split(',') or ALL_SYSTEMS # Make sure the metrics subdirectory exists Env.METRICS_DIR.makedirs_p() @@ -234,7 +241,7 @@ def run_complexity(): Uses radon to examine cyclomatic complexity. For additional details on radon, see http://radon.readthedocs.org/ """ - system_string = 'cms/ lms/ common/ openedx/' + system_string = join(ALL_SYSTEMS, '/ ') + '/' complexity_report_dir = (Env.REPORT_DIR / "complexity") complexity_report = complexity_report_dir / "python_complexity.log"