Paginate edxnotes frontend
TNL-3908
This commit is contained in:
committed by
muzaffaryousaf
parent
840f5017f4
commit
13b14e0a60
@@ -114,7 +114,7 @@ class EdxNotesPageItem(NoteChild):
|
||||
"""
|
||||
BODY_SELECTOR = ".note"
|
||||
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):
|
||||
self.q(css=self._bounded_selector(self.UNIT_LINK_SELECTOR)).click()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Test LMS Notes
|
||||
"""
|
||||
from unittest import skip
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
from nose.plugins.attrib import attr
|
||||
@@ -850,6 +851,7 @@ class EdxNotesPageTest(EventsTestMixin, EdxNotesTestMixin):
|
||||
self.assert_viewed_event('Search Results')
|
||||
self.assert_search_event('note', 4)
|
||||
|
||||
@skip("scroll to tag functionality is removed")
|
||||
def test_scroll_to_tag_recent_activity(self):
|
||||
"""
|
||||
Scenario: Can scroll to a tag group from the Recent Activity view (default view)
|
||||
@@ -861,6 +863,7 @@ class EdxNotesPageTest(EventsTestMixin, EdxNotesTestMixin):
|
||||
self.notes_page.visit()
|
||||
self._scroll_to_tag_and_verify("pear", 3)
|
||||
|
||||
@skip("scroll to tag functionality is removed")
|
||||
def test_scroll_to_tag_course_structure(self):
|
||||
"""
|
||||
Scenario: Can scroll to a tag group from the Course Structure view
|
||||
@@ -872,6 +875,7 @@ class EdxNotesPageTest(EventsTestMixin, EdxNotesTestMixin):
|
||||
self.notes_page.visit().switch_to_tab("structure")
|
||||
self._scroll_to_tag_and_verify("squash", 5)
|
||||
|
||||
@skip("scroll to tag functionality is removed")
|
||||
def test_scroll_to_tag_search(self):
|
||||
"""
|
||||
Scenario: Can scroll to a tag group from the Search Results view
|
||||
@@ -884,6 +888,7 @@ class EdxNotesPageTest(EventsTestMixin, EdxNotesTestMixin):
|
||||
self.notes_page.visit().search("note")
|
||||
self._scroll_to_tag_and_verify("pumpkin", 4)
|
||||
|
||||
@skip("scroll to tag functionality is removed")
|
||||
def test_scroll_to_tag_from_tag_view(self):
|
||||
"""
|
||||
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):
|
||||
params=params
|
||||
)
|
||||
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."))
|
||||
|
||||
return response
|
||||
@@ -141,6 +142,7 @@ def preprocess_collection(user, course, collection):
|
||||
store = modulestore()
|
||||
filtered_collection = list()
|
||||
cache = {}
|
||||
include_extra_info = settings.NOTES_DISABLED_TABS == []
|
||||
with store.bulk_operations(course.id):
|
||||
for model in collection:
|
||||
update = {
|
||||
@@ -176,42 +178,46 @@ def preprocess_collection(user, course, collection):
|
||||
log.debug("Unit not found: %s", usage_key)
|
||||
continue
|
||||
|
||||
section = unit.get_parent()
|
||||
if not section:
|
||||
log.debug("Section not found: %s", usage_key)
|
||||
continue
|
||||
if section in cache:
|
||||
usage_context = cache[section]
|
||||
usage_context.update({
|
||||
"unit": get_module_context(course, unit),
|
||||
})
|
||||
model.update(usage_context)
|
||||
cache[usage_id] = cache[unit] = usage_context
|
||||
filtered_collection.append(model)
|
||||
continue
|
||||
if include_extra_info:
|
||||
section = unit.get_parent()
|
||||
if not section:
|
||||
log.debug("Section not found: %s", usage_key)
|
||||
continue
|
||||
if section in cache:
|
||||
usage_context = cache[section]
|
||||
usage_context.update({
|
||||
"unit": get_module_context(course, unit),
|
||||
})
|
||||
model.update(usage_context)
|
||||
cache[usage_id] = cache[unit] = usage_context
|
||||
filtered_collection.append(model)
|
||||
continue
|
||||
|
||||
chapter = section.get_parent()
|
||||
if not chapter:
|
||||
log.debug("Chapter not found: %s", usage_key)
|
||||
continue
|
||||
if chapter in cache:
|
||||
usage_context = cache[chapter]
|
||||
usage_context.update({
|
||||
"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
|
||||
filtered_collection.append(model)
|
||||
continue
|
||||
chapter = section.get_parent()
|
||||
if not chapter:
|
||||
log.debug("Chapter not found: %s", usage_key)
|
||||
continue
|
||||
if chapter in cache:
|
||||
usage_context = cache[chapter]
|
||||
usage_context.update({
|
||||
"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
|
||||
filtered_collection.append(model)
|
||||
continue
|
||||
|
||||
usage_context = {
|
||||
"unit": get_module_context(course, unit),
|
||||
"section": get_module_context(course, section),
|
||||
"chapter": get_module_context(course, chapter),
|
||||
"section": get_module_context(course, section) if include_extra_info else {},
|
||||
"chapter": get_module_context(course, chapter) if include_extra_info else {},
|
||||
}
|
||||
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)
|
||||
|
||||
return filtered_collection
|
||||
@@ -319,16 +325,24 @@ def get_notes(request, course, page=DEFAULT_PAGE, page_size=DEFAULT_PAGE_SIZE, t
|
||||
try:
|
||||
collection = json.loads(response.content)
|
||||
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
|
||||
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()
|
||||
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
|
||||
del collection['rows']
|
||||
|
||||
collection['next'], collection['previous'] = construct_pagination_urls(
|
||||
request,
|
||||
|
||||
@@ -33,6 +33,16 @@ from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||
|
||||
|
||||
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,
|
||||
"results": [],
|
||||
"current_page": 1,
|
||||
@@ -297,13 +307,13 @@ class EdxNotesHelpersTest(ModuleStoreTestCase):
|
||||
"""
|
||||
mock_get.return_value.content = json.dumps(
|
||||
{
|
||||
"count": 2,
|
||||
"total": 2,
|
||||
"current_page": 1,
|
||||
"start": 0,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"num_pages": 1,
|
||||
"results": [
|
||||
"rows": [
|
||||
{
|
||||
u"quote": u"quote text",
|
||||
u"text": u"text",
|
||||
@@ -404,13 +414,13 @@ class EdxNotesHelpersTest(ModuleStoreTestCase):
|
||||
Tests the result if correct data is received.
|
||||
"""
|
||||
mock_get.return_value.content = json.dumps({
|
||||
"count": 2,
|
||||
"total": 2,
|
||||
"current_page": 1,
|
||||
"start": 0,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"num_pages": 1,
|
||||
"results": [
|
||||
"rows": [
|
||||
{
|
||||
u"quote": u"quote text",
|
||||
u"text": u"text",
|
||||
@@ -511,7 +521,7 @@ class EdxNotesHelpersTest(ModuleStoreTestCase):
|
||||
"""
|
||||
mock_get.return_value.content = json.dumps(NOTES_API_EMPTY_RESPONSE)
|
||||
self.assertItemsEqual(
|
||||
NOTES_API_EMPTY_RESPONSE,
|
||||
NOTES_VIEW_EMPTY_RESPONSE,
|
||||
json.loads(helpers.get_notes(self.request, self.course))
|
||||
)
|
||||
|
||||
@@ -1006,19 +1016,17 @@ class EdxNotesViewsTest(ModuleStoreTestCase):
|
||||
"""
|
||||
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)
|
||||
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)
|
||||
|
||||
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_EDXNOTES": False})
|
||||
@patch("edxnotes.views.get_notes", autospec=True)
|
||||
def test_search_notes_is_disabled(self, mock_search):
|
||||
def test_search_notes_is_disabled(self):
|
||||
"""
|
||||
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"})
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
|
||||
@@ -55,8 +55,10 @@ def edxnotes(request, course_id):
|
||||
"course": course,
|
||||
"notes_endpoint": reverse("notes", kwargs={"course_id": course_id}),
|
||||
"notes": notes_info,
|
||||
"page_size": DEFAULT_PAGE_SIZE,
|
||||
"debug": json.dumps(settings.DEBUG),
|
||||
'position': None,
|
||||
'disabled_tabs': settings.NOTES_DISABLED_TABS,
|
||||
}
|
||||
|
||||
if len(json.loads(notes_info)['results']) == 0:
|
||||
|
||||
@@ -91,6 +91,8 @@ XQUEUE_INTERFACE['url'] = 'http://localhost:8040'
|
||||
EDXNOTES_PUBLIC_API = 'http://localhost:8042/api/v1'
|
||||
EDXNOTES_INTERNAL_API = 'http://localhost:8042/api/v1'
|
||||
|
||||
NOTES_DISABLED_TABS = []
|
||||
|
||||
# Silence noisy logs
|
||||
import logging
|
||||
LOG_OVERRIDES = [
|
||||
|
||||
@@ -2521,6 +2521,9 @@ ENROLLMENT_COURSE_DETAILS_CACHE_TIMEOUT = 60
|
||||
if FEATURES['ENABLE_EDXNOTES']:
|
||||
OAUTH_ID_TOKEN_EXPIRATION = 60 * 60
|
||||
|
||||
# These tabs are currently disabled
|
||||
NOTES_DISABLED_TABS = ['course_structure', 'tags']
|
||||
|
||||
# Configuration used for generating PDF Receipts/Invoices
|
||||
PDF_RECEIPT_TAX_ID = 'add here'
|
||||
PDF_RECEIPT_FOOTER_TEXT = 'add your own specific footer text here'
|
||||
|
||||
@@ -509,6 +509,8 @@ MONGODB_LOG = {
|
||||
'db': 'xlog',
|
||||
}
|
||||
|
||||
NOTES_DISABLED_TABS = []
|
||||
|
||||
# Enable EdxNotes for tests.
|
||||
FEATURES['ENABLE_EDXNOTES'] = True
|
||||
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
;(function (define, undefined) {
|
||||
;(function (define) {
|
||||
'use strict';
|
||||
define([
|
||||
'backbone', 'js/edxnotes/models/note'
|
||||
], function (Backbone, NoteModel) {
|
||||
var NotesCollection = Backbone.Collection.extend({
|
||||
'underscore', 'common/js/components/collections/paging_collection', 'js/edxnotes/models/note'
|
||||
], function (_, PagingCollection, NoteModel) {
|
||||
return PagingCollection.extend({
|
||||
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.
|
||||
* @return {Object}
|
||||
@@ -17,30 +27,26 @@ define([
|
||||
sections = {},
|
||||
units = {};
|
||||
|
||||
if (!courseStructure) {
|
||||
this.each(function (note) {
|
||||
var chapter = note.get('chapter'),
|
||||
section = note.get('section'),
|
||||
unit = note.get('unit');
|
||||
this.each(function (note) {
|
||||
var chapter = note.get('chapter'),
|
||||
section = note.get('section'),
|
||||
unit = note.get('unit');
|
||||
|
||||
chapters[chapter.location] = chapter;
|
||||
sections[section.location] = section;
|
||||
units[unit.location] = units[unit.location] || [];
|
||||
units[unit.location].push(note);
|
||||
});
|
||||
chapters[chapter.location] = chapter;
|
||||
sections[section.location] = section;
|
||||
units[unit.location] = units[unit.location] || [];
|
||||
units[unit.location].push(note);
|
||||
});
|
||||
|
||||
courseStructure = {
|
||||
chapters: _.sortBy(_.toArray(chapters), function (c) {return c.index;}),
|
||||
sections: sections,
|
||||
units: units
|
||||
};
|
||||
}
|
||||
courseStructure = {
|
||||
chapters: _.sortBy(_.toArray(chapters), function (c) {return c.index;}),
|
||||
sections: sections,
|
||||
units: units
|
||||
};
|
||||
|
||||
return courseStructure;
|
||||
};
|
||||
}())
|
||||
});
|
||||
|
||||
return NotesCollection;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
|
||||
@@ -15,17 +15,19 @@ define([
|
||||
this.options = options;
|
||||
this.tabsCollection = new TabsCollection();
|
||||
|
||||
// Must create the Tags view first to get the "scrollToTag" method.
|
||||
this.tagsView = new TagsView({
|
||||
el: this.el,
|
||||
collection: this.collection,
|
||||
tabsCollection: this.tabsCollection
|
||||
});
|
||||
if (!_.contains(this.options.disabledTabs, 'tags')) {
|
||||
// Must create the Tags view first to get the "scrollToTag" method.
|
||||
this.tagsView = new TagsView({
|
||||
el: this.el,
|
||||
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.
|
||||
tagsModel = this.tabsCollection.shift();
|
||||
// Remove the Tags model from the tabs collection because it should not appear first.
|
||||
tagsModel = this.tabsCollection.shift();
|
||||
}
|
||||
|
||||
this.recentActivityView = new RecentActivityView({
|
||||
el: this.el,
|
||||
@@ -34,20 +36,23 @@ define([
|
||||
scrollToTag: scrollToTag
|
||||
});
|
||||
|
||||
this.courseStructureView = new CourseStructureView({
|
||||
el: this.el,
|
||||
collection: this.collection,
|
||||
tabsCollection: this.tabsCollection,
|
||||
scrollToTag: scrollToTag
|
||||
});
|
||||
if (!_.contains(this.options.disabledTabs, 'course_structure')) {
|
||||
this.courseStructureView = new CourseStructureView({
|
||||
el: this.el,
|
||||
collection: this.collection,
|
||||
tabsCollection: this.tabsCollection,
|
||||
scrollToTag: scrollToTag
|
||||
});
|
||||
|
||||
// Add the Tags model after the Course Structure model.
|
||||
this.tabsCollection.push(tagsModel);
|
||||
// Add the Tags model after the Course Structure model.
|
||||
this.tabsCollection.push(tagsModel);
|
||||
}
|
||||
|
||||
this.searchResultsView = new SearchResultsView({
|
||||
el: this.el,
|
||||
tabsCollection: this.tabsCollection,
|
||||
debug: this.options.debug,
|
||||
perPage: this.options.perPage,
|
||||
createTabOnInitialization: false,
|
||||
scrollToTag: scrollToTag
|
||||
});
|
||||
|
||||
@@ -6,19 +6,30 @@ define([
|
||||
/**
|
||||
* Factory method 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 {String} params.endpoint The endpoint of the store.
|
||||
* @return {Object} An instance of NotesPageView.
|
||||
*/
|
||||
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({
|
||||
el: $('.wrapper-student-notes').get(0),
|
||||
collection: collection,
|
||||
debug: params.debugMode,
|
||||
endpoint: params.endpoint
|
||||
endpoint: params.endpoint,
|
||||
perPage: params.pageSize,
|
||||
disabledTabs: params.disabledTabs
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -43,15 +43,12 @@ define([
|
||||
* @return {Array}
|
||||
*/
|
||||
prepareData: function (data) {
|
||||
var collection;
|
||||
|
||||
if (!(data && _.has(data, 'total') && _.has(data, 'rows'))) {
|
||||
if (!(data && _.has(data, 'count') && _.has(data, 'results'))) {
|
||||
this.logger.log('Wrong data', data, this.searchQuery);
|
||||
return null;
|
||||
}
|
||||
|
||||
collection = new NotesCollection(data.rows);
|
||||
return [collection, data.total, this.searchQuery];
|
||||
return [this.collection, this.searchQuery];
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -99,8 +96,8 @@ define([
|
||||
if (args) {
|
||||
this.options.search.apply(this, args);
|
||||
this.logger.emit('edx.course.student_notes.searched', {
|
||||
'number_of_results': args[1],
|
||||
'search_string': args[2]
|
||||
'number_of_results': args[0].totalCount,
|
||||
'search_string': args[1]
|
||||
});
|
||||
} else {
|
||||
this.options.error(this.errorMessage, this.searchQuery);
|
||||
@@ -144,15 +141,15 @@ define([
|
||||
* @return {jQuery.Deferred}
|
||||
*/
|
||||
sendRequest: function (text) {
|
||||
var settings = {
|
||||
url: this.el.action,
|
||||
type: this.el.method,
|
||||
dataType: 'json',
|
||||
data: {text: text}
|
||||
};
|
||||
|
||||
this.logger.log(settings);
|
||||
return $.ajax(settings);
|
||||
this.collection = new NotesCollection(
|
||||
[],
|
||||
{
|
||||
text: text,
|
||||
perPage: this.options.perPage,
|
||||
url: this.el.action
|
||||
}
|
||||
);
|
||||
return this.collection.goTo(1);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define(['gettext', 'underscore', 'backbone', 'js/edxnotes/views/note_item'],
|
||||
function (gettext, _, Backbone, NoteItemView) {
|
||||
define(['gettext', 'underscore', 'backbone', 'js/edxnotes/views/note_item',
|
||||
'common/js/components/views/paging_header', 'common/js/components/views/paging_footer'],
|
||||
function (gettext, _, Backbone, NoteItemView, PagingHeaderView, PagingFooterView) {
|
||||
var TabPanelView = Backbone.View.extend({
|
||||
tagName: 'section',
|
||||
className: 'tab-panel',
|
||||
@@ -13,14 +14,30 @@ function (gettext, _, Backbone, NoteItemView) {
|
||||
|
||||
initialize: function () {
|
||||
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 () {
|
||||
this.$el.html(this.getTitle());
|
||||
this.renderView(this.pagingHeaderView);
|
||||
this.renderContent();
|
||||
this.renderView(this.pagingFooterView);
|
||||
return this;
|
||||
},
|
||||
|
||||
renderView: function(view) {
|
||||
if (this.options.createHeaderFooter && this.collection.models.length) {
|
||||
this.$el.append(view.render().el);
|
||||
view.delegateEvents();
|
||||
}
|
||||
},
|
||||
|
||||
renderContent: function () {
|
||||
return this;
|
||||
},
|
||||
|
||||
@@ -14,7 +14,8 @@ define([
|
||||
initialize: function (options) {
|
||||
_.bindAll(this, 'showLoadingIndicator', 'hideLoadingIndicator');
|
||||
this.options = _.defaults(options || {}, {
|
||||
createTabOnInitialization: true
|
||||
createTabOnInitialization: true,
|
||||
createHeaderFooter: true
|
||||
});
|
||||
|
||||
if (this.options.createTabOnInitialization) {
|
||||
@@ -64,7 +65,13 @@ define([
|
||||
|
||||
getSubView: function () {
|
||||
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 () {
|
||||
|
||||
@@ -58,6 +58,7 @@ define([
|
||||
this.searchBox = new SearchBoxView({
|
||||
el: document.getElementById('search-notes-form'),
|
||||
debug: this.options.debug,
|
||||
perPage: this.options.perPage,
|
||||
beforeSearchStart: this.onBeforeSearchStart,
|
||||
search: this.onSearch,
|
||||
error: this.onSearchError
|
||||
@@ -81,7 +82,8 @@ define([
|
||||
return new this.PanelConstructor({
|
||||
collection: collection,
|
||||
searchQuery: this.searchResults.searchQuery,
|
||||
scrollToTag: this.options.scrollToTag
|
||||
scrollToTag: this.options.scrollToTag,
|
||||
createHeaderFooter: this.options.createHeaderFooter
|
||||
});
|
||||
} else {
|
||||
return new this.NoResultsViewConstructor({
|
||||
@@ -122,10 +124,9 @@ define([
|
||||
}
|
||||
},
|
||||
|
||||
onSearch: function (collection, total, searchQuery) {
|
||||
onSearch: function (collection, searchQuery) {
|
||||
this.searchResults = {
|
||||
collection: collection,
|
||||
total: total,
|
||||
searchQuery: searchQuery
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ define([
|
||||
var notes = Helpers.getDefaultNotes();
|
||||
|
||||
beforeEach(function () {
|
||||
this.collection = new NotesCollection(notes);
|
||||
this.collection = new NotesCollection(notes, {perPage: 10, parse: true});
|
||||
});
|
||||
|
||||
it('can return correct course structure', function () {
|
||||
@@ -23,11 +23,22 @@ define([
|
||||
'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/1': [this.collection.at(2)],
|
||||
'i4x://unit/2': [this.collection.at(3)],
|
||||
'i4x://unit/3': [this.collection.at(4)]
|
||||
};
|
||||
|
||||
_.each(units, function(value, key){
|
||||
compareUnits(structure.units[key], value);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
define(['underscore'], function(_) {
|
||||
define(['underscore', 'URI', 'common/js/spec_helpers/ajax_helpers'], function(_, URI, AjaxHelpers) {
|
||||
'use strict';
|
||||
var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
|
||||
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 = [
|
||||
'Adipisicing elit, sed do eiusmod tempor incididunt ',
|
||||
@@ -106,57 +108,131 @@ define(['underscore'], function(_) {
|
||||
|
||||
getDefaultNotes = function () {
|
||||
// Note that the server returns notes in reverse chronological order (newest first).
|
||||
return [
|
||||
{
|
||||
chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]),
|
||||
section: getSection('Third Section', 0, ['w_n', 1, 0]),
|
||||
unit: getUnit('Fourth Unit', 0),
|
||||
created: 'December 11, 2014 at 11:12AM',
|
||||
updated: 'December 11, 2014 at 11:12AM',
|
||||
text: 'Third added model',
|
||||
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),
|
||||
created: 'December 11, 2014 at 11:11AM',
|
||||
updated: 'December 11, 2014 at 11:11AM',
|
||||
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]),
|
||||
unit: getUnit('Third Unit', 1),
|
||||
created: 'December 11, 2014 at 11:11AM',
|
||||
updated: 'December 11, 2014 at 11:11AM',
|
||||
text: 'Second added model',
|
||||
quote: 'Note 3',
|
||||
tags: ['yummy']
|
||||
},
|
||||
{
|
||||
chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]),
|
||||
section: getSection('Second Section', 1, [2]),
|
||||
unit: getUnit('Second Unit', 2),
|
||||
created: 'December 11, 2014 at 11:10AM',
|
||||
updated: 'December 11, 2014 at 11:10AM',
|
||||
text: 'First added model',
|
||||
quote: 'Note 2',
|
||||
tags: ['PUMPKIN', 'pie']
|
||||
},
|
||||
{
|
||||
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']
|
||||
}
|
||||
];
|
||||
return {
|
||||
'count': 5,
|
||||
'current_page': 1,
|
||||
'num_pages': 1,
|
||||
'start': 0,
|
||||
'next': null,
|
||||
'previous': null,
|
||||
'results': [
|
||||
{
|
||||
chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]),
|
||||
section: getSection('Third Section', 0, ['w_n', 1, 0]),
|
||||
unit: getUnit('Fourth Unit', 0),
|
||||
created: 'December 11, 2014 at 11:12AM',
|
||||
updated: 'December 11, 2014 at 11:12AM',
|
||||
text: 'Third added model',
|
||||
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),
|
||||
created: 'December 11, 2014 at 11:11AM',
|
||||
updated: 'December 11, 2014 at 11:11AM',
|
||||
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]),
|
||||
unit: getUnit('Third Unit', 1),
|
||||
created: 'December 11, 2014 at 11:11AM',
|
||||
updated: 'December 11, 2014 at 11:11AM',
|
||||
text: 'Second added model',
|
||||
quote: 'Note 3',
|
||||
tags: ['yummy']
|
||||
},
|
||||
{
|
||||
chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]),
|
||||
section: getSection('Second Section', 1, [2]),
|
||||
unit: getUnit('Second Unit', 2),
|
||||
created: 'December 11, 2014 at 11:10AM',
|
||||
updated: 'December 11, 2014 at 11:10AM',
|
||||
text: 'First added model',
|
||||
quote: 'Note 2',
|
||||
tags: ['PUMPKIN', 'pie']
|
||||
},
|
||||
{
|
||||
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 {
|
||||
@@ -169,6 +245,12 @@ define(['underscore'], function(_) {
|
||||
getChapter: getChapter,
|
||||
getSection: getSection,
|
||||
getUnit: getUnit,
|
||||
getDefaultNotes: getDefaultNotes
|
||||
getDefaultNotes: getDefaultNotes,
|
||||
verifyUrl: verifyUrl,
|
||||
verifyRequestParams: verifyRequestParams,
|
||||
createNotesData: createNotesData,
|
||||
respondToRequest: respondToRequest,
|
||||
verifyPaginationInfo: verifyPaginationInfo,
|
||||
verifyPageData: verifyPageData
|
||||
};
|
||||
});
|
||||
|
||||
@@ -4,10 +4,23 @@ define([
|
||||
'use strict';
|
||||
describe('EdxNotes NoteModel', function() {
|
||||
beforeEach(function () {
|
||||
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'}
|
||||
]);
|
||||
this.collection = new NotesCollection(
|
||||
{
|
||||
'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 () {
|
||||
|
||||
@@ -67,12 +67,12 @@ define([
|
||||
var view = getView({tags: ["First", "Second"]});
|
||||
expect(view.$('.reference-title').length).toBe(3);
|
||||
expect(view.$('.reference-title')[2]).toContainText('Tags:');
|
||||
expect(view.$('a.reference-tags').length).toBe(2);
|
||||
expect(view.$('a.reference-tags')[0]).toContainText('First');
|
||||
expect(view.$('a.reference-tags')[1]).toContainText('Second');
|
||||
expect(view.$('span.reference-tags').length).toBe(2);
|
||||
expect(view.$('span.reference-tags')[0]).toContainText('First');
|
||||
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 = {
|
||||
scrollToTag: function (tagName){}
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ define([
|
||||
TemplateHelpers.installTemplates([
|
||||
'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([
|
||||
this.view.$('.search-notes-input').val('test_query');
|
||||
this.view.$('.search-notes-submit').click();
|
||||
AjaxHelpers.respondWithJson(requests, {
|
||||
total: 0,
|
||||
rows: []
|
||||
'count': 0,
|
||||
'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-recent-activity')).toExist();
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
define([
|
||||
'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers', 'js/edxnotes/views/search_box',
|
||||
'js/edxnotes/collections/notes', 'js/spec/edxnotes/custom_matchers', 'jasmine-jquery'
|
||||
], function($, _, AjaxHelpers, SearchBoxView, NotesCollection, customMatchers) {
|
||||
'js/edxnotes/collections/notes', 'js/spec/edxnotes/custom_matchers', 'js/spec/edxnotes/helpers', 'jasmine-jquery'
|
||||
], function($, _, AjaxHelpers, SearchBoxView, NotesCollection, customMatchers, Helpers) {
|
||||
'use strict';
|
||||
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) {
|
||||
options = _.defaults(options || {}, {
|
||||
el: $('#search-notes-form').get(0),
|
||||
perPage: 10,
|
||||
beforeSearchStart: jasmine.createSpy(),
|
||||
search: jasmine.createSpy(),
|
||||
error: jasmine.createSpy(),
|
||||
@@ -50,7 +61,11 @@ define([
|
||||
submitForm(this.searchBox, 'test_text');
|
||||
request = requests[0];
|
||||
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 () {
|
||||
@@ -60,13 +75,10 @@ define([
|
||||
'test_text'
|
||||
);
|
||||
assertBoxIsDisabled(this.searchBox);
|
||||
AjaxHelpers.respondWithJson(requests, {
|
||||
total: 2,
|
||||
rows: [null, null]
|
||||
});
|
||||
AjaxHelpers.respondWithJson(requests, searchResponse);
|
||||
assertBoxIsEnabled(this.searchBox);
|
||||
expect(this.searchBox.options.search).toHaveBeenCalledWith(
|
||||
jasmine.any(NotesCollection), 2, 'test_text'
|
||||
jasmine.any(NotesCollection), 'test_text'
|
||||
);
|
||||
expect(this.searchBox.options.complete).toHaveBeenCalledWith(
|
||||
'test_text'
|
||||
@@ -76,10 +88,7 @@ define([
|
||||
it('should log the edx.course.student_notes.searched event properly', function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
submitForm(this.searchBox, 'test_text');
|
||||
AjaxHelpers.respondWithJson(requests, {
|
||||
total: 2,
|
||||
rows: [null, null]
|
||||
});
|
||||
AjaxHelpers.respondWithJson(requests, searchResponse);
|
||||
|
||||
expect(Logger.log).toHaveBeenCalledWith('edx.course.student_notes.searched', {
|
||||
'number_of_results': 2,
|
||||
@@ -140,10 +149,7 @@ define([
|
||||
submitForm(this.searchBox, 'test_text');
|
||||
assertBoxIsDisabled(this.searchBox);
|
||||
submitForm(this.searchBox, 'another_text');
|
||||
AjaxHelpers.respondWithJson(requests, {
|
||||
total: 2,
|
||||
rows: [null, null]
|
||||
});
|
||||
AjaxHelpers.respondWithJson(requests, searchResponse);
|
||||
assertBoxIsEnabled(this.searchBox);
|
||||
expect(requests).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ define([
|
||||
'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();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,32 +1,40 @@
|
||||
define([
|
||||
'jquery', 'common/js/spec_helpers/template_helpers', 'js/edxnotes/collections/notes',
|
||||
'js/edxnotes/collections/tabs', 'js/edxnotes/views/tabs/recent_activity',
|
||||
'js/spec/edxnotes/custom_matchers', 'jasmine-jquery'
|
||||
'jquery', 'common/js/spec_helpers/template_helpers', 'common/js/spec_helpers/ajax_helpers',
|
||||
'js/edxnotes/collections/notes', 'js/edxnotes/collections/tabs', 'js/edxnotes/views/tabs/recent_activity',
|
||||
'js/spec/edxnotes/custom_matchers', 'js/spec/edxnotes/helpers', 'jasmine-jquery'
|
||||
], function(
|
||||
$, TemplateHelpers, NotesCollection, TabsCollection, RecentActivityView, customMatchers
|
||||
$, TemplateHelpers, AjaxHelpers, NotesCollection, TabsCollection, RecentActivityView, customMatchers, Helpers
|
||||
) {
|
||||
'use strict';
|
||||
describe('EdxNotes RecentActivityView', function() {
|
||||
var notes = [
|
||||
{
|
||||
created: 'December 11, 2014 at 11:12AM',
|
||||
updated: 'December 11, 2014 at 11:12AM',
|
||||
text: 'Third added model',
|
||||
quote: 'Should be listed first'
|
||||
},
|
||||
{
|
||||
created: 'December 11, 2014 at 11:11AM',
|
||||
updated: 'December 11, 2014 at 11:11AM',
|
||||
text: 'Second added model',
|
||||
quote: 'Should be listed second'
|
||||
},
|
||||
{
|
||||
created: 'December 11, 2014 at 11:10AM',
|
||||
updated: 'December 11, 2014 at 11:10AM',
|
||||
text: 'First added model',
|
||||
quote: 'Should be listed third'
|
||||
}
|
||||
], getView;
|
||||
var notes = {
|
||||
'count': 3,
|
||||
'current_page': 1,
|
||||
'num_pages': 1,
|
||||
'start': 0,
|
||||
'next': null,
|
||||
'previous': null,
|
||||
'results': [
|
||||
{
|
||||
created: 'December 11, 2014 at 11:12AM',
|
||||
updated: 'December 11, 2014 at 11:12AM',
|
||||
text: 'Third added model',
|
||||
quote: 'Should be listed first'
|
||||
},
|
||||
{
|
||||
created: 'December 11, 2014 at 11:11AM',
|
||||
updated: 'December 11, 2014 at 11:11AM',
|
||||
text: 'Second added model',
|
||||
quote: 'Should be listed second'
|
||||
},
|
||||
{
|
||||
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) {
|
||||
var view;
|
||||
@@ -35,6 +43,7 @@ define([
|
||||
el: $('.wrapper-student-notes'),
|
||||
collection: collection,
|
||||
tabsCollection: tabsCollection,
|
||||
createHeaderFooter: true
|
||||
});
|
||||
|
||||
view = new RecentActivityView(options);
|
||||
@@ -43,6 +52,17 @@ define([
|
||||
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 () {
|
||||
customMatchers(this);
|
||||
loadFixtures('js/fixtures/edxnotes/edxnotes.html');
|
||||
@@ -50,28 +70,135 @@ define([
|
||||
'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();
|
||||
});
|
||||
|
||||
it('displays a tab and content with proper data and order', function () {
|
||||
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);
|
||||
expect(this.tabsCollection.at(0).toJSON()).toEqual({
|
||||
name: 'Recent Activity',
|
||||
identifier: 'view-recent-activity',
|
||||
icon: 'fa fa-clock-o',
|
||||
is_active: true,
|
||||
is_closable: false,
|
||||
view: 'Recent Activity'
|
||||
});
|
||||
expect(view.$('#recent-panel')).toExist();
|
||||
expect(view.$('.note')).toHaveLength(3);
|
||||
_.each(view.$('.note'), function(element, index) {
|
||||
expect($('.note-comments', element)).toContainText(notes[index].text);
|
||||
expect($('.note-excerpt', element)).toContainText(notes[index].quote);
|
||||
});
|
||||
it("will not render header and footer if there are no notes", function () {
|
||||
var notes = {
|
||||
'count': 0,
|
||||
'current_page': 1,
|
||||
'num_pages': 1,
|
||||
'start': 0,
|
||||
'next': null,
|
||||
'previous': null,
|
||||
'results': []
|
||||
};
|
||||
var collection = new NotesCollection(notes, {perPage: 10, parse: true});
|
||||
var view = getView(collection, this.tabsCollection);
|
||||
expect(view.$('.search-tools.listing-tools')).toHaveLength(0);
|
||||
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'}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
define([
|
||||
'jquery', 'underscore', 'common/js/spec_helpers/template_helpers', 'common/js/spec_helpers/ajax_helpers',
|
||||
'logger', 'js/edxnotes/collections/tabs', 'js/edxnotes/views/tabs/search_results',
|
||||
'js/spec/edxnotes/custom_matchers', 'jasmine-jquery'
|
||||
'js/spec/edxnotes/custom_matchers', 'js/spec/edxnotes/helpers', 'jasmine-jquery'
|
||||
], function(
|
||||
$, _, TemplateHelpers, AjaxHelpers, Logger, TabsCollection, SearchResultsView,
|
||||
customMatchers
|
||||
customMatchers, Helpers
|
||||
) {
|
||||
'use strict';
|
||||
describe('EdxNotes SearchResultsView', function() {
|
||||
@@ -29,18 +29,25 @@ define([
|
||||
}
|
||||
],
|
||||
responseJson = {
|
||||
total: 3,
|
||||
rows: notes
|
||||
'count': 3,
|
||||
'current_page': 1,
|
||||
'num_pages': 1,
|
||||
'start': 0,
|
||||
'next': null,
|
||||
'previous': null,
|
||||
'results': notes
|
||||
},
|
||||
getView, submitForm, respondToSearch;
|
||||
getView, submitForm, tabInfo, searchResultsTabId;
|
||||
|
||||
getView = function (tabsCollection, options) {
|
||||
getView = function (tabsCollection, perPage, options) {
|
||||
options = _.defaults(options || {}, {
|
||||
el: $('.wrapper-student-notes'),
|
||||
tabsCollection: tabsCollection,
|
||||
user: 'test_user',
|
||||
courseId: 'course_id',
|
||||
createTabOnInitialization: false
|
||||
createTabOnInitialization: false,
|
||||
createHeaderFooter: true,
|
||||
perPage: perPage || 10
|
||||
});
|
||||
return new SearchResultsView(options);
|
||||
};
|
||||
@@ -50,14 +57,17 @@ define([
|
||||
searchBox.$('.search-notes-submit').click();
|
||||
};
|
||||
|
||||
respondToSearch = function(requests, responseJson) {
|
||||
// First respond to the analytics event
|
||||
AjaxHelpers.respondWithNoContent(requests);
|
||||
|
||||
// Now process the search request
|
||||
AjaxHelpers.respondWithJson(requests, responseJson);
|
||||
tabInfo = {
|
||||
name: 'Search Results',
|
||||
identifier: 'view-search-results',
|
||||
icon: 'fa fa-search',
|
||||
is_active: true,
|
||||
is_closable: true,
|
||||
view: 'Search Results'
|
||||
};
|
||||
|
||||
searchResultsTabId = "#search-results-panel";
|
||||
|
||||
beforeEach(function () {
|
||||
customMatchers(this);
|
||||
loadFixtures('js/fixtures/edxnotes/edxnotes.html');
|
||||
@@ -79,23 +89,9 @@ define([
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
submitForm(view.searchBox, 'second');
|
||||
respondToSearch(requests, responseJson);
|
||||
|
||||
expect(this.tabsCollection).toHaveLength(1);
|
||||
expect(this.tabsCollection.at(0).toJSON()).toEqual({
|
||||
name: 'Search Results',
|
||||
identifier: 'view-search-results',
|
||||
icon: 'fa fa-search',
|
||||
is_active: true,
|
||||
is_closable: true,
|
||||
view: 'Search Results'
|
||||
});
|
||||
expect(view.$('#search-results-panel')).toExist();
|
||||
expect(view.$('#search-results-panel')).toBeFocused();
|
||||
expect(view.$('.note')).toHaveLength(3);
|
||||
view.searchResults.collection.each(function (model, index) {
|
||||
expect(model.get('text')).toBe(notes[index].text);
|
||||
});
|
||||
Helpers.respondToRequest(requests, responseJson, true);
|
||||
Helpers.verifyPageData(view, this.tabsCollection, tabInfo, searchResultsTabId, responseJson);
|
||||
Helpers.verifyPaginationInfo(view, "Showing 1-3 out of 3 total", 1, 1);
|
||||
});
|
||||
|
||||
it('displays loading indicator when search is running', function () {
|
||||
@@ -108,7 +104,7 @@ define([
|
||||
expect(this.tabsCollection).toHaveLength(1);
|
||||
expect(view.searchResults).toBeNull();
|
||||
expect(view.$('.tab-panel')).not.toExist();
|
||||
respondToSearch(requests, responseJson);
|
||||
Helpers.respondToRequest(requests, responseJson, true);
|
||||
expect(view.$('.ui-loading')).toHaveClass('is-hidden');
|
||||
});
|
||||
|
||||
@@ -117,10 +113,7 @@ define([
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
submitForm(view.searchBox, 'some text');
|
||||
respondToSearch(requests, {
|
||||
total: 0,
|
||||
rows: []
|
||||
});
|
||||
Helpers.respondToRequest(requests, _.extend(_.clone(responseJson), {count: 0, results: []}), true);
|
||||
|
||||
expect(view.$('#search-results-panel')).not.toExist();
|
||||
expect(view.$('#no-results-panel')).toBeFocused();
|
||||
@@ -155,7 +148,7 @@ define([
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
submitForm(view.searchBox, 'test_query');
|
||||
respondToSearch(requests, responseJson);
|
||||
Helpers.respondToRequest(requests, responseJson, true);
|
||||
expect(view.searchResults).toBeDefined();
|
||||
this.tabsCollection.at(0).destroy();
|
||||
expect(view.searchResults).toBeNull();
|
||||
@@ -195,20 +188,140 @@ define([
|
||||
}];
|
||||
|
||||
submitForm(view.searchBox, 'test_query');
|
||||
respondToSearch(requests, responseJson);
|
||||
Helpers.respondToRequest(requests, responseJson, true);
|
||||
|
||||
expect(view.$('.note')).toHaveLength(3);
|
||||
|
||||
submitForm(view.searchBox, 'new_test_query');
|
||||
respondToSearch(requests, {
|
||||
total: 1,
|
||||
rows: newNotes
|
||||
});
|
||||
Helpers.respondToRequest(requests, {
|
||||
'count': 1,
|
||||
'current_page': 1,
|
||||
'num_pages': 1,
|
||||
'start': 0,
|
||||
'next': null,
|
||||
'previous': null,
|
||||
'results': newNotes
|
||||
}, true);
|
||||
|
||||
expect(view.$('.note').length).toHaveLength(1);
|
||||
view.searchResults.collection.each(function (model, index) {
|
||||
expect(model.get('text')).toBe(newNotes[index].text);
|
||||
});
|
||||
});
|
||||
|
||||
it("will not render header and footer if there are no notes", function () {
|
||||
var view = getView(this.tabsCollection),
|
||||
requests = AjaxHelpers.requests(this),
|
||||
notes = {
|
||||
'count': 0,
|
||||
'current_page': 1,
|
||||
'num_pages': 1,
|
||||
'start': 0,
|
||||
'next': null,
|
||||
'previous': null,
|
||||
'results': []
|
||||
};
|
||||
submitForm(view.searchBox, 'awesome');
|
||||
Helpers.respondToRequest(requests, notes, true);
|
||||
expect(view.$('.search-tools.listing-tools')).toHaveLength(0);
|
||||
expect(view.$('.pagination.pagination-full.bottom')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("can go to a page number", function () {
|
||||
var view = getView(this.tabsCollection),
|
||||
requests = AjaxHelpers.requests(this),
|
||||
notes = Helpers.createNotesData(
|
||||
{
|
||||
numNotesToCreate: 10,
|
||||
count: 12,
|
||||
num_pages: 2,
|
||||
current_page: 1,
|
||||
start: 0
|
||||
}
|
||||
);
|
||||
|
||||
submitForm(view.searchBox, 'awesome');
|
||||
Helpers.respondToRequest(requests, notes, true);
|
||||
Helpers.verifyPaginationInfo(view, "Showing 1-10 out of 12 total", 1, 2);
|
||||
Helpers.verifyPageData(view, this.tabsCollection, tabInfo, searchResultsTabId, 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, searchResultsTabId, notes);
|
||||
});
|
||||
|
||||
it("can navigate forward and backward", function () {
|
||||
var requests = AjaxHelpers.requests(this),
|
||||
page1Notes = Helpers.createNotesData(
|
||||
{
|
||||
numNotesToCreate: 10,
|
||||
count: 15,
|
||||
num_pages: 2,
|
||||
current_page: 1,
|
||||
start: 0
|
||||
}
|
||||
),
|
||||
view = getView(this.tabsCollection);
|
||||
|
||||
submitForm(view.searchBox, 'awesome');
|
||||
Helpers.respondToRequest(requests, page1Notes, true);
|
||||
Helpers.verifyPaginationInfo(view, "Showing 1-10 out of 15 total", 1, 2);
|
||||
Helpers.verifyPageData(view, this.tabsCollection, tabInfo, searchResultsTabId, 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, searchResultsTabId, 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, searchResultsTabId, page1Notes);
|
||||
});
|
||||
|
||||
it("sends correct page size value", function () {
|
||||
var requests = AjaxHelpers.requests(this),
|
||||
view = getView(this.tabsCollection, 5);
|
||||
|
||||
submitForm(view.searchBox, 'awesome');
|
||||
Helpers.verifyRequestParams(
|
||||
requests[requests.length - 1].url,
|
||||
{page: '1', page_size: '5'}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@ define([
|
||||
'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();
|
||||
});
|
||||
|
||||
|
||||
@@ -235,27 +235,24 @@ $divider-visual-tertiary: ($baseline/20) solid $gray-l4;
|
||||
|
||||
// CASE: tag matches a search query
|
||||
.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;
|
||||
}
|
||||
|
||||
// Put commas between tags.
|
||||
a.reference-meta.reference-tags:after {
|
||||
span.reference-meta.reference-tags:after {
|
||||
content: ",";
|
||||
color: $m-gray-d2;
|
||||
}
|
||||
|
||||
// But not after the last tag.
|
||||
a.reference-meta.reference-tags:last-child:after {
|
||||
span.reference-meta.reference-tags:last-child:after {
|
||||
content: "";
|
||||
}
|
||||
|
||||
// needed for poor base LMS styling scope
|
||||
a.reference-meta {
|
||||
@extend %shame-link-text;
|
||||
@extend %shame-link-text;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,6 +282,15 @@ $divider-visual-tertiary: ($baseline/20) solid $gray-l4;
|
||||
|
||||
.tab-panel, .inline-error, .ui-loading {
|
||||
@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 {
|
||||
|
||||
@@ -12,6 +12,10 @@ import json
|
||||
</%block>
|
||||
|
||||
<%include file="/courseware/course_navigation.html" args="active_page='edxnotes'" />
|
||||
<%
|
||||
_notes = json.loads(notes)
|
||||
has_notes = _notes and _notes.get('results')
|
||||
%>
|
||||
<section class="container">
|
||||
<div class="wrapper-student-notes">
|
||||
<div class="student-notes">
|
||||
@@ -24,7 +28,7 @@ import json
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
% if notes:
|
||||
% if has_notes:
|
||||
<div class="wrapper-notes-search">
|
||||
<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>
|
||||
@@ -51,7 +55,7 @@ import json
|
||||
<h2 id="tab-view" class="tabs-label">${_('View notes by:')}</h2>
|
||||
</div>
|
||||
|
||||
% if notes:
|
||||
% if has_notes:
|
||||
<div class="ui-loading" tabindex="-1">
|
||||
<span class="spin">
|
||||
<i class="icon fa fa-refresh"></i>
|
||||
@@ -103,11 +107,14 @@ import json
|
||||
% endfor
|
||||
</%block>
|
||||
|
||||
% if notes:
|
||||
% if has_notes:
|
||||
<%block name="js_extra">
|
||||
<%static:require_module module_name="js/edxnotes/views/page_factory" class_name="NotesPageFactory">
|
||||
NotesPageFactory({
|
||||
notesList: ${notes if notes is not None else []},
|
||||
disabledTabs: ${disabled_tabs},
|
||||
notes: ${notes},
|
||||
notesEndpoint: '${notes_endpoint}',
|
||||
pageSize: '${page_size}',
|
||||
debugMode: ${debug}
|
||||
});
|
||||
</%static:require_module>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<% if (tags.length > 0) { %>
|
||||
<p class="reference-title"><%- gettext("Tags:") %></p>
|
||||
<% 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>
|
||||
|
||||
Reference in New Issue
Block a user