diff --git a/common/test/acceptance/tests/lms/test_library.py b/common/test/acceptance/tests/lms/test_library.py index 7609cbcd3e..8b8dd1da31 100644 --- a/common/test/acceptance/tests/lms/test_library.py +++ b/common/test/acceptance/tests/lms/test_library.py @@ -127,9 +127,9 @@ class LibraryContentTestBase(UniqueCourseTest): Open library page in LMS """ self.courseware_page.visit() - paragraphs = self.courseware_page.q(css='.course-content p') - if paragraphs and "You were most recently in" in paragraphs.text[0]: - paragraphs[0].find_element_by_tag_name('a').click() + paragraphs = self.courseware_page.q(css='.course-content p').results + if not paragraphs: + self.courseware_page.q(css='.menu-item a').results[0].click() block_id = block_id if block_id is not None else self.lib_block.locator #pylint: disable=attribute-defined-outside-init self.library_content_page = LibraryContentXBlockWrapper(self.browser, block_id) diff --git a/lms/djangoapps/courseware/features/navigation.feature b/lms/djangoapps/courseware/features/navigation.feature index 98105e2a25..4c7320d7e5 100644 --- a/lms/djangoapps/courseware/features/navigation.feature +++ b/lms/djangoapps/courseware/features/navigation.feature @@ -19,10 +19,3 @@ Feature: LMS.Navigate Course When I navigate to an item in a sequence Then I see the content of the sequence item And a "seq_goto" browser event is emitted - - Scenario: I can return to the last section I visited - Given I am viewing a course with multiple sections - When I navigate to a section - And I see the content of the section - And I return to the course - Then I see that I was most recently in the subsection diff --git a/lms/djangoapps/courseware/features/navigation.py b/lms/djangoapps/courseware/features/navigation.py index 7aa4006d54..e0d426cc0e 100644 --- a/lms/djangoapps/courseware/features/navigation.py +++ b/lms/djangoapps/courseware/features/navigation.py @@ -136,12 +136,6 @@ def and_i_return_to_the_course(step): world.css_click(course) -@step(u'I see that I was most recently in the subsection') -def then_i_see_that_i_was_most_recently_in_the_subsection(step): - message = world.css_text('section.course-content > p') - assert_in("You were most recently in Test Subsection 2", message) - - def create_course(): world.clear_courses() world.scenario_dict['COURSE'] = world.CourseFactory.create( diff --git a/lms/djangoapps/courseware/field_overrides.py b/lms/djangoapps/courseware/field_overrides.py index 44207e1e71..9104fd5e33 100644 --- a/lms/djangoapps/courseware/field_overrides.py +++ b/lms/djangoapps/courseware/field_overrides.py @@ -228,11 +228,16 @@ class FieldOverrideProvider(object): @abstractmethod def enabled_for(self, course): # pragma no cover """ - Return True if this provider should be enabled for a given course + Return True if this provider should be enabled for a given course, + and False otherwise. - Return False otherwise + Concrete implementations are responsible for implementing this method. - Concrete implementations are responsible for implementing this method + Arguments: + course (CourseModule or None) + + Returns: + bool """ return False diff --git a/lms/djangoapps/courseware/self_paced_overrides.py b/lms/djangoapps/courseware/self_paced_overrides.py index c38b8961a6..e694705205 100644 --- a/lms/djangoapps/courseware/self_paced_overrides.py +++ b/lms/djangoapps/courseware/self_paced_overrides.py @@ -25,4 +25,4 @@ class SelfPacedDateOverrideProvider(FieldOverrideProvider): @classmethod def enabled_for(cls, course): """This provider is enabled for self-paced courses only.""" - return SelfPacedConfiguration.current().enabled and course.self_paced + return course is not None and course.self_paced and SelfPacedConfiguration.current().enabled diff --git a/lms/djangoapps/courseware/tests/test_course_info.py b/lms/djangoapps/courseware/tests/test_course_info.py index a67bc96fca..ae4940f0be 100644 --- a/lms/djangoapps/courseware/tests/test_course_info.py +++ b/lms/djangoapps/courseware/tests/test_course_info.py @@ -11,6 +11,7 @@ from django.core.urlresolvers import reverse from django.test.utils import override_settings from opaque_keys.edx.locations import SlashSeparatedCourseKey +from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from util.date_utils import strftime_localized from xmodule.modulestore.tests.django_utils import ( ModuleStoreTestCase, @@ -92,6 +93,33 @@ class CourseInfoTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase): response = self.client.get(url) self.assertEqual(response.status_code, 404) + def test_last_accessed_courseware_not_shown(self): + SelfPacedConfiguration(enable_course_home_improvements=True).save() + url = reverse('info', args=(unicode(self.course.id),)) + response = self.client.get(url) + self.assertNotIn('Jump back to where you were last:', response.content) + + def test_last_accessed_shown(self): + SelfPacedConfiguration(enable_course_home_improvements=True).save() + chapter = ItemFactory.create( + category="chapter", parent_location=self.course.location + ) + section = ItemFactory.create( + category='section', parent_location=chapter.location + ) + section_url = reverse( + 'courseware_section', + kwargs={ + 'section': section.url_name, + 'chapter': chapter.url_name, + 'course_id': self.course.id + } + ) + self.client.get(section_url) + info_url = reverse('info', args=(unicode(self.course.id),)) + info_page_response = self.client.get(info_url) + self.assertIn('Jump back to where you were last:', info_page_response.content) + class CourseInfoTestCaseCCX(SharedModuleStoreTestCase, LoginEnrollmentTestCase): """ @@ -169,6 +197,7 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest """ def setUp(self): + SelfPacedConfiguration(enabled=True).save() super(SelfPacedCourseInfoTestCase, self).setUp() self.instructor_paced_course = CourseFactory.create(self_paced=False) self.self_paced_course = CourseFactory.create(self_paced=True) diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 31eaab5b2c..85473e46f1 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -72,6 +72,7 @@ from openedx.core.djangoapps.credit.api import ( ) 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 @@ -549,8 +550,6 @@ def _index_bulk_op(request, course_key, chapter, section, position): context['fragment'] = section_module.render(STUDENT_VIEW, section_render_context) context['section_title'] = section_descriptor.display_name_with_default_escaped else: - # section is none, so display a message - studio_url = get_studio_url(course, 'course') prev_section = get_current_child(chapter_module) if prev_section is None: # Something went wrong -- perhaps this chapter has no sections visible to the user. @@ -559,22 +558,6 @@ def _index_bulk_op(request, course_key, chapter, section, position): course_module.position = None course_module.save() return redirect(reverse('courseware', args=[course.id.to_deprecated_string()])) - prev_section_url = reverse('courseware_section', kwargs={ - 'course_id': course_key.to_deprecated_string(), - 'chapter': chapter_descriptor.url_name, - 'section': prev_section.url_name - }) - context['fragment'] = Fragment(content=render_to_string( - 'courseware/welcome-back.html', - { - 'course': course, - 'studio_url': studio_url, - 'chapter_module': chapter_module, - 'prev_section': prev_section, - 'prev_section_url': prev_section_url - } - )) - result = render_to_response('courseware/courseware.html', context) except Exception as e: @@ -729,6 +712,14 @@ def course_info(request, course_id): 'url_to_enroll': url_to_enroll, } + # Get the URL of the user's last position in order to display the 'where you were last' message + context['last_accessed_courseware'] = None + if SelfPacedConfiguration.current().enable_course_home_improvements: + (section_module, section_url) = get_last_accessed_courseware(course, request) + if section_module is not None and section_url is not None: + context['last_accessed_courseware'] = section_module + context['last_accessed_url'] = section_url + 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: @@ -739,6 +730,30 @@ def course_info(request, course_id): return render_to_response('courseware/info.html', context) +def get_last_accessed_courseware(course, request): + """ + Return a pair of the last-accessed courseware for this request's + user, and a URL for that module. + """ + field_data_cache = FieldDataCache.cache_for_descriptor_descendents( + course.id, request.user, course, depth=2 + ) + course_module = get_module_for_descriptor( + request.user, request, course, field_data_cache, course.id, course=course + ) + chapter_module = get_current_child(course_module) + if chapter_module is not None: + section_module = get_current_child(chapter_module) + if section_module is not None: + url = reverse('courseware_section', kwargs={ + 'course_id': unicode(course.id), + 'chapter': chapter_module.url_name, + 'section': section_module.url_name + }) + return (section_module, url) + return (None, None) + + @ensure_csrf_cookie @ensure_valid_course_key def static_tab(request, course_id, tab_slug): diff --git a/lms/static/sass/base/_variables.scss b/lms/static/sass/base/_variables.scss index c7211d9b86..6e68f74e90 100644 --- a/lms/static/sass/base/_variables.scss +++ b/lms/static/sass/base/_variables.scss @@ -465,6 +465,7 @@ $courseware-navigation-color: $blue !default; $homepage__header--gradient__color--alpha: lighten($gray, 15%) !default; $homepage__header--gradient__color--bravo: saturate($gray, 30%) !default; $homepage__header--background: lighten($gray, 15%) !default; +$homepage-background: rgb(252, 252, 252); $course-card-height: ($baseline*18) !default; $course-image-height: ($baseline*8) !default; $course-info-height: ($baseline*10) !default; diff --git a/lms/static/sass/course/_info.scss b/lms/static/sass/course/_info.scss index 8ccef38430..7831d5d749 100644 --- a/lms/static/sass/course/_info.scss +++ b/lms/static/sass/course/_info.scss @@ -1,9 +1,13 @@ -.home-wrapper { - max-width: 1180px; +.home { + @include clearfix(); + max-width: 1140px; margin: 0 auto; + padding: $baseline $baseline ($baseline/2) $baseline; - .home { - margin: $baseline; + .page-header-main { + display: inline-block; + width: flex-grid(8, 12); + margin: 0; .page-title { margin-bottom: 5px; @@ -17,11 +21,29 @@ text-transform: none; } } + + .page-header-secondary { + display: inline-block; + width: flex-grid(4, 12); + margin: 0; + padding: ($baseline/2) ($baseline*0.75); + border: 1px solid $blue; + background-color: $homepage-background; + @extend %t-title8; + color: $blue-d1; + @extend %cont-truncated; + vertical-align: text-bottom; + + .last-accessed-message { + display: inline-block; + @include margin-left($baseline*0.75); + } + } } div.info-wrapper { - background-color: rgb(252, 252, 252); + background-color: $homepage-background; section.updates { @extend .content; @@ -63,7 +85,7 @@ div.info-wrapper { margin-bottom: ($baseline/4); text-transform: none; background: url('#{$static-path}/images/calendar-icon.png') 0 center no-repeat; - padding-left: $baseline; + @include padding-left($baseline); } section.update-description { diff --git a/lms/static/sass/course/layout/_courseware_header.scss b/lms/static/sass/course/layout/_courseware_header.scss index b88275cbbc..71440c9727 100644 --- a/lms/static/sass/course/layout/_courseware_header.scss +++ b/lms/static/sass/course/layout/_courseware_header.scss @@ -22,7 +22,6 @@ list-style: none; &.prominent { - margin-right: 16px; @include margin-right(16px); background: rgba(255, 255, 255, 0.5); border-radius: 3px; diff --git a/lms/templates/courseware/info.html b/lms/templates/courseware/info.html index 8cdf6b47be..9891959aa3 100644 --- a/lms/templates/courseware/info.html +++ b/lms/templates/courseware/info.html @@ -45,11 +45,18 @@ from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration <%block name="bodyclass">view-in-course view-course-info ${course.css_class or ''}
-
-
+
+

${_("Welcome to {org}'s {course_name}!").format(org=course.id.org, course_name=course.id.course) | h}

${course.display_name | h}

-
+
+ % if last_accessed_courseware: +
+ +

${_("Jump back to where you were last:")}

+ ${last_accessed_courseware.display_name | h} +
+ % endif
% if user.is_authenticated():