diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py index 6fd2f425bf..c8df7669f9 100644 --- a/lms/djangoapps/courseware/tabs.py +++ b/lms/djangoapps/courseware/tabs.py @@ -45,7 +45,7 @@ class CoursewareTab(EnrolledTab): Returns the main course URL for the current user. """ if waffle.flag_is_active(request, UNIFIED_COURSE_VIEW_FLAG): - return 'edx.course_experience.course_home' + return 'openedx.course_experience.course_home' else: return 'courseware' diff --git a/lms/static/sass/shared-v2/_components.scss b/lms/static/sass/shared-v2/_components.scss index 5f36120e42..bafb0dc175 100644 --- a/lms/static/sass/shared-v2/_components.scss +++ b/lms/static/sass/shared-v2/_components.scss @@ -150,6 +150,13 @@ } } +.section { + .icon { + width: 20px; + text-align: center; + } +} + .section:not(:first-child) { margin-top: $baseline; } diff --git a/openedx/features/course_bookmarks/views/course_bookmarks.py b/openedx/features/course_bookmarks/views/course_bookmarks.py index f44f05770f..7d8d2a5e98 100644 --- a/openedx/features/course_bookmarks/views/course_bookmarks.py +++ b/openedx/features/course_bookmarks/views/course_bookmarks.py @@ -18,7 +18,6 @@ from opaque_keys.edx.keys import CourseKey from openedx.core.djangoapps.plugin_api.views import EdxFragmentView from util.views import ensure_valid_course_key from web_fragments.fragment import Fragment -from xmodule.modulestore.django import modulestore class CourseBookmarksView(View): diff --git a/openedx/features/course_experience/templates/course_experience/course-home-fragment.html b/openedx/features/course_experience/templates/course_experience/course-home-fragment.html index fdb0967e99..99429120f5 100644 --- a/openedx/features/course_experience/templates/course_experience/course-home-fragment.html +++ b/openedx/features/course_experience/templates/course_experience/course-home-fragment.html @@ -74,6 +74,12 @@ from openedx.features.course_experience import UNIFIED_COURSE_EXPERIENCE_FLAG ${_("Bookmarks")} +
  • + + + ${_("Updates")} + +
  • % if handouts_html: diff --git a/openedx/features/course_experience/templates/course_experience/course-updates-fragment.html b/openedx/features/course_experience/templates/course_experience/course-updates-fragment.html new file mode 100644 index 0000000000..993b996acf --- /dev/null +++ b/openedx/features/course_experience/templates/course_experience/course-updates-fragment.html @@ -0,0 +1,42 @@ +## mako + +<%page expression_filter="h"/> +<%namespace name='static' file='../static_content.html'/> + +<%! +import json + +from django.conf import settings +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 +from openedx.features.course_experience import UNIFIED_COURSE_EXPERIENCE_FLAG +%> + +<%block name="content"> +
    + +
    + ${HTML(updates_html)} +
    +
    + diff --git a/openedx/features/course_experience/tests/views/test_course_home.py b/openedx/features/course_experience/tests/views/test_course_home.py index 84d984a88e..3094022d45 100644 --- a/openedx/features/course_experience/tests/views/test_course_home.py +++ b/openedx/features/course_experience/tests/views/test_course_home.py @@ -18,7 +18,7 @@ def course_home_url(course): Returns the URL for the course's home page """ return reverse( - 'edx.course_experience.course_home', + 'openedx.course_experience.course_home', kwargs={ 'course_id': unicode(course.id), } diff --git a/openedx/features/course_experience/tests/views/test_course_updates.py b/openedx/features/course_experience/tests/views/test_course_updates.py new file mode 100644 index 0000000000..4afcca0956 --- /dev/null +++ b/openedx/features/course_experience/tests/views/test_course_updates.py @@ -0,0 +1,90 @@ +""" +Tests for the course updates page. +""" +from django.core.urlresolvers import reverse + +from student.models import CourseEnrollment +from student.tests.factories import UserFactory +from xmodule.html_module import CourseInfoModule +from xmodule.modulestore import ModuleStoreEnum +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls + +TEST_PASSWORD = 'test' + + +def course_updates_url(course): + """ + Returns the URL for the course's home page + """ + return reverse( + 'openedx.course_experience.course_updates', + kwargs={ + 'course_id': unicode(course.id), + } + ) + + +class TestCourseUpdatesPage(SharedModuleStoreTestCase): + """ + Test the course updates page. + """ + @classmethod + def setUpClass(cls): + """Set up the simplest course possible.""" + # setUpClassAndTestData() already calls setUpClass on SharedModuleStoreTestCase + # pylint: disable=super-method-not-called + with super(TestCourseUpdatesPage, cls).setUpClassAndTestData(): + with cls.store.default_store(ModuleStoreEnum.Type.split): + cls.course = CourseFactory.create() + with cls.store.bulk_operations(cls.course.id): + # Create a basic course structure + chapter = ItemFactory.create(category='chapter', parent_location=cls.course.location) + section = ItemFactory.create(category='sequential', parent_location=chapter.location) + ItemFactory.create(category='vertical', parent_location=section.location) + + @classmethod + def setUpTestData(cls): + """Set up and enroll our fake user in the course.""" + cls.user = UserFactory(password=TEST_PASSWORD) + CourseEnrollment.enroll(cls.user, cls.course.id) + + # Create course updates + cls.create_course_updates(cls.course, cls.user) + + @classmethod + def create_course_updates(cls, course, user, count=5): + """ + Create some test course updates. + """ + updates_usage_key = course.id.make_usage_key('course_info', 'updates') + course_updates = modulestore().create_item( + user.id, + updates_usage_key.course_key, + updates_usage_key.block_type, + block_id=updates_usage_key.block_id + ) + course_updates.data = u'
    1. Test Update
    ' + modulestore().update_item(course_updates, user.id) + + def setUp(self): + """ + Set up for the tests. + """ + super(TestCourseUpdatesPage, self).setUp() + self.client.login(username=self.user.username, password=TEST_PASSWORD) + + def test_view(self): + url = course_updates_url(self.course) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + response_content = response.content.decode("utf-8") + self.assertIn('Test Update', response_content) + + def test_queries(self): + # Fetch the view and verify that the query counts haven't changed + with self.assertNumQueries(34): + with check_mongo_calls(4): + url = course_updates_url(self.course) + self.client.get(url) diff --git a/openedx/features/course_experience/urls.py b/openedx/features/course_experience/urls.py index 6dded24c98..8f8ed091bb 100644 --- a/openedx/features/course_experience/urls.py +++ b/openedx/features/course_experience/urls.py @@ -6,21 +6,32 @@ from django.conf.urls import url from views.course_home import CourseHomeView, CourseHomeFragmentView from views.course_outline import CourseOutlineFragmentView +from views.course_updates import CourseUpdatesFragmentView, CourseUpdatesView urlpatterns = [ url( r'^$', CourseHomeView.as_view(), - name='edx.course_experience.course_home', + name='openedx.course_experience.course_home', + ), + url( + r'^updates$', + CourseUpdatesView.as_view(), + name='openedx.course_experience.course_updates', ), url( r'^home_fragment$', CourseHomeFragmentView.as_view(), - name='edx.course_experience.course_home_fragment_view', + name='openedx.course_experience.course_home_fragment_view', ), url( r'^outline_fragment$', CourseOutlineFragmentView.as_view(), - name='edx.course_experience.course_outline_fragment_view', + name='openedx.course_experience.course_outline_fragment_view', + ), + url( + r'^updates_fragment$', + CourseUpdatesFragmentView.as_view(), + name='openedx.course_experience.course_updates_fragment_view', ), ] diff --git a/openedx/features/course_experience/views/course_updates.py b/openedx/features/course_experience/views/course_updates.py new file mode 100644 index 0000000000..1080a1016f --- /dev/null +++ b/openedx/features/course_experience/views/course_updates.py @@ -0,0 +1,64 @@ +""" +Views that handle course updates. +""" + +from django.contrib.auth.decorators import login_required +from django.core.context_processors import csrf +from django.core.urlresolvers import reverse +from django.template.loader import render_to_string +from django.utils.decorators import method_decorator +from django.views.decorators.cache import cache_control + +from courseware.courses import get_course_info_section, get_course_with_access +from lms.djangoapps.courseware.tabs import CoursewareTab +from lms.djangoapps.courseware.views.views import CourseTabView +from opaque_keys.edx.keys import CourseKey +from openedx.core.djangoapps.plugin_api.views import EdxFragmentView +from web_fragments.fragment import Fragment + + +class CourseUpdatesView(CourseTabView): + """ + The course updates page. + """ + @method_decorator(login_required) + @method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True)) + def get(self, request, course_id, **kwargs): + """ + Displays the home page for the specified course. + """ + return super(CourseUpdatesView, self).get(request, course_id, 'courseware', **kwargs) + + def render_to_fragment(self, request, course=None, tab=None, **kwargs): + course_id = unicode(course.id) + updates_fragment_view = CourseUpdatesFragmentView() + return updates_fragment_view.render_to_fragment(request, course_id=course_id, **kwargs) + + +class CourseUpdatesFragmentView(EdxFragmentView): + """ + A fragment to render the home page for a course. + """ + def render_to_fragment(self, request, course_id=None, **kwargs): + """ + Renders the course's home page 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_url_name = CoursewareTab.main_course_url_name(request) + course_url = reverse(course_url_name, kwargs={'course_id': unicode(course.id)}) + + # Fetch the updates as HTML + updates_html = get_course_info_section(request, request.user, course, 'updates') + + # Render the course home fragment + context = { + 'csrf': csrf(request)['csrf_token'], + 'course': course, + 'course_url': course_url, + 'updates_html': updates_html, + 'disable_courseware_js': True, + 'uses_pattern_library': True, + } + html = render_to_string('course_experience/course-updates-fragment.html', context) + return Fragment(html)