Merge pull request #14967 from edx/andya/empty-course-message
Add an empty course message to the course home page
This commit is contained in:
@@ -25,4 +25,4 @@
|
||||
|
||||
// Features
|
||||
@import 'features/bookmarks';
|
||||
@import 'features/course-outline';
|
||||
@import 'features/course-experience';
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
// Course sidebar
|
||||
.course-sidebar {
|
||||
@include margin-left(0);
|
||||
@include padding-left($baseline);
|
||||
}
|
||||
|
||||
// Course outline
|
||||
.course-outline {
|
||||
color: $lms-gray;
|
||||
|
||||
@@ -64,7 +64,7 @@ from openedx.features.course_experience import UNIFIED_COURSE_EXPERIENCE_FLAG
|
||||
<main class="layout-col layout-col-b">
|
||||
${HTML(outline_fragment.body_html())}
|
||||
</main>
|
||||
<aside class="layout-col layout-col-a">
|
||||
<aside class="course-sidebar layout-col layout-col-a">
|
||||
<div class="section section-tools">
|
||||
<h3 class="hd-6">${_("Course Tools")}</h3>
|
||||
<ul class="list-unstyled">
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<%!
|
||||
from datetime import date
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
%>
|
||||
|
||||
<%static:require_module_async module_name="course_experience/js/course_outline_factory" class_name="CourseOutlineFactory">
|
||||
@@ -13,65 +17,77 @@ from django.utils.translation import ugettext as _
|
||||
</%static:require_module_async>
|
||||
|
||||
<main role="main" class="course-outline" id="main" tabindex="-1">
|
||||
<ol class="block-tree" role="tree">
|
||||
% for section in blocks.get('children') or []:
|
||||
<li
|
||||
aria-expanded="true"
|
||||
class="outline-item focusable section"
|
||||
id="${ section['id'] }"
|
||||
role="treeitem"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="section-name">
|
||||
<h3>${ section['display_name'] }</h3>
|
||||
</div>
|
||||
<ol class="outline-item focusable" role="group" tabindex="0">
|
||||
% for subsection in section.get('children') or []:
|
||||
<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'] }"
|
||||
% if blocks.get('children'):
|
||||
<ol class="block-tree" role="tree">
|
||||
% for section in blocks.get('children'):
|
||||
<li
|
||||
aria-expanded="true"
|
||||
class="outline-item focusable section"
|
||||
id="${ section['id'] }"
|
||||
role="treeitem"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="section-name">
|
||||
<h3>${ section['display_name'] }</h3>
|
||||
</div>
|
||||
<ol class="outline-item focusable" role="group" tabindex="0">
|
||||
% for subsection in section.get('children') or []:
|
||||
<li
|
||||
class="subsection ${ 'current' if subsection['current'] else '' }"
|
||||
role="treeitem"
|
||||
tabindex="-1"
|
||||
aria-expanded="true"
|
||||
>
|
||||
<div class="subsection-text">
|
||||
## Subsection title
|
||||
<span class="subsection-title">${ subsection['display_name'] }</span>
|
||||
<a
|
||||
class="outline-item focusable"
|
||||
href="${ subsection['lms_web_url'] }"
|
||||
id="${ subsection['id'] }"
|
||||
>
|
||||
<div class="subsection-text">
|
||||
## Subsection title
|
||||
<span class="subsection-title">${ subsection['display_name'] }</span>
|
||||
|
||||
<div class="details">
|
||||
## There are behavior differences between rendering of subsections which have
|
||||
## exams (timed, graded, etc) and those that do not.
|
||||
##
|
||||
## Exam subsections expose exam status message field as well as a status icon
|
||||
<%
|
||||
if subsection.get('due') is None:
|
||||
# examples: Homework, Lab, etc.
|
||||
data_string = subsection.get('format')
|
||||
else:
|
||||
if 'special_exam_info' in subsection:
|
||||
data_string = _('due {date}')
|
||||
<div class="details">
|
||||
## There are behavior differences between rendering of subsections which have
|
||||
## exams (timed, graded, etc) and those that do not.
|
||||
##
|
||||
## Exam subsections expose exam status message field as well as a status icon
|
||||
<%
|
||||
if subsection.get('due') is None:
|
||||
# examples: Homework, Lab, etc.
|
||||
data_string = subsection.get('format')
|
||||
else:
|
||||
data_string = _("{subsection_format} due {{date}}").format(subsection_format=subsection.get('format'))
|
||||
%>
|
||||
% if subsection.get('format') or 'special_exam_info' in subsection:
|
||||
<span class="subtitle">
|
||||
% if 'special_exam' in subsection:
|
||||
## Display the exam status icon and status message
|
||||
<span
|
||||
class="menu-icon icon fa ${subsection['special_exam_info'].get('suggested_icon', 'fa-pencil-square-o')} ${subsection['special_exam_info'].get('status', 'eligible')}"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="subtitle-name">
|
||||
${subsection['special_exam_info'].get('short_description', '')}
|
||||
</span>
|
||||
if 'special_exam_info' in subsection:
|
||||
data_string = _('due {date}')
|
||||
else:
|
||||
data_string = _("{subsection_format} due {{date}}").format(subsection_format=subsection.get('format'))
|
||||
%>
|
||||
% if subsection.get('format') or 'special_exam_info' in subsection:
|
||||
<span class="subtitle">
|
||||
% if 'special_exam' in subsection:
|
||||
## Display the exam status icon and status message
|
||||
<span
|
||||
class="menu-icon icon fa ${subsection['special_exam_info'].get('suggested_icon', 'fa-pencil-square-o')} ${subsection['special_exam_info'].get('status', 'eligible')}"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="subtitle-name">
|
||||
${subsection['special_exam_info'].get('short_description', '')}
|
||||
</span>
|
||||
|
||||
## completed exam statuses should not show the due date
|
||||
## since the exam has already been submitted by the user
|
||||
% if not subsection['special_exam_info'].get('in_completed_state', False):
|
||||
## completed exam statuses should not show the due date
|
||||
## since the exam has already been submitted by the user
|
||||
% if not subsection['special_exam_info'].get('in_completed_state', False):
|
||||
<span
|
||||
class="localized-datetime subtitle-name"
|
||||
data-datetime="${subsection.get('due')}"
|
||||
data-string="${data_string}"
|
||||
data-timezone="${user_timezone}"
|
||||
data-language="${user_language}"
|
||||
></span>
|
||||
% endif
|
||||
% else:
|
||||
## non-graded section, we just show the exam format and the due date
|
||||
## this is the standard case in edx-platform
|
||||
<span
|
||||
class="localized-datetime subtitle-name"
|
||||
data-datetime="${subsection.get('due')}"
|
||||
@@ -79,47 +95,69 @@ from django.utils.translation import ugettext as _
|
||||
data-timezone="${user_timezone}"
|
||||
data-language="${user_language}"
|
||||
></span>
|
||||
% endif
|
||||
% else:
|
||||
## non-graded section, we just show the exam format and the due date
|
||||
## this is the standard case in edx-platform
|
||||
<span
|
||||
class="localized-datetime subtitle-name"
|
||||
data-datetime="${subsection.get('due')}"
|
||||
data-string="${data_string}"
|
||||
data-timezone="${user_timezone}"
|
||||
data-language="${user_language}"
|
||||
></span>
|
||||
|
||||
% if 'graded' in subsection and subsection['graded']:
|
||||
<span
|
||||
class="menu-icon icon fa fa-pencil-square-o"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="sr">${_("This content is graded")}</span>
|
||||
% if 'graded' in subsection and subsection['graded']:
|
||||
<span
|
||||
class="menu-icon icon fa fa-pencil-square-o"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="sr">${_("This content is graded")}</span>
|
||||
% endif
|
||||
% endif
|
||||
</span>
|
||||
% endif
|
||||
</span>
|
||||
% endif
|
||||
</div> <!-- /details -->
|
||||
</div> <!-- /subsection-text -->
|
||||
<div class="subsection-actions">
|
||||
## Resume button (if last visited section)
|
||||
% if subsection['current']:
|
||||
<span class="sr-only">${ _("This is your last visited course section.") }</span>
|
||||
<span class="resume-right">
|
||||
<b>${ _("Resume Course") }</b>
|
||||
<span class="icon fa fa-arrow-circle-right" aria-hidden="true"></span>
|
||||
</span>
|
||||
%endif
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
% endfor
|
||||
</ol>
|
||||
</li>
|
||||
% endfor
|
||||
</ol>
|
||||
</div> <!-- /details -->
|
||||
</div> <!-- /subsection-text -->
|
||||
<div class="subsection-actions">
|
||||
## Resume button (if last visited section)
|
||||
% if subsection['current']:
|
||||
<span class="sr-only">${ _("This is your last visited course section.") }</span>
|
||||
<span class="resume-right">
|
||||
<b>${ _("Resume Course") }</b>
|
||||
<span class="icon fa fa-arrow-circle-right" aria-hidden="true"></span>
|
||||
</span>
|
||||
%endif
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
% endfor
|
||||
</ol>
|
||||
</li>
|
||||
% endfor
|
||||
</ol>
|
||||
% else:
|
||||
<div class="well depth-0 message-area">
|
||||
<div class="copy-large">
|
||||
<span class="icon fa fa-calendar-o" aria-hidden="true"></span>
|
||||
<%
|
||||
course_started = course.start.date() <= date.today()
|
||||
%>
|
||||
|
||||
% if course.start_date_is_still_default:
|
||||
${_("This course has not started yet.")}
|
||||
% elif course_started:
|
||||
${_("We're still working on course content.")}
|
||||
% else:
|
||||
${Text(_("This course has not started yet, and will launch on {launch_date_html}.")).format(
|
||||
launch_date_html=HTML(
|
||||
'<span'
|
||||
' class="localized-datetime start-date"'
|
||||
' data-datetime="{start_date}"'
|
||||
' data-format="shortDate"'
|
||||
' data-timezone="{user_timezone}"'
|
||||
' data-language="{user_language}"'
|
||||
'></span>'
|
||||
).format(
|
||||
start_date=course.start,
|
||||
user_timezone=user_timezone,
|
||||
user_language=user_language,
|
||||
),
|
||||
)}
|
||||
% endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
% endif
|
||||
</main>
|
||||
|
||||
<%static:require_module_async module_name="js/dateutil_factory" class_name="DateUtilFactory">
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Tests for the Course Outline view and supporting views.
|
||||
"""
|
||||
import datetime
|
||||
import ddt
|
||||
from mock import patch
|
||||
import json
|
||||
|
||||
@@ -12,10 +13,13 @@ from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from xmodule.course_module import DEFAULT_START_DATE
|
||||
|
||||
from .test_course_home import course_home_url
|
||||
|
||||
TEST_PASSWORD = 'test'
|
||||
FUTURE_DAY = datetime.datetime.now() + datetime.timedelta(days=30)
|
||||
PAST_DAY = datetime.datetime.now() - datetime.timedelta(days=30)
|
||||
|
||||
|
||||
class TestCourseOutlinePage(SharedModuleStoreTestCase):
|
||||
@@ -160,3 +164,25 @@ class TestCourseOutlinePreview(SharedModuleStoreTestCase):
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotContains(response, 'Future Chapter')
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestEmptyCourseOutlinePage(SharedModuleStoreTestCase):
|
||||
"""
|
||||
Test the new course outline view.
|
||||
"""
|
||||
@ddt.data(
|
||||
(FUTURE_DAY, 'This course has not started yet, and will launch on'),
|
||||
(PAST_DAY, "We're still working on course content."),
|
||||
(DEFAULT_START_DATE, 'This course has not started yet.'),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_empty_course_rendering(self, start_date, expected_text):
|
||||
course = CourseFactory.create(start=start_date)
|
||||
test_user = UserFactory(password=TEST_PASSWORD)
|
||||
CourseEnrollment.enroll(test_user, course.id)
|
||||
self.client.login(username=test_user.username, password=TEST_PASSWORD)
|
||||
url = course_home_url(course)
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, expected_text)
|
||||
|
||||
Reference in New Issue
Block a user