Updates XBlock renders its own template. Adds expand and collapse JS + jasmine tests.
ECOM-2809
This commit is contained in:
@@ -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"<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):
|
||||
"""
|
||||
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)
|
||||
|
||||
@@ -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"<section><article><h2>{date}</h2>{content}</article></section>".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)
|
||||
|
||||
@@ -27,7 +27,7 @@ from openedx.core.lib.js_utils import escape_json_dumps
|
||||
"${handouts_locator | escapejs}",
|
||||
"${base_asset_url}",
|
||||
${escape_json_dumps(push_notification_enabled) | n}
|
||||
);
|
||||
);
|
||||
});
|
||||
</%block>
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
42
lms/static/js/courseware/toggle_element_visibility.js
Normal file
42
lms/static/js/courseware/toggle_element_visibility.js
Normal file
@@ -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);
|
||||
45
lms/static/js/fixtures/courseware/course_updates.html
Normal file
45
lms/static/js/fixtures/courseware/course_updates.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<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>
|
||||
36
lms/static/js/spec/courseware/updates_visibility.js
Normal file
36
lms/static/js/spec/courseware/updates_visibility.js
Normal file
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
|
||||
@@ -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 {
|
||||
|
||||
29
lms/templates/courseware/course_updates.html
Normal file
29
lms/templates/courseware/course_updates.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<%! 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>
|
||||
@@ -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();
|
||||
</%static:require_module>
|
||||
|
||||
<%block name="bodyclass">view-in-course view-course-info ${course.css_class or ''}</%block>
|
||||
<section class="container">
|
||||
<div class="home">
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user