diff --git a/cms/djangoapps/contentstore/course_info_model.py b/cms/djangoapps/contentstore/course_info_model.py index 570763bd28..c5632b3373 100644 --- a/cms/djangoapps/contentstore/course_info_model.py +++ b/cms/djangoapps/contentstore/course_info_model.py @@ -160,25 +160,13 @@ def _get_index(passed_id=None): return 0 -def _get_html(course_updates_items): - """ - Method to create course_updates_html from course_updates items - """ - list_items = [] - for update in reversed(course_updates_items): - # filter course update items which have status "deleted". - if update.get("status") != CourseInfoModule.STATUS_DELETED: - list_items.append(u"

{date}

{content}
".format(**update)) - return u"
{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/views/tests/test_course_updates.py b/cms/djangoapps/contentstore/views/tests/test_course_updates.py index 94f92fe637..f9199c005a 100644 --- a/cms/djangoapps/contentstore/views/tests/test_course_updates.py +++ b/cms/djangoapps/contentstore/views/tests/test_course_updates.py @@ -173,9 +173,8 @@ class CourseUpdateTest(CourseTestCase): self.assertHTMLEqual(update_content, json.loads(resp.content)['content']) course_updates = modulestore().get_item(location) self.assertEqual(course_updates.items, [{u'date': update_date, u'content': update_content, u'id': 1}]) - # course_updates 'data' field should update accordingly - update_data = u"

{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 7fa84c264e..dc2d4640ce 100644 --- a/cms/templates/course_info.html +++ b/cms/templates/course_info.html @@ -27,7 +27,7 @@ from openedx.core.lib.js_utils import escape_json_dumps "${handouts_locator | escapejs}", "${base_asset_url}", ${escape_json_dumps(push_notification_enabled) | n} - ); + ); }); diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index 14b6b19280..62fce42e3f 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -1,13 +1,14 @@ -import os -import sys -import re import copy -import logging -import textwrap -from lxml import etree -from path import Path as path +from datetime import datetime from fs.errors import ResourceNotFoundError +import logging +from lxml import etree +import os +from path import Path as path from pkg_resources import resource_string +import re +import sys +import textwrap import dogstats_wrapper as dog_stats_api from xmodule.util.misc import escape_html_characters @@ -75,10 +76,10 @@ class HtmlBlock(object): return Fragment(self.get_html()) def get_html(self): - """ - When we switch this to an XBlock, we can merge this with student_view, - but for now the XModule mixin requires that this method be defined. - """ + """ Returns html required for rendering XModule. """ + + # When we switch this to an XBlock, we can merge this with student_view, + # but for now the XModule mixin requires that this method be defined. # pylint: disable=no-member if self.system.anonymous_student_id: return self.data.replace("%%USER_ID%%", self.system.anonymous_student_id) @@ -417,6 +418,35 @@ class CourseInfoModule(CourseInfoFields, HtmlModuleMixin): # statuses STATUS_VISIBLE = 'visible' STATUS_DELETED = 'deleted' + TEMPLATE_DIR = 'courseware' + + @XBlock.supports("multi_device") + def student_view(self, _context): + """ + Return a fragment that contains the html for the student view + """ + return Fragment(self.get_html()) + + def get_html(self): + """ Returns html required for rendering XModule. """ + + # When we switch this to an XBlock, we can merge this with student_view, + # but for now the XModule mixin requires that this method be defined. + # pylint: disable=no-member + if self.data != "": + if self.system.anonymous_student_id: + return self.data.replace("%%USER_ID%%", self.system.anonymous_student_id) + return self.data + else: + course_updates = [item for item in self.items if item.get('status') == self.STATUS_VISIBLE] + course_updates.sort(key=lambda item: datetime.strptime(item['date'], '%B %d, %Y'), reverse=True) + + context = { + 'visible_updates': course_updates[:3], + 'hidden_updates': course_updates[3:], + } + + return self.system.render_template("{0}/course_updates.html".format(self.TEMPLATE_DIR), context) @XBlock.tag("detached") diff --git a/lms/static/js/courseware/toggle_element_visibility.js b/lms/static/js/courseware/toggle_element_visibility.js new file mode 100644 index 0000000000..b5dc438b0a --- /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 0000000000..51935ba02f --- /dev/null +++ b/lms/static/js/fixtures/courseware/course_updates.html @@ -0,0 +1,45 @@ +
+
+

December 1, 2015

+ Hide +
+

Assignment 1

+

Please submit your first assignment before due date.

+
+
+
+

December 1, 2015

+ Show +
+

Quiz 1

+

You have a quiz due on coming friday.

+
+
+
+

November 26, 2015

+ Show + +
+
+ + + 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 0000000000..dc03954502 --- /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 427b066f55..3d0ae676d1 100644 --- a/lms/static/js/spec/main.js +++ b/lms/static/js/spec/main.js @@ -708,6 +708,7 @@ 'lms/include/js/spec/edxnotes/collections/notes_spec.js', 'lms/include/js/spec/search/search_spec.js', 'lms/include/js/spec/navigation_spec.js', + 'lms/include/js/spec/courseware/updates_visibility.js', 'lms/include/js/spec/discovery/collections/filters_spec.js', 'lms/include/js/spec/discovery/models/course_card_spec.js', 'lms/include/js/spec/discovery/models/course_directory_spec.js', diff --git a/lms/static/sass/course/_info.scss b/lms/static/sass/course/_info.scss index afa626fa34..59f0fdfbfe 100644 --- a/lms/static/sass/course/_info.scss +++ b/lms/static/sass/course/_info.scss @@ -50,6 +50,8 @@ div.info-wrapper { @extend .content; @include padding-left($baseline); line-height: lh(); + width: 100%; + display: block; h1 { @include text-align(left); @@ -69,6 +71,25 @@ 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; padding: $baseline; @@ -81,12 +102,25 @@ div.info-wrapper { } } - h2 { + h2.date { @extend %t-title9; margin-bottom: ($baseline/4); text-transform: none; background: url('#{$static-path}/images/calendar-icon.png') 0 center no-repeat; @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 { diff --git a/lms/templates/courseware/course_updates.html b/lms/templates/courseware/course_updates.html new file mode 100644 index 0000000000..6fc936b753 --- /dev/null +++ b/lms/templates/courseware/course_updates.html @@ -0,0 +1,29 @@ +<%! from django.utils.translation import ugettext as _ %> +
+
+ % for index, update in enumerate(visible_updates): +
+ % if not update.get("is_error"): +

${update.get("date")}

+ + % endif +
+ ${update.get("content")} +
+
+ % endfor +
+ + +% if len(hidden_updates) > 0: + +% endif +
diff --git a/lms/templates/courseware/info.html b/lms/templates/courseware/info.html index e51d1b7c0f..dbc4b8d41b 100644 --- a/lms/templates/courseware/info.html +++ b/lms/templates/courseware/info.html @@ -34,6 +34,10 @@ from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration <%include file="/courseware/course_navigation.html" args="active_page='info'" /> +<%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 ''}
diff --git a/openedx/core/djangoapps/user_api/accounts/image_helpers.py b/openedx/core/djangoapps/user_api/accounts/image_helpers.py index 00de36609c..b204d6daf6 100644 --- a/openedx/core/djangoapps/user_api/accounts/image_helpers.py +++ b/openedx/core/djangoapps/user_api/accounts/image_helpers.py @@ -92,13 +92,18 @@ def get_profile_image_urls_for_user(user, request=None): dictionary of {size_display_name: url} for each image. """ - if user.profile.has_profile_image: - urls = _get_profile_image_urls( - _make_profile_image_name(user.username), - get_profile_image_storage(), - version=user.profile.profile_image_uploaded_at.strftime("%s"), - ) - else: + try: + if user.profile.has_profile_image: + urls = _get_profile_image_urls( + _make_profile_image_name(user.username), + get_profile_image_storage(), + version=user.profile.profile_image_uploaded_at.strftime("%s"), + ) + else: + urls = _get_default_profile_image_urls() + except UserProfile.DoesNotExist: + # when user does not have profile it raises exception, when exception + # occur we can simply get default image. urls = _get_default_profile_image_urls() if request: