Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
1b3a3a23
Commit
1b3a3a23
authored
Mar 24, 2017
by
Brian Jacobel
Committed by
GitHub
Mar 24, 2017
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #14637 from edx/bjacobel/resume-outline
Add resume indicator to course outline
parents
eb34d3fe
22203453
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
89 additions
and
34 deletions
+89
-34
common/test/acceptance/pages/lms/course_home.py
+36
-23
common/test/acceptance/tests/lms/test_lms.py
+10
-0
lms/djangoapps/courseware/views/views.py
+5
-5
lms/static/sass/features/_course-outline.scss
+8
-0
openedx/features/course_experience/templates/course_experience/course-outline-fragment.html
+14
-2
openedx/features/course_experience/tests/views/test_course_outline.py
+10
-1
openedx/features/course_experience/views/course_outline.py
+6
-3
No files found.
common/test/acceptance/pages/lms/course_home.py
View file @
1b3a3a23
...
...
@@ -16,6 +16,8 @@ class CourseHomePage(CoursePage):
url_path
=
"course/"
HEADER_RESUME_COURSE_SELECTOR
=
'.page-header .action-resume-course'
def
is_browser_on_page
(
self
):
return
self
.
q
(
css
=
'.course-outline'
)
.
present
...
...
@@ -32,6 +34,14 @@ class CourseHomePage(CoursePage):
bookmarks_page
=
BookmarksPage
(
self
.
browser
,
self
.
course_id
)
bookmarks_page
.
visit
()
def
resume_course_from_header
(
self
):
"""
Navigate to courseware using Resume Course button in the header.
"""
self
.
q
(
css
=
self
.
HEADER_RESUME_COURSE_SELECTOR
)
.
first
.
click
()
courseware_page
=
CoursewarePage
(
self
.
browser
,
self
.
course_id
)
courseware_page
.
wait_for_page
()
class
CourseOutlinePage
(
PageObject
):
"""
...
...
@@ -40,10 +50,15 @@ class CourseOutlinePage(PageObject):
url
=
None
SECTION_SELECTOR
=
'.outline-item.section:nth-of-type({0})'
SECTION_TITLES_SELECTOR
=
'.section-name span'
SUBSECTION_SELECTOR
=
SECTION_SELECTOR
+
' .subsection:nth-of-type({1}) .outline-item'
SUBSECTION_TITLES_SELECTOR
=
SECTION_SELECTOR
+
' .subsection a span:first-child'
OUTLINE_RESUME_COURSE_SELECTOR
=
'.outline-item .resume-right'
def
__init__
(
self
,
browser
,
parent_page
):
super
(
CourseOutlinePage
,
self
)
.
__init__
(
browser
)
self
.
parent_page
=
parent_page
self
.
courseware_page
=
CoursewarePage
(
self
.
browser
,
self
.
parent_page
.
course_id
)
def
is_browser_on_page
(
self
):
return
self
.
parent_page
.
is_browser_on_page
...
...
@@ -105,43 +120,34 @@ class CourseOutlinePage(PageObject):
return
# Convert list indices (start at zero) to CSS indices (start at 1)
subsection_css
=
(
".outline-item.section:nth-of-type({0}) .subsection:nth-of-type({1}) .outline-item"
)
.
format
(
section_index
+
1
,
subsection_index
+
1
)
subsection_css
=
self
.
SUBSECTION_SELECTOR
.
format
(
section_index
+
1
,
subsection_index
+
1
)
# Click the subsection and ensure that the page finishes reloading
self
.
q
(
css
=
subsection_css
)
.
first
.
click
()
self
.
courseware_page
.
wait_for_page
()
# TODO: TNL-6546: Remove this if/visit_unified_course_view
if
self
.
parent_page
.
unified_course_view
:
self
.
courseware_page
.
nav
.
visit_unified_course_view
()
self
.
_wait_for_course_section
(
section_title
,
subsection_title
)
def
resume_course_from_outline
(
self
):
"""
Navigate to courseware using Resume Course button in the header.
"""
self
.
q
(
css
=
self
.
OUTLINE_RESUME_COURSE_SELECTOR
)
.
first
.
click
()
courseware_page
=
CoursewarePage
(
self
.
browser
,
self
.
parent_page
.
course_id
)
courseware_page
.
wait_for_page
()
def
_section_titles
(
self
):
"""
Return a list of all section titles on the page.
"""
section_css
=
'.section-name span'
return
self
.
q
(
css
=
section_css
)
.
map
(
lambda
el
:
el
.
text
.
strip
())
.
results
return
self
.
q
(
css
=
self
.
SECTION_TITLES_SELECTOR
)
.
map
(
lambda
el
:
el
.
text
.
strip
())
.
results
def
_subsection_titles
(
self
,
section_index
):
"""
Return a list of all subsection titles on the page
for the section at index `section_index` (starts at 1).
"""
# Retrieve the subsection title for the section
# Add one to the list index to get the CSS index, which starts at one
subsection_css
=
(
# TODO: TNL-6387: Will need to switch to this selector for subsections
# ".outline-item.section:nth-of-type({0}) .subsection span:nth-of-type(1)"
".outline-item.section:nth-of-type({0}) .subsection a"
)
.
format
(
section_index
)
return
self
.
q
(
css
=
subsection_css
)
.
map
(
subsection_css
=
self
.
SUBSECTION_TITLES_SELECTOR
.
format
(
section_index
)
return
self
.
q
(
css
=
subsection_css
)
.
map
(
lambda
el
:
el
.
get_attribute
(
'innerHTML'
)
.
strip
()
)
.
results
...
...
@@ -149,7 +155,14 @@ class CourseOutlinePage(PageObject):
"""
Ensures the user navigates to the course content page with the correct section and subsection.
"""
courseware_page
=
CoursewarePage
(
self
.
browser
,
self
.
parent_page
.
course_id
)
courseware_page
.
wait_for_page
()
# TODO: TNL-6546: Remove this if/visit_unified_course_view
if
self
.
parent_page
.
unified_course_view
:
courseware_page
.
nav
.
visit_unified_course_view
()
self
.
wait_for
(
promise_check_func
=
lambda
:
self
.
courseware_page
.
nav
.
is_on_section
(
section_title
,
subsection_title
),
promise_check_func
=
lambda
:
courseware_page
.
nav
.
is_on_section
(
section_title
,
subsection_title
),
description
=
"Waiting for course page with section '{0}' and subsection '{1}'"
.
format
(
section_title
,
subsection_title
)
)
common/test/acceptance/tests/lms/test_lms.py
View file @
1b3a3a23
...
...
@@ -860,6 +860,16 @@ class HighLevelTabTest(UniqueCourseTest):
bookmarks_page
=
BookmarksPage
(
self
.
browser
,
self
.
course_id
)
self
.
assertTrue
(
bookmarks_page
.
is_browser_on_page
())
# Test "Resume Course" button from header
self
.
course_home_page
.
visit
()
self
.
course_home_page
.
resume_course_from_header
()
self
.
assertTrue
(
self
.
courseware_page
.
nav
.
is_on_section
(
'Test Section 2'
,
'Test Subsection 3'
))
# Test "Resume Course" button from within outline
self
.
course_home_page
.
visit
()
self
.
course_home_page
.
outline
.
resume_course_from_outline
()
self
.
assertTrue
(
self
.
courseware_page
.
nav
.
is_on_section
(
'Test Section 2'
,
'Test Subsection 3'
))
@attr
(
'a11y'
)
def
test_course_home_a11y
(
self
):
self
.
course_home_page
.
visit
()
...
...
lms/djangoapps/courseware/views/views.py
View file @
1b3a3a23
...
...
@@ -392,7 +392,7 @@ def course_info(request, course_id):
# Get the URL of the user's last position in order to display the 'where you were last' message
context
[
'last_accessed_courseware_url'
]
=
None
if
SelfPacedConfiguration
.
current
()
.
enable_course_home_improvements
:
context
[
'last_accessed_courseware_url'
]
=
get_last_accessed_courseware
(
course
,
request
,
user
)
context
[
'last_accessed_courseware_url'
]
,
_
=
get_last_accessed_courseware
(
course
,
request
,
user
)
now
=
datetime
.
now
(
UTC
())
effective_start
=
_adjust_start_date_for_beta_testers
(
user
,
course
,
course_key
)
...
...
@@ -427,8 +427,8 @@ def course_info(request, course_id):
def
get_last_accessed_courseware
(
course
,
request
,
user
):
"""
Return
the courseware module URL
that the user last accessed,
or
None
if it cannot be found.
Return
s a tuple containing the courseware module (URL, id)
that the user last accessed,
or
(None, None)
if it cannot be found.
"""
field_data_cache
=
FieldDataCache
.
cache_for_descriptor_descendents
(
course
.
id
,
request
.
user
,
course
,
depth
=
2
...
...
@@ -445,8 +445,8 @@ def get_last_accessed_courseware(course, request, user):
'chapter'
:
chapter_module
.
url_name
,
'section'
:
section_module
.
url_name
})
return
url
return
None
return
(
url
,
section_module
.
url_name
)
return
(
None
,
None
)
class
StaticCourseTabView
(
FragmentView
):
...
...
lms/static/sass/features/_course-outline.scss
View file @
1b3a3a23
...
...
@@ -43,6 +43,14 @@
text-decoration
:
none
;
}
}
&
.current
{
border
:
1px
solid
$lms-active-color
;
.resume-right
{
@include
float
(
right
);
}
}
}
}
}
...
...
openedx/features/course_experience/templates/course_experience/course-outline-fragment.html
View file @
1b3a3a23
...
...
@@ -27,13 +27,25 @@ from django.utils.translation import ugettext as _
</div>
<ol
class=
"outline-item focusable"
role=
"group"
tabindex=
"0"
>
% for subsection in section.get('children') or []:
<li
class=
"subsection"
role=
"treeitem"
tabindex=
"-1"
aria-expanded=
"true"
>
<li
class=
"subsection ${ 'current' if subsection['current'] else '' }"
role=
"treeitem"
tabindex=
"-1"
aria-expanded=
"true"
>
<a
class=
"outline-item focusable"
href=
"${ subsection['lms_web_url'] }"
id=
"${ subsection['id'] }"
>
${ subsection['display_name'] }
<span>
${ subsection['display_name'] }
</span>
<span
class=
"sr-only"
>
${ _("This is your last visited course section.") }
</span>
% if subsection['current']:
<span
class=
"resume-right"
>
<b>
${ _("Resume Course") }
</b>
<span
class=
"icon fa fa-arrow-circle-right"
aria-hidden=
"true"
></span>
</span>
%endif
</a>
</li>
% endfor
...
...
openedx/features/course_experience/tests/views/test_course_outline.py
View file @
1b3a3a23
"""
Tests for the Course Outline view and supporting views.
"""
from
mock
import
patch
from
django.core.urlresolvers
import
reverse
from
student.models
import
CourseEnrollment
...
...
@@ -26,6 +27,7 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
chapter
=
ItemFactory
.
create
(
category
=
'chapter'
,
parent_location
=
course
.
location
)
section
=
ItemFactory
.
create
(
category
=
'sequential'
,
parent_location
=
chapter
.
location
)
ItemFactory
.
create
(
category
=
'vertical'
,
parent_location
=
section
.
location
)
course
.
last_accessed
=
section
.
url_name
cls
.
courses
.
append
(
course
)
...
...
@@ -36,6 +38,7 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
section2
=
ItemFactory
.
create
(
category
=
'sequential'
,
parent_location
=
chapter
.
location
)
ItemFactory
.
create
(
category
=
'vertical'
,
parent_location
=
section
.
location
)
ItemFactory
.
create
(
category
=
'vertical'
,
parent_location
=
section2
.
location
)
course
.
last_accessed
=
None
@classmethod
def
setUpTestData
(
cls
):
...
...
@@ -52,8 +55,10 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
super
(
TestCourseOutlinePage
,
self
)
.
setUp
()
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
password
)
def
test_render
(
self
):
@patch
(
'openedx.features.course_experience.views.course_outline.get_last_accessed_courseware'
)
def
test_render
(
self
,
patched_get_last_accessed
):
for
course
in
self
.
courses
:
patched_get_last_accessed
.
return_value
=
(
None
,
course
.
last_accessed
)
url
=
reverse
(
'edx.course_experience.course_home'
,
kwargs
=
{
...
...
@@ -64,6 +69,10 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
self
.
assertEqual
(
response
.
status_code
,
200
)
response_content
=
response
.
content
.
decode
(
"utf-8"
)
if
course
.
last_accessed
is
not
None
:
self
.
assertIn
(
'Resume Course'
,
response_content
)
else
:
self
.
assertNotIn
(
'Resume Course'
,
response_content
)
for
chapter
in
course
.
children
:
self
.
assertIn
(
chapter
.
display_name
,
response_content
)
for
section
in
chapter
.
children
:
...
...
openedx/features/course_experience/views/course_outline.py
View file @
1b3a3a23
...
...
@@ -6,6 +6,7 @@ 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.courseware.views.views
import
get_last_accessed_courseware
from
lms.djangoapps.course_api.blocks.api
import
get_blocks
from
opaque_keys.edx.keys
import
CourseKey
from
web_fragments.fragment
import
Fragment
...
...
@@ -18,7 +19,7 @@ class CourseOutlineFragmentView(FragmentView):
Course outline fragment to be shown in the unified course view.
"""
def
populate_children
(
self
,
block
,
all_blocks
):
def
populate_children
(
self
,
block
,
all_blocks
,
course_position
):
"""
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.
...
...
@@ -28,8 +29,9 @@ class CourseOutlineFragmentView(FragmentView):
for
i
in
range
(
len
(
children
)):
child_id
=
block
[
'children'
][
i
]
child_detail
=
self
.
populate_children
(
all_blocks
[
child_id
],
all_blocks
)
child_detail
=
self
.
populate_children
(
all_blocks
[
child_id
],
all_blocks
,
course_position
)
block
[
'children'
][
i
]
=
child_detail
block
[
'children'
][
i
][
'current'
]
=
course_position
==
child_detail
[
'block_id'
]
return
block
...
...
@@ -39,6 +41,7 @@ class CourseOutlineFragmentView(FragmentView):
"""
course_key
=
CourseKey
.
from_string
(
course_id
)
course
=
get_course_with_access
(
request
.
user
,
'load'
,
course_key
,
check_if_enrolled
=
True
)
_
,
course_position
=
get_last_accessed_courseware
(
course
,
request
,
request
.
user
)
course_usage_key
=
modulestore
()
.
make_course_usage_key
(
course_key
)
all_blocks
=
get_blocks
(
request
,
...
...
@@ -55,7 +58,7 @@ class CourseOutlineFragmentView(FragmentView):
'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'
])
'blocks'
:
self
.
populate_children
(
course_block_tree
,
all_blocks
[
'blocks'
]
,
course_position
)
}
html
=
render_to_string
(
'course_experience/course-outline-fragment.html'
,
context
)
return
Fragment
(
html
)
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment