Commit 13b14e0a by muhammad-ammar Committed by muzaffaryousaf

Paginate edxnotes frontend

TNL-3908
parent 840f5017
...@@ -114,7 +114,7 @@ class EdxNotesPageItem(NoteChild): ...@@ -114,7 +114,7 @@ class EdxNotesPageItem(NoteChild):
""" """
BODY_SELECTOR = ".note" BODY_SELECTOR = ".note"
UNIT_LINK_SELECTOR = "a.reference-unit-link" UNIT_LINK_SELECTOR = "a.reference-unit-link"
TAG_SELECTOR = "a.reference-tags" TAG_SELECTOR = "span.reference-tags"
def go_to_unit(self, unit_page=None): def go_to_unit(self, unit_page=None):
self.q(css=self._bounded_selector(self.UNIT_LINK_SELECTOR)).click() self.q(css=self._bounded_selector(self.UNIT_LINK_SELECTOR)).click()
......
""" """
Test LMS Notes Test LMS Notes
""" """
from unittest import skip
from uuid import uuid4 from uuid import uuid4
from datetime import datetime from datetime import datetime
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
...@@ -850,6 +851,7 @@ class EdxNotesPageTest(EventsTestMixin, EdxNotesTestMixin): ...@@ -850,6 +851,7 @@ class EdxNotesPageTest(EventsTestMixin, EdxNotesTestMixin):
self.assert_viewed_event('Search Results') self.assert_viewed_event('Search Results')
self.assert_search_event('note', 4) self.assert_search_event('note', 4)
@skip("scroll to tag functionality is removed")
def test_scroll_to_tag_recent_activity(self): def test_scroll_to_tag_recent_activity(self):
""" """
Scenario: Can scroll to a tag group from the Recent Activity view (default view) Scenario: Can scroll to a tag group from the Recent Activity view (default view)
...@@ -861,6 +863,7 @@ class EdxNotesPageTest(EventsTestMixin, EdxNotesTestMixin): ...@@ -861,6 +863,7 @@ class EdxNotesPageTest(EventsTestMixin, EdxNotesTestMixin):
self.notes_page.visit() self.notes_page.visit()
self._scroll_to_tag_and_verify("pear", 3) self._scroll_to_tag_and_verify("pear", 3)
@skip("scroll to tag functionality is removed")
def test_scroll_to_tag_course_structure(self): def test_scroll_to_tag_course_structure(self):
""" """
Scenario: Can scroll to a tag group from the Course Structure view Scenario: Can scroll to a tag group from the Course Structure view
...@@ -872,6 +875,7 @@ class EdxNotesPageTest(EventsTestMixin, EdxNotesTestMixin): ...@@ -872,6 +875,7 @@ class EdxNotesPageTest(EventsTestMixin, EdxNotesTestMixin):
self.notes_page.visit().switch_to_tab("structure") self.notes_page.visit().switch_to_tab("structure")
self._scroll_to_tag_and_verify("squash", 5) self._scroll_to_tag_and_verify("squash", 5)
@skip("scroll to tag functionality is removed")
def test_scroll_to_tag_search(self): def test_scroll_to_tag_search(self):
""" """
Scenario: Can scroll to a tag group from the Search Results view Scenario: Can scroll to a tag group from the Search Results view
...@@ -884,6 +888,7 @@ class EdxNotesPageTest(EventsTestMixin, EdxNotesTestMixin): ...@@ -884,6 +888,7 @@ class EdxNotesPageTest(EventsTestMixin, EdxNotesTestMixin):
self.notes_page.visit().search("note") self.notes_page.visit().search("note")
self._scroll_to_tag_and_verify("pumpkin", 4) self._scroll_to_tag_and_verify("pumpkin", 4)
@skip("scroll to tag functionality is removed")
def test_scroll_to_tag_from_tag_view(self): def test_scroll_to_tag_from_tag_view(self):
""" """
Scenario: Can scroll to a tag group from the Tags view Scenario: Can scroll to a tag group from the Tags view
......
...@@ -106,6 +106,7 @@ def send_request(user, course_id, page, page_size, path="", text=None): ...@@ -106,6 +106,7 @@ def send_request(user, course_id, page, page_size, path="", text=None):
params=params params=params
) )
except RequestException: except RequestException:
log.error("Failed to connect to edx-notes-api: url=%s, params=%s", url, str(params))
raise EdxNotesServiceUnavailable(_("EdxNotes Service is unavailable. Please try again in a few minutes.")) raise EdxNotesServiceUnavailable(_("EdxNotes Service is unavailable. Please try again in a few minutes."))
return response return response
...@@ -141,6 +142,7 @@ def preprocess_collection(user, course, collection): ...@@ -141,6 +142,7 @@ def preprocess_collection(user, course, collection):
store = modulestore() store = modulestore()
filtered_collection = list() filtered_collection = list()
cache = {} cache = {}
include_extra_info = settings.NOTES_DISABLED_TABS == []
with store.bulk_operations(course.id): with store.bulk_operations(course.id):
for model in collection: for model in collection:
update = { update = {
...@@ -176,42 +178,46 @@ def preprocess_collection(user, course, collection): ...@@ -176,42 +178,46 @@ def preprocess_collection(user, course, collection):
log.debug("Unit not found: %s", usage_key) log.debug("Unit not found: %s", usage_key)
continue continue
section = unit.get_parent() if include_extra_info:
if not section: section = unit.get_parent()
log.debug("Section not found: %s", usage_key) if not section:
continue log.debug("Section not found: %s", usage_key)
if section in cache: continue
usage_context = cache[section] if section in cache:
usage_context.update({ usage_context = cache[section]
"unit": get_module_context(course, unit), usage_context.update({
}) "unit": get_module_context(course, unit),
model.update(usage_context) })
cache[usage_id] = cache[unit] = usage_context model.update(usage_context)
filtered_collection.append(model) cache[usage_id] = cache[unit] = usage_context
continue filtered_collection.append(model)
continue
chapter = section.get_parent()
if not chapter: chapter = section.get_parent()
log.debug("Chapter not found: %s", usage_key) if not chapter:
continue log.debug("Chapter not found: %s", usage_key)
if chapter in cache: continue
usage_context = cache[chapter] if chapter in cache:
usage_context.update({ usage_context = cache[chapter]
"unit": get_module_context(course, unit), usage_context.update({
"section": get_module_context(course, section), "unit": get_module_context(course, unit),
}) "section": get_module_context(course, section),
model.update(usage_context) })
cache[usage_id] = cache[unit] = cache[section] = usage_context model.update(usage_context)
filtered_collection.append(model) cache[usage_id] = cache[unit] = cache[section] = usage_context
continue filtered_collection.append(model)
continue
usage_context = { usage_context = {
"unit": get_module_context(course, unit), "unit": get_module_context(course, unit),
"section": get_module_context(course, section), "section": get_module_context(course, section) if include_extra_info else {},
"chapter": get_module_context(course, chapter), "chapter": get_module_context(course, chapter) if include_extra_info else {},
} }
model.update(usage_context) model.update(usage_context)
cache[usage_id] = cache[unit] = cache[section] = cache[chapter] = usage_context if include_extra_info:
cache[section] = cache[chapter] = usage_context
cache[usage_id] = cache[unit] = usage_context
filtered_collection.append(model) filtered_collection.append(model)
return filtered_collection return filtered_collection
...@@ -319,16 +325,24 @@ def get_notes(request, course, page=DEFAULT_PAGE, page_size=DEFAULT_PAGE_SIZE, t ...@@ -319,16 +325,24 @@ def get_notes(request, course, page=DEFAULT_PAGE, page_size=DEFAULT_PAGE_SIZE, t
try: try:
collection = json.loads(response.content) collection = json.loads(response.content)
except ValueError: except ValueError:
raise EdxNotesParseError(_("Invalid response received from notes api.")) log.error("Invalid JSON response received from notes api: response_content=%s", response.content)
raise EdxNotesParseError(_("Invalid JSON response received from notes api."))
# Verify response dict structure # Verify response dict structure
expected_keys = ['count', 'results', 'num_pages', 'start', 'next', 'previous', 'current_page'] expected_keys = ['total', 'rows', 'num_pages', 'start', 'next', 'previous', 'current_page']
keys = collection.keys() keys = collection.keys()
if not keys or not all(key in expected_keys for key in keys): if not keys or not all(key in expected_keys for key in keys):
raise EdxNotesParseError(_("Invalid response received from notes api.")) log.error("Incorrect data received from notes api: collection_data=%s", str(collection))
raise EdxNotesParseError(_("Incorrect data received from notes api."))
filtered_results = preprocess_collection(request.user, course, collection['results'])
filtered_results = preprocess_collection(request.user, course, collection['rows'])
# Notes API is called from:
# 1. The annotatorjs in courseware. It expects these attributes to be named "total" and "rows".
# 2. The Notes tab Javascript proxied through LMS. It expects these attributes to be called "count" and "results".
collection['count'] = collection['total']
del collection['total']
collection['results'] = filtered_results collection['results'] = filtered_results
del collection['rows']
collection['next'], collection['previous'] = construct_pagination_urls( collection['next'], collection['previous'] = construct_pagination_urls(
request, request,
......
...@@ -33,6 +33,16 @@ from student.tests.factories import UserFactory, CourseEnrollmentFactory ...@@ -33,6 +33,16 @@ from student.tests.factories import UserFactory, CourseEnrollmentFactory
NOTES_API_EMPTY_RESPONSE = { NOTES_API_EMPTY_RESPONSE = {
"total": 0,
"rows": [],
"current_page": 1,
"start": 0,
"next": None,
"previous": None,
"num_pages": 0,
}
NOTES_VIEW_EMPTY_RESPONSE = {
"count": 0, "count": 0,
"results": [], "results": [],
"current_page": 1, "current_page": 1,
...@@ -297,13 +307,13 @@ class EdxNotesHelpersTest(ModuleStoreTestCase): ...@@ -297,13 +307,13 @@ class EdxNotesHelpersTest(ModuleStoreTestCase):
""" """
mock_get.return_value.content = json.dumps( mock_get.return_value.content = json.dumps(
{ {
"count": 2, "total": 2,
"current_page": 1, "current_page": 1,
"start": 0, "start": 0,
"next": None, "next": None,
"previous": None, "previous": None,
"num_pages": 1, "num_pages": 1,
"results": [ "rows": [
{ {
u"quote": u"quote text", u"quote": u"quote text",
u"text": u"text", u"text": u"text",
...@@ -404,13 +414,13 @@ class EdxNotesHelpersTest(ModuleStoreTestCase): ...@@ -404,13 +414,13 @@ class EdxNotesHelpersTest(ModuleStoreTestCase):
Tests the result if correct data is received. Tests the result if correct data is received.
""" """
mock_get.return_value.content = json.dumps({ mock_get.return_value.content = json.dumps({
"count": 2, "total": 2,
"current_page": 1, "current_page": 1,
"start": 0, "start": 0,
"next": None, "next": None,
"previous": None, "previous": None,
"num_pages": 1, "num_pages": 1,
"results": [ "rows": [
{ {
u"quote": u"quote text", u"quote": u"quote text",
u"text": u"text", u"text": u"text",
...@@ -511,7 +521,7 @@ class EdxNotesHelpersTest(ModuleStoreTestCase): ...@@ -511,7 +521,7 @@ class EdxNotesHelpersTest(ModuleStoreTestCase):
""" """
mock_get.return_value.content = json.dumps(NOTES_API_EMPTY_RESPONSE) mock_get.return_value.content = json.dumps(NOTES_API_EMPTY_RESPONSE)
self.assertItemsEqual( self.assertItemsEqual(
NOTES_API_EMPTY_RESPONSE, NOTES_VIEW_EMPTY_RESPONSE,
json.loads(helpers.get_notes(self.request, self.course)) json.loads(helpers.get_notes(self.request, self.course))
) )
...@@ -1006,19 +1016,17 @@ class EdxNotesViewsTest(ModuleStoreTestCase): ...@@ -1006,19 +1016,17 @@ class EdxNotesViewsTest(ModuleStoreTestCase):
""" """
Tests that search notes successfully respond if EdxNotes feature is enabled. Tests that search notes successfully respond if EdxNotes feature is enabled.
""" """
mock_search.return_value = json.dumps(NOTES_API_EMPTY_RESPONSE) mock_search.return_value = json.dumps(NOTES_VIEW_EMPTY_RESPONSE)
enable_edxnotes_for_the_course(self.course, self.user.id) enable_edxnotes_for_the_course(self.course, self.user.id)
response = self.client.get(self.notes_url, {"text": "test"}) response = self.client.get(self.notes_url, {"text": "test"})
self.assertEqual(json.loads(response.content), NOTES_API_EMPTY_RESPONSE) self.assertEqual(json.loads(response.content), NOTES_VIEW_EMPTY_RESPONSE)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_EDXNOTES": False}) @patch.dict("django.conf.settings.FEATURES", {"ENABLE_EDXNOTES": False})
@patch("edxnotes.views.get_notes", autospec=True) def test_search_notes_is_disabled(self):
def test_search_notes_is_disabled(self, mock_search):
""" """
Tests that 404 status code is received if EdxNotes feature is disabled. Tests that 404 status code is received if EdxNotes feature is disabled.
""" """
mock_search.return_value = json.dumps(NOTES_API_EMPTY_RESPONSE)
response = self.client.get(self.notes_url, {"text": "test"}) response = self.client.get(self.notes_url, {"text": "test"})
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
......
...@@ -55,8 +55,10 @@ def edxnotes(request, course_id): ...@@ -55,8 +55,10 @@ def edxnotes(request, course_id):
"course": course, "course": course,
"notes_endpoint": reverse("notes", kwargs={"course_id": course_id}), "notes_endpoint": reverse("notes", kwargs={"course_id": course_id}),
"notes": notes_info, "notes": notes_info,
"page_size": DEFAULT_PAGE_SIZE,
"debug": json.dumps(settings.DEBUG), "debug": json.dumps(settings.DEBUG),
'position': None, 'position': None,
'disabled_tabs': settings.NOTES_DISABLED_TABS,
} }
if len(json.loads(notes_info)['results']) == 0: if len(json.loads(notes_info)['results']) == 0:
......
...@@ -91,6 +91,8 @@ XQUEUE_INTERFACE['url'] = 'http://localhost:8040' ...@@ -91,6 +91,8 @@ XQUEUE_INTERFACE['url'] = 'http://localhost:8040'
EDXNOTES_PUBLIC_API = 'http://localhost:8042/api/v1' EDXNOTES_PUBLIC_API = 'http://localhost:8042/api/v1'
EDXNOTES_INTERNAL_API = 'http://localhost:8042/api/v1' EDXNOTES_INTERNAL_API = 'http://localhost:8042/api/v1'
NOTES_DISABLED_TABS = []
# Silence noisy logs # Silence noisy logs
import logging import logging
LOG_OVERRIDES = [ LOG_OVERRIDES = [
......
...@@ -2521,6 +2521,9 @@ ENROLLMENT_COURSE_DETAILS_CACHE_TIMEOUT = 60 ...@@ -2521,6 +2521,9 @@ ENROLLMENT_COURSE_DETAILS_CACHE_TIMEOUT = 60
if FEATURES['ENABLE_EDXNOTES']: if FEATURES['ENABLE_EDXNOTES']:
OAUTH_ID_TOKEN_EXPIRATION = 60 * 60 OAUTH_ID_TOKEN_EXPIRATION = 60 * 60
# These tabs are currently disabled
NOTES_DISABLED_TABS = ['course_structure', 'tags']
# Configuration used for generating PDF Receipts/Invoices # Configuration used for generating PDF Receipts/Invoices
PDF_RECEIPT_TAX_ID = 'add here' PDF_RECEIPT_TAX_ID = 'add here'
PDF_RECEIPT_FOOTER_TEXT = 'add your own specific footer text here' PDF_RECEIPT_FOOTER_TEXT = 'add your own specific footer text here'
......
...@@ -509,6 +509,8 @@ MONGODB_LOG = { ...@@ -509,6 +509,8 @@ MONGODB_LOG = {
'db': 'xlog', 'db': 'xlog',
} }
NOTES_DISABLED_TABS = []
# Enable EdxNotes for tests. # Enable EdxNotes for tests.
FEATURES['ENABLE_EDXNOTES'] = True FEATURES['ENABLE_EDXNOTES'] = True
......
;(function (define, undefined) { ;(function (define) {
'use strict'; 'use strict';
define([ define([
'backbone', 'js/edxnotes/models/note' 'underscore', 'common/js/components/collections/paging_collection', 'js/edxnotes/models/note'
], function (Backbone, NoteModel) { ], function (_, PagingCollection, NoteModel) {
var NotesCollection = Backbone.Collection.extend({ return PagingCollection.extend({
model: NoteModel, model: NoteModel,
initialize: function(models, options) {
PagingCollection.prototype.initialize.call(this);
this.perPage = options.perPage;
this.server_api = _.pick(PagingCollection.prototype.server_api, "page", "page_size");
if (options.text) {
this.server_api.text = options.text;
}
},
/** /**
* Returns course structure from the list of notes. * Returns course structure from the list of notes.
* @return {Object} * @return {Object}
...@@ -17,30 +27,26 @@ define([ ...@@ -17,30 +27,26 @@ define([
sections = {}, sections = {},
units = {}; units = {};
if (!courseStructure) { this.each(function (note) {
this.each(function (note) { var chapter = note.get('chapter'),
var chapter = note.get('chapter'), section = note.get('section'),
section = note.get('section'), unit = note.get('unit');
unit = note.get('unit');
chapters[chapter.location] = chapter; chapters[chapter.location] = chapter;
sections[section.location] = section; sections[section.location] = section;
units[unit.location] = units[unit.location] || []; units[unit.location] = units[unit.location] || [];
units[unit.location].push(note); units[unit.location].push(note);
}); });
courseStructure = { courseStructure = {
chapters: _.sortBy(_.toArray(chapters), function (c) {return c.index;}), chapters: _.sortBy(_.toArray(chapters), function (c) {return c.index;}),
sections: sections, sections: sections,
units: units units: units
}; };
}
return courseStructure; return courseStructure;
}; };
}()) }())
}); });
return NotesCollection;
}); });
}).call(this, define || RequireJS.define); }).call(this, define || RequireJS.define);
...@@ -15,17 +15,19 @@ define([ ...@@ -15,17 +15,19 @@ define([
this.options = options; this.options = options;
this.tabsCollection = new TabsCollection(); this.tabsCollection = new TabsCollection();
// Must create the Tags view first to get the "scrollToTag" method. if (!_.contains(this.options.disabledTabs, 'tags')) {
this.tagsView = new TagsView({ // Must create the Tags view first to get the "scrollToTag" method.
el: this.el, this.tagsView = new TagsView({
collection: this.collection, el: this.el,
tabsCollection: this.tabsCollection collection: this.collection,
}); tabsCollection: this.tabsCollection
});
scrollToTag = this.tagsView.scrollToTag; scrollToTag = this.tagsView.scrollToTag;
// Remove the Tags model from the tabs collection because it should not appear first. // Remove the Tags model from the tabs collection because it should not appear first.
tagsModel = this.tabsCollection.shift(); tagsModel = this.tabsCollection.shift();
}
this.recentActivityView = new RecentActivityView({ this.recentActivityView = new RecentActivityView({
el: this.el, el: this.el,
...@@ -34,20 +36,23 @@ define([ ...@@ -34,20 +36,23 @@ define([
scrollToTag: scrollToTag scrollToTag: scrollToTag
}); });
this.courseStructureView = new CourseStructureView({ if (!_.contains(this.options.disabledTabs, 'course_structure')) {
el: this.el, this.courseStructureView = new CourseStructureView({
collection: this.collection, el: this.el,
tabsCollection: this.tabsCollection, collection: this.collection,
scrollToTag: scrollToTag tabsCollection: this.tabsCollection,
}); scrollToTag: scrollToTag
});
// Add the Tags model after the Course Structure model. // Add the Tags model after the Course Structure model.
this.tabsCollection.push(tagsModel); this.tabsCollection.push(tagsModel);
}
this.searchResultsView = new SearchResultsView({ this.searchResultsView = new SearchResultsView({
el: this.el, el: this.el,
tabsCollection: this.tabsCollection, tabsCollection: this.tabsCollection,
debug: this.options.debug, debug: this.options.debug,
perPage: this.options.perPage,
createTabOnInitialization: false, createTabOnInitialization: false,
scrollToTag: scrollToTag scrollToTag: scrollToTag
}); });
......
...@@ -6,19 +6,30 @@ define([ ...@@ -6,19 +6,30 @@ define([
/** /**
* Factory method for the Notes page. * Factory method for the Notes page.
* @param {Object} params Params for the Notes page. * @param {Object} params Params for the Notes page.
* @param {Array} params.notesList A list of note models. * @param {List} params.disabledTabs Names of disabled tabs, these tabs will not be shown.
* @param {Object} params.notes Paginated notes info.
* @param {Number} params.pageSize Number of notes per page.
* @param {Boolean} params.debugMode Enable the flag to see debug information. * @param {Boolean} params.debugMode Enable the flag to see debug information.
* @param {String} params.endpoint The endpoint of the store. * @param {String} params.endpoint The endpoint of the store.
* @return {Object} An instance of NotesPageView. * @return {Object} An instance of NotesPageView.
*/ */
return function (params) { return function (params) {
var collection = new NotesCollection(params.notesList); var collection = new NotesCollection(
params.notes,
{
url: params.notesEndpoint,
perPage: params.pageSize,
parse: true
}
);
return new NotesPageView({ return new NotesPageView({
el: $('.wrapper-student-notes').get(0), el: $('.wrapper-student-notes').get(0),
collection: collection, collection: collection,
debug: params.debugMode, debug: params.debugMode,
endpoint: params.endpoint endpoint: params.endpoint,
perPage: params.pageSize,
disabledTabs: params.disabledTabs
}); });
}; };
}); });
......
...@@ -43,15 +43,12 @@ define([ ...@@ -43,15 +43,12 @@ define([
* @return {Array} * @return {Array}
*/ */
prepareData: function (data) { prepareData: function (data) {
var collection; if (!(data && _.has(data, 'count') && _.has(data, 'results'))) {
if (!(data && _.has(data, 'total') && _.has(data, 'rows'))) {
this.logger.log('Wrong data', data, this.searchQuery); this.logger.log('Wrong data', data, this.searchQuery);
return null; return null;
} }
collection = new NotesCollection(data.rows); return [this.collection, this.searchQuery];
return [collection, data.total, this.searchQuery];
}, },
/** /**
...@@ -99,8 +96,8 @@ define([ ...@@ -99,8 +96,8 @@ define([
if (args) { if (args) {
this.options.search.apply(this, args); this.options.search.apply(this, args);
this.logger.emit('edx.course.student_notes.searched', { this.logger.emit('edx.course.student_notes.searched', {
'number_of_results': args[1], 'number_of_results': args[0].totalCount,
'search_string': args[2] 'search_string': args[1]
}); });
} else { } else {
this.options.error(this.errorMessage, this.searchQuery); this.options.error(this.errorMessage, this.searchQuery);
...@@ -144,15 +141,15 @@ define([ ...@@ -144,15 +141,15 @@ define([
* @return {jQuery.Deferred} * @return {jQuery.Deferred}
*/ */
sendRequest: function (text) { sendRequest: function (text) {
var settings = { this.collection = new NotesCollection(
url: this.el.action, [],
type: this.el.method, {
dataType: 'json', text: text,
data: {text: text} perPage: this.options.perPage,
}; url: this.el.action
}
this.logger.log(settings); );
return $.ajax(settings); return this.collection.goTo(1);
} }
}); });
......
;(function (define, undefined) { ;(function (define, undefined) {
'use strict'; 'use strict';
define(['gettext', 'underscore', 'backbone', 'js/edxnotes/views/note_item'], define(['gettext', 'underscore', 'backbone', 'js/edxnotes/views/note_item',
function (gettext, _, Backbone, NoteItemView) { 'common/js/components/views/paging_header', 'common/js/components/views/paging_footer'],
function (gettext, _, Backbone, NoteItemView, PagingHeaderView, PagingFooterView) {
var TabPanelView = Backbone.View.extend({ var TabPanelView = Backbone.View.extend({
tagName: 'section', tagName: 'section',
className: 'tab-panel', className: 'tab-panel',
...@@ -13,14 +14,30 @@ function (gettext, _, Backbone, NoteItemView) { ...@@ -13,14 +14,30 @@ function (gettext, _, Backbone, NoteItemView) {
initialize: function () { initialize: function () {
this.children = []; this.children = [];
if (this.options.createHeaderFooter) {
this.pagingHeaderView = new PagingHeaderView({collection: this.collection});
this.pagingFooterView = new PagingFooterView({collection: this.collection});
}
if (this.hasOwnProperty('collection')) {
this.listenTo(this.collection, 'page_changed', this.render);
}
}, },
render: function () { render: function () {
this.$el.html(this.getTitle()); this.$el.html(this.getTitle());
this.renderView(this.pagingHeaderView);
this.renderContent(); this.renderContent();
this.renderView(this.pagingFooterView);
return this; return this;
}, },
renderView: function(view) {
if (this.options.createHeaderFooter && this.collection.models.length) {
this.$el.append(view.render().el);
view.delegateEvents();
}
},
renderContent: function () { renderContent: function () {
return this; return this;
}, },
......
...@@ -14,7 +14,8 @@ define([ ...@@ -14,7 +14,8 @@ define([
initialize: function (options) { initialize: function (options) {
_.bindAll(this, 'showLoadingIndicator', 'hideLoadingIndicator'); _.bindAll(this, 'showLoadingIndicator', 'hideLoadingIndicator');
this.options = _.defaults(options || {}, { this.options = _.defaults(options || {}, {
createTabOnInitialization: true createTabOnInitialization: true,
createHeaderFooter: true
}); });
if (this.options.createTabOnInitialization) { if (this.options.createTabOnInitialization) {
...@@ -64,7 +65,13 @@ define([ ...@@ -64,7 +65,13 @@ define([
getSubView: function () { getSubView: function () {
var collection = this.getCollection(); var collection = this.getCollection();
return new this.PanelConstructor({collection: collection, scrollToTag: this.options.scrollToTag}); return new this.PanelConstructor(
{
collection: collection,
scrollToTag: this.options.scrollToTag,
createHeaderFooter: this.options.createHeaderFooter
}
);
}, },
destroySubView: function () { destroySubView: function () {
......
...@@ -58,6 +58,7 @@ define([ ...@@ -58,6 +58,7 @@ define([
this.searchBox = new SearchBoxView({ this.searchBox = new SearchBoxView({
el: document.getElementById('search-notes-form'), el: document.getElementById('search-notes-form'),
debug: this.options.debug, debug: this.options.debug,
perPage: this.options.perPage,
beforeSearchStart: this.onBeforeSearchStart, beforeSearchStart: this.onBeforeSearchStart,
search: this.onSearch, search: this.onSearch,
error: this.onSearchError error: this.onSearchError
...@@ -81,7 +82,8 @@ define([ ...@@ -81,7 +82,8 @@ define([
return new this.PanelConstructor({ return new this.PanelConstructor({
collection: collection, collection: collection,
searchQuery: this.searchResults.searchQuery, searchQuery: this.searchResults.searchQuery,
scrollToTag: this.options.scrollToTag scrollToTag: this.options.scrollToTag,
createHeaderFooter: this.options.createHeaderFooter
}); });
} else { } else {
return new this.NoResultsViewConstructor({ return new this.NoResultsViewConstructor({
...@@ -122,10 +124,9 @@ define([ ...@@ -122,10 +124,9 @@ define([
} }
}, },
onSearch: function (collection, total, searchQuery) { onSearch: function (collection, searchQuery) {
this.searchResults = { this.searchResults = {
collection: collection, collection: collection,
total: total,
searchQuery: searchQuery searchQuery: searchQuery
}; };
......
...@@ -6,7 +6,7 @@ define([ ...@@ -6,7 +6,7 @@ define([
var notes = Helpers.getDefaultNotes(); var notes = Helpers.getDefaultNotes();
beforeEach(function () { beforeEach(function () {
this.collection = new NotesCollection(notes); this.collection = new NotesCollection(notes, {perPage: 10, parse: true});
}); });
it('can return correct course structure', function () { it('can return correct course structure', function () {
...@@ -23,11 +23,22 @@ define([ ...@@ -23,11 +23,22 @@ define([
'i4x://section/2': Helpers.getSection('First Section', 2, [3]) 'i4x://section/2': Helpers.getSection('First Section', 2, [3])
}); });
expect(structure.units).toEqual({ var compareUnits = function (structureUnits, collectionUnits) {
expect(structureUnits.length === collectionUnits.length).toBeTruthy();
for(var i = 0; i < structureUnits.length; i++) {
expect(structureUnits[i].attributes).toEqual(collectionUnits[i].attributes);
}
};
var units = {
'i4x://unit/0': [this.collection.at(0), this.collection.at(1)], 'i4x://unit/0': [this.collection.at(0), this.collection.at(1)],
'i4x://unit/1': [this.collection.at(2)], 'i4x://unit/1': [this.collection.at(2)],
'i4x://unit/2': [this.collection.at(3)], 'i4x://unit/2': [this.collection.at(3)],
'i4x://unit/3': [this.collection.at(4)] 'i4x://unit/3': [this.collection.at(4)]
};
_.each(units, function(value, key){
compareUnits(structure.units[key], value);
}); });
}); });
}); });
......
define(['underscore'], function(_) { define(['underscore', 'URI', 'common/js/spec_helpers/ajax_helpers'], function(_, URI, AjaxHelpers) {
'use strict'; 'use strict';
var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
LONG_TEXT, PRUNED_TEXT, TRUNCATED_TEXT, SHORT_TEXT, LONG_TEXT, PRUNED_TEXT, TRUNCATED_TEXT, SHORT_TEXT,
base64Encode, makeToken, getChapter, getSection, getUnit, getDefaultNotes; base64Encode, makeToken, getChapter, getSection, getUnit, getDefaultNotes,
verifyUrl, verifyRequestParams, createNotesData, respondToRequest,
verifyPaginationInfo, verifyPageData;
LONG_TEXT = [ LONG_TEXT = [
'Adipisicing elit, sed do eiusmod tempor incididunt ', 'Adipisicing elit, sed do eiusmod tempor incididunt ',
...@@ -106,57 +108,131 @@ define(['underscore'], function(_) { ...@@ -106,57 +108,131 @@ define(['underscore'], function(_) {
getDefaultNotes = function () { getDefaultNotes = function () {
// Note that the server returns notes in reverse chronological order (newest first). // Note that the server returns notes in reverse chronological order (newest first).
return [ return {
{ 'count': 5,
chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]), 'current_page': 1,
section: getSection('Third Section', 0, ['w_n', 1, 0]), 'num_pages': 1,
unit: getUnit('Fourth Unit', 0), 'start': 0,
created: 'December 11, 2014 at 11:12AM', 'next': null,
updated: 'December 11, 2014 at 11:12AM', 'previous': null,
text: 'Third added model', 'results': [
quote: 'Note 4', {
tags: ['Pumpkin', 'pumpkin', 'yummy'] chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]),
}, section: getSection('Third Section', 0, ['w_n', 1, 0]),
{ unit: getUnit('Fourth Unit', 0),
chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]), created: 'December 11, 2014 at 11:12AM',
section: getSection('Third Section', 0, ['w_n', 1, 0]), updated: 'December 11, 2014 at 11:12AM',
unit: getUnit('Fourth Unit', 0), text: 'Third added model',
created: 'December 11, 2014 at 11:11AM', quote: 'Note 4',
updated: 'December 11, 2014 at 11:11AM', tags: ['Pumpkin', 'pumpkin', 'yummy']
text: 'Third added model', },
quote: 'Note 5' {
}, chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]),
{ section: getSection('Third Section', 0, ['w_n', 1, 0]),
chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]), unit: getUnit('Fourth Unit', 0),
section: getSection('Third Section', 0, ['w_n', 1, 0]), created: 'December 11, 2014 at 11:11AM',
unit: getUnit('Third Unit', 1), updated: 'December 11, 2014 at 11:11AM',
created: 'December 11, 2014 at 11:11AM', text: 'Third added model',
updated: 'December 11, 2014 at 11:11AM', quote: 'Note 5'
text: 'Second added model', },
quote: 'Note 3', {
tags: ['yummy'] chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]),
}, section: getSection('Third Section', 0, ['w_n', 1, 0]),
{ unit: getUnit('Third Unit', 1),
chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]), created: 'December 11, 2014 at 11:11AM',
section: getSection('Second Section', 1, [2]), updated: 'December 11, 2014 at 11:11AM',
unit: getUnit('Second Unit', 2), text: 'Second added model',
created: 'December 11, 2014 at 11:10AM', quote: 'Note 3',
updated: 'December 11, 2014 at 11:10AM', tags: ['yummy']
text: 'First added model', },
quote: 'Note 2', {
tags: ['PUMPKIN', 'pie'] chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]),
}, section: getSection('Second Section', 1, [2]),
{ unit: getUnit('Second Unit', 2),
chapter: getChapter('First Chapter', 1, 0, [2]), created: 'December 11, 2014 at 11:10AM',
section: getSection('First Section', 2, [3]), updated: 'December 11, 2014 at 11:10AM',
unit: getUnit('First Unit', 3), text: 'First added model',
created: 'December 11, 2014 at 11:10AM', quote: 'Note 2',
updated: 'December 11, 2014 at 11:10AM', tags: ['PUMPKIN', 'pie']
text: 'First added model', },
quote: 'Note 1', {
tags: ['pie', 'pumpkin'] chapter: getChapter('First Chapter', 1, 0, [2]),
} section: getSection('First Section', 2, [3]),
]; unit: getUnit('First Unit', 3),
created: 'December 11, 2014 at 11:10AM',
updated: 'December 11, 2014 at 11:10AM',
text: 'First added model',
quote: 'Note 1',
tags: ['pie', 'pumpkin']
}
]
};
};
verifyUrl = function (requestUrl, expectedUrl, expectedParams) {
expect(requestUrl.slice(0, expectedUrl.length) === expectedUrl).toBeTruthy();
verifyRequestParams(requestUrl, expectedParams);
};
verifyRequestParams = function (requestUrl, expectedParams) {
var urlParams = (new URI(requestUrl)).query(true);
_.each(expectedParams, function (value, key) {
expect(urlParams[key]).toBe(value);
});
};
createNotesData = function (options) {
var data = {
count: options.count || 0,
num_pages: options.num_pages || 1,
current_page: options.current_page || 1,
start: options.start || 0,
results: []
};
for(var i = 0; i < options.numNotesToCreate; i++) {
var notesInfo = {
chapter: getChapter('First Chapter__' + i, 1, 0, [2]),
section: getSection('First Section__' + i, 2, [3]),
unit: getUnit('First Unit__' + i, 3),
created: new Date().toISOString(),
updated: new Date().toISOString(),
text: 'text__' + i,
quote: 'Note__' + i,
tags: ['tag__' + i, 'tag__' + i+1]
};
data.results.push(notesInfo);
}
return data;
};
respondToRequest = function(requests, responseJson, respondToEvent) {
// Respond to the analytics event
if (respondToEvent) {
AjaxHelpers.respondWithNoContent(requests);
}
// Now process the actual request
AjaxHelpers.respondWithJson(requests, responseJson);
};
verifyPaginationInfo = function (view, headerMessage, currentPage, totalPages) {
expect(view.$('.search-count.listing-count').text().trim()).toBe(headerMessage);
expect(parseInt(view.$('.pagination span.current-page').text().trim())).toBe(currentPage);
expect(parseInt(view.$('.pagination span.total-pages').text().trim())).toBe(totalPages);
};
verifyPageData = function (view, tabsCollection, tabInfo, tabId, notes) {
expect(tabsCollection).toHaveLength(1);
expect(tabsCollection.at(0).toJSON()).toEqual(tabInfo);
expect(view.$(tabId)).toExist();
expect(view.$('.note')).toHaveLength(notes.results.length);
_.each(view.$('.note'), function(element, index) {
expect($('.note-comments', element)).toContainText(notes.results[index].text);
expect($('.note-excerpt', element)).toContainText(notes.results[index].quote);
});
}; };
return { return {
...@@ -169,6 +245,12 @@ define(['underscore'], function(_) { ...@@ -169,6 +245,12 @@ define(['underscore'], function(_) {
getChapter: getChapter, getChapter: getChapter,
getSection: getSection, getSection: getSection,
getUnit: getUnit, getUnit: getUnit,
getDefaultNotes: getDefaultNotes getDefaultNotes: getDefaultNotes,
verifyUrl: verifyUrl,
verifyRequestParams: verifyRequestParams,
createNotesData: createNotesData,
respondToRequest: respondToRequest,
verifyPaginationInfo: verifyPaginationInfo,
verifyPageData: verifyPageData
}; };
}); });
...@@ -4,10 +4,23 @@ define([ ...@@ -4,10 +4,23 @@ define([
'use strict'; 'use strict';
describe('EdxNotes NoteModel', function() { describe('EdxNotes NoteModel', function() {
beforeEach(function () { beforeEach(function () {
this.collection = new NotesCollection([ this.collection = new NotesCollection(
{quote: Helpers.LONG_TEXT, text: 'text\n with\r\nline\n\rbreaks \r'}, {
{quote: Helpers.SHORT_TEXT, text: 'text\n with\r\nline\n\rbreaks \r'} 'count': 2,
]); 'current_page': 1,
'num_pages': 1,
'start': 0,
'next': null,
'previous': null,
'results': [
{quote: Helpers.LONG_TEXT, text: 'text\n with\r\nline\n\rbreaks \r'},
{quote: Helpers.SHORT_TEXT, text: 'text\n with\r\nline\n\rbreaks \r'}
]
},
{
perPage: 10, parse: true
}
);
}); });
it('has correct values on initialization', function () { it('has correct values on initialization', function () {
......
...@@ -67,12 +67,12 @@ define([ ...@@ -67,12 +67,12 @@ define([
var view = getView({tags: ["First", "Second"]}); var view = getView({tags: ["First", "Second"]});
expect(view.$('.reference-title').length).toBe(3); expect(view.$('.reference-title').length).toBe(3);
expect(view.$('.reference-title')[2]).toContainText('Tags:'); expect(view.$('.reference-title')[2]).toContainText('Tags:');
expect(view.$('a.reference-tags').length).toBe(2); expect(view.$('span.reference-tags').length).toBe(2);
expect(view.$('a.reference-tags')[0]).toContainText('First'); expect(view.$('span.reference-tags')[0]).toContainText('First');
expect(view.$('a.reference-tags')[1]).toContainText('Second'); expect(view.$('span.reference-tags')[1]).toContainText('Second');
}); });
it('should handle a click event on the tag', function() { xit('should handle a click event on the tag', function() {
var scrollToTagSpy = { var scrollToTagSpy = {
scrollToTag: function (tagName){} scrollToTag: function (tagName){}
}; };
......
...@@ -13,7 +13,7 @@ define([ ...@@ -13,7 +13,7 @@ define([
TemplateHelpers.installTemplates([ TemplateHelpers.installTemplates([
'templates/edxnotes/note-item', 'templates/edxnotes/tab-item' 'templates/edxnotes/note-item', 'templates/edxnotes/tab-item'
]); ]);
this.view = new NotesFactory({notesList: notes}); this.view = new NotesFactory({notes: notes, pageSize: 10});
}); });
...@@ -35,8 +35,13 @@ define([ ...@@ -35,8 +35,13 @@ define([
this.view.$('.search-notes-input').val('test_query'); this.view.$('.search-notes-input').val('test_query');
this.view.$('.search-notes-submit').click(); this.view.$('.search-notes-submit').click();
AjaxHelpers.respondWithJson(requests, { AjaxHelpers.respondWithJson(requests, {
total: 0, 'count': 0,
rows: [] 'current_page': 1,
'num_pages': 1,
'start': 0,
'next': null,
'previous': null,
'results': []
}); });
expect(this.view.$('#view-search-results')).toHaveClass('is-active'); expect(this.view.$('#view-search-results')).toHaveClass('is-active');
expect(this.view.$('#view-recent-activity')).toExist(); expect(this.view.$('#view-recent-activity')).toExist();
......
define([ define([
'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers', 'js/edxnotes/views/search_box', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers', 'js/edxnotes/views/search_box',
'js/edxnotes/collections/notes', 'js/spec/edxnotes/custom_matchers', 'jasmine-jquery' 'js/edxnotes/collections/notes', 'js/spec/edxnotes/custom_matchers', 'js/spec/edxnotes/helpers', 'jasmine-jquery'
], function($, _, AjaxHelpers, SearchBoxView, NotesCollection, customMatchers) { ], function($, _, AjaxHelpers, SearchBoxView, NotesCollection, customMatchers, Helpers) {
'use strict'; 'use strict';
describe('EdxNotes SearchBoxView', function() { describe('EdxNotes SearchBoxView', function() {
var getSearchBox, submitForm, assertBoxIsEnabled, assertBoxIsDisabled; var getSearchBox, submitForm, assertBoxIsEnabled, assertBoxIsDisabled, searchResponse;
searchResponse = {
'count': 2,
'current_page': 1,
'num_pages': 1,
'start': 0,
'next': null,
'previous': null,
'results': [null, null]
};
getSearchBox = function (options) { getSearchBox = function (options) {
options = _.defaults(options || {}, { options = _.defaults(options || {}, {
el: $('#search-notes-form').get(0), el: $('#search-notes-form').get(0),
perPage: 10,
beforeSearchStart: jasmine.createSpy(), beforeSearchStart: jasmine.createSpy(),
search: jasmine.createSpy(), search: jasmine.createSpy(),
error: jasmine.createSpy(), error: jasmine.createSpy(),
...@@ -50,7 +61,11 @@ define([ ...@@ -50,7 +61,11 @@ define([
submitForm(this.searchBox, 'test_text'); submitForm(this.searchBox, 'test_text');
request = requests[0]; request = requests[0];
expect(request.method).toBe(form.method.toUpperCase()); expect(request.method).toBe(form.method.toUpperCase());
expect(request.url).toBe(form.action + '?' + $.param({text: 'test_text'})); Helpers.verifyUrl(
request.url,
form.action,
{text: 'test_text', page: '1', page_size: '10'}
);
}); });
it('returns success result', function () { it('returns success result', function () {
...@@ -60,13 +75,10 @@ define([ ...@@ -60,13 +75,10 @@ define([
'test_text' 'test_text'
); );
assertBoxIsDisabled(this.searchBox); assertBoxIsDisabled(this.searchBox);
AjaxHelpers.respondWithJson(requests, { AjaxHelpers.respondWithJson(requests, searchResponse);
total: 2,
rows: [null, null]
});
assertBoxIsEnabled(this.searchBox); assertBoxIsEnabled(this.searchBox);
expect(this.searchBox.options.search).toHaveBeenCalledWith( expect(this.searchBox.options.search).toHaveBeenCalledWith(
jasmine.any(NotesCollection), 2, 'test_text' jasmine.any(NotesCollection), 'test_text'
); );
expect(this.searchBox.options.complete).toHaveBeenCalledWith( expect(this.searchBox.options.complete).toHaveBeenCalledWith(
'test_text' 'test_text'
...@@ -76,10 +88,7 @@ define([ ...@@ -76,10 +88,7 @@ define([
it('should log the edx.course.student_notes.searched event properly', function () { it('should log the edx.course.student_notes.searched event properly', function () {
var requests = AjaxHelpers.requests(this); var requests = AjaxHelpers.requests(this);
submitForm(this.searchBox, 'test_text'); submitForm(this.searchBox, 'test_text');
AjaxHelpers.respondWithJson(requests, { AjaxHelpers.respondWithJson(requests, searchResponse);
total: 2,
rows: [null, null]
});
expect(Logger.log).toHaveBeenCalledWith('edx.course.student_notes.searched', { expect(Logger.log).toHaveBeenCalledWith('edx.course.student_notes.searched', {
'number_of_results': 2, 'number_of_results': 2,
...@@ -140,10 +149,7 @@ define([ ...@@ -140,10 +149,7 @@ define([
submitForm(this.searchBox, 'test_text'); submitForm(this.searchBox, 'test_text');
assertBoxIsDisabled(this.searchBox); assertBoxIsDisabled(this.searchBox);
submitForm(this.searchBox, 'another_text'); submitForm(this.searchBox, 'another_text');
AjaxHelpers.respondWithJson(requests, { AjaxHelpers.respondWithJson(requests, searchResponse);
total: 2,
rows: [null, null]
});
assertBoxIsEnabled(this.searchBox); assertBoxIsEnabled(this.searchBox);
expect(requests).toHaveLength(1); expect(requests).toHaveLength(1);
}); });
......
...@@ -40,7 +40,7 @@ define([ ...@@ -40,7 +40,7 @@ define([
'templates/edxnotes/note-item', 'templates/edxnotes/tab-item' 'templates/edxnotes/note-item', 'templates/edxnotes/tab-item'
]); ]);
this.collection = new NotesCollection(notes); this.collection = new NotesCollection(notes, {perPage: 10, parse: true});
this.tabsCollection = new TabsCollection(); this.tabsCollection = new TabsCollection();
}); });
......
define([ define([
'jquery', 'common/js/spec_helpers/template_helpers', 'js/edxnotes/collections/notes', 'jquery', 'common/js/spec_helpers/template_helpers', 'common/js/spec_helpers/ajax_helpers',
'js/edxnotes/collections/tabs', 'js/edxnotes/views/tabs/recent_activity', 'js/edxnotes/collections/notes', 'js/edxnotes/collections/tabs', 'js/edxnotes/views/tabs/recent_activity',
'js/spec/edxnotes/custom_matchers', 'jasmine-jquery' 'js/spec/edxnotes/custom_matchers', 'js/spec/edxnotes/helpers', 'jasmine-jquery'
], function( ], function(
$, TemplateHelpers, NotesCollection, TabsCollection, RecentActivityView, customMatchers $, TemplateHelpers, AjaxHelpers, NotesCollection, TabsCollection, RecentActivityView, customMatchers, Helpers
) { ) {
'use strict'; 'use strict';
describe('EdxNotes RecentActivityView', function() { describe('EdxNotes RecentActivityView', function() {
var notes = [ var notes = {
{ 'count': 3,
created: 'December 11, 2014 at 11:12AM', 'current_page': 1,
updated: 'December 11, 2014 at 11:12AM', 'num_pages': 1,
text: 'Third added model', 'start': 0,
quote: 'Should be listed first' 'next': null,
}, 'previous': null,
{ 'results': [
created: 'December 11, 2014 at 11:11AM', {
updated: 'December 11, 2014 at 11:11AM', created: 'December 11, 2014 at 11:12AM',
text: 'Second added model', updated: 'December 11, 2014 at 11:12AM',
quote: 'Should be listed second' text: 'Third added model',
}, quote: 'Should be listed first'
{ },
created: 'December 11, 2014 at 11:10AM', {
updated: 'December 11, 2014 at 11:10AM', created: 'December 11, 2014 at 11:11AM',
text: 'First added model', updated: 'December 11, 2014 at 11:11AM',
quote: 'Should be listed third' text: 'Second added model',
} quote: 'Should be listed second'
], getView; },
{
created: 'December 11, 2014 at 11:10AM',
updated: 'December 11, 2014 at 11:10AM',
text: 'First added model',
quote: 'Should be listed third'
}
]
}, getView, tabInfo, recentActivityTabId;
getView = function (collection, tabsCollection, options) { getView = function (collection, tabsCollection, options) {
var view; var view;
...@@ -35,6 +43,7 @@ define([ ...@@ -35,6 +43,7 @@ define([
el: $('.wrapper-student-notes'), el: $('.wrapper-student-notes'),
collection: collection, collection: collection,
tabsCollection: tabsCollection, tabsCollection: tabsCollection,
createHeaderFooter: true
}); });
view = new RecentActivityView(options); view = new RecentActivityView(options);
...@@ -43,6 +52,17 @@ define([ ...@@ -43,6 +52,17 @@ define([
return view; return view;
}; };
tabInfo = {
name: 'Recent Activity',
identifier: 'view-recent-activity',
icon: 'fa fa-clock-o',
is_active: true,
is_closable: false,
view: 'Recent Activity'
};
recentActivityTabId = '#recent-panel';
beforeEach(function () { beforeEach(function () {
customMatchers(this); customMatchers(this);
loadFixtures('js/fixtures/edxnotes/edxnotes.html'); loadFixtures('js/fixtures/edxnotes/edxnotes.html');
...@@ -50,28 +70,135 @@ define([ ...@@ -50,28 +70,135 @@ define([
'templates/edxnotes/note-item', 'templates/edxnotes/tab-item' 'templates/edxnotes/note-item', 'templates/edxnotes/tab-item'
]); ]);
this.collection = new NotesCollection(notes); this.collection = new NotesCollection(notes, {perPage: 10, parse: true});
this.tabsCollection = new TabsCollection(); this.tabsCollection = new TabsCollection();
}); });
it('displays a tab and content with proper data and order', function () { it('displays a tab and content with proper data and order', function () {
var view = getView(this.collection, this.tabsCollection); var view = getView(this.collection, this.tabsCollection);
Helpers.verifyPaginationInfo(view, "Showing 1-3 out of 3 total", 1, 1);
Helpers.verifyPageData(view, this.tabsCollection, tabInfo, recentActivityTabId, notes);
});
expect(this.tabsCollection).toHaveLength(1); it("will not render header and footer if there are no notes", function () {
expect(this.tabsCollection.at(0).toJSON()).toEqual({ var notes = {
name: 'Recent Activity', 'count': 0,
identifier: 'view-recent-activity', 'current_page': 1,
icon: 'fa fa-clock-o', 'num_pages': 1,
is_active: true, 'start': 0,
is_closable: false, 'next': null,
view: 'Recent Activity' 'previous': null,
}); 'results': []
expect(view.$('#recent-panel')).toExist(); };
expect(view.$('.note')).toHaveLength(3); var collection = new NotesCollection(notes, {perPage: 10, parse: true});
_.each(view.$('.note'), function(element, index) { var view = getView(collection, this.tabsCollection);
expect($('.note-comments', element)).toContainText(notes[index].text); expect(view.$('.search-tools.listing-tools')).toHaveLength(0);
expect($('.note-excerpt', element)).toContainText(notes[index].quote); expect(view.$('.pagination.pagination-full.bottom')).toHaveLength(0);
}); });
it("can go to a page number", function () {
var requests = AjaxHelpers.requests(this);
var notes = Helpers.createNotesData(
{
numNotesToCreate: 10,
count: 12,
num_pages: 2,
current_page: 1,
start: 0
}
);
var collection = new NotesCollection(notes, {perPage: 10, parse: true});
var view = getView(collection, this.tabsCollection);
Helpers.verifyPaginationInfo(view, "Showing 1-10 out of 12 total", 1, 2);
Helpers.verifyPageData(view, this.tabsCollection, tabInfo, recentActivityTabId, notes);
view.$('input#page-number-input').val('2');
view.$('input#page-number-input').trigger('change');
Helpers.verifyRequestParams(
requests[requests.length - 1].url,
{page: '2', page_size: '10'}
);
notes = Helpers.createNotesData(
{
numNotesToCreate: 2,
count: 12,
num_pages: 2,
current_page: 2,
start: 10
}
);
Helpers.respondToRequest(requests, notes, true);
Helpers.verifyPaginationInfo(view, "Showing 11-12 out of 12 total", 2, 2);
Helpers.verifyPageData(view, this.tabsCollection, tabInfo, recentActivityTabId, notes);
});
it("can navigate forward and backward", function () {
var requests = AjaxHelpers.requests(this);
var page1Notes = Helpers.createNotesData(
{
numNotesToCreate: 10,
count: 15,
num_pages: 2,
current_page: 1,
start: 0
}
);
var collection = new NotesCollection(page1Notes, {perPage: 10, parse: true});
var view = getView(collection, this.tabsCollection);
Helpers.verifyPaginationInfo(view, "Showing 1-10 out of 15 total", 1, 2);
Helpers.verifyPageData(view, this.tabsCollection, tabInfo, recentActivityTabId, page1Notes);
view.$('.pagination .next-page-link').click();
Helpers.verifyRequestParams(
requests[requests.length - 1].url,
{page: '2', page_size: '10'}
);
var page2Notes = Helpers.createNotesData(
{
numNotesToCreate: 5,
count: 15,
num_pages: 2,
current_page: 2,
start: 10
}
);
Helpers.respondToRequest(requests, page2Notes, true);
Helpers.verifyPaginationInfo(view, "Showing 11-15 out of 15 total", 2, 2);
Helpers.verifyPageData(view, this.tabsCollection, tabInfo, recentActivityTabId, page2Notes);
view.$('.pagination .previous-page-link').click();
Helpers.verifyRequestParams(
requests[requests.length - 1].url,
{page: '1', page_size: '10'}
);
Helpers.respondToRequest(requests, page1Notes);
Helpers.verifyPaginationInfo(view, "Showing 1-10 out of 15 total", 1, 2);
Helpers.verifyPageData(view, this.tabsCollection, tabInfo, recentActivityTabId, page1Notes);
});
it("sends correct page size value", function () {
var requests = AjaxHelpers.requests(this);
var notes = Helpers.createNotesData(
{
numNotesToCreate: 5,
count: 7,
num_pages: 2,
current_page: 1,
start: 0
}
);
var collection = new NotesCollection(notes, {perPage: 5, parse: true});
var view = getView(collection, this.tabsCollection);
view.$('.pagination .next-page-link').click();
Helpers.verifyRequestParams(
requests[requests.length - 1].url,
{page: '2', page_size: '5'}
);
}); });
}); });
}); });
...@@ -44,7 +44,7 @@ define([ ...@@ -44,7 +44,7 @@ define([
'templates/edxnotes/note-item', 'templates/edxnotes/tab-item' 'templates/edxnotes/note-item', 'templates/edxnotes/tab-item'
]); ]);
this.collection = new NotesCollection(notes); this.collection = new NotesCollection(notes, {perPage: 10, parse: true});
this.tabsCollection = new TabsCollection(); this.tabsCollection = new TabsCollection();
}); });
......
...@@ -235,27 +235,24 @@ $divider-visual-tertiary: ($baseline/20) solid $gray-l4; ...@@ -235,27 +235,24 @@ $divider-visual-tertiary: ($baseline/20) solid $gray-l4;
// CASE: tag matches a search query // CASE: tag matches a search query
.reference-meta.reference-tags .note-highlight { .reference-meta.reference-tags .note-highlight {
// needed because .note-highlight is a span, which overrides the color
@extend %shame-link-text;
background-color: $result-highlight-color-base; background-color: $result-highlight-color-base;
} }
// Put commas between tags. // Put commas between tags.
a.reference-meta.reference-tags:after { span.reference-meta.reference-tags:after {
content: ","; content: ",";
color: $m-gray-d2; color: $m-gray-d2;
} }
// But not after the last tag. // But not after the last tag.
a.reference-meta.reference-tags:last-child:after { span.reference-meta.reference-tags:last-child:after {
content: ""; content: "";
} }
// needed for poor base LMS styling scope // needed for poor base LMS styling scope
a.reference-meta { a.reference-meta {
@extend %shame-link-text; @extend %shame-link-text;
} }
} }
} }
...@@ -285,6 +282,15 @@ $divider-visual-tertiary: ($baseline/20) solid $gray-l4; ...@@ -285,6 +282,15 @@ $divider-visual-tertiary: ($baseline/20) solid $gray-l4;
.tab-panel, .inline-error, .ui-loading { .tab-panel, .inline-error, .ui-loading {
@extend %no-outline; @extend %no-outline;
border-top: $divider-visual-primary;
.listing-tools {
@include margin($baseline $baseline (-$baseline/2) 0);
}
.note-group:first-of-type {
border-top: none;
}
} }
.tab-panel.note-group { .tab-panel.note-group {
......
...@@ -12,6 +12,10 @@ import json ...@@ -12,6 +12,10 @@ import json
</%block> </%block>
<%include file="/courseware/course_navigation.html" args="active_page='edxnotes'" /> <%include file="/courseware/course_navigation.html" args="active_page='edxnotes'" />
<%
_notes = json.loads(notes)
has_notes = _notes and _notes.get('results')
%>
<section class="container"> <section class="container">
<div class="wrapper-student-notes"> <div class="wrapper-student-notes">
<div class="student-notes"> <div class="student-notes">
...@@ -24,7 +28,7 @@ import json ...@@ -24,7 +28,7 @@ import json
</h1> </h1>
</div> </div>
% if notes: % if has_notes:
<div class="wrapper-notes-search"> <div class="wrapper-notes-search">
<form role="search" action="${notes_endpoint}" method="GET" id="search-notes-form" class="is-hidden"> <form role="search" action="${notes_endpoint}" method="GET" id="search-notes-form" class="is-hidden">
<label for="search-notes-input" class="sr">${_('Search notes for:')}</label> <label for="search-notes-input" class="sr">${_('Search notes for:')}</label>
...@@ -51,7 +55,7 @@ import json ...@@ -51,7 +55,7 @@ import json
<h2 id="tab-view" class="tabs-label">${_('View notes by:')}</h2> <h2 id="tab-view" class="tabs-label">${_('View notes by:')}</h2>
</div> </div>
% if notes: % if has_notes:
<div class="ui-loading" tabindex="-1"> <div class="ui-loading" tabindex="-1">
<span class="spin"> <span class="spin">
<i class="icon fa fa-refresh"></i> <i class="icon fa fa-refresh"></i>
...@@ -103,11 +107,14 @@ import json ...@@ -103,11 +107,14 @@ import json
% endfor % endfor
</%block> </%block>
% if notes: % if has_notes:
<%block name="js_extra"> <%block name="js_extra">
<%static:require_module module_name="js/edxnotes/views/page_factory" class_name="NotesPageFactory"> <%static:require_module module_name="js/edxnotes/views/page_factory" class_name="NotesPageFactory">
NotesPageFactory({ NotesPageFactory({
notesList: ${notes if notes is not None else []}, disabledTabs: ${disabled_tabs},
notes: ${notes},
notesEndpoint: '${notes_endpoint}',
pageSize: '${page_size}',
debugMode: ${debug} debugMode: ${debug}
}); });
</%static:require_module> </%static:require_module>
......
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
<% if (tags.length > 0) { %> <% if (tags.length > 0) { %>
<p class="reference-title"><%- gettext("Tags:") %></p> <p class="reference-title"><%- gettext("Tags:") %></p>
<% for (var i = 0; i < tags.length; i++) { %> <% for (var i = 0; i < tags.length; i++) { %>
<a class="reference-meta reference-tags" href="#"><%= tags[i] %></a> <span class="reference-meta reference-tags"><%= tags[i] %></span>
<% } %> <% } %>
<% } %> <% } %>
</div> </div>
......
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