Commit 786c4456 by Peter Fogg

Last-accessed courseware on the home page.

ECOM-2806
parent 96d030b6
...@@ -127,9 +127,9 @@ class LibraryContentTestBase(UniqueCourseTest): ...@@ -127,9 +127,9 @@ class LibraryContentTestBase(UniqueCourseTest):
Open library page in LMS Open library page in LMS
""" """
self.courseware_page.visit() self.courseware_page.visit()
paragraphs = self.courseware_page.q(css='.course-content p') paragraphs = self.courseware_page.q(css='.course-content p').results
if paragraphs and "You were most recently in" in paragraphs.text[0]: if not paragraphs:
paragraphs[0].find_element_by_tag_name('a').click() 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 block_id = block_id if block_id is not None else self.lib_block.locator
#pylint: disable=attribute-defined-outside-init #pylint: disable=attribute-defined-outside-init
self.library_content_page = LibraryContentXBlockWrapper(self.browser, block_id) self.library_content_page = LibraryContentXBlockWrapper(self.browser, block_id)
......
...@@ -19,10 +19,3 @@ Feature: LMS.Navigate Course ...@@ -19,10 +19,3 @@ Feature: LMS.Navigate Course
When I navigate to an item in a sequence When I navigate to an item in a sequence
Then I see the content of the sequence item Then I see the content of the sequence item
And a "seq_goto" browser event is emitted 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
...@@ -136,12 +136,6 @@ def and_i_return_to_the_course(step): ...@@ -136,12 +136,6 @@ def and_i_return_to_the_course(step):
world.css_click(course) 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(): def create_course():
world.clear_courses() world.clear_courses()
world.scenario_dict['COURSE'] = world.CourseFactory.create( world.scenario_dict['COURSE'] = world.CourseFactory.create(
......
...@@ -228,11 +228,16 @@ class FieldOverrideProvider(object): ...@@ -228,11 +228,16 @@ class FieldOverrideProvider(object):
@abstractmethod @abstractmethod
def enabled_for(self, course): # pragma no cover 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 return False
......
...@@ -25,4 +25,4 @@ class SelfPacedDateOverrideProvider(FieldOverrideProvider): ...@@ -25,4 +25,4 @@ class SelfPacedDateOverrideProvider(FieldOverrideProvider):
@classmethod @classmethod
def enabled_for(cls, course): def enabled_for(cls, course):
"""This provider is enabled for self-paced courses only.""" """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
...@@ -11,6 +11,7 @@ from django.core.urlresolvers import reverse ...@@ -11,6 +11,7 @@ from django.core.urlresolvers import reverse
from django.test.utils import override_settings from django.test.utils import override_settings
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from util.date_utils import strftime_localized from util.date_utils import strftime_localized
from xmodule.modulestore.tests.django_utils import ( from xmodule.modulestore.tests.django_utils import (
ModuleStoreTestCase, ModuleStoreTestCase,
...@@ -92,6 +93,33 @@ class CourseInfoTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase): ...@@ -92,6 +93,33 @@ class CourseInfoTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 404) 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): class CourseInfoTestCaseCCX(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
""" """
...@@ -169,6 +197,7 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest ...@@ -169,6 +197,7 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest
""" """
def setUp(self): def setUp(self):
SelfPacedConfiguration(enabled=True).save()
super(SelfPacedCourseInfoTestCase, self).setUp() super(SelfPacedCourseInfoTestCase, self).setUp()
self.instructor_paced_course = CourseFactory.create(self_paced=False) self.instructor_paced_course = CourseFactory.create(self_paced=False)
self.self_paced_course = CourseFactory.create(self_paced=True) self.self_paced_course = CourseFactory.create(self_paced=True)
......
...@@ -72,6 +72,7 @@ from openedx.core.djangoapps.credit.api import ( ...@@ -72,6 +72,7 @@ from openedx.core.djangoapps.credit.api import (
) )
from shoppingcart.models import CourseRegistrationCode from shoppingcart.models import CourseRegistrationCode
from shoppingcart.utils import is_shopping_cart_enabled 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.models import UserTestGroup, CourseEnrollment
from student.views import is_course_blocked from student.views import is_course_blocked
from util.cache import cache, cache_if_anonymous from util.cache import cache, cache_if_anonymous
...@@ -549,8 +550,6 @@ def _index_bulk_op(request, course_key, chapter, section, position): ...@@ -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['fragment'] = section_module.render(STUDENT_VIEW, section_render_context)
context['section_title'] = section_descriptor.display_name_with_default_escaped context['section_title'] = section_descriptor.display_name_with_default_escaped
else: else:
# section is none, so display a message
studio_url = get_studio_url(course, 'course')
prev_section = get_current_child(chapter_module) prev_section = get_current_child(chapter_module)
if prev_section is None: if prev_section is None:
# Something went wrong -- perhaps this chapter has no sections visible to the user. # 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): ...@@ -559,22 +558,6 @@ def _index_bulk_op(request, course_key, chapter, section, position):
course_module.position = None course_module.position = None
course_module.save() course_module.save()
return redirect(reverse('courseware', args=[course.id.to_deprecated_string()])) 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) result = render_to_response('courseware/courseware.html', context)
except Exception as e: except Exception as e:
...@@ -729,6 +712,14 @@ def course_info(request, course_id): ...@@ -729,6 +712,14 @@ def course_info(request, course_id):
'url_to_enroll': url_to_enroll, '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()) now = datetime.now(UTC())
effective_start = _adjust_start_date_for_beta_testers(user, course, course_key) effective_start = _adjust_start_date_for_beta_testers(user, course, course_key)
if not in_preview_mode() and staff_access and now < effective_start: if not in_preview_mode() and staff_access and now < effective_start:
...@@ -739,6 +730,30 @@ def course_info(request, course_id): ...@@ -739,6 +730,30 @@ def course_info(request, course_id):
return render_to_response('courseware/info.html', context) 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_csrf_cookie
@ensure_valid_course_key @ensure_valid_course_key
def static_tab(request, course_id, tab_slug): def static_tab(request, course_id, tab_slug):
......
...@@ -465,6 +465,7 @@ $courseware-navigation-color: $blue !default; ...@@ -465,6 +465,7 @@ $courseware-navigation-color: $blue !default;
$homepage__header--gradient__color--alpha: lighten($gray, 15%) !default; $homepage__header--gradient__color--alpha: lighten($gray, 15%) !default;
$homepage__header--gradient__color--bravo: saturate($gray, 30%) !default; $homepage__header--gradient__color--bravo: saturate($gray, 30%) !default;
$homepage__header--background: lighten($gray, 15%) !default; $homepage__header--background: lighten($gray, 15%) !default;
$homepage-background: rgb(252, 252, 252);
$course-card-height: ($baseline*18) !default; $course-card-height: ($baseline*18) !default;
$course-image-height: ($baseline*8) !default; $course-image-height: ($baseline*8) !default;
$course-info-height: ($baseline*10) !default; $course-info-height: ($baseline*10) !default;
......
.home-wrapper { .home {
max-width: 1180px; @include clearfix();
max-width: 1140px;
margin: 0 auto; margin: 0 auto;
padding: $baseline $baseline ($baseline/2) $baseline;
.home { .page-header-main {
margin: $baseline; display: inline-block;
width: flex-grid(8, 12);
margin: 0;
.page-title { .page-title {
margin-bottom: 5px; margin-bottom: 5px;
...@@ -17,11 +21,29 @@ ...@@ -17,11 +21,29 @@
text-transform: none; 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 { div.info-wrapper {
background-color: rgb(252, 252, 252); background-color: $homepage-background;
section.updates { section.updates {
@extend .content; @extend .content;
...@@ -63,7 +85,7 @@ div.info-wrapper { ...@@ -63,7 +85,7 @@ div.info-wrapper {
margin-bottom: ($baseline/4); margin-bottom: ($baseline/4);
text-transform: none; text-transform: none;
background: url('#{$static-path}/images/calendar-icon.png') 0 center no-repeat; background: url('#{$static-path}/images/calendar-icon.png') 0 center no-repeat;
padding-left: $baseline; @include padding-left($baseline);
} }
section.update-description { section.update-description {
......
...@@ -22,7 +22,6 @@ ...@@ -22,7 +22,6 @@
list-style: none; list-style: none;
&.prominent { &.prominent {
margin-right: 16px;
@include margin-right(16px); @include margin-right(16px);
background: rgba(255, 255, 255, 0.5); background: rgba(255, 255, 255, 0.5);
border-radius: 3px; border-radius: 3px;
......
...@@ -45,11 +45,18 @@ from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration ...@@ -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 ''}</%block> <%block name="bodyclass">view-in-course view-course-info ${course.css_class or ''}</%block>
<section class="container"> <section class="container">
<div class="home-wrapper"> <div class="home">
<section class="home"> <div class="page-header-main">
<h1 class="page-title">${_("Welcome to {org}'s {course_name}!").format(org=course.id.org, course_name=course.id.course) | h}</h1> <h1 class="page-title">${_("Welcome to {org}'s {course_name}!").format(org=course.id.org, course_name=course.id.course) | h}</h1>
<h2 class="page-subtitle">${course.display_name | h}</h2> <h2 class="page-subtitle">${course.display_name | h}</h2>
</section> </div>
% if last_accessed_courseware:
<div class="page-header-secondary">
<i class="fa fa-clock-o"></i>
<p class="last-accessed-message">${_("Jump back to where you were last:")}</p>
<a href="${last_accessed_url}">${last_accessed_courseware.display_name | h}</a>
</div>
% endif
</div> </div>
<div class="info-wrapper"> <div class="info-wrapper">
% if user.is_authenticated(): % if user.is_authenticated():
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment