diff --git a/cms/djangoapps/contentstore/course_info_model.py b/cms/djangoapps/contentstore/course_info_model.py
index 570763bd2812..c5632b3373d3 100644
--- a/cms/djangoapps/contentstore/course_info_model.py
+++ b/cms/djangoapps/contentstore/course_info_model.py
@@ -160,25 +160,13 @@ def _get_index(passed_id=None):
return 0
-def _get_html(course_updates_items):
- """
- Method to create course_updates_html from course_updates items
- """
- list_items = []
- for update in reversed(course_updates_items):
- # filter course update items which have status "deleted".
- if update.get("status") != CourseInfoModule.STATUS_DELETED:
- list_items.append(u"{date}
{content}".format(**update))
- return u"".format(list_items="".join(list_items))
-
-
def save_course_update_items(location, course_updates, course_update_items, user=None):
"""
Save list of course_updates data dictionaries in new field ("course_updates.items")
and html related to course update in 'data' ("course_updates.data") field.
"""
course_updates.items = course_update_items
- course_updates.data = _get_html(course_update_items)
+ course_updates.data = ""
# update db record
modulestore().update_item(course_updates, user.id)
diff --git a/cms/djangoapps/contentstore/features/pages.py b/cms/djangoapps/contentstore/features/pages.py
index 464e76d7f868..bb3e113b643e 100644
--- a/cms/djangoapps/contentstore/features/pages.py
+++ b/cms/djangoapps/contentstore/features/pages.py
@@ -98,25 +98,25 @@ def _verify_page_names(first, second):
@step(u'the built-in pages are in the default order$')
def built_in_pages_in_default_order(step):
- expected_pages = ['Courseware', 'Course Info', 'Wiki', 'Progress']
+ expected_pages = ['Home', 'Course', 'Wiki', 'Progress']
see_pages_in_expected_order(expected_pages)
@step(u'the built-in pages are switched$')
def built_in_pages_switched(step):
- expected_pages = ['Courseware', 'Course Info', 'Progress', 'Wiki']
+ expected_pages = ['Home', 'Course', 'Progress', 'Wiki']
see_pages_in_expected_order(expected_pages)
@step(u'the pages are in the default order$')
def pages_in_default_order(step):
- expected_pages = ['Courseware', 'Course Info', 'Wiki', 'Progress', 'First', 'Empty']
+ expected_pages = ['Home', 'Course', 'Wiki', 'Progress', 'First', 'Empty']
see_pages_in_expected_order(expected_pages)
@step(u'the pages are switched$$')
def pages_are_switched(step):
- expected_pages = ['Courseware', 'Course Info', 'Progress', 'First', 'Empty', 'Wiki']
+ expected_pages = ['Home', 'Course', 'Progress', 'First', 'Empty', 'Wiki']
see_pages_in_expected_order(expected_pages)
diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py
index f880df7b2787..ac1cf917e1fe 100644
--- a/cms/djangoapps/contentstore/tests/test_course_settings.py
+++ b/cms/djangoapps/contentstore/tests/test_course_settings.py
@@ -942,8 +942,8 @@ def test_advanced_components_munge_tabs_validation_failure(self):
self.assertNotIn("notes", course.advanced_modules)
@ddt.data(
- [{'type': 'courseware'}, {'type': 'course_info'}, {'type': 'wiki', 'is_hidden': True}],
- [{'type': 'courseware', 'name': 'Courses'}, {'type': 'course_info', 'name': 'Info'}],
+ [{'type': 'course_info'}, {'type': 'courseware'}, {'type': 'wiki', 'is_hidden': True}],
+ [{'type': 'course_info', 'name': 'Home'}, {'type': 'courseware', 'name': 'Course'}],
)
def test_course_tab_configurations(self, tab_list):
self.course.tabs = tab_list
diff --git a/cms/djangoapps/contentstore/views/tests/test_course_updates.py b/cms/djangoapps/contentstore/views/tests/test_course_updates.py
index 94f92fe6377c..f9199c005a35 100644
--- a/cms/djangoapps/contentstore/views/tests/test_course_updates.py
+++ b/cms/djangoapps/contentstore/views/tests/test_course_updates.py
@@ -173,9 +173,8 @@ def test_course_updates_compatibility(self):
self.assertHTMLEqual(update_content, json.loads(resp.content)['content'])
course_updates = modulestore().get_item(location)
self.assertEqual(course_updates.items, [{u'date': update_date, u'content': update_content, u'id': 1}])
- # course_updates 'data' field should update accordingly
- update_data = u"".format(date=update_date, content=update_content)
- self.assertEqual(course_updates.data, update_data)
+ # course_updates 'data' field should not update automatically
+ self.assertEqual(course_updates.data, '')
# test delete course update item (soft delete)
course_updates = modulestore().get_item(location)
diff --git a/cms/templates/course_info.html b/cms/templates/course_info.html
index 7fa84c264eab..dc2d4640ce94 100644
--- a/cms/templates/course_info.html
+++ b/cms/templates/course_info.html
@@ -27,7 +27,7 @@
"${handouts_locator | escapejs}",
"${base_asset_url}",
${escape_json_dumps(push_notification_enabled) | n}
- );
+ );
});
%block>
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index c4a187f6701f..c8a9235f3ca1 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -414,13 +414,13 @@ class CourseFields(object):
scope=Scope.settings
)
has_children = True
- info_sidebar_name = String(
- display_name=_("Course Info Sidebar Name"),
+ info_section_name = String(
+ display_name=_("Course Home Sidebar Name"),
help=_(
- "Enter the heading that you want students to see above your course handouts on the Course Info page. "
+ "Enter the heading that you want students to see above your course handouts on the Course Home page. "
"Your course handouts appear in the right panel of the page."
),
- scope=Scope.settings, default='Course Handouts')
+ scope=Scope.settings, default=_('Course Resources and Tools'))
show_timezone = Boolean(
help=_(
"True if timezones should be shown on dates in the courseware. "
diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py
index 14b6b1928047..62fce42e3f83 100644
--- a/common/lib/xmodule/xmodule/html_module.py
+++ b/common/lib/xmodule/xmodule/html_module.py
@@ -1,13 +1,14 @@
-import os
-import sys
-import re
import copy
+from datetime import datetime
+from fs.errors import ResourceNotFoundError
import logging
-import textwrap
from lxml import etree
+import os
from path import Path as path
-from fs.errors import ResourceNotFoundError
from pkg_resources import resource_string
+import re
+import sys
+import textwrap
import dogstats_wrapper as dog_stats_api
from xmodule.util.misc import escape_html_characters
@@ -75,10 +76,10 @@ def student_view(self, _context):
return Fragment(self.get_html())
def get_html(self):
- """
- When we switch this to an XBlock, we can merge this with student_view,
- but for now the XModule mixin requires that this method be defined.
- """
+ """ Returns html required for rendering XModule. """
+
+ # When we switch this to an XBlock, we can merge this with student_view,
+ # but for now the XModule mixin requires that this method be defined.
# pylint: disable=no-member
if self.system.anonymous_student_id:
return self.data.replace("%%USER_ID%%", self.system.anonymous_student_id)
@@ -417,6 +418,35 @@ class CourseInfoModule(CourseInfoFields, HtmlModuleMixin):
# statuses
STATUS_VISIBLE = 'visible'
STATUS_DELETED = 'deleted'
+ TEMPLATE_DIR = 'courseware'
+
+ @XBlock.supports("multi_device")
+ def student_view(self, _context):
+ """
+ Return a fragment that contains the html for the student view
+ """
+ return Fragment(self.get_html())
+
+ def get_html(self):
+ """ Returns html required for rendering XModule. """
+
+ # When we switch this to an XBlock, we can merge this with student_view,
+ # but for now the XModule mixin requires that this method be defined.
+ # pylint: disable=no-member
+ if self.data != "":
+ if self.system.anonymous_student_id:
+ return self.data.replace("%%USER_ID%%", self.system.anonymous_student_id)
+ return self.data
+ else:
+ course_updates = [item for item in self.items if item.get('status') == self.STATUS_VISIBLE]
+ course_updates.sort(key=lambda item: datetime.strptime(item['date'], '%B %d, %Y'), reverse=True)
+
+ context = {
+ 'visible_updates': course_updates[:3],
+ 'hidden_updates': course_updates[3:],
+ }
+
+ return self.system.render_template("{0}/course_updates.html".format(self.TEMPLATE_DIR), context)
@XBlock.tag("detached")
diff --git a/common/lib/xmodule/xmodule/tabs.py b/common/lib/xmodule/xmodule/tabs.py
index 833e63f2a24f..480c850d2b8d 100644
--- a/common/lib/xmodule/xmodule/tabs.py
+++ b/common/lib/xmodule/xmodule/tabs.py
@@ -305,8 +305,8 @@ def initialize_default(course):
"""
course.tabs.extend([
- CourseTab.load('courseware'),
- CourseTab.load('course_info')
+ CourseTab.load('course_info'),
+ CourseTab.load('courseware')
])
# Presence of syllabus tab is indicated by a course attribute
@@ -389,6 +389,19 @@ def iterate_displayable(course, user=None, inline_collections=True):
else:
yield tab
+ @classmethod
+ def upgrade_tabs(cls, tabs):
+ """
+ Reverse and Rename Courseware to Course and Course Info to Home Tabs.
+ """
+ if tabs and len(tabs) > 1:
+ if tabs[0].get('type') == 'courseware' and tabs[1].get('type') == 'course_info':
+ tabs[0], tabs[1] = tabs[1], tabs[0]
+ tabs[0]['name'] = _('Home')
+ tabs[1]['name'] = _('Course')
+
+ return tabs
+
@classmethod
def validate_tabs(cls, tabs):
"""
@@ -406,13 +419,13 @@ def validate_tabs(cls, tabs):
if len(tabs) < 2:
raise InvalidTabsException("Expected at least two tabs. tabs: '{0}'".format(tabs))
- if tabs[0].get('type') != 'courseware':
+ if tabs[0].get('type') != 'course_info':
raise InvalidTabsException(
- "Expected first tab to have type 'courseware'. tabs: '{0}'".format(tabs))
+ "Expected first tab to have type 'course_info'. tabs: '{0}'".format(tabs))
- if tabs[1].get('type') != 'course_info':
+ if tabs[1].get('type') != 'courseware':
raise InvalidTabsException(
- "Expected second tab to have type 'course_info'. tabs: '{0}'".format(tabs))
+ "Expected second tab to have type 'courseware'. tabs: '{0}'".format(tabs))
# the following tabs should appear only once
# TODO: don't import openedx capabilities from common
@@ -455,6 +468,7 @@ def from_json(self, values):
"""
Overrides the from_json method to de-serialize the CourseTab objects from a json-like representation.
"""
+ self.upgrade_tabs(values)
self.validate_tabs(values)
tabs = []
for tab_dict in values:
diff --git a/common/test/acceptance/pages/lms/tab_nav.py b/common/test/acceptance/pages/lms/tab_nav.py
index 410caff2b382..e926b88414ef 100644
--- a/common/test/acceptance/pages/lms/tab_nav.py
+++ b/common/test/acceptance/pages/lms/tab_nav.py
@@ -21,7 +21,7 @@ def go_to_tab(self, tab_name):
Navigate to the tab `tab_name`.
"""
- if tab_name not in ['Courseware', 'Course Info', 'Discussion', 'Wiki', 'Progress']:
+ if tab_name not in ['Course', 'Home', 'Discussion', 'Wiki', 'Progress']:
self.warning("'{0}' is not a valid tab name".format(tab_name))
# The only identifier for individual tabs is the link href
diff --git a/common/test/acceptance/pages/studio/settings_advanced.py b/common/test/acceptance/pages/studio/settings_advanced.py
index 322e73b0737b..d6a822fbbb8e 100644
--- a/common/test/acceptance/pages/studio/settings_advanced.py
+++ b/common/test/acceptance/pages/studio/settings_advanced.py
@@ -176,7 +176,7 @@ def expected_settings_names(self):
'advertised_start',
'announcement',
'display_name',
- 'info_sidebar_name',
+ 'info_section_name',
'is_new',
'issue_badges',
'max_student_enrollments_allowed',
diff --git a/common/test/acceptance/tests/lms/test_certificate_web_view.py b/common/test/acceptance/tests/lms/test_certificate_web_view.py
index f6f5acb6e770..b547a9d3a678 100644
--- a/common/test/acceptance/tests/lms/test_certificate_web_view.py
+++ b/common/test/acceptance/tests/lms/test_certificate_web_view.py
@@ -206,7 +206,7 @@ def complete_course_problems(self):
Problems were added in the setUp
"""
self.course_info_page.visit()
- self.tab_nav.go_to_tab('Courseware')
+ self.tab_nav.go_to_tab('Course')
# Navigate to Test Subsection in Test Section Section
self.course_nav.go_to_section('Test Section', 'Test Subsection')
diff --git a/common/test/acceptance/tests/lms/test_library.py b/common/test/acceptance/tests/lms/test_library.py
index b9df22cc450c..ff7d502ba74a 100644
--- a/common/test/acceptance/tests/lms/test_library.py
+++ b/common/test/acceptance/tests/lms/test_library.py
@@ -127,9 +127,9 @@ def _goto_library_block_page(self, block_id=None):
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/common/test/acceptance/tests/lms/test_lms.py b/common/test/acceptance/tests/lms/test_lms.py
index 6165a59270a4..a1de769c2d9c 100644
--- a/common/test/acceptance/tests/lms/test_lms.py
+++ b/common/test/acceptance/tests/lms/test_lms.py
@@ -592,7 +592,7 @@ def test_course_info(self):
# Navigate to the course info page from the progress page
self.progress_page.visit()
- self.tab_nav.go_to_tab('Course Info')
+ self.tab_nav.go_to_tab('Home')
# Expect just one update
self.assertEqual(self.course_info_page.num_updates, 1)
@@ -650,13 +650,13 @@ def test_wiki_tab_first_time(self):
def test_courseware_nav(self):
"""
- Navigate to a particular unit in the courseware.
+ Navigate to a particular unit in the course.
"""
- # Navigate to the courseware page from the info page
+ # Navigate to the course page from the info page
self.course_info_page.visit()
- self.tab_nav.go_to_tab('Courseware')
+ self.tab_nav.go_to_tab('Course')
- # Check that the courseware navigation appears correctly
+ # Check that the course navigation appears correctly
EXPECTED_SECTIONS = {
'Test Section': ['Test Subsection'],
'Test Section 2': ['Test Subsection 2', 'Test Subsection 3']
@@ -844,7 +844,7 @@ def test_tooltip(self):
Verify that tooltips are displayed when you hover over the sequence nav bar.
"""
self.course_info_page.visit()
- self.tab_nav.go_to_tab('Courseware')
+ self.tab_nav.go_to_tab('Course')
self.assertTrue(self.courseware_page.tooltips_displayed())
@@ -993,7 +993,7 @@ def check_function(expect, ans):
def test_python_execution_in_problem(self):
# Navigate to the problem page
self.course_info_page.visit()
- self.tab_nav.go_to_tab('Courseware')
+ self.tab_nav.go_to_tab('Course')
self.course_nav.go_to_section('Test Section', 'Test Subsection')
problem_page = ProblemPage(self.browser)
@@ -1043,14 +1043,14 @@ def setUp(self):
def test_entrance_exam_section(self):
"""
- Scenario: Any course that is enabled for an entrance exam, should have entrance exam chapter at courseware
+ Scenario: Any course that is enabled for an entrance exam, should have entrance exam chapter at course
page.
- Given that I am on the courseware page
- When I view the courseware that has an entrance exam
+ Given that I am on the course page
+ When I view the course that has an entrance exam
Then there should be an "Entrance Exam" chapter.'
"""
entrance_exam_link_selector = '.accordion .course-navigation .chapter .group-heading'
- # visit courseware page and make sure there is not entrance exam chapter.
+ # visit course page and make sure there is not entrance exam chapter.
self.courseware_page.visit()
self.courseware_page.wait_for_page()
self.assertFalse(element_has_text(
diff --git a/common/test/acceptance/tests/lms/test_lms_acid_xblock.py b/common/test/acceptance/tests/lms/test_lms_acid_xblock.py
index 14fd06b49697..13ffbb9fa887 100644
--- a/common/test/acceptance/tests/lms/test_lms_acid_xblock.py
+++ b/common/test/acceptance/tests/lms/test_lms_acid_xblock.py
@@ -75,7 +75,7 @@ def test_acid_block(self):
"""
self.course_info_page.visit()
- self.tab_nav.go_to_tab('Courseware')
+ self.tab_nav.go_to_tab('Course')
acid_block = AcidView(self.browser, '.xblock-student_view[data-block-type=acid]')
self.validate_acid_block_view(acid_block)
@@ -119,7 +119,7 @@ def test_acid_block(self):
"""
self.course_info_page.visit()
- self.tab_nav.go_to_tab('Courseware')
+ self.tab_nav.go_to_tab('Course')
acid_parent_block = AcidView(self.browser, '.xblock-student_view[data-block-type=acid_parent]')
self.validate_acid_parent_block_view(acid_parent_block)
@@ -159,7 +159,7 @@ def test_acid_block(self):
"""
self.course_info_page.visit()
- self.tab_nav.go_to_tab('Courseware')
+ self.tab_nav.go_to_tab('Course')
acid_aside = AcidView(self.browser, '.xblock_asides-v1-student_view[data-block-type=acid_aside]')
self.validate_acid_aside_view(acid_aside)
diff --git a/common/test/acceptance/tests/video/test_video_module.py b/common/test/acceptance/tests/video/test_video_module.py
index 979b6d1befd1..913c1194f4a9 100644
--- a/common/test/acceptance/tests/video/test_video_module.py
+++ b/common/test/acceptance/tests/video/test_video_module.py
@@ -132,7 +132,7 @@ def _navigate_to_courseware_video(self):
self.auth_page.visit()
self.user_info = self.auth_page.user_info
self.course_info_page.visit()
- self.tab_nav.go_to_tab('Courseware')
+ self.tab_nav.go_to_tab('Course')
def _navigate_to_courseware_video_and_render(self):
""" Wait for the video player to render """
diff --git a/lms/djangoapps/course_wiki/tests/tests.py b/lms/djangoapps/course_wiki/tests/tests.py
index 172ac0ad8573..a34be83ef9d5 100644
--- a/lms/djangoapps/course_wiki/tests/tests.py
+++ b/lms/djangoapps/course_wiki/tests/tests.py
@@ -103,8 +103,8 @@ def has_course_navigator(self, resp):
"""
Ensure that the response has the course navigator.
"""
- self.assertContains(resp, "Course Info")
- self.assertContains(resp, "courseware")
+ self.assertContains(resp, "Home")
+ self.assertContains(resp, "Course")
@patch.dict("django.conf.settings.FEATURES", {'ALLOW_WIKI_ROOT_ACCESS': True})
def test_course_navigator(self):
diff --git a/lms/djangoapps/courseware/date_summary.py b/lms/djangoapps/courseware/date_summary.py
index 20a9a2fca70c..59448608cffe 100644
--- a/lms/djangoapps/courseware/date_summary.py
+++ b/lms/djangoapps/courseware/date_summary.py
@@ -186,7 +186,10 @@ class VerifiedUpgradeDeadlineDate(DateSummary):
"""
css_class = 'verified-upgrade-deadline'
title = _('Verification Upgrade Deadline')
- description = _('You are still eligible to upgrade to a Verified Certificate!')
+ description = _(
+ 'You are still eligible to upgrade to a Verified Certificate! '
+ 'Pursue it to highlight the knowledge and skills you gain in this course.'
+ )
link_text = _('Upgrade to Verified Certificate')
@property
diff --git a/lms/djangoapps/courseware/features/navigation.feature b/lms/djangoapps/courseware/features/navigation.feature
index 40a71d561520..4c7320d7e523 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 courseware
- 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 c63ff1070ab0..7483e4f1b6c8 100644
--- a/lms/djangoapps/courseware/features/navigation.py
+++ b/lms/djangoapps/courseware/features/navigation.py
@@ -1,5 +1,6 @@
# pylint: disable=missing-docstring
# pylint: disable=redefined-outer-name
+# pylint: disable=unused-argument
from lettuce import world, step
from common import course_location
@@ -127,17 +128,12 @@ def then_i_see_the_content_of_the_sequence_item(step):
wait_for_problem('PROBLEM 6')
-@step(u'I return to the courseware')
-def and_i_return_to_the_courseware(step):
+@step(u'I return to the course')
+def and_i_return_to_the_course(step):
world.visit('/')
world.click_link("View Course")
- world.click_link("Courseware")
-
-
-@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)
+ course = 'a[href*="/courseware"]'
+ world.css_click(course)
def create_course():
diff --git a/lms/djangoapps/courseware/field_overrides.py b/lms/djangoapps/courseware/field_overrides.py
index 44207e1e711a..9104fd5e336c 100644
--- a/lms/djangoapps/courseware/field_overrides.py
+++ b/lms/djangoapps/courseware/field_overrides.py
@@ -228,11 +228,16 @@ def get(self, block, name, default): # pragma no cover
@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 c38b8961a635..e694705205be 100644
--- a/lms/djangoapps/courseware/self_paced_overrides.py
+++ b/lms/djangoapps/courseware/self_paced_overrides.py
@@ -25,4 +25,4 @@ def get(self, block, name, default):
@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/tabs.py b/lms/djangoapps/courseware/tabs.py
index c4ddb6787e0e..721c967be06a 100644
--- a/lms/djangoapps/courseware/tabs.py
+++ b/lms/djangoapps/courseware/tabs.py
@@ -28,7 +28,7 @@ class CoursewareTab(EnrolledTab):
The main courseware view.
"""
type = 'courseware'
- title = ugettext_noop('Courseware')
+ title = ugettext_noop('Course')
priority = 10
view_name = 'courseware'
is_movable = False
@@ -40,7 +40,7 @@ class CourseInfoTab(CourseTab):
The course info view.
"""
type = 'course_info'
- title = ugettext_noop('Course Info')
+ title = ugettext_noop('Home')
priority = 20
view_name = 'info'
tab_id = 'info'
diff --git a/lms/djangoapps/courseware/tests/test_about.py b/lms/djangoapps/courseware/tests/test_about.py
index fb457e079dcb..76c62e83437c 100644
--- a/lms/djangoapps/courseware/tests/test_about.py
+++ b/lms/djangoapps/courseware/tests/test_about.py
@@ -99,7 +99,7 @@ def test_already_enrolled(self):
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertIn("You are enrolled in this course", resp.content)
- self.assertIn("View Courseware", resp.content)
+ self.assertIn("View Course", resp.content)
@override_settings(COURSE_ABOUT_VISIBILITY_PERMISSION="see_about_page")
def test_visible_about_page_settings(self):
@@ -474,7 +474,7 @@ def test_already_enrolled(self):
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertIn("You are enrolled in this course", resp.content)
- self.assertIn("View Courseware", resp.content)
+ self.assertIn("View Course", resp.content)
self.assertNotIn("Add buyme to Cart ($10 USD)", resp.content)
def test_closed_enrollment(self):
diff --git a/lms/djangoapps/courseware/tests/test_course_info.py b/lms/djangoapps/courseware/tests/test_course_info.py
index 1c9217d495b1..03e48fb79a54 100644
--- a/lms/djangoapps/courseware/tests/test_course_info.py
+++ b/lms/djangoapps/courseware/tests/test_course_info.py
@@ -3,6 +3,7 @@
"""
import mock
from nose.plugins.attrib import attr
+from pyquery import PyQuery as pq
from urllib import urlencode
from django.conf import settings
@@ -10,6 +11,7 @@
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, SharedModuleStoreTestCase
from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_CLOSED_MODULESTORE
@@ -84,6 +86,35 @@ def test_nonexistent_course(self):
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)
+ content = pq(response.content)
+ self.assertEqual(content('.page-header-secondary a').length, 0)
+
+ 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)
+ content = pq(info_page_response.content)
+ self.assertEqual(content('.page-header-secondary .last-accessed-link').attr('href'), section_url)
+
@attr('shard_1')
class CourseInfoTestCaseXML(LoginEnrollmentTestCase, ModuleStoreTestCase):
@@ -125,6 +156,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)
@@ -142,7 +174,7 @@ def fetch_course_info_with_queries(self, course, sql_queries, mongo_queries):
self.assertEqual(resp.status_code, 200)
def test_num_queries_instructor_paced(self):
- self.fetch_course_info_with_queries(self.instructor_paced_course, 17, 4)
+ self.fetch_course_info_with_queries(self.instructor_paced_course, 19, 4)
def test_num_queries_self_paced(self):
- self.fetch_course_info_with_queries(self.self_paced_course, 17, 4)
+ self.fetch_course_info_with_queries(self.self_paced_course, 19, 4)
diff --git a/lms/djangoapps/courseware/tests/test_tabs.py b/lms/djangoapps/courseware/tests/test_tabs.py
index 79d120e2d2ef..225d335ad652 100644
--- a/lms/djangoapps/courseware/tests/test_tabs.py
+++ b/lms/djangoapps/courseware/tests/test_tabs.py
@@ -484,9 +484,10 @@ def setUp(self):
[{'type': CoursewareTab.type}],
# missing course_info
[{'type': CoursewareTab.type}, {'type': 'discussion', 'name': 'fake_name'}],
+ [{'type': 'unknown_type'}],
# incorrect order
- [{'type': CourseInfoTab.type, 'name': 'fake_name'}, {'type': CoursewareTab.type}],
- [{'type': 'unknown_type'}]
+ [{'type': 'discussion', 'name': 'fake_name'},
+ {'type': CourseInfoTab.type, 'name': 'fake_name'}, {'type': CoursewareTab.type}],
]
# tab types that should appear only once
diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py
index 64b9d35bd6d2..0e8100ddc796 100644
--- a/lms/djangoapps/courseware/tests/test_views.py
+++ b/lms/djangoapps/courseware/tests/test_views.py
@@ -247,14 +247,6 @@ def test_get_current_child(self):
mock_xmodule_2.get_display_items.return_value = []
self.assertIsNone(views.get_current_child(mock_xmodule_2))
- def test_redirect_to_course_position(self):
- mock_module = MagicMock()
- mock_module.descriptor.id = 'Underwater Basketweaving'
- mock_module.position = 3
- mock_module.get_display_items.return_value = []
- self.assertRaises(Http404, views.redirect_to_course_position,
- mock_module, views.CONTENT_DEPTH)
-
def test_invalid_course_id(self):
response = self.client.get('/courses/MITx/3.091X/')
self.assertEqual(response.status_code, 404)
diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py
index c86064d880ba..6e33ec35cc70 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -55,6 +55,7 @@
is_credit_course
)
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
+from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from courseware.models import StudentModuleHistory
from courseware.model_data import FieldDataCache, ScoresClient
from .module_render import toc_for_course, get_module_for_descriptor, get_module, get_module_by_usage_id
@@ -227,38 +228,6 @@ def _get_default_child_module(child_modules):
return child
-def redirect_to_course_position(course_module, content_depth):
- """
- Return a redirect to the user's current place in the course.
-
- If this is the user's first time, redirects to COURSE/CHAPTER/SECTION.
- If this isn't the users's first time, redirects to COURSE/CHAPTER,
- and the view will find the current section and display a message
- about reusing the stored position.
-
- If there is no current position in the course or chapter, then selects
- the first child.
-
- """
- urlargs = {'course_id': course_module.id.to_deprecated_string()}
- chapter = get_current_child(course_module, min_depth=content_depth)
- if chapter is None:
- # oops. Something bad has happened.
- raise Http404("No chapter found when loading current position in course")
-
- urlargs['chapter'] = chapter.url_name
- if course_module.position is not None:
- return redirect(reverse('courseware_chapter', kwargs=urlargs))
-
- # Relying on default of returning first child
- section = get_current_child(chapter, min_depth=content_depth - 1)
- if section is None:
- raise Http404("No section found when loading current position in course")
-
- urlargs['section'] = section.url_name
- return redirect(reverse('courseware_section', kwargs=urlargs))
-
-
def save_child_position(seq_module, child_name):
"""
child_name: url_name of the child
@@ -467,9 +436,12 @@ def _index_bulk_op(request, course_key, chapter, section, position):
chapter=exam_chapter.url_name,
section=exam_section.url_name)
- # passing CONTENT_DEPTH avoids returning 404 for a course with an
- # empty first section and a second section with content
- return redirect_to_course_position(course_module, CONTENT_DEPTH)
+ # Otherwise, try to redirect to the user's last position in the courseware
+ __, section_url = get_last_accessed_courseware(course, request)
+ if section_url is not None:
+ return redirect(section_url)
+ else:
+ raise Http404("No chapter found when loading current position in course")
chapter_descriptor = course.get_child_by(lambda m: m.location.name == chapter)
if chapter_descriptor is not None:
@@ -544,8 +516,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.
@@ -554,22 +524,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:
@@ -717,6 +671,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:
@@ -727,6 +689,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/djangoapps/lms_xblock/mixin.py b/lms/djangoapps/lms_xblock/mixin.py
index a05fe5a8ece3..e4eb865363cd 100644
--- a/lms/djangoapps/lms_xblock/mixin.py
+++ b/lms/djangoapps/lms_xblock/mixin.py
@@ -42,7 +42,7 @@ class LmsBlockMixin(XBlockMixin):
scope=Scope.settings,
)
chrome = String(
- display_name=_("Courseware Chrome"),
+ display_name=_("Course Chrome"),
# Translators: DO NOT translate the words in quotes here, they are
# specific words for the acceptable values.
help=_("Enter the chrome, or navigation tools, to use for the XBlock in the LMS. Valid values are: \n"
@@ -55,7 +55,7 @@ class LmsBlockMixin(XBlockMixin):
)
default_tab = String(
display_name=_("Default Tab"),
- help=_("Enter the tab that is selected in the XBlock. If not set, the Courseware tab is selected."),
+ help=_("Enter the tab that is selected in the XBlock. If not set, the Course tab is selected."),
scope=Scope.settings,
default=None,
)
diff --git a/lms/djangoapps/survey/tests/test_views.py b/lms/djangoapps/survey/tests/test_views.py
index 66f4c3f0c735..ebd16373fff2 100644
--- a/lms/djangoapps/survey/tests/test_views.py
+++ b/lms/djangoapps/survey/tests/test_views.py
@@ -11,6 +11,7 @@
from survey.models import SurveyForm, SurveyAnswer
+from student.tests.factories import UserFactory
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
@@ -29,7 +30,7 @@ def setUp(self):
# Create two accounts
self.password = 'abc'
- self.student = User.objects.create_user('student', 'student@test.com', self.password)
+ self.student = UserFactory.create(username='student', email='student@test.com', password=self.password)
self.test_survey_name = 'TestSurvey'
self.test_form = '''
diff --git a/lms/djangoapps/teams/tests/test_views.py b/lms/djangoapps/teams/tests/test_views.py
index 55cc4770da7f..6064931cecf8 100644
--- a/lms/djangoapps/teams/tests/test_views.py
+++ b/lms/djangoapps/teams/tests/test_views.py
@@ -113,8 +113,8 @@ def test_query_counts(self):
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id)
self.client.login(username=self.user.username, password=self.test_password)
- # Check the query count on the dashboard With no teams
- with self.assertNumQueries(17):
+ # Check the query count on the dashboard with no teams
+ with self.assertNumQueries(18):
self.client.get(self.teams_url)
# Create some teams
@@ -129,7 +129,7 @@ def test_query_counts(self):
team.add_user(self.user)
# Check the query count on the dashboard again
- with self.assertNumQueries(23):
+ with self.assertNumQueries(24):
self.client.get(self.teams_url)
def test_bad_course_id(self):
diff --git a/lms/static/js/courseware/toggle_element_visibility.js b/lms/static/js/courseware/toggle_element_visibility.js
new file mode 100644
index 000000000000..b5dc438b0a1e
--- /dev/null
+++ b/lms/static/js/courseware/toggle_element_visibility.js
@@ -0,0 +1,42 @@
+;(function (define) {
+ 'use strict';
+
+ define(["jquery"],
+ function ($) {
+
+ return function () {
+ // define variables for code legibility
+ var toggleActionElements = $('.toggle-visibility-button');
+
+ var updateToggleActionText = function (targetElement, actionElement) {
+ var show_text = actionElement.data('show');
+ var hide_text = actionElement.data('hide');
+
+ if (targetElement.is(":visible")) {
+ if (hide_text) {
+ actionElement.html(actionElement.data('hide'));
+ } else {
+ actionElement.hide();
+ }
+ } else {
+ if (show_text) {
+ actionElement.html(actionElement.data('show'));
+ }
+ }
+ };
+
+ $.each(toggleActionElements, function (i, elem) {
+ var toggleActionElement = $(elem);
+ var toggleTargetElement = toggleActionElement.siblings('.toggle-visibility-element');
+
+ updateToggleActionText(toggleTargetElement, toggleActionElement);
+
+ toggleActionElement.on('click', function (event) {
+ event.preventDefault();
+ toggleTargetElement.toggleClass('hidden');
+ updateToggleActionText(toggleTargetElement, toggleActionElement);
+ });
+ });
+ };
+ });
+})(define || RequireJS.define);
diff --git a/lms/static/js/fixtures/courseware/course_updates.html b/lms/static/js/fixtures/courseware/course_updates.html
new file mode 100644
index 000000000000..51935ba02ff8
--- /dev/null
+++ b/lms/static/js/fixtures/courseware/course_updates.html
@@ -0,0 +1,45 @@
+
+
+ December 1, 2015
+ Hide
+
+
Assignment 1
+
Please submit your first assignment before due date.
+
+
+
+ December 1, 2015
+ Show
+
+
Quiz 1
+
You have a quiz due on coming friday.
+
+
+
+ November 26, 2015
+ Show
+
+
Assignment update
+
Please submit your first assignment before due date.
+
+
+
+
+
+ November 20, 2015
+ Show
+ Orientation
+
Orientation will held on monday
+
+
+
+ November 19, 2015
+ Show
+ Course starting
+
Starting info about course.
+
+
+
+
+ Show Earlier Course Updates
+
diff --git a/lms/static/js/spec/courseware/updates_visibility.js b/lms/static/js/spec/courseware/updates_visibility.js
new file mode 100644
index 000000000000..dc0395450263
--- /dev/null
+++ b/lms/static/js/spec/courseware/updates_visibility.js
@@ -0,0 +1,36 @@
+define(['jquery', 'js/courseware/toggle_element_visibility'],
+ function ($, ToggleElementVisibility) {
+ 'use strict';
+
+ describe('show/hide with mouse click', function () {
+
+ beforeEach(function() {
+ loadFixtures('js/fixtures/courseware/course_updates.html');
+ /*jshint newcap: false */
+ ToggleElementVisibility();
+ /*jshint newcap: true */
+ });
+
+ it('ensures update will hide on hide button click', function () {
+ var $shownUpdate = $('.toggle-visibility-element:not(.hidden)').first();
+ $shownUpdate.siblings('.toggle-visibility-button').trigger('click');
+ expect($shownUpdate).toHaveClass('hidden');
+ });
+
+ it('ensures update will show on show button click', function () {
+ var $hiddenUpdate = $('.toggle-visibility-element.hidden').first();
+ $hiddenUpdate.siblings('.toggle-visibility-button').trigger('click');
+ expect($hiddenUpdate).not.toHaveClass('hidden');
+ });
+
+ it('ensures old updates will show on button click', function () {
+ // on page load old updates will be hidden
+ var $oldUpdates = $('.toggle-visibility-element.old-updates');
+ expect($oldUpdates).toHaveClass('hidden');
+
+ // on click on show earlier update button old updates will be shown
+ $('.toggle-visibility-button.show-older-updates').trigger('click');
+ expect($oldUpdates).not.toHaveClass('hidden');
+ });
+ });
+ });
diff --git a/lms/static/js/spec/main.js b/lms/static/js/spec/main.js
index 7ba4686fd318..58c59081448b 100644
--- a/lms/static/js/spec/main.js
+++ b/lms/static/js/spec/main.js
@@ -707,6 +707,7 @@
'lms/include/js/spec/edxnotes/collections/notes_spec.js',
'lms/include/js/spec/search/search_spec.js',
'lms/include/js/spec/navigation_spec.js',
+ 'lms/include/js/spec/courseware/updates_visibility.js',
'lms/include/js/spec/discovery/collections/filters_spec.js',
'lms/include/js/spec/discovery/models/course_card_spec.js',
'lms/include/js/spec/discovery/models/course_directory_spec.js',
diff --git a/lms/static/sass/_developer.scss b/lms/static/sass/_developer.scss
index d1d8102dca91..5570d4a40bd5 100644
--- a/lms/static/sass/_developer.scss
+++ b/lms/static/sass/_developer.scss
@@ -231,40 +231,41 @@
.date-summary-container {
.date-summary {
@include clearfix;
- margin-top: $baseline/2;
- margin-bottom: $baseline/2;
padding: 10px;
- background-color: $gray-l4;
@include border-left(3px solid $gray-l3);
+ .heading {
+ @extend %t-title7;
+ color: $gray-d2;
+ }
+
.description {
margin-top: $baseline/2;
margin-bottom: $baseline/2;
display: inline-block;
- color: $lighter-base-font-color;
- font-size: 80%;
+ color: $gray-d1;
+ @extend %t-title8;
}
.date-summary-link {
- @include float(right);
- font-size: 80%;
+ @extend %t-title8;
font-weight: $font-semibold;
a {
- color: $base-font-color;
+ color: $link-color;
+ font-weight: normal;
}
}
.date {
- color: $lighter-base-font-color;
- font-size: 80%;
+ color: $gray-d1;
+ @extend %t-title9;
}
&-todays-date {
@include border-left(3px solid $blue);
.heading {
- font-weight: $font-regular;
- font-size: 80%;
+ @extend %t-title8;
}
}
diff --git a/lms/static/sass/base/_layouts.scss b/lms/static/sass/base/_layouts.scss
index 6694e23db07d..94bf3654ec13 100644
--- a/lms/static/sass/base/_layouts.scss
+++ b/lms/static/sass/base/_layouts.scss
@@ -32,9 +32,16 @@ body.view-in-course {
.wrapper-course-material .course-material,
.wrapper-preview-menu .preview-menu {
width: auto;
+ }
+
+ .wrapper-preview-menu .preview-menu {
padding: 15px 2%;
}
+ .wrapper-course-material .course-material {
+ padding: ($baseline/2) 0 0 0;
+ }
+
.wrapper-course-material .course-material .course-tabs {
padding: 0;
}
diff --git a/lms/static/sass/base/_variables.scss b/lms/static/sass/base/_variables.scss
index f72403bd3712..6e68f74e90f7 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;
@@ -514,7 +515,7 @@ $light-gray: rgb(221, 221, 221) !default;
$dark-gray: rgb(51, 51, 51) !default;
$border-color: rgb(200, 200, 200) !default;
$sidebar-color: rgb(246, 246, 246) !default;
-$outer-border-color: rgb(170, 170, 170);
+$outer-border-color: $gray-l3;
$light-gray: rgb(221,221,221) !default;
// used by descriptor css
diff --git a/lms/static/sass/course/_info.scss b/lms/static/sass/course/_info.scss
index 0bbca1d75710..b987e19908da 100644
--- a/lms/static/sass/course/_info.scss
+++ b/lms/static/sass/course/_info.scss
@@ -1,11 +1,60 @@
+.home {
+ @include clearfix();
+ max-width: 1140px;
+ margin: 0 auto;
+ padding: $baseline $baseline ($baseline/2) $baseline;
+
+ .page-header-main {
+ display: inline-block;
+ width: flex-grid(8, 12);
+ margin: 0;
+
+ .page-title {
+ margin-bottom: 5px;
+ color: $dark-gray1;
+ font-size: 24px;
+ }
+
+ .page-subtitle {
+ color: $dark-gray2;
+ font-size: 14px;
+ text-transform: none;
+ }
+ }
+
+ .page-header-secondary {
+ @include float(right);
+ display: inline-block;
+ margin: ($baseline/2);
+ padding: ($baseline/2) ($baseline*0.75);
+ background-color: $blue;
+
+ .last-accessed-link {
+ @extend %t-title6;
+ color: $very-light-text;
+ }
+ }
+}
+
div.info-wrapper {
+ background-color: $homepage-background;
+ border-top: none;
+
section.updates {
@extend .content;
+ @include padding-left($baseline);
line-height: lh();
+ width: 100%;
+ display: block;
- > h1 {
- @extend .top-header;
+ h1 {
+ @include text-align(left);
+ @extend %t-strong;
+ @extend %t-title6;
+ margin-bottom: 0;
+ @include padding(12px, 26px, 20px, 0);
+ font-style: normal;
}
> p {
@@ -17,12 +66,30 @@ div.info-wrapper {
margin-bottom: lh();
padding-left: 0;
+ .updates-article {
+ border-radius:3px;
+ background-color: $white;
+ border:1px solid transparent;
+ &:hover {
+ border: 1px solid $gray-l3;
+ }
+ }
+
+ .show-older-updates {
+ @extend %btn-pl-white-base;
+ padding: ($baseline/2);
+ @include font-size(14);
+ width: 100%;
+ display: block;
+ text-align: center;
+ cursor: pointer;
+ }
+
> li,article {
@extend .clearfix;
- border-bottom: 1px solid lighten($border-color, 10%);
+ padding: $baseline;
list-style-type: none;
margin-bottom: lh(1.5);
- padding-bottom: lh(.75);
ol, ul {
ol,ul {
@@ -30,11 +97,25 @@ div.info-wrapper {
}
}
- h2 {
- font-size: $body-font-size;
- font-weight: bold;
+ h2.date {
+ @extend %t-title9;
+ 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);
+ @include float(left);
+ }
+
+ .toggle-visibility-button {
+ @extend %t-title9;
+ @include float(right);
+ cursor: pointer;
+ }
+
+ .toggle-visibility-element {
+ content:'';
+ display:block;
+ clear: both;
}
section.update-description {
@@ -77,170 +158,82 @@ div.info-wrapper {
}
}
- section.handouts {
- padding: 20px 30px;
+ section.course-dates {
+ @include padding(32px, 30px, 20px, 30px);
margin: 0;
@extend .sidebar;
- border-radius: 0 4px 4px 0;
- @include border-left(1px solid #ddd);
+ background: rgba(0, 0, 0, 0);
box-shadow: none;
font-size: 14px;
- a {
- color: $link-color;
- }
-
&:after {
left: -1px;
right: auto;
}
- h1 {
+ .course-info-heading {
@include text-align(left);
+ @extend %t-strong;
+ @extend %t-title6;
margin-bottom: 0;
padding: 12px 26px 20px 0;
- font-size: 18px;
font-style: normal;
- font-weight: bold;
}
- ul {
- background-color: #f6f6f6;
- margin-bottom: 14px;
+ @media print {
+ background: transparent !important;
}
+ }
- ol {
- margin-bottom: 14px;
- li {
- @include text-align(left);
-
- a {
- display: block;
- padding: 0;
- color: $link-color;
-
- &:hover, &:focus {
- background: transparent;
- }
- }
-
- &.expandable,
- &.collapsable {
- margin: 0 16px 14px 16px;
- @include transition(all .2s linear 0s);
-
- h4 {
- color: $link-color;
- font-size: 1em;
- font-weight: normal;
- padding-left: 30px;
- }
- }
-
- &.collapsable {
- background: $white;
- border-radius: 3px;
- padding: 14px 0;
- box-shadow: 0 0 1px 1px $shadow-l1, 0 1px 3px rgba(0, 0, 0, .25);
-
- h4 {
- margin-bottom: 16px;
- }
- }
-
- &.multiple {
-
- a {
- display: inline-block;
- padding: 0;
-
- &:hover, &:focus {
- background: transparent;
- }
- }
- }
-
- ul {
- background: none;
- margin: 0;
-
- li {
- border-bottom: 0;
- border-top: 1px solid #e6e6e6;
- font-size: 0.9em;
- margin: 0;
- padding: 15px 30px;
-
- a {
- display: inline-block;
- padding: 0;
-
- &:hover, &:focus {
- background: transparent;
- }
- }
- }
- }
+ section.handouts {
+ margin: $baseline;
+ border-top: ($baseline/4) solid $gray-l5;
- div.hitarea {
- background-image: url('#{$static-path}/images/treeview-default.gif') no-repeat;
- display: block;
- height: 100%;
- margin-left: 0;
- max-height: 20px;
- position: absolute;
- width: 100%;
-
- &:hover, &:focus {
- opacity: 0.6;
- filter: alpha(opacity=60);
-
- + h4 {
- @extend a:hover;
- text-decoration: underline;
- }
- }
+ .course-info-heading {
+ @include text-align(left);
+ @extend %t-strong;
+ @extend %t-title6;
+ margin-top: $baseline;
+ margin-bottom: ($baseline/2);
+ padding: 0;
+ font-style: normal;
+ }
- &.expandable-hitarea {
- background-position: -72px 0px;
- }
+ h1 {
+ @include text-align(left);
+ @extend %t-strong;
+ @extend %t-title7;
+ margin-bottom: 0;
+ }
- &.collapsable-hitarea {
- background-position: -55px -23px;
- }
- }
+ a {
+ color: $link-color;
+ }
- h3 {
- border-bottom: 0;
- box-shadow: none;
- color: #888;
- font-size: 1em;
- margin-bottom: 0;
- }
+ ul {
+ margin: ($baseline/2) 0;
+ padding: 0;
+ list-style: none;
- p {
- letter-spacing: 0;
- margin: 0;
- text-transform: none;
+ > li {
+ margin: ($baseline/4) 0;
+ }
+ }
- a {
- padding-right: 8px;
+ ol {
+ margin: 0;
+ padding: 0;
+ display: inline-block;
+ list-style: none;
+ @include font-size(14);
- &:before {
- color: $gray-l3;
- content: "•";
- display: inline-block;
- padding-right: 8px;
- }
+ > li {
+ margin: ($baseline/4) 0;
+ border-bottom: 2px solid $gray-l4;
+ }
- &:first-child {
- &:before {
- content: "";
- padding-right: 0;
- }
- }
- }
- }
+ li:last-child {
+ border: none;
}
}
diff --git a/lms/static/sass/course/base/_base.scss b/lms/static/sass/course/base/_base.scss
index 7aba39d2b6c7..775a77995ad6 100644
--- a/lms/static/sass/course/base/_base.scss
+++ b/lms/static/sass/course/base/_base.scss
@@ -24,8 +24,7 @@
display: table;
table-layout: fixed;
width: 100%;
- border-radius: 3px;
- border: 1px solid $outer-border-color;
+ border: 1px solid $border-color-2;
background: $container-bg;
box-shadow: 0 1px 2px $shadow-l2;
}
diff --git a/lms/static/sass/course/layout/_courseware_header.scss b/lms/static/sass/course/layout/_courseware_header.scss
index 4b01542e8cd2..f3519cd4fdc6 100644
--- a/lms/static/sass/course/layout/_courseware_header.scss
+++ b/lms/static/sass/course/layout/_courseware_header.scss
@@ -15,48 +15,49 @@
ol.course-tabs {
@include border-top-radius(4px);
@include clearfix();
- @include margin-left(10px);
padding: ($baseline*0.75) 0 ($baseline*0.75) 0;
li {
@include float(left);
list-style: none;
- margin-right: 6px;
&.prominent {
- margin-right: 16px;
- background: rgba(255, 255, 255, .5);
+ @include margin-right(16px);
+ background: rgba(255, 255, 255, 0.5);
border-radius: 3px;
}
&.prominent + li {
- padding-left: ($baseline*0.75);
- border-left: 1px solid #333;
+ @include padding-left($baseline*0.75);
+ @include border-left(1px solid $gray-d3);
}
a {
- border-radius: 3px;
- color: #555;
+ @include padding(($baseline/2), ($baseline*0.75), 13px, ($baseline*0.75));
+ @extend %t-title7;
+ @extend %t-regular;
+ border-bottom: 3px solid transparent;
+ color: $gray-d1;
display: block;
text-align: center;
- padding: ($baseline/2) 13px 12px;
- font-size: 14px;
- font-weight: bold;
text-decoration: none;
// text-shadow: 0 1px 0 rgba(0, 0, 0, .4);
- &:hover, &:focus {
- color: #333;
- background: rgba(255, 255, 255, .6);
+ &:hover,
+ &:focus {
+ color: $blue;
+ border-bottom: 3px solid $blue;
}
&.active {
- // background: $shadow;
- @include linear-gradient(top, rgba(0, 0, 0, .4), rgba(0, 0, 0, .25));
+ border-bottom: 3px solid $gray-d4;
background-color: transparent;
- box-shadow: 0 1px 0 rgba(255, 255, 255, .5), 0 1px 1px rgba(0, 0, 0, .3) inset;
- color: $white;
- text-shadow: 0 1px 0 rgba(0, 0, 0, .4);
+ color: $gray-d4;
+
+ &:hover,
+ &:focus {
+ color: $gray-d4;
+ }
}
}
}
@@ -87,7 +88,7 @@ header.global.slim {
}
.guest .secondary {
- margin-right: 0;
+ @include margin-right(0);
}
.guest .secondary a {
diff --git a/lms/static/sass/shared/_header.scss b/lms/static/sass/shared/_header.scss
index c66b2ed905c1..e00249906718 100644
--- a/lms/static/sass/shared/_header.scss
+++ b/lms/static/sass/shared/_header.scss
@@ -115,8 +115,8 @@ header.global {
.user {
@include float(right);
@extend %ui-print-excluded;
- margin-top: ($baseline/4);
padding-left: 0;
+ margin: 0;
> .primary {
display: block;
@@ -131,7 +131,7 @@ header.global {
&:last-child {
> a {
- padding: ($baseline/5) ($baseline/2);
+ padding: ($baseline/2);
&.shopping-cart {
border-radius: 4px;
@@ -143,14 +143,19 @@ header.global {
}
}
- a.user-link {
- @include padding(5px, 2px, 10px, 10px);
+ .user-link {
+ padding: 0;
position: relative;
text-transform: none;
font-size: 14px;
font-weight: bold;
letter-spacing: 0;
+ .user-image-frame {
+ max-width: ($baseline*2);
+ border-radius: 10%;
+ }
+
.icon {
display: inline-block;
@include float(left);
@@ -159,35 +164,9 @@ header.global {
color: $m-gray;
}
- .avatar {
-
- // CASE: right to left layout
-
- display: inline-block;
- @include left(8px);
- opacity: 0.5;
- overflow: hidden;
- top: 4px;
- margin-top: 1px;
- margin-right: 2px;
- @include transition(all 0.15s linear 0s);
- width: 19px;
- }
- div {
- margin-top: 3px;
- float: right;
- margin-left: 4px;
- }
- div {
- margin-top: 3px;
- float: right;
- margin-left: 4px;
- }
-
- &:hover, &:focus {
- .avatar {
- opacity: 0.8;
- }
+ .label-username {
+ @include float(right);
+ @include margin(($baseline*0.75), ($baseline/4), ($baseline*0.75), ($baseline*0.75));
}
}
@@ -467,7 +446,6 @@ header.global-new {
.user {
@include float(right);
- margin-top: 4px;
> .primary {
display: block;
@@ -485,7 +463,7 @@ header.global-new {
> a {
@include border-radius(0, 4px, 4px, 0);
@include border-left(none);
- padding: ($baseline/5) ($baseline/2);
+ padding: ($baseline/2) ($baseline/2);
&.shopping-cart {
border-radius: 4px;
@@ -496,14 +474,19 @@ header.global-new {
}
}
}
- a.user-link {
- @include padding(5px, 2px, 10px, 10px);
+ .user-link {
+ padding: 0;
position: relative;
text-transform: none;
font-size: 14px;
font-weight: bold;
letter-spacing: 0;
+ .user-image-frame {
+ max-width: ($baseline/2);
+ border-radius: 10%;
+ }
+
.icon {
display: inline-block;
@include float(left);
@@ -512,35 +495,9 @@ header.global-new {
color: $m-gray;
}
- .avatar {
-
- // CASE: right to left layout
-
- display: inline-block;
- @include left(8px);
- opacity: 0.5;
- overflow: hidden;
- top: 4px;
- margin-top: 1px;
- margin-right: 2px;
- @include transition(all 0.15s linear 0s);
- width: 19px;
- }
- div {
- margin-top: 3px;
- float: right;
- margin-left: 4px;
- }
- div {
- margin-top: 3px;
- float: right;
- margin-left: 4px;
- }
-
- &:hover, &:focus {
- .avatar {
- opacity: 0.8;
- }
+ .label-username {
+ @include float(right);
+ @include margin(($baseline*0.75), ($baseline/4), ($baseline*0.75), ($baseline*0.75));
}
}
diff --git a/lms/templates/courseware/course_about.html b/lms/templates/courseware/course_about.html
index dbddd8a5c108..02404fe1ad33 100644
--- a/lms/templates/courseware/course_about.html
+++ b/lms/templates/courseware/course_about.html
@@ -127,7 +127,7 @@
${_("You are enrolled in this course")}
%if show_courseware_link:
- ${_("View Courseware")}
+ ${_("View Course")}
%endif
diff --git a/lms/templates/courseware/course_updates.html b/lms/templates/courseware/course_updates.html
new file mode 100644
index 000000000000..6fc936b75324
--- /dev/null
+++ b/lms/templates/courseware/course_updates.html
@@ -0,0 +1,29 @@
+<%! from django.utils.translation import ugettext as _ %>
+
+
+ % for index, update in enumerate(visible_updates):
+
+ % if not update.get("is_error"):
+ ${update.get("date")}
+
+ % endif
+
+ ${update.get("content")}
+
+
+ % endfor
+
+
+ % for update in hidden_updates:
+
+ ${update.get("date")}
+
+ ${update.get("content")}
+
+ % endfor
+
+
+% if len(hidden_updates) > 0:
+
+% endif
+
diff --git a/lms/templates/courseware/date_summary.html b/lms/templates/courseware/date_summary.html
index 5fec70a850d0..754b78612d8e 100644
--- a/lms/templates/courseware/date_summary.html
+++ b/lms/templates/courseware/date_summary.html
@@ -11,7 +11,7 @@ ${date}
% endif
% if link and link_text:
- ${link_text}
+ ${link_text}
% endif
diff --git a/lms/templates/courseware/info.html b/lms/templates/courseware/info.html
index 0b9b20f39c93..416126a6e6c7 100644
--- a/lms/templates/courseware/info.html
+++ b/lms/templates/courseware/info.html
@@ -14,70 +14,80 @@
<%static:css group='style-course'/>
%block>
-
% if show_enroll_banner:
-
-
-
-
${_("You are not enrolled yet")}
-
+
+
+
+
${_("You are not enrolled yet")}
+
+
% endif
<%include file="/courseware/course_navigation.html" args="active_page='info'" />
-<%block name="js_extra">
-
-
-%block>
+<%static:require_module module_name="js/courseware/toggle_element_visibility" class_name="ToggleElementVisibility">
+ ToggleElementVisibility();
+%static:require_module>
<%block name="bodyclass">view-in-course view-course-info ${course.css_class or ''}%block>
+
+
+
${_("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:
+
+ % endif
+
- % if user.is_authenticated():
-
- % if studio_url is not None and masquerade and masquerade.role == 'staff':
-
- % endif
-
- ${_("Course Updates & News")}
- ${get_course_info_section(request, course, 'updates')}
-
-
- % if False:
- ${_("Important Course Dates")}
- ${get_course_date_summary(course, user)}
+
+ % if user.is_authenticated():
+
+ % if studio_url is not None and masquerade and masquerade.role == 'staff':
+
+ % endif
+ ${_("Course Updates and News")}
+ ${get_course_info_section(request, course, 'updates')}
+
+ % if SelfPacedConfiguration.current().enable_course_home_improvements:
+
+ ${_("Important Course Dates")}
+ ${get_course_date_summary(course, user)}
+
+ % endif
+ % else:
+
+ ${_("Course Updates and News")}
+ ${get_course_info_section(request, course, 'guest_updates')}
+
% endif
-
-
${_(course.info_sidebar_name)}
- ${get_course_info_section(request, course, 'handouts')}
-
+
+ % if user.is_authenticated():
+
+ ${_(course.info_section_name)}
+ ${get_course_info_section(request, course, 'handouts')}
+
% else:
-
- ${_("Course Updates & News")}
- ${get_course_info_section(request, course, 'guest_updates')}
-
-
- ${_("Course Handouts")}
- ${get_course_info_section(request, course, 'guest_handouts')}
-
+
+ ${_("Course Handouts")}
+ ${get_course_info_section(request, course, 'guest_handouts')}
+
% endif
diff --git a/lms/templates/navigation-edx.html b/lms/templates/navigation-edx.html
index 553ae3f7a711..609c01452611 100644
--- a/lms/templates/navigation-edx.html
+++ b/lms/templates/navigation-edx.html
@@ -7,6 +7,7 @@
from microsite_configuration import microsite
from microsite_configuration.templatetags.microsite import platform_name
+from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_image_urls_for_user
# App that handles subdomain specific branding
from branding import api as branding_api
@@ -79,7 +80,12 @@
${_("Dashboard for:")}
- ${user.username}
+ <%
+ username = user.username
+ profile_image_url = get_profile_image_urls_for_user(user)['medium']
+ %>
+
+ ${username}
diff --git a/lms/templates/navigation.html b/lms/templates/navigation.html
index f99ad4202f85..f4a21d55ef44 100644
--- a/lms/templates/navigation.html
+++ b/lms/templates/navigation.html
@@ -5,9 +5,10 @@
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
+from lms.djangoapps.ccx.overrides import get_current_ccx
from microsite_configuration import microsite
from microsite_configuration.templatetags.microsite import platform_name
-from lms.djangoapps.ccx.overrides import get_current_ccx
+from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_image_urls_for_user
# App that handles subdomain specific branding
from branding import api as branding_api
@@ -80,9 +81,12 @@