diff --git a/cms/djangoapps/contentstore/course_info_model.py b/cms/djangoapps/contentstore/course_info_model.py
index 570763bd28..c5632b3373 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 464e76d7f8..bb3e113b64 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 2b300b74b4..5e732e64ca 100644
--- a/cms/djangoapps/contentstore/tests/test_course_settings.py
+++ b/cms/djangoapps/contentstore/tests/test_course_settings.py
@@ -936,8 +936,8 @@ class CourseMetadataEditingTest(CourseTestCase):
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 94f92fe637..f9199c005a 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 @@ class CourseUpdateTest(CourseTestCase):
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 7fa84c264e..dc2d4640ce 100644
--- a/cms/templates/course_info.html
+++ b/cms/templates/course_info.html
@@ -27,7 +27,7 @@ from openedx.core.lib.js_utils import escape_json_dumps
"${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 d2e7b12b42..19c9a6a0e6 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -424,12 +424,12 @@ class CourseFields(object):
)
has_children = True
info_sidebar_name = String(
- display_name=_("Course Info Sidebar Name"),
+ 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 Handouts'))
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 14b6b19280..62fce42e3f 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
-import logging
-import textwrap
-from lxml import etree
-from path import Path as path
+from datetime import datetime
from fs.errors import ResourceNotFoundError
+import logging
+from lxml import etree
+import os
+from path import Path as path
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 @@ class HtmlBlock(object):
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 833e63f2a2..480c850d2b 100644
--- a/common/lib/xmodule/xmodule/tabs.py
+++ b/common/lib/xmodule/xmodule/tabs.py
@@ -305,8 +305,8 @@ class CourseTabList(List):
"""
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 @@ class CourseTabList(List):
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 @@ class CourseTabList(List):
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 @@ class CourseTabList(List):
"""
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 09949f2d91..74fc6ba4a7 100644
--- a/common/test/acceptance/pages/lms/tab_nav.py
+++ b/common/test/acceptance/pages/lms/tab_nav.py
@@ -21,7 +21,7 @@ class TabNavPage(PageObject):
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/tests/lms/test_certificate_web_view.py b/common/test/acceptance/tests/lms/test_certificate_web_view.py
index f6f5acb6e7..b547a9d3a6 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 @@ class CertificateProgressPageTest(UniqueCourseTest):
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 7609cbcd3e..8b8dd1da31 100644
--- a/common/test/acceptance/tests/lms/test_library.py
+++ b/common/test/acceptance/tests/lms/test_library.py
@@ -127,9 +127,9 @@ class LibraryContentTestBase(UniqueCourseTest):
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 984403a83f..dfff902371 100644
--- a/common/test/acceptance/tests/lms/test_lms.py
+++ b/common/test/acceptance/tests/lms/test_lms.py
@@ -597,7 +597,7 @@ class HighLevelTabTest(UniqueCourseTest):
# 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)
@@ -667,13 +667,13 @@ class HighLevelTabTest(UniqueCourseTest):
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']
@@ -862,7 +862,7 @@ class TooltipTest(UniqueCourseTest):
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.courseware_page.verify_tooltips_displayed()
@@ -1011,7 +1011,7 @@ class ProblemExecutionTest(UniqueCourseTest):
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)
@@ -1061,14 +1061,14 @@ class EntranceExamTest(UniqueCourseTest):
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 14fd06b496..13ffbb9fa8 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 @@ class XBlockAcidNoChildTest(XBlockAcidBase):
"""
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 @@ class XBlockAcidChildTest(XBlockAcidBase):
"""
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 @@ class XBlockAcidAsideTest(XBlockAcidBase):
"""
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 6413686223..0ed1493e2c 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 @@ class VideoBaseTest(UniqueCourseTest):
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 c7d79bc8aa..2d4285899b 100644
--- a/lms/djangoapps/course_wiki/tests/tests.py
+++ b/lms/djangoapps/course_wiki/tests/tests.py
@@ -103,8 +103,8 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
"""
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 20a9a2fca7..59448608cf 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 40a71d5615..4c7320d7e5 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 4520b410b0..e0d426cc0e 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 44207e1e71..9104fd5e33 100644
--- a/lms/djangoapps/courseware/field_overrides.py
+++ b/lms/djangoapps/courseware/field_overrides.py
@@ -228,11 +228,16 @@ class FieldOverrideProvider(object):
@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 c38b8961a6..e694705205 100644
--- a/lms/djangoapps/courseware/self_paced_overrides.py
+++ b/lms/djangoapps/courseware/self_paced_overrides.py
@@ -25,4 +25,4 @@ class SelfPacedDateOverrideProvider(FieldOverrideProvider):
@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 c4ddb6787e..721c967be0 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 a26ccd8490..de00034ce3 100644
--- a/lms/djangoapps/courseware/tests/test_about.py
+++ b/lms/djangoapps/courseware/tests/test_about.py
@@ -105,7 +105,7 @@ class AboutTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, EventTrackingT
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):
@@ -478,7 +478,7 @@ class AboutPurchaseCourseTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
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 a67bc96fca..9a600413bc 100644
--- a/lms/djangoapps/courseware/tests/test_course_info.py
+++ b/lms/djangoapps/courseware/tests/test_course_info.py
@@ -3,6 +3,7 @@ Test the course_info xblock
"""
import mock
from nose.plugins.attrib import attr
+from pyquery import PyQuery as pq
from urllib import urlencode
from ccx_keys.locator import CCXLocator
@@ -11,6 +12,7 @@ from django.core.urlresolvers import reverse
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,
@@ -92,6 +94,39 @@ class CourseInfoTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
+ def test_last_accessed_courseware_not_shown(self):
+ """
+ Test that the last accessed courseware link is not shown if there
+ is no course content.
+ """
+ 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)
+
class CourseInfoTestCaseCCX(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
"""
@@ -169,6 +204,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)
@@ -186,7 +222,7 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest
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 149bcbcdae..70c6d49858 100644
--- a/lms/djangoapps/courseware/tests/test_tabs.py
+++ b/lms/djangoapps/courseware/tests/test_tabs.py
@@ -483,9 +483,10 @@ class TabListTestCase(TabTestCase):
[{'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/views.py b/lms/djangoapps/courseware/views.py
index 31eaab5b2c..11d69b9176 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -72,6 +72,7 @@ from openedx.core.djangoapps.credit.api import (
)
from shoppingcart.models import CourseRegistrationCode
from shoppingcart.utils import is_shopping_cart_enabled
+from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from student.models import UserTestGroup, CourseEnrollment
from student.views import is_course_blocked
from util.cache import cache, cache_if_anonymous
@@ -549,8 +550,6 @@ def _index_bulk_op(request, course_key, chapter, section, position):
context['fragment'] = section_module.render(STUDENT_VIEW, section_render_context)
context['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.
@@ -559,22 +558,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:
@@ -729,6 +712,11 @@ 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_url'] = None
+ if SelfPacedConfiguration.current().enable_course_home_improvements:
+ context['last_accessed_courseware_url'] = get_last_accessed_courseware(course, request)
+
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:
@@ -739,6 +727,30 @@ def course_info(request, course_id):
return render_to_response('courseware/info.html', context)
+def get_last_accessed_courseware(course, request):
+ """
+ Return the URL the courseware module that this request's user last
+ accessed, or None if it cannot be found.
+ """
+ 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 url
+ return 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 a05fe5a8ec..e4eb865363 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 66f4c3f0c7..ebd16373ff 100644
--- a/lms/djangoapps/survey/tests/test_views.py
+++ b/lms/djangoapps/survey/tests/test_views.py
@@ -11,6 +11,7 @@ from django.core.urlresolvers import reverse
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 @@ class SurveyViewsTests(ModuleStoreTestCase):
# 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 ad50ac83a2..f2c946f96c 100644
--- a/lms/djangoapps/teams/tests/test_views.py
+++ b/lms/djangoapps/teams/tests/test_views.py
@@ -115,8 +115,8 @@ class TestDashboard(SharedModuleStoreTestCase):
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(22):
+ # Check the query count on the dashboard with no teams
+ with self.assertNumQueries(18):
self.client.get(self.teams_url)
# Create some teams
@@ -131,7 +131,7 @@ class TestDashboard(SharedModuleStoreTestCase):
team.add_user(self.user)
# Check the query count on the dashboard again
- with self.assertNumQueries(22):
+ with self.assertNumQueries(24):
self.client.get(self.teams_url)
def test_bad_course_id(self):
diff --git a/lms/static/js/courseware/course_home_events.js b/lms/static/js/courseware/course_home_events.js
new file mode 100644
index 0000000000..d1956bee97
--- /dev/null
+++ b/lms/static/js/courseware/course_home_events.js
@@ -0,0 +1,16 @@
+;(function(define) {
+ 'use strict';
+
+ define(['jquery', 'logger'], function ($, Logger) {
+ return function () {
+ $('.last-accessed-link').on('click', function (event) {
+ Logger.log('edx.course.home.resume_course.clicked', {
+ url: event.currentTarget.href
+ });
+ });
+ $('.date-summary-verified-upgrade-deadline .date-summary-link').on('click', function () {
+ Logger.log('edx.course.home.upgrade_verified.clicked', {});
+ });
+ };
+ });
+}).call(this, define || RequireJS.define);
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 0000000000..353c6a8078
--- /dev/null
+++ b/lms/static/js/courseware/toggle_element_visibility.js
@@ -0,0 +1,48 @@
+;(function (define) {
+ 'use strict';
+
+ define(['jquery', 'logger', 'moment'],
+ function ($, Logger, moment) {
+
+ return function () {
+ // define variables for code legibility
+ var toggleActionElements = $('.toggle-visibility-button');
+
+ var updateToggleActionText = function (elementIsHidden, actionElement) {
+ var show_text = actionElement.data('show');
+ var hide_text = actionElement.data('hide');
+
+ if (elementIsHidden) {
+ 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),
+ toggleTargetElement = toggleActionElement.siblings('.toggle-visibility-element'),
+ elementIsHidden = toggleTargetElement.is(':visible'),
+ date = toggleTargetElement.siblings('.date').text();
+
+ updateToggleActionText(elementIsHidden, toggleActionElement);
+
+ toggleActionElement.on('click', function (event) {
+ event.preventDefault();
+ toggleTargetElement.toggleClass('hidden');
+ updateToggleActionText(!toggleTargetElement.hasClass('hidden'), toggleActionElement);
+ Logger.log('edx.course.home.course_update.toggled', {
+ action: elementIsHidden ? 'hide' : 'show',
+ publish_date: moment(date, 'MMM DD, YYYY').format()
+ });
+ });
+ });
+ };
+ });
+})(define || RequireJS.define);
diff --git a/lms/static/js/fixtures/courseware/course_home_events.html b/lms/static/js/fixtures/courseware/course_home_events.html
new file mode 100644
index 0000000000..e115db5f41
--- /dev/null
+++ b/lms/static/js/fixtures/courseware/course_home_events.html
@@ -0,0 +1,14 @@
+
+
+
Verification Upgrade Deadline
+
in 3 weeks - Feb 12, 2016
+
You are still eligible to upgrade to a Verified Certificate! Pursue it to highlight the knowledge and skills you gain in this course.
+
+ Upgrade to Verified Certificate
+
+
+
+
+
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 0000000000..51935ba02f
--- /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/course_home_events.js b/lms/static/js/spec/courseware/course_home_events.js
new file mode 100644
index 0000000000..64eac2e901
--- /dev/null
+++ b/lms/static/js/spec/courseware/course_home_events.js
@@ -0,0 +1,23 @@
+define(['jquery', 'logger', 'js/courseware/course_home_events'], function ($, Logger, courseHomeEvents) {
+ 'use strict';
+
+ describe('Course home page eventing', function () {
+ beforeEach(function () {
+ loadFixtures('js/fixtures/courseware/course_home_events.html');
+ courseHomeEvents();
+ spyOn(Logger, 'log');
+ });
+
+ it('sends an event when "Resume Course" is clicked', function () {
+ $('.last-accessed-link').click();
+ expect(Logger.log).toHaveBeenCalledWith('edx.course.home.resume_course.clicked', {
+ url: "/courses/course-v1:edX+DemoX+Demo_Course/courseware/19a30717eff543078a5d94ae9d6c18a5/"
+ });
+ });
+
+ it('sends an event when "Upgrade to Verified" is clicked', function () {
+ $('.date-summary-link').click();
+ expect(Logger.log).toHaveBeenCalledWith('edx.course.home.upgrade_verified.clicked', {});
+ });
+ });
+});
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 0000000000..f8523f6974
--- /dev/null
+++ b/lms/static/js/spec/courseware/updates_visibility.js
@@ -0,0 +1,50 @@
+define(['jquery', 'logger', 'js/courseware/toggle_element_visibility'],
+ function ($, Logger, 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 */
+ spyOn(Logger, 'log');
+ });
+
+ it('ensures update will hide on hide button click', function () {
+ var $shownUpdate = $('.toggle-visibility-element:not(.hidden)').first(),
+ $updateButton = $shownUpdate.siblings('.toggle-visibility-button');
+ $updateButton.trigger('click');
+ expect($shownUpdate).toHaveClass('hidden');
+ expect($updateButton.text()).toEqual('Show');
+ });
+
+ it('ensures update will show on show button click', function () {
+ var $hiddenUpdate = $('.toggle-visibility-element.hidden').first(),
+ $updateButton = $hiddenUpdate.siblings('.toggle-visibility-button');
+ $updateButton.trigger('click');
+ expect($hiddenUpdate).not.toHaveClass('hidden');
+ expect($updateButton.text()).toEqual('Hide');
+ });
+
+ 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');
+ });
+
+ it('sends a tracking event on hide and show', function () {
+ var $update = $('.toggle-visibility-element:not(.hidden)').first();
+ $update.siblings('.toggle-visibility-button').trigger('click');
+ expect(Logger.log).toHaveBeenCalledWith('edx.course.home.course_update.toggled', {
+ action: 'hide',
+ publish_date: '2015-12-01T00:00:00+00:00'
+ });
+ });
+ });
+ });
diff --git a/lms/static/js/spec/main.js b/lms/static/js/spec/main.js
index 427b066f55..3d0ae676d1 100644
--- a/lms/static/js/spec/main.js
+++ b/lms/static/js/spec/main.js
@@ -708,6 +708,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 d1d8102dca..5570d4a40b 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 01afb7624d..128f14f526 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 f72403bd37..6e68f74e90 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 0bbca1d757..f20bfe4baa 100644
--- a/lms/static/sass/course/_info.scss
+++ b/lms/static/sass/course/_info.scss
@@ -1,11 +1,59 @@
+.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;
+ border-radius: 2px;
+
+ .last-accessed-link {
+ @extend %t-title6;
+ color: $very-light-text;
+ }
+ }
+}
+
div.info-wrapper {
+ background-color: $homepage-background;
+
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: $baseline;
+ font-style: normal;
}
> p {
@@ -17,12 +65,31 @@ 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);
+ background-color: $white;
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 {
@@ -81,8 +162,7 @@ div.info-wrapper {
padding: 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;
@@ -97,15 +177,14 @@ div.info-wrapper {
h1 {
@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;
}
@@ -119,7 +198,8 @@ div.info-wrapper {
padding: 0;
color: $link-color;
- &:hover, &:focus {
+ &:hover,
+ &:focus {
background: transparent;
}
}
@@ -154,7 +234,8 @@ div.info-wrapper {
display: inline-block;
padding: 0;
- &:hover, &:focus {
+ &:hover,
+ &:focus {
background: transparent;
}
}
@@ -175,7 +256,8 @@ div.info-wrapper {
display: inline-block;
padding: 0;
- &:hover, &:focus {
+ &:hover,
+ &:focus {
background: transparent;
}
}
@@ -191,7 +273,8 @@ div.info-wrapper {
position: absolute;
width: 100%;
- &:hover, &:focus {
+ &:hover,
+ &:focus {
opacity: 0.6;
filter: alpha(opacity=60);
diff --git a/lms/static/sass/course/base/_base.scss b/lms/static/sass/course/base/_base.scss
index 7aba39d2b6..775a77995a 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 4b01542e8c..f3519cd4fd 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 fd5bc1f996..d1dc31e846 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;
.settings-language-form {
margin-top: 4px;
@@ -135,7 +135,7 @@ header.global {
&:last-child {
> a {
- padding: ($baseline/5) ($baseline/2);
+ padding: ($baseline/2);
&.shopping-cart {
border-radius: 4px;
@@ -147,14 +147,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);
@@ -163,35 +168,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));
}
}
@@ -471,7 +450,6 @@ header.global-new {
.user {
@include float(right);
- margin-top: 4px;
> .primary {
display: block;
@@ -489,7 +467,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;
@@ -500,14 +478,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);
@@ -516,35 +499,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 d376099e85..b6cedfda38 100644
--- a/lms/templates/courseware/course_about.html
+++ b/lms/templates/courseware/course_about.html
@@ -126,7 +126,7 @@ from openedx.core.lib.courses import course_image_url
${_("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 0000000000..6fc936b753
--- /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 5fec70a850..754b78612d 100644
--- a/lms/templates/courseware/date_summary.html
+++ b/lms/templates/courseware/date_summary.html
@@ -11,7 +11,7 @@
% 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 71eedeb4f5..1ee8b6b396 100644
--- a/lms/templates/courseware/info.html
+++ b/lms/templates/courseware/info.html
@@ -14,50 +14,67 @@ from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
<%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'" />
+<%static:require_module module_name="js/courseware/toggle_element_visibility" class_name="ToggleElementVisibility">
+ ToggleElementVisibility();
+%static:require_module>
+<%static:require_module module_name="js/courseware/course_home_events" class_name="CourseHomeEvents">
+ CourseHomeEvents();
+%static:require_module>
+
<%block name="js_extra">
-
-
+
+
%block>
<%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_url:
+
+ % endif
+
% if user.is_authenticated():
% if studio_url is not None and masquerade and masquerade.role == 'staff':
-
+
% endif
- ${_("Course Updates & News")}
+ ${_("Course Updates and News")}
${get_course_info_section(request, masquerade_user, course, 'updates')}
diff --git a/lms/templates/navigation-edx.html b/lms/templates/navigation-edx.html
index 0e005d0d1d..320dbbc953 100644
--- a/lms/templates/navigation-edx.html
+++ b/lms/templates/navigation-edx.html
@@ -5,6 +5,9 @@
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
+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
# app that handles site status messages
@@ -76,7 +79,12 @@ site_status_msg = get_site_status_msg(course_id)
${_("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 75f7b1f612..962153e5d1 100644
--- a/lms/templates/navigation.html
+++ b/lms/templates/navigation.html
@@ -6,6 +6,9 @@ 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 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
@@ -101,9 +104,12 @@ site_status_msg = get_site_status_msg(course_id)
${_("Dashboard for:")}
-
- ${user.username}
-
+ <%
+ username = user.username
+ profile_image_url = get_profile_image_urls_for_user(user)['medium']
+ %>
+
+ ${username}
diff --git a/openedx/core/djangoapps/user_api/accounts/image_helpers.py b/openedx/core/djangoapps/user_api/accounts/image_helpers.py
index 00de36609c..b204d6daf6 100644
--- a/openedx/core/djangoapps/user_api/accounts/image_helpers.py
+++ b/openedx/core/djangoapps/user_api/accounts/image_helpers.py
@@ -92,13 +92,18 @@ def get_profile_image_urls_for_user(user, request=None):
dictionary of {size_display_name: url} for each image.
"""
- if user.profile.has_profile_image:
- urls = _get_profile_image_urls(
- _make_profile_image_name(user.username),
- get_profile_image_storage(),
- version=user.profile.profile_image_uploaded_at.strftime("%s"),
- )
- else:
+ try:
+ if user.profile.has_profile_image:
+ urls = _get_profile_image_urls(
+ _make_profile_image_name(user.username),
+ get_profile_image_storage(),
+ version=user.profile.profile_image_uploaded_at.strftime("%s"),
+ )
+ else:
+ urls = _get_default_profile_image_urls()
+ except UserProfile.DoesNotExist:
+ # when user does not have profile it raises exception, when exception
+ # occur we can simply get default image.
urls = _get_default_profile_image_urls()
if request:
diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py
index 2ce732e056..e525c14743 100644
--- a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py
+++ b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py
@@ -538,6 +538,11 @@ class TestAccountAPI(UserAPITestCase):
verify_change_info(name_change_info[0], old_name, self.user.username, "Donald Duck",)
verify_change_info(name_change_info[1], "Mickey Mouse", self.user.username, "Donald Duck")
+ @patch.dict(
+ 'openedx.core.djangoapps.user_api.accounts.image_helpers.PROFILE_IMAGE_SIZES_MAP',
+ {'full': 50, 'medium': 30, 'small': 10},
+ clear=True
+ )
def test_patch_email(self):
"""
Test that the user can request an email change through the accounts API.