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"
{list_items}
".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"

{date}

{content}
".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} - ); + ); }); 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 + +
+
+ + + 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 +
+ + +% 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'/> - % if show_enroll_banner: -
-
-
-

${_("You are not enrolled yet")}

-
-

- ${_(u"You are not currently enrolled in this course. {link_start}Sign up now!{link_end}").format( - link_start=u"".format(url_to_enroll), - link_end=u"" - )} -

-
+
+
+
+

${_("You are not enrolled yet")}

+
+

+ ${_(u"You are not currently enrolled in this course. {link_start}Sign up now!{link_end}").format( + link_start=u"".format(url_to_enroll), + link_end=u"" + )} +

+
% endif <%include file="/courseware/course_navigation.html" args="active_page='info'" /> -<%block name="js_extra"> - - - +<%static:require_module module_name="js/courseware/toggle_element_visibility" class_name="ToggleElementVisibility"> + ToggleElementVisibility(); + <%block name="bodyclass">view-in-course view-course-info ${course.css_class or ''}
+
+
+

${_("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'] + %> + ${_('Profile image for {username}').format(username=username)} +
    ${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 @@

    ${course.display_org_with_defau
  • ${_("Dashboard for:")} -
    - ${user.username} -
    + <% + username = user.username + profile_image_url = get_profile_image_urls_for_user(user)['medium'] + %> + ${_('Profile image for {username}').format(username=username)} +
    ${username}
  • diff --git a/openedx/core/djangoapps/user_api/accounts/image_helpers.py b/openedx/core/djangoapps/user_api/accounts/image_helpers.py index 00de36609c39..b204d6daf6ee 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 2ce732e056a5..e525c14743c7 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 @@ def verify_change_info(change_info, old_name, requester, new_name): 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.