Commit 8ed051e3 by Peter Fogg

Merge pull request #10642 from edx/feature/self-paced

Course home page improvements.
parents 9a786d08 244843d9
...@@ -160,25 +160,13 @@ def _get_index(passed_id=None): ...@@ -160,25 +160,13 @@ def _get_index(passed_id=None):
return 0 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"<article><h2>{date}</h2>{content}</article>".format(**update))
return u"<section>{list_items}</section>".format(list_items="".join(list_items))
def save_course_update_items(location, course_updates, course_update_items, user=None): 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") 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. and html related to course update in 'data' ("course_updates.data") field.
""" """
course_updates.items = course_update_items course_updates.items = course_update_items
course_updates.data = _get_html(course_update_items) course_updates.data = ""
# update db record # update db record
modulestore().update_item(course_updates, user.id) modulestore().update_item(course_updates, user.id)
......
...@@ -98,25 +98,25 @@ def _verify_page_names(first, second): ...@@ -98,25 +98,25 @@ def _verify_page_names(first, second):
@step(u'the built-in pages are in the default order$') @step(u'the built-in pages are in the default order$')
def built_in_pages_in_default_order(step): 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) see_pages_in_expected_order(expected_pages)
@step(u'the built-in pages are switched$') @step(u'the built-in pages are switched$')
def built_in_pages_switched(step): 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) see_pages_in_expected_order(expected_pages)
@step(u'the pages are in the default order$') @step(u'the pages are in the default order$')
def pages_in_default_order(step): 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) see_pages_in_expected_order(expected_pages)
@step(u'the pages are switched$$') @step(u'the pages are switched$$')
def pages_are_switched(step): 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) see_pages_in_expected_order(expected_pages)
......
...@@ -936,8 +936,8 @@ class CourseMetadataEditingTest(CourseTestCase): ...@@ -936,8 +936,8 @@ class CourseMetadataEditingTest(CourseTestCase):
self.assertNotIn("notes", course.advanced_modules) self.assertNotIn("notes", course.advanced_modules)
@ddt.data( @ddt.data(
[{'type': 'courseware'}, {'type': 'course_info'}, {'type': 'wiki', 'is_hidden': True}], [{'type': 'course_info'}, {'type': 'courseware'}, {'type': 'wiki', 'is_hidden': True}],
[{'type': 'courseware', 'name': 'Courses'}, {'type': 'course_info', 'name': 'Info'}], [{'type': 'course_info', 'name': 'Home'}, {'type': 'courseware', 'name': 'Course'}],
) )
def test_course_tab_configurations(self, tab_list): def test_course_tab_configurations(self, tab_list):
self.course.tabs = tab_list self.course.tabs = tab_list
......
...@@ -173,9 +173,8 @@ class CourseUpdateTest(CourseTestCase): ...@@ -173,9 +173,8 @@ class CourseUpdateTest(CourseTestCase):
self.assertHTMLEqual(update_content, json.loads(resp.content)['content']) self.assertHTMLEqual(update_content, json.loads(resp.content)['content'])
course_updates = modulestore().get_item(location) course_updates = modulestore().get_item(location)
self.assertEqual(course_updates.items, [{u'date': update_date, u'content': update_content, u'id': 1}]) self.assertEqual(course_updates.items, [{u'date': update_date, u'content': update_content, u'id': 1}])
# course_updates 'data' field should update accordingly # course_updates 'data' field should not update automatically
update_data = u"<section><article><h2>{date}</h2>{content}</article></section>".format(date=update_date, content=update_content) self.assertEqual(course_updates.data, '')
self.assertEqual(course_updates.data, update_data)
# test delete course update item (soft delete) # test delete course update item (soft delete)
course_updates = modulestore().get_item(location) course_updates = modulestore().get_item(location)
......
...@@ -27,7 +27,7 @@ from openedx.core.lib.js_utils import escape_json_dumps ...@@ -27,7 +27,7 @@ from openedx.core.lib.js_utils import escape_json_dumps
"${handouts_locator | escapejs}", "${handouts_locator | escapejs}",
"${base_asset_url}", "${base_asset_url}",
${escape_json_dumps(push_notification_enabled) | n} ${escape_json_dumps(push_notification_enabled) | n}
); );
}); });
</%block> </%block>
......
...@@ -424,12 +424,12 @@ class CourseFields(object): ...@@ -424,12 +424,12 @@ class CourseFields(object):
) )
has_children = True has_children = True
info_sidebar_name = String( info_sidebar_name = String(
display_name=_("Course Info Sidebar Name"), display_name=_("Course Home Sidebar Name"),
help=_( 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." "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( show_timezone = Boolean(
help=_( help=_(
"True if timezones should be shown on dates in the courseware. " "True if timezones should be shown on dates in the courseware. "
......
import os
import sys
import re
import copy import copy
from datetime import datetime
from fs.errors import ResourceNotFoundError
import logging import logging
import textwrap
from lxml import etree from lxml import etree
import os
from path import Path as path from path import Path as path
from fs.errors import ResourceNotFoundError
from pkg_resources import resource_string from pkg_resources import resource_string
import re
import sys
import textwrap
import dogstats_wrapper as dog_stats_api import dogstats_wrapper as dog_stats_api
from xmodule.util.misc import escape_html_characters from xmodule.util.misc import escape_html_characters
...@@ -75,10 +76,10 @@ class HtmlBlock(object): ...@@ -75,10 +76,10 @@ class HtmlBlock(object):
return Fragment(self.get_html()) return Fragment(self.get_html())
def get_html(self): 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. # 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 # pylint: disable=no-member
if self.system.anonymous_student_id: if self.system.anonymous_student_id:
return self.data.replace("%%USER_ID%%", self.system.anonymous_student_id) return self.data.replace("%%USER_ID%%", self.system.anonymous_student_id)
...@@ -417,6 +418,35 @@ class CourseInfoModule(CourseInfoFields, HtmlModuleMixin): ...@@ -417,6 +418,35 @@ class CourseInfoModule(CourseInfoFields, HtmlModuleMixin):
# statuses # statuses
STATUS_VISIBLE = 'visible' STATUS_VISIBLE = 'visible'
STATUS_DELETED = 'deleted' 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") @XBlock.tag("detached")
......
...@@ -305,8 +305,8 @@ class CourseTabList(List): ...@@ -305,8 +305,8 @@ class CourseTabList(List):
""" """
course.tabs.extend([ 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 # Presence of syllabus tab is indicated by a course attribute
...@@ -390,6 +390,19 @@ class CourseTabList(List): ...@@ -390,6 +390,19 @@ class CourseTabList(List):
yield tab yield tab
@classmethod @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): def validate_tabs(cls, tabs):
""" """
Check that the tabs set for the specified course is valid. If it Check that the tabs set for the specified course is valid. If it
...@@ -406,13 +419,13 @@ class CourseTabList(List): ...@@ -406,13 +419,13 @@ class CourseTabList(List):
if len(tabs) < 2: if len(tabs) < 2:
raise InvalidTabsException("Expected at least two tabs. tabs: '{0}'".format(tabs)) 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( 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( 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 # the following tabs should appear only once
# TODO: don't import openedx capabilities from common # TODO: don't import openedx capabilities from common
...@@ -455,6 +468,7 @@ class CourseTabList(List): ...@@ -455,6 +468,7 @@ class CourseTabList(List):
""" """
Overrides the from_json method to de-serialize the CourseTab objects from a json-like representation. Overrides the from_json method to de-serialize the CourseTab objects from a json-like representation.
""" """
self.upgrade_tabs(values)
self.validate_tabs(values) self.validate_tabs(values)
tabs = [] tabs = []
for tab_dict in values: for tab_dict in values:
......
...@@ -21,7 +21,7 @@ class TabNavPage(PageObject): ...@@ -21,7 +21,7 @@ class TabNavPage(PageObject):
Navigate to the tab `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)) self.warning("'{0}' is not a valid tab name".format(tab_name))
# The only identifier for individual tabs is the link href # The only identifier for individual tabs is the link href
......
...@@ -206,7 +206,7 @@ class CertificateProgressPageTest(UniqueCourseTest): ...@@ -206,7 +206,7 @@ class CertificateProgressPageTest(UniqueCourseTest):
Problems were added in the setUp Problems were added in the setUp
""" """
self.course_info_page.visit() 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 # Navigate to Test Subsection in Test Section Section
self.course_nav.go_to_section('Test Section', 'Test Subsection') self.course_nav.go_to_section('Test Section', 'Test Subsection')
......
...@@ -127,9 +127,9 @@ class LibraryContentTestBase(UniqueCourseTest): ...@@ -127,9 +127,9 @@ class LibraryContentTestBase(UniqueCourseTest):
Open library page in LMS Open library page in LMS
""" """
self.courseware_page.visit() self.courseware_page.visit()
paragraphs = self.courseware_page.q(css='.course-content p') paragraphs = self.courseware_page.q(css='.course-content p').results
if paragraphs and "You were most recently in" in paragraphs.text[0]: if not paragraphs:
paragraphs[0].find_element_by_tag_name('a').click() 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 block_id = block_id if block_id is not None else self.lib_block.locator
#pylint: disable=attribute-defined-outside-init #pylint: disable=attribute-defined-outside-init
self.library_content_page = LibraryContentXBlockWrapper(self.browser, block_id) self.library_content_page = LibraryContentXBlockWrapper(self.browser, block_id)
......
...@@ -597,7 +597,7 @@ class HighLevelTabTest(UniqueCourseTest): ...@@ -597,7 +597,7 @@ class HighLevelTabTest(UniqueCourseTest):
# Navigate to the course info page from the progress page # Navigate to the course info page from the progress page
self.progress_page.visit() self.progress_page.visit()
self.tab_nav.go_to_tab('Course Info') self.tab_nav.go_to_tab('Home')
# Expect just one update # Expect just one update
self.assertEqual(self.course_info_page.num_updates, 1) self.assertEqual(self.course_info_page.num_updates, 1)
...@@ -667,13 +667,13 @@ class HighLevelTabTest(UniqueCourseTest): ...@@ -667,13 +667,13 @@ class HighLevelTabTest(UniqueCourseTest):
def test_courseware_nav(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.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 = { EXPECTED_SECTIONS = {
'Test Section': ['Test Subsection'], 'Test Section': ['Test Subsection'],
'Test Section 2': ['Test Subsection 2', 'Test Subsection 3'] 'Test Section 2': ['Test Subsection 2', 'Test Subsection 3']
...@@ -862,7 +862,7 @@ class TooltipTest(UniqueCourseTest): ...@@ -862,7 +862,7 @@ class TooltipTest(UniqueCourseTest):
Verify that tooltips are displayed when you hover over the sequence nav bar. Verify that tooltips are displayed when you hover over the sequence nav bar.
""" """
self.course_info_page.visit() 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() self.courseware_page.verify_tooltips_displayed()
...@@ -1011,7 +1011,7 @@ class ProblemExecutionTest(UniqueCourseTest): ...@@ -1011,7 +1011,7 @@ class ProblemExecutionTest(UniqueCourseTest):
def test_python_execution_in_problem(self): def test_python_execution_in_problem(self):
# Navigate to the problem page # Navigate to the problem page
self.course_info_page.visit() 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') self.course_nav.go_to_section('Test Section', 'Test Subsection')
problem_page = ProblemPage(self.browser) problem_page = ProblemPage(self.browser)
...@@ -1061,14 +1061,14 @@ class EntranceExamTest(UniqueCourseTest): ...@@ -1061,14 +1061,14 @@ class EntranceExamTest(UniqueCourseTest):
def test_entrance_exam_section(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. page.
Given that I am on the courseware page Given that I am on the course page
When I view the courseware that has an entrance exam When I view the course that has an entrance exam
Then there should be an "Entrance Exam" chapter.' Then there should be an "Entrance Exam" chapter.'
""" """
entrance_exam_link_selector = '.accordion .course-navigation .chapter .group-heading' 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.visit()
self.courseware_page.wait_for_page() self.courseware_page.wait_for_page()
self.assertFalse(element_has_text( self.assertFalse(element_has_text(
......
...@@ -75,7 +75,7 @@ class XBlockAcidNoChildTest(XBlockAcidBase): ...@@ -75,7 +75,7 @@ class XBlockAcidNoChildTest(XBlockAcidBase):
""" """
self.course_info_page.visit() 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]') acid_block = AcidView(self.browser, '.xblock-student_view[data-block-type=acid]')
self.validate_acid_block_view(acid_block) self.validate_acid_block_view(acid_block)
...@@ -119,7 +119,7 @@ class XBlockAcidChildTest(XBlockAcidBase): ...@@ -119,7 +119,7 @@ class XBlockAcidChildTest(XBlockAcidBase):
""" """
self.course_info_page.visit() 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]') acid_parent_block = AcidView(self.browser, '.xblock-student_view[data-block-type=acid_parent]')
self.validate_acid_parent_block_view(acid_parent_block) self.validate_acid_parent_block_view(acid_parent_block)
...@@ -159,7 +159,7 @@ class XBlockAcidAsideTest(XBlockAcidBase): ...@@ -159,7 +159,7 @@ class XBlockAcidAsideTest(XBlockAcidBase):
""" """
self.course_info_page.visit() 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]') acid_aside = AcidView(self.browser, '.xblock_asides-v1-student_view[data-block-type=acid_aside]')
self.validate_acid_aside_view(acid_aside) self.validate_acid_aside_view(acid_aside)
......
...@@ -132,7 +132,7 @@ class VideoBaseTest(UniqueCourseTest): ...@@ -132,7 +132,7 @@ class VideoBaseTest(UniqueCourseTest):
self.auth_page.visit() self.auth_page.visit()
self.user_info = self.auth_page.user_info self.user_info = self.auth_page.user_info
self.course_info_page.visit() 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): def _navigate_to_courseware_video_and_render(self):
""" Wait for the video player to render """ """ Wait for the video player to render """
......
...@@ -103,8 +103,8 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase): ...@@ -103,8 +103,8 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
""" """
Ensure that the response has the course navigator. Ensure that the response has the course navigator.
""" """
self.assertContains(resp, "Course Info") self.assertContains(resp, "Home")
self.assertContains(resp, "courseware") self.assertContains(resp, "Course")
@patch.dict("django.conf.settings.FEATURES", {'ALLOW_WIKI_ROOT_ACCESS': True}) @patch.dict("django.conf.settings.FEATURES", {'ALLOW_WIKI_ROOT_ACCESS': True})
def test_course_navigator(self): def test_course_navigator(self):
......
...@@ -186,7 +186,10 @@ class VerifiedUpgradeDeadlineDate(DateSummary): ...@@ -186,7 +186,10 @@ class VerifiedUpgradeDeadlineDate(DateSummary):
""" """
css_class = 'verified-upgrade-deadline' css_class = 'verified-upgrade-deadline'
title = _('Verification 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') link_text = _('Upgrade to Verified Certificate')
@property @property
......
...@@ -19,10 +19,3 @@ Feature: LMS.Navigate Course ...@@ -19,10 +19,3 @@ Feature: LMS.Navigate Course
When I navigate to an item in a sequence When I navigate to an item in a sequence
Then I see the content of the sequence item Then I see the content of the sequence item
And a "seq_goto" browser event is emitted 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
# pylint: disable=missing-docstring # pylint: disable=missing-docstring
# pylint: disable=redefined-outer-name # pylint: disable=redefined-outer-name
# pylint: disable=unused-argument
from lettuce import world, step from lettuce import world, step
from common import course_location from common import course_location
...@@ -127,17 +128,12 @@ def then_i_see_the_content_of_the_sequence_item(step): ...@@ -127,17 +128,12 @@ def then_i_see_the_content_of_the_sequence_item(step):
wait_for_problem('Problem 6') wait_for_problem('Problem 6')
@step(u'I return to the courseware') @step(u'I return to the course')
def and_i_return_to_the_courseware(step): def and_i_return_to_the_course(step):
world.visit('/') world.visit('/')
world.click_link("View Course") world.click_link("View Course")
world.click_link("Courseware") course = 'a[href*="/courseware"]'
world.css_click(course)
@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)
def create_course(): def create_course():
......
...@@ -228,11 +228,16 @@ class FieldOverrideProvider(object): ...@@ -228,11 +228,16 @@ class FieldOverrideProvider(object):
@abstractmethod @abstractmethod
def enabled_for(self, course): # pragma no cover 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 return False
......
...@@ -25,4 +25,4 @@ class SelfPacedDateOverrideProvider(FieldOverrideProvider): ...@@ -25,4 +25,4 @@ class SelfPacedDateOverrideProvider(FieldOverrideProvider):
@classmethod @classmethod
def enabled_for(cls, course): def enabled_for(cls, course):
"""This provider is enabled for self-paced courses only.""" """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
...@@ -28,7 +28,7 @@ class CoursewareTab(EnrolledTab): ...@@ -28,7 +28,7 @@ class CoursewareTab(EnrolledTab):
The main courseware view. The main courseware view.
""" """
type = 'courseware' type = 'courseware'
title = ugettext_noop('Courseware') title = ugettext_noop('Course')
priority = 10 priority = 10
view_name = 'courseware' view_name = 'courseware'
is_movable = False is_movable = False
...@@ -40,7 +40,7 @@ class CourseInfoTab(CourseTab): ...@@ -40,7 +40,7 @@ class CourseInfoTab(CourseTab):
The course info view. The course info view.
""" """
type = 'course_info' type = 'course_info'
title = ugettext_noop('Course Info') title = ugettext_noop('Home')
priority = 20 priority = 20
view_name = 'info' view_name = 'info'
tab_id = 'info' tab_id = 'info'
......
...@@ -105,7 +105,7 @@ class AboutTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, EventTrackingT ...@@ -105,7 +105,7 @@ class AboutTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, EventTrackingT
resp = self.client.get(url) resp = self.client.get(url)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
self.assertIn("You are enrolled in this course", resp.content) 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") @override_settings(COURSE_ABOUT_VISIBILITY_PERMISSION="see_about_page")
def test_visible_about_page_settings(self): def test_visible_about_page_settings(self):
...@@ -478,7 +478,7 @@ class AboutPurchaseCourseTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase): ...@@ -478,7 +478,7 @@ class AboutPurchaseCourseTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
resp = self.client.get(url) resp = self.client.get(url)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
self.assertIn("You are enrolled in this course", resp.content) 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 <span>($10 USD)</span>", resp.content) self.assertNotIn("Add buyme to Cart <span>($10 USD)</span>", resp.content)
def test_closed_enrollment(self): def test_closed_enrollment(self):
......
...@@ -3,6 +3,7 @@ Test the course_info xblock ...@@ -3,6 +3,7 @@ Test the course_info xblock
""" """
import mock import mock
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from pyquery import PyQuery as pq
from urllib import urlencode from urllib import urlencode
from ccx_keys.locator import CCXLocator from ccx_keys.locator import CCXLocator
...@@ -11,6 +12,7 @@ from django.core.urlresolvers import reverse ...@@ -11,6 +12,7 @@ from django.core.urlresolvers import reverse
from django.test.utils import override_settings from django.test.utils import override_settings
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from util.date_utils import strftime_localized from util.date_utils import strftime_localized
from xmodule.modulestore.tests.django_utils import ( from xmodule.modulestore.tests.django_utils import (
ModuleStoreTestCase, ModuleStoreTestCase,
...@@ -92,6 +94,39 @@ class CourseInfoTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase): ...@@ -92,6 +94,39 @@ class CourseInfoTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 404) 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): class CourseInfoTestCaseCCX(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
""" """
...@@ -169,6 +204,7 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest ...@@ -169,6 +204,7 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest
""" """
def setUp(self): def setUp(self):
SelfPacedConfiguration(enabled=True).save()
super(SelfPacedCourseInfoTestCase, self).setUp() super(SelfPacedCourseInfoTestCase, self).setUp()
self.instructor_paced_course = CourseFactory.create(self_paced=False) self.instructor_paced_course = CourseFactory.create(self_paced=False)
self.self_paced_course = CourseFactory.create(self_paced=True) self.self_paced_course = CourseFactory.create(self_paced=True)
...@@ -186,7 +222,7 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest ...@@ -186,7 +222,7 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
def test_num_queries_instructor_paced(self): 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): 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)
...@@ -483,9 +483,10 @@ class TabListTestCase(TabTestCase): ...@@ -483,9 +483,10 @@ class TabListTestCase(TabTestCase):
[{'type': CoursewareTab.type}], [{'type': CoursewareTab.type}],
# missing course_info # missing course_info
[{'type': CoursewareTab.type}, {'type': 'discussion', 'name': 'fake_name'}], [{'type': CoursewareTab.type}, {'type': 'discussion', 'name': 'fake_name'}],
[{'type': 'unknown_type'}],
# incorrect order # incorrect order
[{'type': CourseInfoTab.type, 'name': 'fake_name'}, {'type': CoursewareTab.type}], [{'type': 'discussion', 'name': 'fake_name'},
[{'type': 'unknown_type'}] {'type': CourseInfoTab.type, 'name': 'fake_name'}, {'type': CoursewareTab.type}],
] ]
# tab types that should appear only once # tab types that should appear only once
......
...@@ -72,6 +72,7 @@ from openedx.core.djangoapps.credit.api import ( ...@@ -72,6 +72,7 @@ from openedx.core.djangoapps.credit.api import (
) )
from shoppingcart.models import CourseRegistrationCode from shoppingcart.models import CourseRegistrationCode
from shoppingcart.utils import is_shopping_cart_enabled 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.models import UserTestGroup, CourseEnrollment
from student.views import is_course_blocked from student.views import is_course_blocked
from util.cache import cache, cache_if_anonymous from util.cache import cache, cache_if_anonymous
...@@ -549,8 +550,6 @@ def _index_bulk_op(request, course_key, chapter, section, position): ...@@ -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['fragment'] = section_module.render(STUDENT_VIEW, section_render_context)
context['section_title'] = section_descriptor.display_name_with_default_escaped context['section_title'] = section_descriptor.display_name_with_default_escaped
else: else:
# section is none, so display a message
studio_url = get_studio_url(course, 'course')
prev_section = get_current_child(chapter_module) prev_section = get_current_child(chapter_module)
if prev_section is None: if prev_section is None:
# Something went wrong -- perhaps this chapter has no sections visible to the user. # 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): ...@@ -559,22 +558,6 @@ def _index_bulk_op(request, course_key, chapter, section, position):
course_module.position = None course_module.position = None
course_module.save() course_module.save()
return redirect(reverse('courseware', args=[course.id.to_deprecated_string()])) 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) result = render_to_response('courseware/courseware.html', context)
except Exception as e: except Exception as e:
...@@ -729,6 +712,11 @@ def course_info(request, course_id): ...@@ -729,6 +712,11 @@ def course_info(request, course_id):
'url_to_enroll': url_to_enroll, '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()) now = datetime.now(UTC())
effective_start = _adjust_start_date_for_beta_testers(user, course, course_key) effective_start = _adjust_start_date_for_beta_testers(user, course, course_key)
if not in_preview_mode() and staff_access and now < effective_start: if not in_preview_mode() and staff_access and now < effective_start:
...@@ -739,6 +727,30 @@ def course_info(request, course_id): ...@@ -739,6 +727,30 @@ def course_info(request, course_id):
return render_to_response('courseware/info.html', context) 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_csrf_cookie
@ensure_valid_course_key @ensure_valid_course_key
def static_tab(request, course_id, tab_slug): def static_tab(request, course_id, tab_slug):
......
...@@ -42,7 +42,7 @@ class LmsBlockMixin(XBlockMixin): ...@@ -42,7 +42,7 @@ class LmsBlockMixin(XBlockMixin):
scope=Scope.settings, scope=Scope.settings,
) )
chrome = String( chrome = String(
display_name=_("Courseware Chrome"), display_name=_("Course Chrome"),
# Translators: DO NOT translate the words in quotes here, they are # Translators: DO NOT translate the words in quotes here, they are
# specific words for the acceptable values. # 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" 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): ...@@ -55,7 +55,7 @@ class LmsBlockMixin(XBlockMixin):
) )
default_tab = String( default_tab = String(
display_name=_("Default Tab"), 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, scope=Scope.settings,
default=None, default=None,
) )
......
...@@ -11,6 +11,7 @@ from django.core.urlresolvers import reverse ...@@ -11,6 +11,7 @@ from django.core.urlresolvers import reverse
from survey.models import SurveyForm, SurveyAnswer from survey.models import SurveyForm, SurveyAnswer
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
...@@ -29,7 +30,7 @@ class SurveyViewsTests(ModuleStoreTestCase): ...@@ -29,7 +30,7 @@ class SurveyViewsTests(ModuleStoreTestCase):
# Create two accounts # Create two accounts
self.password = 'abc' 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_survey_name = 'TestSurvey'
self.test_form = ''' self.test_form = '''
......
...@@ -115,8 +115,8 @@ class TestDashboard(SharedModuleStoreTestCase): ...@@ -115,8 +115,8 @@ class TestDashboard(SharedModuleStoreTestCase):
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id) CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id)
self.client.login(username=self.user.username, password=self.test_password) self.client.login(username=self.user.username, password=self.test_password)
# Check the query count on the dashboard With no teams # Check the query count on the dashboard with no teams
with self.assertNumQueries(22): with self.assertNumQueries(18):
self.client.get(self.teams_url) self.client.get(self.teams_url)
# Create some teams # Create some teams
...@@ -131,7 +131,7 @@ class TestDashboard(SharedModuleStoreTestCase): ...@@ -131,7 +131,7 @@ class TestDashboard(SharedModuleStoreTestCase):
team.add_user(self.user) team.add_user(self.user)
# Check the query count on the dashboard again # Check the query count on the dashboard again
with self.assertNumQueries(22): with self.assertNumQueries(24):
self.client.get(self.teams_url) self.client.get(self.teams_url)
def test_bad_course_id(self): def test_bad_course_id(self):
......
;(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);
;(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);
<div class="date-summary-container">
<div class="date-summary date-summary-verified-upgrade-deadline">
<h3 class="heading">Verification Upgrade Deadline</h3>
<h4 class="date">in 3 weeks - Feb 12, 2016</h4>
<p class="description">You are still eligible to upgrade to a Verified Certificate! Pursue it to highlight the knowledge and skills you gain in this course.</p>
<span class="date-summary-link">
<a href="/verify_student/upgrade/course-v1:Test+TestX+2015/">Upgrade to Verified Certificate</a>
</span>
</div>
</div>
<div class="page-header-secondary">
<a href="/courses/course-v1:edX+DemoX+Demo_Course/courseware/19a30717eff543078a5d94ae9d6c18a5/" class="last-accessed-link">Resume Course</a>
</div>
<div class="recent-updates">
<article>
<h2 class="date">December 1, 2015</h2>
<a class="toggle-visibility-button" data-hide="Hide" data-show="Show">Hide</a>
<div class="toggle-visibility-element article-content ">
<h1>Assignment 1</h1>
<p>Please submit your first assignment before due date.</p>
</div>
</article>
<article>
<h2 class="date">December 1, 2015</h2>
<a class="toggle-visibility-button" data-hide="Hide" data-show="Show">Show</a>
<div class="toggle-visibility-element article-content">
<h1>Quiz 1</h1>
<p>You have a quiz due on coming friday.</p>
</div>
</article>
<article>
<h2 class="date">November 26, 2015</h2>
<a class="toggle-visibility-button" data-hide="Hide" data-show="Show">Show</a>
<div class="toggle-visibility-element article-content hidden">
<h1>Assignment update</h1>
<p>Please submit your first assignment before due date.</p>
</div>
</article>
</div>
<div class="old-updates hidden toggle-visibility-element">
<article>
<h2 class="date">November 20, 2015</h2>
<a class="toggle-visibility-button" data-hide="Hide" data-show="Show">Show</a>
<div class="toggle-visibility-element article-content hidden"><h1>Orientation</h1>
<p>Orientation will held on monday</p>
</div>
</article>
<article>
<h2 class="date">November 19, 2015</h2>
<a class="toggle-visibility-button" data-hide="Hide" data-show="Show">Show</a>
<div class="toggle-visibility-element article-content hidden"><h1>Course starting</h1>
<p>Starting info about course.</p>
</div>
</article>
</div>
<a class="toggle-visibility-button show-older-updates" data-hide="" data-show="Show Earlier Course Updates">
Show Earlier Course Updates
</a>
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', {});
});
});
});
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'
});
});
});
});
...@@ -708,6 +708,7 @@ ...@@ -708,6 +708,7 @@
'lms/include/js/spec/edxnotes/collections/notes_spec.js', 'lms/include/js/spec/edxnotes/collections/notes_spec.js',
'lms/include/js/spec/search/search_spec.js', 'lms/include/js/spec/search/search_spec.js',
'lms/include/js/spec/navigation_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/collections/filters_spec.js',
'lms/include/js/spec/discovery/models/course_card_spec.js', 'lms/include/js/spec/discovery/models/course_card_spec.js',
'lms/include/js/spec/discovery/models/course_directory_spec.js', 'lms/include/js/spec/discovery/models/course_directory_spec.js',
......
...@@ -231,40 +231,41 @@ ...@@ -231,40 +231,41 @@
.date-summary-container { .date-summary-container {
.date-summary { .date-summary {
@include clearfix; @include clearfix;
margin-top: $baseline/2;
margin-bottom: $baseline/2;
padding: 10px; padding: 10px;
background-color: $gray-l4;
@include border-left(3px solid $gray-l3); @include border-left(3px solid $gray-l3);
.heading {
@extend %t-title7;
color: $gray-d2;
}
.description { .description {
margin-top: $baseline/2; margin-top: $baseline/2;
margin-bottom: $baseline/2; margin-bottom: $baseline/2;
display: inline-block; display: inline-block;
color: $lighter-base-font-color; color: $gray-d1;
font-size: 80%; @extend %t-title8;
} }
.date-summary-link { .date-summary-link {
@include float(right); @extend %t-title8;
font-size: 80%;
font-weight: $font-semibold; font-weight: $font-semibold;
a { a {
color: $base-font-color; color: $link-color;
font-weight: normal;
} }
} }
.date { .date {
color: $lighter-base-font-color; color: $gray-d1;
font-size: 80%; @extend %t-title9;
} }
&-todays-date { &-todays-date {
@include border-left(3px solid $blue); @include border-left(3px solid $blue);
.heading { .heading {
font-weight: $font-regular; @extend %t-title8;
font-size: 80%;
} }
} }
......
...@@ -32,9 +32,16 @@ body.view-in-course { ...@@ -32,9 +32,16 @@ body.view-in-course {
.wrapper-course-material .course-material, .wrapper-course-material .course-material,
.wrapper-preview-menu .preview-menu { .wrapper-preview-menu .preview-menu {
width: auto; width: auto;
}
.wrapper-preview-menu .preview-menu {
padding: 15px 2%; padding: 15px 2%;
} }
.wrapper-course-material .course-material {
padding: ($baseline/2) 0 0 0;
}
.wrapper-course-material .course-material .course-tabs { .wrapper-course-material .course-material .course-tabs {
padding: 0; padding: 0;
} }
......
...@@ -465,6 +465,7 @@ $courseware-navigation-color: $blue !default; ...@@ -465,6 +465,7 @@ $courseware-navigation-color: $blue !default;
$homepage__header--gradient__color--alpha: lighten($gray, 15%) !default; $homepage__header--gradient__color--alpha: lighten($gray, 15%) !default;
$homepage__header--gradient__color--bravo: saturate($gray, 30%) !default; $homepage__header--gradient__color--bravo: saturate($gray, 30%) !default;
$homepage__header--background: lighten($gray, 15%) !default; $homepage__header--background: lighten($gray, 15%) !default;
$homepage-background: rgb(252, 252, 252);
$course-card-height: ($baseline*18) !default; $course-card-height: ($baseline*18) !default;
$course-image-height: ($baseline*8) !default; $course-image-height: ($baseline*8) !default;
$course-info-height: ($baseline*10) !default; $course-info-height: ($baseline*10) !default;
...@@ -514,7 +515,7 @@ $light-gray: rgb(221, 221, 221) !default; ...@@ -514,7 +515,7 @@ $light-gray: rgb(221, 221, 221) !default;
$dark-gray: rgb(51, 51, 51) !default; $dark-gray: rgb(51, 51, 51) !default;
$border-color: rgb(200, 200, 200) !default; $border-color: rgb(200, 200, 200) !default;
$sidebar-color: rgb(246, 246, 246) !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; $light-gray: rgb(221,221,221) !default;
// used by descriptor css // used by descriptor css
......
.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 { div.info-wrapper {
background-color: $homepage-background;
section.updates { section.updates {
@extend .content; @extend .content;
@include padding-left($baseline);
line-height: lh(); line-height: lh();
width: 100%;
display: block;
> h1 { h1 {
@extend .top-header; @include text-align(left);
@extend %t-strong;
@extend %t-title6;
margin-bottom: $baseline;
font-style: normal;
} }
> p { > p {
...@@ -17,12 +65,31 @@ div.info-wrapper { ...@@ -17,12 +65,31 @@ div.info-wrapper {
margin-bottom: lh(); margin-bottom: lh();
padding-left: 0; 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 { > li,article {
@extend .clearfix; @extend .clearfix;
border-bottom: 1px solid lighten($border-color, 10%); padding: $baseline;
list-style-type: none; list-style-type: none;
margin-bottom: lh(1.5); margin-bottom: lh(1.5);
padding-bottom: lh(.75); background-color: $white;
ol, ul { ol, ul {
ol,ul { ol,ul {
...@@ -30,11 +97,25 @@ div.info-wrapper { ...@@ -30,11 +97,25 @@ div.info-wrapper {
} }
} }
h2 { h2.date {
font-size: $body-font-size; @extend %t-title9;
font-weight: bold; margin-bottom: ($baseline/4);
text-transform: none;
background: url('#{$static-path}/images/calendar-icon.png') 0 center no-repeat; 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 { section.update-description {
...@@ -81,8 +162,7 @@ div.info-wrapper { ...@@ -81,8 +162,7 @@ div.info-wrapper {
padding: 20px 30px; padding: 20px 30px;
margin: 0; margin: 0;
@extend .sidebar; @extend .sidebar;
border-radius: 0 4px 4px 0; background: rgba(0, 0, 0, 0);
@include border-left(1px solid #ddd);
box-shadow: none; box-shadow: none;
font-size: 14px; font-size: 14px;
...@@ -97,15 +177,14 @@ div.info-wrapper { ...@@ -97,15 +177,14 @@ div.info-wrapper {
h1 { h1 {
@include text-align(left); @include text-align(left);
@extend %t-strong;
@extend %t-title6;
margin-bottom: 0; margin-bottom: 0;
padding: 12px 26px 20px 0; padding: 12px 26px 20px 0;
font-size: 18px;
font-style: normal; font-style: normal;
font-weight: bold;
} }
ul { ul {
background-color: #f6f6f6;
margin-bottom: 14px; margin-bottom: 14px;
} }
...@@ -119,7 +198,8 @@ div.info-wrapper { ...@@ -119,7 +198,8 @@ div.info-wrapper {
padding: 0; padding: 0;
color: $link-color; color: $link-color;
&:hover, &:focus { &:hover,
&:focus {
background: transparent; background: transparent;
} }
} }
...@@ -154,7 +234,8 @@ div.info-wrapper { ...@@ -154,7 +234,8 @@ div.info-wrapper {
display: inline-block; display: inline-block;
padding: 0; padding: 0;
&:hover, &:focus { &:hover,
&:focus {
background: transparent; background: transparent;
} }
} }
...@@ -175,7 +256,8 @@ div.info-wrapper { ...@@ -175,7 +256,8 @@ div.info-wrapper {
display: inline-block; display: inline-block;
padding: 0; padding: 0;
&:hover, &:focus { &:hover,
&:focus {
background: transparent; background: transparent;
} }
} }
...@@ -191,7 +273,8 @@ div.info-wrapper { ...@@ -191,7 +273,8 @@ div.info-wrapper {
position: absolute; position: absolute;
width: 100%; width: 100%;
&:hover, &:focus { &:hover,
&:focus {
opacity: 0.6; opacity: 0.6;
filter: alpha(opacity=60); filter: alpha(opacity=60);
......
...@@ -24,8 +24,7 @@ ...@@ -24,8 +24,7 @@
display: table; display: table;
table-layout: fixed; table-layout: fixed;
width: 100%; width: 100%;
border-radius: 3px; border: 1px solid $border-color-2;
border: 1px solid $outer-border-color;
background: $container-bg; background: $container-bg;
box-shadow: 0 1px 2px $shadow-l2; box-shadow: 0 1px 2px $shadow-l2;
} }
......
...@@ -15,48 +15,49 @@ ...@@ -15,48 +15,49 @@
ol.course-tabs { ol.course-tabs {
@include border-top-radius(4px); @include border-top-radius(4px);
@include clearfix(); @include clearfix();
@include margin-left(10px);
padding: ($baseline*0.75) 0 ($baseline*0.75) 0; padding: ($baseline*0.75) 0 ($baseline*0.75) 0;
li { li {
@include float(left); @include float(left);
list-style: none; list-style: none;
margin-right: 6px;
&.prominent { &.prominent {
margin-right: 16px; @include margin-right(16px);
background: rgba(255, 255, 255, .5); background: rgba(255, 255, 255, 0.5);
border-radius: 3px; border-radius: 3px;
} }
&.prominent + li { &.prominent + li {
padding-left: ($baseline*0.75); @include padding-left($baseline*0.75);
border-left: 1px solid #333; @include border-left(1px solid $gray-d3);
} }
a { a {
border-radius: 3px; @include padding(($baseline/2), ($baseline*0.75), 13px, ($baseline*0.75));
color: #555; @extend %t-title7;
@extend %t-regular;
border-bottom: 3px solid transparent;
color: $gray-d1;
display: block; display: block;
text-align: center; text-align: center;
padding: ($baseline/2) 13px 12px;
font-size: 14px;
font-weight: bold;
text-decoration: none; text-decoration: none;
// text-shadow: 0 1px 0 rgba(0, 0, 0, .4); // text-shadow: 0 1px 0 rgba(0, 0, 0, .4);
&:hover, &:focus { &:hover,
color: #333; &:focus {
background: rgba(255, 255, 255, .6); color: $blue;
border-bottom: 3px solid $blue;
} }
&.active { &.active {
// background: $shadow; border-bottom: 3px solid $gray-d4;
@include linear-gradient(top, rgba(0, 0, 0, .4), rgba(0, 0, 0, .25));
background-color: transparent; background-color: transparent;
box-shadow: 0 1px 0 rgba(255, 255, 255, .5), 0 1px 1px rgba(0, 0, 0, .3) inset; color: $gray-d4;
color: $white;
text-shadow: 0 1px 0 rgba(0, 0, 0, .4); &:hover,
&:focus {
color: $gray-d4;
}
} }
} }
} }
...@@ -87,7 +88,7 @@ header.global.slim { ...@@ -87,7 +88,7 @@ header.global.slim {
} }
.guest .secondary { .guest .secondary {
margin-right: 0; @include margin-right(0);
} }
.guest .secondary a { .guest .secondary a {
......
...@@ -115,8 +115,8 @@ header.global { ...@@ -115,8 +115,8 @@ header.global {
.user { .user {
@include float(right); @include float(right);
@extend %ui-print-excluded; @extend %ui-print-excluded;
margin-top: ($baseline/4);
padding-left: 0; padding-left: 0;
margin: 0;
.settings-language-form { .settings-language-form {
margin-top: 4px; margin-top: 4px;
...@@ -135,7 +135,7 @@ header.global { ...@@ -135,7 +135,7 @@ header.global {
&:last-child { &:last-child {
> a { > a {
padding: ($baseline/5) ($baseline/2); padding: ($baseline/2);
&.shopping-cart { &.shopping-cart {
border-radius: 4px; border-radius: 4px;
...@@ -147,14 +147,19 @@ header.global { ...@@ -147,14 +147,19 @@ header.global {
} }
} }
a.user-link { .user-link {
@include padding(5px, 2px, 10px, 10px); padding: 0;
position: relative; position: relative;
text-transform: none; text-transform: none;
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
letter-spacing: 0; letter-spacing: 0;
.user-image-frame {
max-width: ($baseline*2);
border-radius: 10%;
}
.icon { .icon {
display: inline-block; display: inline-block;
@include float(left); @include float(left);
...@@ -163,35 +168,9 @@ header.global { ...@@ -163,35 +168,9 @@ header.global {
color: $m-gray; color: $m-gray;
} }
.avatar { .label-username {
@include float(right);
// CASE: right to left layout @include margin(($baseline*0.75), ($baseline/4), ($baseline*0.75), ($baseline*0.75));
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;
}
} }
} }
...@@ -471,7 +450,6 @@ header.global-new { ...@@ -471,7 +450,6 @@ header.global-new {
.user { .user {
@include float(right); @include float(right);
margin-top: 4px;
> .primary { > .primary {
display: block; display: block;
...@@ -489,7 +467,7 @@ header.global-new { ...@@ -489,7 +467,7 @@ header.global-new {
> a { > a {
@include border-radius(0, 4px, 4px, 0); @include border-radius(0, 4px, 4px, 0);
@include border-left(none); @include border-left(none);
padding: ($baseline/5) ($baseline/2); padding: ($baseline/2) ($baseline/2);
&.shopping-cart { &.shopping-cart {
border-radius: 4px; border-radius: 4px;
...@@ -500,14 +478,19 @@ header.global-new { ...@@ -500,14 +478,19 @@ header.global-new {
} }
} }
} }
a.user-link { .user-link {
@include padding(5px, 2px, 10px, 10px); padding: 0;
position: relative; position: relative;
text-transform: none; text-transform: none;
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
letter-spacing: 0; letter-spacing: 0;
.user-image-frame {
max-width: ($baseline/2);
border-radius: 10%;
}
.icon { .icon {
display: inline-block; display: inline-block;
@include float(left); @include float(left);
...@@ -516,35 +499,9 @@ header.global-new { ...@@ -516,35 +499,9 @@ header.global-new {
color: $m-gray; color: $m-gray;
} }
.avatar { .label-username {
@include float(right);
// CASE: right to left layout @include margin(($baseline*0.75), ($baseline/4), ($baseline*0.75), ($baseline*0.75));
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;
}
} }
} }
......
...@@ -126,7 +126,7 @@ from openedx.core.lib.courses import course_image_url ...@@ -126,7 +126,7 @@ from openedx.core.lib.courses import course_image_url
<span class="register disabled">${_("You are enrolled in this course")}</span> <span class="register disabled">${_("You are enrolled in this course")}</span>
%if show_courseware_link: %if show_courseware_link:
<strong>${_("View Courseware")}</strong> <strong>${_("View Course")}</strong>
</a> </a>
%endif %endif
......
<%! from django.utils.translation import ugettext as _ %>
<section>
<div class="recent-updates">
% for index, update in enumerate(visible_updates):
<article class="updates-article">
% if not update.get("is_error"):
<h2 class="date">${update.get("date")}</h2>
<a class="toggle-visibility-button" data-hide="${_('Hide')}" data-show="${_('Show')}"></a>
% endif
<div class="toggle-visibility-element article-content ${'hidden' if index >= 1 else ''}">
${update.get("content")}
</div>
</article>
% endfor
</div>
<div class="old-updates hidden toggle-visibility-element">
% for update in hidden_updates:
<article class="updates-article">
<h2 class="date">${update.get("date")}</h2>
<a class="toggle-visibility-button" data-hide="${_('Hide')}" data-show="${_('Show')}"></a>
<div class="toggle-visibility-element article-content hidden">${update.get("content")}</div>
</article>
% endfor
</div>
% if len(hidden_updates) > 0:
<a class="toggle-visibility-button show-older-updates" data-hide="" data-show="${_('Show Earlier Course Updates')}"></a>
% endif
</section>
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
% endif % endif
% if link and link_text: % if link and link_text:
<span class="date-summary-link"> <span class="date-summary-link">
<a href="${link}">${link_text} <i class="fa fa-arrow-right" aria-hidden="true"></i></a> <a href="${link}">${link_text}</a>
</span> </span>
% endif % endif
</div> </div>
......
...@@ -14,50 +14,67 @@ from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration ...@@ -14,50 +14,67 @@ from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
<%static:css group='style-course'/> <%static:css group='style-course'/>
</%block> </%block>
% if show_enroll_banner: % if show_enroll_banner:
<div class="wrapper-msg urgency-low" id="failed-verification-banner"> <div class="wrapper-msg urgency-low" id="failed-verification-banner">
<div class="msg msg-reverify is-dismissable"> <div class="msg msg-reverify is-dismissable">
<div class="msg-content"> <div class="msg-content">
<h2 class="title">${_("You are not enrolled yet")}</h2> <h2 class="title">${_("You are not enrolled yet")}</h2>
<div class="copy"> <div class="copy">
<p class='enroll-message'> <p class='enroll-message'>
${_(u"You are not currently enrolled in this course. {link_start}Sign up now!{link_end}").format( ${_(u"You are not currently enrolled in this course. {link_start}Sign up now!{link_end}").format(
link_start=u"<a href={}>".format(url_to_enroll), link_start=u"<a href={}>".format(url_to_enroll),
link_end=u"</a>" link_end=u"</a>"
)} )}
</p> </p>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
% endif % endif
<%include file="/courseware/course_navigation.html" args="active_page='info'" /> <%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 name="js_extra">
<script type="text/javascript" src="${static.url('js/jquery.treeview.js')}"></script> <script type="text/javascript" src="${static.url('js/jquery.treeview.js')}"></script>
<script type="text/javascript" charset="utf-8"> <script type="text/javascript" charset="utf-8">
$(document).ready(function(){ $(document).ready(function(){
$(".handouts").treeview({collapsed:true, unique:true/*, cookieId: "treeview-book-nav", persist: "cookie"*/}); $(".handouts").treeview({collapsed:true, unique:true/*, cookieId: "treeview-book-nav", persist: "cookie"*/});
}); });
</script> </script>
</%block> </%block>
<%block name="bodyclass">view-in-course view-course-info ${course.css_class or ''}</%block> <%block name="bodyclass">view-in-course view-course-info ${course.css_class or ''}</%block>
<section class="container"> <section class="container">
<div class="home">
<div class="page-header-main">
<h1 class="page-title">${_("Welcome to {org}'s {course_name}!").format(org=course.id.org, course_name=course.id.course) | h}</h1>
<h2 class="page-subtitle">${course.display_name | h}</h2>
</div>
% if last_accessed_courseware_url:
<div class="page-header-secondary">
<a href="${last_accessed_courseware_url}" class="last-accessed-link">${_("Resume Course")}</a>
</div>
% endif
</div>
<div class="info-wrapper"> <div class="info-wrapper">
% if user.is_authenticated(): % if user.is_authenticated():
<section class="updates"> <section class="updates">
% if studio_url is not None and masquerade and masquerade.role == 'staff': % if studio_url is not None and masquerade and masquerade.role == 'staff':
<div class="wrap-instructor-info studio-view"> <div class="wrap-instructor-info studio-view">
<a class="instructor-info-action" href="${studio_url}"> <a class="instructor-info-action" href="${studio_url}">
${_("View Updates in Studio")} ${_("View Updates in Studio")}
</a> </a>
</div> </div>
% endif % endif
<h1>${_("Course Updates &amp; News")}</h1> <h1>${_("Course Updates and News")}</h1>
${get_course_info_section(request, masquerade_user, course, 'updates')} ${get_course_info_section(request, masquerade_user, course, 'updates')}
</section> </section>
<section aria-label="${_('Handout Navigation')}" class="handouts"> <section aria-label="${_('Handout Navigation')}" class="handouts">
......
...@@ -5,6 +5,9 @@ ...@@ -5,6 +5,9 @@
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _ 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 # App that handles subdomain specific branding
from branding import api as branding_api from branding import api as branding_api
# app that handles site status messages # app that handles site status messages
...@@ -76,7 +79,12 @@ site_status_msg = get_site_status_msg(course_id) ...@@ -76,7 +79,12 @@ site_status_msg = get_site_status_msg(course_id)
<li class="primary"> <li class="primary">
<a href="${reverse('dashboard')}" class="user-link"> <a href="${reverse('dashboard')}" class="user-link">
<span class="sr">${_("Dashboard for:")}</span> <span class="sr">${_("Dashboard for:")}</span>
<div>${user.username}</div> <%
username = user.username
profile_image_url = get_profile_image_urls_for_user(user)['medium']
%>
<img class="user-image-frame" src="${profile_image_url}" alt="${_('Profile image for {username}').format(username=username)}">
<div class="label-username">${username}</div>
</a> </a>
</li> </li>
<li class="primary"> <li class="primary">
......
...@@ -6,6 +6,9 @@ from django.core.urlresolvers import reverse ...@@ -6,6 +6,9 @@ from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from lms.djangoapps.ccx.overrides import get_current_ccx 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 # App that handles subdomain specific branding
from branding import api as branding_api from branding import api as branding_api
...@@ -101,9 +104,12 @@ site_status_msg = get_site_status_msg(course_id) ...@@ -101,9 +104,12 @@ site_status_msg = get_site_status_msg(course_id)
<li class="primary"> <li class="primary">
<a href="${reverse('dashboard')}" class="user-link"> <a href="${reverse('dashboard')}" class="user-link">
<span class="sr">${_("Dashboard for:")}</span> <span class="sr">${_("Dashboard for:")}</span>
<div> <%
${user.username} username = user.username
</div> profile_image_url = get_profile_image_urls_for_user(user)['medium']
%>
<img class="user-image-frame" src="${profile_image_url}" alt="${_('Profile image for {username}').format(username=username)}">
<div class="label-username">${username}</div>
</a> </a>
</li> </li>
<li class="primary"> <li class="primary">
......
...@@ -92,13 +92,18 @@ def get_profile_image_urls_for_user(user, request=None): ...@@ -92,13 +92,18 @@ def get_profile_image_urls_for_user(user, request=None):
dictionary of {size_display_name: url} for each image. dictionary of {size_display_name: url} for each image.
""" """
if user.profile.has_profile_image: try:
urls = _get_profile_image_urls( if user.profile.has_profile_image:
_make_profile_image_name(user.username), urls = _get_profile_image_urls(
get_profile_image_storage(), _make_profile_image_name(user.username),
version=user.profile.profile_image_uploaded_at.strftime("%s"), get_profile_image_storage(),
) version=user.profile.profile_image_uploaded_at.strftime("%s"),
else: )
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() urls = _get_default_profile_image_urls()
if request: if request:
......
...@@ -538,6 +538,11 @@ class TestAccountAPI(UserAPITestCase): ...@@ -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[0], old_name, self.user.username, "Donald Duck",)
verify_change_info(name_change_info[1], "Mickey Mouse", 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): def test_patch_email(self):
""" """
Test that the user can request an email change through the accounts API. Test that the user can request an email change through the accounts API.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment