Commit c9b87aa0 by muhammad-ammar Committed by muzaffaryousaf

Unit Bookmarks List View

TNL-1958
parent cfd1fabb
This source diff could not be displayed because it is too large. You can view the blob instead.
"""
Courseware Boomarks
"""
from bok_choy.promise import EmptyPromise
from .course_page import CoursePage
class BookmarksPage(CoursePage):
"""
Coursware Bookmarks Page.
"""
url = None
url_path = "courseware/"
BOOKMARKS_BUTTON_SELECTOR = '.bookmarks-list-button'
BOOKMARKED_ITEMS_SELECTOR = '.bookmarks-results-list .bookmarks-results-list-item'
BOOKMARKED_BREADCRUMBS = BOOKMARKED_ITEMS_SELECTOR + ' .list-item-breadcrumbtrail'
def is_browser_on_page(self):
""" Verify if we are on correct page """
return self.q(css=self.BOOKMARKS_BUTTON_SELECTOR).visible
def bookmarks_button_visible(self):
""" Check if bookmarks button is visible """
return self.q(css=self.BOOKMARKS_BUTTON_SELECTOR).visible
def click_bookmarks_button(self):
""" Click on Bookmarks button """
self.q(css=self.BOOKMARKS_BUTTON_SELECTOR).first.click()
EmptyPromise(self.results_present, "Bookmarks results present").fulfill()
def results_present(self):
""" Check if bookmarks results are present """
return self.q(css='#my-bookmarks').present
def results_header_text(self):
""" Returns the bookmarks results header text """
return self.q(css='.bookmarks-results-header').text[0]
def empty_header_text(self):
""" Returns the bookmarks empty header text """
return self.q(css='.bookmarks-empty-header').text[0]
def empty_list_text(self):
""" Returns the bookmarks empty list text """
return self.q(css='.bookmarks-empty-detail-title').text[0]
def count(self):
""" Returns the total number of bookmarks in the list """
return len(self.q(css=self.BOOKMARKED_ITEMS_SELECTOR).results)
def breadcrumbs(self):
""" Return list of breadcrumbs for all bookmarks """
breadcrumbs = self.q(css=self.BOOKMARKED_BREADCRUMBS).text
return [breadcrumb.replace('\n', '').split('-') for breadcrumb in breadcrumbs]
def click_bookmark(self, index):
"""
Click on bookmark at index `index`
Arguments:
index (int): bookmark index in the list
"""
self.q(css=self.BOOKMARKED_ITEMS_SELECTOR).nth(index).click()
...@@ -171,6 +171,12 @@ class CoursewarePage(CoursePage): ...@@ -171,6 +171,12 @@ class CoursewarePage(CoursePage):
""" """
return self.q(css=".proctored_exam_status .exam-timer").is_present() return self.q(css=".proctored_exam_status .exam-timer").is_present()
def active_usage_id(self):
""" Returns the usage id of active sequence item """
get_active = lambda el: 'active' in el.get_attribute('class')
attribute_value = lambda el: el.get_attribute('data-id')
return self.q(css='#sequence-list a').filter(get_active).map(attribute_value).results[0]
class CoursewareSequentialTabPage(CoursePage): class CoursewareSequentialTabPage(CoursePage):
""" """
......
...@@ -16,7 +16,7 @@ class CoursewareSearchPage(CoursePage): ...@@ -16,7 +16,7 @@ class CoursewareSearchPage(CoursePage):
@property @property
def search_results(self): def search_results(self):
""" search results list showing """ """ search results list showing """
return self.q(css='#courseware-search-results') return self.q(css='.courseware-results')
def is_browser_on_page(self): def is_browser_on_page(self):
""" did we find the search bar in the UI """ """ did we find the search bar in the UI """
......
...@@ -344,6 +344,11 @@ def get_element_padding(page, selector): ...@@ -344,6 +344,11 @@ def get_element_padding(page, selector):
return page.browser.execute_script(js_script) return page.browser.execute_script(js_script)
def is_404_page(browser):
""" Check if page is 404 """
return 'Page not found (404)' in browser.find_element_by_tag_name('h1').text
class EventsTestMixin(TestCase): class EventsTestMixin(TestCase):
""" """
Helpers and setup for running tests that evaluate events emitted Helpers and setup for running tests that evaluate events emitted
......
# -*- coding: utf-8 -*-
"""
End-to-end tests for the courseware unit bookmarks.
"""
import json
import requests
from ...pages.studio.auto_auth import AutoAuthPage
from ...pages.lms.bookmarks import BookmarksPage
from ...pages.lms.courseware import CoursewarePage
from ...pages.studio.overview import CourseOutlinePage
from ...pages.common.logout import LogoutPage
from ...fixtures.course import CourseFixture, XBlockFixtureDesc
from ...fixtures import LMS_BASE_URL
from ..helpers import EventsTestMixin, UniqueCourseTest, is_404_page
class BookmarksTestMixin(EventsTestMixin, UniqueCourseTest):
"""
Mixin with helper methods for testing Bookmarks.
"""
USERNAME = "STUDENT"
EMAIL = "student@example.com"
COURSE_TREE_INFO = [
['TestSection1', 'TestSubsection1', 'TestProblem1'],
['TestSection2', 'TestSubsection2', 'TestProblem2']
]
def create_course_fixture(self):
""" Create course fixture """
self.course_fixture = CourseFixture( # pylint: disable=attribute-defined-outside-init
self.course_info['org'], self.course_info['number'],
self.course_info['run'], self.course_info['display_name']
)
self.course_fixture.add_children(
XBlockFixtureDesc('chapter', self.COURSE_TREE_INFO[0][0]).add_children(
XBlockFixtureDesc('sequential', self.COURSE_TREE_INFO[0][1]).add_children(
XBlockFixtureDesc('problem', self.COURSE_TREE_INFO[0][2])
)
),
XBlockFixtureDesc('chapter', self.COURSE_TREE_INFO[1][0]).add_children(
XBlockFixtureDesc('sequential', self.COURSE_TREE_INFO[1][1]).add_children(
XBlockFixtureDesc('problem', self.COURSE_TREE_INFO[1][2])
)
)
).install()
class BookmarksTest(BookmarksTestMixin):
"""
Tests to verify bookmarks functionality.
"""
def setUp(self):
"""
Initialize test setup.
"""
super(BookmarksTest, self).setUp()
self.course_outline_page = CourseOutlinePage(
self.browser,
self.course_info['org'],
self.course_info['number'],
self.course_info['run']
)
self.create_course_fixture()
# Auto-auth register for the course.
AutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL, course_id=self.course_id).visit()
self.courseware_page = CoursewarePage(self.browser, self.course_id)
self.courseware_page.visit()
self.bookmarks = BookmarksPage(self.browser, self.course_id)
# Use auto-auth to retrieve the session for a logged in user
self.session = requests.Session()
response = self.session.get(LMS_BASE_URL + "/auto_auth?username=STUDENT&email=student@example.com")
self.assertTrue(response.ok, "Failed to get session info")
def _bookmark_unit(self, course_id, usage_id):
""" Bookmark a single unit """
csrftoken = self.session.cookies['csrftoken']
headers = {'Content-type': 'application/json', "X-CSRFToken": csrftoken}
url = LMS_BASE_URL + "/api/bookmarks/v0/bookmarks/?course_id=" + course_id + '&fields=path'
data = json.dumps({'usage_id': usage_id})
response = self.session.post(url, data=data, headers=headers, cookies=self.session.cookies)
response = json.loads(response.text)
self.assertTrue(response['usage_id'] == usage_id, "Failed to bookmark unit")
def _bookmarks_blocks(self, xblocks):
""" Bookmark all units in a course """
for xblock in xblocks:
self._bookmark_unit(self.course_id, usage_id=xblock.locator)
def _delete_section(self, index):
""" Delete a section at index `index` """
# Logout and login as staff
LogoutPage(self.browser).visit()
AutoAuthPage(
self.browser, username=self.USERNAME, email=self.EMAIL, course_id=self.course_id, staff=True
).visit()
# Visit course outline page in studio.
self.course_outline_page.visit()
self.course_outline_page.wait_for_page()
self.course_outline_page.section_at(index).delete()
# Logout and login as a student.
LogoutPage(self.browser).visit()
AutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL, course_id=self.course_id).visit()
# Visit courseware as a student.
self.courseware_page.visit()
self.courseware_page.wait_for_page()
def test_empty_bookmarks_list(self):
"""
Scenario: An empty bookmarks list is shown if there are no bookmarked units.
Given that I am a registered user
And I visit my courseware page
And I can see the Bookmarks button
When I click on Bookmarks button
Then I should see an empty bookmarks list
And empty bookmarks list content is correct
"""
self.assertTrue(self.bookmarks.bookmarks_button_visible())
self.bookmarks.click_bookmarks_button()
self.assertEqual(self.bookmarks.results_header_text(), 'MY BOOKMARKS')
self.assertEqual(self.bookmarks.empty_header_text(), 'You have not bookmarked any courseware pages yet.')
empty_list_text = ("Use bookmarks to help you easily return to courseware pages. To bookmark a page, "
"select Bookmark in the upper right corner of that page. To see a list of all your "
"bookmarks, select Bookmarks in the upper left corner of any courseware page.")
self.assertEqual(self.bookmarks.empty_list_text(), empty_list_text)
def test_bookmarks_list(self):
"""
Scenario: A bookmarks list is shown if there are bookmarked units.
Given that I am a registered user
And I visit my courseware page
And I have bookmarked 2 units
When I click on Bookmarks button
Then I should see a bookmarked list with 2 bookmark links
And breadcrumb trail is correct for a bookmark
When I click on bookmarked link
Then I can navigate to correct bookmarked unit
"""
# NOTE: We are checking the order of bookmarked units at API
# We are unable to check the order here because we are bookmarking
# the units by sending POSTs to API, And the time(created) between
# the bookmarked units is in milliseconds. These milliseconds are
# discarded by the current version of MySQL we are using due to the
# lack of support. Due to which order of bookmarked units will be
# incorrect.
xblocks = self.course_fixture.get_nested_xblocks(category="problem")
self._bookmarks_blocks(xblocks)
self.bookmarks.click_bookmarks_button()
self.assertTrue(self.bookmarks.results_present())
self.assertEqual(self.bookmarks.results_header_text(), 'MY BOOKMARKS')
self.assertEqual(self.bookmarks.count(), 2)
bookmarked_breadcrumbs = self.bookmarks.breadcrumbs()
# Verify bookmarked breadcrumbs
self.assertItemsEqual(bookmarked_breadcrumbs, self.COURSE_TREE_INFO)
xblock_usage_ids = [xblock.locator for xblock in xblocks]
# Verify link navigation
for index in range(2):
self.bookmarks.click_bookmark(index)
self.courseware_page.wait_for_page()
self.assertTrue(self.courseware_page.active_usage_id() in xblock_usage_ids)
self.courseware_page.visit().wait_for_page()
self.bookmarks.click_bookmarks_button()
def test_unreachable_bookmark(self):
"""
Scenario: We should get a HTTP 404 for an unreachable bookmark.
Given that I am a registered user
And I visit my courseware page
And I have bookmarked 2 units
Then I delete a bookmarked unit
Then I click on Bookmarks button
And I should see a bookmarked list
When I click on deleted bookmark
Then I should navigated to 404 page
"""
self._bookmarks_blocks(self.course_fixture.get_nested_xblocks(category="problem"))
self._delete_section(0)
self.bookmarks.click_bookmarks_button()
self.assertTrue(self.bookmarks.results_present())
self.assertEqual(self.bookmarks.count(), 2)
self.bookmarks.click_bookmark(0)
self.assertTrue(is_404_page(self.browser))
...@@ -97,6 +97,10 @@ from eventtracking import tracker ...@@ -97,6 +97,10 @@ from eventtracking import tracker
import analytics import analytics
from courseware.url_helpers import get_redirect_url from courseware.url_helpers import get_redirect_url
from lang_pref import LANGUAGE_KEY
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
log = logging.getLogger("edx.courseware") log = logging.getLogger("edx.courseware")
template_imports = {'urllib': urllib} template_imports = {'urllib': urllib}
...@@ -424,6 +428,8 @@ def _index_bulk_op(request, course_key, chapter, section, position): ...@@ -424,6 +428,8 @@ def _index_bulk_op(request, course_key, chapter, section, position):
'studio_url': studio_url, 'studio_url': studio_url,
'masquerade': masquerade, 'masquerade': masquerade,
'xqa_server': settings.FEATURES.get('XQA_SERVER', "http://your_xqa_server.com"), 'xqa_server': settings.FEATURES.get('XQA_SERVER', "http://your_xqa_server.com"),
'reverifications': fetch_reverify_banner_info(request, course_key),
'language_preference': language_preference,
} }
now = datetime.now(UTC()) now = datetime.now(UTC())
......
...@@ -1176,6 +1176,7 @@ courseware_js = ( ...@@ -1176,6 +1176,7 @@ courseware_js = (
for pth in ['courseware', 'histogram', 'navigation'] for pth in ['courseware', 'histogram', 'navigation']
] + ] +
['js/' + pth + '.js' for pth in ['ajax-error']] + ['js/' + pth + '.js' for pth in ['ajax-error']] +
['js/bookmarks/main.js'] +
sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/modules/**/*.js')) sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/modules/**/*.js'))
) )
......
;(function (define) {
define(['backbone', 'js/bookmarks/models/bookmark'],
function (Backbone, BookmarkModel) {
'use strict';
return Backbone.Collection.extend({
model : BookmarkModel,
url: '/api/bookmarks/v0/bookmarks/',
parse: function(response) {
return response.results;
}
});
});
})(define || RequireJS.define);
RequireJS.require([
'js/bookmarks/views/bookmarks_button'
], function (BookmarksButton) {
'use strict';
return new BookmarksButton();
});
;(function (define) {
define(['backbone'], function (Backbone) {
'use strict';
return Backbone.Model.extend({
idAttribute: 'id',
defaults: {
course_id: '',
usage_id: '',
display_name: '',
path: [],
created: ''
},
blockUrl: function () {
return '/courses/' + this.get('course_id') + '/jump_to/' + this.get('usage_id');
}
});
});
})(define || RequireJS.define);
;(function (define, undefined) {
'use strict';
define(['gettext', 'jquery', 'underscore', 'backbone', 'js/bookmarks/views/bookmarks_list',
'js/bookmarks/collections/bookmarks', 'js/views/message'],
function (gettext, $, _, Backbone, BookmarksListView, BookmarksCollection, MessageView) {
return Backbone.View.extend({
el: '.courseware-bookmarks-button',
loadingMessageElement: '#loading-message',
errorMessageElement: '#error-message',
events: {
'click .bookmarks-list-button': 'toggleBookmarksListView'
},
initialize: function () {
this.template = _.template($('#bookmarks_button-tpl').text());
this.bookmarksListView = new BookmarksListView({
collection: new BookmarksCollection(),
loadingMessageView: new MessageView({el: $(this.loadingMessageElement)}),
errorMessageView: new MessageView({el: $(this.errorMessageElement)})
});
},
toggleBookmarksListView: function () {
if (this.bookmarksListView.areBookmarksVisible()) {
this.bookmarksListView.hideBookmarks();
this.$('.bookmarks-list-button').attr('aria-pressed', 'false');
this.$('.bookmarks-list-button').removeClass('is-active').addClass('is-inactive');
} else {
this.bookmarksListView.showBookmarks();
this.$('.bookmarks-list-button').attr('aria-pressed', 'true');
this.$('.bookmarks-list-button').removeClass('is-inactive').addClass('is-active');
}
}
});
});
}).call(this, define || RequireJS.define);
;(function (define, undefined) {
'use strict';
define(['gettext', 'jquery', 'underscore', 'backbone', 'moment'],
function (gettext, $, _, Backbone, _moment) {
var moment = _moment || window.moment;
return Backbone.View.extend({
el: '.courseware-results',
coursewareContentEl: '#course-content',
errorIcon: '<i class="fa fa-fw fa-exclamation-triangle message-error" aria-hidden="true"></i>',
loadingIcon: '<i class="fa fa-fw fa-spinner fa-pulse message-in-progress" aria-hidden="true"></i>',
errorMessage: gettext('An error has occurred. Please try again.'),
loadingMessage: gettext('Loading'),
events : {
'click .bookmarks-results-list-item': 'visitBookmark'
},
initialize: function (options) {
this.template = _.template($('#bookmarks_list-tpl').text());
this.loadingMessageView = options.loadingMessageView;
this.errorMessageView = options.errorMessageView;
this.courseId = $(this.el).data('courseId');
this.langCode = $(this.el).data('langCode');
_.bindAll(this, 'render', 'humanFriendlyDate');
},
render: function () {
var data = {
bookmarks: this.collection.models,
humanFriendlyDate: this.humanFriendlyDate
};
this.$el.html(this.template(data));
this.delegateEvents();
return this;
},
showBookmarks: function () {
var view = this;
this.hideErrorMessage();
this.showBookmarksContainer();
this.showLoadingMessage();
this.collection.fetch({
reset: true,
data: {course_id: this.courseId, fields: 'display_name,path'}
}).done(function () {
view.hideLoadingMessage();
view.render();
view.focusBookmarksElement();
}).fail(function () {
view.hideLoadingMessage();
view.showErrorMessage();
});
},
visitBookmark: function (event) {
window.location = event.target.pathname;
},
/**
* Convert ISO 8601 formatted date into human friendly format. e.g, `2014-05-23T14:00:00Z` to `May 23, 2014`
* @param {String} isoDate - ISO 8601 formatted date string.
*/
humanFriendlyDate: function (isoDate) {
moment.locale(this.langCode);
return moment(isoDate).format('LL');
},
areBookmarksVisible: function () {
return this.$('#my-bookmarks').is(":visible");
},
hideBookmarks: function () {
this.$el.hide();
$(this.coursewareContentEl).show();
},
showBookmarksContainer: function () {
$(this.coursewareContentEl).hide();
// Empty el if it's not empty to get the clean state.
this.$el.html('');
this.$el.show();
},
showLoadingMessage: function () {
this.loadingMessageView.showMessage(this.loadingMessage, this.loadingIcon);
},
hideLoadingMessage: function () {
this.loadingMessageView.hideMessage();
},
showErrorMessage: function () {
this.errorMessageView.showMessage(this.errorMessage, this.errorIcon);
},
hideErrorMessage: function () {
this.errorMessageView.hideMessage();
},
focusBookmarksElement: function () {
this.$('#my-bookmarks').focus();
}
});
});
}).call(this, define || RequireJS.define);
<div class="courseware-bookmarks-button">
<button type="button" class="bookmarks-list-button is-inactive" aria-pressed="false">
Bookmarks
</button>
</div>
<section class="courseware-results-wrapper">
<div id="loading-message" aria-live="assertive" aria-relevant="all"></div>
<div id="error-message" aria-live="polite"></div>
<div class="courseware-results" data-course-id="a/b/c" data-lang-code="en"></div>
</section>
...@@ -72,6 +72,7 @@ define([ ...@@ -72,6 +72,7 @@ define([
}, },
showLoadingMessage: function () { showLoadingMessage: function () {
this.doCleanup();
this.$el.html(this.loadingTemplate()); this.$el.html(this.loadingTemplate());
this.$el.show(); this.$el.show();
this.$contentElement.hide(); this.$contentElement.hide();
...@@ -83,6 +84,15 @@ define([ ...@@ -83,6 +84,15 @@ define([
this.$contentElement.hide(); this.$contentElement.hide();
}, },
doCleanup: function () {
// Empty any loading/error message and empty the el
// Bookmarks share the same container element, So we are doing
// this to ensure that elements are in clean/initial state
$('#loading-message').html('');
$('#error-message').html('');
this.$el.html('');
},
loadNext: function (event) { loadNext: function (event) {
event && event.preventDefault(); event && event.preventDefault();
this.$el.find(this.spinner).show(); this.$el.find(this.spinner).show();
......
RequireJS.require([
'jquery',
'backbone',
'js/search/course/search_app',
'js/search/base/routers/search_router',
'js/search/course/views/search_form',
'js/search/base/collections/search_collection',
'js/search/course/views/search_results_view'
], function ($, Backbone, SearchApp, SearchRouter, CourseSearchForm, SearchCollection, CourseSearchResultsView) {
'use strict';
var courseId = $('.courseware-results').data('courseId');
var app = new SearchApp(
courseId,
SearchRouter,
CourseSearchForm,
SearchCollection,
CourseSearchResultsView
);
Backbone.history.start();
});
...@@ -8,7 +8,7 @@ define([ ...@@ -8,7 +8,7 @@ define([
return SearchResultsView.extend({ return SearchResultsView.extend({
el: '#courseware-search-results', el: '.courseware-results',
contentElement: '#course-content', contentElement: '#course-content',
resultsTemplateId: '#course_search_results-tpl', resultsTemplateId: '#course_search_results-tpl',
loadingTemplateId: '#search_loading-tpl', loadingTemplateId: '#search_loading-tpl',
......
define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers',
'js/bookmarks/views/bookmarks_button'
],
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, BookmarksButtonView) {
'use strict';
describe("lms.courseware.bookmarks", function () {
var bookmarksButtonView;
var BOOKMARKS_API_URL = '/api/bookmarks/v0/bookmarks/';
beforeEach(function () {
loadFixtures('js/fixtures/bookmarks/bookmarks.html');
TemplateHelpers.installTemplates(
[
'templates/message_view',
'templates/bookmarks/bookmarks_list'
]
);
bookmarksButtonView = new BookmarksButtonView();
this.addMatchers({
toHaveBeenCalledWithUrl: function (expectedUrl) {
return expectedUrl === this.actual.argsForCall[0][0].target.pathname;
}
});
});
var createBookmarksData = function (count) {
var data = {
results: []
};
for(var i = 0; i < count; i++) {
var bookmarkInfo = {
id: i,
display_name: 'UNIT_DISPLAY_NAME_' + i,
created: new Date().toISOString(),
course_id: 'COURSE_ID',
usage_id: 'UNIT_USAGE_ID_' + i,
path: [
{display_name: 'SECTION_DISAPLAY_NAME', usage_id: 'SECTION_USAGE_ID'},
{display_name: 'SUBSECTION_DISAPLAY_NAME', usage_id: 'SUBSECTION_USAGE_ID'}
]
};
data.results.push(bookmarkInfo);
}
return data;
};
var createBookmarkUrl = function (courseId, usageId) {
return '/courses/' + courseId + '/jump_to/' + usageId;
};
var breadcrumbTrail = function (path, unitDisplayName) {
return _.pluck(path, 'display_name').
concat([unitDisplayName]).
join(' <i class="icon fa fa-caret-right" aria-hidden="true"></i><span class="sr">-</span> ');
};
var verifyBookmarkedData = function (view, expectedData) {
var courseId, usageId;
var bookmarks = view.$('.bookmarks-results-list-item');
var results = expectedData.results;
expect(bookmarks.length, results.length);
for(var b = 0; b < results.length; b++) {
courseId = results[b].course_id;
usageId = results[b].usage_id;
expect(bookmarks[b]).toHaveAttr('href', createBookmarkUrl(courseId, usageId));
expect($(bookmarks[b]).find('.list-item-breadcrumbtrail').html().trim()).
toBe(breadcrumbTrail(results[b].path, results[b].display_name));
expect($(bookmarks[b]).find('.list-item-date').text().trim()).
toBe('Bookmarked on ' + view.humanFriendlyDate(results[b].created));
}
};
it("has correct behavior for bookmarks button", function () {
var requests = AjaxHelpers.requests(this);
spyOn(bookmarksButtonView, 'toggleBookmarksListView').andCallThrough();
bookmarksButtonView.delegateEvents();
expect(bookmarksButtonView.$('.bookmarks-list-button')).toHaveAttr('aria-pressed', 'false');
expect(bookmarksButtonView.$('.bookmarks-list-button')).toHaveClass('is-inactive');
bookmarksButtonView.$('.bookmarks-list-button').click();
expect(bookmarksButtonView.toggleBookmarksListView).toHaveBeenCalled();
expect(bookmarksButtonView.$('.bookmarks-list-button')).toHaveAttr('aria-pressed', 'true');
expect(bookmarksButtonView.$('.bookmarks-list-button')).toHaveClass('is-active');
AjaxHelpers.respondWithJson(requests, createBookmarksData(1));
bookmarksButtonView.$('.bookmarks-list-button').click();
expect(bookmarksButtonView.$('.bookmarks-list-button')).toHaveAttr('aria-pressed', 'false');
expect(bookmarksButtonView.$('.bookmarks-list-button')).toHaveClass('is-inactive');
});
it("has rendered empty bookmarks list correctly", function () {
var requests = AjaxHelpers.requests(this);
var expectedData = createBookmarksData(0);
bookmarksButtonView.$('.bookmarks-list-button').click();
AjaxHelpers.respondWithJson(requests, expectedData);
expect(bookmarksButtonView.bookmarksListView.$('.bookmarks-empty-header').text().trim()).
toBe('You have not bookmarked any courseware pages yet.');
var emptyListText = "Use bookmarks to help you easily return to courseware pages. " +
"To bookmark a page, select Bookmark in the upper right corner of that page. " +
"To see a list of all your bookmarks, select Bookmarks in the upper left " +
"corner of any courseware page.";
expect(bookmarksButtonView.bookmarksListView.$('.bookmarks-empty-detail-title').text().trim()).
toBe(emptyListText);
});
it("has rendered bookmarked list correctly", function () {
var requests = AjaxHelpers.requests(this);
var url = BOOKMARKS_API_URL + '?course_id=COURSE_ID&fields=display_name%2Cpath';
var expectedData = createBookmarksData(3);
spyOn(bookmarksButtonView.bookmarksListView, 'courseId').andReturn('COURSE_ID');
bookmarksButtonView.$('.bookmarks-list-button').click();
expect($('#loading-message').text().trim()).
toBe(bookmarksButtonView.bookmarksListView.loadingMessage);
AjaxHelpers.expectRequest(requests, 'GET', url);
AjaxHelpers.respondWithJson(requests, expectedData);
expect(bookmarksButtonView.bookmarksListView.$('.bookmarks-results-header').text().trim()).
toBe('My Bookmarks');
verifyBookmarkedData(bookmarksButtonView.bookmarksListView, expectedData);
});
it("can navigate to correct url", function () {
var requests = AjaxHelpers.requests(this);
spyOn(bookmarksButtonView.bookmarksListView, 'visitBookmark');
bookmarksButtonView.$('.bookmarks-list-button').click();
AjaxHelpers.respondWithJson(requests, createBookmarksData(1));
bookmarksButtonView.bookmarksListView.$('.bookmarks-results-list-item').click();
var url = bookmarksButtonView.bookmarksListView.$('.bookmarks-results-list-item').attr('href');
expect(bookmarksButtonView.bookmarksListView.visitBookmark).toHaveBeenCalledWithUrl(url);
});
it("shows an error message for HTTP 500", function () {
var requests = AjaxHelpers.requests(this);
bookmarksButtonView.$('.bookmarks-list-button').click();
AjaxHelpers.respondWithError(requests);
expect(bookmarksButtonView.bookmarksListView.$('.bookmarks-results-header').text().trim()).not
.toBe('My Bookmarks');
expect($('#error-message').text().trim()).toBe(bookmarksButtonView.bookmarksListView.errorMessage);
});
});
});
...@@ -66,6 +66,7 @@ ...@@ -66,6 +66,7 @@
'_split': 'js/split', '_split': 'js/split',
'mathjax_delay_renderer': 'coffee/src/mathjax_delay_renderer', 'mathjax_delay_renderer': 'coffee/src/mathjax_delay_renderer',
'MathJaxProcessor': 'coffee/src/customwmd', 'MathJaxProcessor': 'coffee/src/customwmd',
'moment': 'xmodule_js/common_static/js/src/moment',
// Manually specify LMS files that are not converted to RequireJS // Manually specify LMS files that are not converted to RequireJS
'history': 'js/vendor/history', 'history': 'js/vendor/history',
...@@ -733,7 +734,11 @@ ...@@ -733,7 +734,11 @@
'lms/include/teams/js/spec/views/topic_teams_spec.js', 'lms/include/teams/js/spec/views/topic_teams_spec.js',
'lms/include/teams/js/spec/views/topics_spec.js', 'lms/include/teams/js/spec/views/topics_spec.js',
'lms/include/teams/js/spec/views/team_profile_header_actions_spec.js', 'lms/include/teams/js/spec/views/team_profile_header_actions_spec.js',
'lms/include/js/spec/financial-assistance/financial_assistance_form_view_spec.js' 'lms/include/js/spec/financial-assistance/financial_assistance_form_view_spec.js',
'lms/include/teams/js/spec/views/team_join_spec.js'
'lms/include/js/spec/discovery/discovery_spec.js',
'lms/include/js/spec/ccx/schedule_spec.js',
'lms/include/js/spec/bookmarks/bookmarks_spec.js'
]); ]);
}).call(this, requirejs, define); }).call(this, requirejs, define);
...@@ -361,20 +361,90 @@ define([ ...@@ -361,20 +361,90 @@ define([
expect($('.search-button')).toBeVisible(); expect($('.search-button')).toBeVisible();
} }
describe('CourseSearchForm', function () { function rendersSearchResults () {
beforeEach(function () { var searchResults = [{
loadFixtures('js/fixtures/search/course_search_form.html'); location: ['section', 'subsection', 'unit'],
this.form = new CourseSearchForm(); url: '/some/url/to/content',
this.onClear = jasmine.createSpy('onClear'); content_type: 'text',
this.onSearch = jasmine.createSpy('onSearch'); course_name: '',
this.form.on('clear', this.onClear); excerpt: 'this is a short excerpt'
this.form.on('search', this.onSearch); }];
}); this.collection.set(searchResults);
it('trims input string', trimsInputString); this.collection.latestModelsCount = 1;
it('handles calls to doSearch', doesSearch); this.collection.totalCount = 1;
it('triggers a search event and changes to active state', triggersSearchEvent);
it('clears search when clicking on cancel button', clearsSearchOnCancel); this.resultsView.render();
it('clears search when search box is empty', clearsSearchOnEmpty); expect(this.resultsView.$el.find('ol')[0]).toExist();
expect(this.resultsView.$el.find('li').length).toEqual(1);
expect(this.resultsView.$el).toContainHtml('Search Results');
expect(this.resultsView.$el).toContainHtml('this is a short excerpt');
this.collection.set(searchResults);
this.collection.totalCount = 2;
this.resultsView.renderNext();
expect(this.resultsView.$el.find('.search-count')).toContainHtml('2');
expect(this.resultsView.$el.find('li').length).toEqual(2);
}
function showsMoreResultsLink () {
this.collection.totalCount = 123;
this.collection.hasNextPage = function () { return true; };
this.resultsView.render();
expect(this.resultsView.$el.find('a.search-load-next')[0]).toExist();
this.collection.totalCount = 123;
this.collection.hasNextPage = function () { return false; };
this.resultsView.render();
expect(this.resultsView.$el.find('a.search-load-next')[0]).not.toExist();
}
function triggersNextPageEvent () {
var onNext = jasmine.createSpy('onNext');
this.resultsView.on('next', onNext);
this.collection.totalCount = 123;
this.collection.hasNextPage = function () { return true; };
this.resultsView.render();
this.resultsView.$el.find('a.search-load-next').click();
expect(onNext).toHaveBeenCalled();
}
function showsLoadMoreSpinner () {
this.collection.totalCount = 123;
this.collection.hasNextPage = function () { return true; };
this.resultsView.render();
expect(this.resultsView.$el.find('a.search-load-next .icon')).toBeHidden();
this.resultsView.loadNext();
// toBeVisible does not work with inline
expect(this.resultsView.$el.find('a.search-load-next .icon')).toHaveCss({ 'display': 'inline' });
this.resultsView.renderNext();
expect(this.resultsView.$el.find('a.search-load-next .icon')).toBeHidden();
}
function beforeEachHelper(SearchResultsView) {
appendSetFixtures(
'<section id="courseware-search-results"></section>' +
'<section id="course-content"></section>' +
'<section id="dashboard-search-results"></section>' +
'<section id="my-courses"></section>'
);
TemplateHelpers.installTemplates([
'templates/search/course_search_item',
'templates/search/dashboard_search_item',
'templates/search/course_search_results',
'templates/search/dashboard_search_results',
'templates/search/search_list',
'templates/search/search_loading',
'templates/search/search_error'
]);
var MockCollection = Backbone.Collection.extend({
hasNextPage: function () {},
latestModelsCount: 0,
pageSize: 20,
latestModels: function () {
return SearchCollection.prototype.latestModels.apply(this, arguments);
}
}); });
describe('DashSearchForm', function () { describe('DashSearchForm', function () {
...@@ -503,6 +573,12 @@ define([ ...@@ -503,6 +573,12 @@ define([
'templates/search/search_error' 'templates/search/search_error'
]); ]);
var courseId = 'a/b/c';
CourseSearchFactory(courseId);
spyOn(Backbone.history, 'navigate');
this.$contentElement = $('#course-content');
this.$searchResults = $('.courseware-results');
var MockCollection = Backbone.Collection.extend({ var MockCollection = Backbone.Collection.extend({
hasNextPage: function () {}, hasNextPage: function () {},
latestModelsCount: 0, latestModelsCount: 0,
......
...@@ -8,7 +8,7 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers ...@@ -8,7 +8,7 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers
'js/student_profile/views/learner_profile_view', 'js/student_profile/views/learner_profile_view',
'js/student_profile/views/learner_profile_fields', 'js/student_profile/views/learner_profile_fields',
'js/student_profile/views/learner_profile_factory', 'js/student_profile/views/learner_profile_factory',
'js/views/message_banner' 'js/views/message'
], ],
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, LearnerProfileHelpers, FieldViews, function (Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, LearnerProfileHelpers, FieldViews,
UserAccountModel, UserPreferencesModel, LearnerProfileView, LearnerProfileFields, LearnerProfilePage) { UserAccountModel, UserPreferencesModel, LearnerProfileView, LearnerProfileFields, LearnerProfilePage) {
......
...@@ -2,10 +2,10 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers ...@@ -2,10 +2,10 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers
'js/spec/student_account/helpers', 'js/spec/student_account/helpers',
'js/student_account/models/user_account_model', 'js/student_account/models/user_account_model',
'js/student_profile/views/learner_profile_fields', 'js/student_profile/views/learner_profile_fields',
'js/views/message_banner' 'js/views/message'
], ],
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, UserAccountModel, LearnerProfileFields, function (Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, UserAccountModel, LearnerProfileFields,
MessageBannerView) { MessageView) {
'use strict'; 'use strict';
describe("edx.user.LearnerProfileFields", function () { describe("edx.user.LearnerProfileFields", function () {
...@@ -31,8 +31,9 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers ...@@ -31,8 +31,9 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers
accountSettingsModel.url = Helpers.USER_ACCOUNTS_API_URL; accountSettingsModel.url = Helpers.USER_ACCOUNTS_API_URL;
var messageView = new MessageBannerView({ var messageView = new MessageView({
el: $('.message-banner') el: $('.message-banner'),
templateId: '#message_banner-tpl'
}); });
return new LearnerProfileFields.ProfileImageFieldView({ return new LearnerProfileFields.ProfileImageFieldView({
......
...@@ -7,11 +7,11 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers ...@@ -7,11 +7,11 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers
'js/student_profile/views/learner_profile_fields', 'js/student_profile/views/learner_profile_fields',
'js/student_profile/views/learner_profile_view', 'js/student_profile/views/learner_profile_view',
'js/student_account/views/account_settings_fields', 'js/student_account/views/account_settings_fields',
'js/views/message_banner' 'js/views/message'
], ],
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, LearnerProfileHelpers, FieldViews, function (Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, LearnerProfileHelpers, FieldViews,
UserAccountModel, AccountPreferencesModel, LearnerProfileFields, LearnerProfileView, UserAccountModel, AccountPreferencesModel, LearnerProfileFields, LearnerProfileView,
AccountSettingsFieldViews, MessageBannerView) { AccountSettingsFieldViews, MessageView) {
'use strict'; 'use strict';
describe("edx.user.LearnerProfileView", function () { describe("edx.user.LearnerProfileView", function () {
...@@ -45,8 +45,9 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers ...@@ -45,8 +45,9 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers
accountSettingsPageUrl: '/account/settings/' accountSettingsPageUrl: '/account/settings/'
}); });
var messageView = new MessageBannerView({ var messageView = new MessageView({
el: $('.message-banner') el: $('.message-banner'),
templateId: '#message_banner-tpl'
}); });
var profileImageFieldView = new LearnerProfileFields.ProfileImageFieldView({ var profileImageFieldView = new LearnerProfileFields.ProfileImageFieldView({
......
define(['backbone', 'jquery', 'underscore', 'js/views/message_banner' define(['backbone', 'jquery', 'underscore', 'js/views/message', 'js/common_helpers/template_helpers'
], ],
function (Backbone, $, _, MessageBannerView) { function (Backbone, $, _, MessageView, TemplateHelpers) {
'use strict'; 'use strict';
describe("MessageBannerView", function () { describe("MessageView", function () {
var messageEl = '.message-banner';
beforeEach(function () { beforeEach(function () {
setFixtures('<div class="message-banner"></div>'); setFixtures('<div class="message-banner"></div>');
TemplateHelpers.installTemplate("templates/fields/message_banner"); TemplateHelpers.installTemplate("templates/fields/message_banner");
TemplateHelpers.installTemplate("templates/message_view");
}); });
it('renders message correctly', function() { var createMessageView = function (messageContainer, templateId) {
var messageSelector = '.message-banner'; return new MessageView({
var messageView = new MessageBannerView({ el: $(messageContainer),
el: $(messageSelector) templateId: templateId
}); });
};
it('renders correctly with the /fields/message_banner template', function() {
var messageView = createMessageView(messageSelector, '#message_banner-tpl');
messageView.showMessage('I am message view'); messageView.showMessage('I am message view');
// Verify error message expect($(messageEl).text().trim()).toBe('I am message view');
expect($(messageSelector).text().trim()).toBe('I am message view');
messageView.hideMessage();
expect($(messageEl).text().trim()).toBe('');
});
it('renders correctly with the /message_view template', function() {
var messageView = createMessageView(messageEl, '#message-tpl');
var icon = '<i class="fa fa-thumbs-up"></i>';
messageView.showMessage('I am message view', icon);
expect($(messageEl).text().trim()).toBe('I am message view');
expect($(messageEl).html()).toContain(icon);
messageView.hideMessage(); messageView.hideMessage();
expect($(messageSelector).text().trim()).toBe(''); expect($(messageEl).text().trim()).toBe('');
}); });
}); });
}); });
...@@ -8,10 +8,10 @@ ...@@ -8,10 +8,10 @@
'js/student_profile/views/learner_profile_fields', 'js/student_profile/views/learner_profile_fields',
'js/student_profile/views/learner_profile_view', 'js/student_profile/views/learner_profile_view',
'js/student_account/views/account_settings_fields', 'js/student_account/views/account_settings_fields',
'js/views/message_banner', 'js/views/message',
'string_utils' 'string_utils'
], function (gettext, $, _, Backbone, Logger, AccountSettingsModel, AccountPreferencesModel, FieldsView, ], function (gettext, $, _, Backbone, Logger, AccountSettingsModel, AccountPreferencesModel, FieldsView,
LearnerProfileFieldsView, LearnerProfileView, AccountSettingsFieldViews, MessageBannerView) { LearnerProfileFieldsView, LearnerProfileView, AccountSettingsFieldViews, MessageView) {
return function (options) { return function (options) {
...@@ -36,8 +36,9 @@ ...@@ -36,8 +36,9 @@
var editable = options.own_profile ? 'toggle' : 'never'; var editable = options.own_profile ? 'toggle' : 'never';
var messageView = new MessageBannerView({ var messageView = new MessageView({
el: $('.message-banner') el: $('.message-banner'),
templateId: '#message_banner-tpl'
}); });
var accountPrivacyFieldView = new LearnerProfileFieldsView.AccountPrivacyFieldView({ var accountPrivacyFieldView = new LearnerProfileFieldsView.AccountPrivacyFieldView({
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -17,15 +17,17 @@ ...@@ -17,15 +17,17 @@
if (_.isUndefined(this.message) || _.isNull(this.message)) { if (_.isUndefined(this.message) || _.isNull(this.message)) {
this.$el.html(''); this.$el.html('');
} else { } else {
this.$el.html(_.template(messageBannerTemplate, _.extend(this.options, { this.$el.html(this.template({
message: this.message message: this.message,
}))); icon: this.icon
}));
} }
return this; return this;
}, },
showMessage: function (message) { showMessage: function (message, icon) {
this.message = message; this.message = message;
this.icon = icon;
this.render(); this.render();
}, },
......
...@@ -114,6 +114,9 @@ fixture_paths: ...@@ -114,6 +114,9 @@ fixture_paths:
- common/templates - common/templates
- teams/templates - teams/templates
- support/templates - support/templates
- js/fixtures/bookmarks
- templates/bookmarks
- templates/message_view.underscore
requirejs: requirejs:
paths: paths:
......
;(function (require, define) {
var paths = {}, config;
// jquery, underscore, gettext, URI, tinymce, or jquery.tinymce may already
// have been loaded and we do not want to load them a second time. Check if
// it is the case and use the global var instead.
if (window.jQuery) {
define("jquery", [], function() {return window.jQuery;});
} else {
paths.jquery = "js/vendor/jquery.min";
}
if (window._) {
define("underscore", [], function() {return window._;});
} else {
paths.jquery = "js/vendor/underscore-min";
}
if (window.gettext) {
define("gettext", [], function() {return window.gettext;});
} else {
paths.gettext = "/i18n";
}
if (window.Logger) {
define("logger", [], function() {return window.Logger;});
} else {
paths.logger = "js/src/logger";
}
if (window.URI) {
define("URI", [], function() {return window.URI;});
} else {
paths.URI = "js/vendor/URI.min";
}
if (window.tinymce) {
define('tinymce', [], function() {return window.tinymce;});
} else {
paths.tinymce = "js/vendor/tinymce/js/tinymce/tinymce.full.min";
}
if (window.jquery && window.jquery.tinymce) {
define("jquery.tinymce", [], function() {return window.jquery.tinymce;});
} else {
paths.tinymce = "js/vendor/tinymce/js/tinymce/jquery.tinymce.min";
}
config = {
// NOTE: baseUrl has been previously set in lms/static/templates/main.html
waitSeconds: 60,
paths: {
"annotator_1.2.9": "js/vendor/edxnotes/annotator-full.min",
"date": "js/vendor/date",
"text": 'js/vendor/requirejs/text',
"backbone": "js/vendor/backbone-min",
"backbone-super": "js/vendor/backbone-super",
"backbone.paginator": "js/vendor/backbone.paginator.min",
"underscore.string": "js/vendor/underscore.string.min",
// Files needed by OVA
"annotator": "js/vendor/ova/annotator-full",
"annotator-harvardx": "js/vendor/ova/annotator-full-firebase-auth",
"video.dev": "js/vendor/ova/video.dev",
"vjs.youtube": 'js/vendor/ova/vjs.youtube',
"rangeslider": 'js/vendor/ova/rangeslider',
"share-annotator": 'js/vendor/ova/share-annotator',
"richText-annotator": 'js/vendor/ova/richText-annotator',
"reply-annotator": 'js/vendor/ova/reply-annotator',
"grouping-annotator": 'js/vendor/ova/grouping-annotator',
"tags-annotator": 'js/vendor/ova/tags-annotator',
"diacritic-annotator": 'js/vendor/ova/diacritic-annotator',
"flagging-annotator": 'js/vendor/ova/flagging-annotator',
"jquery-Watch": 'js/vendor/ova/jquery-Watch',
"openseadragon": 'js/vendor/ova/openseadragon',
"osda": 'js/vendor/ova/OpenSeaDragonAnnotation',
"ova": 'js/vendor/ova/ova',
"catch": 'js/vendor/ova/catch/js/catch',
"handlebars": 'js/vendor/ova/catch/js/handlebars-1.1.2',
"moment": "js/vendor/moment-with-locales.min"
// end of files needed by OVA
},
shim: {
"annotator_1.2.9": {
deps: ["jquery"],
exports: "Annotator"
},
"date": {
exports: "Date"
},
"jquery": {
exports: "$"
},
"underscore": {
exports: "_"
},
"backbone": {
deps: ["underscore", "jquery"],
exports: "Backbone"
},
"backbone.paginator": {
deps: ["backbone"],
exports: "Backbone.Paginator"
},
"backbone-super": {
deps: ["backbone"]
},
"logger": {
exports: "Logger"
},
// Needed by OVA
"video.dev": {
exports:"videojs"
},
"vjs.youtube": {
deps: ["video.dev"]
},
"rangeslider": {
deps: ["video.dev"]
},
"annotator": {
exports: "Annotator"
},
"annotator-harvardx":{
deps: ["annotator"]
},
"share-annotator": {
deps: ["annotator"]
},
"richText-annotator": {
deps: ["annotator", "tinymce"]
},
"reply-annotator": {
deps: ["annotator"]
},
"tags-annotator": {
deps: ["annotator"]
},
"diacritic-annotator": {
deps: ["annotator"]
},
"flagging-annotator": {
deps: ["annotator"]
},
"grouping-annotator": {
deps: ["annotator"]
},
"ova": {
exports: "ova",
deps: [
"annotator", "annotator-harvardx", "video.dev", "vjs.youtube", "rangeslider", "share-annotator",
"richText-annotator", "reply-annotator", "tags-annotator", "flagging-annotator",
"grouping-annotator", "diacritic-annotator", "jquery-Watch", "catch", "handlebars", "URI"
]
},
"osda": {
exports: "osda",
deps: [
"annotator", "annotator-harvardx", "video.dev", "vjs.youtube", "rangeslider", "share-annotator",
"richText-annotator", "reply-annotator", "tags-annotator", "flagging-annotator",
"grouping-annotator", "diacritic-annotator", "openseadragon", "jquery-Watch", "catch", "handlebars",
"URI"
]
}
// End of needed by OVA
}
};
for (var key in paths) {
if ({}.hasOwnProperty.call(paths, key)) {
config.paths[key] = paths[key];
}
}
require.config(config);
}).call(this, require || RequireJS.require, define || RequireJS.define);
...@@ -54,6 +54,7 @@ ...@@ -54,6 +54,7 @@
@import 'views/homepage'; @import 'views/homepage';
@import 'views/support'; @import 'views/support';
@import "views/financial-assistance"; @import "views/financial-assistance";
@import 'views/bookmarks';
@import 'course/auto-cert'; @import 'course/auto-cert';
// app - discussion // app - discussion
......
...@@ -649,3 +649,11 @@ section.self-assessment { ...@@ -649,3 +649,11 @@ section.self-assessment {
font-weight: bold; font-weight: bold;
} }
} }
.courseware-results-wrapper {
padding: ($baseline*2);
.courseware-results {
display: none;
}
}
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
@include box-sizing(border-box); @include box-sizing(border-box);
position: relative; position: relative;
padding: ($baseline/4);
.search-field-wrapper { .search-field-wrapper {
position: relative; position: relative;
...@@ -124,7 +123,7 @@ ...@@ -124,7 +123,7 @@
} }
.courseware-search-bar { .courseware-search-bar {
box-shadow: 0 1px 0 $white inset, 0 -1px 0 $shadow-l1 inset; width: flex-grid(7);
} }
...@@ -171,6 +170,3 @@ ...@@ -171,6 +170,3 @@
} }
} }
.courseware-search-results {
padding: ($baseline*2);
}
// Rules for placing bookmarks and search button side by side
.wrapper-course-modes {
border-bottom: 1px solid $gray-l3;
padding: ($baseline/4);
> div {
@include box-sizing(border-box);
display: inline-block;
}
}
// Rules for Bookmarks Button
.courseware-bookmarks-button {
width: flex-grid(5);
vertical-align: top;
button {
&.is-active {
background-color: $white;
&:before {
content: "\f02e";
font-family: FontAwesome;
}
}
&.is-inactive {
background: none;
&:before {
content: "\f097";
font-family: FontAwesome;
}
}
}
.bookmarks-list-button {
background: none;
border-radius: ($baseline/4);
padding: ($baseline/2);
width: 100%;
&:hover {
background: none;
}
&:focus, &:active {
box-shadow: none;
}
}
}
// Rules for Bookmarks Results Header
.bookmarks-results-header {
margin: 0;
}
// Rules for Bookmarks Results
.bookmarks-results-list {
padding-top: $baseline;
.bookmarks-results-list-item {
@include padding(($baseline/4), $baseline, ($baseline/2), $baseline);
display: block;
border: 1px solid $gray;
border-radius: ($baseline/4);
margin-bottom: $baseline;
&:hover {
border-color: $link-color;
color: $link-color;
}
}
.results-list-item-view {
@include float(right);
margin-top: $baseline;
}
.list-item-date {
@extend %t-title7;
color: $gray;
}
a.bookmarks-results-list-item:before {
content: "\f02e";
position: relative;
top: -7px;
font-family: FontAwesome;
}
.list-item-content {
overflow: hidden;
}
.list-item-left-section {
@include float(left);
width: 90%;
.list-item-breadcrumbtrail, .list-item-date {
@extend %t-ultralight;
}
}
.list-item-right-section {
@include float(right);
margin-top: 7px;
}
}
// Rules for empty bookmarks list
.bookmarks-empty {
margin-top: $baseline;
border: 1px solid $gray-l4;
padding: $baseline;
background-color: $gray-l6;
}
.bookmarks-empty-header {
@extend %t-title5;
margin-bottom: ($baseline/2);
}
.bookmarks-empty-detail {
@extend %t-copy-sub1;
}
\ No newline at end of file
<div id="my-bookmarks" class="sr-is-focusable" tabindex="-1"></div>
<h2 class="bookmarks-results-header"><%= gettext("My Bookmarks") %></h2>
<% if (bookmarks.length) { %>
<div class='bookmarks-results-list'>
<% _.each(bookmarks, function(bookmark, index) { %>
<a class="bookmarks-results-list-item" href="<%= bookmark.blockUrl() %>" aria-labelledby="bookmark-link-<%= index %>" aria-describedby="bookmark-type-<%= index %> bookmark-date-<%= index %>">
<div class="list-item-content">
<div class="list-item-left-section">
<h3 id="bookmark-link-<%= index %>" class="list-item-breadcrumbtrail"> <%= _.pluck(bookmark.get('path'), 'display_name').concat([bookmark.get('display_name')]).join(' <i class="icon fa fa-caret-right" aria-hidden="true"></i><span class="sr">-</span> ') %> </h3>
<p id="bookmark-date-<%= index %>" class="list-item-date"> <%= gettext("Bookmarked on") %> <%= humanFriendlyDate(bookmark.get('created')) %> </p>
</div>
<p id="bookmark-type-<%= index %>" class="list-item-right-section">
<span aria-hidden="true"><%= gettext("View") %></span>
<i class="icon fa fa-arrow-right" aria-hidden="true"></i>
</p>
</div>
</a>
<% }); %>
</div>
<% } else {%>
<div class="bookmarks-empty" tabindex="0">
<div class="bookmarks-empty-header">
<i class="icon fa fa-bookmark-o bookmarks-empty-header-icon" aria-hidden="true"></i>
<%= gettext("You have not bookmarked any courseware pages yet.") %>
<br>
</div>
<div class="bookmarks-empty-detail">
<span class="bookmarks-empty-detail-title">
<%= gettext("Use bookmarks to help you easily return to courseware pages. To bookmark a page, select Bookmark in the upper right corner of that page. To see a list of all your bookmarks, select Bookmarks in the upper left corner of any courseware page.") %>
</span>
</div>
</div>
<% } %>
...@@ -46,6 +46,18 @@ ${page_title_breadcrumbs(course_name())} ...@@ -46,6 +46,18 @@ ${page_title_breadcrumbs(course_name())}
% endfor % endfor
% endif % endif
% for template_name in ["bookmarks_list"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="bookmarks/${template_name}.underscore" />
</script>
% endfor
% for template_name in ["message_view"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="${template_name}.underscore" />
</script>
% endfor
</%block> </%block>
<%block name="headextra"> <%block name="headextra">
...@@ -131,6 +143,26 @@ ${fragment.foot_html()} ...@@ -131,6 +143,26 @@ ${fragment.foot_html()}
<input id="course-search-input" type="text" class="search-field"/> <input id="course-search-input" type="text" class="search-field"/>
<button type="submit" class="search-button"> <button type="submit" class="search-button">
${_('search')} <i class="icon fa fa-search" aria-hidden="true"></i> ${_('search')} <i class="icon fa fa-search" aria-hidden="true"></i>
<header id="open_close_accordion">
<a href="#">${_("close")}</a>
</header>
<div class="wrapper-course-modes">
<div class="courseware-bookmarks-button">
<button type="button" class="bookmarks-list-button is-inactive" aria-pressed="false">
${_('Bookmarks')}
</button>
</div>
% if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
<div id="courseware-search-bar" class="search-bar courseware-search-bar" role="search" aria-label="Course">
<form>
<label for="course-search-input" class="sr">${_('Course Search')}</label>
<div class="search-field-wrapper">
<input id="course-search-input" type="text" class="search-field"/>
<button type="submit" class="search-button">
${_('search')} <i class="icon fa fa-search" aria-hidden="true"></i>
</button> </button>
<button type="button" class="cancel-button" aria-label="${_('Clear search')}"> <button type="button" class="cancel-button" aria-label="${_('Clear search')}">
<i class="icon fa fa-remove" aria-hidden="true"></i> <i class="icon fa fa-remove" aria-hidden="true"></i>
...@@ -142,6 +174,10 @@ ${fragment.foot_html()} ...@@ -142,6 +174,10 @@ ${fragment.foot_html()}
<div class="accordion"> <div class="accordion">
<nav class="course-navigation" aria-label="${_('Course')}"> <nav class="course-navigation" aria-label="${_('Course')}">
</div>
<div id="accordion" style="display: none">
<nav aria-label="${_('Course Navigation')}">
% if accordion.strip(): % if accordion.strip():
${accordion} ${accordion}
% else: % else:
...@@ -185,10 +221,13 @@ ${fragment.foot_html()} ...@@ -185,10 +221,13 @@ ${fragment.foot_html()}
${fragment.body_html()} ${fragment.body_html()}
</section> </section>
% if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
<section id="courseware-search-results" class="search-results courseware-search-results" data-course-id="${course.id}"> <section class="courseware-results-wrapper">
<div id="loading-message" aria-live="polite" aria-relevant="all"></div>
<div id="error-message" aria-live="polite"></div>
<div class="courseware-results search-results" data-course-id="${course.id}" data-lang-code="${language_preference}"></div>
</section> </section>
% endif
</div> </div>
</div> </div>
<div class="container-footer"> <div class="container-footer">
......
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