Commit abd25b8b by cahrens

Add a "tags" view in student notes.

TNL-1926
parent b328a2e8
...@@ -34,9 +34,9 @@ class NoteChild(PageObject): ...@@ -34,9 +34,9 @@ class NoteChild(PageObject):
return None return None
class EdxNotesPageGroup(NoteChild): class EdxNotesChapterGroup(NoteChild):
""" """
Helper class that works with note groups on Note page of the course. Helper class that works with chapter (section) grouping of notes in the Course Structure view on the Note page.
""" """
BODY_SELECTOR = ".note-group" BODY_SELECTOR = ".note-group"
...@@ -51,18 +51,16 @@ class EdxNotesPageGroup(NoteChild): ...@@ -51,18 +51,16 @@ class EdxNotesPageGroup(NoteChild):
@property @property
def children(self): def children(self):
children = self.q(css=self._bounded_selector('.note-section')) children = self.q(css=self._bounded_selector('.note-section'))
return [EdxNotesPageSection(self.browser, child.get_attribute("id")) for child in children] return [EdxNotesSubsectionGroup(self.browser, child.get_attribute("id")) for child in children]
class EdxNotesPageSection(NoteChild): class EdxNotesGroupMixin(object):
""" """
Helper class that works with note sections on Note page of the course. Helper mixin that works with note groups (used for subsection and tag groupings).
""" """
BODY_SELECTOR = ".note-section"
@property @property
def title(self): def title(self):
return self._get_element_text(".course-subtitle") return self._get_element_text(self.TITLE_SELECTOR)
@property @property
def children(self): def children(self):
...@@ -74,12 +72,49 @@ class EdxNotesPageSection(NoteChild): ...@@ -74,12 +72,49 @@ class EdxNotesPageSection(NoteChild):
return [section.text for section in self.children] return [section.text for section in self.children]
class EdxNotesSubsectionGroup(NoteChild, EdxNotesGroupMixin):
"""
Helper class that works with subsection grouping of notes in the Course Structure view on the Note page.
"""
BODY_SELECTOR = ".note-section"
TITLE_SELECTOR = ".course-subtitle"
class EdxNotesTagsGroup(NoteChild, EdxNotesGroupMixin):
"""
Helper class that works with tags grouping of notes in the Tags view on the Note page.
"""
BODY_SELECTOR = ".note-group"
TITLE_SELECTOR = ".tags-title"
def scrolled_to_top(self, group_index):
"""
Returns True if the group with supplied group)index is scrolled near the top of the page
(expects 10 px padding).
The group_index must be supplied because JQuery must be used to get this information, and it
does not have access to the bounded selector.
"""
title_selector = "$('" + self.TITLE_SELECTOR + "')[" + str(group_index) + "]"
top_script = "return " + title_selector + ".getBoundingClientRect().top;"
EmptyPromise(
lambda: 8 < self.browser.execute_script(top_script) < 12,
"Expected tag title '{}' to scroll to top, but was at location {}".format(
self.title, self.browser.execute_script(top_script)
)
).fulfill()
# Now also verify that focus has moved to this title (for screen readers):
active_script = "return " + title_selector + " === document.activeElement;"
return self.browser.execute_script(active_script)
class EdxNotesPageItem(NoteChild): class EdxNotesPageItem(NoteChild):
""" """
Helper class that works with note items on Note page of the course. Helper class that works with note items on Note page of the course.
""" """
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"
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()
...@@ -102,6 +137,18 @@ class EdxNotesPageItem(NoteChild): ...@@ -102,6 +137,18 @@ class EdxNotesPageItem(NoteChild):
def time_updated(self): def time_updated(self):
return self._get_element_text(".reference-updated-date") return self._get_element_text(".reference-updated-date")
@property
def tags(self):
""" The tags associated with this note. """
tag_links = self.q(css=self._bounded_selector(self.TAG_SELECTOR))
if len(tag_links) == 0:
return None
return[tag_link.text for tag_link in tag_links]
def go_to_tag(self, tag_name):
""" Clicks a tag associated with the note to change to the tags view (and scroll to the tag group). """
self.q(css=self._bounded_selector(self.TAG_SELECTOR)).filter(lambda el: tag_name in el.text).click()
class EdxNotesPageView(PageObject): class EdxNotesPageView(PageObject):
""" """
...@@ -174,7 +221,17 @@ class CourseStructureView(EdxNotesPageView): ...@@ -174,7 +221,17 @@ class CourseStructureView(EdxNotesPageView):
BODY_SELECTOR = "#structure-panel" BODY_SELECTOR = "#structure-panel"
TAB_SELECTOR = ".tab#view-course-structure" TAB_SELECTOR = ".tab#view-course-structure"
CHILD_SELECTOR = ".note-group" CHILD_SELECTOR = ".note-group"
CHILD_CLASS = EdxNotesPageGroup CHILD_CLASS = EdxNotesChapterGroup
class TagsView(EdxNotesPageView):
"""
Helper class for Tags view.
"""
BODY_SELECTOR = "#tags-panel"
TAB_SELECTOR = ".tab#view-tags"
CHILD_SELECTOR = ".note-group"
CHILD_CLASS = EdxNotesTagsGroup
class SearchResultsView(EdxNotesPageView): class SearchResultsView(EdxNotesPageView):
...@@ -193,6 +250,7 @@ class EdxNotesPage(CoursePage): ...@@ -193,6 +250,7 @@ class EdxNotesPage(CoursePage):
MAPPING = { MAPPING = {
"recent": RecentActivityView, "recent": RecentActivityView,
"structure": CourseStructureView, "structure": CourseStructureView,
"tags": TagsView,
"search": SearchResultsView, "search": SearchResultsView,
} }
...@@ -210,9 +268,9 @@ class EdxNotesPage(CoursePage): ...@@ -210,9 +268,9 @@ class EdxNotesPage(CoursePage):
self.current_view = self.MAPPING[tab_name](self.browser) self.current_view = self.MAPPING[tab_name](self.browser)
self.current_view.visit() self.current_view.visit()
def close_tab(self, tab_name): def close_tab(self):
""" """
Closes the tab `tab_name(str)`. Closes the current view.
""" """
self.current_view.close() self.current_view.close()
self.current_view = self.MAPPING["recent"](self.browser) self.current_view = self.MAPPING["recent"](self.browser)
...@@ -267,20 +325,28 @@ class EdxNotesPage(CoursePage): ...@@ -267,20 +325,28 @@ class EdxNotesPage(CoursePage):
return [EdxNotesPageItem(self.browser, child.get_attribute("id")) for child in children] return [EdxNotesPageItem(self.browser, child.get_attribute("id")) for child in children]
@property @property
def groups(self): def chapter_groups(self):
""" """
Returns all groups on the page. Returns all chapter groups on the page.
""" """
children = self.q(css='.note-group') children = self.q(css='.note-group')
return [EdxNotesPageGroup(self.browser, child.get_attribute("id")) for child in children] return [EdxNotesChapterGroup(self.browser, child.get_attribute("id")) for child in children]
@property @property
def sections(self): def subsection_groups(self):
""" """
Returns all sections on the page. Returns all subsection groups on the page.
""" """
children = self.q(css='.note-section') children = self.q(css='.note-section')
return [EdxNotesPageSection(self.browser, child.get_attribute("id")) for child in children] return [EdxNotesSubsectionGroup(self.browser, child.get_attribute("id")) for child in children]
@property
def tag_groups(self):
"""
Returns all tag groups on the page.
"""
children = self.q(css='.note-group')
return [EdxNotesTagsGroup(self.browser, child.get_attribute("id")) for child in children]
class EdxNotesPageNoContent(CoursePage): class EdxNotesPageNoContent(CoursePage):
......
;(function (define, undefined) { ;(function (define, undefined) {
'use strict'; 'use strict';
define(['backbone'], function (Backbone) { define(['underscore', 'backbone'], function (_, Backbone) {
var TabModel = Backbone.Model.extend({ var TabModel = Backbone.Model.extend({
defaults: { defaults: {
'identifier': '', 'identifier': '',
......
...@@ -3,19 +3,22 @@ ...@@ -3,19 +3,22 @@
define([ define([
'gettext', 'underscore', 'backbone' 'gettext', 'underscore', 'backbone'
], function (gettext, _, Backbone) { ], function (gettext, _, Backbone) {
var NoteSectionView, NoteGroupView; var GroupView, ChapterView;
NoteSectionView = Backbone.View.extend({ GroupView = Backbone.View.extend({
tagName: 'section', tagName: 'section',
className: 'note-section',
id: function () { id: function () {
return 'note-section-' + _.uniqueId(); return 'note-section-' + _.uniqueId();
}, },
template: _.template('<h4 class="course-subtitle"><%- sectionName %></h4>'),
initialize: function () {
this.template = _.template(this.options.template);
this.className = this.options.className;
},
render: function () { render: function () {
this.$el.prepend(this.template({ this.$el.prepend(this.template({
sectionName: this.options.section.display_name displayName: this.options.displayName
})); }));
return this; return this;
...@@ -26,7 +29,7 @@ define([ ...@@ -26,7 +29,7 @@ define([
} }
}); });
NoteGroupView = Backbone.View.extend({ ChapterView = Backbone.View.extend({
tagName: 'section', tagName: 'section',
className: 'note-group', className: 'note-group',
id: function () { id: function () {
...@@ -52,7 +55,13 @@ define([ ...@@ -52,7 +55,13 @@ define([
}, },
addChild: function (sectionInfo) { addChild: function (sectionInfo) {
var section = new NoteSectionView({section: sectionInfo}); var section = new GroupView(
{
displayName: sectionInfo.display_name,
template: '<h4 class="course-subtitle"><%- displayName %></h4>',
className: "note-section"
}
);
this.children.push(section); this.children.push(section);
return section; return section;
}, },
...@@ -65,6 +74,6 @@ define([ ...@@ -65,6 +74,6 @@ define([
} }
}); });
return NoteGroupView; return {GroupView: GroupView, ChapterView: ChapterView};
}); });
}).call(this, define || RequireJS.define); }).call(this, define || RequireJS.define);
...@@ -13,6 +13,7 @@ define([ ...@@ -13,6 +13,7 @@ define([
events: { events: {
'click .note-excerpt-more-link': 'moreHandler', 'click .note-excerpt-more-link': 'moreHandler',
'click .reference-unit-link': 'unitLinkHandler', 'click .reference-unit-link': 'unitLinkHandler',
'click .reference-tags': 'tagHandler'
}, },
initialize: function (options) { initialize: function (options) {
...@@ -56,6 +57,11 @@ define([ ...@@ -56,6 +57,11 @@ define([
}, this)); }, this));
}, },
tagHandler: function (event) {
event.preventDefault();
this.options.scrollToTag(event.currentTarget.text);
},
redirectTo: function (uri) { redirectTo: function (uri) {
window.location = uri; window.location = uri;
}, },
......
...@@ -7,7 +7,7 @@ define([ ...@@ -7,7 +7,7 @@ define([
'js/edxnotes/plugins/caret_navigation' 'js/edxnotes/plugins/caret_navigation'
], function ($, _, Annotator, NotesLogger) { ], function ($, _, Annotator, NotesLogger) {
var plugins = ['Auth', 'Store', 'Scroller', 'Events', 'Accessibility', 'CaretNavigation', 'Tags'], var plugins = ['Auth', 'Store', 'Scroller', 'Events', 'Accessibility', 'CaretNavigation', 'Tags'],
getOptions, setupPlugins, updateHeaders, getAnnotator; getOptions, setupPlugins, getAnnotator;
/** /**
* Returns options for the annotator. * Returns options for the annotator.
......
...@@ -3,33 +3,53 @@ ...@@ -3,33 +3,53 @@
define([ define([
'backbone', 'js/edxnotes/collections/tabs', 'js/edxnotes/views/tabs_list', 'backbone', 'js/edxnotes/collections/tabs', 'js/edxnotes/views/tabs_list',
'js/edxnotes/views/tabs/recent_activity', 'js/edxnotes/views/tabs/course_structure', 'js/edxnotes/views/tabs/recent_activity', 'js/edxnotes/views/tabs/course_structure',
'js/edxnotes/views/tabs/search_results' 'js/edxnotes/views/tabs/search_results', 'js/edxnotes/views/tabs/tags'
], function ( ], function (
Backbone, TabsCollection, TabsListView, RecentActivityView, CourseStructureView, Backbone, TabsCollection, TabsListView, RecentActivityView, CourseStructureView,
SearchResultsView SearchResultsView, TagsView
) { ) {
var NotesPageView = Backbone.View.extend({ var NotesPageView = Backbone.View.extend({
initialize: function (options) { initialize: function (options) {
var scrollToTag, tagsModel;
this.options = options; this.options = options;
this.tabsCollection = new TabsCollection(); this.tabsCollection = new TabsCollection();
this.recentActivityView = new RecentActivityView({ // Must create the Tags view first to get the "scrollToTag" method.
this.tagsView = new TagsView({
el: this.el, el: this.el,
collection: this.collection, collection: this.collection,
tabsCollection: this.tabsCollection tabsCollection: this.tabsCollection
}); });
scrollToTag = this.tagsView.scrollToTag;
// 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,
collection: this.collection,
tabsCollection: this.tabsCollection,
scrollToTag: scrollToTag
});
this.courseStructureView = new CourseStructureView({ this.courseStructureView = new CourseStructureView({
el: this.el, el: this.el,
collection: this.collection, collection: this.collection,
tabsCollection: this.tabsCollection tabsCollection: this.tabsCollection,
scrollToTag: scrollToTag
}); });
// Add the Tags model after the Course Structure model.
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,
createTabOnInitialization: false createTabOnInitialization: false,
scrollToTag: scrollToTag
}); });
this.tabsView = new TabsListView({collection: this.tabsCollection}); this.tabsView = new TabsListView({collection: this.tabsCollection});
......
;(function (define, undefined) { ;(function (define, undefined) {
'use strict'; 'use strict';
define(['gettext', 'underscore', 'backbone', 'js/edxnotes/utils/template'], define(['gettext', 'underscore', 'jquery', 'backbone', 'js/edxnotes/utils/template'],
function (gettext, _, Backbone, templateUtils) { function (gettext, _, $, Backbone, templateUtils) {
var TabItemView = Backbone.View.extend({ var TabItemView = Backbone.View.extend({
tagName: 'li', tagName: 'li',
className: 'tab', className: 'tab',
......
...@@ -26,9 +26,9 @@ function (gettext, _, Backbone, NoteItemView) { ...@@ -26,9 +26,9 @@ function (gettext, _, Backbone, NoteItemView) {
}, },
getNotes: function (collection) { getNotes: function (collection) {
var container = document.createDocumentFragment(), var container = document.createDocumentFragment(), scrollToTag = this.options.scrollToTag,
notes = _.map(collection, function (model) { notes = _.map(collection, function (model) {
var note = new NoteItemView({model: model}); var note = new NoteItemView({model: model, scrollToTag: scrollToTag});
container.appendChild(note.render().el); container.appendChild(note.render().el);
return note; return note;
}); });
......
;(function (define, undefined) { ;(function (define, undefined) {
'use strict'; 'use strict';
define([ define([
'underscore', 'backbone', 'js/edxnotes/models/tab' 'jquery', 'underscore', 'backbone', 'js/edxnotes/models/tab'
], function (_, Backbone, TabModel) { ], function ($, _, Backbone, TabModel) {
var TabView = Backbone.View.extend({ var TabView = Backbone.View.extend({
PanelConstructor: null, PanelConstructor: null,
...@@ -52,6 +52,7 @@ define([ ...@@ -52,6 +52,7 @@ define([
// If the view is already rendered, destroy it. // If the view is already rendered, destroy it.
this.destroySubView(); this.destroySubView();
this.renderContent().always(this.hideLoadingIndicator); this.renderContent().always(this.hideLoadingIndicator);
this.$('.sr-is-focusable.sr-tab-panel').focus();
return this; return this;
}, },
...@@ -63,7 +64,7 @@ define([ ...@@ -63,7 +64,7 @@ define([
getSubView: function () { getSubView: function () {
var collection = this.getCollection(); var collection = this.getCollection();
return new this.PanelConstructor({collection: collection}); return new this.PanelConstructor({collection: collection, scrollToTag: this.options.scrollToTag});
}, },
destroySubView: function () { destroySubView: function () {
......
;(function (define, undefined) { ;(function (define, undefined) {
'use strict'; 'use strict';
define([ define([
'gettext', 'js/edxnotes/views/note_group', 'js/edxnotes/views/tab_panel', 'gettext', 'underscore', 'js/edxnotes/views/note_group', 'js/edxnotes/views/tab_panel',
'js/edxnotes/views/tab_view' 'js/edxnotes/views/tab_view'
], function (gettext, NoteGroupView, TabPanelView, TabView) { ], function (gettext, _, NoteGroupView, TabPanelView, TabView) {
var CourseStructureView = TabView.extend({ var CourseStructureView = TabView.extend({
PanelConstructor: TabPanelView.extend({ PanelConstructor: TabPanelView.extend({
id: 'structure-panel', id: 'structure-panel',
...@@ -14,28 +14,28 @@ define([ ...@@ -14,28 +14,28 @@ define([
container = document.createDocumentFragment(); container = document.createDocumentFragment();
_.each(courseStructure.chapters, function (chapterInfo) { _.each(courseStructure.chapters, function (chapterInfo) {
var group = this.getGroup(chapterInfo); var chapterView = this.getChapterGroupView(chapterInfo);
_.each(chapterInfo.children, function (location) { _.each(chapterInfo.children, function (location) {
var sectionInfo = courseStructure.sections[location], var sectionInfo = courseStructure.sections[location],
section; sectionView;
if (sectionInfo) { if (sectionInfo) {
section = group.addChild(sectionInfo); sectionView = chapterView.addChild(sectionInfo);
_.each(sectionInfo.children, function (location) { _.each(sectionInfo.children, function (location) {
var notes = courseStructure.units[location]; var notes = courseStructure.units[location];
if (notes) { if (notes) {
section.addChild(this.getNotes(notes)) sectionView.addChild(this.getNotes(notes));
} }
}, this); }, this);
} }
}, this); }, this);
container.appendChild(group.render().el); container.appendChild(chapterView.render().el);
}, this); }, this);
this.$el.append(container); this.$el.append(container);
return this; return this;
}, },
getGroup: function (chapter, section) { getChapterGroupView: function (chapter, section) {
var group = new NoteGroupView({ var group = new NoteGroupView.ChapterView({
chapter: chapter, chapter: chapter,
section: section section: section
}); });
......
...@@ -11,7 +11,7 @@ define([ ...@@ -11,7 +11,7 @@ define([
return [ return [
TabPanelView.prototype.className, TabPanelView.prototype.className,
'note-group' 'note-group'
].join(' ') ].join(' ');
}, },
renderContent: function () { renderContent: function () {
this.$el.append(this.getNotes(this.collection.toArray())); this.$el.append(this.getNotes(this.collection.toArray()));
......
;(function (define, undefined) { ;(function (define, undefined) {
'use strict'; 'use strict';
define([ define([
'gettext', 'js/edxnotes/views/tab_panel', 'js/edxnotes/views/tab_view', 'jquery', 'underscore', 'gettext', 'js/edxnotes/views/tab_panel', 'js/edxnotes/views/tab_view',
'js/edxnotes/views/search_box' 'js/edxnotes/views/search_box'
], function (gettext, TabPanelView, TabView, SearchBoxView) { ], function ($, _, gettext, TabPanelView, TabView, SearchBoxView) {
var SearchResultsView = TabView.extend({ var SearchResultsView = TabView.extend({
PanelConstructor: TabPanelView.extend({ PanelConstructor: TabPanelView.extend({
id: 'search-results-panel', id: 'search-results-panel',
...@@ -78,7 +78,8 @@ define([ ...@@ -78,7 +78,8 @@ define([
if (collection.length) { if (collection.length) {
return new this.PanelConstructor({ return new this.PanelConstructor({
collection: collection, collection: collection,
searchQuery: this.searchResults.searchQuery searchQuery: this.searchResults.searchQuery,
scrollToTag: this.options.scrollToTag
}); });
} else { } else {
return new this.NoResultsViewConstructor({ return new this.NoResultsViewConstructor({
......
;(function (define, undefined) {
'use strict';
define([
'gettext', 'jquery', 'underscore', 'js/edxnotes/views/note_group', 'js/edxnotes/views/tab_panel',
'js/edxnotes/views/tab_view'
], function (gettext, $, _, NoteGroupView, TabPanelView, TabView) {
var TagsView = TabView.extend({
scrollToTag: function(tagName) {
var titleElement, displayedTitle;
if (!this.tabModel.isActive()) {
this.tabModel.activate();
}
displayedTitle = this.contentView.titleMap[tagName.toLowerCase()];
titleElement = this.$el.find('.tags-title').filter(
function(){ return $(this).text() === displayedTitle; }
);
$('html,body').animate(
{ scrollTop: titleElement.offset().top - 10 },
'slow',
function () { titleElement.focus(); }
);
},
initialize: function (options) {
TabView.prototype.initialize.call(this, options);
_.bindAll(this, 'scrollToTag');
this.options.scrollToTag = this.scrollToTag;
},
PanelConstructor: TabPanelView.extend({
id: 'tags-panel',
title: 'Tags',
// Translators: this is a title shown before all Notes that have no associated tags. It is put within
// brackets to differentiate it from user-defined tags, but it should still be translated.
noTags: gettext('[no tags]'), // User-defined tags cannot have spaces, so no risk of a collision.
renderContent: function () {
var notesByTag = {}, noTags = this.noTags, addNoteForTag, noteList, tags, i,
sortedTagNames, container, group, noteGroup, tagTitle, titleMap;
// Iterate through all the notes and build up a dictionary structure by tag.
// Note that the collection will be in most-recently updated order already.
addNoteForTag = function (note, tag) {
noteList = notesByTag[tag.toLowerCase()];
if (noteList === undefined) {
noteList = [];
notesByTag[tag.toLowerCase()] = noteList;
}
// If a note was tagged with the same tag more than once, don't add again.
// We can assume it would be the last element of the list because we iterate through
// all tags on a given note before moving on to the text note.
if (noteList.length === 0 || noteList[noteList.length - 1] !== note) {
noteList.push(note);
}
};
this.collection.each(function(note){
tags = note.get('tags');
if (tags.length === 0) {
addNoteForTag(note, noTags);
}
else {
for (i = 0; i < tags.length; i++) {
addNoteForTag(note, tags[i]);
}
}
});
sortedTagNames = Object.keys(notesByTag).sort(function (a, b) {
// "no tags" should always appear last
if (a === noTags) {
return 1;
}
else if (b === noTags) {
return -1;
}
else if (notesByTag[a].length > notesByTag[b].length) {
return -1;
}
else if (notesByTag[a].length < notesByTag[b].length) {
return 1;
}
else {
return a.toLowerCase() <= b.toLowerCase() ? -1 : 1;
}
});
container = document.createDocumentFragment();
// Store map of titles for scrollToTag functionality.
this.titleMap = {};
titleMap = this.titleMap;
_.each(sortedTagNames, function (tagName) {
noteGroup = notesByTag[tagName];
var tagTitle = interpolate_text(
"{tagName} ({numberOfNotesWithTag})",
{tagName: tagName, numberOfNotesWithTag: noteGroup.length}
);
group = this.getGroup(tagTitle);
titleMap[tagName] = tagTitle;
group.addChild(this.getNotes(noteGroup));
container.appendChild(group.render().el);
}, this);
this.$el.append(container);
return this;
},
getGroup: function (tagName) {
var group = new NoteGroupView.GroupView({
displayName: tagName,
template: '<h3 class="tags-title sr-is-focusable" tabindex="-1"><%- displayName %></h3>',
className: "note-group"
});
this.children.push(group);
return group;
}
}),
tabInfo: {
// Translators: 'Tags' is the name of the view (noun) within the Student Notes page that shows all
// notes organized by the tags the student has associated with them (if any). When defining a
// note in the courseware, the student can choose to associate 1 or more tags with the note
// in order to group similar notes together and help with search.
name: gettext('Tags'),
identifier: 'view-tags',
icon: 'fa fa-tag'
}
});
return TagsView;
});
}).call(this, define || RequireJS.define);
...@@ -105,6 +105,7 @@ define(['underscore'], function(_) { ...@@ -105,6 +105,7 @@ define(['underscore'], function(_) {
}; };
getDefaultNotes = function () { getDefaultNotes = function () {
// Note that the server returns notes in reverse chronological order (newest first).
return [ return [
{ {
chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]), chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]),
...@@ -113,7 +114,8 @@ define(['underscore'], function(_) { ...@@ -113,7 +114,8 @@ define(['underscore'], function(_) {
created: 'December 11, 2014 at 11:12AM', created: 'December 11, 2014 at 11:12AM',
updated: 'December 11, 2014 at 11:12AM', updated: 'December 11, 2014 at 11:12AM',
text: 'Third added model', text: 'Third added model',
quote: 'Note 4' quote: 'Note 4',
tags: ['Pumpkin', 'pumpkin', 'yummy']
}, },
{ {
chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]), chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]),
...@@ -131,7 +133,8 @@ define(['underscore'], function(_) { ...@@ -131,7 +133,8 @@ define(['underscore'], function(_) {
created: 'December 11, 2014 at 11:11AM', created: 'December 11, 2014 at 11:11AM',
updated: 'December 11, 2014 at 11:11AM', updated: 'December 11, 2014 at 11:11AM',
text: 'Second added model', text: 'Second added model',
quote: 'Note 3' quote: 'Note 3',
tags: ['yummy']
}, },
{ {
chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]), chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]),
...@@ -140,7 +143,8 @@ define(['underscore'], function(_) { ...@@ -140,7 +143,8 @@ define(['underscore'], function(_) {
created: 'December 11, 2014 at 11:10AM', created: 'December 11, 2014 at 11:10AM',
updated: 'December 11, 2014 at 11:10AM', updated: 'December 11, 2014 at 11:10AM',
text: 'First added model', text: 'First added model',
quote: 'Note 2' quote: 'Note 2',
tags: ['PUMPKIN', 'pie']
}, },
{ {
chapter: getChapter('First Chapter', 1, 0, [2]), chapter: getChapter('First Chapter', 1, 0, [2]),
...@@ -149,7 +153,8 @@ define(['underscore'], function(_) { ...@@ -149,7 +153,8 @@ define(['underscore'], function(_) {
created: 'December 11, 2014 at 11:10AM', created: 'December 11, 2014 at 11:10AM',
updated: 'December 11, 2014 at 11:10AM', updated: 'December 11, 2014 at 11:10AM',
text: 'First added model', text: 'First added model',
quote: 'Note 1' quote: 'Note 1',
tags: ['pie', 'pumpkin']
} }
]; ];
}; };
......
...@@ -9,7 +9,7 @@ define([ ...@@ -9,7 +9,7 @@ define([
) { ) {
'use strict'; 'use strict';
describe('EdxNotes NoteItemView', function() { describe('EdxNotes NoteItemView', function() {
var getView = function (model) { var getView = function (model, scrollToTag) {
model = new NoteModel(_.defaults(model || {}, { model = new NoteModel(_.defaults(model || {}, {
id: 'id-123', id: 'id-123',
user: 'user-123', user: 'user-123',
...@@ -23,7 +23,7 @@ define([ ...@@ -23,7 +23,7 @@ define([
} }
})); }));
return new NoteItemView({model: model}).render(); return new NoteItemView({model: model, scrollToTag: scrollToTag}).render();
}; };
beforeEach(function() { beforeEach(function() {
...@@ -67,7 +67,19 @@ define([ ...@@ -67,7 +67,19 @@ 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.$('.reference-tags').last()).toContainText('First, Second'); expect(view.$('a.reference-tags').length).toBe(2);
expect(view.$('a.reference-tags')[0]).toContainText('First');
expect(view.$('a.reference-tags')[1]).toContainText('Second');
});
it('should handle a click event on the tag', function() {
var scrollToTagSpy = {
scrollToTag: function (tagName){}
};
spyOn(scrollToTagSpy, 'scrollToTag');
var view = getView({tags: ["only"]}, scrollToTagSpy.scrollToTag);
view.$('a.reference-tags').click();
expect(scrollToTagSpy.scrollToTag).toHaveBeenCalledWith("only");
}); });
it('should log the edx.student_notes.used_unit_link event properly', function () { it('should log the edx.student_notes.used_unit_link event properly', function () {
......
define([
'jquery', 'underscore', 'js/common_helpers/template_helpers', 'js/spec/edxnotes/helpers',
'js/edxnotes/collections/notes', 'js/edxnotes/collections/tabs',
'js/edxnotes/views/tabs/tags', 'js/spec/edxnotes/custom_matchers',
'jasmine-jquery'
], function(
$, _, TemplateHelpers, Helpers, NotesCollection, TabsCollection, TagsView,
customMatchers
) {
'use strict';
describe('EdxNotes TagsView', function() {
var notes = Helpers.getDefaultNotes(),
getView, getText, getNoteText;
getText = function (selector) {
return $(selector).map(function () { return _.trim($(this).text()); }).toArray();
};
getNoteText = function (groupIndex) {
return $($('.note-group')[groupIndex]).find('.note-excerpt-p').map(function () {
return _.trim($(this).text());
}).toArray();
};
getView = function (collection, tabsCollection, options) {
var view;
options = _.defaults(options || {}, {
el: $('.wrapper-student-notes'),
collection: collection,
tabsCollection: tabsCollection
});
view = new TagsView(options);
tabsCollection.at(0).activate();
return view;
};
beforeEach(function () {
customMatchers(this);
loadFixtures('js/fixtures/edxnotes/edxnotes.html');
TemplateHelpers.installTemplates([
'templates/edxnotes/note-item', 'templates/edxnotes/tab-item'
]);
this.collection = new NotesCollection(notes);
this.tabsCollection = new TabsCollection();
});
it('displays a tab and content properly ordered by tag', function () {
var view = getView(this.collection, this.tabsCollection),
tags = getText('.tags-title'),
pumpkinNotes = getNoteText(0),
pieNotes = getNoteText(1),
yummyNotes = getNoteText(2),
noTagsNotes = getNoteText(3);
expect(this.tabsCollection).toHaveLength(1);
expect(this.tabsCollection.at(0).toJSON()).toEqual({
name: 'Tags',
identifier: 'view-tags',
icon: 'fa fa-tag',
is_active: true,
is_closable: false
});
expect(view.$('#tags-panel')).toExist();
// Pumpkin notes has the greatest number of notes, and therefore should come first.
// Yummy and pie notes have the same number of notes. They should be sorted alphabetically.
// "no tags" should always be last.
expect(tags).toEqual(['pumpkin (3)', 'pie (2)', 'yummy (2)', '[no tags] (1)']);
expect(pumpkinNotes).toEqual(['Note 4', 'Note 2', 'Note 1']);
expect(yummyNotes).toEqual(['Note 4', 'Note 3']);
expect(pieNotes).toEqual(['Note 2', 'Note 1']);
expect(noTagsNotes).toEqual(['Note 5']);
});
});
});
...@@ -616,6 +616,7 @@ ...@@ -616,6 +616,7 @@
'lms/include/js/spec/edxnotes/views/tabs/search_results_spec.js', 'lms/include/js/spec/edxnotes/views/tabs/search_results_spec.js',
'lms/include/js/spec/edxnotes/views/tabs/recent_activity_spec.js', 'lms/include/js/spec/edxnotes/views/tabs/recent_activity_spec.js',
'lms/include/js/spec/edxnotes/views/tabs/course_structure_spec.js', 'lms/include/js/spec/edxnotes/views/tabs/course_structure_spec.js',
'lms/include/js/spec/edxnotes/views/tabs/tags_spec.js',
'lms/include/js/spec/edxnotes/views/visibility_decorator_spec.js', 'lms/include/js/spec/edxnotes/views/visibility_decorator_spec.js',
'lms/include/js/spec/edxnotes/views/toggle_notes_factory_spec.js', 'lms/include/js/spec/edxnotes/views/toggle_notes_factory_spec.js',
'lms/include/js/spec/edxnotes/models/tab_spec.js', 'lms/include/js/spec/edxnotes/models/tab_spec.js',
......
...@@ -108,7 +108,7 @@ $divider-visual-tertiary: ($baseline/20) solid $gray-l4; ...@@ -108,7 +108,7 @@ $divider-visual-tertiary: ($baseline/20) solid $gray-l4;
padding-top: ($baseline*1.5); padding-top: ($baseline*1.5);
// course structure labels // course structure labels
.course-title { .course-title, .tags-title {
@extend %t-title6; @extend %t-title6;
@extend %t-weight4; @extend %t-weight4;
margin: 0 0 ($baseline/2) 0; margin: 0 0 ($baseline/2) 0;
...@@ -124,6 +124,11 @@ $divider-visual-tertiary: ($baseline/20) solid $gray-l4; ...@@ -124,6 +124,11 @@ $divider-visual-tertiary: ($baseline/20) solid $gray-l4;
color: $gray-d3; color: $gray-d3;
} }
.tags-title {
border-bottom: $divider-visual-tertiary;
padding-bottom: ($baseline/2);
}
// individual note // individual note
.note { .note {
@include clearfix(); @include clearfix();
...@@ -228,10 +233,22 @@ $divider-visual-tertiary: ($baseline/20) solid $gray-l4; ...@@ -228,10 +233,22 @@ $divider-visual-tertiary: ($baseline/20) solid $gray-l4;
color: $m-gray-d2; color: $m-gray-d2;
} }
// Put commas between tags.
a.reference-meta.reference-tags:after {
content: ",";
color: $m-gray-d2;
}
// But not after the last tag.
a.reference-meta.reference-tags:last-child:after {
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;
} }
} }
} }
......
...@@ -56,6 +56,7 @@ ...@@ -56,6 +56,7 @@
</span> </span>
<span class="copy">${_("Loading")}</span> <span class="copy">${_("Loading")}</span>
</div> </div>
<div class="sr-is-focusable sr-tab-panel" tabindex="-1"></div>
% else: % else:
<section class="placeholder is-empty"> <section class="placeholder is-empty">
......
...@@ -16,28 +16,30 @@ ...@@ -16,28 +16,30 @@
<% if (text) { %> <% if (text) { %>
<ol class="note-comments" role="region" aria-label="<%- gettext('Note') %>"> <ol class="note-comments" role="region" aria-label="<%- gettext('Note') %>">
<li class="note-comment"> <li class="note-comment">
<h3 class="note-comment-title"><%- gettext("You commented...") %></h3> <p class="note-comment-title"><%- gettext("You commented...") %></p>
<p class="note-comment-p"><%= text %></p> <p class="note-comment-p"><%= text %></p>
</li> </li>
</ol> </ol>
<% } %> <% } %>
</div> </div>
<footer class="reference" role="complementary" aria-label="<%- gettext('References') %>"> <footer class="reference">
<div class="wrapper-reference-content"> <div class="wrapper-reference-content">
<h3 class="reference-title"><%- gettext("Noted in:") %></h3> <p class="reference-title"><%- gettext("Noted in:") %></p>
<% if (unit.url) { %> <% if (unit.url) { %>
<a class="reference-meta reference-unit-link" href="<%= unit.url %>#<%= id %>"><%- unit.display_name %></a> <a class="reference-meta reference-unit-link" href="<%= unit.url %>#<%= id %>"><%- unit.display_name %></a>
<% } else { %> <% } else { %>
<span class="reference-meta"><%- unit.display_name %></span> <span class="reference-meta"><%- unit.display_name %></span>
<% } %> <% } %>
<h3 class="reference-title"><%- gettext("Last Edited:") %></h3> <p class="reference-title"><%- gettext("Last Edited:") %></p>
<span class="reference-meta reference-updated-date"><%- updated %></span> <span class="reference-meta reference-updated-date"><%- updated %></span>
<% if (tags.length > 0) { %> <% if (tags.length > 0) { %>
<h3 class="reference-title"><%- gettext("Tags:") %></h3> <p class="reference-title"><%- gettext("Tags:") %></p>
<span class="reference-meta reference-tags"><%- tags.join(", ") %></span> <% for (var i = 0; i < tags.length; i++) { %>
<a class="reference-meta reference-tags" href="#"><%- tags[i] %></a>
<% } %>
<% } %> <% } %>
</div> </div>
</footer> </footer>
<% var hasIcon = icon ? 1 : 0; %> <% var hasIcon = icon ? 1 : 0; %>
<a class="tab-label <% if (hasIcon) { print('has-icon') } %>" href="#"> <a class="tab-label <% if (hasIcon) { print('has-icon') } %>" href="#">
<% if (hasIcon) { %><i class="icon <%= icon %>"></i> <% } %><%- gettext(name) %> <% if (hasIcon) { %><i class="icon <%= icon %>" aria-hidden="true"></i> <% } %><%- gettext(name) %>
</a> </a>
<% if (is_closable) { %> <% if (is_closable) { %>
......
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