Commit 0c7af25c by cahrens

Update student notes eventing for tags.

TNL-2172
parent 3f2c370d
......@@ -24,6 +24,7 @@ from xmodule.partitions.tests.test_partitions import MockUserPartitionScheme
from selenium.webdriver.support.select import Select
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from unittest import TestCase
from ..pages.common import BASE_URL
......@@ -282,7 +283,7 @@ def get_modal_alert(browser):
return browser.switch_to.alert
class EventsTestMixin(object):
class EventsTestMixin(TestCase):
"""
Helpers and setup for running tests that evaluate events emitted
"""
......
......@@ -8,6 +8,7 @@ from ...pages.lms.course_nav import CourseNavPage
from ...pages.lms.courseware import CoursewarePage
from ...pages.lms.edxnotes import EdxNotesUnitPage, EdxNotesPage, EdxNotesPageNoContent
from ...fixtures.edxnotes import EdxNotesFixture, Note, Range
from ..helpers import EventsTestMixin
class EdxNotesTestMixin(UniqueCourseTest):
......@@ -332,7 +333,7 @@ class EdxNotesDefaultInteractionsTest(EdxNotesTestMixin):
self.assertTrue(note.has_sr_label(1, 3, "Tags (space-separated)"))
class EdxNotesPageTest(EdxNotesTestMixin):
class EdxNotesPageTest(EventsTestMixin, EdxNotesTestMixin):
"""
Tests for Notes page.
"""
......@@ -346,7 +347,8 @@ class EdxNotesPageTest(EdxNotesTestMixin):
If tags are specified, they will be used for each of the 3 notes that have tags.
"""
xblocks = self.course_fixture.get_nested_xblocks(category="html")
self._add_notes([
# pylint: disable=attribute-defined-outside-init
self.raw_note_list = [
Note(
usage_id=xblocks[4].locator,
user=self.username,
......@@ -389,9 +391,10 @@ class EdxNotesPageTest(EdxNotesTestMixin):
course_id=self.course_fixture._course_key,
text="Fifth note",
quote="Annotate this text",
updated=datetime(2015, 1, 1, 1, 1, 1, 1).isoformat(),
updated=datetime(2015, 1, 1, 1, 1, 1, 1).isoformat()
),
])
]
self._add_notes(self.raw_note_list)
def assertNoteContent(self, item, text=None, quote=None, unit_name=None, time_updated=None, tags=None):
""" Verifies the expected properties of the note. """
......@@ -418,6 +421,50 @@ class EdxNotesPageTest(EdxNotesTestMixin):
self.assertEqual(item.title, title)
self.assertEqual(item.notes, notes)
def assert_viewed_event(self, view=None):
"""
Verifies that the correct view event was captured for the Notes page.
"""
# There will always be an initial event for "Recent Activity" because that is the default view.
# If view is something besides "Recent Activity", expect 2 events, with the second one being
# the view name passed in.
if view == 'Recent Activity':
view = None
actual_events = self.wait_for_events(
event_filter={'event_type': 'edx.course.student_notes.notes_page_viewed'},
number_of_matches=1 if view is None else 2
)
expected_events = [{'event': {'view': 'Recent Activity'}}]
if view:
expected_events.append({'event': {'view': view}})
self.assert_events_match(expected_events, actual_events)
def assert_unit_link_event(self, usage_id, view):
"""
Verifies that the correct used_unit_link event was captured for the Notes page.
"""
actual_events = self.wait_for_events(
event_filter={'event_type': 'edx.course.student_notes.used_unit_link'},
number_of_matches=1
)
expected_events = [
{'event': {'component_usage_id': usage_id, 'view': view}}
]
self.assert_events_match(expected_events, actual_events)
def assert_search_event(self, search_string, number_of_results):
"""
Verifies that the correct searched event was captured for the Notes page.
"""
actual_events = self.wait_for_events(
event_filter={'event_type': 'edx.course.student_notes.searched'},
number_of_matches=1
)
expected_events = [
{'event': {'search_string': search_string, 'number_of_results': number_of_results}}
]
self.assert_events_match(expected_events, actual_events)
def test_no_content(self):
"""
Scenario: User can see `No content` message.
......@@ -438,6 +485,7 @@ class EdxNotesPageTest(EdxNotesTestMixin):
When I open Notes page
Then I see 5 notes sorted by the updated date
And I see correct content in the notes
And an event has fired indicating that the Recent Activity view was selected
"""
self._add_default_notes()
self.notes_page.visit()
......@@ -485,6 +533,8 @@ class EdxNotesPageTest(EdxNotesTestMixin):
time_updated="Jan 01, 2011 at 01:01 UTC"
)
self.assert_viewed_event()
def test_course_structure_view(self):
"""
Scenario: User can view all notes by location in Course.
......@@ -493,6 +543,7 @@ class EdxNotesPageTest(EdxNotesTestMixin):
And I switch to "Location in Course" view
Then I see 2 groups, 3 sections and 5 notes
And I see correct content in the notes and groups
And an event has fired indicating that the Location in Course view was selected
"""
self._add_default_notes()
self.notes_page.visit().switch_to_tab("structure")
......@@ -575,6 +626,8 @@ class EdxNotesPageTest(EdxNotesTestMixin):
time_updated="Jan 01, 2011 at 01:01 UTC"
)
self.assert_viewed_event('Location in Course')
def test_tags_view(self):
"""
Scenario: User can view all notes by associated tags.
......@@ -582,6 +635,7 @@ class EdxNotesPageTest(EdxNotesTestMixin):
When I switch to the "Tags" view
Then I see 4 tag groups
And I see correct content in the notes and groups
And an event has fired indicating that the Tags view was selected
"""
self._add_default_notes()
self.notes_page.visit().switch_to_tab("tags")
......@@ -677,40 +731,53 @@ class EdxNotesPageTest(EdxNotesTestMixin):
time_updated="Jan 01, 2011 at 01:01 UTC"
)
self.assert_viewed_event('Tags')
def test_easy_access_from_notes_page(self):
"""
Scenario: Ensure that the link to the Unit works correctly.
Given I have a course with 5 notes
When I open Notes page
And I click on the first unit link
Then I see correct text on the unit page
Then I see correct text on the unit page and a unit link event was fired
When go back to the Notes page
And I switch to "Location in Course" view
And I click on the second unit link
Then I see correct text on the unit page
Then I see correct text on the unit page and a unit link event was fired
When go back to the Notes page
And I switch to "Tags" view
And I click on the first unit link
Then I see correct text on the unit page and a unit link event was fired
When go back to the Notes page
And I run the search with "Fifth" query
And I click on the first unit link
Then I see correct text on the unit page
Then I see correct text on the unit page and a unit link event was fired
"""
def assert_page(note):
def assert_page(note, usage_id, view):
""" Verify that clicking on the unit link works properly. """
quote = note.quote
note.go_to_unit()
self.courseware_page.wait_for_page()
self.assertIn(quote, self.courseware_page.xblock_component_html_content())
self.assert_unit_link_event(usage_id, view)
self.reset_event_tracking()
self._add_default_notes()
self.notes_page.visit()
note = self.notes_page.notes[0]
assert_page(note)
assert_page(note, self.raw_note_list[4]['usage_id'], "Recent Activity")
self.notes_page.visit().switch_to_tab("structure")
note = self.notes_page.notes[1]
assert_page(note)
assert_page(note, self.raw_note_list[2]['usage_id'], "Location in Course")
self.notes_page.visit().switch_to_tab("tags")
note = self.notes_page.notes[0]
assert_page(note, self.raw_note_list[2]['usage_id'], "Tags")
self.notes_page.visit().search("Fifth")
note = self.notes_page.notes[0]
assert_page(note)
assert_page(note, self.raw_note_list[4]['usage_id'], "Search Results")
def test_search_behaves_correctly(self):
"""
......@@ -723,6 +790,8 @@ class EdxNotesPageTest(EdxNotesTestMixin):
When I run the search with "note" query
Then I see that error message disappears
And I see that "Search Results" tab appears with 4 notes found
And an event has fired indicating that the Search Results view was selected
And an event has fired recording the search that was performed
"""
self._add_default_notes()
self.notes_page.visit()
......@@ -774,6 +843,9 @@ class EdxNotesPageTest(EdxNotesTestMixin):
time_updated="Jan 01, 2011 at 01:01 UTC"
)
self.assert_viewed_event('Search Results')
self.assert_search_event('note', 4)
def test_scroll_to_tag_recent_activity(self):
"""
Scenario: Can scroll to a tag group from the Recent Activity view (default view)
......
......@@ -47,6 +47,7 @@ def edxnotes(cls):
"tokenUrl": get_token_url(self.runtime.course_id),
"endpoint": get_endpoint(),
"debug": settings.DEBUG,
"eventStringLimit": settings.TRACK_MAX_EVENT / 6,
},
})
......
......@@ -98,6 +98,7 @@ class EdxNotesDecoratorTest(ModuleStoreTestCase):
"tokenUrl": "/tokenUrl",
"endpoint": "/endpoint",
"debug": settings.DEBUG,
"eventStringLimit": settings.TRACK_MAX_EVENT / 6,
},
}
self.assertEqual(
......
;(function (define, undefined) {
'use strict';
define(['underscore', 'backbone'], function (_, Backbone) {
define(['underscore', 'backbone', 'js/edxnotes/utils/logger'], function (_, Backbone, NotesLogger) {
var TabModel = Backbone.Model.extend({
defaults: {
'identifier': '',
'name': '',
'icon': '',
'is_active': false,
'is_closable': false
'is_closable': false,
'view': ''
},
initialize: function () {
this.logger = NotesLogger.getLogger('tab');
},
activate: function () {
......@@ -18,6 +23,9 @@ define(['underscore', 'backbone'], function (_, Backbone) {
}
}, this));
this.set('is_active', true);
this.logger.emit('edx.course.student_notes.notes_page_viewed', {
'view': this.get('view')
});
},
inactivate: function () {
......
......@@ -85,18 +85,22 @@ define([
annotationEditorShown: function (editor, annotation) {
this.oldNoteText = annotation.text || '';
this.oldTags = annotation.tags || [];
},
annotationEditorHidden: function () {
this.oldNoteText = null;
this.oldTags = null;
},
annotationUpdated: function (annotation) {
var data;
var data, defaultData;
if (!this.isNew(annotation)) {
defaultData = this.getDefaultData(annotation);
data = _.extend(
this.getDefaultData(annotation),
this.getText('old_note_text', this.oldNoteText)
defaultData,
this.getText('old_note_text', this.oldNoteText, defaultData.truncated),
this.getTextArray('old_tags', this.oldTags, defaultData.truncated)
);
this.log('edx.course.student_notes.edited', data);
}
......@@ -113,28 +117,51 @@ define([
},
getDefaultData: function (annotation) {
var truncated = [];
return _.extend(
{
'note_id': annotation.id,
'component_usage_id': annotation.usage_id
'component_usage_id': annotation.usage_id,
'truncated': truncated
},
this.getText('note_text', annotation.text),
this.getText('highlighted_content', annotation.quote)
this.getText('note_text', annotation.text, truncated),
this.getText('highlighted_content', annotation.quote, truncated),
this.getTextArray('tags', annotation.tags, truncated)
);
},
getText: function (fieldName, text) {
getText: function (fieldName, text, truncated) {
var info = {},
truncated = false,
limit = this.options.stringLimit;
if (_.isNumber(limit) && _.isString(text) && text.length > limit) {
text = String(text).slice(0, limit);
truncated = true;
truncated.push(fieldName);
}
info[fieldName] = text;
info[fieldName + '_truncated'] = truncated;
return info;
},
getTextArray: function (fieldName, textArray, truncated) {
var info = {}, limit = this.options.stringLimit, totalLength=0, returnArray=[], i;
if (_.isNumber(limit) && _.isArray(textArray)) {
for (i=0; i < textArray.length; i++) {
if (_.isString(textArray[i]) && totalLength + textArray[i].length > limit) {
truncated.push(fieldName);
break;
}
totalLength += textArray[i].length;
returnArray[i] = textArray[i];
}
}
else {
returnArray = textArray;
}
info[fieldName] = returnArray;
return info;
},
......
......@@ -49,9 +49,10 @@ define([
unitLinkHandler: function (event) {
var REQUEST_TIMEOUT = 2000;
event.preventDefault();
this.logger.emit('edx.student_notes.used_unit_link', {
this.logger.emit('edx.course.student_notes.used_unit_link', {
'note_id': this.model.get('id'),
'component_usage_id': this.model.get('usage_id')
'component_usage_id': this.model.get('usage_id'),
'view': this.options.view
}, REQUEST_TIMEOUT).always(_.bind(function () {
this.redirectTo(event.target.href);
}, this));
......
......@@ -34,7 +34,7 @@ define([
tokenUrl: params.tokenUrl
},
events: {
stringLimit: 300
stringLimit: params.eventStringLimit
},
store: {
prefix: prefix,
......
......@@ -98,7 +98,7 @@ define([
var args = this.prepareData(data);
if (args) {
this.options.search.apply(this, args);
this.logger.emit('edx.student_notes.searched', {
this.logger.emit('edx.course.student_notes.searched', {
'number_of_results': args[1],
'search_string': args[2]
});
......
......@@ -26,9 +26,9 @@ function (gettext, _, Backbone, NoteItemView) {
},
getNotes: function (collection) {
var container = document.createDocumentFragment(), scrollToTag = this.options.scrollToTag,
var container = document.createDocumentFragment(), scrollToTag = this.options.scrollToTag, view = this.title,
notes = _.map(collection, function (model) {
var note = new NoteItemView({model: model, scrollToTag: scrollToTag});
var note = new NoteItemView({model: model, scrollToTag: scrollToTag, view: view});
container.appendChild(note.render().el);
return note;
});
......
......@@ -4,10 +4,11 @@ define([
'gettext', 'underscore', 'js/edxnotes/views/note_group', 'js/edxnotes/views/tab_panel',
'js/edxnotes/views/tab_view'
], function (gettext, _, NoteGroupView, TabPanelView, TabView) {
var view = "Location in Course";
var CourseStructureView = TabView.extend({
PanelConstructor: TabPanelView.extend({
id: 'structure-panel',
title: 'Location in Course',
title: view,
renderContent: function () {
var courseStructure = this.collection.getCourseStructure(),
......@@ -47,7 +48,8 @@ define([
tabInfo: {
name: gettext('Location in Course'),
identifier: 'view-course-structure',
icon: 'fa fa-list-ul'
icon: 'fa fa-list-ul',
view: view
}
});
......
......@@ -3,10 +3,11 @@
define([
'gettext', 'js/edxnotes/views/tab_panel', 'js/edxnotes/views/tab_view'
], function (gettext, TabPanelView, TabView) {
var view = 'Recent Activity';
var RecentActivityView = TabView.extend({
PanelConstructor: TabPanelView.extend({
id: 'recent-panel',
title: 'Recent Activity',
title: view,
className: function () {
return [
TabPanelView.prototype.className,
......@@ -22,7 +23,8 @@ define([
tabInfo: {
identifier: 'view-recent-activity',
name: gettext('Recent Activity'),
icon: 'fa fa-clock-o'
icon: 'fa fa-clock-o',
view: view
}
});
......
......@@ -4,10 +4,11 @@ define([
'jquery', 'underscore', 'gettext', 'js/edxnotes/views/tab_panel', 'js/edxnotes/views/tab_view',
'js/edxnotes/views/search_box'
], function ($, _, gettext, TabPanelView, TabView, SearchBoxView) {
var view = 'Search Results';
var SearchResultsView = TabView.extend({
PanelConstructor: TabPanelView.extend({
id: 'search-results-panel',
title: 'Search Results',
title: view,
className: function () {
return [
TabPanelView.prototype.className,
......@@ -46,7 +47,8 @@ define([
identifier: 'view-search-results',
name: gettext('Search Results'),
icon: 'fa fa-search',
is_closable: true
is_closable: true,
view: view
},
initialize: function (options) {
......
......@@ -4,7 +4,7 @@ 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 view = 'Tags';
var TagsView = TabView.extend({
scrollToTag: function(tagName) {
var titleElement, displayedTitle;
......@@ -31,7 +31,7 @@ define([
PanelConstructor: TabPanelView.extend({
id: 'tags-panel',
title: 'Tags',
title: view,
// 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.
......@@ -128,7 +128,8 @@ define([
// in order to group similar notes together and help with search.
name: gettext('Tags'),
identifier: 'view-tags',
icon: 'fa fa-tag'
icon: 'fa fa-tag',
view: view
}
});
......
......@@ -9,19 +9,22 @@ define([
id: 'note-123',
text: 'text-123',
quote: 'quote-123',
usage_id: 'usage-123'
usage_id: 'usage-123',
tags: ["tag1", "tag2"]
},
noteWithoutId = {
user: 'user-123',
text: 'text-123',
quote: 'quote-123',
usage_id: 'usage-123'
usage_id: 'usage-123',
tags: ["tag1", "tag2"]
};
beforeEach(function() {
this.annotator = NotesFactory.factory(
$('<div />').get(0), {
endpoint: 'http://example.com/'
endpoint: 'http://example.com/',
eventStringLimit: 300
}
);
spyOn(Logger, 'log');
......@@ -65,9 +68,9 @@ define([
'edx.course.student_notes.added', {
'note_id': 'note-123',
'note_text': 'text-123',
'note_text_truncated': false,
'tags': ["tag1", "tag2"],
'highlighted_content': 'quote-123',
'highlighted_content_truncated': false,
'truncated': [],
'component_usage_id': 'usage-123'
}
);
......@@ -75,7 +78,7 @@ define([
it('should log the edx.course.student_notes.edited event properly', function() {
var oldNote = note,
newNote = $.extend({}, note, {text: 'text-456'});
newNote = $.extend({}, note, {text: 'text-456', tags: []});
this.annotator.publish('annotationEditorShown', [this.annotator.editor, oldNote]);
expect(this.annotator.plugins.Events.oldNoteText).toBe('text-123');
......@@ -86,11 +89,11 @@ define([
'edx.course.student_notes.edited', {
'note_id': 'note-123',
'old_note_text': 'text-123',
'old_note_text_truncated': false,
'note_text': 'text-456',
'note_text_truncated': false,
'old_tags': ["tag1", "tag2"],
'tags': [],
'highlighted_content': 'quote-123',
'highlighted_content_truncated': false,
'truncated': [],
'component_usage_id': 'usage-123'
}
);
......@@ -115,9 +118,9 @@ define([
'edx.course.student_notes.deleted', {
'note_id': 'note-123',
'note_text': 'text-123',
'note_text_truncated': false,
'tags': ["tag1", "tag2"],
'highlighted_content': 'quote-123',
'highlighted_content_truncated': false,
'truncated': [],
'component_usage_id': 'usage-123'
}
);
......@@ -129,10 +132,11 @@ define([
});
it('should truncate values of some fields', function() {
var oldNote = $.extend({}, note, {text: Helpers.LONG_TEXT}),
var oldNote = $.extend({}, note, {text: Helpers.LONG_TEXT, tags: ["review", Helpers.LONG_TEXT]}),
newNote = $.extend({}, note, {
text: Helpers.LONG_TEXT + '123',
quote: Helpers.LONG_TEXT + '123'
quote: Helpers.LONG_TEXT + '123',
tags: ["short", "tags", "will", "stay", Helpers.LONG_TEXT]
});
this.annotator.publish('annotationEditorShown', [this.annotator.editor, oldNote]);
......@@ -144,11 +148,11 @@ define([
'edx.course.student_notes.edited', {
'note_id': 'note-123',
'old_note_text': Helpers.TRUNCATED_TEXT,
'old_note_text_truncated': true,
'old_tags': ["review"],
'tags': ["short", "tags", "will", "stay"],
'note_text': Helpers.TRUNCATED_TEXT,
'note_text_truncated': true,
'highlighted_content': Helpers.TRUNCATED_TEXT,
'highlighted_content_truncated': true,
'truncated': ["note_text", "highlighted_content", "tags", "old_note_text", "old_tags"],
'component_usage_id': 'usage-123'
}
);
......
......@@ -23,7 +23,7 @@ define([
}
}));
return new NoteItemView({model: model, scrollToTag: scrollToTag}).render();
return new NoteItemView({model: model, scrollToTag: scrollToTag, view: "Test View"}).render();
};
beforeEach(function() {
......@@ -82,16 +82,17 @@ define([
expect(scrollToTagSpy.scrollToTag).toHaveBeenCalledWith("only");
});
it('should log the edx.student_notes.used_unit_link event properly', function () {
it('should log the edx.course.student_notes.used_unit_link event properly', function () {
var requests = AjaxHelpers.requests(this),
view = getView();
spyOn(view, 'redirectTo');
view.$('.reference-unit-link').click();
expect(Logger.log).toHaveBeenCalledWith(
'edx.student_notes.used_unit_link',
'edx.course.student_notes.used_unit_link',
{
'note_id': 'id-123',
'component_usage_id': 'usage_id-123'
'component_usage_id': 'usage_id-123',
'view': 'Test View'
},
null,
{
......
......@@ -73,7 +73,7 @@ define([
);
});
it('should log the edx.student_notes.searched event properly', function () {
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, {
......@@ -81,7 +81,7 @@ define([
rows: [null, null]
});
expect(Logger.log).toHaveBeenCalledWith('edx.student_notes.searched', {
expect(Logger.log).toHaveBeenCalledWith('edx.course.student_notes.searched', {
'number_of_results': 2,
'search_string': 'test_text'
});
......
......@@ -56,7 +56,8 @@ define([
identifier: 'view-course-structure',
icon: 'fa fa-list-ul',
is_active: true,
is_closable: false
is_closable: false,
view: 'Location in Course'
});
expect(view.$('#structure-panel')).toExist();
expect(chapters).toEqual(['First Chapter', 'Second Chapter']);
......
......@@ -63,7 +63,8 @@ define([
identifier: 'view-recent-activity',
icon: 'fa fa-clock-o',
is_active: true,
is_closable: false
is_closable: false,
view: 'Recent Activity'
});
expect(view.$('#recent-panel')).toExist();
expect(view.$('.note')).toHaveLength(3);
......
define([
'jquery', 'js/common_helpers/template_helpers', 'js/common_helpers/ajax_helpers',
'jquery', 'underscore', 'js/common_helpers/template_helpers', 'js/common_helpers/ajax_helpers',
'logger', 'js/edxnotes/collections/tabs', 'js/edxnotes/views/tabs/search_results',
'js/spec/edxnotes/custom_matchers', 'jasmine-jquery'
], function(
$, TemplateHelpers, AjaxHelpers, Logger, TabsCollection, SearchResultsView,
$, _, TemplateHelpers, AjaxHelpers, Logger, TabsCollection, SearchResultsView,
customMatchers
) {
'use strict';
......@@ -79,7 +79,8 @@ define([
identifier: 'view-search-results',
icon: 'fa fa-search',
is_active: true,
is_closable: true
is_closable: true,
view: 'Search Results'
});
expect(view.$('#search-results-panel')).toExist();
expect(view.$('#search-results-panel')).toBeFocused();
......@@ -157,12 +158,7 @@ define([
requests = AjaxHelpers.requests(this);
submitForm(view.searchBox, 'test error');
requests[0].respond(
500, {'Content-Type': 'application/json'},
JSON.stringify({
error: 'test error message'
})
);
AjaxHelpers.respondWithError(requests, 500, {error: 'test error message'});
expect(view.$('.wrapper-msg')).not.toHaveClass('is-hidden');
expect(view.$('.wrapper-msg .copy')).toContainText('test error message');
......
......@@ -62,7 +62,8 @@ define([
identifier: 'view-tags',
icon: 'fa fa-tag',
is_active: true,
is_closable: false
is_closable: false,
view: 'Tags'
});
expect(view.$('#tags-panel')).toExist();
......
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