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):
...
@@ -16,6 +16,8 @@ class CourseHomePage(CoursePage):
url_path
=
"course/"
url_path
=
"course/"
HEADER_RESUME_COURSE_SELECTOR
=
'.page-header .action-resume-course'
def
is_browser_on_page
(
self
):
def
is_browser_on_page
(
self
):
return
self
.
q
(
css
=
'.course-outline'
)
.
present
return
self
.
q
(
css
=
'.course-outline'
)
.
present
...
@@ -32,6 +34,14 @@ class CourseHomePage(CoursePage):
...
@@ -32,6 +34,14 @@ class CourseHomePage(CoursePage):
bookmarks_page
=
BookmarksPage
(
self
.
browser
,
self
.
course_id
)
bookmarks_page
=
BookmarksPage
(
self
.
browser
,
self
.
course_id
)
bookmarks_page
.
visit
()
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
):
class
CourseOutlinePage
(
PageObject
):
"""
"""
...
@@ -40,10 +50,15 @@ class CourseOutlinePage(PageObject):
...
@@ -40,10 +50,15 @@ class CourseOutlinePage(PageObject):
url
=
None
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
):
def
__init__
(
self
,
browser
,
parent_page
):
super
(
CourseOutlinePage
,
self
)
.
__init__
(
browser
)
super
(
CourseOutlinePage
,
self
)
.
__init__
(
browser
)
self
.
parent_page
=
parent_page
self
.
parent_page
=
parent_page
self
.
courseware_page
=
CoursewarePage
(
self
.
browser
,
self
.
parent_page
.
course_id
)
def
is_browser_on_page
(
self
):
def
is_browser_on_page
(
self
):
return
self
.
parent_page
.
is_browser_on_page
return
self
.
parent_page
.
is_browser_on_page
...
@@ -105,43 +120,34 @@ class CourseOutlinePage(PageObject):
...
@@ -105,43 +120,34 @@ class CourseOutlinePage(PageObject):
return
return
# Convert list indices (start at zero) to CSS indices (start at 1)
# Convert list indices (start at zero) to CSS indices (start at 1)
subsection_css
=
(
subsection_css
=
self
.
SUBSECTION_SELECTOR
.
format
(
section_index
+
1
,
subsection_index
+
1
)
".outline-item.section:nth-of-type({0}) .subsection:nth-of-type({1}) .outline-item"
)
.
format
(
section_index
+
1
,
subsection_index
+
1
)
# Click the subsection and ensure that the page finishes reloading
# Click the subsection and ensure that the page finishes reloading
self
.
q
(
css
=
subsection_css
)
.
first
.
click
()
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
)
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
):
def
_section_titles
(
self
):
"""
"""
Return a list of all section titles on the page.
Return a list of all section titles on the page.
"""
"""
section_css
=
'.section-name span'
return
self
.
q
(
css
=
self
.
SECTION_TITLES_SELECTOR
)
.
map
(
lambda
el
:
el
.
text
.
strip
())
.
results
return
self
.
q
(
css
=
section_css
)
.
map
(
lambda
el
:
el
.
text
.
strip
())
.
results
def
_subsection_titles
(
self
,
section_index
):
def
_subsection_titles
(
self
,
section_index
):
"""
"""
Return a list of all subsection titles on the page
Return a list of all subsection titles on the page
for the section at index `section_index` (starts at 1).
for the section at index `section_index` (starts at 1).
"""
"""
# Retrieve the subsection title for the section
subsection_css
=
self
.
SUBSECTION_TITLES_SELECTOR
.
format
(
section_index
)
# Add one to the list index to get the CSS index, which starts at one
return
self
.
q
(
css
=
subsection_css
)
.
map
(
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
(
lambda
el
:
el
.
get_attribute
(
'innerHTML'
)
.
strip
()
lambda
el
:
el
.
get_attribute
(
'innerHTML'
)
.
strip
()
)
.
results
)
.
results
...
@@ -149,7 +155,14 @@ class CourseOutlinePage(PageObject):
...
@@ -149,7 +155,14 @@ class CourseOutlinePage(PageObject):
"""
"""
Ensures the user navigates to the course content page with the correct section and subsection.
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
(
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
)
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):
...
@@ -860,6 +860,16 @@ class HighLevelTabTest(UniqueCourseTest):
bookmarks_page
=
BookmarksPage
(
self
.
browser
,
self
.
course_id
)
bookmarks_page
=
BookmarksPage
(
self
.
browser
,
self
.
course_id
)
self
.
assertTrue
(
bookmarks_page
.
is_browser_on_page
())
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'
)
@attr
(
'a11y'
)
def
test_course_home_a11y
(
self
):
def
test_course_home_a11y
(
self
):
self
.
course_home_page
.
visit
()
self
.
course_home_page
.
visit
()
...
...
lms/djangoapps/courseware/views/views.py
View file @
1b3a3a23
...
@@ -392,7 +392,7 @@ def course_info(request, course_id):
...
@@ -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
# 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
context
[
'last_accessed_courseware_url'
]
=
None
if
SelfPacedConfiguration
.
current
()
.
enable_course_home_improvements
:
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
())
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
)
...
@@ -427,8 +427,8 @@ def course_info(request, course_id):
...
@@ -427,8 +427,8 @@ def course_info(request, course_id):
def
get_last_accessed_courseware
(
course
,
request
,
user
):
def
get_last_accessed_courseware
(
course
,
request
,
user
):
"""
"""
Return
the courseware module URL
that the user last accessed,
Return
s a tuple containing the courseware module (URL, id)
that the user last accessed,
or
None
if it cannot be found.
or
(None, None)
if it cannot be found.
"""
"""
field_data_cache
=
FieldDataCache
.
cache_for_descriptor_descendents
(
field_data_cache
=
FieldDataCache
.
cache_for_descriptor_descendents
(
course
.
id
,
request
.
user
,
course
,
depth
=
2
course
.
id
,
request
.
user
,
course
,
depth
=
2
...
@@ -445,8 +445,8 @@ def get_last_accessed_courseware(course, request, user):
...
@@ -445,8 +445,8 @@ def get_last_accessed_courseware(course, request, user):
'chapter'
:
chapter_module
.
url_name
,
'chapter'
:
chapter_module
.
url_name
,
'section'
:
section_module
.
url_name
'section'
:
section_module
.
url_name
})
})
return
url
return
(
url
,
section_module
.
url_name
)
return
None
return
(
None
,
None
)
class
StaticCourseTabView
(
FragmentView
):
class
StaticCourseTabView
(
FragmentView
):
...
...
lms/static/sass/features/_course-outline.scss
View file @
1b3a3a23
...
@@ -43,6 +43,14 @@
...
@@ -43,6 +43,14 @@
text-decoration
:
none
;
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 _
...
@@ -27,13 +27,25 @@ from django.utils.translation import ugettext as _
</div>
</div>
<ol
class=
"outline-item focusable"
role=
"group"
tabindex=
"0"
>
<ol
class=
"outline-item focusable"
role=
"group"
tabindex=
"0"
>
% for subsection in section.get('children') or []:
% 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
<a
class=
"outline-item focusable"
class=
"outline-item focusable"
href=
"${ subsection['lms_web_url'] }"
href=
"${ subsection['lms_web_url'] }"
id=
"${ subsection['id'] }"
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>
</a>
</li>
</li>
% endfor
% endfor
...
...
openedx/features/course_experience/tests/views/test_course_outline.py
View file @
1b3a3a23
"""
"""
Tests for the Course Outline view and supporting views.
Tests for the Course Outline view and supporting views.
"""
"""
from
mock
import
patch
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
from
student.models
import
CourseEnrollment
from
student.models
import
CourseEnrollment
...
@@ -26,6 +27,7 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
...
@@ -26,6 +27,7 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
chapter
=
ItemFactory
.
create
(
category
=
'chapter'
,
parent_location
=
course
.
location
)
chapter
=
ItemFactory
.
create
(
category
=
'chapter'
,
parent_location
=
course
.
location
)
section
=
ItemFactory
.
create
(
category
=
'sequential'
,
parent_location
=
chapter
.
location
)
section
=
ItemFactory
.
create
(
category
=
'sequential'
,
parent_location
=
chapter
.
location
)
ItemFactory
.
create
(
category
=
'vertical'
,
parent_location
=
section
.
location
)
ItemFactory
.
create
(
category
=
'vertical'
,
parent_location
=
section
.
location
)
course
.
last_accessed
=
section
.
url_name
cls
.
courses
.
append
(
course
)
cls
.
courses
.
append
(
course
)
...
@@ -36,6 +38,7 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
...
@@ -36,6 +38,7 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
section2
=
ItemFactory
.
create
(
category
=
'sequential'
,
parent_location
=
chapter
.
location
)
section2
=
ItemFactory
.
create
(
category
=
'sequential'
,
parent_location
=
chapter
.
location
)
ItemFactory
.
create
(
category
=
'vertical'
,
parent_location
=
section
.
location
)
ItemFactory
.
create
(
category
=
'vertical'
,
parent_location
=
section
.
location
)
ItemFactory
.
create
(
category
=
'vertical'
,
parent_location
=
section2
.
location
)
ItemFactory
.
create
(
category
=
'vertical'
,
parent_location
=
section2
.
location
)
course
.
last_accessed
=
None
@classmethod
@classmethod
def
setUpTestData
(
cls
):
def
setUpTestData
(
cls
):
...
@@ -52,8 +55,10 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
...
@@ -52,8 +55,10 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
super
(
TestCourseOutlinePage
,
self
)
.
setUp
()
super
(
TestCourseOutlinePage
,
self
)
.
setUp
()
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
password
)
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
:
for
course
in
self
.
courses
:
patched_get_last_accessed
.
return_value
=
(
None
,
course
.
last_accessed
)
url
=
reverse
(
url
=
reverse
(
'edx.course_experience.course_home'
,
'edx.course_experience.course_home'
,
kwargs
=
{
kwargs
=
{
...
@@ -64,6 +69,10 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
...
@@ -64,6 +69,10 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response_content
=
response
.
content
.
decode
(
"utf-8"
)
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
:
for
chapter
in
course
.
children
:
self
.
assertIn
(
chapter
.
display_name
,
response_content
)
self
.
assertIn
(
chapter
.
display_name
,
response_content
)
for
section
in
chapter
.
children
:
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
...
@@ -6,6 +6,7 @@ from django.core.context_processors import csrf
from
django.template.loader
import
render_to_string
from
django.template.loader
import
render_to_string
from
courseware.courses
import
get_course_with_access
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
lms.djangoapps.course_api.blocks.api
import
get_blocks
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.keys
import
CourseKey
from
web_fragments.fragment
import
Fragment
from
web_fragments.fragment
import
Fragment
...
@@ -18,7 +19,7 @@ class CourseOutlineFragmentView(FragmentView):
...
@@ -18,7 +19,7 @@ class CourseOutlineFragmentView(FragmentView):
Course outline fragment to be shown in the unified course view.
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,
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.
which will be looked up by id in the passed all_blocks dict.
...
@@ -28,8 +29,9 @@ class CourseOutlineFragmentView(FragmentView):
...
@@ -28,8 +29,9 @@ class CourseOutlineFragmentView(FragmentView):
for
i
in
range
(
len
(
children
)):
for
i
in
range
(
len
(
children
)):
child_id
=
block
[
'children'
][
i
]
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
]
=
child_detail
block
[
'children'
][
i
][
'current'
]
=
course_position
==
child_detail
[
'block_id'
]
return
block
return
block
...
@@ -39,6 +41,7 @@ class CourseOutlineFragmentView(FragmentView):
...
@@ -39,6 +41,7 @@ class CourseOutlineFragmentView(FragmentView):
"""
"""
course_key
=
CourseKey
.
from_string
(
course_id
)
course_key
=
CourseKey
.
from_string
(
course_id
)
course
=
get_course_with_access
(
request
.
user
,
'load'
,
course_key
,
check_if_enrolled
=
True
)
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
)
course_usage_key
=
modulestore
()
.
make_course_usage_key
(
course_key
)
all_blocks
=
get_blocks
(
all_blocks
=
get_blocks
(
request
,
request
,
...
@@ -55,7 +58,7 @@ class CourseOutlineFragmentView(FragmentView):
...
@@ -55,7 +58,7 @@ class CourseOutlineFragmentView(FragmentView):
'csrf'
:
csrf
(
request
)[
'csrf_token'
],
'csrf'
:
csrf
(
request
)[
'csrf_token'
],
'course'
:
course
,
'course'
:
course
,
# Recurse through the block tree, fleshing out each child object
# 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
)
html
=
render_to_string
(
'course_experience/course-outline-fragment.html'
,
context
)
return
Fragment
(
html
)
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